cross-tab-worker-databus 0.20.87 → 0.20.89

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.
@@ -1659,8 +1659,9 @@ var DataBusTraceReporter = class {
1659
1659
  /** Start the periodic metrics flush interval. No-op when mode is 'events'
1660
1660
  * (no metrics to emit), when disabled, or when already running. */
1661
1661
  start() {
1662
- if (!this.enabled || this.intervalHandle || this.mode === TRACE_MODE.EVENTS) return;
1662
+ if (!this.enabled) return;
1663
1663
  this.stopped = false;
1664
+ if (this.intervalHandle || this.mode === TRACE_MODE.EVENTS) return;
1664
1665
  this.intervalStartedAt = this.now();
1665
1666
  this.intervalHandle = setInterval(() => this.flush(), this.metricsIntervalMs);
1666
1667
  }
@@ -1673,6 +1674,7 @@ var DataBusTraceReporter = class {
1673
1674
  }
1674
1675
  stop() {
1675
1676
  this.stopped = true;
1677
+ this.pendingEvents = [];
1676
1678
  this.pause();
1677
1679
  }
1678
1680
  /** Synchronous sink state for diagnostics: whether delivery is async and how
@@ -1683,7 +1685,7 @@ var DataBusTraceReporter = class {
1683
1685
  }
1684
1686
  /** Record an instantaneous trace event (lifecycle, status, error, etc.). */
1685
1687
  event(event) {
1686
- if (!this.enabled || this.mode === TRACE_MODE.METRICS) return;
1688
+ if (!this.enabled || this.stopped || this.mode === TRACE_MODE.METRICS) return;
1687
1689
  this.emit({ ...event, timestamp: this.now() });
1688
1690
  }
1689
1691
  /** Synchronous snapshot of the current metrics window without resetting it.
@@ -1766,7 +1768,7 @@ var DataBusTraceReporter = class {
1766
1768
  * Extracted so the four record / flush methods share one guard expression
1767
1769
  * instead of repeating `!this.enabled || this.mode === 'events'` at each. */
1768
1770
  get metricsActive() {
1769
- return this.enabled && this.mode !== TRACE_MODE.EVENTS;
1771
+ return this.enabled && !this.stopped && this.mode !== TRACE_MODE.EVENTS;
1770
1772
  }
1771
1773
  /** Emit the accumulated metrics snapshot if the interval is active. */
1772
1774
  flush() {
@@ -2068,6 +2070,8 @@ var ReplayManager = class {
2068
2070
  * or stopped bus does not keep hammering the store) and stop the sweep. */
2069
2071
  suspend() {
2070
2072
  this.retryGeneration += 1;
2073
+ this.pendingReplayPersistence = [];
2074
+ this.retentionCutoff = null;
2071
2075
  this.stop();
2072
2076
  }
2073
2077
  /** Drop all in-memory buffers (used on full teardown). */
@@ -2166,19 +2170,20 @@ var ReplayManager = class {
2166
2170
  this.retentionCutoff = cutoff;
2167
2171
  }
2168
2172
  if (this.retentionCleanup) return;
2173
+ const generation = this.retryGeneration;
2169
2174
  this.retentionCleanup = (async () => {
2170
- while (this.retentionCutoff !== null) {
2175
+ while (this.retentionCutoff !== null && generation === this.retryGeneration) {
2171
2176
  const nextCutoff = this.retentionCutoff;
2172
2177
  this.retentionCutoff = null;
2173
2178
  try {
2174
2179
  await this.persistence.clearBefore(nextCutoff);
2175
2180
  } catch (error) {
2176
- this.onPersistenceError(error);
2181
+ if (generation === this.retryGeneration) this.onPersistenceError(error);
2177
2182
  }
2178
2183
  }
2179
2184
  })().finally(() => {
2180
2185
  this.retentionCleanup = null;
2181
- if (this.retentionCutoff !== null) {
2186
+ if (this.retentionCutoff !== null && generation === this.retryGeneration) {
2182
2187
  this.scheduleRetentionCleanup(this.retentionCutoff);
2183
2188
  }
2184
2189
  });
@@ -2195,7 +2200,9 @@ var ReplayManager = class {
2195
2200
  attempt += 1;
2196
2201
  try {
2197
2202
  if (generation !== this.retryGeneration) throw new PersistenceRetryCancelledError();
2198
- return await operation();
2203
+ const result = await operation();
2204
+ if (generation !== this.retryGeneration) throw new PersistenceRetryCancelledError();
2205
+ return result;
2199
2206
  } catch (error) {
2200
2207
  if (error instanceof PersistenceRetryCancelledError || generation !== this.retryGeneration) {
2201
2208
  throw new PersistenceRetryCancelledError();
@@ -2329,7 +2336,7 @@ var DedupManager = class {
2329
2336
  };
2330
2337
 
2331
2338
  // src/core/version.ts
2332
- var SDK_VERSION = true ? "0.20.87" : "";
2339
+ var SDK_VERSION = true ? "0.20.89" : "";
2333
2340
 
2334
2341
  // src/core/data-bus.ts
2335
2342
  var DEFAULT_REPLAY_MAX_PER_TOPIC = 100;
@@ -2523,10 +2530,7 @@ var CrossTabDataBus = class {
2523
2530
  this.suspendTransport();
2524
2531
  },
2525
2532
  onResume: () => {
2526
- this.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.RESUME });
2527
- this.trace.start();
2528
- this.startDedupSweep();
2529
- this.replayManager.start();
2533
+ this.resumeSuspendedResources();
2530
2534
  this.resumeTransport();
2531
2535
  },
2532
2536
  onDiagnostic: (event) => {
@@ -2556,15 +2560,19 @@ var CrossTabDataBus = class {
2556
2560
  if (!transportDown) return Promise.resolve();
2557
2561
  this.activeConfig = config;
2558
2562
  this.resetFailureState();
2559
- return this.reopenTransport();
2563
+ const resumingFromSuspend = this.suspended;
2564
+ if (resumingFromSuspend) this.resumeSuspendedResources();
2565
+ const opening2 = this.reopenTransport();
2566
+ if (resumingFromSuspend) this.cluster.start();
2567
+ return opening2;
2560
2568
  }
2561
2569
  this.started = true;
2562
2570
  this.stopping = false;
2563
2571
  this.suspended = false;
2564
2572
  this.activeConfig = config;
2565
2573
  this.resetFailureState();
2566
- this.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.START });
2567
2574
  this.trace.start();
2575
+ this.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.START });
2568
2576
  this.startDedupSweep();
2569
2577
  this.replayManager.start();
2570
2578
  this.updateStatus(WORKER_STATUS.CONNECTING);
@@ -2679,6 +2687,7 @@ var CrossTabDataBus = class {
2679
2687
  this.transportReady = false;
2680
2688
  const chainedPendingStop = this.pendingStop;
2681
2689
  const isCurrentLifecycle = () => lifecycleEpoch === this.lifecycleEpoch;
2690
+ let startupInProgress = true;
2682
2691
  return before.catch(() => void 0).then(() => {
2683
2692
  if (!isCurrentLifecycle() || this.stopping || this.suspended) return;
2684
2693
  if (this.pendingStop === chainedPendingStop) this.pendingStop = null;
@@ -2689,13 +2698,16 @@ var CrossTabDataBus = class {
2689
2698
  if (isCurrentLifecycle()) this.handleTransportMessage(message);
2690
2699
  },
2691
2700
  onStatus: (status) => {
2692
- if (isCurrentLifecycle()) this.updateStatus(status);
2701
+ if (isCurrentLifecycle()) {
2702
+ this.updateStatus(status, status !== WORKER_STATUS.ERROR || !startupInProgress);
2703
+ }
2693
2704
  },
2694
2705
  onError: (error) => {
2695
2706
  if (isCurrentLifecycle()) this.reportError(error);
2696
2707
  }
2697
2708
  })
2698
2709
  ).then(() => {
2710
+ startupInProgress = false;
2699
2711
  if (!isCurrentLifecycle()) return;
2700
2712
  if (this.status === WORKER_STATUS.ERROR) {
2701
2713
  throw new Error("Transport failed during startup.");
@@ -2709,18 +2721,21 @@ var CrossTabDataBus = class {
2709
2721
  });
2710
2722
  }).catch((error) => {
2711
2723
  if (!isCurrentLifecycle()) throw error;
2724
+ startupInProgress = false;
2712
2725
  if (stopClusterOnFailure) this.started = false;
2713
2726
  if (!this.pendingStop) {
2714
2727
  this.pendingStop = this.createStopPromise();
2715
2728
  }
2716
2729
  this.transportReady = false;
2717
- this.updateStatus(WORKER_STATUS.ERROR);
2718
- this.reportError(error);
2719
2730
  if (stopClusterOnFailure) {
2720
2731
  this.stopping = true;
2721
2732
  this.cluster.stop();
2722
2733
  this.stopping = false;
2723
2734
  }
2735
+ this.recordError(error);
2736
+ this.startPromise = null;
2737
+ this.updateStatus(WORKER_STATUS.ERROR);
2738
+ this.notifyError(error);
2724
2739
  throw error;
2725
2740
  });
2726
2741
  }
@@ -2917,7 +2932,7 @@ var CrossTabDataBus = class {
2917
2932
  getHealthSummary() {
2918
2933
  const transport = this.transport;
2919
2934
  const transportDown = this.status !== WORKER_STATUS.CONNECTED;
2920
- 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;
2935
+ const state = !this.started || this.stopping ? 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;
2921
2936
  return {
2922
2937
  healthy: state === HEALTH_STATE.HEALTHY,
2923
2938
  state,
@@ -2988,7 +3003,7 @@ var CrossTabDataBus = class {
2988
3003
  this.canceledQueuedStartToken = this.queuedStartToken;
2989
3004
  this.queuedStart = null;
2990
3005
  }
2991
- if (this.stopPromise) return this.stopPromise;
3006
+ if (this.stopPromise && this.stopping) return this.stopPromise;
2992
3007
  if (!this.started && !this.startPromise && !this.pendingStop && !this.transportReady) {
2993
3008
  return Promise.resolve();
2994
3009
  }
@@ -3084,7 +3099,7 @@ var CrossTabDataBus = class {
3084
3099
  * Propagate a status change to the cluster, trace, and all registered
3085
3100
  * status handlers. On reconnect, re-subscribe any topics assigned to us.
3086
3101
  */
3087
- updateStatus(status) {
3102
+ updateStatus(status, notifyHandlers = true) {
3088
3103
  const previousStatus = this.status;
3089
3104
  this.status = status;
3090
3105
  if (status === WORKER_STATUS.CONNECTED) this.transportHasConnected = true;
@@ -3136,9 +3151,9 @@ var CrossTabDataBus = class {
3136
3151
  } else if (status === WORKER_STATUS.ERROR) {
3137
3152
  this.releaseRecoveryGate();
3138
3153
  }
3139
- this.invokeHandlers(this.statusHandlers, (handler) => handler(status));
3154
+ if (notifyHandlers) this.invokeHandlers(this.statusHandlers, (handler) => handler(status));
3140
3155
  }
3141
- reportError(error, source = FAILURE_SOURCE.TRANSPORT) {
3156
+ recordError(error, source = FAILURE_SOURCE.TRANSPORT) {
3142
3157
  const at = this.now();
3143
3158
  if (source === FAILURE_SOURCE.TRANSPORT) {
3144
3159
  this.lastError = error;
@@ -3158,8 +3173,14 @@ var CrossTabDataBus = class {
3158
3173
  type: TRACE_EVENT_TYPE.ERROR,
3159
3174
  source: source === FAILURE_SOURCE.TRANSPORT ? TRACE_ERROR_SOURCE.TRANSPORT : TRACE_ERROR_SOURCE.OPERATION
3160
3175
  });
3176
+ }
3177
+ notifyError(error) {
3161
3178
  this.invokeHandlers(this.errorHandlers, (handler) => handler(error), INVOKE_LABEL.ERROR_HANDLER);
3162
3179
  }
3180
+ reportError(error, source = FAILURE_SOURCE.TRANSPORT) {
3181
+ this.recordError(error, source);
3182
+ this.notifyError(error);
3183
+ }
3163
3184
  /** Report a persistence failure to the trace and the unified failure ledger,
3164
3185
  * unless it is a {@link PersistenceRetryCancelledError} cancellation from a
3165
3186
  * lifecycle transition (teardown should stay quiet). */
@@ -3222,6 +3243,15 @@ var CrossTabDataBus = class {
3222
3243
  }
3223
3244
  }
3224
3245
  }
3246
+ /** Resume the resources paused by a pagehide suspension. Both the native
3247
+ * pageshow path and explicit start() must run this so an explicit resume
3248
+ * cannot leave trace metrics and periodic cleanup timers permanently off. */
3249
+ resumeSuspendedResources() {
3250
+ this.trace.start();
3251
+ this.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.RESUME });
3252
+ this.startDedupSweep();
3253
+ this.replayManager.start();
3254
+ }
3225
3255
  /**
3226
3256
  * Suspend the transport when the tab goes hidden. Stops the transport and
3227
3257
  * clears subscription state so it will be re-established on resume.
@@ -3234,8 +3264,11 @@ var CrossTabDataBus = class {
3234
3264
  this.transportReady = false;
3235
3265
  this.transportSubscribedTopics.clear();
3236
3266
  this.updateStatus(WORKER_STATUS.DISCONNECTED);
3237
- if (this.pendingStop) return;
3238
- const pending = this.startPromise ?? Promise.resolve();
3267
+ if (this.pendingStop && (this.startPromise === null || this.startPromise === this.pendingStop)) {
3268
+ this.startPromise = this.pendingStop;
3269
+ return;
3270
+ }
3271
+ const pending = this.startPromise ?? this.pendingStop ?? Promise.resolve();
3239
3272
  const stopping = pending.catch(() => void 0).then(() => this.transport.stop()).catch((error) => this.reportError(error));
3240
3273
  this.startPromise = stopping;
3241
3274
  this.pendingStop = stopping;
@@ -3330,10 +3363,17 @@ var CrossTabDataBus = class {
3330
3363
  ready = this.reopenTransport();
3331
3364
  }
3332
3365
  if (!ready || this.stopping) return;
3333
- void ready.then(() => {
3334
- if (!this.started || this.stopping || this.suspended) return;
3335
- return operation();
3336
- }).catch((error) => this.reportError(error));
3366
+ void ready.then(
3367
+ () => {
3368
+ if (!this.started || this.stopping || this.suspended) return;
3369
+ return operation();
3370
+ },
3371
+ // The opening promise reports its own lifecycle failure through
3372
+ // openTransport(). Swallowing it here prevents a stale startup
3373
+ // rejection from being recorded again after an onStatus/onError
3374
+ // callback has already started and reset the ledger for a retry.
3375
+ () => void 0
3376
+ ).catch((error) => this.reportError(error));
3337
3377
  }
3338
3378
  /**
3339
3379
  * Publications started after teardown begins cannot reach any transport.
@@ -3903,10 +3943,16 @@ var WebSocketTransport = class {
3903
3943
  * server cannot crash the message path. */
3904
3944
  async handleMessage(raw) {
3905
3945
  if (typeof Blob !== "undefined" && raw instanceof Blob) {
3946
+ const socket = this.socket;
3947
+ const handlers = this.handlers;
3906
3948
  try {
3907
- await this.handleMessage(await raw.arrayBuffer());
3949
+ const buffer = await raw.arrayBuffer();
3950
+ if (this.socket !== socket || this.handlers !== handlers || !this.socketActive) return;
3951
+ await this.handleMessage(buffer);
3908
3952
  } catch (error) {
3909
- this.handlers?.onError(error);
3953
+ if (this.socket === socket && this.handlers === handlers && this.socketActive) {
3954
+ this.handlers?.onError(error);
3955
+ }
3910
3956
  }
3911
3957
  return;
3912
3958
  }