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,132 @@
# PlayerManager
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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`).