@componentor/fs 3.0.46 → 3.0.48
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 +64 -21
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/readme.md +12 -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,78 @@ 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
|
+
}
|
|
2896
2924
|
}
|
|
2897
|
-
}
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
}
|
|
2904
|
-
|
|
2905
|
-
});
|
|
2925
|
+
};
|
|
2926
|
+
mc.port1.start();
|
|
2927
|
+
this.brokerControlPort = mc.port1;
|
|
2928
|
+
}).catch((err) => {
|
|
2929
|
+
console.warn("[VFS] SW broker unavailable, single-tab only:", err.message);
|
|
2930
|
+
});
|
|
2931
|
+
};
|
|
2932
|
+
register();
|
|
2933
|
+
const bc = new BroadcastChannel(`${this.ns}-leader-change`);
|
|
2934
|
+
bc.postMessage({ type: "leader-changed" });
|
|
2935
|
+
bc.close();
|
|
2936
|
+
if (this.brokerHeartbeatTimer) clearInterval(this.brokerHeartbeatTimer);
|
|
2937
|
+
this.brokerHeartbeatTimer = setInterval(register, 5e3);
|
|
2906
2938
|
}
|
|
2907
2939
|
/** Promote from follower to leader (after leader tab dies and lock is acquired) */
|
|
2908
2940
|
promoteToLeader() {
|
|
2909
2941
|
this.isFollower = false;
|
|
2910
2942
|
this.isReady = false;
|
|
2911
2943
|
this.brokerInitialized = false;
|
|
2944
|
+
if (this.brokerHeartbeatTimer) {
|
|
2945
|
+
clearInterval(this.brokerHeartbeatTimer);
|
|
2946
|
+
this.brokerHeartbeatTimer = null;
|
|
2947
|
+
}
|
|
2948
|
+
if (this.brokerControlPort) {
|
|
2949
|
+
try {
|
|
2950
|
+
this.brokerControlPort.close();
|
|
2951
|
+
} catch {
|
|
2952
|
+
}
|
|
2953
|
+
this.brokerControlPort = null;
|
|
2954
|
+
}
|
|
2912
2955
|
if (this.leaderChangeBc) {
|
|
2913
2956
|
this.leaderChangeBc.close();
|
|
2914
2957
|
this.leaderChangeBc = null;
|