@openclaw/fs-safe 0.4.7 → 0.5.1
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 +58 -0
- package/README.md +82 -43
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +5 -4
- package/dist/archive-errors.d.ts +11 -0
- package/dist/archive-errors.d.ts.map +1 -0
- package/dist/archive-errors.js +16 -0
- package/dist/archive-input.d.ts +12 -0
- package/dist/archive-input.d.ts.map +1 -0
- package/dist/archive-input.js +72 -0
- package/dist/archive-kind.d.ts +1 -1
- package/dist/archive-kind.d.ts.map +1 -1
- package/dist/archive-kind.js +18 -0
- package/dist/archive-limits.d.ts +10 -0
- package/dist/archive-limits.d.ts.map +1 -1
- package/dist/archive-limits.js +18 -0
- package/dist/archive-native.d.ts +18 -0
- package/dist/archive-native.d.ts.map +1 -0
- package/dist/archive-native.js +115 -0
- package/dist/archive-options.d.ts +21 -0
- package/dist/archive-options.d.ts.map +1 -0
- package/dist/archive-options.js +1 -0
- package/dist/archive-policy.d.ts +20 -0
- package/dist/archive-policy.d.ts.map +1 -0
- package/dist/archive-policy.js +29 -0
- package/dist/archive-read.d.ts +6 -0
- package/dist/archive-read.d.ts.map +1 -0
- package/dist/archive-read.js +200 -0
- package/dist/archive-staging.d.ts +2 -5
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +19 -10
- package/dist/archive-tar-meta.d.ts +6 -0
- package/dist/archive-tar-meta.d.ts.map +1 -0
- package/dist/archive-tar-meta.js +143 -0
- package/dist/archive-tar-runtime.d.ts +43 -0
- package/dist/archive-tar-runtime.d.ts.map +1 -0
- package/dist/archive-tar-runtime.js +8 -0
- package/dist/archive-tar.d.ts +5 -1
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +24 -6
- package/dist/archive-zip-entry.d.ts +15 -0
- package/dist/archive-zip-entry.d.ts.map +1 -0
- package/dist/archive-zip-entry.js +17 -0
- package/dist/archive.d.ts +7 -17
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +111 -108
- package/dist/atomic.d.ts +1 -1
- package/dist/atomic.d.ts.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/containment.d.ts +2 -0
- package/dist/containment.d.ts.map +1 -0
- package/dist/containment.js +1 -0
- package/dist/device-path.d.ts.map +1 -1
- package/dist/device-path.js +24 -2
- package/dist/durability.d.ts +2 -0
- package/dist/durability.d.ts.map +1 -1
- package/dist/durability.js +2 -0
- package/dist/errors.d.ts +4 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -0
- package/dist/file-hash.d.ts +10 -0
- package/dist/file-hash.d.ts.map +1 -0
- package/dist/file-hash.js +73 -0
- package/dist/file-lock-sync.d.ts +35 -0
- package/dist/file-lock-sync.d.ts.map +1 -0
- package/dist/file-lock-sync.js +233 -0
- package/dist/file-lock.d.ts +2 -0
- package/dist/file-lock.d.ts.map +1 -1
- package/dist/file-lock.js +1 -0
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +4 -1
- package/dist/guarded-mkdir.d.ts +7 -1
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +45 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +30 -15
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +101 -17
- package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
- package/dist/native/darwin-x64/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
- package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
- package/dist/native-binding.d.ts +79 -0
- package/dist/native-binding.d.ts.map +1 -0
- package/dist/native-binding.js +1 -0
- package/dist/native-config.d.ts +17 -0
- package/dist/native-config.d.ts.map +1 -0
- package/dist/native-config.js +71 -0
- package/dist/native-operations.d.ts +21 -0
- package/dist/native-operations.d.ts.map +1 -0
- package/dist/native-operations.js +115 -0
- package/dist/native-pinned-write.d.ts +5 -0
- package/dist/native-pinned-write.d.ts.map +1 -0
- package/dist/native-pinned-write.js +115 -0
- package/dist/native.d.ts +14 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +204 -0
- package/dist/output-sibling.d.ts +8 -0
- package/dist/output-sibling.d.ts.map +1 -0
- package/dist/output-sibling.js +122 -0
- package/dist/output.d.ts +2 -0
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +32 -5
- package/dist/owner-dacl.d.ts +31 -0
- package/dist/owner-dacl.d.ts.map +1 -0
- package/dist/owner-dacl.js +27 -0
- package/dist/path-policy.js +1 -1
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +3 -2
- package/dist/permissions-public.d.ts +2 -0
- package/dist/permissions-public.d.ts.map +1 -1
- package/dist/permissions-public.js +2 -0
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +6 -0
- package/dist/pinned-operation.d.ts +2 -0
- package/dist/pinned-operation.d.ts.map +1 -0
- package/dist/pinned-operation.js +35 -0
- package/dist/pinned-write.d.ts +3 -16
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +20 -85
- package/dist/private-directory.d.ts +5 -0
- package/dist/private-directory.d.ts.map +1 -0
- package/dist/private-directory.js +13 -0
- package/dist/private-temp-workspace.d.ts +6 -2
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +40 -13
- package/dist/publish-file-failure.d.ts +27 -0
- package/dist/publish-file-failure.d.ts.map +1 -0
- package/dist/publish-file-failure.js +21 -0
- package/dist/publish-file.d.ts +21 -0
- package/dist/publish-file.d.ts.map +1 -0
- package/dist/publish-file.js +351 -0
- package/dist/read-opened-file.d.ts +3 -0
- package/dist/read-opened-file.d.ts.map +1 -1
- package/dist/read-opened-file.js +1 -0
- package/dist/replace-file-copy-fallback.d.ts +34 -0
- package/dist/replace-file-copy-fallback.d.ts.map +1 -0
- package/dist/replace-file-copy-fallback.js +339 -0
- package/dist/replace-file.d.ts +6 -2
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +39 -90
- package/dist/root-context.d.ts +4 -1
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +20 -1
- package/dist/root-impl.d.ts +5 -0
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +128 -225
- package/dist/root-path-existing.d.ts +3 -0
- package/dist/root-path-existing.d.ts.map +1 -0
- package/dist/root-path-existing.js +69 -0
- package/dist/root-path.d.ts +1 -2
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +67 -81
- package/dist/root-walk.d.ts +41 -0
- package/dist/root-walk.d.ts.map +1 -0
- package/dist/root-walk.js +114 -0
- package/dist/root.d.ts +2 -0
- package/dist/root.d.ts.map +1 -1
- package/dist/safe-path-segment.d.ts.map +1 -1
- package/dist/safe-path-segment.js +16 -4
- package/dist/secret-file.d.ts +5 -2
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +20 -2
- package/dist/secret-read-async.d.ts +4 -0
- package/dist/secret-read-async.d.ts.map +1 -0
- package/dist/secret-read-async.js +95 -0
- package/dist/secret.d.ts +2 -1
- package/dist/secret.d.ts.map +1 -1
- package/dist/secret.js +2 -1
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +1 -0
- package/dist/sidecar-lock-handle.d.ts +20 -0
- package/dist/sidecar-lock-handle.d.ts.map +1 -0
- package/dist/sidecar-lock-handle.js +28 -0
- package/dist/sidecar-lock-policy.d.ts +10 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -0
- package/dist/sidecar-lock-policy.js +29 -0
- package/dist/sidecar-lock-reclaim.d.ts +22 -6
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +106 -20
- package/dist/sidecar-lock-types.d.ts +56 -0
- package/dist/sidecar-lock-types.d.ts.map +1 -0
- package/dist/sidecar-lock-types.js +1 -0
- package/dist/sidecar-lock.d.ts +2 -45
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +94 -55
- package/dist/temp-cleanup.d.ts +7 -1
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +35 -5
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +51 -3
- package/dist/temp.d.ts +2 -1
- package/dist/temp.d.ts.map +1 -1
- package/dist/test-hooks.d.ts +3 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/dist/windows-permissions-native.d.ts +9 -0
- package/dist/windows-permissions-native.d.ts.map +1 -0
- package/dist/windows-permissions-native.js +31 -0
- package/docs/archive.md +131 -13
- package/docs/atomic.md +30 -1
- package/docs/config.md +51 -21
- package/docs/durability.md +211 -1
- package/docs/errors.md +36 -6
- package/docs/filename.md +0 -0
- package/docs/index.md +11 -8
- package/docs/install.md +27 -26
- package/docs/json-store.md +18 -3
- package/docs/migrating-to-0.5.md +196 -0
- package/docs/native-helper.md +85 -0
- package/docs/native.md +134 -0
- package/docs/output.md +37 -11
- package/docs/path.md +1 -1
- package/docs/permissions.md +84 -1
- package/docs/quickstart.md +5 -2
- package/docs/reading.md +4 -4
- package/docs/root.md +31 -15
- package/docs/secret-file.md +50 -1
- package/docs/security-model.md +29 -8
- package/docs/sidecar-lock.md +92 -7
- package/docs/temp.md +24 -1
- package/docs/test-hooks.md +22 -2
- package/docs/testing.md +18 -7
- package/docs/types.md +3 -1
- package/docs/walk.md +55 -0
- package/docs/writing.md +3 -3
- package/package.json +10 -4
- package/dist/pinned-helper.d.ts +0 -9
- package/dist/pinned-helper.d.ts.map +0 -1
- package/dist/pinned-helper.js +0 -19
- package/dist/pinned-path.d.ts +0 -7
- package/dist/pinned-path.d.ts.map +0 -1
- package/dist/pinned-path.js +0 -21
- package/dist/pinned-python-config.d.ts +0 -9
- package/dist/pinned-python-config.d.ts.map +0 -1
- package/dist/pinned-python-config.js +0 -38
- package/dist/pinned-python.d.ts +0 -12
- package/dist/pinned-python.d.ts.map +0 -1
- package/dist/pinned-python.js +0 -688
- package/docs/python-helper.md +0 -103
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { FsSafeError } from "./errors.js";
|
|
4
|
+
import { readSidecarLockSnapshotSync, relativeSidecarLockPath, removeSidecarLockIfUnchangedSync, serializeSidecarLockPayload, sidecarLockSnapshotMatches, } from "./sidecar-lock-reclaim.js";
|
|
5
|
+
import { computeSidecarLockDelayMs, sidecarLockPayloadIsStale, } from "./sidecar-lock-policy.js";
|
|
6
|
+
const SYNC_HELD_LOCKS_KEY = Symbol.for("fsSafe.syncSidecarLocks");
|
|
7
|
+
function getSyncHeldLocks() {
|
|
8
|
+
const globalWithState = globalThis;
|
|
9
|
+
if (!globalWithState[SYNC_HELD_LOCKS_KEY]) {
|
|
10
|
+
globalWithState[SYNC_HELD_LOCKS_KEY] = new Map();
|
|
11
|
+
}
|
|
12
|
+
return globalWithState[SYNC_HELD_LOCKS_KEY];
|
|
13
|
+
}
|
|
14
|
+
function verifySyncHeldLock(held) {
|
|
15
|
+
const current = readSidecarLockSnapshotSync(held.lockPath, held.parsePayload);
|
|
16
|
+
return !!current && sidecarLockSnapshotMatches(current, held.snapshot);
|
|
17
|
+
}
|
|
18
|
+
function releaseSyncHeldLock(held) {
|
|
19
|
+
const heldLocks = getSyncHeldLocks();
|
|
20
|
+
if (heldLocks.get(held.normalizedTargetPath) !== held)
|
|
21
|
+
return false;
|
|
22
|
+
held.refCount -= 1;
|
|
23
|
+
if (held.refCount > 0)
|
|
24
|
+
return false;
|
|
25
|
+
heldLocks.delete(held.normalizedTargetPath);
|
|
26
|
+
if (held.timer) {
|
|
27
|
+
clearInterval(held.timer);
|
|
28
|
+
held.timer = undefined;
|
|
29
|
+
}
|
|
30
|
+
fs.closeSync(held.fd);
|
|
31
|
+
removeSidecarLockIfUnchangedSync(held.lockPath, held.snapshot);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
function createSyncHeldLockHandle(held) {
|
|
35
|
+
let released = false;
|
|
36
|
+
const release = () => {
|
|
37
|
+
if (released)
|
|
38
|
+
return;
|
|
39
|
+
released = true;
|
|
40
|
+
releaseSyncHeldLock(held);
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
lockPath: held.lockPath,
|
|
44
|
+
normalizedTargetPath: held.normalizedTargetPath,
|
|
45
|
+
verifyStillHeld: () => verifySyncHeldLock(held),
|
|
46
|
+
release,
|
|
47
|
+
[Symbol.dispose]: release,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function normalizeTargetPath(targetPath) {
|
|
51
|
+
const resolved = path.resolve(targetPath);
|
|
52
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
53
|
+
try {
|
|
54
|
+
return path.join(fs.realpathSync(path.dirname(resolved)), path.basename(resolved));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return resolved;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function boundedLockPath(lockPath, lockRoot) {
|
|
61
|
+
const resolved = path.resolve(lockPath);
|
|
62
|
+
if (!lockRoot)
|
|
63
|
+
return resolved;
|
|
64
|
+
relativeSidecarLockPath(lockRoot, resolved);
|
|
65
|
+
const parent = path.dirname(resolved);
|
|
66
|
+
const parentReal = fs.realpathSync(parent);
|
|
67
|
+
const parentRelative = path.relative(lockRoot.rootReal, parentReal);
|
|
68
|
+
if (parentRelative === ".." || parentRelative.startsWith(`..${path.sep}`) || path.isAbsolute(parentRelative)) {
|
|
69
|
+
throw new FsSafeError("outside-workspace", "sidecar lock parent is outside lockRoot");
|
|
70
|
+
}
|
|
71
|
+
return path.join(parentReal, path.basename(resolved));
|
|
72
|
+
}
|
|
73
|
+
function defaultShouldReclaim(payload, lockPath, staleMs, nowMs) {
|
|
74
|
+
if (sidecarLockPayloadIsStale(payload, staleMs, nowMs))
|
|
75
|
+
return true;
|
|
76
|
+
try {
|
|
77
|
+
return nowMs - fs.statSync(lockPath).mtimeMs > staleMs;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function sleep(milliseconds) {
|
|
84
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
|
|
85
|
+
}
|
|
86
|
+
export function acquireFileLockSync(targetPath, options) {
|
|
87
|
+
const normalizedTargetPath = normalizeTargetPath(targetPath);
|
|
88
|
+
const lockPath = boundedLockPath(options.lockPath ?? `${normalizedTargetPath}.lock`, options.lockRoot);
|
|
89
|
+
const heldLocks = getSyncHeldLocks();
|
|
90
|
+
const held = heldLocks.get(normalizedTargetPath);
|
|
91
|
+
if (held &&
|
|
92
|
+
options.reentrantOwner !== undefined &&
|
|
93
|
+
held.reentrantOwner !== undefined &&
|
|
94
|
+
options.reentrantOwner === held.reentrantOwner) {
|
|
95
|
+
held.refCount += 1;
|
|
96
|
+
return createSyncHeldLockHandle(held);
|
|
97
|
+
}
|
|
98
|
+
const staleMs = options.staleMs ?? 30_000;
|
|
99
|
+
const retry = options.retry ?? {};
|
|
100
|
+
const startedAt = Date.now();
|
|
101
|
+
let attempt = 0;
|
|
102
|
+
while (true) {
|
|
103
|
+
let fd;
|
|
104
|
+
try {
|
|
105
|
+
const payload = options.payload();
|
|
106
|
+
const { raw, ownershipToken } = serializeSidecarLockPayload(payload);
|
|
107
|
+
const noFollow = process.platform !== "win32" && typeof fs.constants.O_NOFOLLOW === "number"
|
|
108
|
+
? fs.constants.O_NOFOLLOW
|
|
109
|
+
: 0;
|
|
110
|
+
fd = fs.openSync(lockPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | noFollow, 0o600);
|
|
111
|
+
fs.writeFileSync(fd, raw, "utf8");
|
|
112
|
+
fs.fsyncSync(fd);
|
|
113
|
+
const snapshot = {
|
|
114
|
+
raw,
|
|
115
|
+
payload,
|
|
116
|
+
stat: fs.fstatSync(fd),
|
|
117
|
+
ownershipToken,
|
|
118
|
+
};
|
|
119
|
+
const createdHeld = {
|
|
120
|
+
fd,
|
|
121
|
+
lockPath,
|
|
122
|
+
normalizedTargetPath,
|
|
123
|
+
parsePayload: options.parsePayload,
|
|
124
|
+
refCount: 1,
|
|
125
|
+
reentrantOwner: options.reentrantOwner,
|
|
126
|
+
snapshot,
|
|
127
|
+
};
|
|
128
|
+
heldLocks.set(normalizedTargetPath, createdHeld);
|
|
129
|
+
const returnedHandle = createSyncHeldLockHandle(createdHeld);
|
|
130
|
+
if (options.onCompromised && (options.compromiseCheckIntervalMs ?? 0) > 0) {
|
|
131
|
+
createdHeld.timer = setInterval(() => {
|
|
132
|
+
if (!returnedHandle.verifyStillHeld()) {
|
|
133
|
+
if (createdHeld.timer)
|
|
134
|
+
clearInterval(createdHeld.timer);
|
|
135
|
+
createdHeld.timer = undefined;
|
|
136
|
+
options.onCompromised?.({ lockPath, normalizedTargetPath });
|
|
137
|
+
}
|
|
138
|
+
}, options.compromiseCheckIntervalMs);
|
|
139
|
+
createdHeld.timer.unref();
|
|
140
|
+
}
|
|
141
|
+
fd = undefined;
|
|
142
|
+
return returnedHandle;
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
if (fd !== undefined) {
|
|
146
|
+
const failed = { payload: null, stat: fs.fstatSync(fd) };
|
|
147
|
+
fs.closeSync(fd);
|
|
148
|
+
fd = undefined;
|
|
149
|
+
removeSidecarLockIfUnchangedSync(lockPath, failed);
|
|
150
|
+
}
|
|
151
|
+
if (error.code !== "EEXIST")
|
|
152
|
+
throw error;
|
|
153
|
+
if (heldLocks.has(normalizedTargetPath)) {
|
|
154
|
+
const elapsed = Date.now() - startedAt;
|
|
155
|
+
const timedOut = options.timeoutMs !== undefined && elapsed >= options.timeoutMs;
|
|
156
|
+
if (timedOut || (retry.retries !== undefined && attempt >= retry.retries)) {
|
|
157
|
+
throw Object.assign(new Error(`file lock timeout for ${normalizedTargetPath}`), {
|
|
158
|
+
code: "file_lock_timeout",
|
|
159
|
+
lockPath,
|
|
160
|
+
normalizedTargetPath,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
sleep(computeSidecarLockDelayMs(retry, attempt));
|
|
164
|
+
attempt += 1;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const snapshot = readSidecarLockSnapshotSync(lockPath, options.parsePayload);
|
|
168
|
+
if (!snapshot)
|
|
169
|
+
continue;
|
|
170
|
+
const nowMs = Date.now();
|
|
171
|
+
const reclaim = options.shouldReclaim
|
|
172
|
+
? options.shouldReclaim({
|
|
173
|
+
lockPath,
|
|
174
|
+
normalizedTargetPath,
|
|
175
|
+
payload: snapshot.payload,
|
|
176
|
+
staleMs,
|
|
177
|
+
nowMs,
|
|
178
|
+
heldByThisProcess: false,
|
|
179
|
+
})
|
|
180
|
+
: defaultShouldReclaim(snapshot.payload, lockPath, staleMs, nowMs);
|
|
181
|
+
if (reclaim) {
|
|
182
|
+
if (options.staleRecovery === "remove-if-unchanged" &&
|
|
183
|
+
snapshot.raw !== undefined &&
|
|
184
|
+
options.shouldRemoveStaleLock?.({
|
|
185
|
+
lockPath,
|
|
186
|
+
normalizedTargetPath,
|
|
187
|
+
raw: snapshot.raw,
|
|
188
|
+
payload: snapshot.payload,
|
|
189
|
+
})) {
|
|
190
|
+
const reclaimGuard = `${lockPath}.reclaim`;
|
|
191
|
+
try {
|
|
192
|
+
fs.mkdirSync(reclaimGuard);
|
|
193
|
+
if (removeSidecarLockIfUnchangedSync(lockPath, snapshot))
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
try {
|
|
198
|
+
fs.rmdirSync(reclaimGuard);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// A surviving reclaim guard fails closed.
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
throw Object.assign(new Error(`file lock stale for ${normalizedTargetPath}`), {
|
|
206
|
+
code: "file_lock_stale",
|
|
207
|
+
lockPath,
|
|
208
|
+
normalizedTargetPath,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
const elapsed = Date.now() - startedAt;
|
|
212
|
+
const timedOut = options.timeoutMs !== undefined && elapsed >= options.timeoutMs;
|
|
213
|
+
if (timedOut || (retry.retries !== undefined && attempt >= retry.retries)) {
|
|
214
|
+
throw Object.assign(new Error(`file lock timeout for ${normalizedTargetPath}`), {
|
|
215
|
+
code: "file_lock_timeout",
|
|
216
|
+
lockPath,
|
|
217
|
+
normalizedTargetPath,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
sleep(computeSidecarLockDelayMs(retry, attempt));
|
|
221
|
+
attempt += 1;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
export function withFileLockSync(targetPath, options, fn) {
|
|
226
|
+
const lock = acquireFileLockSync(targetPath, options);
|
|
227
|
+
try {
|
|
228
|
+
return fn();
|
|
229
|
+
}
|
|
230
|
+
finally {
|
|
231
|
+
lock.release();
|
|
232
|
+
}
|
|
233
|
+
}
|
package/dist/file-lock.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type SidecarLockAcquireOptions, type SidecarLockHandle, type SidecarLockHeldEntry, type SidecarLockRetryOptions, type SidecarLockStaleRecovery } from "./sidecar-lock.js";
|
|
2
|
+
export { acquireFileLockSync, withFileLockSync, type FileLockSyncAcquireOptions, type FileLockSyncHandle, } from "./file-lock-sync.js";
|
|
2
3
|
export type FileLockRetryOptions = SidecarLockRetryOptions;
|
|
3
4
|
export type FileLockStaleRecovery = SidecarLockStaleRecovery;
|
|
4
5
|
export type FileLockAcquireOptions<TPayload extends Record<string, unknown>> = Omit<SidecarLockAcquireOptions<TPayload>, "targetPath" | "staleMs"> & {
|
|
@@ -7,6 +8,7 @@ export type FileLockAcquireOptions<TPayload extends Record<string, unknown>> = O
|
|
|
7
8
|
};
|
|
8
9
|
export type FileLockHandle = SidecarLockHandle;
|
|
9
10
|
export type FileLockHeldEntry = SidecarLockHeldEntry;
|
|
11
|
+
export type { SidecarLockCompromisedInfo } from "./sidecar-lock.js";
|
|
10
12
|
export type FileLockManager = {
|
|
11
13
|
acquire<TPayload extends Record<string, unknown>>(targetPath: string, options: FileLockAcquireOptions<TPayload>): Promise<FileLockHandle>;
|
|
12
14
|
withLock<T, TPayload extends Record<string, unknown>>(targetPath: string, options: FileLockAcquireOptions<TPayload>, fn: () => Promise<T>): Promise<T>;
|
package/dist/file-lock.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-lock.d.ts","sourceRoot":"","sources":["../src/file-lock.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,yBAAyB,EAC9B,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EACzB,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,EAC9B,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"file-lock.d.ts","sourceRoot":"","sources":["../src/file-lock.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,yBAAyB,EAC9B,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EACzB,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,EAC9B,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,GACxB,MAAM,qBAAqB,CAAC;AAE7B,MAAM,MAAM,oBAAoB,GAAG,uBAAuB,CAAC;AAC3D,MAAM,MAAM,qBAAqB,GAAG,wBAAwB,CAAC;AAE7D,MAAM,MAAM,sBAAsB,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,IAAI,CACjF,yBAAyB,CAAC,QAAQ,CAAC,EACnC,YAAY,GAAG,SAAS,CACzB,GAAG;IACF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,iBAAiB,CAAC;AAC/C,MAAM,MAAM,iBAAiB,GAAG,oBAAoB,CAAC;AACrD,YAAY,EAAE,0BAA0B,EAAE,MAAM,mBAAmB,CAAC;AAEpE,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9C,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,sBAAsB,CAAC,QAAQ,CAAC,GACxC,OAAO,CAAC,cAAc,CAAC,CAAC;IAC3B,QAAQ,CAAC,CAAC,EAAE,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAClD,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,sBAAsB,CAAC,QAAQ,CAAC,EACzC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC,CAAC;IACd,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,KAAK,IAAI,IAAI,CAAC;IACd,WAAW,IAAI,iBAAiB,EAAE,CAAC;CACpC,CAAC;AAmBF,wBAAsB,eAAe,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5E,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,sBAAsB,CAAC,QAAQ,CAAC,GACxC,OAAO,CAAC,cAAc,CAAC,CAGzB;AAED,wBAAsB,YAAY,CAAC,CAAC,EAAE,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5E,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,sBAAsB,CAAC,QAAQ,CAAC,EACzC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC,CAGZ;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAelE;AAED,wBAAsB,2BAA2B,CAC/C,UAAU,EAAE,MAAM,EAClB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC,CAEf;AAED,wBAAgB,2BAA2B,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAEzF"}
|
package/dist/file-lock.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createSidecarLockManager, } from "./sidecar-lock.js";
|
|
2
2
|
import { getFsSafeLockConfig } from "./lock-config.js";
|
|
3
|
+
export { acquireFileLockSync, withFileLockSync, } from "./file-lock-sync.js";
|
|
3
4
|
function resolveFileLockManagerKey(targetPath, managerKey) {
|
|
4
5
|
return managerKey ?? `fs-safe.file-lock:${targetPath}`;
|
|
5
6
|
}
|
package/dist/filename.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"filename.d.ts","sourceRoot":"","sources":["../src/filename.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"filename.d.ts","sourceRoot":"","sources":["../src/filename.ts"],"names":[],"mappings":"AAIA,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CA2BxF"}
|
package/dist/filename.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
const WINDOWS_INVALID_FILE_NAME_CHARACTERS = new Set('<>:"/\\|?*');
|
|
2
3
|
export function sanitizeUntrustedFileName(fileName, fallbackName) {
|
|
3
4
|
const trimmed = typeof fileName === "string" ? fileName.trim() : "";
|
|
4
5
|
if (!trimmed) {
|
|
@@ -9,7 +10,9 @@ export function sanitizeUntrustedFileName(fileName, fallbackName) {
|
|
|
9
10
|
let cleaned = "";
|
|
10
11
|
for (let i = 0; i < base.length; i++) {
|
|
11
12
|
const code = base.charCodeAt(i);
|
|
12
|
-
if (code < 0x20 ||
|
|
13
|
+
if (code < 0x20 ||
|
|
14
|
+
(code >= 0x7f && code <= 0x9f) ||
|
|
15
|
+
WINDOWS_INVALID_FILE_NAME_CHARACTERS.has(base[i])) {
|
|
13
16
|
continue;
|
|
14
17
|
}
|
|
15
18
|
cleaned += base[i];
|
package/dist/guarded-mkdir.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates each missing path component from `rootReal` down to `targetPath`,
|
|
3
|
+
* guarding every step. Returns the real (symlink-resolved) path of the final
|
|
4
|
+
* component so callers can guard/use that path directly instead of
|
|
5
|
+
* re-deriving it from the original, possibly-symlinked, lexical path.
|
|
6
|
+
*/
|
|
1
7
|
export declare function mkdirPathComponentsWithGuards(params: {
|
|
2
8
|
rootReal: string;
|
|
3
9
|
targetPath: string;
|
|
4
10
|
beforeComponent?: (componentPath: string) => Promise<void> | void;
|
|
5
|
-
}): Promise<
|
|
11
|
+
}): Promise<string>;
|
|
6
12
|
//# sourceMappingURL=guarded-mkdir.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guarded-mkdir.d.ts","sourceRoot":"","sources":["../src/guarded-mkdir.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"guarded-mkdir.d.ts","sourceRoot":"","sources":["../src/guarded-mkdir.ts"],"names":[],"mappings":"AA0BA;;;;;GAKG;AACH,wBAAsB,6BAA6B,CAAC,MAAM,EAAE;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACnE,GAAG,OAAO,CAAC,MAAM,CAAC,CAuDlB"}
|
package/dist/guarded-mkdir.js
CHANGED
|
@@ -2,11 +2,32 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard } from "./directory-guard.js";
|
|
4
4
|
import { FsSafeError } from "./errors.js";
|
|
5
|
-
import { isPathRelativeEscape } from "./path.js";
|
|
5
|
+
import { isNotFoundPathError, isPathRelativeEscape } from "./path.js";
|
|
6
6
|
function isSameOrChildPath(candidate, parent) {
|
|
7
7
|
const parentPrefix = parent.endsWith(path.sep) ? parent : `${parent}${path.sep}`;
|
|
8
8
|
return candidate === parent || candidate.startsWith(parentPrefix);
|
|
9
9
|
}
|
|
10
|
+
async function realpathOrThrowNotFile(target) {
|
|
11
|
+
try {
|
|
12
|
+
return path.resolve(await fs.realpath(target));
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (isNotFoundPathError(error)) {
|
|
16
|
+
// A dangling symlink (or a component removed between lstat and
|
|
17
|
+
// realpath) is not a usable directory component.
|
|
18
|
+
throw new FsSafeError("not-file", "directory component must be a directory", {
|
|
19
|
+
cause: error instanceof Error ? error : undefined,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Creates each missing path component from `rootReal` down to `targetPath`,
|
|
27
|
+
* guarding every step. Returns the real (symlink-resolved) path of the final
|
|
28
|
+
* component so callers can guard/use that path directly instead of
|
|
29
|
+
* re-deriving it from the original, possibly-symlinked, lexical path.
|
|
30
|
+
*/
|
|
10
31
|
export async function mkdirPathComponentsWithGuards(params) {
|
|
11
32
|
const root = path.resolve(params.rootReal);
|
|
12
33
|
const rootCanonical = path.resolve(await fs.realpath(root));
|
|
@@ -30,16 +51,37 @@ export async function mkdirPathComponentsWithGuards(params) {
|
|
|
30
51
|
}
|
|
31
52
|
}
|
|
32
53
|
const stat = await fs.lstat(next);
|
|
33
|
-
if (stat.isSymbolicLink()
|
|
54
|
+
if (!stat.isSymbolicLink() && !stat.isDirectory()) {
|
|
34
55
|
throw new FsSafeError("not-file", "directory component must be a directory");
|
|
35
56
|
}
|
|
36
57
|
// Node's recursive mkdir follows symlinks in missing components. Build one
|
|
37
58
|
// segment at a time and realpath-check each segment before descending.
|
|
38
|
-
|
|
59
|
+
const nextReal = await realpathOrThrowNotFile(next);
|
|
60
|
+
if (!isSameOrChildPath(nextReal, rootCanonical)) {
|
|
39
61
|
throw new FsSafeError("outside-workspace", "directory escaped workspace root");
|
|
40
62
|
}
|
|
63
|
+
if (stat.isSymbolicLink()) {
|
|
64
|
+
// An existing path component may legitimately be a symlink to a real
|
|
65
|
+
// directory inside the root (e.g. a skill-bank layout). We already
|
|
66
|
+
// verified above that it resolves inside the root, so treat the
|
|
67
|
+
// resolved real path as the directory for the rest of this walk
|
|
68
|
+
// instead of rejecting it outright. Guard checks from here on operate
|
|
69
|
+
// on the real (non-symlink) path, preserving TOCTOU protection for
|
|
70
|
+
// every subsequent segment. Callers that need the final directory
|
|
71
|
+
// (e.g. to guard it themselves after this function returns) must use
|
|
72
|
+
// the returned resolved path, not their own lexical parent path.
|
|
73
|
+
const targetStat = await fs.stat(nextReal);
|
|
74
|
+
if (!targetStat.isDirectory()) {
|
|
75
|
+
throw new FsSafeError("not-file", "directory component must be a directory");
|
|
76
|
+
}
|
|
77
|
+
await createAsyncDirectoryGuard(nextReal);
|
|
78
|
+
await assertAsyncDirectoryGuard(parentGuard);
|
|
79
|
+
current = nextReal;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
41
82
|
await createAsyncDirectoryGuard(next);
|
|
42
83
|
await assertAsyncDirectoryGuard(parentGuard);
|
|
43
84
|
current = next;
|
|
44
85
|
}
|
|
86
|
+
return current;
|
|
45
87
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export { FsSafeError, categorizeFsSafeError, type FsSafeErrorCategory, type FsSafeErrorCode, } from "./errors.js";
|
|
2
|
-
export { DEFAULT_ROOT_MAX_BYTES, root, type DenyMutationPolicy, type HardlinkPolicy, type RenameIdentityPolicy, type OpenResult, type ReadResult, type Root, type RootAppendOptions, type RootCopyOptions, type RootCreateJsonOptions, type RootCreateOptions, type RootDefaults, type RootMkdirOptions, type RootMoveOptions, type RootOpenOptions, type RootOpenWritableOptions, type RootOptions, type RootReadOptions, type RootRemoveOptions, type RootWriteJsonOptions, type RootWriteOptions, type SymlinkPolicy, type WritableOpenMode, type WritableOpenResult, } from "./root.js";
|
|
3
|
-
export
|
|
1
|
+
export { FsSafeError, categorizeFsSafeError, type FsSafeErrorCategory, type FsSafeErrorCode, type FsSafeErrorDetails, } from "./errors.js";
|
|
2
|
+
export { DEFAULT_ROOT_MAX_BYTES, root, type DenyMutationPolicy, type HardlinkPolicy, type RenameIdentityPolicy, type OpenResult, type ReadResult, type Root, type RootAppendOptions, type RootCopyOptions, type RootCreateJsonOptions, type RootCreateOptions, type RootDefaults, type RootMkdirOptions, type RootMoveOptions, type RootOpenOptions, type RootOpenWritableOptions, type RootOptions, type RootReadOptions, type RootRemoveOptions, type RootWriteJsonOptions, type RootWriteOptions, type RootWalkDataEntry, type RootWalkDataEntryKind, type RootWalkDirectoryErrorBehavior, type RootWalkEntry, type RootWalkEntryFilter, type RootWalkEntryFilterResult, type RootWalkEntryKind, type RootWalkLimitBehavior, type RootWalkOptions, type RootWalkSymlinkPolicy, type SymlinkPolicy, type WritableOpenMode, type WritableOpenResult, } from "./root.js";
|
|
3
|
+
export type { ContainmentGuarantee } from "./containment.js";
|
|
4
|
+
export { configureFsSafePython, configureFsSafeNative, getFsSafeNativeConfig, type FsSafePythonConfig, type FsSafeNativeConfig, type FsSafeNativeMode, } from "./native-config.js";
|
|
4
5
|
export { writeExternalFileWithinRoot, type ExternalFileWriteOptions, type ExternalFileWriteResult, } from "./output.js";
|
|
5
6
|
export { configureFsSafeLocks, getFsSafeLockConfig, type FsSafeLockConfig, } from "./lock-config.js";
|
|
6
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,qBAAqB,EACrB,KAAK,mBAAmB,EACxB,KAAK,eAAe,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,qBAAqB,EACrB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,KAAK,kBAAkB,GACxB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,sBAAsB,EACtB,IAAI,EACJ,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,IAAI,EACT,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,KAAK,uBAAuB,EAC5B,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC1B,KAAK,8BAA8B,EACnC,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACxB,KAAK,yBAAyB,EAC9B,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,GACxB,MAAM,WAAW,CAAC;AACnB,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,GACtB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,2BAA2B,EAC3B,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,GAC7B,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,oBAAoB,EACpB,mBAAmB,EACnB,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { FsSafeError, categorizeFsSafeError, } from "./errors.js";
|
|
2
2
|
export { DEFAULT_ROOT_MAX_BYTES, root, } from "./root.js";
|
|
3
|
-
export { configureFsSafePython,
|
|
3
|
+
export { configureFsSafePython, configureFsSafeNative, getFsSafeNativeConfig, } from "./native-config.js";
|
|
4
4
|
export { writeExternalFileWithinRoot, } from "./output.js";
|
|
5
5
|
export { configureFsSafeLocks, getFsSafeLockConfig, } from "./lock-config.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"json-document-store.d.ts","sourceRoot":"","sources":["../src/json-document-store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"json-document-store.d.ts","sourceRoot":"","sources":["../src/json-document-store.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAG3D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAKlE,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAC7B,aAAa,CAAC,EAAE,wBAAwB,CAAC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,IAAI,CAAC,EAAE,OAAO,GAAG,oBAAoB,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,IAAI,IAAI,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IAC/B,MAAM,CAAC,QAAQ,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAChC,YAAY,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC;IAC3B,KAAK,CAAC,KAAK,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,SAAS,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACpE,QAAQ,CAAC,QAAQ,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACxE,CAAC;AAEF,MAAM,MAAM,gBAAgB,CAAC,CAAC,IAAI;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,IAAI,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IACvC,YAAY,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC;IAC3B,KAAK,CAAC,KAAK,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzE,CAAC;AA2BF,wBAAgB,eAAe,CAAC,CAAC,EAC/B,OAAO,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC5B,OAAO,GAAE,oBAAyB,GACjC,SAAS,CAAC,CAAC,CAAC,CA0Ed"}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { canonicalPathFromExistingAncestor } from "./absolute-path.js";
|
|
3
|
+
import { FsSafeError } from "./errors.js";
|
|
1
4
|
import { getFsSafeLockConfig } from "./lock-config.js";
|
|
2
5
|
import { createSidecarLockManager } from "./sidecar-lock.js";
|
|
6
|
+
import { serializePathWrite } from "./write-queue.js";
|
|
7
|
+
const activeStoreMutations = new AsyncLocalStorage();
|
|
3
8
|
function cloneFallback(value) {
|
|
4
9
|
if (value && typeof value === "object") {
|
|
5
10
|
return structuredClone(value);
|
|
@@ -35,19 +40,29 @@ export function createJsonStore(adapter, options = {}) {
|
|
|
35
40
|
trailingNewline: options.trailingNewline ?? true,
|
|
36
41
|
});
|
|
37
42
|
}
|
|
38
|
-
async function
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
async function withSerializedMutation(run) {
|
|
44
|
+
const canonicalPath = await canonicalPathFromExistingAncestor(adapter.filePath);
|
|
45
|
+
const activePaths = activeStoreMutations.getStore();
|
|
46
|
+
if (activePaths?.has(canonicalPath)) {
|
|
47
|
+
throw new FsSafeError("store-reentrant-update", `jsonStore cannot write or update ${canonicalPath} from inside its active update callback; return the complete next value from the outer update instead`);
|
|
41
48
|
}
|
|
42
|
-
return await
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
return await serializePathWrite(`json-store:${canonicalPath}`, async () => {
|
|
50
|
+
const mutationPaths = new Set(activePaths);
|
|
51
|
+
mutationPaths.add(canonicalPath);
|
|
52
|
+
return await activeStoreMutations.run(mutationPaths, async () => {
|
|
53
|
+
if (!locks || !lockOptions) {
|
|
54
|
+
return await run();
|
|
55
|
+
}
|
|
56
|
+
return await locks.withLock({
|
|
57
|
+
targetPath: adapter.filePath,
|
|
58
|
+
staleMs: lockOptions.staleMs,
|
|
59
|
+
timeoutMs: lockOptions.timeoutMs,
|
|
60
|
+
retry: lockOptions.retry,
|
|
61
|
+
staleRecovery: lockOptions.staleRecovery,
|
|
62
|
+
payload: () => ({ pid: process.pid, createdAt: new Date().toISOString() }),
|
|
63
|
+
}, run);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
51
66
|
}
|
|
52
67
|
return {
|
|
53
68
|
filePath: adapter.filePath,
|
|
@@ -55,16 +70,16 @@ export function createJsonStore(adapter, options = {}) {
|
|
|
55
70
|
readOr,
|
|
56
71
|
readRequired: adapter.readRequired,
|
|
57
72
|
write: async (value) => {
|
|
58
|
-
await
|
|
73
|
+
await withSerializedMutation(async () => {
|
|
59
74
|
await write(value);
|
|
60
75
|
});
|
|
61
76
|
},
|
|
62
|
-
update: async (run) => await
|
|
77
|
+
update: async (run) => await withSerializedMutation(async () => {
|
|
63
78
|
const next = await run(await read());
|
|
64
79
|
await write(next);
|
|
65
80
|
return next;
|
|
66
81
|
}),
|
|
67
|
-
updateOr: async (fallback, run) => await
|
|
82
|
+
updateOr: async (fallback, run) => await withSerializedMutation(async () => {
|
|
68
83
|
const current = await read();
|
|
69
84
|
const next = await run(current === undefined ? cloneFallback(fallback) : current);
|
|
70
85
|
await write(next);
|
package/dist/move-path.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"move-path.d.ts","sourceRoot":"","sources":["../src/move-path.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"move-path.d.ts","sourceRoot":"","sources":["../src/move-path.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,+BAA+B,GAAG;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IACrC,EAAE,EAAE,MAAM,CAAC;CACZ,CAAC;AAEF,KAAK,sBAAsB,GAAG,cAAc,GAAG,uBAAuB,CAAC;AAEvE,wBAAgB,oCAAoC,CAClD,KAAK,EAAE,OAAO,EACd,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,sBAAsB,GAAG,SAAS,CASpC;AA4UD,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,+BAA+B,GACvC,OAAO,CAAC,IAAI,CAAC,CAuCf"}
|