Files
orca/src/main/sqlite/row-cache/legacy-json-source.ts
T
Neil d101312bee refactor(sqlite): share one row cache across the SQLite-backed caches (#40028)
The last-status, GitHub cache, notification dismissal and model catalog stores each grew their
own legacy-file import, read-only startup read, changed-row writer and retry backoff. They now
share src/main/sqlite/row-cache/ (KeyedJsonRowCache with load/write, the legacy JSON source,
one failure classifier and backoff); each store keeps only its cadence and fencing policy.

Edge cases now behave the same in every store:
- a corrupt or rejected older-build file keeps the stored rows (it no longer wipes them);
- an older-build file that exists but cannot be read is never treated as absent;
- the import receipt is the file's content hash, without its mtime, so a copied leftover is
  still recognised as imported;
- while the rows or that file cannot be read, the store saves nothing that session and never
  deletes a file it did not load (dismissals keep retrying, as before);
- files are user-only and hardened everywhere.

The last-status and GitHub caches move to the shared row layout, so each rebuilds empty once.
2026-11-08 16:37:17 -08:00

46 lines
1.7 KiB
TypeScript

import { createHash } from 'node:crypto'
import { readFileSync, rmSync } from 'node:fs'
import { isDefinitiveAbsence } from '../../../shared/definitive-filesystem-absence'
/** A JSON file an older build may have written, before its rows moved into SQLite. */
export type LegacyJsonSource =
| { kind: 'absent' }
/** Present but not readable now (permissions, a Windows lock): neither absent nor imported. */
| { kind: 'unreadable'; error: unknown }
| { kind: 'read'; text: string; receipt: string }
/**
* One-shot startup read. `receipt` is the hash of these exact bytes (not their mtime, so a
* copied or restored leftover is still recognised as already imported).
*/
export function readLegacyJsonSource(path: string): LegacyJsonSource {
let bytes: Buffer
try {
bytes = readFileSync(path)
} catch (error) {
return isDefinitiveAbsence(error) ? { kind: 'absent' } : { kind: 'unreadable', error }
}
return {
kind: 'read',
text: bytes.toString('utf8'),
receipt: createHash('sha256').update(bytes).digest('hex')
}
}
/**
* Removes the file only while it still holds the bytes `receipt` names, never a rewrite by
* another build since. One-shot: after the import commits, or when its bytes were rejected.
*/
export function removeLegacyJsonSource(path: string, receipt: string): void {
const current = readLegacyJsonSource(path)
if (current.kind !== 'read' || current.receipt !== receipt) {
return
}
try {
rmSync(path, { force: true })
} catch (error) {
// A committed receipt marks it imported, so a leftover file cannot shadow newer rows.
console.warn('[sqlite] Could not remove a migrated legacy JSON file:', error)
}
}