cross-tab-worker-databus 0.20.91 → 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 +28 -0
- package/dist/centrifuge.js +1 -1
- package/dist/{chunk-DMM5KBNG.js → chunk-QB74C4EH.js} +242 -87
- package/dist/chunk-QB74C4EH.js.map +7 -0
- package/dist/cjs/centrifuge.cjs +241 -86
- package/dist/cjs/centrifuge.cjs.map +2 -2
- package/dist/cjs/index.cjs +241 -86
- 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 +1 -1
- package/docs/architecture.md +9 -6
- package/docs/benchmarks.md +8 -8
- package/docs/capabilities.md +3 -1
- package/docs/release-checklist.md +2 -2
- package/docs/roadmap.md +10 -1
- package/docs/zh/architecture.md +9 -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 +10 -1
- package/package.json +1 -1
- package/dist/chunk-DMM5KBNG.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);
|
|
@@ -1021,8 +1035,10 @@ var WorkerClusterRuntime = class {
|
|
|
1021
1035
|
handlePageHide = () => this.pause();
|
|
1022
1036
|
handlePageShow = () => {
|
|
1023
1037
|
if (!this.suspended) return;
|
|
1038
|
+
const generation = ++this.lifecycleGeneration;
|
|
1024
1039
|
this.suspended = false;
|
|
1025
1040
|
this.handlers.onResume?.();
|
|
1041
|
+
if (generation !== this.lifecycleGeneration) return;
|
|
1026
1042
|
this.activate();
|
|
1027
1043
|
};
|
|
1028
1044
|
handleVisibilityChange = () => {
|
|
@@ -1077,10 +1093,16 @@ var WorkerClusterRuntime = class {
|
|
|
1077
1093
|
if (message.targetWorkerId !== this.workerId) return;
|
|
1078
1094
|
this.rememberTopic(message.topic);
|
|
1079
1095
|
switch (message.action) {
|
|
1080
|
-
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
|
+
}
|
|
1081
1102
|
this.assignedTopics.set(message.topicKey, message.topic);
|
|
1082
1103
|
this.confirmRoute(message.topicKey);
|
|
1083
1104
|
break;
|
|
1105
|
+
}
|
|
1084
1106
|
case CONTROL_ACTION.UNSUBSCRIBE:
|
|
1085
1107
|
if (this.releaseHandoffOnUnsubscribe(message)) return;
|
|
1086
1108
|
break;
|
|
@@ -1141,8 +1163,8 @@ var WorkerClusterRuntime = class {
|
|
|
1141
1163
|
}
|
|
1142
1164
|
/**
|
|
1143
1165
|
* Accept a graceful handoff only when the route still points to this worker,
|
|
1144
|
-
* the release comes from the recorded previous owner, and the generation
|
|
1145
|
-
*
|
|
1166
|
+
* the release comes from the recorded previous owner, and the generation
|
|
1167
|
+
* exactly matches ours. Any other ROUTE_RELEASED is stale and dropped.
|
|
1146
1168
|
*/
|
|
1147
1169
|
handleRouteReleasedMessage(message) {
|
|
1148
1170
|
if (message.targetWorkerId !== this.workerId) return;
|
|
@@ -1155,11 +1177,11 @@ var WorkerClusterRuntime = class {
|
|
|
1155
1177
|
}
|
|
1156
1178
|
/** A ROUTE_RELEASED is stale (and must be dropped) unless the route still
|
|
1157
1179
|
* points to us, the release comes from the recorded previous owner, and
|
|
1158
|
-
* the release generation
|
|
1159
|
-
* earlier handoff round
|
|
1160
|
-
*
|
|
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. */
|
|
1161
1183
|
isStaleRouteRelease(route, message) {
|
|
1162
|
-
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;
|
|
1163
1185
|
}
|
|
1164
1186
|
/** True when an unconfirmed handoff route has been stuck longer than a
|
|
1165
1187
|
* worker TTL. The ACK for a live handoff is posted synchronously with the
|
|
@@ -1786,7 +1808,7 @@ var ReplayManager = class {
|
|
|
1786
1808
|
this.trace = deps.trace;
|
|
1787
1809
|
this.onPersistenceError = deps.onPersistenceError;
|
|
1788
1810
|
this.onDispatchError = deps.onDispatchError;
|
|
1789
|
-
|
|
1811
|
+
void this.requestHydration();
|
|
1790
1812
|
}
|
|
1791
1813
|
deps;
|
|
1792
1814
|
buffers;
|
|
@@ -1805,7 +1827,18 @@ var ReplayManager = class {
|
|
|
1805
1827
|
retryGeneration = 0;
|
|
1806
1828
|
pendingReplayPersistence = [];
|
|
1807
1829
|
persistenceFlushScheduled = false;
|
|
1808
|
-
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;
|
|
1809
1842
|
/** Coalesced retention cleanup: the newest cutoff wins while one is running. */
|
|
1810
1843
|
retentionCleanup = null;
|
|
1811
1844
|
retentionCutoff = null;
|
|
@@ -1859,7 +1892,7 @@ var ReplayManager = class {
|
|
|
1859
1892
|
if (!this.buffers) return;
|
|
1860
1893
|
const limit = typeof replayOption === "number" ? Math.min(Math.floor(replayOption), this.maxPerTopic) : this.maxPerTopic;
|
|
1861
1894
|
if (this.persistence) {
|
|
1862
|
-
void this.
|
|
1895
|
+
void this.requestHydration().then(() => {
|
|
1863
1896
|
if (isHandlerActive?.() ?? true) this.deliver(topic, limit, handler);
|
|
1864
1897
|
});
|
|
1865
1898
|
return;
|
|
@@ -1871,22 +1904,24 @@ var ReplayManager = class {
|
|
|
1871
1904
|
* clearTopic), and prune durable history. */
|
|
1872
1905
|
onTopicUnsubscribed(topic) {
|
|
1873
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);
|
|
1874
1909
|
this.buffers.delete(topic);
|
|
1875
1910
|
this.pendingReplayPersistence = this.pendingReplayPersistence.filter((message) => message.topic !== topic);
|
|
1876
|
-
if (this.
|
|
1877
|
-
void this.withPersistenceRetry(PERSISTENCE_OPERATION.CLEAR_TOPIC, () => this.persistence.clearTopic(topic)).catch((error) => this.onPersistenceError(error));
|
|
1878
|
-
}
|
|
1911
|
+
if (clearing) void clearing.catch((error) => this.onPersistenceError(error));
|
|
1879
1912
|
}
|
|
1880
1913
|
/** Clear all in-memory replay buffers and, when supported, durable history.
|
|
1881
1914
|
* Reports persistence failures and rethrows, mirroring the public API
|
|
1882
1915
|
* contract that callers can observe a failed clear. */
|
|
1883
1916
|
async clearAll() {
|
|
1884
1917
|
if (!this.buffers) return;
|
|
1918
|
+
const clearing = this.persistence?.clear ? this.withPersistenceRetry(PERSISTENCE_OPERATION.CLEAR, () => this.persistence.clear()) : null;
|
|
1919
|
+
this.hydrationClearAll = true;
|
|
1885
1920
|
this.buffers.clear();
|
|
1886
1921
|
this.pendingReplayPersistence = [];
|
|
1887
|
-
if (
|
|
1922
|
+
if (clearing) {
|
|
1888
1923
|
try {
|
|
1889
|
-
await
|
|
1924
|
+
await clearing;
|
|
1890
1925
|
} catch (error) {
|
|
1891
1926
|
this.onPersistenceError(error);
|
|
1892
1927
|
throw error;
|
|
@@ -1896,11 +1931,13 @@ var ReplayManager = class {
|
|
|
1896
1931
|
/** Clear replay history for one exact topic, including durable storage. */
|
|
1897
1932
|
async clearTopic(topic) {
|
|
1898
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);
|
|
1899
1936
|
this.buffers.delete(topic);
|
|
1900
1937
|
this.pendingReplayPersistence = this.pendingReplayPersistence.filter((message) => message.topic !== topic);
|
|
1901
|
-
if (
|
|
1938
|
+
if (clearing) {
|
|
1902
1939
|
try {
|
|
1903
|
-
await
|
|
1940
|
+
await clearing;
|
|
1904
1941
|
} catch (error) {
|
|
1905
1942
|
this.onPersistenceError(error);
|
|
1906
1943
|
throw error;
|
|
@@ -1910,7 +1947,9 @@ var ReplayManager = class {
|
|
|
1910
1947
|
/** Remove replay entries older than an epoch-millisecond cutoff. */
|
|
1911
1948
|
async clearBefore(timestamp) {
|
|
1912
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;
|
|
1913
1951
|
if (this.buffers) {
|
|
1952
|
+
this.hydrationClearBefore = this.hydrationClearBefore === null ? timestamp : Math.max(this.hydrationClearBefore, timestamp);
|
|
1914
1953
|
for (const [topic, messages] of this.buffers) {
|
|
1915
1954
|
const kept = messages.filter((message) => message.timestamp === void 0 || message.timestamp >= timestamp);
|
|
1916
1955
|
if (kept.length) this.buffers.set(topic, kept);
|
|
@@ -1920,9 +1959,9 @@ var ReplayManager = class {
|
|
|
1920
1959
|
this.pendingReplayPersistence = this.pendingReplayPersistence.filter(
|
|
1921
1960
|
(message) => message.timestamp === void 0 || message.timestamp >= timestamp
|
|
1922
1961
|
);
|
|
1923
|
-
if (
|
|
1962
|
+
if (clearing) {
|
|
1924
1963
|
try {
|
|
1925
|
-
await
|
|
1964
|
+
await clearing;
|
|
1926
1965
|
} catch (error) {
|
|
1927
1966
|
this.onPersistenceError(error);
|
|
1928
1967
|
throw error;
|
|
@@ -1932,6 +1971,11 @@ var ReplayManager = class {
|
|
|
1932
1971
|
/** Start the periodic retention sweep. No-op when no durable retention
|
|
1933
1972
|
* config makes it necessary. */
|
|
1934
1973
|
start() {
|
|
1974
|
+
if (this.hydrationFailed) {
|
|
1975
|
+
this.hydrationComplete = false;
|
|
1976
|
+
this.hydrationFailed = false;
|
|
1977
|
+
}
|
|
1978
|
+
void this.requestHydration();
|
|
1935
1979
|
if (this.retentionTimer || !this.retentionMs || !this.retentionSweepMs || !this.persistence?.clearBefore) return;
|
|
1936
1980
|
this.retentionTimer = setInterval(() => {
|
|
1937
1981
|
this.scheduleRetentionCleanup(this.now() - this.retentionMs);
|
|
@@ -1946,13 +1990,25 @@ var ReplayManager = class {
|
|
|
1946
1990
|
* or stopped bus does not keep hammering the store) and stop the sweep. */
|
|
1947
1991
|
suspend() {
|
|
1948
1992
|
this.retryGeneration += 1;
|
|
1993
|
+
if (this.hydration) {
|
|
1994
|
+
this.hydrationEpoch += 1;
|
|
1995
|
+
this.hydration = null;
|
|
1996
|
+
this.hydrationComplete = false;
|
|
1997
|
+
}
|
|
1949
1998
|
this.pendingReplayPersistence = [];
|
|
1950
1999
|
this.retentionCutoff = null;
|
|
1951
2000
|
this.stop();
|
|
1952
2001
|
}
|
|
1953
|
-
/** Drop all in-memory buffers
|
|
2002
|
+
/** Drop all in-memory buffers and require hydration for the next lifecycle. */
|
|
1954
2003
|
resetBuffers() {
|
|
1955
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;
|
|
1956
2012
|
}
|
|
1957
2013
|
/** Buffer occupancy for diagnostics. `bytes` is an approximate in-memory
|
|
1958
2014
|
* payload footprint (same heuristic as adaptive load weighting), computed on
|
|
@@ -2004,38 +2060,78 @@ var ReplayManager = class {
|
|
|
2004
2060
|
void this.withPersistenceRetry(PERSISTENCE_OPERATION.APPEND, () => this.persistence.appendBatch(batch)).catch((error) => this.onPersistenceError(error));
|
|
2005
2061
|
});
|
|
2006
2062
|
}
|
|
2007
|
-
/**
|
|
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
|
|
2008
2079
|
* entries past the retention window first. Failures are reported but do not
|
|
2009
2080
|
* block startup — the bus runs with whatever survived. */
|
|
2010
|
-
async hydrate() {
|
|
2081
|
+
async hydrate(epoch, generation) {
|
|
2011
2082
|
if (!this.buffers || !this.persistence) {
|
|
2012
2083
|
return;
|
|
2013
2084
|
}
|
|
2014
2085
|
try {
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
if (
|
|
2022
|
-
|
|
2023
|
-
|
|
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);
|
|
2116
|
+
}
|
|
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;
|
|
2024
2125
|
}
|
|
2025
|
-
|
|
2126
|
+
if (epoch !== this.hydrationEpoch) return;
|
|
2127
|
+
this.onPersistenceError(error);
|
|
2128
|
+
this.hydrationComplete = true;
|
|
2129
|
+
this.hydrationFailed = true;
|
|
2026
2130
|
}
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
maxPerTopic: this.maxPerTopic,
|
|
2031
|
-
pruneStrategy: this.pruneStrategy,
|
|
2032
|
-
retentionMs: this.retentionMs,
|
|
2033
|
-
now: hydrationNow
|
|
2034
|
-
});
|
|
2035
|
-
if (pruned !== buffer) this.buffers.set(topic, pruned);
|
|
2131
|
+
} finally {
|
|
2132
|
+
if (this.hydrationEpoch === epoch && this.retryGeneration === generation) {
|
|
2133
|
+
this.hydration = null;
|
|
2036
2134
|
}
|
|
2037
|
-
} catch (error) {
|
|
2038
|
-
this.onPersistenceError(error);
|
|
2039
2135
|
}
|
|
2040
2136
|
}
|
|
2041
2137
|
/** Coalesce retention cleanup: the newest cutoff wins while one pass runs,
|
|
@@ -2059,7 +2155,7 @@ var ReplayManager = class {
|
|
|
2059
2155
|
}
|
|
2060
2156
|
})().finally(() => {
|
|
2061
2157
|
this.retentionCleanup = null;
|
|
2062
|
-
if (this.retentionCutoff !== null
|
|
2158
|
+
if (this.retentionCutoff !== null) {
|
|
2063
2159
|
this.scheduleRetentionCleanup(this.retentionCutoff);
|
|
2064
2160
|
}
|
|
2065
2161
|
});
|
|
@@ -2212,7 +2308,7 @@ var DedupManager = class {
|
|
|
2212
2308
|
};
|
|
2213
2309
|
|
|
2214
2310
|
// src/core/version.ts
|
|
2215
|
-
var SDK_VERSION = true ? "0.20.
|
|
2311
|
+
var SDK_VERSION = true ? "0.20.92" : "";
|
|
2216
2312
|
|
|
2217
2313
|
// src/core/data-bus.ts
|
|
2218
2314
|
var DEFAULT_REPLAY_MAX_PER_TOPIC = 100;
|
|
@@ -2288,11 +2384,21 @@ var CrossTabDataBus = class {
|
|
|
2288
2384
|
recoveryGateRelease = null;
|
|
2289
2385
|
recoveryTimer = null;
|
|
2290
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;
|
|
2291
2392
|
// Once an automatic attempt fails, an explicit transport operation may
|
|
2292
2393
|
// recover immediately instead of waiting for the next paced attempt. The
|
|
2293
2394
|
// gate still stays closed so the operation cannot reach the failed
|
|
2294
2395
|
// transport; it is released by the successful on-demand reopen.
|
|
2295
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;
|
|
2296
2402
|
/** Monotonic generation incremented on every successful transport open.
|
|
2297
2403
|
* Stays in lockstep with `lastSuccessAt` so callers can detect that the
|
|
2298
2404
|
* transport has been reopened even if the timestamp window is short. */
|
|
@@ -2407,7 +2513,7 @@ var CrossTabDataBus = class {
|
|
|
2407
2513
|
this.suspendTransport();
|
|
2408
2514
|
},
|
|
2409
2515
|
onResume: () => {
|
|
2410
|
-
this.resumeSuspendedResources();
|
|
2516
|
+
if (!this.resumeSuspendedResources()) return;
|
|
2411
2517
|
this.resumeTransport();
|
|
2412
2518
|
},
|
|
2413
2519
|
onDiagnostic: (event) => {
|
|
@@ -2436,11 +2542,13 @@ var CrossTabDataBus = class {
|
|
|
2436
2542
|
const transportDown = !this.transportReady || this.status === WORKER_STATUS.ERROR || this.status === WORKER_STATUS.DISCONNECTED;
|
|
2437
2543
|
if (!transportDown) return Promise.resolve();
|
|
2438
2544
|
this.activeConfig = config;
|
|
2439
|
-
this.resetFailureState();
|
|
2545
|
+
this.resetFailureState(true);
|
|
2440
2546
|
const resumingFromSuspend = this.suspended;
|
|
2441
|
-
if (resumingFromSuspend
|
|
2547
|
+
if (resumingFromSuspend && !this.resumeSuspendedResources()) {
|
|
2548
|
+
return this.stopPromise ?? Promise.resolve();
|
|
2549
|
+
}
|
|
2442
2550
|
const opening2 = this.reopenTransport();
|
|
2443
|
-
if (resumingFromSuspend) this.cluster.start();
|
|
2551
|
+
if (resumingFromSuspend && !this.stopping && !this.suspended) this.cluster.start();
|
|
2444
2552
|
return opening2;
|
|
2445
2553
|
}
|
|
2446
2554
|
this.started = true;
|
|
@@ -2448,12 +2556,6 @@ var CrossTabDataBus = class {
|
|
|
2448
2556
|
this.suspended = false;
|
|
2449
2557
|
this.activeConfig = config;
|
|
2450
2558
|
this.resetFailureState();
|
|
2451
|
-
this.trace.start();
|
|
2452
|
-
this.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.START });
|
|
2453
|
-
this.startDedupSweep();
|
|
2454
|
-
this.replayManager.start();
|
|
2455
|
-
this.updateStatus(WORKER_STATUS.CONNECTING);
|
|
2456
|
-
this.cluster.start();
|
|
2457
2559
|
const lifecycleEpoch = ++this.lifecycleEpoch;
|
|
2458
2560
|
const opening = this.openTransport(
|
|
2459
2561
|
config,
|
|
@@ -2462,7 +2564,17 @@ var CrossTabDataBus = class {
|
|
|
2462
2564
|
lifecycleEpoch
|
|
2463
2565
|
);
|
|
2464
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;
|
|
2465
2576
|
for (const topic of this.topicHandlers.keys()) {
|
|
2577
|
+
if (lifecycleEpoch !== this.lifecycleEpoch || this.stopping) break;
|
|
2466
2578
|
this.cluster.subscribe(topic);
|
|
2467
2579
|
}
|
|
2468
2580
|
void opening.then(
|
|
@@ -2524,27 +2636,49 @@ var CrossTabDataBus = class {
|
|
|
2524
2636
|
/** Cancel a pending automatic retry when an explicit lifecycle transition
|
|
2525
2637
|
* supersedes it. The released gate re-enters runTransport(), which then
|
|
2526
2638
|
* follows the newest start/stop/suspend intent. */
|
|
2527
|
-
cancelScheduledRecovery() {
|
|
2639
|
+
cancelScheduledRecovery(invalidateParkedOperations = false, releaseGate = true) {
|
|
2528
2640
|
this.recoveryTimerToken += 1;
|
|
2641
|
+
if (invalidateParkedOperations) this.recoveryCancellationToken += 1;
|
|
2529
2642
|
if (this.recoveryTimer !== null) {
|
|
2530
2643
|
clearTimeout(this.recoveryTimer);
|
|
2531
2644
|
this.recoveryTimer = null;
|
|
2532
2645
|
}
|
|
2533
|
-
this.releaseRecoveryGate();
|
|
2646
|
+
if (releaseGate) this.releaseRecoveryGate();
|
|
2534
2647
|
}
|
|
2535
2648
|
/** Keep the recovery gate closed after a failed attempt while allowing the
|
|
2536
2649
|
* next explicit transport operation to start an immediate on-demand reopen.
|
|
2537
|
-
* If no gate/successor retry remains, release any waiters.
|
|
2538
|
-
|
|
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) {
|
|
2539
2658
|
if (this.recoveryGate !== null && this.started && !this.stopping && !this.suspended && this.status === WORKER_STATUS.ERROR) {
|
|
2540
2659
|
this.recoveryDemandAllowed = true;
|
|
2660
|
+
if (kickParkedWaiters && this.recoveryWaiters > 0) this.startDemandRecovery();
|
|
2541
2661
|
return;
|
|
2542
2662
|
}
|
|
2543
2663
|
this.releaseRecoveryGate();
|
|
2544
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
|
+
}
|
|
2545
2678
|
/** Reset failure and recovery diagnostics for a new explicit start session. */
|
|
2546
|
-
resetFailureState() {
|
|
2547
|
-
this.cancelScheduledRecovery();
|
|
2679
|
+
resetFailureState(preserveRecoveryGate = false) {
|
|
2680
|
+
this.cancelScheduledRecovery(false, !preserveRecoveryGate);
|
|
2681
|
+
if (preserveRecoveryGate) this.recoveryDemandAllowed = false;
|
|
2548
2682
|
this.lastError = null;
|
|
2549
2683
|
this.lastErrorAt = null;
|
|
2550
2684
|
this.lastFailure = null;
|
|
@@ -2884,22 +3018,36 @@ var CrossTabDataBus = class {
|
|
|
2884
3018
|
if (!this.started && !this.startPromise && !this.pendingStop && !this.transportReady) {
|
|
2885
3019
|
return Promise.resolve();
|
|
2886
3020
|
}
|
|
3021
|
+
let resolveGate;
|
|
3022
|
+
const stopGate = new Promise((resolve) => {
|
|
3023
|
+
resolveGate = resolve;
|
|
3024
|
+
});
|
|
3025
|
+
this.stopPromise = stopGate;
|
|
3026
|
+
this.beginStop();
|
|
2887
3027
|
const stopPromise = this.performStop();
|
|
2888
|
-
this.stopPromise = stopPromise;
|
|
2889
3028
|
void stopPromise.then(
|
|
2890
3029
|
() => {
|
|
2891
|
-
if (this.stopPromise ===
|
|
3030
|
+
if (this.stopPromise === stopGate) this.stopPromise = null;
|
|
3031
|
+
resolveGate();
|
|
2892
3032
|
},
|
|
2893
3033
|
() => {
|
|
2894
|
-
if (this.stopPromise ===
|
|
3034
|
+
if (this.stopPromise === stopGate) this.stopPromise = null;
|
|
3035
|
+
resolveGate();
|
|
2895
3036
|
}
|
|
2896
3037
|
);
|
|
2897
|
-
return
|
|
3038
|
+
return stopGate;
|
|
2898
3039
|
}
|
|
2899
|
-
|
|
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() {
|
|
2900
3048
|
this.lifecycleEpoch += 1;
|
|
2901
3049
|
this.stopping = true;
|
|
2902
|
-
this.cancelScheduledRecovery();
|
|
3050
|
+
this.cancelScheduledRecovery(true);
|
|
2903
3051
|
this.replayManager.suspend();
|
|
2904
3052
|
this.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.STOP });
|
|
2905
3053
|
this.trace.stop();
|
|
@@ -2907,6 +3055,8 @@ var CrossTabDataBus = class {
|
|
|
2907
3055
|
this.topicHandlers.clear();
|
|
2908
3056
|
this.replayManager.resetBuffers();
|
|
2909
3057
|
this.cluster.stop();
|
|
3058
|
+
}
|
|
3059
|
+
async performStop() {
|
|
2910
3060
|
try {
|
|
2911
3061
|
await this.startPromise?.catch(() => void 0);
|
|
2912
3062
|
const pendingStop = this.pendingStop;
|
|
@@ -2923,8 +3073,6 @@ var CrossTabDataBus = class {
|
|
|
2923
3073
|
this.transportReady = false;
|
|
2924
3074
|
this.startPromise = null;
|
|
2925
3075
|
this.pendingStop = null;
|
|
2926
|
-
this.lastError = null;
|
|
2927
|
-
this.lastErrorAt = null;
|
|
2928
3076
|
this.activeConfig = void 0;
|
|
2929
3077
|
this.recoveryAttempt = 0;
|
|
2930
3078
|
this.recoveryExhausted = false;
|
|
@@ -3021,7 +3169,9 @@ var CrossTabDataBus = class {
|
|
|
3021
3169
|
const opening = this.reopenTransport(attempt);
|
|
3022
3170
|
void opening.then(
|
|
3023
3171
|
() => this.releaseRecoveryGate(),
|
|
3024
|
-
|
|
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)
|
|
3025
3175
|
);
|
|
3026
3176
|
}, this.recoveryCooldownMs);
|
|
3027
3177
|
}
|
|
@@ -3124,10 +3274,13 @@ var CrossTabDataBus = class {
|
|
|
3124
3274
|
* pageshow path and explicit start() must run this so an explicit resume
|
|
3125
3275
|
* cannot leave trace metrics and periodic cleanup timers permanently off. */
|
|
3126
3276
|
resumeSuspendedResources() {
|
|
3277
|
+
const lifecycleEpoch = this.lifecycleEpoch;
|
|
3127
3278
|
this.trace.start();
|
|
3128
3279
|
this.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.RESUME });
|
|
3280
|
+
if (lifecycleEpoch !== this.lifecycleEpoch || this.stopping) return false;
|
|
3129
3281
|
this.startDedupSweep();
|
|
3130
3282
|
this.replayManager.start();
|
|
3283
|
+
return lifecycleEpoch === this.lifecycleEpoch && !this.stopping;
|
|
3131
3284
|
}
|
|
3132
3285
|
/**
|
|
3133
3286
|
* Suspend the transport when the tab goes hidden. Stops the transport and
|
|
@@ -3135,12 +3288,13 @@ var CrossTabDataBus = class {
|
|
|
3135
3288
|
*/
|
|
3136
3289
|
suspendTransport() {
|
|
3137
3290
|
if (this.stopping) return;
|
|
3138
|
-
this.lifecycleEpoch
|
|
3291
|
+
const suspensionEpoch = ++this.lifecycleEpoch;
|
|
3139
3292
|
this.suspended = true;
|
|
3140
|
-
this.cancelScheduledRecovery();
|
|
3293
|
+
this.cancelScheduledRecovery(true);
|
|
3141
3294
|
this.transportReady = false;
|
|
3142
3295
|
this.transportSubscribedTopics.clear();
|
|
3143
3296
|
this.updateStatus(WORKER_STATUS.DISCONNECTED);
|
|
3297
|
+
if (suspensionEpoch !== this.lifecycleEpoch || this.stopping || !this.suspended) return;
|
|
3144
3298
|
if (this.pendingStop && (this.startPromise === null || this.startPromise === this.pendingStop)) {
|
|
3145
3299
|
this.startPromise = this.pendingStop;
|
|
3146
3300
|
return;
|
|
@@ -3175,13 +3329,18 @@ var CrossTabDataBus = class {
|
|
|
3175
3329
|
if (this.startPromise && this.startPromise !== this.pendingStop) return this.startPromise;
|
|
3176
3330
|
const config = this.activeConfig;
|
|
3177
3331
|
const traceAttempt = recoveryAttempt ?? (this.recoveryAttempt > 0 ? this.recoveryAttempt : void 0);
|
|
3178
|
-
this.started = true;
|
|
3179
|
-
this.suspended = false;
|
|
3180
|
-
this.updateStatus(WORKER_STATUS.CONNECTING);
|
|
3181
3332
|
const lifecycleEpoch = ++this.lifecycleEpoch;
|
|
3182
3333
|
const pending = this.startPromise ?? this.pendingStop ?? Promise.resolve();
|
|
3183
3334
|
const opening = pending.catch(() => void 0).then(() => this.openTransport(config, Promise.resolve(), false, lifecycleEpoch));
|
|
3184
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
|
+
}
|
|
3185
3344
|
void opening.then(
|
|
3186
3345
|
() => {
|
|
3187
3346
|
if (this.startPromise === opening) this.startPromise = null;
|
|
@@ -3211,17 +3370,13 @@ var CrossTabDataBus = class {
|
|
|
3211
3370
|
runTransport(operation) {
|
|
3212
3371
|
if (this.suspended) return;
|
|
3213
3372
|
if (this.recoveryGate && !this.stopping) {
|
|
3214
|
-
if (this.recoveryDemandAllowed && this.status === WORKER_STATUS.ERROR && !this.suspended) {
|
|
3215
|
-
this.recoveryDemandAllowed = false;
|
|
3216
|
-
const opening = this.reopenTransport();
|
|
3217
|
-
void opening.then(
|
|
3218
|
-
() => this.releaseRecoveryGate(),
|
|
3219
|
-
() => this.allowDemandRecovery()
|
|
3220
|
-
);
|
|
3221
|
-
}
|
|
3222
3373
|
const gate = this.recoveryGate;
|
|
3374
|
+
const cancellationToken = this.recoveryCancellationToken;
|
|
3375
|
+
this.startDemandRecovery();
|
|
3376
|
+
this.recoveryWaiters += 1;
|
|
3223
3377
|
void gate.then(() => {
|
|
3224
|
-
|
|
3378
|
+
this.recoveryWaiters -= 1;
|
|
3379
|
+
if (this.stopping || this.suspended || cancellationToken !== this.recoveryCancellationToken) return;
|
|
3225
3380
|
this.runTransport(operation);
|
|
3226
3381
|
});
|
|
3227
3382
|
return;
|
|
@@ -3345,4 +3500,4 @@ export {
|
|
|
3345
3500
|
parseDataBusPublication,
|
|
3346
3501
|
selectWorkerBackend
|
|
3347
3502
|
};
|
|
3348
|
-
//# sourceMappingURL=chunk-
|
|
3503
|
+
//# sourceMappingURL=chunk-QB74C4EH.js.map
|