@dbx-tools/core 0.6.69 → 0.6.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.ts CHANGED
@@ -6,6 +6,7 @@ export * as bin from "./src/bin.ts";
6
6
  export * as brand from "./src/brand.ts";
7
7
  export * as exec from "./src/exec.ts";
8
8
  export * as file from "./src/file.ts";
9
+ export * as fileLock from "./src/file-lock.ts";
9
10
  export * as processLock from "./src/process-lock.ts";
10
11
  export * as project from "./src/project.ts";
11
12
  export type { BinContext, BinSelectionContext, BinSelector, BinVersionOutput, BinVersionParser, BinOptions, BinUrl } from "./src/bin.ts";
@@ -13,4 +14,5 @@ export { BrandContextSchema, defaultBrandContext, parseBrandContext, brandContex
13
14
  export type { BrandContext, BrandContextInput } from "./src/brand.ts";
14
15
  export { COMMAND_NOT_FOUND_EXIT_CODE } from "./src/exec.ts";
15
16
  export type { ExecStdio, LineHandler, StdioOption, ExecResult, ChildProcessResult, ExecOptions, SyncExecStdio, SyncExecOptions, SpawnArgs } from "./src/exec.ts";
17
+ export type { FileLockBackend, FileLockAcquisition, FileLockOptions } from "./src/file-lock.ts";
16
18
  export type { ProjectContext } from "./src/project.ts";
package/lib/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export * as bin from "./src/bin.ts";
2
2
  export * as brand from "./src/brand.ts";
3
3
  export * as exec from "./src/exec.ts";
4
4
  export * as file from "./src/file.ts";
5
+ export * as fileLock from "./src/file-lock.ts";
5
6
  export * as processLock from "./src/process-lock.ts";
6
7
  export * as project from "./src/project.ts";
7
8
  export type { BinContext, BinSelectionContext, BinSelector, BinVersionOutput, BinVersionParser, BinOptions, BinUrl } from "./src/bin.ts";
@@ -9,4 +10,5 @@ export { BrandContextSchema, defaultBrandContext, parseBrandContext, brandContex
9
10
  export type { BrandContext, BrandContextInput } from "./src/brand.ts";
10
11
  export { COMMAND_NOT_FOUND_EXIT_CODE } from "./src/exec.ts";
11
12
  export type { ExecStdio, LineHandler, StdioOption, ExecResult, ChildProcessResult, ExecOptions, SyncExecStdio, SyncExecOptions, SpawnArgs } from "./src/exec.ts";
13
+ export type { FileLockBackend, FileLockAcquisition, FileLockOptions } from "./src/file-lock.ts";
12
14
  export type { ProjectContext } from "./src/project.ts";
package/lib/index.js CHANGED
@@ -5,8 +5,9 @@ export * as bin from "./src/bin.js";
5
5
  export * as brand from "./src/brand.js";
6
6
  export * as exec from "./src/exec.js";
7
7
  export * as file from "./src/file.js";
8
+ export * as fileLock from "./src/file-lock.js";
8
9
  export * as processLock from "./src/process-lock.js";
9
10
  export * as project from "./src/project.js";
10
11
  export { BrandContextSchema, defaultBrandContext, parseBrandContext, brandContextJsonSchema, brandContextPrompt } from "./src/brand.js";
11
12
  export { COMMAND_NOT_FOUND_EXIT_CODE } from "./src/exec.js";
12
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwyQ0FBMkM7QUFDM0MsbURBQW1EO0FBQ25ELHdFQUF3RTtBQUV4RSxPQUFPLEtBQUssR0FBRyxNQUFNLGNBQWMsQ0FBQztBQUNwQyxPQUFPLEtBQUssS0FBSyxNQUFNLGdCQUFnQixDQUFDO0FBQ3hDLE9BQU8sS0FBSyxJQUFJLE1BQU0sZUFBZSxDQUFDO0FBQ3RDLE9BQU8sS0FBSyxJQUFJLE1BQU0sZUFBZSxDQUFDO0FBQ3RDLE9BQU8sS0FBSyxXQUFXLE1BQU0sdUJBQXVCLENBQUM7QUFDckQsT0FBTyxLQUFLLE9BQU8sTUFBTSxrQkFBa0IsQ0FBQztBQUU1QyxPQUFPLEVBQUUsa0JBQWtCLEVBQUUsbUJBQW1CLEVBQUUsaUJBQWlCLEVBQUUsc0JBQXNCLEVBQUUsa0JBQWtCLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUV4SSxPQUFPLEVBQUUsMkJBQTJCLEVBQUUsTUFBTSxlQUFlLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBHRU5FUkFURUQgYnkgcHJvamVuIHdhdGNoIC0gRE8gTk9UIEVESVQuXG4vLyBSZWdlbmVyYXRlZCBmcm9tIHRoZSBleHBvcnRpbmcgbW9kdWxlcyBpbiAuL3NyYy5cbi8vIEhhbmQgZWRpdHMgYXJlIG92ZXJ3cml0dGVuIG9uIHRoZSBuZXh0IHdhdGNoOyB0aGlzIGZpbGUgaXMgcmVhZC1vbmx5LlxuXG5leHBvcnQgKiBhcyBiaW4gZnJvbSBcIi4vc3JjL2Jpbi50c1wiO1xuZXhwb3J0ICogYXMgYnJhbmQgZnJvbSBcIi4vc3JjL2JyYW5kLnRzXCI7XG5leHBvcnQgKiBhcyBleGVjIGZyb20gXCIuL3NyYy9leGVjLnRzXCI7XG5leHBvcnQgKiBhcyBmaWxlIGZyb20gXCIuL3NyYy9maWxlLnRzXCI7XG5leHBvcnQgKiBhcyBwcm9jZXNzTG9jayBmcm9tIFwiLi9zcmMvcHJvY2Vzcy1sb2NrLnRzXCI7XG5leHBvcnQgKiBhcyBwcm9qZWN0IGZyb20gXCIuL3NyYy9wcm9qZWN0LnRzXCI7XG5leHBvcnQgdHlwZSB7IEJpbkNvbnRleHQsIEJpblNlbGVjdGlvbkNvbnRleHQsIEJpblNlbGVjdG9yLCBCaW5WZXJzaW9uT3V0cHV0LCBCaW5WZXJzaW9uUGFyc2VyLCBCaW5PcHRpb25zLCBCaW5VcmwgfSBmcm9tIFwiLi9zcmMvYmluLnRzXCI7XG5leHBvcnQgeyBCcmFuZENvbnRleHRTY2hlbWEsIGRlZmF1bHRCcmFuZENvbnRleHQsIHBhcnNlQnJhbmRDb250ZXh0LCBicmFuZENvbnRleHRKc29uU2NoZW1hLCBicmFuZENvbnRleHRQcm9tcHQgfSBmcm9tIFwiLi9zcmMvYnJhbmQudHNcIjtcbmV4cG9ydCB0eXBlIHsgQnJhbmRDb250ZXh0LCBCcmFuZENvbnRleHRJbnB1dCB9IGZyb20gXCIuL3NyYy9icmFuZC50c1wiO1xuZXhwb3J0IHsgQ09NTUFORF9OT1RfRk9VTkRfRVhJVF9DT0RFIH0gZnJvbSBcIi4vc3JjL2V4ZWMudHNcIjtcbmV4cG9ydCB0eXBlIHsgRXhlY1N0ZGlvLCBMaW5lSGFuZGxlciwgU3RkaW9PcHRpb24sIEV4ZWNSZXN1bHQsIENoaWxkUHJvY2Vzc1Jlc3VsdCwgRXhlY09wdGlvbnMsIFN5bmNFeGVjU3RkaW8sIFN5bmNFeGVjT3B0aW9ucywgU3Bhd25BcmdzIH0gZnJvbSBcIi4vc3JjL2V4ZWMudHNcIjtcbmV4cG9ydCB0eXBlIHsgUHJvamVjdENvbnRleHQgfSBmcm9tIFwiLi9zcmMvcHJvamVjdC50c1wiO1xuIl19
13
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwyQ0FBMkM7QUFDM0MsbURBQW1EO0FBQ25ELHdFQUF3RTtBQUV4RSxPQUFPLEtBQUssR0FBRyxNQUFNLGNBQWMsQ0FBQztBQUNwQyxPQUFPLEtBQUssS0FBSyxNQUFNLGdCQUFnQixDQUFDO0FBQ3hDLE9BQU8sS0FBSyxJQUFJLE1BQU0sZUFBZSxDQUFDO0FBQ3RDLE9BQU8sS0FBSyxJQUFJLE1BQU0sZUFBZSxDQUFDO0FBQ3RDLE9BQU8sS0FBSyxRQUFRLE1BQU0sb0JBQW9CLENBQUM7QUFDL0MsT0FBTyxLQUFLLFdBQVcsTUFBTSx1QkFBdUIsQ0FBQztBQUNyRCxPQUFPLEtBQUssT0FBTyxNQUFNLGtCQUFrQixDQUFDO0FBRTVDLE9BQU8sRUFBRSxrQkFBa0IsRUFBRSxtQkFBbUIsRUFBRSxpQkFBaUIsRUFBRSxzQkFBc0IsRUFBRSxrQkFBa0IsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBRXhJLE9BQU8sRUFBRSwyQkFBMkIsRUFBRSxNQUFNLGVBQWUsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8vIEdFTkVSQVRFRCBieSBwcm9qZW4gd2F0Y2ggLSBETyBOT1QgRURJVC5cbi8vIFJlZ2VuZXJhdGVkIGZyb20gdGhlIGV4cG9ydGluZyBtb2R1bGVzIGluIC4vc3JjLlxuLy8gSGFuZCBlZGl0cyBhcmUgb3ZlcndyaXR0ZW4gb24gdGhlIG5leHQgd2F0Y2g7IHRoaXMgZmlsZSBpcyByZWFkLW9ubHkuXG5cbmV4cG9ydCAqIGFzIGJpbiBmcm9tIFwiLi9zcmMvYmluLnRzXCI7XG5leHBvcnQgKiBhcyBicmFuZCBmcm9tIFwiLi9zcmMvYnJhbmQudHNcIjtcbmV4cG9ydCAqIGFzIGV4ZWMgZnJvbSBcIi4vc3JjL2V4ZWMudHNcIjtcbmV4cG9ydCAqIGFzIGZpbGUgZnJvbSBcIi4vc3JjL2ZpbGUudHNcIjtcbmV4cG9ydCAqIGFzIGZpbGVMb2NrIGZyb20gXCIuL3NyYy9maWxlLWxvY2sudHNcIjtcbmV4cG9ydCAqIGFzIHByb2Nlc3NMb2NrIGZyb20gXCIuL3NyYy9wcm9jZXNzLWxvY2sudHNcIjtcbmV4cG9ydCAqIGFzIHByb2plY3QgZnJvbSBcIi4vc3JjL3Byb2plY3QudHNcIjtcbmV4cG9ydCB0eXBlIHsgQmluQ29udGV4dCwgQmluU2VsZWN0aW9uQ29udGV4dCwgQmluU2VsZWN0b3IsIEJpblZlcnNpb25PdXRwdXQsIEJpblZlcnNpb25QYXJzZXIsIEJpbk9wdGlvbnMsIEJpblVybCB9IGZyb20gXCIuL3NyYy9iaW4udHNcIjtcbmV4cG9ydCB7IEJyYW5kQ29udGV4dFNjaGVtYSwgZGVmYXVsdEJyYW5kQ29udGV4dCwgcGFyc2VCcmFuZENvbnRleHQsIGJyYW5kQ29udGV4dEpzb25TY2hlbWEsIGJyYW5kQ29udGV4dFByb21wdCB9IGZyb20gXCIuL3NyYy9icmFuZC50c1wiO1xuZXhwb3J0IHR5cGUgeyBCcmFuZENvbnRleHQsIEJyYW5kQ29udGV4dElucHV0IH0gZnJvbSBcIi4vc3JjL2JyYW5kLnRzXCI7XG5leHBvcnQgeyBDT01NQU5EX05PVF9GT1VORF9FWElUX0NPREUgfSBmcm9tIFwiLi9zcmMvZXhlYy50c1wiO1xuZXhwb3J0IHR5cGUgeyBFeGVjU3RkaW8sIExpbmVIYW5kbGVyLCBTdGRpb09wdGlvbiwgRXhlY1Jlc3VsdCwgQ2hpbGRQcm9jZXNzUmVzdWx0LCBFeGVjT3B0aW9ucywgU3luY0V4ZWNTdGRpbywgU3luY0V4ZWNPcHRpb25zLCBTcGF3bkFyZ3MgfSBmcm9tIFwiLi9zcmMvZXhlYy50c1wiO1xuZXhwb3J0IHR5cGUgeyBGaWxlTG9ja0JhY2tlbmQsIEZpbGVMb2NrQWNxdWlzaXRpb24sIEZpbGVMb2NrT3B0aW9ucyB9IGZyb20gXCIuL3NyYy9maWxlLWxvY2sudHNcIjtcbmV4cG9ydCB0eXBlIHsgUHJvamVjdENvbnRleHQgfSBmcm9tIFwiLi9zcmMvcHJvamVjdC50c1wiO1xuIl19
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Cascading cross-process mutual exclusion via a lockfile.
3
+ *
4
+ * Serializes concurrent *processes* (two `bun run demo` shells, a CLI beside a
5
+ * server) on the same key. For in-process / worker-thread exclusion use
6
+ * {@link withProcessLock} from `./process-lock.ts` instead.
7
+ *
8
+ * Backends, in order:
9
+ *
10
+ * 1. **flock** — `flock(2)` via Bun FFI on Unix when `bun:ffi` can load libc.
11
+ * Not available on Windows, and not available under plain Node (no FFI).
12
+ * Kernel releases the lock when the fd closes (including process death).
13
+ * 2. **file** — atomic lock-directory creation. This is the strategy used by
14
+ * `proper-lockfile`: `mkdir` is atomic on Windows, Unix, and network file
15
+ * systems where `open(..., "wx")` may not be reliable. Stale detection is
16
+ * always on (fixed {@link STALE_MS} + heartbeat) so a crashed holder on this
17
+ * backend can be reclaimed.
18
+ *
19
+ * The first backend that can be *initialized* is used for the whole call. A busy
20
+ * lock waits; an unavailable backend falls through to the next. Callers may only
21
+ * bound how long to wait ({@link FileLockOptions.timeoutMs}); stale timing is
22
+ * not configurable so every holder and waiter agrees.
23
+ *
24
+ * @module
25
+ */
26
+ /** Backends {@link withFileLock} can attempt, in cascade order. */
27
+ export type FileLockBackend = "flock" | "file";
28
+ /** Result metadata when a caller wants to know which backend ran. */
29
+ export interface FileLockAcquisition {
30
+ backend: FileLockBackend;
31
+ }
32
+ export interface FileLockOptions {
33
+ /**
34
+ * Directory for lockfiles. Defaults to `$TMPDIR/dbx-tools-locks`.
35
+ */
36
+ dir?: string;
37
+ /** Override the cascade. Defaults to `flock` → `file`. */
38
+ backends?: readonly FileLockBackend[];
39
+ /**
40
+ * Stop waiting and throw after this many milliseconds. Omit to poll forever.
41
+ * This is the only wait-loop knob; stale reclaim timing is fixed.
42
+ */
43
+ timeoutMs?: number;
44
+ /** Invoked once the backend for this call has been chosen. */
45
+ onAcquire?: (acquisition: FileLockAcquisition) => void;
46
+ }
47
+ /**
48
+ * Run `fn` while holding a cross-process lock named by `key`.
49
+ *
50
+ * `key` is canonicalized with `object.toStableKey`, the same identity rule as
51
+ * process and Postgres advisory locks.
52
+ *
53
+ * @example
54
+ * await withFileLock(["cache", "appkit"], async () => {
55
+ * await migrate();
56
+ * });
57
+ */
58
+ export declare function withFileLock<T>(key: unknown, fn: () => T | Promise<T>, options?: FileLockOptions): Promise<T>;
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Cascading cross-process mutual exclusion via a lockfile.
3
+ *
4
+ * Serializes concurrent *processes* (two `bun run demo` shells, a CLI beside a
5
+ * server) on the same key. For in-process / worker-thread exclusion use
6
+ * {@link withProcessLock} from `./process-lock.ts` instead.
7
+ *
8
+ * Backends, in order:
9
+ *
10
+ * 1. **flock** — `flock(2)` via Bun FFI on Unix when `bun:ffi` can load libc.
11
+ * Not available on Windows, and not available under plain Node (no FFI).
12
+ * Kernel releases the lock when the fd closes (including process death).
13
+ * 2. **file** — atomic lock-directory creation. This is the strategy used by
14
+ * `proper-lockfile`: `mkdir` is atomic on Windows, Unix, and network file
15
+ * systems where `open(..., "wx")` may not be reliable. Stale detection is
16
+ * always on (fixed {@link STALE_MS} + heartbeat) so a crashed holder on this
17
+ * backend can be reclaimed.
18
+ *
19
+ * The first backend that can be *initialized* is used for the whole call. A busy
20
+ * lock waits; an unavailable backend falls through to the next. Callers may only
21
+ * bound how long to wait ({@link FileLockOptions.timeoutMs}); stale timing is
22
+ * not configurable so every holder and waiter agrees.
23
+ *
24
+ * @module
25
+ */
26
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
27
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
28
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
29
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
30
+ });
31
+ }
32
+ return path;
33
+ };
34
+ import { mkdir, open, rm, stat, utimes } from "node:fs/promises";
35
+ import { tmpdir } from "node:os";
36
+ import { dirname, join } from "node:path";
37
+ import { async, error, functionModule, hash, log, object } from "@dbx-tools/shared-core";
38
+ const logger = log.logger("core:file-lock");
39
+ const DEFAULT_BACKENDS = ["flock", "file"];
40
+ /** Poll interval while waiting for a contended OS lock. */
41
+ const POLL_MS = 50;
42
+ /**
43
+ * Age after which an unrefreshed lock directory may be removed.
44
+ * Matches `proper-lockfile`'s default. Not caller-configurable: every process
45
+ * must share the same threshold or a live holder can be mistaken for stale.
46
+ */
47
+ const STALE_MS = object.toNumber(process.env.FILE_LOCK_STALE_MS) ?? 10_000;
48
+ /** Heartbeat interval for refreshing lock-directory mtime (`STALE_MS / 2`). */
49
+ const UPDATE_MS = object.toNumber(process.env.FILE_LOCK_UPDATE_MS) ?? STALE_MS / 2;
50
+ /** `flock(2)` operation bits (Linux / macOS / BSD). */
51
+ const LOCK_EX = 2;
52
+ const LOCK_NB = 4;
53
+ const LOCK_UN = 8;
54
+ /**
55
+ * Run `fn` while holding a cross-process lock named by `key`.
56
+ *
57
+ * `key` is canonicalized with `object.toStableKey`, the same identity rule as
58
+ * process and Postgres advisory locks.
59
+ *
60
+ * @example
61
+ * await withFileLock(["cache", "appkit"], async () => {
62
+ * await migrate();
63
+ * });
64
+ */
65
+ export async function withFileLock(key, fn, options = {}) {
66
+ if (options.timeoutMs !== undefined && options.timeoutMs < 0) {
67
+ throw new TypeError("timeoutMs must be non-negative");
68
+ }
69
+ const id = lockId(key);
70
+ const backends = options.backends ?? DEFAULT_BACKENDS;
71
+ const dir = options.dir ?? join(tmpdir(), "dbx-tools-locks");
72
+ const deadline = options.timeoutMs === undefined ? undefined : Date.now() + options.timeoutMs;
73
+ for (const backend of backends) {
74
+ switch (backend) {
75
+ case "flock": {
76
+ const lockPath = join(dir, `${id}.flock`);
77
+ const flock = await resolveFlock();
78
+ if (!flock) {
79
+ logger.debug("lock backend unavailable", { backend, key: id });
80
+ continue;
81
+ }
82
+ logger.debug("acquiring lock", { backend, key: id, path: lockPath });
83
+ options.onAcquire?.({ backend });
84
+ return holdFlock(lockPath, flock, deadline, fn);
85
+ }
86
+ case "file": {
87
+ const lockPath = join(dir, `${id}.lock`);
88
+ logger.debug("acquiring lock", { backend, key: id, path: lockPath });
89
+ options.onAcquire?.({ backend });
90
+ return holdLockDirectory(lockPath, deadline, fn);
91
+ }
92
+ default: {
93
+ const _exhaustive = backend;
94
+ throw new Error(`unknown file-lock backend: ${String(_exhaustive)}`);
95
+ }
96
+ }
97
+ }
98
+ throw new Error("withFileLock: no lock backend available");
99
+ }
100
+ /** Canonical filesystem-safe id for a lock key. */
101
+ function lockId(key) {
102
+ const stable = object
103
+ .toOneOrMany(key)
104
+ .map((part) => object.toStableKey(part))
105
+ .join("\u0000");
106
+ // Short digest so paths stay well under OS limits; collisions are fine — they
107
+ // only merge critical sections that already shared a key string.
108
+ return hash.fnvHash(stable);
109
+ }
110
+ /**
111
+ * Resolve `flock(2)` through Bun FFI when possible.
112
+ *
113
+ * Memoized: the FFI import + libc `dlopen` run once per process. Returns
114
+ * `undefined` on Windows, under plain Node, or when libc cannot be loaded —
115
+ * callers fall through to the next backend. A miss is cached too, so a process
116
+ * that cannot flock never retries the load.
117
+ */
118
+ const resolveFlock = functionModule.memoize(async () => {
119
+ if (process.platform === "win32")
120
+ return undefined;
121
+ if (!process.versions.bun)
122
+ return undefined;
123
+ try {
124
+ // Dynamic specifier so `tsc` (no `@types` for `bun:ffi`) does not resolve it;
125
+ // the import only succeeds under Bun at runtime.
126
+ const specifier = "bun:ffi";
127
+ const ffi = (await import(__rewriteRelativeImportExtension(specifier)));
128
+ const libPath = process.platform === "darwin" ? "libSystem.B.dylib" : `libc.${ffi.suffix}`;
129
+ const lib = ffi.dlopen(libPath, {
130
+ flock: { args: ["i32", "i32"], returns: "i32" },
131
+ });
132
+ return lib.symbols.flock;
133
+ }
134
+ catch (cause) {
135
+ logger.debug("flock FFI unavailable", { error: error.errorMessage(cause) });
136
+ return undefined;
137
+ }
138
+ });
139
+ async function holdFlock(lockPath, flock, deadline, fn) {
140
+ await ensureParentDir(lockPath);
141
+ const handle = await open(lockPath, "a+");
142
+ try {
143
+ await waitForFlock(handle.fd, flock, lockPath, deadline);
144
+ try {
145
+ return await fn();
146
+ }
147
+ finally {
148
+ flock(handle.fd, LOCK_UN);
149
+ }
150
+ }
151
+ finally {
152
+ await handle.close().catch(() => { });
153
+ }
154
+ }
155
+ async function waitForFlock(fd, flock, lockPath, deadline) {
156
+ for (;;) {
157
+ const rc = flock(fd, LOCK_EX | LOCK_NB);
158
+ if (rc === 0)
159
+ return;
160
+ assertBeforeDeadline(lockPath, deadline);
161
+ await async.sleep(POLL_MS);
162
+ }
163
+ }
164
+ /**
165
+ * Atomic lock-directory creation — the portable / Windows path.
166
+ *
167
+ * `mkdir` is the same primitive used by `proper-lockfile`: it is atomic across
168
+ * supported local and network filesystems. The holder refreshes mtime on a fixed
169
+ * heartbeat so a live long-running lock is never reclaimed; a crashed holder's
170
+ * directory becomes reclaimable after {@link STALE_MS}.
171
+ */
172
+ async function holdLockDirectory(lockPath, deadline, fn) {
173
+ await ensureParentDir(lockPath);
174
+ await acquireLockDirectory(lockPath, deadline);
175
+ const stopHeartbeat = startHeartbeat(lockPath);
176
+ try {
177
+ return await fn();
178
+ }
179
+ finally {
180
+ stopHeartbeat();
181
+ await rm(lockPath, { recursive: true, force: true }).catch(() => { });
182
+ }
183
+ }
184
+ async function acquireLockDirectory(lockPath, deadline) {
185
+ for (;;) {
186
+ try {
187
+ await mkdir(lockPath);
188
+ return;
189
+ }
190
+ catch (cause) {
191
+ const err = cause;
192
+ if (err.code !== "EEXIST")
193
+ throw error.toError(cause);
194
+ await maybeReclaimStale(lockPath);
195
+ assertBeforeDeadline(lockPath, deadline);
196
+ await async.sleep(POLL_MS);
197
+ }
198
+ }
199
+ }
200
+ async function maybeReclaimStale(lockPath) {
201
+ try {
202
+ const { mtimeMs } = await stat(lockPath);
203
+ const age = Date.now() - mtimeMs;
204
+ if (age < STALE_MS)
205
+ return;
206
+ logger.debug("reclaiming stale lock directory", { path: lockPath, ageMs: age });
207
+ await rm(lockPath, { recursive: true, force: true });
208
+ }
209
+ catch (cause) {
210
+ const err = cause;
211
+ if (err.code !== "ENOENT")
212
+ throw error.toError(cause);
213
+ }
214
+ }
215
+ function startHeartbeat(lockPath) {
216
+ const timer = setInterval(() => {
217
+ const now = new Date();
218
+ void utimes(lockPath, now, now).catch((cause) => {
219
+ logger.warn("file lock heartbeat failed", {
220
+ path: lockPath,
221
+ error: error.errorMessage(cause),
222
+ });
223
+ });
224
+ }, UPDATE_MS);
225
+ timer.unref();
226
+ return () => clearInterval(timer);
227
+ }
228
+ function assertBeforeDeadline(lockPath, deadline) {
229
+ if (deadline !== undefined && Date.now() >= deadline) {
230
+ throw new Error(`Timed out waiting for file lock: ${lockPath}`);
231
+ }
232
+ }
233
+ async function ensureParentDir(lockPath) {
234
+ await mkdir(dirname(lockPath), { recursive: true });
235
+ }
236
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZmlsZS1sb2NrLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2ZpbGUtbG9jay50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBd0JHOzs7Ozs7Ozs7QUFFSCxPQUFPLEVBQUUsS0FBSyxFQUFFLElBQUksRUFBRSxFQUFFLEVBQUUsSUFBSSxFQUFFLE1BQU0sRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBQ2pFLE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxTQUFTLENBQUM7QUFDakMsT0FBTyxFQUFFLE9BQU8sRUFBRSxJQUFJLEVBQUUsTUFBTSxXQUFXLENBQUM7QUFFMUMsT0FBTyxFQUFFLEtBQUssRUFBRSxLQUFLLEVBQUUsY0FBYyxFQUFFLElBQUksRUFBRSxHQUFHLEVBQUUsTUFBTSxFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFFekYsTUFBTSxNQUFNLEdBQUcsR0FBRyxDQUFDLE1BQU0sQ0FBQyxnQkFBZ0IsQ0FBQyxDQUFDO0FBSzVDLE1BQU0sZ0JBQWdCLEdBQStCLENBQUMsT0FBTyxFQUFFLE1BQU0sQ0FBQyxDQUFDO0FBRXZFLDJEQUEyRDtBQUMzRCxNQUFNLE9BQU8sR0FBRyxFQUFFLENBQUM7QUFFbkI7Ozs7R0FJRztBQUNILE1BQU0sUUFBUSxHQUFHLE1BQU0sQ0FBQyxRQUFRLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxrQkFBa0IsQ0FBQyxJQUFJLE1BQU0sQ0FBQztBQUUzRSwrRUFBK0U7QUFDL0UsTUFBTSxTQUFTLEdBQUcsTUFBTSxDQUFDLFFBQVEsQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLG1CQUFtQixDQUFDLElBQUksUUFBUSxHQUFHLENBQUMsQ0FBQztBQUVuRix1REFBdUQ7QUFDdkQsTUFBTSxPQUFPLEdBQUcsQ0FBQyxDQUFDO0FBQ2xCLE1BQU0sT0FBTyxHQUFHLENBQUMsQ0FBQztBQUNsQixNQUFNLE9BQU8sR0FBRyxDQUFDLENBQUM7QUFpQ2xC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UyxvQkFBb0IsQ0FBQyxRQUFnQixFQUFFLFFBQTRCO0lBQzFFLElBQUksUUFBUSxLQUFLLFNBQVMsSUFBSSxJQUFJLENBQUMsR0FBRyxFQUFFLElBQUksUUFBUSxFQUFFLENBQUM7UUFDckQsTUFBTSxJQUFJLEtBQUssQ0FBQyxvQ0FBb0MsUUFBUSxFQUFFLENBQUMsQ0FBQztJQUNsRSxDQUFDO0FBQ0gsQ0FBQztBQUVELEtBQUssVUFBVSxlQUFlLENBQUMsUUFBZ0I7SUFDN0MsTUFBTSxLQUFLLENBQUMsT0FBTyxDQUFDLFFBQVEsQ0FBQyxFQUFFLEVBQUUsU0FBUyxFQUFFLElBQUksRUFBRSxDQUFDLENBQUM7QUFDdEQsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogQ2FzY2FkaW5nIGNyb3NzLXByb2Nlc3MgbXV0dWFsIGV4Y2x1c2lvbiB2aWEgYSBsb2NrZmlsZS5cbiAqXG4gKiBTZXJpYWxpemVzIGNvbmN1cnJlbnQgKnByb2Nlc3NlcyogKHR3byBgYnVuIHJ1biBkZW1vYCBzaGVsbHMsIGEgQ0xJIGJlc2lkZSBhXG4gKiBzZXJ2ZXIpIG9uIHRoZSBzYW1lIGtleS4gRm9yIGluLXByb2Nlc3MgLyB3b3JrZXItdGhyZWFkIGV4Y2x1c2lvbiB1c2VcbiAqIHtAbGluayB3aXRoUHJvY2Vzc0xvY2t9IGZyb20gYC4vcHJvY2Vzcy1sb2NrLnRzYCBpbnN0ZWFkLlxuICpcbiAqIEJhY2tlbmRzLCBpbiBvcmRlcjpcbiAqXG4gKiAxLiAqKmZsb2NrKiog4oCUIGBmbG9jaygyKWAgdmlhIEJ1biBGRkkgb24gVW5peCB3aGVuIGBidW46ZmZpYCBjYW4gbG9hZCBsaWJjLlxuICogICAgTm90IGF2YWlsYWJsZSBvbiBXaW5kb3dzLCBhbmQgbm90IGF2YWlsYWJsZSB1bmRlciBwbGFpbiBOb2RlIChubyBGRkkpLlxuICogICAgS2VybmVsIHJlbGVhc2VzIHRoZSBsb2NrIHdoZW4gdGhlIGZkIGNsb3NlcyAoaW5jbHVkaW5nIHByb2Nlc3MgZGVhdGgpLlxuICogMi4gKipmaWxlKiog4oCUIGF0b21pYyBsb2NrLWRpcmVjdG9yeSBjcmVhdGlvbi4gVGhpcyBpcyB0aGUgc3RyYXRlZ3kgdXNlZCBieVxuICogICAgYHByb3Blci1sb2NrZmlsZWA6IGBta2RpcmAgaXMgYXRvbWljIG9uIFdpbmRvd3MsIFVuaXgsIGFuZCBuZXR3b3JrIGZpbGVcbiAqICAgIHN5c3RlbXMgd2hlcmUgYG9wZW4oLi4uLCBcInd4XCIpYCBtYXkgbm90IGJlIHJlbGlhYmxlLiBTdGFsZSBkZXRlY3Rpb24gaXNcbiAqICAgIGFsd2F5cyBvbiAoZml4ZWQge0BsaW5rIFNUQUxFX01TfSArIGhlYXJ0YmVhdCkgc28gYSBjcmFzaGVkIGhvbGRlciBvbiB0aGlzXG4gKiAgICBiYWNrZW5kIGNhbiBiZSByZWNsYWltZWQuXG4gKlxuICogVGhlIGZpcnN0IGJhY2tlbmQgdGhhdCBjYW4gYmUgKmluaXRpYWxpemVkKiBpcyB1c2VkIGZvciB0aGUgd2hvbGUgY2FsbC4gQSBidXN5XG4gKiBsb2NrIHdhaXRzOyBhbiB1bmF2YWlsYWJsZSBiYWNrZW5kIGZhbGxzIHRocm91Z2ggdG8gdGhlIG5leHQuIENhbGxlcnMgbWF5IG9ubHlcbiAqIGJvdW5kIGhvdyBsb25nIHRvIHdhaXQgKHtAbGluayBGaWxlTG9ja09wdGlvbnMudGltZW91dE1zfSk7IHN0YWxlIHRpbWluZyBpc1xuICogbm90IGNvbmZpZ3VyYWJsZSBzbyBldmVyeSBob2xkZXIgYW5kIHdhaXRlciBhZ3JlZXMuXG4gKlxuICogQG1vZHVsZVxuICovXG5cbmltcG9ydCB7IG1rZGlyLCBvcGVuLCBybSwgc3RhdCwgdXRpbWVzIH0gZnJvbSBcIm5vZGU6ZnMvcHJvbWlzZXNcIjtcbmltcG9ydCB7IHRtcGRpciB9IGZyb20gXCJub2RlOm9zXCI7XG5pbXBvcnQgeyBkaXJuYW1lLCBqb2luIH0gZnJvbSBcIm5vZGU6cGF0aFwiO1xuXG5pbXBvcnQgeyBhc3luYywgZXJyb3IsIGZ1bmN0aW9uTW9kdWxlLCBoYXNoLCBsb2csIG9iamVjdCB9IGZyb20gXCJAZGJ4LXRvb2xzL3NoYXJlZC1jb3JlXCI7XG5cbmNvbnN0IGxvZ2dlciA9IGxvZy5sb2dnZXIoXCJjb3JlOmZpbGUtbG9ja1wiKTtcblxuLyoqIEJhY2tlbmRzIHtAbGluayB3aXRoRmlsZUxvY2t9IGNhbiBhdHRlbXB0LCBpbiBjYXNjYWRlIG9yZGVyLiAqL1xuZXhwb3J0IHR5cGUgRmlsZUxvY2tCYWNrZW5kID0gXCJmbG9ja1wiIHwgXCJmaWxlXCI7XG5cbmNvbnN0IERFRkFVTFRfQkFDS0VORFM6IHJlYWRvbmx5IEZpbGVMb2NrQmFja2VuZFtdID0gW1wiZmxvY2tcIiwgXCJmaWxlXCJdO1xuXG4vKiogUG9sbCBpbnRlcnZhbCB3aGlsZSB3YWl0aW5nIGZvciBhIGNvbnRlbmRlZCBPUyBsb2NrLiAqL1xuY29uc3QgUE9MTF9NUyA9IDUwO1xuXG4vKipcbiAqIEFnZSBhZnRlciB3aGljaCBhbiB1bnJlZnJlc2hlZCBsb2NrIGRpcmVjdG9yeSBtYXkgYmUgcmVtb3ZlZC5cbiAqIE1hdGNoZXMgYHByb3Blci1sb2NrZmlsZWAncyBkZWZhdWx0LiBOb3QgY2FsbGVyLWNvbmZpZ3VyYWJsZTogZXZlcnkgcHJvY2Vzc1xuICogbXVzdCBzaGFyZSB0aGUgc2FtZSB0aHJlc2hvbGQgb3IgYSBsaXZlIGhvbGRlciBjYW4gYmUgbWlzdGFrZW4gZm9yIHN0YWxlLlxuICovXG5jb25zdCBTVEFMRV9NUyA9IG9iamVjdC50b051bWJlcihwcm9jZXNzLmVudi5GSUxFX0xPQ0tfU1RBTEVfTVMpID8/IDEwXzAwMDtcblxuLyoqIEhlYXJ0YmVhdCBpbnRlcnZhbCBmb3IgcmVmcmVzaGluZyBsb2NrLWRpcmVjdG9yeSBtdGltZSAoYFNUQUxFX01TIC8gMmApLiAqL1xuY29uc3QgVVBEQVRFX01TID0gb2JqZWN0LnRvTnVtYmVyKHByb2Nlc3MuZW52LkZJTEVfTE9DS19VUERBVEVfTVMpID8/IFNUQUxFX01TIC8gMjtcblxuLyoqIGBmbG9jaygyKWAgb3BlcmF0aW9uIGJpdHMgKExpbnV4IC8gbWFjT1MgLyBCU0QpLiAqL1xuY29uc3QgTE9DS19FWCA9IDI7XG5jb25zdCBMT0NLX05CID0gNDtcbmNvbnN0IExPQ0tfVU4gPSA4O1xuXG4vKiogUmVzdWx0IG1ldGFkYXRhIHdoZW4gYSBjYWxsZXIgd2FudHMgdG8ga25vdyB3aGljaCBiYWNrZW5kIHJhbi4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgRmlsZUxvY2tBY3F1aXNpdGlvbiB7XG4gIGJhY2tlbmQ6IEZpbGVMb2NrQmFja2VuZDtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBGaWxlTG9ja09wdGlvbnMge1xuICAvKipcbiAgICogRGlyZWN0b3J5IGZvciBsb2NrZmlsZXMuIERlZmF1bHRzIHRvIGAkVE1QRElSL2RieC10b29scy1sb2Nrc2AuXG4gICAqL1xuICBkaXI/OiBzdHJpbmc7XG4gIC8qKiBPdmVycmlkZSB0aGUgY2FzY2FkZS4gRGVmYXVsdHMgdG8gYGZsb2NrYCDihpIgYGZpbGVgLiAqL1xuICBiYWNrZW5kcz86IHJlYWRvbmx5IEZpbGVMb2NrQmFja2VuZFtdO1xuICAvKipcbiAgICogU3RvcCB3YWl0aW5nIGFuZCB0aHJvdyBhZnRlciB0aGlzIG1hbnkgbWlsbGlzZWNvbmRzLiBPbWl0IHRvIHBvbGwgZm9yZXZlci5cbiAgICogVGhpcyBpcyB0aGUgb25seSB3YWl0LWxvb3Aga25vYjsgc3RhbGUgcmVjbGFpbSB0aW1pbmcgaXMgZml4ZWQuXG4gICAqL1xuICB0aW1lb3V0TXM/OiBudW1iZXI7XG4gIC8qKiBJbnZva2VkIG9uY2UgdGhlIGJhY2tlbmQgZm9yIHRoaXMgY2FsbCBoYXMgYmVlbiBjaG9zZW4uICovXG4gIG9uQWNxdWlyZT86IChhY3F1aXNpdGlvbjogRmlsZUxvY2tBY3F1aXNpdGlvbikgPT4gdm9pZDtcbn1cblxudHlwZSBGbG9ja0ZuID0gKGZkOiBudW1iZXIsIG9wZXJhdGlvbjogbnVtYmVyKSA9PiBudW1iZXI7XG5cbmludGVyZmFjZSBCdW5GZmlNb2R1bGUge1xuICBkbG9wZW46IChcbiAgICBwYXRoOiBzdHJpbmcsXG4gICAgc3ltYm9sczogUmVjb3JkPHN0cmluZywgeyBhcmdzOiBzdHJpbmdbXTsgcmV0dXJuczogc3RyaW5nIH0+LFxuICApID0+IHsgc3ltYm9sczogeyBmbG9jazogRmxvY2tGbiB9IH07XG4gIHN1ZmZpeDogc3RyaW5nO1xufVxuXG4vKipcbiAqIFJ1biBgZm5gIHdoaWxlIGhvbGRpbmcgYSBjcm9zcy1wcm9jZXNzIGxvY2sgbmFtZWQgYnkgYGtleWAuXG4gKlxuICogYGtleWAgaXMgY2Fub25pY2FsaXplZCB3aXRoIGBvYmplY3QudG9TdGFibGVLZXlgLCB0aGUgc2FtZSBpZGVudGl0eSBydWxlIGFzXG4gKiBwcm9jZXNzIGFuZCBQb3N0Z3JlcyBhZHZpc29yeSBsb2Nrcy5cbiAqXG4gKiBAZXhhbXBsZVxuICogYXdhaXQgd2l0aEZpbGVMb2NrKFtcImNhY2hlXCIsIFwiYXBwa2l0XCJdLCBhc3luYyAoKSA9PiB7XG4gKiAgIGF3YWl0IG1pZ3JhdGUoKTtcbiAqIH0pO1xuICovXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gd2l0aEZpbGVMb2NrPFQ+KFxuICBrZXk6IHVua25vd24sXG4gIGZuOiAoKSA9PiBUIHwgUHJvbWlzZTxUPixcbiAgb3B0aW9uczogRmlsZUxvY2tPcHRpb25zID0ge30sXG4pOiBQcm9taXNlPFQ+IHtcbiAgaWYgKG9wdGlvbnMudGltZW91dE1zICE9PSB1bmRlZmluZWQgJiYgb3B0aW9ucy50aW1lb3V0TXMgPCAwKSB7XG4gICAgdGhyb3cgbmV3IFR5cGVFcnJvcihcInRpbWVvdXRNcyBtdXN0IGJlIG5vbi1uZWdhdGl2ZVwiKTtcbiAgfVxuICBjb25zdCBpZCA9IGxvY2tJZChrZXkpO1xuICBjb25zdCBiYWNrZW5kcyA9IG9wdGlvbnMuYmFja2VuZHMgPz8gREVGQVVMVF9CQUNLRU5EUztcbiAgY29uc3QgZGlyID0gb3B0aW9ucy5kaXIgPz8gam9pbih0bXBkaXIoKSwgXCJkYngtdG9vbHMtbG9ja3NcIik7XG4gIGNvbnN0IGRlYWRsaW5lID0gb3B0aW9ucy50aW1lb3V0TXMgPT09IHVuZGVmaW5lZCA/IHVuZGVmaW5lZCA6IERhdGUubm93KCkgKyBvcHRpb25zLnRpbWVvdXRNcztcblxuICBmb3IgKGNvbnN0IGJhY2tlbmQgb2YgYmFja2VuZHMpIHtcbiAgICBzd2l0Y2ggKGJhY2tlbmQpIHtcbiAgICAgIGNhc2UgXCJmbG9ja1wiOiB7XG4gICAgICAgIGNvbnN0IGxvY2tQYXRoID0gam9pbihkaXIsIGAke2lkfS5mbG9ja2ApO1xuICAgICAgICBjb25zdCBmbG9jayA9IGF3YWl0IHJlc29sdmVGbG9jaygpO1xuICAgICAgICBpZiAoIWZsb2NrKSB7XG4gICAgICAgICAgbG9nZ2VyLmRlYnVnKFwibG9jayBiYWNrZW5kIHVuYXZhaWxhYmxlXCIsIHsgYmFja2VuZCwga2V5OiBpZCB9KTtcbiAgICAgICAgICBjb250aW51ZTtcbiAgICAgICAgfVxuICAgICAgICBsb2dnZXIuZGVidWcoXCJhY3F1aXJpbmcgbG9ja1wiLCB7IGJhY2tlbmQsIGtleTogaWQsIHBhdGg6IGxvY2tQYXRoIH0pO1xuICAgICAgICBvcHRpb25zLm9uQWNxdWlyZT8uKHsgYmFja2VuZCB9KTtcbiAgICAgICAgcmV0dXJuIGhvbGRGbG9jayhsb2NrUGF0aCwgZmxvY2ssIGRlYWRsaW5lLCBmbik7XG4gICAgICB9XG4gICAgICBjYXNlIFwiZmlsZVwiOiB7XG4gICAgICAgIGNvbnN0IGxvY2tQYXRoID0gam9pbihkaXIsIGAke2lkfS5sb2NrYCk7XG4gICAgICAgIGxvZ2dlci5kZWJ1ZyhcImFjcXVpcmluZyBsb2NrXCIsIHsgYmFja2VuZCwga2V5OiBpZCwgcGF0aDogbG9ja1BhdGggfSk7XG4gICAgICAgIG9wdGlvbnMub25BY3F1aXJlPy4oeyBiYWNrZW5kIH0pO1xuICAgICAgICByZXR1cm4gaG9sZExvY2tEaXJlY3RvcnkobG9ja1BhdGgsIGRlYWRsaW5lLCBmbik7XG4gICAgICB9XG4gICAgICBkZWZhdWx0OiB7XG4gICAgICAgIGNvbnN0IF9leGhhdXN0aXZlOiBuZXZlciA9IGJhY2tlbmQ7XG4gICAgICAgIHRocm93IG5ldyBFcnJvcihgdW5rbm93biBmaWxlLWxvY2sgYmFja2VuZDogJHtTdHJpbmcoX2V4aGF1c3RpdmUpfWApO1xuICAgICAgfVxuICAgIH1cbiAgfVxuXG4gIHRocm93IG5ldyBFcnJvcihcIndpdGhGaWxlTG9jazogbm8gbG9jayBiYWNrZW5kIGF2YWlsYWJsZVwiKTtcbn1cblxuLyoqIENhbm9uaWNhbCBmaWxlc3lzdGVtLXNhZmUgaWQgZm9yIGEgbG9jayBrZXkuICovXG5mdW5jdGlvbiBsb2NrSWQoa2V5OiB1bmtub3duKTogc3RyaW5nIHtcbiAgY29uc3Qgc3RhYmxlID0gb2JqZWN0XG4gICAgLnRvT25lT3JNYW55KGtleSlcbiAgICAubWFwKChwYXJ0KSA9PiBvYmplY3QudG9TdGFibGVLZXkocGFydCkpXG4gICAgLmpvaW4oXCJcXHUwMDAwXCIpO1xuICAvLyBTaG9ydCBkaWdlc3Qgc28gcGF0aHMgc3RheSB3ZWxsIHVuZGVyIE9TIGxpbWl0czsgY29sbGlzaW9ucyBhcmUgZmluZSDigJQgdGhleVxuICAvLyBvbmx5IG1lcmdlIGNyaXRpY2FsIHNlY3Rpb25zIHRoYXQgYWxyZWFkeSBzaGFyZWQgYSBrZXkgc3RyaW5nLlxuICByZXR1cm4gaGFzaC5mbnZIYXNoKHN0YWJsZSk7XG59XG5cbi8qKlxuICogUmVzb2x2ZSBgZmxvY2soMilgIHRocm91Z2ggQnVuIEZGSSB3aGVuIHBvc3NpYmxlLlxuICpcbiAqIE1lbW9pemVkOiB0aGUgRkZJIGltcG9ydCArIGxpYmMgYGRsb3BlbmAgcnVuIG9uY2UgcGVyIHByb2Nlc3MuIFJldHVybnNcbiAqIGB1bmRlZmluZWRgIG9uIFdpbmRvd3MsIHVuZGVyIHBsYWluIE5vZGUsIG9yIHdoZW4gbGliYyBjYW5ub3QgYmUgbG9hZGVkIOKAlFxuICogY2FsbGVycyBmYWxsIHRocm91Z2ggdG8gdGhlIG5leHQgYmFja2VuZC4gQSBtaXNzIGlzIGNhY2hlZCB0b28sIHNvIGEgcHJvY2Vzc1xuICogdGhhdCBjYW5ub3QgZmxvY2sgbmV2ZXIgcmV0cmllcyB0aGUgbG9hZC5cbiAqL1xuY29uc3QgcmVzb2x2ZUZsb2NrID0gZnVuY3Rpb25Nb2R1bGUubWVtb2l6ZShhc3luYyAoKTogUHJvbWlzZTxGbG9ja0ZuIHwgdW5kZWZpbmVkPiA9PiB7XG4gIGlmIChwcm9jZXNzLnBsYXRmb3JtID09PSBcIndpbjMyXCIpIHJldHVybiB1bmRlZmluZWQ7XG4gIGlmICghcHJvY2Vzcy52ZXJzaW9ucy5idW4pIHJldHVybiB1bmRlZmluZWQ7XG5cbiAgdHJ5IHtcbiAgICAvLyBEeW5hbWljIHNwZWNpZmllciBzbyBgdHNjYCAobm8gYEB0eXBlc2AgZm9yIGBidW46ZmZpYCkgZG9lcyBub3QgcmVzb2x2ZSBpdDtcbiAgICAvLyB0aGUgaW1wb3J0IG9ubHkgc3VjY2VlZHMgdW5kZXIgQnVuIGF0IHJ1bnRpbWUuXG4gICAgY29uc3Qgc3BlY2lmaWVyID0gXCJidW46ZmZpXCI7XG4gICAgY29uc3QgZmZpID0gKGF3YWl0IGltcG9ydChzcGVjaWZpZXIpKSBhcyB1bmtub3duIGFzIEJ1bkZmaU1vZHVsZTtcbiAgICBjb25zdCBsaWJQYXRoID1cbiAgICAgIHByb2Nlc3MucGxhdGZvcm0gPT09IFwiZGFyd2luXCIgPyBcImxpYlN5c3RlbS5CLmR5bGliXCIgOiBgbGliYy4ke2ZmaS5zdWZmaXh9YDtcbiAgICBjb25zdCBsaWIgPSBmZmkuZGxvcGVuKGxpYlBhdGgsIHtcbiAgICAgIGZsb2NrOiB7IGFyZ3M6IFtcImkzMlwiLCBcImkzMlwiXSwgcmV0dXJuczogXCJpMzJcIiB9LFxuICAgIH0pO1xuICAgIHJldHVybiBsaWIuc3ltYm9scy5mbG9jaztcbiAgfSBjYXRjaCAoY2F1c2UpIHtcbiAgICBsb2dnZXIuZGVidWcoXCJmbG9jayBGRkkgdW5hdmFpbGFibGVcIiwgeyBlcnJvcjogZXJyb3IuZXJyb3JNZXNzYWdlKGNhdXNlKSB9KTtcbiAgICByZXR1cm4gdW5kZWZpbmVkO1xuICB9XG59KTtcblxuYXN5bmMgZnVuY3Rpb24gaG9sZEZsb2NrPFQ+KFxuICBsb2NrUGF0aDogc3RyaW5nLFxuICBmbG9jazogRmxvY2tGbixcbiAgZGVhZGxpbmU6IG51bWJlciB8IHVuZGVmaW5lZCxcbiAgZm46ICgpID0+IFQgfCBQcm9taXNlPFQ+LFxuKTogUHJvbWlzZTxUPiB7XG4gIGF3YWl0IGVuc3VyZVBhcmVudERpcihsb2NrUGF0aCk7XG4gIGNvbnN0IGhhbmRsZSA9IGF3YWl0IG9wZW4obG9ja1BhdGgsIFwiYStcIik7XG4gIHRyeSB7XG4gICAgYXdhaXQgd2FpdEZvckZsb2NrKGhhbmRsZS5mZCwgZmxvY2ssIGxvY2tQYXRoLCBkZWFkbGluZSk7XG4gICAgdHJ5IHtcbiAgICAgIHJldHVybiBhd2FpdCBmbigpO1xuICAgIH0gZmluYWxseSB7XG4gICAgICBmbG9jayhoYW5kbGUuZmQsIExPQ0tfVU4pO1xuICAgIH1cbiAgfSBmaW5hbGx5IHtcbiAgICBhd2FpdCBoYW5kbGUuY2xvc2UoKS5jYXRjaCgoKSA9PiB7IH0pO1xuICB9XG59XG5cbmFzeW5jIGZ1bmN0aW9uIHdhaXRGb3JGbG9jayhcbiAgZmQ6IG51bWJlcixcbiAgZmxvY2s6IEZsb2NrRm4sXG4gIGxvY2tQYXRoOiBzdHJpbmcsXG4gIGRlYWRsaW5lOiBudW1iZXIgfCB1bmRlZmluZWQsXG4pOiBQcm9taXNlPHZvaWQ+IHtcbiAgZm9yICg7IDspIHtcbiAgICBjb25zdCByYyA9IGZsb2NrKGZkLCBMT0NLX0VYIHwgTE9DS19OQik7XG4gICAgaWYgKHJjID09PSAwKSByZXR1cm47XG4gICAgYXNzZXJ0QmVmb3JlRGVhZGxpbmUobG9ja1BhdGgsIGRlYWRsaW5lKTtcbiAgICBhd2FpdCBhc3luYy5zbGVlcChQT0xMX01TKTtcbiAgfVxufVxuXG4vKipcbiAqIEF0b21pYyBsb2NrLWRpcmVjdG9yeSBjcmVhdGlvbiDigJQgdGhlIHBvcnRhYmxlIC8gV2luZG93cyBwYXRoLlxuICpcbiAqIGBta2RpcmAgaXMgdGhlIHNhbWUgcHJpbWl0aXZlIHVzZWQgYnkgYHByb3Blci1sb2NrZmlsZWA6IGl0IGlzIGF0b21pYyBhY3Jvc3NcbiAqIHN1cHBvcnRlZCBsb2NhbCBhbmQgbmV0d29yayBmaWxlc3lzdGVtcy4gVGhlIGhvbGRlciByZWZyZXNoZXMgbXRpbWUgb24gYSBmaXhlZFxuICogaGVhcnRiZWF0IHNvIGEgbGl2ZSBsb25nLXJ1bm5pbmcgbG9jayBpcyBuZXZlciByZWNsYWltZWQ7IGEgY3Jhc2hlZCBob2xkZXInc1xuICogZGlyZWN0b3J5IGJlY29tZXMgcmVjbGFpbWFibGUgYWZ0ZXIge0BsaW5rIFNUQUxFX01TfS5cbiAqL1xuYXN5bmMgZnVuY3Rpb24gaG9sZExvY2tEaXJlY3Rvcnk8VD4oXG4gIGxvY2tQYXRoOiBzdHJpbmcsXG4gIGRlYWRsaW5lOiBudW1iZXIgfCB1bmRlZmluZWQsXG4gIGZuOiAoKSA9PiBUIHwgUHJvbWlzZTxUPixcbik6IFByb21pc2U8VD4ge1xuICBhd2FpdCBlbnN1cmVQYXJlbnREaXIobG9ja1BhdGgpO1xuICBhd2FpdCBhY3F1aXJlTG9ja0RpcmVjdG9yeShsb2NrUGF0aCwgZGVhZGxpbmUpO1xuICBjb25zdCBzdG9wSGVhcnRiZWF0ID0gc3RhcnRIZWFydGJlYXQobG9ja1BhdGgpO1xuICB0cnkge1xuICAgIHJldHVybiBhd2FpdCBmbigpO1xuICB9IGZpbmFsbHkge1xuICAgIHN0b3BIZWFydGJlYXQoKTtcbiAgICBhd2FpdCBybShsb2NrUGF0aCwgeyByZWN1cnNpdmU6IHRydWUsIGZvcmNlOiB0cnVlIH0pLmNhdGNoKCgpID0+IHsgfSk7XG4gIH1cbn1cblxuYXN5bmMgZnVuY3Rpb24gYWNxdWlyZUxvY2tEaXJlY3RvcnkoXG4gIGxvY2tQYXRoOiBzdHJpbmcsXG4gIGRlYWRsaW5lOiBudW1iZXIgfCB1bmRlZmluZWQsXG4pOiBQcm9taXNlPHZvaWQ+IHtcbiAgZm9yICg7IDspIHtcbiAgICB0cnkge1xuICAgICAgYXdhaXQgbWtkaXIobG9ja1BhdGgpO1xuICAgICAgcmV0dXJuO1xuICAgIH0gY2F0Y2ggKGNhdXNlKSB7XG4gICAgICBjb25zdCBlcnIgPSBjYXVzZSBhcyBOb2RlSlMuRXJybm9FeGNlcHRpb247XG4gICAgICBpZiAoZXJyLmNvZGUgIT09IFwiRUVYSVNUXCIpIHRocm93IGVycm9yLnRvRXJyb3IoY2F1c2UpO1xuICAgICAgYXdhaXQgbWF5YmVSZWNsYWltU3RhbGUobG9ja1BhdGgpO1xuICAgICAgYXNzZXJ0QmVmb3JlRGVhZGxpbmUobG9ja1BhdGgsIGRlYWRsaW5lKTtcbiAgICAgIGF3YWl0IGFzeW5jLnNsZWVwKFBPTExfTVMpO1xuICAgIH1cbiAgfVxufVxuXG5hc3luYyBmdW5jdGlvbiBtYXliZVJlY2xhaW1TdGFsZShsb2NrUGF0aDogc3RyaW5nKTogUHJvbWlzZTx2b2lkPiB7XG4gIHRyeSB7XG4gICAgY29uc3QgeyBtdGltZU1zIH0gPSBhd2FpdCBzdGF0KGxvY2tQYXRoKTtcbiAgICBjb25zdCBhZ2UgPSBEYXRlLm5vdygpIC0gbXRpbWVNcztcbiAgICBpZiAoYWdlIDwgU1RBTEVfTVMpIHJldHVybjtcbiAgICBsb2dnZXIuZGVidWcoXCJyZWNsYWltaW5nIHN0YWxlIGxvY2sgZGlyZWN0b3J5XCIsIHsgcGF0aDogbG9ja1BhdGgsIGFnZU1zOiBhZ2UgfSk7XG4gICAgYXdhaXQgcm0obG9ja1BhdGgsIHsgcmVjdXJzaXZlOiB0cnVlLCBmb3JjZTogdHJ1ZSB9KTtcbiAgfSBjYXRjaCAoY2F1c2UpIHtcbiAgICBjb25zdCBlcnIgPSBjYXVzZSBhcyBOb2RlSlMuRXJybm9FeGNlcHRpb247XG4gICAgaWYgKGVyci5jb2RlICE9PSBcIkVOT0VOVFwiKSB0aHJvdyBlcnJvci50b0Vycm9yKGNhdXNlKTtcbiAgfVxufVxuXG5mdW5jdGlvbiBzdGFydEhlYXJ0YmVhdChsb2NrUGF0aDogc3RyaW5nKTogKCkgPT4gdm9pZCB7XG4gIGNvbnN0IHRpbWVyID0gc2V0SW50ZXJ2YWwoKCkgPT4ge1xuICAgIGNvbnN0IG5vdyA9IG5ldyBEYXRlKCk7XG4gICAgdm9pZCB1dGltZXMobG9ja1BhdGgsIG5vdywgbm93KS5jYXRjaCgoY2F1c2UpID0+IHtcbiAgICAgIGxvZ2dlci53YXJuKFwiZmlsZSBsb2NrIGhlYXJ0YmVhdCBmYWlsZWRcIiwge1xuICAgICAgICBwYXRoOiBsb2NrUGF0aCxcbiAgICAgICAgZXJyb3I6IGVycm9yLmVycm9yTWVzc2FnZShjYXVzZSksXG4gICAgICB9KTtcbiAgICB9KTtcbiAgfSwgVVBEQVRFX01TKTtcbiAgdGltZXIudW5yZWYoKTtcbiAgcmV0dXJuICgpID0+IGNsZWFySW50ZXJ2YWwodGltZXIpO1xufVxuXG5mdW5jdGlvbiBhc3NlcnRCZWZvcmVEZWFkbGluZShsb2NrUGF0aDogc3RyaW5nLCBkZWFkbGluZTogbnVtYmVyIHwgdW5kZWZpbmVkKTogdm9pZCB7XG4gIGlmIChkZWFkbGluZSAhPT0gdW5kZWZpbmVkICYmIERhdGUubm93KCkgPj0gZGVhZGxpbmUpIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IoYFRpbWVkIG91dCB3YWl0aW5nIGZvciBmaWxlIGxvY2s6ICR7bG9ja1BhdGh9YCk7XG4gIH1cbn1cblxuYXN5bmMgZnVuY3Rpb24gZW5zdXJlUGFyZW50RGlyKGxvY2tQYXRoOiBzdHJpbmcpOiBQcm9taXNlPHZvaWQ+IHtcbiAgYXdhaXQgbWtkaXIoZGlybmFtZShsb2NrUGF0aCksIHsgcmVjdXJzaXZlOiB0cnVlIH0pO1xufVxuIl19
@@ -1 +1 @@
1
- {"root":["../src/bin.ts","../src/brand.ts","../src/exec.ts","../src/file.ts","../src/process-lock.ts","../src/project.ts","../index.ts"],"version":"5.9.3"}
1
+ {"root":["../src/bin.ts","../src/brand.ts","../src/exec.ts","../src/file-lock.ts","../src/file.ts","../src/process-lock.ts","../src/project.ts","../index.ts"],"version":"5.9.3"}
package/package.json CHANGED
@@ -23,7 +23,7 @@
23
23
  "typescript": "^5.9.3"
24
24
  },
25
25
  "dependencies": {
26
- "@dbx-tools/shared-core": "0.6.69",
26
+ "@dbx-tools/shared-core": "0.6.71",
27
27
  "extract-zip": "^2.0.1",
28
28
  "tar": "^7.5.22",
29
29
  "yaml": "^2.9.0"
@@ -42,7 +42,7 @@
42
42
  "./package.json": "./package.json"
43
43
  }
44
44
  },
45
- "version": "0.6.69",
45
+ "version": "0.6.71",
46
46
  "types": "./lib/index.d.ts",
47
47
  "type": "module",
48
48
  "exports": {
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Cascading cross-process mutual exclusion via a lockfile.
3
+ *
4
+ * Serializes concurrent *processes* (two `bun run demo` shells, a CLI beside a
5
+ * server) on the same key. For in-process / worker-thread exclusion use
6
+ * {@link withProcessLock} from `./process-lock.ts` instead.
7
+ *
8
+ * Backends, in order:
9
+ *
10
+ * 1. **flock** — `flock(2)` via Bun FFI on Unix when `bun:ffi` can load libc.
11
+ * Not available on Windows, and not available under plain Node (no FFI).
12
+ * Kernel releases the lock when the fd closes (including process death).
13
+ * 2. **file** — atomic lock-directory creation. This is the strategy used by
14
+ * `proper-lockfile`: `mkdir` is atomic on Windows, Unix, and network file
15
+ * systems where `open(..., "wx")` may not be reliable. Stale detection is
16
+ * always on (fixed {@link STALE_MS} + heartbeat) so a crashed holder on this
17
+ * backend can be reclaimed.
18
+ *
19
+ * The first backend that can be *initialized* is used for the whole call. A busy
20
+ * lock waits; an unavailable backend falls through to the next. Callers may only
21
+ * bound how long to wait ({@link FileLockOptions.timeoutMs}); stale timing is
22
+ * not configurable so every holder and waiter agrees.
23
+ *
24
+ * @module
25
+ */
26
+
27
+ import { mkdir, open, rm, stat, utimes } from "node:fs/promises";
28
+ import { tmpdir } from "node:os";
29
+ import { dirname, join } from "node:path";
30
+
31
+ import { async, error, functionModule, hash, log, object } from "@dbx-tools/shared-core";
32
+
33
+ const logger = log.logger("core:file-lock");
34
+
35
+ /** Backends {@link withFileLock} can attempt, in cascade order. */
36
+ export type FileLockBackend = "flock" | "file";
37
+
38
+ const DEFAULT_BACKENDS: readonly FileLockBackend[] = ["flock", "file"];
39
+
40
+ /** Poll interval while waiting for a contended OS lock. */
41
+ const POLL_MS = 50;
42
+
43
+ /**
44
+ * Age after which an unrefreshed lock directory may be removed.
45
+ * Matches `proper-lockfile`'s default. Not caller-configurable: every process
46
+ * must share the same threshold or a live holder can be mistaken for stale.
47
+ */
48
+ const STALE_MS = object.toNumber(process.env.FILE_LOCK_STALE_MS) ?? 10_000;
49
+
50
+ /** Heartbeat interval for refreshing lock-directory mtime (`STALE_MS / 2`). */
51
+ const UPDATE_MS = object.toNumber(process.env.FILE_LOCK_UPDATE_MS) ?? STALE_MS / 2;
52
+
53
+ /** `flock(2)` operation bits (Linux / macOS / BSD). */
54
+ const LOCK_EX = 2;
55
+ const LOCK_NB = 4;
56
+ const LOCK_UN = 8;
57
+
58
+ /** Result metadata when a caller wants to know which backend ran. */
59
+ export interface FileLockAcquisition {
60
+ backend: FileLockBackend;
61
+ }
62
+
63
+ export interface FileLockOptions {
64
+ /**
65
+ * Directory for lockfiles. Defaults to `$TMPDIR/dbx-tools-locks`.
66
+ */
67
+ dir?: string;
68
+ /** Override the cascade. Defaults to `flock` → `file`. */
69
+ backends?: readonly FileLockBackend[];
70
+ /**
71
+ * Stop waiting and throw after this many milliseconds. Omit to poll forever.
72
+ * This is the only wait-loop knob; stale reclaim timing is fixed.
73
+ */
74
+ timeoutMs?: number;
75
+ /** Invoked once the backend for this call has been chosen. */
76
+ onAcquire?: (acquisition: FileLockAcquisition) => void;
77
+ }
78
+
79
+ type FlockFn = (fd: number, operation: number) => number;
80
+
81
+ interface BunFfiModule {
82
+ dlopen: (
83
+ path: string,
84
+ symbols: Record<string, { args: string[]; returns: string }>,
85
+ ) => { symbols: { flock: FlockFn } };
86
+ suffix: string;
87
+ }
88
+
89
+ /**
90
+ * Run `fn` while holding a cross-process lock named by `key`.
91
+ *
92
+ * `key` is canonicalized with `object.toStableKey`, the same identity rule as
93
+ * process and Postgres advisory locks.
94
+ *
95
+ * @example
96
+ * await withFileLock(["cache", "appkit"], async () => {
97
+ * await migrate();
98
+ * });
99
+ */
100
+ export async function withFileLock<T>(
101
+ key: unknown,
102
+ fn: () => T | Promise<T>,
103
+ options: FileLockOptions = {},
104
+ ): Promise<T> {
105
+ if (options.timeoutMs !== undefined && options.timeoutMs < 0) {
106
+ throw new TypeError("timeoutMs must be non-negative");
107
+ }
108
+ const id = lockId(key);
109
+ const backends = options.backends ?? DEFAULT_BACKENDS;
110
+ const dir = options.dir ?? join(tmpdir(), "dbx-tools-locks");
111
+ const deadline = options.timeoutMs === undefined ? undefined : Date.now() + options.timeoutMs;
112
+
113
+ for (const backend of backends) {
114
+ switch (backend) {
115
+ case "flock": {
116
+ const lockPath = join(dir, `${id}.flock`);
117
+ const flock = await resolveFlock();
118
+ if (!flock) {
119
+ logger.debug("lock backend unavailable", { backend, key: id });
120
+ continue;
121
+ }
122
+ logger.debug("acquiring lock", { backend, key: id, path: lockPath });
123
+ options.onAcquire?.({ backend });
124
+ return holdFlock(lockPath, flock, deadline, fn);
125
+ }
126
+ case "file": {
127
+ const lockPath = join(dir, `${id}.lock`);
128
+ logger.debug("acquiring lock", { backend, key: id, path: lockPath });
129
+ options.onAcquire?.({ backend });
130
+ return holdLockDirectory(lockPath, deadline, fn);
131
+ }
132
+ default: {
133
+ const _exhaustive: never = backend;
134
+ throw new Error(`unknown file-lock backend: ${String(_exhaustive)}`);
135
+ }
136
+ }
137
+ }
138
+
139
+ throw new Error("withFileLock: no lock backend available");
140
+ }
141
+
142
+ /** Canonical filesystem-safe id for a lock key. */
143
+ function lockId(key: unknown): string {
144
+ const stable = object
145
+ .toOneOrMany(key)
146
+ .map((part) => object.toStableKey(part))
147
+ .join("\u0000");
148
+ // Short digest so paths stay well under OS limits; collisions are fine — they
149
+ // only merge critical sections that already shared a key string.
150
+ return hash.fnvHash(stable);
151
+ }
152
+
153
+ /**
154
+ * Resolve `flock(2)` through Bun FFI when possible.
155
+ *
156
+ * Memoized: the FFI import + libc `dlopen` run once per process. Returns
157
+ * `undefined` on Windows, under plain Node, or when libc cannot be loaded —
158
+ * callers fall through to the next backend. A miss is cached too, so a process
159
+ * that cannot flock never retries the load.
160
+ */
161
+ const resolveFlock = functionModule.memoize(async (): Promise<FlockFn | undefined> => {
162
+ if (process.platform === "win32") return undefined;
163
+ if (!process.versions.bun) return undefined;
164
+
165
+ try {
166
+ // Dynamic specifier so `tsc` (no `@types` for `bun:ffi`) does not resolve it;
167
+ // the import only succeeds under Bun at runtime.
168
+ const specifier = "bun:ffi";
169
+ const ffi = (await import(specifier)) as unknown as BunFfiModule;
170
+ const libPath =
171
+ process.platform === "darwin" ? "libSystem.B.dylib" : `libc.${ffi.suffix}`;
172
+ const lib = ffi.dlopen(libPath, {
173
+ flock: { args: ["i32", "i32"], returns: "i32" },
174
+ });
175
+ return lib.symbols.flock;
176
+ } catch (cause) {
177
+ logger.debug("flock FFI unavailable", { error: error.errorMessage(cause) });
178
+ return undefined;
179
+ }
180
+ });
181
+
182
+ async function holdFlock<T>(
183
+ lockPath: string,
184
+ flock: FlockFn,
185
+ deadline: number | undefined,
186
+ fn: () => T | Promise<T>,
187
+ ): Promise<T> {
188
+ await ensureParentDir(lockPath);
189
+ const handle = await open(lockPath, "a+");
190
+ try {
191
+ await waitForFlock(handle.fd, flock, lockPath, deadline);
192
+ try {
193
+ return await fn();
194
+ } finally {
195
+ flock(handle.fd, LOCK_UN);
196
+ }
197
+ } finally {
198
+ await handle.close().catch(() => { });
199
+ }
200
+ }
201
+
202
+ async function waitForFlock(
203
+ fd: number,
204
+ flock: FlockFn,
205
+ lockPath: string,
206
+ deadline: number | undefined,
207
+ ): Promise<void> {
208
+ for (; ;) {
209
+ const rc = flock(fd, LOCK_EX | LOCK_NB);
210
+ if (rc === 0) return;
211
+ assertBeforeDeadline(lockPath, deadline);
212
+ await async.sleep(POLL_MS);
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Atomic lock-directory creation — the portable / Windows path.
218
+ *
219
+ * `mkdir` is the same primitive used by `proper-lockfile`: it is atomic across
220
+ * supported local and network filesystems. The holder refreshes mtime on a fixed
221
+ * heartbeat so a live long-running lock is never reclaimed; a crashed holder's
222
+ * directory becomes reclaimable after {@link STALE_MS}.
223
+ */
224
+ async function holdLockDirectory<T>(
225
+ lockPath: string,
226
+ deadline: number | undefined,
227
+ fn: () => T | Promise<T>,
228
+ ): Promise<T> {
229
+ await ensureParentDir(lockPath);
230
+ await acquireLockDirectory(lockPath, deadline);
231
+ const stopHeartbeat = startHeartbeat(lockPath);
232
+ try {
233
+ return await fn();
234
+ } finally {
235
+ stopHeartbeat();
236
+ await rm(lockPath, { recursive: true, force: true }).catch(() => { });
237
+ }
238
+ }
239
+
240
+ async function acquireLockDirectory(
241
+ lockPath: string,
242
+ deadline: number | undefined,
243
+ ): Promise<void> {
244
+ for (; ;) {
245
+ try {
246
+ await mkdir(lockPath);
247
+ return;
248
+ } catch (cause) {
249
+ const err = cause as NodeJS.ErrnoException;
250
+ if (err.code !== "EEXIST") throw error.toError(cause);
251
+ await maybeReclaimStale(lockPath);
252
+ assertBeforeDeadline(lockPath, deadline);
253
+ await async.sleep(POLL_MS);
254
+ }
255
+ }
256
+ }
257
+
258
+ async function maybeReclaimStale(lockPath: string): Promise<void> {
259
+ try {
260
+ const { mtimeMs } = await stat(lockPath);
261
+ const age = Date.now() - mtimeMs;
262
+ if (age < STALE_MS) return;
263
+ logger.debug("reclaiming stale lock directory", { path: lockPath, ageMs: age });
264
+ await rm(lockPath, { recursive: true, force: true });
265
+ } catch (cause) {
266
+ const err = cause as NodeJS.ErrnoException;
267
+ if (err.code !== "ENOENT") throw error.toError(cause);
268
+ }
269
+ }
270
+
271
+ function startHeartbeat(lockPath: string): () => void {
272
+ const timer = setInterval(() => {
273
+ const now = new Date();
274
+ void utimes(lockPath, now, now).catch((cause) => {
275
+ logger.warn("file lock heartbeat failed", {
276
+ path: lockPath,
277
+ error: error.errorMessage(cause),
278
+ });
279
+ });
280
+ }, UPDATE_MS);
281
+ timer.unref();
282
+ return () => clearInterval(timer);
283
+ }
284
+
285
+ function assertBeforeDeadline(lockPath: string, deadline: number | undefined): void {
286
+ if (deadline !== undefined && Date.now() >= deadline) {
287
+ throw new Error(`Timed out waiting for file lock: ${lockPath}`);
288
+ }
289
+ }
290
+
291
+ async function ensureParentDir(lockPath: string): Promise<void> {
292
+ await mkdir(dirname(lockPath), { recursive: true });
293
+ }