# `bun-ws-engine` _(no submodules)_ ## Purpose `createBunWsEngine` is the package's headline factory. It composes every other module in the package — registry, bus, instruments, backpressure guard, heartbeat controller, reconnect token issuer, reconnect buffer, principal-expiry watcher — into a single `BunWsEngine` value that the host wires into `Bun.serve`'s `websocket` option. The factory is also the entry the conformance harness drives via `makeTestConnection`, so the unit-level conformance run exercises the same code paths a production handshake takes. The engine owns connection lifetime: it freezes the principal at handshake, holds per-connection backpressure and heartbeat state, fires `principal_expired` listeners when the underlying token lapses, and emits the OTel surface from design §11.7 against every state transition. ## Features - One-line composition of the full §11 connection lifecycle. - Drop-in `(bus, registry, broker)` triple for `runWsEngineConformance`. - `engine.attach({ ... })` runs handshake validation, enforces the per-tenant cap, instantiates the backpressure / heartbeat / principal-expiry machinery, and wires the sink onto the resulting `ManagedConnection`. - `engine.broadcast(caller, topic, opts)` records to `ws.broadcasts_total`, fans out via the bus, and persists the broadcast into the reconnect-event buffer in one call. - `engine.onPrincipalExpired(listener)` registers a host-level listener for the `connection.principal_expired` event. - `engine.dispose()` closes every open connection and tears down the reconnect buffer. ## Usage: Basic ```ts import { createBunWsEngine } from "@suaveplan/api-ws-bun"; const engine = createBunWsEngine({ reconnectTokenSecret: new Uint8Array(32).fill(7), }); const conn = engine.attach({ id: "c1", principal, tenantId, capabilities: new Set(), tokenExpiresAt: Date.now() + 60_000, sink: { send: () => undefined, close: () => undefined }, }); conn.subscribe("orders"); engine.broadcast( { tenant: tenantId, capabilities: new Set() }, "orders", { tenant: tenantId, payload: { ok: true } } ); ``` ## Usage: Advanced ```ts import { createInMemoryWsBroker } from "@suaveplan/api-ws-broker-in-memory"; import { createBunWsEngine, WS_BUN_CODES } from "@suaveplan/api-ws-bun"; const broker = createInMemoryWsBroker(); const engine = createBunWsEngine({ reconnectTokenSecret: new Uint8Array(32).fill(11), broker, heartbeatMs: 10_000, missedPongLimit: 3, maxConnectionsPerTenant: 5_000, reconnectEventBufferStrategy: "auto", pluginId: "demo-plugin", }); engine.onPrincipalExpired((evt) => log.warn(evt)); try { engine.attach({ id: "c-overflow", principal, tenantId, capabilities: new Set(), tokenExpiresAt: Date.now() + 60_000, sink }); } catch (err) { if ((err as { code: string }).code === WS_BUN_CODES.TENANT_CAP_EXCEEDED) { // surface 429 } } await engine.dispose(); ``` ## API Reference - `CreateBunWsEngineInput` — `BunWsEngineOptions & { broker?, clock? }`. - `BunWsEngine` — `{ bus, registry, broker, instruments, options, reconnectTokens, reconnectBuffer, attach, recordInbound, broadcast, onPrincipalExpired, dispose }`. - `AttachConnectionInput` — args to `engine.attach`. - `ManagedConnection extends WsConnection` — exposes the per-connection guards for assertions. - `BunWsSocketSink` — `{ send, close, ping? }`. - `createBunWsEngine(input): BunWsEngine` — factory. - `makeTestConnection(engine, { id, tenant })` — conformance harness helper. ## Implementation Notes The engine deliberately keeps the sink abstract (`BunWsSocketSink`) rather than depending on Bun's `ServerWebSocket` type. In production the host adapts a `ServerWebSocket` into a sink with three methods; in tests the sink is a closure capturing sent frames. This split is what lets the unit test suite drive the engine without spinning up Bun.serve, while the integration test (`tests/integration/end-to-end.test.ts`) drives the full Bun listener path against the same sink contract. The `lastEventId` and `state` fields on `ManagedConnection` use getters so the engine can mutate the underlying closures without breaking `WsConnection`'s readonly contract. The contract is a structural type, so this is fine. The principal-expiry callback flips `state` to `"closing"` but does not eagerly close the socket; the engine waits for the next handshake or send to drain the connection. This matches the spec wording — "host emits `connection.principal_expired`; plugin's `onClose` MUST then be invokable" — which puts the actual close decision on the plugin, not the engine.