// Generic exclusive file lock, same idiom as calendar.mjs but reusable for any // critical section. Used for the global BUILD lock so cron builds (promoteDraft) // and Developer Console builds (preview + publish) never run npm/astro // concurrently — important on this memory-constrained box. import { writeFileSync, openSync, closeSync, unlinkSync, statSync } from "node:fs"; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); /** * Acquire an exclusive lockfile, run fn, always release. * @param {string} lockPath absolute path to the lockfile * @param {() => Promise|T} fn critical section * @param {{timeoutMs?: number, staleMs?: number}} [opts] * @returns {Promise} */ export async function withLock(lockPath, fn, { timeoutMs = 300000, staleMs = 15 * 60 * 1000 } = {}) { const start = Date.now(); for (;;) { try { const fd = openSync(lockPath, "wx"); // exclusive create — fails if it exists writeFileSync(lockPath, `${process.pid} ${new Date().toISOString()}`); closeSync(fd); break; } catch { try { if (Date.now() - statSync(lockPath).mtimeMs > staleMs) { unlinkSync(lockPath); // presumed orphaned continue; } } catch { /* lock vanished between failed create and stat — retry immediately */ } if (Date.now() - start > timeoutMs) throw new Error(`lock timeout: ${lockPath}`); await sleep(200); } } try { return await fn(); } finally { try { unlinkSync(lockPath); } catch { /* already gone */ } } }