cross-tab-worker-databus 0.20.87 → 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 +9 -0
- package/dist/centrifuge.js +1 -1
- package/dist/{chunk-ZNHJ5OMY.js → chunk-4UOLOWOD.js} +52 -19
- package/dist/{chunk-ZNHJ5OMY.js.map → chunk-4UOLOWOD.js.map} +3 -3
- package/dist/cjs/centrifuge.cjs +51 -18
- package/dist/cjs/centrifuge.cjs.map +3 -3
- package/dist/cjs/index.cjs +51 -18
- package/dist/cjs/index.cjs.map +3 -3
- package/dist/core/data-bus.d.ts +6 -0
- package/dist/core/data-bus.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/docs/api.md +2 -2
- package/docs/architecture.md +4 -3
- package/docs/roadmap.md +7 -1
- package/docs/zh/api.md +2 -2
- package/docs/zh/architecture.md +4 -3
- package/docs/zh/roadmap.md +7 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
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
|
+
|
|
3
12
|
## [0.20.87] - 2026-09-16
|
|
4
13
|
|
|
5
14
|
### Fixed
|
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;
|
|
@@ -2397,10 +2397,7 @@ var CrossTabDataBus = class {
|
|
|
2397
2397
|
this.suspendTransport();
|
|
2398
2398
|
},
|
|
2399
2399
|
onResume: () => {
|
|
2400
|
-
this.
|
|
2401
|
-
this.trace.start();
|
|
2402
|
-
this.startDedupSweep();
|
|
2403
|
-
this.replayManager.start();
|
|
2400
|
+
this.resumeSuspendedResources();
|
|
2404
2401
|
this.resumeTransport();
|
|
2405
2402
|
},
|
|
2406
2403
|
onDiagnostic: (event) => {
|
|
@@ -2430,7 +2427,11 @@ var CrossTabDataBus = class {
|
|
|
2430
2427
|
if (!transportDown) return Promise.resolve();
|
|
2431
2428
|
this.activeConfig = config;
|
|
2432
2429
|
this.resetFailureState();
|
|
2433
|
-
|
|
2430
|
+
const resumingFromSuspend = this.suspended;
|
|
2431
|
+
if (resumingFromSuspend) this.resumeSuspendedResources();
|
|
2432
|
+
const opening2 = this.reopenTransport();
|
|
2433
|
+
if (resumingFromSuspend) this.cluster.start();
|
|
2434
|
+
return opening2;
|
|
2434
2435
|
}
|
|
2435
2436
|
this.started = true;
|
|
2436
2437
|
this.stopping = false;
|
|
@@ -2553,6 +2554,7 @@ var CrossTabDataBus = class {
|
|
|
2553
2554
|
this.transportReady = false;
|
|
2554
2555
|
const chainedPendingStop = this.pendingStop;
|
|
2555
2556
|
const isCurrentLifecycle = () => lifecycleEpoch === this.lifecycleEpoch;
|
|
2557
|
+
let startupInProgress = true;
|
|
2556
2558
|
return before.catch(() => void 0).then(() => {
|
|
2557
2559
|
if (!isCurrentLifecycle() || this.stopping || this.suspended) return;
|
|
2558
2560
|
if (this.pendingStop === chainedPendingStop) this.pendingStop = null;
|
|
@@ -2563,13 +2565,16 @@ var CrossTabDataBus = class {
|
|
|
2563
2565
|
if (isCurrentLifecycle()) this.handleTransportMessage(message);
|
|
2564
2566
|
},
|
|
2565
2567
|
onStatus: (status) => {
|
|
2566
|
-
if (isCurrentLifecycle())
|
|
2568
|
+
if (isCurrentLifecycle()) {
|
|
2569
|
+
this.updateStatus(status, status !== WORKER_STATUS.ERROR || !startupInProgress);
|
|
2570
|
+
}
|
|
2567
2571
|
},
|
|
2568
2572
|
onError: (error) => {
|
|
2569
2573
|
if (isCurrentLifecycle()) this.reportError(error);
|
|
2570
2574
|
}
|
|
2571
2575
|
})
|
|
2572
2576
|
).then(() => {
|
|
2577
|
+
startupInProgress = false;
|
|
2573
2578
|
if (!isCurrentLifecycle()) return;
|
|
2574
2579
|
if (this.status === WORKER_STATUS.ERROR) {
|
|
2575
2580
|
throw new Error("Transport failed during startup.");
|
|
@@ -2583,18 +2588,21 @@ var CrossTabDataBus = class {
|
|
|
2583
2588
|
});
|
|
2584
2589
|
}).catch((error) => {
|
|
2585
2590
|
if (!isCurrentLifecycle()) throw error;
|
|
2591
|
+
startupInProgress = false;
|
|
2586
2592
|
if (stopClusterOnFailure) this.started = false;
|
|
2587
2593
|
if (!this.pendingStop) {
|
|
2588
2594
|
this.pendingStop = this.createStopPromise();
|
|
2589
2595
|
}
|
|
2590
2596
|
this.transportReady = false;
|
|
2591
|
-
this.updateStatus(WORKER_STATUS.ERROR);
|
|
2592
|
-
this.reportError(error);
|
|
2593
2597
|
if (stopClusterOnFailure) {
|
|
2594
2598
|
this.stopping = true;
|
|
2595
2599
|
this.cluster.stop();
|
|
2596
2600
|
this.stopping = false;
|
|
2597
2601
|
}
|
|
2602
|
+
this.recordError(error);
|
|
2603
|
+
this.startPromise = null;
|
|
2604
|
+
this.updateStatus(WORKER_STATUS.ERROR);
|
|
2605
|
+
this.notifyError(error);
|
|
2598
2606
|
throw error;
|
|
2599
2607
|
});
|
|
2600
2608
|
}
|
|
@@ -2958,7 +2966,7 @@ var CrossTabDataBus = class {
|
|
|
2958
2966
|
* Propagate a status change to the cluster, trace, and all registered
|
|
2959
2967
|
* status handlers. On reconnect, re-subscribe any topics assigned to us.
|
|
2960
2968
|
*/
|
|
2961
|
-
updateStatus(status) {
|
|
2969
|
+
updateStatus(status, notifyHandlers = true) {
|
|
2962
2970
|
const previousStatus = this.status;
|
|
2963
2971
|
this.status = status;
|
|
2964
2972
|
if (status === WORKER_STATUS.CONNECTED) this.transportHasConnected = true;
|
|
@@ -3010,9 +3018,9 @@ var CrossTabDataBus = class {
|
|
|
3010
3018
|
} else if (status === WORKER_STATUS.ERROR) {
|
|
3011
3019
|
this.releaseRecoveryGate();
|
|
3012
3020
|
}
|
|
3013
|
-
this.invokeHandlers(this.statusHandlers, (handler) => handler(status));
|
|
3021
|
+
if (notifyHandlers) this.invokeHandlers(this.statusHandlers, (handler) => handler(status));
|
|
3014
3022
|
}
|
|
3015
|
-
|
|
3023
|
+
recordError(error, source = FAILURE_SOURCE.TRANSPORT) {
|
|
3016
3024
|
const at = this.now();
|
|
3017
3025
|
if (source === FAILURE_SOURCE.TRANSPORT) {
|
|
3018
3026
|
this.lastError = error;
|
|
@@ -3032,8 +3040,14 @@ var CrossTabDataBus = class {
|
|
|
3032
3040
|
type: TRACE_EVENT_TYPE.ERROR,
|
|
3033
3041
|
source: source === FAILURE_SOURCE.TRANSPORT ? TRACE_ERROR_SOURCE.TRANSPORT : TRACE_ERROR_SOURCE.OPERATION
|
|
3034
3042
|
});
|
|
3043
|
+
}
|
|
3044
|
+
notifyError(error) {
|
|
3035
3045
|
this.invokeHandlers(this.errorHandlers, (handler) => handler(error), INVOKE_LABEL.ERROR_HANDLER);
|
|
3036
3046
|
}
|
|
3047
|
+
reportError(error, source = FAILURE_SOURCE.TRANSPORT) {
|
|
3048
|
+
this.recordError(error, source);
|
|
3049
|
+
this.notifyError(error);
|
|
3050
|
+
}
|
|
3037
3051
|
/** Report a persistence failure to the trace and the unified failure ledger,
|
|
3038
3052
|
* unless it is a {@link PersistenceRetryCancelledError} cancellation from a
|
|
3039
3053
|
* lifecycle transition (teardown should stay quiet). */
|
|
@@ -3096,6 +3110,15 @@ var CrossTabDataBus = class {
|
|
|
3096
3110
|
}
|
|
3097
3111
|
}
|
|
3098
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
|
+
}
|
|
3099
3122
|
/**
|
|
3100
3123
|
* Suspend the transport when the tab goes hidden. Stops the transport and
|
|
3101
3124
|
* clears subscription state so it will be re-established on resume.
|
|
@@ -3108,8 +3131,11 @@ var CrossTabDataBus = class {
|
|
|
3108
3131
|
this.transportReady = false;
|
|
3109
3132
|
this.transportSubscribedTopics.clear();
|
|
3110
3133
|
this.updateStatus(WORKER_STATUS.DISCONNECTED);
|
|
3111
|
-
if (this.pendingStop)
|
|
3112
|
-
|
|
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();
|
|
3113
3139
|
const stopping = pending.catch(() => void 0).then(() => this.transport.stop()).catch((error) => this.reportError(error));
|
|
3114
3140
|
this.startPromise = stopping;
|
|
3115
3141
|
this.pendingStop = stopping;
|
|
@@ -3204,10 +3230,17 @@ var CrossTabDataBus = class {
|
|
|
3204
3230
|
ready = this.reopenTransport();
|
|
3205
3231
|
}
|
|
3206
3232
|
if (!ready || this.stopping) return;
|
|
3207
|
-
void ready.then(
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
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));
|
|
3211
3244
|
}
|
|
3212
3245
|
/**
|
|
3213
3246
|
* Publications started after teardown begins cannot reach any transport.
|
|
@@ -3302,4 +3335,4 @@ export {
|
|
|
3302
3335
|
parseDataBusPublication,
|
|
3303
3336
|
selectWorkerBackend
|
|
3304
3337
|
};
|
|
3305
|
-
//# sourceMappingURL=chunk-
|
|
3338
|
+
//# sourceMappingURL=chunk-4UOLOWOD.js.map
|