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