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

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)
  • 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 Channel registered on a gateway instance. Clients never see a raw Channel — every chann…
    • channel-ops — Free-function module containing every channel-level mutation the gateway exposes — subscribeToChannel, `unsubscribeFr…
    • client-lifecycle — Free-function module containing connectClient and disconnectClient — 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 Message envelopes 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)
  • otel — (no submodules)
    • otel — (no submodules)
  • presence — (no submodules)
  • room — (no submodules)
    • room — (no submodules)
  • types — (no submodules)
    • types — (no submodules)
- [channel](./src/channel/channel.md) — _(no submodules)_- [errors](./src/errors/errors.md) — _(no submodules)_- [errors](./src/errors/errors.md) — _(no submodules)_- [gateway](./src/gateway/gateway.md) — _(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 Channel registered on a gateway instance. Clients never see a raw Channel — every chann…
  • channel-ops — Free-function module containing every channel-level mutation the gateway exposes — subscribeToChannel, `unsubscribeFr…
  • client-lifecycle — Free-function module containing connectClient and disconnectClient — 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 Message envelopes 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 Channel registered on a gateway instance. Clients never see a raw Channel — every chann…
    • channel-ops — Free-function module containing every channel-level mutation the gateway exposes — subscribeToChannel, `unsubscribeFr…
    • client-lifecycle — Free-function module containing connectClient and disconnectClient — 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 Message envelopes 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 Channel registered on a gateway instance. Clients never see a raw Channel — every chann…
    • channel-ops — Free-function module containing every channel-level mutation the gateway exposes — subscribeToChannel, `unsubscribeFr…
    • client-lifecycle — Free-function module containing connectClient and disconnectClient — 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 Message envelopes 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) - 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 Channel registered on a gateway instance. Clients never see a raw Channel — every chann…
    • channel-ops — Free-function module containing every channel-level mutation the gateway exposes — subscribeToChannel, `unsubscribeFr…
    • client-lifecycle — Free-function module containing connectClient and disconnectClient — 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 Message envelopes 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)

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

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