@componentor/fs 3.0.50 → 3.0.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4257,6 +4257,22 @@ var VFSEngine = class {
4257
4257
  // generation when implicitDirs was last rebuilt
4258
4258
  pathIndexGen = 0;
4259
4259
  // bumped on every pathIndex mutation
4260
+ // Incrementally maintained "number of pathIndex entries that have this
4261
+ // path as a strict ancestor" map. Lets `isImplicitDirectory` answer in
4262
+ // O(1) — an implicit dir P is exactly !pathIndex.has(P) && descCount[P] > 0.
4263
+ // Without this, every `isImplicitDirectory` call triggered an O(N×depth)
4264
+ // rebuild of `implicitDirs`, and the 3.0.49 fix put one of those calls on
4265
+ // the hot path of every fresh write/symlink/link/copy — making batch
4266
+ // writes O(N²) on total path count.
4267
+ descCount = /* @__PURE__ */ new Map();
4268
+ // descCount is in sync with pathIndex iff descCountGen >= pathIndexGen.
4269
+ // Helpers `setPathIndex`/`deletePathIndex` keep them in sync. Code that
4270
+ // mutates `pathIndex` directly (only test scaffolding does this in
4271
+ // practice — see the implicit-directory tests in vfs-engine.test.ts)
4272
+ // bumps `pathIndexGen` without going through the helpers, which leaves
4273
+ // descCount stale; `isImplicitDirectory` notices the mismatch and
4274
+ // recomputes descCount on demand.
4275
+ descCountGen = 0;
4260
4276
  // Configurable upper bounds
4261
4277
  maxInodes = 4e6;
4262
4278
  maxBlocks = 4e6;
@@ -4539,7 +4555,7 @@ var VFSEngine = class {
4539
4555
  if (!path.startsWith("/") || path.includes("\0")) {
4540
4556
  throw new Error(`Corrupt VFS: inode ${i} has invalid path "${path.substring(0, 50)}"`);
4541
4557
  }
4542
- this.pathIndex.set(path, i);
4558
+ this.setPathIndex(path, i);
4543
4559
  }
4544
4560
  this.pathIndexGen++;
4545
4561
  }
@@ -4861,7 +4877,7 @@ var VFSEngine = class {
4861
4877
  gid: this.processGid
4862
4878
  };
4863
4879
  this.writeInode(idx, inode);
4864
- this.pathIndex.set(path, idx);
4880
+ this.setPathIndex(path, idx);
4865
4881
  this.pathIndexGen++;
4866
4882
  return idx;
4867
4883
  }
@@ -5019,7 +5035,7 @@ var VFSEngine = class {
5019
5035
  this.freeBlockRange(inode.firstBlock, inode.blockCount);
5020
5036
  inode.type = INODE_TYPE.FREE;
5021
5037
  this.writeInode(idx, inode);
5022
- this.pathIndex.delete(path);
5038
+ this.deletePathIndex(path);
5023
5039
  this.pathIndexGen++;
5024
5040
  if (idx < this.freeInodeHint) this.freeInodeHint = idx;
5025
5041
  this.commitPending();
@@ -5141,7 +5157,7 @@ var VFSEngine = class {
5141
5157
  this.freeBlockRange(descInode.firstBlock, descInode.blockCount);
5142
5158
  descInode.type = INODE_TYPE.FREE;
5143
5159
  this.writeInode(descIdx, descInode);
5144
- this.pathIndex.delete(desc);
5160
+ this.deletePathIndex(desc);
5145
5161
  }
5146
5162
  this.pathIndexGen++;
5147
5163
  this.commitPending();
@@ -5161,12 +5177,12 @@ var VFSEngine = class {
5161
5177
  this.freeBlockRange(childInode.firstBlock, childInode.blockCount);
5162
5178
  childInode.type = INODE_TYPE.FREE;
5163
5179
  this.writeInode(childIdx, childInode);
5164
- this.pathIndex.delete(child);
5180
+ this.deletePathIndex(child);
5165
5181
  }
5166
5182
  }
5167
5183
  inode.type = INODE_TYPE.FREE;
5168
5184
  this.writeInode(idx, inode);
5169
- this.pathIndex.delete(path);
5185
+ this.deletePathIndex(path);
5170
5186
  this.pathIndexGen++;
5171
5187
  if (idx < this.freeInodeHint) this.freeInodeHint = idx;
5172
5188
  this.commitPending();
@@ -5257,7 +5273,7 @@ var VFSEngine = class {
5257
5273
  this.freeBlockRange(existingInode.firstBlock, existingInode.blockCount);
5258
5274
  existingInode.type = INODE_TYPE.FREE;
5259
5275
  this.writeInode(existingIdx, existingInode);
5260
- this.pathIndex.delete(newPath);
5276
+ this.deletePathIndex(newPath);
5261
5277
  if (existingIdx < this.freeInodeHint) this.freeInodeHint = existingIdx;
5262
5278
  }
5263
5279
  if (cleanDescendants) {
@@ -5267,7 +5283,7 @@ var VFSEngine = class {
5267
5283
  this.freeBlockRange(descInode.firstBlock, descInode.blockCount);
5268
5284
  descInode.type = INODE_TYPE.FREE;
5269
5285
  this.writeInode(descIdx, descInode);
5270
- this.pathIndex.delete(desc);
5286
+ this.deletePathIndex(desc);
5271
5287
  if (descIdx < this.freeInodeHint) this.freeInodeHint = descIdx;
5272
5288
  }
5273
5289
  }
@@ -5278,8 +5294,8 @@ var VFSEngine = class {
5278
5294
  inode.pathLength = pathLen;
5279
5295
  inode.mtime = Date.now();
5280
5296
  this.writeInode(idx, inode);
5281
- this.pathIndex.delete(oldPath);
5282
- this.pathIndex.set(newPath, idx);
5297
+ this.deletePathIndex(oldPath);
5298
+ this.setPathIndex(newPath, idx);
5283
5299
  this.pathIndexGen++;
5284
5300
  if (inode.type === INODE_TYPE.DIRECTORY) {
5285
5301
  const prefix = oldPath === "/" ? "/" : oldPath + "/";
@@ -5297,8 +5313,8 @@ var VFSEngine = class {
5297
5313
  childInode.pathOffset = cpo;
5298
5314
  childInode.pathLength = cpl;
5299
5315
  this.writeInode(i, childInode);
5300
- this.pathIndex.delete(p);
5301
- this.pathIndex.set(childNewPath, i);
5316
+ this.deletePathIndex(p);
5317
+ this.setPathIndex(childNewPath, i);
5302
5318
  }
5303
5319
  }
5304
5320
  this.commitPending();
@@ -5767,11 +5783,71 @@ var VFSEngine = class {
5767
5783
  /**
5768
5784
  * Check if a path is an implicit directory (exists because files exist under it,
5769
5785
  * but no explicit directory inode was created for it).
5786
+ *
5787
+ * O(1) via the incrementally maintained `descCount` map (an implicit dir
5788
+ * is exactly !pathIndex.has(P) && descCount[P] > 0). If `pathIndex` was
5789
+ * mutated directly without going through the helpers (test scaffolding),
5790
+ * descCount is stale and we rebuild it from scratch — once — to resync.
5770
5791
  */
5771
5792
  isImplicitDirectory(path) {
5772
5793
  if (path === "/") return false;
5773
- this.rebuildImplicitDirs();
5774
- return this.implicitDirs.has(path);
5794
+ if (this.pathIndex.has(path)) return false;
5795
+ if (this.descCountGen < this.pathIndexGen) this.rebuildDescCount();
5796
+ return (this.descCount.get(path) ?? 0) > 0;
5797
+ }
5798
+ /**
5799
+ * Recompute `descCount` from scratch by walking every pathIndex entry's
5800
+ * ancestor chain. O(N×depth). Only triggered when something bypassed the
5801
+ * setPathIndex/deletePathIndex helpers — in production code that's
5802
+ * never; the tests exercise this path.
5803
+ */
5804
+ rebuildDescCount() {
5805
+ this.descCount.clear();
5806
+ for (const path of this.pathIndex.keys()) {
5807
+ this.bumpDescCount(path);
5808
+ }
5809
+ this.descCountGen = this.pathIndexGen;
5810
+ }
5811
+ // ---- pathIndex helpers — keep `descCount` in sync ----
5812
+ // Every pathIndex.set/delete in the engine MUST go through these so the
5813
+ // `descCount` map (used by `isImplicitDirectory`) stays correct. We
5814
+ // anticipate the caller's `pathIndexGen++` by setting `descCountGen` to
5815
+ // `pathIndexGen + 1`; idempotent across multiple helper calls within a
5816
+ // single logical op (e.g. rmdir doing N deletes then one bump). Test
5817
+ // code that mutates `pathIndex` directly leaves descCountGen behind,
5818
+ // which is what triggers the rebuild path in `isImplicitDirectory`.
5819
+ setPathIndex(path, idx) {
5820
+ const had = this.pathIndex.has(path);
5821
+ this.pathIndex.set(path, idx);
5822
+ if (!had) this.bumpDescCount(path);
5823
+ this.descCountGen = this.pathIndexGen + 1;
5824
+ }
5825
+ deletePathIndex(path) {
5826
+ const had = this.pathIndex.delete(path);
5827
+ if (had) this.decDescCount(path);
5828
+ this.descCountGen = this.pathIndexGen + 1;
5829
+ return had;
5830
+ }
5831
+ bumpDescCount(path) {
5832
+ let pos = path.length;
5833
+ while (true) {
5834
+ pos = path.lastIndexOf("/", pos - 1);
5835
+ if (pos <= 0) break;
5836
+ const ancestor = path.substring(0, pos);
5837
+ this.descCount.set(ancestor, (this.descCount.get(ancestor) ?? 0) + 1);
5838
+ }
5839
+ }
5840
+ decDescCount(path) {
5841
+ let pos = path.length;
5842
+ while (true) {
5843
+ pos = path.lastIndexOf("/", pos - 1);
5844
+ if (pos <= 0) break;
5845
+ const ancestor = path.substring(0, pos);
5846
+ const cur = this.descCount.get(ancestor);
5847
+ if (cur === void 0) break;
5848
+ if (cur <= 1) this.descCount.delete(ancestor);
5849
+ else this.descCount.set(ancestor, cur - 1);
5850
+ }
5775
5851
  }
5776
5852
  /**
5777
5853
  * Get direct children of a directory path, including implicit subdirectories.