import { randomUUID } from "node:crypto"; import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs"; import { dirname } from "node:path"; export function readJsonFile(file: string): unknown | null { if (!existsSync(file)) return null; return JSON.parse(readFileSync(file, "utf-8")); } export function readJsonObject( file: string, onError?: (err: unknown) => void, ): Record | null { try { const parsed = readJsonFile(file); return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as Record) : null; } catch (err) { onError?.(err); return null; } } function tryFsync(path: string): void { try { const fd = openSync(path, "r"); try { fsyncSync(fd); } finally { closeSync(fd); } } catch {} } export function writeJsonAtomic(file: string, value: unknown): void { const tmp = `${file}.tmp.${process.pid}.${randomUUID()}`; try { mkdirSync(dirname(file), { recursive: true }); writeFileSync(tmp, JSON.stringify(value, null, 2), "utf-8"); tryFsync(tmp); renameSync(tmp, file); tryFsync(dirname(file)); } catch (err) { try { unlinkSync(tmp); } catch {} throw err; } } export function compareNumericKeys(a: string, b: string): number { const na = Number(a); const nb = Number(b); if (Number.isNaN(na) && Number.isNaN(nb)) return a.localeCompare(b); if (Number.isNaN(na)) return 1; if (Number.isNaN(nb)) return -1; return na - nb; } export function sortedByNumericKeys(obj: Record): Record { return Object.fromEntries(Object.entries(obj).sort(([a], [b]) => compareNumericKeys(a, b))) as Record; }