import { openSaveDB } from './db'; import type { SaveEnvelope } from './envelope'; export interface SnapshotEntry { id: string; schemaVersion: number; savedAt: string; envelope: SaveEnvelope; } /** * Last-N pre-migration snapshot retention. CORE-08 mandates exactly 3. * If this needs to grow (e.g., Season 4 prestige rollback), update the * constant; do NOT make it a parameter — the single value across the app * is part of the contract Phase 2's settings UI shows the user. */ const RETAIN = 3; /** * Write a pre-migration snapshot. After every write, prune to the * `RETAIN` newest entries by savedAt (descending). Works against both * IndexedDB and localStorage backends — `db.transaction(...).objectStore(...)` * is the common shape exposed by both. * * The snapshot ID includes a small entropy suffix because two snapshots * can fire in the same millisecond in tests (and theoretically in * production during a Phase-2 migration burst). */ export async function snapshot(envelope: SaveEnvelope): Promise { const db = await openSaveDB(); const tx = db.transaction('save_snapshots', 'readwrite'); const store = tx.objectStore('save_snapshots'); const savedAt = new Date().toISOString(); const id = `${envelope.schemaVersion}-${savedAt}-${Math.random() .toString(36) .slice(2, 8)}`; await store.put({ id, schemaVersion: envelope.schemaVersion, savedAt, envelope, }); // Prune oldest beyond RETAIN const all = await store.getAll(); const sorted = all.sort((a, b) => b.savedAt.localeCompare(a.savedAt)); const toDelete = sorted.slice(RETAIN); await Promise.all(toDelete.map((e) => store.delete(e.id))); await tx.done; } /** * List snapshots in newest-first order. Returns `[]` on an empty store. */ export async function listSnapshots(): Promise { const db = await openSaveDB(); const all = await db.getAll('save_snapshots'); return all.sort((a, b) => b.savedAt.localeCompare(a.savedAt)); }