// lockfile.js - Modernized with Promises, types, and proper sync imports import * as fsSync from "node:fs"; import fs from "node:fs/promises"; import { platform } from "node:os"; import util from "node:util"; import { onExit } from "signal-exit"; const { O_TRUNC, O_CREAT, O_WRONLY, O_EXCL } = fsSync.constants; const wx = O_TRUNC | O_CREAT | O_WRONLY | O_EXCL; const locks = new Map(); const filetime: "mtime" | "ctime" = platform() === "win32" ? "mtime" : "ctime"; type LockOptions = { req?: number; start?: number; stale?: false | number; // false means no stale check wait?: number; retries?: number; retryWait?: number; pollPeriod?: number; }; const debug = util.debuglog ? util.debuglog("LOCKFILE") : (...args: any[]) => { if (/\blockfile\b/i.test(process.env.NODE_DEBUG || "")) { console.error(`LOCKFILE ${process.pid}`, util.format(...args)); } }; onExit(() => { debug("exit listener"); for (const path of locks.keys()) { LockFile.unlockSync(path); } }); export default class LockFile { static async unlock(path: string): Promise { debug("unlock", path); locks.delete(path); try { await fsSync.unlinkSync(path); } catch {} } static unlockSync(path: string): void { debug("unlockSync", path); try { fsSync.unlinkSync(path); } catch {} locks.delete(path); } static async check(path: string, opts: Partial = {}): Promise { debug("check", path, opts); try { const fd = await fs.open(path, "r"); if (!opts.stale) { await fd.close(); return true; } const stat = await fd.stat(); await fd.close(); const age = Date.now() - stat[filetime].getTime(); return age <= opts.stale; } catch (err: any) { if (err.code !== "ENOENT") throw err; return false; } } static checkSync(path: string, opts: Partial = {}): boolean { debug("checkSync", path, opts); if (opts.wait) throw new Error("opts.wait not supported sync"); try { const fd = fsSync.openSync(path, "r"); if (!opts.stale) { fsSync.closeSync(fd); return true; } const stat = fsSync.fstatSync(fd); fsSync.closeSync(fd); return Date.now() - stat[filetime].getTime() <= opts.stale; } catch (err: any) { if (err.code !== "ENOENT") throw err; return false; } } static req = 1; static async lock(path: string, opts: Partial = {}): Promise { opts.req ??= LockFile.req++; opts.start ??= Date.now(); const tryLock = async (): Promise => { try { const fd = await fs.open(path, wx); debug("locked", path, fd.fd); locks.set(path, fd.fd); await fd.close(); } catch (err: any) { if (err.code !== "EEXIST") throw err; if (!opts.stale) { await LockFile.handleWait(err, path, opts); return; } await LockFile.maybeStale(err, path, opts, false); } }; let retries = opts.retries ?? 0; while (true) { try { await tryLock(); return; } catch (err) { if (--retries < 0) throw err; debug("retry", path); await new Promise((res) => setTimeout(res, opts.retryWait || 100)); opts.start = Date.now(); } } } static async handleWait(err: any, path: string, opts: LockOptions): Promise { if (typeof opts.wait !== "number" || opts.wait <= 0) throw err; const now = Date.now(); if (opts.start! + opts.wait <= now) throw err; const wait = Math.min(opts.start! + opts.wait - now, opts.pollPeriod || 100); await new Promise((res) => setTimeout(res, wait)); await LockFile.lock(path, opts); return; } static async maybeStale(originalErr: any, path: string, opts: LockOptions, hasStaleLock: boolean): Promise { try { const stat = await fs.stat(path); if (opts.stale && Date.now() - stat[filetime].getTime() <= opts.stale!) throw originalErr; if (hasStaleLock) { await LockFile.unlock(path); try { await fs.link(`${path}.STALE`, path); } catch {} try { await fs.unlink(`${path}.STALE`); } catch {} } else { await LockFile.lock(`${path}.STALE`, opts); await LockFile.maybeStale(originalErr, path, opts, true); } } catch (err: any) { if (err.code === "ENOENT") { opts.stale = false; return LockFile.lock(path, opts); } throw err; } } static lockSync(path: string, opts: Partial = {}): void { opts.req ??= LockFile.req++; if (opts.wait || opts.retryWait) throw new Error("opts.wait not supported sync"); try { const fd = fsSync.openSync(path, wx); locks.set(path, fd); fsSync.closeSync(fd); return; } catch (err: any) { if (err.code !== "EEXIST") { LockFile.handleRetrySync(path, opts, err); return; } if (opts.stale) { const stat = fsSync.statSync(path); const age = Date.now() - stat[filetime].getTime(); if (age > opts.stale) { LockFile.unlockSync(path); LockFile.lockSync(path, opts); return; } } LockFile.handleRetrySync(path, opts, err); } } static handleRetrySync(path: string, opts: Partial, err: any): void { if (opts.retries && opts.retries > 0) { opts.retries -= 1; LockFile.lockSync(path, opts); return; } throw err; } }