cross-tab-worker-databus 0.21.43 → 0.21.44

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.
@@ -740,6 +740,10 @@ var WorkerClusterRuntime = class {
740
740
  channelName;
741
741
  /** Adaptive load weighting options; undefined keeps legacy topic-count routing. */
742
742
  loadWeighting;
743
+ /** The most recent finite reading of the environment's clock, and the fallback for
744
+ * a broken one — see {@link nowMs}. Seeded to 0 because a clock that has never
745
+ * produced a finite reading leaves nothing to hold. */
746
+ lastFiniteNow = 0;
743
747
  /** Rolling traffic accumulator folded into the worker record on writeRecord. */
744
748
  throughputWindow = {
745
749
  startedAt: 0,
@@ -801,7 +805,7 @@ var WorkerClusterRuntime = class {
801
805
  this.storage = canUseStorage(this.environment.storage, `${baseKey}:probe`) ? new BatchingStorageWriter(this.environment.storage) : null;
802
806
  this.tabId = options.tabId ?? getOrCreateTabId(this.environment, `${prefix}:tab-id`);
803
807
  this.workerId = options.workerId ?? `worker-${this.tabId}-${this.environment.randomId()}`;
804
- const now = this.environment.now();
808
+ const now = this.nowMs();
805
809
  this.currentRecord = {
806
810
  protocolVersion: CLUSTER_PROTOCOL_VERSION,
807
811
  workerId: this.workerId,
@@ -814,6 +818,33 @@ var WorkerClusterRuntime = class {
814
818
  registeredAt: now
815
819
  };
816
820
  }
821
+ /**
822
+ * The environment's clock, normalized.
823
+ *
824
+ * `ClusterEnvironment.now` is a required field, and the documented hazard is a
825
+ * *backwards* step rather than a missing reading — the library's own subtraction
826
+ * sites tolerate one, since every `now - stamped > ttl` reads a backwards clock as
827
+ * "not yet expired". A non-finite reading is outside that contract, and it does not
828
+ * degrade one field: measured before this guard, a `NaN` clock was written straight
829
+ * into durable state — the worker, route and subscriber records all carried
830
+ * `"updatedAt": null`, because `JSON.stringify(NaN)` is `null` — and that is worse
831
+ * than a wrong number rather than merely different, because `null` coerces to `0`
832
+ * in the TTL arithmetic, so the first tick after the clock recovered would read
833
+ * every record as ancient and prune the whole cluster at once.
834
+ *
835
+ * Holding the last finite reading is the same treatment `data-bus.ts` applies to
836
+ * `dedup.now` and `DedupManager` applies to its own boundary: a paused clock, not a
837
+ * poisoned one. It is a helper rather than nine conditions because the broken input
838
+ * is the clock, not any particular comparison.
839
+ */
840
+ nowMs() {
841
+ const reading = this.environment.now();
842
+ if (Number.isFinite(reading)) {
843
+ this.lastFiniteNow = reading;
844
+ return reading;
845
+ }
846
+ return this.lastFiniteNow;
847
+ }
817
848
  /** Start the cluster: register, listen for lifecycle events, and begin heartbeats. */
818
849
  start() {
819
850
  if (this.started) return;
@@ -870,7 +901,7 @@ var WorkerClusterRuntime = class {
870
901
  return;
871
902
  }
872
903
  this.channel?.addEventListener("message", this.handleMessage);
873
- const now = this.environment.now();
904
+ const now = this.nowMs();
874
905
  this.currentRecord = {
875
906
  ...this.currentRecord,
876
907
  heartbeatAt: now,
@@ -1356,7 +1387,7 @@ var WorkerClusterRuntime = class {
1356
1387
  * will never complete — while a fresh unconfirmed route may simply be
1357
1388
  * waiting out its confirmation flush and must be left alone. */
1358
1389
  isStaleHandoff(route) {
1359
- return this.environment.now() - route.updatedAt > this.workerTtlMs;
1390
+ return this.nowMs() - route.updatedAt > this.workerTtlMs;
1360
1391
  }
1361
1392
  /** Full reconciliation cycle: workers, subscriptions, and assigned topics. */
1362
1393
  reconcile() {
@@ -1541,7 +1572,7 @@ var WorkerClusterRuntime = class {
1541
1572
  /** Read all live worker records from storage, pruning stale entries past the TTL. */
1542
1573
  readWorkers() {
1543
1574
  if (!this.storage) return [this.currentRecord];
1544
- const now = this.environment.now();
1575
+ const now = this.nowMs();
1545
1576
  const workers = [];
1546
1577
  for (const { key, value: worker } of readAllByPrefix(this.storage, this.workerPrefix)) {
1547
1578
  if (worker.workerId !== this.workerId && now - worker.heartbeatAt > this.workerTtlMs) {
@@ -1593,7 +1624,7 @@ var WorkerClusterRuntime = class {
1593
1624
  topicKey,
1594
1625
  workerId: this.workerId,
1595
1626
  tabId: this.tabId,
1596
- updatedAt: this.environment.now(),
1627
+ updatedAt: this.nowMs(),
1597
1628
  generation: 1
1598
1629
  };
1599
1630
  }
@@ -1610,7 +1641,7 @@ var WorkerClusterRuntime = class {
1610
1641
  topicKey,
1611
1642
  workerId: owner.workerId,
1612
1643
  tabId: owner.tabId,
1613
- updatedAt: this.environment.now(),
1644
+ updatedAt: this.nowMs(),
1614
1645
  generation,
1615
1646
  ...handoffFromWorkerId ? { handoffFromWorkerId } : {}
1616
1647
  };
@@ -1622,7 +1653,7 @@ var WorkerClusterRuntime = class {
1622
1653
  if (!route || route.workerId !== this.workerId || route.confirmedAt !== void 0) return;
1623
1654
  writeJson(this.storage, this.routeStorageKey(topicKey), {
1624
1655
  ...route,
1625
- confirmedAt: this.environment.now()
1656
+ confirmedAt: this.nowMs()
1626
1657
  });
1627
1658
  const topic = this.knownTopics.get(topicKey);
1628
1659
  if (topic) this.handlers.onDiagnostic?.({ operation: RELIABILITY_OPERATION.ROUTE_ACK, topic });
@@ -1630,7 +1661,7 @@ var WorkerClusterRuntime = class {
1630
1661
  /** Remove routes whose topic has no subscribers and whose TTL has expired. */
1631
1662
  cleanupOrphanedRoutes(workers) {
1632
1663
  if (!this.storage) return;
1633
- const now = this.environment.now();
1664
+ const now = this.nowMs();
1634
1665
  for (const { key, value: route } of readAllByPrefix(this.storage, this.routePrefix)) {
1635
1666
  if (now - route.updatedAt <= this.workerTtlMs) continue;
1636
1667
  if (this.readSubscriberTabIds(route.topicKey, workers).length > 0) continue;
@@ -1650,7 +1681,7 @@ var WorkerClusterRuntime = class {
1650
1681
  if (!this.storage) return;
1651
1682
  writeJson(this.storage, this.subscriberStorageKey(topicKey, this.tabId), {
1652
1683
  tabId: this.tabId,
1653
- updatedAt: this.environment.now()
1684
+ updatedAt: this.nowMs()
1654
1685
  });
1655
1686
  }
1656
1687
  /** Persist the current worker record with an updated heartbeat timestamp.
@@ -1661,7 +1692,7 @@ var WorkerClusterRuntime = class {
1661
1692
  * status, visibility and load changes pass true; the heartbeat tick and a
1662
1693
  * role change pass false. */
1663
1694
  writeRecord(notify) {
1664
- const now = this.environment.now();
1695
+ const now = this.nowMs();
1665
1696
  const sample = this.loadWeighting !== void 0 ? this.sampleThroughput(now) : void 0;
1666
1697
  this.currentRecord = {
1667
1698
  ...this.currentRecord,
@@ -2604,7 +2635,7 @@ var SDK_VERSION = (
2604
2635
  // declaration's type is `string | undefined` and the export's is `string`
2605
2636
  // (dropping the conditional is TS2322, measured). Recorded here so the leg reads
2606
2637
  // as a shape the harness cannot reach rather than as a case nobody exercised.
2607
- true ? "0.21.43" : ""
2638
+ true ? "0.21.44" : ""
2608
2639
  );
2609
2640
 
2610
2641
  // src/core/data-bus.ts
@@ -2724,7 +2755,7 @@ var CrossTabDataBus = class {
2724
2755
  assertRecoveryOptions(recovery);
2725
2756
  this.recoveryCooldownMs = recovery?.cooldownMs ?? 1e3;
2726
2757
  this.recoveryMaxAttempts = recovery?.maxAttempts ?? Number.POSITIVE_INFINITY;
2727
- this.now = dedup?.now ?? Date.now;
2758
+ this.now = holdLastFinite(dedup?.now ?? Date.now);
2728
2759
  this.transport = transport;
2729
2760
  this.initialConfig = initialConfig;
2730
2761
  this.hasInitialConfig = "initialConfig" in options;
@@ -3783,6 +3814,19 @@ function formatWorkerTrace(worker) {
3783
3814
  function formatRouteTrace(route) {
3784
3815
  return `${route.topicKey}@${route.workerId}|confirmed=${route.confirmedAt !== void 0}`;
3785
3816
  }
3817
+ function holdLastFinite(source) {
3818
+ let last = 0;
3819
+ let primed = false;
3820
+ return () => {
3821
+ const reading = source();
3822
+ if (Number.isFinite(reading)) {
3823
+ last = reading;
3824
+ primed = true;
3825
+ return reading;
3826
+ }
3827
+ return primed ? last : 0;
3828
+ };
3829
+ }
3786
3830
 
3787
3831
  // src/core/replay-persistence.ts
3788
3832
  function createIndexedDbReplayPersistence(options) {