// Atomic, locked access to calendar.json so concurrent runs (write-daily, publish-tick, // research refill) can't clobber each other. Every mutation re-reads under an exclusive // lockfile, mutates, and writes — no long-held in-memory copies. import { existsSync, readFileSync, writeFileSync, openSync, closeSync, unlinkSync, statSync } from "node:fs"; const LOCK_STALE_MS = 10 * 60 * 1000; // a lock older than this is presumed orphaned const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); async function acquire(calPath, timeoutMs = 120000) { const lp = `${calPath}.lock`; const start = Date.now(); for (;;) { try { const fd = openSync(lp, "wx"); // exclusive create — fails if it exists writeFileSync(lp, `${process.pid} ${new Date().toISOString()}`); closeSync(fd); return lp; } catch { try { if (Date.now() - statSync(lp).mtimeMs > LOCK_STALE_MS) { unlinkSync(lp); continue; } } catch { /* lock vanished — retry */ } if (Date.now() - start > timeoutMs) throw new Error(`calendar lock timeout: ${lp}`); await sleep(200); } } } const release = (lp) => { try { unlinkSync(lp); } catch { /* already gone */ } }; export async function withCalendarLock(calPath, fn) { const lp = await acquire(calPath); try { return await fn(); } finally { release(lp); } } export const readCalendar = (calPath) => existsSync(calPath) ? JSON.parse(readFileSync(calPath, "utf8")) : []; const writeCalendar = (calPath, cal) => writeFileSync(calPath, JSON.stringify(cal, null, 2)); /** Locked read-modify-write of one entry (Object.assign patch). Returns the updated entry. */ export async function updateEntry(calPath, slug, patch) { return withCalendarLock(calPath, () => { const cal = readCalendar(calPath); const e = cal.find((x) => x.slug === slug); if (e) { Object.assign(e, patch); writeCalendar(calPath, cal); } return e; }); } /** Locked append if the slug is not already present. */ export async function addEntry(calPath, entry) { return withCalendarLock(calPath, () => { const cal = readCalendar(calPath); if (!cal.some((x) => x.slug === entry.slug)) { cal.push(entry); writeCalendar(calPath, cal); } return entry; }); } /** Locked merge: append each new entry whose slug isn't present. Returns count added. */ export async function mergeEntries(calPath, entries) { return withCalendarLock(calPath, () => { const cal = readCalendar(calPath); const have = new Set(cal.map((x) => x.slug)); let added = 0; for (const e of entries) { if (!have.has(e.slug)) { cal.push(e); have.add(e.slug); added++; } } writeCalendar(calPath, cal); return added; }); }