UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

99 lines (98 loc) • 3.26 kB
import { mkdirSync, readFileSync, rmSync, statSync, utimesSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { randomBytes } from "node:crypto"; const DEFAULT_STALE_MS = 10_000; const RETRIES = 10; const MIN_TIMEOUT_MS = 5; const MAX_TIMEOUT_MS = 100; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); export async function acquireCcLock(target, opts) { const lockPath = `${target}.lock`; const ownerPath = join(lockPath, "owner"); const staleMs = opts?.staleMs ?? DEFAULT_STALE_MS; const nonce = randomBytes(12).toString("hex"); const ownedByUs = () => { try { return readFileSync(ownerPath, "utf8") === nonce; } catch { return false; } }; for (let attempt = 0;; attempt++) { if (attempt >= RETRIES) { throw new Error(`cc lockfile: could not acquire ${lockPath} after ${RETRIES} retries`); } try { mkdirSync(lockPath); writeFileSync(ownerPath, nonce); let compromised = false; const beat = setInterval(() => { if (!ownedByUs()) { compromised = true; clearInterval(beat); return; } const now = new Date(); try { utimesSync(lockPath, now, now); } catch { compromised = true; clearInterval(beat); } }, Math.max(1, Math.floor(staleMs / 2))); beat.unref?.(); return () => { clearInterval(beat); if (compromised || !ownedByUs()) { throw new Error(`cc lockfile: lock on ${target} was reclaimed while held (critical section exceeded the ` + `stale window and another process took over) — the guarded write may have raced; do not trust it`); } try { rmSync(lockPath, { recursive: true, force: true }); } catch { } }; } catch (err) { if (err.code !== "EEXIST") throw err; try { const st = statSync(lockPath); if (Date.now() - st.mtimeMs > staleMs) { const st2 = statSync(lockPath); if (st2.mtimeMs === st.mtimeMs) { try { rmSync(lockPath, { recursive: true, force: true }); } catch { } } } } catch { } const backoff = Math.min(MAX_TIMEOUT_MS, MIN_TIMEOUT_MS * 2 ** attempt); await sleep(backoff / 2 + Math.random() * (backoff / 2)); } } } export async function withCcLock(target, fn) { const release = await acquireCcLock(target); let result; try { result = await fn(); } catch (err) { try { release(); } catch { } throw err; } release(); return result; }