first commit
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
# `PresenceTracker`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## 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.
|
||||
Reference in New Issue
Block a user