Initial Commit
This commit is contained in:
156
openspec/specs/api-ws-broker-in-memory/spec.md
Normal file
156
openspec/specs/api-ws-broker-in-memory/spec.md
Normal file
@@ -0,0 +1,156 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# 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"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
Reference in New Issue
Block a user