first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -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.