3.1 KiB
PresenceTracker
(no submodules)
Purpose
Per-gateway-instance presence map tracking which clients are online, away, busy, or offline. The gateway uses one tracker shared across rooms — presence is a property of the connection, not the room — and exposes it to listeners via the typed EventBusLike events. The tracker takes an injectable Clock so unit + property tests can drive deterministic heartbeat timeouts without vi.useFakeTimers().
Features
track/untrack/getPresencefor the lifecycle hooks.heartbeat(clientId)— the gateway's per-frame keepalive update; auto-promotesofflineback toonline.updateStatus(clientId, status)for explicit caller-driven transitions (away / busy).updateState(clientId, state)— merges arbitrary state into the per-client custom payload.detectTimedOut()— sweep used by the gateway's heartbeat interval to mark clients offline once they exceedclientTimeoutMs.listActive()/listAll()for read-side fan-out to listeners.
Usage: Basic Example
import { PresenceTracker } from "@suaveplan/websocket-gateway";
const tracker = new PresenceTracker(90_000);
tracker.track("alice");
tracker.heartbeat("alice");
Usage: Advanced Example
import { PresenceTracker } from "@suaveplan/websocket-gateway";
let now = 1_000;
const tracker = new PresenceTracker(5_000, () => now);
tracker.track("alice");
now = 10_000;
expect(tracker.detectTimedOut()).toEqual(["alice"]);
tracker.heartbeat("alice"); // back to online
tracker.updateState("alice", { typing: true });
tracker.updateStatus("alice", "busy");
API Reference
new PresenceTracker(timeoutMs: number, clock?: Clock)—clockdefaults toDate.now. ImplementingClocklets you fake time.track(clientId, initialState?)— registers a new client withstatus: "online".untrack(clientId): boolean— removes a client; returns true if present.heartbeat(clientId): PresenceInfo | null— bumpslastSeen, auto-promotes offline → online.updateStatus(clientId, status): PresenceInfo | null— caller-driven status set.updateState(clientId, state): PresenceInfo | null— merges custom state.getPresence(clientId): PresenceInfo | null— returns a defensive copy.listActive()/listAll()— defensive copies of non-offline / all entries.detectTimedOut(): string[]— mark every client pasttimeoutMsas offline and return their ids.isTracked(clientId)/clear().
Implementation Notes
The tracker holds presence state in-process; clustering across multiple gateway instances is out of scope and is the future job of a websocket-gateway-store-* adapter. detectTimedOut is O(n) over the tracked set — n is bounded by the concurrent-client cap and the sweep runs at the configurable heartbeatIntervalMs, so the cost stays predictable. Mutation methods return defensive copies of PresenceInfo so the caller cannot accidentally mutate internal state by holding a reference; the internal records mutate in-place for performance.