# `@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.