# `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` / `getPresence` for the lifecycle hooks. - `heartbeat(clientId)` — the gateway's per-frame keepalive update; auto-promotes `offline` back to `online`. - `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 exceed `clientTimeoutMs`. - `listActive()` / `listAll()` for read-side fan-out to listeners. ## Usage: Basic Example ```ts import { PresenceTracker } from "@suaveplan/websocket-gateway"; const tracker = new PresenceTracker(90_000); tracker.track("alice"); tracker.heartbeat("alice"); ``` ## Usage: Advanced Example ```ts 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)` — `clock` defaults to `Date.now`. Implementing `Clock` lets you fake time. - `track(clientId, initialState?)` — registers a new client with `status: "online"`. - `untrack(clientId): boolean` — removes a client; returns true if present. - `heartbeat(clientId): PresenceInfo | null` — bumps `lastSeen`, 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 past `timeoutMs` as 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.