@openclaw/fs-safe 0.6.0 → 0.7.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.
Files changed (202) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +7 -0
  3. package/dist/archive-deadline.d.ts +3 -0
  4. package/dist/archive-deadline.d.ts.map +1 -1
  5. package/dist/archive-deadline.js +44 -8
  6. package/dist/archive-entry.d.ts.map +1 -1
  7. package/dist/archive-entry.js +1 -0
  8. package/dist/archive-errors.d.ts +1 -0
  9. package/dist/archive-errors.d.ts.map +1 -1
  10. package/dist/archive-errors.js +3 -0
  11. package/dist/archive-input.d.ts.map +1 -1
  12. package/dist/archive-input.js +26 -16
  13. package/dist/archive-limits.d.ts +11 -3
  14. package/dist/archive-limits.d.ts.map +1 -1
  15. package/dist/archive-limits.js +24 -0
  16. package/dist/archive-native.d.ts +3 -2
  17. package/dist/archive-native.d.ts.map +1 -1
  18. package/dist/archive-native.js +24 -9
  19. package/dist/archive-policy.d.ts +2 -0
  20. package/dist/archive-policy.d.ts.map +1 -1
  21. package/dist/archive-policy.js +9 -1
  22. package/dist/archive-read.d.ts.map +1 -1
  23. package/dist/archive-read.js +69 -41
  24. package/dist/archive-staging.d.ts +3 -0
  25. package/dist/archive-staging.d.ts.map +1 -1
  26. package/dist/archive-staging.js +91 -43
  27. package/dist/archive-tar-admission.d.ts +7 -0
  28. package/dist/archive-tar-admission.d.ts.map +1 -0
  29. package/dist/archive-tar-admission.js +43 -0
  30. package/dist/archive-tar-gnu.d.ts +2 -0
  31. package/dist/archive-tar-gnu.d.ts.map +1 -0
  32. package/dist/archive-tar-gnu.js +20 -0
  33. package/dist/archive-tar-header.d.ts +8 -0
  34. package/dist/archive-tar-header.d.ts.map +1 -0
  35. package/dist/archive-tar-header.js +47 -0
  36. package/dist/archive-tar-meta.d.ts +29 -1
  37. package/dist/archive-tar-meta.d.ts.map +1 -1
  38. package/dist/archive-tar-meta.js +104 -35
  39. package/dist/archive-tar-runtime.d.ts +3 -0
  40. package/dist/archive-tar-runtime.d.ts.map +1 -1
  41. package/dist/archive-tar-runtime.js +3 -0
  42. package/dist/archive-tar.d.ts.map +1 -1
  43. package/dist/archive-tar.js +8 -2
  44. package/dist/archive-zip-names.js +2 -2
  45. package/dist/archive.d.ts.map +1 -1
  46. package/dist/archive.js +38 -27
  47. package/dist/atomic.d.ts +1 -1
  48. package/dist/atomic.d.ts.map +1 -1
  49. package/dist/bounded-read-stream.d.ts.map +1 -1
  50. package/dist/bounded-read-stream.js +2 -4
  51. package/dist/bounded-read.d.ts.map +1 -1
  52. package/dist/bounded-read.js +3 -10
  53. package/dist/byte-budget.d.ts +5 -0
  54. package/dist/byte-budget.d.ts.map +1 -0
  55. package/dist/byte-budget.js +9 -0
  56. package/dist/directory-guard.d.ts +2 -1
  57. package/dist/directory-guard.d.ts.map +1 -1
  58. package/dist/directory-guard.js +10 -0
  59. package/dist/file-lock-sync.d.ts.map +1 -1
  60. package/dist/file-lock-sync.js +81 -30
  61. package/dist/file-observation.d.ts +9 -0
  62. package/dist/file-observation.d.ts.map +1 -0
  63. package/dist/file-observation.js +22 -0
  64. package/dist/file-store-boundary.d.ts.map +1 -1
  65. package/dist/file-store-boundary.js +9 -5
  66. package/dist/file-store-limit.d.ts +2 -0
  67. package/dist/file-store-limit.d.ts.map +1 -0
  68. package/dist/file-store-limit.js +8 -0
  69. package/dist/file-store-sync-write.d.ts.map +1 -1
  70. package/dist/file-store-sync-write.js +38 -8
  71. package/dist/file-store.d.ts.map +1 -1
  72. package/dist/file-store.js +26 -26
  73. package/dist/json-durable-queue-directory.d.ts +2 -0
  74. package/dist/json-durable-queue-directory.d.ts.map +1 -0
  75. package/dist/json-durable-queue-directory.js +20 -0
  76. package/dist/json-durable-queue-ownership.d.ts +14 -0
  77. package/dist/json-durable-queue-ownership.d.ts.map +1 -0
  78. package/dist/json-durable-queue-ownership.js +168 -0
  79. package/dist/json-durable-queue-retirement.d.ts +9 -0
  80. package/dist/json-durable-queue-retirement.d.ts.map +1 -0
  81. package/dist/json-durable-queue-retirement.js +126 -0
  82. package/dist/json-durable-queue-transfer-lock.d.ts +2 -0
  83. package/dist/json-durable-queue-transfer-lock.d.ts.map +1 -0
  84. package/dist/json-durable-queue-transfer-lock.js +19 -0
  85. package/dist/json-durable-queue.d.ts +1 -0
  86. package/dist/json-durable-queue.d.ts.map +1 -1
  87. package/dist/json-durable-queue.js +90 -57
  88. package/dist/json.d.ts.map +1 -1
  89. package/dist/json.js +32 -13
  90. package/dist/local-roots.d.ts.map +1 -1
  91. package/dist/local-roots.js +4 -2
  92. package/dist/native-binding.d.ts +12 -3
  93. package/dist/native-binding.d.ts.map +1 -1
  94. package/dist/native-pinned-write-windows.js +1 -1
  95. package/dist/opened-file-failure.d.ts +7 -0
  96. package/dist/opened-file-failure.d.ts.map +1 -0
  97. package/dist/opened-file-failure.js +41 -0
  98. package/dist/opened-realpath.d.ts.map +1 -1
  99. package/dist/opened-realpath.js +8 -2
  100. package/dist/output-sibling.d.ts.map +1 -1
  101. package/dist/output-sibling.js +11 -110
  102. package/dist/output.d.ts.map +1 -1
  103. package/dist/output.js +4 -2
  104. package/dist/pinned-open.d.ts.map +1 -1
  105. package/dist/pinned-open.js +27 -48
  106. package/dist/pinned-write.d.ts.map +1 -1
  107. package/dist/pinned-write.js +10 -10
  108. package/dist/private-temp-workspace.d.ts +3 -1
  109. package/dist/private-temp-workspace.d.ts.map +1 -1
  110. package/dist/private-temp-workspace.js +81 -56
  111. package/dist/publish-file.d.ts.map +1 -1
  112. package/dist/publish-file.js +2 -4
  113. package/dist/read-opened-file.d.ts.map +1 -1
  114. package/dist/read-opened-file.js +6 -4
  115. package/dist/regular-file.d.ts.map +1 -1
  116. package/dist/regular-file.js +136 -82
  117. package/dist/replace-file-copy-fallback.d.ts +3 -1
  118. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  119. package/dist/replace-file-copy-fallback.js +21 -36
  120. package/dist/replace-file-copy-source.d.ts +21 -0
  121. package/dist/replace-file-copy-source.d.ts.map +1 -0
  122. package/dist/replace-file-copy-source.js +112 -0
  123. package/dist/replace-file-descriptor.d.ts +13 -3
  124. package/dist/replace-file-descriptor.d.ts.map +1 -1
  125. package/dist/replace-file-descriptor.js +32 -7
  126. package/dist/replace-file-rename-policy.d.ts +7 -0
  127. package/dist/replace-file-rename-policy.d.ts.map +1 -0
  128. package/dist/replace-file-rename-policy.js +30 -0
  129. package/dist/replace-file-temp-owner.d.ts +46 -0
  130. package/dist/replace-file-temp-owner.d.ts.map +1 -0
  131. package/dist/replace-file-temp-owner.js +346 -0
  132. package/dist/replace-file.d.ts +6 -1
  133. package/dist/replace-file.d.ts.map +1 -1
  134. package/dist/replace-file.js +72 -58
  135. package/dist/root-impl.d.ts.map +1 -1
  136. package/dist/root-impl.js +113 -82
  137. package/dist/secret-file.d.ts.map +1 -1
  138. package/dist/secret-file.js +6 -3
  139. package/dist/secret-read-async.d.ts.map +1 -1
  140. package/dist/secret-read-async.js +6 -5
  141. package/dist/secure-file.d.ts.map +1 -1
  142. package/dist/secure-file.js +17 -13
  143. package/dist/sibling-staged-file.d.ts +15 -0
  144. package/dist/sibling-staged-file.d.ts.map +1 -0
  145. package/dist/sibling-staged-file.js +148 -0
  146. package/dist/sibling-temp.d.ts +3 -0
  147. package/dist/sibling-temp.d.ts.map +1 -1
  148. package/dist/sibling-temp.js +30 -74
  149. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  150. package/dist/sidecar-lock-acquire.js +91 -37
  151. package/dist/sidecar-lock-handle.d.ts +6 -2
  152. package/dist/sidecar-lock-handle.d.ts.map +1 -1
  153. package/dist/sidecar-lock-handle.js +17 -3
  154. package/dist/sidecar-lock-policy.d.ts +2 -0
  155. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  156. package/dist/sidecar-lock-policy.js +29 -0
  157. package/dist/sidecar-lock-reclaim.d.ts +16 -5
  158. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  159. package/dist/sidecar-lock-reclaim.js +60 -28
  160. package/dist/sidecar-lock-root.d.ts +3 -0
  161. package/dist/sidecar-lock-root.d.ts.map +1 -0
  162. package/dist/sidecar-lock-root.js +76 -0
  163. package/dist/sidecar-lock.d.ts.map +1 -1
  164. package/dist/sidecar-lock.js +28 -16
  165. package/dist/strict-file-identity.d.ts.map +1 -1
  166. package/dist/strict-file-identity.js +4 -1
  167. package/dist/suppressed-error.d.ts +6 -0
  168. package/dist/suppressed-error.d.ts.map +1 -0
  169. package/dist/suppressed-error.js +15 -0
  170. package/dist/temp-cleanup.d.ts +2 -0
  171. package/dist/temp-cleanup.d.ts.map +1 -1
  172. package/dist/temp-cleanup.js +25 -10
  173. package/dist/temp-workspace-owner.d.ts +23 -0
  174. package/dist/temp-workspace-owner.d.ts.map +1 -0
  175. package/dist/temp-workspace-owner.js +320 -0
  176. package/dist/temp.d.ts +1 -1
  177. package/dist/temp.d.ts.map +1 -1
  178. package/dist/test-hooks.d.ts +5 -0
  179. package/dist/test-hooks.d.ts.map +1 -1
  180. package/docs/advanced.md +8 -1
  181. package/docs/archive.md +202 -26
  182. package/docs/atomic.md +11 -2
  183. package/docs/durability.md +1 -1
  184. package/docs/file-store.md +2 -0
  185. package/docs/install.md +8 -4
  186. package/docs/json.md +11 -4
  187. package/docs/native-helper.md +15 -3
  188. package/docs/native.md +33 -6
  189. package/docs/output.md +16 -3
  190. package/docs/public-api.md +6 -3
  191. package/docs/quickstart.md +1 -1
  192. package/docs/reading.md +1 -1
  193. package/docs/regular-file.md +9 -2
  194. package/docs/root.md +2 -0
  195. package/docs/secret-file.md +5 -1
  196. package/docs/secure-file.md +4 -2
  197. package/docs/security-model.md +18 -0
  198. package/docs/sidecar-lock.md +60 -4
  199. package/docs/store.md +13 -0
  200. package/docs/temp.md +128 -14
  201. package/docs/writing.md +6 -0
  202. package/package.json +10 -10
@@ -1,9 +1,12 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { FsSafeError } from "./errors.js";
4
+ import { readFileHandleBounded } from "./bounded-read.js";
5
+ import { openSidecarRoot } from "./sidecar-lock-root.js";
4
6
  import { createNativeExclusiveFile } from "./native-operations.js";
5
- import { computeSidecarLockDelayMs, defaultSidecarLockShouldReclaim, isTransientLockFileDenial, maxTransientLockDenials, } from "./sidecar-lock-policy.js";
6
- import { readSidecarLockSnapshot, relativeSidecarLockPath, releaseSidecarReclaimGuard, removeSidecarLockIfUnchanged, removeStaleSidecarLockIfAllowed, serializeSidecarLockPayload, sidecarLockSnapshotStillPresent, sidecarReclaimGuardExists, tryAcquireSidecarReclaimGuard, } from "./sidecar-lock-reclaim.js";
7
+ import { computeSidecarLockDelayMs, defaultSidecarLockShouldReclaim, isTransientLockFileDenial, maxTransientLockDenials, validateSidecarLockRetryOptions, validateSidecarLockTimeoutMs, } from "./sidecar-lock-policy.js";
8
+ import { readSidecarLockRawSnapshot, parseSidecarLockSnapshot, relativeSidecarLockPath, releaseSidecarReclaimGuard, removeSidecarLockIfUnchanged, removeStaleSidecarLockIfAllowed, serializeSidecarLockPayload, sidecarLockSnapshotStillPresent, sidecarReclaimGuardExists, tryAcquireSidecarReclaimGuard, } from "./sidecar-lock-reclaim.js";
9
+ import { createSuppressedError } from "./suppressed-error.js";
7
10
  async function resolveNormalizedTargetPath(targetPath) {
8
11
  const resolved = path.resolve(targetPath);
9
12
  const dir = path.dirname(resolved);
@@ -16,19 +19,34 @@ async function resolveNormalizedTargetPath(targetPath) {
16
19
  }
17
20
  }
18
21
  export async function acquireSidecarLock(options, context) {
22
+ const retry = options.retry ?? {};
23
+ validateSidecarLockRetryOptions(retry);
24
+ validateSidecarLockTimeoutMs(options.timeoutMs);
19
25
  context.ensureExitCleanupRegistered();
20
26
  const normalizedTargetPath = await resolveNormalizedTargetPath(options.targetPath);
21
27
  const lockPath = options.lockPath ?? `${normalizedTargetPath}.lock`;
22
- const held = context.held.get(normalizedTargetPath);
28
+ let held = context.held.get(normalizedTargetPath);
23
29
  if (held &&
24
30
  options.reentrantOwner !== undefined &&
25
31
  held.reentrantOwner !== undefined &&
26
32
  options.reentrantOwner === held.reentrantOwner) {
27
- held.refCount += 1;
28
- return context.handleForHeldLock(normalizedTargetPath, held);
33
+ // A final release may already have decremented the count to zero and be
34
+ // removing the sidecar. Do not admit a new reentrant handle until that
35
+ // cleanup settles: successful cleanup requires a fresh acquisition, while
36
+ // failed cleanup leaves the existing sidecar held for a retry.
37
+ if (held.releasePromise) {
38
+ await held.releasePromise.catch(() => undefined);
39
+ held = context.held.get(normalizedTargetPath);
40
+ }
41
+ if (held &&
42
+ options.reentrantOwner !== undefined &&
43
+ held.reentrantOwner !== undefined &&
44
+ options.reentrantOwner === held.reentrantOwner) {
45
+ held.refCount += 1;
46
+ return context.handleForHeldLock(normalizedTargetPath, held);
47
+ }
29
48
  }
30
49
  const startedAt = Date.now();
31
- const retry = options.retry ?? {};
32
50
  const maxRetries = options.timeoutMs === Number.POSITIVE_INFINITY ? undefined : retry.retries;
33
51
  const reclaimGuardPath = `${lockPath}.reclaim`;
34
52
  let ownsReclaimGuard = false;
@@ -77,13 +95,14 @@ export async function acquireSidecarLock(options, context) {
77
95
  let handle = null;
78
96
  let createdSnapshot = null;
79
97
  let lockFileCreateDenied = false;
98
+ const payload = await options.payload();
99
+ const { raw, ownershipToken } = serializeSidecarLockPayload(payload);
80
100
  try {
81
- const payload = await options.payload();
82
- const { raw, ownershipToken } = serializeSidecarLockPayload(payload);
83
101
  if (options.lockRoot) {
84
- const relativeLockPath = relativeSidecarLockPath(options.lockRoot, lockPath);
102
+ const lockRoot = options.lockRoot;
103
+ const relativeLockPath = relativeSidecarLockPath(lockRoot, lockPath);
85
104
  try {
86
- await options.lockRoot.create(relativeLockPath, raw, { mkdir: true, mode: 0o600 });
105
+ await lockRoot.create(relativeLockPath, raw, { mkdir: true, mode: 0o600 });
87
106
  }
88
107
  catch (error) {
89
108
  if (error instanceof FsSafeError && error.code === "already-exists") {
@@ -92,7 +111,23 @@ export async function acquireSidecarLock(options, context) {
92
111
  throw error;
93
112
  }
94
113
  createdSnapshot = { raw, payload, ownershipToken };
95
- handle = (await options.lockRoot.open(relativeLockPath)).handle;
114
+ const opened = await openSidecarRoot(lockRoot, relativeLockPath, "unlinked");
115
+ if (!opened) {
116
+ await waitForRetry();
117
+ continue;
118
+ }
119
+ try {
120
+ // Root.open admits the current file, not necessarily the one we created.
121
+ const currentRaw = await readFileHandleBounded(opened.handle, Buffer.byteLength(raw));
122
+ if (!currentRaw.equals(Buffer.from(raw))) {
123
+ throw new FsSafeError("path-mismatch", "created sidecar lock changed before admission");
124
+ }
125
+ }
126
+ catch (error) {
127
+ await opened.handle.close().catch(() => undefined);
128
+ throw error;
129
+ }
130
+ handle = opened.handle;
96
131
  }
97
132
  else {
98
133
  try {
@@ -107,6 +142,12 @@ export async function acquireSidecarLock(options, context) {
107
142
  await handle.writeFile(raw, "utf8");
108
143
  }
109
144
  const snapshot = { raw, payload, stat: await handle.stat(), ownershipToken };
145
+ if (snapshot.stat.nlink === 0) {
146
+ await handle.close();
147
+ handle = null;
148
+ await waitForRetry();
149
+ continue;
150
+ }
110
151
  const createdHeld = {
111
152
  refCount: 1,
112
153
  reentrantOwner: options.reentrantOwner,
@@ -149,31 +190,36 @@ export async function acquireSidecarLock(options, context) {
149
190
  return returnedHandle;
150
191
  }
151
192
  catch (err) {
152
- if (handle) {
153
- const failedSnapshot = { payload: null };
154
- try {
155
- failedSnapshot.stat = await handle.stat();
156
- }
157
- catch {
158
- // Best-effort cleanup of a failed exclusive create.
193
+ try {
194
+ if (handle) {
195
+ const failedSnapshot = createdSnapshot ?? { payload: null };
196
+ try {
197
+ failedSnapshot.stat = await handle.stat();
198
+ }
199
+ catch {
200
+ // Best-effort cleanup of a failed exclusive create.
201
+ }
202
+ const current = context.held.get(normalizedTargetPath);
203
+ if (current?.handle === handle) {
204
+ context.held.delete(normalizedTargetPath);
205
+ }
206
+ await handle.close().catch(() => undefined);
207
+ // Root-created records retain the creator's byte/token receipt;
208
+ // partial direct writes use the exclusive descriptor's identity.
209
+ await removeSidecarLockIfUnchanged(lockPath, failedSnapshot, {
210
+ lockRoot: options.lockRoot,
211
+ parsePayload: options.parsePayload,
212
+ });
159
213
  }
160
- const current = context.held.get(normalizedTargetPath);
161
- if (current?.handle === handle) {
162
- context.held.delete(normalizedTargetPath);
214
+ else if (createdSnapshot) {
215
+ await removeSidecarLockIfUnchanged(lockPath, createdSnapshot, {
216
+ lockRoot: options.lockRoot,
217
+ parsePayload: options.parsePayload,
218
+ });
163
219
  }
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
220
  }
172
- else if (createdSnapshot) {
173
- await removeSidecarLockIfUnchanged(lockPath, createdSnapshot, {
174
- lockRoot: options.lockRoot,
175
- parsePayload: options.parsePayload,
176
- });
221
+ catch (cleanupError) {
222
+ throw createSuppressedError(cleanupError, err, "file lock acquisition and cleanup both failed");
177
223
  }
178
224
  if (lockFileCreateDenied && withinDenialBudget()) {
179
225
  await retryOrRethrowDenial(err);
@@ -185,24 +231,29 @@ export async function acquireSidecarLock(options, context) {
185
231
  if (ownsReclaimGuard) {
186
232
  await releaseSidecarReclaimGuard(context.reclaimGuards, reclaimGuardPath);
187
233
  ownsReclaimGuard = false;
234
+ await waitForRetry();
188
235
  continue;
189
236
  }
190
237
  const nowMs = Date.now();
191
- let snapshot;
238
+ let rawSnapshot;
239
+ let lockFileOpenDenied = false;
192
240
  try {
193
- snapshot = await readSidecarLockSnapshot(lockPath, {
241
+ rawSnapshot = await readSidecarLockRawSnapshot(lockPath, {
194
242
  lockRoot: options.lockRoot,
195
- parsePayload: options.parsePayload,
196
243
  rejectNonFile: true,
244
+ discardObservation: "changed",
245
+ onOpenFailure: (error) => { lockFileOpenDenied = isTransientLockFileDenial(error, lockPath); },
197
246
  });
198
247
  }
199
248
  catch (readErr) {
200
- if (!isTransientLockFileDenial(readErr, lockPath) || !withinDenialBudget())
249
+ if (!lockFileOpenDenied || !withinDenialBudget())
201
250
  throw readErr;
202
251
  await retryOrRethrowDenial(readErr);
203
252
  continue;
204
253
  }
254
+ const snapshot = parseSidecarLockSnapshot(rawSnapshot, options.parsePayload);
205
255
  if (!snapshot) {
256
+ await waitForRetry();
206
257
  continue;
207
258
  }
208
259
  if (context.held.has(normalizedTargetPath)) {
@@ -222,6 +273,7 @@ export async function acquireSidecarLock(options, context) {
222
273
  lockRoot: options.lockRoot,
223
274
  parsePayload: options.parsePayload,
224
275
  }))) {
276
+ await waitForRetry();
225
277
  continue;
226
278
  }
227
279
  const staleRecovery = options.staleRecovery ?? "fail-closed";
@@ -240,6 +292,8 @@ export async function acquireSidecarLock(options, context) {
240
292
  parsePayload: options.parsePayload,
241
293
  });
242
294
  if (removal === "removed" || removal === "changed") {
295
+ if (removal === "changed")
296
+ await waitForRetry();
243
297
  continue;
244
298
  }
245
299
  await releaseSidecarReclaimGuard(context.reclaimGuards, reclaimGuardPath);
@@ -5,7 +5,9 @@ export declare function createSidecarLockHandle(params: {
5
5
  lockPath: string;
6
6
  normalizedTargetPath: string;
7
7
  verifyStillHeld: () => Promise<boolean>;
8
- release: () => Promise<unknown>;
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: () => Promise<unknown>;
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,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CACjC,GAAG,iBAAiB,CAcpB;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,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;CACjC,GAAG,iBAAiB,CAWpB"}
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
- released = true;
8
- await params.release();
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;AAEvE,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,uBAAuB,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAOjG;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
+ {"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;
@@ -12,6 +12,10 @@ export type SidecarLockSnapshot = {
12
12
  stat?: Stats;
13
13
  ownershipToken?: string;
14
14
  };
15
+ type SidecarLockRawSnapshot = Omit<SidecarLockSnapshot, "payload"> & {
16
+ raw: string;
17
+ };
18
+ export declare function parseSidecarLockSnapshot(snapshot: SidecarLockRawSnapshot | null, parser?: (raw: string) => unknown): SidecarLockSnapshot | null;
15
19
  export declare function readSidecarLockOwnershipToken(raw: string): string | undefined;
16
20
  export declare function serializeSidecarLockPayload(payload: Record<string, unknown>): {
17
21
  raw: string;
@@ -19,15 +23,21 @@ export declare function serializeSidecarLockPayload(payload: Record<string, unkn
19
23
  };
20
24
  export declare function relativeSidecarLockPath(lockRoot: Root, lockPath: string): string;
21
25
  export declare function parseSidecarLockPayload(raw: string, parser?: (raw: string) => unknown): unknown;
22
- export declare function readSidecarLockSnapshot(lockPath: string, options?: {
23
- lockRoot?: Root;
26
+ export declare function readSidecarLockSnapshot(lockPath: string, options?: Parameters<typeof readSidecarLockRawSnapshot>[1] & {
24
27
  parsePayload?: (raw: string) => unknown;
28
+ }): Promise<SidecarLockSnapshot | null>;
29
+ export declare function readSidecarLockRawSnapshot(lockPath: string, options?: {
30
+ lockRoot?: Root;
25
31
  rejectNonFile?: boolean;
26
32
  allowDescriptorIdentityDrift?: boolean;
27
- }): Promise<SidecarLockSnapshot | null>;
28
- export declare function readSidecarLockSnapshotSync(lockPath: string, parsePayload?: (raw: string) => unknown, options?: {
33
+ discardObservation?: "unlinked" | "changed";
34
+ onOpenFailure?: (error: unknown) => void;
35
+ }): Promise<SidecarLockRawSnapshot | null>;
36
+ export declare function readSidecarLockSnapshotSync(lockPath: string, parsePayload?: (raw: string) => unknown, options?: Parameters<typeof readSidecarLockRawSnapshotSync>[1]): SidecarLockSnapshot | null;
37
+ export declare function readSidecarLockRawSnapshotSync(lockPath: string, options?: {
29
38
  rejectNonFile?: boolean;
30
- }): SidecarLockSnapshot | null;
39
+ onOpenFailure?: (error: unknown) => void;
40
+ }): SidecarLockRawSnapshot | null;
31
41
  export declare function removeSidecarLockIfUnchangedSync(lockPath: string, observed: SidecarLockSnapshot): boolean;
32
42
  export declare function sidecarLockSnapshotMatches(current: SidecarLockSnapshot, observed: SidecarLockSnapshot): boolean;
33
43
  export declare function removeSidecarLockIfUnchanged(lockPath: string, observed: SidecarLockSnapshot | null, options?: {
@@ -49,4 +59,5 @@ export declare function removeStaleSidecarLockIfAllowed(params: {
49
59
  lockRoot?: Root;
50
60
  parsePayload?: (raw: string) => unknown;
51
61
  }): Promise<"removed" | "changed" | "not-approved">;
62
+ export {};
52
63
  //# sourceMappingURL=sidecar-lock-reclaim.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sidecar-lock-reclaim.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-reclaim.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sidecar-lock-reclaim.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-reclaim.ts"],"names":[],"mappings":"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"}
@@ -5,12 +5,17 @@ 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";
9
+ import { openSidecarRoot } from "./sidecar-lock-root.js";
8
10
  import { getFsSafeTestHooks } from "./test-hooks.js";
9
11
  const MAX_LOCK_PAYLOAD_BYTES = 1024 * 1024;
10
12
  const SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES = 16;
11
13
  const SIDECAR_LOCK_OWNERSHIP_TOKEN_BITS = SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES * 8;
12
14
  const SIDECAR_LOCK_OWNERSHIP_TOKEN_PREFIX = "\t".repeat(8);
13
15
  const SIDECAR_LOCK_OWNERSHIP_TOKEN_PATTERN = new RegExp(`\\n(${SIDECAR_LOCK_OWNERSHIP_TOKEN_PREFIX}[ \\t]{${SIDECAR_LOCK_OWNERSHIP_TOKEN_BITS}})\\n$`);
16
+ export function parseSidecarLockSnapshot(snapshot, parser) {
17
+ return snapshot && { ...snapshot, payload: parseSidecarLockPayload(snapshot.raw, parser) };
18
+ }
14
19
  function createSidecarLockOwnershipToken() {
15
20
  let token = SIDECAR_LOCK_OWNERSHIP_TOKEN_PREFIX;
16
21
  for (const byte of randomBytes(SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES)) {
@@ -55,16 +60,27 @@ export function parseSidecarLockPayload(raw, parser) {
55
60
  return null;
56
61
  }
57
62
  }
63
+ function missingSnapshotPath(error) {
64
+ if (error.code === "ENOENT")
65
+ return null;
66
+ throw error;
67
+ }
58
68
  export async function readSidecarLockSnapshot(lockPath, options = {}) {
69
+ return parseSidecarLockSnapshot(await readSidecarLockRawSnapshot(lockPath, options), options.parsePayload);
70
+ }
71
+ export async function readSidecarLockRawSnapshot(lockPath, options = {}) {
59
72
  let handle;
60
73
  try {
61
74
  if (options.lockRoot) {
62
- const opened = await options.lockRoot.open(relativeSidecarLockPath(options.lockRoot, lockPath));
75
+ const lockRoot = options.lockRoot;
76
+ const relative = relativeSidecarLockPath(lockRoot, lockPath);
77
+ const opened = await openSidecarRoot(lockRoot, relative, options.discardObservation, options.onOpenFailure);
78
+ if (!opened)
79
+ return null;
63
80
  try {
64
81
  const raw = (await readFileHandleBounded(opened.handle, MAX_LOCK_PAYLOAD_BYTES)).toString("utf8");
65
82
  return {
66
83
  raw,
67
- payload: parseSidecarLockPayload(raw, options.parsePayload),
68
84
  stat: opened.stat,
69
85
  };
70
86
  }
@@ -72,7 +88,9 @@ export async function readSidecarLockSnapshot(lockPath, options = {}) {
72
88
  await opened.handle.close().catch(() => undefined);
73
89
  }
74
90
  }
75
- const before = await fs.lstat(lockPath);
91
+ const before = await fs.lstat(lockPath).catch(missingSnapshotPath);
92
+ if (!before)
93
+ return null;
76
94
  if (!before.isFile() || before.isSymbolicLink()) {
77
95
  if (options.rejectNonFile) {
78
96
  throw new FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`);
@@ -89,11 +107,14 @@ export async function readSidecarLockSnapshot(lockPath, options = {}) {
89
107
  (typeof fsSync.constants.O_NONBLOCK === "number" ? fsSync.constants.O_NONBLOCK : 0));
90
108
  }
91
109
  catch (error) {
110
+ if (error.code === "ENOENT")
111
+ return null;
92
112
  if (options.rejectNonFile && error.code === "ELOOP") {
93
113
  throw new FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`, {
94
114
  cause: error,
95
115
  });
96
116
  }
117
+ options.onOpenFailure?.(error);
97
118
  throw error;
98
119
  }
99
120
  const opened = await handle.stat();
@@ -106,53 +127,64 @@ export async function readSidecarLockSnapshot(lockPath, options = {}) {
106
127
  if (!options.allowDescriptorIdentityDrift && !sameFileIdentity(before, opened))
107
128
  return null;
108
129
  const raw = (await readFileHandleBounded(handle, MAX_LOCK_PAYLOAD_BYTES)).toString("utf8");
109
- const after = await fs.lstat(lockPath);
110
- if (!after.isFile() || !sameFileIdentity(before, after))
130
+ const after = await fs.lstat(lockPath).catch(missingSnapshotPath);
131
+ if (!after || !after.isFile() || !sameFileIdentity(before, after))
111
132
  return null;
112
- return { raw, payload: parseSidecarLockPayload(raw, options.parsePayload), stat: after };
113
- }
114
- catch (err) {
115
- if (err.code === "ENOENT" ||
116
- (err instanceof FsSafeError && err.code === "not-found")) {
117
- return null;
118
- }
119
- throw err;
133
+ return { raw, stat: after };
120
134
  }
121
135
  finally {
122
136
  await handle?.close().catch(() => undefined);
123
137
  }
124
138
  }
139
+ function lstatSidecarLockSync(lockPath) {
140
+ try {
141
+ return fsSync.lstatSync(lockPath);
142
+ }
143
+ catch (error) {
144
+ return missingSnapshotPath(error);
145
+ }
146
+ }
125
147
  export function readSidecarLockSnapshotSync(lockPath, parsePayload, options = {}) {
148
+ return parseSidecarLockSnapshot(readSidecarLockRawSnapshotSync(lockPath, options), parsePayload);
149
+ }
150
+ export function readSidecarLockRawSnapshotSync(lockPath, options = {}) {
126
151
  let fd;
127
152
  try {
128
- const before = fsSync.lstatSync(lockPath);
153
+ const before = lstatSidecarLockSync(lockPath);
154
+ if (!before)
155
+ return null;
129
156
  if (!before.isFile() || before.isSymbolicLink()) {
130
157
  if (options.rejectNonFile) {
131
158
  throw new FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`);
132
159
  }
133
160
  return null;
134
161
  }
135
- const noFollow = process.platform !== "win32" && typeof fsSync.constants.O_NOFOLLOW === "number"
136
- ? fsSync.constants.O_NOFOLLOW
137
- : 0;
138
- fd = fsSync.openSync(lockPath, fsSync.constants.O_RDONLY | noFollow);
162
+ try {
163
+ fd = fsSync.openSync(lockPath, resolveReadOpenFlags());
164
+ }
165
+ catch (error) {
166
+ if (error.code === "ENOENT")
167
+ return null;
168
+ options.onOpenFailure?.(error);
169
+ throw error;
170
+ }
139
171
  const opened = fsSync.fstatSync(fd);
172
+ if (!opened.isFile()) {
173
+ if (options.rejectNonFile) {
174
+ throw new FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`);
175
+ }
176
+ return null;
177
+ }
140
178
  const raw = readFileDescriptorBoundedSync(fd, MAX_LOCK_PAYLOAD_BYTES).toString("utf8");
141
- const after = fsSync.lstatSync(lockPath);
142
- if (!sameFileIdentity(before, opened) || !sameFileIdentity(opened, after))
179
+ const after = lstatSidecarLockSync(lockPath);
180
+ if (!after || !sameFileIdentity(before, opened) || !sameFileIdentity(opened, after))
143
181
  return null;
144
182
  return {
145
183
  raw,
146
- payload: parseSidecarLockPayload(raw, parsePayload),
147
184
  stat: after,
148
185
  ownershipToken: readSidecarLockOwnershipToken(raw),
149
186
  };
150
187
  }
151
- catch (error) {
152
- if (error.code === "ENOENT")
153
- return null;
154
- throw error;
155
- }
156
188
  finally {
157
189
  if (fd !== undefined)
158
190
  fsSync.closeSync(fd);
@@ -191,10 +223,10 @@ export async function removeSidecarLockIfUnchanged(lockPath, observed, options =
191
223
  return false;
192
224
  }
193
225
  if (options.lockRoot) {
194
- await options.lockRoot.remove(relativeSidecarLockPath(options.lockRoot, lockPath)).catch(() => undefined);
226
+ await options.lockRoot.remove(relativeSidecarLockPath(options.lockRoot, lockPath));
195
227
  }
196
228
  else {
197
- await fs.rm(lockPath, { force: true }).catch(() => undefined);
229
+ await fs.rm(lockPath, { force: true });
198
230
  }
199
231
  return true;
200
232
  }
@@ -0,0 +1,3 @@
1
+ import type { OpenResult, Root } from "./root-impl.js";
2
+ export declare function openSidecarRoot(lockRoot: Root, relative: string, discardObservation?: "unlinked" | "changed", onOpenFailure?: (error: unknown) => void): Promise<OpenResult | null>;
3
+ //# sourceMappingURL=sidecar-lock-root.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sidecar-lock-root.d.ts","sourceRoot":"","sources":["../src/sidecar-lock-root.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAGvD,wBAAsB,eAAe,CACnC,QAAQ,EAAE,IAAI,EACd,QAAQ,EAAE,MAAM,EAChB,kBAAkB,CAAC,EAAE,UAAU,GAAG,SAAS,EAC3C,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GACvC,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAuD5B"}
@@ -0,0 +1,76 @@
1
+ import path from "node:path";
2
+ import { inspectDirectoryIdentity } from "./directory-guard.js";
3
+ import { FsSafeError } from "./errors.js";
4
+ import { fileObservation } from "./file-observation.js";
5
+ import { isNotFoundPathError } from "./path.js";
6
+ import { resolveRootPath } from "./root-path.js";
7
+ export async function openSidecarRoot(lockRoot, relative, discardObservation, onOpenFailure) {
8
+ const resolved = await lockRoot.resolve(relative);
9
+ const canonicalPath = async () => (await resolveRootPath({
10
+ absolutePath: resolved,
11
+ rootPath: lockRoot.rootReal,
12
+ rootCanonicalPath: lockRoot.rootReal,
13
+ boundaryLabel: "sidecar lock root",
14
+ })).canonicalPath;
15
+ const expectedRealPath = await canonicalPath();
16
+ if (expectedRealPath === lockRoot.rootReal)
17
+ throw new FsSafeError("not-file", "sidecar lock is a directory");
18
+ const parents = [];
19
+ let completeParents = true;
20
+ if (discardObservation) {
21
+ // Follow permitted in-root aliases first; receipts cover canonical ancestry.
22
+ for (let dir = path.dirname(expectedRealPath);; dir = path.dirname(dir)) {
23
+ try {
24
+ parents.push({ dir, stat: await inspectDirectoryIdentity(dir) });
25
+ }
26
+ catch (error) {
27
+ if (!isNotFoundPathError(error))
28
+ throw error;
29
+ completeParents = false;
30
+ }
31
+ if (dir === lockRoot.rootReal)
32
+ break;
33
+ }
34
+ }
35
+ const observation = fileObservation();
36
+ try {
37
+ const opened = await observation.run(() => lockRoot.open(relative));
38
+ if (opened.realPath !== expectedRealPath) {
39
+ await opened.handle.close().catch(() => undefined);
40
+ throw new FsSafeError("path-mismatch", "sidecar lock path changed during open");
41
+ }
42
+ return opened;
43
+ }
44
+ catch (error) {
45
+ const openFailed = observation.has(error, `open:${resolved}`);
46
+ if (openFailed)
47
+ onOpenFailure?.(error);
48
+ const missing = openFailed && error instanceof FsSafeError && error.code === "not-found";
49
+ if (missing && !discardObservation)
50
+ return null;
51
+ const discardable = observation.has(error, `unlinked:${resolved}`) ||
52
+ (discardObservation === "changed" && observation.has(error, `changed:${resolved}`));
53
+ if (!discardObservation || (!missing && (!completeParents || !discardable)))
54
+ throw error;
55
+ try {
56
+ try {
57
+ const current = await lockRoot.stat(relative);
58
+ if (!current.isFile || current.isSymbolicLink || current.nlink !== 1)
59
+ throw error;
60
+ }
61
+ catch (probeError) {
62
+ if (!(probeError instanceof FsSafeError && probeError.code === "not-found"))
63
+ throw probeError;
64
+ }
65
+ for (const { dir, stat } of parents)
66
+ await inspectDirectoryIdentity(dir, stat);
67
+ await lockRoot.resolve(relative);
68
+ if (await canonicalPath() === expectedRealPath)
69
+ return null;
70
+ }
71
+ catch {
72
+ // Keep the original failure when directory or confinement proof fails.
73
+ }
74
+ throw error;
75
+ }
76
+ }