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
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…
-
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…
-
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…
-
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…
-
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…
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.AbortSignalis 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 throughIdempotencyGuard),@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-loggingship . Defaults are no-op pass-throughs documented indefaults.md. - OTel surface: spans
ws_gateway.{start,stop,connect,message,broadcast}, countersrooms_active(UpDown),presence_updates_total{kind},actions_total{action,outcome}, histogramsmessage_dispatch_ms,start_duration_ms. - Bounded heartbeat sweep with injectable
Clock. Times-out clients pastclientTimeoutMsand 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; throwsWsGatewayError(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()—HealthSnapshotfor inline health probes.boundAddress/boundPortgetters available afterstart().
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.