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
package/dist/cjs/index.cjs
CHANGED
|
@@ -738,6 +738,9 @@ var WorkerClusterRuntime = class {
|
|
|
738
738
|
started = false;
|
|
739
739
|
suspended = false;
|
|
740
740
|
lifecycleListening = false;
|
|
741
|
+
/** Invalidates an in-flight pageshow resume when a synchronous onResume
|
|
742
|
+
* callback stops or pauses the cluster before activate() is reached. */
|
|
743
|
+
lifecycleGeneration = 0;
|
|
741
744
|
currentRecord;
|
|
742
745
|
constructor(options) {
|
|
743
746
|
assertClusterOptions(options);
|
|
@@ -774,8 +777,14 @@ var WorkerClusterRuntime = class {
|
|
|
774
777
|
/** Start the cluster: register, listen for lifecycle events, and begin heartbeats. */
|
|
775
778
|
start() {
|
|
776
779
|
if (this.started) return;
|
|
777
|
-
this.
|
|
780
|
+
this.lifecycleGeneration += 1;
|
|
778
781
|
this.addLifecycleListeners();
|
|
782
|
+
if (this.environment.getVisibilityState() === TAB_VISIBILITY.HIDDEN) {
|
|
783
|
+
this.suspended = true;
|
|
784
|
+
this.handlers.onSuspend?.();
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
this.suspended = false;
|
|
779
788
|
this.activate();
|
|
780
789
|
}
|
|
781
790
|
/**
|
|
@@ -786,6 +795,7 @@ var WorkerClusterRuntime = class {
|
|
|
786
795
|
* stop() path where callers expect every Set/Map to be empty afterwards.
|
|
787
796
|
*/
|
|
788
797
|
stop() {
|
|
798
|
+
this.lifecycleGeneration += 1;
|
|
789
799
|
if (!this.started && !this.suspended) return;
|
|
790
800
|
this.pause();
|
|
791
801
|
this.flushStorage();
|
|
@@ -832,7 +842,11 @@ var WorkerClusterRuntime = class {
|
|
|
832
842
|
* to other workers, remove our worker record, and close the channel.
|
|
833
843
|
*/
|
|
834
844
|
pause() {
|
|
835
|
-
|
|
845
|
+
this.lifecycleGeneration += 1;
|
|
846
|
+
if (!this.started) {
|
|
847
|
+
if (this.lifecycleListening) this.suspended = true;
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
836
850
|
this.started = false;
|
|
837
851
|
this.suspended = true;
|
|
838
852
|
if (this.heartbeatHandle !== null) this.environment.clearInterval(this.heartbeatHandle);
|
|
@@ -1147,8 +1161,10 @@ var WorkerClusterRuntime = class {
|
|
|
1147
1161
|
handlePageHide = () => this.pause();
|
|
1148
1162
|
handlePageShow = () => {
|
|
1149
1163
|
if (!this.suspended) return;
|
|
1164
|
+
const generation = ++this.lifecycleGeneration;
|
|
1150
1165
|
this.suspended = false;
|
|
1151
1166
|
this.handlers.onResume?.();
|
|
1167
|
+
if (generation !== this.lifecycleGeneration) return;
|
|
1152
1168
|
this.activate();
|
|
1153
1169
|
};
|
|
1154
1170
|
handleVisibilityChange = () => {
|
|
@@ -1203,10 +1219,16 @@ var WorkerClusterRuntime = class {
|
|
|
1203
1219
|
if (message.targetWorkerId !== this.workerId) return;
|
|
1204
1220
|
this.rememberTopic(message.topic);
|
|
1205
1221
|
switch (message.action) {
|
|
1206
|
-
case CONTROL_ACTION.SUBSCRIBE:
|
|
1222
|
+
case CONTROL_ACTION.SUBSCRIBE: {
|
|
1223
|
+
const route = this.readRoute(message.topicKey);
|
|
1224
|
+
if (route && route.workerId !== this.workerId) return;
|
|
1225
|
+
if (route?.workerId === this.workerId && route.handoffFromWorkerId !== void 0 && route.confirmedAt === void 0) {
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1207
1228
|
this.assignedTopics.set(message.topicKey, message.topic);
|
|
1208
1229
|
this.confirmRoute(message.topicKey);
|
|
1209
1230
|
break;
|
|
1231
|
+
}
|
|
1210
1232
|
case CONTROL_ACTION.UNSUBSCRIBE:
|
|
1211
1233
|
if (this.releaseHandoffOnUnsubscribe(message)) return;
|
|
1212
1234
|
break;
|
|
@@ -1267,8 +1289,8 @@ var WorkerClusterRuntime = class {
|
|
|
1267
1289
|
}
|
|
1268
1290
|
/**
|
|
1269
1291
|
* Accept a graceful handoff only when the route still points to this worker,
|
|
1270
|
-
* the release comes from the recorded previous owner, and the generation
|
|
1271
|
-
*
|
|
1292
|
+
* the release comes from the recorded previous owner, and the generation
|
|
1293
|
+
* exactly matches ours. Any other ROUTE_RELEASED is stale and dropped.
|
|
1272
1294
|
*/
|
|
1273
1295
|
handleRouteReleasedMessage(message) {
|
|
1274
1296
|
if (message.targetWorkerId !== this.workerId) return;
|
|
@@ -1281,11 +1303,11 @@ var WorkerClusterRuntime = class {
|
|
|
1281
1303
|
}
|
|
1282
1304
|
/** A ROUTE_RELEASED is stale (and must be dropped) unless the route still
|
|
1283
1305
|
* points to us, the release comes from the recorded previous owner, and
|
|
1284
|
-
* the release generation
|
|
1285
|
-
* earlier handoff round
|
|
1286
|
-
*
|
|
1306
|
+
* the release generation exactly matches ours. A delayed ACK from either an
|
|
1307
|
+
* earlier or later handoff round belongs to a different route and must not
|
|
1308
|
+
* confirm the current round. */
|
|
1287
1309
|
isStaleRouteRelease(route, message) {
|
|
1288
|
-
return route.workerId !== this.workerId || route.handoffFromWorkerId !== message.sourceWorkerId || message.generation
|
|
1310
|
+
return route.workerId !== this.workerId || route.handoffFromWorkerId !== message.sourceWorkerId || message.generation !== route.generation;
|
|
1289
1311
|
}
|
|
1290
1312
|
/** True when an unconfirmed handoff route has been stuck longer than a
|
|
1291
1313
|
* worker TTL. The ACK for a live handoff is posted synchronously with the
|
|
@@ -1912,7 +1934,7 @@ var ReplayManager = class {
|
|
|
1912
1934
|
this.trace = deps.trace;
|
|
1913
1935
|
this.onPersistenceError = deps.onPersistenceError;
|
|
1914
1936
|
this.onDispatchError = deps.onDispatchError;
|
|
1915
|
-
|
|
1937
|
+
void this.requestHydration();
|
|
1916
1938
|
}
|
|
1917
1939
|
deps;
|
|
1918
1940
|
buffers;
|
|
@@ -1931,7 +1953,18 @@ var ReplayManager = class {
|
|
|
1931
1953
|
retryGeneration = 0;
|
|
1932
1954
|
pendingReplayPersistence = [];
|
|
1933
1955
|
persistenceFlushScheduled = false;
|
|
1934
|
-
hydration
|
|
1956
|
+
/** Current hydration operation, if one belongs to the active lifecycle. */
|
|
1957
|
+
hydration = null;
|
|
1958
|
+
/** Invalidates a load from a superseded suspend/reset lifecycle. */
|
|
1959
|
+
hydrationEpoch = 0;
|
|
1960
|
+
/** Whether the active lifecycle has finished (or deliberately skipped) hydration. */
|
|
1961
|
+
hydrationComplete = false;
|
|
1962
|
+
/** A failed load is retried by the next explicit start(), not every replay request. */
|
|
1963
|
+
hydrationFailed = false;
|
|
1964
|
+
/** Mutations that must be applied to a load already in flight. */
|
|
1965
|
+
hydrationClearAll = false;
|
|
1966
|
+
hydrationClearedTopics = /* @__PURE__ */ new Set();
|
|
1967
|
+
hydrationClearBefore = null;
|
|
1935
1968
|
/** Coalesced retention cleanup: the newest cutoff wins while one is running. */
|
|
1936
1969
|
retentionCleanup = null;
|
|
1937
1970
|
retentionCutoff = null;
|
|
@@ -1985,7 +2018,7 @@ var ReplayManager = class {
|
|
|
1985
2018
|
if (!this.buffers) return;
|
|
1986
2019
|
const limit = typeof replayOption === "number" ? Math.min(Math.floor(replayOption), this.maxPerTopic) : this.maxPerTopic;
|
|
1987
2020
|
if (this.persistence) {
|
|
1988
|
-
void this.
|
|
2021
|
+
void this.requestHydration().then(() => {
|
|
1989
2022
|
if (isHandlerActive?.() ?? true) this.deliver(topic, limit, handler);
|
|
1990
2023
|
});
|
|
1991
2024
|
return;
|
|
@@ -1997,22 +2030,24 @@ var ReplayManager = class {
|
|
|
1997
2030
|
* clearTopic), and prune durable history. */
|
|
1998
2031
|
onTopicUnsubscribed(topic) {
|
|
1999
2032
|
if (!this.buffers) return;
|
|
2033
|
+
const clearing = this.persistence?.clearTopic ? this.withPersistenceRetry(PERSISTENCE_OPERATION.CLEAR_TOPIC, () => this.persistence.clearTopic(topic)) : null;
|
|
2034
|
+
this.hydrationClearedTopics.add(topic);
|
|
2000
2035
|
this.buffers.delete(topic);
|
|
2001
2036
|
this.pendingReplayPersistence = this.pendingReplayPersistence.filter((message) => message.topic !== topic);
|
|
2002
|
-
if (this.
|
|
2003
|
-
void this.withPersistenceRetry(PERSISTENCE_OPERATION.CLEAR_TOPIC, () => this.persistence.clearTopic(topic)).catch((error) => this.onPersistenceError(error));
|
|
2004
|
-
}
|
|
2037
|
+
if (clearing) void clearing.catch((error) => this.onPersistenceError(error));
|
|
2005
2038
|
}
|
|
2006
2039
|
/** Clear all in-memory replay buffers and, when supported, durable history.
|
|
2007
2040
|
* Reports persistence failures and rethrows, mirroring the public API
|
|
2008
2041
|
* contract that callers can observe a failed clear. */
|
|
2009
2042
|
async clearAll() {
|
|
2010
2043
|
if (!this.buffers) return;
|
|
2044
|
+
const clearing = this.persistence?.clear ? this.withPersistenceRetry(PERSISTENCE_OPERATION.CLEAR, () => this.persistence.clear()) : null;
|
|
2045
|
+
this.hydrationClearAll = true;
|
|
2011
2046
|
this.buffers.clear();
|
|
2012
2047
|
this.pendingReplayPersistence = [];
|
|
2013
|
-
if (
|
|
2048
|
+
if (clearing) {
|
|
2014
2049
|
try {
|
|
2015
|
-
await
|
|
2050
|
+
await clearing;
|
|
2016
2051
|
} catch (error) {
|
|
2017
2052
|
this.onPersistenceError(error);
|
|
2018
2053
|
throw error;
|
|
@@ -2022,11 +2057,13 @@ var ReplayManager = class {
|
|
|
2022
2057
|
/** Clear replay history for one exact topic, including durable storage. */
|
|
2023
2058
|
async clearTopic(topic) {
|
|
2024
2059
|
if (!this.buffers) return;
|
|
2060
|
+
const clearing = this.persistence?.clearTopic ? this.withPersistenceRetry(PERSISTENCE_OPERATION.CLEAR_TOPIC, () => this.persistence.clearTopic(topic)) : null;
|
|
2061
|
+
this.hydrationClearedTopics.add(topic);
|
|
2025
2062
|
this.buffers.delete(topic);
|
|
2026
2063
|
this.pendingReplayPersistence = this.pendingReplayPersistence.filter((message) => message.topic !== topic);
|
|
2027
|
-
if (
|
|
2064
|
+
if (clearing) {
|
|
2028
2065
|
try {
|
|
2029
|
-
await
|
|
2066
|
+
await clearing;
|
|
2030
2067
|
} catch (error) {
|
|
2031
2068
|
this.onPersistenceError(error);
|
|
2032
2069
|
throw error;
|
|
@@ -2036,7 +2073,9 @@ var ReplayManager = class {
|
|
|
2036
2073
|
/** Remove replay entries older than an epoch-millisecond cutoff. */
|
|
2037
2074
|
async clearBefore(timestamp) {
|
|
2038
2075
|
if (!Number.isFinite(timestamp)) throw new TypeError("timestamp must be finite.");
|
|
2076
|
+
const clearing = this.persistence?.clearBefore ? this.withPersistenceRetry(PERSISTENCE_OPERATION.CLEAR_BEFORE, () => this.persistence.clearBefore(timestamp)) : null;
|
|
2039
2077
|
if (this.buffers) {
|
|
2078
|
+
this.hydrationClearBefore = this.hydrationClearBefore === null ? timestamp : Math.max(this.hydrationClearBefore, timestamp);
|
|
2040
2079
|
for (const [topic, messages] of this.buffers) {
|
|
2041
2080
|
const kept = messages.filter((message) => message.timestamp === void 0 || message.timestamp >= timestamp);
|
|
2042
2081
|
if (kept.length) this.buffers.set(topic, kept);
|
|
@@ -2046,9 +2085,9 @@ var ReplayManager = class {
|
|
|
2046
2085
|
this.pendingReplayPersistence = this.pendingReplayPersistence.filter(
|
|
2047
2086
|
(message) => message.timestamp === void 0 || message.timestamp >= timestamp
|
|
2048
2087
|
);
|
|
2049
|
-
if (
|
|
2088
|
+
if (clearing) {
|
|
2050
2089
|
try {
|
|
2051
|
-
await
|
|
2090
|
+
await clearing;
|
|
2052
2091
|
} catch (error) {
|
|
2053
2092
|
this.onPersistenceError(error);
|
|
2054
2093
|
throw error;
|
|
@@ -2058,6 +2097,11 @@ var ReplayManager = class {
|
|
|
2058
2097
|
/** Start the periodic retention sweep. No-op when no durable retention
|
|
2059
2098
|
* config makes it necessary. */
|
|
2060
2099
|
start() {
|
|
2100
|
+
if (this.hydrationFailed) {
|
|
2101
|
+
this.hydrationComplete = false;
|
|
2102
|
+
this.hydrationFailed = false;
|
|
2103
|
+
}
|
|
2104
|
+
void this.requestHydration();
|
|
2061
2105
|
if (this.retentionTimer || !this.retentionMs || !this.retentionSweepMs || !this.persistence?.clearBefore) return;
|
|
2062
2106
|
this.retentionTimer = setInterval(() => {
|
|
2063
2107
|
this.scheduleRetentionCleanup(this.now() - this.retentionMs);
|
|
@@ -2072,13 +2116,25 @@ var ReplayManager = class {
|
|
|
2072
2116
|
* or stopped bus does not keep hammering the store) and stop the sweep. */
|
|
2073
2117
|
suspend() {
|
|
2074
2118
|
this.retryGeneration += 1;
|
|
2119
|
+
if (this.hydration) {
|
|
2120
|
+
this.hydrationEpoch += 1;
|
|
2121
|
+
this.hydration = null;
|
|
2122
|
+
this.hydrationComplete = false;
|
|
2123
|
+
}
|
|
2075
2124
|
this.pendingReplayPersistence = [];
|
|
2076
2125
|
this.retentionCutoff = null;
|
|
2077
2126
|
this.stop();
|
|
2078
2127
|
}
|
|
2079
|
-
/** Drop all in-memory buffers
|
|
2128
|
+
/** Drop all in-memory buffers and require hydration for the next lifecycle. */
|
|
2080
2129
|
resetBuffers() {
|
|
2081
2130
|
this.buffers?.clear();
|
|
2131
|
+
this.hydrationEpoch += 1;
|
|
2132
|
+
this.hydration = null;
|
|
2133
|
+
this.hydrationComplete = false;
|
|
2134
|
+
this.hydrationFailed = false;
|
|
2135
|
+
this.hydrationClearAll = false;
|
|
2136
|
+
this.hydrationClearedTopics.clear();
|
|
2137
|
+
this.hydrationClearBefore = null;
|
|
2082
2138
|
}
|
|
2083
2139
|
/** Buffer occupancy for diagnostics. `bytes` is an approximate in-memory
|
|
2084
2140
|
* payload footprint (same heuristic as adaptive load weighting), computed on
|
|
@@ -2130,38 +2186,78 @@ var ReplayManager = class {
|
|
|
2130
2186
|
void this.withPersistenceRetry(PERSISTENCE_OPERATION.APPEND, () => this.persistence.appendBatch(batch)).catch((error) => this.onPersistenceError(error));
|
|
2131
2187
|
});
|
|
2132
2188
|
}
|
|
2133
|
-
/**
|
|
2189
|
+
/** Start the active lifecycle's one-shot hydration, if it has not completed
|
|
2190
|
+
* or deliberately skipped hydration already. */
|
|
2191
|
+
requestHydration() {
|
|
2192
|
+
if (!this.buffers || !this.persistence) {
|
|
2193
|
+
this.hydrationComplete = true;
|
|
2194
|
+
return Promise.resolve();
|
|
2195
|
+
}
|
|
2196
|
+
if (this.hydration) return this.hydration;
|
|
2197
|
+
if (this.hydrationComplete) return Promise.resolve();
|
|
2198
|
+
const epoch = this.hydrationEpoch;
|
|
2199
|
+
const generation = this.retryGeneration;
|
|
2200
|
+
const operation = this.hydrate(epoch, generation);
|
|
2201
|
+
this.hydration = operation;
|
|
2202
|
+
return operation;
|
|
2203
|
+
}
|
|
2204
|
+
/** Load durable history into the in-memory rings once per lifecycle, pruning
|
|
2134
2205
|
* entries past the retention window first. Failures are reported but do not
|
|
2135
2206
|
* block startup — the bus runs with whatever survived. */
|
|
2136
|
-
async hydrate() {
|
|
2207
|
+
async hydrate(epoch, generation) {
|
|
2137
2208
|
if (!this.buffers || !this.persistence) {
|
|
2138
2209
|
return;
|
|
2139
2210
|
}
|
|
2140
2211
|
try {
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
if (
|
|
2148
|
-
|
|
2149
|
-
|
|
2212
|
+
try {
|
|
2213
|
+
if (this.retentionMs !== void 0 && this.persistence.clearBefore) {
|
|
2214
|
+
await this.withPersistenceRetry(PERSISTENCE_OPERATION.CLEAR_BEFORE, () => this.persistence.clearBefore(this.now() - this.retentionMs));
|
|
2215
|
+
}
|
|
2216
|
+
const loaded = await this.withPersistenceRetry(PERSISTENCE_OPERATION.LOAD, () => this.persistence.load());
|
|
2217
|
+
if (generation !== this.retryGeneration) throw new PersistenceRetryCancelledError();
|
|
2218
|
+
if (epoch !== this.hydrationEpoch) return;
|
|
2219
|
+
const loadedByTopic = /* @__PURE__ */ new Map();
|
|
2220
|
+
for (const message of loaded) {
|
|
2221
|
+
if (this.hydrationClearAll || this.hydrationClearedTopics.has(message.topic) || this.hydrationClearBefore !== null && message.timestamp !== void 0 && message.timestamp < this.hydrationClearBefore) continue;
|
|
2222
|
+
let buffer = loadedByTopic.get(message.topic);
|
|
2223
|
+
if (!buffer) {
|
|
2224
|
+
buffer = [];
|
|
2225
|
+
loadedByTopic.set(message.topic, buffer);
|
|
2226
|
+
}
|
|
2227
|
+
buffer.push(message);
|
|
2228
|
+
}
|
|
2229
|
+
for (const [topic, durableBuffer] of loadedByTopic) {
|
|
2230
|
+
const liveBuffer = this.buffers.get(topic);
|
|
2231
|
+
this.buffers.set(topic, liveBuffer ? [...durableBuffer, ...liveBuffer] : durableBuffer);
|
|
2232
|
+
}
|
|
2233
|
+
const hydrationNow = this.now();
|
|
2234
|
+
for (const [topic, buffer] of this.buffers) {
|
|
2235
|
+
const pruned = pruneReplayHistory(buffer, {
|
|
2236
|
+
maxPerTopic: this.maxPerTopic,
|
|
2237
|
+
pruneStrategy: this.pruneStrategy,
|
|
2238
|
+
retentionMs: this.retentionMs,
|
|
2239
|
+
now: hydrationNow
|
|
2240
|
+
});
|
|
2241
|
+
if (pruned !== buffer) this.buffers.set(topic, pruned);
|
|
2242
|
+
}
|
|
2243
|
+
this.hydrationComplete = true;
|
|
2244
|
+
this.hydrationFailed = false;
|
|
2245
|
+
} catch (error) {
|
|
2246
|
+
if (generation !== this.retryGeneration) {
|
|
2247
|
+
this.onPersistenceError(
|
|
2248
|
+
error instanceof PersistenceRetryCancelledError ? error : new PersistenceRetryCancelledError()
|
|
2249
|
+
);
|
|
2250
|
+
return;
|
|
2150
2251
|
}
|
|
2151
|
-
|
|
2252
|
+
if (epoch !== this.hydrationEpoch) return;
|
|
2253
|
+
this.onPersistenceError(error);
|
|
2254
|
+
this.hydrationComplete = true;
|
|
2255
|
+
this.hydrationFailed = true;
|
|
2152
2256
|
}
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
maxPerTopic: this.maxPerTopic,
|
|
2157
|
-
pruneStrategy: this.pruneStrategy,
|
|
2158
|
-
retentionMs: this.retentionMs,
|
|
2159
|
-
now: hydrationNow
|
|
2160
|
-
});
|
|
2161
|
-
if (pruned !== buffer) this.buffers.set(topic, pruned);
|
|
2257
|
+
} finally {
|
|
2258
|
+
if (this.hydrationEpoch === epoch && this.retryGeneration === generation) {
|
|
2259
|
+
this.hydration = null;
|
|
2162
2260
|
}
|
|
2163
|
-
} catch (error) {
|
|
2164
|
-
this.onPersistenceError(error);
|
|
2165
2261
|
}
|
|
2166
2262
|
}
|
|
2167
2263
|
/** Coalesce retention cleanup: the newest cutoff wins while one pass runs,
|
|
@@ -2185,7 +2281,7 @@ var ReplayManager = class {
|
|
|
2185
2281
|
}
|
|
2186
2282
|
})().finally(() => {
|
|
2187
2283
|
this.retentionCleanup = null;
|
|
2188
|
-
if (this.retentionCutoff !== null
|
|
2284
|
+
if (this.retentionCutoff !== null) {
|
|
2189
2285
|
this.scheduleRetentionCleanup(this.retentionCutoff);
|
|
2190
2286
|
}
|
|
2191
2287
|
});
|
|
@@ -2338,7 +2434,7 @@ var DedupManager = class {
|
|
|
2338
2434
|
};
|
|
2339
2435
|
|
|
2340
2436
|
// src/core/version.ts
|
|
2341
|
-
var SDK_VERSION = true ? "0.20.
|
|
2437
|
+
var SDK_VERSION = true ? "0.20.92" : "";
|
|
2342
2438
|
|
|
2343
2439
|
// src/core/data-bus.ts
|
|
2344
2440
|
var DEFAULT_REPLAY_MAX_PER_TOPIC = 100;
|
|
@@ -2414,11 +2510,21 @@ var CrossTabDataBus = class {
|
|
|
2414
2510
|
recoveryGateRelease = null;
|
|
2415
2511
|
recoveryTimer = null;
|
|
2416
2512
|
recoveryTimerToken = 0;
|
|
2513
|
+
// Invalidates operations parked on the recovery gate when a hide/stop
|
|
2514
|
+
// supersedes the recovery cycle. Their microtask may run after an immediate
|
|
2515
|
+
// explicit start has cleared `suspended`, so a state check alone is not
|
|
2516
|
+
// enough to keep stale work from reaching the replacement transport.
|
|
2517
|
+
recoveryCancellationToken = 0;
|
|
2417
2518
|
// Once an automatic attempt fails, an explicit transport operation may
|
|
2418
2519
|
// recover immediately instead of waiting for the next paced attempt. The
|
|
2419
2520
|
// gate still stays closed so the operation cannot reach the failed
|
|
2420
2521
|
// transport; it is released by the successful on-demand reopen.
|
|
2421
2522
|
recoveryDemandAllowed = false;
|
|
2523
|
+
// Number of transport operations currently parked behind `recoveryGate`.
|
|
2524
|
+
// When an automatic attempt fails, these already-parked operations are
|
|
2525
|
+
// themselves demand: the failure path starts an on-demand reopen instead of
|
|
2526
|
+
// stranding them until some unrelated future operation arrives.
|
|
2527
|
+
recoveryWaiters = 0;
|
|
2422
2528
|
/** Monotonic generation incremented on every successful transport open.
|
|
2423
2529
|
* Stays in lockstep with `lastSuccessAt` so callers can detect that the
|
|
2424
2530
|
* transport has been reopened even if the timestamp window is short. */
|
|
@@ -2533,7 +2639,7 @@ var CrossTabDataBus = class {
|
|
|
2533
2639
|
this.suspendTransport();
|
|
2534
2640
|
},
|
|
2535
2641
|
onResume: () => {
|
|
2536
|
-
this.resumeSuspendedResources();
|
|
2642
|
+
if (!this.resumeSuspendedResources()) return;
|
|
2537
2643
|
this.resumeTransport();
|
|
2538
2644
|
},
|
|
2539
2645
|
onDiagnostic: (event) => {
|
|
@@ -2562,11 +2668,13 @@ var CrossTabDataBus = class {
|
|
|
2562
2668
|
const transportDown = !this.transportReady || this.status === WORKER_STATUS.ERROR || this.status === WORKER_STATUS.DISCONNECTED;
|
|
2563
2669
|
if (!transportDown) return Promise.resolve();
|
|
2564
2670
|
this.activeConfig = config;
|
|
2565
|
-
this.resetFailureState();
|
|
2671
|
+
this.resetFailureState(true);
|
|
2566
2672
|
const resumingFromSuspend = this.suspended;
|
|
2567
|
-
if (resumingFromSuspend
|
|
2673
|
+
if (resumingFromSuspend && !this.resumeSuspendedResources()) {
|
|
2674
|
+
return this.stopPromise ?? Promise.resolve();
|
|
2675
|
+
}
|
|
2568
2676
|
const opening2 = this.reopenTransport();
|
|
2569
|
-
if (resumingFromSuspend) this.cluster.start();
|
|
2677
|
+
if (resumingFromSuspend && !this.stopping && !this.suspended) this.cluster.start();
|
|
2570
2678
|
return opening2;
|
|
2571
2679
|
}
|
|
2572
2680
|
this.started = true;
|
|
@@ -2574,12 +2682,6 @@ var CrossTabDataBus = class {
|
|
|
2574
2682
|
this.suspended = false;
|
|
2575
2683
|
this.activeConfig = config;
|
|
2576
2684
|
this.resetFailureState();
|
|
2577
|
-
this.trace.start();
|
|
2578
|
-
this.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.START });
|
|
2579
|
-
this.startDedupSweep();
|
|
2580
|
-
this.replayManager.start();
|
|
2581
|
-
this.updateStatus(WORKER_STATUS.CONNECTING);
|
|
2582
|
-
this.cluster.start();
|
|
2583
2685
|
const lifecycleEpoch = ++this.lifecycleEpoch;
|
|
2584
2686
|
const opening = this.openTransport(
|
|
2585
2687
|
config,
|
|
@@ -2588,7 +2690,17 @@ var CrossTabDataBus = class {
|
|
|
2588
2690
|
lifecycleEpoch
|
|
2589
2691
|
);
|
|
2590
2692
|
this.startPromise = opening;
|
|
2693
|
+
this.trace.start();
|
|
2694
|
+
this.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.START });
|
|
2695
|
+
if (lifecycleEpoch !== this.lifecycleEpoch || this.stopping) return opening;
|
|
2696
|
+
this.startDedupSweep();
|
|
2697
|
+
this.replayManager.start();
|
|
2698
|
+
this.updateStatus(WORKER_STATUS.CONNECTING);
|
|
2699
|
+
if (lifecycleEpoch !== this.lifecycleEpoch || this.stopping) return opening;
|
|
2700
|
+
this.cluster.start();
|
|
2701
|
+
if (lifecycleEpoch !== this.lifecycleEpoch || this.stopping) return opening;
|
|
2591
2702
|
for (const topic of this.topicHandlers.keys()) {
|
|
2703
|
+
if (lifecycleEpoch !== this.lifecycleEpoch || this.stopping) break;
|
|
2592
2704
|
this.cluster.subscribe(topic);
|
|
2593
2705
|
}
|
|
2594
2706
|
void opening.then(
|
|
@@ -2650,27 +2762,49 @@ var CrossTabDataBus = class {
|
|
|
2650
2762
|
/** Cancel a pending automatic retry when an explicit lifecycle transition
|
|
2651
2763
|
* supersedes it. The released gate re-enters runTransport(), which then
|
|
2652
2764
|
* follows the newest start/stop/suspend intent. */
|
|
2653
|
-
cancelScheduledRecovery() {
|
|
2765
|
+
cancelScheduledRecovery(invalidateParkedOperations = false, releaseGate = true) {
|
|
2654
2766
|
this.recoveryTimerToken += 1;
|
|
2767
|
+
if (invalidateParkedOperations) this.recoveryCancellationToken += 1;
|
|
2655
2768
|
if (this.recoveryTimer !== null) {
|
|
2656
2769
|
clearTimeout(this.recoveryTimer);
|
|
2657
2770
|
this.recoveryTimer = null;
|
|
2658
2771
|
}
|
|
2659
|
-
this.releaseRecoveryGate();
|
|
2772
|
+
if (releaseGate) this.releaseRecoveryGate();
|
|
2660
2773
|
}
|
|
2661
2774
|
/** Keep the recovery gate closed after a failed attempt while allowing the
|
|
2662
2775
|
* next explicit transport operation to start an immediate on-demand reopen.
|
|
2663
|
-
* If no gate/successor retry remains, release any waiters.
|
|
2664
|
-
|
|
2776
|
+
* If no gate/successor retry remains, release any waiters.
|
|
2777
|
+
*
|
|
2778
|
+
* `kickParkedWaiters` is set only when the failure is an automatic attempt: an
|
|
2779
|
+
* operation that was already parked on the gate is itself demand, so it must
|
|
2780
|
+
* not wait for some unrelated future operation. A failed *on-demand* reopen
|
|
2781
|
+
* passes `false`, so it re-arms the flag for a later operation instead of
|
|
2782
|
+
* looping on its own failure. */
|
|
2783
|
+
allowDemandRecovery(kickParkedWaiters = false) {
|
|
2665
2784
|
if (this.recoveryGate !== null && this.started && !this.stopping && !this.suspended && this.status === WORKER_STATUS.ERROR) {
|
|
2666
2785
|
this.recoveryDemandAllowed = true;
|
|
2786
|
+
if (kickParkedWaiters && this.recoveryWaiters > 0) this.startDemandRecovery();
|
|
2667
2787
|
return;
|
|
2668
2788
|
}
|
|
2669
2789
|
this.releaseRecoveryGate();
|
|
2670
2790
|
}
|
|
2791
|
+
/** Start one on-demand reopen if a failed attempt has left parked operations
|
|
2792
|
+
* and enabled demand recovery. Consumes the demand token so at most one
|
|
2793
|
+
* reopen is issued; every waiter stays behind the gate until it succeeds. */
|
|
2794
|
+
startDemandRecovery() {
|
|
2795
|
+
if (!this.recoveryDemandAllowed) return;
|
|
2796
|
+
if (this.status !== WORKER_STATUS.ERROR || this.suspended || this.stopping) return;
|
|
2797
|
+
this.recoveryDemandAllowed = false;
|
|
2798
|
+
const opening = this.reopenTransport();
|
|
2799
|
+
void opening.then(
|
|
2800
|
+
() => this.releaseRecoveryGate(),
|
|
2801
|
+
() => this.allowDemandRecovery()
|
|
2802
|
+
);
|
|
2803
|
+
}
|
|
2671
2804
|
/** Reset failure and recovery diagnostics for a new explicit start session. */
|
|
2672
|
-
resetFailureState() {
|
|
2673
|
-
this.cancelScheduledRecovery();
|
|
2805
|
+
resetFailureState(preserveRecoveryGate = false) {
|
|
2806
|
+
this.cancelScheduledRecovery(false, !preserveRecoveryGate);
|
|
2807
|
+
if (preserveRecoveryGate) this.recoveryDemandAllowed = false;
|
|
2674
2808
|
this.lastError = null;
|
|
2675
2809
|
this.lastErrorAt = null;
|
|
2676
2810
|
this.lastFailure = null;
|
|
@@ -3010,22 +3144,36 @@ var CrossTabDataBus = class {
|
|
|
3010
3144
|
if (!this.started && !this.startPromise && !this.pendingStop && !this.transportReady) {
|
|
3011
3145
|
return Promise.resolve();
|
|
3012
3146
|
}
|
|
3147
|
+
let resolveGate;
|
|
3148
|
+
const stopGate = new Promise((resolve) => {
|
|
3149
|
+
resolveGate = resolve;
|
|
3150
|
+
});
|
|
3151
|
+
this.stopPromise = stopGate;
|
|
3152
|
+
this.beginStop();
|
|
3013
3153
|
const stopPromise = this.performStop();
|
|
3014
|
-
this.stopPromise = stopPromise;
|
|
3015
3154
|
void stopPromise.then(
|
|
3016
3155
|
() => {
|
|
3017
|
-
if (this.stopPromise ===
|
|
3156
|
+
if (this.stopPromise === stopGate) this.stopPromise = null;
|
|
3157
|
+
resolveGate();
|
|
3018
3158
|
},
|
|
3019
3159
|
() => {
|
|
3020
|
-
if (this.stopPromise ===
|
|
3160
|
+
if (this.stopPromise === stopGate) this.stopPromise = null;
|
|
3161
|
+
resolveGate();
|
|
3021
3162
|
}
|
|
3022
3163
|
);
|
|
3023
|
-
return
|
|
3164
|
+
return stopGate;
|
|
3024
3165
|
}
|
|
3025
|
-
|
|
3166
|
+
/**
|
|
3167
|
+
* Synchronous teardown prelude. Flips `stopping` (the authoritative
|
|
3168
|
+
* in-flight signal), cancels scheduled work, releases handlers, and emits the
|
|
3169
|
+
* observable STOP lifecycle event. stop() calls it after the shared gate is
|
|
3170
|
+
* installed but in the same tick, so the stop still takes effect immediately
|
|
3171
|
+
* while a re-entrant stop() from the STOP event shares the one teardown.
|
|
3172
|
+
*/
|
|
3173
|
+
beginStop() {
|
|
3026
3174
|
this.lifecycleEpoch += 1;
|
|
3027
3175
|
this.stopping = true;
|
|
3028
|
-
this.cancelScheduledRecovery();
|
|
3176
|
+
this.cancelScheduledRecovery(true);
|
|
3029
3177
|
this.replayManager.suspend();
|
|
3030
3178
|
this.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.STOP });
|
|
3031
3179
|
this.trace.stop();
|
|
@@ -3033,6 +3181,8 @@ var CrossTabDataBus = class {
|
|
|
3033
3181
|
this.topicHandlers.clear();
|
|
3034
3182
|
this.replayManager.resetBuffers();
|
|
3035
3183
|
this.cluster.stop();
|
|
3184
|
+
}
|
|
3185
|
+
async performStop() {
|
|
3036
3186
|
try {
|
|
3037
3187
|
await this.startPromise?.catch(() => void 0);
|
|
3038
3188
|
const pendingStop = this.pendingStop;
|
|
@@ -3049,8 +3199,6 @@ var CrossTabDataBus = class {
|
|
|
3049
3199
|
this.transportReady = false;
|
|
3050
3200
|
this.startPromise = null;
|
|
3051
3201
|
this.pendingStop = null;
|
|
3052
|
-
this.lastError = null;
|
|
3053
|
-
this.lastErrorAt = null;
|
|
3054
3202
|
this.activeConfig = void 0;
|
|
3055
3203
|
this.recoveryAttempt = 0;
|
|
3056
3204
|
this.recoveryExhausted = false;
|
|
@@ -3147,7 +3295,9 @@ var CrossTabDataBus = class {
|
|
|
3147
3295
|
const opening = this.reopenTransport(attempt);
|
|
3148
3296
|
void opening.then(
|
|
3149
3297
|
() => this.releaseRecoveryGate(),
|
|
3150
|
-
|
|
3298
|
+
// Operations already parked on the gate are demand: run one
|
|
3299
|
+
// on-demand reopen now instead of waiting for an unrelated event.
|
|
3300
|
+
() => this.allowDemandRecovery(true)
|
|
3151
3301
|
);
|
|
3152
3302
|
}, this.recoveryCooldownMs);
|
|
3153
3303
|
}
|
|
@@ -3250,10 +3400,13 @@ var CrossTabDataBus = class {
|
|
|
3250
3400
|
* pageshow path and explicit start() must run this so an explicit resume
|
|
3251
3401
|
* cannot leave trace metrics and periodic cleanup timers permanently off. */
|
|
3252
3402
|
resumeSuspendedResources() {
|
|
3403
|
+
const lifecycleEpoch = this.lifecycleEpoch;
|
|
3253
3404
|
this.trace.start();
|
|
3254
3405
|
this.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.RESUME });
|
|
3406
|
+
if (lifecycleEpoch !== this.lifecycleEpoch || this.stopping) return false;
|
|
3255
3407
|
this.startDedupSweep();
|
|
3256
3408
|
this.replayManager.start();
|
|
3409
|
+
return lifecycleEpoch === this.lifecycleEpoch && !this.stopping;
|
|
3257
3410
|
}
|
|
3258
3411
|
/**
|
|
3259
3412
|
* Suspend the transport when the tab goes hidden. Stops the transport and
|
|
@@ -3261,12 +3414,13 @@ var CrossTabDataBus = class {
|
|
|
3261
3414
|
*/
|
|
3262
3415
|
suspendTransport() {
|
|
3263
3416
|
if (this.stopping) return;
|
|
3264
|
-
this.lifecycleEpoch
|
|
3417
|
+
const suspensionEpoch = ++this.lifecycleEpoch;
|
|
3265
3418
|
this.suspended = true;
|
|
3266
|
-
this.cancelScheduledRecovery();
|
|
3419
|
+
this.cancelScheduledRecovery(true);
|
|
3267
3420
|
this.transportReady = false;
|
|
3268
3421
|
this.transportSubscribedTopics.clear();
|
|
3269
3422
|
this.updateStatus(WORKER_STATUS.DISCONNECTED);
|
|
3423
|
+
if (suspensionEpoch !== this.lifecycleEpoch || this.stopping || !this.suspended) return;
|
|
3270
3424
|
if (this.pendingStop && (this.startPromise === null || this.startPromise === this.pendingStop)) {
|
|
3271
3425
|
this.startPromise = this.pendingStop;
|
|
3272
3426
|
return;
|
|
@@ -3301,13 +3455,18 @@ var CrossTabDataBus = class {
|
|
|
3301
3455
|
if (this.startPromise && this.startPromise !== this.pendingStop) return this.startPromise;
|
|
3302
3456
|
const config = this.activeConfig;
|
|
3303
3457
|
const traceAttempt = recoveryAttempt ?? (this.recoveryAttempt > 0 ? this.recoveryAttempt : void 0);
|
|
3304
|
-
this.started = true;
|
|
3305
|
-
this.suspended = false;
|
|
3306
|
-
this.updateStatus(WORKER_STATUS.CONNECTING);
|
|
3307
3458
|
const lifecycleEpoch = ++this.lifecycleEpoch;
|
|
3308
3459
|
const pending = this.startPromise ?? this.pendingStop ?? Promise.resolve();
|
|
3309
3460
|
const opening = pending.catch(() => void 0).then(() => this.openTransport(config, Promise.resolve(), false, lifecycleEpoch));
|
|
3310
3461
|
this.startPromise = opening;
|
|
3462
|
+
this.started = true;
|
|
3463
|
+
this.suspended = false;
|
|
3464
|
+
this.transportReady = false;
|
|
3465
|
+
this.updateStatus(WORKER_STATUS.CONNECTING);
|
|
3466
|
+
if (lifecycleEpoch !== this.lifecycleEpoch || this.stopping || this.suspended) {
|
|
3467
|
+
void opening.catch(() => void 0);
|
|
3468
|
+
return opening;
|
|
3469
|
+
}
|
|
3311
3470
|
void opening.then(
|
|
3312
3471
|
() => {
|
|
3313
3472
|
if (this.startPromise === opening) this.startPromise = null;
|
|
@@ -3337,17 +3496,13 @@ var CrossTabDataBus = class {
|
|
|
3337
3496
|
runTransport(operation) {
|
|
3338
3497
|
if (this.suspended) return;
|
|
3339
3498
|
if (this.recoveryGate && !this.stopping) {
|
|
3340
|
-
if (this.recoveryDemandAllowed && this.status === WORKER_STATUS.ERROR && !this.suspended) {
|
|
3341
|
-
this.recoveryDemandAllowed = false;
|
|
3342
|
-
const opening = this.reopenTransport();
|
|
3343
|
-
void opening.then(
|
|
3344
|
-
() => this.releaseRecoveryGate(),
|
|
3345
|
-
() => this.allowDemandRecovery()
|
|
3346
|
-
);
|
|
3347
|
-
}
|
|
3348
3499
|
const gate = this.recoveryGate;
|
|
3500
|
+
const cancellationToken = this.recoveryCancellationToken;
|
|
3501
|
+
this.startDemandRecovery();
|
|
3502
|
+
this.recoveryWaiters += 1;
|
|
3349
3503
|
void gate.then(() => {
|
|
3350
|
-
|
|
3504
|
+
this.recoveryWaiters -= 1;
|
|
3505
|
+
if (this.stopping || this.suspended || cancellationToken !== this.recoveryCancellationToken) return;
|
|
3351
3506
|
this.runTransport(operation);
|
|
3352
3507
|
});
|
|
3353
3508
|
return;
|