5.6 KiB
Capability Spec: api-ws-broker-in-memory
Package: @suaveplan/api-ws-broker-in-memory
Status: SHIPPED 2026-05-26 (Stage 5 — Testing kit + conformance suites)
Purpose
Single-host in-process implementation of the @suaveplan/api-ws
WsBroker contract. Maintains a per-topic Set<handler> registry and
fans publish(topic, envelope) out to every currently-subscribed
handler. Production-grade for single-host deployments AND the default
broker for createTestHost (Stage 5). Multi-host topologies use
@suaveplan/api-ws-broker-redis (Stage 7) over the same contract.
ADDED Requirements
Requirement: Fan-out to every subscriber on publish
The broker SHALL deliver publish(topic, envelope) to every handler
currently subscribed to topic. Delivery order across subscribers is
unspecified; per-subscriber message order is FIFO.
Scenario: every subscriber receives every message in publish order
- WHEN three handlers subscribe to
"orders"AND two envelopes are published in sequence - THEN each handler receives both envelopes in publish order
Verified by: src/broker/broker.test.ts::"fans out a single envelope to every subscriber"
Requirement: Subscription lifecycle (subscribe / unsubscribe / dispose)
The broker SHALL expose subscribe(topic, handler) returning a
BrokerSubscription whose unsubscribe() removes the handler. The
broker SHALL expose dispose() which clears every registered handler
and refuses subsequent publish / subscribe calls.
Scenario: unsubscribe stops delivery
- WHEN a handler subscribes, then unsubscribes, then a new envelope is published
- THEN the handler is NOT invoked
Scenario: unsubscribe is idempotent
- WHEN
BrokerSubscription.unsubscribe()is called twice - THEN the second call is a no-op (no throw)
Scenario: dispose is idempotent
- WHEN
broker.dispose()is called twice - THEN the second call is a no-op and
isDisposed()returnstrue
Verified by: src/broker/broker.test.ts::"unsubscribe stops further delivery", src/broker/broker.test.ts::"unsubscribe is idempotent", src/broker/broker.test.ts::"dispose is idempotent"
Requirement: Use-after-dispose raises BROKER_DISPOSED
When publish or subscribe is invoked after dispose() has
resolved, the broker SHALL throw a
WsBrokerInMemoryError carrying the
WS_BROKER_IN_MEMORY.BROKER_DISPOSED code.
Scenario: publish after dispose throws
- WHEN
broker.dispose()is called thenbroker.publish(...)is awaited - THEN the call throws
WS_BROKER_IN_MEMORY.BROKER_DISPOSED
Scenario: subscribe after dispose throws
- WHEN
broker.dispose()is called thenbroker.subscribe(...)is called - THEN the call throws
WS_BROKER_IN_MEMORY.BROKER_DISPOSED
Verified by: src/broker/broker.test.ts::"dispose throws BROKER_DISPOSED on subsequent publish/subscribe"
Requirement: Invalid options raise INVALID_OPTIONS
createInMemoryWsBroker(options) SHALL validate options against
inMemoryWsBrokerOptionsSchema (strict-mode Zod). Any failure SHALL
throw a WsBrokerInMemoryError carrying
WS_BROKER_IN_MEMORY.INVALID_OPTIONS.
Scenario: unknown options key throws
- WHEN
createInMemoryWsBroker({ unknown: true })is called - THEN the call throws
WS_BROKER_IN_MEMORY.INVALID_OPTIONS
Scenario: invalid deliveryMode value throws
- WHEN
createInMemoryWsBroker({ deliveryMode: "blocking" })is called - THEN the call throws
WS_BROKER_IN_MEMORY.INVALID_OPTIONS
Verified by: src/broker/broker.test.ts::"invalid options throws INVALID_OPTIONS", src/broker/broker.test.ts::"invalid deliveryMode value throws INVALID_OPTIONS"
Requirement: Delivery mode semantics
options.deliveryMode: "await-all" SHALL await every handler before
publish resolves. "fire-and-forget" (default) SHALL dispatch
handlers without awaiting them. Per-subscriber failures in either
mode SHALL be swallowed individually and SHALL NOT reject the outer
publish nor abort sibling deliveries.
Scenario: await-all blocks publish until every handler settles
- WHEN
deliveryMode: "await-all"AND two handlers each return a delayed Promise - THEN
publishresolves only after both promises settle
Scenario: fire-and-forget isolates a throwing handler
- WHEN one handler throws synchronously AND another is correct
- THEN the correct handler still runs AND
publishresolves cleanly
Verified by: src/broker/broker.test.ts::"isolates handler rejections in fire-and-forget mode", src/broker/broker.test.ts::"isolates handler rejections in await-all mode"
Requirement: Conforms to WsBroker type contract
Scenario: type-shape matches WsBroker
- WHEN
expectTypeOf(broker)is compared toWsBroker<Payload> - THEN the comparison passes — every method declared on the abstract contract exists with a compatible signature
Verified by: tests/conformance/api-ws-broker.conformance.test.ts::"type-shape matches WsBroker"
Requirement: Testing harness
A ./testing subpath SHALL export createInMemoryBrokerHarness that
wraps the broker with a captured-envelope observer, a publishOne
convenience, and an idempotent dispose() for use in afterEach.
Scenario: harness captures every published envelope
- WHEN
harness.publishOne(payload)is called twice - THEN
harness.receivedcontains both envelopes in publish order
Verified by: src/testing/testing.test.ts::"captures every envelope published on the default topic"