refactor(persistence): extract shared JSON I/O into json-store.ts; add parent-dir creation
This commit is contained in:
parent
43a03272bf
commit
23cef06580
4 changed files with 53 additions and 41 deletions
32
src/main/json-store.ts
Normal file
32
src/main/json-store.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// Tolerant JSON file primitives shared by the persistence modules
|
||||
// (project/recents/settings). Deliberately Electron-free so test harnesses
|
||||
// can bundle them alongside their consumers.
|
||||
//
|
||||
// Semantics every caller used to hand-roll:
|
||||
// - read: missing/corrupt file resolves to null (caller picks the
|
||||
// fallback shape: [], {}, or a thrown domain error)
|
||||
// - write: pretty-printed UTF-8, parent directories created on demand.
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
/** Parse a JSON file, or return null when it is missing or unreadable. */
|
||||
export function readJsonFile<T = unknown>(file: string): T | null {
|
||||
try {
|
||||
const raw = fs.readFileSync(file, "utf-8");
|
||||
return JSON.parse(raw) as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Read a JSON file expected to hold an array; anything else yields []. */
|
||||
export function readJsonArray<T = unknown>(file: string): T[] {
|
||||
const parsed = readJsonFile<T[]>(file);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
}
|
||||
|
||||
/** Pretty-write a JSON file, creating parent directories when needed. */
|
||||
export function writeJsonFile(file: string, value: unknown): void {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, JSON.stringify(value, null, 2), "utf-8");
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { readJsonFile, readJsonArray, writeJsonFile } from "./json-store.js";
|
||||
|
||||
export interface ChapterEntry {
|
||||
id: string;
|
||||
|
|
@ -103,9 +104,10 @@ export function createBook(bookPath: string, title: string): BookMeta {
|
|||
}
|
||||
|
||||
export function loadBook(bookPath: string): BookMeta {
|
||||
const raw = fs.readFileSync(metaPath(bookPath), "utf-8");
|
||||
const data = JSON.parse(raw) as BookMeta;
|
||||
if (data.app !== APP_ID) {
|
||||
// Strict on purpose: a wrong app tag means "not a Folio book" and callers
|
||||
// rely on the throw. This is domain validation, not generic JSON I/O.
|
||||
const data = readJsonFile<BookMeta>(metaPath(bookPath));
|
||||
if (!data || data.app !== APP_ID) {
|
||||
throw new Error(
|
||||
`Not a Folio book (no valid folio.json): ${bookPath}`
|
||||
);
|
||||
|
|
@ -117,38 +119,22 @@ export function loadBook(bookPath: string): BookMeta {
|
|||
}
|
||||
|
||||
export function saveMeta(bookPath: string, meta: BookMeta): void {
|
||||
fs.writeFileSync(
|
||||
metaPath(bookPath),
|
||||
JSON.stringify(meta, null, 2),
|
||||
"utf-8"
|
||||
);
|
||||
}
|
||||
|
||||
function ensureParent(filePath: string): void {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
writeJsonFile(metaPath(bookPath), meta);
|
||||
}
|
||||
|
||||
export function listRecents(filePath: string): Recent[] {
|
||||
try {
|
||||
const raw = fs.readFileSync(filePath, "utf-8");
|
||||
const arr = JSON.parse(raw);
|
||||
return Array.isArray(arr) ? (arr as Recent[]) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
return readJsonArray<Recent>(filePath);
|
||||
}
|
||||
|
||||
export function addRecent(filePath: string, recent: Recent): Recent[] {
|
||||
const recents = listRecents(filePath).filter((r) => r.path !== recent.path);
|
||||
recents.unshift(recent);
|
||||
ensureParent(filePath);
|
||||
fs.writeFileSync(filePath, JSON.stringify(recents, null, 2), "utf-8");
|
||||
writeJsonFile(filePath, recents);
|
||||
return recents;
|
||||
}
|
||||
|
||||
export function removeRecent(filePath: string, recentPath: string): Recent[] {
|
||||
const recents = listRecents(filePath).filter((r) => r.path !== recentPath);
|
||||
ensureParent(filePath);
|
||||
fs.writeFileSync(filePath, JSON.stringify(recents, null, 2), "utf-8");
|
||||
writeJsonFile(filePath, recents);
|
||||
return recents;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { app } from "electron";
|
||||
import { APP_ID, hasBook } from "./project.js";
|
||||
import {
|
||||
APP_ID,
|
||||
hasBook,
|
||||
listRecents as _listRecents,
|
||||
addRecent as _addRecent,
|
||||
removeRecent as _removeRecent,
|
||||
} from "./project.js";
|
||||
import { readJsonArray, writeJsonFile } from "./json-store.js";
|
||||
import type { Recent } from "./project.js";
|
||||
|
||||
export type { Recent };
|
||||
|
|
@ -28,11 +27,10 @@ function legacyElectronRecentsFile(): string {
|
|||
}
|
||||
|
||||
function loadRaw(file: string): Recent[] {
|
||||
try {
|
||||
return _listRecents(file).filter((r) => r && typeof r.path === "string");
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
// Tolerate corrupt files ([]), then drop entries without a path.
|
||||
return readJsonArray<Recent>(file).filter(
|
||||
(r) => r && typeof r.path === "string"
|
||||
);
|
||||
}
|
||||
|
||||
// One-time migration: fold entries from a stray ~/.config/Electron recents
|
||||
|
|
@ -49,7 +47,7 @@ export function migrateLegacyRecents(): void {
|
|||
const known = new Set(current.map((r) => r.path));
|
||||
const merged = [...current, ...incoming.filter((r) => !known.has(r.path))];
|
||||
if (merged.length !== current.length) {
|
||||
fs.writeFileSync(canonical, JSON.stringify(merged, null, 2), "utf-8");
|
||||
writeJsonFile(canonical, merged);
|
||||
}
|
||||
fs.rmSync(legacy, { force: true });
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { app } from "electron";
|
||||
import { APP_ID } from "./project.js";
|
||||
import { readJsonFile, writeJsonFile } from "./json-store.js";
|
||||
|
||||
// Tiny per-user settings store (a JSON file in the Electron userData dir).
|
||||
// Used for preferences that should survive restarts, such as the theme.
|
||||
|
|
@ -12,13 +12,9 @@ let cache: Record<string, unknown> | null = null;
|
|||
|
||||
function load(): Record<string, unknown> {
|
||||
if (cache) return cache;
|
||||
try {
|
||||
const raw = fs.readFileSync(SETTINGS_FILE(), "utf-8");
|
||||
const parsed = JSON.parse(raw);
|
||||
cache = parsed && typeof parsed === "object" ? parsed : {};
|
||||
} catch {
|
||||
cache = {};
|
||||
}
|
||||
// Missing or corrupt file both mean "no settings yet" -> {}.
|
||||
cache =
|
||||
readJsonFile<Record<string, unknown>>(SETTINGS_FILE()) ?? {};
|
||||
return cache as Record<string, unknown>;
|
||||
}
|
||||
|
||||
|
|
@ -30,5 +26,5 @@ export function getSetting<T>(key: string, fallback: T): T {
|
|||
export function setSetting(key: string, value: unknown): void {
|
||||
const settings = load();
|
||||
settings[key] = value;
|
||||
fs.writeFileSync(SETTINGS_FILE(), JSON.stringify(settings, null, 2), "utf-8");
|
||||
writeJsonFile(SETTINGS_FILE(), settings);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue