@volter/browser-wali 0.1.166 → 0.1.168

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/LICENSE +8 -0
  2. package/README.md +9 -0
  3. package/dist/browser-wali-command.d.ts +7 -0
  4. package/dist/browser-wali-command.d.ts.map +1 -1
  5. package/dist/browser-wali-command.js +70 -18
  6. package/dist/browser-wali-command.js.map +1 -1
  7. package/dist/bulk-memory.d.ts +4 -2
  8. package/dist/bulk-memory.d.ts.map +1 -1
  9. package/dist/bulk-memory.js +32 -9
  10. package/dist/bulk-memory.js.map +1 -1
  11. package/dist/capabilities.d.ts +1 -1
  12. package/dist/capabilities.d.ts.map +1 -1
  13. package/dist/capabilities.js +6 -2
  14. package/dist/capabilities.js.map +1 -1
  15. package/dist/child-processes.d.ts +2 -0
  16. package/dist/child-processes.d.ts.map +1 -1
  17. package/dist/child-processes.js +9 -1
  18. package/dist/child-processes.js.map +1 -1
  19. package/dist/clocks.d.ts +3 -0
  20. package/dist/clocks.d.ts.map +1 -0
  21. package/dist/clocks.js +44 -0
  22. package/dist/clocks.js.map +1 -0
  23. package/dist/coherent-file-locks.d.ts.map +1 -1
  24. package/dist/coherent-file-locks.js +2 -1
  25. package/dist/coherent-file-locks.js.map +1 -1
  26. package/dist/credentials.d.ts +4 -0
  27. package/dist/credentials.d.ts.map +1 -0
  28. package/dist/credentials.js +46 -0
  29. package/dist/credentials.js.map +1 -0
  30. package/dist/descriptors.d.ts +14 -0
  31. package/dist/descriptors.d.ts.map +1 -1
  32. package/dist/descriptors.js +45 -5
  33. package/dist/descriptors.js.map +1 -1
  34. package/dist/environment.d.ts +4 -0
  35. package/dist/environment.d.ts.map +1 -0
  36. package/dist/environment.js +23 -0
  37. package/dist/environment.js.map +1 -0
  38. package/dist/epoll-ready.d.ts +27 -0
  39. package/dist/epoll-ready.d.ts.map +1 -0
  40. package/dist/epoll-ready.js +88 -0
  41. package/dist/epoll-ready.js.map +1 -0
  42. package/dist/events.d.ts +1 -0
  43. package/dist/events.d.ts.map +1 -1
  44. package/dist/events.js +55 -24
  45. package/dist/events.js.map +1 -1
  46. package/dist/filesystem-host-session.d.ts +65 -0
  47. package/dist/filesystem-host-session.d.ts.map +1 -0
  48. package/dist/filesystem-host-session.js +951 -0
  49. package/dist/filesystem-host-session.js.map +1 -0
  50. package/dist/filesystem-host.d.ts +13 -81
  51. package/dist/filesystem-host.d.ts.map +1 -1
  52. package/dist/filesystem-host.js +14 -882
  53. package/dist/filesystem-host.js.map +1 -1
  54. package/dist/filesystem-protocol.d.ts +3 -1
  55. package/dist/filesystem-protocol.d.ts.map +1 -1
  56. package/dist/filesystem-remote.d.ts +8 -3
  57. package/dist/filesystem-remote.d.ts.map +1 -1
  58. package/dist/filesystem-remote.js +43 -20
  59. package/dist/filesystem-remote.js.map +1 -1
  60. package/dist/filesystem-service-client.d.ts +4 -0
  61. package/dist/filesystem-service-client.d.ts.map +1 -0
  62. package/dist/filesystem-service-client.js +16 -0
  63. package/dist/filesystem-service-client.js.map +1 -0
  64. package/dist/filesystem.d.ts +26 -4
  65. package/dist/filesystem.d.ts.map +1 -1
  66. package/dist/filesystem.js +633 -175
  67. package/dist/filesystem.js.map +1 -1
  68. package/dist/fork-memory.d.ts +23 -0
  69. package/dist/fork-memory.d.ts.map +1 -0
  70. package/dist/fork-memory.js +93 -0
  71. package/dist/fork-memory.js.map +1 -0
  72. package/dist/fork-worker.js +32 -11
  73. package/dist/fork-worker.js.map +1 -1
  74. package/dist/jumps.d.ts +7 -0
  75. package/dist/jumps.d.ts.map +1 -1
  76. package/dist/jumps.js +19 -1
  77. package/dist/jumps.js.map +1 -1
  78. package/dist/kernel-filesystem-worker.d.ts +2 -0
  79. package/dist/kernel-filesystem-worker.d.ts.map +1 -0
  80. package/dist/kernel-filesystem-worker.js +28 -0
  81. package/dist/kernel-filesystem-worker.js.map +1 -0
  82. package/dist/kernel-filesystem.d.ts +12 -0
  83. package/dist/kernel-filesystem.d.ts.map +1 -0
  84. package/dist/kernel-filesystem.js +45 -0
  85. package/dist/kernel-filesystem.js.map +1 -0
  86. package/dist/kernel-network-worker.d.ts +2 -0
  87. package/dist/kernel-network-worker.d.ts.map +1 -0
  88. package/dist/kernel-network-worker.js +14 -0
  89. package/dist/kernel-network-worker.js.map +1 -0
  90. package/dist/kernel-network.d.ts +14 -0
  91. package/dist/kernel-network.d.ts.map +1 -0
  92. package/dist/kernel-network.js +50 -0
  93. package/dist/kernel-network.js.map +1 -0
  94. package/dist/kernel-process-client.d.ts +6 -1
  95. package/dist/kernel-process-client.d.ts.map +1 -1
  96. package/dist/kernel-process-client.js +10 -1
  97. package/dist/kernel-process-client.js.map +1 -1
  98. package/dist/module-layout.d.ts +8 -0
  99. package/dist/module-layout.d.ts.map +1 -1
  100. package/dist/module-layout.js +263 -2
  101. package/dist/module-layout.js.map +1 -1
  102. package/dist/network-host.d.ts +6 -0
  103. package/dist/network-host.d.ts.map +1 -0
  104. package/dist/network-host.js +1582 -0
  105. package/dist/network-host.js.map +1 -0
  106. package/dist/network-protocol.d.ts +9 -2
  107. package/dist/network-protocol.d.ts.map +1 -1
  108. package/dist/network-protocol.js +3 -0
  109. package/dist/network-protocol.js.map +1 -1
  110. package/dist/network-service-client.d.ts +4 -0
  111. package/dist/network-service-client.d.ts.map +1 -0
  112. package/dist/network-service-client.js +16 -0
  113. package/dist/network-service-client.js.map +1 -0
  114. package/dist/network-worker.js +5 -1150
  115. package/dist/network-worker.js.map +1 -1
  116. package/dist/network.d.ts +13 -1
  117. package/dist/network.d.ts.map +1 -1
  118. package/dist/network.js +175 -48
  119. package/dist/network.js.map +1 -1
  120. package/dist/paths.d.ts +11 -0
  121. package/dist/paths.d.ts.map +1 -0
  122. package/dist/paths.js +97 -0
  123. package/dist/paths.js.map +1 -0
  124. package/dist/process-bridge.d.ts +12 -4
  125. package/dist/process-bridge.d.ts.map +1 -1
  126. package/dist/process-bridge.js +53 -17
  127. package/dist/process-bridge.js.map +1 -1
  128. package/dist/program.d.ts.map +1 -1
  129. package/dist/program.js +8 -0
  130. package/dist/program.js.map +1 -1
  131. package/dist/runtime.d.ts.map +1 -1
  132. package/dist/runtime.js +150 -92
  133. package/dist/runtime.js.map +1 -1
  134. package/dist/shared-buffer-pool.d.ts +18 -5
  135. package/dist/shared-buffer-pool.d.ts.map +1 -1
  136. package/dist/shared-buffer-pool.js +86 -14
  137. package/dist/shared-buffer-pool.js.map +1 -1
  138. package/dist/shared-file-layer.d.ts +3 -1
  139. package/dist/shared-file-layer.d.ts.map +1 -1
  140. package/dist/shared-file-layer.js +150 -35
  141. package/dist/shared-file-layer.js.map +1 -1
  142. package/dist/shared-pipes.d.ts +3 -1
  143. package/dist/shared-pipes.d.ts.map +1 -1
  144. package/dist/shared-pipes.js +105 -33
  145. package/dist/shared-pipes.js.map +1 -1
  146. package/dist/signal-interruption.d.ts +9 -0
  147. package/dist/signal-interruption.d.ts.map +1 -0
  148. package/dist/signal-interruption.js +6 -0
  149. package/dist/signal-interruption.js.map +1 -0
  150. package/dist/signals.d.ts +17 -6
  151. package/dist/signals.d.ts.map +1 -1
  152. package/dist/signals.js +101 -22
  153. package/dist/signals.js.map +1 -1
  154. package/dist/snapshot-filesystem.d.ts +12 -1
  155. package/dist/snapshot-filesystem.d.ts.map +1 -1
  156. package/dist/snapshot-filesystem.js +241 -73
  157. package/dist/snapshot-filesystem.js.map +1 -1
  158. package/dist/socket-flags.d.ts +3 -0
  159. package/dist/socket-flags.d.ts.map +1 -0
  160. package/dist/socket-flags.js +9 -0
  161. package/dist/socket-flags.js.map +1 -0
  162. package/dist/special-files.d.ts +6 -0
  163. package/dist/special-files.d.ts.map +1 -0
  164. package/dist/special-files.js +56 -0
  165. package/dist/special-files.js.map +1 -0
  166. package/dist/stdin.d.ts.map +1 -1
  167. package/dist/stdin.js +13 -5
  168. package/dist/stdin.js.map +1 -1
  169. package/dist/symlink.d.ts +2 -1
  170. package/dist/symlink.d.ts.map +1 -1
  171. package/dist/symlink.js +25 -93
  172. package/dist/symlink.js.map +1 -1
  173. package/dist/system.d.ts +2 -4
  174. package/dist/system.d.ts.map +1 -1
  175. package/dist/system.js +46 -29
  176. package/dist/system.js.map +1 -1
  177. package/dist/thread-coordinator-worker.js +58 -24
  178. package/dist/thread-coordinator-worker.js.map +1 -1
  179. package/dist/thread-lifecycle.d.ts +9 -0
  180. package/dist/thread-lifecycle.d.ts.map +1 -0
  181. package/dist/thread-lifecycle.js +30 -0
  182. package/dist/thread-lifecycle.js.map +1 -0
  183. package/dist/thread-worker.js +125 -44
  184. package/dist/thread-worker.js.map +1 -1
  185. package/dist/threads.d.ts +6 -0
  186. package/dist/threads.d.ts.map +1 -1
  187. package/dist/threads.js +46 -6
  188. package/dist/threads.js.map +1 -1
  189. package/dist/tty-job-control.d.ts +13 -0
  190. package/dist/tty-job-control.d.ts.map +1 -0
  191. package/dist/tty-job-control.js +56 -0
  192. package/dist/tty-job-control.js.map +1 -0
  193. package/dist/tty-settings.d.ts +3 -0
  194. package/dist/tty-settings.d.ts.map +1 -0
  195. package/dist/tty-settings.js +25 -0
  196. package/dist/tty-settings.js.map +1 -0
  197. package/dist/types.d.ts +26 -0
  198. package/dist/types.d.ts.map +1 -1
  199. package/dist/wait-state.d.ts +8 -0
  200. package/dist/wait-state.d.ts.map +1 -1
  201. package/dist/wait-state.js +15 -0
  202. package/dist/wait-state.js.map +1 -1
  203. package/dist/worker-program-protocol.d.ts +22 -2
  204. package/dist/worker-program-protocol.d.ts.map +1 -1
  205. package/dist/worker-program-worker.js +31 -9
  206. package/dist/worker-program-worker.js.map +1 -1
  207. package/dist/worker-program.d.ts +12 -2
  208. package/dist/worker-program.d.ts.map +1 -1
  209. package/dist/worker-program.js +214 -36
  210. package/dist/worker-program.js.map +1 -1
  211. package/dist/write-vector.d.ts +3 -0
  212. package/dist/write-vector.d.ts.map +1 -0
  213. package/dist/write-vector.js +35 -0
  214. package/dist/write-vector.js.map +1 -0
  215. package/package.json +1 -1
@@ -8,12 +8,35 @@ import { encodeBase64, decodeBase64 } from "@volter/browser-runtime/browser-base
8
8
  */
9
9
  class NodeMap extends Map {
10
10
  names = new Map();
11
+ paths = new Map();
12
+ pathsFor(node) { return this.paths.get(node) ?? new Set(); }
11
13
  static rooted() {
12
14
  const nodes = new NodeMap();
13
15
  nodes.set("/", rootNode());
14
16
  return nodes;
15
17
  }
16
18
  set(path, node) {
19
+ const previous = super.get(path);
20
+ if (previous && previous !== node && previous.ino === node.ino) {
21
+ // A write replaces the content version, not the inode's names. Every
22
+ // alias observes the new version without a scan of the whole tree.
23
+ replaceNode(previous, node);
24
+ node = previous;
25
+ }
26
+ else if (previous !== node) {
27
+ if (previous) {
28
+ const aliases = this.paths.get(previous);
29
+ aliases.delete(path);
30
+ if (!aliases.size)
31
+ this.paths.delete(previous);
32
+ }
33
+ let aliases = this.paths.get(node);
34
+ if (!aliases) {
35
+ aliases = new Set();
36
+ this.paths.set(node, aliases);
37
+ }
38
+ aliases.add(path);
39
+ }
17
40
  if (!this.has(path) && path !== "/") {
18
41
  const directory = parent(path);
19
42
  let names = this.names.get(directory);
@@ -26,8 +49,13 @@ class NodeMap extends Map {
26
49
  return super.set(path, node);
27
50
  }
28
51
  delete(path) {
52
+ const node = super.get(path);
29
53
  if (!super.delete(path))
30
54
  return false;
55
+ const aliases = this.paths.get(node);
56
+ aliases.delete(path);
57
+ if (!aliases.size)
58
+ this.paths.delete(node);
31
59
  const directory = parent(path);
32
60
  const names = this.names.get(directory);
33
61
  names?.delete(path.slice(directory === "/" ? 1 : directory.length + 1));
@@ -38,6 +66,7 @@ class NodeMap extends Map {
38
66
  clear() {
39
67
  super.clear();
40
68
  this.names.clear();
69
+ this.paths.clear();
41
70
  }
42
71
  /** The names directly under a directory. */
43
72
  namesIn(directory) {
@@ -62,6 +91,10 @@ class NodeMap extends Map {
62
91
  /** Small transferable filesystem used inside WALI process workers. */
63
92
  export class SnapshotWaliFileSystem {
64
93
  nodes = NodeMap.rooted();
94
+ // WasmFS's OpenFileState holds a File independently of the directory.
95
+ // Adapted from The Emscripten Authors' file_table.cpp/file.h (MIT/NCSA);
96
+ // our owner keeps the inode and its names once across all process tables.
97
+ opened = new Map();
65
98
  /**
66
99
  * The project's store, when the tree is over one (ADR-0042): its index
67
100
  * answers every path this tree has not itself touched, and a path is made a
@@ -140,6 +173,7 @@ export class SnapshotWaliFileSystem {
140
173
  node.atimeMs = node.mtimeMs = entry.mtimeMs;
141
174
  }
142
175
  }
176
+ filesystem.restoreHardLinks(snapshot);
143
177
  filesystem.mutated = false;
144
178
  });
145
179
  return filesystem;
@@ -316,7 +350,7 @@ export class SnapshotWaliFileSystem {
316
350
  const node = this.node(normalized);
317
351
  if (!node)
318
352
  return undefined;
319
- const details = { mode: node.mode, mtimeMs: node.mtimeMs };
353
+ const details = { mode: node.mode, mtimeMs: node.mtimeMs, inode: node.ino };
320
354
  if (node.type === "directory")
321
355
  return { path: normalized, type: "directory", ...details };
322
356
  // Raw for the live channel to the page (ADR-0046): a copy, whose buffer the post transfers.
@@ -358,6 +392,11 @@ export class SnapshotWaliFileSystem {
358
392
  this.mkdirSync(normalized, { recursive: true });
359
393
  }
360
394
  else if (entry.content !== undefined) {
395
+ const kept = previous.get(normalized);
396
+ if (kept?.type === "file") {
397
+ this.mkdirSync(parent(normalized), { recursive: true });
398
+ this.nodes.set(normalized, kept);
399
+ }
361
400
  this.writeFileSync(normalized, decodeBase64(entry.content));
362
401
  // RESTORED, NOT WRITTEN. These bytes came FROM the page; sending
363
402
  // them back is the whole cost `dirty` exists to avoid.
@@ -387,8 +426,30 @@ export class SnapshotWaliFileSystem {
387
426
  this.markMutation(normalized);
388
427
  }
389
428
  }
429
+ this.restoreHardLinks(snapshot);
430
+ for (const node of new Set(previous.values()))
431
+ this.releaseUnlinkedNode(node);
390
432
  });
391
433
  }
434
+ restoreHardLinks(snapshot) {
435
+ const inodes = new Map();
436
+ for (const entry of snapshot.files) {
437
+ if (entry.type !== "file" || !Number.isSafeInteger(entry.inode) || entry.inode < 2)
438
+ continue;
439
+ const path = normalize(entry.path);
440
+ const node = this.node(path);
441
+ if (node?.type !== "file")
442
+ continue;
443
+ const inode = entry.inode;
444
+ const existing = inodes.get(inode);
445
+ if (existing)
446
+ this.nodes.set(path, existing);
447
+ // Snapshot inode numbers describe alias groups, not this owner's
448
+ // allocator. Keep local identities unique, including directories.
449
+ else
450
+ inodes.set(inode, node);
451
+ }
452
+ }
392
453
  /**
393
454
  * The tree for a peer that holds NOTHING: every file's bytes, except the
394
455
  * ones still in the store, which such a peer reads from the store itself.
@@ -407,7 +468,7 @@ export class SnapshotWaliFileSystem {
407
468
  ? { path, type: "directory", mode: node.mode, mtimeMs: node.mtimeMs }
408
469
  : node.load && !node.dirty
409
470
  ? this.sized(path, node)
410
- : { path, type: "file", content: encodeBase64(node.content ?? new Uint8Array()), mode: node.mode, mtimeMs: node.mtimeMs }).concat(this.untouchedStoreEntries()) };
471
+ : { path, type: "file", content: encodeBase64(node.content ?? new Uint8Array()), mode: node.mode, mtimeMs: node.mtimeMs, inode: node.ino }).concat(this.untouchedStoreEntries()) };
411
472
  }
412
473
  /** The store's entries this tree has not made nodes of, as sized entries: their bytes are the store's. */
413
474
  untouchedStoreEntries() {
@@ -445,11 +506,11 @@ export class SnapshotWaliFileSystem {
445
506
  return { files: [...this.nodes.entries()].map(([path, node]) => node.type === "directory"
446
507
  ? { path, type: "directory", mode: node.mode, mtimeMs: node.mtimeMs }
447
508
  : node.dirty
448
- ? { path, type: "file", content: encodeBase64(node.content ?? new Uint8Array()), mode: node.mode, mtimeMs: node.mtimeMs }
509
+ ? { path, type: "file", content: encodeBase64(node.content ?? new Uint8Array()), mode: node.mode, mtimeMs: node.mtimeMs, inode: node.ino }
449
510
  : this.sized(path, node)).concat(this.untouchedStoreEntries()) };
450
511
  }
451
512
  sized(path, node) {
452
- return { path, type: "file", size: node.content?.byteLength ?? node.size ?? 0, mode: node.mode, mtimeMs: node.mtimeMs };
513
+ return { path, type: "file", size: node.content?.byteLength ?? node.size ?? 0, mode: node.mode, mtimeMs: node.mtimeMs, inode: node.ino };
453
514
  }
454
515
  existsSync(path) { return this.node(normalize(path)) !== undefined; }
455
516
  statSync(path) {
@@ -457,6 +518,9 @@ export class SnapshotWaliFileSystem {
457
518
  const node = this.node(normalized);
458
519
  if (!node)
459
520
  throw new Error(`ENOENT: no such file or directory, stat '${normalized}'`);
521
+ return this.statNode(node);
522
+ }
523
+ statNode(node) {
460
524
  const size = node.content?.byteLength ?? node.size ?? 0;
461
525
  return {
462
526
  isFile: () => node.type === "file",
@@ -466,7 +530,7 @@ export class SnapshotWaliFileSystem {
466
530
  mtimeMs: node.mtimeMs,
467
531
  atimeMs: node.atimeMs,
468
532
  ctimeMs: node.mtimeMs,
469
- nlink: 1,
533
+ nlink: node.type === "file" ? this.nodes.pathsFor(node).size : 1,
470
534
  uid: 1000,
471
535
  gid: 1000,
472
536
  dev: 1,
@@ -477,6 +541,60 @@ export class SnapshotWaliFileSystem {
477
541
  };
478
542
  }
479
543
  lstatSync(path) { return this.statSync(path); }
544
+ openFileSync(path) {
545
+ const normalized = normalize(path), node = this.node(normalized);
546
+ if (!node)
547
+ throw new Error(`ENOENT: no such file, open '${normalized}'`);
548
+ if (node.type !== "file")
549
+ throw new Error(`EISDIR: illegal operation on a directory, open '${normalized}'`);
550
+ return this.retainNode(node);
551
+ }
552
+ retainNode(node) {
553
+ this.opened.set(node, (this.opened.get(node) ?? 0) + 1);
554
+ let closed = false;
555
+ const check = () => { if (closed)
556
+ throw new Error("EBADF: closed inode handle"); };
557
+ return {
558
+ stat: () => { check(); return this.statNode(node); },
559
+ read: (offset, length) => {
560
+ check();
561
+ return node.range && node.content === undefined ? node.range(offset, length)
562
+ : this.bytesOf(node).subarray(offset, offset + length);
563
+ },
564
+ write: (offset, data) => {
565
+ check();
566
+ this.mutate(() => { replaceNode(node, this.writeNode(node, offset, data)); this.markNodeMutation(node); });
567
+ },
568
+ truncate: (length) => {
569
+ check();
570
+ this.mutate(() => { replaceNode(node, this.truncateNode(node, length)); this.markNodeMutation(node); });
571
+ },
572
+ chmod: (mode) => { check(); this.mutate(() => { node.mode = mode & 0o7777; this.markNodeMutation(node); }); },
573
+ retain: () => { check(); return this.retainNode(node); },
574
+ close: () => {
575
+ if (closed)
576
+ return;
577
+ closed = true;
578
+ const left = this.opened.get(node) - 1;
579
+ if (left)
580
+ this.opened.set(node, left);
581
+ else {
582
+ this.opened.delete(node);
583
+ this.releaseUnlinkedNode(node);
584
+ }
585
+ },
586
+ };
587
+ }
588
+ releaseUnlinkedNode(node) {
589
+ if (!this.opened.has(node) && !this.nodes.pathsFor(node).size && node.dirty && node.content) {
590
+ this.release(node.content);
591
+ delete node.content;
592
+ }
593
+ }
594
+ markNodeMutation(node) {
595
+ for (const path of this.nodes.pathsFor(node))
596
+ this.markMutation(path);
597
+ }
480
598
  async readFile(path) { return this.readFileSync(path); }
481
599
  readFileSync(path) {
482
600
  const normalized = normalize(path);
@@ -555,42 +673,45 @@ export class SnapshotWaliFileSystem {
555
673
  const existing = removed ? this.nodes.get(normalized) : this.node(normalized);
556
674
  if (existing?.type === "directory")
557
675
  throw new Error(`EISDIR: illegal operation on a directory, write '${normalized}'`);
558
- const current = existing ? this.bytesOf(existing) : new Uint8Array();
559
- const end = offset + data.byteLength;
560
- let content;
561
- if (end <= current.byteLength) {
562
- // In place only in bytes this tree owns: a loaded file's may be a view of a shared layer.
563
- content = existing?.dirty ? current : current.slice();
564
- }
565
- else if (end <= current.buffer.byteLength - current.byteOffset && existing?.dirty) {
566
- // Room in a buffer this store already grew for the file (a dirty
567
- // node's buffer is ours; a loaded or snapshot one may be shared).
568
- content = new Uint8Array(current.buffer, current.byteOffset, end);
569
- content.fill(0, current.byteLength, offset);
570
- }
571
- else {
572
- const spare = this.takeSpare(end);
573
- content = new Uint8Array(spare ?? new ArrayBuffer(Math.max(end, current.byteLength * 2, 4096)), 0, end);
574
- content.set(current);
575
- // A spare buffer holds what its last file did; a new one is zero.
576
- if (spare)
577
- content.fill(0, current.byteLength, offset);
578
- if (existing?.dirty)
579
- this.release(current);
580
- }
581
- content.set(data, offset);
582
- this.nodes.set(normalized, {
583
- type: "file",
584
- content,
585
- dirty: true,
586
- mode: existing?.type === "file" ? existing.mode : 0o644,
587
- atimeMs: realtimeMilliseconds(),
588
- mtimeMs: realtimeMilliseconds(),
589
- ino: existing?.type === "file" ? existing.ino : this.nextInode++,
590
- });
676
+ this.nodes.set(normalized, this.writeNode(existing, offset, data));
591
677
  this.markMutation(normalized);
592
678
  });
593
679
  }
680
+ writeNode(existing, offset, data) {
681
+ const current = existing ? this.bytesOf(existing) : new Uint8Array();
682
+ const end = offset + data.byteLength;
683
+ let content;
684
+ if (end <= current.byteLength) {
685
+ // In place only in bytes this tree owns: a loaded file's may be a view of a shared layer.
686
+ content = existing?.dirty ? current : current.slice();
687
+ }
688
+ else if (end <= current.buffer.byteLength - current.byteOffset && existing?.dirty) {
689
+ // Room in a buffer this store already grew for the file (a dirty
690
+ // node's buffer is ours; a loaded or snapshot one may be shared).
691
+ content = new Uint8Array(current.buffer, current.byteOffset, end);
692
+ content.fill(0, current.byteLength, offset);
693
+ }
694
+ else {
695
+ const spare = this.takeSpare(end);
696
+ content = new Uint8Array(spare ?? new ArrayBuffer(Math.max(end, current.byteLength * 2, 4096)), 0, end);
697
+ content.set(current);
698
+ // A spare buffer holds what its last file did; a new one is zero.
699
+ if (spare)
700
+ content.fill(0, current.byteLength, offset);
701
+ if (existing?.dirty)
702
+ this.release(current);
703
+ }
704
+ content.set(data, offset);
705
+ return {
706
+ type: "file",
707
+ content,
708
+ dirty: true,
709
+ mode: existing?.type === "file" ? existing.mode : 0o644,
710
+ atimeMs: realtimeMilliseconds(),
711
+ mtimeMs: realtimeMilliseconds(),
712
+ ino: existing?.type === "file" ? existing.ino : this.nextInode++,
713
+ };
714
+ }
594
715
  /**
595
716
  * A file cut or extended to `length`, in place where its bytes are this
596
717
  * tree's own: a rewrite (`O_TRUNC`, then writes) keeps the buffer the file
@@ -608,39 +729,42 @@ export class SnapshotWaliFileSystem {
608
729
  throw new Error(`ENOENT: no such file, truncate '${normalized}'`);
609
730
  if (existing.type !== "file")
610
731
  throw new Error(`EISDIR: illegal operation on a directory, truncate '${normalized}'`);
611
- let content;
612
- if (length === 0 && existing.content === undefined) {
613
- // Emptied before it was ever read: the store's bytes are not needed at all.
614
- content = new Uint8Array();
732
+ this.nodes.set(normalized, this.truncateNode(existing, length));
733
+ this.markMutation(normalized);
734
+ });
735
+ }
736
+ truncateNode(existing, length) {
737
+ let content;
738
+ if (length === 0 && existing.content === undefined) {
739
+ // Emptied before it was ever read: the store's bytes are not needed at all.
740
+ content = new Uint8Array();
741
+ }
742
+ else {
743
+ const current = this.bytesOf(existing);
744
+ const capacity = current.buffer.byteLength - current.byteOffset;
745
+ if (existing.dirty && length <= capacity && (capacity <= SPARE_MINIMUM || length * 4 >= capacity)) {
746
+ content = new Uint8Array(current.buffer, current.byteOffset, length);
747
+ content.fill(0, current.byteLength);
615
748
  }
616
749
  else {
617
- const current = this.bytesOf(existing);
618
- const capacity = current.buffer.byteLength - current.byteOffset;
619
- if (existing.dirty && length <= capacity && (capacity <= SPARE_MINIMUM || length * 4 >= capacity)) {
620
- content = new Uint8Array(current.buffer, current.byteOffset, length);
621
- content.fill(0, current.byteLength);
622
- }
623
- else {
624
- // Cut far below what the buffer holds: the file stops holding it, and
625
- // it goes where the next file to grow may take it back (a rewrite's
626
- // own writes, usually), within the spares' bound.
627
- content = new Uint8Array(length);
628
- content.set(current.subarray(0, length));
629
- if (existing.dirty)
630
- this.release(current);
631
- }
750
+ // Cut far below what the buffer holds: the file stops holding it, and
751
+ // it goes where the next file to grow may take it back (a rewrite's
752
+ // own writes, usually), within the spares' bound.
753
+ content = new Uint8Array(length);
754
+ content.set(current.subarray(0, length));
755
+ if (existing.dirty)
756
+ this.release(current);
632
757
  }
633
- this.nodes.set(normalized, {
634
- type: "file",
635
- content,
636
- dirty: true,
637
- mode: existing.mode,
638
- atimeMs: realtimeMilliseconds(),
639
- mtimeMs: realtimeMilliseconds(),
640
- ino: existing.ino,
641
- });
642
- this.markMutation(normalized);
643
- });
758
+ }
759
+ return {
760
+ type: "file",
761
+ content,
762
+ dirty: true,
763
+ mode: existing.mode,
764
+ atimeMs: realtimeMilliseconds(),
765
+ mtimeMs: realtimeMilliseconds(),
766
+ ino: existing.ino,
767
+ };
644
768
  }
645
769
  /**
646
770
  * Buffers of files this tree owned and dropped (replaced by a rename,
@@ -757,11 +881,32 @@ export class SnapshotWaliFileSystem {
757
881
  throw new Error(`ENOENT: no such file, unlink '${normalized}'`);
758
882
  if (node.type !== "file")
759
883
  throw new Error(`EISDIR: illegal operation on a directory, unlink '${normalized}'`);
760
- if (node.dirty && node.content)
761
- this.release(node.content);
762
884
  this.nodes.delete(normalized);
885
+ this.releaseUnlinkedNode(node);
763
886
  this.hide(normalized);
764
887
  this.markMutation(normalized);
888
+ for (const alias of this.nodes.pathsFor(node))
889
+ this.markMutation(alias);
890
+ });
891
+ }
892
+ linkSync(source, destination) {
893
+ this.mutate(() => {
894
+ const from = normalize(source), to = normalize(destination);
895
+ const node = this.node(from);
896
+ if (!node)
897
+ throw new Error(`ENOENT: no such file, link '${from}'`);
898
+ if (node.type === "directory")
899
+ throw new Error(`EPERM: cannot hard-link directory '${from}'`);
900
+ if (this.node(to))
901
+ throw new Error(`EEXIST: destination exists, link '${to}'`);
902
+ const directory = this.node(parent(to));
903
+ if (!directory)
904
+ throw new Error(`ENOENT: no such directory, link '${to}'`);
905
+ if (directory.type !== "directory")
906
+ throw new Error(`ENOTDIR: not a directory, link '${to}'`);
907
+ this.unhide(to);
908
+ this.nodes.set(to, node);
909
+ this.markMutation(to);
765
910
  });
766
911
  }
767
912
  rmdirSync(path) {
@@ -788,6 +933,8 @@ export class SnapshotWaliFileSystem {
788
933
  const node = this.node(source);
789
934
  if (!node)
790
935
  throw new Error(`ENOENT: no such file or directory, rename '${source}'`);
936
+ if (this.node(destination) === node)
937
+ return; // Two names of one inode: Linux rename is a no-op.
791
938
  this.mkdirSync(parent(destination), { recursive: true });
792
939
  // What the store holds under the source moves too: it is made nodes here first.
793
940
  if (this.lower && !this.isHidden(source)) {
@@ -811,9 +958,9 @@ export class SnapshotWaliFileSystem {
811
958
  }
812
959
  const target = `${destination}${path.slice(source.length)}`;
813
960
  const replaced = this.nodes.get(target);
814
- if (replaced?.type === "file" && replaced.dirty && replaced.content && replaced !== value)
815
- this.release(replaced.content);
816
961
  this.nodes.set(target, value);
962
+ if (replaced && replaced !== value)
963
+ this.releaseUnlinkedNode(replaced);
817
964
  this.markMutation(path);
818
965
  this.markMutation(target);
819
966
  }
@@ -860,6 +1007,9 @@ export class SnapshotWaliFileSystem {
860
1007
  this.markMutation(normalized);
861
1008
  });
862
1009
  }
1010
+ utimesMsSync(path, atimeMs, mtimeMs) {
1011
+ this.utimesSync(path, atimeMs, mtimeMs);
1012
+ }
863
1013
  mutate(operation) {
864
1014
  this.mutationDepth += 1;
865
1015
  try {
@@ -875,7 +1025,12 @@ export class SnapshotWaliFileSystem {
875
1025
  }
876
1026
  markMutation(path) {
877
1027
  this.mutated = true;
878
- this.changedPaths?.add(normalize(path));
1028
+ const normalized = normalize(path);
1029
+ this.changedPaths?.add(normalized);
1030
+ const node = this.nodes.get(normalized);
1031
+ if (node)
1032
+ for (const alias of this.nodes.pathsFor(node))
1033
+ this.changedPaths?.add(alias);
879
1034
  }
880
1035
  }
881
1036
  /** A store's file this large is read by range until the program writes it (ADR-0048); a smaller one is read whole, once. */
@@ -889,6 +1044,19 @@ const RANGE_UNCACHED_MINIMUM = 1 << 20;
889
1044
  const SPARE_MINIMUM = 64 * 1024;
890
1045
  const SPARE_COUNT = 2;
891
1046
  const SPARE_TOTAL = 32 * 1024 * 1024;
1047
+ /** Replace an inode's content version without invalidating open descriptions. */
1048
+ function replaceNode(target, source) {
1049
+ if (target === source)
1050
+ return;
1051
+ delete target.content;
1052
+ delete target.load;
1053
+ delete target.range;
1054
+ delete target.size;
1055
+ delete target.dirty;
1056
+ delete target.fromStore;
1057
+ delete target.link;
1058
+ Object.assign(target, source);
1059
+ }
892
1060
  function rootNode() {
893
1061
  const now = realtimeMilliseconds();
894
1062
  return { type: "directory", mode: 0o755, atimeMs: now, mtimeMs: now, ino: 1 };