mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 02:17:31 +00:00
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.
46 lines
1.7 KiB
TypeScript
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)
|
|
}
|
|
}
|