cross-tab-worker-databus 0.20.59 → 0.20.61
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 +21 -0
- package/dist/centrifuge.js +1 -1
- package/dist/{chunk-RAUX7RZC.js → chunk-VB4P6DS6.js} +123 -12
- package/dist/chunk-VB4P6DS6.js.map +7 -0
- package/dist/cjs/centrifuge.cjs +122 -11
- package/dist/cjs/centrifuge.cjs.map +2 -2
- package/dist/cjs/index.cjs +122 -11
- package/dist/cjs/index.cjs.map +2 -2
- package/dist/core/cluster.d.ts +26 -4
- package/dist/core/cluster.d.ts.map +1 -1
- package/dist/core/data-bus.d.ts +11 -0
- package/dist/core/data-bus.d.ts.map +1 -1
- package/dist/core/types.d.ts +22 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/docs/roadmap.md +12 -1
- package/docs/zh/roadmap.md +12 -1
- package/package.json +1 -1
- package/dist/chunk-RAUX7RZC.js.map +0 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.20.61] - 2026-09-05
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `originTabId?: string` on every `DataBusMessage` (and on the cluster `EVENT` wire frame) so cross-tab replay history is attributed to the tab that produced it.
|
|
7
|
+
- `WorkerClusterRuntime.broadcastEvent()` now defaults `originTabId` to the producing runtime's `tabId` so existing callers (and tests) get source-tab attribution without threading the value manually.
|
|
8
|
+
- `CrossTabDataBus.handleTransportMessage` stamps `originTabId = cluster.tabId` before broadcasting, so neighbors and IndexedDB-replayed late subscribers see the same attribution.
|
|
9
|
+
- Four unit tests under `CrossTabDataBus cross-tab replay consistency contract` cover the producing-tab stamp, local-handler parity, post-write late join with replay, and local-origin replay path.
|
|
10
|
+
- One e2e replay-persistence test now uses `toMatchObject` to tolerate the additional `originTabId` field on persisted entries.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- `WorkerClusterRuntime.onEvent` handler signature now includes a fourth `originTabId?: string` argument; existing call sites use `toMatchObject` so the extra argument does not break strict equality.
|
|
14
|
+
|
|
15
|
+
# [0.20.60] - 2026-09-04
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- `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.
|
|
19
|
+
- Per-item `messageId` / `timestamp` is preserved across the batched wire frame, so dedup / replay / ordering still apply per item in the original order.
|
|
20
|
+
- Empty batch is a no-op; single-item batch delegates to `publish()` so callers do not have to special-case the boundary.
|
|
21
|
+
- Added a `data bus hot paths` bench case (`publishBatch / 1000 messages / 10 per call`) to give an upper-bound reference for the burst path.
|
|
22
|
+
|
|
2
23
|
## [0.20.59] - 2026-09-04
|
|
3
24
|
|
|
4
25
|
### Added
|
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).
|
|
@@ -590,9 +661,12 @@ var WorkerClusterRuntime = class {
|
|
|
590
661
|
routeOwnerIsLive(route, workers) {
|
|
591
662
|
return Boolean(route && workers.some((worker) => worker.workerId === route.workerId));
|
|
592
663
|
}
|
|
593
|
-
/** Broadcast an event to every tab — used to fan out transport publications.
|
|
594
|
-
|
|
595
|
-
|
|
664
|
+
/** Broadcast an event to every tab — used to fan out transport publications.
|
|
665
|
+
* `originTabId` (when set) is propagated across the BroadcastChannel hop so
|
|
666
|
+
* listeners can attribute the event to its source tab even after fan-out. */
|
|
667
|
+
broadcastEvent(eventType, payload, originTabId) {
|
|
668
|
+
const effectiveOriginTabId = originTabId ?? this.tabId;
|
|
669
|
+
this.send({ type: "EVENT", sourceWorkerId: this.workerId, eventType, payload, originTabId: effectiveOriginTabId });
|
|
596
670
|
}
|
|
597
671
|
isAssigned(topic) {
|
|
598
672
|
const topicKey = createOpaqueKey(topic);
|
|
@@ -680,7 +754,7 @@ var WorkerClusterRuntime = class {
|
|
|
680
754
|
case "ROUTE_RELEASED":
|
|
681
755
|
return this.handleRouteReleasedMessage(message);
|
|
682
756
|
case "EVENT":
|
|
683
|
-
this.handlers.onEvent(message.eventType, message.payload, message.sourceWorkerId);
|
|
757
|
+
this.handlers.onEvent(message.eventType, message.payload, message.sourceWorkerId, message.originTabId);
|
|
684
758
|
return;
|
|
685
759
|
case "REGISTRY":
|
|
686
760
|
default:
|
|
@@ -701,6 +775,15 @@ var WorkerClusterRuntime = class {
|
|
|
701
775
|
if (this.releaseHandoffOnUnsubscribe(message)) return;
|
|
702
776
|
break;
|
|
703
777
|
case "PUBLISH":
|
|
778
|
+
if (message.items && message.items.length > 0) {
|
|
779
|
+
for (const item of message.items) {
|
|
780
|
+
const itemMeta = publicationMetadata(item.messageId, item.timestamp);
|
|
781
|
+
if (itemMeta) this.handlers.onControl("PUBLISH", message.topic, item.data, itemMeta.messageId, itemMeta.timestamp);
|
|
782
|
+
else this.handlers.onControl("PUBLISH", message.topic, item.data);
|
|
783
|
+
}
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
break;
|
|
704
787
|
default:
|
|
705
788
|
break;
|
|
706
789
|
}
|
|
@@ -1423,9 +1506,10 @@ var CrossTabDataBus = class {
|
|
|
1423
1506
|
// typed `unknown` at the cluster boundary (the cluster is transport-
|
|
1424
1507
|
// agnostic); here we narrow it to DataBusMessage — the sender is our
|
|
1425
1508
|
// own broadcastEvent call, which always posts a DataBusMessage.
|
|
1426
|
-
onEvent: (eventType, payload) => {
|
|
1509
|
+
onEvent: (eventType, payload, _sourceWorkerId, originTabId) => {
|
|
1427
1510
|
if (eventType !== PUBLICATION_EVENT) return;
|
|
1428
|
-
const
|
|
1511
|
+
const incoming = payload;
|
|
1512
|
+
const message = incoming.originTabId !== void 0 ? incoming : originTabId !== void 0 ? { ...incoming, originTabId } : incoming;
|
|
1429
1513
|
if (this.cluster.hasLocalSubscriber(message.topic)) this.dispatch(message);
|
|
1430
1514
|
},
|
|
1431
1515
|
onSuspend: () => {
|
|
@@ -1670,6 +1754,32 @@ var CrossTabDataBus = class {
|
|
|
1670
1754
|
);
|
|
1671
1755
|
}
|
|
1672
1756
|
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Burst-friendly variant of `publish()`: delivers `items` in a single
|
|
1759
|
+
* BroadcastChannel postMessage so the receiving owner can dispatch them all
|
|
1760
|
+
* in one tick. Per-item dedup / replay / ordering is preserved; each item
|
|
1761
|
+
* may carry its own `messageId` / `timestamp` via `options`. Empty array is
|
|
1762
|
+
* a no-op; single-item array delegates to `publish()`.
|
|
1763
|
+
*/
|
|
1764
|
+
publishBatch(topic, items) {
|
|
1765
|
+
this.ensureStarted();
|
|
1766
|
+
if (items.length === 0) return;
|
|
1767
|
+
if (items.length === 1) {
|
|
1768
|
+
const first = items[0];
|
|
1769
|
+
this.publish(topic, first.data, first.options);
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
const mapped = items.map((item) => ({
|
|
1773
|
+
data: item.data,
|
|
1774
|
+
...item.options?.messageId !== void 0 ? { messageId: item.options.messageId } : {},
|
|
1775
|
+
...item.options?.timestamp !== void 0 ? { timestamp: item.options.timestamp } : {}
|
|
1776
|
+
}));
|
|
1777
|
+
if (!this.cluster.publishBatch(topic, mapped)) {
|
|
1778
|
+
this.reportError(
|
|
1779
|
+
new Error("Failed to send the batched publish control message to the owning worker.")
|
|
1780
|
+
);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1673
1783
|
/** Register a handler that fires on every transport status change. Immediately invoked with the current status. */
|
|
1674
1784
|
onStatus(handler) {
|
|
1675
1785
|
this.statusHandlers.add(handler);
|
|
@@ -1762,9 +1872,10 @@ var CrossTabDataBus = class {
|
|
|
1762
1872
|
this.trace.recordDiscarded(message.topic);
|
|
1763
1873
|
return;
|
|
1764
1874
|
}
|
|
1765
|
-
this.cluster.
|
|
1875
|
+
const stamped = message.originTabId === void 0 ? { ...message, originTabId: this.cluster.tabId } : message;
|
|
1876
|
+
this.cluster.broadcastEvent(PUBLICATION_EVENT, stamped, stamped.originTabId);
|
|
1766
1877
|
if (this.cluster.hasLocalSubscriber(message.topic)) {
|
|
1767
|
-
this.dispatch(
|
|
1878
|
+
this.dispatch(stamped);
|
|
1768
1879
|
return;
|
|
1769
1880
|
}
|
|
1770
1881
|
this.trace.recordDiscarded(message.topic);
|
|
@@ -2182,4 +2293,4 @@ export {
|
|
|
2182
2293
|
parseDataBusPublication,
|
|
2183
2294
|
selectWorkerBackend
|
|
2184
2295
|
};
|
|
2185
|
-
//# sourceMappingURL=chunk-
|
|
2296
|
+
//# sourceMappingURL=chunk-VB4P6DS6.js.map
|