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

10 KiB

WebSocketGateway

(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…

  • 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…

  • 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…

  • 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…

  • 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…

Purpose

Server-tier WebSocket gateway daemon. Hosts rooms (broadcast / direct / capacity-controlled), presence tracking, channel-multiplexed pub/sub, and a four-callback access pipeline (auth / authz / rate-limit / audit) on top of an injectable WsServerLike adapter. Production binds ws.WebSocketServer from ws; tests bind a deterministic in-memory FakeWsServer so the no-mocks rule holds. Pairs with @suaveplan/network-transport-websockets on the browser side.

Features

  • Idempotent start({signal?}) → {address, port} / stop() lifecycle. stop() drains in-flight clients and is safe to call multiple times. AbortSignal is honoured at every checkpoint.
  • Five core subsystems: gateway (this class), room, presence, channel, health — each in its own feature module under src/.
  • Direct integration with three shipped packages: @suaveplan/idempotency (every mutating event runs through IdempotencyGuard), @suaveplan/webhook-client (signed presence webhook delivery), @suaveplan/retry (consumed transitively by webhook-client).
  • Four deferred-dep callbacks (authVerifier, authorizationCheck, rateLimitGate, auditLogger) that become real package gates when @suaveplan/jwt-tenant / @suaveplan/permissions / @suaveplan/rate-limiter / @suaveplan/audit-logging ship . Defaults are no-op pass-throughs documented in defaults.md.
  • OTel surface: spans ws_gateway.{start,stop,connect,message,broadcast}, counters rooms_active (UpDown), presence_updates_total{kind}, actions_total{action,outcome}, histograms message_dispatch_ms, start_duration_ms.
  • Bounded heartbeat sweep with injectable Clock. Times-out clients past clientTimeoutMs and disconnects them.

Usage: Basic Example

import { WebSocketGateway } from "@suaveplan/websocket-gateway";
import { WebSocketServer } from "ws";

const gateway = new WebSocketGateway({
    port: 8080,
    wsServerFactory: () => new MyWsAdapter(WebSocketServer),
});
const { address, port } = await gateway.start();
// … later
await gateway.stop();

Usage: Advanced Example

import { WebSocketGateway } from "@suaveplan/websocket-gateway";

const gateway = new WebSocketGateway({
    port: 0,
    host: "127.0.0.1",
    wsServerFactory: realWsFactory,
    presenceWebhook: {
        url: "https://hooks.example.com/presence",
        secret: process.env.PRESENCE_SECRET,
        events: ["presence:join", "presence:leave"],
    },
    authVerifier: async (handshake) => {
        const token = handshake.headers.authorization?.replace(/^Bearer /, "");
        const claims = await verifyJwt(token);
        return {
            clientId: claims.sub,
            principal: claims.sub,
            tenantId: claims.tid,
            scopes: claims.scope.split(" "),
            metadata: {},
        };
    },
    rateLimitGate: async (ctx, action) =>
        action === "room.broadcast" ? bucket.tryConsume(ctx.principal) : true,
});
const { address, port } = await gateway.start();
gateway.createRoom({ id: "general", maxClients: 100 });
await gateway.joinRoom("general", "alice");
await gateway.broadcastToRoom("general", "alice", { text: "hi" });

API Reference

  • new WebSocketGateway(config, clock?) — Zod-validates config; throws WsGatewayError(INVALID_OPTIONS) on failure.
  • start(options?): Promise<{address, port}> / stop(): Promise<void> — lifecycle.
  • connectClient(id, metadata?) / disconnectClient(id, reason?) / isClientConnected(id) / getClient(id) / getAuthContext(id) — client surface.
  • createRoom(config) / getRoom(id) / deleteRoom(id) / joinRoom(roomId, clientId, idempotencyKey?) / leaveRoom / broadcastToRoom(roomId, senderId, payload, idempotencyKey?) / unicast(senderId, targetId, payload) — room surface.
  • getOrCreateChannel(name, allowPatterns?) / subscribeToChannel(channel, clientId, topic) / unsubscribeFromChannel / publishToChannel(channel, senderId, topic, payload) — channel surface.
  • getPresenceTracker() / handleHeartbeat(clientId) — presence surface.
  • snapshot() — HealthSnapshot for inline health probes.
  • boundAddress / boundPort getters available after start().

Implementation Notes

The daemon's state lives in a single GatewayState struct so lifecycle helpers (in lifecycle.ts) can drive start, stop, and runAuthHandshake as free functions; the public class is then a thin facade. The same factoring lets the four op modules (client-lifecycle.ts, room-ops.ts, channel-ops.ts, access-gate.ts) stay free of class state — every test can drive them with hand-rolled fakes.

Capacity enforcement throws typed errors rather than returning sentinel booleans: a caller that runs out of room slots or hits a missing room receives ROOM_FULL / ROOM_NOT_FOUND and can branch on error.code. Auth failure during the handshake closes the socket with WS code 4001 and surfaces an AUTH_REJECTED error. Rate-limit and authorization denials surface RATE_LIMITED / AUTH_REJECTED (the latter shared with handshake denials — every authorisation rejection lands in the same bucket so audit dashboards stay clean).

Idempotency is wired on joinRoom and broadcastToRoom via IdempotencyGuard from @suaveplan/idempotency. Callers can pass an explicit idempotencyKey; without one, the daemon synthesizes a deterministic key including the clock so two same-second broadcastToRoom calls do NOT collapse unless the caller wants them to.