/** * @file Per-lane serialization lock. Ensures that only one async operation runs * on a lane at a time, preventing concurrent git checkout races and other * worktree collisions. Implemented as a chain of promises keyed by lane ID; * acquiring a lock waits for the previous holder to settle (success or throw), * then runs the new work, and releases for the next waiter. * @author Nguyễn Ngọc Trí Vĩ */ const chains = new Map(); /** * Acquire the per-lane lock, run fn, and release. * The lock serialises work per lane_id: the next waiter runs only after the * previous holder settles (success or error). If fn throws, the exception * propagates to the caller; the chain is *not* poisoned — the next waiter * still gets a fresh attempt. * * @param {number|string} id - Lane ID, converted to string for deduplication. * @param {() => Promise} fn - Async function to run while holding the lock. * @returns {Promise} The result of fn, or its thrown error. */ function withLaneLock(id, fn) { const key = String(id); const prev = chains.get(key) || Promise.resolve(); const run = prev.then(fn, fn); // run regardless of how the previous holder settled // Keep the chain alive but never let a rejection poison the next waiter. const settled = run.then( () => {}, () => {} ); chains.set(key, settled); // Clean up the chain entry when it settles to prevent unbounded map growth. settled.then(() => { if (chains.get(key) === settled) chains.delete(key); }); return run; } module.exports = { withLaneLock };