cross-tab-worker-databus 0.20.91 → 0.20.93
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 +38 -1
- package/dist/centrifuge.js +1 -1
- package/dist/{chunk-DMM5KBNG.js → chunk-OXEPZGXR.js} +273 -90
- package/dist/chunk-OXEPZGXR.js.map +7 -0
- package/dist/cjs/centrifuge.cjs +272 -89
- package/dist/cjs/centrifuge.cjs.map +2 -2
- package/dist/cjs/index.cjs +272 -89
- 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/environment.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/core/storage-batch.d.ts +4 -0
- package/dist/core/storage-batch.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 +17 -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 +17 -1
- package/package.json +5 -5
- package/dist/chunk-DMM5KBNG.js.map +0 -7
package/dist/cjs/index.cjs
CHANGED
|
@@ -229,6 +229,7 @@ function createStorageEventChannel(options) {
|
|
|
229
229
|
};
|
|
230
230
|
}
|
|
231
231
|
var tabIdentityInitialized = false;
|
|
232
|
+
var cachedTabId = null;
|
|
232
233
|
function createBrowserEnvironment(options) {
|
|
233
234
|
const channelFallback = options?.channelFallback ?? CHANNEL_FALLBACK.NONE;
|
|
234
235
|
return {
|
|
@@ -274,8 +275,9 @@ function canUseStorage(storage, probeKey) {
|
|
|
274
275
|
if (!storage) return false;
|
|
275
276
|
try {
|
|
276
277
|
storage.setItem(probeKey, "1");
|
|
278
|
+
const readable = storage.getItem(probeKey) === "1";
|
|
277
279
|
storage.removeItem(probeKey);
|
|
278
|
-
return
|
|
280
|
+
return readable;
|
|
279
281
|
} catch {
|
|
280
282
|
return false;
|
|
281
283
|
}
|
|
@@ -287,14 +289,21 @@ function getOrCreateTabId(environment, key = TAB_ID_STORAGE_KEY) {
|
|
|
287
289
|
const hasOpener = typeof window !== "undefined" && Boolean(window.opener);
|
|
288
290
|
if (existing && (!hasOpener || tabIdentityInitialized)) {
|
|
289
291
|
tabIdentityInitialized = true;
|
|
292
|
+
cachedTabId = existing;
|
|
290
293
|
return existing;
|
|
291
294
|
}
|
|
295
|
+
if (tabIdentityInitialized && cachedTabId) return cachedTabId;
|
|
292
296
|
const created = `tab-${environment.randomId()}`;
|
|
293
297
|
storage?.setItem(key, created);
|
|
294
298
|
tabIdentityInitialized = true;
|
|
299
|
+
cachedTabId = created;
|
|
295
300
|
return created;
|
|
296
301
|
} catch {
|
|
297
|
-
|
|
302
|
+
if (cachedTabId) return cachedTabId;
|
|
303
|
+
const created = `tab-${environment.randomId()}`;
|
|
304
|
+
tabIdentityInitialized = true;
|
|
305
|
+
cachedTabId = created;
|
|
306
|
+
return created;
|
|
298
307
|
}
|
|
299
308
|
}
|
|
300
309
|
|
|
@@ -455,7 +464,17 @@ var BatchingStorageWriter = class {
|
|
|
455
464
|
clear() {
|
|
456
465
|
this.pending.clear();
|
|
457
466
|
this.flushScheduled = false;
|
|
467
|
+
this.cancelRetry();
|
|
468
|
+
this.retryCount.clear();
|
|
469
|
+
this.retryDelayMs = INITIAL_RETRY_DELAY_MS;
|
|
458
470
|
this.storage.clear();
|
|
471
|
+
}
|
|
472
|
+
/** Drop queued mutations and cancel retry state without clearing storage.
|
|
473
|
+
* Used when the owning runtime is torn down: final best-effort writes have
|
|
474
|
+
* already been flushed, and failed writes must not keep timers alive. */
|
|
475
|
+
discardPending() {
|
|
476
|
+
this.pending.clear();
|
|
477
|
+
this.flushScheduled = false;
|
|
459
478
|
this.cancelRetry();
|
|
460
479
|
this.retryCount.clear();
|
|
461
480
|
this.retryDelayMs = INITIAL_RETRY_DELAY_MS;
|
|
@@ -738,6 +757,9 @@ var WorkerClusterRuntime = class {
|
|
|
738
757
|
started = false;
|
|
739
758
|
suspended = false;
|
|
740
759
|
lifecycleListening = false;
|
|
760
|
+
/** Invalidates an in-flight pageshow resume when a synchronous onResume
|
|
761
|
+
* callback stops or pauses the cluster before activate() is reached. */
|
|
762
|
+
lifecycleGeneration = 0;
|
|
741
763
|
currentRecord;
|
|
742
764
|
constructor(options) {
|
|
743
765
|
assertClusterOptions(options);
|
|
@@ -774,8 +796,14 @@ var WorkerClusterRuntime = class {
|
|
|
774
796
|
/** Start the cluster: register, listen for lifecycle events, and begin heartbeats. */
|
|
775
797
|
start() {
|
|
776
798
|
if (this.started) return;
|
|
777
|
-
this.
|
|
799
|
+
this.lifecycleGeneration += 1;
|
|
778
800
|
this.addLifecycleListeners();
|
|
801
|
+
if (this.environment.getVisibilityState() === TAB_VISIBILITY.HIDDEN) {
|
|
802
|
+
this.suspended = true;
|
|
803
|
+
this.handlers.onSuspend?.();
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
this.suspended = false;
|
|
779
807
|
this.activate();
|
|
780
808
|
}
|
|
781
809
|
/**
|
|
@@ -786,6 +814,7 @@ var WorkerClusterRuntime = class {
|
|
|
786
814
|
* stop() path where callers expect every Set/Map to be empty afterwards.
|
|
787
815
|
*/
|
|
788
816
|
stop() {
|
|
817
|
+
this.lifecycleGeneration += 1;
|
|
789
818
|
if (!this.started && !this.suspended) return;
|
|
790
819
|
this.pause();
|
|
791
820
|
this.flushStorage();
|
|
@@ -804,7 +833,15 @@ var WorkerClusterRuntime = class {
|
|
|
804
833
|
activate() {
|
|
805
834
|
if (this.started) return;
|
|
806
835
|
this.started = true;
|
|
807
|
-
|
|
836
|
+
if (this.storage) {
|
|
837
|
+
try {
|
|
838
|
+
this.channel = this.environment.createChannel(this.channelName);
|
|
839
|
+
} catch {
|
|
840
|
+
this.channel = null;
|
|
841
|
+
}
|
|
842
|
+
} else {
|
|
843
|
+
this.channel = null;
|
|
844
|
+
}
|
|
808
845
|
if (!this.channel) this.storage = null;
|
|
809
846
|
this.channel?.addEventListener("message", this.handleMessage);
|
|
810
847
|
const now = this.environment.now();
|
|
@@ -832,7 +869,11 @@ var WorkerClusterRuntime = class {
|
|
|
832
869
|
* to other workers, remove our worker record, and close the channel.
|
|
833
870
|
*/
|
|
834
871
|
pause() {
|
|
835
|
-
|
|
872
|
+
this.lifecycleGeneration += 1;
|
|
873
|
+
if (!this.started) {
|
|
874
|
+
if (this.lifecycleListening) this.suspended = true;
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
836
877
|
this.started = false;
|
|
837
878
|
this.suspended = true;
|
|
838
879
|
if (this.heartbeatHandle !== null) this.environment.clearInterval(this.heartbeatHandle);
|
|
@@ -846,6 +887,7 @@ var WorkerClusterRuntime = class {
|
|
|
846
887
|
this.removeStorage(this.workerStorageKey(this.workerId));
|
|
847
888
|
this.flushStorage();
|
|
848
889
|
this.notifyRegistry();
|
|
890
|
+
if (this.storage instanceof BatchingStorageWriter) this.storage.discardPending();
|
|
849
891
|
const channel = this.channel;
|
|
850
892
|
this.channel = null;
|
|
851
893
|
this.handlers.onSuspend?.();
|
|
@@ -1147,8 +1189,10 @@ var WorkerClusterRuntime = class {
|
|
|
1147
1189
|
handlePageHide = () => this.pause();
|
|
1148
1190
|
handlePageShow = () => {
|
|
1149
1191
|
if (!this.suspended) return;
|
|
1192
|
+
const generation = ++this.lifecycleGeneration;
|
|
1150
1193
|
this.suspended = false;
|
|
1151
1194
|
this.handlers.onResume?.();
|
|
1195
|
+
if (generation !== this.lifecycleGeneration) return;
|
|
1152
1196
|
this.activate();
|
|
1153
1197
|
};
|
|
1154
1198
|
handleVisibilityChange = () => {
|
|
@@ -1203,10 +1247,16 @@ var WorkerClusterRuntime = class {
|
|
|
1203
1247
|
if (message.targetWorkerId !== this.workerId) return;
|
|
1204
1248
|
this.rememberTopic(message.topic);
|
|
1205
1249
|
switch (message.action) {
|
|
1206
|
-
case CONTROL_ACTION.SUBSCRIBE:
|
|
1250
|
+
case CONTROL_ACTION.SUBSCRIBE: {
|
|
1251
|
+
const route = this.readRoute(message.topicKey);
|
|
1252
|
+
if (route && route.workerId !== this.workerId) return;
|
|
1253
|
+
if (route?.workerId === this.workerId && route.handoffFromWorkerId !== void 0 && route.confirmedAt === void 0) {
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1207
1256
|
this.assignedTopics.set(message.topicKey, message.topic);
|
|
1208
1257
|
this.confirmRoute(message.topicKey);
|
|
1209
1258
|
break;
|
|
1259
|
+
}
|
|
1210
1260
|
case CONTROL_ACTION.UNSUBSCRIBE:
|
|
1211
1261
|
if (this.releaseHandoffOnUnsubscribe(message)) return;
|
|
1212
1262
|
break;
|
|
@@ -1267,8 +1317,8 @@ var WorkerClusterRuntime = class {
|
|
|
1267
1317
|
}
|
|
1268
1318
|
/**
|
|
1269
1319
|
* 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
|
-
*
|
|
1320
|
+
* the release comes from the recorded previous owner, and the generation
|
|
1321
|
+
* exactly matches ours. Any other ROUTE_RELEASED is stale and dropped.
|
|
1272
1322
|
*/
|
|
1273
1323
|
handleRouteReleasedMessage(message) {
|
|
1274
1324
|
if (message.targetWorkerId !== this.workerId) return;
|
|
@@ -1281,11 +1331,11 @@ var WorkerClusterRuntime = class {
|
|
|
1281
1331
|
}
|
|
1282
1332
|
/** A ROUTE_RELEASED is stale (and must be dropped) unless the route still
|
|
1283
1333
|
* points to us, the release comes from the recorded previous owner, and
|
|
1284
|
-
* the release generation
|
|
1285
|
-
* earlier handoff round
|
|
1286
|
-
*
|
|
1334
|
+
* the release generation exactly matches ours. A delayed ACK from either an
|
|
1335
|
+
* earlier or later handoff round belongs to a different route and must not
|
|
1336
|
+
* confirm the current round. */
|
|
1287
1337
|
isStaleRouteRelease(route, message) {
|
|
1288
|
-
return route.workerId !== this.workerId || route.handoffFromWorkerId !== message.sourceWorkerId || message.generation
|
|
1338
|
+
return route.workerId !== this.workerId || route.handoffFromWorkerId !== message.sourceWorkerId || message.generation !== route.generation;
|
|
1289
1339
|
}
|
|
1290
1340
|
/** True when an unconfirmed handoff route has been stuck longer than a
|
|
1291
1341
|
* worker TTL. The ACK for a live handoff is posted synchronously with the
|
|
@@ -1912,7 +1962,7 @@ var ReplayManager = class {
|
|
|
1912
1962
|
this.trace = deps.trace;
|
|
1913
1963
|
this.onPersistenceError = deps.onPersistenceError;
|
|
1914
1964
|
this.onDispatchError = deps.onDispatchError;
|
|
1915
|
-
|
|
1965
|
+
void this.requestHydration();
|
|
1916
1966
|
}
|
|
1917
1967
|
deps;
|
|
1918
1968
|
buffers;
|
|
@@ -1931,7 +1981,18 @@ var ReplayManager = class {
|
|
|
1931
1981
|
retryGeneration = 0;
|
|
1932
1982
|
pendingReplayPersistence = [];
|
|
1933
1983
|
persistenceFlushScheduled = false;
|
|
1934
|
-
hydration
|
|
1984
|
+
/** Current hydration operation, if one belongs to the active lifecycle. */
|
|
1985
|
+
hydration = null;
|
|
1986
|
+
/** Invalidates a load from a superseded suspend/reset lifecycle. */
|
|
1987
|
+
hydrationEpoch = 0;
|
|
1988
|
+
/** Whether the active lifecycle has finished (or deliberately skipped) hydration. */
|
|
1989
|
+
hydrationComplete = false;
|
|
1990
|
+
/** A failed load is retried by the next explicit start(), not every replay request. */
|
|
1991
|
+
hydrationFailed = false;
|
|
1992
|
+
/** Mutations that must be applied to a load already in flight. */
|
|
1993
|
+
hydrationClearAll = false;
|
|
1994
|
+
hydrationClearedTopics = /* @__PURE__ */ new Set();
|
|
1995
|
+
hydrationClearBefore = null;
|
|
1935
1996
|
/** Coalesced retention cleanup: the newest cutoff wins while one is running. */
|
|
1936
1997
|
retentionCleanup = null;
|
|
1937
1998
|
retentionCutoff = null;
|
|
@@ -1985,7 +2046,7 @@ var ReplayManager = class {
|
|
|
1985
2046
|
if (!this.buffers) return;
|
|
1986
2047
|
const limit = typeof replayOption === "number" ? Math.min(Math.floor(replayOption), this.maxPerTopic) : this.maxPerTopic;
|
|
1987
2048
|
if (this.persistence) {
|
|
1988
|
-
void this.
|
|
2049
|
+
void this.requestHydration().then(() => {
|
|
1989
2050
|
if (isHandlerActive?.() ?? true) this.deliver(topic, limit, handler);
|
|
1990
2051
|
});
|
|
1991
2052
|
return;
|
|
@@ -1997,22 +2058,24 @@ var ReplayManager = class {
|
|
|
1997
2058
|
* clearTopic), and prune durable history. */
|
|
1998
2059
|
onTopicUnsubscribed(topic) {
|
|
1999
2060
|
if (!this.buffers) return;
|
|
2061
|
+
const clearing = this.persistence?.clearTopic ? this.withPersistenceRetry(PERSISTENCE_OPERATION.CLEAR_TOPIC, () => this.persistence.clearTopic(topic)) : null;
|
|
2062
|
+
this.hydrationClearedTopics.add(topic);
|
|
2000
2063
|
this.buffers.delete(topic);
|
|
2001
2064
|
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
|
-
}
|
|
2065
|
+
if (clearing) void clearing.catch((error) => this.onPersistenceError(error));
|
|
2005
2066
|
}
|
|
2006
2067
|
/** Clear all in-memory replay buffers and, when supported, durable history.
|
|
2007
2068
|
* Reports persistence failures and rethrows, mirroring the public API
|
|
2008
2069
|
* contract that callers can observe a failed clear. */
|
|
2009
2070
|
async clearAll() {
|
|
2010
2071
|
if (!this.buffers) return;
|
|
2072
|
+
const clearing = this.persistence?.clear ? this.withPersistenceRetry(PERSISTENCE_OPERATION.CLEAR, () => this.persistence.clear()) : null;
|
|
2073
|
+
this.hydrationClearAll = true;
|
|
2011
2074
|
this.buffers.clear();
|
|
2012
2075
|
this.pendingReplayPersistence = [];
|
|
2013
|
-
if (
|
|
2076
|
+
if (clearing) {
|
|
2014
2077
|
try {
|
|
2015
|
-
await
|
|
2078
|
+
await clearing;
|
|
2016
2079
|
} catch (error) {
|
|
2017
2080
|
this.onPersistenceError(error);
|
|
2018
2081
|
throw error;
|
|
@@ -2022,11 +2085,13 @@ var ReplayManager = class {
|
|
|
2022
2085
|
/** Clear replay history for one exact topic, including durable storage. */
|
|
2023
2086
|
async clearTopic(topic) {
|
|
2024
2087
|
if (!this.buffers) return;
|
|
2088
|
+
const clearing = this.persistence?.clearTopic ? this.withPersistenceRetry(PERSISTENCE_OPERATION.CLEAR_TOPIC, () => this.persistence.clearTopic(topic)) : null;
|
|
2089
|
+
this.hydrationClearedTopics.add(topic);
|
|
2025
2090
|
this.buffers.delete(topic);
|
|
2026
2091
|
this.pendingReplayPersistence = this.pendingReplayPersistence.filter((message) => message.topic !== topic);
|
|
2027
|
-
if (
|
|
2092
|
+
if (clearing) {
|
|
2028
2093
|
try {
|
|
2029
|
-
await
|
|
2094
|
+
await clearing;
|
|
2030
2095
|
} catch (error) {
|
|
2031
2096
|
this.onPersistenceError(error);
|
|
2032
2097
|
throw error;
|
|
@@ -2036,7 +2101,9 @@ var ReplayManager = class {
|
|
|
2036
2101
|
/** Remove replay entries older than an epoch-millisecond cutoff. */
|
|
2037
2102
|
async clearBefore(timestamp) {
|
|
2038
2103
|
if (!Number.isFinite(timestamp)) throw new TypeError("timestamp must be finite.");
|
|
2104
|
+
const clearing = this.persistence?.clearBefore ? this.withPersistenceRetry(PERSISTENCE_OPERATION.CLEAR_BEFORE, () => this.persistence.clearBefore(timestamp)) : null;
|
|
2039
2105
|
if (this.buffers) {
|
|
2106
|
+
this.hydrationClearBefore = this.hydrationClearBefore === null ? timestamp : Math.max(this.hydrationClearBefore, timestamp);
|
|
2040
2107
|
for (const [topic, messages] of this.buffers) {
|
|
2041
2108
|
const kept = messages.filter((message) => message.timestamp === void 0 || message.timestamp >= timestamp);
|
|
2042
2109
|
if (kept.length) this.buffers.set(topic, kept);
|
|
@@ -2046,9 +2113,9 @@ var ReplayManager = class {
|
|
|
2046
2113
|
this.pendingReplayPersistence = this.pendingReplayPersistence.filter(
|
|
2047
2114
|
(message) => message.timestamp === void 0 || message.timestamp >= timestamp
|
|
2048
2115
|
);
|
|
2049
|
-
if (
|
|
2116
|
+
if (clearing) {
|
|
2050
2117
|
try {
|
|
2051
|
-
await
|
|
2118
|
+
await clearing;
|
|
2052
2119
|
} catch (error) {
|
|
2053
2120
|
this.onPersistenceError(error);
|
|
2054
2121
|
throw error;
|
|
@@ -2058,6 +2125,11 @@ var ReplayManager = class {
|
|
|
2058
2125
|
/** Start the periodic retention sweep. No-op when no durable retention
|
|
2059
2126
|
* config makes it necessary. */
|
|
2060
2127
|
start() {
|
|
2128
|
+
if (this.hydrationFailed) {
|
|
2129
|
+
this.hydrationComplete = false;
|
|
2130
|
+
this.hydrationFailed = false;
|
|
2131
|
+
}
|
|
2132
|
+
void this.requestHydration();
|
|
2061
2133
|
if (this.retentionTimer || !this.retentionMs || !this.retentionSweepMs || !this.persistence?.clearBefore) return;
|
|
2062
2134
|
this.retentionTimer = setInterval(() => {
|
|
2063
2135
|
this.scheduleRetentionCleanup(this.now() - this.retentionMs);
|
|
@@ -2072,13 +2144,25 @@ var ReplayManager = class {
|
|
|
2072
2144
|
* or stopped bus does not keep hammering the store) and stop the sweep. */
|
|
2073
2145
|
suspend() {
|
|
2074
2146
|
this.retryGeneration += 1;
|
|
2147
|
+
if (this.hydration) {
|
|
2148
|
+
this.hydrationEpoch += 1;
|
|
2149
|
+
this.hydration = null;
|
|
2150
|
+
this.hydrationComplete = false;
|
|
2151
|
+
}
|
|
2075
2152
|
this.pendingReplayPersistence = [];
|
|
2076
2153
|
this.retentionCutoff = null;
|
|
2077
2154
|
this.stop();
|
|
2078
2155
|
}
|
|
2079
|
-
/** Drop all in-memory buffers
|
|
2156
|
+
/** Drop all in-memory buffers and require hydration for the next lifecycle. */
|
|
2080
2157
|
resetBuffers() {
|
|
2081
2158
|
this.buffers?.clear();
|
|
2159
|
+
this.hydrationEpoch += 1;
|
|
2160
|
+
this.hydration = null;
|
|
2161
|
+
this.hydrationComplete = false;
|
|
2162
|
+
this.hydrationFailed = false;
|
|
2163
|
+
this.hydrationClearAll = false;
|
|
2164
|
+
this.hydrationClearedTopics.clear();
|
|
2165
|
+
this.hydrationClearBefore = null;
|
|
2082
2166
|
}
|
|
2083
2167
|
/** Buffer occupancy for diagnostics. `bytes` is an approximate in-memory
|
|
2084
2168
|
* payload footprint (same heuristic as adaptive load weighting), computed on
|
|
@@ -2130,38 +2214,78 @@ var ReplayManager = class {
|
|
|
2130
2214
|
void this.withPersistenceRetry(PERSISTENCE_OPERATION.APPEND, () => this.persistence.appendBatch(batch)).catch((error) => this.onPersistenceError(error));
|
|
2131
2215
|
});
|
|
2132
2216
|
}
|
|
2133
|
-
/**
|
|
2217
|
+
/** Start the active lifecycle's one-shot hydration, if it has not completed
|
|
2218
|
+
* or deliberately skipped hydration already. */
|
|
2219
|
+
requestHydration() {
|
|
2220
|
+
if (!this.buffers || !this.persistence) {
|
|
2221
|
+
this.hydrationComplete = true;
|
|
2222
|
+
return Promise.resolve();
|
|
2223
|
+
}
|
|
2224
|
+
if (this.hydration) return this.hydration;
|
|
2225
|
+
if (this.hydrationComplete) return Promise.resolve();
|
|
2226
|
+
const epoch = this.hydrationEpoch;
|
|
2227
|
+
const generation = this.retryGeneration;
|
|
2228
|
+
const operation = this.hydrate(epoch, generation);
|
|
2229
|
+
this.hydration = operation;
|
|
2230
|
+
return operation;
|
|
2231
|
+
}
|
|
2232
|
+
/** Load durable history into the in-memory rings once per lifecycle, pruning
|
|
2134
2233
|
* entries past the retention window first. Failures are reported but do not
|
|
2135
2234
|
* block startup — the bus runs with whatever survived. */
|
|
2136
|
-
async hydrate() {
|
|
2235
|
+
async hydrate(epoch, generation) {
|
|
2137
2236
|
if (!this.buffers || !this.persistence) {
|
|
2138
2237
|
return;
|
|
2139
2238
|
}
|
|
2140
2239
|
try {
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
if (
|
|
2148
|
-
|
|
2149
|
-
|
|
2240
|
+
try {
|
|
2241
|
+
if (this.retentionMs !== void 0 && this.persistence.clearBefore) {
|
|
2242
|
+
await this.withPersistenceRetry(PERSISTENCE_OPERATION.CLEAR_BEFORE, () => this.persistence.clearBefore(this.now() - this.retentionMs));
|
|
2243
|
+
}
|
|
2244
|
+
const loaded = await this.withPersistenceRetry(PERSISTENCE_OPERATION.LOAD, () => this.persistence.load());
|
|
2245
|
+
if (generation !== this.retryGeneration) throw new PersistenceRetryCancelledError();
|
|
2246
|
+
if (epoch !== this.hydrationEpoch) return;
|
|
2247
|
+
const loadedByTopic = /* @__PURE__ */ new Map();
|
|
2248
|
+
for (const message of loaded) {
|
|
2249
|
+
if (this.hydrationClearAll || this.hydrationClearedTopics.has(message.topic) || this.hydrationClearBefore !== null && message.timestamp !== void 0 && message.timestamp < this.hydrationClearBefore) continue;
|
|
2250
|
+
let buffer = loadedByTopic.get(message.topic);
|
|
2251
|
+
if (!buffer) {
|
|
2252
|
+
buffer = [];
|
|
2253
|
+
loadedByTopic.set(message.topic, buffer);
|
|
2254
|
+
}
|
|
2255
|
+
buffer.push(message);
|
|
2256
|
+
}
|
|
2257
|
+
for (const [topic, durableBuffer] of loadedByTopic) {
|
|
2258
|
+
const liveBuffer = this.buffers.get(topic);
|
|
2259
|
+
this.buffers.set(topic, liveBuffer ? [...durableBuffer, ...liveBuffer] : durableBuffer);
|
|
2260
|
+
}
|
|
2261
|
+
const hydrationNow = this.now();
|
|
2262
|
+
for (const [topic, buffer] of this.buffers) {
|
|
2263
|
+
const pruned = pruneReplayHistory(buffer, {
|
|
2264
|
+
maxPerTopic: this.maxPerTopic,
|
|
2265
|
+
pruneStrategy: this.pruneStrategy,
|
|
2266
|
+
retentionMs: this.retentionMs,
|
|
2267
|
+
now: hydrationNow
|
|
2268
|
+
});
|
|
2269
|
+
if (pruned !== buffer) this.buffers.set(topic, pruned);
|
|
2270
|
+
}
|
|
2271
|
+
this.hydrationComplete = true;
|
|
2272
|
+
this.hydrationFailed = false;
|
|
2273
|
+
} catch (error) {
|
|
2274
|
+
if (generation !== this.retryGeneration) {
|
|
2275
|
+
this.onPersistenceError(
|
|
2276
|
+
error instanceof PersistenceRetryCancelledError ? error : new PersistenceRetryCancelledError()
|
|
2277
|
+
);
|
|
2278
|
+
return;
|
|
2150
2279
|
}
|
|
2151
|
-
|
|
2280
|
+
if (epoch !== this.hydrationEpoch) return;
|
|
2281
|
+
this.onPersistenceError(error);
|
|
2282
|
+
this.hydrationComplete = true;
|
|
2283
|
+
this.hydrationFailed = true;
|
|
2152
2284
|
}
|
|
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);
|
|
2285
|
+
} finally {
|
|
2286
|
+
if (this.hydrationEpoch === epoch && this.retryGeneration === generation) {
|
|
2287
|
+
this.hydration = null;
|
|
2162
2288
|
}
|
|
2163
|
-
} catch (error) {
|
|
2164
|
-
this.onPersistenceError(error);
|
|
2165
2289
|
}
|
|
2166
2290
|
}
|
|
2167
2291
|
/** Coalesce retention cleanup: the newest cutoff wins while one pass runs,
|
|
@@ -2185,7 +2309,7 @@ var ReplayManager = class {
|
|
|
2185
2309
|
}
|
|
2186
2310
|
})().finally(() => {
|
|
2187
2311
|
this.retentionCleanup = null;
|
|
2188
|
-
if (this.retentionCutoff !== null
|
|
2312
|
+
if (this.retentionCutoff !== null) {
|
|
2189
2313
|
this.scheduleRetentionCleanup(this.retentionCutoff);
|
|
2190
2314
|
}
|
|
2191
2315
|
});
|
|
@@ -2338,7 +2462,7 @@ var DedupManager = class {
|
|
|
2338
2462
|
};
|
|
2339
2463
|
|
|
2340
2464
|
// src/core/version.ts
|
|
2341
|
-
var SDK_VERSION = true ? "0.20.
|
|
2465
|
+
var SDK_VERSION = true ? "0.20.93" : "";
|
|
2342
2466
|
|
|
2343
2467
|
// src/core/data-bus.ts
|
|
2344
2468
|
var DEFAULT_REPLAY_MAX_PER_TOPIC = 100;
|
|
@@ -2414,11 +2538,21 @@ var CrossTabDataBus = class {
|
|
|
2414
2538
|
recoveryGateRelease = null;
|
|
2415
2539
|
recoveryTimer = null;
|
|
2416
2540
|
recoveryTimerToken = 0;
|
|
2541
|
+
// Invalidates operations parked on the recovery gate when a hide/stop
|
|
2542
|
+
// supersedes the recovery cycle. Their microtask may run after an immediate
|
|
2543
|
+
// explicit start has cleared `suspended`, so a state check alone is not
|
|
2544
|
+
// enough to keep stale work from reaching the replacement transport.
|
|
2545
|
+
recoveryCancellationToken = 0;
|
|
2417
2546
|
// Once an automatic attempt fails, an explicit transport operation may
|
|
2418
2547
|
// recover immediately instead of waiting for the next paced attempt. The
|
|
2419
2548
|
// gate still stays closed so the operation cannot reach the failed
|
|
2420
2549
|
// transport; it is released by the successful on-demand reopen.
|
|
2421
2550
|
recoveryDemandAllowed = false;
|
|
2551
|
+
// Number of transport operations currently parked behind `recoveryGate`.
|
|
2552
|
+
// When an automatic attempt fails, these already-parked operations are
|
|
2553
|
+
// themselves demand: the failure path starts an on-demand reopen instead of
|
|
2554
|
+
// stranding them until some unrelated future operation arrives.
|
|
2555
|
+
recoveryWaiters = 0;
|
|
2422
2556
|
/** Monotonic generation incremented on every successful transport open.
|
|
2423
2557
|
* Stays in lockstep with `lastSuccessAt` so callers can detect that the
|
|
2424
2558
|
* transport has been reopened even if the timestamp window is short. */
|
|
@@ -2533,7 +2667,7 @@ var CrossTabDataBus = class {
|
|
|
2533
2667
|
this.suspendTransport();
|
|
2534
2668
|
},
|
|
2535
2669
|
onResume: () => {
|
|
2536
|
-
this.resumeSuspendedResources();
|
|
2670
|
+
if (!this.resumeSuspendedResources()) return;
|
|
2537
2671
|
this.resumeTransport();
|
|
2538
2672
|
},
|
|
2539
2673
|
onDiagnostic: (event) => {
|
|
@@ -2562,11 +2696,13 @@ var CrossTabDataBus = class {
|
|
|
2562
2696
|
const transportDown = !this.transportReady || this.status === WORKER_STATUS.ERROR || this.status === WORKER_STATUS.DISCONNECTED;
|
|
2563
2697
|
if (!transportDown) return Promise.resolve();
|
|
2564
2698
|
this.activeConfig = config;
|
|
2565
|
-
this.resetFailureState();
|
|
2699
|
+
this.resetFailureState(true);
|
|
2566
2700
|
const resumingFromSuspend = this.suspended;
|
|
2567
|
-
if (resumingFromSuspend
|
|
2701
|
+
if (resumingFromSuspend && !this.resumeSuspendedResources()) {
|
|
2702
|
+
return this.stopPromise ?? Promise.resolve();
|
|
2703
|
+
}
|
|
2568
2704
|
const opening2 = this.reopenTransport();
|
|
2569
|
-
if (resumingFromSuspend) this.cluster.start();
|
|
2705
|
+
if (resumingFromSuspend && !this.stopping && !this.suspended) this.cluster.start();
|
|
2570
2706
|
return opening2;
|
|
2571
2707
|
}
|
|
2572
2708
|
this.started = true;
|
|
@@ -2574,12 +2710,6 @@ var CrossTabDataBus = class {
|
|
|
2574
2710
|
this.suspended = false;
|
|
2575
2711
|
this.activeConfig = config;
|
|
2576
2712
|
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
2713
|
const lifecycleEpoch = ++this.lifecycleEpoch;
|
|
2584
2714
|
const opening = this.openTransport(
|
|
2585
2715
|
config,
|
|
@@ -2588,7 +2718,17 @@ var CrossTabDataBus = class {
|
|
|
2588
2718
|
lifecycleEpoch
|
|
2589
2719
|
);
|
|
2590
2720
|
this.startPromise = opening;
|
|
2721
|
+
this.trace.start();
|
|
2722
|
+
this.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.START });
|
|
2723
|
+
if (lifecycleEpoch !== this.lifecycleEpoch || this.stopping) return opening;
|
|
2724
|
+
this.startDedupSweep();
|
|
2725
|
+
this.replayManager.start();
|
|
2726
|
+
this.updateStatus(WORKER_STATUS.CONNECTING);
|
|
2727
|
+
if (lifecycleEpoch !== this.lifecycleEpoch || this.stopping) return opening;
|
|
2728
|
+
this.cluster.start();
|
|
2729
|
+
if (lifecycleEpoch !== this.lifecycleEpoch || this.stopping) return opening;
|
|
2591
2730
|
for (const topic of this.topicHandlers.keys()) {
|
|
2731
|
+
if (lifecycleEpoch !== this.lifecycleEpoch || this.stopping) break;
|
|
2592
2732
|
this.cluster.subscribe(topic);
|
|
2593
2733
|
}
|
|
2594
2734
|
void opening.then(
|
|
@@ -2650,27 +2790,49 @@ var CrossTabDataBus = class {
|
|
|
2650
2790
|
/** Cancel a pending automatic retry when an explicit lifecycle transition
|
|
2651
2791
|
* supersedes it. The released gate re-enters runTransport(), which then
|
|
2652
2792
|
* follows the newest start/stop/suspend intent. */
|
|
2653
|
-
cancelScheduledRecovery() {
|
|
2793
|
+
cancelScheduledRecovery(invalidateParkedOperations = false, releaseGate = true) {
|
|
2654
2794
|
this.recoveryTimerToken += 1;
|
|
2795
|
+
if (invalidateParkedOperations) this.recoveryCancellationToken += 1;
|
|
2655
2796
|
if (this.recoveryTimer !== null) {
|
|
2656
2797
|
clearTimeout(this.recoveryTimer);
|
|
2657
2798
|
this.recoveryTimer = null;
|
|
2658
2799
|
}
|
|
2659
|
-
this.releaseRecoveryGate();
|
|
2800
|
+
if (releaseGate) this.releaseRecoveryGate();
|
|
2660
2801
|
}
|
|
2661
2802
|
/** Keep the recovery gate closed after a failed attempt while allowing the
|
|
2662
2803
|
* next explicit transport operation to start an immediate on-demand reopen.
|
|
2663
|
-
* If no gate/successor retry remains, release any waiters.
|
|
2664
|
-
|
|
2804
|
+
* If no gate/successor retry remains, release any waiters.
|
|
2805
|
+
*
|
|
2806
|
+
* `kickParkedWaiters` is set only when the failure is an automatic attempt: an
|
|
2807
|
+
* operation that was already parked on the gate is itself demand, so it must
|
|
2808
|
+
* not wait for some unrelated future operation. A failed *on-demand* reopen
|
|
2809
|
+
* passes `false`, so it re-arms the flag for a later operation instead of
|
|
2810
|
+
* looping on its own failure. */
|
|
2811
|
+
allowDemandRecovery(kickParkedWaiters = false) {
|
|
2665
2812
|
if (this.recoveryGate !== null && this.started && !this.stopping && !this.suspended && this.status === WORKER_STATUS.ERROR) {
|
|
2666
2813
|
this.recoveryDemandAllowed = true;
|
|
2814
|
+
if (kickParkedWaiters && this.recoveryWaiters > 0) this.startDemandRecovery();
|
|
2667
2815
|
return;
|
|
2668
2816
|
}
|
|
2669
2817
|
this.releaseRecoveryGate();
|
|
2670
2818
|
}
|
|
2819
|
+
/** Start one on-demand reopen if a failed attempt has left parked operations
|
|
2820
|
+
* and enabled demand recovery. Consumes the demand token so at most one
|
|
2821
|
+
* reopen is issued; every waiter stays behind the gate until it succeeds. */
|
|
2822
|
+
startDemandRecovery() {
|
|
2823
|
+
if (!this.recoveryDemandAllowed) return;
|
|
2824
|
+
if (this.status !== WORKER_STATUS.ERROR || this.suspended || this.stopping) return;
|
|
2825
|
+
this.recoveryDemandAllowed = false;
|
|
2826
|
+
const opening = this.reopenTransport();
|
|
2827
|
+
void opening.then(
|
|
2828
|
+
() => this.releaseRecoveryGate(),
|
|
2829
|
+
() => this.allowDemandRecovery()
|
|
2830
|
+
);
|
|
2831
|
+
}
|
|
2671
2832
|
/** Reset failure and recovery diagnostics for a new explicit start session. */
|
|
2672
|
-
resetFailureState() {
|
|
2673
|
-
this.cancelScheduledRecovery();
|
|
2833
|
+
resetFailureState(preserveRecoveryGate = false) {
|
|
2834
|
+
this.cancelScheduledRecovery(false, !preserveRecoveryGate);
|
|
2835
|
+
if (preserveRecoveryGate) this.recoveryDemandAllowed = false;
|
|
2674
2836
|
this.lastError = null;
|
|
2675
2837
|
this.lastErrorAt = null;
|
|
2676
2838
|
this.lastFailure = null;
|
|
@@ -3010,22 +3172,36 @@ var CrossTabDataBus = class {
|
|
|
3010
3172
|
if (!this.started && !this.startPromise && !this.pendingStop && !this.transportReady) {
|
|
3011
3173
|
return Promise.resolve();
|
|
3012
3174
|
}
|
|
3175
|
+
let resolveGate;
|
|
3176
|
+
const stopGate = new Promise((resolve) => {
|
|
3177
|
+
resolveGate = resolve;
|
|
3178
|
+
});
|
|
3179
|
+
this.stopPromise = stopGate;
|
|
3180
|
+
this.beginStop();
|
|
3013
3181
|
const stopPromise = this.performStop();
|
|
3014
|
-
this.stopPromise = stopPromise;
|
|
3015
3182
|
void stopPromise.then(
|
|
3016
3183
|
() => {
|
|
3017
|
-
if (this.stopPromise ===
|
|
3184
|
+
if (this.stopPromise === stopGate) this.stopPromise = null;
|
|
3185
|
+
resolveGate();
|
|
3018
3186
|
},
|
|
3019
3187
|
() => {
|
|
3020
|
-
if (this.stopPromise ===
|
|
3188
|
+
if (this.stopPromise === stopGate) this.stopPromise = null;
|
|
3189
|
+
resolveGate();
|
|
3021
3190
|
}
|
|
3022
3191
|
);
|
|
3023
|
-
return
|
|
3192
|
+
return stopGate;
|
|
3024
3193
|
}
|
|
3025
|
-
|
|
3194
|
+
/**
|
|
3195
|
+
* Synchronous teardown prelude. Flips `stopping` (the authoritative
|
|
3196
|
+
* in-flight signal), cancels scheduled work, releases handlers, and emits the
|
|
3197
|
+
* observable STOP lifecycle event. stop() calls it after the shared gate is
|
|
3198
|
+
* installed but in the same tick, so the stop still takes effect immediately
|
|
3199
|
+
* while a re-entrant stop() from the STOP event shares the one teardown.
|
|
3200
|
+
*/
|
|
3201
|
+
beginStop() {
|
|
3026
3202
|
this.lifecycleEpoch += 1;
|
|
3027
3203
|
this.stopping = true;
|
|
3028
|
-
this.cancelScheduledRecovery();
|
|
3204
|
+
this.cancelScheduledRecovery(true);
|
|
3029
3205
|
this.replayManager.suspend();
|
|
3030
3206
|
this.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.STOP });
|
|
3031
3207
|
this.trace.stop();
|
|
@@ -3033,6 +3209,8 @@ var CrossTabDataBus = class {
|
|
|
3033
3209
|
this.topicHandlers.clear();
|
|
3034
3210
|
this.replayManager.resetBuffers();
|
|
3035
3211
|
this.cluster.stop();
|
|
3212
|
+
}
|
|
3213
|
+
async performStop() {
|
|
3036
3214
|
try {
|
|
3037
3215
|
await this.startPromise?.catch(() => void 0);
|
|
3038
3216
|
const pendingStop = this.pendingStop;
|
|
@@ -3049,8 +3227,6 @@ var CrossTabDataBus = class {
|
|
|
3049
3227
|
this.transportReady = false;
|
|
3050
3228
|
this.startPromise = null;
|
|
3051
3229
|
this.pendingStop = null;
|
|
3052
|
-
this.lastError = null;
|
|
3053
|
-
this.lastErrorAt = null;
|
|
3054
3230
|
this.activeConfig = void 0;
|
|
3055
3231
|
this.recoveryAttempt = 0;
|
|
3056
3232
|
this.recoveryExhausted = false;
|
|
@@ -3147,7 +3323,9 @@ var CrossTabDataBus = class {
|
|
|
3147
3323
|
const opening = this.reopenTransport(attempt);
|
|
3148
3324
|
void opening.then(
|
|
3149
3325
|
() => this.releaseRecoveryGate(),
|
|
3150
|
-
|
|
3326
|
+
// Operations already parked on the gate are demand: run one
|
|
3327
|
+
// on-demand reopen now instead of waiting for an unrelated event.
|
|
3328
|
+
() => this.allowDemandRecovery(true)
|
|
3151
3329
|
);
|
|
3152
3330
|
}, this.recoveryCooldownMs);
|
|
3153
3331
|
}
|
|
@@ -3250,10 +3428,13 @@ var CrossTabDataBus = class {
|
|
|
3250
3428
|
* pageshow path and explicit start() must run this so an explicit resume
|
|
3251
3429
|
* cannot leave trace metrics and periodic cleanup timers permanently off. */
|
|
3252
3430
|
resumeSuspendedResources() {
|
|
3431
|
+
const lifecycleEpoch = this.lifecycleEpoch;
|
|
3253
3432
|
this.trace.start();
|
|
3254
3433
|
this.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.RESUME });
|
|
3434
|
+
if (lifecycleEpoch !== this.lifecycleEpoch || this.stopping) return false;
|
|
3255
3435
|
this.startDedupSweep();
|
|
3256
3436
|
this.replayManager.start();
|
|
3437
|
+
return lifecycleEpoch === this.lifecycleEpoch && !this.stopping;
|
|
3257
3438
|
}
|
|
3258
3439
|
/**
|
|
3259
3440
|
* Suspend the transport when the tab goes hidden. Stops the transport and
|
|
@@ -3261,12 +3442,13 @@ var CrossTabDataBus = class {
|
|
|
3261
3442
|
*/
|
|
3262
3443
|
suspendTransport() {
|
|
3263
3444
|
if (this.stopping) return;
|
|
3264
|
-
this.lifecycleEpoch
|
|
3445
|
+
const suspensionEpoch = ++this.lifecycleEpoch;
|
|
3265
3446
|
this.suspended = true;
|
|
3266
|
-
this.cancelScheduledRecovery();
|
|
3447
|
+
this.cancelScheduledRecovery(true);
|
|
3267
3448
|
this.transportReady = false;
|
|
3268
3449
|
this.transportSubscribedTopics.clear();
|
|
3269
3450
|
this.updateStatus(WORKER_STATUS.DISCONNECTED);
|
|
3451
|
+
if (suspensionEpoch !== this.lifecycleEpoch || this.stopping || !this.suspended) return;
|
|
3270
3452
|
if (this.pendingStop && (this.startPromise === null || this.startPromise === this.pendingStop)) {
|
|
3271
3453
|
this.startPromise = this.pendingStop;
|
|
3272
3454
|
return;
|
|
@@ -3301,13 +3483,18 @@ var CrossTabDataBus = class {
|
|
|
3301
3483
|
if (this.startPromise && this.startPromise !== this.pendingStop) return this.startPromise;
|
|
3302
3484
|
const config = this.activeConfig;
|
|
3303
3485
|
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
3486
|
const lifecycleEpoch = ++this.lifecycleEpoch;
|
|
3308
3487
|
const pending = this.startPromise ?? this.pendingStop ?? Promise.resolve();
|
|
3309
3488
|
const opening = pending.catch(() => void 0).then(() => this.openTransport(config, Promise.resolve(), false, lifecycleEpoch));
|
|
3310
3489
|
this.startPromise = opening;
|
|
3490
|
+
this.started = true;
|
|
3491
|
+
this.suspended = false;
|
|
3492
|
+
this.transportReady = false;
|
|
3493
|
+
this.updateStatus(WORKER_STATUS.CONNECTING);
|
|
3494
|
+
if (lifecycleEpoch !== this.lifecycleEpoch || this.stopping || this.suspended) {
|
|
3495
|
+
void opening.catch(() => void 0);
|
|
3496
|
+
return opening;
|
|
3497
|
+
}
|
|
3311
3498
|
void opening.then(
|
|
3312
3499
|
() => {
|
|
3313
3500
|
if (this.startPromise === opening) this.startPromise = null;
|
|
@@ -3337,17 +3524,13 @@ var CrossTabDataBus = class {
|
|
|
3337
3524
|
runTransport(operation) {
|
|
3338
3525
|
if (this.suspended) return;
|
|
3339
3526
|
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
3527
|
const gate = this.recoveryGate;
|
|
3528
|
+
const cancellationToken = this.recoveryCancellationToken;
|
|
3529
|
+
this.startDemandRecovery();
|
|
3530
|
+
this.recoveryWaiters += 1;
|
|
3349
3531
|
void gate.then(() => {
|
|
3350
|
-
|
|
3532
|
+
this.recoveryWaiters -= 1;
|
|
3533
|
+
if (this.stopping || this.suspended || cancellationToken !== this.recoveryCancellationToken) return;
|
|
3351
3534
|
this.runTransport(operation);
|
|
3352
3535
|
});
|
|
3353
3536
|
return;
|