Files
skills/docs/networking/network/websocket-gateway/README.md
2026-05-29 08:20:56 +01:00

193 lines
22 KiB
Markdown

# @suaveplan/websocket-gateway - Server-tier WebSocket gateway daemon — rooms, presence, and channel multiplexing over WebSocket connections. Idempotent start/stop lifecycle, injectable wsServerFactory, AbortSignal-aware, inline /health endpoints, OTel observability, callbacks for auth/authz/rate-limit/audit until those packages land .
## Summary
Server-tier WebSocket gateway daemon with rooms, presence tracking, and channel-multiplexed pub/sub. Pairs with [`@suaveplan/network-transport-websockets`](../network-transport-websockets) on the browser side.
<!-- begin:auto-docs -->
## Documentation tree
- [channel](./src/channel/channel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [channel](./src/channel/channel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [gateway](./src/gateway/gateway.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - heartbeat — Builds the heartbeat sweep that the dae…
- [access-gate](./src/gateway/access-gate.md) — Central rate-limit / authorization / audit pipeline. The gateway calls `gate(...)` once before every mutating action (r…
- [channel-manager](./src/gateway/channel-manager.md) — Owns the lifecycle of every `Channel` registered on a gateway instance. Clients never see a raw `Channel` — every chann…
- [channel-ops](./src/gateway/channel-ops.md) — Free-function module containing every channel-level mutation the gateway exposes — `subscribeToChannel`, `unsubscribeFr…
- [client-lifecycle](./src/gateway/client-lifecycle.md) — Free-function module containing `connectClient` and `disconnectClient` — every state mutation that happens when a clien…
- [defaults](./src/gateway/defaults.md) — The four deferred-dependency hooks (`AuthVerifier`, `AuthorizationCheck`, `RateLimitGate`, `AuditLogger`) need sensible…
- [gateway-deps](./src/gateway/gateway-deps.md) — Centralises the cross-module dependency-bundle construction the daemon needs once at construction time. Without this mo…
- [gateway](./src/gateway/gateway.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - heartbeat — Builds the heartbeat sweep that the dae…
- [heartbeat](./src/gateway/heartbeat.md) — Builds the heartbeat sweep that the daemon installs while running. The sweep periodically calls `PresenceTracker.detect…
- [lifecycle](./src/gateway/lifecycle.md) — Centralises three pieces of lifecycle plumbing the daemon needs but doesn't want inline: WS-server boot, start-completi…
- [message-factory](./src/gateway/message-factory.md) — Tiny factory that produces immutable `Message` envelopes for every outbound dispatch the gateway emits. The factory thr…
- [presence-fire](./src/gateway/presence-fire.md) — Single side-effect helper that fans out one presence transition (`join` / `leave` / `update`) to every observation sink…
- [presence-webhook](./src/gateway/presence-webhook.md) — Best-effort, signed delivery of a single presence event to a configured webhook receiver. The gateway calls this functi…
- [room-ops](./src/gateway/room-ops.md) — Free-function module containing every room-level mutation the gateway exposes — create / delete / join / leave / broadc…
- [health](./src/health/health.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [health](./src/health/health.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [presence](./src/presence/presence.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [presence](./src/presence/presence.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [room](./src/room/room.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [room](./src/room/room.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - [channel](./src/channel/channel.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [gateway](./src/gateway/gateway.md) — <!-- begin:auto-docs --> _(no submodules)_- heartbeat — Builds the heartbeat sweep that the dae…
- [access-gate](./src/gateway/access-gate.md) — Central rate-limit / authorization / audit pipeline. The gateway calls `gate(...)` once before every mutating action (r…
- [channel-manager](./src/gateway/channel-manager.md) — Owns the lifecycle of every `Channel` registered on a gateway instance. Clients never see a raw `Channel` — every chann…
- [channel-ops](./src/gateway/channel-ops.md) — Free-function module containing every channel-level mutation the gateway exposes — `subscribeToChannel`, `unsubscribeFr…
- [client-lifecycle](./src/gateway/client-lifecycle.md) — Free-function module containing `connectClient` and `disconnectClient` — every state mutation that happens when a clien…
- [defaults](./src/gateway/defaults.md) — The four deferred-dependency hooks (`AuthVerifier`, `AuthorizationCheck`, `RateLimitGate`, `AuditLogger`) need sensible…
- [gateway-deps](./src/gateway/gateway-deps.md) — Centralises the cross-module dependency-bundle construction the daemon needs once at construction time. Without this mo…
- [gateway](./src/gateway/gateway.md) — <!-- begin:auto-docs --> _(no submodules)_- heartbeat — Builds the heartbeat sweep that the dae…
- [heartbeat](./src/gateway/heartbeat.md) — Builds the heartbeat sweep that the daemon installs while running. The sweep periodically calls `PresenceTracker.detect…
- [lifecycle](./src/gateway/lifecycle.md) — Centralises three pieces of lifecycle plumbing the daemon needs but doesn't want inline: WS-server boot, start-completi…
- [message-factory](./src/gateway/message-factory.md) — Tiny factory that produces immutable `Message` envelopes for every outbound dispatch the gateway emits. The factory thr…
- [presence-fire](./src/gateway/presence-fire.md) — Single side-effect helper that fans out one presence transition (`join` / `leave` / `update`) to every observation sink…
- [presence-webhook](./src/gateway/presence-webhook.md) — Best-effort, signed delivery of a single presence event to a configured webhook receiver. The gateway calls this functi…
- [room-ops](./src/gateway/room-ops.md) — Free-function module containing every room-level mutation the gateway exposes — create / delete / join / leave / broadc…
- [health](./src/health/health.md) — <!-- begin:auto-docs --> _(no submodules)_- [health](./src/health/health.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [presence](./src/presence/presence.md) — <!-- begin:auto-docs --> _(no submodules)_- [presence](./src/presence/presence.md) — <!-- begin:auto-docs --> _(no submodules)_- [room](./src/room/room.md) — <!-- begin:auto-docs --> _(no submodules)_- [room](./src/room/room.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [channel](./src/channel/channel.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [gateway](./src/gateway/gateway.md) — <!-- begin:auto-docs --> _(no submodules)_- heartbeat — Builds the heartbeat sweep that the dae…
- [access-gate](./src/gateway/access-gate.md) — Central rate-limit / authorization / audit pipeline. The gateway calls `gate(...)` once before every mutating action (r…
- [channel-manager](./src/gateway/channel-manager.md) — Owns the lifecycle of every `Channel` registered on a gateway instance. Clients never see a raw `Channel` — every chann…
- [channel-ops](./src/gateway/channel-ops.md) — Free-function module containing every channel-level mutation the gateway exposes — `subscribeToChannel`, `unsubscribeFr…
- [client-lifecycle](./src/gateway/client-lifecycle.md) — Free-function module containing `connectClient` and `disconnectClient` — every state mutation that happens when a clien…
- [defaults](./src/gateway/defaults.md) — The four deferred-dependency hooks (`AuthVerifier`, `AuthorizationCheck`, `RateLimitGate`, `AuditLogger`) need sensible…
- [gateway-deps](./src/gateway/gateway-deps.md) — Centralises the cross-module dependency-bundle construction the daemon needs once at construction time. Without this mo…
- [gateway](./src/gateway/gateway.md) — <!-- begin:auto-docs --> _(no submodules)_- heartbeat — Builds the heartbeat sweep that the dae…
- [heartbeat](./src/gateway/heartbeat.md) — Builds the heartbeat sweep that the daemon installs while running. The sweep periodically calls `PresenceTracker.detect…
- [lifecycle](./src/gateway/lifecycle.md) — Centralises three pieces of lifecycle plumbing the daemon needs but doesn't want inline: WS-server boot, start-completi…
- [message-factory](./src/gateway/message-factory.md) — Tiny factory that produces immutable `Message` envelopes for every outbound dispatch the gateway emits. The factory thr…
- [presence-fire](./src/gateway/presence-fire.md) — Single side-effect helper that fans out one presence transition (`join` / `leave` / `update`) to every observation sink…
- [presence-webhook](./src/gateway/presence-webhook.md) — Best-effort, signed delivery of a single presence event to a configured webhook receiver. The gateway calls this functi…
- [room-ops](./src/gateway/room-ops.md) — Free-function module containing every room-level mutation the gateway exposes — create / delete / join / leave / broadc…
- [health](./src/health/health.md) — <!-- begin:auto-docs --> _(no submodules)_- [health](./src/health/health.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [presence](./src/presence/presence.md) — <!-- begin:auto-docs --> _(no submodules)_- [presence](./src/presence/presence.md) — <!-- begin:auto-docs --> _(no submodules)_- [room](./src/room/room.md) — <!-- begin:auto-docs --> _(no submodules)_- [room](./src/room/room.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [channel](./src/channel/channel.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [gateway](./src/gateway/gateway.md) — <!-- begin:auto-docs --> _(no submodules)_- heartbeat — Builds the heartbeat sweep that the dae…
- [access-gate](./src/gateway/access-gate.md) — Central rate-limit / authorization / audit pipeline. The gateway calls `gate(...)` once before every mutating action (r…
- [channel-manager](./src/gateway/channel-manager.md) — Owns the lifecycle of every `Channel` registered on a gateway instance. Clients never see a raw `Channel` — every chann…
- [channel-ops](./src/gateway/channel-ops.md) — Free-function module containing every channel-level mutation the gateway exposes — `subscribeToChannel`, `unsubscribeFr…
- [client-lifecycle](./src/gateway/client-lifecycle.md) — Free-function module containing `connectClient` and `disconnectClient` — every state mutation that happens when a clien…
- [defaults](./src/gateway/defaults.md) — The four deferred-dependency hooks (`AuthVerifier`, `AuthorizationCheck`, `RateLimitGate`, `AuditLogger`) need sensible…
- [gateway-deps](./src/gateway/gateway-deps.md) — Centralises the cross-module dependency-bundle construction the daemon needs once at construction time. Without this mo…
- [gateway](./src/gateway/gateway.md) — <!-- begin:auto-docs --> _(no submodules)_- heartbeat — Builds the heartbeat sweep that the dae…
- [heartbeat](./src/gateway/heartbeat.md) — Builds the heartbeat sweep that the daemon installs while running. The sweep periodically calls `PresenceTracker.detect…
- [lifecycle](./src/gateway/lifecycle.md) — Centralises three pieces of lifecycle plumbing the daemon needs but doesn't want inline: WS-server boot, start-completi…
- [message-factory](./src/gateway/message-factory.md) — Tiny factory that produces immutable `Message` envelopes for every outbound dispatch the gateway emits. The factory thr…
- [presence-fire](./src/gateway/presence-fire.md) — Single side-effect helper that fans out one presence transition (`join` / `leave` / `update`) to every observation sink…
- [presence-webhook](./src/gateway/presence-webhook.md) — Best-effort, signed delivery of a single presence event to a configured webhook receiver. The gateway calls this functi…
- [room-ops](./src/gateway/room-ops.md) — Free-function module containing every room-level mutation the gateway exposes — create / delete / join / leave / broadc…
- [health](./src/health/health.md) — <!-- begin:auto-docs --> _(no submodules)_- [health](./src/health/health.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [presence](./src/presence/presence.md) — <!-- begin:auto-docs --> _(no submodules)_- [presence](./src/presence/presence.md) — <!-- begin:auto-docs --> _(no submodules)_- [room](./src/room/room.md) — <!-- begin:auto-docs --> _(no submodules)_- [room](./src/room/room.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [channel](./src/channel/channel.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_ - [errors](./src/errors/errors.md) — <!-- begin:auto-docs --> _(no submodules)_- [gateway](./src/gateway/gateway.md) — <!-- begin:auto-docs --> _(no submodules)_ - [access-gate](./src/gateway/access-gate.md) — Central rate-limit / authorization / audit pipeline. The gateway calls `gate(...)` once before every mutating action (r…
- [channel-manager](./src/gateway/channel-manager.md) — Owns the lifecycle of every `Channel` registered on a gateway instance. Clients never see a raw `Channel` — every chann…
- [channel-ops](./src/gateway/channel-ops.md) — Free-function module containing every channel-level mutation the gateway exposes — `subscribeToChannel`, `unsubscribeFr…
- [client-lifecycle](./src/gateway/client-lifecycle.md) — Free-function module containing `connectClient` and `disconnectClient` — every state mutation that happens when a clien…
- [defaults](./src/gateway/defaults.md) — The four deferred-dependency hooks (`AuthVerifier`, `AuthorizationCheck`, `RateLimitGate`, `AuditLogger`) need sensible…
- [gateway-deps](./src/gateway/gateway-deps.md) — Centralises the cross-module dependency-bundle construction the daemon needs once at construction time. Without this mo…
- [gateway](./src/gateway/gateway.md) — <!-- begin:auto-docs --> _(no submodules)_ - [heartbeat](./src/gateway/heartbeat.md) — Builds the heartbeat sweep that the daemon installs while running. The sweep periodically calls `PresenceTracker.detect…
- [lifecycle](./src/gateway/lifecycle.md) — Centralises three pieces of lifecycle plumbing the daemon needs but doesn't want inline: WS-server boot, start-completi…
- [message-factory](./src/gateway/message-factory.md) — Tiny factory that produces immutable `Message` envelopes for every outbound dispatch the gateway emits. The factory thr…
- [presence-fire](./src/gateway/presence-fire.md) — Single side-effect helper that fans out one presence transition (`join` / `leave` / `update`) to every observation sink…
- [presence-webhook](./src/gateway/presence-webhook.md) — Best-effort, signed delivery of a single presence event to a configured webhook receiver. The gateway calls this functi…
- [room-ops](./src/gateway/room-ops.md) — Free-function module containing every room-level mutation the gateway exposes — create / delete / join / leave / broadc…
- [health](./src/health/health.md) — <!-- begin:auto-docs --> _(no submodules)_ - [health](./src/health/health.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_ - [otel](./src/otel/otel.md) — <!-- begin:auto-docs --> _(no submodules)_- [presence](./src/presence/presence.md) — <!-- begin:auto-docs --> _(no submodules)_ - [presence](./src/presence/presence.md) — <!-- begin:auto-docs --> _(no submodules)_- [room](./src/room/room.md) — <!-- begin:auto-docs --> _(no submodules)_ - [room](./src/room/room.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ - [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_
## Install
```bash
bun add @suaveplan/websocket-gateway
```
## Quick start
```ts
import { WebSocketGateway } from "@suaveplan/websocket-gateway";
import { makeRealWsServerFactory } from "@suaveplan/websocket-gateway/tests/setup/real-ws-adapter";
const gateway = new WebSocketGateway({
port: 8080,
host: "0.0.0.0",
wsServerFactory: makeRealWsServerFactory(() => gateway.snapshot()),
});
const { address, port } = await gateway.start();
console.log(`gateway listening on ws://${address}:${port}`);
// Rooms
gateway.createRoom({ id: "general", maxClients: 100 });
await gateway.joinRoom("general", "alice");
await gateway.broadcastToRoom("general", "alice", { text: "hi" });
// Channels
await gateway.subscribeToChannel("events", "alice", "user.created");
await gateway.publishToChannel("events", "alice", "user.created", { id: "u1" });
// Graceful shutdown
await gateway.stop();
```
## Architecture
Five core subsystems, each in its own feature module:
- `room` — capacity-controlled member sets with broadcast + history.
- `presence` — online/away/busy/offline state with heartbeat sweep.
- `channel` — pub/sub with `.`-delimited topic wildcards (`*`, `**`).
- `gateway` — the orchestrator that owns the WS server, the gate pipeline, and idempotency.
- `health` — inline `/health`, `/ready`, `/live` probes served from the same TCP port.
The daemon binds an injectable `WsServerLike` adapter: production wires the real `ws.WebSocketServer`, tests bind a deterministic in-memory `FakeWsServer` so the no-mocks policy holds.
## Deferred integrations (replace these callbacks when packages land )
The proposal explicitly defers full integration with four upstream packages until they ship. Until then, the gateway accepts callbacks:
| Callback | Default | Replace with |
|---|---|---|
| `authVerifier(handshake) => Promise<AuthContext>` | anonymous pass-through | `@suaveplan/jwt-tenant` JWT verifier |
| `authorizationCheck(ctx, action) => Promise<boolean>` | always allow | `@suaveplan/permissions` RBAC/ABAC gate |
| `rateLimitGate(ctx, action) => Promise<boolean>` | always allow | `@suaveplan/rate-limiter` bucket gate |
| `auditLogger(event) => void` | no-op | `@suaveplan/audit-logging` sink |
Replacement is per-callback: when `@suaveplan/permissions` ships, swap `authorizationCheck` and leave the others alone. Public types (`AuthContext`, `GatewayAction`, `AuditEvent`) do not change.
```ts
import { WebSocketGateway } from "@suaveplan/websocket-gateway";
const gateway = new WebSocketGateway({
port: 0,
wsServerFactory: realFactory,
authVerifier: async (handshake) => {
// When @suaveplan/jwt-tenant lands, replace this body with:
// return jwtTenant.verify(handshake.headers.authorization);
return defaultAuthVerifier(handshake);
},
});
```
## Direct integrations (already wired)
These packages have shipped and the gateway uses them directly:
- `@suaveplan/idempotency` — `IdempotencyGuard` wraps every mutating event (room create, broadcast, presence transition). Pass an `idempotencyKey` on `joinRoom` / `broadcastToRoom` to deduplicate at-least-once retries.
- `@suaveplan/webhook-client` — `deliver()` ships every presence event (when `presenceWebhook` is configured) with HMAC-SHA-256 signing and retry budget. Failures are swallowed so presence is best-effort.
- `@suaveplan/retry` — consumed transitively by `webhook-client` for exponential backoff + circuit breaking.
- `@suaveplan/logger` — structured logging (peer dep; consumer wires its own logger).
## Observability
OTel spans: `ws_gateway.{start, stop, connect, message, broadcast}`. Counters: `ws_gateway.rooms_active` (UpDown), `ws_gateway.presence_updates_total{kind}`, `ws_gateway.actions_total{action, outcome}`. Histograms: `ws_gateway.message_dispatch_ms`, `ws_gateway.start_duration_ms`. All instruments live in `./otel/otel.ts`.
## Configuration
See `WebSocketGatewayConfig` in `src/types/types.ts` for the full surface. Highlights:
- `port` / `host` — listener bind. `port: 0` for ephemeral.
- `maxRooms` / `maxClientsPerRoom` — capacity caps; overflow throws `ROOM_FULL`.
- `heartbeatIntervalMs` / `clientTimeoutMs` — heartbeat sweep cadence + timeout.
- `enablePresence` / `enableChannels` — feature toggles.
- `presenceWebhook` — optional signed webhook delivery for presence events.
- `bus` — optional `EventBusLike<GatewayEventMap>` for typed observability.
## License
MIT