@dbx-tools/core 0.6.68 → 0.6.70
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 +2 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -1
- package/lib/src/file-lock.d.ts +64 -0
- package/lib/src/file-lock.js +242 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/file-lock.ts +312 -0
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,
|
|
13
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwyQ0FBMkM7QUFDM0MsbURBQW1EO0FBQ25ELHdFQUF3RTtBQUV4RSxPQUFPLEtBQUssR0FBRyxNQUFNLGNBQWMsQ0FBQztBQUNwQyxPQUFPLEtBQUssS0FBSyxNQUFNLGdCQUFnQixDQUFDO0FBQ3hDLE9BQU8sS0FBSyxJQUFJLE1BQU0sZUFBZSxDQUFDO0FBQ3RDLE9BQU8sS0FBSyxJQUFJLE1BQU0sZUFBZSxDQUFDO0FBQ3RDLE9BQU8sS0FBSyxRQUFRLE1BQU0sb0JBQW9CLENBQUM7QUFDL0MsT0FBTyxLQUFLLFdBQVcsTUFBTSx1QkFBdUIsQ0FBQztBQUNyRCxPQUFPLEtBQUssT0FBTyxNQUFNLGtCQUFrQixDQUFDO0FBRTVDLE9BQU8sRUFBRSxrQkFBa0IsRUFBRSxtQkFBbUIsRUFBRSxpQkFBaUIsRUFBRSxzQkFBc0IsRUFBRSxrQkFBa0IsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBRXhJLE9BQU8sRUFBRSwyQkFBMkIsRUFBRSxNQUFNLGVBQWUsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbIi8vIEdFTkVSQVRFRCBieSBwcm9qZW4gd2F0Y2ggLSBETyBOT1QgRURJVC5cbi8vIFJlZ2VuZXJhdGVkIGZyb20gdGhlIGV4cG9ydGluZyBtb2R1bGVzIGluIC4vc3JjLlxuLy8gSGFuZCBlZGl0cyBhcmUgb3ZlcndyaXR0ZW4gb24gdGhlIG5leHQgd2F0Y2g7IHRoaXMgZmlsZSBpcyByZWFkLW9ubHkuXG5cbmV4cG9ydCAqIGFzIGJpbiBmcm9tIFwiLi9zcmMvYmluLnRzXCI7XG5leHBvcnQgKiBhcyBicmFuZCBmcm9tIFwiLi9zcmMvYnJhbmQudHNcIjtcbmV4cG9ydCAqIGFzIGV4ZWMgZnJvbSBcIi4vc3JjL2V4ZWMudHNcIjtcbmV4cG9ydCAqIGFzIGZpbGUgZnJvbSBcIi4vc3JjL2ZpbGUudHNcIjtcbmV4cG9ydCAqIGFzIGZpbGVMb2NrIGZyb20gXCIuL3NyYy9maWxlLWxvY2sudHNcIjtcbmV4cG9ydCAqIGFzIHByb2Nlc3NMb2NrIGZyb20gXCIuL3NyYy9wcm9jZXNzLWxvY2sudHNcIjtcbmV4cG9ydCAqIGFzIHByb2plY3QgZnJvbSBcIi4vc3JjL3Byb2plY3QudHNcIjtcbmV4cG9ydCB0eXBlIHsgQmluQ29udGV4dCwgQmluU2VsZWN0aW9uQ29udGV4dCwgQmluU2VsZWN0b3IsIEJpblZlcnNpb25PdXRwdXQsIEJpblZlcnNpb25QYXJzZXIsIEJpbk9wdGlvbnMsIEJpblVybCB9IGZyb20gXCIuL3NyYy9iaW4udHNcIjtcbmV4cG9ydCB7IEJyYW5kQ29udGV4dFNjaGVtYSwgZGVmYXVsdEJyYW5kQ29udGV4dCwgcGFyc2VCcmFuZENvbnRleHQsIGJyYW5kQ29udGV4dEpzb25TY2hlbWEsIGJyYW5kQ29udGV4dFByb21wdCB9IGZyb20gXCIuL3NyYy9icmFuZC50c1wiO1xuZXhwb3J0IHR5cGUgeyBCcmFuZENvbnRleHQsIEJyYW5kQ29udGV4dElucHV0IH0gZnJvbSBcIi4vc3JjL2JyYW5kLnRzXCI7XG5leHBvcnQgeyBDT01NQU5EX05PVF9GT1VORF9FWElUX0NPREUgfSBmcm9tIFwiLi9zcmMvZXhlYy50c1wiO1xuZXhwb3J0IHR5cGUgeyBFeGVjU3RkaW8sIExpbmVIYW5kbGVyLCBTdGRpb09wdGlvbiwgRXhlY1Jlc3VsdCwgQ2hpbGRQcm9jZXNzUmVzdWx0LCBFeGVjT3B0aW9ucywgU3luY0V4ZWNTdGRpbywgU3luY0V4ZWNPcHRpb25zLCBTcGF3bkFyZ3MgfSBmcm9tIFwiLi9zcmMvZXhlYy50c1wiO1xuZXhwb3J0IHR5cGUgeyBGaWxlTG9ja0JhY2tlbmQsIEZpbGVMb2NrQWNxdWlzaXRpb24sIEZpbGVMb2NrT3B0aW9ucyB9IGZyb20gXCIuL3NyYy9maWxlLWxvY2sudHNcIjtcbmV4cG9ydCB0eXBlIHsgUHJvamVjdENvbnRleHQgfSBmcm9tIFwiLi9zcmMvcHJvamVjdC50c1wiO1xuIl19
|
|
@@ -0,0 +1,64 @@
|
|
|
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. Optional stale
|
|
16
|
+
* detection uses the directory mtime plus a heartbeat.
|
|
17
|
+
*
|
|
18
|
+
* The first backend that can be *initialized* is used for the whole call. A busy
|
|
19
|
+
* lock waits; an unavailable backend falls through to the next.
|
|
20
|
+
*
|
|
21
|
+
* @module
|
|
22
|
+
*/
|
|
23
|
+
/** Backends {@link withFileLock} can attempt, in cascade order. */
|
|
24
|
+
export type FileLockBackend = "flock" | "file";
|
|
25
|
+
/** Result metadata when a caller wants to know which backend ran. */
|
|
26
|
+
export interface FileLockAcquisition {
|
|
27
|
+
backend: FileLockBackend;
|
|
28
|
+
}
|
|
29
|
+
export interface FileLockOptions {
|
|
30
|
+
/**
|
|
31
|
+
* Directory for lockfiles. Defaults to `$TMPDIR/dbx-tools-locks`.
|
|
32
|
+
*/
|
|
33
|
+
dir?: string;
|
|
34
|
+
/** Override the cascade. Defaults to `flock` → `file`. */
|
|
35
|
+
backends?: readonly FileLockBackend[];
|
|
36
|
+
/**
|
|
37
|
+
* Stop waiting and throw after this many milliseconds. Omit to poll forever.
|
|
38
|
+
*/
|
|
39
|
+
timeoutMs?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Age after which an unrefreshed lock directory may be removed. Omit to
|
|
42
|
+
* disable stale detection. Only used by the `file` backend.
|
|
43
|
+
*/
|
|
44
|
+
staleMs?: number;
|
|
45
|
+
/**
|
|
46
|
+
* Interval for refreshing the lock directory mtime when `staleMs` is set.
|
|
47
|
+
* Defaults to half of `staleMs`.
|
|
48
|
+
*/
|
|
49
|
+
updateMs?: number;
|
|
50
|
+
/** Invoked once the backend for this call has been chosen. */
|
|
51
|
+
onAcquire?: (acquisition: FileLockAcquisition) => void;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Run `fn` while holding a cross-process lock named by `key`.
|
|
55
|
+
*
|
|
56
|
+
* `key` is canonicalized with `object.toStableKey`, the same identity rule as
|
|
57
|
+
* process and Postgres advisory locks.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* await withFileLock(["cache", "appkit"], async () => {
|
|
61
|
+
* await migrate();
|
|
62
|
+
* });
|
|
63
|
+
*/
|
|
64
|
+
export declare function withFileLock<T>(key: unknown, fn: () => T | Promise<T>, options?: FileLockOptions): Promise<T>;
|
|
@@ -0,0 +1,242 @@
|
|
|
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. Optional stale
|
|
16
|
+
* detection uses the directory mtime plus a heartbeat.
|
|
17
|
+
*
|
|
18
|
+
* The first backend that can be *initialized* is used for the whole call. A busy
|
|
19
|
+
* lock waits; an unavailable backend falls through to the next.
|
|
20
|
+
*
|
|
21
|
+
* @module
|
|
22
|
+
*/
|
|
23
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
24
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
25
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
26
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return path;
|
|
30
|
+
};
|
|
31
|
+
import { mkdir, open, rm, stat, utimes } from "node:fs/promises";
|
|
32
|
+
import { tmpdir } from "node:os";
|
|
33
|
+
import { dirname, join } from "node:path";
|
|
34
|
+
import { async, error, functionModule, hash, log, object } from "@dbx-tools/shared-core";
|
|
35
|
+
const logger = log.logger("core:file-lock");
|
|
36
|
+
const DEFAULT_BACKENDS = ["flock", "file"];
|
|
37
|
+
/** Poll interval while waiting for a contended OS lock. */
|
|
38
|
+
const POLL_MS = 50;
|
|
39
|
+
/** `flock(2)` operation bits (Linux / macOS / BSD). */
|
|
40
|
+
const LOCK_EX = 2;
|
|
41
|
+
const LOCK_NB = 4;
|
|
42
|
+
const LOCK_UN = 8;
|
|
43
|
+
/**
|
|
44
|
+
* Run `fn` while holding a cross-process lock named by `key`.
|
|
45
|
+
*
|
|
46
|
+
* `key` is canonicalized with `object.toStableKey`, the same identity rule as
|
|
47
|
+
* process and Postgres advisory locks.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* await withFileLock(["cache", "appkit"], async () => {
|
|
51
|
+
* await migrate();
|
|
52
|
+
* });
|
|
53
|
+
*/
|
|
54
|
+
export async function withFileLock(key, fn, options = {}) {
|
|
55
|
+
validateOptions(options);
|
|
56
|
+
const id = lockId(key);
|
|
57
|
+
const backends = options.backends ?? DEFAULT_BACKENDS;
|
|
58
|
+
const dir = options.dir ?? join(tmpdir(), "dbx-tools-locks");
|
|
59
|
+
const deadline = options.timeoutMs === undefined ? undefined : Date.now() + options.timeoutMs;
|
|
60
|
+
for (const backend of backends) {
|
|
61
|
+
switch (backend) {
|
|
62
|
+
case "flock": {
|
|
63
|
+
const lockPath = join(dir, `${id}.flock`);
|
|
64
|
+
const flock = await resolveFlock();
|
|
65
|
+
if (!flock) {
|
|
66
|
+
logger.debug("lock backend unavailable", { backend, key: id });
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
logger.debug("acquiring lock", { backend, key: id, path: lockPath });
|
|
70
|
+
options.onAcquire?.({ backend });
|
|
71
|
+
return holdFlock(lockPath, flock, deadline, fn);
|
|
72
|
+
}
|
|
73
|
+
case "file": {
|
|
74
|
+
const lockPath = join(dir, `${id}.lock`);
|
|
75
|
+
logger.debug("acquiring lock", { backend, key: id, path: lockPath });
|
|
76
|
+
options.onAcquire?.({ backend });
|
|
77
|
+
return holdLockDirectory(lockPath, deadline, options.staleMs, options.updateMs, fn);
|
|
78
|
+
}
|
|
79
|
+
default: {
|
|
80
|
+
const _exhaustive = backend;
|
|
81
|
+
throw new Error(`unknown file-lock backend: ${String(_exhaustive)}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
throw new Error("withFileLock: no lock backend available");
|
|
86
|
+
}
|
|
87
|
+
function validateOptions(options) {
|
|
88
|
+
if (options.timeoutMs !== undefined && options.timeoutMs < 0) {
|
|
89
|
+
throw new TypeError("timeoutMs must be non-negative");
|
|
90
|
+
}
|
|
91
|
+
if (options.staleMs !== undefined && options.staleMs <= 0) {
|
|
92
|
+
throw new TypeError("staleMs must be positive");
|
|
93
|
+
}
|
|
94
|
+
if (options.updateMs !== undefined) {
|
|
95
|
+
if (options.staleMs === undefined) {
|
|
96
|
+
throw new TypeError("updateMs requires staleMs");
|
|
97
|
+
}
|
|
98
|
+
if (options.updateMs <= 0 || options.updateMs > options.staleMs / 2) {
|
|
99
|
+
throw new TypeError("updateMs must be positive and no greater than half of staleMs");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** Canonical filesystem-safe id for a lock key. */
|
|
104
|
+
function lockId(key) {
|
|
105
|
+
const stable = object
|
|
106
|
+
.toOneOrMany(key)
|
|
107
|
+
.map((part) => object.toStableKey(part))
|
|
108
|
+
.join("\u0000");
|
|
109
|
+
// Short digest so paths stay well under OS limits; collisions are fine — they
|
|
110
|
+
// only merge critical sections that already shared a key string.
|
|
111
|
+
return hash.fnvHash(stable);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Resolve `flock(2)` through Bun FFI when possible.
|
|
115
|
+
*
|
|
116
|
+
* Memoized: the FFI import + libc `dlopen` run once per process. Returns
|
|
117
|
+
* `undefined` on Windows, under plain Node, or when libc cannot be loaded —
|
|
118
|
+
* callers fall through to the next backend. A miss is cached too, so a process
|
|
119
|
+
* that cannot flock never retries the load.
|
|
120
|
+
*/
|
|
121
|
+
const resolveFlock = functionModule.memoize(async () => {
|
|
122
|
+
if (process.platform === "win32")
|
|
123
|
+
return undefined;
|
|
124
|
+
if (!process.versions.bun)
|
|
125
|
+
return undefined;
|
|
126
|
+
try {
|
|
127
|
+
// Dynamic specifier so `tsc` (no `@types` for `bun:ffi`) does not resolve it;
|
|
128
|
+
// the import only succeeds under Bun at runtime.
|
|
129
|
+
const specifier = "bun:ffi";
|
|
130
|
+
const ffi = (await import(__rewriteRelativeImportExtension(specifier)));
|
|
131
|
+
const libPath = process.platform === "darwin" ? "libSystem.B.dylib" : `libc.${ffi.suffix}`;
|
|
132
|
+
const lib = ffi.dlopen(libPath, {
|
|
133
|
+
flock: { args: ["i32", "i32"], returns: "i32" },
|
|
134
|
+
});
|
|
135
|
+
return lib.symbols.flock;
|
|
136
|
+
}
|
|
137
|
+
catch (cause) {
|
|
138
|
+
logger.debug("flock FFI unavailable", { error: error.errorMessage(cause) });
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
async function holdFlock(lockPath, flock, deadline, fn) {
|
|
143
|
+
await ensureParentDir(lockPath);
|
|
144
|
+
const handle = await open(lockPath, "a+");
|
|
145
|
+
try {
|
|
146
|
+
await waitForFlock(handle.fd, flock, lockPath, deadline);
|
|
147
|
+
try {
|
|
148
|
+
return await fn();
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
flock(handle.fd, LOCK_UN);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
await handle.close().catch(() => { });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function waitForFlock(fd, flock, lockPath, deadline) {
|
|
159
|
+
for (;;) {
|
|
160
|
+
const rc = flock(fd, LOCK_EX | LOCK_NB);
|
|
161
|
+
if (rc === 0)
|
|
162
|
+
return;
|
|
163
|
+
assertBeforeDeadline(lockPath, deadline);
|
|
164
|
+
await async.sleep(POLL_MS);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Atomic lock-directory creation — the portable / Windows path.
|
|
169
|
+
*
|
|
170
|
+
* `mkdir` is the same primitive used by `proper-lockfile`: it is atomic across
|
|
171
|
+
* supported local and network filesystems. When stale detection is enabled,
|
|
172
|
+
* the holder refreshes mtime so a live long-running lock is never reclaimed.
|
|
173
|
+
*/
|
|
174
|
+
async function holdLockDirectory(lockPath, deadline, staleMs, updateMs, fn) {
|
|
175
|
+
await ensureParentDir(lockPath);
|
|
176
|
+
await acquireLockDirectory(lockPath, deadline, staleMs);
|
|
177
|
+
const stopHeartbeat = startHeartbeat(lockPath, staleMs, updateMs);
|
|
178
|
+
try {
|
|
179
|
+
return await fn();
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
stopHeartbeat();
|
|
183
|
+
await rm(lockPath, { recursive: true, force: true }).catch(() => { });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async function acquireLockDirectory(lockPath, deadline, staleMs) {
|
|
187
|
+
for (;;) {
|
|
188
|
+
try {
|
|
189
|
+
await mkdir(lockPath);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
catch (cause) {
|
|
193
|
+
const err = cause;
|
|
194
|
+
if (err.code !== "EEXIST")
|
|
195
|
+
throw error.toError(cause);
|
|
196
|
+
if (staleMs !== undefined)
|
|
197
|
+
await maybeReclaimStale(lockPath, staleMs);
|
|
198
|
+
assertBeforeDeadline(lockPath, deadline);
|
|
199
|
+
await async.sleep(POLL_MS);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async function maybeReclaimStale(lockPath, staleMs) {
|
|
204
|
+
try {
|
|
205
|
+
const { mtimeMs } = await stat(lockPath);
|
|
206
|
+
const age = Date.now() - mtimeMs;
|
|
207
|
+
if (age < staleMs)
|
|
208
|
+
return;
|
|
209
|
+
logger.debug("reclaiming stale lock directory", { path: lockPath, ageMs: age });
|
|
210
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
211
|
+
}
|
|
212
|
+
catch (cause) {
|
|
213
|
+
const err = cause;
|
|
214
|
+
if (err.code !== "ENOENT")
|
|
215
|
+
throw error.toError(cause);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function startHeartbeat(lockPath, staleMs, updateMs) {
|
|
219
|
+
if (staleMs === undefined)
|
|
220
|
+
return () => { };
|
|
221
|
+
const intervalMs = updateMs ?? Math.max(1, Math.floor(staleMs / 2));
|
|
222
|
+
const timer = setInterval(() => {
|
|
223
|
+
const now = new Date();
|
|
224
|
+
void utimes(lockPath, now, now).catch((cause) => {
|
|
225
|
+
logger.warn("file lock heartbeat failed", {
|
|
226
|
+
path: lockPath,
|
|
227
|
+
error: error.errorMessage(cause),
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
}, intervalMs);
|
|
231
|
+
timer.unref();
|
|
232
|
+
return () => clearInterval(timer);
|
|
233
|
+
}
|
|
234
|
+
function assertBeforeDeadline(lockPath, deadline) {
|
|
235
|
+
if (deadline !== undefined && Date.now() >= deadline) {
|
|
236
|
+
throw new Error(`Timed out waiting for file lock: ${lockPath}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
async function ensureParentDir(lockPath) {
|
|
240
|
+
await mkdir(dirname(lockPath), { recursive: true });
|
|
241
|
+
}
|
|
242
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZmlsZS1sb2NrLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2ZpbGUtbG9jay50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBcUJHOzs7Ozs7Ozs7QUFFSCxPQUFPLEVBQUUsS0FBSyxFQUFFLElBQUksRUFBRSxFQUFFLEVBQUUsSUFBSSxFQUFFLE1BQU0sRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBQ2pFLE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxTQUFTLENBQUM7QUFDakMsT0FBTyxFQUFFLE9BQU8sRUFBRSxJQUFJLEVBQUUsTUFBTSxXQUFXLENBQUM7QUFFMUMsT0FBTyxFQUFFLEtBQUssRUFBRSxLQUFLLEVBQUUsY0FBYyxFQUFFLElBQUksRUFBRSxHQUFHLEVBQUUsTUFBTSxFQUFFLE1BQU0sd0JBQXdCLENBQUM7QUFFekYsTUFBTSxNQUFNLEdBQUcsR0FBRyxDQUFDLE1BQU0sQ0FBQyxnQkFBZ0IsQ0FBQyxDQUFDO0FBSzVDLE1BQU0sZ0JBQWdCLEdBQStCLENBQUMsT0FBTyxFQUFFLE1BQU0sQ0FBQyxDQUFDO0FBRXZFLDJEQUEyRDtBQUMzRCxNQUFNLE9BQU8sR0FBRyxFQUFFLENBQUM7QUFFbkIsdURBQXVEO0FBQ3ZELE1BQU0sT0FBTyxHQUFHLENBQUMsQ0FBQztBQUNsQixNQUFNLE9BQU8sR0FBRyxDQUFDLENBQUM7QUFDbEIsTUFBTSxPQUFPLEdBQUcsQ0FBQyxDQUFDO0FBMENsQjs7Ozs7Ozs7OztHQVVHO0FBQ0gsTUFBTSxDQUFDLEtBQUssVUFBVSxZQUFZLENBQ2hDLEdBQVksRUFDWixFQUF3QixFQUN4QixVQUEyQixFQUFFO0lBRTdCLGVBQWUsQ0FBQyxPQUFPLENBQUMsQ0FBQztJQUN6QixNQUFNLEVBQUUsR0FBRyxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUM7SUFDdkIsTUFBTSxRQUFRLEdBQUcsT0FBTyxDQUFDLFFBQVEsSUFBSSxnQkFBZ0IsQ0FBQztJ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RSxJQUFJLEVBQUUsQ0FBQyxDQUFDO0FBQ3RELENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIENhc2NhZGluZyBjcm9zcy1wcm9jZXNzIG11dHVhbCBleGNsdXNpb24gdmlhIGEgbG9ja2ZpbGUuXG4gKlxuICogU2VyaWFsaXplcyBjb25jdXJyZW50ICpwcm9jZXNzZXMqICh0d28gYGJ1biBydW4gZGVtb2Agc2hlbGxzLCBhIENMSSBiZXNpZGUgYVxuICogc2VydmVyKSBvbiB0aGUgc2FtZSBrZXkuIEZvciBpbi1wcm9jZXNzIC8gd29ya2VyLXRocmVhZCBleGNsdXNpb24gdXNlXG4gKiB7QGxpbmsgd2l0aFByb2Nlc3NMb2NrfSBmcm9tIGAuL3Byb2Nlc3MtbG9jay50c2AgaW5zdGVhZC5cbiAqXG4gKiBCYWNrZW5kcywgaW4gb3JkZXI6XG4gKlxuICogMS4gKipmbG9jayoqIOKAlCBgZmxvY2soMilgIHZpYSBCdW4gRkZJIG9uIFVuaXggd2hlbiBgYnVuOmZmaWAgY2FuIGxvYWQgbGliYy5cbiAqICAgIE5vdCBhdmFpbGFibGUgb24gV2luZG93cywgYW5kIG5vdCBhdmFpbGFibGUgdW5kZXIgcGxhaW4gTm9kZSAobm8gRkZJKS5cbiAqICAgIEtlcm5lbCByZWxlYXNlcyB0aGUgbG9jayB3aGVuIHRoZSBmZCBjbG9zZXMgKGluY2x1ZGluZyBwcm9jZXNzIGRlYXRoKS5cbiAqIDIuICoqZmlsZSoqIOKAlCBhdG9taWMgbG9jay1kaXJlY3RvcnkgY3JlYXRpb24uIFRoaXMgaXMgdGhlIHN0cmF0ZWd5IHVzZWQgYnlcbiAqICAgIGBwcm9wZXItbG9ja2ZpbGVgOiBgbWtkaXJgIGlzIGF0b21pYyBvbiBXaW5kb3dzLCBVbml4LCBhbmQgbmV0d29yayBmaWxlXG4gKiAgICBzeXN0ZW1zIHdoZXJlIGBvcGVuKC4uLiwgXCJ3eFwiKWAgbWF5IG5vdCBiZSByZWxpYWJsZS4gT3B0aW9uYWwgc3RhbGVcbiAqICAgIGRldGVjdGlvbiB1c2VzIHRoZSBkaXJlY3RvcnkgbXRpbWUgcGx1cyBhIGhlYXJ0YmVhdC5cbiAqXG4gKiBUaGUgZmlyc3QgYmFja2VuZCB0aGF0IGNhbiBiZSAqaW5pdGlhbGl6ZWQqIGlzIHVzZWQgZm9yIHRoZSB3aG9sZSBjYWxsLiBBIGJ1c3lcbiAqIGxvY2sgd2FpdHM7IGFuIHVuYXZhaWxhYmxlIGJhY2tlbmQgZmFsbHMgdGhyb3VnaCB0byB0aGUgbmV4dC5cbiAqXG4gKiBAbW9kdWxlXG4gKi9cblxuaW1wb3J0IHsgbWtkaXIsIG9wZW4sIHJtLCBzdGF0LCB1dGltZXMgfSBmcm9tIFwibm9kZTpmcy9wcm9taXNlc1wiO1xuaW1wb3J0IHsgdG1wZGlyIH0gZnJvbSBcIm5vZGU6b3NcIjtcbmltcG9ydCB7IGRpcm5hbWUsIGpvaW4gfSBmcm9tIFwibm9kZTpwYXRoXCI7XG5cbmltcG9ydCB7IGFzeW5jLCBlcnJvciwgZnVuY3Rpb25Nb2R1bGUsIGhhc2gsIGxvZywgb2JqZWN0IH0gZnJvbSBcIkBkYngtdG9vbHMvc2hhcmVkLWNvcmVcIjtcblxuY29uc3QgbG9nZ2VyID0gbG9nLmxvZ2dlcihcImNvcmU6ZmlsZS1sb2NrXCIpO1xuXG4vKiogQmFja2VuZHMge0BsaW5rIHdpdGhGaWxlTG9ja30gY2FuIGF0dGVtcHQsIGluIGNhc2NhZGUgb3JkZXIuICovXG5leHBvcnQgdHlwZSBGaWxlTG9ja0JhY2tlbmQgPSBcImZsb2NrXCIgfCBcImZpbGVcIjtcblxuY29uc3QgREVGQVVMVF9CQUNLRU5EUzogcmVhZG9ubHkgRmlsZUxvY2tCYWNrZW5kW10gPSBbXCJmbG9ja1wiLCBcImZpbGVcIl07XG5cbi8qKiBQb2xsIGludGVydmFsIHdoaWxlIHdhaXRpbmcgZm9yIGEgY29udGVuZGVkIE9TIGxvY2suICovXG5jb25zdCBQT0xMX01TID0gNTA7XG5cbi8qKiBgZmxvY2soMilgIG9wZXJhdGlvbiBiaXRzIChMaW51eCAvIG1hY09TIC8gQlNEKS4gKi9cbmNvbnN0IExPQ0tfRVggPSAyO1xuY29uc3QgTE9DS19OQiA9IDQ7XG5jb25zdCBMT0NLX1VOID0gODtcblxuLyoqIFJlc3VsdCBtZXRhZGF0YSB3aGVuIGEgY2FsbGVyIHdhbnRzIHRvIGtub3cgd2hpY2ggYmFja2VuZCByYW4uICovXG5leHBvcnQgaW50ZXJmYWNlIEZpbGVMb2NrQWNxdWlzaXRpb24ge1xuICBiYWNrZW5kOiBGaWxlTG9ja0JhY2tlbmQ7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgRmlsZUxvY2tPcHRpb25zIHtcbiAgLyoqXG4gICAqIERpcmVjdG9yeSBmb3IgbG9ja2ZpbGVzLiBEZWZhdWx0cyB0byBgJFRNUERJUi9kYngtdG9vbHMtbG9ja3NgLlxuICAgKi9cbiAgZGlyPzogc3RyaW5nO1xuICAvKiogT3ZlcnJpZGUgdGhlIGNhc2NhZGUuIERlZmF1bHRzIHRvIGBmbG9ja2Ag4oaSIGBmaWxlYC4gKi9cbiAgYmFja2VuZHM/OiByZWFkb25seSBGaWxlTG9ja0JhY2tlbmRbXTtcbiAgLyoqXG4gICAqIFN0b3Agd2FpdGluZyBhbmQgdGhyb3cgYWZ0ZXIgdGhpcyBtYW55IG1pbGxpc2Vjb25kcy4gT21pdCB0byBwb2xsIGZvcmV2ZXIuXG4gICAqL1xuICB0aW1lb3V0TXM/OiBudW1iZXI7XG4gIC8qKlxuICAgKiBBZ2UgYWZ0ZXIgd2hpY2ggYW4gdW5yZWZyZXNoZWQgbG9jayBkaXJlY3RvcnkgbWF5IGJlIHJlbW92ZWQuIE9taXQgdG9cbiAgICogZGlzYWJsZSBzdGFsZSBkZXRlY3Rpb24uIE9ubHkgdXNlZCBieSB0aGUgYGZpbGVgIGJhY2tlbmQuXG4gICAqL1xuICBzdGFsZU1zPzogbnVtYmVyO1xuICAvKipcbiAgICogSW50ZXJ2YWwgZm9yIHJlZnJlc2hpbmcgdGhlIGxvY2sgZGlyZWN0b3J5IG10aW1lIHdoZW4gYHN0YWxlTXNgIGlzIHNldC5cbiAgICogRGVmYXVsdHMgdG8gaGFsZiBvZiBgc3RhbGVNc2AuXG4gICAqL1xuICB1cGRhdGVNcz86IG51bWJlcjtcbiAgLyoqIEludm9rZWQgb25jZSB0aGUgYmFja2VuZCBmb3IgdGhpcyBjYWxsIGhhcyBiZWVuIGNob3Nlbi4gKi9cbiAgb25BY3F1aXJlPzogKGFjcXVpc2l0aW9uOiBGaWxlTG9ja0FjcXVpc2l0aW9uKSA9PiB2b2lkO1xufVxuXG50eXBlIEZsb2NrRm4gPSAoZmQ6IG51bWJlciwgb3BlcmF0aW9uOiBudW1iZXIpID0+IG51bWJlcjtcblxuaW50ZXJmYWNlIEJ1bkZmaU1vZHVsZSB7XG4gIGRsb3BlbjogKFxuICAgIHBhdGg6IHN0cmluZyxcbiAgICBzeW1ib2xzOiBSZWNvcmQ8c3RyaW5nLCB7IGFyZ3M6IHN0cmluZ1tdOyByZXR1cm5zOiBzdHJpbmcgfT4sXG4gICkgPT4geyBzeW1ib2xzOiB7IGZsb2NrOiBGbG9ja0ZuIH0gfTtcbiAgc3VmZml4OiBzdHJpbmc7XG59XG5cbi8qKlxuICogUnVuIGBmbmAgd2hpbGUgaG9sZGluZyBhIGNyb3NzLXByb2Nlc3MgbG9jayBuYW1lZCBieSBga2V5YC5cbiAqXG4gKiBga2V5YCBpcyBjYW5vbmljYWxpemVkIHdpdGggYG9iamVjdC50b1N0YWJsZUtleWAsIHRoZSBzYW1lIGlkZW50aXR5IHJ1bGUgYXNcbiAqIHByb2Nlc3MgYW5kIFBvc3RncmVzIGFkdmlzb3J5IGxvY2tzLlxuICpcbiAqIEBleGFtcGxlXG4gKiBhd2FpdCB3aXRoRmlsZUxvY2soW1wiY2FjaGVcIiwgXCJhcHBraXRcIl0sIGFzeW5jICgpID0+IHtcbiAqICAgYXdhaXQgbWlncmF0ZSgpO1xuICogfSk7XG4gKi9cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiB3aXRoRmlsZUxvY2s8VD4oXG4gIGtleTogdW5rbm93bixcbiAgZm46ICgpID0+IFQgfCBQcm9taXNlPFQ+LFxuICBvcHRpb25zOiBGaWxlTG9ja09wdGlvbnMgPSB7fSxcbik6IFByb21pc2U8VD4ge1xuICB2YWxpZGF0ZU9wdGlvbnMob3B0aW9ucyk7XG4gIGNvbnN0IGlkID0gbG9ja0lkKGtleSk7XG4gIGNvbnN0IGJhY2tlbmRzID0gb3B0aW9ucy5iYWNrZW5kcyA/PyBERUZBVUxUX0JBQ0tFTkRTO1xuICBjb25zdCBkaXIgPSBvcHRpb25zLmRpciA/PyBqb2luKHRtcGRpcigpLCBcImRieC10b29scy1sb2Nrc1wiKTtcbiAgY29uc3QgZGVhZGxpbmUgPSBvcHRpb25zLnRpbWVvdXRNcyA9PT0gdW5kZWZpbmVkID8gdW5kZWZpbmVkIDogRGF0ZS5ub3coKSArIG9wdGlvbnMudGltZW91dE1zO1xuXG4gIGZvciAoY29uc3QgYmFja2VuZCBvZiBiYWNrZW5kcykge1xuICAgIHN3aXRjaCAoYmFja2VuZCkge1xuICAgICAgY2FzZSBcImZsb2NrXCI6IHtcbiAgICAgICAgY29uc3QgbG9ja1BhdGggPSBqb2luKGRpciwgYCR7aWR9LmZsb2NrYCk7XG4gICAgICAgIGNvbnN0IGZsb2NrID0gYXdhaXQgcmVzb2x2ZUZsb2NrKCk7XG4gICAgICAgIGlmICghZmxvY2spIHtcbiAgICAgICAgICBsb2dnZXIuZGVidWcoXCJsb2NrIGJhY2tlbmQgdW5hdmFpbGFibGVcIiwgeyBiYWNrZW5kLCBrZXk6IGlkIH0pO1xuICAgICAgICAgIGNvbnRpbnVlO1xuICAgICAgICB9XG4gICAgICAgIGxvZ2dlci5kZWJ1ZyhcImFjcXVpcmluZyBsb2NrXCIsIHsgYmFja2VuZCwga2V5OiBpZCwgcGF0aDogbG9ja1BhdGggfSk7XG4gICAgICAgIG9wdGlvbnMub25BY3F1aXJlPy4oeyBiYWNrZW5kIH0pO1xuICAgICAgICByZXR1cm4gaG9sZEZsb2NrKGxvY2tQYXRoLCBmbG9jaywgZGVhZGxpbmUsIGZuKTtcbiAgICAgIH1cbiAgICAgIGNhc2UgXCJmaWxlXCI6IHtcbiAgICAgICAgY29uc3QgbG9ja1BhdGggPSBqb2luKGRpciwgYCR7aWR9LmxvY2tgKTtcbiAgICAgICAgbG9nZ2VyLmRlYnVnKFwiYWNxdWlyaW5nIGxvY2tcIiwgeyBiYWNrZW5kLCBrZXk6IGlkLCBwYXRoOiBsb2NrUGF0aCB9KTtcbiAgICAgICAgb3B0aW9ucy5vbkFjcXVpcmU/Lih7IGJhY2tlbmQgfSk7XG4gICAgICAgIHJldHVybiBob2xkTG9ja0RpcmVjdG9yeShsb2NrUGF0aCwgZGVhZGxpbmUsIG9wdGlvbnMuc3RhbGVNcywgb3B0aW9ucy51cGRhdGVNcywgZm4pO1xuICAgICAgfVxuICAgICAgZGVmYXVsdDoge1xuICAgICAgICBjb25zdCBfZXhoYXVzdGl2ZTogbmV2ZXIgPSBiYWNrZW5kO1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYHVua25vd24gZmlsZS1sb2NrIGJhY2tlbmQ6ICR7U3RyaW5nKF9leGhhdXN0aXZlKX1gKTtcbiAgICAgIH1cbiAgICB9XG4gIH1cblxuICB0aHJvdyBuZXcgRXJyb3IoXCJ3aXRoRmlsZUxvY2s6IG5vIGxvY2sgYmFja2VuZCBhdmFpbGFibGVcIik7XG59XG5cbmZ1bmN0aW9uIHZhbGlkYXRlT3B0aW9ucyhvcHRpb25zOiBGaWxlTG9ja09wdGlvbnMpOiB2b2lkIHtcbiAgaWYgKG9wdGlvbnMudGltZW91dE1zICE9PSB1bmRlZmluZWQgJiYgb3B0aW9ucy50aW1lb3V0TXMgPCAwKSB7XG4gICAgdGhyb3cgbmV3IFR5cGVFcnJvcihcInRpbWVvdXRNcyBtdXN0IGJlIG5vbi1uZWdhdGl2ZVwiKTtcbiAgfVxuICBpZiAob3B0aW9ucy5zdGFsZU1zICE9PSB1bmRlZmluZWQgJiYgb3B0aW9ucy5zdGFsZU1zIDw9IDApIHtcbiAgICB0aHJvdyBuZXcgVHlwZUVycm9yKFwic3RhbGVNcyBtdXN0IGJlIHBvc2l0aXZlXCIpO1xuICB9XG4gIGlmIChvcHRpb25zLnVwZGF0ZU1zICE9PSB1bmRlZmluZWQpIHtcbiAgICBpZiAob3B0aW9ucy5zdGFsZU1zID09PSB1bmRlZmluZWQpIHtcbiAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoXCJ1cGRhdGVNcyByZXF1aXJlcyBzdGFsZU1zXCIpO1xuICAgIH1cbiAgICBpZiAob3B0aW9ucy51cGRhdGVNcyA8PSAwIHx8IG9wdGlvbnMudXBkYXRlTXMgPiBvcHRpb25zLnN0YWxlTXMgLyAyKSB7XG4gICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKFwidXBkYXRlTXMgbXVzdCBiZSBwb3NpdGl2ZSBhbmQgbm8gZ3JlYXRlciB0aGFuIGhhbGYgb2Ygc3RhbGVNc1wiKTtcbiAgICB9XG4gIH1cbn1cblxuLyoqIENhbm9uaWNhbCBmaWxlc3lzdGVtLXNhZmUgaWQgZm9yIGEgbG9jayBrZXkuICovXG5mdW5jdGlvbiBsb2NrSWQoa2V5OiB1bmtub3duKTogc3RyaW5nIHtcbiAgY29uc3Qgc3RhYmxlID0gb2JqZWN0XG4gICAgLnRvT25lT3JNYW55KGtleSlcbiAgICAubWFwKChwYXJ0KSA9PiBvYmplY3QudG9TdGFibGVLZXkocGFydCkpXG4gICAgLmpvaW4oXCJcXHUwMDAwXCIpO1xuICAvLyBTaG9ydCBkaWdlc3Qgc28gcGF0aHMgc3RheSB3ZWxsIHVuZGVyIE9TIGxpbWl0czsgY29sbGlzaW9ucyBhcmUgZmluZSDigJQgdGhleVxuICAvLyBvbmx5IG1lcmdlIGNyaXRpY2FsIHNlY3Rpb25zIHRoYXQgYWxyZWFkeSBzaGFyZWQgYSBrZXkgc3RyaW5nLlxuICByZXR1cm4gaGFzaC5mbnZIYXNoKHN0YWJsZSk7XG59XG5cbi8qKlxuICogUmVzb2x2ZSBgZmxvY2soMilgIHRocm91Z2ggQnVuIEZGSSB3aGVuIHBvc3NpYmxlLlxuICpcbiAqIE1lbW9pemVkOiB0aGUgRkZJIGltcG9ydCArIGxpYmMgYGRsb3BlbmAgcnVuIG9uY2UgcGVyIHByb2Nlc3MuIFJldHVybnNcbiAqIGB1bmRlZmluZWRgIG9uIFdpbmRvd3MsIHVuZGVyIHBsYWluIE5vZGUsIG9yIHdoZW4gbGliYyBjYW5ub3QgYmUgbG9hZGVkIOKAlFxuICogY2FsbGVycyBmYWxsIHRocm91Z2ggdG8gdGhlIG5leHQgYmFja2VuZC4gQSBtaXNzIGlzIGNhY2hlZCB0b28sIHNvIGEgcHJvY2Vzc1xuICogdGhhdCBjYW5ub3QgZmxvY2sgbmV2ZXIgcmV0cmllcyB0aGUgbG9hZC5cbiAqL1xuY29uc3QgcmVzb2x2ZUZsb2NrID0gZnVuY3Rpb25Nb2R1bGUubWVtb2l6ZShhc3luYyAoKTogUHJvbWlzZTxGbG9ja0ZuIHwgdW5kZWZpbmVkPiA9PiB7XG4gIGlmIChwcm9jZXNzLnBsYXRmb3JtID09PSBcIndpbjMyXCIpIHJldHVybiB1bmRlZmluZWQ7XG4gIGlmICghcHJvY2Vzcy52ZXJzaW9ucy5idW4pIHJldHVybiB1bmRlZmluZWQ7XG5cbiAgdHJ5IHtcbiAgICAvLyBEeW5hbWljIHNwZWNpZmllciBzbyBgdHNjYCAobm8gYEB0eXBlc2AgZm9yIGBidW46ZmZpYCkgZG9lcyBub3QgcmVzb2x2ZSBpdDtcbiAgICAvLyB0aGUgaW1wb3J0IG9ubHkgc3VjY2VlZHMgdW5kZXIgQnVuIGF0IHJ1bnRpbWUuXG4gICAgY29uc3Qgc3BlY2lmaWVyID0gXCJidW46ZmZpXCI7XG4gICAgY29uc3QgZmZpID0gKGF3YWl0IGltcG9ydChzcGVjaWZpZXIpKSBhcyB1bmtub3duIGFzIEJ1bkZmaU1vZHVsZTtcbiAgICBjb25zdCBsaWJQYXRoID1cbiAgICAgIHByb2Nlc3MucGxhdGZvcm0gPT09IFwiZGFyd2luXCIgPyBcImxpYlN5c3RlbS5CLmR5bGliXCIgOiBgbGliYy4ke2ZmaS5zdWZmaXh9YDtcbiAgICBjb25zdCBsaWIgPSBmZmkuZGxvcGVuKGxpYlBhdGgsIHtcbiAgICAgIGZsb2NrOiB7IGFyZ3M6IFtcImkzMlwiLCBcImkzMlwiXSwgcmV0dXJuczogXCJpMzJcIiB9LFxuICAgIH0pO1xuICAgIHJldHVybiBsaWIuc3ltYm9scy5mbG9jaztcbiAgfSBjYXRjaCAoY2F1c2UpIHtcbiAgICBsb2dnZXIuZGVidWcoXCJmbG9jayBGRkkgdW5hdmFpbGFibGVcIiwgeyBlcnJvcjogZXJyb3IuZXJyb3JNZXNzYWdlKGNhdXNlKSB9KTtcbiAgICByZXR1cm4gdW5kZWZpbmVkO1xuICB9XG59KTtcblxuYXN5bmMgZnVuY3Rpb24gaG9sZEZsb2NrPFQ+KFxuICBsb2NrUGF0aDogc3RyaW5nLFxuICBmbG9jazogRmxvY2tGbixcbiAgZGVhZGxpbmU6IG51bWJlciB8IHVuZGVmaW5lZCxcbiAgZm46ICgpID0+IFQgfCBQcm9taXNlPFQ+LFxuKTogUHJvbWlzZTxUPiB7XG4gIGF3YWl0IGVuc3VyZVBhcmVudERpcihsb2NrUGF0aCk7XG4gIGNvbnN0IGhhbmRsZSA9IGF3YWl0IG9wZW4obG9ja1BhdGgsIFwiYStcIik7XG4gIHRyeSB7XG4gICAgYXdhaXQgd2FpdEZvckZsb2NrKGhhbmRsZS5mZCwgZmxvY2ssIGxvY2tQYXRoLCBkZWFkbGluZSk7XG4gICAgdHJ5IHtcbiAgICAgIHJldHVybiBhd2FpdCBmbigpO1xuICAgIH0gZmluYWxseSB7XG4gICAgICBmbG9jayhoYW5kbGUuZmQsIExPQ0tfVU4pO1xuICAgIH1cbiAgfSBmaW5hbGx5IHtcbiAgICBhd2FpdCBoYW5kbGUuY2xvc2UoKS5jYXRjaCgoKSA9PiB7fSk7XG4gIH1cbn1cblxuYXN5bmMgZnVuY3Rpb24gd2FpdEZvckZsb2NrKFxuICBmZDogbnVtYmVyLFxuICBmbG9jazogRmxvY2tGbixcbiAgbG9ja1BhdGg6IHN0cmluZyxcbiAgZGVhZGxpbmU6IG51bWJlciB8IHVuZGVmaW5lZCxcbik6IFByb21pc2U8dm9pZD4ge1xuICBmb3IgKDs7KSB7XG4gICAgY29uc3QgcmMgPSBmbG9jayhmZCwgTE9DS19FWCB8IExPQ0tfTkIpO1xuICAgIGlmIChyYyA9PT0gMCkgcmV0dXJuO1xuICAgIGFzc2VydEJlZm9yZURlYWRsaW5lKGxvY2tQYXRoLCBkZWFkbGluZSk7XG4gICAgYXdhaXQgYXN5bmMuc2xlZXAoUE9MTF9NUyk7XG4gIH1cbn1cblxuLyoqXG4gKiBBdG9taWMgbG9jay1kaXJlY3RvcnkgY3JlYXRpb24g4oCUIHRoZSBwb3J0YWJsZSAvIFdpbmRvd3MgcGF0aC5cbiAqXG4gKiBgbWtkaXJgIGlzIHRoZSBzYW1lIHByaW1pdGl2ZSB1c2VkIGJ5IGBwcm9wZXItbG9ja2ZpbGVgOiBpdCBpcyBhdG9taWMgYWNyb3NzXG4gKiBzdXBwb3J0ZWQgbG9jYWwgYW5kIG5ldHdvcmsgZmlsZXN5c3RlbXMuIFdoZW4gc3RhbGUgZGV0ZWN0aW9uIGlzIGVuYWJsZWQsXG4gKiB0aGUgaG9sZGVyIHJlZnJlc2hlcyBtdGltZSBzbyBhIGxpdmUgbG9uZy1ydW5uaW5nIGxvY2sgaXMgbmV2ZXIgcmVjbGFpbWVkLlxuICovXG5hc3luYyBmdW5jdGlvbiBob2xkTG9ja0RpcmVjdG9yeTxUPihcbiAgbG9ja1BhdGg6IHN0cmluZyxcbiAgZGVhZGxpbmU6IG51bWJlciB8IHVuZGVmaW5lZCxcbiAgc3RhbGVNczogbnVtYmVyIHwgdW5kZWZpbmVkLFxuICB1cGRhdGVNczogbnVtYmVyIHwgdW5kZWZpbmVkLFxuICBmbjogKCkgPT4gVCB8IFByb21pc2U8VD4sXG4pOiBQcm9taXNlPFQ+IHtcbiAgYXdhaXQgZW5zdXJlUGFyZW50RGlyKGxvY2tQYXRoKTtcbiAgYXdhaXQgYWNxdWlyZUxvY2tEaXJlY3RvcnkobG9ja1BhdGgsIGRlYWRsaW5lLCBzdGFsZU1zKTtcbiAgY29uc3Qgc3RvcEhlYXJ0YmVhdCA9IHN0YXJ0SGVhcnRiZWF0KGxvY2tQYXRoLCBzdGFsZU1zLCB1cGRhdGVNcyk7XG4gIHRyeSB7XG4gICAgcmV0dXJuIGF3YWl0IGZuKCk7XG4gIH0gZmluYWxseSB7XG4gICAgc3RvcEhlYXJ0YmVhdCgpO1xuICAgIGF3YWl0IHJtKGxvY2tQYXRoLCB7IHJlY3Vyc2l2ZTogdHJ1ZSwgZm9yY2U6IHRydWUgfSkuY2F0Y2goKCkgPT4ge30pO1xuICB9XG59XG5cbmFzeW5jIGZ1bmN0aW9uIGFjcXVpcmVMb2NrRGlyZWN0b3J5KFxuICBsb2NrUGF0aDogc3RyaW5nLFxuICBkZWFkbGluZTogbnVtYmVyIHwgdW5kZWZpbmVkLFxuICBzdGFsZU1zOiBudW1iZXIgfCB1bmRlZmluZWQsXG4pOiBQcm9taXNlPHZvaWQ+IHtcbiAgZm9yICg7Oykge1xuICAgIHRyeSB7XG4gICAgICBhd2FpdCBta2Rpcihsb2NrUGF0aCk7XG4gICAgICByZXR1cm47XG4gICAgfSBjYXRjaCAoY2F1c2UpIHtcbiAgICAgIGNvbnN0IGVyciA9IGNhdXNlIGFzIE5vZGVKUy5FcnJub0V4Y2VwdGlvbjtcbiAgICAgIGlmIChlcnIuY29kZSAhPT0gXCJFRVhJU1RcIikgdGhyb3cgZXJyb3IudG9FcnJvcihjYXVzZSk7XG4gICAgICBpZiAoc3RhbGVNcyAhPT0gdW5kZWZpbmVkKSBhd2FpdCBtYXliZVJlY2xhaW1TdGFsZShsb2NrUGF0aCwgc3RhbGVNcyk7XG4gICAgICBhc3NlcnRCZWZvcmVEZWFkbGluZShsb2NrUGF0aCwgZGVhZGxpbmUpO1xuICAgICAgYXdhaXQgYXN5bmMuc2xlZXAoUE9MTF9NUyk7XG4gICAgfVxuICB9XG59XG5cbmFzeW5jIGZ1bmN0aW9uIG1heWJlUmVjbGFpbVN0YWxlKGxvY2tQYXRoOiBzdHJpbmcsIHN0YWxlTXM6IG51bWJlcik6IFByb21pc2U8dm9pZD4ge1xuICB0cnkge1xuICAgIGNvbnN0IHsgbXRpbWVNcyB9ID0gYXdhaXQgc3RhdChsb2NrUGF0aCk7XG4gICAgY29uc3QgYWdlID0gRGF0ZS5ub3coKSAtIG10aW1lTXM7XG4gICAgaWYgKGFnZSA8IHN0YWxlTXMpIHJldHVybjtcbiAgICBsb2dnZXIuZGVidWcoXCJyZWNsYWltaW5nIHN0YWxlIGxvY2sgZGlyZWN0b3J5XCIsIHsgcGF0aDogbG9ja1BhdGgsIGFnZU1zOiBhZ2UgfSk7XG4gICAgYXdhaXQgcm0obG9ja1BhdGgsIHsgcmVjdXJzaXZlOiB0cnVlLCBmb3JjZTogdHJ1ZSB9KTtcbiAgfSBjYXRjaCAoY2F1c2UpIHtcbiAgICBjb25zdCBlcnIgPSBjYXVzZSBhcyBOb2RlSlMuRXJybm9FeGNlcHRpb247XG4gICAgaWYgKGVyci5jb2RlICE9PSBcIkVOT0VOVFwiKSB0aHJvdyBlcnJvci50b0Vycm9yKGNhdXNlKTtcbiAgfVxufVxuXG5mdW5jdGlvbiBzdGFydEhlYXJ0YmVhdChcbiAgbG9ja1BhdGg6IHN0cmluZyxcbiAgc3RhbGVNczogbnVtYmVyIHwgdW5kZWZpbmVkLFxuICB1cGRhdGVNczogbnVtYmVyIHwgdW5kZWZpbmVkLFxuKTogKCkgPT4gdm9pZCB7XG4gIGlmIChzdGFsZU1zID09PSB1bmRlZmluZWQpIHJldHVybiAoKSA9PiB7fTtcbiAgY29uc3QgaW50ZXJ2YWxNcyA9IHVwZGF0ZU1zID8/IE1hdGgubWF4KDEsIE1hdGguZmxvb3Ioc3RhbGVNcyAvIDIpKTtcbiAgY29uc3QgdGltZXIgPSBzZXRJbnRlcnZhbCgoKSA9PiB7XG4gICAgY29uc3Qgbm93ID0gbmV3IERhdGUoKTtcbiAgICB2b2lkIHV0aW1lcyhsb2NrUGF0aCwgbm93LCBub3cpLmNhdGNoKChjYXVzZSkgPT4ge1xuICAgICAgbG9nZ2VyLndhcm4oXCJmaWxlIGxvY2sgaGVhcnRiZWF0IGZhaWxlZFwiLCB7XG4gICAgICAgIHBhdGg6IGxvY2tQYXRoLFxuICAgICAgICBlcnJvcjogZXJyb3IuZXJyb3JNZXNzYWdlKGNhdXNlKSxcbiAgICAgIH0pO1xuICAgIH0pO1xuICB9LCBpbnRlcnZhbE1zKTtcbiAgdGltZXIudW5yZWYoKTtcbiAgcmV0dXJuICgpID0+IGNsZWFySW50ZXJ2YWwodGltZXIpO1xufVxuXG5mdW5jdGlvbiBhc3NlcnRCZWZvcmVEZWFkbGluZShsb2NrUGF0aDogc3RyaW5nLCBkZWFkbGluZTogbnVtYmVyIHwgdW5kZWZpbmVkKTogdm9pZCB7XG4gIGlmIChkZWFkbGluZSAhPT0gdW5kZWZpbmVkICYmIERhdGUubm93KCkgPj0gZGVhZGxpbmUpIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IoYFRpbWVkIG91dCB3YWl0aW5nIGZvciBmaWxlIGxvY2s6ICR7bG9ja1BhdGh9YCk7XG4gIH1cbn1cblxuYXN5bmMgZnVuY3Rpb24gZW5zdXJlUGFyZW50RGlyKGxvY2tQYXRoOiBzdHJpbmcpOiBQcm9taXNlPHZvaWQ+IHtcbiAgYXdhaXQgbWtkaXIoZGlybmFtZShsb2NrUGF0aCksIHsgcmVjdXJzaXZlOiB0cnVlIH0pO1xufVxuIl19
|
package/lib/tsconfig.tsbuildinfo
CHANGED
|
@@ -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.
|
|
26
|
+
"@dbx-tools/shared-core": "0.6.70",
|
|
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.
|
|
45
|
+
"version": "0.6.70",
|
|
46
46
|
"types": "./lib/index.d.ts",
|
|
47
47
|
"type": "module",
|
|
48
48
|
"exports": {
|
package/src/file-lock.ts
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
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. Optional stale
|
|
16
|
+
* detection uses the directory mtime plus a heartbeat.
|
|
17
|
+
*
|
|
18
|
+
* The first backend that can be *initialized* is used for the whole call. A busy
|
|
19
|
+
* lock waits; an unavailable backend falls through to the next.
|
|
20
|
+
*
|
|
21
|
+
* @module
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { mkdir, open, rm, stat, utimes } from "node:fs/promises";
|
|
25
|
+
import { tmpdir } from "node:os";
|
|
26
|
+
import { dirname, join } from "node:path";
|
|
27
|
+
|
|
28
|
+
import { async, error, functionModule, hash, log, object } from "@dbx-tools/shared-core";
|
|
29
|
+
|
|
30
|
+
const logger = log.logger("core:file-lock");
|
|
31
|
+
|
|
32
|
+
/** Backends {@link withFileLock} can attempt, in cascade order. */
|
|
33
|
+
export type FileLockBackend = "flock" | "file";
|
|
34
|
+
|
|
35
|
+
const DEFAULT_BACKENDS: readonly FileLockBackend[] = ["flock", "file"];
|
|
36
|
+
|
|
37
|
+
/** Poll interval while waiting for a contended OS lock. */
|
|
38
|
+
const POLL_MS = 50;
|
|
39
|
+
|
|
40
|
+
/** `flock(2)` operation bits (Linux / macOS / BSD). */
|
|
41
|
+
const LOCK_EX = 2;
|
|
42
|
+
const LOCK_NB = 4;
|
|
43
|
+
const LOCK_UN = 8;
|
|
44
|
+
|
|
45
|
+
/** Result metadata when a caller wants to know which backend ran. */
|
|
46
|
+
export interface FileLockAcquisition {
|
|
47
|
+
backend: FileLockBackend;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface FileLockOptions {
|
|
51
|
+
/**
|
|
52
|
+
* Directory for lockfiles. Defaults to `$TMPDIR/dbx-tools-locks`.
|
|
53
|
+
*/
|
|
54
|
+
dir?: string;
|
|
55
|
+
/** Override the cascade. Defaults to `flock` → `file`. */
|
|
56
|
+
backends?: readonly FileLockBackend[];
|
|
57
|
+
/**
|
|
58
|
+
* Stop waiting and throw after this many milliseconds. Omit to poll forever.
|
|
59
|
+
*/
|
|
60
|
+
timeoutMs?: number;
|
|
61
|
+
/**
|
|
62
|
+
* Age after which an unrefreshed lock directory may be removed. Omit to
|
|
63
|
+
* disable stale detection. Only used by the `file` backend.
|
|
64
|
+
*/
|
|
65
|
+
staleMs?: number;
|
|
66
|
+
/**
|
|
67
|
+
* Interval for refreshing the lock directory mtime when `staleMs` is set.
|
|
68
|
+
* Defaults to half of `staleMs`.
|
|
69
|
+
*/
|
|
70
|
+
updateMs?: number;
|
|
71
|
+
/** Invoked once the backend for this call has been chosen. */
|
|
72
|
+
onAcquire?: (acquisition: FileLockAcquisition) => void;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type FlockFn = (fd: number, operation: number) => number;
|
|
76
|
+
|
|
77
|
+
interface BunFfiModule {
|
|
78
|
+
dlopen: (
|
|
79
|
+
path: string,
|
|
80
|
+
symbols: Record<string, { args: string[]; returns: string }>,
|
|
81
|
+
) => { symbols: { flock: FlockFn } };
|
|
82
|
+
suffix: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Run `fn` while holding a cross-process lock named by `key`.
|
|
87
|
+
*
|
|
88
|
+
* `key` is canonicalized with `object.toStableKey`, the same identity rule as
|
|
89
|
+
* process and Postgres advisory locks.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* await withFileLock(["cache", "appkit"], async () => {
|
|
93
|
+
* await migrate();
|
|
94
|
+
* });
|
|
95
|
+
*/
|
|
96
|
+
export async function withFileLock<T>(
|
|
97
|
+
key: unknown,
|
|
98
|
+
fn: () => T | Promise<T>,
|
|
99
|
+
options: FileLockOptions = {},
|
|
100
|
+
): Promise<T> {
|
|
101
|
+
validateOptions(options);
|
|
102
|
+
const id = lockId(key);
|
|
103
|
+
const backends = options.backends ?? DEFAULT_BACKENDS;
|
|
104
|
+
const dir = options.dir ?? join(tmpdir(), "dbx-tools-locks");
|
|
105
|
+
const deadline = options.timeoutMs === undefined ? undefined : Date.now() + options.timeoutMs;
|
|
106
|
+
|
|
107
|
+
for (const backend of backends) {
|
|
108
|
+
switch (backend) {
|
|
109
|
+
case "flock": {
|
|
110
|
+
const lockPath = join(dir, `${id}.flock`);
|
|
111
|
+
const flock = await resolveFlock();
|
|
112
|
+
if (!flock) {
|
|
113
|
+
logger.debug("lock backend unavailable", { backend, key: id });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
logger.debug("acquiring lock", { backend, key: id, path: lockPath });
|
|
117
|
+
options.onAcquire?.({ backend });
|
|
118
|
+
return holdFlock(lockPath, flock, deadline, fn);
|
|
119
|
+
}
|
|
120
|
+
case "file": {
|
|
121
|
+
const lockPath = join(dir, `${id}.lock`);
|
|
122
|
+
logger.debug("acquiring lock", { backend, key: id, path: lockPath });
|
|
123
|
+
options.onAcquire?.({ backend });
|
|
124
|
+
return holdLockDirectory(lockPath, deadline, options.staleMs, options.updateMs, fn);
|
|
125
|
+
}
|
|
126
|
+
default: {
|
|
127
|
+
const _exhaustive: never = backend;
|
|
128
|
+
throw new Error(`unknown file-lock backend: ${String(_exhaustive)}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
throw new Error("withFileLock: no lock backend available");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function validateOptions(options: FileLockOptions): void {
|
|
137
|
+
if (options.timeoutMs !== undefined && options.timeoutMs < 0) {
|
|
138
|
+
throw new TypeError("timeoutMs must be non-negative");
|
|
139
|
+
}
|
|
140
|
+
if (options.staleMs !== undefined && options.staleMs <= 0) {
|
|
141
|
+
throw new TypeError("staleMs must be positive");
|
|
142
|
+
}
|
|
143
|
+
if (options.updateMs !== undefined) {
|
|
144
|
+
if (options.staleMs === undefined) {
|
|
145
|
+
throw new TypeError("updateMs requires staleMs");
|
|
146
|
+
}
|
|
147
|
+
if (options.updateMs <= 0 || options.updateMs > options.staleMs / 2) {
|
|
148
|
+
throw new TypeError("updateMs must be positive and no greater than half of staleMs");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Canonical filesystem-safe id for a lock key. */
|
|
154
|
+
function lockId(key: unknown): string {
|
|
155
|
+
const stable = object
|
|
156
|
+
.toOneOrMany(key)
|
|
157
|
+
.map((part) => object.toStableKey(part))
|
|
158
|
+
.join("\u0000");
|
|
159
|
+
// Short digest so paths stay well under OS limits; collisions are fine — they
|
|
160
|
+
// only merge critical sections that already shared a key string.
|
|
161
|
+
return hash.fnvHash(stable);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Resolve `flock(2)` through Bun FFI when possible.
|
|
166
|
+
*
|
|
167
|
+
* Memoized: the FFI import + libc `dlopen` run once per process. Returns
|
|
168
|
+
* `undefined` on Windows, under plain Node, or when libc cannot be loaded —
|
|
169
|
+
* callers fall through to the next backend. A miss is cached too, so a process
|
|
170
|
+
* that cannot flock never retries the load.
|
|
171
|
+
*/
|
|
172
|
+
const resolveFlock = functionModule.memoize(async (): Promise<FlockFn | undefined> => {
|
|
173
|
+
if (process.platform === "win32") return undefined;
|
|
174
|
+
if (!process.versions.bun) return undefined;
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
// Dynamic specifier so `tsc` (no `@types` for `bun:ffi`) does not resolve it;
|
|
178
|
+
// the import only succeeds under Bun at runtime.
|
|
179
|
+
const specifier = "bun:ffi";
|
|
180
|
+
const ffi = (await import(specifier)) as unknown as BunFfiModule;
|
|
181
|
+
const libPath =
|
|
182
|
+
process.platform === "darwin" ? "libSystem.B.dylib" : `libc.${ffi.suffix}`;
|
|
183
|
+
const lib = ffi.dlopen(libPath, {
|
|
184
|
+
flock: { args: ["i32", "i32"], returns: "i32" },
|
|
185
|
+
});
|
|
186
|
+
return lib.symbols.flock;
|
|
187
|
+
} catch (cause) {
|
|
188
|
+
logger.debug("flock FFI unavailable", { error: error.errorMessage(cause) });
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
async function holdFlock<T>(
|
|
194
|
+
lockPath: string,
|
|
195
|
+
flock: FlockFn,
|
|
196
|
+
deadline: number | undefined,
|
|
197
|
+
fn: () => T | Promise<T>,
|
|
198
|
+
): Promise<T> {
|
|
199
|
+
await ensureParentDir(lockPath);
|
|
200
|
+
const handle = await open(lockPath, "a+");
|
|
201
|
+
try {
|
|
202
|
+
await waitForFlock(handle.fd, flock, lockPath, deadline);
|
|
203
|
+
try {
|
|
204
|
+
return await fn();
|
|
205
|
+
} finally {
|
|
206
|
+
flock(handle.fd, LOCK_UN);
|
|
207
|
+
}
|
|
208
|
+
} finally {
|
|
209
|
+
await handle.close().catch(() => {});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function waitForFlock(
|
|
214
|
+
fd: number,
|
|
215
|
+
flock: FlockFn,
|
|
216
|
+
lockPath: string,
|
|
217
|
+
deadline: number | undefined,
|
|
218
|
+
): Promise<void> {
|
|
219
|
+
for (;;) {
|
|
220
|
+
const rc = flock(fd, LOCK_EX | LOCK_NB);
|
|
221
|
+
if (rc === 0) return;
|
|
222
|
+
assertBeforeDeadline(lockPath, deadline);
|
|
223
|
+
await async.sleep(POLL_MS);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Atomic lock-directory creation — the portable / Windows path.
|
|
229
|
+
*
|
|
230
|
+
* `mkdir` is the same primitive used by `proper-lockfile`: it is atomic across
|
|
231
|
+
* supported local and network filesystems. When stale detection is enabled,
|
|
232
|
+
* the holder refreshes mtime so a live long-running lock is never reclaimed.
|
|
233
|
+
*/
|
|
234
|
+
async function holdLockDirectory<T>(
|
|
235
|
+
lockPath: string,
|
|
236
|
+
deadline: number | undefined,
|
|
237
|
+
staleMs: number | undefined,
|
|
238
|
+
updateMs: number | undefined,
|
|
239
|
+
fn: () => T | Promise<T>,
|
|
240
|
+
): Promise<T> {
|
|
241
|
+
await ensureParentDir(lockPath);
|
|
242
|
+
await acquireLockDirectory(lockPath, deadline, staleMs);
|
|
243
|
+
const stopHeartbeat = startHeartbeat(lockPath, staleMs, updateMs);
|
|
244
|
+
try {
|
|
245
|
+
return await fn();
|
|
246
|
+
} finally {
|
|
247
|
+
stopHeartbeat();
|
|
248
|
+
await rm(lockPath, { recursive: true, force: true }).catch(() => {});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function acquireLockDirectory(
|
|
253
|
+
lockPath: string,
|
|
254
|
+
deadline: number | undefined,
|
|
255
|
+
staleMs: number | undefined,
|
|
256
|
+
): Promise<void> {
|
|
257
|
+
for (;;) {
|
|
258
|
+
try {
|
|
259
|
+
await mkdir(lockPath);
|
|
260
|
+
return;
|
|
261
|
+
} catch (cause) {
|
|
262
|
+
const err = cause as NodeJS.ErrnoException;
|
|
263
|
+
if (err.code !== "EEXIST") throw error.toError(cause);
|
|
264
|
+
if (staleMs !== undefined) await maybeReclaimStale(lockPath, staleMs);
|
|
265
|
+
assertBeforeDeadline(lockPath, deadline);
|
|
266
|
+
await async.sleep(POLL_MS);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function maybeReclaimStale(lockPath: string, staleMs: number): Promise<void> {
|
|
272
|
+
try {
|
|
273
|
+
const { mtimeMs } = await stat(lockPath);
|
|
274
|
+
const age = Date.now() - mtimeMs;
|
|
275
|
+
if (age < staleMs) return;
|
|
276
|
+
logger.debug("reclaiming stale lock directory", { path: lockPath, ageMs: age });
|
|
277
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
278
|
+
} catch (cause) {
|
|
279
|
+
const err = cause as NodeJS.ErrnoException;
|
|
280
|
+
if (err.code !== "ENOENT") throw error.toError(cause);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function startHeartbeat(
|
|
285
|
+
lockPath: string,
|
|
286
|
+
staleMs: number | undefined,
|
|
287
|
+
updateMs: number | undefined,
|
|
288
|
+
): () => void {
|
|
289
|
+
if (staleMs === undefined) return () => {};
|
|
290
|
+
const intervalMs = updateMs ?? Math.max(1, Math.floor(staleMs / 2));
|
|
291
|
+
const timer = setInterval(() => {
|
|
292
|
+
const now = new Date();
|
|
293
|
+
void utimes(lockPath, now, now).catch((cause) => {
|
|
294
|
+
logger.warn("file lock heartbeat failed", {
|
|
295
|
+
path: lockPath,
|
|
296
|
+
error: error.errorMessage(cause),
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
}, intervalMs);
|
|
300
|
+
timer.unref();
|
|
301
|
+
return () => clearInterval(timer);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function assertBeforeDeadline(lockPath: string, deadline: number | undefined): void {
|
|
305
|
+
if (deadline !== undefined && Date.now() >= deadline) {
|
|
306
|
+
throw new Error(`Timed out waiting for file lock: ${lockPath}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function ensureParentDir(lockPath: string): Promise<void> {
|
|
311
|
+
await mkdir(dirname(lockPath), { recursive: true });
|
|
312
|
+
}
|