cross-tab-worker-databus 0.20.86 → 0.20.88
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/CHANGELOG.md +26 -0
- package/dist/centrifuge.js +1 -1
- package/dist/{chunk-SDOV3UHG.js → chunk-4UOLOWOD.js} +175 -33
- package/dist/{chunk-SDOV3UHG.js.map → chunk-4UOLOWOD.js.map} +3 -3
- package/dist/cjs/centrifuge.cjs +174 -32
- package/dist/cjs/centrifuge.cjs.map +3 -3
- package/dist/cjs/hooks.cjs +2 -2
- package/dist/cjs/hooks.cjs.map +2 -2
- package/dist/cjs/index.cjs +281 -52
- package/dist/cjs/index.cjs.map +3 -3
- package/dist/cjs/vue.cjs +1 -1
- package/dist/cjs/vue.cjs.map +2 -2
- package/dist/core/data-bus.d.ts +34 -5
- package/dist/core/data-bus.d.ts.map +1 -1
- package/dist/hooks.d.ts +2 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +2 -2
- package/dist/hooks.js.map +2 -2
- package/dist/index.js +108 -21
- package/dist/index.js.map +2 -2
- package/dist/vue.d.ts +2 -1
- package/dist/vue.d.ts.map +1 -1
- package/dist/vue.js +1 -1
- package/dist/vue.js.map +2 -2
- package/dist/websocket.d.ts +24 -2
- package/dist/websocket.d.ts.map +1 -1
- package/docs/api.md +15 -10
- package/docs/architecture.md +9 -4
- package/docs/roadmap.md +14 -1
- package/docs/transports.md +19 -2
- package/docs/zh/api.md +15 -10
- package/docs/zh/architecture.md +9 -4
- package/docs/zh/roadmap.md +14 -1
- package/docs/zh/transports.md +14 -2
- package/package.json +3 -3
package/dist/cjs/index.cjs
CHANGED
|
@@ -2329,7 +2329,7 @@ var DedupManager = class {
|
|
|
2329
2329
|
};
|
|
2330
2330
|
|
|
2331
2331
|
// src/core/version.ts
|
|
2332
|
-
var SDK_VERSION = true ? "0.20.
|
|
2332
|
+
var SDK_VERSION = true ? "0.20.88" : "";
|
|
2333
2333
|
|
|
2334
2334
|
// src/core/data-bus.ts
|
|
2335
2335
|
var DEFAULT_REPLAY_MAX_PER_TOPIC = 100;
|
|
@@ -2354,6 +2354,11 @@ var CrossTabDataBus = class {
|
|
|
2354
2354
|
started = false;
|
|
2355
2355
|
stopping = false;
|
|
2356
2356
|
transportReady = false;
|
|
2357
|
+
// Whether the installed transport has reported `connected` at least once
|
|
2358
|
+
// since the current open began. A clean `disconnected` after this point is
|
|
2359
|
+
// a lost working connection, not the pre-connect window of a worker-style
|
|
2360
|
+
// backend whose start() resolves before it reports the connection.
|
|
2361
|
+
transportHasConnected = false;
|
|
2357
2362
|
// Last transport failure, retained so ready() can surface it to callers who
|
|
2358
2363
|
// never awaited start() directly. Cleared on the next successful start.
|
|
2359
2364
|
lastError = null;
|
|
@@ -2392,6 +2397,19 @@ var CrossTabDataBus = class {
|
|
|
2392
2397
|
// a transport reopen succeeds so traces can correlate repeated failures.
|
|
2393
2398
|
recoveryAttempt = 0;
|
|
2394
2399
|
recoveryExhausted = false;
|
|
2400
|
+
// Gate that holds transport operations issued after a runtime `error` until
|
|
2401
|
+
// the scheduled recovery attempt has actually run. Without it, a dead
|
|
2402
|
+
// transport still has `transportReady === true` during the cooldown, so
|
|
2403
|
+
// publishes/subscribes would be written to the failed connection and lost.
|
|
2404
|
+
recoveryGate = null;
|
|
2405
|
+
recoveryGateRelease = null;
|
|
2406
|
+
recoveryTimer = null;
|
|
2407
|
+
recoveryTimerToken = 0;
|
|
2408
|
+
// Once an automatic attempt fails, an explicit transport operation may
|
|
2409
|
+
// recover immediately instead of waiting for the next paced attempt. The
|
|
2410
|
+
// gate still stays closed so the operation cannot reach the failed
|
|
2411
|
+
// transport; it is released by the successful on-demand reopen.
|
|
2412
|
+
recoveryDemandAllowed = false;
|
|
2395
2413
|
/** Monotonic generation incremented on every successful transport open.
|
|
2396
2414
|
* Stays in lockstep with `lastSuccessAt` so callers can detect that the
|
|
2397
2415
|
* transport has been reopened even if the timestamp window is short. */
|
|
@@ -2505,10 +2523,7 @@ var CrossTabDataBus = class {
|
|
|
2505
2523
|
this.suspendTransport();
|
|
2506
2524
|
},
|
|
2507
2525
|
onResume: () => {
|
|
2508
|
-
this.
|
|
2509
|
-
this.trace.start();
|
|
2510
|
-
this.startDedupSweep();
|
|
2511
|
-
this.replayManager.start();
|
|
2526
|
+
this.resumeSuspendedResources();
|
|
2512
2527
|
this.resumeTransport();
|
|
2513
2528
|
},
|
|
2514
2529
|
onDiagnostic: (event) => {
|
|
@@ -2538,7 +2553,11 @@ var CrossTabDataBus = class {
|
|
|
2538
2553
|
if (!transportDown) return Promise.resolve();
|
|
2539
2554
|
this.activeConfig = config;
|
|
2540
2555
|
this.resetFailureState();
|
|
2541
|
-
|
|
2556
|
+
const resumingFromSuspend = this.suspended;
|
|
2557
|
+
if (resumingFromSuspend) this.resumeSuspendedResources();
|
|
2558
|
+
const opening2 = this.reopenTransport();
|
|
2559
|
+
if (resumingFromSuspend) this.cluster.start();
|
|
2560
|
+
return opening2;
|
|
2542
2561
|
}
|
|
2543
2562
|
this.started = true;
|
|
2544
2563
|
this.stopping = false;
|
|
@@ -2610,8 +2629,38 @@ var CrossTabDataBus = class {
|
|
|
2610
2629
|
this.queuedStart = queued;
|
|
2611
2630
|
return queued;
|
|
2612
2631
|
}
|
|
2632
|
+
/** Release every operation waiting on the scheduled recovery attempt. */
|
|
2633
|
+
releaseRecoveryGate() {
|
|
2634
|
+
const release = this.recoveryGateRelease;
|
|
2635
|
+
this.recoveryGate = null;
|
|
2636
|
+
this.recoveryGateRelease = null;
|
|
2637
|
+
this.recoveryDemandAllowed = false;
|
|
2638
|
+
release?.();
|
|
2639
|
+
}
|
|
2640
|
+
/** Cancel a pending automatic retry when an explicit lifecycle transition
|
|
2641
|
+
* supersedes it. The released gate re-enters runTransport(), which then
|
|
2642
|
+
* follows the newest start/stop/suspend intent. */
|
|
2643
|
+
cancelScheduledRecovery() {
|
|
2644
|
+
this.recoveryTimerToken += 1;
|
|
2645
|
+
if (this.recoveryTimer !== null) {
|
|
2646
|
+
clearTimeout(this.recoveryTimer);
|
|
2647
|
+
this.recoveryTimer = null;
|
|
2648
|
+
}
|
|
2649
|
+
this.releaseRecoveryGate();
|
|
2650
|
+
}
|
|
2651
|
+
/** Keep the recovery gate closed after a failed attempt while allowing the
|
|
2652
|
+
* next explicit transport operation to start an immediate on-demand reopen.
|
|
2653
|
+
* If no gate/successor retry remains, release any waiters. */
|
|
2654
|
+
allowDemandRecovery() {
|
|
2655
|
+
if (this.recoveryGate !== null && this.started && !this.stopping && !this.suspended && this.status === WORKER_STATUS.ERROR) {
|
|
2656
|
+
this.recoveryDemandAllowed = true;
|
|
2657
|
+
return;
|
|
2658
|
+
}
|
|
2659
|
+
this.releaseRecoveryGate();
|
|
2660
|
+
}
|
|
2613
2661
|
/** Reset failure and recovery diagnostics for a new explicit start session. */
|
|
2614
2662
|
resetFailureState() {
|
|
2663
|
+
this.cancelScheduledRecovery();
|
|
2615
2664
|
this.lastError = null;
|
|
2616
2665
|
this.lastErrorAt = null;
|
|
2617
2666
|
this.lastFailure = null;
|
|
@@ -2631,22 +2680,27 @@ var CrossTabDataBus = class {
|
|
|
2631
2680
|
this.transportReady = false;
|
|
2632
2681
|
const chainedPendingStop = this.pendingStop;
|
|
2633
2682
|
const isCurrentLifecycle = () => lifecycleEpoch === this.lifecycleEpoch;
|
|
2683
|
+
let startupInProgress = true;
|
|
2634
2684
|
return before.catch(() => void 0).then(() => {
|
|
2635
2685
|
if (!isCurrentLifecycle() || this.stopping || this.suspended) return;
|
|
2636
2686
|
if (this.pendingStop === chainedPendingStop) this.pendingStop = null;
|
|
2687
|
+
this.transportHasConnected = false;
|
|
2637
2688
|
return Promise.resolve(
|
|
2638
2689
|
this.transport.start(config, {
|
|
2639
2690
|
onMessage: (message) => {
|
|
2640
2691
|
if (isCurrentLifecycle()) this.handleTransportMessage(message);
|
|
2641
2692
|
},
|
|
2642
2693
|
onStatus: (status) => {
|
|
2643
|
-
if (isCurrentLifecycle())
|
|
2694
|
+
if (isCurrentLifecycle()) {
|
|
2695
|
+
this.updateStatus(status, status !== WORKER_STATUS.ERROR || !startupInProgress);
|
|
2696
|
+
}
|
|
2644
2697
|
},
|
|
2645
2698
|
onError: (error) => {
|
|
2646
2699
|
if (isCurrentLifecycle()) this.reportError(error);
|
|
2647
2700
|
}
|
|
2648
2701
|
})
|
|
2649
2702
|
).then(() => {
|
|
2703
|
+
startupInProgress = false;
|
|
2650
2704
|
if (!isCurrentLifecycle()) return;
|
|
2651
2705
|
if (this.status === WORKER_STATUS.ERROR) {
|
|
2652
2706
|
throw new Error("Transport failed during startup.");
|
|
@@ -2655,24 +2709,26 @@ var CrossTabDataBus = class {
|
|
|
2655
2709
|
this.recoveryGeneration += 1;
|
|
2656
2710
|
this.lastSuccessAt = this.now();
|
|
2657
2711
|
this.transportReady = true;
|
|
2712
|
+
this.releaseRecoveryGate();
|
|
2658
2713
|
}
|
|
2659
2714
|
});
|
|
2660
2715
|
}).catch((error) => {
|
|
2661
2716
|
if (!isCurrentLifecycle()) throw error;
|
|
2717
|
+
startupInProgress = false;
|
|
2662
2718
|
if (stopClusterOnFailure) this.started = false;
|
|
2663
2719
|
if (!this.pendingStop) {
|
|
2664
2720
|
this.pendingStop = this.createStopPromise();
|
|
2665
2721
|
}
|
|
2666
|
-
this.updateStatus(WORKER_STATUS.ERROR);
|
|
2667
|
-
this.reportError(error);
|
|
2668
|
-
this.lastError = error;
|
|
2669
|
-
this.lastErrorAt = this.now();
|
|
2670
2722
|
this.transportReady = false;
|
|
2671
2723
|
if (stopClusterOnFailure) {
|
|
2672
2724
|
this.stopping = true;
|
|
2673
2725
|
this.cluster.stop();
|
|
2674
2726
|
this.stopping = false;
|
|
2675
2727
|
}
|
|
2728
|
+
this.recordError(error);
|
|
2729
|
+
this.startPromise = null;
|
|
2730
|
+
this.updateStatus(WORKER_STATUS.ERROR);
|
|
2731
|
+
this.notifyError(error);
|
|
2676
2732
|
throw error;
|
|
2677
2733
|
});
|
|
2678
2734
|
}
|
|
@@ -2681,7 +2737,10 @@ var CrossTabDataBus = class {
|
|
|
2681
2737
|
* Returns a rejected promise when the transport has failed and no start is in
|
|
2682
2738
|
* flight — the caller can retry by calling start() or ready() again. While an
|
|
2683
2739
|
* explicit stop() is settling, this rejects unless a restart is queued behind
|
|
2684
|
-
* it; false readiness during teardown is never reported.
|
|
2740
|
+
* it; false readiness during teardown is never reported. While the tab is
|
|
2741
|
+
* BFCache-suspended (pagehide without a following pageshow), this also
|
|
2742
|
+
* rejects: the suspended start promise is the transport-stop gate, not a
|
|
2743
|
+
* readiness signal.
|
|
2685
2744
|
*/
|
|
2686
2745
|
ready() {
|
|
2687
2746
|
if (this.queuedStart) return this.getQueuedStartReady();
|
|
@@ -2690,6 +2749,11 @@ var CrossTabDataBus = class {
|
|
|
2690
2749
|
"CrossTabDataBus is stopping; ready() cannot report readiness until stop() resolves. Wait for stop() to resolve, then call start() before awaiting ready()."
|
|
2691
2750
|
));
|
|
2692
2751
|
}
|
|
2752
|
+
if (this.suspended) {
|
|
2753
|
+
return Promise.reject(new Error(
|
|
2754
|
+
"CrossTabDataBus is suspended; ready() cannot report readiness until pageshow resumes the transport."
|
|
2755
|
+
));
|
|
2756
|
+
}
|
|
2693
2757
|
if (!this.started && !this.hasInitialConfig && this.lastError !== null) {
|
|
2694
2758
|
return Promise.reject(this.lastError);
|
|
2695
2759
|
}
|
|
@@ -2825,11 +2889,15 @@ var CrossTabDataBus = class {
|
|
|
2825
2889
|
getStatus() {
|
|
2826
2890
|
return this.status;
|
|
2827
2891
|
}
|
|
2828
|
-
/** Return the current automatic transport recovery state. `hasError` means a transport error is currently retained. */
|
|
2829
2892
|
/** Return the current automatic transport recovery state plus diagnostics.
|
|
2830
|
-
* `
|
|
2831
|
-
*
|
|
2832
|
-
*
|
|
2893
|
+
* `hasError`/`errorMessage`/`errorAt` describe the most recent retained
|
|
2894
|
+
* *transport* failure — from a transport open or a runtime `onError`. They
|
|
2895
|
+
* share the lifetime of the unified `lastFailure` ledger: a successful
|
|
2896
|
+
* recovery keeps the last failure visible, and only an explicit `start()`
|
|
2897
|
+
* clears it. `generation` increments on every successful transport open
|
|
2898
|
+
* (initial start and every recovery); `lastSuccessAt` is the timestamp of
|
|
2899
|
+
* the most recent successful open, or `null` until the transport reaches
|
|
2900
|
+
* `ready`. */
|
|
2833
2901
|
getRecoveryStats() {
|
|
2834
2902
|
const errorMessage = this.lastError instanceof Error ? this.lastError.message : this.lastError === null ? null : String(this.lastError);
|
|
2835
2903
|
return {
|
|
@@ -2856,7 +2924,7 @@ var CrossTabDataBus = class {
|
|
|
2856
2924
|
* unified failure ledger and recovery context that explains the verdict. */
|
|
2857
2925
|
getHealthSummary() {
|
|
2858
2926
|
const transport = this.transport;
|
|
2859
|
-
const transportDown =
|
|
2927
|
+
const transportDown = this.status !== WORKER_STATUS.CONNECTED;
|
|
2860
2928
|
const state = !this.started ? HEALTH_STATE.STOPPED : this.suspended ? HEALTH_STATE.SUSPENDED : transportDown ? this.recoveryExhausted ? HEALTH_STATE.DEGRADED : this.status === WORKER_STATUS.CONNECTING && this.recoveryAttempt === 0 ? HEALTH_STATE.STARTING : HEALTH_STATE.RECOVERING : HEALTH_STATE.HEALTHY;
|
|
2861
2929
|
return {
|
|
2862
2930
|
healthy: state === HEALTH_STATE.HEALTHY,
|
|
@@ -2947,6 +3015,7 @@ var CrossTabDataBus = class {
|
|
|
2947
3015
|
async performStop() {
|
|
2948
3016
|
this.lifecycleEpoch += 1;
|
|
2949
3017
|
this.stopping = true;
|
|
3018
|
+
this.cancelScheduledRecovery();
|
|
2950
3019
|
this.replayManager.suspend();
|
|
2951
3020
|
this.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.STOP });
|
|
2952
3021
|
this.trace.stop();
|
|
@@ -2959,6 +3028,8 @@ var CrossTabDataBus = class {
|
|
|
2959
3028
|
const pendingStop = this.pendingStop;
|
|
2960
3029
|
if (pendingStop) await pendingStop.catch(() => void 0);
|
|
2961
3030
|
else await this.transport.stop();
|
|
3031
|
+
} catch (error) {
|
|
3032
|
+
this.reportError(error);
|
|
2962
3033
|
} finally {
|
|
2963
3034
|
this.transportSubscribedTopics.clear();
|
|
2964
3035
|
this.resetDedup();
|
|
@@ -3021,13 +3092,15 @@ var CrossTabDataBus = class {
|
|
|
3021
3092
|
* Propagate a status change to the cluster, trace, and all registered
|
|
3022
3093
|
* status handlers. On reconnect, re-subscribe any topics assigned to us.
|
|
3023
3094
|
*/
|
|
3024
|
-
updateStatus(status) {
|
|
3095
|
+
updateStatus(status, notifyHandlers = true) {
|
|
3025
3096
|
const previousStatus = this.status;
|
|
3026
3097
|
this.status = status;
|
|
3098
|
+
if (status === WORKER_STATUS.CONNECTED) this.transportHasConnected = true;
|
|
3027
3099
|
if (previousStatus !== status) this.trace.event({ type: TRACE_EVENT_TYPE.STATUS, status });
|
|
3028
3100
|
this.cluster.setStatus(status);
|
|
3029
3101
|
if (status === WORKER_STATUS.DISCONNECTED || status === WORKER_STATUS.ERROR) this.transportSubscribedTopics.clear();
|
|
3030
3102
|
if (status === WORKER_STATUS.CONNECTED && previousStatus !== WORKER_STATUS.CONNECTED) {
|
|
3103
|
+
if (this.transportReady) this.releaseRecoveryGate();
|
|
3031
3104
|
for (const topic of this.cluster.getSnapshot().assignedTopics) this.subscribeTransport(topic);
|
|
3032
3105
|
}
|
|
3033
3106
|
if (status === WORKER_STATUS.ERROR && this.started && !this.stopping) {
|
|
@@ -3040,23 +3113,49 @@ var CrossTabDataBus = class {
|
|
|
3040
3113
|
this.recoveryExhausted = true;
|
|
3041
3114
|
this.trace.event({ type: TRACE_EVENT_TYPE.RELIABILITY, operation: RELIABILITY_OPERATION.TRANSPORT_RECOVERY, attempt: this.recoveryMaxAttempts, outcome: RECOVERY_OUTCOME.EXHAUSTED });
|
|
3042
3115
|
}
|
|
3116
|
+
this.releaseRecoveryGate();
|
|
3043
3117
|
return;
|
|
3044
3118
|
}
|
|
3045
3119
|
this.trace.event({ type: TRACE_EVENT_TYPE.RELIABILITY, operation: RELIABILITY_OPERATION.TRANSPORT_RECOVERY, attempt, outcome: RECOVERY_OUTCOME.SCHEDULED });
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3120
|
+
if (this.recoveryGate === null) {
|
|
3121
|
+
let release;
|
|
3122
|
+
this.recoveryGate = new Promise((resolve) => {
|
|
3123
|
+
release = resolve;
|
|
3124
|
+
});
|
|
3125
|
+
this.recoveryGateRelease = release;
|
|
3126
|
+
}
|
|
3127
|
+
this.recoveryDemandAllowed = false;
|
|
3128
|
+
const timerToken = ++this.recoveryTimerToken;
|
|
3129
|
+
this.recoveryTimer = setTimeout(() => {
|
|
3130
|
+
if (timerToken !== this.recoveryTimerToken) return;
|
|
3131
|
+
this.recoveryTimer = null;
|
|
3132
|
+
if (this.stopping || !this.started || this.suspended || this.status !== WORKER_STATUS.ERROR) {
|
|
3133
|
+
this.releaseRecoveryGate();
|
|
3134
|
+
return;
|
|
3135
|
+
}
|
|
3136
|
+
this.recoveryDemandAllowed = false;
|
|
3137
|
+
const opening = this.reopenTransport(attempt);
|
|
3138
|
+
void opening.then(
|
|
3139
|
+
() => this.releaseRecoveryGate(),
|
|
3140
|
+
() => this.allowDemandRecovery()
|
|
3141
|
+
);
|
|
3050
3142
|
}, this.recoveryCooldownMs);
|
|
3051
3143
|
}
|
|
3144
|
+
} else if (status === WORKER_STATUS.ERROR) {
|
|
3145
|
+
this.releaseRecoveryGate();
|
|
3052
3146
|
}
|
|
3053
|
-
this.invokeHandlers(this.statusHandlers, (handler) => handler(status));
|
|
3147
|
+
if (notifyHandlers) this.invokeHandlers(this.statusHandlers, (handler) => handler(status));
|
|
3054
3148
|
}
|
|
3055
|
-
|
|
3149
|
+
recordError(error, source = FAILURE_SOURCE.TRANSPORT) {
|
|
3150
|
+
const at = this.now();
|
|
3151
|
+
if (source === FAILURE_SOURCE.TRANSPORT) {
|
|
3152
|
+
this.lastError = error;
|
|
3153
|
+
this.lastErrorAt = at;
|
|
3154
|
+
}
|
|
3056
3155
|
this.lastFailure = {
|
|
3057
3156
|
source,
|
|
3058
3157
|
message: error instanceof Error ? error.message : String(error),
|
|
3059
|
-
at
|
|
3158
|
+
at
|
|
3060
3159
|
};
|
|
3061
3160
|
if (source === FAILURE_SOURCE.PERSISTENCE) {
|
|
3062
3161
|
this.persistenceFailureCount += 1;
|
|
@@ -3067,8 +3166,14 @@ var CrossTabDataBus = class {
|
|
|
3067
3166
|
type: TRACE_EVENT_TYPE.ERROR,
|
|
3068
3167
|
source: source === FAILURE_SOURCE.TRANSPORT ? TRACE_ERROR_SOURCE.TRANSPORT : TRACE_ERROR_SOURCE.OPERATION
|
|
3069
3168
|
});
|
|
3169
|
+
}
|
|
3170
|
+
notifyError(error) {
|
|
3070
3171
|
this.invokeHandlers(this.errorHandlers, (handler) => handler(error), INVOKE_LABEL.ERROR_HANDLER);
|
|
3071
3172
|
}
|
|
3173
|
+
reportError(error, source = FAILURE_SOURCE.TRANSPORT) {
|
|
3174
|
+
this.recordError(error, source);
|
|
3175
|
+
this.notifyError(error);
|
|
3176
|
+
}
|
|
3072
3177
|
/** Report a persistence failure to the trace and the unified failure ledger,
|
|
3073
3178
|
* unless it is a {@link PersistenceRetryCancelledError} cancellation from a
|
|
3074
3179
|
* lifecycle transition (teardown should stay quiet). */
|
|
@@ -3131,6 +3236,15 @@ var CrossTabDataBus = class {
|
|
|
3131
3236
|
}
|
|
3132
3237
|
}
|
|
3133
3238
|
}
|
|
3239
|
+
/** Resume the resources paused by a pagehide suspension. Both the native
|
|
3240
|
+
* pageshow path and explicit start() must run this so an explicit resume
|
|
3241
|
+
* cannot leave trace metrics and periodic cleanup timers permanently off. */
|
|
3242
|
+
resumeSuspendedResources() {
|
|
3243
|
+
this.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.RESUME });
|
|
3244
|
+
this.trace.start();
|
|
3245
|
+
this.startDedupSweep();
|
|
3246
|
+
this.replayManager.start();
|
|
3247
|
+
}
|
|
3134
3248
|
/**
|
|
3135
3249
|
* Suspend the transport when the tab goes hidden. Stops the transport and
|
|
3136
3250
|
* clears subscription state so it will be re-established on resume.
|
|
@@ -3139,11 +3253,15 @@ var CrossTabDataBus = class {
|
|
|
3139
3253
|
if (this.stopping) return;
|
|
3140
3254
|
this.lifecycleEpoch += 1;
|
|
3141
3255
|
this.suspended = true;
|
|
3256
|
+
this.cancelScheduledRecovery();
|
|
3142
3257
|
this.transportReady = false;
|
|
3143
3258
|
this.transportSubscribedTopics.clear();
|
|
3144
3259
|
this.updateStatus(WORKER_STATUS.DISCONNECTED);
|
|
3145
|
-
if (this.pendingStop)
|
|
3146
|
-
|
|
3260
|
+
if (this.pendingStop && (this.startPromise === null || this.startPromise === this.pendingStop)) {
|
|
3261
|
+
this.startPromise = this.pendingStop;
|
|
3262
|
+
return;
|
|
3263
|
+
}
|
|
3264
|
+
const pending = this.startPromise ?? this.pendingStop ?? Promise.resolve();
|
|
3147
3265
|
const stopping = pending.catch(() => void 0).then(() => this.transport.stop()).catch((error) => this.reportError(error));
|
|
3148
3266
|
this.startPromise = stopping;
|
|
3149
3267
|
this.pendingStop = stopping;
|
|
@@ -3208,7 +3326,24 @@ var CrossTabDataBus = class {
|
|
|
3208
3326
|
*/
|
|
3209
3327
|
runTransport(operation) {
|
|
3210
3328
|
if (this.suspended) return;
|
|
3211
|
-
if (this.
|
|
3329
|
+
if (this.recoveryGate && !this.stopping) {
|
|
3330
|
+
if (this.recoveryDemandAllowed && this.status === WORKER_STATUS.ERROR && !this.suspended) {
|
|
3331
|
+
this.recoveryDemandAllowed = false;
|
|
3332
|
+
const opening = this.reopenTransport();
|
|
3333
|
+
void opening.then(
|
|
3334
|
+
() => this.releaseRecoveryGate(),
|
|
3335
|
+
() => this.allowDemandRecovery()
|
|
3336
|
+
);
|
|
3337
|
+
}
|
|
3338
|
+
const gate = this.recoveryGate;
|
|
3339
|
+
void gate.then(() => {
|
|
3340
|
+
if (this.stopping || this.suspended) return;
|
|
3341
|
+
this.runTransport(operation);
|
|
3342
|
+
});
|
|
3343
|
+
return;
|
|
3344
|
+
}
|
|
3345
|
+
const droppedAfterConnect = this.transportHasConnected && this.status === WORKER_STATUS.DISCONNECTED;
|
|
3346
|
+
if (this.transportReady && this.status !== WORKER_STATUS.ERROR && !droppedAfterConnect && !this.stopping) {
|
|
3212
3347
|
try {
|
|
3213
3348
|
void Promise.resolve(operation()).catch((error) => this.reportError(error));
|
|
3214
3349
|
} catch (error) {
|
|
@@ -3221,10 +3356,17 @@ var CrossTabDataBus = class {
|
|
|
3221
3356
|
ready = this.reopenTransport();
|
|
3222
3357
|
}
|
|
3223
3358
|
if (!ready || this.stopping) return;
|
|
3224
|
-
void ready.then(
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3359
|
+
void ready.then(
|
|
3360
|
+
() => {
|
|
3361
|
+
if (!this.started || this.stopping || this.suspended) return;
|
|
3362
|
+
return operation();
|
|
3363
|
+
},
|
|
3364
|
+
// The opening promise reports its own lifecycle failure through
|
|
3365
|
+
// openTransport(). Swallowing it here prevents a stale startup
|
|
3366
|
+
// rejection from being recorded again after an onStatus/onError
|
|
3367
|
+
// callback has already started and reset the ledger for a retry.
|
|
3368
|
+
() => void 0
|
|
3369
|
+
).catch((error) => this.reportError(error));
|
|
3228
3370
|
}
|
|
3229
3371
|
/**
|
|
3230
3372
|
* Publications started after teardown begins cannot reach any transport.
|
|
@@ -3562,6 +3704,7 @@ function parseDataBusPublication(value, fallbackTopic) {
|
|
|
3562
3704
|
}
|
|
3563
3705
|
|
|
3564
3706
|
// src/websocket.ts
|
|
3707
|
+
var DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
|
|
3565
3708
|
var WS_OPEN = 1;
|
|
3566
3709
|
var WebSocketTransport = class {
|
|
3567
3710
|
constructor(connection) {
|
|
@@ -3571,12 +3714,27 @@ var WebSocketTransport = class {
|
|
|
3571
3714
|
diagnosticsName = "websocket";
|
|
3572
3715
|
diagnosticsBackend = "native-websocket";
|
|
3573
3716
|
socket = null;
|
|
3717
|
+
socketActive = false;
|
|
3574
3718
|
handlers = null;
|
|
3575
3719
|
subscribedTopics = /* @__PURE__ */ new Set();
|
|
3576
|
-
|
|
3577
|
-
|
|
3720
|
+
// Handshake gate for the current start(). Resolves once the socket opens,
|
|
3721
|
+
// rejects when the attempt fails, so the DataBus start Promise — and every
|
|
3722
|
+
// operation parked behind it — settles at the real connection boundary.
|
|
3723
|
+
connectPromise = null;
|
|
3724
|
+
connectResolve = null;
|
|
3725
|
+
connectReject = null;
|
|
3726
|
+
connectTimer = null;
|
|
3727
|
+
/** Open the WebSocket and wire lifecycle listeners. Resolves once the
|
|
3728
|
+
* handshake completes and rejects when the attempt fails, matching the
|
|
3729
|
+
* `DataBusTransport.start` contract ("resolves on connect or rejects on
|
|
3730
|
+
* failure"). A factory failure is reported through `onStatus('error')` so
|
|
3731
|
+
* the DataBus can recover. */
|
|
3578
3732
|
start(config, handlers) {
|
|
3579
|
-
if (this.socket)
|
|
3733
|
+
if (this.socket && this.socketActive) {
|
|
3734
|
+
return this.connectPromise ?? void 0;
|
|
3735
|
+
}
|
|
3736
|
+
this.socket = null;
|
|
3737
|
+
this.socketActive = false;
|
|
3580
3738
|
this.handlers = handlers;
|
|
3581
3739
|
const factory = config.webSocketFactory ?? this.connection.webSocketFactory ?? defaultWebSocketFactory;
|
|
3582
3740
|
const protocols = config.protocols ?? this.connection.protocols;
|
|
@@ -3588,23 +3746,66 @@ var WebSocketTransport = class {
|
|
|
3588
3746
|
handlers.onError(error);
|
|
3589
3747
|
return;
|
|
3590
3748
|
}
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3749
|
+
const opening = new Promise((resolve, reject) => {
|
|
3750
|
+
this.connectResolve = resolve;
|
|
3751
|
+
this.connectReject = reject;
|
|
3752
|
+
let handshakeCompleted = false;
|
|
3753
|
+
let handshakeFailed = false;
|
|
3754
|
+
const timeoutMs = config.connectTimeoutMs ?? this.connection.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
3755
|
+
if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
3756
|
+
this.connectTimer = setTimeout(() => {
|
|
3757
|
+
if (this.socket !== socket || this.handlers !== handlers || handshakeCompleted) return;
|
|
3758
|
+
handshakeFailed = true;
|
|
3759
|
+
this.connectTimer = null;
|
|
3760
|
+
this.socketActive = false;
|
|
3761
|
+
const error = new Error(`WebSocket did not open within ${timeoutMs}ms.`);
|
|
3762
|
+
handlers.onStatus(WORKER_STATUS.ERROR);
|
|
3763
|
+
handlers.onError(error);
|
|
3764
|
+
this.failConnect(error);
|
|
3765
|
+
socket.close();
|
|
3766
|
+
}, timeoutMs);
|
|
3595
3767
|
}
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3768
|
+
socket.onopen = () => {
|
|
3769
|
+
if (this.socket !== socket || this.handlers !== handlers || handshakeFailed) return;
|
|
3770
|
+
this.socketActive = true;
|
|
3771
|
+
this.clearConnectTimer();
|
|
3772
|
+
for (const topic of this.subscribedTopics) {
|
|
3773
|
+
this.sendFrame({ op: WS_OP.SUBSCRIBE, topic });
|
|
3774
|
+
}
|
|
3775
|
+
handlers.onStatus(WORKER_STATUS.CONNECTED);
|
|
3776
|
+
if (!handshakeCompleted) {
|
|
3777
|
+
handshakeCompleted = true;
|
|
3778
|
+
this.settleConnect();
|
|
3779
|
+
}
|
|
3780
|
+
};
|
|
3781
|
+
socket.onclose = () => {
|
|
3782
|
+
if (this.socket !== socket || this.handlers !== handlers || !this.socketActive) return;
|
|
3783
|
+
this.socketActive = false;
|
|
3784
|
+
handlers.onStatus(WORKER_STATUS.DISCONNECTED);
|
|
3785
|
+
if (!handshakeCompleted) {
|
|
3786
|
+
handshakeFailed = true;
|
|
3787
|
+
this.failConnect(new Error("WebSocket closed before the handshake completed."));
|
|
3788
|
+
}
|
|
3789
|
+
};
|
|
3790
|
+
socket.onerror = () => {
|
|
3791
|
+
if (this.socket !== socket || this.handlers !== handlers || !this.socketActive) return;
|
|
3792
|
+
this.socketActive = false;
|
|
3793
|
+
handlers.onStatus(WORKER_STATUS.ERROR);
|
|
3794
|
+
if (!handshakeCompleted) {
|
|
3795
|
+
handshakeFailed = true;
|
|
3796
|
+
this.failConnect(new Error("WebSocket failed to open."));
|
|
3797
|
+
}
|
|
3798
|
+
};
|
|
3799
|
+
socket.onmessage = (event) => {
|
|
3800
|
+
if (this.socket === socket && this.handlers === handlers && this.socketActive) {
|
|
3801
|
+
void this.handleMessage(event.data);
|
|
3802
|
+
}
|
|
3803
|
+
};
|
|
3804
|
+
this.socket = socket;
|
|
3805
|
+
this.socketActive = true;
|
|
3806
|
+
});
|
|
3807
|
+
this.connectPromise = opening;
|
|
3808
|
+
return opening;
|
|
3608
3809
|
}
|
|
3609
3810
|
/** Idempotent: re-subscribing an active topic re-sends the frame but does
|
|
3610
3811
|
* not duplicate the local tracking entry. */
|
|
@@ -3660,10 +3861,38 @@ var WebSocketTransport = class {
|
|
|
3660
3861
|
/** Close the socket and drop all state. Safe to call multiple times. */
|
|
3661
3862
|
stop() {
|
|
3662
3863
|
const socket = this.socket;
|
|
3864
|
+
const shouldClose = this.socketActive;
|
|
3663
3865
|
this.socket = null;
|
|
3866
|
+
this.socketActive = false;
|
|
3664
3867
|
this.handlers = null;
|
|
3665
3868
|
this.subscribedTopics.clear();
|
|
3666
|
-
|
|
3869
|
+
this.settleConnect();
|
|
3870
|
+
this.connectPromise = null;
|
|
3871
|
+
if (shouldClose) socket?.close();
|
|
3872
|
+
}
|
|
3873
|
+
/** Resolve the in-flight handshake gate. Idempotent: once the socket has
|
|
3874
|
+
* opened (or a newer attempt replaced it) later calls are no-ops. */
|
|
3875
|
+
settleConnect() {
|
|
3876
|
+
this.clearConnectTimer();
|
|
3877
|
+
const resolve = this.connectResolve;
|
|
3878
|
+
this.connectResolve = null;
|
|
3879
|
+
this.connectReject = null;
|
|
3880
|
+
resolve?.();
|
|
3881
|
+
}
|
|
3882
|
+
/** Reject the in-flight handshake gate. Idempotent on the same terms as
|
|
3883
|
+
* {@link settleConnect}. */
|
|
3884
|
+
failConnect(error) {
|
|
3885
|
+
this.clearConnectTimer();
|
|
3886
|
+
const reject = this.connectReject;
|
|
3887
|
+
this.connectResolve = null;
|
|
3888
|
+
this.connectReject = null;
|
|
3889
|
+
reject?.(error);
|
|
3890
|
+
}
|
|
3891
|
+
clearConnectTimer() {
|
|
3892
|
+
if (this.connectTimer !== null) {
|
|
3893
|
+
clearTimeout(this.connectTimer);
|
|
3894
|
+
this.connectTimer = null;
|
|
3895
|
+
}
|
|
3667
3896
|
}
|
|
3668
3897
|
/** Send one JSON frame. Frames are dropped with an `onError` report when
|
|
3669
3898
|
* the socket is not open — subscribe frames are re-sent on open, so the
|