cross-tab-worker-databus 0.20.58 → 0.20.60
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 +14 -0
- package/dist/centrifuge.js +1 -1
- package/dist/{chunk-BWSJUUUP.js → chunk-KLHPSFVU.js} +136 -6
- package/dist/chunk-KLHPSFVU.js.map +7 -0
- package/dist/cjs/centrifuge.cjs +135 -5
- package/dist/cjs/centrifuge.cjs.map +2 -2
- package/dist/cjs/index.cjs +135 -5
- package/dist/cjs/index.cjs.map +2 -2
- package/dist/core/cluster.d.ts +17 -0
- package/dist/core/cluster.d.ts.map +1 -1
- package/dist/core/data-bus.d.ts +24 -0
- package/dist/core/data-bus.d.ts.map +1 -1
- package/dist/core/types.d.ts +12 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/docs/roadmap.md +9 -1
- package/docs/zh/roadmap.md +9 -1
- package/package.json +1 -1
- package/dist/chunk-BWSJUUUP.js.map +0 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
|
+
## [0.20.60] - 2026-09-04
|
|
3
|
+
|
|
4
|
+
### Added
|
|
5
|
+
- `publishBatch(topic, items)` on both `CrossTabDataBus` and `WorkerClusterRuntime` packs multiple items into a single BroadcastChannel postMessage so the receiving owner can dispatch them in one tick instead of one channel post per item.
|
|
6
|
+
- Per-item `messageId` / `timestamp` is preserved across the batched wire frame, so dedup / replay / ordering still apply per item in the original order.
|
|
7
|
+
- Empty batch is a no-op; single-item batch delegates to `publish()` so callers do not have to special-case the boundary.
|
|
8
|
+
- Added a `data bus hot paths` bench case (`publishBatch / 1000 messages / 10 per call`) to give an upper-bound reference for the burst path.
|
|
9
|
+
|
|
10
|
+
## [0.20.59] - 2026-09-04
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Extended `CrossTabDataBus.getRecoveryStats()` with `generation` (monotonic counter incremented on every successful transport open) and `lastSuccessAt` (timestamp of the most recent successful open, or `null` until the transport reaches `ready`).
|
|
14
|
+
- Added a unit test asserting generation/lastSuccessAt advance on the initial start and after a recovery, and stay stable across failed recovery attempts.
|
|
15
|
+
|
|
2
16
|
## [0.20.58] - 2026-09-04
|
|
3
17
|
|
|
4
18
|
### Changed
|
package/dist/centrifuge.js
CHANGED
|
@@ -572,6 +572,67 @@ var WorkerClusterRuntime = class {
|
|
|
572
572
|
}
|
|
573
573
|
this.wildcardPublishCache.set(topic, null);
|
|
574
574
|
}
|
|
575
|
+
return this.sendControl(this.resolvePublishTarget(topic, topicKey), "PUBLISH", topic, topicKey, data, metadata);
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Burst-friendly variant of `publish()`: packs up to N items into a single
|
|
579
|
+
* BroadcastChannel postMessage so the receiving owner dispatches them all in
|
|
580
|
+
* one tick. Per-item dedup / replay / dispatch ordering is preserved; items
|
|
581
|
+
* may carry their own messageId/timestamp. Empty batch is a no-op,
|
|
582
|
+
* single-item batch delegates to `publish()`.
|
|
583
|
+
*/
|
|
584
|
+
publishBatch(topic, items) {
|
|
585
|
+
if (items.length === 0) return true;
|
|
586
|
+
if (items.length === 1) {
|
|
587
|
+
const single = items[0];
|
|
588
|
+
const metadata = single.messageId !== void 0 || single.timestamp !== void 0 ? {
|
|
589
|
+
...single.messageId !== void 0 ? { messageId: single.messageId } : {},
|
|
590
|
+
...single.timestamp !== void 0 ? { timestamp: single.timestamp } : {}
|
|
591
|
+
} : void 0;
|
|
592
|
+
return this.publish(topic, single.data, metadata);
|
|
593
|
+
}
|
|
594
|
+
const topicKey = this.rememberTopic(topic);
|
|
595
|
+
if (this.assignedTopics.has(topicKey)) {
|
|
596
|
+
for (const item of items) this.dispatchLocalPublish(topic, topicKey, item.data, item.messageId, item.timestamp);
|
|
597
|
+
return true;
|
|
598
|
+
}
|
|
599
|
+
const cachedPattern = this.wildcardPublishCache.get(topic);
|
|
600
|
+
if (cachedPattern !== void 0 && cachedPattern !== null && this.assignedTopics.has(cachedPattern)) {
|
|
601
|
+
for (const item of items) this.dispatchLocalPublish(topic, topicKey, item.data, item.messageId, item.timestamp);
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
if (cachedPattern === void 0) {
|
|
605
|
+
for (const pattern of this.assignedTopics.values()) {
|
|
606
|
+
if (pattern !== topic && topicMatchesPattern(pattern, topic)) {
|
|
607
|
+
this.wildcardPublishCache.set(topic, pattern);
|
|
608
|
+
for (const item of items) this.dispatchLocalPublish(topic, topicKey, item.data, item.messageId, item.timestamp);
|
|
609
|
+
return true;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
this.wildcardPublishCache.set(topic, null);
|
|
613
|
+
}
|
|
614
|
+
const target = this.resolvePublishTarget(topic, topicKey);
|
|
615
|
+
if (target === this.workerId) {
|
|
616
|
+
for (const item of items) this.dispatchLocalPublish(topic, topicKey, item.data, item.messageId, item.timestamp);
|
|
617
|
+
return true;
|
|
618
|
+
}
|
|
619
|
+
return this.send({
|
|
620
|
+
type: "CONTROL",
|
|
621
|
+
sourceWorkerId: this.workerId,
|
|
622
|
+
targetWorkerId: target,
|
|
623
|
+
action: "PUBLISH",
|
|
624
|
+
topic,
|
|
625
|
+
topicKey,
|
|
626
|
+
items: items.map((item) => ({
|
|
627
|
+
data: item.data,
|
|
628
|
+
...item.messageId !== void 0 ? { messageId: item.messageId } : {},
|
|
629
|
+
...item.timestamp !== void 0 ? { timestamp: item.timestamp } : {}
|
|
630
|
+
}))
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
/** Resolve which worker should receive a PUBLISH for `topic`. Centralises the
|
|
634
|
+
* route-owner cache lookup so `publish()` and `publishBatch()` share one path. */
|
|
635
|
+
resolvePublishTarget(topic, topicKey) {
|
|
575
636
|
const workers = this.readWorkers();
|
|
576
637
|
const route = this.readRoute(topicKey);
|
|
577
638
|
const cached = this.routeOwnerCache.get(topicKey);
|
|
@@ -579,9 +640,19 @@ var WorkerClusterRuntime = class {
|
|
|
579
640
|
if (cachedLive) this.routeOwnerCacheHits += 1;
|
|
580
641
|
else this.routeOwnerCacheMisses += 1;
|
|
581
642
|
const target = cachedLive ? cached.workerId : this.routeOwnerIsLive(route, workers) ? route?.workerId ?? this.workerId : this.workerId;
|
|
582
|
-
if (route && target === route.workerId)
|
|
583
|
-
|
|
584
|
-
|
|
643
|
+
if (route && target === route.workerId) {
|
|
644
|
+
this.touchRouteOwnerCache(topicKey, { workerId: route.workerId, generation: route.generation });
|
|
645
|
+
} else {
|
|
646
|
+
this.routeOwnerCache.delete(topicKey);
|
|
647
|
+
}
|
|
648
|
+
return target;
|
|
649
|
+
}
|
|
650
|
+
/** Fan out a single batched item to the local onControl path. */
|
|
651
|
+
dispatchLocalPublish(topic, topicKey, data, messageId, timestamp) {
|
|
652
|
+
void topicKey;
|
|
653
|
+
const meta = publicationMetadata(messageId, timestamp);
|
|
654
|
+
if (meta) this.handlers.onControl("PUBLISH", topic, data, meta.messageId, meta.timestamp);
|
|
655
|
+
else this.handlers.onControl("PUBLISH", topic, data);
|
|
585
656
|
}
|
|
586
657
|
/** True when `route` exists and its owner worker is among `workers`.
|
|
587
658
|
* Shared by subscribe (skip re-assignment) and publish (route to owner).
|
|
@@ -701,6 +772,15 @@ var WorkerClusterRuntime = class {
|
|
|
701
772
|
if (this.releaseHandoffOnUnsubscribe(message)) return;
|
|
702
773
|
break;
|
|
703
774
|
case "PUBLISH":
|
|
775
|
+
if (message.items && message.items.length > 0) {
|
|
776
|
+
for (const item of message.items) {
|
|
777
|
+
const itemMeta = publicationMetadata(item.messageId, item.timestamp);
|
|
778
|
+
if (itemMeta) this.handlers.onControl("PUBLISH", message.topic, item.data, itemMeta.messageId, itemMeta.timestamp);
|
|
779
|
+
else this.handlers.onControl("PUBLISH", message.topic, item.data);
|
|
780
|
+
}
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
break;
|
|
704
784
|
default:
|
|
705
785
|
break;
|
|
706
786
|
}
|
|
@@ -1316,6 +1396,13 @@ var CrossTabDataBus = class {
|
|
|
1316
1396
|
// a transport reopen succeeds so traces can correlate repeated failures.
|
|
1317
1397
|
recoveryAttempt = 0;
|
|
1318
1398
|
recoveryExhausted = false;
|
|
1399
|
+
/** Monotonic generation incremented on every successful transport open.
|
|
1400
|
+
* Stays in lockstep with `lastSuccessAt` so callers can detect that the
|
|
1401
|
+
* transport has been reopened even if the timestamp window is short. */
|
|
1402
|
+
recoveryGeneration = 0;
|
|
1403
|
+
/** Timestamp of the most recent successful transport open. Null until the
|
|
1404
|
+
* transport has reached the `ready` state at least once. */
|
|
1405
|
+
lastSuccessAt = null;
|
|
1319
1406
|
// True while the tab is hidden so an in-flight transport start does not mark
|
|
1320
1407
|
// the transport ready after suspendTransport() has stopped it.
|
|
1321
1408
|
suspended = false;
|
|
@@ -1509,7 +1596,11 @@ var CrossTabDataBus = class {
|
|
|
1509
1596
|
if (this.status === "error") {
|
|
1510
1597
|
throw new Error("Transport failed during startup.");
|
|
1511
1598
|
}
|
|
1512
|
-
if (!this.suspended && !this.stopping)
|
|
1599
|
+
if (!this.suspended && !this.stopping) {
|
|
1600
|
+
this.recoveryGeneration += 1;
|
|
1601
|
+
this.lastSuccessAt = this.now();
|
|
1602
|
+
this.transportReady = true;
|
|
1603
|
+
}
|
|
1513
1604
|
});
|
|
1514
1605
|
}).catch((error) => {
|
|
1515
1606
|
if (stopClusterOnFailure) this.started = false;
|
|
@@ -1659,6 +1750,32 @@ var CrossTabDataBus = class {
|
|
|
1659
1750
|
);
|
|
1660
1751
|
}
|
|
1661
1752
|
}
|
|
1753
|
+
/**
|
|
1754
|
+
* Burst-friendly variant of `publish()`: delivers `items` in a single
|
|
1755
|
+
* BroadcastChannel postMessage so the receiving owner can dispatch them all
|
|
1756
|
+
* in one tick. Per-item dedup / replay / ordering is preserved; each item
|
|
1757
|
+
* may carry its own `messageId` / `timestamp` via `options`. Empty array is
|
|
1758
|
+
* a no-op; single-item array delegates to `publish()`.
|
|
1759
|
+
*/
|
|
1760
|
+
publishBatch(topic, items) {
|
|
1761
|
+
this.ensureStarted();
|
|
1762
|
+
if (items.length === 0) return;
|
|
1763
|
+
if (items.length === 1) {
|
|
1764
|
+
const first = items[0];
|
|
1765
|
+
this.publish(topic, first.data, first.options);
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
const mapped = items.map((item) => ({
|
|
1769
|
+
data: item.data,
|
|
1770
|
+
...item.options?.messageId !== void 0 ? { messageId: item.options.messageId } : {},
|
|
1771
|
+
...item.options?.timestamp !== void 0 ? { timestamp: item.options.timestamp } : {}
|
|
1772
|
+
}));
|
|
1773
|
+
if (!this.cluster.publishBatch(topic, mapped)) {
|
|
1774
|
+
this.reportError(
|
|
1775
|
+
new Error("Failed to send the batched publish control message to the owning worker.")
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1662
1779
|
/** Register a handler that fires on every transport status change. Immediately invoked with the current status. */
|
|
1663
1780
|
onStatus(handler) {
|
|
1664
1781
|
this.statusHandlers.add(handler);
|
|
@@ -1679,9 +1796,22 @@ var CrossTabDataBus = class {
|
|
|
1679
1796
|
return this.status;
|
|
1680
1797
|
}
|
|
1681
1798
|
/** Return the current automatic transport recovery state. `hasError` means a transport error is currently retained. */
|
|
1799
|
+
/** Return the current automatic transport recovery state plus diagnostics.
|
|
1800
|
+
* `generation` increments on every successful transport open (initial start
|
|
1801
|
+
* and every recovery); `lastSuccessAt` is the timestamp of the most recent
|
|
1802
|
+
* successful open, or `null` until the transport reaches `ready`. */
|
|
1682
1803
|
getRecoveryStats() {
|
|
1683
1804
|
const errorMessage = this.lastError instanceof Error ? this.lastError.message : this.lastError === null ? null : String(this.lastError);
|
|
1684
|
-
return {
|
|
1805
|
+
return {
|
|
1806
|
+
attempt: this.recoveryAttempt,
|
|
1807
|
+
exhausted: this.recoveryExhausted,
|
|
1808
|
+
maxAttempts: this.recoveryMaxAttempts,
|
|
1809
|
+
hasError: this.lastError !== null,
|
|
1810
|
+
errorMessage,
|
|
1811
|
+
errorAt: this.lastErrorAt,
|
|
1812
|
+
generation: this.recoveryGeneration,
|
|
1813
|
+
lastSuccessAt: this.lastSuccessAt
|
|
1814
|
+
};
|
|
1685
1815
|
}
|
|
1686
1816
|
/** Snapshot of the cluster state (workers, routes, assignments).
|
|
1687
1817
|
* For diagnostics only — the returned object is a shallow copy but
|
|
@@ -2158,4 +2288,4 @@ export {
|
|
|
2158
2288
|
parseDataBusPublication,
|
|
2159
2289
|
selectWorkerBackend
|
|
2160
2290
|
};
|
|
2161
|
-
//# sourceMappingURL=chunk-
|
|
2291
|
+
//# sourceMappingURL=chunk-KLHPSFVU.js.map
|