@openclaw/fs-safe 0.14.0 → 0.15.0
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/CHANGELOG.md +50 -0
- package/README.md +10 -4
- package/dist/advanced.d.ts +1 -1
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +1 -1
- package/dist/archive-merge.d.ts.map +1 -1
- package/dist/archive-merge.js +113 -46
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +4 -4
- package/dist/archive-zip-directory.d.ts +4 -0
- package/dist/archive-zip-directory.d.ts.map +1 -1
- package/dist/archive-zip-directory.js +2 -0
- package/dist/archive-zip-entry.d.ts +6 -2
- package/dist/archive-zip-entry.d.ts.map +1 -1
- package/dist/archive-zip-entry.js +23 -8
- package/dist/archive-zip-integrity.d.ts.map +1 -1
- package/dist/archive-zip-integrity.js +3 -4
- package/dist/archive-zip-loader.d.ts.map +1 -1
- package/dist/archive-zip-loader.js +107 -31
- package/dist/archive-zip-names.d.ts +1 -0
- package/dist/archive-zip-names.d.ts.map +1 -1
- package/dist/archive-zip-names.js +6 -0
- package/dist/archive.js +6 -5
- package/dist/bounded-read-stream.d.ts +0 -1
- package/dist/bounded-read-stream.d.ts.map +1 -1
- package/dist/bounded-read-stream.js +0 -6
- package/dist/copy-publication.d.ts +6 -0
- package/dist/copy-publication.d.ts.map +1 -1
- package/dist/copy-publication.js +3 -0
- package/dist/copy-tree-portable.d.ts.map +1 -1
- package/dist/copy-tree-portable.js +44 -24
- package/dist/copy.d.ts.map +1 -1
- package/dist/copy.js +29 -11
- package/dist/directory-mode-owner.js +5 -5
- package/dist/file-handle-transfer.d.ts +2 -0
- package/dist/file-handle-transfer.d.ts.map +1 -1
- package/dist/file-handle-transfer.js +57 -2
- package/dist/file-identity.d.ts.map +1 -1
- package/dist/file-identity.js +18 -4
- package/dist/file-lock-sync-admission.d.ts +19 -0
- package/dist/file-lock-sync-admission.d.ts.map +1 -0
- package/dist/file-lock-sync-admission.js +93 -0
- package/dist/file-lock-sync-root-acquire.d.ts +4 -0
- package/dist/file-lock-sync-root-acquire.d.ts.map +1 -0
- package/dist/file-lock-sync-root-acquire.js +370 -0
- package/dist/file-lock-sync-root-arbitration.d.ts +18 -0
- package/dist/file-lock-sync-root-arbitration.d.ts.map +1 -0
- package/dist/file-lock-sync-root-arbitration.js +66 -0
- package/dist/file-lock-sync-root-held.d.ts +34 -0
- package/dist/file-lock-sync-root-held.d.ts.map +1 -0
- package/dist/file-lock-sync-root-held.js +393 -0
- package/dist/file-lock-sync-root-io.d.ts +44 -0
- package/dist/file-lock-sync-root-io.d.ts.map +1 -0
- package/dist/file-lock-sync-root-io.js +209 -0
- package/dist/file-lock-sync-root-mutation.d.ts +17 -0
- package/dist/file-lock-sync-root-mutation.d.ts.map +1 -0
- package/dist/file-lock-sync-root-mutation.js +277 -0
- package/dist/file-lock-sync-root-options.d.ts +20 -0
- package/dist/file-lock-sync-root-options.d.ts.map +1 -0
- package/dist/file-lock-sync-root-options.js +58 -0
- package/dist/file-lock-sync-root-registration.d.ts +2 -0
- package/dist/file-lock-sync-root-registration.d.ts.map +1 -0
- package/dist/file-lock-sync-root-registration.js +90 -0
- package/dist/file-lock-sync-root.d.ts +36 -0
- package/dist/file-lock-sync-root.d.ts.map +1 -0
- package/dist/file-lock-sync-root.js +361 -0
- package/dist/file-lock-sync-stale-admission.d.ts +24 -0
- package/dist/file-lock-sync-stale-admission.d.ts.map +1 -0
- package/dist/file-lock-sync-stale-admission.js +205 -0
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +245 -205
- package/dist/file-store-prune.d.ts.map +1 -1
- package/dist/file-store-prune.js +5 -1
- package/dist/file-store-sync-write.js +3 -3
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +47 -12
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +22 -15
- package/dist/json-durable-queue-ownership.d.ts +0 -1
- package/dist/json-durable-queue-ownership.d.ts.map +1 -1
- package/dist/json-durable-queue-ownership.js +0 -6
- package/dist/native-binding.d.ts +2 -0
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-parent-admission.d.ts +3 -2
- package/dist/native-parent-admission.d.ts.map +1 -1
- package/dist/native-parent-admission.js +24 -5
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +7 -7
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +7 -11
- package/dist/native-policy-parent-windows.d.ts +14 -0
- package/dist/native-policy-parent-windows.d.ts.map +1 -0
- package/dist/native-policy-parent-windows.js +200 -0
- package/dist/native-rename-outcome.d.ts +4 -0
- package/dist/native-rename-outcome.d.ts.map +1 -0
- package/dist/native-rename-outcome.js +8 -0
- package/dist/native-staged-file.d.ts +2 -2
- package/dist/native-staged-file.d.ts.map +1 -1
- package/dist/native-staged-file.js +42 -40
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +12 -8
- package/dist/path-prefix.d.ts.map +1 -1
- package/dist/path-prefix.js +30 -8
- package/dist/path-suffix-aliases.d.ts +2 -0
- package/dist/path-suffix-aliases.d.ts.map +1 -1
- package/dist/path-suffix-aliases.js +25 -17
- package/dist/permission-exec.d.ts +2 -0
- package/dist/permission-exec.d.ts.map +1 -1
- package/dist/permission-exec.js +150 -21
- package/dist/permissions-windows.js +1 -1
- package/dist/pinned-mutation-admission.d.ts.map +1 -1
- package/dist/pinned-mutation-admission.js +10 -5
- package/dist/pinned-mutation-observation.d.ts +0 -1
- package/dist/pinned-mutation-observation.d.ts.map +1 -1
- package/dist/pinned-mutation-observation.js +0 -19
- package/dist/pinned-mutation-shared-route.d.ts +1 -0
- package/dist/pinned-mutation-shared-route.d.ts.map +1 -1
- package/dist/pinned-mutation-shared-route.js +1 -1
- package/dist/pinned-write-types.d.ts +2 -0
- package/dist/pinned-write-types.d.ts.map +1 -1
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +75 -121
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +1 -15
- package/dist/replace-directory.d.ts.map +1 -1
- package/dist/replace-directory.js +256 -18
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +62 -70
- package/dist/replace-file-copy-source.d.ts.map +1 -1
- package/dist/replace-file-copy-source.js +10 -12
- package/dist/replace-file-temp-owner.d.ts +5 -2
- package/dist/replace-file-temp-owner.d.ts.map +1 -1
- package/dist/replace-file-temp-owner.js +65 -30
- package/dist/replace-file.js +6 -6
- package/dist/retained-directory-replacement.d.ts +26 -0
- package/dist/retained-directory-replacement.d.ts.map +1 -0
- package/dist/retained-directory-replacement.js +193 -0
- package/dist/root-boundary.d.ts +1 -0
- package/dist/root-boundary.d.ts.map +1 -1
- package/dist/root-boundary.js +4 -0
- package/dist/root-context.d.ts +0 -8
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +0 -3
- package/dist/root-create-input.d.ts +6 -0
- package/dist/root-create-input.d.ts.map +1 -1
- package/dist/root-create-input.js +5 -1
- package/dist/root-directory-list.d.ts +1 -0
- package/dist/root-directory-list.d.ts.map +1 -1
- package/dist/root-directory-list.js +1 -0
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +18 -10
- package/dist/root-move-noreplace.d.ts.map +1 -1
- package/dist/root-move-noreplace.js +2 -2
- package/dist/root-path-errors.d.ts +1 -0
- package/dist/root-path-errors.d.ts.map +1 -1
- package/dist/root-path-errors.js +11 -2
- package/dist/root-path-existing.d.ts.map +1 -1
- package/dist/root-path-existing.js +11 -35
- package/dist/root-path.js +1 -13
- package/dist/root-remove.d.ts +1 -0
- package/dist/root-remove.d.ts.map +1 -1
- package/dist/root-remove.js +4 -0
- package/dist/root-walk.d.ts +1 -1
- package/dist/root-walk.d.ts.map +1 -1
- package/dist/root-walk.js +17 -2
- package/dist/root-write-admission.d.ts +0 -2
- package/dist/root-write-admission.d.ts.map +1 -1
- package/dist/root-write-admission.js +1 -15
- package/dist/root-write-complete-parent.d.ts.map +1 -1
- package/dist/root-write-complete-parent.js +7 -23
- package/dist/root-write-verification.d.ts.map +1 -1
- package/dist/root-write-verification.js +29 -42
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +2 -24
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +3 -24
- package/dist/secret-read-policy.d.ts +6 -2
- package/dist/secret-read-policy.d.ts.map +1 -1
- package/dist/secret-read-policy.js +26 -2
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +23 -12
- package/dist/sidecar-lock-acquire.d.ts +2 -28
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +288 -199
- package/dist/sidecar-lock-admission-context.d.ts +19 -0
- package/dist/sidecar-lock-admission-context.d.ts.map +1 -0
- package/dist/sidecar-lock-admission-context.js +60 -0
- package/dist/sidecar-lock-admission-parser.d.ts +43 -0
- package/dist/sidecar-lock-admission-parser.d.ts.map +1 -0
- package/dist/sidecar-lock-admission-parser.js +113 -0
- package/dist/sidecar-lock-admission.d.ts +35 -0
- package/dist/sidecar-lock-admission.d.ts.map +1 -0
- package/dist/sidecar-lock-admission.js +7 -0
- package/dist/sidecar-lock-reclaim.d.ts +9 -4
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +80 -25
- package/dist/sidecar-lock-stale-admission.d.ts +39 -0
- package/dist/sidecar-lock-stale-admission.d.ts.map +1 -0
- package/dist/sidecar-lock-stale-admission.js +232 -0
- package/dist/sidecar-lock-target.d.ts +8 -0
- package/dist/sidecar-lock-target.d.ts.map +1 -0
- package/dist/sidecar-lock-target.js +55 -0
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +100 -16
- package/dist/temp-workspace-descriptor.d.ts.map +1 -1
- package/dist/temp-workspace-descriptor.js +9 -27
- package/dist/temp-workspace-owner.d.ts.map +1 -1
- package/dist/temp-workspace-owner.js +8 -8
- package/dist/walk.d.ts +5 -1
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +19 -6
- package/dist/windows-owner.d.ts.map +1 -1
- package/dist/windows-owner.js +2 -2
- package/docs/advanced.md +1 -1
- package/docs/archive.md +36 -9
- package/docs/atomic.md +85 -8
- package/docs/copy.md +35 -0
- package/docs/file-store.md +19 -0
- package/docs/json-store.md +5 -0
- package/docs/native-helper.md +10 -3
- package/docs/output.md +6 -0
- package/docs/path-prefix.md +10 -0
- package/docs/path-suffix-aliases.md +51 -6
- package/docs/permissions.md +13 -0
- package/docs/public-api.md +3 -2
- package/docs/root.md +22 -3
- package/docs/sidecar-lock.md +109 -4
- package/docs/staged-file.md +7 -3
- package/docs/temp.md +20 -3
- package/docs/walk.md +67 -1
- package/docs/writing.md +5 -1
- package/package.json +10 -10
- package/dist/darwin-acl.d.ts +0 -4
- package/dist/darwin-acl.d.ts.map +0 -1
- package/dist/darwin-acl.js +0 -24
|
@@ -1,9 +1,64 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
2
3
|
import { FsSafeError } from "./errors.js";
|
|
3
4
|
import { assertSynchronousCallbackResult, composeMutationAssertions, rethrowMutationAuthorityError, } from "./mutation-authority.js";
|
|
4
5
|
import { writeAllToFile } from "./write-file-handle.js";
|
|
5
|
-
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
6
|
+
import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
6
7
|
const NO_CALLBACK_ARGUMENTS = Object.freeze([]);
|
|
8
|
+
function createTransferBuffer(sizeHint, limit) {
|
|
9
|
+
return Buffer.allocUnsafe(Math.min(512 * 1024, Math.max(64 * 1024, sizeHint), limit + 1));
|
|
10
|
+
}
|
|
11
|
+
/** Copies caller-owned regular-file descriptors without moving either cursor. */
|
|
12
|
+
export function copyFileDescriptorSync(sourceFd, targetFd, options = {}) {
|
|
13
|
+
const signal = options.signal;
|
|
14
|
+
signal?.throwIfAborted();
|
|
15
|
+
const maxBytes = normalizeMaxBytes(options.maxBytes);
|
|
16
|
+
const onChunk = options.onChunk;
|
|
17
|
+
const assertBeforeMutation = options.assertBeforeMutation;
|
|
18
|
+
const sourceStat = inspectFileIdentitySync(() => {
|
|
19
|
+
signal?.throwIfAborted();
|
|
20
|
+
return fs.fstatSync(sourceFd, { bigint: true });
|
|
21
|
+
});
|
|
22
|
+
if (!sourceStat.isFile())
|
|
23
|
+
throw new FsSafeError("not-file", "copy source descriptor must be a regular file");
|
|
24
|
+
if (maxBytes !== undefined && Number.isFinite(maxBytes) && sourceStat.size > BigInt(maxBytes)) {
|
|
25
|
+
throw new FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes`);
|
|
26
|
+
}
|
|
27
|
+
const targetStat = inspectFileIdentitySync(() => {
|
|
28
|
+
signal?.throwIfAborted();
|
|
29
|
+
return fs.fstatSync(targetFd, { bigint: true });
|
|
30
|
+
});
|
|
31
|
+
if (!targetStat.isFile())
|
|
32
|
+
throw new FsSafeError("not-file", "copy target descriptor must be a regular file");
|
|
33
|
+
if (sourceStat.dev === targetStat.dev && sourceStat.ino === targetStat.ino) {
|
|
34
|
+
throw new FsSafeError("path-alias", "copy source and target descriptors refer to the same file");
|
|
35
|
+
}
|
|
36
|
+
const limit = maxBytes ?? Infinity;
|
|
37
|
+
const buffer = createTransferBuffer(Number(sourceStat.size), limit);
|
|
38
|
+
let position = 0;
|
|
39
|
+
while (true) {
|
|
40
|
+
signal?.throwIfAborted();
|
|
41
|
+
const bytesRead = fs.readSync(sourceFd, buffer, 0, Math.min(buffer.length, limit - position + 1), position);
|
|
42
|
+
if (bytesRead > limit - position) {
|
|
43
|
+
throw new FsSafeError("too-large", `file exceeds limit of ${limit} bytes`);
|
|
44
|
+
}
|
|
45
|
+
if (bytesRead === 0)
|
|
46
|
+
return position;
|
|
47
|
+
const chunk = buffer.subarray(0, bytesRead);
|
|
48
|
+
assertSynchronousCallbackResult(onChunk?.(chunk), "onChunk");
|
|
49
|
+
let offset = 0;
|
|
50
|
+
while (offset < bytesRead) {
|
|
51
|
+
signal?.throwIfAborted();
|
|
52
|
+
assertSynchronousCallbackResult(assertBeforeMutation?.(), "assertBeforeMutation");
|
|
53
|
+
signal?.throwIfAborted();
|
|
54
|
+
const written = fs.writeSync(targetFd, chunk, offset, bytesRead - offset, position + offset);
|
|
55
|
+
if (written <= 0)
|
|
56
|
+
throw new FsSafeError("helper-failed", "file write made no progress");
|
|
57
|
+
offset += written;
|
|
58
|
+
}
|
|
59
|
+
position += bytesRead;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
7
62
|
function defineReceiverValue(receiver, key, value) {
|
|
8
63
|
Object.defineProperty(receiver, key, {
|
|
9
64
|
value, enumerable: true, writable: true, configurable: true,
|
|
@@ -110,7 +165,7 @@ export function transferFileHandle(source, target, options) {
|
|
|
110
165
|
}
|
|
111
166
|
async function transferFileHandleCore(source, target, sizeHint, maxBytes, targetPosition, signal, onChunk, beforeWrite, callbackThis) {
|
|
112
167
|
const limit = maxBytes ?? Infinity;
|
|
113
|
-
const buffer =
|
|
168
|
+
const buffer = createTransferBuffer(sizeHint, limit);
|
|
114
169
|
let position = 0;
|
|
115
170
|
while (true) {
|
|
116
171
|
signal?.throwIfAborted();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-identity.d.ts","sourceRoot":"","sources":["../src/file-identity.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IACrB,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;CACtB,CAAC;AAsBF,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,QAAQ,CAAC,EAAE,cAAc,GAAG,MAAM,CAGlF;AAED,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,gBAAgB,EACtB,KAAK,EAAE,gBAAgB,EACvB,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,OAAO,CAQT;AAED,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,gBAAgB,EACtB,KAAK,EAAE,gBAAgB,EACvB,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,OAAO,
|
|
1
|
+
{"version":3,"file":"file-identity.d.ts","sourceRoot":"","sources":["../src/file-identity.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IACrB,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;CACtB,CAAC;AAsBF,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,QAAQ,CAAC,EAAE,cAAc,GAAG,MAAM,CAGlF;AAED,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,gBAAgB,EACtB,KAAK,EAAE,gBAAgB,EACvB,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,OAAO,CAQT;AAED,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,gBAAgB,EACtB,KAAK,EAAE,gBAAgB,EACvB,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,OAAO,CAqBT"}
|
package/dist/file-identity.js
CHANGED
|
@@ -23,12 +23,26 @@ export function sameFileIdentity(left, right, platform = process.platform) {
|
|
|
23
23
|
!isStatValueProvablyDifferent(left.ino, right.ino, platform));
|
|
24
24
|
}
|
|
25
25
|
export function sameFileIdentityForCleanup(left, right, platform = process.platform) {
|
|
26
|
+
if (platform !== "win32") {
|
|
27
|
+
return sameStatValue(left.dev, right.dev) && sameStatValue(left.ino, right.ino);
|
|
28
|
+
}
|
|
26
29
|
// A zero Windows device or inode is unknown, not proof that a pathname still
|
|
27
30
|
// names the object we created. Cleanup must fail closed rather than delete a
|
|
28
31
|
// replacement that happens to share the known half of the identity.
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
// Capture each potentially adapter-backed observation once. This strengthens
|
|
33
|
+
// the fence against changing accessors; exact legacy getter traces are not a
|
|
34
|
+
// compatibility contract.
|
|
35
|
+
const leftDev = left.dev;
|
|
36
|
+
if (isZero(leftDev))
|
|
31
37
|
return false;
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
const leftIno = left.ino;
|
|
39
|
+
if (isZero(leftIno))
|
|
40
|
+
return false;
|
|
41
|
+
const rightDev = right.dev;
|
|
42
|
+
if (isZero(rightDev))
|
|
43
|
+
return false;
|
|
44
|
+
const rightIno = right.ino;
|
|
45
|
+
if (isZero(rightIno))
|
|
46
|
+
return false;
|
|
47
|
+
return sameStatValue(leftDev, rightDev) && sameStatValue(leftIno, rightIno);
|
|
34
48
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { SidecarLockSnapshot } from "./sidecar-lock-reclaim.js";
|
|
2
|
+
export type SyncHeldLock = {
|
|
3
|
+
fd: number | undefined;
|
|
4
|
+
lockPath: string;
|
|
5
|
+
normalizedTargetPath: string;
|
|
6
|
+
parsePayload?: (raw: string) => unknown;
|
|
7
|
+
refCount: number;
|
|
8
|
+
reentrantOwner?: string;
|
|
9
|
+
snapshot: SidecarLockSnapshot;
|
|
10
|
+
timer?: NodeJS.Timeout;
|
|
11
|
+
};
|
|
12
|
+
export declare function foreignSyncHeldLock(route: "raw" | "root", target: string): object | undefined;
|
|
13
|
+
export declare function getSyncHeldLocks(): Map<string, SyncHeldLock>;
|
|
14
|
+
export declare function getSyncLockAdmissions(): Map<string, object>;
|
|
15
|
+
export declare function syncLockTimeout(lockPath: string, normalizedTargetPath: string): Error;
|
|
16
|
+
export declare function defaultSyncShouldReclaim(snapshot: SidecarLockSnapshot, staleMs: number, nowMs: number): boolean;
|
|
17
|
+
export declare function syncReclaimGuardExists(reclaimGuardPath: string): boolean;
|
|
18
|
+
export declare function ensureSyncLockExitCleanupRegistered(cleanup: () => void, lockPath: string, normalizedTargetPath: string): void;
|
|
19
|
+
//# sourceMappingURL=file-lock-sync-admission.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-lock-sync-admission.d.ts","sourceRoot":"","sources":["../src/file-lock-sync-admission.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAErE,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,MAAM,GAAG,SAAS,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC;CACxB,CAAC;AAUF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAO7F;AAED,wBAAgB,gBAAgB,IAAI,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAQ5D;AAED,wBAAgB,qBAAqB,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAQ3D;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,oBAAoB,EAAE,MAAM,GAAG,KAAK,CAMrF;AAED,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ,OAAO,CAIT;AAED,wBAAgB,sBAAsB,CAAC,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAQxE;AAED,wBAAgB,mCAAmC,CACjD,OAAO,EAAE,MAAM,IAAI,EACnB,QAAQ,EAAE,MAAM,EAChB,oBAAoB,EAAE,MAAM,GAC3B,IAAI,CAwCN"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { sidecarLockPayloadCreatedAtMs } from "./sidecar-lock-policy.js";
|
|
3
|
+
const SYNC_HELD_LOCKS_KEY = Symbol.for("fsSafe.syncSidecarLocks");
|
|
4
|
+
const SYNC_ADMISSIONS_KEY = Symbol.for("fsSafe.syncSidecarLockAdmissions");
|
|
5
|
+
const SYNC_CLEANUP_REGISTERED_KEY = Symbol.for("fsSafe.syncSidecarLockCleanupRegistered");
|
|
6
|
+
const SYNC_CLEANUP_HANDLER_KEY = Symbol.for("fsSafe.syncSidecarLockCleanupHandler");
|
|
7
|
+
const SYNC_CLEANUP_REGISTRATION_KEY = Symbol.for("fsSafe.syncSidecarLockCleanupRegistration");
|
|
8
|
+
const ROOT_SYNC_HELD_LOCKS_KEY = Symbol.for("fsSafe.syncRootSidecarLocks.v1");
|
|
9
|
+
export function foreignSyncHeldLock(route, target) {
|
|
10
|
+
const key = route === "raw" ? ROOT_SYNC_HELD_LOCKS_KEY : SYNC_HELD_LOCKS_KEY;
|
|
11
|
+
const state = globalThis;
|
|
12
|
+
return state[key]?.get(target);
|
|
13
|
+
}
|
|
14
|
+
export function getSyncHeldLocks() {
|
|
15
|
+
const globalWithState = globalThis;
|
|
16
|
+
if (!globalWithState[SYNC_HELD_LOCKS_KEY]) {
|
|
17
|
+
globalWithState[SYNC_HELD_LOCKS_KEY] = new Map();
|
|
18
|
+
}
|
|
19
|
+
return globalWithState[SYNC_HELD_LOCKS_KEY];
|
|
20
|
+
}
|
|
21
|
+
export function getSyncLockAdmissions() {
|
|
22
|
+
const globalWithState = globalThis;
|
|
23
|
+
if (!globalWithState[SYNC_ADMISSIONS_KEY]) {
|
|
24
|
+
globalWithState[SYNC_ADMISSIONS_KEY] = new Map();
|
|
25
|
+
}
|
|
26
|
+
return globalWithState[SYNC_ADMISSIONS_KEY];
|
|
27
|
+
}
|
|
28
|
+
export function syncLockTimeout(lockPath, normalizedTargetPath) {
|
|
29
|
+
return Object.assign(new Error(`file lock timeout for ${normalizedTargetPath}`), {
|
|
30
|
+
code: "file_lock_timeout",
|
|
31
|
+
lockPath,
|
|
32
|
+
normalizedTargetPath,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export function defaultSyncShouldReclaim(snapshot, staleMs, nowMs) {
|
|
36
|
+
const createdAtMs = sidecarLockPayloadCreatedAtMs(snapshot.payload);
|
|
37
|
+
if (createdAtMs !== null)
|
|
38
|
+
return nowMs - createdAtMs > staleMs;
|
|
39
|
+
return !snapshot.stat || nowMs - snapshot.stat.mtimeMs > staleMs;
|
|
40
|
+
}
|
|
41
|
+
export function syncReclaimGuardExists(reclaimGuardPath) {
|
|
42
|
+
try {
|
|
43
|
+
fs.lstatSync(reclaimGuardPath);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (error.code === "ENOENT")
|
|
48
|
+
return false;
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function ensureSyncLockExitCleanupRegistered(cleanup, lockPath, normalizedTargetPath) {
|
|
53
|
+
const globalWithCleanup = globalThis;
|
|
54
|
+
if (globalWithCleanup[SYNC_CLEANUP_REGISTERED_KEY])
|
|
55
|
+
return;
|
|
56
|
+
if (globalWithCleanup[SYNC_CLEANUP_REGISTRATION_KEY]) {
|
|
57
|
+
// process.on() emits newListener synchronously. A nested synchronous
|
|
58
|
+
// acquisition cannot wait for this stack to finish registering.
|
|
59
|
+
throw syncLockTimeout(lockPath, normalizedTargetPath);
|
|
60
|
+
}
|
|
61
|
+
const registration = {};
|
|
62
|
+
globalWithCleanup[SYNC_CLEANUP_REGISTRATION_KEY] = registration;
|
|
63
|
+
try {
|
|
64
|
+
process.on("exit", cleanup);
|
|
65
|
+
globalWithCleanup[SYNC_CLEANUP_HANDLER_KEY] = cleanup;
|
|
66
|
+
globalWithCleanup[SYNC_CLEANUP_REGISTERED_KEY] = true;
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (process.listeners("exit").includes(cleanup)) {
|
|
70
|
+
try {
|
|
71
|
+
process.off("exit", cleanup);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Reconcile the marker with the actual listener below.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (process.listeners("exit").includes(cleanup)) {
|
|
78
|
+
globalWithCleanup[SYNC_CLEANUP_HANDLER_KEY] = cleanup;
|
|
79
|
+
globalWithCleanup[SYNC_CLEANUP_REGISTERED_KEY] = true;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
if (globalWithCleanup[SYNC_CLEANUP_HANDLER_KEY] === cleanup) {
|
|
83
|
+
delete globalWithCleanup[SYNC_CLEANUP_HANDLER_KEY];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
if (globalWithCleanup[SYNC_CLEANUP_REGISTRATION_KEY] === registration) {
|
|
90
|
+
delete globalWithCleanup[SYNC_CLEANUP_REGISTRATION_KEY];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { FileLockSyncAcquireOptions, FileLockSyncHandle } from "./file-lock-sync.js";
|
|
2
|
+
import type { Root } from "./root-impl.js";
|
|
3
|
+
export declare function acquireFileLockSyncWithRoot<TPayload extends Record<string, unknown>>(targetPath: string, inputOptions: FileLockSyncAcquireOptions<TPayload>, lockRoot: Root): FileLockSyncHandle;
|
|
4
|
+
//# sourceMappingURL=file-lock-sync-root-acquire.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-lock-sync-root-acquire.d.ts","sourceRoot":"","sources":["../src/file-lock-sync-root-acquire.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,0BAA0B,EAC1B,kBAAkB,EACnB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAgD3C,wBAAgB,2BAA2B,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAClF,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,0BAA0B,CAAC,QAAQ,CAAC,EAClD,QAAQ,EAAE,IAAI,GACb,kBAAkB,CA8VpB"}
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { FsSafeError } from "./errors.js";
|
|
4
|
+
import { captureRootSyncAcquireOptions } from "./file-lock-sync-root-options.js";
|
|
5
|
+
import { defaultSyncShouldReclaim, foreignSyncHeldLock, getSyncLockAdmissions, syncLockTimeout } from "./file-lock-sync-admission.js";
|
|
6
|
+
import { computeSidecarLockDelayMs, isTransientLockFileDenial, maxTransientLockDenials, } from "./sidecar-lock-policy.js";
|
|
7
|
+
import { serializeSidecarLockPayload, } from "./sidecar-lock-reclaim.js";
|
|
8
|
+
import { createSuppressedError } from "./suppressed-error.js";
|
|
9
|
+
import { sleepSync } from "./timing.js";
|
|
10
|
+
import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
|
|
11
|
+
import { admitFileLockSyncRootPath, assertFileLockSyncRootPathsCurrent, captureFileLockSyncRootAuthority, normalizeFileLockSyncTargetWithRoot, } from "./file-lock-sync-root.js";
|
|
12
|
+
import { cleanupCreatedRootSyncLock, FileLockSyncRootArbitrationCollision, tryReuseCurrentRootSyncHeldLock, } from "./file-lock-sync-root-arbitration.js";
|
|
13
|
+
import { fileLockSyncRootSnapshotStillCurrent, } from "./file-lock-sync-root-io.js";
|
|
14
|
+
import { createFileLockSyncRootDirectory, createFileLockSyncRootFile, fileLockSyncRootGuardExists, fileLockSyncRootDirectoryReceiptStillCurrent, refreshFileLockSyncRootFileReceipt, removeFileLockSyncRootDirectory, removeFileLockSyncRootFile, } from "./file-lock-sync-root-mutation.js";
|
|
15
|
+
import { createRootSyncHeldLockHandle, ensureRootSyncExitCleanupRegistered, getRootSyncHeldLocks, readRootSidecarSnapshotSync, } from "./file-lock-sync-root-held.js";
|
|
16
|
+
export function acquireFileLockSyncWithRoot(targetPath, inputOptions, lockRoot) {
|
|
17
|
+
// The public dispatcher already read lockRoot exactly once. Validate and
|
|
18
|
+
// snapshot that genuine Root before any remaining option or retry getter can
|
|
19
|
+
// mutate its defaults or retained policy inputs.
|
|
20
|
+
const authority = captureFileLockSyncRootAuthority(lockRoot);
|
|
21
|
+
const options = captureRootSyncAcquireOptions(inputOptions);
|
|
22
|
+
ensureRootSyncExitCleanupRegistered();
|
|
23
|
+
assertNoWindowsPathAlias(targetPath);
|
|
24
|
+
if (options.lockPath !== undefined)
|
|
25
|
+
assertNoWindowsPathAlias(options.lockPath);
|
|
26
|
+
const resolvedTargetPath = path.resolve(targetPath);
|
|
27
|
+
const normalizedTargetPath = normalizeFileLockSyncTargetWithRoot(authority, resolvedTargetPath);
|
|
28
|
+
const requestedLockPath = path.resolve(options.lockPath ?? `${normalizedTargetPath}.lock`);
|
|
29
|
+
const lockRootPath = admitFileLockSyncRootPath(authority, requestedLockPath);
|
|
30
|
+
// A followed final alias and its admitted canonical spelling name the same
|
|
31
|
+
// sidecar. Derive their arbitration guard from that shared admitted path,
|
|
32
|
+
// then independently admit the derived mutation target under Root policy.
|
|
33
|
+
const reclaimRootPath = admitFileLockSyncRootPath(authority, `${lockRootPath.path}.reclaim`);
|
|
34
|
+
const guardedPaths = Object.freeze([lockRootPath, reclaimRootPath]);
|
|
35
|
+
const lockPath = lockRootPath.path;
|
|
36
|
+
const heldLocks = getRootSyncHeldLocks();
|
|
37
|
+
const currentTargetHolder = () => heldLocks.get(normalizedTargetPath) ??
|
|
38
|
+
foreignSyncHeldLock("root", normalizedTargetPath);
|
|
39
|
+
const admissions = getSyncLockAdmissions();
|
|
40
|
+
const admissionToken = {};
|
|
41
|
+
let ownsAdmission = false;
|
|
42
|
+
const assertAdmission = () => {
|
|
43
|
+
if (admissions.get(normalizedTargetPath) !== admissionToken) {
|
|
44
|
+
throw syncLockTimeout(lockPath, normalizedTargetPath);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const releaseAdmission = () => {
|
|
48
|
+
if (ownsAdmission && admissions.get(normalizedTargetPath) === admissionToken) {
|
|
49
|
+
admissions.delete(normalizedTargetPath);
|
|
50
|
+
}
|
|
51
|
+
ownsAdmission = false;
|
|
52
|
+
};
|
|
53
|
+
const arbitration = Object.freeze({
|
|
54
|
+
authority,
|
|
55
|
+
heldLocks,
|
|
56
|
+
lockRootPath,
|
|
57
|
+
normalizedTargetPath,
|
|
58
|
+
reentrantOwner: options.reentrantOwner,
|
|
59
|
+
});
|
|
60
|
+
if (heldLocks.has(normalizedTargetPath) && !foreignSyncHeldLock("root", normalizedTargetPath)) {
|
|
61
|
+
assertFileLockSyncRootPathsCurrent(guardedPaths, true, true);
|
|
62
|
+
const initiallyReusable = tryReuseCurrentRootSyncHeldLock(arbitration);
|
|
63
|
+
if (initiallyReusable)
|
|
64
|
+
return initiallyReusable;
|
|
65
|
+
}
|
|
66
|
+
const startedAt = Date.now();
|
|
67
|
+
let attempt = 0;
|
|
68
|
+
let transientDenials = 0;
|
|
69
|
+
let ownedReclaimGuard;
|
|
70
|
+
let reclaimCleanupAttempted = false;
|
|
71
|
+
const reuseCurrentHeld = () => ownedReclaimGuard ? undefined : tryReuseCurrentRootSyncHeldLock(arbitration);
|
|
72
|
+
const waitForRetry = () => {
|
|
73
|
+
releaseAdmission();
|
|
74
|
+
const elapsed = Date.now() - startedAt;
|
|
75
|
+
const timedOut = options.timeoutMs !== undefined && elapsed >= options.timeoutMs;
|
|
76
|
+
if (timedOut || (options.retry.retries !== undefined && attempt >= options.retry.retries)) {
|
|
77
|
+
throw syncLockTimeout(lockPath, normalizedTargetPath);
|
|
78
|
+
}
|
|
79
|
+
const remaining = options.timeoutMs === undefined || options.timeoutMs === Number.POSITIVE_INFINITY
|
|
80
|
+
? Number.POSITIVE_INFINITY
|
|
81
|
+
: Math.max(0, options.timeoutMs - elapsed);
|
|
82
|
+
sleepSync(Math.min(computeSidecarLockDelayMs(options.retry, attempt), remaining));
|
|
83
|
+
attempt += 1;
|
|
84
|
+
};
|
|
85
|
+
const retryLockFileDenial = (error) => {
|
|
86
|
+
if (!isTransientLockFileDenial(error, lockPath) ||
|
|
87
|
+
++transientDenials > maxTransientLockDenials)
|
|
88
|
+
return false;
|
|
89
|
+
try {
|
|
90
|
+
waitForRetry();
|
|
91
|
+
}
|
|
92
|
+
catch (waitError) {
|
|
93
|
+
if (waitError.code === "file_lock_timeout")
|
|
94
|
+
throw error;
|
|
95
|
+
throw waitError;
|
|
96
|
+
}
|
|
97
|
+
return true;
|
|
98
|
+
};
|
|
99
|
+
const releaseReclaimGuard = () => {
|
|
100
|
+
const receipt = ownedReclaimGuard;
|
|
101
|
+
if (!receipt)
|
|
102
|
+
return;
|
|
103
|
+
reclaimCleanupAttempted = true;
|
|
104
|
+
if (!removeFileLockSyncRootDirectory(reclaimRootPath, receipt)) {
|
|
105
|
+
throw new FsSafeError("path-mismatch", "owned sidecar reclaim guard changed before release");
|
|
106
|
+
}
|
|
107
|
+
ownedReclaimGuard = undefined;
|
|
108
|
+
reclaimCleanupAttempted = false;
|
|
109
|
+
};
|
|
110
|
+
const assertOwnedReclaimGuardCurrent = () => {
|
|
111
|
+
const receipt = ownedReclaimGuard;
|
|
112
|
+
if (!receipt || !fileLockSyncRootDirectoryReceiptStillCurrent(reclaimRootPath, receipt)) {
|
|
113
|
+
throw new FsSafeError("path-mismatch", "owned sidecar reclaim guard changed during acquisition");
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
try {
|
|
117
|
+
while (true) {
|
|
118
|
+
if (!ownsAdmission) {
|
|
119
|
+
if (admissions.has(normalizedTargetPath))
|
|
120
|
+
throw syncLockTimeout(lockPath, normalizedTargetPath);
|
|
121
|
+
admissions.set(normalizedTargetPath, admissionToken);
|
|
122
|
+
ownsAdmission = true;
|
|
123
|
+
}
|
|
124
|
+
assertAdmission();
|
|
125
|
+
assertFileLockSyncRootPathsCurrent(guardedPaths, true, true);
|
|
126
|
+
assertAdmission();
|
|
127
|
+
if (ownedReclaimGuard)
|
|
128
|
+
assertOwnedReclaimGuardCurrent();
|
|
129
|
+
if (!ownedReclaimGuard && fileLockSyncRootGuardExists(reclaimRootPath, true)) {
|
|
130
|
+
waitForRetry();
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (currentTargetHolder() !== undefined) {
|
|
134
|
+
const reused = reuseCurrentHeld();
|
|
135
|
+
if (reused)
|
|
136
|
+
return reused;
|
|
137
|
+
}
|
|
138
|
+
const payload = Reflect.apply(options.payload, options.optionsReceiver, []);
|
|
139
|
+
assertAdmission();
|
|
140
|
+
const { raw, ownershipToken } = serializeSidecarLockPayload(payload);
|
|
141
|
+
assertAdmission();
|
|
142
|
+
if (ownedReclaimGuard)
|
|
143
|
+
assertOwnedReclaimGuardCurrent();
|
|
144
|
+
if (currentTargetHolder() !== undefined) {
|
|
145
|
+
const reused = reuseCurrentHeld();
|
|
146
|
+
if (reused)
|
|
147
|
+
return reused;
|
|
148
|
+
waitForRetry();
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
let fd;
|
|
152
|
+
let rootReceipt;
|
|
153
|
+
let unpublishedTimer;
|
|
154
|
+
let lockFileCreateOpenFailure;
|
|
155
|
+
try {
|
|
156
|
+
const created = createFileLockSyncRootFile(lockRootPath, 0o600, {
|
|
157
|
+
assertBeforeOpen: () => {
|
|
158
|
+
assertAdmission();
|
|
159
|
+
if (ownedReclaimGuard)
|
|
160
|
+
assertOwnedReclaimGuardCurrent();
|
|
161
|
+
if (currentTargetHolder() !== undefined)
|
|
162
|
+
throw new FileLockSyncRootArbitrationCollision();
|
|
163
|
+
},
|
|
164
|
+
onOpenFailure: (error) => {
|
|
165
|
+
lockFileCreateOpenFailure = { error };
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
fd = created.fd;
|
|
169
|
+
rootReceipt = created.receipt;
|
|
170
|
+
fs.writeFileSync(fd, raw, "utf8");
|
|
171
|
+
fs.fsyncSync(fd);
|
|
172
|
+
const snapshot = {
|
|
173
|
+
raw,
|
|
174
|
+
payload,
|
|
175
|
+
ownershipToken,
|
|
176
|
+
};
|
|
177
|
+
if (ownedReclaimGuard)
|
|
178
|
+
releaseReclaimGuard();
|
|
179
|
+
rootReceipt = refreshFileLockSyncRootFileReceipt(lockRootPath, rootReceipt);
|
|
180
|
+
const createdHeld = {
|
|
181
|
+
fd,
|
|
182
|
+
lockPath,
|
|
183
|
+
normalizedTargetPath,
|
|
184
|
+
parsePayload: options.parsePayload,
|
|
185
|
+
refCount: 1,
|
|
186
|
+
reentrantOwner: options.reentrantOwner,
|
|
187
|
+
releaseState: "active",
|
|
188
|
+
revision: 0,
|
|
189
|
+
rootAuthority: authority,
|
|
190
|
+
rootPath: lockRootPath,
|
|
191
|
+
rootReceipt,
|
|
192
|
+
snapshot,
|
|
193
|
+
};
|
|
194
|
+
const returnedHandle = createRootSyncHeldLockHandle(createdHeld);
|
|
195
|
+
if (options.onCompromised && (options.compromiseCheckIntervalMs ?? 0) > 0) {
|
|
196
|
+
const timer = setInterval(() => {
|
|
197
|
+
let stillHeld;
|
|
198
|
+
try {
|
|
199
|
+
stillHeld = returnedHandle.verifyStillHeld();
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
stillHeld = false;
|
|
203
|
+
}
|
|
204
|
+
if (!stillHeld && createdHeld.timer) {
|
|
205
|
+
clearInterval(createdHeld.timer);
|
|
206
|
+
createdHeld.timer = undefined;
|
|
207
|
+
Reflect.apply(options.onCompromised, options.optionsReceiver, [{ lockPath, normalizedTargetPath }]);
|
|
208
|
+
}
|
|
209
|
+
}, options.compromiseCheckIntervalMs);
|
|
210
|
+
unpublishedTimer = timer;
|
|
211
|
+
createdHeld.timer = timer;
|
|
212
|
+
timer.unref();
|
|
213
|
+
}
|
|
214
|
+
if (currentTargetHolder() !== undefined) {
|
|
215
|
+
throw new FileLockSyncRootArbitrationCollision();
|
|
216
|
+
}
|
|
217
|
+
assertAdmission();
|
|
218
|
+
heldLocks.set(normalizedTargetPath, createdHeld);
|
|
219
|
+
fd = undefined;
|
|
220
|
+
unpublishedTimer = undefined;
|
|
221
|
+
return returnedHandle;
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
if (fd !== undefined && rootReceipt) {
|
|
225
|
+
const cleanupFd = fd;
|
|
226
|
+
const cleanupReceipt = rootReceipt;
|
|
227
|
+
const cleanupTimer = unpublishedTimer;
|
|
228
|
+
fd = undefined;
|
|
229
|
+
rootReceipt = undefined;
|
|
230
|
+
unpublishedTimer = undefined;
|
|
231
|
+
try {
|
|
232
|
+
cleanupCreatedRootSyncLock(lockRootPath, cleanupFd, cleanupReceipt, cleanupTimer);
|
|
233
|
+
}
|
|
234
|
+
catch (cleanupError) {
|
|
235
|
+
throw createSuppressedError(error, cleanupError, "file lock acquisition and Root cleanup both failed");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (error instanceof FileLockSyncRootArbitrationCollision) {
|
|
239
|
+
const reused = reuseCurrentHeld();
|
|
240
|
+
if (reused)
|
|
241
|
+
return reused;
|
|
242
|
+
waitForRetry();
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const fromLockFileOpen = lockFileCreateOpenFailure !== undefined &&
|
|
246
|
+
lockFileCreateOpenFailure.error === error;
|
|
247
|
+
if (fromLockFileOpen && retryLockFileDenial(error))
|
|
248
|
+
continue;
|
|
249
|
+
if (!fromLockFileOpen || error.code !== "EEXIST")
|
|
250
|
+
throw error;
|
|
251
|
+
if (ownedReclaimGuard) {
|
|
252
|
+
releaseReclaimGuard();
|
|
253
|
+
const reused = reuseCurrentHeld();
|
|
254
|
+
if (reused)
|
|
255
|
+
return reused;
|
|
256
|
+
waitForRetry();
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (currentTargetHolder() !== undefined) {
|
|
260
|
+
const reused = reuseCurrentHeld();
|
|
261
|
+
if (reused)
|
|
262
|
+
return reused;
|
|
263
|
+
waitForRetry();
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
let lockFileOpenDenied = false;
|
|
267
|
+
let current;
|
|
268
|
+
try {
|
|
269
|
+
current = readRootSidecarSnapshotSync(lockRootPath, options.parsePayload, (openError) => {
|
|
270
|
+
lockFileOpenDenied = isTransientLockFileDenial(openError, lockPath);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
catch (readError) {
|
|
274
|
+
if (lockFileOpenDenied && retryLockFileDenial(readError))
|
|
275
|
+
continue;
|
|
276
|
+
throw readError;
|
|
277
|
+
}
|
|
278
|
+
if (currentTargetHolder() !== undefined) {
|
|
279
|
+
const reused = reuseCurrentHeld();
|
|
280
|
+
if (reused)
|
|
281
|
+
return reused;
|
|
282
|
+
waitForRetry();
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
assertAdmission();
|
|
286
|
+
if (!current) {
|
|
287
|
+
waitForRetry();
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const snapshot = current.snapshot;
|
|
291
|
+
const nowMs = Date.now();
|
|
292
|
+
let reclaim;
|
|
293
|
+
if (options.shouldReclaim) {
|
|
294
|
+
reclaim = Reflect.apply(options.shouldReclaim, options.optionsReceiver, [{
|
|
295
|
+
lockPath,
|
|
296
|
+
normalizedTargetPath,
|
|
297
|
+
payload: snapshot.payload,
|
|
298
|
+
staleMs: options.staleMs,
|
|
299
|
+
nowMs,
|
|
300
|
+
heldByThisProcess: false,
|
|
301
|
+
}]);
|
|
302
|
+
assertAdmission();
|
|
303
|
+
if (!fileLockSyncRootSnapshotStillCurrent(lockRootPath, current)) {
|
|
304
|
+
throw new FsSafeError("path-mismatch", "sidecar changed during reclaim policy callback");
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
reclaim = defaultSyncShouldReclaim(snapshot, options.staleMs, nowMs);
|
|
309
|
+
}
|
|
310
|
+
if (reclaim) {
|
|
311
|
+
if (options.staleRecovery === "remove-if-unchanged" &&
|
|
312
|
+
snapshot.raw !== undefined &&
|
|
313
|
+
options.shouldRemoveStaleLock) {
|
|
314
|
+
const guard = createFileLockSyncRootDirectory(reclaimRootPath);
|
|
315
|
+
if (!guard) {
|
|
316
|
+
waitForRetry();
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
ownedReclaimGuard = guard;
|
|
320
|
+
reclaimCleanupAttempted = false;
|
|
321
|
+
const approved = Reflect.apply(options.shouldRemoveStaleLock, options.optionsReceiver, [{
|
|
322
|
+
lockPath,
|
|
323
|
+
normalizedTargetPath,
|
|
324
|
+
raw: snapshot.raw,
|
|
325
|
+
payload: snapshot.payload,
|
|
326
|
+
}]);
|
|
327
|
+
assertAdmission();
|
|
328
|
+
assertOwnedReclaimGuardCurrent();
|
|
329
|
+
if (approved) {
|
|
330
|
+
const removed = removeFileLockSyncRootFile(lockRootPath, current.receipt, snapshot, () => {
|
|
331
|
+
assertAdmission();
|
|
332
|
+
assertOwnedReclaimGuardCurrent();
|
|
333
|
+
if (currentTargetHolder() !== undefined)
|
|
334
|
+
throw new FileLockSyncRootArbitrationCollision();
|
|
335
|
+
});
|
|
336
|
+
if (!removed) {
|
|
337
|
+
throw new FsSafeError("path-mismatch", "stale sidecar changed before removal");
|
|
338
|
+
}
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (!fileLockSyncRootSnapshotStillCurrent(lockRootPath, current)) {
|
|
342
|
+
throw new FsSafeError("path-mismatch", "sidecar changed during stale-removal policy callback");
|
|
343
|
+
}
|
|
344
|
+
releaseReclaimGuard();
|
|
345
|
+
}
|
|
346
|
+
throw Object.assign(new Error(`file lock stale for ${normalizedTargetPath}`), {
|
|
347
|
+
code: "file_lock_stale",
|
|
348
|
+
lockPath,
|
|
349
|
+
normalizedTargetPath,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
waitForRetry();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
if (ownedReclaimGuard && !reclaimCleanupAttempted) {
|
|
358
|
+
try {
|
|
359
|
+
releaseReclaimGuard();
|
|
360
|
+
}
|
|
361
|
+
catch (cleanupError) {
|
|
362
|
+
throw createSuppressedError(error, cleanupError, "file lock acquisition and reclaim cleanup both failed");
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
throw error;
|
|
366
|
+
}
|
|
367
|
+
finally {
|
|
368
|
+
releaseAdmission();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { FsSafeError } from "./errors.js";
|
|
2
|
+
import type { FileLockSyncHandle } from "./file-lock-sync.js";
|
|
3
|
+
import { type FileLockSyncRootAuthority, type FileLockSyncRootPath } from "./file-lock-sync-root.js";
|
|
4
|
+
import type { FileLockSyncRootFileReceipt } from "./file-lock-sync-root-io.js";
|
|
5
|
+
import { type RootSyncHeldLock } from "./file-lock-sync-root-held.js";
|
|
6
|
+
export declare class FileLockSyncRootArbitrationCollision extends FsSafeError {
|
|
7
|
+
constructor();
|
|
8
|
+
}
|
|
9
|
+
export type FileLockSyncRootArbitration = Readonly<{
|
|
10
|
+
authority: FileLockSyncRootAuthority;
|
|
11
|
+
heldLocks: Map<string, RootSyncHeldLock>;
|
|
12
|
+
lockRootPath: FileLockSyncRootPath;
|
|
13
|
+
normalizedTargetPath: string;
|
|
14
|
+
reentrantOwner?: string;
|
|
15
|
+
}>;
|
|
16
|
+
export declare function tryReuseCurrentRootSyncHeldLock(arbitration: FileLockSyncRootArbitration): FileLockSyncHandle | undefined;
|
|
17
|
+
export declare function cleanupCreatedRootSyncLock(lockRootPath: FileLockSyncRootPath, fd: number, receipt: FileLockSyncRootFileReceipt, timer?: NodeJS.Timeout): void;
|
|
18
|
+
//# sourceMappingURL=file-lock-sync-root-arbitration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-lock-sync-root-arbitration.d.ts","sourceRoot":"","sources":["../src/file-lock-sync-root-arbitration.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,EACL,KAAK,yBAAyB,EAC9B,KAAK,oBAAoB,EAC1B,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,6BAA6B,CAAC;AAG/E,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,+BAA+B,CAAC;AAEvC,qBAAa,oCAAqC,SAAQ,WAAW;IACnE,cAEC;CACF;AAED,MAAM,MAAM,2BAA2B,GAAG,QAAQ,CAAC;IACjD,SAAS,EAAE,yBAAyB,CAAC;IACrC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IACzC,YAAY,EAAE,oBAAoB,CAAC;IACnC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC,CAAC;AAMH,wBAAgB,+BAA+B,CAC7C,WAAW,EAAE,2BAA2B,GACvC,kBAAkB,GAAG,SAAS,CAsBhC;AAED,wBAAgB,0BAA0B,CACxC,YAAY,EAAE,oBAAoB,EAClC,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,2BAA2B,EACpC,KAAK,CAAC,EAAE,MAAM,CAAC,OAAO,GACrB,IAAI,CAyBN"}
|