4.5 KiB
4.5 KiB
broker — createInMemoryWsBroker
(no submodules)
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
publishpromise. - 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 subsequentpublish/subscribecalls with a typedWS_BROKER_IN_MEMORY.BROKER_DISPOSEDerror. subscriberCount(topic)andisDisposed()introspection helpers used by tests, observability adapters, and the testing harness in./testing.
Usage: Basic
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
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): BrokerSubscriptiondispose(): voidsubscriberCount(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 byunsubscribeso the outer Map does not accumulate dead entries. - Each subscription gets a unique
RegistryEntryreference even if the samehandlercallback is registered twice — duplicate registrations are allowed and each is independently dispatched and independently disposed. publishsnapshots the subscriber set into an array before invoking handlers. Without the snapshot, an unsubscribing handler could shrink the liveSetmid-iteration and skip its peers.dispose()clears the topic map and flips adisposedflag. Subsequentpublish/subscribethrowWS_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.