seedproject-web/agents/scripts/lib/calendar.mjs

96 lines
2.8 KiB
JavaScript
Raw Normal View History

// 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;
});
}