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/index.cjs
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|
|
@@ -2461,6 +2492,8 @@ var DedupManager = class {
|
|
|
2461
2492
|
seenMessageIds = /* @__PURE__ */ new Map();
|
|
2462
2493
|
sweepTimer = null;
|
|
2463
2494
|
windowStartedAt = 0;
|
|
2495
|
+
/** The most recent finite reading of `now`, and the fallback for a broken one. */
|
|
2496
|
+
lastFiniteNow = 0;
|
|
2464
2497
|
windowAccepted = 0;
|
|
2465
2498
|
suppressed = 0;
|
|
2466
2499
|
accepted = 0;
|
|
@@ -2472,14 +2505,46 @@ var DedupManager = class {
|
|
|
2472
2505
|
this.sweepMs = options.sweepMs;
|
|
2473
2506
|
this.now = options.now;
|
|
2474
2507
|
this.trace = options.trace;
|
|
2475
|
-
this.
|
|
2508
|
+
this.lastFiniteNow = Number.isFinite(this.now()) ? this.now() : 0;
|
|
2509
|
+
this.windowStartedAt = this.lastFiniteNow;
|
|
2510
|
+
}
|
|
2511
|
+
/**
|
|
2512
|
+
* The injected clock, normalized.
|
|
2513
|
+
*
|
|
2514
|
+
* `dedup.now` is a public option and a non-wall-clock host is exactly the case it
|
|
2515
|
+
* exists for — one whose reading can be *missing* rather than merely unusual. Every
|
|
2516
|
+
* comparison this class makes is arithmetic on a timestamp, so a single non-finite
|
|
2517
|
+
* reading does not degrade one field: `now - timestamp > ttlMs` becomes
|
|
2518
|
+
* `NaN > ttlMs` (false, so nothing expires), the adaptive window's
|
|
2519
|
+
* `elapsed >= ADAPTIVE_WINDOW_MS` never fires, and `getStats().ttlMs` reports
|
|
2520
|
+
* `NaN` — which `JSON.stringify` turns into `null`, so a health summary or a
|
|
2521
|
+
* diagnostics log would carry a null where a duration belongs. Measured on the
|
|
2522
|
+
* pre-fix class: `getStats().ttlMs` → `NaN`, serialized → `null`, and an id
|
|
2523
|
+
* stayed suppressed for as long as it stayed in the map.
|
|
2524
|
+
*
|
|
2525
|
+
* Holding the last finite reading is the degradation that changes least: time
|
|
2526
|
+
* stands still, expiry pauses, and the adaptive window keeps the value it last
|
|
2527
|
+
* computed. Substituting `Date.now()` would be worse — it silently mixes two
|
|
2528
|
+
* clocks — and zeroing would expire the whole map on the next call. This is the
|
|
2529
|
+
* same class of guard `trace.ts` carries on its own public clock, and that
|
|
2530
|
+
* `websocket.ts` and `replay-manager.ts` apply to their numeric options; it is one
|
|
2531
|
+
* helper rather than a condition per site because the broken input is the *clock*,
|
|
2532
|
+
* not any particular comparison.
|
|
2533
|
+
*/
|
|
2534
|
+
nowMs() {
|
|
2535
|
+
const reading = this.now();
|
|
2536
|
+
if (Number.isFinite(reading)) {
|
|
2537
|
+
this.lastFiniteNow = reading;
|
|
2538
|
+
return reading;
|
|
2539
|
+
}
|
|
2540
|
+
return this.lastFiniteNow;
|
|
2476
2541
|
}
|
|
2477
2542
|
/** True when a publication carrying `messageId` was already seen. Records the
|
|
2478
2543
|
* ID and updates counters/trace on acceptance. Disabled or ID-less messages
|
|
2479
2544
|
* always pass through. */
|
|
2480
2545
|
isDuplicate(messageId, topic) {
|
|
2481
2546
|
if (!this.enabled || !messageId) return false;
|
|
2482
|
-
const now = this.
|
|
2547
|
+
const now = this.nowMs();
|
|
2483
2548
|
const ttlMs = this.currentTtl();
|
|
2484
2549
|
for (const [id, timestamp] of this.seenMessageIds) {
|
|
2485
2550
|
if (now - timestamp > ttlMs) this.seenMessageIds.delete(id);
|
|
@@ -2530,12 +2595,12 @@ var DedupManager = class {
|
|
|
2530
2595
|
this.seenMessageIds.clear();
|
|
2531
2596
|
this.suppressed = 0;
|
|
2532
2597
|
this.accepted = 0;
|
|
2533
|
-
this.windowStartedAt = this.
|
|
2598
|
+
this.windowStartedAt = this.nowMs();
|
|
2534
2599
|
this.windowAccepted = 0;
|
|
2535
2600
|
}
|
|
2536
2601
|
/** Remove IDs whose timestamp predates the effective TTL cutoff. */
|
|
2537
2602
|
pruneExpired() {
|
|
2538
|
-
const cutoff = this.
|
|
2603
|
+
const cutoff = this.nowMs() - this.currentTtl();
|
|
2539
2604
|
for (const [id, timestamp] of this.seenMessageIds) {
|
|
2540
2605
|
if (timestamp < cutoff) this.seenMessageIds.delete(id);
|
|
2541
2606
|
}
|
|
@@ -2546,7 +2611,7 @@ var DedupManager = class {
|
|
|
2546
2611
|
* every ADAPTIVE_WINDOW_MS. */
|
|
2547
2612
|
currentTtl() {
|
|
2548
2613
|
if (!this.adaptiveBounds) return this.ttlMs;
|
|
2549
|
-
const now = this.
|
|
2614
|
+
const now = this.nowMs();
|
|
2550
2615
|
const elapsed = now - this.windowStartedAt;
|
|
2551
2616
|
if (elapsed >= ADAPTIVE_WINDOW_MS) {
|
|
2552
2617
|
this.windowStartedAt = now;
|
|
@@ -2570,7 +2635,7 @@ var SDK_VERSION = (
|
|
|
2570
2635
|
// declaration's type is `string | undefined` and the export's is `string`
|
|
2571
2636
|
// (dropping the conditional is TS2322, measured). Recorded here so the leg reads
|
|
2572
2637
|
// as a shape the harness cannot reach rather than as a case nobody exercised.
|
|
2573
|
-
true ? "0.21.
|
|
2638
|
+
true ? "0.21.44" : ""
|
|
2574
2639
|
);
|
|
2575
2640
|
|
|
2576
2641
|
// src/core/data-bus.ts
|
|
@@ -2690,7 +2755,7 @@ var CrossTabDataBus = class {
|
|
|
2690
2755
|
assertRecoveryOptions(recovery);
|
|
2691
2756
|
this.recoveryCooldownMs = recovery?.cooldownMs ?? 1e3;
|
|
2692
2757
|
this.recoveryMaxAttempts = recovery?.maxAttempts ?? Number.POSITIVE_INFINITY;
|
|
2693
|
-
this.now = dedup?.now ?? Date.now;
|
|
2758
|
+
this.now = holdLastFinite(dedup?.now ?? Date.now);
|
|
2694
2759
|
this.transport = transport;
|
|
2695
2760
|
this.initialConfig = initialConfig;
|
|
2696
2761
|
this.hasInitialConfig = "initialConfig" in options;
|
|
@@ -3749,6 +3814,19 @@ function formatWorkerTrace(worker) {
|
|
|
3749
3814
|
function formatRouteTrace(route) {
|
|
3750
3815
|
return `${route.topicKey}@${route.workerId}|confirmed=${route.confirmedAt !== void 0}`;
|
|
3751
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
|
+
}
|
|
3752
3830
|
|
|
3753
3831
|
// src/core/replay-persistence.ts
|
|
3754
3832
|
function createIndexedDbReplayPersistence(options) {
|