const fs = require('node:fs/promises'); const path = require('node:path'); function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function acquireFileLock(lockDir, options = {}) { const timeoutMs = Number(options.timeoutMs || 10 * 60 * 1000); const pollMs = Number(options.pollMs || 1000); const deadline = Date.now() + timeoutMs; const parent = path.dirname(lockDir); await fs.mkdir(parent, { recursive: true }); while (Date.now() <= deadline) { try { await fs.mkdir(lockDir); await fs.writeFile(path.join(lockDir, 'owner.json'), JSON.stringify({ pid: process.pid, acquiredAt: new Date().toISOString(), }, null, 2), 'utf8'); return; } catch (error) { if (error && error.code !== 'EEXIST') throw error; if (await removeOrphanedLock(lockDir)) continue; await delay(pollMs); } } throw new Error(`Timed out waiting for ERP task lock: ${lockDir}`); } async function readLockOwner(lockDir) { try { const text = await fs.readFile(path.join(lockDir, 'owner.json'), 'utf8'); const owner = JSON.parse(text); const pid = Number(owner && owner.pid); return Number.isInteger(pid) && pid > 0 ? { ...owner, pid } : null; } catch { return null; } } function processIsRunning(pid) { try { process.kill(pid, 0); return true; } catch (error) { if (error && error.code === 'ESRCH') return false; return true; } } async function removeOrphanedLock(lockDir) { const owner = await readLockOwner(lockDir); if (!owner || processIsRunning(owner.pid)) return false; await fs.rm(lockDir, { recursive: true, force: true }); return true; } async function releaseFileLock(lockDir) { await fs.rm(lockDir, { recursive: true, force: true }); } async function withFileLock(lockDir, fn, options = {}) { await acquireFileLock(lockDir, options); try { return await fn(); } finally { await releaseFileLock(lockDir); } } module.exports = { acquireFileLock, releaseFileLock, withFileLock, };