Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,49 @@
# `@suaveplan/api-ws-broker-in-memory` — In-memory WebSocket broker for tests
**Package**: `@suaveplan/api-ws-broker-in-memory`
**Status**: PROPOSED
**Type**: new-package
**Stage**: 5 (Testing kit + conformance suites) — see umbrella `openspec/changes/api-plugin-foundation/`
## Why
Per design doc §11, `api-ws` defines a `WsBroker` abstraction for subscription fan-out. Multi-host deployments wire this to Redis via `api-ws-broker-redis` (Stage 7). For tests AND single-host development, an in-memory broker is sufficient — it pumps published messages directly to every interested connection without crossing a network.
The package is a load-bearing piece of `createTestHost`: any plugin manifesting WebSocket endpoints can be tested end-to-end (connect → subscribe → publish → assert received) without spinning up Redis. The broker is production-grade for single-host topologies; "in-memory" describes implementation, not test-double status.
## What changes
Net-new package depending on `@suaveplan/api-ws` (contract + `./testing` conformance), `@suaveplan/api-types`, `@suaveplan/error`, `zod`.
- ADDED: `createInMemoryWsBroker(opts)` — factory returning a `WsBroker` (per `@suaveplan/api-types`) that maintains a per-subscription-topic set of connection ids and a `publish(topic, message)` method that delivers to every subscriber in O(subscribers).
- ADDED: `InMemoryWsBrokerOptions` — Zod schema (`maxSubscribersPerTopic?: number` (default unlimited), `deliveryMode: "fire-and-forget" | "await-all"` (default `"fire-and-forget"`)).
- ADDED: `subscribe(connectionId, topic)`, `unsubscribe(connectionId, topic)`, `publish(topic, message)`, `subscribersOf(topic)`, `cleanupConnection(connectionId)`.
- ADDED: `WS_BROKER_IN_MEMORY_*` error codes (`WS_BROKER_IN_MEMORY_MAX_SUBSCRIBERS_EXCEEDED`, `WS_BROKER_IN_MEMORY_INVALID_OPTIONS`).
- ADDED: Registered in `@suaveplan/api-host/testing`'s adapter table as `ws.broker: "in-memory"` (default for `createTestHost`).
## Out of scope
- Network distribution — by design.
- Reconnect token persistence — that lives in the WebSocket engine (`api-ws-bun` Stage 7), not the broker.
- Message persistence — broker is fan-out only; tests assert via `host.ws.connectionsOf(topic)` and direct connection observation.
## Source mapping
Genesis-original. Source of design intent: `docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md`.
| Requirement | Design doc section |
|---|---|
| Subscription bus + broker abstraction | §11.2, §11.3 |
| Single-host + test broker | §11.3 (final sentence) |
| Registered in `api-host/testing` adapter table | §15 Stage 5 |
## Verification approach
- `src/factory/create-in-memory-ws-broker.test.ts` — subscribe, publish, fan-out count; unsubscribe; cleanupConnection on disconnect.
- `src/factory/create-in-memory-ws-broker.spec-conformance.test.ts` — `runWsBrokerConformance("in-memory-direct", ...)`.
- `src/delivery/delivery-mode.test.ts` — `"await-all"` waits for every subscriber's handler to settle; `"fire-and-forget"` returns synchronously.
- `tests/integration/integration.test.ts` — via `createTestHost`; full subscribe-then-publish flow against a plugin manifesting a WS endpoint.
## Migration impact
None — additive only.

View File

@@ -0,0 +1,97 @@
<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: api-ws-broker-in-memory (api-ws-broker-in-memory)
**Package**: `@suaveplan/api-ws-broker-in-memory`
**Status**: PROPOSED
## ADDED Requirements
### Requirement: Fan-out to every subscriber on publish
The broker SHALL deliver `publish(topic, message)` to every connection currently subscribed to `topic`. Delivery order across subscribers is unspecified; per-subscriber message order is FIFO.
#### Scenario: every subscriber receives every message
- **WHEN** three connections subscribe to `"orders.created"` AND two messages are published
- **THEN** each connection receives both messages in publish order
**Verified by:** `src/factory/create-in-memory-ws-broker.test.ts::"every subscriber receives every published message in FIFO order"`
---
### Requirement: Subscription lifecycle
The broker SHALL expose `subscribe(connectionId, topic)`, `unsubscribe(connectionId, topic)`, and `cleanupConnection(connectionId)`. `cleanupConnection` removes the connection from every topic it was subscribed to.
#### Scenario: unsubscribe stops delivery
- **WHEN** a connection subscribes, then unsubscribes, then a message is published
- **THEN** the connection does NOT receive the message
#### Scenario: cleanupConnection removes from every topic
- **WHEN** a connection is subscribed to `["a", "b", "c"]` AND `cleanupConnection(id)` is called
- **THEN** `subscribersOf("a")`, `subscribersOf("b")`, `subscribersOf("c")` no longer include `id`
#### Scenario: cleanup is idempotent
- **WHEN** `cleanupConnection(id)` is called twice
- **THEN** the second call is a no-op AND no error is thrown
**Verified by:** `src/factory/create-in-memory-ws-broker.test.ts::"unsubscribe stops delivery"` AND `src/factory/create-in-memory-ws-broker.test.ts::"cleanupConnection removes from every topic"` AND `src/factory/create-in-memory-ws-broker.test.ts::"cleanupConnection is idempotent"`
---
### Requirement: maxSubscribersPerTopic enforced when set
When `options.maxSubscribersPerTopic` is set AND a new `subscribe` would exceed it, the broker SHALL throw `WS_BROKER_IN_MEMORY_MAX_SUBSCRIBERS_EXCEEDED`.
#### Scenario: subscribe rejected at cap
- **WHEN** `maxSubscribersPerTopic: 2` AND two connections subscribe AND a third tries to subscribe
- **THEN** the third call throws `WS_BROKER_IN_MEMORY_MAX_SUBSCRIBERS_EXCEEDED`
**Verified by:** `src/factory/create-in-memory-ws-broker.test.ts::"subscribe throws when maxSubscribersPerTopic is exceeded"`
---
### Requirement: Delivery mode semantics
`deliveryMode: "await-all"` SHALL await every subscriber's handler before `publish` resolves. `"fire-and-forget"` SHALL return immediately after dispatch.
#### Scenario: await-all awaits every handler
- **WHEN** `deliveryMode: "await-all"` AND two subscribers each return a 50ms-delayed promise
- **THEN** `publish` resolves only after both promises settle (measurable timing)
#### Scenario: fire-and-forget returns synchronously
- **WHEN** `deliveryMode: "fire-and-forget"`
- **THEN** `publish` resolves before any subscriber's delayed promise has settled
**Verified by:** `src/delivery/delivery-mode.test.ts::"await-all blocks publish until every handler settles"` AND `src/delivery/delivery-mode.test.ts::"fire-and-forget resolves publish synchronously"`
---
### Requirement: Conforms to WsBroker contract
#### Scenario: conformance suite passes
- **WHEN** `runWsBrokerConformance("in-memory-direct", () => createInMemoryWsBroker(...))` runs
- **THEN** every invariant passes
**Verified by:** `src/factory/create-in-memory-ws-broker.spec-conformance.test.ts::"in-memory-direct conforms to WsBroker contract"`
---
### Requirement: Registered in `api-host/testing` adapter table
#### Scenario: createTestHost resolves the sentinel
- **WHEN** `createTestHost({ adapters: { "ws.broker": "in-memory" } })`
- **THEN** the resulting `host.ws.broker` is an instance produced by this factory
**Verified by:** `tests/integration/integration.test.ts::"createTestHost resolves ws.broker:\"in-memory\" to this adapter"`
<!-- markdownlint-enable MD024 -->

View File

@@ -0,0 +1,66 @@
# Tasks — `@suaveplan/api-ws-broker-in-memory`
> Every checkbox MUST reflect work actually done in this change.
## 0. Pre-flight (non-negotiable)
- [x] 0.1 Clean working tree confirmed in worktree (`git status` clean before changes).
- [x] 0.2 Worktree accounted for — `agent-a080e5bdf978057c8` is this active agent; will be removed on merge.
- [x] 0.3 Manifest re-generation triggered after package add (see §7.4).
## 1. Spec authoring
- [x] 1.1 Read api-ws Stage-1 contract (`packages/api-stack/api/api-ws/src/broker/broker.ts`)
- [x] 1.2 Read api-ws testing harness (`runWsEngineConformance`)
- [x] 1.3 Read `openspec/changes/api-ws-broker-in-memory/proposal.md` and existing spec delta
- [x] 1.4 Reconciled spec to actual `WsBroker` contract (publish/subscribe(topic,handler)/dispose) — diverged from initial connectionId-style proposal which did not match the upstream contract
- [x] 1.5 Folded reconciled spec into `openspec/specs/api-ws-broker-in-memory/spec.md`
- [x] 1.6 Every requirement carries `**Verified by:**`
## 2. Implementation
- [x] 2.1 Scaffolded `packages/api-stack/api/api-ws-broker-in-memory/` (deps: api-ws, api-types, error, tslib, zod)
- [x] 2.2 `src/options/options.ts` — `InMemoryWsBrokerOptions` + strict Zod schema + `resolveInMemoryWsBrokerOptions`
- [x] 2.3 `src/broker/broker.ts` — `createInMemoryWsBroker(options?)` factory returning `InMemoryWsBroker<Payload>` (extends abstract `WsBroker<Payload>` + `subscriberCount` + `isDisposed`)
- [x] 2.4 Delivery mode handled inline in `broker.ts` `dispatch()` — `await-all` awaits `Promise.all`, `fire-and-forget` returns sync after dispatch
- [x] 2.5 `src/error-codes/error-codes.ts` — `WS_BROKER_IN_MEMORY_CODES` (`INVALID_OPTIONS`, `BROKER_DISPOSED`) via `defineCodes`
- [x] 2.6 `src/index.ts` — explicit named re-exports (no `export *`)
- [x] 2.7 `src/testing/testing.ts` — `createInMemoryBrokerHarness` shipped under `./testing` subpath
## 3. Tests
- [x] 3.1 `src/broker/broker.test.ts` — 15 tests covering fan-out, isolation, lifecycle, dispose
- [x] 3.2 `tests/conformance/api-ws-broker.conformance.test.ts` — `WsBroker` type+behaviour conformance
- [x] 3.3 Delivery-mode behaviour exercised inline in `broker.test.ts` (`isolates handler rejections in {fire-and-forget,await-all} mode`)
- [x] 3.4 `src/testing/testing.test.ts` — harness behaviour
- [x] 3.5 Every `it(...)` carries `{ timeout: 5000 }`
## 4. Documentation
- [x] 4.1 Sibling `.md` per implementation `.ts` — `broker.md`, `options.md`, `error-codes.md`, `testing.md` (docs:check 4/4 100%)
- [x] 4.2 README documents single-host topology and points operators to `api-ws-broker-redis` for multi-host
## 5. Pre-archive verification gates
- [x] 5.1 `bunx turbo lint --filter=@suaveplan/api-ws-broker-in-memory` — green
- [x] 5.2 `bunx turbo typecheck --filter=@suaveplan/api-ws-broker-in-memory` — green
- [x] 5.3 `bunx turbo test --filter=@suaveplan/api-ws-broker-in-memory` — 34/34 passed
- [x] 5.4 `bunx turbo test:coverage --filter=@suaveplan/api-ws-broker-in-memory` — 100/100/100/100 (istanbul)
- [x] 5.5 `bunx turbo build --filter=@suaveplan/api-ws-broker-in-memory` — green
## 6. Pre-archive checklist
- [x] 6.1 `expectTypeOf(broker).toMatchTypeOf<WsBroker<Payload>>()` asserted in conformance test
- [x] 6.2 Zod strict-mode schema at options boundary
- [x] 6.3 Both `WS_BROKER_IN_MEMORY.*` codes (`INVALID_OPTIONS`, `BROKER_DISPOSED`) thrown by ≥1 production path each; covered by tests
- [x] 6.4 Both delivery modes (`fire-and-forget`, `await-all`) exercised under both happy-path and rejecting-handler scenarios
- [x] 6.5 Snapshot-before-dispatch behaviour asserted ("publish snapshot is stable against mid-fan-out unsubscribe")
- [x] 6.6 Dispose idempotency asserted explicitly
- [x] 6.7 No `// biome-ignore` suppressions; no direct `vitest`/`bun:test` imports (verified by grep)
## 7. Archive
- [x] 7.1 Folded into `openspec/specs/api-ws-broker-in-memory/spec.md`
- [x] 7.2 Moved to `openspec/archive/2026-05-26-api-ws-broker-in-memory/`
- [x] 7.3 STATUS.md ticked (see commit)
- [x] 7.4 Manifest regenerated via `bun scripts/folder-reorg/generate-manifest.ts`