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 fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import { readJsonFile, readJsonArray, writeJsonFile } from "./json-store.js";
|
||||||
|
|
||||||
export interface ChapterEntry {
|
export interface ChapterEntry {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -103,9 +104,10 @@ export function createBook(bookPath: string, title: string): BookMeta {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadBook(bookPath: string): BookMeta {
|
export function loadBook(bookPath: string): BookMeta {
|
||||||
const raw = fs.readFileSync(metaPath(bookPath), "utf-8");
|
// Strict on purpose: a wrong app tag means "not a Folio book" and callers
|
||||||
const data = JSON.parse(raw) as BookMeta;
|
// rely on the throw. This is domain validation, not generic JSON I/O.
|
||||||
if (data.app !== APP_ID) {
|
const data = readJsonFile<BookMeta>(metaPath(bookPath));
|
||||||
|
if (!data || data.app !== APP_ID) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Not a Folio book (no valid folio.json): ${bookPath}`
|
`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 {
|
export function saveMeta(bookPath: string, meta: BookMeta): void {
|
||||||
fs.writeFileSync(
|
writeJsonFile(metaPath(bookPath), meta);
|
||||||
metaPath(bookPath),
|
|
||||||
JSON.stringify(meta, null, 2),
|
|
||||||
"utf-8"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureParent(filePath: string): void {
|
|
||||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listRecents(filePath: string): Recent[] {
|
export function listRecents(filePath: string): Recent[] {
|
||||||
try {
|
return readJsonArray<Recent>(filePath);
|
||||||
const raw = fs.readFileSync(filePath, "utf-8");
|
|
||||||
const arr = JSON.parse(raw);
|
|
||||||
return Array.isArray(arr) ? (arr as Recent[]) : [];
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addRecent(filePath: string, recent: Recent): Recent[] {
|
export function addRecent(filePath: string, recent: Recent): Recent[] {
|
||||||
const recents = listRecents(filePath).filter((r) => r.path !== recent.path);
|
const recents = listRecents(filePath).filter((r) => r.path !== recent.path);
|
||||||
recents.unshift(recent);
|
recents.unshift(recent);
|
||||||
ensureParent(filePath);
|
writeJsonFile(filePath, recents);
|
||||||
fs.writeFileSync(filePath, JSON.stringify(recents, null, 2), "utf-8");
|
|
||||||
return recents;
|
return recents;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeRecent(filePath: string, recentPath: string): Recent[] {
|
export function removeRecent(filePath: string, recentPath: string): Recent[] {
|
||||||
const recents = listRecents(filePath).filter((r) => r.path !== recentPath);
|
const recents = listRecents(filePath).filter((r) => r.path !== recentPath);
|
||||||
ensureParent(filePath);
|
writeJsonFile(filePath, recents);
|
||||||
fs.writeFileSync(filePath, JSON.stringify(recents, null, 2), "utf-8");
|
|
||||||
return recents;
|
return recents;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
|
import { APP_ID, hasBook } from "./project.js";
|
||||||
import {
|
import {
|
||||||
APP_ID,
|
|
||||||
hasBook,
|
|
||||||
listRecents as _listRecents,
|
|
||||||
addRecent as _addRecent,
|
addRecent as _addRecent,
|
||||||
removeRecent as _removeRecent,
|
removeRecent as _removeRecent,
|
||||||
} from "./project.js";
|
} from "./project.js";
|
||||||
|
import { readJsonArray, writeJsonFile } from "./json-store.js";
|
||||||
import type { Recent } from "./project.js";
|
import type { Recent } from "./project.js";
|
||||||
|
|
||||||
export type { Recent };
|
export type { Recent };
|
||||||
|
|
@ -28,11 +27,10 @@ function legacyElectronRecentsFile(): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadRaw(file: string): Recent[] {
|
function loadRaw(file: string): Recent[] {
|
||||||
try {
|
// Tolerate corrupt files ([]), then drop entries without a path.
|
||||||
return _listRecents(file).filter((r) => r && typeof r.path === "string");
|
return readJsonArray<Recent>(file).filter(
|
||||||
} catch {
|
(r) => r && typeof r.path === "string"
|
||||||
return [];
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// One-time migration: fold entries from a stray ~/.config/Electron recents
|
// 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 known = new Set(current.map((r) => r.path));
|
||||||
const merged = [...current, ...incoming.filter((r) => !known.has(r.path))];
|
const merged = [...current, ...incoming.filter((r) => !known.has(r.path))];
|
||||||
if (merged.length !== current.length) {
|
if (merged.length !== current.length) {
|
||||||
fs.writeFileSync(canonical, JSON.stringify(merged, null, 2), "utf-8");
|
writeJsonFile(canonical, merged);
|
||||||
}
|
}
|
||||||
fs.rmSync(legacy, { force: true });
|
fs.rmSync(legacy, { force: true });
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import { APP_ID } from "./project.js";
|
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).
|
// Tiny per-user settings store (a JSON file in the Electron userData dir).
|
||||||
// Used for preferences that should survive restarts, such as the theme.
|
// 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> {
|
function load(): Record<string, unknown> {
|
||||||
if (cache) return cache;
|
if (cache) return cache;
|
||||||
try {
|
// Missing or corrupt file both mean "no settings yet" -> {}.
|
||||||
const raw = fs.readFileSync(SETTINGS_FILE(), "utf-8");
|
cache =
|
||||||
const parsed = JSON.parse(raw);
|
readJsonFile<Record<string, unknown>>(SETTINGS_FILE()) ?? {};
|
||||||
cache = parsed && typeof parsed === "object" ? parsed : {};
|
|
||||||
} catch {
|
|
||||||
cache = {};
|
|
||||||
}
|
|
||||||
return cache as Record<string, unknown>;
|
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 {
|
export function setSetting(key: string, value: unknown): void {
|
||||||
const settings = load();
|
const settings = load();
|
||||||
settings[key] = value;
|
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