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,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.