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
88 lines
3 KiB
TypeScript
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;
|
|
};
|