@componentor/fs 3.0.45 → 3.0.47

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.d.mts CHANGED
@@ -359,6 +359,8 @@ declare class VFSFileSystem {
359
359
  private isFollower;
360
360
  private holdingLeaderLock;
361
361
  private brokerInitialized;
362
+ private brokerHeartbeatTimer;
363
+ private brokerControlPort;
362
364
  private leaderChangeBc;
363
365
  private _sync;
364
366
  private _async;
@@ -390,7 +392,31 @@ declare class VFSFileSystem {
390
392
  private connectToLeader;
391
393
  /** Register the VFS service worker and return the active SW */
392
394
  private getServiceWorker;
393
- /** Register as leader with SW broker (receives follower ports via control channel) */
395
+ /** Register as leader with SW broker (receives follower ports via control channel).
396
+ *
397
+ * Re-registers on a heartbeat so the broker survives SW idle-kill. Without this,
398
+ * a follower opening a tab after the SW has been killed (≥30s idle on Chrome)
399
+ * sees its `transfer-port` queued in the new SW's `pending` array forever:
400
+ * the prior leader's `port2` was held by the dead SW instance, the new SW
401
+ * starts with `serverPort=null`, and the leader has no way to know to
402
+ * re-register.
403
+ *
404
+ * Re-posting `register-server` is idempotent in the SW handler — it replaces
405
+ * `serverPort` and flushes `pending` — so the heartbeat alone unsticks
406
+ * followers without needing to disturb anyone else. The follower's queued
407
+ * `mc.port2` rides through the pending-flush, and because it's a
408
+ * MessageChannel, any messages the follower's sync-relay had already posted
409
+ * on `port1` are buffered on `port2` until the leader's syncWorker starts
410
+ * the received port. Standard MessageChannel semantics — no follower-side
411
+ * notification required.
412
+ *
413
+ * We deliberately do NOT broadcast `leader-changed` from the heartbeat:
414
+ * followers receiving it call `connectToLeader()`, which tears down the
415
+ * existing `leader-port` and resolves any in-flight sync FS request with
416
+ * EIO (sync-relay.worker.ts: `pendingResolve(EIO)`). Broadcasting on every
417
+ * tick would inject random EIOs into long-running ops on every connected
418
+ * follower. Broadcast only fires once, at initial registration, to wake any
419
+ * pre-existing followers (e.g. left over from a previous leader). */
394
420
  private initLeaderBroker;
395
421
  /** Promote from follower to leader (after leader tab dies and lock is acquired) */
396
422
  private promoteToLeader;
package/dist/index.js CHANGED
@@ -2606,6 +2606,8 @@ var VFSFileSystem = class {
2606
2606
  isFollower = false;
2607
2607
  holdingLeaderLock = false;
2608
2608
  brokerInitialized = false;
2609
+ brokerHeartbeatTimer = null;
2610
+ brokerControlPort = null;
2609
2611
  leaderChangeBc = null;
2610
2612
  // Bound request functions for method delegation
2611
2613
  _sync = (buf) => this.syncRequest(buf);
@@ -2878,37 +2880,85 @@ var VFSFileSystem = class {
2878
2880
  onState();
2879
2881
  });
2880
2882
  }
2881
- /** Register as leader with SW broker (receives follower ports via control channel) */
2883
+ /** Register as leader with SW broker (receives follower ports via control channel).
2884
+ *
2885
+ * Re-registers on a heartbeat so the broker survives SW idle-kill. Without this,
2886
+ * a follower opening a tab after the SW has been killed (≥30s idle on Chrome)
2887
+ * sees its `transfer-port` queued in the new SW's `pending` array forever:
2888
+ * the prior leader's `port2` was held by the dead SW instance, the new SW
2889
+ * starts with `serverPort=null`, and the leader has no way to know to
2890
+ * re-register.
2891
+ *
2892
+ * Re-posting `register-server` is idempotent in the SW handler — it replaces
2893
+ * `serverPort` and flushes `pending` — so the heartbeat alone unsticks
2894
+ * followers without needing to disturb anyone else. The follower's queued
2895
+ * `mc.port2` rides through the pending-flush, and because it's a
2896
+ * MessageChannel, any messages the follower's sync-relay had already posted
2897
+ * on `port1` are buffered on `port2` until the leader's syncWorker starts
2898
+ * the received port. Standard MessageChannel semantics — no follower-side
2899
+ * notification required.
2900
+ *
2901
+ * We deliberately do NOT broadcast `leader-changed` from the heartbeat:
2902
+ * followers receiving it call `connectToLeader()`, which tears down the
2903
+ * existing `leader-port` and resolves any in-flight sync FS request with
2904
+ * EIO (sync-relay.worker.ts: `pendingResolve(EIO)`). Broadcasting on every
2905
+ * tick would inject random EIOs into long-running ops on every connected
2906
+ * follower. Broadcast only fires once, at initial registration, to wake any
2907
+ * pre-existing followers (e.g. left over from a previous leader). */
2882
2908
  initLeaderBroker() {
2883
2909
  if (this.brokerInitialized) return;
2884
2910
  this.brokerInitialized = true;
2885
- this.getServiceWorker().then((sw) => {
2886
- const mc = new MessageChannel();
2887
- sw.postMessage({ type: "register-server" }, [mc.port2]);
2888
- mc.port1.onmessage = (event) => {
2889
- if (event.data.type === "client-port") {
2890
- const clientPort = event.ports[0];
2891
- if (clientPort) {
2892
- this.syncWorker.postMessage(
2893
- { type: "client-port", tabId: event.data.tabId, port: clientPort },
2894
- [clientPort]
2895
- );
2911
+ const register = () => {
2912
+ this.getServiceWorker().then((sw) => {
2913
+ const mc = new MessageChannel();
2914
+ sw.postMessage({ type: "register-server" }, [mc.port2]);
2915
+ mc.port1.onmessage = (event) => {
2916
+ if (event.data.type === "client-port") {
2917
+ const clientPort = event.ports[0];
2918
+ if (clientPort) {
2919
+ this.syncWorker.postMessage(
2920
+ { type: "client-port", tabId: event.data.tabId, port: clientPort },
2921
+ [clientPort]
2922
+ );
2923
+ }
2924
+ }
2925
+ };
2926
+ mc.port1.start();
2927
+ const oldPort = this.brokerControlPort;
2928
+ this.brokerControlPort = mc.port1;
2929
+ if (oldPort) {
2930
+ try {
2931
+ oldPort.close();
2932
+ } catch {
2896
2933
  }
2897
2934
  }
2898
- };
2899
- mc.port1.start();
2900
- const bc = new BroadcastChannel(`${this.ns}-leader-change`);
2901
- bc.postMessage({ type: "leader-changed" });
2902
- bc.close();
2903
- }).catch((err) => {
2904
- console.warn("[VFS] SW broker unavailable, single-tab only:", err.message);
2905
- });
2935
+ }).catch((err) => {
2936
+ console.warn("[VFS] SW broker unavailable, single-tab only:", err.message);
2937
+ });
2938
+ };
2939
+ register();
2940
+ const bc = new BroadcastChannel(`${this.ns}-leader-change`);
2941
+ bc.postMessage({ type: "leader-changed" });
2942
+ bc.close();
2943
+ if (this.brokerHeartbeatTimer) clearInterval(this.brokerHeartbeatTimer);
2944
+ this.brokerHeartbeatTimer = setInterval(register, 5e3);
2906
2945
  }
2907
2946
  /** Promote from follower to leader (after leader tab dies and lock is acquired) */
2908
2947
  promoteToLeader() {
2909
2948
  this.isFollower = false;
2910
2949
  this.isReady = false;
2911
2950
  this.brokerInitialized = false;
2951
+ if (this.brokerHeartbeatTimer) {
2952
+ clearInterval(this.brokerHeartbeatTimer);
2953
+ this.brokerHeartbeatTimer = null;
2954
+ }
2955
+ if (this.brokerControlPort) {
2956
+ try {
2957
+ this.brokerControlPort.close();
2958
+ } catch {
2959
+ }
2960
+ this.brokerControlPort = null;
2961
+ }
2912
2962
  if (this.leaderChangeBc) {
2913
2963
  this.leaderChangeBc.close();
2914
2964
  this.leaderChangeBc = null;
@@ -4204,6 +4254,16 @@ var VFSEngine = class {
4204
4254
  superblockDirty = false;
4205
4255
  // Free inode hint — skip O(n) scan
4206
4256
  freeInodeHint = 0;
4257
+ // Implicit directory support — tracks all directory prefixes implied by file paths.
4258
+ // Rebuilt lazily when pathIndex changes (tracked via generation counter).
4259
+ // Map value is the stable timestamp (ms since epoch) assigned when the implicit
4260
+ // dir was first discovered, so that stat() returns consistent mtime/ctime/atime
4261
+ // across repeated calls.
4262
+ implicitDirs = /* @__PURE__ */ new Map();
4263
+ implicitDirsGen = -1;
4264
+ // generation when implicitDirs was last rebuilt
4265
+ pathIndexGen = 0;
4266
+ // bumped on every pathIndex mutation
4207
4267
  // Configurable upper bounds
4208
4268
  maxInodes = 4e6;
4209
4269
  maxBlocks = 4e6;
@@ -4488,6 +4548,7 @@ var VFSEngine = class {
4488
4548
  }
4489
4549
  this.pathIndex.set(path, i);
4490
4550
  }
4551
+ this.pathIndexGen++;
4491
4552
  }
4492
4553
  // ========== Low-level inode I/O ==========
4493
4554
  readInode(idx) {
@@ -4808,6 +4869,7 @@ var VFSEngine = class {
4808
4869
  };
4809
4870
  this.writeInode(idx, inode);
4810
4871
  this.pathIndex.set(path, idx);
4872
+ this.pathIndexGen++;
4811
4873
  return idx;
4812
4874
  }
4813
4875
  // ========== Public API — called by server worker dispatch ==========
@@ -4964,6 +5026,7 @@ var VFSEngine = class {
4964
5026
  inode.type = INODE_TYPE.FREE;
4965
5027
  this.writeInode(idx, inode);
4966
5028
  this.pathIndex.delete(path);
5029
+ this.pathIndexGen++;
4967
5030
  if (idx < this.freeInodeHint) this.freeInodeHint = idx;
4968
5031
  this.commitPending();
4969
5032
  return { status: 0 };
@@ -4972,7 +5035,12 @@ var VFSEngine = class {
4972
5035
  stat(path) {
4973
5036
  path = this.normalizePath(path);
4974
5037
  const idx = this.resolvePathComponents(path, true);
4975
- if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT, data: null };
5038
+ if (idx === void 0) {
5039
+ if (this.isImplicitDirectory(path)) {
5040
+ return this.encodeImplicitDirStatResponse(path);
5041
+ }
5042
+ return { status: CODE_TO_STATUS.ENOENT, data: null };
5043
+ }
4976
5044
  return this.encodeStatResponse(idx);
4977
5045
  }
4978
5046
  // ---- LSTAT (no symlink follow for the FINAL component) ----
@@ -4981,7 +5049,12 @@ var VFSEngine = class {
4981
5049
  let idx = this.resolvePathComponents(path, false);
4982
5050
  if (idx === void 0) {
4983
5051
  idx = this.resolvePathComponents(path, true);
4984
- if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT, data: null };
5052
+ if (idx === void 0) {
5053
+ if (this.isImplicitDirectory(path)) {
5054
+ return this.encodeImplicitDirStatResponse(path);
5055
+ }
5056
+ return { status: CODE_TO_STATUS.ENOENT, data: null };
5057
+ }
4985
5058
  }
4986
5059
  return this.encodeStatResponse(idx);
4987
5060
  }
@@ -4990,13 +5063,17 @@ var VFSEngine = class {
4990
5063
  let nlink = inode.nlink;
4991
5064
  if (inode.type === INODE_TYPE.DIRECTORY) {
4992
5065
  const path = this.readPath(inode.pathOffset, inode.pathLength);
4993
- const children = this.getDirectChildren(path);
5066
+ const children = this.getDirectChildrenWithImplicit(path);
4994
5067
  let subdirCount = 0;
4995
5068
  for (const child of children) {
4996
- const childIdx = this.pathIndex.get(child);
4997
- if (childIdx !== void 0) {
4998
- const childInode = this.readInode(childIdx);
4999
- if (childInode.type === INODE_TYPE.DIRECTORY) subdirCount++;
5069
+ if (child.type === "implicit") {
5070
+ subdirCount++;
5071
+ } else {
5072
+ const childIdx = this.pathIndex.get(child.path);
5073
+ if (childIdx !== void 0) {
5074
+ const childInode = this.readInode(childIdx);
5075
+ if (childInode.type === INODE_TYPE.DIRECTORY) subdirCount++;
5076
+ }
5000
5077
  }
5001
5078
  }
5002
5079
  nlink = 2 + subdirCount;
@@ -5022,7 +5099,9 @@ var VFSEngine = class {
5022
5099
  if (recursive) {
5023
5100
  return this.mkdirRecursive(path);
5024
5101
  }
5025
- if (this.pathIndex.has(path)) return { status: CODE_TO_STATUS.EEXIST, data: null };
5102
+ if (this.pathIndex.has(path) || this.isImplicitDirectory(path)) {
5103
+ return { status: CODE_TO_STATUS.EEXIST, data: null };
5104
+ }
5026
5105
  const parentStatus = this.ensureParent(path);
5027
5106
  if (parentStatus !== 0) return { status: parentStatus, data: null };
5028
5107
  const mode = DEFAULT_DIR_MODE & ~(this.umask & 511);
@@ -5057,7 +5136,26 @@ var VFSEngine = class {
5057
5136
  path = this.normalizePath(path);
5058
5137
  const recursive = (flags & 1) !== 0;
5059
5138
  const idx = this.pathIndex.get(path);
5060
- if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT };
5139
+ if (idx === void 0) {
5140
+ if (this.isImplicitDirectory(path)) {
5141
+ const children2 = this.getDirectChildrenWithImplicit(path);
5142
+ if (children2.length > 0) {
5143
+ if (!recursive) return { status: CODE_TO_STATUS.ENOTEMPTY };
5144
+ for (const desc of this.getAllDescendants(path)) {
5145
+ const descIdx = this.pathIndex.get(desc);
5146
+ const descInode = this.readInode(descIdx);
5147
+ this.freeBlockRange(descInode.firstBlock, descInode.blockCount);
5148
+ descInode.type = INODE_TYPE.FREE;
5149
+ this.writeInode(descIdx, descInode);
5150
+ this.pathIndex.delete(desc);
5151
+ }
5152
+ this.pathIndexGen++;
5153
+ this.commitPending();
5154
+ }
5155
+ return { status: 0 };
5156
+ }
5157
+ return { status: CODE_TO_STATUS.ENOENT };
5158
+ }
5061
5159
  const inode = this.readInode(idx);
5062
5160
  if (inode.type !== INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.ENOTDIR };
5063
5161
  const children = this.getDirectChildren(path);
@@ -5075,6 +5173,7 @@ var VFSEngine = class {
5075
5173
  inode.type = INODE_TYPE.FREE;
5076
5174
  this.writeInode(idx, inode);
5077
5175
  this.pathIndex.delete(path);
5176
+ this.pathIndexGen++;
5078
5177
  if (idx < this.freeInodeHint) this.freeInodeHint = idx;
5079
5178
  this.commitPending();
5080
5179
  return { status: 0 };
@@ -5083,20 +5182,33 @@ var VFSEngine = class {
5083
5182
  readdir(path, flags = 0) {
5084
5183
  path = this.normalizePath(path);
5085
5184
  const resolved = this.resolvePathFull(path, true);
5086
- if (!resolved) return { status: CODE_TO_STATUS.ENOENT, data: null };
5087
- const inode = this.readInode(resolved.idx);
5088
- if (inode.type !== INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.ENOTDIR, data: null };
5185
+ let effectiveDirPath;
5186
+ if (resolved) {
5187
+ const inode = this.readInode(resolved.idx);
5188
+ if (inode.type !== INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.ENOTDIR, data: null };
5189
+ effectiveDirPath = resolved.resolvedPath;
5190
+ } else if (this.isImplicitDirectory(path)) {
5191
+ effectiveDirPath = path;
5192
+ } else {
5193
+ return { status: CODE_TO_STATUS.ENOENT, data: null };
5194
+ }
5089
5195
  const withFileTypes = (flags & 1) !== 0;
5090
- const children = this.getDirectChildren(resolved.resolvedPath);
5196
+ const children = this.getDirectChildrenWithImplicit(effectiveDirPath);
5091
5197
  if (withFileTypes) {
5092
5198
  let totalSize2 = 4;
5093
5199
  const entries = [];
5094
- for (const childPath of children) {
5095
- const name = childPath.substring(childPath.lastIndexOf("/") + 1);
5200
+ for (const child of children) {
5201
+ const name = child.path.substring(child.path.lastIndexOf("/") + 1);
5096
5202
  const nameBytes = encoder10.encode(name);
5097
- const childIdx = this.pathIndex.get(childPath);
5098
- const childInode = this.readInode(childIdx);
5099
- entries.push({ name: nameBytes, type: childInode.type });
5203
+ let type;
5204
+ if (child.type === "implicit") {
5205
+ type = INODE_TYPE.DIRECTORY;
5206
+ } else {
5207
+ const childIdx = this.pathIndex.get(child.path);
5208
+ const childInode = this.readInode(childIdx);
5209
+ type = childInode.type;
5210
+ }
5211
+ entries.push({ name: nameBytes, type });
5100
5212
  totalSize2 += 2 + nameBytes.byteLength + 1;
5101
5213
  }
5102
5214
  const buf2 = new Uint8Array(totalSize2);
@@ -5114,8 +5226,8 @@ var VFSEngine = class {
5114
5226
  }
5115
5227
  let totalSize = 4;
5116
5228
  const nameEntries = [];
5117
- for (const childPath of children) {
5118
- const name = childPath.substring(childPath.lastIndexOf("/") + 1);
5229
+ for (const child of children) {
5230
+ const name = child.path.substring(child.path.lastIndexOf("/") + 1);
5119
5231
  const nameBytes = encoder10.encode(name);
5120
5232
  nameEntries.push(nameBytes);
5121
5233
  totalSize += 2 + nameBytes.byteLength;
@@ -5156,6 +5268,7 @@ var VFSEngine = class {
5156
5268
  this.writeInode(idx, inode);
5157
5269
  this.pathIndex.delete(oldPath);
5158
5270
  this.pathIndex.set(newPath, idx);
5271
+ this.pathIndexGen++;
5159
5272
  if (inode.type === INODE_TYPE.DIRECTORY) {
5160
5273
  const prefix = oldPath === "/" ? "/" : oldPath + "/";
5161
5274
  const toRename = [];
@@ -5184,7 +5297,7 @@ var VFSEngine = class {
5184
5297
  path = this.normalizePath(path);
5185
5298
  const idx = this.resolvePathComponents(path, true);
5186
5299
  const buf = new Uint8Array(1);
5187
- buf[0] = idx !== void 0 ? 1 : 0;
5300
+ buf[0] = idx !== void 0 || this.isImplicitDirectory(path) ? 1 : 0;
5188
5301
  return { status: 0, data: buf };
5189
5302
  }
5190
5303
  // ---- TRUNCATE ----
@@ -5287,7 +5400,10 @@ var VFSEngine = class {
5287
5400
  access(path, mode = 0) {
5288
5401
  path = this.normalizePath(path);
5289
5402
  const idx = this.resolvePathComponents(path, true);
5290
- if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT };
5403
+ if (idx === void 0) {
5404
+ if (this.isImplicitDirectory(path)) return { status: 0 };
5405
+ return { status: CODE_TO_STATUS.ENOENT };
5406
+ }
5291
5407
  if (mode === 0) return { status: 0 };
5292
5408
  if (!this.strictPermissions) return { status: 0 };
5293
5409
  const inode = this.readInode(idx);
@@ -5307,7 +5423,12 @@ var VFSEngine = class {
5307
5423
  realpath(path) {
5308
5424
  path = this.normalizePath(path);
5309
5425
  const idx = this.resolvePathComponents(path, true);
5310
- if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT, data: null };
5426
+ if (idx === void 0) {
5427
+ if (this.isImplicitDirectory(path)) {
5428
+ return { status: 0, data: encoder10.encode(path) };
5429
+ }
5430
+ return { status: CODE_TO_STATUS.ENOENT, data: null };
5431
+ }
5311
5432
  const inode = this.readInode(idx);
5312
5433
  const resolvedPath = this.readPath(inode.pathOffset, inode.pathLength);
5313
5434
  return { status: 0, data: encoder10.encode(resolvedPath) };
@@ -5496,6 +5617,7 @@ var VFSEngine = class {
5496
5617
  fstat(fd) {
5497
5618
  const entry = this.fdTable.get(fd);
5498
5619
  if (!entry) return { status: CODE_TO_STATUS.EBADF, data: null };
5620
+ if (entry.implicitPath) return this.encodeImplicitDirStatResponse(entry.implicitPath);
5499
5621
  return this.encodeStatResponse(entry.inodeIdx);
5500
5622
  }
5501
5623
  // ---- FTRUNCATE ----
@@ -5518,6 +5640,7 @@ var VFSEngine = class {
5518
5640
  fchmod(fd, mode) {
5519
5641
  const entry = this.fdTable.get(fd);
5520
5642
  if (!entry) return { status: CODE_TO_STATUS.EBADF };
5643
+ if (entry.implicitPath) return { status: 0 };
5521
5644
  const inode = this.readInode(entry.inodeIdx);
5522
5645
  inode.mode = inode.mode & S_IFMT | mode & 4095;
5523
5646
  inode.ctime = Date.now();
@@ -5528,6 +5651,7 @@ var VFSEngine = class {
5528
5651
  fchown(fd, uid, gid) {
5529
5652
  const entry = this.fdTable.get(fd);
5530
5653
  if (!entry) return { status: CODE_TO_STATUS.EBADF };
5654
+ if (entry.implicitPath) return { status: 0 };
5531
5655
  const inode = this.readInode(entry.inodeIdx);
5532
5656
  inode.uid = uid;
5533
5657
  inode.gid = gid;
@@ -5539,6 +5663,7 @@ var VFSEngine = class {
5539
5663
  futimes(fd, atime, mtime) {
5540
5664
  const entry = this.fdTable.get(fd);
5541
5665
  if (!entry) return { status: CODE_TO_STATUS.EBADF };
5666
+ if (entry.implicitPath) return { status: 0 };
5542
5667
  const inode = this.readInode(entry.inodeIdx);
5543
5668
  inode.atime = atime;
5544
5669
  inode.mtime = mtime;
@@ -5550,7 +5675,16 @@ var VFSEngine = class {
5550
5675
  opendir(path, tabId) {
5551
5676
  path = this.normalizePath(path);
5552
5677
  const idx = this.resolvePathComponents(path, true);
5553
- if (idx === void 0) return { status: CODE_TO_STATUS.ENOENT, data: null };
5678
+ if (idx === void 0) {
5679
+ if (this.isImplicitDirectory(path)) {
5680
+ const fd2 = this.nextFd++;
5681
+ this.fdTable.set(fd2, { tabId, inodeIdx: -1, position: 0, flags: 0, implicitPath: path });
5682
+ const buf2 = new Uint8Array(4);
5683
+ new DataView(buf2.buffer).setUint32(0, fd2, true);
5684
+ return { status: 0, data: buf2 };
5685
+ }
5686
+ return { status: CODE_TO_STATUS.ENOENT, data: null };
5687
+ }
5554
5688
  const inode = this.readInode(idx);
5555
5689
  if (inode.type !== INODE_TYPE.DIRECTORY) return { status: CODE_TO_STATUS.ENOTDIR, data: null };
5556
5690
  const fd = this.nextFd++;
@@ -5589,6 +5723,106 @@ var VFSEngine = class {
5589
5723
  }
5590
5724
  return children.sort();
5591
5725
  }
5726
+ /**
5727
+ * Rebuild the set of all implicit directory paths.
5728
+ * An implicit directory is any ancestor path of a file/symlink in pathIndex
5729
+ * that doesn't itself have an explicit inode entry.
5730
+ * Only rebuilt when pathIndex has changed (tracked via generation counter).
5731
+ */
5732
+ rebuildImplicitDirs() {
5733
+ if (this.implicitDirsGen === this.pathIndexGen) return;
5734
+ const now = Date.now();
5735
+ const prev = this.implicitDirs;
5736
+ this.implicitDirs = /* @__PURE__ */ new Map();
5737
+ for (const filePath of this.pathIndex.keys()) {
5738
+ let pos = filePath.length;
5739
+ while (true) {
5740
+ pos = filePath.lastIndexOf("/", pos - 1);
5741
+ if (pos <= 0) break;
5742
+ const ancestor = filePath.substring(0, pos);
5743
+ if (this.implicitDirs.has(ancestor)) break;
5744
+ if (!this.pathIndex.has(ancestor)) {
5745
+ this.implicitDirs.set(ancestor, prev.get(ancestor) ?? now);
5746
+ }
5747
+ }
5748
+ }
5749
+ this.implicitDirsGen = this.pathIndexGen;
5750
+ }
5751
+ /**
5752
+ * Check if a path is an implicit directory (exists because files exist under it,
5753
+ * but no explicit directory inode was created for it).
5754
+ */
5755
+ isImplicitDirectory(path) {
5756
+ if (path === "/") return false;
5757
+ this.rebuildImplicitDirs();
5758
+ return this.implicitDirs.has(path);
5759
+ }
5760
+ /**
5761
+ * Get direct children of a directory path, including implicit subdirectories.
5762
+ * Returns unique child full paths. Each entry is tagged with whether it's a
5763
+ * real inode or an implicit directory.
5764
+ */
5765
+ getDirectChildrenWithImplicit(dirPath) {
5766
+ const prefix = dirPath === "/" ? "/" : dirPath + "/";
5767
+ const childNames = /* @__PURE__ */ new Map();
5768
+ for (const path of this.pathIndex.keys()) {
5769
+ if (path === dirPath) continue;
5770
+ if (!path.startsWith(prefix)) continue;
5771
+ const rest = path.substring(prefix.length);
5772
+ const slashPos = rest.indexOf("/");
5773
+ if (slashPos === -1) {
5774
+ childNames.set(rest, "real");
5775
+ } else {
5776
+ const childName = rest.substring(0, slashPos);
5777
+ if (!childNames.has(childName)) {
5778
+ const childFullPath = prefix + childName;
5779
+ childNames.set(childName, this.pathIndex.has(childFullPath) ? "real" : "implicit");
5780
+ }
5781
+ }
5782
+ }
5783
+ const result = [];
5784
+ for (const [name, type] of childNames) {
5785
+ result.push({ path: prefix + name, type });
5786
+ }
5787
+ result.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
5788
+ return result;
5789
+ }
5790
+ /**
5791
+ * Encode a synthetic stat response for an implicit directory.
5792
+ * Returns directory stats with default mode, zero size, current timestamps.
5793
+ */
5794
+ encodeImplicitDirStatResponse(path) {
5795
+ this.rebuildImplicitDirs();
5796
+ const ts = this.implicitDirs.get(path) ?? Date.now();
5797
+ const mode = DEFAULT_DIR_MODE & ~(this.umask & 511);
5798
+ const children = this.getDirectChildrenWithImplicit(path);
5799
+ let subdirCount = 0;
5800
+ for (const child of children) {
5801
+ if (child.type === "implicit") {
5802
+ subdirCount++;
5803
+ } else {
5804
+ const childIdx = this.pathIndex.get(child.path);
5805
+ if (childIdx !== void 0) {
5806
+ const childInode = this.readInode(childIdx);
5807
+ if (childInode.type === INODE_TYPE.DIRECTORY) subdirCount++;
5808
+ }
5809
+ }
5810
+ }
5811
+ const nlink = 2 + subdirCount;
5812
+ const buf = new Uint8Array(53);
5813
+ const view = new DataView(buf.buffer);
5814
+ view.setUint8(0, INODE_TYPE.DIRECTORY);
5815
+ view.setUint32(1, mode, true);
5816
+ view.setFloat64(5, 0, true);
5817
+ view.setFloat64(13, ts, true);
5818
+ view.setFloat64(21, ts, true);
5819
+ view.setFloat64(29, ts, true);
5820
+ view.setUint32(37, this.processUid, true);
5821
+ view.setUint32(41, this.processGid, true);
5822
+ view.setUint32(45, 0, true);
5823
+ view.setUint32(49, nlink, true);
5824
+ return { status: 0, data: buf };
5825
+ }
5592
5826
  getAllDescendants(dirPath) {
5593
5827
  const prefix = dirPath === "/" ? "/" : dirPath + "/";
5594
5828
  const descendants = [];
@@ -5606,7 +5840,10 @@ var VFSEngine = class {
5606
5840
  if (lastSlash <= 0) return 0;
5607
5841
  const parentPath = path.substring(0, lastSlash);
5608
5842
  const parentIdx = this.pathIndex.get(parentPath);
5609
- if (parentIdx === void 0) return CODE_TO_STATUS.ENOENT;
5843
+ if (parentIdx === void 0) {
5844
+ if (this.isImplicitDirectory(parentPath)) return 0;
5845
+ return CODE_TO_STATUS.ENOENT;
5846
+ }
5610
5847
  const parentInode = this.readInode(parentIdx);
5611
5848
  if (parentInode.type !== INODE_TYPE.DIRECTORY) return CODE_TO_STATUS.ENOTDIR;
5612
5849
  return 0;