Files
OpenFrontIO/src/client/LocalPersistantStats.ts
T
Evan Pelle 6e23c26723 Fix bugs from code audit (server respawn, leaks, hardening)
Verified findings from a code audit and fixed the legitimate, well-scoped
ones. Notable: crashed cluster workers were never respawned because
worker.process.env is undefined in the exit handler, so WORKER_ID was always
undefined (confirmed empirically).

Fixes:
- Master: respawn crashed workers via a worker.id -> WORKER_ID registry
  instead of the always-undefined worker.process.env.WORKER_ID.
- GameServer: report Finished when _hasEnded so abandoned private lobbies are
  cleaned up instead of lingering until max duration; remove sockets from the
  websockets Set on close/rejoin; reset hasReachedMaxPlayerCount when a player
  leaves before start (avoids premature under-capacity starts, e.g. ranked 1v1).
- Worker: reap WebSocket connections that upgrade but never authenticate
  (30s join timeout) to prevent Slowloris-style FD exhaustion.
- ServerEnv: add a 1h TTL to the cached JWKS public key so IdP key rotation
  doesn't break all logins until a restart.
- SpawnExecution: ignore a client-supplied tile in random-spawn mode so a
  forged spawn intent can't pick its own coordinates (+ test).
- GameRenderer / MultiTabDetector: remove window listeners on teardown
  (bind once so add/removeEventListener references match).
- LocalPersistantStats: guard localStorage access with try/catch so sandboxed
  iframes / disabled-storage contexts don't crash game start.
- StructurePass: reuse per-frame uniform arrays instead of allocating each draw.

Also documents a known, intentionally-unfixed auth gap on
/api/archive_singleplayer_game (pending a product decision on logged-out users).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 17:49:59 +00:00

61 lines
1.5 KiB
TypeScript

import { GameConfig, GameID, PartialGameRecord } from "../core/Schemas";
import { replacer } from "../core/Util";
export interface LocalStatsData {
[key: GameID]: {
lobby: Partial<GameConfig>;
// Only once the game is over
gameRecord?: PartialGameRecord;
};
}
let _startTime: number;
function getStats(): LocalStatsData {
try {
const statsStr = localStorage.getItem("game-records");
return statsStr ? JSON.parse(statsStr) : {};
} catch {
// Accessing localStorage throws in sandboxed iframes (e.g. gaming portals)
// or when storage is disabled; treat as empty rather than crashing.
return {};
}
}
function save(stats: LocalStatsData) {
// To execute asynchronously
setTimeout(() => {
try {
localStorage.setItem("game-records", JSON.stringify(stats, replacer));
} catch {
// Storage unavailable (sandboxed iframe / disabled) — skip persistence.
}
}, 0);
}
// The user can quit the game anytime so better save the lobby as soon as the
// game starts.
export function startGame(id: GameID, lobby: Partial<GameConfig>) {
_startTime = Date.now();
const stats = getStats();
stats[id] = { lobby };
save(stats);
}
export function startTime() {
return _startTime;
}
export function endGame(gameRecord: PartialGameRecord) {
const stats = getStats();
const gameStat = stats[gameRecord.info.gameID];
if (!gameStat) {
console.log("LocalPersistantStats: game not found");
return;
}
gameStat.gameRecord = gameRecord;
save(stats);
}