Ready for a crowd: idle rooms sleep, share cards render once

Memory now carries only live tables — a sweep puts finished rooms to
bed after 30 minutes and anything untouched after a day, and getRoom
wakes a sleeping room from its ledger the moment anyone asks (the
seed plus the log IS the game). The room cap counts ledgers on disk,
not just rooms awake. Share-card PNGs render once per share id and
cache immutable. Rate-limit kicks use terminate() so an abuser cannot
hold the socket open by ignoring the closing handshake.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
Eric Wagoner
2026-08-30 13:43:23 -04:00
co-authored by Claude Fable 5
parent 9cc0430ee2
commit 002698085f
3 changed files with 128 additions and 32 deletions
+23
View File
@@ -97,6 +97,29 @@ export function appendLine(roomId: string, line: RoomLine): void {
appendFileSync(fileFor(roomId), JSON.stringify(line) + "\n", "utf8");
}
/** Read one persisted room's lines, or null if it has no ledger. */
export function readRoom(roomId: string): RoomLine[] | null {
const file = fileFor(roomId);
if (!existsSync(file)) return null;
try {
const lines = readFileSync(file, "utf8")
.split("\n")
.filter((l) => l.trim().length > 0)
.map((l) => JSON.parse(l) as RoomLine);
return lines.length > 0 && lines[0]!.kind === "meta" ? lines : null;
} catch (e) {
console.error(`unreadable room file ${roomId}:`, e);
return null;
}
}
/** How many rooms have ledgers on disk — the server-wide cap counts these,
* not just the rooms currently awake in memory. */
export function countRoomFiles(): number {
ensureDataDir();
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[]> {
ensureDataDir();