22 KiB
@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 on the browser side.
Documentation tree
- channel — (no submodules)
- channel — (no submodules)
- errors — (no submodules)
- errors — (no submodules)
- gateway — (no submodules) - heartbeat — Builds the heartbeat sweep that the dae…
- access-gate — Central rate-limit / authorization / audit pipeline. The gateway calls
gate(...)once before every mutating action (r… - channel-manager — Owns the lifecycle of every
Channelregistered on a gateway instance. Clients never see a rawChannel— every chann… - channel-ops — Free-function module containing every channel-level mutation the gateway exposes —
subscribeToChannel, `unsubscribeFr… - client-lifecycle — Free-function module containing
connectClientanddisconnectClient— every state mutation that happens when a clien… - defaults — The four deferred-dependency hooks (
AuthVerifier,AuthorizationCheck,RateLimitGate,AuditLogger) need sensible… - gateway-deps — Centralises the cross-module dependency-bundle construction the daemon needs once at construction time. Without this mo…
- gateway — (no submodules) - heartbeat — Builds the heartbeat sweep that the dae…
- heartbeat — Builds the heartbeat sweep that the daemon installs while running. The sweep periodically calls `PresenceTracker.detect…
- lifecycle — Centralises three pieces of lifecycle plumbing the daemon needs but doesn't want inline: WS-server boot, start-completi…
- message-factory — Tiny factory that produces immutable
Messageenvelopes for every outbound dispatch the gateway emits. The factory thr… - presence-fire — Single side-effect helper that fans out one presence transition (
join/leave/update) to every observation sink… - presence-webhook — Best-effort, signed delivery of a single presence event to a configured webhook receiver. The gateway calls this functi…
- room-ops — Free-function module containing every room-level mutation the gateway exposes — create / delete / join / leave / broadc…
- access-gate — Central rate-limit / authorization / audit pipeline. The gateway calls
- health — (no submodules)
- health — (no submodules)
- otel — (no submodules)
- otel — (no submodules)
- presence — (no submodules)
- presence — (no submodules)
- room — (no submodules)
- room — (no submodules)
- types — (no submodules)
- types — (no submodules)
- access-gate — Central rate-limit / authorization / audit pipeline. The gateway calls
gate(...)once before every mutating action (r… - channel-manager — Owns the lifecycle of every
Channelregistered on a gateway instance. Clients never see a rawChannel— every chann… - channel-ops — Free-function module containing every channel-level mutation the gateway exposes —
subscribeToChannel, `unsubscribeFr… - client-lifecycle — Free-function module containing
connectClientanddisconnectClient— every state mutation that happens when a clien… - defaults — The four deferred-dependency hooks (
AuthVerifier,AuthorizationCheck,RateLimitGate,AuditLogger) need sensible… - gateway-deps — Centralises the cross-module dependency-bundle construction the daemon needs once at construction time. Without this mo…
- gateway — (no submodules)- heartbeat — Builds the heartbeat sweep that the dae…
- heartbeat — Builds the heartbeat sweep that the daemon installs while running. The sweep periodically calls `PresenceTracker.detect…
- lifecycle — Centralises three pieces of lifecycle plumbing the daemon needs but doesn't want inline: WS-server boot, start-completi…
- message-factory — Tiny factory that produces immutable
Messageenvelopes for every outbound dispatch the gateway emits. The factory thr… - presence-fire — Single side-effect helper that fans out one presence transition (
join/leave/update) to every observation sink… - presence-webhook — Best-effort, signed delivery of a single presence event to a configured webhook receiver. The gateway calls this functi…
- room-ops — Free-function module containing every room-level mutation the gateway exposes — create / delete / join / leave / broadc…
- health — (no submodules)- health — (no submodules)- otel — (no submodules)- otel — (no submodules)- presence — (no submodules)- presence — (no submodules)- room — (no submodules)- room — (no submodules)- types — (no submodules)- types — (no submodules)- channel — (no submodules)- errors — (no submodules)- errors — (no submodules)- gateway — (no submodules)- heartbeat — Builds the heartbeat sweep that the dae…
- access-gate — Central rate-limit / authorization / audit pipeline. The gateway calls
gate(...)once before every mutating action (r… - channel-manager — Owns the lifecycle of every
Channelregistered on a gateway instance. Clients never see a rawChannel— every chann… - channel-ops — Free-function module containing every channel-level mutation the gateway exposes —
subscribeToChannel, `unsubscribeFr… - client-lifecycle — Free-function module containing
connectClientanddisconnectClient— every state mutation that happens when a clien… - defaults — The four deferred-dependency hooks (
AuthVerifier,AuthorizationCheck,RateLimitGate,AuditLogger) need sensible… - gateway-deps — Centralises the cross-module dependency-bundle construction the daemon needs once at construction time. Without this mo…
- gateway — (no submodules)- heartbeat — Builds the heartbeat sweep that the dae…
- heartbeat — Builds the heartbeat sweep that the daemon installs while running. The sweep periodically calls `PresenceTracker.detect…
- lifecycle — Centralises three pieces of lifecycle plumbing the daemon needs but doesn't want inline: WS-server boot, start-completi…
- message-factory — Tiny factory that produces immutable
Messageenvelopes for every outbound dispatch the gateway emits. The factory thr… - presence-fire — Single side-effect helper that fans out one presence transition (
join/leave/update) to every observation sink… - presence-webhook — Best-effort, signed delivery of a single presence event to a configured webhook receiver. The gateway calls this functi…
- room-ops — Free-function module containing every room-level mutation the gateway exposes — create / delete / join / leave / broadc…
- access-gate — Central rate-limit / authorization / audit pipeline. The gateway calls
- health — (no submodules)- health — (no submodules)- otel — (no submodules)- otel — (no submodules)- presence — (no submodules)- presence — (no submodules)- room — (no submodules)- room — (no submodules)- types — (no submodules)- types — (no submodules)- channel — (no submodules)- errors — (no submodules)- errors — (no submodules)- gateway — (no submodules)- heartbeat — Builds the heartbeat sweep that the dae…
- access-gate — Central rate-limit / authorization / audit pipeline. The gateway calls
gate(...)once before every mutating action (r… - channel-manager — Owns the lifecycle of every
Channelregistered on a gateway instance. Clients never see a rawChannel— every chann… - channel-ops — Free-function module containing every channel-level mutation the gateway exposes —
subscribeToChannel, `unsubscribeFr… - client-lifecycle — Free-function module containing
connectClientanddisconnectClient— every state mutation that happens when a clien… - defaults — The four deferred-dependency hooks (
AuthVerifier,AuthorizationCheck,RateLimitGate,AuditLogger) need sensible… - gateway-deps — Centralises the cross-module dependency-bundle construction the daemon needs once at construction time. Without this mo…
- gateway — (no submodules)- heartbeat — Builds the heartbeat sweep that the dae…
- heartbeat — Builds the heartbeat sweep that the daemon installs while running. The sweep periodically calls `PresenceTracker.detect…
- lifecycle — Centralises three pieces of lifecycle plumbing the daemon needs but doesn't want inline: WS-server boot, start-completi…
- message-factory — Tiny factory that produces immutable
Messageenvelopes for every outbound dispatch the gateway emits. The factory thr… - presence-fire — Single side-effect helper that fans out one presence transition (
join/leave/update) to every observation sink… - presence-webhook — Best-effort, signed delivery of a single presence event to a configured webhook receiver. The gateway calls this functi…
- room-ops — Free-function module containing every room-level mutation the gateway exposes — create / delete / join / leave / broadc…
- access-gate — Central rate-limit / authorization / audit pipeline. The gateway calls
- health — (no submodules)- health — (no submodules)- otel — (no submodules)- otel — (no submodules)- presence — (no submodules)- presence — (no submodules)- room — (no submodules)- room — (no submodules)- types — (no submodules)- types — (no submodules)- channel — (no submodules)- errors — (no submodules) - errors — (no submodules)- gateway — (no submodules) - access-gate — Central rate-limit / authorization / audit pipeline. The gateway calls
gate(...)once before every mutating action (r…- channel-manager — Owns the lifecycle of every
Channelregistered on a gateway instance. Clients never see a rawChannel— every chann… - channel-ops — Free-function module containing every channel-level mutation the gateway exposes —
subscribeToChannel, `unsubscribeFr… - client-lifecycle — Free-function module containing
connectClientanddisconnectClient— every state mutation that happens when a clien… - defaults — The four deferred-dependency hooks (
AuthVerifier,AuthorizationCheck,RateLimitGate,AuditLogger) need sensible… - gateway-deps — Centralises the cross-module dependency-bundle construction the daemon needs once at construction time. Without this mo…
- gateway — (no submodules) - heartbeat — Builds the heartbeat sweep that the daemon installs while running. The sweep periodically calls `PresenceTracker.detect…
- lifecycle — Centralises three pieces of lifecycle plumbing the daemon needs but doesn't want inline: WS-server boot, start-completi…
- message-factory — Tiny factory that produces immutable
Messageenvelopes for every outbound dispatch the gateway emits. The factory thr… - presence-fire — Single side-effect helper that fans out one presence transition (
join/leave/update) to every observation sink… - presence-webhook — Best-effort, signed delivery of a single presence event to a configured webhook receiver. The gateway calls this functi…
- room-ops — Free-function module containing every room-level mutation the gateway exposes — create / delete / join / leave / broadc…
- channel-manager — Owns the lifecycle of every
- health — (no submodules) - health — (no submodules)- otel — (no submodules) - otel — (no submodules)- presence — (no submodules) - presence — (no submodules)- room — (no submodules) - room — (no submodules)- types — (no submodules) - types — (no submodules)
Install
bun add @suaveplan/websocket-gateway
Quick start
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,/liveprobes 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.
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—IdempotencyGuardwraps every mutating event (room create, broadcast, presence transition). Pass anidempotencyKeyonjoinRoom/broadcastToRoomto deduplicate at-least-once retries.@suaveplan/webhook-client—deliver()ships every presence event (whenpresenceWebhookis configured) with HMAC-SHA-256 signing and retry budget. Failures are swallowed so presence is best-effort.@suaveplan/retry— consumed transitively bywebhook-clientfor 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: 0for ephemeral.maxRooms/maxClientsPerRoom— capacity caps; overflow throwsROOM_FULL.heartbeatIntervalMs/clientTimeoutMs— heartbeat sweep cadence + timeout.enablePresence/enableChannels— feature toggles.presenceWebhook— optional signed webhook delivery for presence events.bus— optionalEventBusLike<GatewayEventMap>for typed observability.
License
MIT