/** * Durable snapshot store — the reconcile cache, persisted to disk. On boot the * app shows the last snapshot instantly (stale-while-revalidate) instead of a * blank board while ~2N gitea calls run; if gitea is unreachable, reads fall * back to it (offline). It's a rebuildable mirror — the durable truth stays in * gitea (the purity split, D4). A plain JSON file: the whole snapshot fits in * memory at this scale, so indexed SQL buys nothing yet (see the PR). */ import { readFileSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import { app } from 'electron' /** The shape we persist — kept loose so a schema drift degrades to "no cache", not a crash. */ export interface PersistedSnapshot { issues: unknown[] milestones: unknown[] deps: unknown[] timelines: Record /** ISO time the snapshot was reconciled — shown as "cached since". */ savedAt: string } function snapshotPath(): string { return join(app.getPath('userData'), 'commitea-snapshot.json') } /** * Load the last persisted snapshot, or null if absent/corrupt. Never throws — a * deleted or unreadable cache degrades to "no cache" (the purity guarantee, D4: * the next reconcile rebuilds it from gitea). `path` is injectable for tests; * production always uses the userData file. */ export function loadSnapshot(path: string = snapshotPath()): PersistedSnapshot | null { try { const parsed = JSON.parse(readFileSync(path, 'utf8')) as PersistedSnapshot if (parsed && Array.isArray(parsed.issues)) return parsed return null } catch { return null // missing file, bad JSON, or drift — treat as no cache } } /** Persist a freshly reconciled snapshot. Best-effort — a write failure never breaks a reconcile. */ export function saveSnapshot(snap: Omit, savedAt: string, path: string = snapshotPath()): void { try { writeFileSync(path, JSON.stringify({ ...snap, savedAt }), 'utf8') } catch { // disk full / permissions — the in-memory cache still works this session } }