@openclaw/fs-safe 0.6.0 → 0.7.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 +7 -0
- package/dist/archive-deadline.d.ts +3 -0
- package/dist/archive-deadline.d.ts.map +1 -1
- package/dist/archive-deadline.js +44 -8
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +1 -0
- package/dist/archive-errors.d.ts +1 -0
- package/dist/archive-errors.d.ts.map +1 -1
- package/dist/archive-errors.js +3 -0
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +26 -16
- package/dist/archive-limits.d.ts +11 -3
- package/dist/archive-limits.d.ts.map +1 -1
- package/dist/archive-limits.js +24 -0
- package/dist/archive-native.d.ts +3 -2
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +24 -9
- package/dist/archive-policy.d.ts +2 -0
- package/dist/archive-policy.d.ts.map +1 -1
- package/dist/archive-policy.js +9 -1
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +69 -41
- package/dist/archive-staging.d.ts +3 -0
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +91 -43
- package/dist/archive-tar-admission.d.ts +7 -0
- package/dist/archive-tar-admission.d.ts.map +1 -0
- package/dist/archive-tar-admission.js +43 -0
- package/dist/archive-tar-gnu.d.ts +2 -0
- package/dist/archive-tar-gnu.d.ts.map +1 -0
- package/dist/archive-tar-gnu.js +20 -0
- package/dist/archive-tar-header.d.ts +8 -0
- package/dist/archive-tar-header.d.ts.map +1 -0
- package/dist/archive-tar-header.js +47 -0
- package/dist/archive-tar-meta.d.ts +29 -1
- package/dist/archive-tar-meta.d.ts.map +1 -1
- package/dist/archive-tar-meta.js +104 -35
- package/dist/archive-tar-runtime.d.ts +3 -0
- package/dist/archive-tar-runtime.d.ts.map +1 -1
- package/dist/archive-tar-runtime.js +3 -0
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +8 -2
- package/dist/archive-zip-names.js +2 -2
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +38 -26
- package/dist/atomic.d.ts +1 -1
- package/dist/atomic.d.ts.map +1 -1
- package/dist/bounded-read-stream.d.ts.map +1 -1
- package/dist/bounded-read-stream.js +2 -4
- package/dist/bounded-read.d.ts.map +1 -1
- package/dist/bounded-read.js +3 -10
- package/dist/byte-budget.d.ts +5 -0
- package/dist/byte-budget.d.ts.map +1 -0
- package/dist/byte-budget.js +9 -0
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +10 -5
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +9 -5
- package/dist/file-store-limit.d.ts +2 -0
- package/dist/file-store-limit.d.ts.map +1 -0
- package/dist/file-store-limit.js +8 -0
- package/dist/file-store-sync-write.d.ts.map +1 -1
- package/dist/file-store-sync-write.js +38 -8
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +26 -26
- package/dist/json-durable-queue-directory.d.ts +2 -0
- package/dist/json-durable-queue-directory.d.ts.map +1 -0
- package/dist/json-durable-queue-directory.js +20 -0
- package/dist/json-durable-queue-ownership.d.ts +14 -0
- package/dist/json-durable-queue-ownership.d.ts.map +1 -0
- package/dist/json-durable-queue-ownership.js +168 -0
- package/dist/json-durable-queue-retirement.d.ts +9 -0
- package/dist/json-durable-queue-retirement.d.ts.map +1 -0
- package/dist/json-durable-queue-retirement.js +126 -0
- package/dist/json-durable-queue-transfer-lock.d.ts +2 -0
- package/dist/json-durable-queue-transfer-lock.d.ts.map +1 -0
- package/dist/json-durable-queue-transfer-lock.js +19 -0
- package/dist/json-durable-queue.d.ts +1 -0
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +90 -57
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +27 -8
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +4 -2
- package/dist/native-binding.d.ts +12 -3
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/output-sibling.d.ts.map +1 -1
- package/dist/output-sibling.js +11 -110
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +4 -2
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +27 -48
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +8 -9
- package/dist/private-temp-workspace.d.ts +3 -1
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +81 -56
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +2 -4
- package/dist/read-opened-file.d.ts.map +1 -1
- package/dist/read-opened-file.js +6 -4
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +136 -82
- package/dist/replace-file-copy-fallback.d.ts +3 -1
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +21 -36
- package/dist/replace-file-copy-source.d.ts +21 -0
- package/dist/replace-file-copy-source.d.ts.map +1 -0
- package/dist/replace-file-copy-source.js +112 -0
- package/dist/replace-file-descriptor.d.ts +13 -3
- package/dist/replace-file-descriptor.d.ts.map +1 -1
- package/dist/replace-file-descriptor.js +32 -7
- package/dist/replace-file-rename-policy.d.ts +7 -0
- package/dist/replace-file-rename-policy.d.ts.map +1 -0
- package/dist/replace-file-rename-policy.js +30 -0
- package/dist/replace-file-temp-owner.d.ts +46 -0
- package/dist/replace-file-temp-owner.d.ts.map +1 -0
- package/dist/replace-file-temp-owner.js +346 -0
- package/dist/replace-file.d.ts +6 -1
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +72 -58
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +40 -26
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +6 -3
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +6 -5
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +17 -13
- package/dist/sibling-staged-file.d.ts +15 -0
- package/dist/sibling-staged-file.d.ts.map +1 -0
- package/dist/sibling-staged-file.js +148 -0
- package/dist/sibling-temp.d.ts +3 -0
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +30 -74
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +48 -27
- package/dist/sidecar-lock-handle.d.ts +6 -2
- package/dist/sidecar-lock-handle.d.ts.map +1 -1
- package/dist/sidecar-lock-handle.js +17 -3
- package/dist/sidecar-lock-policy.d.ts +2 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +29 -0
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +29 -7
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +28 -16
- package/dist/suppressed-error.d.ts +6 -0
- package/dist/suppressed-error.d.ts.map +1 -0
- package/dist/suppressed-error.js +15 -0
- package/dist/temp-cleanup.d.ts +2 -0
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +25 -10
- package/dist/temp-workspace-owner.d.ts +23 -0
- package/dist/temp-workspace-owner.d.ts.map +1 -0
- package/dist/temp-workspace-owner.js +320 -0
- package/dist/temp.d.ts +1 -1
- package/dist/temp.d.ts.map +1 -1
- package/dist/test-hooks.d.ts +5 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/docs/advanced.md +8 -1
- package/docs/archive.md +194 -26
- package/docs/atomic.md +11 -2
- package/docs/durability.md +1 -1
- package/docs/file-store.md +2 -0
- package/docs/install.md +8 -4
- package/docs/json.md +8 -4
- package/docs/native-helper.md +15 -3
- package/docs/native.md +33 -6
- package/docs/output.md +10 -0
- package/docs/public-api.md +6 -3
- package/docs/quickstart.md +1 -1
- package/docs/reading.md +1 -1
- package/docs/regular-file.md +9 -2
- package/docs/root.md +2 -0
- package/docs/secret-file.md +5 -1
- package/docs/secure-file.md +4 -2
- package/docs/security-model.md +18 -0
- package/docs/sidecar-lock.md +4 -0
- package/docs/store.md +13 -0
- package/docs/temp.md +128 -14
- package/package.json +10 -10
package/dist/sibling-temp.js
CHANGED
|
@@ -2,85 +2,40 @@ import crypto, { randomUUID } from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard } from "./directory-guard.js";
|
|
5
|
-
import { syncDirectoryBestEffort } from "./directory-durability.js";
|
|
6
|
-
import { withAsyncDirectoryGuards } from "./guarded-mutation.js";
|
|
7
5
|
import { sanitizeUntrustedFileName } from "./filename.js";
|
|
6
|
+
import { applyDirectoryMode } from "./replace-file-descriptor.js";
|
|
8
7
|
import { root } from "./root.js";
|
|
9
8
|
import { assertSafePathPrefix } from "./safe-path-segment.js";
|
|
10
9
|
import { resolveSecureTempRoot } from "./secure-temp-dir.js";
|
|
11
|
-
import {
|
|
10
|
+
import { writeCallbackSibling } from "./sibling-staged-file.js";
|
|
11
|
+
import { tempFile } from "./temp-target.js";
|
|
12
12
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
13
|
-
import { serializePathWrite } from "./write-queue.js";
|
|
14
13
|
function buildTempPath(dir, tempPrefix) {
|
|
15
14
|
const safePrefix = assertSafePathPrefix(tempPrefix ?? ".fs-safe-stream", {
|
|
16
15
|
label: "sibling temp prefix",
|
|
17
16
|
});
|
|
18
17
|
return path.join(dir, `${safePrefix}.${process.pid}.${randomUUID()}.tmp`);
|
|
19
18
|
}
|
|
20
|
-
async function syncFileBestEffort(filePath) {
|
|
21
|
-
const handle = await fs.open(filePath, "r+");
|
|
22
|
-
try {
|
|
23
|
-
await handle.sync();
|
|
24
|
-
}
|
|
25
|
-
catch (error) {
|
|
26
|
-
if (error.code !== "EPERM") {
|
|
27
|
-
throw error;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
finally {
|
|
31
|
-
await handle.close();
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
function assertFinalPathIsSibling(dir, filePath) {
|
|
35
|
-
const resolvedDir = path.resolve(dir);
|
|
36
|
-
const resolvedFile = path.resolve(filePath);
|
|
37
|
-
if (path.dirname(resolvedFile) !== resolvedDir) {
|
|
38
|
-
throw new Error("Final path must be in the sibling temp directory.");
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
19
|
export async function writeSiblingTempFile(options) {
|
|
42
20
|
const dir = path.resolve(options.dir);
|
|
43
21
|
await fs.mkdir(dir, { recursive: true, mode: options.dirMode ?? 0o700 });
|
|
44
22
|
if (options.chmodDir !== false) {
|
|
45
|
-
await
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
let tempExists = false;
|
|
51
|
-
try {
|
|
52
|
-
tempExists = true;
|
|
53
|
-
const result = await options.writeTemp(tempPath);
|
|
54
|
-
unregisterTempPath.setIdentity(await fs.lstat(tempPath, { bigint: true }));
|
|
55
|
-
if (options.mode !== undefined) {
|
|
56
|
-
await fs.chmod(tempPath, options.mode).catch(() => undefined);
|
|
57
|
-
}
|
|
58
|
-
if (options.syncTempFile) {
|
|
59
|
-
await syncFileBestEffort(tempPath);
|
|
60
|
-
}
|
|
61
|
-
const filePath = path.resolve(options.resolveFinalPath(result));
|
|
62
|
-
assertFinalPathIsSibling(dir, filePath);
|
|
63
|
-
await serializePathWrite(filePath, async () => {
|
|
64
|
-
await withAsyncDirectoryGuards([dirGuard], async () => {
|
|
65
|
-
await fs.rename(tempPath, filePath);
|
|
66
|
-
});
|
|
67
|
-
tempExists = false;
|
|
68
|
-
unregisterTempPath();
|
|
69
|
-
if (options.mode !== undefined) {
|
|
70
|
-
await fs.chmod(filePath, options.mode).catch(() => undefined);
|
|
71
|
-
}
|
|
72
|
-
if (options.syncParentDir) {
|
|
73
|
-
await syncDirectoryBestEffort(dir);
|
|
74
|
-
}
|
|
23
|
+
await applyDirectoryMode({
|
|
24
|
+
fsModule: fs,
|
|
25
|
+
dirPath: dir,
|
|
26
|
+
mode: options.dirMode ?? 0o700,
|
|
27
|
+
ignoreChmodError: true,
|
|
75
28
|
});
|
|
76
|
-
return { filePath, result };
|
|
77
|
-
}
|
|
78
|
-
finally {
|
|
79
|
-
if (tempExists) {
|
|
80
|
-
await fs.rm(tempPath, { force: true }).catch(() => undefined);
|
|
81
|
-
}
|
|
82
|
-
unregisterTempPath();
|
|
83
29
|
}
|
|
30
|
+
return await writeCallbackSibling({
|
|
31
|
+
tempPath: buildTempPath(dir, options.tempPrefix),
|
|
32
|
+
write: options.writeTemp,
|
|
33
|
+
resolveFinalPath: options.resolveFinalPath,
|
|
34
|
+
mode: options.mode,
|
|
35
|
+
ignoreModeError: true,
|
|
36
|
+
syncTempFile: options.syncTempFile === true,
|
|
37
|
+
syncParentDir: options.syncParentDir === true,
|
|
38
|
+
});
|
|
84
39
|
}
|
|
85
40
|
function buildSiblingTempPath(params) {
|
|
86
41
|
const id = crypto.randomUUID();
|
|
@@ -107,18 +62,20 @@ export async function writeViaSiblingTempPath(params) {
|
|
|
107
62
|
throw new Error("Target path is outside the allowed root");
|
|
108
63
|
}
|
|
109
64
|
const rootGuard = await createAsyncDirectoryGuard(rootDir);
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
fallbackFileName: params.fallbackFileName ?? "output.bin",
|
|
118
|
-
tempPrefix: params.tempPrefix ?? ".fs-safe-output-",
|
|
65
|
+
const workspace = await tempFile({
|
|
66
|
+
rootDir: resolveSecureTempRoot({
|
|
67
|
+
fallbackPrefix: "fs-safe-output",
|
|
68
|
+
unsafeFallbackLabel: "sibling temp output dir",
|
|
69
|
+
warn: () => undefined,
|
|
70
|
+
}),
|
|
71
|
+
prefix: "fs-safe-output",
|
|
119
72
|
});
|
|
120
|
-
const unregisterTempPath = registerTempPathForExit(tempDir, { recursive: true });
|
|
121
73
|
try {
|
|
74
|
+
const tempPath = buildSiblingTempPath({
|
|
75
|
+
targetPath: path.join(workspace.dir, path.basename(targetPath)),
|
|
76
|
+
fallbackFileName: params.fallbackFileName ?? "output.bin",
|
|
77
|
+
tempPrefix: params.tempPrefix ?? ".fs-safe-output-",
|
|
78
|
+
});
|
|
122
79
|
await getFsSafeTestHooks()?.beforeSiblingTempWrite?.(tempPath);
|
|
123
80
|
await params.writeTemp(tempPath);
|
|
124
81
|
await assertAsyncDirectoryGuard(rootGuard);
|
|
@@ -127,7 +84,6 @@ export async function writeViaSiblingTempPath(params) {
|
|
|
127
84
|
await assertAsyncDirectoryGuard(rootGuard);
|
|
128
85
|
}
|
|
129
86
|
finally {
|
|
130
|
-
await
|
|
131
|
-
unregisterTempPath();
|
|
87
|
+
await workspace.cleanup();
|
|
132
88
|
}
|
|
133
89
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidecar-lock-acquire.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-acquire.ts"],"names":[],"mappings":"AAGA,OAAO,EAA6B,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1F,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"sidecar-lock-acquire.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-acquire.ts"],"names":[],"mappings":"AAGA,OAAO,EAA6B,KAAK,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1F,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAS3C,OAAO,EAUL,KAAK,mBAAmB,EACzB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,yBAAyB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAG5F,KAAK,iBAAiB,GAAG,IAAI,CAAC,gBAAgB,EAAE,OAAO,GAAG,MAAM,GAAG,WAAW,CAAC,CAAC;AAEhF,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,iBAAiB,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,cAAc,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,QAAQ,CAAC,EAAE,IAAI,CAAC;IAChB,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACxC,eAAe,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC;CAClC,CAAC;AAEF,KAAK,6BAA6B,GAAG;IACnC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACnC,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,2BAA2B,IAAI,IAAI,CAAC;IACpC,iBAAiB,CAAC,oBAAoB,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,iBAAiB,CAAC;IAC1F,eAAe,CACb,oBAAoB,EAAE,MAAM,EAC5B,IAAI,EAAE,eAAe,EACrB,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAC5B,OAAO,CAAC,OAAO,CAAC,CAAC;CACrB,CAAC;AAaF,wBAAsB,kBAAkB,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/E,OAAO,EAAE,yBAAyB,CAAC,QAAQ,CAAC,EAC5C,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC,iBAAiB,CAAC,CA+Q5B"}
|
|
@@ -2,8 +2,9 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { FsSafeError } from "./errors.js";
|
|
4
4
|
import { createNativeExclusiveFile } from "./native-operations.js";
|
|
5
|
-
import { computeSidecarLockDelayMs, defaultSidecarLockShouldReclaim, isTransientLockFileDenial, maxTransientLockDenials, } from "./sidecar-lock-policy.js";
|
|
5
|
+
import { computeSidecarLockDelayMs, defaultSidecarLockShouldReclaim, isTransientLockFileDenial, maxTransientLockDenials, validateSidecarLockRetryOptions, validateSidecarLockTimeoutMs, } from "./sidecar-lock-policy.js";
|
|
6
6
|
import { readSidecarLockSnapshot, relativeSidecarLockPath, releaseSidecarReclaimGuard, removeSidecarLockIfUnchanged, removeStaleSidecarLockIfAllowed, serializeSidecarLockPayload, sidecarLockSnapshotStillPresent, sidecarReclaimGuardExists, tryAcquireSidecarReclaimGuard, } from "./sidecar-lock-reclaim.js";
|
|
7
|
+
import { createSuppressedError } from "./suppressed-error.js";
|
|
7
8
|
async function resolveNormalizedTargetPath(targetPath) {
|
|
8
9
|
const resolved = path.resolve(targetPath);
|
|
9
10
|
const dir = path.dirname(resolved);
|
|
@@ -16,19 +17,34 @@ async function resolveNormalizedTargetPath(targetPath) {
|
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
19
|
export async function acquireSidecarLock(options, context) {
|
|
20
|
+
const retry = options.retry ?? {};
|
|
21
|
+
validateSidecarLockRetryOptions(retry);
|
|
22
|
+
validateSidecarLockTimeoutMs(options.timeoutMs);
|
|
19
23
|
context.ensureExitCleanupRegistered();
|
|
20
24
|
const normalizedTargetPath = await resolveNormalizedTargetPath(options.targetPath);
|
|
21
25
|
const lockPath = options.lockPath ?? `${normalizedTargetPath}.lock`;
|
|
22
|
-
|
|
26
|
+
let held = context.held.get(normalizedTargetPath);
|
|
23
27
|
if (held &&
|
|
24
28
|
options.reentrantOwner !== undefined &&
|
|
25
29
|
held.reentrantOwner !== undefined &&
|
|
26
30
|
options.reentrantOwner === held.reentrantOwner) {
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
// A final release may already have decremented the count to zero and be
|
|
32
|
+
// removing the sidecar. Do not admit a new reentrant handle until that
|
|
33
|
+
// cleanup settles: successful cleanup requires a fresh acquisition, while
|
|
34
|
+
// failed cleanup leaves the existing sidecar held for a retry.
|
|
35
|
+
if (held.releasePromise) {
|
|
36
|
+
await held.releasePromise.catch(() => undefined);
|
|
37
|
+
held = context.held.get(normalizedTargetPath);
|
|
38
|
+
}
|
|
39
|
+
if (held &&
|
|
40
|
+
options.reentrantOwner !== undefined &&
|
|
41
|
+
held.reentrantOwner !== undefined &&
|
|
42
|
+
options.reentrantOwner === held.reentrantOwner) {
|
|
43
|
+
held.refCount += 1;
|
|
44
|
+
return context.handleForHeldLock(normalizedTargetPath, held);
|
|
45
|
+
}
|
|
29
46
|
}
|
|
30
47
|
const startedAt = Date.now();
|
|
31
|
-
const retry = options.retry ?? {};
|
|
32
48
|
const maxRetries = options.timeoutMs === Number.POSITIVE_INFINITY ? undefined : retry.retries;
|
|
33
49
|
const reclaimGuardPath = `${lockPath}.reclaim`;
|
|
34
50
|
let ownsReclaimGuard = false;
|
|
@@ -149,31 +165,36 @@ export async function acquireSidecarLock(options, context) {
|
|
|
149
165
|
return returnedHandle;
|
|
150
166
|
}
|
|
151
167
|
catch (err) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
168
|
+
try {
|
|
169
|
+
if (handle) {
|
|
170
|
+
const failedSnapshot = { payload: null };
|
|
171
|
+
try {
|
|
172
|
+
failedSnapshot.stat = await handle.stat();
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// Best-effort cleanup of a failed exclusive create.
|
|
176
|
+
}
|
|
177
|
+
const current = context.held.get(normalizedTargetPath);
|
|
178
|
+
if (current?.handle === handle) {
|
|
179
|
+
context.held.delete(normalizedTargetPath);
|
|
180
|
+
}
|
|
181
|
+
await handle.close().catch(() => undefined);
|
|
182
|
+
// The file may be empty or partial JSON, so remove by the identity
|
|
183
|
+
// captured from our exclusive handle rather than by pathname alone.
|
|
184
|
+
await removeSidecarLockIfUnchanged(lockPath, failedSnapshot, {
|
|
185
|
+
lockRoot: options.lockRoot,
|
|
186
|
+
parsePayload: options.parsePayload,
|
|
187
|
+
});
|
|
159
188
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
189
|
+
else if (createdSnapshot) {
|
|
190
|
+
await removeSidecarLockIfUnchanged(lockPath, createdSnapshot, {
|
|
191
|
+
lockRoot: options.lockRoot,
|
|
192
|
+
parsePayload: options.parsePayload,
|
|
193
|
+
});
|
|
163
194
|
}
|
|
164
|
-
await handle.close().catch(() => undefined);
|
|
165
|
-
// The file may be empty or partial JSON, so remove by the identity
|
|
166
|
-
// captured from our exclusive handle rather than by pathname alone.
|
|
167
|
-
await removeSidecarLockIfUnchanged(lockPath, failedSnapshot, {
|
|
168
|
-
lockRoot: options.lockRoot,
|
|
169
|
-
parsePayload: options.parsePayload,
|
|
170
|
-
});
|
|
171
195
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
lockRoot: options.lockRoot,
|
|
175
|
-
parsePayload: options.parsePayload,
|
|
176
|
-
});
|
|
196
|
+
catch (cleanupError) {
|
|
197
|
+
throw createSuppressedError(cleanupError, err, "file lock acquisition and cleanup both failed");
|
|
177
198
|
}
|
|
178
199
|
if (lockFileCreateDenied && withinDenialBudget()) {
|
|
179
200
|
await retryOrRethrowDenial(err);
|
|
@@ -5,7 +5,9 @@ export declare function createSidecarLockHandle(params: {
|
|
|
5
5
|
lockPath: string;
|
|
6
6
|
normalizedTargetPath: string;
|
|
7
7
|
verifyStillHeld: () => Promise<boolean>;
|
|
8
|
-
release: (
|
|
8
|
+
release: (options?: {
|
|
9
|
+
retry?: boolean;
|
|
10
|
+
}) => Promise<unknown>;
|
|
9
11
|
}): SidecarLockHandle;
|
|
10
12
|
export declare function createHeldSidecarLockHandle(params: {
|
|
11
13
|
normalizedTargetPath: string;
|
|
@@ -15,6 +17,8 @@ export declare function createHeldSidecarLockHandle(params: {
|
|
|
15
17
|
lockRoot?: Root;
|
|
16
18
|
parsePayload?: (raw: string) => unknown;
|
|
17
19
|
};
|
|
18
|
-
release: (
|
|
20
|
+
release: (options?: {
|
|
21
|
+
retry?: boolean;
|
|
22
|
+
}) => Promise<unknown>;
|
|
19
23
|
}): SidecarLockHandle;
|
|
20
24
|
//# sourceMappingURL=sidecar-lock-handle.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidecar-lock-handle.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-handle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,eAAe,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACxC,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"sidecar-lock-handle.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-handle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,eAAe,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACxC,OAAO,EAAE,CAAC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAC9D,GAAG,iBAAiB,CA2BpB;AAED,wBAAgB,2BAA2B,CAAC,MAAM,EAAE;IAClD,oBAAoB,EAAE,MAAM,CAAC;IAC7B,IAAI,EAAE;QACJ,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,mBAAmB,CAAC;QAC9B,QAAQ,CAAC,EAAE,IAAI,CAAC;QAChB,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;KACzC,CAAC;IACF,OAAO,EAAE,CAAC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CAC9D,GAAG,iBAAiB,CAWpB"}
|
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import { sidecarLockSnapshotStillPresent, } from "./sidecar-lock-reclaim.js";
|
|
2
2
|
export function createSidecarLockHandle(params) {
|
|
3
3
|
let released = false;
|
|
4
|
+
let releasePromise;
|
|
5
|
+
let releaseStarted = false;
|
|
4
6
|
const release = async () => {
|
|
5
7
|
if (released)
|
|
6
8
|
return;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
if (!releasePromise) {
|
|
10
|
+
const retry = releaseStarted;
|
|
11
|
+
releaseStarted = true;
|
|
12
|
+
releasePromise = (async () => {
|
|
13
|
+
await params.release({ retry });
|
|
14
|
+
released = true;
|
|
15
|
+
})();
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
await releasePromise;
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
releasePromise = undefined;
|
|
22
|
+
}
|
|
9
23
|
};
|
|
10
24
|
return {
|
|
11
25
|
lockPath: params.lockPath,
|
|
@@ -23,6 +37,6 @@ export function createHeldSidecarLockHandle(params) {
|
|
|
23
37
|
lockRoot: params.held.lockRoot,
|
|
24
38
|
parsePayload: params.held.parsePayload,
|
|
25
39
|
}),
|
|
26
|
-
release: params.release,
|
|
40
|
+
release: (options) => params.release(options),
|
|
27
41
|
});
|
|
28
42
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { SidecarLockRetryOptions } from "./sidecar-lock-types.js";
|
|
2
|
+
export declare function validateSidecarLockRetryOptions(retry: SidecarLockRetryOptions): void;
|
|
3
|
+
export declare function validateSidecarLockTimeoutMs(timeoutMs: number | undefined): void;
|
|
2
4
|
export declare function computeSidecarLockDelayMs(retry: SidecarLockRetryOptions, attempt: number): number;
|
|
3
5
|
export declare const maxTransientLockDenials = 8;
|
|
4
6
|
export declare function isTransientLockFileDenial(error: unknown, lockPath: string): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidecar-lock-policy.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-policy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"sidecar-lock-policy.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-policy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAQvE,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,uBAAuB,GAAG,IAAI,CAkBpF;AAED,wBAAgB,4BAA4B,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAGhF;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,uBAAuB,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAQjG;AASD,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAEzC,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAGnF;AAED,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,GACZ,OAAO,CAGT;AAED,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAU7E;AAED,wBAAsB,+BAA+B,CAAC,MAAM,EAAE;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,OAAO,CAAC,CAQnB"}
|
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
+
function assertFiniteNonNegative(value, label) {
|
|
3
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
4
|
+
throw new RangeError(`${label} must be a finite non-negative number`);
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export function validateSidecarLockRetryOptions(retry) {
|
|
8
|
+
if (retry.retries !== undefined && (!Number.isSafeInteger(retry.retries) || retry.retries < 0)) {
|
|
9
|
+
throw new RangeError("lock retry.retries must be a non-negative safe integer");
|
|
10
|
+
}
|
|
11
|
+
if (retry.factor !== undefined)
|
|
12
|
+
assertFiniteNonNegative(retry.factor, "lock retry.factor");
|
|
13
|
+
if (retry.minTimeout !== undefined) {
|
|
14
|
+
assertFiniteNonNegative(retry.minTimeout, "lock retry.minTimeout");
|
|
15
|
+
}
|
|
16
|
+
if (retry.maxTimeout !== undefined) {
|
|
17
|
+
assertFiniteNonNegative(retry.maxTimeout, "lock retry.maxTimeout");
|
|
18
|
+
}
|
|
19
|
+
if (retry.minTimeout !== undefined &&
|
|
20
|
+
retry.maxTimeout !== undefined &&
|
|
21
|
+
retry.minTimeout > retry.maxTimeout) {
|
|
22
|
+
throw new RangeError("lock retry.minTimeout must not exceed retry.maxTimeout");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function validateSidecarLockTimeoutMs(timeoutMs) {
|
|
26
|
+
if (timeoutMs === undefined || timeoutMs === Number.POSITIVE_INFINITY)
|
|
27
|
+
return;
|
|
28
|
+
assertFiniteNonNegative(timeoutMs, "lock timeoutMs");
|
|
29
|
+
}
|
|
2
30
|
export function computeSidecarLockDelayMs(retry, attempt) {
|
|
31
|
+
validateSidecarLockRetryOptions(retry);
|
|
3
32
|
const minTimeout = retry.minTimeout ?? 50;
|
|
4
33
|
const maxTimeout = retry.maxTimeout ?? 1000;
|
|
5
34
|
const factor = retry.factor ?? 1;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidecar-lock-reclaim.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-reclaim.ts"],"names":[],"mappings":"AACA,OAAe,EAAE,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"sidecar-lock-reclaim.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-reclaim.ts"],"names":[],"mappings":"AACA,OAAe,EAAE,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAO7C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAY3C,MAAM,MAAM,wBAAwB,GAAG;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,KAAK,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAYF,wBAAgB,6BAA6B,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE7E;AAED,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IAC7E,GAAG,EAAE,MAAM,CAAC;IACZ,cAAc,EAAE,MAAM,CAAC;CACxB,CAMA;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAahF;AAED,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,GAChC,OAAO,CAUT;AAED,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;IACP,QAAQ,CAAC,EAAE,IAAI,CAAC;IAChB,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACxC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,4BAA4B,CAAC,EAAE,OAAO,CAAC;CACnC,GACL,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAkFrC;AAED,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,MAAM,EAChB,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,EACvC,OAAO,GAAE;IAAE,aAAa,CAAC,EAAE,OAAO,CAAA;CAAO,GACxC,mBAAmB,GAAG,IAAI,CAiC5B;AAED,wBAAgB,gCAAgC,CAC9C,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,mBAAmB,GAC5B,OAAO,CAKT;AAED,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,mBAAmB,EAC5B,QAAQ,EAAE,mBAAmB,GAC5B,OAAO,CAkBT;AAED,wBAAsB,4BAA4B,CAChD,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,mBAAmB,GAAG,IAAI,EACpC,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,IAAI,CAAC;IAAC,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAA;CAAO,GACzE,OAAO,CAAC,OAAO,CAAC,CAclB;AAED,wBAAsB,+BAA+B,CACnD,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,mBAAmB,GAAG,IAAI,EACpC,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,IAAI,CAAC;IAAC,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAA;CAAO,GACzE,OAAO,CAAC,OAAO,CAAC,CAMlB;AAED,wBAAsB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAUlF;AAED,wBAAsB,6BAA6B,CACjD,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,EAC1B,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC,OAAO,CAAC,CAWlB;AAED,wBAAsB,0BAA0B,CAC9C,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,EAC1B,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC,IAAI,CAAC,CAGf;AAED,wBAAsB,+BAA+B,CAAC,MAAM,EAAE;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,qBAAqB,CAAC,EAAE,CAAC,QAAQ,EAAE,wBAAwB,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3F,QAAQ,CAAC,EAAE,IAAI,CAAC;IAChB,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;CACzC,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,GAAG,cAAc,CAAC,CAkClD"}
|
|
@@ -5,6 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { readFileDescriptorBoundedSync, readFileHandleBounded } from "./bounded-read.js";
|
|
6
6
|
import { FsSafeError } from "./errors.js";
|
|
7
7
|
import { sameFileIdentity } from "./file-identity.js";
|
|
8
|
+
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
8
9
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
9
10
|
const MAX_LOCK_PAYLOAD_BYTES = 1024 * 1024;
|
|
10
11
|
const SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES = 16;
|
|
@@ -59,7 +60,25 @@ export async function readSidecarLockSnapshot(lockPath, options = {}) {
|
|
|
59
60
|
let handle;
|
|
60
61
|
try {
|
|
61
62
|
if (options.lockRoot) {
|
|
62
|
-
const
|
|
63
|
+
const lockRoot = options.lockRoot;
|
|
64
|
+
const relative = relativeSidecarLockPath(lockRoot, lockPath);
|
|
65
|
+
const opened = await lockRoot.open(relative).catch(async (error) => {
|
|
66
|
+
if (error instanceof FsSafeError && error.code === "path-mismatch") {
|
|
67
|
+
// An owner can unlink after open. Prove absence through the same root;
|
|
68
|
+
// acquisition retries create-only, so a later replacement stays guarded.
|
|
69
|
+
try {
|
|
70
|
+
await lockRoot.stat(relative);
|
|
71
|
+
}
|
|
72
|
+
catch (probeError) {
|
|
73
|
+
if (probeError instanceof FsSafeError && probeError.code === "not-found") {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
throw error;
|
|
79
|
+
});
|
|
80
|
+
if (!opened)
|
|
81
|
+
return null;
|
|
63
82
|
try {
|
|
64
83
|
const raw = (await readFileHandleBounded(opened.handle, MAX_LOCK_PAYLOAD_BYTES)).toString("utf8");
|
|
65
84
|
return {
|
|
@@ -132,11 +151,14 @@ export function readSidecarLockSnapshotSync(lockPath, parsePayload, options = {}
|
|
|
132
151
|
}
|
|
133
152
|
return null;
|
|
134
153
|
}
|
|
135
|
-
|
|
136
|
-
? fsSync.constants.O_NOFOLLOW
|
|
137
|
-
: 0;
|
|
138
|
-
fd = fsSync.openSync(lockPath, fsSync.constants.O_RDONLY | noFollow);
|
|
154
|
+
fd = fsSync.openSync(lockPath, resolveReadOpenFlags());
|
|
139
155
|
const opened = fsSync.fstatSync(fd);
|
|
156
|
+
if (!opened.isFile()) {
|
|
157
|
+
if (options.rejectNonFile) {
|
|
158
|
+
throw new FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`);
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
140
162
|
const raw = readFileDescriptorBoundedSync(fd, MAX_LOCK_PAYLOAD_BYTES).toString("utf8");
|
|
141
163
|
const after = fsSync.lstatSync(lockPath);
|
|
142
164
|
if (!sameFileIdentity(before, opened) || !sameFileIdentity(opened, after))
|
|
@@ -191,10 +213,10 @@ export async function removeSidecarLockIfUnchanged(lockPath, observed, options =
|
|
|
191
213
|
return false;
|
|
192
214
|
}
|
|
193
215
|
if (options.lockRoot) {
|
|
194
|
-
await options.lockRoot.remove(relativeSidecarLockPath(options.lockRoot, lockPath))
|
|
216
|
+
await options.lockRoot.remove(relativeSidecarLockPath(options.lockRoot, lockPath));
|
|
195
217
|
}
|
|
196
218
|
else {
|
|
197
|
-
await fs.rm(lockPath, { force: true })
|
|
219
|
+
await fs.rm(lockPath, { force: true });
|
|
198
220
|
}
|
|
199
221
|
return true;
|
|
200
222
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidecar-lock.d.ts","sourceRoot":"","sources":["../src/sidecar-lock.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sidecar-lock.d.ts","sourceRoot":"","sources":["../src/sidecar-lock.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,yBAAyB,EACzB,iBAAiB,EACjB,oBAAoB,EACpB,sBAAsB,EACvB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAC1E,YAAY,EACV,yBAAyB,EACzB,0BAA0B,EAC1B,iBAAiB,EACjB,oBAAoB,EACpB,uBAAuB,EACvB,wBAAwB,EACxB,sBAAsB,GACvB,MAAM,yBAAyB,CAAC;AAsLjC,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM;cAS3B,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,WACpD,yBAAyB,CAAC,QAAQ,CAAC,KAC3C,OAAO,CAAC,iBAAiB,CAAC;eAYL,CAAC,EAAE,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,WACxD,yBAAyB,CAAC,QAAQ,CAAC,MACxC,MAAM,OAAO,CAAC,CAAC,CAAC,KACnB,OAAO,CAAC,CAAC,CAAC;iBAqBW,OAAO,CAAC,IAAI,CAAC;iBAQnB,IAAI;uBAIE,oBAAoB,EAAE;EAW/C;AAED,wBAAsB,eAAe,CAAC,CAAC,EAAE,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/E,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,sBAAsB,CAAC,QAAQ,CAAC,EACzC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC,CAMZ"}
|
package/dist/sidecar-lock.js
CHANGED
|
@@ -3,6 +3,7 @@ import { sameFileIdentity } from "./file-identity.js";
|
|
|
3
3
|
import { removeSidecarLockIfUnchanged, sidecarLockSnapshotMatches, } from "./sidecar-lock-reclaim.js";
|
|
4
4
|
import { acquireSidecarLock } from "./sidecar-lock-acquire.js";
|
|
5
5
|
import { createHeldSidecarLockHandle } from "./sidecar-lock-handle.js";
|
|
6
|
+
import { createSuppressedError } from "./suppressed-error.js";
|
|
6
7
|
const GLOBAL_STATE_KEY = Symbol.for("fsSafe.sidecarLockManagers");
|
|
7
8
|
const GLOBAL_CLEANUP_KEY = Symbol.for("fsSafe.sidecarLockCleanupRegistered");
|
|
8
9
|
const GLOBAL_CLEANUP_HANDLER_KEY = Symbol.for("fsSafe.sidecarLockCleanupHandler");
|
|
@@ -121,23 +122,18 @@ async function releaseHeldLock(state, normalizedTargetPath, held, options = {})
|
|
|
121
122
|
if (current !== held) {
|
|
122
123
|
return false;
|
|
123
124
|
}
|
|
125
|
+
if (held.releasePromise) {
|
|
126
|
+
await held.releasePromise;
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
124
129
|
if (options.force) {
|
|
125
130
|
held.refCount = 0;
|
|
126
131
|
}
|
|
127
|
-
else {
|
|
132
|
+
else if (!options.retry && held.refCount > 0) {
|
|
128
133
|
held.refCount -= 1;
|
|
129
|
-
if (held.refCount > 0) {
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
if (held.releasePromise) {
|
|
134
|
-
await held.releasePromise.catch(() => undefined);
|
|
135
|
-
return true;
|
|
136
134
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
clearInterval(held.compromiseTimer);
|
|
140
|
-
held.compromiseTimer = undefined;
|
|
135
|
+
if (held.refCount > 0) {
|
|
136
|
+
return false;
|
|
141
137
|
}
|
|
142
138
|
held.releasePromise = (async () => {
|
|
143
139
|
await held.handle.close().catch(() => undefined);
|
|
@@ -145,6 +141,13 @@ async function releaseHeldLock(state, normalizedTargetPath, held, options = {})
|
|
|
145
141
|
lockRoot: held.lockRoot,
|
|
146
142
|
parsePayload: held.parsePayload,
|
|
147
143
|
});
|
|
144
|
+
if (state.held.get(normalizedTargetPath) === held) {
|
|
145
|
+
state.held.delete(normalizedTargetPath);
|
|
146
|
+
}
|
|
147
|
+
if (held.compromiseTimer) {
|
|
148
|
+
clearInterval(held.compromiseTimer);
|
|
149
|
+
held.compromiseTimer = undefined;
|
|
150
|
+
}
|
|
148
151
|
})();
|
|
149
152
|
try {
|
|
150
153
|
await held.releasePromise;
|
|
@@ -158,7 +161,7 @@ function handleForHeldLock(state, normalizedTargetPath, held) {
|
|
|
158
161
|
return createHeldSidecarLockHandle({
|
|
159
162
|
normalizedTargetPath,
|
|
160
163
|
held,
|
|
161
|
-
release: async () => await releaseHeldLock(state, normalizedTargetPath, held),
|
|
164
|
+
release: async (options) => await releaseHeldLock(state, normalizedTargetPath, held, { retry: options?.retry }),
|
|
162
165
|
});
|
|
163
166
|
}
|
|
164
167
|
export function createSidecarLockManager(key) {
|
|
@@ -179,12 +182,21 @@ export function createSidecarLockManager(key) {
|
|
|
179
182
|
}
|
|
180
183
|
async function withLock(options, fn) {
|
|
181
184
|
const lock = await acquire(options);
|
|
185
|
+
let result;
|
|
182
186
|
try {
|
|
183
|
-
|
|
187
|
+
result = await fn();
|
|
184
188
|
}
|
|
185
|
-
|
|
186
|
-
|
|
189
|
+
catch (bodyError) {
|
|
190
|
+
try {
|
|
191
|
+
await lock.release();
|
|
192
|
+
}
|
|
193
|
+
catch (releaseError) {
|
|
194
|
+
throw createSuppressedError(releaseError, bodyError, "file lock callback and release both failed");
|
|
195
|
+
}
|
|
196
|
+
throw bodyError;
|
|
187
197
|
}
|
|
198
|
+
await lock.release();
|
|
199
|
+
return result;
|
|
188
200
|
}
|
|
189
201
|
async function drain() {
|
|
190
202
|
for (const [normalizedTargetPath, held] of Array.from(state.held.entries())) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"suppressed-error.d.ts","sourceRoot":"","sources":["../src/suppressed-error.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,mBAAmB,GAAG,KAAK,GAAG;IACxC,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,EAAE,OAAO,CAAC;CACrB,CAAC;AAQF,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,OAAO,EACd,UAAU,EAAE,OAAO,EACnB,OAAO,EAAE,MAAM,GACd,mBAAmB,CAgBrB"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function createSuppressedError(error, suppressed, message) {
|
|
2
|
+
const constructor = globalThis.SuppressedError;
|
|
3
|
+
if (constructor) {
|
|
4
|
+
return new constructor(error, suppressed, message);
|
|
5
|
+
}
|
|
6
|
+
const combined = new Error(message, {
|
|
7
|
+
cause: error instanceof Error ? error : undefined,
|
|
8
|
+
});
|
|
9
|
+
combined.name = "SuppressedError";
|
|
10
|
+
Object.defineProperties(combined, {
|
|
11
|
+
error: { configurable: true, value: error },
|
|
12
|
+
suppressed: { configurable: true, value: suppressed },
|
|
13
|
+
});
|
|
14
|
+
return combined;
|
|
15
|
+
}
|
package/dist/temp-cleanup.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export type TempPathRegistration = (() => void) & {
|
|
|
6
6
|
export declare function registerTempPathForExit(tempPath: string, options?: {
|
|
7
7
|
recursive?: boolean;
|
|
8
8
|
identity?: FileIdentityStat;
|
|
9
|
+
singleLinkFile?: boolean;
|
|
10
|
+
cleanupSync?: () => unknown;
|
|
9
11
|
}): TempPathRegistration;
|
|
10
12
|
export declare function __cleanupRegisteredTempPathsForTest(): void;
|
|
11
13
|
export declare function __cleanupRegisteredTempPathForTest(tempPath: string): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"temp-cleanup.d.ts","sourceRoot":"","sources":["../src/temp-cleanup.ts"],"names":[],"mappings":"AACA,OAAO,EAA8B,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEvF,MAAM,MAAM,uBAAuB,GAAG,gBAAgB,CAAC;AAEvD,MAAM,MAAM,oBAAoB,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG;IAChD,WAAW,CAAC,QAAQ,EAAE,gBAAgB,GAAG,IAAI,CAAC;CAC/C,CAAC;
|
|
1
|
+
{"version":3,"file":"temp-cleanup.d.ts","sourceRoot":"","sources":["../src/temp-cleanup.ts"],"names":[],"mappings":"AACA,OAAO,EAA8B,KAAK,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEvF,MAAM,MAAM,uBAAuB,GAAG,gBAAgB,CAAC;AAEvD,MAAM,MAAM,oBAAoB,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG;IAChD,WAAW,CAAC,QAAQ,EAAE,gBAAgB,GAAG,IAAI,CAAC;CAC/C,CAAC;AAkDF,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;IACR,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC;CAC7B,GACA,oBAAoB,CA2BtB;AAED,wBAAgB,mCAAmC,IAAI,IAAI,CAE1D;AAED,wBAAgB,kCAAkC,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAUzE"}
|