@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 +125 -25
- package/dist/index.js.map +1 -1
- package/dist/workers/async-relay.worker.js +5 -2
- package/dist/workers/async-relay.worker.js.map +1 -1
- package/dist/workers/repair.worker.js +90 -14
- package/dist/workers/repair.worker.js.map +1 -1
- package/dist/workers/server.worker.js +90 -14
- package/dist/workers/server.worker.js.map +1 -1
- package/dist/workers/sync-relay.worker.js +107 -16
- package/dist/workers/sync-relay.worker.js.map +1 -1
- package/package.json +1 -1
- package/readme.md +13 -0
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 -
|
|
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
|
|
2560
|
-
|
|
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
|
-
|
|
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 >
|
|
2575
|
+
if (performance.now() - start > SPIN_NO_HEARTBEAT_TIMEOUT_MS) {
|
|
2567
2576
|
throw new Error(
|
|
2568
|
-
`VFS sync operation timed out after ${
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
5204
|
+
this.deletePathIndex(child);
|
|
5165
5205
|
}
|
|
5166
5206
|
}
|
|
5167
5207
|
inode.type = INODE_TYPE.FREE;
|
|
5168
5208
|
this.writeInode(idx, inode);
|
|
5169
|
-
this.
|
|
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.
|
|
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.
|
|
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.
|
|
5282
|
-
this.
|
|
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.
|
|
5301
|
-
this.
|
|
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.
|
|
5774
|
-
|
|
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.
|