houserules/packages/server/src/logging.ts
Joey Yakimowich-Payne 2d0b8399f8
feat(server): add reconnection with 60s grace + snapshot resume (P4.7)
On disconnect, a player's slot is held for 60s via ReconnectManager.
During the grace window, game.delta frames destined for the absent
slot are buffered in order. If the client reconnects with its
original token (envelope-level), the grace timer is cancelled and
the server replays a fresh game.state snapshot plus every buffered
delta. Timer expiry triggers the original "player_left" game.end
cleanup that previously ran immediately on disconnect.

- new packages/server/src/reconnect.ts: ReconnectManager (no WS refs,
  no registry coupling, unref'd timers so tests don't block exit)
- broadcast.ts: unregisterConnection starts grace; handleRoomJoin
  routes reconnect path via envelope token + isPending check;
  handleGameMove buffers deltas for disconnected opponents
- reconnect.test.ts: 9 unit cases (grace/cancel/buffer/expire/reset)
- broadcast.test.ts: end-to-end reconnect scenario + negative case
2026-04-16 17:38:26 -06:00

88 lines
3 KiB
TypeScript

// Prometheus-format metrics for the chess server (P4.8).
//
// Intentionally hand-rolled — no prom-client dependency. All state is
// module-level and resets on process restart, which is intentional for v1.
// ---------------------------------------------------------------------------
// Counters & gauges
// ---------------------------------------------------------------------------
const counters = {
messages_received_total: 0,
moves_validated_total_ok: 0,
moves_validated_total_fail: 0,
};
let activeRooms = 0;
// ---------------------------------------------------------------------------
// Summary: tick (move-processing) duration in milliseconds
// ---------------------------------------------------------------------------
const tickDurations: number[] = [];
// ---------------------------------------------------------------------------
// Mutators — called from broadcast.ts
// ---------------------------------------------------------------------------
export function incMessages(): void {
counters.messages_received_total++;
}
export function incMovesOk(): void {
counters.moves_validated_total_ok++;
}
export function incMovesFail(): void {
counters.moves_validated_total_fail++;
}
export function setActiveRooms(n: number): void {
activeRooms = n;
}
export function recordTickDuration(ms: number): void {
tickDurations.push(ms);
}
// ---------------------------------------------------------------------------
// Renderer — Prometheus text exposition format 0.0.4
// ---------------------------------------------------------------------------
export function renderMetrics(): string {
const sum = tickDurations.reduce((acc, v) => acc + v, 0);
const count = tickDurations.length;
return [
"# HELP rooms_active Number of active game rooms",
"# TYPE rooms_active gauge",
`rooms_active ${activeRooms}`,
"",
"# HELP messages_received_total Total messages received",
"# TYPE messages_received_total counter",
`messages_received_total ${counters.messages_received_total}`,
"",
"# HELP moves_validated_total Total moves validated",
"# TYPE moves_validated_total counter",
`moves_validated_total{result="ok"} ${counters.moves_validated_total_ok}`,
`moves_validated_total{result="fail"} ${counters.moves_validated_total_fail}`,
"",
"# HELP tick_duration_milliseconds Tick (move-processing) duration in milliseconds",
"# TYPE tick_duration_milliseconds summary",
`tick_duration_milliseconds_sum ${sum}`,
`tick_duration_milliseconds_count ${count}`,
"",
].join("\n");
}
// ---------------------------------------------------------------------------
// Test-only reset — never call from production code
// ---------------------------------------------------------------------------
export const __resetForTests = (): void => {
counters.messages_received_total = 0;
counters.moves_validated_total_ok = 0;
counters.moves_validated_total_fail = 0;
activeRooms = 0;
tickDurations.length = 0;
};