@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.
package/dist/index.js CHANGED
@@ -362,7 +362,12 @@ var OP = {
362
362
  var SAB_OFFSETS = {
363
363
  // Int32 - bytes in this chunk
364
364
  TOTAL_LEN: 16,
365
- // Int32 - reserved
365
+ // Int32 - 0-based chunk index
366
+ HEARTBEAT: 28,
367
+ // Int32 - liveness counter; the relay worker bumps this ~1×/s
368
+ // while its event loop is alive (incl. mid-await of a
369
+ // long op) so a spin-waiting main thread can tell
370
+ // "slow" from "dead". Never written by the main thread.
366
371
  HEADER_SIZE: 32
367
372
  // Data payload starts here
368
373
  };
@@ -2556,19 +2561,37 @@ var DEFAULT_SAB_SIZE = 2 * 1024 * 1024;
2556
2561
  var instanceRegistry = /* @__PURE__ */ new Map();
2557
2562
  var HEADER_SIZE = SAB_OFFSETS.HEADER_SIZE;
2558
2563
  var _canAtomicsWait = typeof globalThis.WorkerGlobalScope !== "undefined";
2559
- var SPIN_TIMEOUT_MS = 1e4;
2560
- function spinWait(arr, index, value) {
2564
+ var SAB_HEARTBEAT_INDEX = SAB_OFFSETS.HEARTBEAT >> 2;
2565
+ var SPIN_STALL_TIMEOUT_MS = 2e4;
2566
+ var SPIN_NO_HEARTBEAT_TIMEOUT_MS = 3e4;
2567
+ function spinWait(arr, index, value, heartbeatArr) {
2561
2568
  if (_canAtomicsWait) {
2562
2569
  Atomics.wait(arr, index, value);
2563
- } else {
2570
+ return;
2571
+ }
2572
+ if (!heartbeatArr) {
2564
2573
  const start = performance.now();
2565
2574
  while (Atomics.load(arr, index) === value) {
2566
- if (performance.now() - start > SPIN_TIMEOUT_MS) {
2575
+ if (performance.now() - start > SPIN_NO_HEARTBEAT_TIMEOUT_MS) {
2567
2576
  throw new Error(
2568
- `VFS sync operation timed out after ${SPIN_TIMEOUT_MS / 1e3}s \u2014 SharedWorker may be unresponsive`
2577
+ `VFS sync operation timed out after ${SPIN_NO_HEARTBEAT_TIMEOUT_MS / 1e3}s \u2014 relay worker did not respond`
2569
2578
  );
2570
2579
  }
2571
2580
  }
2581
+ return;
2582
+ }
2583
+ let lastBeat = Atomics.load(heartbeatArr, SAB_HEARTBEAT_INDEX);
2584
+ let lastProgress = performance.now();
2585
+ while (Atomics.load(arr, index) === value) {
2586
+ const beat = Atomics.load(heartbeatArr, SAB_HEARTBEAT_INDEX);
2587
+ if (beat !== lastBeat) {
2588
+ lastBeat = beat;
2589
+ lastProgress = performance.now();
2590
+ } else if (performance.now() - lastProgress > SPIN_STALL_TIMEOUT_MS) {
2591
+ throw new Error(
2592
+ `VFS sync operation aborted: relay worker heartbeat stalled for ${SPIN_STALL_TIMEOUT_MS / 1e3}s \u2014 worker is unresponsive`
2593
+ );
2594
+ }
2572
2595
  }
2573
2596
  }
2574
2597
  var VFSFileSystem = class {
@@ -3041,7 +3064,7 @@ var VFSFileSystem = class {
3041
3064
  if (signal === -1) {
3042
3065
  throw this.initError ?? new Error("VFS initialization failed");
3043
3066
  }
3044
- spinWait(this.readySignal, 0, 0);
3067
+ spinWait(this.readySignal, 0, 0, this.ctrl);
3045
3068
  const finalSignal = Atomics.load(this.readySignal, 0);
3046
3069
  if (finalSignal === -1) {
3047
3070
  throw this.initError ?? new Error("VFS initialization failed");
@@ -3055,7 +3078,8 @@ var VFSFileSystem = class {
3055
3078
  const maxChunk = this.sab.byteLength - HEADER_SIZE;
3056
3079
  const requestBytes = new Uint8Array(requestBuf);
3057
3080
  const totalLenView = new BigUint64Array(this.sab, SAB_OFFSETS.TOTAL_LEN, 1);
3058
- if (requestBytes.byteLength <= maxChunk) {
3081
+ const multiChunkRequest = requestBytes.byteLength > maxChunk;
3082
+ if (!multiChunkRequest) {
3059
3083
  new Uint8Array(this.sab, HEADER_SIZE, requestBytes.byteLength).set(requestBytes);
3060
3084
  Atomics.store(this.ctrl, 3, requestBytes.byteLength);
3061
3085
  Atomics.store(totalLenView, 0, BigInt(requestBytes.byteLength));
@@ -3079,11 +3103,11 @@ var VFSFileSystem = class {
3079
3103
  Atomics.notify(this.ctrl, 0);
3080
3104
  sent += chunkSize;
3081
3105
  if (sent < requestBytes.byteLength) {
3082
- spinWait(this.ctrl, 0, sent === chunkSize ? SIGNAL.REQUEST : SIGNAL.CHUNK);
3106
+ spinWait(this.ctrl, 0, sent === chunkSize ? SIGNAL.REQUEST : SIGNAL.CHUNK, this.ctrl);
3083
3107
  }
3084
3108
  }
3085
3109
  }
3086
- spinWait(this.ctrl, 0, SIGNAL.REQUEST);
3110
+ spinWait(this.ctrl, 0, multiChunkRequest ? SIGNAL.CHUNK : SIGNAL.REQUEST, this.ctrl);
3087
3111
  const signal = Atomics.load(this.ctrl, 0);
3088
3112
  const respChunkLen = Atomics.load(this.ctrl, 3);
3089
3113
  const respTotalLen = Number(Atomics.load(totalLenView, 0));
@@ -3099,7 +3123,7 @@ var VFSFileSystem = class {
3099
3123
  while (received < respTotalLen) {
3100
3124
  Atomics.store(this.ctrl, 0, SIGNAL.CHUNK_ACK);
3101
3125
  Atomics.notify(this.ctrl, 0);
3102
- spinWait(this.ctrl, 0, SIGNAL.CHUNK_ACK);
3126
+ spinWait(this.ctrl, 0, SIGNAL.CHUNK_ACK, this.ctrl);
3103
3127
  const nextLen = Atomics.load(this.ctrl, 3);
3104
3128
  responseBytes.set(new Uint8Array(this.sab, HEADER_SIZE, nextLen), received);
3105
3129
  received += nextLen;
@@ -4257,6 +4281,22 @@ var VFSEngine = class {
4257
4281
  // generation when implicitDirs was last rebuilt
4258
4282
  pathIndexGen = 0;
4259
4283
  // bumped on every pathIndex mutation
4284
+ // Incrementally maintained "number of pathIndex entries that have this
4285
+ // path as a strict ancestor" map. Lets `isImplicitDirectory` answer in
4286
+ // O(1) — an implicit dir P is exactly !pathIndex.has(P) && descCount[P] > 0.
4287
+ // Without this, every `isImplicitDirectory` call triggered an O(N×depth)
4288
+ // rebuild of `implicitDirs`, and the 3.0.49 fix put one of those calls on
4289
+ // the hot path of every fresh write/symlink/link/copy — making batch
4290
+ // writes O(N²) on total path count.
4291
+ descCount = /* @__PURE__ */ new Map();
4292
+ // descCount is in sync with pathIndex iff descCountGen >= pathIndexGen.
4293
+ // Helpers `setPathIndex`/`deletePathIndex` keep them in sync. Code that
4294
+ // mutates `pathIndex` directly (only test scaffolding does this in
4295
+ // practice — see the implicit-directory tests in vfs-engine.test.ts)
4296
+ // bumps `pathIndexGen` without going through the helpers, which leaves
4297
+ // descCount stale; `isImplicitDirectory` notices the mismatch and
4298
+ // recomputes descCount on demand.
4299
+ descCountGen = 0;
4260
4300
  // Configurable upper bounds
4261
4301
  maxInodes = 4e6;
4262
4302
  maxBlocks = 4e6;
@@ -4539,7 +4579,7 @@ var VFSEngine = class {
4539
4579
  if (!path.startsWith("/") || path.includes("\0")) {
4540
4580
  throw new Error(`Corrupt VFS: inode ${i} has invalid path "${path.substring(0, 50)}"`);
4541
4581
  }
4542
- this.pathIndex.set(path, i);
4582
+ this.setPathIndex(path, i);
4543
4583
  }
4544
4584
  this.pathIndexGen++;
4545
4585
  }
@@ -4861,7 +4901,7 @@ var VFSEngine = class {
4861
4901
  gid: this.processGid
4862
4902
  };
4863
4903
  this.writeInode(idx, inode);
4864
- this.pathIndex.set(path, idx);
4904
+ this.setPathIndex(path, idx);
4865
4905
  this.pathIndexGen++;
4866
4906
  return idx;
4867
4907
  }
@@ -5019,7 +5059,7 @@ var VFSEngine = class {
5019
5059
  this.freeBlockRange(inode.firstBlock, inode.blockCount);
5020
5060
  inode.type = INODE_TYPE.FREE;
5021
5061
  this.writeInode(idx, inode);
5022
- this.pathIndex.delete(path);
5062
+ this.deletePathIndex(path);
5023
5063
  this.pathIndexGen++;
5024
5064
  if (idx < this.freeInodeHint) this.freeInodeHint = idx;
5025
5065
  this.commitPending();
@@ -5141,7 +5181,7 @@ var VFSEngine = class {
5141
5181
  this.freeBlockRange(descInode.firstBlock, descInode.blockCount);
5142
5182
  descInode.type = INODE_TYPE.FREE;
5143
5183
  this.writeInode(descIdx, descInode);
5144
- this.pathIndex.delete(desc);
5184
+ this.deletePathIndex(desc);
5145
5185
  }
5146
5186
  this.pathIndexGen++;
5147
5187
  this.commitPending();
@@ -5161,12 +5201,12 @@ var VFSEngine = class {
5161
5201
  this.freeBlockRange(childInode.firstBlock, childInode.blockCount);
5162
5202
  childInode.type = INODE_TYPE.FREE;
5163
5203
  this.writeInode(childIdx, childInode);
5164
- this.pathIndex.delete(child);
5204
+ this.deletePathIndex(child);
5165
5205
  }
5166
5206
  }
5167
5207
  inode.type = INODE_TYPE.FREE;
5168
5208
  this.writeInode(idx, inode);
5169
- this.pathIndex.delete(path);
5209
+ this.deletePathIndex(path);
5170
5210
  this.pathIndexGen++;
5171
5211
  if (idx < this.freeInodeHint) this.freeInodeHint = idx;
5172
5212
  this.commitPending();
@@ -5257,7 +5297,7 @@ var VFSEngine = class {
5257
5297
  this.freeBlockRange(existingInode.firstBlock, existingInode.blockCount);
5258
5298
  existingInode.type = INODE_TYPE.FREE;
5259
5299
  this.writeInode(existingIdx, existingInode);
5260
- this.pathIndex.delete(newPath);
5300
+ this.deletePathIndex(newPath);
5261
5301
  if (existingIdx < this.freeInodeHint) this.freeInodeHint = existingIdx;
5262
5302
  }
5263
5303
  if (cleanDescendants) {
@@ -5267,7 +5307,7 @@ var VFSEngine = class {
5267
5307
  this.freeBlockRange(descInode.firstBlock, descInode.blockCount);
5268
5308
  descInode.type = INODE_TYPE.FREE;
5269
5309
  this.writeInode(descIdx, descInode);
5270
- this.pathIndex.delete(desc);
5310
+ this.deletePathIndex(desc);
5271
5311
  if (descIdx < this.freeInodeHint) this.freeInodeHint = descIdx;
5272
5312
  }
5273
5313
  }
@@ -5278,8 +5318,8 @@ var VFSEngine = class {
5278
5318
  inode.pathLength = pathLen;
5279
5319
  inode.mtime = Date.now();
5280
5320
  this.writeInode(idx, inode);
5281
- this.pathIndex.delete(oldPath);
5282
- this.pathIndex.set(newPath, idx);
5321
+ this.deletePathIndex(oldPath);
5322
+ this.setPathIndex(newPath, idx);
5283
5323
  this.pathIndexGen++;
5284
5324
  if (inode.type === INODE_TYPE.DIRECTORY) {
5285
5325
  const prefix = oldPath === "/" ? "/" : oldPath + "/";
@@ -5297,8 +5337,8 @@ var VFSEngine = class {
5297
5337
  childInode.pathOffset = cpo;
5298
5338
  childInode.pathLength = cpl;
5299
5339
  this.writeInode(i, childInode);
5300
- this.pathIndex.delete(p);
5301
- this.pathIndex.set(childNewPath, i);
5340
+ this.deletePathIndex(p);
5341
+ this.setPathIndex(childNewPath, i);
5302
5342
  }
5303
5343
  }
5304
5344
  this.commitPending();
@@ -5767,11 +5807,71 @@ var VFSEngine = class {
5767
5807
  /**
5768
5808
  * Check if a path is an implicit directory (exists because files exist under it,
5769
5809
  * but no explicit directory inode was created for it).
5810
+ *
5811
+ * O(1) via the incrementally maintained `descCount` map (an implicit dir
5812
+ * is exactly !pathIndex.has(P) && descCount[P] > 0). If `pathIndex` was
5813
+ * mutated directly without going through the helpers (test scaffolding),
5814
+ * descCount is stale and we rebuild it from scratch — once — to resync.
5770
5815
  */
5771
5816
  isImplicitDirectory(path) {
5772
5817
  if (path === "/") return false;
5773
- this.rebuildImplicitDirs();
5774
- return this.implicitDirs.has(path);
5818
+ if (this.pathIndex.has(path)) return false;
5819
+ if (this.descCountGen < this.pathIndexGen) this.rebuildDescCount();
5820
+ return (this.descCount.get(path) ?? 0) > 0;
5821
+ }
5822
+ /**
5823
+ * Recompute `descCount` from scratch by walking every pathIndex entry's
5824
+ * ancestor chain. O(N×depth). Only triggered when something bypassed the
5825
+ * setPathIndex/deletePathIndex helpers — in production code that's
5826
+ * never; the tests exercise this path.
5827
+ */
5828
+ rebuildDescCount() {
5829
+ this.descCount.clear();
5830
+ for (const path of this.pathIndex.keys()) {
5831
+ this.bumpDescCount(path);
5832
+ }
5833
+ this.descCountGen = this.pathIndexGen;
5834
+ }
5835
+ // ---- pathIndex helpers — keep `descCount` in sync ----
5836
+ // Every pathIndex.set/delete in the engine MUST go through these so the
5837
+ // `descCount` map (used by `isImplicitDirectory`) stays correct. We
5838
+ // anticipate the caller's `pathIndexGen++` by setting `descCountGen` to
5839
+ // `pathIndexGen + 1`; idempotent across multiple helper calls within a
5840
+ // single logical op (e.g. rmdir doing N deletes then one bump). Test
5841
+ // code that mutates `pathIndex` directly leaves descCountGen behind,
5842
+ // which is what triggers the rebuild path in `isImplicitDirectory`.
5843
+ setPathIndex(path, idx) {
5844
+ const had = this.pathIndex.has(path);
5845
+ this.pathIndex.set(path, idx);
5846
+ if (!had) this.bumpDescCount(path);
5847
+ this.descCountGen = this.pathIndexGen + 1;
5848
+ }
5849
+ deletePathIndex(path) {
5850
+ const had = this.pathIndex.delete(path);
5851
+ if (had) this.decDescCount(path);
5852
+ this.descCountGen = this.pathIndexGen + 1;
5853
+ return had;
5854
+ }
5855
+ bumpDescCount(path) {
5856
+ let pos = path.length;
5857
+ while (true) {
5858
+ pos = path.lastIndexOf("/", pos - 1);
5859
+ if (pos <= 0) break;
5860
+ const ancestor = path.substring(0, pos);
5861
+ this.descCount.set(ancestor, (this.descCount.get(ancestor) ?? 0) + 1);
5862
+ }
5863
+ }
5864
+ decDescCount(path) {
5865
+ let pos = path.length;
5866
+ while (true) {
5867
+ pos = path.lastIndexOf("/", pos - 1);
5868
+ if (pos <= 0) break;
5869
+ const ancestor = path.substring(0, pos);
5870
+ const cur = this.descCount.get(ancestor);
5871
+ if (cur === void 0) break;
5872
+ if (cur <= 1) this.descCount.delete(ancestor);
5873
+ else this.descCount.set(ancestor, cur - 1);
5874
+ }
5775
5875
  }
5776
5876
  /**
5777
5877
  * Get direct children of a directory path, including implicit subdirectories.