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.
@@ -613,6 +613,67 @@ var WorkerClusterRuntime = class {
613
613
  }
614
614
  this.wildcardPublishCache.set(topic, null);
615
615
  }
616
+ return this.sendControl(this.resolvePublishTarget(topic, topicKey), "PUBLISH", topic, topicKey, data, metadata);
617
+ }
618
+ /**
619
+ * Burst-friendly variant of `publish()`: packs up to N items into a single
620
+ * BroadcastChannel postMessage so the receiving owner dispatches them all in
621
+ * one tick. Per-item dedup / replay / dispatch ordering is preserved; items
622
+ * may carry their own messageId/timestamp. Empty batch is a no-op,
623
+ * single-item batch delegates to `publish()`.
624
+ */
625
+ publishBatch(topic, items) {
626
+ if (items.length === 0) return true;
627
+ if (items.length === 1) {
628
+ const single = items[0];
629
+ const metadata = single.messageId !== void 0 || single.timestamp !== void 0 ? {
630
+ ...single.messageId !== void 0 ? { messageId: single.messageId } : {},
631
+ ...single.timestamp !== void 0 ? { timestamp: single.timestamp } : {}
632
+ } : void 0;
633
+ return this.publish(topic, single.data, metadata);
634
+ }
635
+ const topicKey = this.rememberTopic(topic);
636
+ if (this.assignedTopics.has(topicKey)) {
637
+ for (const item of items) this.dispatchLocalPublish(topic, topicKey, item.data, item.messageId, item.timestamp);
638
+ return true;
639
+ }
640
+ const cachedPattern = this.wildcardPublishCache.get(topic);
641
+ if (cachedPattern !== void 0 && cachedPattern !== null && this.assignedTopics.has(cachedPattern)) {
642
+ for (const item of items) this.dispatchLocalPublish(topic, topicKey, item.data, item.messageId, item.timestamp);
643
+ return true;
644
+ }
645
+ if (cachedPattern === void 0) {
646
+ for (const pattern of this.assignedTopics.values()) {
647
+ if (pattern !== topic && topicMatchesPattern(pattern, topic)) {
648
+ this.wildcardPublishCache.set(topic, pattern);
649
+ for (const item of items) this.dispatchLocalPublish(topic, topicKey, item.data, item.messageId, item.timestamp);
650
+ return true;
651
+ }
652
+ }
653
+ this.wildcardPublishCache.set(topic, null);
654
+ }
655
+ const target = this.resolvePublishTarget(topic, topicKey);
656
+ if (target === this.workerId) {
657
+ for (const item of items) this.dispatchLocalPublish(topic, topicKey, item.data, item.messageId, item.timestamp);
658
+ return true;
659
+ }
660
+ return this.send({
661
+ type: "CONTROL",
662
+ sourceWorkerId: this.workerId,
663
+ targetWorkerId: target,
664
+ action: "PUBLISH",
665
+ topic,
666
+ topicKey,
667
+ items: items.map((item) => ({
668
+ data: item.data,
669
+ ...item.messageId !== void 0 ? { messageId: item.messageId } : {},
670
+ ...item.timestamp !== void 0 ? { timestamp: item.timestamp } : {}
671
+ }))
672
+ });
673
+ }
674
+ /** Resolve which worker should receive a PUBLISH for `topic`. Centralises the
675
+ * route-owner cache lookup so `publish()` and `publishBatch()` share one path. */
676
+ resolvePublishTarget(topic, topicKey) {
616
677
  const workers = this.readWorkers();
617
678
  const route = this.readRoute(topicKey);
618
679
  const cached = this.routeOwnerCache.get(topicKey);
@@ -620,9 +681,19 @@ var WorkerClusterRuntime = class {
620
681
  if (cachedLive) this.routeOwnerCacheHits += 1;
621
682
  else this.routeOwnerCacheMisses += 1;
622
683
  const target = cachedLive ? cached.workerId : this.routeOwnerIsLive(route, workers) ? route?.workerId ?? this.workerId : this.workerId;
623
- if (route && target === route.workerId) this.touchRouteOwnerCache(topicKey, { workerId: route.workerId, generation: route.generation });
624
- else this.routeOwnerCache.delete(topicKey);
625
- return this.sendControl(target, "PUBLISH", topic, topicKey, data, metadata);
684
+ if (route && target === route.workerId) {
685
+ this.touchRouteOwnerCache(topicKey, { workerId: route.workerId, generation: route.generation });
686
+ } else {
687
+ this.routeOwnerCache.delete(topicKey);
688
+ }
689
+ return target;
690
+ }
691
+ /** Fan out a single batched item to the local onControl path. */
692
+ dispatchLocalPublish(topic, topicKey, data, messageId, timestamp) {
693
+ void topicKey;
694
+ const meta = publicationMetadata(messageId, timestamp);
695
+ if (meta) this.handlers.onControl("PUBLISH", topic, data, meta.messageId, meta.timestamp);
696
+ else this.handlers.onControl("PUBLISH", topic, data);
626
697
  }
627
698
  /** True when `route` exists and its owner worker is among `workers`.
628
699
  * Shared by subscribe (skip re-assignment) and publish (route to owner).
@@ -631,9 +702,12 @@ var WorkerClusterRuntime = class {
631
702
  routeOwnerIsLive(route, workers) {
632
703
  return Boolean(route && workers.some((worker) => worker.workerId === route.workerId));
633
704
  }
634
- /** Broadcast an event to every tab — used to fan out transport publications. */
635
- broadcastEvent(eventType, payload) {
636
- this.send({ type: "EVENT", sourceWorkerId: this.workerId, eventType, payload });
705
+ /** Broadcast an event to every tab — used to fan out transport publications.
706
+ * `originTabId` (when set) is propagated across the BroadcastChannel hop so
707
+ * listeners can attribute the event to its source tab even after fan-out. */
708
+ broadcastEvent(eventType, payload, originTabId) {
709
+ const effectiveOriginTabId = originTabId ?? this.tabId;
710
+ this.send({ type: "EVENT", sourceWorkerId: this.workerId, eventType, payload, originTabId: effectiveOriginTabId });
637
711
  }
638
712
  isAssigned(topic) {
639
713
  const topicKey = createOpaqueKey(topic);
@@ -721,7 +795,7 @@ var WorkerClusterRuntime = class {
721
795
  case "ROUTE_RELEASED":
722
796
  return this.handleRouteReleasedMessage(message);
723
797
  case "EVENT":
724
- this.handlers.onEvent(message.eventType, message.payload, message.sourceWorkerId);
798
+ this.handlers.onEvent(message.eventType, message.payload, message.sourceWorkerId, message.originTabId);
725
799
  return;
726
800
  case "REGISTRY":
727
801
  default:
@@ -742,6 +816,15 @@ var WorkerClusterRuntime = class {
742
816
  if (this.releaseHandoffOnUnsubscribe(message)) return;
743
817
  break;
744
818
  case "PUBLISH":
819
+ if (message.items && message.items.length > 0) {
820
+ for (const item of message.items) {
821
+ const itemMeta = publicationMetadata(item.messageId, item.timestamp);
822
+ if (itemMeta) this.handlers.onControl("PUBLISH", message.topic, item.data, itemMeta.messageId, itemMeta.timestamp);
823
+ else this.handlers.onControl("PUBLISH", message.topic, item.data);
824
+ }
825
+ return;
826
+ }
827
+ break;
745
828
  default:
746
829
  break;
747
830
  }
@@ -1464,9 +1547,10 @@ var CrossTabDataBus = class {
1464
1547
  // typed `unknown` at the cluster boundary (the cluster is transport-
1465
1548
  // agnostic); here we narrow it to DataBusMessage — the sender is our
1466
1549
  // own broadcastEvent call, which always posts a DataBusMessage.
1467
- onEvent: (eventType, payload) => {
1550
+ onEvent: (eventType, payload, _sourceWorkerId, originTabId) => {
1468
1551
  if (eventType !== PUBLICATION_EVENT) return;
1469
- const message = payload;
1552
+ const incoming = payload;
1553
+ const message = incoming.originTabId !== void 0 ? incoming : originTabId !== void 0 ? { ...incoming, originTabId } : incoming;
1470
1554
  if (this.cluster.hasLocalSubscriber(message.topic)) this.dispatch(message);
1471
1555
  },
1472
1556
  onSuspend: () => {
@@ -1711,6 +1795,32 @@ var CrossTabDataBus = class {
1711
1795
  );
1712
1796
  }
1713
1797
  }
1798
+ /**
1799
+ * Burst-friendly variant of `publish()`: delivers `items` in a single
1800
+ * BroadcastChannel postMessage so the receiving owner can dispatch them all
1801
+ * in one tick. Per-item dedup / replay / ordering is preserved; each item
1802
+ * may carry its own `messageId` / `timestamp` via `options`. Empty array is
1803
+ * a no-op; single-item array delegates to `publish()`.
1804
+ */
1805
+ publishBatch(topic, items) {
1806
+ this.ensureStarted();
1807
+ if (items.length === 0) return;
1808
+ if (items.length === 1) {
1809
+ const first = items[0];
1810
+ this.publish(topic, first.data, first.options);
1811
+ return;
1812
+ }
1813
+ const mapped = items.map((item) => ({
1814
+ data: item.data,
1815
+ ...item.options?.messageId !== void 0 ? { messageId: item.options.messageId } : {},
1816
+ ...item.options?.timestamp !== void 0 ? { timestamp: item.options.timestamp } : {}
1817
+ }));
1818
+ if (!this.cluster.publishBatch(topic, mapped)) {
1819
+ this.reportError(
1820
+ new Error("Failed to send the batched publish control message to the owning worker.")
1821
+ );
1822
+ }
1823
+ }
1714
1824
  /** Register a handler that fires on every transport status change. Immediately invoked with the current status. */
1715
1825
  onStatus(handler) {
1716
1826
  this.statusHandlers.add(handler);
@@ -1803,9 +1913,10 @@ var CrossTabDataBus = class {
1803
1913
  this.trace.recordDiscarded(message.topic);
1804
1914
  return;
1805
1915
  }
1806
- this.cluster.broadcastEvent(PUBLICATION_EVENT, message);
1916
+ const stamped = message.originTabId === void 0 ? { ...message, originTabId: this.cluster.tabId } : message;
1917
+ this.cluster.broadcastEvent(PUBLICATION_EVENT, stamped, stamped.originTabId);
1807
1918
  if (this.cluster.hasLocalSubscriber(message.topic)) {
1808
- this.dispatch(message);
1919
+ this.dispatch(stamped);
1809
1920
  return;
1810
1921
  }
1811
1922
  this.trace.recordDiscarded(message.topic);