2761bcc1e0
- codec.ts: exportToBase64 / importFromBase64 via lz-string with
MAX_IMPORT_BYTES=50MB DoS cap (T-01-02 in plan threat model); import
validates against SaveEnvelopeSchema before returning. lz-string sync
caveat documented per RESEARCH Pitfall 5 (Web Worker mitigation deferred
to Phase 8 per CONTEXT D-09)
- index.ts: 14 public re-exports — the only entry point Phase 2 should
import from. Includes the LocalStorageDBAdapter class so consumers can
type-check the fallback path explicitly if needed
[Rule 1 - Bug] Build was failing because the original SaveDB type was a
union (IDBPDatabase<SaveDBSchema> | LocalStorageDBAdapter) — TypeScript
cannot resolve method calls through a union when each branch has
differently-shaped overloads ('no compatible signature' on every db.put).
Fixed by:
- Defining SaveDB as a single common-contract interface that both
backends MUST satisfy (get/put/delete/getAll/transaction with
conditional-type RecordOf<S> return values)
- Hoisting the canonical SavedRecord/SnapshotRecord/StoreName types
into db-localstorage-adapter.ts (lower-level module) and re-exporting
them from db.ts to avoid a circular import
- Casting the idb-returned IDBPDatabase to SaveDB at the open-call
boundary (the casts are isolated to openSaveDB; Phase 2 only sees
the SaveDB interface)
- Promoting SnapshotEntry to a type-alias of SnapshotRecord so
snapshots.ts no longer redeclares the shape and can rely on
canonical types
Tests: 36/36 pass under 'npx vitest run src/save/' (full suite incl
sentinel: 37/37). 'npm run build' exits 0 under TypeScript strict.
'npm run lint' is not invoked here because Plan 02 (eslint-firewall) has
not landed yet — the lint script will fail until it does, by design per
the Plan 01-01 SUMMARY ('Plan 02 owns it').
60 lines
2.1 KiB
TypeScript
60 lines
2.1 KiB
TypeScript
import { openSaveDB } from './db';
|
|
import type { SnapshotRecord } from './db';
|
|
import type { SaveEnvelope } from './envelope';
|
|
|
|
/**
|
|
* Public type for what listSnapshots returns. Structurally identical to
|
|
* SnapshotRecord but exposed under a friendlier name for Phase 2's UI.
|
|
*/
|
|
export type SnapshotEntry = SnapshotRecord;
|
|
|
|
/**
|
|
* 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<void> {
|
|
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<SnapshotEntry[]> {
|
|
const db = await openSaveDB();
|
|
const all = await db.getAll('save_snapshots');
|
|
return all.sort((a, b) => b.savedAt.localeCompare(a.savedAt));
|
|
}
|