Initial Commit
This commit is contained in:
69
openspec/archive/2026-05-26-api-ws-bun/proposal.md
Normal file
69
openspec/archive/2026-05-26-api-ws-bun/proposal.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# `@suaveplan/api-ws-bun` — `Bun.serve` WebSocket engine
|
||||
|
||||
**Package**: `@suaveplan/api-ws-bun`
|
||||
**Status**: PROPOSED
|
||||
**Type**: new-package
|
||||
**Stage**: 7 (GraphQL + WebSocket / SSE engines) — see umbrella `openspec/changes/api-plugin-foundation/`
|
||||
|
||||
## Why
|
||||
|
||||
Per design doc §11 persistent connections are first-class in the foundation: plugins declare WS endpoints in their manifest and the host runs the engine, manages connection lifecycle, fans out subscriptions. This package implements the abstract `api-ws` contract on top of `Bun.serve`'s WebSocket primitives — the fastest native path on the foundation's target runtime. Without this adapter the `api-ws` contract has no production implementation and Stage-7 acceptance ("publishes a tenant-scoped WebSocket broadcast") cannot be reached.
|
||||
|
||||
## What changes
|
||||
|
||||
Net-new package depending on `@suaveplan/api-types`, `@suaveplan/api-ws` (abstract contract), `@suaveplan/api-router` (handshake upgrade route), `@suaveplan/error`, `@suaveplan/logger`, `@suaveplan/opentelemetry`, `@suaveplan/crypto-hash` (HMAC for reconnect tokens), and `zod`.
|
||||
|
||||
- ADDED: `createBunWsEngine(opts)` — factory returning the engine value implementing `@suaveplan/api-ws`'s contract.
|
||||
- ADDED: `BunWsEngineOptions` — Zod-validated config: `maxQueuedMessages` (default 256), `heartbeatMs` (default 30000, min 5000), `missedPongLimit` (default 2), `maxConnectionsPerTenant` (default 1000), `reconnectTokenTtlMs` (default 300000 = 5 min), `reconnectTokenSecret` (required, used for HMAC), `reconnectEventBufferStrategy` (`"auto"` | `"memory"` | `"broker"`; default `"auto"` resolves to broker-backed when a Redis broker is wired, in-memory otherwise — this is the §14 open-question default), `subprotocols`.
|
||||
- ADDED: Connection lifecycle:
|
||||
- Handshake → per-request pipeline steps 1–7 → `onConnect` with `Connection` bound to `principal` + `tenantId`. Principal frozen for connection lifetime; mid-connection token expiry emits `connection.principal_expired`.
|
||||
- `ConnectionRegistry` indexed by `tenantId` for fan-out lookup.
|
||||
- `onMessage` invoked per inbound frame.
|
||||
- `onClose` invoked on either side's close.
|
||||
- ADDED: Backpressure — per-connection send queue capped at `maxQueuedMessages`. Exceeding sends `BACKPRESSURE_EVICTED` close frame and drops the connection.
|
||||
- ADDED: Heartbeats — ping every `heartbeatMs`; `missedPongLimit` missed pongs in a row → connection considered dead, `onClose` fires with `code: ABNORMAL`.
|
||||
- ADDED: Per-tenant caps — `maxConnectionsPerTenant` checked at handshake; exceeding rejects with 429 and `WS_BUN_TENANT_CAP_EXCEEDED`.
|
||||
- ADDED: Reconnect tokens — on `onClose`, host issues an HMAC-signed token (5-min TTL) carrying `{ tenant, principalId, lastEventId }`. Client reconnects with `?reconnectToken=...`; engine validates and resumes subscriptions automatically. Reconnect-event buffer storage follows `reconnectEventBufferStrategy`: when the host has a Redis broker wired (Redis-backed), the event buffer lives in Redis; otherwise it lives in-memory on the local host (default per design §11.5 paragraph 4, which this package pins as the operating default).
|
||||
- ADDED: Subscription bus integration — `conn.subscribe(topic, { tenant?, filter? })` registers against `api-ws`'s subscription bus; `bus.broadcast(topic, { tenant, payload })` triggers fan-out.
|
||||
- ADDED: `WS_BUN_*` error codes: `WS_BUN_HANDSHAKE_REJECTED`, `WS_BUN_TENANT_CAP_EXCEEDED`, `WS_BUN_BACKPRESSURE_EVICTED`, `WS_BUN_HEARTBEAT_FAILED`, `WS_BUN_RECONNECT_TOKEN_INVALID`, `WS_BUN_RECONNECT_TOKEN_EXPIRED`, `WS_BUN_RECONNECT_BUFFER_MISS`, `WS_BUN_SUBPROTOCOL_UNSUPPORTED`, `WS_BUN_CLOSE_TIMEOUT`, `WS_BUN_PRINCIPAL_EXPIRED`.
|
||||
- ADDED: OTel surface from design §11.7 — `ws.handshake.duration_ms { plugin, path }`, `ws.connections { plugin, tenant }` gauge, `ws.messages_in_total`, `ws.messages_out_total`, `ws.broadcasts_total { topic, tenant, recipients }`, `ws.backpressure_evicted_total`, `ws.heartbeat_failures_total`.
|
||||
- ADDED: `./testing` subpath — `runWsEngineConformance` from `api-ws/testing` bound to `createBunWsEngine`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Cross-host broadcast — that's `api-ws-broker-redis`. This engine integrates with whichever broker the host wires.
|
||||
- GraphQL subscription transport — `api-graphql-yoga`/`api-graphql-apollo` ride this engine via `graphql-ws`; the transport bridging is in those packages.
|
||||
- SSE — separate engine `api-sse-bun`. Reuses the same broker abstraction but the transport differs.
|
||||
|
||||
## Source mapping
|
||||
|
||||
Genesis-original. Source of design intent: `docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md`.
|
||||
|
||||
| Requirement | Design doc section |
|
||||
|---|---|
|
||||
| Connection lifecycle (`onConnect` / `onMessage` / `onClose`) | §11.1 |
|
||||
| Handshake flow steps 1–7 + frozen principal | §11.1 (paragraph 2) |
|
||||
| `ConnectionRegistry` indexed by `tenantId` | §11.1 (paragraph 3) |
|
||||
| Subscription bus + per-tenant fan-out | §11.2 |
|
||||
| Backpressure: `maxQueuedMessages` default 256 + eviction close frame | §11.4 |
|
||||
| Heartbeats: `heartbeatMs` default 30000, 2 missed pongs → drop | §11.4 |
|
||||
| Per-tenant caps `maxConnectionsPerTenant` | §11.4 |
|
||||
| Reconnect tokens: HMAC-signed, 5-min TTL, `{tenant, principalId, lastEventId}` | §11.5 |
|
||||
| Reconnect-event-buffer storage default (broker-when-Redis / memory-otherwise) | §11.5 (paragraph 4) + §14 |
|
||||
| OTel instruments listed above | §11.7 |
|
||||
|
||||
## Verification approach
|
||||
|
||||
- `src/engine/bun-ws-engine.test.ts` — handshake, message dispatch, close handling, principal frozen at handshake.
|
||||
- `src/engine/bun-ws-engine.spec-conformance.test.ts` — runs `runWsEngineConformance("bun", createBunWsEngine)`.
|
||||
- `src/backpressure/backpressure.test.ts` — queue overflow evicts connection with `BACKPRESSURE_EVICTED`.
|
||||
- `src/heartbeat/heartbeat.test.ts` — 2 missed pongs drops connection; uses `FakeClock`.
|
||||
- `src/limits/tenant-cap.test.ts` — 1001st connection from same tenant rejected.
|
||||
- `src/reconnect/reconnect-token.test.ts` — token issue + validation + expiry + replay-rejection.
|
||||
- `src/reconnect/event-buffer.test.ts` — strategy `"auto"` resolves to broker-backed when a Redis broker is wired, in-memory otherwise.
|
||||
- `src/registry/connection-registry.test.ts` — index by tenant; cross-tenant fan-out gated by `ws.broadcast-cross-tenant` capability.
|
||||
- `tests/integration/end-to-end.test.ts` (30000ms) — real `Bun.serve` listener, real WS client, full handshake → subscribe → broadcast → close → reconnect → resume.
|
||||
|
||||
## Migration impact
|
||||
|
||||
None — additive only.
|
||||
Reference in New Issue
Block a user