first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,123 @@
# `broker` — `createInMemoryWsBroker`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module implements the `WsBroker` contract from
`@suaveplan/api-ws` by maintaining an in-process `Map<topic, Set<handler>>`
table and fanning out every `publish(topic, envelope)` call to every
subscriber currently registered for that topic. The broker is the
single-host reference for the multi-host `api-ws-broker-redis` adapter
and the default broker wired into the API testing kit.
"In-memory" describes the storage substrate, not the maturity level —
this broker is production-grade for any deployment that fits on a
single host (one HTTP server process, one WS engine). Multi-host
topologies need the Redis adapter because in-memory has no
cross-process visibility by definition.
## Features
- Direct in-process subscription map keyed by topic; O(1) subscribe,
unsubscribe, and lookup.
- Snapshot-before-dispatch so a handler that unsubscribes mid-fan-out
does not skip its peers.
- Per-subscriber error isolation — a rejecting handler never aborts
sibling deliveries nor surfaces as a rejected `publish` promise.
- Two delivery modes via `options.deliveryMode`: `"fire-and-forget"`
(default; publish resolves on next microtask) and `"await-all"`
(publish awaits every handler).
- Idempotent `dispose()` that clears the registry and refuses
subsequent `publish` / `subscribe` calls with a typed
`WS_BROKER_IN_MEMORY.BROKER_DISPOSED` error.
- `subscriberCount(topic)` and `isDisposed()` introspection helpers
used by tests, observability adapters, and the testing harness in
`./testing`.
## Usage: Basic
```ts
import { createInMemoryWsBroker } from "@suaveplan/api-ws-broker-in-memory/broker";
import type { TenantId } from "@suaveplan/api-types/tenant";
const broker = createInMemoryWsBroker();
const sub = broker.subscribe("orders", (envelope) => {
console.log("received", envelope.payload);
});
await broker.publish("orders", {
topic: "orders",
tenant: "t1" as unknown as TenantId,
payload: { id: 1 },
});
await sub.unsubscribe();
await broker.dispose();
```
## Usage: Advanced
```ts
import { createInMemoryWsBroker } from "@suaveplan/api-ws-broker-in-memory/broker";
import type { TenantId } from "@suaveplan/api-types/tenant";
// "await-all" mode: publish waits for every handler to settle, even
// async ones. Useful in test scenarios that assert side-effects
// immediately after publish resolves.
const broker = createInMemoryWsBroker<{ id: number }>({
deliveryMode: "await-all",
});
const observed: number[] = [];
broker.subscribe("orders", async (env) => {
await new Promise((r) => setTimeout(r, 5));
observed.push(env.payload.id);
});
broker.subscribe("orders", async (env) => {
observed.push(-env.payload.id);
});
await broker.publish("orders", {
topic: "orders",
tenant: "t1" as unknown as TenantId,
payload: { id: 42 },
});
// observed contains 42 and -42 in some order; both are present
// because publish awaited both handlers before resolving.
```
## API Reference
- `createInMemoryWsBroker<Payload = unknown>(options?): InMemoryWsBroker<Payload>`
— factory.
- `interface InMemoryWsBroker<Payload> extends WsBroker<Payload>`
- `publish(topic, envelope): Promise<void>`
- `subscribe(topic, handler): BrokerSubscription`
- `dispose(): void`
- `subscriberCount(topic): number` — adapter-specific introspection.
- `isDisposed(): boolean` — adapter-specific lifecycle query.
## Implementation Notes
- The registry is a `Map<string, Set<RegistryEntry>>`. Topics with
zero subscribers are removed from the map by `unsubscribe` so the
outer Map does not accumulate dead entries.
- Each subscription gets a unique `RegistryEntry` reference even if
the same `handler` callback is registered twice — duplicate
registrations are allowed and each is independently dispatched and
independently disposed.
- `publish` snapshots the subscriber set into an array before
invoking handlers. Without the snapshot, an unsubscribing handler
could shrink the live `Set` mid-iteration and skip its peers.
- `dispose()` clears the topic map and flips a `disposed` flag.
Subsequent `publish` / `subscribe` throw
`WS_BROKER_IN_MEMORY.BROKER_DISPOSED`. `dispose()` itself is a
no-op on second call so cleanup helpers can call it
defensively.
- Handler rejections are individually caught (`.catch(() => {})`) so
a misbehaving subscriber cannot poison the outer publish. This
matches the multi-host broker's semantics: brokers are fan-out
pipes, not error reporters.