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 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
@@ -5,7 +5,7 @@ import {
5
5
  parseDataBusPublication,
6
6
  publicationMetadata,
7
7
  selectWorkerBackend
8
- } from "./chunk-ZNHJ5OMY.js";
8
+ } from "./chunk-4UOLOWOD.js";
9
9
  import {
10
10
  CENTRIFUGE_INPUT_TYPE,
11
11
  CENTRIFUGE_OUTPUT_TYPE,
@@ -2203,7 +2203,7 @@ var DedupManager = class {
2203
2203
  };
2204
2204
 
2205
2205
  // src/core/version.ts
2206
- var SDK_VERSION = true ? "0.20.87" : "";
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.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.RESUME });
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
- return this.reopenTransport();
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()) this.updateStatus(status);
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
- reportError(error, source = FAILURE_SOURCE.TRANSPORT) {
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) return;
3112
- const pending = this.startPromise ?? Promise.resolve();
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
- if (!this.started || this.stopping || this.suspended) return;
3209
- return operation();
3210
- }).catch((error) => this.reportError(error));
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-ZNHJ5OMY.js.map
3338
+ //# sourceMappingURL=chunk-4UOLOWOD.js.map