@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 +27 -1
- package/dist/index.js +282 -45
- package/dist/index.js.map +1 -1
- package/dist/workers/repair.worker.js +212 -25
- package/dist/workers/repair.worker.js.map +1 -1
- package/dist/workers/server.worker.js +212 -25
- package/dist/workers/server.worker.js.map +1 -1
- package/dist/workers/sync-relay.worker.js +212 -25
- package/dist/workers/sync-relay.worker.js.map +1 -1
- package/package.json +1 -1
- package/readme.md +22 -0
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
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
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
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
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)
|
|
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)
|
|
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.
|
|
5066
|
+
const children = this.getDirectChildrenWithImplicit(path);
|
|
4994
5067
|
let subdirCount = 0;
|
|
4995
5068
|
for (const child of children) {
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
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)
|
|
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)
|
|
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
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
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.
|
|
5196
|
+
const children = this.getDirectChildrenWithImplicit(effectiveDirPath);
|
|
5091
5197
|
if (withFileTypes) {
|
|
5092
5198
|
let totalSize2 = 4;
|
|
5093
5199
|
const entries = [];
|
|
5094
|
-
for (const
|
|
5095
|
-
const name =
|
|
5200
|
+
for (const child of children) {
|
|
5201
|
+
const name = child.path.substring(child.path.lastIndexOf("/") + 1);
|
|
5096
5202
|
const nameBytes = encoder10.encode(name);
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
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
|
|
5118
|
-
const name =
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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;
|