// SQLite persistence for games (one row per game id; the full GameState as JSON). import { mkdirSync } from 'node:fs'; import path from 'node:path'; import Database from 'better-sqlite3'; import type { GameState } from '../shared/types'; import { migrateTerritoryToRegions } from './regions'; export interface Db { save(state: GameState): void; load(id: string): GameState | null; listIds(): string[]; delete(id: string): boolean; close(): void; } export function openDb(filePath: string): Db { const dir = path.dirname(filePath); if (dir) mkdirSync(dir, { recursive: true }); const db = new Database(filePath); db.pragma('journal_mode = WAL'); db.exec( `CREATE TABLE IF NOT EXISTS games ( id TEXT PRIMARY KEY, data TEXT NOT NULL, updated_at TEXT NOT NULL )`, ); const upsert = db.prepare( `INSERT INTO games (id, data, updated_at) VALUES (?, ?, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at`, ); const byId = db.prepare('SELECT data FROM games WHERE id = ?'); const all = db.prepare('SELECT id FROM games ORDER BY updated_at DESC'); const del = db.prepare('DELETE FROM games WHERE id = ?'); return { save(state) { upsert.run(state.id, JSON.stringify(state), new Date().toISOString()); }, load(id) { const row = byId.get(id) as { data: string } | undefined; if (!row) return null; try { // v0.19: migrate any pre-v0.19 (whole-country ISO_A3) territory to // sub-region ids on read. A no-op when the region geo is not loaded, // and idempotent once a state has been migrated (the next save then // persists the region-level territory). return migrateTerritoryToRegions(JSON.parse(row.data) as GameState); } catch { return null; } }, listIds() { return (all.all() as { id: string }[]).map((r) => r.id); }, delete(id) { return del.run(id).changes > 0; }, close() { db.close(); }, }; }