import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import lockfile from "proper-lockfile"; const STORE_LOCK_OPTIONS = { retries: { retries: 10, factor: 2, minTimeout: 100, maxTimeout: 10_000, randomize: true, }, stale: 30_000, } as const; function safeParseJson(raw: string): T | null { try { return JSON.parse(raw) as T; } catch { return null; } } export async function readJsonFile( filePath: string, fallback: T, ): Promise<{ value: T; exists: boolean }> { try { const raw = await fs.promises.readFile(filePath, "utf-8"); const parsed = safeParseJson(raw); if (parsed == null) return { value: fallback, exists: true }; return { value: parsed, exists: true }; } catch (err) { const code = (err as { code?: string }).code; if (code === "ENOENT") return { value: fallback, exists: false }; return { value: fallback, exists: false }; } } export async function writeJsonFile(filePath: string, value: unknown): Promise { const dir = path.dirname(filePath); await fs.promises.mkdir(dir, { recursive: true, mode: 0o700 }); const tmp = path.join(dir, `${path.basename(filePath)}.${crypto.randomUUID()}.tmp`); await fs.promises.writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf-8", }); await fs.promises.chmod(tmp, 0o600); await fs.promises.rename(tmp, filePath); } async function ensureJsonFile(filePath: string, fallback: unknown) { try { await fs.promises.access(filePath); } catch { await writeJsonFile(filePath, fallback); } } export async function withFileLock( filePath: string, fallback: unknown, fn: () => Promise, ): Promise { await ensureJsonFile(filePath, fallback); let release: (() => Promise) | undefined; try { release = await lockfile.lock(filePath, STORE_LOCK_OPTIONS); return await fn(); } finally { if (release) { try { await release(); } catch { // ignore unlock errors } } } }