cross-tab-worker-databus 0.20.90 → 0.20.92
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 +41 -0
- package/dist/centrifuge-session.d.ts.map +1 -1
- package/dist/centrifuge.js +7 -9
- package/dist/centrifuge.js.map +2 -2
- package/dist/centrifuge.shared.worker.js +6 -8
- package/dist/centrifuge.shared.worker.js.map +2 -2
- package/dist/centrifuge.worker.js +6 -8
- package/dist/centrifuge.worker.js.map +2 -2
- package/dist/{chunk-L24ETFVK.js → chunk-QB74C4EH.js} +249 -91
- package/dist/chunk-QB74C4EH.js.map +7 -0
- package/dist/cjs/centrifuge.cjs +254 -98
- package/dist/cjs/centrifuge.cjs.map +2 -2
- package/dist/cjs/index.cjs +258 -91
- package/dist/cjs/index.cjs.map +2 -2
- package/dist/core/cluster.d.ts +8 -5
- package/dist/core/cluster.d.ts.map +1 -1
- package/dist/core/data-bus.d.ts +21 -1
- package/dist/core/data-bus.d.ts.map +1 -1
- package/dist/core/replay-manager.d.ts +17 -3
- package/dist/core/replay-manager.d.ts.map +1 -1
- package/dist/index.js +11 -2
- package/dist/index.js.map +2 -2
- package/dist/websocket.d.ts +3 -0
- package/dist/websocket.d.ts.map +1 -1
- package/docs/architecture.md +11 -6
- package/docs/benchmarks.md +8 -8
- package/docs/capabilities.md +3 -1
- package/docs/release-checklist.md +2 -2
- package/docs/roadmap.md +19 -1
- package/docs/zh/architecture.md +11 -6
- package/docs/zh/benchmarks.md +8 -8
- package/docs/zh/capabilities.md +3 -1
- package/docs/zh/release-checklist.md +2 -2
- package/docs/zh/roadmap.md +19 -1
- package/package.json +1 -1
- package/dist/chunk-L24ETFVK.js.map +0 -7
|
@@ -612,6 +612,9 @@ var WorkerClusterRuntime = class {
|
|
|
612
612
|
started = false;
|
|
613
613
|
suspended = false;
|
|
614
614
|
lifecycleListening = false;
|
|
615
|
+
/** Invalidates an in-flight pageshow resume when a synchronous onResume
|
|
616
|
+
* callback stops or pauses the cluster before activate() is reached. */
|
|
617
|
+
lifecycleGeneration = 0;
|
|
615
618
|
currentRecord;
|
|
616
619
|
constructor(options) {
|
|
617
620
|
assertClusterOptions(options);
|
|
@@ -648,8 +651,14 @@ var WorkerClusterRuntime = class {
|
|
|
648
651
|
/** Start the cluster: register, listen for lifecycle events, and begin heartbeats. */
|
|
649
652
|
start() {
|
|
650
653
|
if (this.started) return;
|
|
651
|
-
this.
|
|
654
|
+
this.lifecycleGeneration += 1;
|
|
652
655
|
this.addLifecycleListeners();
|
|
656
|
+
if (this.environment.getVisibilityState() === TAB_VISIBILITY.HIDDEN) {
|
|
657
|
+
this.suspended = true;
|
|
658
|
+
this.handlers.onSuspend?.();
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
this.suspended = false;
|
|
653
662
|
this.activate();
|
|
654
663
|
}
|
|
655
664
|
/**
|
|
@@ -660,6 +669,7 @@ var WorkerClusterRuntime = class {
|
|
|
660
669
|
* stop() path where callers expect every Set/Map to be empty afterwards.
|
|
661
670
|
*/
|
|
662
671
|
stop() {
|
|
672
|
+
this.lifecycleGeneration += 1;
|
|
663
673
|
if (!this.started && !this.suspended) return;
|
|
664
674
|
this.pause();
|
|
665
675
|
this.flushStorage();
|
|
@@ -706,7 +716,11 @@ var WorkerClusterRuntime = class {
|
|
|
706
716
|
* to other workers, remove our worker record, and close the channel.
|
|
707
717
|
*/
|
|
708
718
|
pause() {
|
|
709
|
-
|
|
719
|
+
this.lifecycleGeneration += 1;
|
|
720
|
+
if (!this.started) {
|
|
721
|
+
if (this.lifecycleListening) this.suspended = true;
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
710
724
|
this.started = false;
|
|
711
725
|
this.suspended = true;
|
|
712
726
|
if (this.heartbeatHandle !== null) this.environment.clearInterval(this.heartbeatHandle);
|
|
@@ -723,10 +737,12 @@ var WorkerClusterRuntime = class {
|
|
|
723
737
|
const channel = this.channel;
|
|
724
738
|
this.channel = null;
|
|
725
739
|
this.handlers.onSuspend?.();
|
|
726
|
-
if (
|
|
727
|
-
globalThis.setTimeout
|
|
728
|
-
|
|
729
|
-
|
|
740
|
+
if (channel) {
|
|
741
|
+
if (typeof globalThis.setTimeout === "function") {
|
|
742
|
+
globalThis.setTimeout(() => channel.close(), 0);
|
|
743
|
+
} else {
|
|
744
|
+
channel.close();
|
|
745
|
+
}
|
|
730
746
|
}
|
|
731
747
|
}
|
|
732
748
|
/** Update the worker's connection status and persist the change. */
|
|
@@ -1019,8 +1035,10 @@ var WorkerClusterRuntime = class {
|
|
|
1019
1035
|
handlePageHide = () => this.pause();
|
|
1020
1036
|
handlePageShow = () => {
|
|
1021
1037
|
if (!this.suspended) return;
|
|
1038
|
+
const generation = ++this.lifecycleGeneration;
|
|
1022
1039
|
this.suspended = false;
|
|
1023
1040
|
this.handlers.onResume?.();
|
|
1041
|
+
if (generation !== this.lifecycleGeneration) return;
|
|
1024
1042
|
this.activate();
|
|
1025
1043
|
};
|
|
1026
1044
|
handleVisibilityChange = () => {
|
|
@@ -1075,10 +1093,16 @@ var WorkerClusterRuntime = class {
|
|
|
1075
1093
|
if (message.targetWorkerId !== this.workerId) return;
|
|
1076
1094
|
this.rememberTopic(message.topic);
|
|
1077
1095
|
switch (message.action) {
|
|
1078
|
-
case CONTROL_ACTION.SUBSCRIBE:
|
|
1096
|
+
case CONTROL_ACTION.SUBSCRIBE: {
|
|
1097
|
+
const route = this.readRoute(message.topicKey);
|
|
1098
|
+
if (route && route.workerId !== this.workerId) return;
|
|
1099
|
+
if (route?.workerId === this.workerId && route.handoffFromWorkerId !== void 0 && route.confirmedAt === void 0) {
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1079
1102
|
this.assignedTopics.set(message.topicKey, message.topic);
|
|
1080
1103
|
this.confirmRoute(message.topicKey);
|
|
1081
1104
|
break;
|
|
1105
|
+
}
|
|
1082
1106
|
case CONTROL_ACTION.UNSUBSCRIBE:
|
|
1083
1107
|
if (this.releaseHandoffOnUnsubscribe(message)) return;
|
|
1084
1108
|
break;
|
|
@@ -1139,8 +1163,8 @@ var WorkerClusterRuntime = class {
|
|
|
1139
1163
|
}
|
|
1140
1164
|
/**
|
|
1141
1165
|
* Accept a graceful handoff only when the route still points to this worker,
|
|
1142
|
-
* the release comes from the recorded previous owner, and the generation
|
|
1143
|
-
*
|
|
1166
|
+
* the release comes from the recorded previous owner, and the generation
|
|
1167
|
+
* exactly matches ours. Any other ROUTE_RELEASED is stale and dropped.
|
|
1144
1168
|
*/
|
|
1145
1169
|
handleRouteReleasedMessage(message) {
|
|
1146
1170
|
if (message.targetWorkerId !== this.workerId) return;
|
|
@@ -1153,11 +1177,11 @@ var WorkerClusterRuntime = class {
|
|
|
1153
1177
|
}
|
|
1154
1178
|
/** A ROUTE_RELEASED is stale (and must be dropped) unless the route still
|
|
1155
1179
|
* points to us, the release comes from the recorded previous owner, and
|
|
1156
|
-
* the release generation
|
|
1157
|
-
* earlier handoff round
|
|
1158
|
-
*
|
|
1180
|
+
* the release generation exactly matches ours. A delayed ACK from either an
|
|
1181
|
+
* earlier or later handoff round belongs to a different route and must not
|
|
1182
|
+
* confirm the current round. */
|
|
1159
1183
|
isStaleRouteRelease(route, message) {
|
|
1160
|
-
return route.workerId !== this.workerId || route.handoffFromWorkerId !== message.sourceWorkerId || message.generation
|
|
1184
|
+
return route.workerId !== this.workerId || route.handoffFromWorkerId !== message.sourceWorkerId || message.generation !== route.generation;
|
|
1161
1185
|
}
|
|
1162
1186
|
/** True when an unconfirmed handoff route has been stuck longer than a
|
|
1163
1187
|
* worker TTL. The ACK for a live handoff is posted synchronously with the
|
|
@@ -1784,7 +1808,7 @@ var ReplayManager = class {
|
|
|
1784
1808
|
this.trace = deps.trace;
|
|
1785
1809
|
this.onPersistenceError = deps.onPersistenceError;
|
|
1786
1810
|
this.onDispatchError = deps.onDispatchError;
|
|
1787
|
-
|
|
1811
|
+
void this.requestHydration();
|
|
1788
1812
|
}
|
|
1789
1813
|
deps;
|
|
1790
1814
|
buffers;
|
|
@@ -1803,7 +1827,18 @@ var ReplayManager = class {
|
|
|
1803
1827
|
retryGeneration = 0;
|
|
1804
1828
|
pendingReplayPersistence = [];
|
|
1805
1829
|
persistenceFlushScheduled = false;
|
|
1806
|
-
hydration
|
|
1830
|
+
/** Current hydration operation, if one belongs to the active lifecycle. */
|
|
1831
|
+
hydration = null;
|
|
1832
|
+
/** Invalidates a load from a superseded suspend/reset lifecycle. */
|
|
1833
|
+
hydrationEpoch = 0;
|
|
1834
|
+
/** Whether the active lifecycle has finished (or deliberately skipped) hydration. */
|
|
1835
|
+
hydrationComplete = false;
|
|
1836
|
+
/** A failed load is retried by the next explicit start(), not every replay request. */
|
|
1837
|
+
hydrationFailed = false;
|
|
1838
|
+
/** Mutations that must be applied to a load already in flight. */
|
|
1839
|
+
hydrationClearAll = false;
|
|
1840
|
+
hydrationClearedTopics = /* @__PURE__ */ new Set();
|
|
1841
|
+
hydrationClearBefore = null;
|
|
1807
1842
|
/** Coalesced retention cleanup: the newest cutoff wins while one is running. */
|
|
1808
1843
|
retentionCleanup = null;
|
|
1809
1844
|
retentionCutoff = null;
|
|
@@ -1857,7 +1892,7 @@ var ReplayManager = class {
|
|
|
1857
1892
|
if (!this.buffers) return;
|
|
1858
1893
|
const limit = typeof replayOption === "number" ? Math.min(Math.floor(replayOption), this.maxPerTopic) : this.maxPerTopic;
|
|
1859
1894
|
if (this.persistence) {
|
|
1860
|
-
void this.
|
|
1895
|
+
void this.requestHydration().then(() => {
|
|
1861
1896
|
if (isHandlerActive?.() ?? true) this.deliver(topic, limit, handler);
|
|
1862
1897
|
});
|
|
1863
1898
|
return;
|
|
@@ -1869,22 +1904,24 @@ var ReplayManager = class {
|
|
|
1869
1904
|
* clearTopic), and prune durable history. */
|
|
1870
1905
|
onTopicUnsubscribed(topic) {
|
|
1871
1906
|
if (!this.buffers) return;
|
|
1907
|
+
const clearing = this.persistence?.clearTopic ? this.withPersistenceRetry(PERSISTENCE_OPERATION.CLEAR_TOPIC, () => this.persistence.clearTopic(topic)) : null;
|
|
1908
|
+
this.hydrationClearedTopics.add(topic);
|
|
1872
1909
|
this.buffers.delete(topic);
|
|
1873
1910
|
this.pendingReplayPersistence = this.pendingReplayPersistence.filter((message) => message.topic !== topic);
|
|
1874
|
-
if (this.
|
|
1875
|
-
void this.withPersistenceRetry(PERSISTENCE_OPERATION.CLEAR_TOPIC, () => this.persistence.clearTopic(topic)).catch((error) => this.onPersistenceError(error));
|
|
1876
|
-
}
|
|
1911
|
+
if (clearing) void clearing.catch((error) => this.onPersistenceError(error));
|
|
1877
1912
|
}
|
|
1878
1913
|
/** Clear all in-memory replay buffers and, when supported, durable history.
|
|
1879
1914
|
* Reports persistence failures and rethrows, mirroring the public API
|
|
1880
1915
|
* contract that callers can observe a failed clear. */
|
|
1881
1916
|
async clearAll() {
|
|
1882
1917
|
if (!this.buffers) return;
|
|
1918
|
+
const clearing = this.persistence?.clear ? this.withPersistenceRetry(PERSISTENCE_OPERATION.CLEAR, () => this.persistence.clear()) : null;
|
|
1919
|
+
this.hydrationClearAll = true;
|
|
1883
1920
|
this.buffers.clear();
|
|
1884
1921
|
this.pendingReplayPersistence = [];
|
|
1885
|
-
if (
|
|
1922
|
+
if (clearing) {
|
|
1886
1923
|
try {
|
|
1887
|
-
await
|
|
1924
|
+
await clearing;
|
|
1888
1925
|
} catch (error) {
|
|
1889
1926
|
this.onPersistenceError(error);
|
|
1890
1927
|
throw error;
|
|
@@ -1894,11 +1931,13 @@ var ReplayManager = class {
|
|
|
1894
1931
|
/** Clear replay history for one exact topic, including durable storage. */
|
|
1895
1932
|
async clearTopic(topic) {
|
|
1896
1933
|
if (!this.buffers) return;
|
|
1934
|
+
const clearing = this.persistence?.clearTopic ? this.withPersistenceRetry(PERSISTENCE_OPERATION.CLEAR_TOPIC, () => this.persistence.clearTopic(topic)) : null;
|
|
1935
|
+
this.hydrationClearedTopics.add(topic);
|
|
1897
1936
|
this.buffers.delete(topic);
|
|
1898
1937
|
this.pendingReplayPersistence = this.pendingReplayPersistence.filter((message) => message.topic !== topic);
|
|
1899
|
-
if (
|
|
1938
|
+
if (clearing) {
|
|
1900
1939
|
try {
|
|
1901
|
-
await
|
|
1940
|
+
await clearing;
|
|
1902
1941
|
} catch (error) {
|
|
1903
1942
|
this.onPersistenceError(error);
|
|
1904
1943
|
throw error;
|
|
@@ -1908,7 +1947,9 @@ var ReplayManager = class {
|
|
|
1908
1947
|
/** Remove replay entries older than an epoch-millisecond cutoff. */
|
|
1909
1948
|
async clearBefore(timestamp) {
|
|
1910
1949
|
if (!Number.isFinite(timestamp)) throw new TypeError("timestamp must be finite.");
|
|
1950
|
+
const clearing = this.persistence?.clearBefore ? this.withPersistenceRetry(PERSISTENCE_OPERATION.CLEAR_BEFORE, () => this.persistence.clearBefore(timestamp)) : null;
|
|
1911
1951
|
if (this.buffers) {
|
|
1952
|
+
this.hydrationClearBefore = this.hydrationClearBefore === null ? timestamp : Math.max(this.hydrationClearBefore, timestamp);
|
|
1912
1953
|
for (const [topic, messages] of this.buffers) {
|
|
1913
1954
|
const kept = messages.filter((message) => message.timestamp === void 0 || message.timestamp >= timestamp);
|
|
1914
1955
|
if (kept.length) this.buffers.set(topic, kept);
|
|
@@ -1918,9 +1959,9 @@ var ReplayManager = class {
|
|
|
1918
1959
|
this.pendingReplayPersistence = this.pendingReplayPersistence.filter(
|
|
1919
1960
|
(message) => message.timestamp === void 0 || message.timestamp >= timestamp
|
|
1920
1961
|
);
|
|
1921
|
-
if (
|
|
1962
|
+
if (clearing) {
|
|
1922
1963
|
try {
|
|
1923
|
-
await
|
|
1964
|
+
await clearing;
|
|
1924
1965
|
} catch (error) {
|
|
1925
1966
|
this.onPersistenceError(error);
|
|
1926
1967
|
throw error;
|
|
@@ -1930,6 +1971,11 @@ var ReplayManager = class {
|
|
|
1930
1971
|
/** Start the periodic retention sweep. No-op when no durable retention
|
|
1931
1972
|
* config makes it necessary. */
|
|
1932
1973
|
start() {
|
|
1974
|
+
if (this.hydrationFailed) {
|
|
1975
|
+
this.hydrationComplete = false;
|
|
1976
|
+
this.hydrationFailed = false;
|
|
1977
|
+
}
|
|
1978
|
+
void this.requestHydration();
|
|
1933
1979
|
if (this.retentionTimer || !this.retentionMs || !this.retentionSweepMs || !this.persistence?.clearBefore) return;
|
|
1934
1980
|
this.retentionTimer = setInterval(() => {
|
|
1935
1981
|
this.scheduleRetentionCleanup(this.now() - this.retentionMs);
|
|
@@ -1944,13 +1990,25 @@ var ReplayManager = class {
|
|
|
1944
1990
|
* or stopped bus does not keep hammering the store) and stop the sweep. */
|
|
1945
1991
|
suspend() {
|
|
1946
1992
|
this.retryGeneration += 1;
|
|
1993
|
+
if (this.hydration) {
|
|
1994
|
+
this.hydrationEpoch += 1;
|
|
1995
|
+
this.hydration = null;
|
|
1996
|
+
this.hydrationComplete = false;
|
|
1997
|
+
}
|
|
1947
1998
|
this.pendingReplayPersistence = [];
|
|
1948
1999
|
this.retentionCutoff = null;
|
|
1949
2000
|
this.stop();
|
|
1950
2001
|
}
|
|
1951
|
-
/** Drop all in-memory buffers
|
|
2002
|
+
/** Drop all in-memory buffers and require hydration for the next lifecycle. */
|
|
1952
2003
|
resetBuffers() {
|
|
1953
2004
|
this.buffers?.clear();
|
|
2005
|
+
this.hydrationEpoch += 1;
|
|
2006
|
+
this.hydration = null;
|
|
2007
|
+
this.hydrationComplete = false;
|
|
2008
|
+
this.hydrationFailed = false;
|
|
2009
|
+
this.hydrationClearAll = false;
|
|
2010
|
+
this.hydrationClearedTopics.clear();
|
|
2011
|
+
this.hydrationClearBefore = null;
|
|
1954
2012
|
}
|
|
1955
2013
|
/** Buffer occupancy for diagnostics. `bytes` is an approximate in-memory
|
|
1956
2014
|
* payload footprint (same heuristic as adaptive load weighting), computed on
|
|
@@ -2002,38 +2060,78 @@ var ReplayManager = class {
|
|
|
2002
2060
|
void this.withPersistenceRetry(PERSISTENCE_OPERATION.APPEND, () => this.persistence.appendBatch(batch)).catch((error) => this.onPersistenceError(error));
|
|
2003
2061
|
});
|
|
2004
2062
|
}
|
|
2005
|
-
/**
|
|
2063
|
+
/** Start the active lifecycle's one-shot hydration, if it has not completed
|
|
2064
|
+
* or deliberately skipped hydration already. */
|
|
2065
|
+
requestHydration() {
|
|
2066
|
+
if (!this.buffers || !this.persistence) {
|
|
2067
|
+
this.hydrationComplete = true;
|
|
2068
|
+
return Promise.resolve();
|
|
2069
|
+
}
|
|
2070
|
+
if (this.hydration) return this.hydration;
|
|
2071
|
+
if (this.hydrationComplete) return Promise.resolve();
|
|
2072
|
+
const epoch = this.hydrationEpoch;
|
|
2073
|
+
const generation = this.retryGeneration;
|
|
2074
|
+
const operation = this.hydrate(epoch, generation);
|
|
2075
|
+
this.hydration = operation;
|
|
2076
|
+
return operation;
|
|
2077
|
+
}
|
|
2078
|
+
/** Load durable history into the in-memory rings once per lifecycle, pruning
|
|
2006
2079
|
* entries past the retention window first. Failures are reported but do not
|
|
2007
2080
|
* block startup — the bus runs with whatever survived. */
|
|
2008
|
-
async hydrate() {
|
|
2081
|
+
async hydrate(epoch, generation) {
|
|
2009
2082
|
if (!this.buffers || !this.persistence) {
|
|
2010
2083
|
return;
|
|
2011
2084
|
}
|
|
2012
2085
|
try {
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
if (
|
|
2020
|
-
|
|
2021
|
-
|
|
2086
|
+
try {
|
|
2087
|
+
if (this.retentionMs !== void 0 && this.persistence.clearBefore) {
|
|
2088
|
+
await this.withPersistenceRetry(PERSISTENCE_OPERATION.CLEAR_BEFORE, () => this.persistence.clearBefore(this.now() - this.retentionMs));
|
|
2089
|
+
}
|
|
2090
|
+
const loaded = await this.withPersistenceRetry(PERSISTENCE_OPERATION.LOAD, () => this.persistence.load());
|
|
2091
|
+
if (generation !== this.retryGeneration) throw new PersistenceRetryCancelledError();
|
|
2092
|
+
if (epoch !== this.hydrationEpoch) return;
|
|
2093
|
+
const loadedByTopic = /* @__PURE__ */ new Map();
|
|
2094
|
+
for (const message of loaded) {
|
|
2095
|
+
if (this.hydrationClearAll || this.hydrationClearedTopics.has(message.topic) || this.hydrationClearBefore !== null && message.timestamp !== void 0 && message.timestamp < this.hydrationClearBefore) continue;
|
|
2096
|
+
let buffer = loadedByTopic.get(message.topic);
|
|
2097
|
+
if (!buffer) {
|
|
2098
|
+
buffer = [];
|
|
2099
|
+
loadedByTopic.set(message.topic, buffer);
|
|
2100
|
+
}
|
|
2101
|
+
buffer.push(message);
|
|
2102
|
+
}
|
|
2103
|
+
for (const [topic, durableBuffer] of loadedByTopic) {
|
|
2104
|
+
const liveBuffer = this.buffers.get(topic);
|
|
2105
|
+
this.buffers.set(topic, liveBuffer ? [...durableBuffer, ...liveBuffer] : durableBuffer);
|
|
2106
|
+
}
|
|
2107
|
+
const hydrationNow = this.now();
|
|
2108
|
+
for (const [topic, buffer] of this.buffers) {
|
|
2109
|
+
const pruned = pruneReplayHistory(buffer, {
|
|
2110
|
+
maxPerTopic: this.maxPerTopic,
|
|
2111
|
+
pruneStrategy: this.pruneStrategy,
|
|
2112
|
+
retentionMs: this.retentionMs,
|
|
2113
|
+
now: hydrationNow
|
|
2114
|
+
});
|
|
2115
|
+
if (pruned !== buffer) this.buffers.set(topic, pruned);
|
|
2022
2116
|
}
|
|
2023
|
-
|
|
2117
|
+
this.hydrationComplete = true;
|
|
2118
|
+
this.hydrationFailed = false;
|
|
2119
|
+
} catch (error) {
|
|
2120
|
+
if (generation !== this.retryGeneration) {
|
|
2121
|
+
this.onPersistenceError(
|
|
2122
|
+
error instanceof PersistenceRetryCancelledError ? error : new PersistenceRetryCancelledError()
|
|
2123
|
+
);
|
|
2124
|
+
return;
|
|
2125
|
+
}
|
|
2126
|
+
if (epoch !== this.hydrationEpoch) return;
|
|
2127
|
+
this.onPersistenceError(error);
|
|
2128
|
+
this.hydrationComplete = true;
|
|
2129
|
+
this.hydrationFailed = true;
|
|
2024
2130
|
}
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
maxPerTopic: this.maxPerTopic,
|
|
2029
|
-
pruneStrategy: this.pruneStrategy,
|
|
2030
|
-
retentionMs: this.retentionMs,
|
|
2031
|
-
now: hydrationNow
|
|
2032
|
-
});
|
|
2033
|
-
if (pruned !== buffer) this.buffers.set(topic, pruned);
|
|
2131
|
+
} finally {
|
|
2132
|
+
if (this.hydrationEpoch === epoch && this.retryGeneration === generation) {
|
|
2133
|
+
this.hydration = null;
|
|
2034
2134
|
}
|
|
2035
|
-
} catch (error) {
|
|
2036
|
-
this.onPersistenceError(error);
|
|
2037
2135
|
}
|
|
2038
2136
|
}
|
|
2039
2137
|
/** Coalesce retention cleanup: the newest cutoff wins while one pass runs,
|
|
@@ -2057,7 +2155,7 @@ var ReplayManager = class {
|
|
|
2057
2155
|
}
|
|
2058
2156
|
})().finally(() => {
|
|
2059
2157
|
this.retentionCleanup = null;
|
|
2060
|
-
if (this.retentionCutoff !== null
|
|
2158
|
+
if (this.retentionCutoff !== null) {
|
|
2061
2159
|
this.scheduleRetentionCleanup(this.retentionCutoff);
|
|
2062
2160
|
}
|
|
2063
2161
|
});
|
|
@@ -2210,7 +2308,7 @@ var DedupManager = class {
|
|
|
2210
2308
|
};
|
|
2211
2309
|
|
|
2212
2310
|
// src/core/version.ts
|
|
2213
|
-
var SDK_VERSION = true ? "0.20.
|
|
2311
|
+
var SDK_VERSION = true ? "0.20.92" : "";
|
|
2214
2312
|
|
|
2215
2313
|
// src/core/data-bus.ts
|
|
2216
2314
|
var DEFAULT_REPLAY_MAX_PER_TOPIC = 100;
|
|
@@ -2286,11 +2384,21 @@ var CrossTabDataBus = class {
|
|
|
2286
2384
|
recoveryGateRelease = null;
|
|
2287
2385
|
recoveryTimer = null;
|
|
2288
2386
|
recoveryTimerToken = 0;
|
|
2387
|
+
// Invalidates operations parked on the recovery gate when a hide/stop
|
|
2388
|
+
// supersedes the recovery cycle. Their microtask may run after an immediate
|
|
2389
|
+
// explicit start has cleared `suspended`, so a state check alone is not
|
|
2390
|
+
// enough to keep stale work from reaching the replacement transport.
|
|
2391
|
+
recoveryCancellationToken = 0;
|
|
2289
2392
|
// Once an automatic attempt fails, an explicit transport operation may
|
|
2290
2393
|
// recover immediately instead of waiting for the next paced attempt. The
|
|
2291
2394
|
// gate still stays closed so the operation cannot reach the failed
|
|
2292
2395
|
// transport; it is released by the successful on-demand reopen.
|
|
2293
2396
|
recoveryDemandAllowed = false;
|
|
2397
|
+
// Number of transport operations currently parked behind `recoveryGate`.
|
|
2398
|
+
// When an automatic attempt fails, these already-parked operations are
|
|
2399
|
+
// themselves demand: the failure path starts an on-demand reopen instead of
|
|
2400
|
+
// stranding them until some unrelated future operation arrives.
|
|
2401
|
+
recoveryWaiters = 0;
|
|
2294
2402
|
/** Monotonic generation incremented on every successful transport open.
|
|
2295
2403
|
* Stays in lockstep with `lastSuccessAt` so callers can detect that the
|
|
2296
2404
|
* transport has been reopened even if the timestamp window is short. */
|
|
@@ -2392,6 +2500,7 @@ var CrossTabDataBus = class {
|
|
|
2392
2500
|
// own broadcastEvent call, which always posts a DataBusMessage.
|
|
2393
2501
|
onEvent: (eventType, payload, _sourceWorkerId, originTabId) => {
|
|
2394
2502
|
if (eventType !== PUBLICATION_EVENT) return;
|
|
2503
|
+
if (typeof payload !== "object" || payload === null || typeof payload.topic !== "string") return;
|
|
2395
2504
|
const incoming = payload;
|
|
2396
2505
|
const message = incoming.originTabId !== void 0 ? incoming : originTabId !== void 0 ? { ...incoming, originTabId } : incoming;
|
|
2397
2506
|
if (this.cluster.hasLocalSubscriber(message.topic)) this.dispatch(message);
|
|
@@ -2404,7 +2513,7 @@ var CrossTabDataBus = class {
|
|
|
2404
2513
|
this.suspendTransport();
|
|
2405
2514
|
},
|
|
2406
2515
|
onResume: () => {
|
|
2407
|
-
this.resumeSuspendedResources();
|
|
2516
|
+
if (!this.resumeSuspendedResources()) return;
|
|
2408
2517
|
this.resumeTransport();
|
|
2409
2518
|
},
|
|
2410
2519
|
onDiagnostic: (event) => {
|
|
@@ -2433,11 +2542,13 @@ var CrossTabDataBus = class {
|
|
|
2433
2542
|
const transportDown = !this.transportReady || this.status === WORKER_STATUS.ERROR || this.status === WORKER_STATUS.DISCONNECTED;
|
|
2434
2543
|
if (!transportDown) return Promise.resolve();
|
|
2435
2544
|
this.activeConfig = config;
|
|
2436
|
-
this.resetFailureState();
|
|
2545
|
+
this.resetFailureState(true);
|
|
2437
2546
|
const resumingFromSuspend = this.suspended;
|
|
2438
|
-
if (resumingFromSuspend
|
|
2547
|
+
if (resumingFromSuspend && !this.resumeSuspendedResources()) {
|
|
2548
|
+
return this.stopPromise ?? Promise.resolve();
|
|
2549
|
+
}
|
|
2439
2550
|
const opening2 = this.reopenTransport();
|
|
2440
|
-
if (resumingFromSuspend) this.cluster.start();
|
|
2551
|
+
if (resumingFromSuspend && !this.stopping && !this.suspended) this.cluster.start();
|
|
2441
2552
|
return opening2;
|
|
2442
2553
|
}
|
|
2443
2554
|
this.started = true;
|
|
@@ -2445,12 +2556,6 @@ var CrossTabDataBus = class {
|
|
|
2445
2556
|
this.suspended = false;
|
|
2446
2557
|
this.activeConfig = config;
|
|
2447
2558
|
this.resetFailureState();
|
|
2448
|
-
this.trace.start();
|
|
2449
|
-
this.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.START });
|
|
2450
|
-
this.startDedupSweep();
|
|
2451
|
-
this.replayManager.start();
|
|
2452
|
-
this.updateStatus(WORKER_STATUS.CONNECTING);
|
|
2453
|
-
this.cluster.start();
|
|
2454
2559
|
const lifecycleEpoch = ++this.lifecycleEpoch;
|
|
2455
2560
|
const opening = this.openTransport(
|
|
2456
2561
|
config,
|
|
@@ -2459,7 +2564,17 @@ var CrossTabDataBus = class {
|
|
|
2459
2564
|
lifecycleEpoch
|
|
2460
2565
|
);
|
|
2461
2566
|
this.startPromise = opening;
|
|
2567
|
+
this.trace.start();
|
|
2568
|
+
this.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.START });
|
|
2569
|
+
if (lifecycleEpoch !== this.lifecycleEpoch || this.stopping) return opening;
|
|
2570
|
+
this.startDedupSweep();
|
|
2571
|
+
this.replayManager.start();
|
|
2572
|
+
this.updateStatus(WORKER_STATUS.CONNECTING);
|
|
2573
|
+
if (lifecycleEpoch !== this.lifecycleEpoch || this.stopping) return opening;
|
|
2574
|
+
this.cluster.start();
|
|
2575
|
+
if (lifecycleEpoch !== this.lifecycleEpoch || this.stopping) return opening;
|
|
2462
2576
|
for (const topic of this.topicHandlers.keys()) {
|
|
2577
|
+
if (lifecycleEpoch !== this.lifecycleEpoch || this.stopping) break;
|
|
2463
2578
|
this.cluster.subscribe(topic);
|
|
2464
2579
|
}
|
|
2465
2580
|
void opening.then(
|
|
@@ -2521,27 +2636,49 @@ var CrossTabDataBus = class {
|
|
|
2521
2636
|
/** Cancel a pending automatic retry when an explicit lifecycle transition
|
|
2522
2637
|
* supersedes it. The released gate re-enters runTransport(), which then
|
|
2523
2638
|
* follows the newest start/stop/suspend intent. */
|
|
2524
|
-
cancelScheduledRecovery() {
|
|
2639
|
+
cancelScheduledRecovery(invalidateParkedOperations = false, releaseGate = true) {
|
|
2525
2640
|
this.recoveryTimerToken += 1;
|
|
2641
|
+
if (invalidateParkedOperations) this.recoveryCancellationToken += 1;
|
|
2526
2642
|
if (this.recoveryTimer !== null) {
|
|
2527
2643
|
clearTimeout(this.recoveryTimer);
|
|
2528
2644
|
this.recoveryTimer = null;
|
|
2529
2645
|
}
|
|
2530
|
-
this.releaseRecoveryGate();
|
|
2646
|
+
if (releaseGate) this.releaseRecoveryGate();
|
|
2531
2647
|
}
|
|
2532
2648
|
/** Keep the recovery gate closed after a failed attempt while allowing the
|
|
2533
2649
|
* next explicit transport operation to start an immediate on-demand reopen.
|
|
2534
|
-
* If no gate/successor retry remains, release any waiters.
|
|
2535
|
-
|
|
2650
|
+
* If no gate/successor retry remains, release any waiters.
|
|
2651
|
+
*
|
|
2652
|
+
* `kickParkedWaiters` is set only when the failure is an automatic attempt: an
|
|
2653
|
+
* operation that was already parked on the gate is itself demand, so it must
|
|
2654
|
+
* not wait for some unrelated future operation. A failed *on-demand* reopen
|
|
2655
|
+
* passes `false`, so it re-arms the flag for a later operation instead of
|
|
2656
|
+
* looping on its own failure. */
|
|
2657
|
+
allowDemandRecovery(kickParkedWaiters = false) {
|
|
2536
2658
|
if (this.recoveryGate !== null && this.started && !this.stopping && !this.suspended && this.status === WORKER_STATUS.ERROR) {
|
|
2537
2659
|
this.recoveryDemandAllowed = true;
|
|
2660
|
+
if (kickParkedWaiters && this.recoveryWaiters > 0) this.startDemandRecovery();
|
|
2538
2661
|
return;
|
|
2539
2662
|
}
|
|
2540
2663
|
this.releaseRecoveryGate();
|
|
2541
2664
|
}
|
|
2665
|
+
/** Start one on-demand reopen if a failed attempt has left parked operations
|
|
2666
|
+
* and enabled demand recovery. Consumes the demand token so at most one
|
|
2667
|
+
* reopen is issued; every waiter stays behind the gate until it succeeds. */
|
|
2668
|
+
startDemandRecovery() {
|
|
2669
|
+
if (!this.recoveryDemandAllowed) return;
|
|
2670
|
+
if (this.status !== WORKER_STATUS.ERROR || this.suspended || this.stopping) return;
|
|
2671
|
+
this.recoveryDemandAllowed = false;
|
|
2672
|
+
const opening = this.reopenTransport();
|
|
2673
|
+
void opening.then(
|
|
2674
|
+
() => this.releaseRecoveryGate(),
|
|
2675
|
+
() => this.allowDemandRecovery()
|
|
2676
|
+
);
|
|
2677
|
+
}
|
|
2542
2678
|
/** Reset failure and recovery diagnostics for a new explicit start session. */
|
|
2543
|
-
resetFailureState() {
|
|
2544
|
-
this.cancelScheduledRecovery();
|
|
2679
|
+
resetFailureState(preserveRecoveryGate = false) {
|
|
2680
|
+
this.cancelScheduledRecovery(false, !preserveRecoveryGate);
|
|
2681
|
+
if (preserveRecoveryGate) this.recoveryDemandAllowed = false;
|
|
2545
2682
|
this.lastError = null;
|
|
2546
2683
|
this.lastErrorAt = null;
|
|
2547
2684
|
this.lastFailure = null;
|
|
@@ -2881,22 +3018,36 @@ var CrossTabDataBus = class {
|
|
|
2881
3018
|
if (!this.started && !this.startPromise && !this.pendingStop && !this.transportReady) {
|
|
2882
3019
|
return Promise.resolve();
|
|
2883
3020
|
}
|
|
3021
|
+
let resolveGate;
|
|
3022
|
+
const stopGate = new Promise((resolve) => {
|
|
3023
|
+
resolveGate = resolve;
|
|
3024
|
+
});
|
|
3025
|
+
this.stopPromise = stopGate;
|
|
3026
|
+
this.beginStop();
|
|
2884
3027
|
const stopPromise = this.performStop();
|
|
2885
|
-
this.stopPromise = stopPromise;
|
|
2886
3028
|
void stopPromise.then(
|
|
2887
3029
|
() => {
|
|
2888
|
-
if (this.stopPromise ===
|
|
3030
|
+
if (this.stopPromise === stopGate) this.stopPromise = null;
|
|
3031
|
+
resolveGate();
|
|
2889
3032
|
},
|
|
2890
3033
|
() => {
|
|
2891
|
-
if (this.stopPromise ===
|
|
3034
|
+
if (this.stopPromise === stopGate) this.stopPromise = null;
|
|
3035
|
+
resolveGate();
|
|
2892
3036
|
}
|
|
2893
3037
|
);
|
|
2894
|
-
return
|
|
3038
|
+
return stopGate;
|
|
2895
3039
|
}
|
|
2896
|
-
|
|
3040
|
+
/**
|
|
3041
|
+
* Synchronous teardown prelude. Flips `stopping` (the authoritative
|
|
3042
|
+
* in-flight signal), cancels scheduled work, releases handlers, and emits the
|
|
3043
|
+
* observable STOP lifecycle event. stop() calls it after the shared gate is
|
|
3044
|
+
* installed but in the same tick, so the stop still takes effect immediately
|
|
3045
|
+
* while a re-entrant stop() from the STOP event shares the one teardown.
|
|
3046
|
+
*/
|
|
3047
|
+
beginStop() {
|
|
2897
3048
|
this.lifecycleEpoch += 1;
|
|
2898
3049
|
this.stopping = true;
|
|
2899
|
-
this.cancelScheduledRecovery();
|
|
3050
|
+
this.cancelScheduledRecovery(true);
|
|
2900
3051
|
this.replayManager.suspend();
|
|
2901
3052
|
this.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.STOP });
|
|
2902
3053
|
this.trace.stop();
|
|
@@ -2904,6 +3055,8 @@ var CrossTabDataBus = class {
|
|
|
2904
3055
|
this.topicHandlers.clear();
|
|
2905
3056
|
this.replayManager.resetBuffers();
|
|
2906
3057
|
this.cluster.stop();
|
|
3058
|
+
}
|
|
3059
|
+
async performStop() {
|
|
2907
3060
|
try {
|
|
2908
3061
|
await this.startPromise?.catch(() => void 0);
|
|
2909
3062
|
const pendingStop = this.pendingStop;
|
|
@@ -2920,8 +3073,6 @@ var CrossTabDataBus = class {
|
|
|
2920
3073
|
this.transportReady = false;
|
|
2921
3074
|
this.startPromise = null;
|
|
2922
3075
|
this.pendingStop = null;
|
|
2923
|
-
this.lastError = null;
|
|
2924
|
-
this.lastErrorAt = null;
|
|
2925
3076
|
this.activeConfig = void 0;
|
|
2926
3077
|
this.recoveryAttempt = 0;
|
|
2927
3078
|
this.recoveryExhausted = false;
|
|
@@ -3018,7 +3169,9 @@ var CrossTabDataBus = class {
|
|
|
3018
3169
|
const opening = this.reopenTransport(attempt);
|
|
3019
3170
|
void opening.then(
|
|
3020
3171
|
() => this.releaseRecoveryGate(),
|
|
3021
|
-
|
|
3172
|
+
// Operations already parked on the gate are demand: run one
|
|
3173
|
+
// on-demand reopen now instead of waiting for an unrelated event.
|
|
3174
|
+
() => this.allowDemandRecovery(true)
|
|
3022
3175
|
);
|
|
3023
3176
|
}, this.recoveryCooldownMs);
|
|
3024
3177
|
}
|
|
@@ -3121,10 +3274,13 @@ var CrossTabDataBus = class {
|
|
|
3121
3274
|
* pageshow path and explicit start() must run this so an explicit resume
|
|
3122
3275
|
* cannot leave trace metrics and periodic cleanup timers permanently off. */
|
|
3123
3276
|
resumeSuspendedResources() {
|
|
3277
|
+
const lifecycleEpoch = this.lifecycleEpoch;
|
|
3124
3278
|
this.trace.start();
|
|
3125
3279
|
this.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.RESUME });
|
|
3280
|
+
if (lifecycleEpoch !== this.lifecycleEpoch || this.stopping) return false;
|
|
3126
3281
|
this.startDedupSweep();
|
|
3127
3282
|
this.replayManager.start();
|
|
3283
|
+
return lifecycleEpoch === this.lifecycleEpoch && !this.stopping;
|
|
3128
3284
|
}
|
|
3129
3285
|
/**
|
|
3130
3286
|
* Suspend the transport when the tab goes hidden. Stops the transport and
|
|
@@ -3132,12 +3288,13 @@ var CrossTabDataBus = class {
|
|
|
3132
3288
|
*/
|
|
3133
3289
|
suspendTransport() {
|
|
3134
3290
|
if (this.stopping) return;
|
|
3135
|
-
this.lifecycleEpoch
|
|
3291
|
+
const suspensionEpoch = ++this.lifecycleEpoch;
|
|
3136
3292
|
this.suspended = true;
|
|
3137
|
-
this.cancelScheduledRecovery();
|
|
3293
|
+
this.cancelScheduledRecovery(true);
|
|
3138
3294
|
this.transportReady = false;
|
|
3139
3295
|
this.transportSubscribedTopics.clear();
|
|
3140
3296
|
this.updateStatus(WORKER_STATUS.DISCONNECTED);
|
|
3297
|
+
if (suspensionEpoch !== this.lifecycleEpoch || this.stopping || !this.suspended) return;
|
|
3141
3298
|
if (this.pendingStop && (this.startPromise === null || this.startPromise === this.pendingStop)) {
|
|
3142
3299
|
this.startPromise = this.pendingStop;
|
|
3143
3300
|
return;
|
|
@@ -3172,13 +3329,18 @@ var CrossTabDataBus = class {
|
|
|
3172
3329
|
if (this.startPromise && this.startPromise !== this.pendingStop) return this.startPromise;
|
|
3173
3330
|
const config = this.activeConfig;
|
|
3174
3331
|
const traceAttempt = recoveryAttempt ?? (this.recoveryAttempt > 0 ? this.recoveryAttempt : void 0);
|
|
3175
|
-
this.started = true;
|
|
3176
|
-
this.suspended = false;
|
|
3177
|
-
this.updateStatus(WORKER_STATUS.CONNECTING);
|
|
3178
3332
|
const lifecycleEpoch = ++this.lifecycleEpoch;
|
|
3179
3333
|
const pending = this.startPromise ?? this.pendingStop ?? Promise.resolve();
|
|
3180
3334
|
const opening = pending.catch(() => void 0).then(() => this.openTransport(config, Promise.resolve(), false, lifecycleEpoch));
|
|
3181
3335
|
this.startPromise = opening;
|
|
3336
|
+
this.started = true;
|
|
3337
|
+
this.suspended = false;
|
|
3338
|
+
this.transportReady = false;
|
|
3339
|
+
this.updateStatus(WORKER_STATUS.CONNECTING);
|
|
3340
|
+
if (lifecycleEpoch !== this.lifecycleEpoch || this.stopping || this.suspended) {
|
|
3341
|
+
void opening.catch(() => void 0);
|
|
3342
|
+
return opening;
|
|
3343
|
+
}
|
|
3182
3344
|
void opening.then(
|
|
3183
3345
|
() => {
|
|
3184
3346
|
if (this.startPromise === opening) this.startPromise = null;
|
|
@@ -3208,17 +3370,13 @@ var CrossTabDataBus = class {
|
|
|
3208
3370
|
runTransport(operation) {
|
|
3209
3371
|
if (this.suspended) return;
|
|
3210
3372
|
if (this.recoveryGate && !this.stopping) {
|
|
3211
|
-
if (this.recoveryDemandAllowed && this.status === WORKER_STATUS.ERROR && !this.suspended) {
|
|
3212
|
-
this.recoveryDemandAllowed = false;
|
|
3213
|
-
const opening = this.reopenTransport();
|
|
3214
|
-
void opening.then(
|
|
3215
|
-
() => this.releaseRecoveryGate(),
|
|
3216
|
-
() => this.allowDemandRecovery()
|
|
3217
|
-
);
|
|
3218
|
-
}
|
|
3219
3373
|
const gate = this.recoveryGate;
|
|
3374
|
+
const cancellationToken = this.recoveryCancellationToken;
|
|
3375
|
+
this.startDemandRecovery();
|
|
3376
|
+
this.recoveryWaiters += 1;
|
|
3220
3377
|
void gate.then(() => {
|
|
3221
|
-
|
|
3378
|
+
this.recoveryWaiters -= 1;
|
|
3379
|
+
if (this.stopping || this.suspended || cancellationToken !== this.recoveryCancellationToken) return;
|
|
3222
3380
|
this.runTransport(operation);
|
|
3223
3381
|
});
|
|
3224
3382
|
return;
|
|
@@ -3342,4 +3500,4 @@ export {
|
|
|
3342
3500
|
parseDataBusPublication,
|
|
3343
3501
|
selectWorkerBackend
|
|
3344
3502
|
};
|
|
3345
|
-
//# sourceMappingURL=chunk-
|
|
3503
|
+
//# sourceMappingURL=chunk-QB74C4EH.js.map
|