import { db } from '../db/index.js'; import type { GameTelemetry } from '../../telemetry.js'; import { MIN_RECORDED_ROUND } from '../../match-history.js'; import { recordLeaderboardTelemetry } from './leaderboard-ingest.js'; export function listTelemetry(): GameTelemetry[] { const rows = db().prepare(`SELECT t.payload FROM server_telemetry t JOIN servers s ON s.name = t.server WHERE s.destroyed_at IS NULL AND t.payload IS NOT NULL`).all() as Array<{ payload: string }>; return rows.map(row => JSON.parse(row.payload) as GameTelemetry); } export function saveTelemetry(report: GameTelemetry): GameTelemetry { // Keep callers outside the HTTP route atomic too. const handle = db(); return handle.transaction(() => { const row = handle.prepare('SELECT payload FROM server_telemetry WHERE server = ?').get(report.server) as { payload: string | null } | undefined; const previous: GameTelemetry | undefined = row?.payload ? JSON.parse(row.payload) : undefined; recordLeaderboardTelemetry(handle, report); const match = handle.prepare('SELECT id FROM leaderboard_matches WHERE server = ? ORDER BY id DESC LIMIT 1').get(report.server) as { id: number }; const sameMatch = previous?.timing?.matchId === match.id; // A gap beyond the normal heartbeat window cannot establish continuity. const continuous = previous && Date.parse(report.receivedAt) - Date.parse(previous.receivedAt) <= 120_000; report = { ...report, timing: { matchId: match.id, players: Object.fromEntries(report.players.map(player => { const prior = continuous ? previous?.timing?.players[player.guid] : undefined; return [player.guid, { serverJoinedAt: prior?.serverJoinedAt ?? report.receivedAt, matchJoinedAt: sameMatch && prior ? prior.matchJoinedAt : report.receivedAt, roundStarted: sameMatch && prior ? prior.roundStarted : report.event === 'game_start' ? 1 : report.round }]; })) } }; handle.prepare(`INSERT INTO server_telemetry (server, round, players, received_at, payload) VALUES (?, ?, ?, ?, ?) ON CONFLICT(server) DO UPDATE SET round = excluded.round, players = excluded.players, received_at = excluded.received_at, payload = excluded.payload`) .run(report.server, report.event === 'game_start' ? 0 : report.round, report.players.length, report.receivedAt, JSON.stringify(report)); if (report.event === 'game_start') { handle.prepare('UPDATE servers SET game_started_at = ?, game_ended_at = NULL WHERE name = ?').run(report.receivedAt, report.server); } else if (report.event === 'game_end') { handle.prepare('UPDATE servers SET game_ended_at = ? WHERE name = ?').run(report.receivedAt, report.server); // Legacy betas also take match completion directly from the script. if (report.round >= MIN_RECORDED_ROUND) handle.prepare(`UPDATE beta_servers SET match_seen_at = ? WHERE server = ? AND stage = 'awaiting-match' AND healthy_at <= ? AND beta_id IN (SELECT id FROM betas WHERE state = 'running')`) .run(report.receivedAt, report.server, report.receivedAt); } return report; })(); }