@componentor/fs 3.0.50 → 3.0.52

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.
@@ -164,6 +164,22 @@ var VFSEngine = class {
164
164
  // generation when implicitDirs was last rebuilt
165
165
  pathIndexGen = 0;
166
166
  // bumped on every pathIndex mutation
167
+ // Incrementally maintained "number of pathIndex entries that have this
168
+ // path as a strict ancestor" map. Lets `isImplicitDirectory` answer in
169
+ // O(1) — an implicit dir P is exactly !pathIndex.has(P) && descCount[P] > 0.
170
+ // Without this, every `isImplicitDirectory` call triggered an O(N×depth)
171
+ // rebuild of `implicitDirs`, and the 3.0.49 fix put one of those calls on
172
+ // the hot path of every fresh write/symlink/link/copy — making batch
173
+ // writes O(N²) on total path count.
174
+ descCount = /* @__PURE__ */ new Map();
175
+ // descCount is in sync with pathIndex iff descCountGen >= pathIndexGen.
176
+ // Helpers `setPathIndex`/`deletePathIndex` keep them in sync. Code that
177
+ // mutates `pathIndex` directly (only test scaffolding does this in
178
+ // practice — see the implicit-directory tests in vfs-engine.test.ts)
179
+ // bumps `pathIndexGen` without going through the helpers, which leaves
180
+ // descCount stale; `isImplicitDirectory` notices the mismatch and
181
+ // recomputes descCount on demand.
182
+ descCountGen = 0;
167
183
  // Configurable upper bounds
168
184
  maxInodes = 4e6;
169
185
  maxBlocks = 4e6;
@@ -446,7 +462,7 @@ var VFSEngine = class {
446
462
  if (!path.startsWith("/") || path.includes("\0")) {
447
463
  throw new Error(`Corrupt VFS: inode ${i} has invalid path "${path.substring(0, 50)}"`);
448
464
  }
449
- this.pathIndex.set(path, i);
465
+ this.setPathIndex(path, i);
450
466
  }
451
467
  this.pathIndexGen++;
452
468
  }
@@ -768,7 +784,7 @@ var VFSEngine = class {
768
784
  gid: this.processGid
769
785
  };
770
786
  this.writeInode(idx, inode);
771
- this.pathIndex.set(path, idx);
787
+ this.setPathIndex(path, idx);
772
788
  this.pathIndexGen++;
773
789
  return idx;
774
790
  }
@@ -926,7 +942,7 @@ var VFSEngine = class {
926
942
  this.freeBlockRange(inode.firstBlock, inode.blockCount);
927
943
  inode.type = INODE_TYPE.FREE;
928
944
  this.writeInode(idx, inode);
929
- this.pathIndex.delete(path);
945
+ this.deletePathIndex(path);
930
946
  this.pathIndexGen++;
931
947
  if (idx < this.freeInodeHint) this.freeInodeHint = idx;
932
948
  this.commitPending();
@@ -1048,7 +1064,7 @@ var VFSEngine = class {
1048
1064
  this.freeBlockRange(descInode.firstBlock, descInode.blockCount);
1049
1065
  descInode.type = INODE_TYPE.FREE;
1050
1066
  this.writeInode(descIdx, descInode);
1051
- this.pathIndex.delete(desc);
1067
+ this.deletePathIndex(desc);
1052
1068
  }
1053
1069
  this.pathIndexGen++;
1054
1070
  this.commitPending();
@@ -1068,12 +1084,12 @@ var VFSEngine = class {
1068
1084
  this.freeBlockRange(childInode.firstBlock, childInode.blockCount);
1069
1085
  childInode.type = INODE_TYPE.FREE;
1070
1086
  this.writeInode(childIdx, childInode);
1071
- this.pathIndex.delete(child);
1087
+ this.deletePathIndex(child);
1072
1088
  }
1073
1089
  }
1074
1090
  inode.type = INODE_TYPE.FREE;
1075
1091
  this.writeInode(idx, inode);
1076
- this.pathIndex.delete(path);
1092
+ this.deletePathIndex(path);
1077
1093
  this.pathIndexGen++;
1078
1094
  if (idx < this.freeInodeHint) this.freeInodeHint = idx;
1079
1095
  this.commitPending();
@@ -1164,7 +1180,7 @@ var VFSEngine = class {
1164
1180
  this.freeBlockRange(existingInode.firstBlock, existingInode.blockCount);
1165
1181
  existingInode.type = INODE_TYPE.FREE;
1166
1182
  this.writeInode(existingIdx, existingInode);
1167
- this.pathIndex.delete(newPath);
1183
+ this.deletePathIndex(newPath);
1168
1184
  if (existingIdx < this.freeInodeHint) this.freeInodeHint = existingIdx;
1169
1185
  }
1170
1186
  if (cleanDescendants) {
@@ -1174,7 +1190,7 @@ var VFSEngine = class {
1174
1190
  this.freeBlockRange(descInode.firstBlock, descInode.blockCount);
1175
1191
  descInode.type = INODE_TYPE.FREE;
1176
1192
  this.writeInode(descIdx, descInode);
1177
- this.pathIndex.delete(desc);
1193
+ this.deletePathIndex(desc);
1178
1194
  if (descIdx < this.freeInodeHint) this.freeInodeHint = descIdx;
1179
1195
  }
1180
1196
  }
@@ -1185,8 +1201,8 @@ var VFSEngine = class {
1185
1201
  inode.pathLength = pathLen;
1186
1202
  inode.mtime = Date.now();
1187
1203
  this.writeInode(idx, inode);
1188
- this.pathIndex.delete(oldPath);
1189
- this.pathIndex.set(newPath, idx);
1204
+ this.deletePathIndex(oldPath);
1205
+ this.setPathIndex(newPath, idx);
1190
1206
  this.pathIndexGen++;
1191
1207
  if (inode.type === INODE_TYPE.DIRECTORY) {
1192
1208
  const prefix = oldPath === "/" ? "/" : oldPath + "/";
@@ -1204,8 +1220,8 @@ var VFSEngine = class {
1204
1220
  childInode.pathOffset = cpo;
1205
1221
  childInode.pathLength = cpl;
1206
1222
  this.writeInode(i, childInode);
1207
- this.pathIndex.delete(p);
1208
- this.pathIndex.set(childNewPath, i);
1223
+ this.deletePathIndex(p);
1224
+ this.setPathIndex(childNewPath, i);
1209
1225
  }
1210
1226
  }
1211
1227
  this.commitPending();
@@ -1674,11 +1690,71 @@ var VFSEngine = class {
1674
1690
  /**
1675
1691
  * Check if a path is an implicit directory (exists because files exist under it,
1676
1692
  * but no explicit directory inode was created for it).
1693
+ *
1694
+ * O(1) via the incrementally maintained `descCount` map (an implicit dir
1695
+ * is exactly !pathIndex.has(P) && descCount[P] > 0). If `pathIndex` was
1696
+ * mutated directly without going through the helpers (test scaffolding),
1697
+ * descCount is stale and we rebuild it from scratch — once — to resync.
1677
1698
  */
1678
1699
  isImplicitDirectory(path) {
1679
1700
  if (path === "/") return false;
1680
- this.rebuildImplicitDirs();
1681
- return this.implicitDirs.has(path);
1701
+ if (this.pathIndex.has(path)) return false;
1702
+ if (this.descCountGen < this.pathIndexGen) this.rebuildDescCount();
1703
+ return (this.descCount.get(path) ?? 0) > 0;
1704
+ }
1705
+ /**
1706
+ * Recompute `descCount` from scratch by walking every pathIndex entry's
1707
+ * ancestor chain. O(N×depth). Only triggered when something bypassed the
1708
+ * setPathIndex/deletePathIndex helpers — in production code that's
1709
+ * never; the tests exercise this path.
1710
+ */
1711
+ rebuildDescCount() {
1712
+ this.descCount.clear();
1713
+ for (const path of this.pathIndex.keys()) {
1714
+ this.bumpDescCount(path);
1715
+ }
1716
+ this.descCountGen = this.pathIndexGen;
1717
+ }
1718
+ // ---- pathIndex helpers — keep `descCount` in sync ----
1719
+ // Every pathIndex.set/delete in the engine MUST go through these so the
1720
+ // `descCount` map (used by `isImplicitDirectory`) stays correct. We
1721
+ // anticipate the caller's `pathIndexGen++` by setting `descCountGen` to
1722
+ // `pathIndexGen + 1`; idempotent across multiple helper calls within a
1723
+ // single logical op (e.g. rmdir doing N deletes then one bump). Test
1724
+ // code that mutates `pathIndex` directly leaves descCountGen behind,
1725
+ // which is what triggers the rebuild path in `isImplicitDirectory`.
1726
+ setPathIndex(path, idx) {
1727
+ const had = this.pathIndex.has(path);
1728
+ this.pathIndex.set(path, idx);
1729
+ if (!had) this.bumpDescCount(path);
1730
+ this.descCountGen = this.pathIndexGen + 1;
1731
+ }
1732
+ deletePathIndex(path) {
1733
+ const had = this.pathIndex.delete(path);
1734
+ if (had) this.decDescCount(path);
1735
+ this.descCountGen = this.pathIndexGen + 1;
1736
+ return had;
1737
+ }
1738
+ bumpDescCount(path) {
1739
+ let pos = path.length;
1740
+ while (true) {
1741
+ pos = path.lastIndexOf("/", pos - 1);
1742
+ if (pos <= 0) break;
1743
+ const ancestor = path.substring(0, pos);
1744
+ this.descCount.set(ancestor, (this.descCount.get(ancestor) ?? 0) + 1);
1745
+ }
1746
+ }
1747
+ decDescCount(path) {
1748
+ let pos = path.length;
1749
+ while (true) {
1750
+ pos = path.lastIndexOf("/", pos - 1);
1751
+ if (pos <= 0) break;
1752
+ const ancestor = path.substring(0, pos);
1753
+ const cur = this.descCount.get(ancestor);
1754
+ if (cur === void 0) break;
1755
+ if (cur <= 1) this.descCount.delete(ancestor);
1756
+ else this.descCount.set(ancestor, cur - 1);
1757
+ }
1682
1758
  }
1683
1759
  /**
1684
1760
  * Get direct children of a directory path, including implicit subdirectories.
@@ -2393,8 +2469,11 @@ var SAB_OFFSETS = {
2393
2469
  // BigUint64 - full data size across all chunks
2394
2470
  CHUNK_IDX: 24,
2395
2471
  // Int32 - 0-based chunk index
2396
- RESERVED: 28,
2397
- // Int32 - reserved
2472
+ HEARTBEAT: 28,
2473
+ // Int32 - liveness counter; the relay worker bumps this ~1×/s
2474
+ // while its event loop is alive (incl. mid-await of a
2475
+ // long op) so a spin-waiting main thread can tell
2476
+ // "slow" from "dead". Never written by the main thread.
2398
2477
  HEADER_SIZE: 32
2399
2478
  // Data payload starts here
2400
2479
  };
@@ -2462,6 +2541,15 @@ var asyncSab = null;
2462
2541
  var asyncCtrl = null;
2463
2542
  var tabId = "";
2464
2543
  var HEADER_SIZE = SAB_OFFSETS.HEADER_SIZE;
2544
+ var HEARTBEAT_INDEX = SAB_OFFSETS.HEARTBEAT >> 2;
2545
+ var HEARTBEAT_INTERVAL_MS = 1e3;
2546
+ var heartbeatTimer = null;
2547
+ function startHeartbeat() {
2548
+ if (heartbeatTimer !== null || !ctrl) return;
2549
+ heartbeatTimer = setInterval(() => {
2550
+ Atomics.add(ctrl, HEARTBEAT_INDEX, 1);
2551
+ }, HEARTBEAT_INTERVAL_MS);
2552
+ }
2465
2553
  var clientPorts = /* @__PURE__ */ new Map();
2466
2554
  var portQueue = [];
2467
2555
  var yieldChannel = new MessageChannel();
@@ -3668,6 +3756,7 @@ self.onmessage = async (e) => {
3668
3756
  readySab = msg.readySab;
3669
3757
  ctrl = new Int32Array(sab, 0, 8);
3670
3758
  readySignal = new Int32Array(readySab, 0, 1);
3759
+ startHeartbeat();
3671
3760
  }
3672
3761
  if (msg.asyncSab) {
3673
3762
  asyncSab = msg.asyncSab;
@@ -3706,6 +3795,7 @@ self.onmessage = async (e) => {
3706
3795
  readySab = msg.readySab;
3707
3796
  ctrl = new Int32Array(sab, 0, 8);
3708
3797
  readySignal = new Int32Array(readySab, 0, 1);
3798
+ startHeartbeat();
3709
3799
  }
3710
3800
  if (msg.asyncSab) {
3711
3801
  asyncSab = msg.asyncSab;
@@ -3742,6 +3832,7 @@ self.onmessage = async (e) => {
3742
3832
  readySab = msg.readySab;
3743
3833
  ctrl = new Int32Array(sab, 0, 8);
3744
3834
  readySignal = new Int32Array(readySab, 0, 1);
3835
+ startHeartbeat();
3745
3836
  }
3746
3837
  if (msg.asyncSab) {
3747
3838
  asyncSab = msg.asyncSab;