cross-tab-worker-databus 0.20.88 → 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.
@@ -1661,8 +1661,9 @@ var DataBusTraceReporter = class {
1661
1661
  /** Start the periodic metrics flush interval. No-op when mode is 'events'
1662
1662
  * (no metrics to emit), when disabled, or when already running. */
1663
1663
  start() {
1664
- if (!this.enabled || this.intervalHandle || this.mode === TRACE_MODE.EVENTS) return;
1664
+ if (!this.enabled) return;
1665
1665
  this.stopped = false;
1666
+ if (this.intervalHandle || this.mode === TRACE_MODE.EVENTS) return;
1666
1667
  this.intervalStartedAt = this.now();
1667
1668
  this.intervalHandle = setInterval(() => this.flush(), this.metricsIntervalMs);
1668
1669
  }
@@ -1675,6 +1676,7 @@ var DataBusTraceReporter = class {
1675
1676
  }
1676
1677
  stop() {
1677
1678
  this.stopped = true;
1679
+ this.pendingEvents = [];
1678
1680
  this.pause();
1679
1681
  }
1680
1682
  /** Synchronous sink state for diagnostics: whether delivery is async and how
@@ -1685,7 +1687,7 @@ var DataBusTraceReporter = class {
1685
1687
  }
1686
1688
  /** Record an instantaneous trace event (lifecycle, status, error, etc.). */
1687
1689
  event(event) {
1688
- if (!this.enabled || this.mode === TRACE_MODE.METRICS) return;
1690
+ if (!this.enabled || this.stopped || this.mode === TRACE_MODE.METRICS) return;
1689
1691
  this.emit({ ...event, timestamp: this.now() });
1690
1692
  }
1691
1693
  /** Synchronous snapshot of the current metrics window without resetting it.
@@ -1768,7 +1770,7 @@ var DataBusTraceReporter = class {
1768
1770
  * Extracted so the four record / flush methods share one guard expression
1769
1771
  * instead of repeating `!this.enabled || this.mode === 'events'` at each. */
1770
1772
  get metricsActive() {
1771
- return this.enabled && this.mode !== TRACE_MODE.EVENTS;
1773
+ return this.enabled && !this.stopped && this.mode !== TRACE_MODE.EVENTS;
1772
1774
  }
1773
1775
  /** Emit the accumulated metrics snapshot if the interval is active. */
1774
1776
  flush() {
@@ -2070,6 +2072,8 @@ var ReplayManager = class {
2070
2072
  * or stopped bus does not keep hammering the store) and stop the sweep. */
2071
2073
  suspend() {
2072
2074
  this.retryGeneration += 1;
2075
+ this.pendingReplayPersistence = [];
2076
+ this.retentionCutoff = null;
2073
2077
  this.stop();
2074
2078
  }
2075
2079
  /** Drop all in-memory buffers (used on full teardown). */
@@ -2168,19 +2172,20 @@ var ReplayManager = class {
2168
2172
  this.retentionCutoff = cutoff;
2169
2173
  }
2170
2174
  if (this.retentionCleanup) return;
2175
+ const generation = this.retryGeneration;
2171
2176
  this.retentionCleanup = (async () => {
2172
- while (this.retentionCutoff !== null) {
2177
+ while (this.retentionCutoff !== null && generation === this.retryGeneration) {
2173
2178
  const nextCutoff = this.retentionCutoff;
2174
2179
  this.retentionCutoff = null;
2175
2180
  try {
2176
2181
  await this.persistence.clearBefore(nextCutoff);
2177
2182
  } catch (error) {
2178
- this.onPersistenceError(error);
2183
+ if (generation === this.retryGeneration) this.onPersistenceError(error);
2179
2184
  }
2180
2185
  }
2181
2186
  })().finally(() => {
2182
2187
  this.retentionCleanup = null;
2183
- if (this.retentionCutoff !== null) {
2188
+ if (this.retentionCutoff !== null && generation === this.retryGeneration) {
2184
2189
  this.scheduleRetentionCleanup(this.retentionCutoff);
2185
2190
  }
2186
2191
  });
@@ -2197,7 +2202,9 @@ var ReplayManager = class {
2197
2202
  attempt += 1;
2198
2203
  try {
2199
2204
  if (generation !== this.retryGeneration) throw new PersistenceRetryCancelledError();
2200
- return await operation();
2205
+ const result = await operation();
2206
+ if (generation !== this.retryGeneration) throw new PersistenceRetryCancelledError();
2207
+ return result;
2201
2208
  } catch (error) {
2202
2209
  if (error instanceof PersistenceRetryCancelledError || generation !== this.retryGeneration) {
2203
2210
  throw new PersistenceRetryCancelledError();
@@ -2331,7 +2338,7 @@ var DedupManager = class {
2331
2338
  };
2332
2339
 
2333
2340
  // src/core/version.ts
2334
- var SDK_VERSION = true ? "0.20.88" : "";
2341
+ var SDK_VERSION = true ? "0.20.89" : "";
2335
2342
 
2336
2343
  // src/core/data-bus.ts
2337
2344
  var DEFAULT_REPLAY_MAX_PER_TOPIC = 100;
@@ -2566,8 +2573,8 @@ var CrossTabDataBus = class {
2566
2573
  this.suspended = false;
2567
2574
  this.activeConfig = config;
2568
2575
  this.resetFailureState();
2569
- this.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.START });
2570
2576
  this.trace.start();
2577
+ this.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.START });
2571
2578
  this.startDedupSweep();
2572
2579
  this.replayManager.start();
2573
2580
  this.updateStatus(WORKER_STATUS.CONNECTING);
@@ -2927,7 +2934,7 @@ var CrossTabDataBus = class {
2927
2934
  getHealthSummary() {
2928
2935
  const transport = this.transport;
2929
2936
  const transportDown = this.status !== WORKER_STATUS.CONNECTED;
2930
- 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;
2937
+ 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;
2931
2938
  return {
2932
2939
  healthy: state === HEALTH_STATE.HEALTHY,
2933
2940
  state,
@@ -2998,7 +3005,7 @@ var CrossTabDataBus = class {
2998
3005
  this.canceledQueuedStartToken = this.queuedStartToken;
2999
3006
  this.queuedStart = null;
3000
3007
  }
3001
- if (this.stopPromise) return this.stopPromise;
3008
+ if (this.stopPromise && this.stopping) return this.stopPromise;
3002
3009
  if (!this.started && !this.startPromise && !this.pendingStop && !this.transportReady) {
3003
3010
  return Promise.resolve();
3004
3011
  }
@@ -3242,8 +3249,8 @@ var CrossTabDataBus = class {
3242
3249
  * pageshow path and explicit start() must run this so an explicit resume
3243
3250
  * cannot leave trace metrics and periodic cleanup timers permanently off. */
3244
3251
  resumeSuspendedResources() {
3245
- this.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.RESUME });
3246
3252
  this.trace.start();
3253
+ this.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.RESUME });
3247
3254
  this.startDedupSweep();
3248
3255
  this.replayManager.start();
3249
3256
  }
@@ -3474,6 +3481,10 @@ var CentrifugeSession = class {
3474
3481
  transferable = false;
3475
3482
  tokenBridge = false;
3476
3483
  nextRequestId = 1;
3484
+ // Bumped on every initialize/stop. Async client callbacks capture the value
3485
+ // from the connection that created them so a stopped client cannot emit into
3486
+ // a later session instance (including the main-thread local fallback).
3487
+ lifecycle = 0;
3477
3488
  pendingTokenRequests = /* @__PURE__ */ new Map();
3478
3489
  /** Dispatch an incoming Worker message to the matching operation.
3479
3490
  * Unknown message types are ignored rather than thrown, so a future protocol
@@ -3509,6 +3520,7 @@ var CentrifugeSession = class {
3509
3520
  /** Create the Centrifuge client, wire up lifecycle listeners, and connect. */
3510
3521
  initialize(url, config, transferable, tokenBridge) {
3511
3522
  if (this.client) return;
3523
+ const lifecycle = ++this.lifecycle;
3512
3524
  this.transferable = transferable;
3513
3525
  this.tokenBridge = tokenBridge;
3514
3526
  const clientOptions = tokenBridge ? {
@@ -3522,12 +3534,23 @@ var CentrifugeSession = class {
3522
3534
  const client = new import_centrifuge.Centrifuge(url, clientOptions);
3523
3535
  this.client = client;
3524
3536
  client.on("state", (context) => {
3537
+ if (this.lifecycle !== lifecycle) return;
3525
3538
  this.post({ type: CENTRIFUGE_OUTPUT_TYPE.STATUS, status: normalizeStatus(context.newState) });
3526
3539
  });
3527
- client.on("connected", () => this.post({ type: CENTRIFUGE_OUTPUT_TYPE.STATUS, status: WORKER_STATUS.CONNECTED }));
3528
- client.on("disconnected", () => this.post({ type: CENTRIFUGE_OUTPUT_TYPE.STATUS, status: WORKER_STATUS.DISCONNECTED }));
3529
- client.on("error", (context) => this.postError(context));
3540
+ client.on("connected", () => {
3541
+ if (this.lifecycle !== lifecycle) return;
3542
+ this.post({ type: CENTRIFUGE_OUTPUT_TYPE.STATUS, status: WORKER_STATUS.CONNECTED });
3543
+ });
3544
+ client.on("disconnected", () => {
3545
+ if (this.lifecycle !== lifecycle) return;
3546
+ this.post({ type: CENTRIFUGE_OUTPUT_TYPE.STATUS, status: WORKER_STATUS.DISCONNECTED });
3547
+ });
3548
+ client.on("error", (context) => {
3549
+ if (this.lifecycle !== lifecycle) return;
3550
+ this.postError(context);
3551
+ });
3530
3552
  client.on("publication", (context) => {
3553
+ if (this.lifecycle !== lifecycle) return;
3531
3554
  const topic = context.channel || getPayloadTopic(context.data);
3532
3555
  if (!topic || this.subscriptions.has(topic)) return;
3533
3556
  this.postPublication(topic, context.data);
@@ -3540,6 +3563,7 @@ var CentrifugeSession = class {
3540
3563
  * avoiding the removeAllListeners + re-on churn on every duplicate message. */
3541
3564
  subscribe(topic) {
3542
3565
  if (!this.client) return this.postError(new Error("Centrifuge client is not initialized."));
3566
+ const lifecycle = this.lifecycle;
3543
3567
  const existing = this.subscriptions.get(topic);
3544
3568
  if (existing) {
3545
3569
  existing.subscribe();
@@ -3552,10 +3576,16 @@ var CentrifugeSession = class {
3552
3576
  subscription.removeAllListeners("unsubscribed");
3553
3577
  this.subscriptions.set(topic, subscription);
3554
3578
  subscription.on("publication", (context) => {
3579
+ if (this.lifecycle !== lifecycle) return;
3555
3580
  this.postPublication(topic, context.data);
3556
3581
  });
3557
- subscription.on("error", (context) => this.postError(context));
3558
- subscription.on("unsubscribed", () => this.subscriptions.delete(topic));
3582
+ subscription.on("error", (context) => {
3583
+ if (this.lifecycle !== lifecycle) return;
3584
+ this.postError(context);
3585
+ });
3586
+ subscription.on("unsubscribed", () => {
3587
+ if (this.lifecycle === lifecycle) this.subscriptions.delete(topic);
3588
+ });
3559
3589
  subscription.subscribe();
3560
3590
  }
3561
3591
  /** Unsubscribe from a Centrifuge channel and clean up the local reference.
@@ -3573,13 +3603,16 @@ var CentrifugeSession = class {
3573
3603
  /** Publish a message to the Centrifuge channel. */
3574
3604
  publish(topic, data, messageId, timestamp) {
3575
3605
  if (!this.client) return this.postError(new Error("Centrifuge client is not initialized."));
3606
+ const lifecycle = this.lifecycle;
3576
3607
  const hasMetadata = messageId !== void 0 || timestamp !== void 0;
3577
3608
  const payload = hasMetadata ? {
3578
3609
  data,
3579
3610
  ...messageId === void 0 ? {} : { messageId },
3580
3611
  ...timestamp === void 0 ? {} : { timestamp }
3581
3612
  } : data;
3582
- void this.client.publish(topic, payload).catch((error) => this.postError(error));
3613
+ void this.client.publish(topic, payload).catch((error) => {
3614
+ if (this.lifecycle === lifecycle) this.postError(error);
3615
+ });
3583
3616
  }
3584
3617
  /** Forward a publication to the transport. Binary payloads take the
3585
3618
  * zero-copy `MESSAGE_BIN` path when `transferable` is enabled; everything
@@ -3602,6 +3635,7 @@ var CentrifugeSession = class {
3602
3635
  }
3603
3636
  /** Disconnect the client and clear all subscriptions. */
3604
3637
  stop() {
3638
+ this.lifecycle += 1;
3605
3639
  for (const [, pending] of this.pendingTokenRequests) {
3606
3640
  pending.reject(new Error("Centrifuge session stopped before the credential was resolved."));
3607
3641
  }
@@ -3615,9 +3649,14 @@ var CentrifugeSession = class {
3615
3649
  * Used as Centrifuge's `getToken` / `getChannelToken` when token bridging is
3616
3650
  * enabled; resolved or rejected by a matching TOKEN_RESPONSE / TOKEN_ERROR. */
3617
3651
  requestToken(kind, channel) {
3652
+ const lifecycle = this.lifecycle;
3618
3653
  const requestId = this.nextRequestId;
3619
3654
  this.nextRequestId += 1;
3620
3655
  return new Promise((resolve, reject) => {
3656
+ if (this.lifecycle !== lifecycle) {
3657
+ reject(new Error("Centrifuge session stopped before the credential was requested."));
3658
+ return;
3659
+ }
3621
3660
  this.pendingTokenRequests.set(requestId, { resolve, reject });
3622
3661
  this.post({
3623
3662
  type: CENTRIFUGE_OUTPUT_TYPE.TOKEN_REQUEST,
@@ -3838,10 +3877,24 @@ var CentrifugeWorkerTransport = class {
3838
3877
  * credentialProvider, then post the fresh token (or a serialized failure)
3839
3878
  * back to the session that issued the request. */
3840
3879
  resolveTokenRequest(requestId, kind, channel) {
3880
+ const generation = this.generation;
3881
+ const worker = this.worker;
3882
+ const port = this.port;
3883
+ const localSession = this.localSession;
3884
+ const isCurrentBackend = () => this.generation === generation && this.worker === worker && this.port === port && this.localSession === localSession;
3841
3885
  const provider = this.credentialProvider;
3842
- const value = kind === "channelToken" && provider?.getChannelToken ? provider.getChannelToken(channel ?? "") : provider?.getToken?.();
3886
+ let value;
3887
+ try {
3888
+ value = kind === "channelToken" && provider?.getChannelToken ? provider.getChannelToken(channel ?? "") : provider?.getToken?.();
3889
+ } catch (error) {
3890
+ if (isCurrentBackend()) {
3891
+ this.post({ type: CENTRIFUGE_INPUT_TYPE.TOKEN_ERROR, requestId, error: serializeError(error) });
3892
+ }
3893
+ return;
3894
+ }
3843
3895
  Promise.resolve(value).then(
3844
3896
  (token) => {
3897
+ if (!isCurrentBackend()) return;
3845
3898
  if (typeof token !== "string" || token.length === 0) {
3846
3899
  this.post({
3847
3900
  type: CENTRIFUGE_INPUT_TYPE.TOKEN_ERROR,
@@ -3853,6 +3906,7 @@ var CentrifugeWorkerTransport = class {
3853
3906
  this.post({ type: CENTRIFUGE_INPUT_TYPE.TOKEN_RESPONSE, requestId, token });
3854
3907
  },
3855
3908
  (error) => {
3909
+ if (!isCurrentBackend()) return;
3856
3910
  this.post({ type: CENTRIFUGE_INPUT_TYPE.TOKEN_ERROR, requestId, error: serializeError(error) });
3857
3911
  }
3858
3912
  );