Fix C1/C2 + reliability cluster (L140/L141/L148, R6/R7)
Critical: - C1: single source of truth for IPC mock + Settings defaults. DEFAULT_SETTINGS in @shared/ipc; main DEFAULTS, renderer FALLBACK, and preview MOCK_SETTINGS all derive from it. Whole browser mock collapsed into one typed mockApi.ts; main.tsx shrinks to window.api = mockApi. PREVIEW check single-sourced in isPreview.ts. - C2: break the downloads<->sources circular import via a typed event bus (store/coordinator.ts). App eagerly imports sources for side-effects so startup load + scheduled --sync + downloadCompleted subscription still run. Reliability: - L140/L148: cancel/pause release the active slot synchronously; identity-guarded releaseActive; per-spawn cookie jar so a same-id retry/resume is never rejected by a stale entry nor run over the concurrency cap. - L141: renderer history capped at 500 + url de-dup to match main. - R6: writeJsonAtomic reports/logs write failures and keeps data dirty for retry instead of an empty catch. - R7: encryptSecret warns before the plaintext fallback. typecheck + 243 tests + eslint green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+23
-8
@@ -1,6 +1,6 @@
|
||||
import { app } from 'electron'
|
||||
import { join } from 'path'
|
||||
import { readFileSync, writeFileSync, renameSync, existsSync, copyFileSync } from 'fs'
|
||||
import { readFileSync, writeFileSync, renameSync, existsSync, copyFileSync, unlinkSync } from 'fs'
|
||||
|
||||
/**
|
||||
* One shared persistence layer for the hand-rolled JSON array stores (history,
|
||||
@@ -55,16 +55,29 @@ export function readJsonArraySafe<T>(path: string, isValid: (o: unknown) => o is
|
||||
* `rename` it over the target (an atomic operation on the same volume), so a
|
||||
* crash mid-write leaves either the old file or the new one -- never a truncated
|
||||
* one (R1). `pretty` indents for human-readable files; pass false for large
|
||||
* machine-only stores to avoid inflating size/write time (R8). Best-effort: a
|
||||
* read-only data dir just means nothing is persisted.
|
||||
* machine-only stores to avoid inflating size/write time (R8). Returns whether the
|
||||
* write landed: a failure (disk full, read-only data dir) is logged rather than
|
||||
* swallowed silently, so a record that looked saved but vanished on restart is at
|
||||
* least diagnosable (R6). The leftover temp file is removed on failure so a partial
|
||||
* `.tmp` can't accumulate.
|
||||
*/
|
||||
export function writeJsonAtomic(path: string, value: unknown, pretty = true): void {
|
||||
export function writeJsonAtomic(path: string, value: unknown, pretty = true): boolean {
|
||||
const tmp = `${path}.tmp`
|
||||
try {
|
||||
writeFileSync(tmp, pretty ? JSON.stringify(value, null, 2) : JSON.stringify(value))
|
||||
renameSync(tmp, path)
|
||||
} catch {
|
||||
/* best-effort; e.g. a read-only data dir */
|
||||
return true
|
||||
} catch (e) {
|
||||
// R6: previously a silent catch — a failed persist (disk full, read-only
|
||||
// profile) dropped the change with no trace. Log it; the user-facing warning
|
||||
// is deferred to the leveled file logger (CC8).
|
||||
console.error(`[AeroFetch] failed to write ${path}:`, e)
|
||||
try {
|
||||
if (existsSync(tmp)) unlinkSync(tmp)
|
||||
} catch {
|
||||
/* best-effort temp cleanup */
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,8 +123,10 @@ export function createJsonStore<T>(
|
||||
timer = null
|
||||
}
|
||||
if (!dirty) return
|
||||
dirty = false
|
||||
writeJsonAtomic(filePath(), cache ?? [], pretty)
|
||||
// Only clear the dirty flag once the write actually lands. A failed flush
|
||||
// (disk full, transient lock) stays dirty so the next write() or the
|
||||
// quit-time flushAllStores() retries it instead of silently dropping it (R6).
|
||||
if (writeJsonAtomic(filePath(), cache ?? [], pretty)) dirty = false
|
||||
}
|
||||
|
||||
function write(items: T[]): T[] {
|
||||
|
||||
Reference in New Issue
Block a user