@superbee/core 0.2.0-pre.1 → 0.2.0-pre.10

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 (215) hide show
  1. package/README.md +127 -0
  2. package/dist/artifact-contract.d.ts +90 -0
  3. package/dist/artifact-contract.d.ts.map +1 -0
  4. package/dist/artifact-contract.js +205 -0
  5. package/dist/artifact-contract.js.map +1 -0
  6. package/dist/backend.d.ts +16 -1
  7. package/dist/backend.d.ts.map +1 -1
  8. package/dist/backend.js +85 -42
  9. package/dist/backend.js.map +1 -1
  10. package/dist/bundle-ops.d.ts +1 -1
  11. package/dist/bundle-ops.d.ts.map +1 -1
  12. package/dist/bundle-ops.js +2 -2
  13. package/dist/bundle-ops.js.map +1 -1
  14. package/dist/bundle.d.ts +2 -1
  15. package/dist/bundle.d.ts.map +1 -1
  16. package/dist/bundle.js +3 -3
  17. package/dist/bundle.js.map +1 -1
  18. package/dist/document-codec.d.ts +6 -1
  19. package/dist/document-codec.d.ts.map +1 -1
  20. package/dist/document-codec.js +8 -4
  21. package/dist/document-codec.js.map +1 -1
  22. package/dist/document-field-actions.d.ts +13 -1
  23. package/dist/document-field-actions.d.ts.map +1 -1
  24. package/dist/document-field-actions.js +47 -6
  25. package/dist/document-field-actions.js.map +1 -1
  26. package/dist/document-mutation.d.ts +15 -1
  27. package/dist/document-mutation.d.ts.map +1 -1
  28. package/dist/document-mutation.js +42 -11
  29. package/dist/document-mutation.js.map +1 -1
  30. package/dist/document-write-policy.d.ts.map +1 -1
  31. package/dist/document-write-policy.js +4 -2
  32. package/dist/document-write-policy.js.map +1 -1
  33. package/dist/engine.d.ts +1 -1
  34. package/dist/engine.d.ts.map +1 -1
  35. package/dist/engine.js +4 -3
  36. package/dist/engine.js.map +1 -1
  37. package/dist/file-journaled-backend-internal.d.ts +19 -0
  38. package/dist/file-journaled-backend-internal.d.ts.map +1 -0
  39. package/dist/file-journaled-backend-internal.js +10 -0
  40. package/dist/file-journaled-backend-internal.js.map +1 -0
  41. package/dist/file-journaled-backend.d.ts +162 -0
  42. package/dist/file-journaled-backend.d.ts.map +1 -0
  43. package/dist/file-journaled-backend.js +1113 -0
  44. package/dist/file-journaled-backend.js.map +1 -0
  45. package/dist/filesystem-host.d.ts +15 -0
  46. package/dist/filesystem-host.d.ts.map +1 -0
  47. package/dist/filesystem-host.js +20 -0
  48. package/dist/filesystem-host.js.map +1 -0
  49. package/dist/filesystem-identity.d.ts +86 -4
  50. package/dist/filesystem-identity.d.ts.map +1 -1
  51. package/dist/filesystem-identity.js +338 -102
  52. package/dist/filesystem-identity.js.map +1 -1
  53. package/dist/filesystem-lock.d.ts +16 -3
  54. package/dist/filesystem-lock.d.ts.map +1 -1
  55. package/dist/filesystem-lock.js +323 -77
  56. package/dist/filesystem-lock.js.map +1 -1
  57. package/dist/filesystem-push-role.d.ts +77 -0
  58. package/dist/filesystem-push-role.d.ts.map +1 -0
  59. package/dist/filesystem-push-role.js +165 -0
  60. package/dist/filesystem-push-role.js.map +1 -0
  61. package/dist/filesystem.d.ts +17 -0
  62. package/dist/filesystem.d.ts.map +1 -0
  63. package/dist/filesystem.js +18 -0
  64. package/dist/filesystem.js.map +1 -0
  65. package/dist/frontmatter.d.ts +8 -0
  66. package/dist/frontmatter.d.ts.map +1 -1
  67. package/dist/frontmatter.js +14 -4
  68. package/dist/frontmatter.js.map +1 -1
  69. package/dist/governed-body-write.d.ts +106 -0
  70. package/dist/governed-body-write.d.ts.map +1 -0
  71. package/dist/governed-body-write.js +251 -0
  72. package/dist/governed-body-write.js.map +1 -0
  73. package/dist/heads-digest.d.ts +32 -0
  74. package/dist/heads-digest.d.ts.map +1 -0
  75. package/dist/heads-digest.js +37 -0
  76. package/dist/heads-digest.js.map +1 -0
  77. package/dist/host-policy-snapshot.d.ts +9 -0
  78. package/dist/host-policy-snapshot.d.ts.map +1 -0
  79. package/dist/host-policy-snapshot.js +33 -0
  80. package/dist/host-policy-snapshot.js.map +1 -0
  81. package/dist/host-text.d.ts +31 -0
  82. package/dist/host-text.d.ts.map +1 -0
  83. package/dist/host-text.js +77 -0
  84. package/dist/host-text.js.map +1 -0
  85. package/dist/hosted-transport/answer-rows.d.ts +216 -0
  86. package/dist/hosted-transport/answer-rows.d.ts.map +1 -0
  87. package/dist/hosted-transport/answer-rows.js +396 -0
  88. package/dist/hosted-transport/answer-rows.js.map +1 -0
  89. package/dist/hosted-transport/carrier.d.ts +142 -0
  90. package/dist/hosted-transport/carrier.d.ts.map +1 -0
  91. package/dist/hosted-transport/carrier.js +241 -0
  92. package/dist/hosted-transport/carrier.js.map +1 -0
  93. package/dist/hosted-transport/history-pages.d.ts +55 -0
  94. package/dist/hosted-transport/history-pages.d.ts.map +1 -0
  95. package/dist/hosted-transport/history-pages.js +69 -0
  96. package/dist/hosted-transport/history-pages.js.map +1 -0
  97. package/dist/hosted-transport/index.d.ts +18 -0
  98. package/dist/hosted-transport/index.d.ts.map +1 -0
  99. package/dist/hosted-transport/index.js +18 -0
  100. package/dist/hosted-transport/index.js.map +1 -0
  101. package/dist/hosted-transport/operations.d.ts +85 -0
  102. package/dist/hosted-transport/operations.d.ts.map +1 -0
  103. package/dist/hosted-transport/operations.js +128 -0
  104. package/dist/hosted-transport/operations.js.map +1 -0
  105. package/dist/hosted-transport/paged-reads.d.ts +87 -0
  106. package/dist/hosted-transport/paged-reads.d.ts.map +1 -0
  107. package/dist/hosted-transport/paged-reads.js +238 -0
  108. package/dist/hosted-transport/paged-reads.js.map +1 -0
  109. package/dist/hosted-transport/read-adapter.d.ts +250 -0
  110. package/dist/hosted-transport/read-adapter.d.ts.map +1 -0
  111. package/dist/hosted-transport/read-adapter.js +557 -0
  112. package/dist/hosted-transport/read-adapter.js.map +1 -0
  113. package/dist/hosted-transport/root-write.d.ts +114 -0
  114. package/dist/hosted-transport/root-write.d.ts.map +1 -0
  115. package/dist/hosted-transport/root-write.js +171 -0
  116. package/dist/hosted-transport/root-write.js.map +1 -0
  117. package/dist/hosted-transport/whole-document-transport.d.ts +174 -0
  118. package/dist/hosted-transport/whole-document-transport.d.ts.map +1 -0
  119. package/dist/hosted-transport/whole-document-transport.js +411 -0
  120. package/dist/hosted-transport/whole-document-transport.js.map +1 -0
  121. package/dist/index.d.ts +22 -8
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/index.js +15 -5
  124. package/dist/index.js.map +1 -1
  125. package/dist/indexeddb-backend.d.ts +38 -6
  126. package/dist/indexeddb-backend.d.ts.map +1 -1
  127. package/dist/indexeddb-backend.js +511 -46
  128. package/dist/indexeddb-backend.js.map +1 -1
  129. package/dist/journaled-backend.d.ts +296 -7
  130. package/dist/journaled-backend.d.ts.map +1 -1
  131. package/dist/journaled-backend.js +312 -0
  132. package/dist/journaled-backend.js.map +1 -1
  133. package/dist/kind-coverage.d.ts +99 -0
  134. package/dist/kind-coverage.d.ts.map +1 -0
  135. package/dist/kind-coverage.js +186 -0
  136. package/dist/kind-coverage.js.map +1 -0
  137. package/dist/kinds.d.ts +31 -1
  138. package/dist/kinds.d.ts.map +1 -1
  139. package/dist/kinds.js +83 -16
  140. package/dist/kinds.js.map +1 -1
  141. package/dist/links.d.ts +1 -1
  142. package/dist/links.d.ts.map +1 -1
  143. package/dist/links.js +5 -10
  144. package/dist/links.js.map +1 -1
  145. package/dist/managed-document-metadata.d.ts +2 -0
  146. package/dist/managed-document-metadata.d.ts.map +1 -0
  147. package/dist/managed-document-metadata.js +9 -0
  148. package/dist/managed-document-metadata.js.map +1 -0
  149. package/dist/memory-backend.d.ts +2 -2
  150. package/dist/memory-backend.d.ts.map +1 -1
  151. package/dist/memory-backend.js +32 -16
  152. package/dist/memory-backend.js.map +1 -1
  153. package/dist/paths.d.ts +6 -2
  154. package/dist/paths.d.ts.map +1 -1
  155. package/dist/paths.js +35 -33
  156. package/dist/paths.js.map +1 -1
  157. package/dist/platform.d.ts +84 -6
  158. package/dist/platform.d.ts.map +1 -1
  159. package/dist/platform.js +9 -4
  160. package/dist/platform.js.map +1 -1
  161. package/dist/portable-export.d.ts +119 -0
  162. package/dist/portable-export.d.ts.map +1 -0
  163. package/dist/portable-export.js +501 -0
  164. package/dist/portable-export.js.map +1 -0
  165. package/dist/publication-filesystem.d.ts +1 -1
  166. package/dist/publication-filesystem.d.ts.map +1 -1
  167. package/dist/publication-filesystem.js +1 -1
  168. package/dist/publication-filesystem.js.map +1 -1
  169. package/dist/query-filter.d.ts +7 -2
  170. package/dist/query-filter.d.ts.map +1 -1
  171. package/dist/query-filter.js +24 -1
  172. package/dist/query-filter.js.map +1 -1
  173. package/dist/query-order.d.ts +22 -0
  174. package/dist/query-order.d.ts.map +1 -0
  175. package/dist/query-order.js +42 -0
  176. package/dist/query-order.js.map +1 -0
  177. package/dist/remote-backend.d.ts +91 -27
  178. package/dist/remote-backend.d.ts.map +1 -1
  179. package/dist/remote-backend.js +217 -49
  180. package/dist/remote-backend.js.map +1 -1
  181. package/dist/remote-document-codec.d.ts +15 -0
  182. package/dist/remote-document-codec.d.ts.map +1 -0
  183. package/dist/remote-document-codec.js +103 -0
  184. package/dist/remote-document-codec.js.map +1 -0
  185. package/dist/remote-error.d.ts +38 -0
  186. package/dist/remote-error.d.ts.map +1 -0
  187. package/dist/remote-error.js +59 -0
  188. package/dist/remote-error.js.map +1 -0
  189. package/dist/remote-operations.d.ts +6 -3
  190. package/dist/remote-operations.d.ts.map +1 -1
  191. package/dist/remote-operations.js +20 -5
  192. package/dist/remote-operations.js.map +1 -1
  193. package/dist/remote-parsers.d.ts +86 -0
  194. package/dist/remote-parsers.d.ts.map +1 -0
  195. package/dist/remote-parsers.js +181 -0
  196. package/dist/remote-parsers.js.map +1 -0
  197. package/dist/remote.d.ts +7 -2
  198. package/dist/remote.d.ts.map +1 -1
  199. package/dist/remote.js +6 -1
  200. package/dist/remote.js.map +1 -1
  201. package/dist/storage.d.ts +4 -2
  202. package/dist/storage.d.ts.map +1 -1
  203. package/dist/storage.js +4 -1
  204. package/dist/storage.js.map +1 -1
  205. package/dist/types.d.ts +28 -5
  206. package/dist/types.d.ts.map +1 -1
  207. package/dist/uncertain-write.d.ts +49 -13
  208. package/dist/uncertain-write.d.ts.map +1 -1
  209. package/dist/uncertain-write.js +36 -12
  210. package/dist/uncertain-write.js.map +1 -1
  211. package/dist/versioning.d.ts +9 -4
  212. package/dist/versioning.d.ts.map +1 -1
  213. package/dist/versioning.js +9 -4
  214. package/dist/versioning.js.map +1 -1
  215. package/package.json +34 -2
@@ -15,6 +15,15 @@
15
15
  * entirely between two probes of one observation can be traversed by the operating system
16
16
  * during a listing or open, but its bytes can never be returned as EXACT, because the leaf
17
17
  * witness at the post-walk must match the entry the requested path reaches.
18
+ * - A bulk read runs its observations inside one {@link ObservationPass}: the same protocol per
19
+ * path, but a directory's listing is taken once and reused while the directory's change
20
+ * witness proves its entry set unchanged, and every EXACT verdict that used a reused listing has
21
+ * its post-walk repeated at the end of the pass without trusting timestamps. Listing the parent
22
+ * per observation would make a pass over a directory of N documents read O(N²) directory
23
+ * entries. The speedup assumes local disks, whose directory times advance on every entry change;
24
+ * on Linux any other filesystem type lists per use, and elsewhere a mount whose times can lie
25
+ * (network or FUSE attribute caches, settable Windows directory times) is covered by the
26
+ * end-of-pass re-verification, short of an inode number reused within the pass.
18
27
  * - Mutations hold one cross-process lock keyed by a pure fold of the root and `rel`, decide
19
28
  * inside it, create directories one segment at a time with a spelling check, and publish a
20
29
  * first-created leaf with `link` so the host's own equivalence refuses a second spelling the
@@ -37,8 +46,8 @@
37
46
  * and the root's spelling is folded into the lock key rather than verified.
38
47
  *
39
48
  * Every filesystem call goes through a {@link FilesystemIdentityPort} passed to the protocol
40
- * functions. Production binds {@link nodeFilesystemIdentityPort} once, as a module constant;
41
- * nothing on a backend instance can substitute it. Normalizing stores that rewrite names on
49
+ * functions. Production constructs a Node binding with the backend's captured host policy;
50
+ * the policy supplies classifications, never a replacement identity protocol. Normalizing stores that rewrite names on
42
51
  * write (legacy HFS+) are unsupported: an id whose NFD form differs from its own form is written
43
52
  * and then refused as an alias on every later observation, never silently aliased.
44
53
  */
@@ -47,12 +56,12 @@ import path from "node:path";
47
56
  import { createHash, randomUUID } from "node:crypto";
48
57
  import { ConcurrentReplacementError, FilesystemIdentityAliasError, InvalidInputError } from "./errors.js";
49
58
  import { acquireFilesystemIdentityLock } from "./filesystem-lock.js";
50
- const WINDOWS_REPLACE_CONFLICT_CODES = new Set(["EACCES", "EBUSY", "EPERM"]);
59
+ import { captureFilesystemHostPolicy } from "./filesystem-host.js";
51
60
  /**
52
- * Windows can transiently deny replacement while another process is closing a read handle or a
61
+ * A host can transiently deny replacement while another process is closing a read handle or a
53
62
  * scanner holds the destination. An in-place rename retry cannot remain bound to the complete
54
63
  * parent-and-leaf observation: either can change after any final check. Abort this attempt instead,
55
- * mapping only the errno values Node uses for a Windows replacement sharing violation to the typed
64
+ * mapping only a host-classified replacement conflict to the typed
56
65
  * contention signal owned by the higher-level fresh read/decide/CAS loop. Every other platform and
57
66
  * errno remains the original immediate failure.
58
67
  */
@@ -61,8 +70,7 @@ async function replaceExistingLeaf(port, tmp, target, rel) {
61
70
  await port.rename(tmp, target);
62
71
  }
63
72
  catch (err) {
64
- const code = err.code ?? "";
65
- if ((port.platform ?? process.platform) === "win32" && WINDOWS_REPLACE_CONFLICT_CODES.has(code)) {
73
+ if (port.isReplacementConflict?.(err)) {
66
74
  throw new ConcurrentReplacementError(rel, 1, 10);
67
75
  }
68
76
  throw err;
@@ -219,6 +227,13 @@ async function probeSegment(port, candidate, rel, segment) {
219
227
  function hasExact(listing, segment) {
220
228
  return listing.some((entry) => entry.name === segment);
221
229
  }
230
+ /** One fresh `entries` call per use: the single-read protocol. */
231
+ function freshListings(port) {
232
+ return async (dir) => {
233
+ const listing = await port.entries(dir);
234
+ return listing === null ? null : { has: (name) => hasExact(listing, name) };
235
+ };
236
+ }
222
237
  /**
223
238
  * M-REALIZE: per segment, root to leaf, it must exist and the parent listing must carry its exact
224
239
  * spelling. A listing-only alias decision is sound here because the identity lock is held: no
@@ -277,15 +292,15 @@ async function recordWalk(port, rootResolved, segments, rel) {
277
292
  * (`handle` given) applies that leaf table against the handle's inode, probing the leaf
278
293
  * unconditionally.
279
294
  */
280
- async function confirmedWalk(port, rootResolved, segments, rel, recorded, handle) {
295
+ async function confirmedWalk(port, rootResolved, segments, rel, recorded, handle, listings = freshListings(port)) {
281
296
  let parent = rootResolved;
282
297
  for (let index = 0; index < segments.length; index++) {
283
298
  const segment = segments[index];
284
299
  const candidate = path.join(parent, segment);
285
- const listing = await port.entries(parent);
300
+ const listing = await listings(parent);
286
301
  if (listing === null)
287
302
  return "absent";
288
- const listed = hasExact(listing, segment);
303
+ const listed = listing.has(segment);
289
304
  if (handle !== null && index === segments.length - 1) {
290
305
  const verdict = classifyLeaf({ listed, probe: await probeSegment(port, candidate, rel, segment), handle });
291
306
  if (verdict === "aliased")
@@ -316,14 +331,17 @@ function countRestart(restarts, rel) {
316
331
  * from O-PROBE, bounded; the handle is closed before any restart opens another.
317
332
  */
318
333
  export async function observeExact(port, root, rel, read) {
334
+ return observeWith(port, path.resolve(root), rel, read, freshListings(port));
335
+ }
336
+ /** The observation protocol of {@link observeExact}, with the parent listings drawn from `listings`. */
337
+ async function observeWith(port, rootResolved, rel, read, listings, onExact) {
319
338
  const segments = relSegments(rel);
320
- const rootResolved = path.resolve(root);
321
339
  const target = path.join(rootResolved, ...segments);
322
340
  for (let restarts = 0;;) {
323
341
  const recorded = await recordWalk(port, rootResolved, segments, rel);
324
342
  if (recorded === null)
325
343
  return { state: "absent" };
326
- const verified = await confirmedWalk(port, rootResolved, segments, rel, recorded, null);
344
+ const verified = await confirmedWalk(port, rootResolved, segments, rel, recorded, null, listings);
327
345
  if (verified === "absent")
328
346
  return { state: "absent" };
329
347
  if (verified === "restart") {
@@ -337,10 +355,9 @@ export async function observeExact(port, root, rel, read) {
337
355
  catch (err) {
338
356
  if (isAbsentPathError(err))
339
357
  return { state: "absent" };
340
- // Win32 can report EPERM when a concurrent delete/replace briefly denies an open. That is
341
- // an uncertain generation, not a durable permission verdict; re-run the witnessed walk and
358
+ // A host-classified transient open failure reports an uncertain generation; re-run the witnessed walk and
342
359
  // let the existing bound turn persistent contention into ConcurrentReplacementError.
343
- if (process.platform === "win32" && err.code === "EPERM") {
360
+ if (port.isTransientOpenError?.(err)) {
344
361
  restarts = countRestart(restarts, rel);
345
362
  continue;
346
363
  }
@@ -352,13 +369,15 @@ export async function observeExact(port, root, rel, read) {
352
369
  value = await read(opened.handle, target);
353
370
  if (value === null)
354
371
  return { state: "absent" };
355
- verdict = await confirmedWalk(port, rootResolved, segments, rel, recorded, opened);
372
+ verdict = await confirmedWalk(port, rootResolved, segments, rel, recorded, opened, listings);
356
373
  }
357
374
  finally {
358
375
  await port.close(opened.handle);
359
376
  }
360
- if (verdict === "exact")
377
+ if (verdict === "exact") {
378
+ onExact?.(recorded, { dev: opened.dev, ino: opened.ino });
361
379
  return { state: "exact", value };
380
+ }
362
381
  if (verdict === "absent")
363
382
  return { state: "absent" };
364
383
  restarts = countRestart(restarts, rel);
@@ -385,6 +404,154 @@ export async function probeExact(port, root, rel) {
385
404
  restarts = countRestart(restarts, rel);
386
405
  }
387
406
  }
407
+ // ── observation pass ──────────────────────────────────────────────────────────
408
+ /**
409
+ * How long a directory's modification and status-change times must have been still before a
410
+ * listing of it may be reused. A change that lands in the same timestamp tick as the directory's
411
+ * previous change leaves both times unchanged, so a witness is only a change detector once the
412
+ * clock has moved past that tick. Three seconds covers the coarsest supported granularity (FAT's
413
+ * two-second mtime) plus margin; finer clocks (APFS, ext4, NTFS) are covered with room to spare.
414
+ * A time in the future (clock skew) is never settled.
415
+ */
416
+ const SETTLED_AFTER_NS = 3000000000n;
417
+ function witnessKey(witness) {
418
+ return `${witness.dev}:${witness.ino}:${witness.mtimeNs}:${witness.ctimeNs}`;
419
+ }
420
+ /**
421
+ * Open an {@link ObservationPass} over `(port, root)`.
422
+ *
423
+ * Every observation runs the exact single-read protocol of {@link observeExact} — recorded walk,
424
+ * confirmed pre-walk, open, read, confirmed post-walk with the handle still open, bounded
425
+ * restarts — and differs only in where a walk's parent listing comes from. The first time a
426
+ * directory is needed the pass takes `witness, entries, witness`; when the two witnesses agree and
427
+ * the directory's times have settled, that listing is kept. Each later use re-takes the witness
428
+ * (one `stat`, constant cost) and reuses the kept listing only if the witness is unchanged;
429
+ * otherwise it lists afresh exactly as a single read would.
430
+ *
431
+ * Two independent arguments make a reused listing safe:
432
+ *
433
+ * 1. Timestamps (the fast path is right in the moment). Where a directory's times advance on every
434
+ * entry change — local APFS, HFS+, ext4, XFS, btrfs, NTFS, FAT — an unchanged settled witness
435
+ * means the entry set is the kept listing at the moment of use, so every verdict is the one a
436
+ * single read reaches against the same state.
437
+ * 2. Re-verification (no timestamps trusted). Where the witness can lie — attribute caches on
438
+ * network and FUSE mounts, settable directory times on Windows, a clock stepped backwards — an
439
+ * observation that used a reused listing may be wrong in the moment, so its verdict is not
440
+ * trusted: a refusal or absence reached through a reused listing is re-taken at once by
441
+ * {@link observeExact}, and `finish` repeats the post-walk of every EXACT verdict that used one
442
+ * against fresh listings (one per directory) and fresh probes. Each segment must still be
443
+ * listed under its exact spelling and reach the inode recorded for it, the leaf the inode the
444
+ * bytes were read from; anything else is returned for a fresh single read. Since published
445
+ * bytes are immutable per inode, a verdict that passes is a state of this identity at `finish`.
446
+ * What this cannot see is the read file being deleted and its inode NUMBER reused by a new file
447
+ * under the same spelling within the pass, because the handle is closed by then.
448
+ *
449
+ * On Linux, a mount whose filesystem type is not a known local one (NFS, SMB, FUSE, 9p, ...) gets
450
+ * no directory witness at all, so it lists exactly as single reads do.
451
+ */
452
+ export function openObservationPass(port, root, now = Date.now) {
453
+ const rootResolved = path.resolve(root);
454
+ const kept = new Map();
455
+ const reusedVerdicts = [];
456
+ const fresh = freshListings(port);
457
+ const witnessOf = port.directoryWitness?.bind(port);
458
+ const settled = (witness) => {
459
+ const nowNs = BigInt(Math.floor(now())) * 1000000n;
460
+ const latest = witness.mtimeNs > witness.ctimeNs ? witness.mtimeNs : witness.ctimeNs;
461
+ return nowNs - latest >= SETTLED_AFTER_NS;
462
+ };
463
+ /** Listings for one observation; `reused()` says whether any came from the kept set. */
464
+ const listingsFor = () => {
465
+ let reused = false;
466
+ if (witnessOf === undefined)
467
+ return { source: fresh, reused: () => false };
468
+ const source = async (dir) => {
469
+ const keep = kept.get(dir);
470
+ if (keep !== undefined) {
471
+ const current = await witnessOf(dir);
472
+ if (current !== null && witnessKey(current) === keep.witness) {
473
+ reused = true;
474
+ return keep.names;
475
+ }
476
+ kept.delete(dir);
477
+ }
478
+ const before = await witnessOf(dir);
479
+ const listing = await port.entries(dir);
480
+ if (listing === null)
481
+ return null;
482
+ const names = new Set(listing.map((entry) => entry.name));
483
+ const after = await witnessOf(dir);
484
+ if (before !== null && after !== null && witnessKey(before) === witnessKey(after) && settled(after)) {
485
+ kept.set(dir, { witness: witnessKey(after), names });
486
+ }
487
+ return names;
488
+ };
489
+ return { source, reused: () => reused };
490
+ };
491
+ return {
492
+ async observe(rel, read) {
493
+ const listings = listingsFor();
494
+ let exact;
495
+ let observed;
496
+ try {
497
+ observed = await observeWith(port, rootResolved, rel, read, listings.source, (recorded, handle) => {
498
+ exact = { rel, segments: relSegments(rel), recorded, handle };
499
+ });
500
+ }
501
+ catch (error) {
502
+ if (!listings.reused())
503
+ throw error;
504
+ return observeExact(port, rootResolved, rel, read);
505
+ }
506
+ if (!listings.reused())
507
+ return observed;
508
+ if (observed.state === "absent")
509
+ return observeExact(port, rootResolved, rel, read);
510
+ if (exact !== undefined)
511
+ reusedVerdicts.push(exact);
512
+ return observed;
513
+ },
514
+ async finish() {
515
+ const retry = new Set();
516
+ const endListings = new Map();
517
+ const listedAtEnd = async (dir) => {
518
+ if (!endListings.has(dir)) {
519
+ const listing = await port.entries(dir);
520
+ endListings.set(dir, listing === null ? null : new Set(listing.map((entry) => entry.name)));
521
+ }
522
+ return endListings.get(dir);
523
+ };
524
+ try {
525
+ for (const verdict of reusedVerdicts) {
526
+ if (!(await holdsAtEnd(port, rootResolved, verdict, listedAtEnd)))
527
+ retry.add(verdict.rel);
528
+ }
529
+ }
530
+ finally {
531
+ kept.clear();
532
+ reusedVerdicts.length = 0;
533
+ }
534
+ return [...retry];
535
+ },
536
+ };
537
+ }
538
+ /** The post-walk of one EXACT verdict, repeated against end-of-pass listings and fresh probes. */
539
+ async function holdsAtEnd(port, rootResolved, verdict, listedAtEnd) {
540
+ let parent = rootResolved;
541
+ for (let index = 0; index < verdict.segments.length; index++) {
542
+ const segment = verdict.segments[index];
543
+ const listing = await listedAtEnd(parent);
544
+ if (listing === null || !listing.has(segment))
545
+ return false;
546
+ const candidate = path.join(parent, segment);
547
+ const probed = await port.probe(candidate);
548
+ const expected = index === verdict.segments.length - 1 ? verdict.handle : verdict.recorded[index];
549
+ if (probed === null || probed.kind === "symlink" || !sameWitness(probed, expected))
550
+ return false;
551
+ parent = candidate;
552
+ }
553
+ return true;
554
+ }
388
555
  // ── mutation ──────────────────────────────────────────────────────────────────
389
556
  /**
390
557
  * Roots (resolved) whose filesystem refused a hard link. Learned lazily on the first creation
@@ -394,7 +561,15 @@ export async function probeExact(port, root, rel) {
394
561
  * first-creation writers of a host-equated pair the fold misses are not excluded" applies there
395
562
  * and only there.
396
563
  */
397
- const linkUnsupportedRoots = new Set();
564
+ const linkUnsupportedRoots = new WeakMap();
565
+ function unsupportedLinkRoots(port) {
566
+ let roots = linkUnsupportedRoots.get(port);
567
+ if (!roots) {
568
+ roots = new Set();
569
+ linkUnsupportedRoots.set(port, roots);
570
+ }
571
+ return roots;
572
+ }
398
573
  /**
399
574
  * Process-local queue per identity key, shared by every `FilesystemBackend` instance: the
400
575
  * engine constructs a fresh adapter per bundle operation, so an instance-level map would
@@ -508,7 +683,7 @@ async function createExactLeaf(port, tmp, target, rel, leaf, rootResolved) {
508
683
  return;
509
684
  }
510
685
  if (outcome === "unsupported") {
511
- linkUnsupportedRoots.add(rootResolved);
686
+ unsupportedLinkRoots(port).add(rootResolved);
512
687
  await port.rename(tmp, target);
513
688
  return;
514
689
  }
@@ -556,7 +731,7 @@ export async function mutateExact(port, root, rel, body) {
556
731
  await replaceExistingLeaf(port, tmp, target, rel);
557
732
  return;
558
733
  }
559
- if (linkUnsupportedRoots.has(rootResolved)) {
734
+ if (unsupportedLinkRoots(port).has(rootResolved)) {
560
735
  await port.rename(tmp, target);
561
736
  return;
562
737
  }
@@ -588,88 +763,149 @@ function kindOf(stats) {
588
763
  return "file";
589
764
  return "other";
590
765
  }
591
- /** The one production binding: `node:fs`, the temp-file convention, and the identity lock. */
592
- export const nodeFilesystemIdentityPort = Object.freeze({
593
- platform: process.platform,
594
- async probe(target) {
595
- let lstats;
596
- try {
597
- lstats = await fs.lstat(target);
598
- }
599
- catch (err) {
600
- if (isAbsentPathError(err))
601
- return null;
602
- throw err;
603
- }
604
- return { kind: kindOf(lstats), dev: lstats.dev, ino: lstats.ino };
605
- },
606
- async entries(dir) {
607
- try {
608
- const dirents = await fs.readdir(dir, { withFileTypes: true });
609
- return dirents.map((dirent) => ({ name: dirent.name, kind: kindOf(dirent) }));
766
+ /**
767
+ * Linux filesystem types (`statfs` magic numbers) whose directory times are kept by the local
768
+ * kernel and advance on every entry change. Anything else on Linux — NFS, SMB/CIFS, FUSE, 9p,
769
+ * Ceph, and every type not named here — gets no directory witness, so an observation pass lists it
770
+ * exactly as single reads do. Other platforms expose no stable type through Node, so their
771
+ * witnesses are offered and the pass's end-of-pass re-verification is what covers a remote mount.
772
+ */
773
+ const LOCAL_LINUX_FILESYSTEMS = new Set([
774
+ 0xef53, // ext2/3/4
775
+ 0x58465342, // xfs
776
+ 0x9123683e, // btrfs
777
+ 0x01021994, // tmpfs
778
+ 0x858458f6, // ramfs
779
+ 0x794c7630, // overlayfs
780
+ 0x2fc12fc1, // zfs
781
+ 0xf2f52010, // f2fs
782
+ 0xca451a4e, // bcachefs
783
+ 0x4d44, // vfat
784
+ 0x2011bab0, // exfat
785
+ 0x7366746e, // ntfs3
786
+ 0x482b, // hfsplus
787
+ ]);
788
+ /** Whether a directory witness may be offered on `platform` for a filesystem of `filesystemType`. */
789
+ export function directoryWitnessTrusted(platform, filesystemType) {
790
+ if (platform !== "linux")
791
+ return true;
792
+ return filesystemType !== null && LOCAL_LINUX_FILESYSTEMS.has(filesystemType);
793
+ }
794
+ /** Whether `dir`'s filesystem may offer directory witnesses, asked once per device per port. */
795
+ function witnessTrust() {
796
+ const devices = new Map();
797
+ return (dev, dir) => {
798
+ if (process.platform !== "linux")
799
+ return Promise.resolve(true);
800
+ let trusted = devices.get(dev);
801
+ if (trusted === undefined) {
802
+ trusted = fs.statfs(dir).then((info) => directoryWitnessTrusted("linux", Number(info.type)), () => false);
803
+ devices.set(dev, trusted);
610
804
  }
611
- catch (err) {
612
- if (isAbsentPathError(err))
805
+ return trusted;
806
+ };
807
+ }
808
+ /** The one production binding: `node:fs`, the temp-file convention, and the identity lock. */
809
+ export function createNodeFilesystemIdentityPort(hostPolicy) {
810
+ const policy = captureFilesystemHostPolicy(hostPolicy);
811
+ const witnessTrustedOn = witnessTrust();
812
+ return Object.freeze({
813
+ isTransientOpenError: policy.isTransientOpenError,
814
+ isReplacementConflict: policy.isReplacementConflict,
815
+ async probe(target) {
816
+ let lstats;
817
+ try {
818
+ lstats = await fs.lstat(target);
819
+ }
820
+ catch (err) {
821
+ if (isAbsentPathError(err))
822
+ return null;
823
+ throw err;
824
+ }
825
+ return { kind: kindOf(lstats), dev: lstats.dev, ino: lstats.ino };
826
+ },
827
+ async entries(dir) {
828
+ try {
829
+ const dirents = await fs.readdir(dir, { withFileTypes: true });
830
+ return dirents.map((dirent) => ({ name: dirent.name, kind: kindOf(dirent) }));
831
+ }
832
+ catch (err) {
833
+ if (isAbsentPathError(err))
834
+ return null;
835
+ throw err;
836
+ }
837
+ },
838
+ async directoryWitness(dir) {
839
+ let stats;
840
+ try {
841
+ stats = await fs.stat(dir, { bigint: true });
842
+ }
843
+ catch (err) {
844
+ if (isAbsentPathError(err))
845
+ return null;
846
+ throw err;
847
+ }
848
+ if (!(await witnessTrustedOn(stats.dev, dir)))
613
849
  return null;
614
- throw err;
615
- }
616
- },
617
- async open(target) {
618
- const handle = await fs.open(target, "r");
619
- try {
620
- const stats = await handle.stat();
621
- return { handle, dev: stats.dev, ino: stats.ino };
622
- }
623
- catch (err) {
624
- await handle.close().catch(() => { });
625
- throw err;
626
- }
627
- },
628
- readAll(handle) {
629
- return handle.readFile();
630
- },
631
- close(handle) {
632
- return handle.close();
633
- },
634
- async stat(target) {
635
- return { mtime: (await fs.stat(target)).mtime };
636
- },
637
- async mkdir(dir) {
638
- try {
639
- await fs.mkdir(dir);
640
- return "created";
641
- }
642
- catch (err) {
643
- if (err.code === "EEXIST")
644
- return "exists";
645
- throw err;
646
- }
647
- },
648
- async writeTemp(dir, name, bytes) {
649
- await fs.writeFile(path.join(dir, name), bytes);
650
- },
651
- async link(from, to) {
652
- try {
653
- await fs.link(from, to);
654
- return "linked";
655
- }
656
- catch (err) {
657
- const code = err.code;
658
- if (code === "EEXIST")
659
- return "exists";
660
- if (code === "EPERM" || code === "ENOTSUP" || code === "EOPNOTSUPP" || code === "EXDEV")
661
- return "unsupported";
662
- throw err;
663
- }
664
- },
665
- async rename(from, to) {
666
- await fs.rename(from, to);
667
- },
668
- async unlink(target) {
669
- await fs.unlink(target);
670
- },
671
- claim(key, identity) {
672
- return acquireFilesystemIdentityLock(key, `${identity.root}:${identity.rel}`, { portableRoot: identity.root });
673
- },
674
- });
850
+ return { dev: stats.dev, ino: stats.ino, mtimeNs: stats.mtimeNs, ctimeNs: stats.ctimeNs };
851
+ },
852
+ async open(target) {
853
+ const handle = await fs.open(target, "r");
854
+ try {
855
+ const stats = await handle.stat();
856
+ return { handle, dev: stats.dev, ino: stats.ino };
857
+ }
858
+ catch (err) {
859
+ await handle.close().catch(() => { });
860
+ throw err;
861
+ }
862
+ },
863
+ readAll(handle) {
864
+ return handle.readFile();
865
+ },
866
+ close(handle) {
867
+ return handle.close();
868
+ },
869
+ async stat(target) {
870
+ return { mtime: (await fs.stat(target)).mtime };
871
+ },
872
+ async mkdir(dir) {
873
+ try {
874
+ await fs.mkdir(dir);
875
+ return "created";
876
+ }
877
+ catch (err) {
878
+ if (err.code === "EEXIST")
879
+ return "exists";
880
+ throw err;
881
+ }
882
+ },
883
+ async writeTemp(dir, name, bytes) {
884
+ await fs.writeFile(path.join(dir, name), bytes);
885
+ },
886
+ async link(from, to) {
887
+ try {
888
+ await fs.link(from, to);
889
+ return "linked";
890
+ }
891
+ catch (err) {
892
+ const code = err.code;
893
+ if (code === "EEXIST")
894
+ return "exists";
895
+ if (code === "EPERM" || code === "ENOTSUP" || code === "EOPNOTSUPP" || code === "EXDEV")
896
+ return "unsupported";
897
+ throw err;
898
+ }
899
+ },
900
+ async rename(from, to) {
901
+ await fs.rename(from, to);
902
+ },
903
+ async unlink(target) {
904
+ await fs.unlink(target);
905
+ },
906
+ claim(key, identity) {
907
+ return acquireFilesystemIdentityLock(key, `${identity.root}:${identity.rel}`, { portableRoot: identity.root, hostPolicy: policy });
908
+ },
909
+ });
910
+ }
675
911
  //# sourceMappingURL=filesystem-identity.js.map