# `broker` — `createInMemoryWsBroker` _(no submodules)_ ## Purpose This module implements the `WsBroker` contract from `@suaveplan/api-ws` by maintaining an in-process `Map>` 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(options?): InMemoryWsBroker` — factory. - `interface InMemoryWsBroker extends WsBroker` - `publish(topic, envelope): Promise` - `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>`. 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.