Sleeping rooms stay asleep across a restart
Idle rooms already went back to their ledgers, but a boot replayed every ledger before the first sweep could put them to bed again — most of a restart's fifteen seconds spent rebuilding games nobody was at. A room put to sleep now leaves a stub beside the ledgers, with the ledger's size at that moment; at boot a stub whose size still matches answers for the room and the ledger stays unreplayed. Replayed rooms take their eviction clock from the ledger's last line, so the first sweep after a boot writes the stubs for everything long idle. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
c7edfa74ad
commit
6293c36add
@@ -3,7 +3,7 @@
|
||||
// command. Because the engine is deterministic, replaying a file rebuilds
|
||||
// the exact game state — server restarts lose nothing.
|
||||
|
||||
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync } from "node:fs";
|
||||
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
export interface RoomMetaLine {
|
||||
@@ -128,24 +128,48 @@ export function countRoomFiles(): number {
|
||||
return readdirSync(DATA_DIR).filter((f) => f.endsWith(".jsonl")).length;
|
||||
}
|
||||
|
||||
/** Read every persisted room's lines, keyed by room id. */
|
||||
export function readAllRooms(): Map<string, RoomLine[]> {
|
||||
/** Every room with a ledger on disk. */
|
||||
export function listRoomIds(): string[] {
|
||||
ensureDataDir();
|
||||
const rooms = new Map<string, RoomLine[]>();
|
||||
for (const file of readdirSync(DATA_DIR)) {
|
||||
if (!file.endsWith(".jsonl")) continue;
|
||||
const id = file.slice(0, -".jsonl".length);
|
||||
try {
|
||||
const lines = readFileSync(join(DATA_DIR, file), "utf8")
|
||||
.split("\n")
|
||||
.filter((l) => l.trim().length > 0)
|
||||
.map((l) => JSON.parse(l) as RoomLine);
|
||||
if (lines.length > 0 && lines[0]!.kind === "meta") rooms.set(id, lines);
|
||||
} catch (e) {
|
||||
console.error(`skipping unreadable room file ${file}:`, e);
|
||||
}
|
||||
return readdirSync(DATA_DIR).filter((f) => f.endsWith(".jsonl")).map((f) => f.slice(0, -".jsonl".length));
|
||||
}
|
||||
|
||||
/** A ledger's size and the moment of its last line. */
|
||||
export function ledgerStat(roomId: string): { bytes: number; mtimeMs: number } | null {
|
||||
if (!safeRoomId(roomId)) return null;
|
||||
const file = fileFor(roomId);
|
||||
if (!existsSync(file)) return null;
|
||||
const st = statSync(file);
|
||||
return { bytes: st.size, mtimeMs: st.mtimeMs };
|
||||
}
|
||||
|
||||
// --- Stubs of sleeping rooms. ---------------------------------------------
|
||||
// A room put to sleep leaves a stub beside the ledgers: what the games
|
||||
// ledger needs to answer for it, and the ledger's size at that moment. A
|
||||
// ledger only ever grows, so at boot a stub whose size still matches is
|
||||
// the room's whole truth and the room stays asleep, unreplayed.
|
||||
|
||||
const stubDir = () => join(DATA_DIR, "..", "stubs");
|
||||
const stubFor = (roomId: string) => join(stubDir(), `${roomId}.json`);
|
||||
|
||||
export function readStubFile<T>(roomId: string): { bytes: number; stub: T } | null {
|
||||
if (!safeRoomId(roomId)) return null;
|
||||
const file = stubFor(roomId);
|
||||
if (!existsSync(file)) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(readFileSync(file, "utf8")) as { bytes: number; stub: T };
|
||||
return typeof parsed.bytes === "number" && parsed.stub ? parsed : null;
|
||||
} catch (e) {
|
||||
console.error(`unreadable stub file ${roomId}:`, e);
|
||||
return null;
|
||||
}
|
||||
return rooms;
|
||||
}
|
||||
|
||||
export function writeStubFile(roomId: string, bytes: number, stub: unknown): void {
|
||||
const file = stubFor(roomId);
|
||||
mkdirSync(stubDir(), { recursive: true });
|
||||
writeFileSync(file + ".tmp", JSON.stringify({ bytes, stub }), "utf8");
|
||||
renameSync(file + ".tmp", file);
|
||||
}
|
||||
|
||||
/** Player surprise reports and the wizards' replies share one JSONL file
|
||||
|
||||
Reference in New Issue
Block a user