cross-tab-worker-databus 0.21.42 → 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.
- package/CHANGELOG.md +22 -0
- package/dist/centrifuge.js +1 -1
- package/dist/{chunk-VWVYUSIN.js → chunk-QPM2OGBB.js} +96 -18
- package/dist/chunk-QPM2OGBB.js.map +7 -0
- package/dist/cjs/centrifuge.cjs +95 -17
- package/dist/cjs/centrifuge.cjs.map +2 -2
- package/dist/cjs/index.cjs +95 -17
- package/dist/cjs/index.cjs.map +2 -2
- package/dist/core/cluster.d.ts +24 -0
- package/dist/core/cluster.d.ts.map +1 -1
- package/dist/core/data-bus.d.ts.map +1 -1
- package/dist/core/dedup-manager.d.ts +26 -0
- package/dist/core/dedup-manager.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/docs/api.md +1 -1
- package/docs/architecture.md +13 -5
- package/docs/configuration.md +1 -1
- package/docs/roadmap.md +11 -1
- package/docs/zh/api.md +2 -2
- package/docs/zh/architecture.md +6 -4
- package/docs/zh/configuration.md +1 -1
- package/docs/zh/roadmap.md +11 -1
- package/package.json +1 -1
- package/dist/chunk-VWVYUSIN.js.map +0 -7
package/dist/cjs/centrifuge.cjs
CHANGED
|
@@ -774,6 +774,10 @@ var WorkerClusterRuntime = class {
|
|
|
774
774
|
channelName;
|
|
775
775
|
/** Adaptive load weighting options; undefined keeps legacy topic-count routing. */
|
|
776
776
|
loadWeighting;
|
|
777
|
+
/** The most recent finite reading of the environment's clock, and the fallback for
|
|
778
|
+
* a broken one — see {@link nowMs}. Seeded to 0 because a clock that has never
|
|
779
|
+
* produced a finite reading leaves nothing to hold. */
|
|
780
|
+
lastFiniteNow = 0;
|
|
777
781
|
/** Rolling traffic accumulator folded into the worker record on writeRecord. */
|
|
778
782
|
throughputWindow = {
|
|
779
783
|
startedAt: 0,
|
|
@@ -835,7 +839,7 @@ var WorkerClusterRuntime = class {
|
|
|
835
839
|
this.storage = canUseStorage(this.environment.storage, `${baseKey}:probe`) ? new BatchingStorageWriter(this.environment.storage) : null;
|
|
836
840
|
this.tabId = options.tabId ?? getOrCreateTabId(this.environment, `${prefix}:tab-id`);
|
|
837
841
|
this.workerId = options.workerId ?? `worker-${this.tabId}-${this.environment.randomId()}`;
|
|
838
|
-
const now = this.
|
|
842
|
+
const now = this.nowMs();
|
|
839
843
|
this.currentRecord = {
|
|
840
844
|
protocolVersion: CLUSTER_PROTOCOL_VERSION,
|
|
841
845
|
workerId: this.workerId,
|
|
@@ -848,6 +852,33 @@ var WorkerClusterRuntime = class {
|
|
|
848
852
|
registeredAt: now
|
|
849
853
|
};
|
|
850
854
|
}
|
|
855
|
+
/**
|
|
856
|
+
* The environment's clock, normalized.
|
|
857
|
+
*
|
|
858
|
+
* `ClusterEnvironment.now` is a required field, and the documented hazard is a
|
|
859
|
+
* *backwards* step rather than a missing reading — the library's own subtraction
|
|
860
|
+
* sites tolerate one, since every `now - stamped > ttl` reads a backwards clock as
|
|
861
|
+
* "not yet expired". A non-finite reading is outside that contract, and it does not
|
|
862
|
+
* degrade one field: measured before this guard, a `NaN` clock was written straight
|
|
863
|
+
* into durable state — the worker, route and subscriber records all carried
|
|
864
|
+
* `"updatedAt": null`, because `JSON.stringify(NaN)` is `null` — and that is worse
|
|
865
|
+
* than a wrong number rather than merely different, because `null` coerces to `0`
|
|
866
|
+
* in the TTL arithmetic, so the first tick after the clock recovered would read
|
|
867
|
+
* every record as ancient and prune the whole cluster at once.
|
|
868
|
+
*
|
|
869
|
+
* Holding the last finite reading is the same treatment `data-bus.ts` applies to
|
|
870
|
+
* `dedup.now` and `DedupManager` applies to its own boundary: a paused clock, not a
|
|
871
|
+
* poisoned one. It is a helper rather than nine conditions because the broken input
|
|
872
|
+
* is the clock, not any particular comparison.
|
|
873
|
+
*/
|
|
874
|
+
nowMs() {
|
|
875
|
+
const reading = this.environment.now();
|
|
876
|
+
if (Number.isFinite(reading)) {
|
|
877
|
+
this.lastFiniteNow = reading;
|
|
878
|
+
return reading;
|
|
879
|
+
}
|
|
880
|
+
return this.lastFiniteNow;
|
|
881
|
+
}
|
|
851
882
|
/** Start the cluster: register, listen for lifecycle events, and begin heartbeats. */
|
|
852
883
|
start() {
|
|
853
884
|
if (this.started) return;
|
|
@@ -904,7 +935,7 @@ var WorkerClusterRuntime = class {
|
|
|
904
935
|
return;
|
|
905
936
|
}
|
|
906
937
|
this.channel?.addEventListener("message", this.handleMessage);
|
|
907
|
-
const now = this.
|
|
938
|
+
const now = this.nowMs();
|
|
908
939
|
this.currentRecord = {
|
|
909
940
|
...this.currentRecord,
|
|
910
941
|
heartbeatAt: now,
|
|
@@ -1390,7 +1421,7 @@ var WorkerClusterRuntime = class {
|
|
|
1390
1421
|
* will never complete — while a fresh unconfirmed route may simply be
|
|
1391
1422
|
* waiting out its confirmation flush and must be left alone. */
|
|
1392
1423
|
isStaleHandoff(route) {
|
|
1393
|
-
return this.
|
|
1424
|
+
return this.nowMs() - route.updatedAt > this.workerTtlMs;
|
|
1394
1425
|
}
|
|
1395
1426
|
/** Full reconciliation cycle: workers, subscriptions, and assigned topics. */
|
|
1396
1427
|
reconcile() {
|
|
@@ -1575,7 +1606,7 @@ var WorkerClusterRuntime = class {
|
|
|
1575
1606
|
/** Read all live worker records from storage, pruning stale entries past the TTL. */
|
|
1576
1607
|
readWorkers() {
|
|
1577
1608
|
if (!this.storage) return [this.currentRecord];
|
|
1578
|
-
const now = this.
|
|
1609
|
+
const now = this.nowMs();
|
|
1579
1610
|
const workers = [];
|
|
1580
1611
|
for (const { key, value: worker } of readAllByPrefix(this.storage, this.workerPrefix)) {
|
|
1581
1612
|
if (worker.workerId !== this.workerId && now - worker.heartbeatAt > this.workerTtlMs) {
|
|
@@ -1627,7 +1658,7 @@ var WorkerClusterRuntime = class {
|
|
|
1627
1658
|
topicKey,
|
|
1628
1659
|
workerId: this.workerId,
|
|
1629
1660
|
tabId: this.tabId,
|
|
1630
|
-
updatedAt: this.
|
|
1661
|
+
updatedAt: this.nowMs(),
|
|
1631
1662
|
generation: 1
|
|
1632
1663
|
};
|
|
1633
1664
|
}
|
|
@@ -1644,7 +1675,7 @@ var WorkerClusterRuntime = class {
|
|
|
1644
1675
|
topicKey,
|
|
1645
1676
|
workerId: owner.workerId,
|
|
1646
1677
|
tabId: owner.tabId,
|
|
1647
|
-
updatedAt: this.
|
|
1678
|
+
updatedAt: this.nowMs(),
|
|
1648
1679
|
generation,
|
|
1649
1680
|
...handoffFromWorkerId ? { handoffFromWorkerId } : {}
|
|
1650
1681
|
};
|
|
@@ -1656,7 +1687,7 @@ var WorkerClusterRuntime = class {
|
|
|
1656
1687
|
if (!route || route.workerId !== this.workerId || route.confirmedAt !== void 0) return;
|
|
1657
1688
|
writeJson(this.storage, this.routeStorageKey(topicKey), {
|
|
1658
1689
|
...route,
|
|
1659
|
-
confirmedAt: this.
|
|
1690
|
+
confirmedAt: this.nowMs()
|
|
1660
1691
|
});
|
|
1661
1692
|
const topic = this.knownTopics.get(topicKey);
|
|
1662
1693
|
if (topic) this.handlers.onDiagnostic?.({ operation: RELIABILITY_OPERATION.ROUTE_ACK, topic });
|
|
@@ -1664,7 +1695,7 @@ var WorkerClusterRuntime = class {
|
|
|
1664
1695
|
/** Remove routes whose topic has no subscribers and whose TTL has expired. */
|
|
1665
1696
|
cleanupOrphanedRoutes(workers) {
|
|
1666
1697
|
if (!this.storage) return;
|
|
1667
|
-
const now = this.
|
|
1698
|
+
const now = this.nowMs();
|
|
1668
1699
|
for (const { key, value: route } of readAllByPrefix(this.storage, this.routePrefix)) {
|
|
1669
1700
|
if (now - route.updatedAt <= this.workerTtlMs) continue;
|
|
1670
1701
|
if (this.readSubscriberTabIds(route.topicKey, workers).length > 0) continue;
|
|
@@ -1684,7 +1715,7 @@ var WorkerClusterRuntime = class {
|
|
|
1684
1715
|
if (!this.storage) return;
|
|
1685
1716
|
writeJson(this.storage, this.subscriberStorageKey(topicKey, this.tabId), {
|
|
1686
1717
|
tabId: this.tabId,
|
|
1687
|
-
updatedAt: this.
|
|
1718
|
+
updatedAt: this.nowMs()
|
|
1688
1719
|
});
|
|
1689
1720
|
}
|
|
1690
1721
|
/** Persist the current worker record with an updated heartbeat timestamp.
|
|
@@ -1695,7 +1726,7 @@ var WorkerClusterRuntime = class {
|
|
|
1695
1726
|
* status, visibility and load changes pass true; the heartbeat tick and a
|
|
1696
1727
|
* role change pass false. */
|
|
1697
1728
|
writeRecord(notify) {
|
|
1698
|
-
const now = this.
|
|
1729
|
+
const now = this.nowMs();
|
|
1699
1730
|
const sample = this.loadWeighting !== void 0 ? this.sampleThroughput(now) : void 0;
|
|
1700
1731
|
this.currentRecord = {
|
|
1701
1732
|
...this.currentRecord,
|
|
@@ -2495,6 +2526,8 @@ var DedupManager = class {
|
|
|
2495
2526
|
seenMessageIds = /* @__PURE__ */ new Map();
|
|
2496
2527
|
sweepTimer = null;
|
|
2497
2528
|
windowStartedAt = 0;
|
|
2529
|
+
/** The most recent finite reading of `now`, and the fallback for a broken one. */
|
|
2530
|
+
lastFiniteNow = 0;
|
|
2498
2531
|
windowAccepted = 0;
|
|
2499
2532
|
suppressed = 0;
|
|
2500
2533
|
accepted = 0;
|
|
@@ -2506,14 +2539,46 @@ var DedupManager = class {
|
|
|
2506
2539
|
this.sweepMs = options.sweepMs;
|
|
2507
2540
|
this.now = options.now;
|
|
2508
2541
|
this.trace = options.trace;
|
|
2509
|
-
this.
|
|
2542
|
+
this.lastFiniteNow = Number.isFinite(this.now()) ? this.now() : 0;
|
|
2543
|
+
this.windowStartedAt = this.lastFiniteNow;
|
|
2544
|
+
}
|
|
2545
|
+
/**
|
|
2546
|
+
* The injected clock, normalized.
|
|
2547
|
+
*
|
|
2548
|
+
* `dedup.now` is a public option and a non-wall-clock host is exactly the case it
|
|
2549
|
+
* exists for — one whose reading can be *missing* rather than merely unusual. Every
|
|
2550
|
+
* comparison this class makes is arithmetic on a timestamp, so a single non-finite
|
|
2551
|
+
* reading does not degrade one field: `now - timestamp > ttlMs` becomes
|
|
2552
|
+
* `NaN > ttlMs` (false, so nothing expires), the adaptive window's
|
|
2553
|
+
* `elapsed >= ADAPTIVE_WINDOW_MS` never fires, and `getStats().ttlMs` reports
|
|
2554
|
+
* `NaN` — which `JSON.stringify` turns into `null`, so a health summary or a
|
|
2555
|
+
* diagnostics log would carry a null where a duration belongs. Measured on the
|
|
2556
|
+
* pre-fix class: `getStats().ttlMs` → `NaN`, serialized → `null`, and an id
|
|
2557
|
+
* stayed suppressed for as long as it stayed in the map.
|
|
2558
|
+
*
|
|
2559
|
+
* Holding the last finite reading is the degradation that changes least: time
|
|
2560
|
+
* stands still, expiry pauses, and the adaptive window keeps the value it last
|
|
2561
|
+
* computed. Substituting `Date.now()` would be worse — it silently mixes two
|
|
2562
|
+
* clocks — and zeroing would expire the whole map on the next call. This is the
|
|
2563
|
+
* same class of guard `trace.ts` carries on its own public clock, and that
|
|
2564
|
+
* `websocket.ts` and `replay-manager.ts` apply to their numeric options; it is one
|
|
2565
|
+
* helper rather than a condition per site because the broken input is the *clock*,
|
|
2566
|
+
* not any particular comparison.
|
|
2567
|
+
*/
|
|
2568
|
+
nowMs() {
|
|
2569
|
+
const reading = this.now();
|
|
2570
|
+
if (Number.isFinite(reading)) {
|
|
2571
|
+
this.lastFiniteNow = reading;
|
|
2572
|
+
return reading;
|
|
2573
|
+
}
|
|
2574
|
+
return this.lastFiniteNow;
|
|
2510
2575
|
}
|
|
2511
2576
|
/** True when a publication carrying `messageId` was already seen. Records the
|
|
2512
2577
|
* ID and updates counters/trace on acceptance. Disabled or ID-less messages
|
|
2513
2578
|
* always pass through. */
|
|
2514
2579
|
isDuplicate(messageId, topic) {
|
|
2515
2580
|
if (!this.enabled || !messageId) return false;
|
|
2516
|
-
const now = this.
|
|
2581
|
+
const now = this.nowMs();
|
|
2517
2582
|
const ttlMs = this.currentTtl();
|
|
2518
2583
|
for (const [id, timestamp] of this.seenMessageIds) {
|
|
2519
2584
|
if (now - timestamp > ttlMs) this.seenMessageIds.delete(id);
|
|
@@ -2564,12 +2629,12 @@ var DedupManager = class {
|
|
|
2564
2629
|
this.seenMessageIds.clear();
|
|
2565
2630
|
this.suppressed = 0;
|
|
2566
2631
|
this.accepted = 0;
|
|
2567
|
-
this.windowStartedAt = this.
|
|
2632
|
+
this.windowStartedAt = this.nowMs();
|
|
2568
2633
|
this.windowAccepted = 0;
|
|
2569
2634
|
}
|
|
2570
2635
|
/** Remove IDs whose timestamp predates the effective TTL cutoff. */
|
|
2571
2636
|
pruneExpired() {
|
|
2572
|
-
const cutoff = this.
|
|
2637
|
+
const cutoff = this.nowMs() - this.currentTtl();
|
|
2573
2638
|
for (const [id, timestamp] of this.seenMessageIds) {
|
|
2574
2639
|
if (timestamp < cutoff) this.seenMessageIds.delete(id);
|
|
2575
2640
|
}
|
|
@@ -2580,7 +2645,7 @@ var DedupManager = class {
|
|
|
2580
2645
|
* every ADAPTIVE_WINDOW_MS. */
|
|
2581
2646
|
currentTtl() {
|
|
2582
2647
|
if (!this.adaptiveBounds) return this.ttlMs;
|
|
2583
|
-
const now = this.
|
|
2648
|
+
const now = this.nowMs();
|
|
2584
2649
|
const elapsed = now - this.windowStartedAt;
|
|
2585
2650
|
if (elapsed >= ADAPTIVE_WINDOW_MS) {
|
|
2586
2651
|
this.windowStartedAt = now;
|
|
@@ -2604,7 +2669,7 @@ var SDK_VERSION = (
|
|
|
2604
2669
|
// declaration's type is `string | undefined` and the export's is `string`
|
|
2605
2670
|
// (dropping the conditional is TS2322, measured). Recorded here so the leg reads
|
|
2606
2671
|
// as a shape the harness cannot reach rather than as a case nobody exercised.
|
|
2607
|
-
true ? "0.21.
|
|
2672
|
+
true ? "0.21.44" : ""
|
|
2608
2673
|
);
|
|
2609
2674
|
|
|
2610
2675
|
// src/core/data-bus.ts
|
|
@@ -2724,7 +2789,7 @@ var CrossTabDataBus = class {
|
|
|
2724
2789
|
assertRecoveryOptions(recovery);
|
|
2725
2790
|
this.recoveryCooldownMs = recovery?.cooldownMs ?? 1e3;
|
|
2726
2791
|
this.recoveryMaxAttempts = recovery?.maxAttempts ?? Number.POSITIVE_INFINITY;
|
|
2727
|
-
this.now = dedup?.now ?? Date.now;
|
|
2792
|
+
this.now = holdLastFinite(dedup?.now ?? Date.now);
|
|
2728
2793
|
this.transport = transport;
|
|
2729
2794
|
this.initialConfig = initialConfig;
|
|
2730
2795
|
this.hasInitialConfig = "initialConfig" in options;
|
|
@@ -3783,6 +3848,19 @@ function formatWorkerTrace(worker) {
|
|
|
3783
3848
|
function formatRouteTrace(route) {
|
|
3784
3849
|
return `${route.topicKey}@${route.workerId}|confirmed=${route.confirmedAt !== void 0}`;
|
|
3785
3850
|
}
|
|
3851
|
+
function holdLastFinite(source) {
|
|
3852
|
+
let last = 0;
|
|
3853
|
+
let primed = false;
|
|
3854
|
+
return () => {
|
|
3855
|
+
const reading = source();
|
|
3856
|
+
if (Number.isFinite(reading)) {
|
|
3857
|
+
last = reading;
|
|
3858
|
+
primed = true;
|
|
3859
|
+
return reading;
|
|
3860
|
+
}
|
|
3861
|
+
return primed ? last : 0;
|
|
3862
|
+
};
|
|
3863
|
+
}
|
|
3786
3864
|
|
|
3787
3865
|
// src/centrifuge-session.ts
|
|
3788
3866
|
var import_centrifuge = require("centrifuge");
|