// Durable rooms: one append-only JSONL file per room. The first line is the // room's birth certificate; every later line is a join, a start, or a game // command. Because the engine is deterministic, replaying a file rebuilds // the exact game state — server restarts lose nothing. import { appendFileSync, mkdirSync, readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; export interface RoomMetaLine { kind: "meta"; id: string; hostId: string; /** sha-256 of the host's seat token. Raw tokens never touch disk. */ hostTokenHash?: string; /** Legacy plaintext token (pre-hashing files only). */ hostToken?: string; seed: number; createdAt: string; } export interface JoinLine { kind: "join"; name: string; tokenHash?: string; /** Legacy plaintext token (pre-hashing files only). */ token?: string; } export interface StartLine { kind: "start"; expansion: boolean; /** Final wizard colors, in player join order. */ colors?: number[]; } export interface CommandLine { kind: "command"; seq: number; playerId: string; command: unknown; at: string; } export type RoomLine = RoomMetaLine | JoinLine | StartLine | CommandLine; const DATA_DIR = process.env.WIZWAR_DATA_DIR ?? join(process.cwd(), "data", "rooms"); function fileFor(roomId: string): string { return join(DATA_DIR, `${roomId}.jsonl`); } /** Directory holding stats.json — the parent of the rooms dir. */ export function statsDir(): string { return join(DATA_DIR, ".."); } export function ensureDataDir(): void { mkdirSync(DATA_DIR, { recursive: true }); } export function appendLine(roomId: string, line: RoomLine): void { appendFileSync(fileFor(roomId), JSON.stringify(line) + "\n", "utf8"); } /** Read every persisted room's lines, keyed by room id. */ export function readAllRooms(): Map { ensureDataDir(); const rooms = new Map(); 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 rooms; }