@componentor/fs 3.0.51 → 3.0.53
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 +23 -0
- package/dist/index.js +86 -11
- 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/server.worker.js.map +1 -1
- package/dist/workers/sync-relay.worker.js +17 -2
- package/dist/workers/sync-relay.worker.js.map +1 -1
- package/package.json +1 -1
- package/readme.md +38 -0
package/dist/index.d.mts
CHANGED
|
@@ -348,6 +348,12 @@ declare class VFSFileSystem {
|
|
|
348
348
|
private rejectReady;
|
|
349
349
|
private initError;
|
|
350
350
|
private isReady;
|
|
351
|
+
/** True while a leader transition is in flight (promotion to leader, etc.).
|
|
352
|
+
* Cleared the moment the new sync-relay signals `ready`. Consumers can
|
|
353
|
+
* combine this with `isReady` to know when sync FS ops are safe again. */
|
|
354
|
+
private transitioning;
|
|
355
|
+
/** Listeners awaiting the next `ready` signal (used by `whenReady()`). */
|
|
356
|
+
private readyListeners;
|
|
351
357
|
private config;
|
|
352
358
|
private tabId;
|
|
353
359
|
private _mode;
|
|
@@ -514,6 +520,23 @@ declare class VFSFileSystem {
|
|
|
514
520
|
* Rejects with corruption error if VFS was corrupt (but system falls back to OPFS mode).
|
|
515
521
|
* Callers can catch and continue — the fs API works in OPFS mode after rejection. */
|
|
516
522
|
init(): Promise<void>;
|
|
523
|
+
/** True only while the filesystem is fully ready for synchronous operations
|
|
524
|
+
* AND no leader transition is in progress. Reflects the moment-in-time state;
|
|
525
|
+
* use `whenReady()` to await readiness reliably. */
|
|
526
|
+
get ready(): boolean;
|
|
527
|
+
/** Resolves once the filesystem is fully ready for synchronous operations,
|
|
528
|
+
* including any in-flight leader transition (promotion-to-leader, etc.).
|
|
529
|
+
* If already ready and no transition is pending, resolves immediately.
|
|
530
|
+
*
|
|
531
|
+
* Use this when coordinating with other Web-Lock-based systems (e.g. a
|
|
532
|
+
* parent app that elects its own leader independently of the FS) — the
|
|
533
|
+
* timing of the two elections isn't synchronized, so the FS may still be
|
|
534
|
+
* reinitialising when the parent's lock fires. Calling `whenReady()`
|
|
535
|
+
* after your own leader-acquisition guarantees the FS is back in a state
|
|
536
|
+
* where sync ops won't stall the 20-second relay-worker heartbeat. */
|
|
537
|
+
whenReady(): Promise<void>;
|
|
538
|
+
/** Internal — called by lifecycle handlers when sync-relay says 'ready'. */
|
|
539
|
+
private fireReadyListeners;
|
|
517
540
|
/** Switch the filesystem mode at runtime.
|
|
518
541
|
*
|
|
519
542
|
* Typical flow for IDE corruption recovery:
|
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 {
|
|
@@ -2593,6 +2616,12 @@ var VFSFileSystem = class {
|
|
|
2593
2616
|
rejectReady;
|
|
2594
2617
|
initError = null;
|
|
2595
2618
|
isReady = false;
|
|
2619
|
+
/** True while a leader transition is in flight (promotion to leader, etc.).
|
|
2620
|
+
* Cleared the moment the new sync-relay signals `ready`. Consumers can
|
|
2621
|
+
* combine this with `isReady` to know when sync FS ops are safe again. */
|
|
2622
|
+
transitioning = false;
|
|
2623
|
+
/** Listeners awaiting the next `ready` signal (used by `whenReady()`). */
|
|
2624
|
+
readyListeners = /* @__PURE__ */ new Set();
|
|
2596
2625
|
// Config (definite assignment — always set when constructor doesn't return singleton)
|
|
2597
2626
|
config;
|
|
2598
2627
|
tabId;
|
|
@@ -2668,8 +2697,10 @@ var VFSFileSystem = class {
|
|
|
2668
2697
|
const msg = e.data;
|
|
2669
2698
|
if (msg.type === "ready") {
|
|
2670
2699
|
this.isReady = true;
|
|
2700
|
+
this.transitioning = false;
|
|
2671
2701
|
this.initAsyncRelay();
|
|
2672
2702
|
this.resolveReady();
|
|
2703
|
+
this.fireReadyListeners();
|
|
2673
2704
|
if (!this.isFollower) {
|
|
2674
2705
|
this.initLeaderBroker();
|
|
2675
2706
|
}
|
|
@@ -2940,6 +2971,7 @@ var VFSFileSystem = class {
|
|
|
2940
2971
|
promoteToLeader() {
|
|
2941
2972
|
this.isFollower = false;
|
|
2942
2973
|
this.isReady = false;
|
|
2974
|
+
this.transitioning = true;
|
|
2943
2975
|
this.brokerInitialized = false;
|
|
2944
2976
|
if (this.brokerHeartbeatTimer) {
|
|
2945
2977
|
clearInterval(this.brokerHeartbeatTimer);
|
|
@@ -2976,7 +3008,9 @@ var VFSFileSystem = class {
|
|
|
2976
3008
|
const msg = e.data;
|
|
2977
3009
|
if (msg.type === "ready") {
|
|
2978
3010
|
this.isReady = true;
|
|
3011
|
+
this.transitioning = false;
|
|
2979
3012
|
this.resolveReady();
|
|
3013
|
+
this.fireReadyListeners();
|
|
2980
3014
|
this.initLeaderBroker();
|
|
2981
3015
|
} else if (msg.type === "init-failed") {
|
|
2982
3016
|
if (msg.error?.startsWith("Corrupt VFS:")) {
|
|
@@ -3041,7 +3075,7 @@ var VFSFileSystem = class {
|
|
|
3041
3075
|
if (signal === -1) {
|
|
3042
3076
|
throw this.initError ?? new Error("VFS initialization failed");
|
|
3043
3077
|
}
|
|
3044
|
-
spinWait(this.readySignal, 0, 0);
|
|
3078
|
+
spinWait(this.readySignal, 0, 0, this.ctrl);
|
|
3045
3079
|
const finalSignal = Atomics.load(this.readySignal, 0);
|
|
3046
3080
|
if (finalSignal === -1) {
|
|
3047
3081
|
throw this.initError ?? new Error("VFS initialization failed");
|
|
@@ -3055,7 +3089,8 @@ var VFSFileSystem = class {
|
|
|
3055
3089
|
const maxChunk = this.sab.byteLength - HEADER_SIZE;
|
|
3056
3090
|
const requestBytes = new Uint8Array(requestBuf);
|
|
3057
3091
|
const totalLenView = new BigUint64Array(this.sab, SAB_OFFSETS.TOTAL_LEN, 1);
|
|
3058
|
-
|
|
3092
|
+
const multiChunkRequest = requestBytes.byteLength > maxChunk;
|
|
3093
|
+
if (!multiChunkRequest) {
|
|
3059
3094
|
new Uint8Array(this.sab, HEADER_SIZE, requestBytes.byteLength).set(requestBytes);
|
|
3060
3095
|
Atomics.store(this.ctrl, 3, requestBytes.byteLength);
|
|
3061
3096
|
Atomics.store(totalLenView, 0, BigInt(requestBytes.byteLength));
|
|
@@ -3079,11 +3114,11 @@ var VFSFileSystem = class {
|
|
|
3079
3114
|
Atomics.notify(this.ctrl, 0);
|
|
3080
3115
|
sent += chunkSize;
|
|
3081
3116
|
if (sent < requestBytes.byteLength) {
|
|
3082
|
-
spinWait(this.ctrl, 0, sent === chunkSize ? SIGNAL.REQUEST : SIGNAL.CHUNK);
|
|
3117
|
+
spinWait(this.ctrl, 0, sent === chunkSize ? SIGNAL.REQUEST : SIGNAL.CHUNK, this.ctrl);
|
|
3083
3118
|
}
|
|
3084
3119
|
}
|
|
3085
3120
|
}
|
|
3086
|
-
spinWait(this.ctrl, 0, SIGNAL.REQUEST);
|
|
3121
|
+
spinWait(this.ctrl, 0, multiChunkRequest ? SIGNAL.CHUNK : SIGNAL.REQUEST, this.ctrl);
|
|
3087
3122
|
const signal = Atomics.load(this.ctrl, 0);
|
|
3088
3123
|
const respChunkLen = Atomics.load(this.ctrl, 3);
|
|
3089
3124
|
const respTotalLen = Number(Atomics.load(totalLenView, 0));
|
|
@@ -3099,7 +3134,7 @@ var VFSFileSystem = class {
|
|
|
3099
3134
|
while (received < respTotalLen) {
|
|
3100
3135
|
Atomics.store(this.ctrl, 0, SIGNAL.CHUNK_ACK);
|
|
3101
3136
|
Atomics.notify(this.ctrl, 0);
|
|
3102
|
-
spinWait(this.ctrl, 0, SIGNAL.CHUNK_ACK);
|
|
3137
|
+
spinWait(this.ctrl, 0, SIGNAL.CHUNK_ACK, this.ctrl);
|
|
3103
3138
|
const nextLen = Atomics.load(this.ctrl, 3);
|
|
3104
3139
|
responseBytes.set(new Uint8Array(this.sab, HEADER_SIZE, nextLen), received);
|
|
3105
3140
|
received += nextLen;
|
|
@@ -3590,6 +3625,44 @@ var VFSFileSystem = class {
|
|
|
3590
3625
|
}
|
|
3591
3626
|
});
|
|
3592
3627
|
}
|
|
3628
|
+
/** True only while the filesystem is fully ready for synchronous operations
|
|
3629
|
+
* AND no leader transition is in progress. Reflects the moment-in-time state;
|
|
3630
|
+
* use `whenReady()` to await readiness reliably. */
|
|
3631
|
+
get ready() {
|
|
3632
|
+
return this.isReady && !this.transitioning;
|
|
3633
|
+
}
|
|
3634
|
+
/** Resolves once the filesystem is fully ready for synchronous operations,
|
|
3635
|
+
* including any in-flight leader transition (promotion-to-leader, etc.).
|
|
3636
|
+
* If already ready and no transition is pending, resolves immediately.
|
|
3637
|
+
*
|
|
3638
|
+
* Use this when coordinating with other Web-Lock-based systems (e.g. a
|
|
3639
|
+
* parent app that elects its own leader independently of the FS) — the
|
|
3640
|
+
* timing of the two elections isn't synchronized, so the FS may still be
|
|
3641
|
+
* reinitialising when the parent's lock fires. Calling `whenReady()`
|
|
3642
|
+
* after your own leader-acquisition guarantees the FS is back in a state
|
|
3643
|
+
* where sync ops won't stall the 20-second relay-worker heartbeat. */
|
|
3644
|
+
whenReady() {
|
|
3645
|
+
if (this.isReady && !this.transitioning) return Promise.resolve();
|
|
3646
|
+
if (this.transitioning) {
|
|
3647
|
+
return new Promise((resolve2) => {
|
|
3648
|
+
this.readyListeners.add(resolve2);
|
|
3649
|
+
});
|
|
3650
|
+
}
|
|
3651
|
+
return this.readyPromise.then(() => {
|
|
3652
|
+
});
|
|
3653
|
+
}
|
|
3654
|
+
/** Internal — called by lifecycle handlers when sync-relay says 'ready'. */
|
|
3655
|
+
fireReadyListeners() {
|
|
3656
|
+
const listeners = Array.from(this.readyListeners);
|
|
3657
|
+
this.readyListeners.clear();
|
|
3658
|
+
for (const l of listeners) {
|
|
3659
|
+
try {
|
|
3660
|
+
l();
|
|
3661
|
+
} catch (e) {
|
|
3662
|
+
console.warn("[VFS] readyListener threw:", e);
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3593
3666
|
/** Switch the filesystem mode at runtime.
|
|
3594
3667
|
*
|
|
3595
3668
|
* Typical flow for IDE corruption recovery:
|
|
@@ -3627,7 +3700,9 @@ var VFSFileSystem = class {
|
|
|
3627
3700
|
const msg = e.data;
|
|
3628
3701
|
if (msg.type === "ready") {
|
|
3629
3702
|
this.isReady = true;
|
|
3703
|
+
this.transitioning = false;
|
|
3630
3704
|
this.resolveReady();
|
|
3705
|
+
this.fireReadyListeners();
|
|
3631
3706
|
if (!this.isFollower) {
|
|
3632
3707
|
this.initLeaderBroker();
|
|
3633
3708
|
}
|