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/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
## [Unreleased]
|
|
2
|
+
|
|
3
|
+
## [0.20.88] - 2026-09-16
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- A transport that reports `error` synchronously during startup now lets `start()` retry from either the startup-failure `onStatus` callback or the startup-failure `onError` callback. The internal status is updated immediately, but the user-facing `onStatus('error')` notification is deferred until the failed open has cleared `transportReady`, torn down the initial cluster, installed the transport stop gate, recorded the failure, and cleared `startPromise`. The retry therefore opens a fresh lifecycle after cleanup and resets the failure ledger; the superseded opening's rejection is consumed by that lifecycle path and cannot repopulate the ledger after the retry succeeds.
|
|
7
|
+
- A synchronous `start()` retry issued from the startup-failure `onError` callback now opens a fresh lifecycle instead of returning the same rejecting open. `openTransport()` previously notified error handlers before clearing `startPromise`, so the retry shared the failed promise and never called `transport.start()` again. The failed open is now fully torn down and its lifecycle gate cleared before failure notification, and the retry is chained after the failed transport's stop cleanup.
|
|
8
|
+
- A bus whose initial transport open is still pending now recovers from repeated BFCache `pagehide`/`pageshow` cycles. Each suspend used to return early whenever a `pendingStop` gate existed, but a queued resume opening could already sit behind an older stop gate, so `startPromise` and `pendingStop` stopped being the same promise; the next `pageshow` then reused that superseded opening and the bus stayed suspended forever. `suspendTransport()` now reuses the gate only when it still represents this suspend, and otherwise chains a fresh serial `transport.stop()` that restores the `startPromise === pendingStop` invariant, so the following reopen actually restarts the transport.
|
|
9
|
+
- An explicit `start()` that resumes a BFCache-suspended bus now also resumes cross-tab coordination. `pagehide` pauses the bus *and* the `WorkerClusterRuntime` (closing the channel, stopping the heartbeat, releasing route assignments), but the started `start()` fast path only cleared `suspended` and reopened the transport, leaving `cluster.suspended` true. The bus then reported a healthy transport while every publication arriving from a peer tab was discarded by `isAssigned()` against the cleared assignment map, until a later `pageshow` happened to fire. `start()` now resumes the cluster whenever it is taking the bus out of suspension, so an explicit resume restores the full coordination plane.
|
|
10
|
+
- An explicit `start()` that resumes a BFCache-suspended bus now restarts every resource paused by `pagehide`, not only the transport and cluster. Trace metrics, the dedup expiry sweep, and replay retention cleanup were stopped by `onSuspend()`, while the native `pageshow` path restarted them through `onResume()`; the explicit resume bypassed that callback and could leave all three timers permanently off for the rest of a `healthy` session. Both resume paths now share one lifecycle helper.
|
|
11
|
+
|
|
12
|
+
## [0.20.87] - 2026-09-16
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- Transport operations issued after a runtime `error` no longer reach the connection that just failed. While an automatic or demand-driven reopen is pending, `runTransport()` parks `subscribe()` / `publish()` behind a recovery gate and releases them only after a reopen succeeds (or the transport self-heals to `connected`). `transportReady` deliberately stays true through the error so `ready()` keeps tracking the installed transport, which meant a subscribe or publish inside the cooldown window was written to the dead connection and lost. A failed automatic attempt keeps the gate closed but now lets the next explicit operation start an immediate on-demand reopen instead of waiting out another cooldown, and parked operations flush behind that success; exhausting `recovery.maxAttempts`, or superseding the wait with `stop()` / page hide, releases the gate so the explicit-retry path and the documented suspend-drop semantics are unchanged. A clean `disconnected` still never schedules automatic recovery.
|
|
16
|
+
- An operation issued after a clean transport `disconnected` now reopens the transport on demand instead of being written to the closed connection. `runTransport()` previously exempted only `error`, so a `subscribe()` / `publish()` arriving after a clean `close` took the ready fast path and the backend dropped it (the WebSocket send guard reports dropped frames but never sends them). The fast path is now refused once the transport has actually reached `connected` and then reports `disconnected`: the clean close still schedules no background recovery, but the next explicit operation demands exactly one reopen and flushes the parked operation behind it. A transport that resolves `start()` before its first `connected` (worker-style backends report the connection asynchronously) keeps the previous behaviour, so the pre-connect window does not trigger a redundant reopen.
|
|
17
|
+
- `createWebSocketDataBus()` automatic recovery now actually reopens a failed WebSocket. `WebSocketTransport.start()` previously returned whenever `this.socket` was non-null, and an error/close left that reference in place, so every post-cooldown `reopenTransport()` was a no-op against the dead socket. The transport now tracks whether the current socket is active, replaces the stale connection on the next `start()`, re-sends subscriptions after the replacement opens, and ignores late callbacks from the superseded socket. An error followed by close also keeps the `error` status that schedules recovery.
|
|
18
|
+
- React and Vue `useCrossTabHealth` bindings now apply `intervalMs` changes without recreating the bus. React previously ignored an option change unless the bus identity changed, and the Vue composable ignored changes to a reactive options object; switching to `0` therefore left polling active, while changing a positive cadence kept the old timer. Both adapters now tear down the old listener/timer set and install the new cadence.
|
|
19
|
+
- `stop()` now resolves once the bus is torn down even when the transport's own `stop()` rejects or throws. The failure is routed through the same `onError` / unified `lastFailure` channel that page-hide suspension and open-failure cleanup already use, so the fire-and-forget `void bus.stop()` teardown in the React and Vue adapters can no longer surface as an unhandled rejection. The instance stays restartable either way.
|
|
20
|
+
- A failed transport open is now stamped once: `getRecoveryStats().errorAt` and the `lastFailure.at` of the same failure are equal instead of differing by a clock re-read. The open-failure path called the injected clock twice for one failure, so a consumer correlating the two ledgers (or a test using an advancing clock) saw two timestamps for one event. The open-failure path also clears `transportReady` before notifying status/error handlers, keeping the "transport is not accepting operations" verdict local to the failure block.
|
|
21
|
+
- `ready()` no longer reports a BFCache-suspended bus as ready. After `pagehide`, `suspendTransport()` chains `transport.stop()` and reuses `startPromise` as the stop gate; `ready()` previously returned that gate, so it resolved the moment cleanup finished even though the transport was intentionally stopped and publications were dropped. Readiness now rejects with a clear suspended-state error while hidden; `pageshow` (or an explicit `start()`) clears the flag and installs a real reopen promise, after which `ready()` resolves as before. This restores the documented invariant that `ready()` never resolves for a transport that cannot carry data.
|
|
22
|
+
- A runtime transport failure reported through `onError` now lands in the transport recovery ledger, not only in the unified `lastFailure` record. `getRecoveryStats().hasError` / `errorMessage` / `errorAt` were written exclusively by the transport-*open* failure path, so a failure raised after a successful open produced a self-contradicting health snapshot: `state: 'recovering'` and a retained `lastFailure` alongside `recovery.hasError: false, errorMessage: null`. The recovery ledger now tracks every transport-sourced failure (open or runtime) and still keeps non-transport failures (`persistence`, `dispatch`) out of it, where they remain visible through `lastFailure` and `getPersistenceStats()`.
|
|
23
|
+
- The native WebSocket backend now honors the `DataBusTransport.start()` contract: it resolves only after the socket `open` and rejects when the handshake errors, closes before opening, or exceeds the new `connectTimeoutMs` (default 30s). Previously `start()` returned while the socket was still `CONNECTING`, so `await bus.ready()` resolved before the connection was usable and an immediately following `publish()` was dropped by the not-open guard. A timed-out attempt now closes its half-open socket and ignores a late `open`; an in-place re-open after a successful handshake still re-asserts subscriptions.
|
|
24
|
+
- `getHealthSummary()` now treats a live `connected` transport as healthy even during the brief window before `start()` settles and `transportReady` flips to true. With a handshake-gated WebSocket start, the `connected` status event fires while `transportReady` is still false, so an event-driven `useCrossTabHealth` snapshot with `intervalMs: 0` could remain stuck in `starting`/`recovering`. `transportReady` remains a diagnostic field; operations are queued behind the in-flight start rather than dropped.
|
|
25
|
+
|
|
26
|
+
|
|
1
27
|
## [0.20.86] - 2026-09-16
|
|
2
28
|
|
|
3
29
|
### Added
|
package/dist/centrifuge.js
CHANGED
|
@@ -2203,7 +2203,7 @@ var DedupManager = class {
|
|
|
2203
2203
|
};
|
|
2204
2204
|
|
|
2205
2205
|
// src/core/version.ts
|
|
2206
|
-
var SDK_VERSION = true ? "0.20.
|
|
2206
|
+
var SDK_VERSION = true ? "0.20.88" : "";
|
|
2207
2207
|
|
|
2208
2208
|
// src/core/data-bus.ts
|
|
2209
2209
|
var DEFAULT_REPLAY_MAX_PER_TOPIC = 100;
|
|
@@ -2228,6 +2228,11 @@ var CrossTabDataBus = class {
|
|
|
2228
2228
|
started = false;
|
|
2229
2229
|
stopping = false;
|
|
2230
2230
|
transportReady = false;
|
|
2231
|
+
// Whether the installed transport has reported `connected` at least once
|
|
2232
|
+
// since the current open began. A clean `disconnected` after this point is
|
|
2233
|
+
// a lost working connection, not the pre-connect window of a worker-style
|
|
2234
|
+
// backend whose start() resolves before it reports the connection.
|
|
2235
|
+
transportHasConnected = false;
|
|
2231
2236
|
// Last transport failure, retained so ready() can surface it to callers who
|
|
2232
2237
|
// never awaited start() directly. Cleared on the next successful start.
|
|
2233
2238
|
lastError = null;
|
|
@@ -2266,6 +2271,19 @@ var CrossTabDataBus = class {
|
|
|
2266
2271
|
// a transport reopen succeeds so traces can correlate repeated failures.
|
|
2267
2272
|
recoveryAttempt = 0;
|
|
2268
2273
|
recoveryExhausted = false;
|
|
2274
|
+
// Gate that holds transport operations issued after a runtime `error` until
|
|
2275
|
+
// the scheduled recovery attempt has actually run. Without it, a dead
|
|
2276
|
+
// transport still has `transportReady === true` during the cooldown, so
|
|
2277
|
+
// publishes/subscribes would be written to the failed connection and lost.
|
|
2278
|
+
recoveryGate = null;
|
|
2279
|
+
recoveryGateRelease = null;
|
|
2280
|
+
recoveryTimer = null;
|
|
2281
|
+
recoveryTimerToken = 0;
|
|
2282
|
+
// Once an automatic attempt fails, an explicit transport operation may
|
|
2283
|
+
// recover immediately instead of waiting for the next paced attempt. The
|
|
2284
|
+
// gate still stays closed so the operation cannot reach the failed
|
|
2285
|
+
// transport; it is released by the successful on-demand reopen.
|
|
2286
|
+
recoveryDemandAllowed = false;
|
|
2269
2287
|
/** Monotonic generation incremented on every successful transport open.
|
|
2270
2288
|
* Stays in lockstep with `lastSuccessAt` so callers can detect that the
|
|
2271
2289
|
* transport has been reopened even if the timestamp window is short. */
|
|
@@ -2379,10 +2397,7 @@ var CrossTabDataBus = class {
|
|
|
2379
2397
|
this.suspendTransport();
|
|
2380
2398
|
},
|
|
2381
2399
|
onResume: () => {
|
|
2382
|
-
this.
|
|
2383
|
-
this.trace.start();
|
|
2384
|
-
this.startDedupSweep();
|
|
2385
|
-
this.replayManager.start();
|
|
2400
|
+
this.resumeSuspendedResources();
|
|
2386
2401
|
this.resumeTransport();
|
|
2387
2402
|
},
|
|
2388
2403
|
onDiagnostic: (event) => {
|
|
@@ -2412,7 +2427,11 @@ var CrossTabDataBus = class {
|
|
|
2412
2427
|
if (!transportDown) return Promise.resolve();
|
|
2413
2428
|
this.activeConfig = config;
|
|
2414
2429
|
this.resetFailureState();
|
|
2415
|
-
|
|
2430
|
+
const resumingFromSuspend = this.suspended;
|
|
2431
|
+
if (resumingFromSuspend) this.resumeSuspendedResources();
|
|
2432
|
+
const opening2 = this.reopenTransport();
|
|
2433
|
+
if (resumingFromSuspend) this.cluster.start();
|
|
2434
|
+
return opening2;
|
|
2416
2435
|
}
|
|
2417
2436
|
this.started = true;
|
|
2418
2437
|
this.stopping = false;
|
|
@@ -2484,8 +2503,38 @@ var CrossTabDataBus = class {
|
|
|
2484
2503
|
this.queuedStart = queued;
|
|
2485
2504
|
return queued;
|
|
2486
2505
|
}
|
|
2506
|
+
/** Release every operation waiting on the scheduled recovery attempt. */
|
|
2507
|
+
releaseRecoveryGate() {
|
|
2508
|
+
const release = this.recoveryGateRelease;
|
|
2509
|
+
this.recoveryGate = null;
|
|
2510
|
+
this.recoveryGateRelease = null;
|
|
2511
|
+
this.recoveryDemandAllowed = false;
|
|
2512
|
+
release?.();
|
|
2513
|
+
}
|
|
2514
|
+
/** Cancel a pending automatic retry when an explicit lifecycle transition
|
|
2515
|
+
* supersedes it. The released gate re-enters runTransport(), which then
|
|
2516
|
+
* follows the newest start/stop/suspend intent. */
|
|
2517
|
+
cancelScheduledRecovery() {
|
|
2518
|
+
this.recoveryTimerToken += 1;
|
|
2519
|
+
if (this.recoveryTimer !== null) {
|
|
2520
|
+
clearTimeout(this.recoveryTimer);
|
|
2521
|
+
this.recoveryTimer = null;
|
|
2522
|
+
}
|
|
2523
|
+
this.releaseRecoveryGate();
|
|
2524
|
+
}
|
|
2525
|
+
/** Keep the recovery gate closed after a failed attempt while allowing the
|
|
2526
|
+
* next explicit transport operation to start an immediate on-demand reopen.
|
|
2527
|
+
* If no gate/successor retry remains, release any waiters. */
|
|
2528
|
+
allowDemandRecovery() {
|
|
2529
|
+
if (this.recoveryGate !== null && this.started && !this.stopping && !this.suspended && this.status === WORKER_STATUS.ERROR) {
|
|
2530
|
+
this.recoveryDemandAllowed = true;
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
this.releaseRecoveryGate();
|
|
2534
|
+
}
|
|
2487
2535
|
/** Reset failure and recovery diagnostics for a new explicit start session. */
|
|
2488
2536
|
resetFailureState() {
|
|
2537
|
+
this.cancelScheduledRecovery();
|
|
2489
2538
|
this.lastError = null;
|
|
2490
2539
|
this.lastErrorAt = null;
|
|
2491
2540
|
this.lastFailure = null;
|
|
@@ -2505,22 +2554,27 @@ var CrossTabDataBus = class {
|
|
|
2505
2554
|
this.transportReady = false;
|
|
2506
2555
|
const chainedPendingStop = this.pendingStop;
|
|
2507
2556
|
const isCurrentLifecycle = () => lifecycleEpoch === this.lifecycleEpoch;
|
|
2557
|
+
let startupInProgress = true;
|
|
2508
2558
|
return before.catch(() => void 0).then(() => {
|
|
2509
2559
|
if (!isCurrentLifecycle() || this.stopping || this.suspended) return;
|
|
2510
2560
|
if (this.pendingStop === chainedPendingStop) this.pendingStop = null;
|
|
2561
|
+
this.transportHasConnected = false;
|
|
2511
2562
|
return Promise.resolve(
|
|
2512
2563
|
this.transport.start(config, {
|
|
2513
2564
|
onMessage: (message) => {
|
|
2514
2565
|
if (isCurrentLifecycle()) this.handleTransportMessage(message);
|
|
2515
2566
|
},
|
|
2516
2567
|
onStatus: (status) => {
|
|
2517
|
-
if (isCurrentLifecycle())
|
|
2568
|
+
if (isCurrentLifecycle()) {
|
|
2569
|
+
this.updateStatus(status, status !== WORKER_STATUS.ERROR || !startupInProgress);
|
|
2570
|
+
}
|
|
2518
2571
|
},
|
|
2519
2572
|
onError: (error) => {
|
|
2520
2573
|
if (isCurrentLifecycle()) this.reportError(error);
|
|
2521
2574
|
}
|
|
2522
2575
|
})
|
|
2523
2576
|
).then(() => {
|
|
2577
|
+
startupInProgress = false;
|
|
2524
2578
|
if (!isCurrentLifecycle()) return;
|
|
2525
2579
|
if (this.status === WORKER_STATUS.ERROR) {
|
|
2526
2580
|
throw new Error("Transport failed during startup.");
|
|
@@ -2529,24 +2583,26 @@ var CrossTabDataBus = class {
|
|
|
2529
2583
|
this.recoveryGeneration += 1;
|
|
2530
2584
|
this.lastSuccessAt = this.now();
|
|
2531
2585
|
this.transportReady = true;
|
|
2586
|
+
this.releaseRecoveryGate();
|
|
2532
2587
|
}
|
|
2533
2588
|
});
|
|
2534
2589
|
}).catch((error) => {
|
|
2535
2590
|
if (!isCurrentLifecycle()) throw error;
|
|
2591
|
+
startupInProgress = false;
|
|
2536
2592
|
if (stopClusterOnFailure) this.started = false;
|
|
2537
2593
|
if (!this.pendingStop) {
|
|
2538
2594
|
this.pendingStop = this.createStopPromise();
|
|
2539
2595
|
}
|
|
2540
|
-
this.updateStatus(WORKER_STATUS.ERROR);
|
|
2541
|
-
this.reportError(error);
|
|
2542
|
-
this.lastError = error;
|
|
2543
|
-
this.lastErrorAt = this.now();
|
|
2544
2596
|
this.transportReady = false;
|
|
2545
2597
|
if (stopClusterOnFailure) {
|
|
2546
2598
|
this.stopping = true;
|
|
2547
2599
|
this.cluster.stop();
|
|
2548
2600
|
this.stopping = false;
|
|
2549
2601
|
}
|
|
2602
|
+
this.recordError(error);
|
|
2603
|
+
this.startPromise = null;
|
|
2604
|
+
this.updateStatus(WORKER_STATUS.ERROR);
|
|
2605
|
+
this.notifyError(error);
|
|
2550
2606
|
throw error;
|
|
2551
2607
|
});
|
|
2552
2608
|
}
|
|
@@ -2555,7 +2611,10 @@ var CrossTabDataBus = class {
|
|
|
2555
2611
|
* Returns a rejected promise when the transport has failed and no start is in
|
|
2556
2612
|
* flight — the caller can retry by calling start() or ready() again. While an
|
|
2557
2613
|
* explicit stop() is settling, this rejects unless a restart is queued behind
|
|
2558
|
-
* it; false readiness during teardown is never reported.
|
|
2614
|
+
* it; false readiness during teardown is never reported. While the tab is
|
|
2615
|
+
* BFCache-suspended (pagehide without a following pageshow), this also
|
|
2616
|
+
* rejects: the suspended start promise is the transport-stop gate, not a
|
|
2617
|
+
* readiness signal.
|
|
2559
2618
|
*/
|
|
2560
2619
|
ready() {
|
|
2561
2620
|
if (this.queuedStart) return this.getQueuedStartReady();
|
|
@@ -2564,6 +2623,11 @@ var CrossTabDataBus = class {
|
|
|
2564
2623
|
"CrossTabDataBus is stopping; ready() cannot report readiness until stop() resolves. Wait for stop() to resolve, then call start() before awaiting ready()."
|
|
2565
2624
|
));
|
|
2566
2625
|
}
|
|
2626
|
+
if (this.suspended) {
|
|
2627
|
+
return Promise.reject(new Error(
|
|
2628
|
+
"CrossTabDataBus is suspended; ready() cannot report readiness until pageshow resumes the transport."
|
|
2629
|
+
));
|
|
2630
|
+
}
|
|
2567
2631
|
if (!this.started && !this.hasInitialConfig && this.lastError !== null) {
|
|
2568
2632
|
return Promise.reject(this.lastError);
|
|
2569
2633
|
}
|
|
@@ -2699,11 +2763,15 @@ var CrossTabDataBus = class {
|
|
|
2699
2763
|
getStatus() {
|
|
2700
2764
|
return this.status;
|
|
2701
2765
|
}
|
|
2702
|
-
/** Return the current automatic transport recovery state. `hasError` means a transport error is currently retained. */
|
|
2703
2766
|
/** Return the current automatic transport recovery state plus diagnostics.
|
|
2704
|
-
* `
|
|
2705
|
-
*
|
|
2706
|
-
*
|
|
2767
|
+
* `hasError`/`errorMessage`/`errorAt` describe the most recent retained
|
|
2768
|
+
* *transport* failure — from a transport open or a runtime `onError`. They
|
|
2769
|
+
* share the lifetime of the unified `lastFailure` ledger: a successful
|
|
2770
|
+
* recovery keeps the last failure visible, and only an explicit `start()`
|
|
2771
|
+
* clears it. `generation` increments on every successful transport open
|
|
2772
|
+
* (initial start and every recovery); `lastSuccessAt` is the timestamp of
|
|
2773
|
+
* the most recent successful open, or `null` until the transport reaches
|
|
2774
|
+
* `ready`. */
|
|
2707
2775
|
getRecoveryStats() {
|
|
2708
2776
|
const errorMessage = this.lastError instanceof Error ? this.lastError.message : this.lastError === null ? null : String(this.lastError);
|
|
2709
2777
|
return {
|
|
@@ -2730,7 +2798,7 @@ var CrossTabDataBus = class {
|
|
|
2730
2798
|
* unified failure ledger and recovery context that explains the verdict. */
|
|
2731
2799
|
getHealthSummary() {
|
|
2732
2800
|
const transport = this.transport;
|
|
2733
|
-
const transportDown =
|
|
2801
|
+
const transportDown = this.status !== WORKER_STATUS.CONNECTED;
|
|
2734
2802
|
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;
|
|
2735
2803
|
return {
|
|
2736
2804
|
healthy: state === HEALTH_STATE.HEALTHY,
|
|
@@ -2821,6 +2889,7 @@ var CrossTabDataBus = class {
|
|
|
2821
2889
|
async performStop() {
|
|
2822
2890
|
this.lifecycleEpoch += 1;
|
|
2823
2891
|
this.stopping = true;
|
|
2892
|
+
this.cancelScheduledRecovery();
|
|
2824
2893
|
this.replayManager.suspend();
|
|
2825
2894
|
this.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.STOP });
|
|
2826
2895
|
this.trace.stop();
|
|
@@ -2833,6 +2902,8 @@ var CrossTabDataBus = class {
|
|
|
2833
2902
|
const pendingStop = this.pendingStop;
|
|
2834
2903
|
if (pendingStop) await pendingStop.catch(() => void 0);
|
|
2835
2904
|
else await this.transport.stop();
|
|
2905
|
+
} catch (error) {
|
|
2906
|
+
this.reportError(error);
|
|
2836
2907
|
} finally {
|
|
2837
2908
|
this.transportSubscribedTopics.clear();
|
|
2838
2909
|
this.resetDedup();
|
|
@@ -2895,13 +2966,15 @@ var CrossTabDataBus = class {
|
|
|
2895
2966
|
* Propagate a status change to the cluster, trace, and all registered
|
|
2896
2967
|
* status handlers. On reconnect, re-subscribe any topics assigned to us.
|
|
2897
2968
|
*/
|
|
2898
|
-
updateStatus(status) {
|
|
2969
|
+
updateStatus(status, notifyHandlers = true) {
|
|
2899
2970
|
const previousStatus = this.status;
|
|
2900
2971
|
this.status = status;
|
|
2972
|
+
if (status === WORKER_STATUS.CONNECTED) this.transportHasConnected = true;
|
|
2901
2973
|
if (previousStatus !== status) this.trace.event({ type: TRACE_EVENT_TYPE.STATUS, status });
|
|
2902
2974
|
this.cluster.setStatus(status);
|
|
2903
2975
|
if (status === WORKER_STATUS.DISCONNECTED || status === WORKER_STATUS.ERROR) this.transportSubscribedTopics.clear();
|
|
2904
2976
|
if (status === WORKER_STATUS.CONNECTED && previousStatus !== WORKER_STATUS.CONNECTED) {
|
|
2977
|
+
if (this.transportReady) this.releaseRecoveryGate();
|
|
2905
2978
|
for (const topic of this.cluster.getSnapshot().assignedTopics) this.subscribeTransport(topic);
|
|
2906
2979
|
}
|
|
2907
2980
|
if (status === WORKER_STATUS.ERROR && this.started && !this.stopping) {
|
|
@@ -2914,23 +2987,49 @@ var CrossTabDataBus = class {
|
|
|
2914
2987
|
this.recoveryExhausted = true;
|
|
2915
2988
|
this.trace.event({ type: TRACE_EVENT_TYPE.RELIABILITY, operation: RELIABILITY_OPERATION.TRANSPORT_RECOVERY, attempt: this.recoveryMaxAttempts, outcome: RECOVERY_OUTCOME.EXHAUSTED });
|
|
2916
2989
|
}
|
|
2990
|
+
this.releaseRecoveryGate();
|
|
2917
2991
|
return;
|
|
2918
2992
|
}
|
|
2919
2993
|
this.trace.event({ type: TRACE_EVENT_TYPE.RELIABILITY, operation: RELIABILITY_OPERATION.TRANSPORT_RECOVERY, attempt, outcome: RECOVERY_OUTCOME.SCHEDULED });
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2994
|
+
if (this.recoveryGate === null) {
|
|
2995
|
+
let release;
|
|
2996
|
+
this.recoveryGate = new Promise((resolve) => {
|
|
2997
|
+
release = resolve;
|
|
2998
|
+
});
|
|
2999
|
+
this.recoveryGateRelease = release;
|
|
3000
|
+
}
|
|
3001
|
+
this.recoveryDemandAllowed = false;
|
|
3002
|
+
const timerToken = ++this.recoveryTimerToken;
|
|
3003
|
+
this.recoveryTimer = setTimeout(() => {
|
|
3004
|
+
if (timerToken !== this.recoveryTimerToken) return;
|
|
3005
|
+
this.recoveryTimer = null;
|
|
3006
|
+
if (this.stopping || !this.started || this.suspended || this.status !== WORKER_STATUS.ERROR) {
|
|
3007
|
+
this.releaseRecoveryGate();
|
|
3008
|
+
return;
|
|
3009
|
+
}
|
|
3010
|
+
this.recoveryDemandAllowed = false;
|
|
3011
|
+
const opening = this.reopenTransport(attempt);
|
|
3012
|
+
void opening.then(
|
|
3013
|
+
() => this.releaseRecoveryGate(),
|
|
3014
|
+
() => this.allowDemandRecovery()
|
|
3015
|
+
);
|
|
2924
3016
|
}, this.recoveryCooldownMs);
|
|
2925
3017
|
}
|
|
3018
|
+
} else if (status === WORKER_STATUS.ERROR) {
|
|
3019
|
+
this.releaseRecoveryGate();
|
|
2926
3020
|
}
|
|
2927
|
-
this.invokeHandlers(this.statusHandlers, (handler) => handler(status));
|
|
3021
|
+
if (notifyHandlers) this.invokeHandlers(this.statusHandlers, (handler) => handler(status));
|
|
2928
3022
|
}
|
|
2929
|
-
|
|
3023
|
+
recordError(error, source = FAILURE_SOURCE.TRANSPORT) {
|
|
3024
|
+
const at = this.now();
|
|
3025
|
+
if (source === FAILURE_SOURCE.TRANSPORT) {
|
|
3026
|
+
this.lastError = error;
|
|
3027
|
+
this.lastErrorAt = at;
|
|
3028
|
+
}
|
|
2930
3029
|
this.lastFailure = {
|
|
2931
3030
|
source,
|
|
2932
3031
|
message: error instanceof Error ? error.message : String(error),
|
|
2933
|
-
at
|
|
3032
|
+
at
|
|
2934
3033
|
};
|
|
2935
3034
|
if (source === FAILURE_SOURCE.PERSISTENCE) {
|
|
2936
3035
|
this.persistenceFailureCount += 1;
|
|
@@ -2941,8 +3040,14 @@ var CrossTabDataBus = class {
|
|
|
2941
3040
|
type: TRACE_EVENT_TYPE.ERROR,
|
|
2942
3041
|
source: source === FAILURE_SOURCE.TRANSPORT ? TRACE_ERROR_SOURCE.TRANSPORT : TRACE_ERROR_SOURCE.OPERATION
|
|
2943
3042
|
});
|
|
3043
|
+
}
|
|
3044
|
+
notifyError(error) {
|
|
2944
3045
|
this.invokeHandlers(this.errorHandlers, (handler) => handler(error), INVOKE_LABEL.ERROR_HANDLER);
|
|
2945
3046
|
}
|
|
3047
|
+
reportError(error, source = FAILURE_SOURCE.TRANSPORT) {
|
|
3048
|
+
this.recordError(error, source);
|
|
3049
|
+
this.notifyError(error);
|
|
3050
|
+
}
|
|
2946
3051
|
/** Report a persistence failure to the trace and the unified failure ledger,
|
|
2947
3052
|
* unless it is a {@link PersistenceRetryCancelledError} cancellation from a
|
|
2948
3053
|
* lifecycle transition (teardown should stay quiet). */
|
|
@@ -3005,6 +3110,15 @@ var CrossTabDataBus = class {
|
|
|
3005
3110
|
}
|
|
3006
3111
|
}
|
|
3007
3112
|
}
|
|
3113
|
+
/** Resume the resources paused by a pagehide suspension. Both the native
|
|
3114
|
+
* pageshow path and explicit start() must run this so an explicit resume
|
|
3115
|
+
* cannot leave trace metrics and periodic cleanup timers permanently off. */
|
|
3116
|
+
resumeSuspendedResources() {
|
|
3117
|
+
this.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.RESUME });
|
|
3118
|
+
this.trace.start();
|
|
3119
|
+
this.startDedupSweep();
|
|
3120
|
+
this.replayManager.start();
|
|
3121
|
+
}
|
|
3008
3122
|
/**
|
|
3009
3123
|
* Suspend the transport when the tab goes hidden. Stops the transport and
|
|
3010
3124
|
* clears subscription state so it will be re-established on resume.
|
|
@@ -3013,11 +3127,15 @@ var CrossTabDataBus = class {
|
|
|
3013
3127
|
if (this.stopping) return;
|
|
3014
3128
|
this.lifecycleEpoch += 1;
|
|
3015
3129
|
this.suspended = true;
|
|
3130
|
+
this.cancelScheduledRecovery();
|
|
3016
3131
|
this.transportReady = false;
|
|
3017
3132
|
this.transportSubscribedTopics.clear();
|
|
3018
3133
|
this.updateStatus(WORKER_STATUS.DISCONNECTED);
|
|
3019
|
-
if (this.pendingStop)
|
|
3020
|
-
|
|
3134
|
+
if (this.pendingStop && (this.startPromise === null || this.startPromise === this.pendingStop)) {
|
|
3135
|
+
this.startPromise = this.pendingStop;
|
|
3136
|
+
return;
|
|
3137
|
+
}
|
|
3138
|
+
const pending = this.startPromise ?? this.pendingStop ?? Promise.resolve();
|
|
3021
3139
|
const stopping = pending.catch(() => void 0).then(() => this.transport.stop()).catch((error) => this.reportError(error));
|
|
3022
3140
|
this.startPromise = stopping;
|
|
3023
3141
|
this.pendingStop = stopping;
|
|
@@ -3082,7 +3200,24 @@ var CrossTabDataBus = class {
|
|
|
3082
3200
|
*/
|
|
3083
3201
|
runTransport(operation) {
|
|
3084
3202
|
if (this.suspended) return;
|
|
3085
|
-
if (this.
|
|
3203
|
+
if (this.recoveryGate && !this.stopping) {
|
|
3204
|
+
if (this.recoveryDemandAllowed && this.status === WORKER_STATUS.ERROR && !this.suspended) {
|
|
3205
|
+
this.recoveryDemandAllowed = false;
|
|
3206
|
+
const opening = this.reopenTransport();
|
|
3207
|
+
void opening.then(
|
|
3208
|
+
() => this.releaseRecoveryGate(),
|
|
3209
|
+
() => this.allowDemandRecovery()
|
|
3210
|
+
);
|
|
3211
|
+
}
|
|
3212
|
+
const gate = this.recoveryGate;
|
|
3213
|
+
void gate.then(() => {
|
|
3214
|
+
if (this.stopping || this.suspended) return;
|
|
3215
|
+
this.runTransport(operation);
|
|
3216
|
+
});
|
|
3217
|
+
return;
|
|
3218
|
+
}
|
|
3219
|
+
const droppedAfterConnect = this.transportHasConnected && this.status === WORKER_STATUS.DISCONNECTED;
|
|
3220
|
+
if (this.transportReady && this.status !== WORKER_STATUS.ERROR && !droppedAfterConnect && !this.stopping) {
|
|
3086
3221
|
try {
|
|
3087
3222
|
void Promise.resolve(operation()).catch((error) => this.reportError(error));
|
|
3088
3223
|
} catch (error) {
|
|
@@ -3095,10 +3230,17 @@ var CrossTabDataBus = class {
|
|
|
3095
3230
|
ready = this.reopenTransport();
|
|
3096
3231
|
}
|
|
3097
3232
|
if (!ready || this.stopping) return;
|
|
3098
|
-
void ready.then(
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3233
|
+
void ready.then(
|
|
3234
|
+
() => {
|
|
3235
|
+
if (!this.started || this.stopping || this.suspended) return;
|
|
3236
|
+
return operation();
|
|
3237
|
+
},
|
|
3238
|
+
// The opening promise reports its own lifecycle failure through
|
|
3239
|
+
// openTransport(). Swallowing it here prevents a stale startup
|
|
3240
|
+
// rejection from being recorded again after an onStatus/onError
|
|
3241
|
+
// callback has already started and reset the ledger for a retry.
|
|
3242
|
+
() => void 0
|
|
3243
|
+
).catch((error) => this.reportError(error));
|
|
3102
3244
|
}
|
|
3103
3245
|
/**
|
|
3104
3246
|
* Publications started after teardown begins cannot reach any transport.
|
|
@@ -3193,4 +3335,4 @@ export {
|
|
|
3193
3335
|
parseDataBusPublication,
|
|
3194
3336
|
selectWorkerBackend
|
|
3195
3337
|
};
|
|
3196
|
-
//# sourceMappingURL=chunk-
|
|
3338
|
+
//# sourceMappingURL=chunk-4UOLOWOD.js.map
|