{"version":3,"file":"withFileLock.cjs","names":["readFile","unlink","mkdir","dirname","writeFile"],"sources":["../../../src/utils/withFileLock.ts"],"sourcesContent":["import { rmSync } from 'node:fs';\nimport { mkdir, readFile, unlink, writeFile } from 'node:fs/promises';\nimport { dirname } from 'node:path';\n\n/** Payload written inside a lock file, used to detect abandoned locks. */\ntype LockOwnerData = {\n  /** PID of the process currently holding the lock. */\n  pid: number;\n  /** Epoch milliseconds at which the lock was taken. */\n  acquiredAt: number;\n};\n\ntype WithFileLockOptions = {\n  /**\n   * How long a lock may be held before it is considered abandoned and taken\n   * over, in milliseconds.\n   *\n   * @default 30000 = 30 seconds\n   */\n  staleTimeoutMs?: number;\n  /**\n   * How long to wait for the lock before running the callback anyway, in\n   * milliseconds. Giving up serialises nothing, but it is strictly better than\n   * deadlocking a build behind a lock that never clears.\n   *\n   * @default 60000 = 1 minute\n   */\n  acquireTimeoutMs?: number;\n};\n\nconst DEFAULT_OPTIONS = {\n  staleTimeoutMs: 30 * 1000,\n  acquireTimeoutMs: 60 * 1000,\n} satisfies Required<WithFileLockOptions>;\n\n/** Delay between two attempts to take a lock held by someone else. */\nconst POLL_INTERVAL_MS = 25;\n\nconst delay = (durationMs: number): Promise<void> =>\n  new Promise((resolve) => setTimeout(resolve, durationMs));\n\n/**\n * Locks held by this process, released synchronously on exit so a crash never\n * leaves a lock file that would stall every later run for `staleTimeoutMs`.\n */\nconst ownedLockFilePaths = new Set<string>();\n\nprocess.on('exit', () => {\n  for (const lockFilePath of ownedLockFilePaths) {\n    try {\n      rmSync(lockFilePath, { force: true });\n    } catch {}\n  }\n});\n\n/**\n * Whether the process holding the lock is still alive. An unreadable PID is\n * reported as alive so {@link WithFileLockOptions.staleTimeoutMs} stays the\n * only way to reclaim the lock.\n */\nconst getIsOwnerAlive = (pid: number): boolean => {\n  if (!pid || pid === process.pid) return true;\n\n  try {\n    // Signal 0 checks for existence without delivering a signal.\n    process.kill(pid, 0);\n    return true;\n  } catch (error) {\n    // EPERM means the process exists but belongs to another user.\n    return (error as NodeJS.ErrnoException).code === 'EPERM';\n  }\n};\n\nconst readLockOwner = async (\n  lockFilePath: string\n): Promise<LockOwnerData | undefined> => {\n  try {\n    const raw = await readFile(lockFilePath, 'utf8');\n    const parsed = JSON.parse(raw) as Partial<LockOwnerData>;\n\n    return {\n      pid: parsed.pid ?? 0,\n      acquiredAt: parsed.acquiredAt ?? 0,\n    };\n  } catch {\n    return undefined;\n  }\n};\n\nconst releaseLock = async (lockFilePath: string): Promise<void> => {\n  try {\n    await unlink(lockFilePath);\n  } catch {}\n};\n\n/**\n * Attempts to create the lock file. `wx` makes the creation atomic, so exactly\n * one process wins even when several try at the same moment.\n */\nconst tryAcquireLock = async (lockFilePath: string): Promise<boolean> => {\n  const data = JSON.stringify({\n    pid: process.pid,\n    acquiredAt: Date.now(),\n  } satisfies LockOwnerData);\n\n  for (let attempt = 0; attempt < 2; attempt++) {\n    try {\n      await mkdir(dirname(lockFilePath), { recursive: true });\n      await writeFile(lockFilePath, data, { flag: 'wx' });\n\n      return true;\n    } catch (error) {\n      const code = (error as NodeJS.ErrnoException).code;\n\n      if (code === 'EEXIST') return false;\n      // The directory was removed between the mkdir and the write (e.g. a\n      // concurrent output-directory clean); retry once.\n      if (code === 'ENOENT' && attempt === 0) continue;\n\n      throw error;\n    }\n  }\n\n  return false;\n};\n\n/**\n * Runs `callback` under a cross-process mutex materialised by a lock file.\n *\n * Unlike `runOnce`, which lets one process do the work and the others skip it,\n * every caller here runs the callback — just never at the same time. This is\n * what shared, read-modify-write outputs need (e.g. building the `.intlayer`\n * dictionaries, which merges every dictionary on disk): concurrent runs would\n * each read a half-written state.\n *\n * The lock is released even when the callback throws, and its error is\n * re-thrown to the caller unchanged.\n *\n * @param lockFilePath - Path of the lock file coordinating the callers.\n * @param callback - The critical section.\n * @param options - Staleness and acquisition timeouts.\n *\n * @example\n * ```typescript\n * await withFileLock(join(cacheDir, 'build-dictionary.lock'), () =>\n *   buildDictionary([dictionary], configuration)\n * );\n * ```\n */\nexport const withFileLock = async <T>(\n  lockFilePath: string,\n  callback: () => T | Promise<T>,\n  options?: WithFileLockOptions\n): Promise<T> => {\n  const { staleTimeoutMs, acquireTimeoutMs } = {\n    ...DEFAULT_OPTIONS,\n    ...(options ?? {}),\n  };\n\n  const acquireDeadline = Date.now() + acquireTimeoutMs;\n  let hasAcquiredLock = false;\n\n  while (Date.now() < acquireDeadline) {\n    if (await tryAcquireLock(lockFilePath)) {\n      hasAcquiredLock = true;\n      break;\n    }\n\n    const owner = await readLockOwner(lockFilePath);\n\n    // A missing owner means the lock was released between the two calls; loop\n    // back and try to take it.\n    if (owner) {\n      const isAbandoned =\n        !getIsOwnerAlive(owner.pid) ||\n        Date.now() - owner.acquiredAt > staleTimeoutMs;\n\n      if (isAbandoned) {\n        await releaseLock(lockFilePath);\n        continue;\n      }\n    }\n\n    await delay(POLL_INTERVAL_MS);\n  }\n\n  if (hasAcquiredLock) ownedLockFilePaths.add(lockFilePath);\n\n  try {\n    return await callback();\n  } finally {\n    if (hasAcquiredLock) {\n      ownedLockFilePaths.delete(lockFilePath);\n      await releaseLock(lockFilePath);\n    }\n  }\n};\n"],"mappings":";;;;;;AA8BA,MAAM,kBAAkB;CACtB,gBAAgB;CAChB,kBAAkB;AACpB;;AAGA,MAAM,mBAAmB;AAEzB,MAAM,SAAS,eACb,IAAI,SAAS,YAAY,WAAW,SAAS,UAAU,CAAC;;;;;AAM1D,MAAM,qCAAqB,IAAI,IAAY;AAE3C,QAAQ,GAAG,cAAc;CACvB,KAAK,MAAM,gBAAgB,oBACzB,IAAI;EACF,oBAAO,cAAc,EAAE,OAAO,KAAK,CAAC;CACtC,QAAQ,CAAC;AAEb,CAAC;;;;;;AAOD,MAAM,mBAAmB,QAAyB;CAChD,IAAI,CAAC,OAAO,QAAQ,QAAQ,KAAK,OAAO;CAExC,IAAI;EAEF,QAAQ,KAAK,KAAK,CAAC;EACnB,OAAO;CACT,SAAS,OAAO;EAEd,OAAQ,MAAgC,SAAS;CACnD;AACF;AAEA,MAAM,gBAAgB,OACpB,iBACuC;CACvC,IAAI;EACF,MAAM,MAAM,UAAMA,2BAAS,cAAc,MAAM;EAC/C,MAAM,SAAS,KAAK,MAAM,GAAG;EAE7B,OAAO;GACL,KAAK,OAAO,OAAO;GACnB,YAAY,OAAO,cAAc;EACnC;CACF,QAAQ;EACN;CACF;AACF;AAEA,MAAM,cAAc,OAAO,iBAAwC;CACjE,IAAI;EACF,UAAMC,yBAAO,YAAY;CAC3B,QAAQ,CAAC;AACX;;;;;AAMA,MAAM,iBAAiB,OAAO,iBAA2C;CACvE,MAAM,OAAO,KAAK,UAAU;EAC1B,KAAK,QAAQ;EACb,YAAY,KAAK,IAAI;CACvB,CAAyB;CAEzB,KAAK,IAAI,UAAU,GAAG,UAAU,GAAG,WACjC,IAAI;EACF,UAAMC,4BAAMC,mBAAQ,YAAY,GAAG,EAAE,WAAW,KAAK,CAAC;EACtD,UAAMC,4BAAU,cAAc,MAAM,EAAE,MAAM,KAAK,CAAC;EAElD,OAAO;CACT,SAAS,OAAO;EACd,MAAM,OAAQ,MAAgC;EAE9C,IAAI,SAAS,UAAU,OAAO;EAG9B,IAAI,SAAS,YAAY,YAAY,GAAG;EAExC,MAAM;CACR;CAGF,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,MAAa,eAAe,OAC1B,cACA,UACA,YACe;CACf,MAAM,EAAE,gBAAgB,qBAAqB;EAC3C,GAAG;EACH,GAAI,WAAW,CAAC;CAClB;CAEA,MAAM,kBAAkB,KAAK,IAAI,IAAI;CACrC,IAAI,kBAAkB;CAEtB,OAAO,KAAK,IAAI,IAAI,iBAAiB;EACnC,IAAI,MAAM,eAAe,YAAY,GAAG;GACtC,kBAAkB;GAClB;EACF;EAEA,MAAM,QAAQ,MAAM,cAAc,YAAY;EAI9C,IAAI,OAKF;OAHE,CAAC,gBAAgB,MAAM,GAAG,KAC1B,KAAK,IAAI,IAAI,MAAM,aAAa,gBAEjB;IACf,MAAM,YAAY,YAAY;IAC9B;GACF;;EAGF,MAAM,MAAM,gBAAgB;CAC9B;CAEA,IAAI,iBAAiB,mBAAmB,IAAI,YAAY;CAExD,IAAI;EACF,OAAO,MAAM,SAAS;CACxB,UAAU;EACR,IAAI,iBAAiB;GACnB,mBAAmB,OAAO,YAAY;GACtC,MAAM,YAAY,YAAY;EAChC;CACF;AACF"}