/** * desktop/src/settings-store.ts — B3: on-disk prefs persistence (get/set). * * A minimal, synchronous JSON store for DesktopPrefs — hand-rolled instead of * electron-store to match the project's minimal-deps convention. It uses ONLY * node:fs + node:path (no electron) so it is unit-testable against a real temp * dir; the caller supplies the userData directory. Reads always go through * validatePrefs, so a corrupt or hand-edited file degrades to defaults rather * than crashing the app; writes are best-effort (a failure is logged, and the * in-memory merged result is still returned so the session reflects the change). */ import fs from 'node:fs' import path from 'node:path' import type { Logger } from './logger.js' import type { DesktopPrefs } from './types.js' import { defaultPrefs, mergePrefs, validatePrefs } from './prefs.js' /** Filename under the store dir; 2-space JSON stays human-readable/editable. */ const PREFS_FILENAME = 'prefs.json' const JSON_INDENT = 2 export interface SettingsStore { get(): DesktopPrefs set(patch: Partial): DesktopPrefs } /** True iff `err` is a Node "file not found" error — the normal first-run case. */ function isFileNotFound(err: unknown): boolean { return err instanceof Error && (err as NodeJS.ErrnoException).code === 'ENOENT' } /** * Create a SettingsStore backed by `/prefs.json`. * - get(): read + parse + validate; missing file → defaults (silent); * unreadable/corrupt file → defaults (logged warn, never throws). * - set(): merge the patch over current prefs, persist, return the merged prefs; * a write failure is logged (error) but still returns the merged result. */ export function createSettingsStore( dir: string, platform: NodeJS.Platform, logger: Logger, ): SettingsStore { const filePath = path.join(dir, PREFS_FILENAME) function get(): DesktopPrefs { let contents: string try { contents = fs.readFileSync(filePath, 'utf8') } catch (err: unknown) { // A missing file is expected before the first save — don't cry wolf. if (!isFileNotFound(err)) { logger.warn(`could not read prefs at ${filePath}, using defaults`) } return defaultPrefs(platform) } try { const parsed: unknown = JSON.parse(contents) return validatePrefs(parsed, platform) } catch { // Corrupt JSON on disk (hand-edit gone wrong) — fall back, don't crash. logger.warn(`prefs at ${filePath} is not valid JSON, using defaults`) return defaultPrefs(platform) } } function set(patch: Partial): DesktopPrefs { const merged = mergePrefs(get(), patch) try { fs.mkdirSync(dir, { recursive: true }) fs.writeFileSync(filePath, JSON.stringify(merged, null, JSON_INDENT), 'utf8') } catch (err: unknown) { // Best-effort persistence: report the failure but still hand back the // merged prefs so the settings UI reflects the user's change this session. logger.error(`failed to persist prefs to ${filePath}`, err) } return merged } return { get, set } }