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/dist/cjs/centrifuge.cjs
CHANGED
|
@@ -588,6 +588,67 @@ var WorkerClusterRuntime = class {
|
|
|
588
588
|
}
|
|
589
589
|
this.wildcardPublishCache.set(topic, null);
|
|
590
590
|
}
|
|
591
|
+
return this.sendControl(this.resolvePublishTarget(topic, topicKey), "PUBLISH", topic, topicKey, data, metadata);
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Burst-friendly variant of `publish()`: packs up to N items into a single
|
|
595
|
+
* BroadcastChannel postMessage so the receiving owner dispatches them all in
|
|
596
|
+
* one tick. Per-item dedup / replay / dispatch ordering is preserved; items
|
|
597
|
+
* may carry their own messageId/timestamp. Empty batch is a no-op,
|
|
598
|
+
* single-item batch delegates to `publish()`.
|
|
599
|
+
*/
|
|
600
|
+
publishBatch(topic, items) {
|
|
601
|
+
if (items.length === 0) return true;
|
|
602
|
+
if (items.length === 1) {
|
|
603
|
+
const single = items[0];
|
|
604
|
+
const metadata = single.messageId !== void 0 || single.timestamp !== void 0 ? {
|
|
605
|
+
...single.messageId !== void 0 ? { messageId: single.messageId } : {},
|
|
606
|
+
...single.timestamp !== void 0 ? { timestamp: single.timestamp } : {}
|
|
607
|
+
} : void 0;
|
|
608
|
+
return this.publish(topic, single.data, metadata);
|
|
609
|
+
}
|
|
610
|
+
const topicKey = this.rememberTopic(topic);
|
|
611
|
+
if (this.assignedTopics.has(topicKey)) {
|
|
612
|
+
for (const item of items) this.dispatchLocalPublish(topic, topicKey, item.data, item.messageId, item.timestamp);
|
|
613
|
+
return true;
|
|
614
|
+
}
|
|
615
|
+
const cachedPattern = this.wildcardPublishCache.get(topic);
|
|
616
|
+
if (cachedPattern !== void 0 && cachedPattern !== null && this.assignedTopics.has(cachedPattern)) {
|
|
617
|
+
for (const item of items) this.dispatchLocalPublish(topic, topicKey, item.data, item.messageId, item.timestamp);
|
|
618
|
+
return true;
|
|
619
|
+
}
|
|
620
|
+
if (cachedPattern === void 0) {
|
|
621
|
+
for (const pattern of this.assignedTopics.values()) {
|
|
622
|
+
if (pattern !== topic && topicMatchesPattern(pattern, topic)) {
|
|
623
|
+
this.wildcardPublishCache.set(topic, pattern);
|
|
624
|
+
for (const item of items) this.dispatchLocalPublish(topic, topicKey, item.data, item.messageId, item.timestamp);
|
|
625
|
+
return true;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
this.wildcardPublishCache.set(topic, null);
|
|
629
|
+
}
|
|
630
|
+
const target = this.resolvePublishTarget(topic, topicKey);
|
|
631
|
+
if (target === this.workerId) {
|
|
632
|
+
for (const item of items) this.dispatchLocalPublish(topic, topicKey, item.data, item.messageId, item.timestamp);
|
|
633
|
+
return true;
|
|
634
|
+
}
|
|
635
|
+
return this.send({
|
|
636
|
+
type: "CONTROL",
|
|
637
|
+
sourceWorkerId: this.workerId,
|
|
638
|
+
targetWorkerId: target,
|
|
639
|
+
action: "PUBLISH",
|
|
640
|
+
topic,
|
|
641
|
+
topicKey,
|
|
642
|
+
items: items.map((item) => ({
|
|
643
|
+
data: item.data,
|
|
644
|
+
...item.messageId !== void 0 ? { messageId: item.messageId } : {},
|
|
645
|
+
...item.timestamp !== void 0 ? { timestamp: item.timestamp } : {}
|
|
646
|
+
}))
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
/** Resolve which worker should receive a PUBLISH for `topic`. Centralises the
|
|
650
|
+
* route-owner cache lookup so `publish()` and `publishBatch()` share one path. */
|
|
651
|
+
resolvePublishTarget(topic, topicKey) {
|
|
591
652
|
const workers = this.readWorkers();
|
|
592
653
|
const route = this.readRoute(topicKey);
|
|
593
654
|
const cached = this.routeOwnerCache.get(topicKey);
|
|
@@ -595,9 +656,19 @@ var WorkerClusterRuntime = class {
|
|
|
595
656
|
if (cachedLive) this.routeOwnerCacheHits += 1;
|
|
596
657
|
else this.routeOwnerCacheMisses += 1;
|
|
597
658
|
const target = cachedLive ? cached.workerId : this.routeOwnerIsLive(route, workers) ? route?.workerId ?? this.workerId : this.workerId;
|
|
598
|
-
if (route && target === route.workerId)
|
|
599
|
-
|
|
600
|
-
|
|
659
|
+
if (route && target === route.workerId) {
|
|
660
|
+
this.touchRouteOwnerCache(topicKey, { workerId: route.workerId, generation: route.generation });
|
|
661
|
+
} else {
|
|
662
|
+
this.routeOwnerCache.delete(topicKey);
|
|
663
|
+
}
|
|
664
|
+
return target;
|
|
665
|
+
}
|
|
666
|
+
/** Fan out a single batched item to the local onControl path. */
|
|
667
|
+
dispatchLocalPublish(topic, topicKey, data, messageId, timestamp) {
|
|
668
|
+
void topicKey;
|
|
669
|
+
const meta = publicationMetadata(messageId, timestamp);
|
|
670
|
+
if (meta) this.handlers.onControl("PUBLISH", topic, data, meta.messageId, meta.timestamp);
|
|
671
|
+
else this.handlers.onControl("PUBLISH", topic, data);
|
|
601
672
|
}
|
|
602
673
|
/** True when `route` exists and its owner worker is among `workers`.
|
|
603
674
|
* Shared by subscribe (skip re-assignment) and publish (route to owner).
|
|
@@ -717,6 +788,15 @@ var WorkerClusterRuntime = class {
|
|
|
717
788
|
if (this.releaseHandoffOnUnsubscribe(message)) return;
|
|
718
789
|
break;
|
|
719
790
|
case "PUBLISH":
|
|
791
|
+
if (message.items && message.items.length > 0) {
|
|
792
|
+
for (const item of message.items) {
|
|
793
|
+
const itemMeta = publicationMetadata(item.messageId, item.timestamp);
|
|
794
|
+
if (itemMeta) this.handlers.onControl("PUBLISH", message.topic, item.data, itemMeta.messageId, itemMeta.timestamp);
|
|
795
|
+
else this.handlers.onControl("PUBLISH", message.topic, item.data);
|
|
796
|
+
}
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
break;
|
|
720
800
|
default:
|
|
721
801
|
break;
|
|
722
802
|
}
|
|
@@ -1332,6 +1412,13 @@ var CrossTabDataBus = class {
|
|
|
1332
1412
|
// a transport reopen succeeds so traces can correlate repeated failures.
|
|
1333
1413
|
recoveryAttempt = 0;
|
|
1334
1414
|
recoveryExhausted = false;
|
|
1415
|
+
/** Monotonic generation incremented on every successful transport open.
|
|
1416
|
+
* Stays in lockstep with `lastSuccessAt` so callers can detect that the
|
|
1417
|
+
* transport has been reopened even if the timestamp window is short. */
|
|
1418
|
+
recoveryGeneration = 0;
|
|
1419
|
+
/** Timestamp of the most recent successful transport open. Null until the
|
|
1420
|
+
* transport has reached the `ready` state at least once. */
|
|
1421
|
+
lastSuccessAt = null;
|
|
1335
1422
|
// True while the tab is hidden so an in-flight transport start does not mark
|
|
1336
1423
|
// the transport ready after suspendTransport() has stopped it.
|
|
1337
1424
|
suspended = false;
|
|
@@ -1525,7 +1612,11 @@ var CrossTabDataBus = class {
|
|
|
1525
1612
|
if (this.status === "error") {
|
|
1526
1613
|
throw new Error("Transport failed during startup.");
|
|
1527
1614
|
}
|
|
1528
|
-
if (!this.suspended && !this.stopping)
|
|
1615
|
+
if (!this.suspended && !this.stopping) {
|
|
1616
|
+
this.recoveryGeneration += 1;
|
|
1617
|
+
this.lastSuccessAt = this.now();
|
|
1618
|
+
this.transportReady = true;
|
|
1619
|
+
}
|
|
1529
1620
|
});
|
|
1530
1621
|
}).catch((error) => {
|
|
1531
1622
|
if (stopClusterOnFailure) this.started = false;
|
|
@@ -1675,6 +1766,32 @@ var CrossTabDataBus = class {
|
|
|
1675
1766
|
);
|
|
1676
1767
|
}
|
|
1677
1768
|
}
|
|
1769
|
+
/**
|
|
1770
|
+
* Burst-friendly variant of `publish()`: delivers `items` in a single
|
|
1771
|
+
* BroadcastChannel postMessage so the receiving owner can dispatch them all
|
|
1772
|
+
* in one tick. Per-item dedup / replay / ordering is preserved; each item
|
|
1773
|
+
* may carry its own `messageId` / `timestamp` via `options`. Empty array is
|
|
1774
|
+
* a no-op; single-item array delegates to `publish()`.
|
|
1775
|
+
*/
|
|
1776
|
+
publishBatch(topic, items) {
|
|
1777
|
+
this.ensureStarted();
|
|
1778
|
+
if (items.length === 0) return;
|
|
1779
|
+
if (items.length === 1) {
|
|
1780
|
+
const first = items[0];
|
|
1781
|
+
this.publish(topic, first.data, first.options);
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
const mapped = items.map((item) => ({
|
|
1785
|
+
data: item.data,
|
|
1786
|
+
...item.options?.messageId !== void 0 ? { messageId: item.options.messageId } : {},
|
|
1787
|
+
...item.options?.timestamp !== void 0 ? { timestamp: item.options.timestamp } : {}
|
|
1788
|
+
}));
|
|
1789
|
+
if (!this.cluster.publishBatch(topic, mapped)) {
|
|
1790
|
+
this.reportError(
|
|
1791
|
+
new Error("Failed to send the batched publish control message to the owning worker.")
|
|
1792
|
+
);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1678
1795
|
/** Register a handler that fires on every transport status change. Immediately invoked with the current status. */
|
|
1679
1796
|
onStatus(handler) {
|
|
1680
1797
|
this.statusHandlers.add(handler);
|
|
@@ -1695,9 +1812,22 @@ var CrossTabDataBus = class {
|
|
|
1695
1812
|
return this.status;
|
|
1696
1813
|
}
|
|
1697
1814
|
/** Return the current automatic transport recovery state. `hasError` means a transport error is currently retained. */
|
|
1815
|
+
/** Return the current automatic transport recovery state plus diagnostics.
|
|
1816
|
+
* `generation` increments on every successful transport open (initial start
|
|
1817
|
+
* and every recovery); `lastSuccessAt` is the timestamp of the most recent
|
|
1818
|
+
* successful open, or `null` until the transport reaches `ready`. */
|
|
1698
1819
|
getRecoveryStats() {
|
|
1699
1820
|
const errorMessage = this.lastError instanceof Error ? this.lastError.message : this.lastError === null ? null : String(this.lastError);
|
|
1700
|
-
return {
|
|
1821
|
+
return {
|
|
1822
|
+
attempt: this.recoveryAttempt,
|
|
1823
|
+
exhausted: this.recoveryExhausted,
|
|
1824
|
+
maxAttempts: this.recoveryMaxAttempts,
|
|
1825
|
+
hasError: this.lastError !== null,
|
|
1826
|
+
errorMessage,
|
|
1827
|
+
errorAt: this.lastErrorAt,
|
|
1828
|
+
generation: this.recoveryGeneration,
|
|
1829
|
+
lastSuccessAt: this.lastSuccessAt
|
|
1830
|
+
};
|
|
1701
1831
|
}
|
|
1702
1832
|
/** Snapshot of the cluster state (workers, routes, assignments).
|
|
1703
1833
|
* For diagnostics only — the returned object is a shallow copy but
|