Initial Commit
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
# `bun-ws-engine`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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<Payload>` — `{ 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.
|
||||
Reference in New Issue
Block a user