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