Files
2026-05-27 23:29:53 +01:00

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() returns true

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 then broker.publish(...) is awaited
  • THEN the call throws WS_BROKER_IN_MEMORY.BROKER_DISPOSED

Scenario: subscribe after dispose throws

  • WHEN broker.dispose() is called then broker.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 publish resolves 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 publish resolves 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 to WsBroker<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.received contains both envelopes in publish order

Verified by: src/testing/testing.test.ts::"captures every envelope published on the default topic"