# PlayerManager _(no submodules)_ ## Purpose `PlayerManager` is the `IPlayerManager` implementation backing the player lifecycle in `@suaveplan/game-server`. It tracks every player's connection state (`connected`, `disconnected`, `reconnecting`, `timeout`), the session each player is bound to, the heartbeat-driven liveness signal, and the reconnection-grace window that distinguishes a transient disconnect from a permanent departure. Every wall-clock observation routes through the injected `Clock`, and the optional heartbeat-monitor loop schedules itself via the injected `TimersLike` — there are no `Date.now()` or `setInterval` calls. The manager emits a typed `PlayerManagerEventMap` onto the injected EventBus so other subsystems (matchmaking, presence, audit) can react to join/leave/timeout signals without polling. Every state mutation is also mirrored to OTel via `recordPlayerLifecycle` so dashboards see the full counter set. ## Features - Implements Wave A's canonical `IPlayerManager` contract. - Injectable `Clock` and `TimersLike` — fully deterministic under fakes. - Zod-validated options boundary (`timeoutThresholdMs`, `reconnectionGraceMs`) with stable `INVALID_PLAYER_MANAGER_OPTIONS` failure code. - `INVALID_HEARTBEAT_INTERVAL` for non-positive integer interval inputs. - Reconnection path: a `connect` against an existing disconnected / timeout entry transitions to `reconnecting`/`connected` rather than building a fresh record (preserves `connectedAt`). - Grace-window cleanup: in `checkTimeouts`, disconnected entries older than `reconnectionGraceMs` are evicted so a future `connect` builds a fresh record. - Defensive copies — public API returns frozen plain-object snapshots rather than mutable internals. ## Usage: Basic ```typescript import { PlayerManager } from "../player-manager/index.js"; const mgr = new PlayerManager({ clock: () => Date.now(), timers, logger, events, }); mgr.connect("alice", "table-1", { tier: "gold" }); mgr.heartbeat("alice"); console.log(mgr.getConnection("alice")?.status); // "connected" ``` ## Usage: Advanced ```typescript // Custom timeout + grace window, with the heartbeat monitor running // via the injected timers source. const mgr = new PlayerManager(deps, { timeoutThresholdMs: 60_000, reconnectionGraceMs: 5_000, }); mgr.startHeartbeatMonitor(1_000); // sweep every second mgr.connect("alice", "table-1"); // ...network drop... mgr.disconnect("alice", "network_lost"); // 5s pass without reconnect — checkTimeouts evicts the entry. // A later connect("alice", "table-1") builds a fresh record. mgr.stopHeartbeatMonitor(); ``` ## API Reference ### Constructor `new PlayerManager(deps, options?)` where `deps` is `{ clock, timers, logger, events }`. Options: - `timeoutThresholdMs` (default 30000) — ms without heartbeat before a connected player is marked `timeout`. - `reconnectionGraceMs` (default 10000) — ms after a disconnect during which a `connect()` is treated as a reconnection. ### IPlayerManager surface (11 methods) `connect`, `disconnect`, `getConnection`, `getSessionPlayers`, `getAllConnections`, `heartbeat`, `checkTimeouts`, `updateMetadata`, `getPlayer`, `startHeartbeatMonitor`, `stopHeartbeatMonitor`. ### Extras - `getStats()` — connection-status histogram + average duration. - `dispose()` — stop monitor + drop every connection. ### Errors - `INVALID_PLAYER_MANAGER_OPTIONS` — Zod failure on constructor. - `INVALID_HEARTBEAT_INTERVAL` — non-positive integer to `startHeartbeatMonitor`. ### Events `player:connected`, `player:disconnected`, `player:reconnected`, `player:timeout`, `player:heartbeat`, `player:joined`, `player:left`. ## Implementation Notes The heartbeat monitor is implemented via `TimersLike.setTimer` chained in a self-rescheduling tick rather than via `setInterval`. That makes the behaviour deterministic under `FakeTimers` — the test suite explicitly fires due timers rather than depending on real wall-clock progression. The chain self-cancels by checking `heartbeatHandle !== undefined` before scheduling the next tick — so a `stopHeartbeatMonitor` between ticks prevents the next tick from being scheduled. Connection records are mutable internally for hot-path efficiency (`heartbeat` is called every frame in worst case) but every public accessor returns a `freeze(...)`-built defensive copy. The metadata bag in particular is shallow-copied per accessor call — callers can mutate the returned object without affecting the internal state. The grace-window logic in `checkTimeouts` is what distinguishes a "reconnection" from a "fresh connection". A disconnected entry within the grace window is still resolvable via `getConnection`, so a later `connect()` call routes through the reconnection path; once the grace expires, the entry is evicted entirely and a future `connect()` builds a fresh record (with a new `connectedAt`).