Files
skills/docs/api-stack/api/api-ws-broker-in-memory/src/broker/broker.md
2026-05-29 08:20:56 +01:00

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 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

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): 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.