Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
@@ -1,32 +1,49 @@
|
||||
import { db } from '../db/index.js';
|
||||
import { parseGameEvent, type GameTelemetry } from '../../telemetry.js';
|
||||
import { ONLINE_MAX_MS } from '../../server-freshness.js';
|
||||
import { rconGetPlayerCount, rconStatusResult, type RconStatusResult } from '../integrations/rcon.js';
|
||||
import {
|
||||
rconGetPlayerCount,
|
||||
rconStatusResult,
|
||||
type RconStatusResult
|
||||
} from '../integrations/rcon.js';
|
||||
import { latestGameSubmission } from './lifecycle-evidence.js';
|
||||
|
||||
export { latestGameSubmission } from './lifecycle-evidence.js';
|
||||
|
||||
export interface TelemetryEvidence { players: number; map: string; receivedAt: string }
|
||||
export interface TelemetryEvidence {
|
||||
players: number;
|
||||
map: string;
|
||||
receivedAt: string;
|
||||
}
|
||||
|
||||
export function telemetryReport(server: string): GameTelemetry | null {
|
||||
const row = db().prepare('SELECT received_at, payload FROM server_telemetry WHERE server = ?').get(server) as
|
||||
{ received_at: string; payload: string | null } | undefined;
|
||||
const row = db()
|
||||
.prepare('SELECT received_at, payload FROM server_telemetry WHERE server = ?')
|
||||
.get(server) as { received_at: string; payload: string | null } | undefined;
|
||||
if (!row?.payload) return null;
|
||||
try {
|
||||
const saved = JSON.parse(row.payload) as GameTelemetry;
|
||||
const event = parseGameEvent(saved);
|
||||
return event.server === server ? { ...event, receivedAt: row.received_at, timing: saved.timing } : null;
|
||||
} catch { return null; }
|
||||
return event.server === server
|
||||
? { ...event, receivedAt: row.received_at, timing: saved.timing }
|
||||
: null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Only validated, recently received reports can establish current game state. */
|
||||
export function telemetryEvidence(server: string, after: string | null = latestGameSubmission(server)): TelemetryEvidence | null {
|
||||
export function telemetryEvidence(
|
||||
server: string,
|
||||
after: string | null = latestGameSubmission(server)
|
||||
): TelemetryEvidence | null {
|
||||
const report = telemetryReport(server);
|
||||
if (!report) return null;
|
||||
const received = Date.parse(report.receivedAt);
|
||||
const age = Date.now() - received;
|
||||
if (!Number.isFinite(age) || age < 0 || age > ONLINE_MAX_MS) return null;
|
||||
if (after && (!Number.isFinite(Date.parse(after)) || received <= Date.parse(after))) return null;
|
||||
if (after && (!Number.isFinite(Date.parse(after)) || received <= Date.parse(after)))
|
||||
return null;
|
||||
return { players: report.players.length, map: report.map, receivedAt: report.receivedAt };
|
||||
}
|
||||
|
||||
@@ -45,39 +62,68 @@ export interface PlayerEvidence {
|
||||
/** The same eligibility decision, with provenance from the probes already performed. */
|
||||
export async function probePlayerEvidence(server: string): Promise<PlayerEvidence> {
|
||||
const fromTelemetry = (value: TelemetryEvidence): PlayerEvidence => ({
|
||||
players: value.players, source: 'telemetry', observedAt: new Date().toISOString(), receivedAt: value.receivedAt
|
||||
players: value.players,
|
||||
source: 'telemetry',
|
||||
observedAt: new Date().toISOString(),
|
||||
receivedAt: value.receivedAt
|
||||
});
|
||||
const occupied = telemetryEvidence(server);
|
||||
if (occupied && occupied.players > 0) return fromTelemetry(occupied);
|
||||
const players = await rconGetPlayerCount(server).catch(() => null);
|
||||
// Read after the network request: a player may join while RCON is pending.
|
||||
const telemetry = telemetryEvidence(server);
|
||||
if (players !== null && players > 0) return { players, source: 'rcon', observedAt: new Date().toISOString() };
|
||||
if (players !== null && players > 0)
|
||||
return { players, source: 'rcon', observedAt: new Date().toISOString() };
|
||||
if (telemetry && telemetry.players > 0) return fromTelemetry(telemetry);
|
||||
if (players === 0) return { players, source: 'rcon', observedAt: new Date().toISOString() };
|
||||
if (telemetry?.players === 0) return fromTelemetry(telemetry);
|
||||
return { players: null, source: null, observedAt: new Date().toISOString() };
|
||||
}
|
||||
|
||||
export type VerifiedGameStatus = { ok: true; players: number; map: string | null; observedAt: string } & (
|
||||
{ source: 'rcon'; rcon: Extract<RconStatusResult, { ok: true }> }
|
||||
| { source: 'telemetry'; receivedAt: string });
|
||||
export type VerifiedGameStatus = {
|
||||
ok: true;
|
||||
players: number;
|
||||
map: string | null;
|
||||
observedAt: string;
|
||||
} & (
|
||||
| { source: 'rcon'; rcon: Extract<RconStatusResult, { ok: true }> }
|
||||
| { source: 'telemetry'; receivedAt: string }
|
||||
);
|
||||
|
||||
/** Host agent readiness remains a caller prerequisite. Old-process telemetry is excluded. */
|
||||
export async function probeServerStatus(server: string, after?: string | null): Promise<
|
||||
VerifiedGameStatus | { ok: false; reason: string; observedAt: string }
|
||||
> {
|
||||
export async function probeServerStatus(
|
||||
server: string,
|
||||
after?: string | null
|
||||
): Promise<VerifiedGameStatus | { ok: false; reason: string; observedAt: string }> {
|
||||
const current = telemetryEvidence(server, after);
|
||||
if (current) return { ok: true, ...current, source: 'telemetry', observedAt: new Date().toISOString() };
|
||||
let reason = 'RCON unavailable';
|
||||
if (current)
|
||||
return { ok: true, ...current, source: 'telemetry', observedAt: new Date().toISOString() };
|
||||
let reason: string;
|
||||
try {
|
||||
const rcon = await rconStatusResult(server);
|
||||
const latest = telemetryEvidence(server, after);
|
||||
if (latest) return { ok: true, ...latest, source: 'telemetry', observedAt: new Date().toISOString() };
|
||||
if (rcon.ok) return { ok: true, players: rcon.status.players, map: rcon.status.map, source: 'rcon', rcon, observedAt: new Date().toISOString() };
|
||||
if (latest)
|
||||
return {
|
||||
ok: true,
|
||||
...latest,
|
||||
source: 'telemetry',
|
||||
observedAt: new Date().toISOString()
|
||||
};
|
||||
if (rcon.ok)
|
||||
return {
|
||||
ok: true,
|
||||
players: rcon.status.players,
|
||||
map: rcon.status.map,
|
||||
source: 'rcon',
|
||||
rcon,
|
||||
observedAt: new Date().toISOString()
|
||||
};
|
||||
reason = rcon.reason;
|
||||
} catch (err) { reason = (err as Error).message; }
|
||||
} catch (err) {
|
||||
reason = (err as Error).message;
|
||||
}
|
||||
const telemetry = telemetryEvidence(server, after);
|
||||
return telemetry ? { ok: true, ...telemetry, source: 'telemetry', observedAt: new Date().toISOString() }
|
||||
return telemetry
|
||||
? { ok: true, ...telemetry, source: 'telemetry', observedAt: new Date().toISOString() }
|
||||
: { ok: false, reason, observedAt: new Date().toISOString() };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user