cross-tab-worker-databus 0.20.86 → 0.20.87

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.
@@ -2331,7 +2331,7 @@ var DedupManager = class {
2331
2331
  };
2332
2332
 
2333
2333
  // src/core/version.ts
2334
- var SDK_VERSION = true ? "0.20.86" : "";
2334
+ var SDK_VERSION = true ? "0.20.87" : "";
2335
2335
 
2336
2336
  // src/core/data-bus.ts
2337
2337
  var DEFAULT_REPLAY_MAX_PER_TOPIC = 100;
@@ -2356,6 +2356,11 @@ var CrossTabDataBus = class {
2356
2356
  started = false;
2357
2357
  stopping = false;
2358
2358
  transportReady = false;
2359
+ // Whether the installed transport has reported `connected` at least once
2360
+ // since the current open began. A clean `disconnected` after this point is
2361
+ // a lost working connection, not the pre-connect window of a worker-style
2362
+ // backend whose start() resolves before it reports the connection.
2363
+ transportHasConnected = false;
2359
2364
  // Last transport failure, retained so ready() can surface it to callers who
2360
2365
  // never awaited start() directly. Cleared on the next successful start.
2361
2366
  lastError = null;
@@ -2394,6 +2399,19 @@ var CrossTabDataBus = class {
2394
2399
  // a transport reopen succeeds so traces can correlate repeated failures.
2395
2400
  recoveryAttempt = 0;
2396
2401
  recoveryExhausted = false;
2402
+ // Gate that holds transport operations issued after a runtime `error` until
2403
+ // the scheduled recovery attempt has actually run. Without it, a dead
2404
+ // transport still has `transportReady === true` during the cooldown, so
2405
+ // publishes/subscribes would be written to the failed connection and lost.
2406
+ recoveryGate = null;
2407
+ recoveryGateRelease = null;
2408
+ recoveryTimer = null;
2409
+ recoveryTimerToken = 0;
2410
+ // Once an automatic attempt fails, an explicit transport operation may
2411
+ // recover immediately instead of waiting for the next paced attempt. The
2412
+ // gate still stays closed so the operation cannot reach the failed
2413
+ // transport; it is released by the successful on-demand reopen.
2414
+ recoveryDemandAllowed = false;
2397
2415
  /** Monotonic generation incremented on every successful transport open.
2398
2416
  * Stays in lockstep with `lastSuccessAt` so callers can detect that the
2399
2417
  * transport has been reopened even if the timestamp window is short. */
@@ -2612,8 +2630,38 @@ var CrossTabDataBus = class {
2612
2630
  this.queuedStart = queued;
2613
2631
  return queued;
2614
2632
  }
2633
+ /** Release every operation waiting on the scheduled recovery attempt. */
2634
+ releaseRecoveryGate() {
2635
+ const release = this.recoveryGateRelease;
2636
+ this.recoveryGate = null;
2637
+ this.recoveryGateRelease = null;
2638
+ this.recoveryDemandAllowed = false;
2639
+ release?.();
2640
+ }
2641
+ /** Cancel a pending automatic retry when an explicit lifecycle transition
2642
+ * supersedes it. The released gate re-enters runTransport(), which then
2643
+ * follows the newest start/stop/suspend intent. */
2644
+ cancelScheduledRecovery() {
2645
+ this.recoveryTimerToken += 1;
2646
+ if (this.recoveryTimer !== null) {
2647
+ clearTimeout(this.recoveryTimer);
2648
+ this.recoveryTimer = null;
2649
+ }
2650
+ this.releaseRecoveryGate();
2651
+ }
2652
+ /** Keep the recovery gate closed after a failed attempt while allowing the
2653
+ * next explicit transport operation to start an immediate on-demand reopen.
2654
+ * If no gate/successor retry remains, release any waiters. */
2655
+ allowDemandRecovery() {
2656
+ if (this.recoveryGate !== null && this.started && !this.stopping && !this.suspended && this.status === WORKER_STATUS.ERROR) {
2657
+ this.recoveryDemandAllowed = true;
2658
+ return;
2659
+ }
2660
+ this.releaseRecoveryGate();
2661
+ }
2615
2662
  /** Reset failure and recovery diagnostics for a new explicit start session. */
2616
2663
  resetFailureState() {
2664
+ this.cancelScheduledRecovery();
2617
2665
  this.lastError = null;
2618
2666
  this.lastErrorAt = null;
2619
2667
  this.lastFailure = null;
@@ -2636,6 +2684,7 @@ var CrossTabDataBus = class {
2636
2684
  return before.catch(() => void 0).then(() => {
2637
2685
  if (!isCurrentLifecycle() || this.stopping || this.suspended) return;
2638
2686
  if (this.pendingStop === chainedPendingStop) this.pendingStop = null;
2687
+ this.transportHasConnected = false;
2639
2688
  return Promise.resolve(
2640
2689
  this.transport.start(config, {
2641
2690
  onMessage: (message) => {
@@ -2657,6 +2706,7 @@ var CrossTabDataBus = class {
2657
2706
  this.recoveryGeneration += 1;
2658
2707
  this.lastSuccessAt = this.now();
2659
2708
  this.transportReady = true;
2709
+ this.releaseRecoveryGate();
2660
2710
  }
2661
2711
  });
2662
2712
  }).catch((error) => {
@@ -2665,11 +2715,9 @@ var CrossTabDataBus = class {
2665
2715
  if (!this.pendingStop) {
2666
2716
  this.pendingStop = this.createStopPromise();
2667
2717
  }
2718
+ this.transportReady = false;
2668
2719
  this.updateStatus(WORKER_STATUS.ERROR);
2669
2720
  this.reportError(error);
2670
- this.lastError = error;
2671
- this.lastErrorAt = this.now();
2672
- this.transportReady = false;
2673
2721
  if (stopClusterOnFailure) {
2674
2722
  this.stopping = true;
2675
2723
  this.cluster.stop();
@@ -2683,7 +2731,10 @@ var CrossTabDataBus = class {
2683
2731
  * Returns a rejected promise when the transport has failed and no start is in
2684
2732
  * flight — the caller can retry by calling start() or ready() again. While an
2685
2733
  * explicit stop() is settling, this rejects unless a restart is queued behind
2686
- * it; false readiness during teardown is never reported.
2734
+ * it; false readiness during teardown is never reported. While the tab is
2735
+ * BFCache-suspended (pagehide without a following pageshow), this also
2736
+ * rejects: the suspended start promise is the transport-stop gate, not a
2737
+ * readiness signal.
2687
2738
  */
2688
2739
  ready() {
2689
2740
  if (this.queuedStart) return this.getQueuedStartReady();
@@ -2692,6 +2743,11 @@ var CrossTabDataBus = class {
2692
2743
  "CrossTabDataBus is stopping; ready() cannot report readiness until stop() resolves. Wait for stop() to resolve, then call start() before awaiting ready()."
2693
2744
  ));
2694
2745
  }
2746
+ if (this.suspended) {
2747
+ return Promise.reject(new Error(
2748
+ "CrossTabDataBus is suspended; ready() cannot report readiness until pageshow resumes the transport."
2749
+ ));
2750
+ }
2695
2751
  if (!this.started && !this.hasInitialConfig && this.lastError !== null) {
2696
2752
  return Promise.reject(this.lastError);
2697
2753
  }
@@ -2827,11 +2883,15 @@ var CrossTabDataBus = class {
2827
2883
  getStatus() {
2828
2884
  return this.status;
2829
2885
  }
2830
- /** Return the current automatic transport recovery state. `hasError` means a transport error is currently retained. */
2831
2886
  /** Return the current automatic transport recovery state plus diagnostics.
2832
- * `generation` increments on every successful transport open (initial start
2833
- * and every recovery); `lastSuccessAt` is the timestamp of the most recent
2834
- * successful open, or `null` until the transport reaches `ready`. */
2887
+ * `hasError`/`errorMessage`/`errorAt` describe the most recent retained
2888
+ * *transport* failure from a transport open or a runtime `onError`. They
2889
+ * share the lifetime of the unified `lastFailure` ledger: a successful
2890
+ * recovery keeps the last failure visible, and only an explicit `start()`
2891
+ * clears it. `generation` increments on every successful transport open
2892
+ * (initial start and every recovery); `lastSuccessAt` is the timestamp of
2893
+ * the most recent successful open, or `null` until the transport reaches
2894
+ * `ready`. */
2835
2895
  getRecoveryStats() {
2836
2896
  const errorMessage = this.lastError instanceof Error ? this.lastError.message : this.lastError === null ? null : String(this.lastError);
2837
2897
  return {
@@ -2858,7 +2918,7 @@ var CrossTabDataBus = class {
2858
2918
  * unified failure ledger and recovery context that explains the verdict. */
2859
2919
  getHealthSummary() {
2860
2920
  const transport = this.transport;
2861
- const transportDown = !this.transportReady || this.status === WORKER_STATUS.ERROR || this.status === WORKER_STATUS.DISCONNECTED;
2921
+ const transportDown = this.status !== WORKER_STATUS.CONNECTED;
2862
2922
  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;
2863
2923
  return {
2864
2924
  healthy: state === HEALTH_STATE.HEALTHY,
@@ -2949,6 +3009,7 @@ var CrossTabDataBus = class {
2949
3009
  async performStop() {
2950
3010
  this.lifecycleEpoch += 1;
2951
3011
  this.stopping = true;
3012
+ this.cancelScheduledRecovery();
2952
3013
  this.replayManager.suspend();
2953
3014
  this.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.STOP });
2954
3015
  this.trace.stop();
@@ -2961,6 +3022,8 @@ var CrossTabDataBus = class {
2961
3022
  const pendingStop = this.pendingStop;
2962
3023
  if (pendingStop) await pendingStop.catch(() => void 0);
2963
3024
  else await this.transport.stop();
3025
+ } catch (error) {
3026
+ this.reportError(error);
2964
3027
  } finally {
2965
3028
  this.transportSubscribedTopics.clear();
2966
3029
  this.resetDedup();
@@ -3026,10 +3089,12 @@ var CrossTabDataBus = class {
3026
3089
  updateStatus(status) {
3027
3090
  const previousStatus = this.status;
3028
3091
  this.status = status;
3092
+ if (status === WORKER_STATUS.CONNECTED) this.transportHasConnected = true;
3029
3093
  if (previousStatus !== status) this.trace.event({ type: TRACE_EVENT_TYPE.STATUS, status });
3030
3094
  this.cluster.setStatus(status);
3031
3095
  if (status === WORKER_STATUS.DISCONNECTED || status === WORKER_STATUS.ERROR) this.transportSubscribedTopics.clear();
3032
3096
  if (status === WORKER_STATUS.CONNECTED && previousStatus !== WORKER_STATUS.CONNECTED) {
3097
+ if (this.transportReady) this.releaseRecoveryGate();
3033
3098
  for (const topic of this.cluster.getSnapshot().assignedTopics) this.subscribeTransport(topic);
3034
3099
  }
3035
3100
  if (status === WORKER_STATUS.ERROR && this.started && !this.stopping) {
@@ -3042,23 +3107,49 @@ var CrossTabDataBus = class {
3042
3107
  this.recoveryExhausted = true;
3043
3108
  this.trace.event({ type: TRACE_EVENT_TYPE.RELIABILITY, operation: RELIABILITY_OPERATION.TRANSPORT_RECOVERY, attempt: this.recoveryMaxAttempts, outcome: RECOVERY_OUTCOME.EXHAUSTED });
3044
3109
  }
3110
+ this.releaseRecoveryGate();
3045
3111
  return;
3046
3112
  }
3047
3113
  this.trace.event({ type: TRACE_EVENT_TYPE.RELIABILITY, operation: RELIABILITY_OPERATION.TRANSPORT_RECOVERY, attempt, outcome: RECOVERY_OUTCOME.SCHEDULED });
3048
- setTimeout(() => {
3049
- if (this.stopping || !this.started || this.suspended) return;
3050
- if (this.status !== WORKER_STATUS.ERROR) return;
3051
- void this.reopenTransport(attempt);
3114
+ if (this.recoveryGate === null) {
3115
+ let release;
3116
+ this.recoveryGate = new Promise((resolve) => {
3117
+ release = resolve;
3118
+ });
3119
+ this.recoveryGateRelease = release;
3120
+ }
3121
+ this.recoveryDemandAllowed = false;
3122
+ const timerToken = ++this.recoveryTimerToken;
3123
+ this.recoveryTimer = setTimeout(() => {
3124
+ if (timerToken !== this.recoveryTimerToken) return;
3125
+ this.recoveryTimer = null;
3126
+ if (this.stopping || !this.started || this.suspended || this.status !== WORKER_STATUS.ERROR) {
3127
+ this.releaseRecoveryGate();
3128
+ return;
3129
+ }
3130
+ this.recoveryDemandAllowed = false;
3131
+ const opening = this.reopenTransport(attempt);
3132
+ void opening.then(
3133
+ () => this.releaseRecoveryGate(),
3134
+ () => this.allowDemandRecovery()
3135
+ );
3052
3136
  }, this.recoveryCooldownMs);
3053
3137
  }
3138
+ } else if (status === WORKER_STATUS.ERROR) {
3139
+ this.releaseRecoveryGate();
3054
3140
  }
3055
3141
  this.invokeHandlers(this.statusHandlers, (handler) => handler(status));
3056
3142
  }
3057
3143
  reportError(error, source = FAILURE_SOURCE.TRANSPORT) {
3144
+ const at = this.now();
3145
+ if (source === FAILURE_SOURCE.TRANSPORT) {
3146
+ this.lastError = error;
3147
+ this.lastErrorAt = at;
3148
+ }
3058
3149
  this.lastFailure = {
3059
3150
  source,
3060
3151
  message: error instanceof Error ? error.message : String(error),
3061
- at: this.now()
3152
+ at
3062
3153
  };
3063
3154
  if (source === FAILURE_SOURCE.PERSISTENCE) {
3064
3155
  this.persistenceFailureCount += 1;
@@ -3141,6 +3232,7 @@ var CrossTabDataBus = class {
3141
3232
  if (this.stopping) return;
3142
3233
  this.lifecycleEpoch += 1;
3143
3234
  this.suspended = true;
3235
+ this.cancelScheduledRecovery();
3144
3236
  this.transportReady = false;
3145
3237
  this.transportSubscribedTopics.clear();
3146
3238
  this.updateStatus(WORKER_STATUS.DISCONNECTED);
@@ -3210,7 +3302,24 @@ var CrossTabDataBus = class {
3210
3302
  */
3211
3303
  runTransport(operation) {
3212
3304
  if (this.suspended) return;
3213
- if (this.transportReady && !this.stopping) {
3305
+ if (this.recoveryGate && !this.stopping) {
3306
+ if (this.recoveryDemandAllowed && this.status === WORKER_STATUS.ERROR && !this.suspended) {
3307
+ this.recoveryDemandAllowed = false;
3308
+ const opening = this.reopenTransport();
3309
+ void opening.then(
3310
+ () => this.releaseRecoveryGate(),
3311
+ () => this.allowDemandRecovery()
3312
+ );
3313
+ }
3314
+ const gate = this.recoveryGate;
3315
+ void gate.then(() => {
3316
+ if (this.stopping || this.suspended) return;
3317
+ this.runTransport(operation);
3318
+ });
3319
+ return;
3320
+ }
3321
+ const droppedAfterConnect = this.transportHasConnected && this.status === WORKER_STATUS.DISCONNECTED;
3322
+ if (this.transportReady && this.status !== WORKER_STATUS.ERROR && !droppedAfterConnect && !this.stopping) {
3214
3323
  try {
3215
3324
  void Promise.resolve(operation()).catch((error) => this.reportError(error));
3216
3325
  } catch (error) {