import fs from 'fs'; import path from 'path'; // Per-file promise chain. Any writer for a given path queues behind the previous // one so we never have two writers renaming into the same target concurrently. const locks = new Map(); function withLock(key, fn) { const prev = locks.get(key) || Promise.resolve(); // Ignore the previous task's result/errors — every waiter still gets a chance to run. const next = prev.then(fn, fn); const tail = next.catch(() => {}); locks.set(key, tail); tail.then(() => { if (locks.get(key) === tail) locks.delete(key); }); return next; } export function readJSON(filePath) { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } // Write JSON atomically: serialize -> write to tmp in the same directory -> // fsync -> rename over the target. Rename within a single filesystem is atomic // on POSIX, so readers either see the old or new file, never a torn write. export function writeJSON(filePath, data) { return withLock(path.resolve(filePath), async () => { const dir = path.dirname(filePath); const base = path.basename(filePath); const tmp = path.join(dir, `.${base}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`); const payload = JSON.stringify(data, null, 4); const fh = await fs.promises.open(tmp, 'w'); try { await fh.writeFile(payload); await fh.sync(); } finally { await fh.close(); } try { await fs.promises.rename(tmp, filePath); } catch (error) { // If rename fails, don't leave the tmp file behind. fs.promises.unlink(tmp).catch(() => {}); throw error; } }); } // Exported for tests only. export const __test__ = { withLock, locks };