cross-tab-worker-databus 0.20.85 → 0.20.86
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 +13 -1
- package/dist/centrifuge.js +1 -1
- package/dist/{chunk-PW63EWIK.js → chunk-SDOV3UHG.js} +233 -40
- package/dist/{chunk-PW63EWIK.js.map → chunk-SDOV3UHG.js.map} +3 -3
- package/dist/cjs/centrifuge.cjs +231 -39
- package/dist/cjs/centrifuge.cjs.map +3 -3
- package/dist/cjs/index.cjs +300 -78
- package/dist/cjs/index.cjs.map +3 -3
- package/dist/core/data-bus.d.ts +44 -11
- package/dist/core/data-bus.d.ts.map +1 -1
- package/dist/core/replay-manager.d.ts +3 -2
- package/dist/core/replay-manager.d.ts.map +1 -1
- package/dist/core/replay-persistence.d.ts.map +1 -1
- package/dist/core/replay-pruning.d.ts +21 -0
- package/dist/core/replay-pruning.d.ts.map +1 -0
- package/dist/index.js +71 -40
- package/dist/index.js.map +2 -2
- package/docs/api.md +15 -4
- package/docs/architecture.md +12 -1
- package/docs/benchmarks.md +8 -8
- package/docs/configuration.md +2 -2
- package/docs/roadmap.md +8 -1
- package/docs/zh/api.md +15 -4
- package/docs/zh/architecture.md +12 -1
- package/docs/zh/benchmarks.md +8 -8
- package/docs/zh/configuration.md +2 -2
- package/docs/zh/roadmap.md +8 -1
- package/package.json +2 -2
package/dist/cjs/centrifuge.cjs
CHANGED
|
@@ -1857,6 +1857,36 @@ function roundMs(value) {
|
|
|
1857
1857
|
return Math.round(value * 10) / 10;
|
|
1858
1858
|
}
|
|
1859
1859
|
|
|
1860
|
+
// src/core/replay-pruning.ts
|
|
1861
|
+
function pruneReplayHistory(messages, options) {
|
|
1862
|
+
const { maxPerTopic, pruneStrategy, retentionMs, now } = options;
|
|
1863
|
+
const ageEnabled = pruneStrategy !== PRUNE_STRATEGY.COUNT && retentionMs !== void 0;
|
|
1864
|
+
if (!ageEnabled) {
|
|
1865
|
+
return messages.length > maxPerTopic ? messages.slice(-maxPerTopic) : messages;
|
|
1866
|
+
}
|
|
1867
|
+
const cutoff = now - retentionMs;
|
|
1868
|
+
let hasExpired = false;
|
|
1869
|
+
let timestamplessCount = 0;
|
|
1870
|
+
for (const message of messages) {
|
|
1871
|
+
if (message.timestamp === void 0) timestamplessCount += 1;
|
|
1872
|
+
else if (message.timestamp < cutoff) hasExpired = true;
|
|
1873
|
+
}
|
|
1874
|
+
let pruned = hasExpired ? messages.filter((message) => message.timestamp === void 0 || message.timestamp >= cutoff) : messages;
|
|
1875
|
+
if (pruneStrategy === PRUNE_STRATEGY.BOTH) {
|
|
1876
|
+
return pruned.length > maxPerTopic ? pruned.slice(-maxPerTopic) : pruned;
|
|
1877
|
+
}
|
|
1878
|
+
if (timestamplessCount <= maxPerTopic) return pruned;
|
|
1879
|
+
let timestamplessToDrop = timestamplessCount - maxPerTopic;
|
|
1880
|
+
pruned = pruned.filter((message) => {
|
|
1881
|
+
if (message.timestamp === void 0 && timestamplessToDrop > 0) {
|
|
1882
|
+
timestamplessToDrop -= 1;
|
|
1883
|
+
return false;
|
|
1884
|
+
}
|
|
1885
|
+
return true;
|
|
1886
|
+
});
|
|
1887
|
+
return pruned;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1860
1890
|
// src/core/replay-manager.ts
|
|
1861
1891
|
var PersistenceRetryCancelledError = class extends Error {
|
|
1862
1892
|
constructor() {
|
|
@@ -1918,17 +1948,15 @@ var ReplayManager = class {
|
|
|
1918
1948
|
this.buffers.set(message.topic, buffer);
|
|
1919
1949
|
}
|
|
1920
1950
|
buffer.push(message);
|
|
1921
|
-
const
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
buffer.shift();
|
|
1931
|
-
}
|
|
1951
|
+
const pruned = pruneReplayHistory(buffer, {
|
|
1952
|
+
maxPerTopic: this.maxPerTopic,
|
|
1953
|
+
pruneStrategy: this.pruneStrategy,
|
|
1954
|
+
retentionMs: this.retentionMs,
|
|
1955
|
+
now: this.now()
|
|
1956
|
+
});
|
|
1957
|
+
if (pruned !== buffer) {
|
|
1958
|
+
buffer = pruned;
|
|
1959
|
+
this.buffers.set(message.topic, buffer);
|
|
1932
1960
|
}
|
|
1933
1961
|
if (!this.persistence) return;
|
|
1934
1962
|
if (this.persistence.appendBatch) {
|
|
@@ -2109,14 +2137,24 @@ var ReplayManager = class {
|
|
|
2109
2137
|
if (this.retentionMs !== void 0 && this.persistence.clearBefore) {
|
|
2110
2138
|
await this.withPersistenceRetry(PERSISTENCE_OPERATION.CLEAR_BEFORE, () => this.persistence.clearBefore(this.now() - this.retentionMs));
|
|
2111
2139
|
}
|
|
2112
|
-
|
|
2140
|
+
const loaded = await this.withPersistenceRetry(PERSISTENCE_OPERATION.LOAD, () => this.persistence.load());
|
|
2141
|
+
for (const message of loaded) {
|
|
2113
2142
|
let buffer = this.buffers.get(message.topic);
|
|
2114
2143
|
if (!buffer) {
|
|
2115
2144
|
buffer = [];
|
|
2116
2145
|
this.buffers.set(message.topic, buffer);
|
|
2117
2146
|
}
|
|
2118
2147
|
buffer.push(message);
|
|
2119
|
-
|
|
2148
|
+
}
|
|
2149
|
+
const hydrationNow = this.now();
|
|
2150
|
+
for (const [topic, buffer] of this.buffers) {
|
|
2151
|
+
const pruned = pruneReplayHistory(buffer, {
|
|
2152
|
+
maxPerTopic: this.maxPerTopic,
|
|
2153
|
+
pruneStrategy: this.pruneStrategy,
|
|
2154
|
+
retentionMs: this.retentionMs,
|
|
2155
|
+
now: hydrationNow
|
|
2156
|
+
});
|
|
2157
|
+
if (pruned !== buffer) this.buffers.set(topic, pruned);
|
|
2120
2158
|
}
|
|
2121
2159
|
} catch (error) {
|
|
2122
2160
|
this.onPersistenceError(error);
|
|
@@ -2293,7 +2331,7 @@ var DedupManager = class {
|
|
|
2293
2331
|
};
|
|
2294
2332
|
|
|
2295
2333
|
// src/core/version.ts
|
|
2296
|
-
var SDK_VERSION = true ? "0.20.
|
|
2334
|
+
var SDK_VERSION = true ? "0.20.86" : "";
|
|
2297
2335
|
|
|
2298
2336
|
// src/core/data-bus.ts
|
|
2299
2337
|
var DEFAULT_REPLAY_MAX_PER_TOPIC = 100;
|
|
@@ -2331,6 +2369,24 @@ var CrossTabDataBus = class {
|
|
|
2331
2369
|
// Gate that serialises start/stop/suspend/resume — only one lifecycle
|
|
2332
2370
|
// transition at a time. Resets to null once the operation settles.
|
|
2333
2371
|
startPromise = null;
|
|
2372
|
+
// Gate for an explicit stop(). Concurrent stop() calls share it, and a
|
|
2373
|
+
// start() received while stopping chains a fresh start after it.
|
|
2374
|
+
stopPromise = null;
|
|
2375
|
+
// A start() requested while an explicit stop() is still settling. Kept
|
|
2376
|
+
// separate from startPromise because stop()'s finally block clears the
|
|
2377
|
+
// ordinary lifecycle gate before the queued start is allowed to run.
|
|
2378
|
+
queuedStart = null;
|
|
2379
|
+
// Lazy readiness view of queuedStart. start() keeps its documented
|
|
2380
|
+
// resolve-on-cancellation contract, while ready() must reject when the
|
|
2381
|
+
// queued intent was superseded by a later stop().
|
|
2382
|
+
queuedStartReady = null;
|
|
2383
|
+
queuedStartReadyToken = 0;
|
|
2384
|
+
// The queued continuation is chained to the stop promise and cannot be
|
|
2385
|
+
// un-scheduled once scheduled. A later stop() therefore invalidates the
|
|
2386
|
+
// current intent by recording its token; a subsequent start() issues a
|
|
2387
|
+
// higher token so the latest lifecycle request still wins.
|
|
2388
|
+
queuedStartToken = 0;
|
|
2389
|
+
canceledQueuedStartToken = 0;
|
|
2334
2390
|
// Timestamp of the last automatic transport recovery attempt.
|
|
2335
2391
|
// Used to avoid a tight retry loop when the transport fails repeatedly.
|
|
2336
2392
|
lastRecoveryAt = 0;
|
|
@@ -2353,6 +2409,9 @@ var CrossTabDataBus = class {
|
|
|
2353
2409
|
// surfaces a failure while later opens and automatic recovery wait for the
|
|
2354
2410
|
// stop to settle.
|
|
2355
2411
|
pendingStop = null;
|
|
2412
|
+
// Ownership token for asynchronous transport opens. Every lifecycle
|
|
2413
|
+
// transition invalidates callbacks and failure cleanup from older opens.
|
|
2414
|
+
lifecycleEpoch = 0;
|
|
2356
2415
|
// Minimum interval in ms between automatic recovery attempts.
|
|
2357
2416
|
recoveryCooldownMs;
|
|
2358
2417
|
recoveryMaxAttempts;
|
|
@@ -2465,37 +2524,51 @@ var CrossTabDataBus = class {
|
|
|
2465
2524
|
* Start the DataBus with the given transport config.
|
|
2466
2525
|
*
|
|
2467
2526
|
* The first call starts the cluster and opens the transport. Concurrent calls
|
|
2468
|
-
* during an in-flight
|
|
2469
|
-
*
|
|
2470
|
-
*
|
|
2527
|
+
* during an in-flight open return the same promise. A call received while an
|
|
2528
|
+
* explicit stop() is settling queues one fresh start after cleanup; a later
|
|
2529
|
+
* stop() before that queued start runs cancels it, so the latest lifecycle
|
|
2530
|
+
* intent wins. Once an operation settles (success or failure) its promise
|
|
2531
|
+
* gate is cleared so a subsequent start() or resumeTransport() can open a
|
|
2532
|
+
* fresh lifecycle.
|
|
2471
2533
|
*/
|
|
2472
2534
|
start(config) {
|
|
2473
|
-
if (this.
|
|
2474
|
-
if (this.
|
|
2535
|
+
if (this.queuedStart) return this.queuedStart;
|
|
2536
|
+
if (this.stopping) return this.queueStartAfterStop(config);
|
|
2537
|
+
if (this.startPromise && this.startPromise !== this.pendingStop) return this.startPromise;
|
|
2538
|
+
if (this.started) {
|
|
2539
|
+
const transportDown = !this.transportReady || this.status === WORKER_STATUS.ERROR || this.status === WORKER_STATUS.DISCONNECTED;
|
|
2540
|
+
if (!transportDown) return Promise.resolve();
|
|
2541
|
+
this.activeConfig = config;
|
|
2542
|
+
this.resetFailureState();
|
|
2543
|
+
return this.reopenTransport();
|
|
2544
|
+
}
|
|
2475
2545
|
this.started = true;
|
|
2476
2546
|
this.stopping = false;
|
|
2477
2547
|
this.suspended = false;
|
|
2478
2548
|
this.activeConfig = config;
|
|
2479
|
-
this.
|
|
2480
|
-
this.lastFailure = null;
|
|
2481
|
-
this.persistenceFailureCount = 0;
|
|
2482
|
-
this.persistenceLastFailureAt = null;
|
|
2483
|
-
this.persistenceLastErrorMessage = null;
|
|
2549
|
+
this.resetFailureState();
|
|
2484
2550
|
this.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.START });
|
|
2485
2551
|
this.trace.start();
|
|
2486
2552
|
this.startDedupSweep();
|
|
2487
2553
|
this.replayManager.start();
|
|
2488
2554
|
this.updateStatus(WORKER_STATUS.CONNECTING);
|
|
2489
2555
|
this.cluster.start();
|
|
2490
|
-
const
|
|
2556
|
+
const lifecycleEpoch = ++this.lifecycleEpoch;
|
|
2557
|
+
const opening = this.openTransport(
|
|
2558
|
+
config,
|
|
2559
|
+
this.pendingStop ?? Promise.resolve(),
|
|
2560
|
+
true,
|
|
2561
|
+
lifecycleEpoch
|
|
2562
|
+
);
|
|
2491
2563
|
this.startPromise = opening;
|
|
2492
2564
|
for (const topic of this.topicHandlers.keys()) {
|
|
2493
2565
|
this.cluster.subscribe(topic);
|
|
2494
2566
|
}
|
|
2495
2567
|
void opening.then(
|
|
2496
2568
|
() => {
|
|
2569
|
+
if (this.startPromise !== opening) return;
|
|
2497
2570
|
this.emitCoordinationTrace();
|
|
2498
|
-
|
|
2571
|
+
this.startPromise = null;
|
|
2499
2572
|
},
|
|
2500
2573
|
() => {
|
|
2501
2574
|
if (this.startPromise === opening) this.startPromise = null;
|
|
@@ -2503,24 +2576,80 @@ var CrossTabDataBus = class {
|
|
|
2503
2576
|
);
|
|
2504
2577
|
return opening;
|
|
2505
2578
|
}
|
|
2579
|
+
/** Return a cancellation-aware readiness view of the current queued start. */
|
|
2580
|
+
getQueuedStartReady() {
|
|
2581
|
+
const queued = this.queuedStart;
|
|
2582
|
+
if (!queued) {
|
|
2583
|
+
return Promise.reject(new Error("No queued start is in flight."));
|
|
2584
|
+
}
|
|
2585
|
+
const token = this.queuedStartToken;
|
|
2586
|
+
if (this.queuedStartReady && this.queuedStartReadyToken === token) {
|
|
2587
|
+
return this.queuedStartReady;
|
|
2588
|
+
}
|
|
2589
|
+
this.queuedStartReadyToken = token;
|
|
2590
|
+
this.queuedStartReady = queued.then(() => {
|
|
2591
|
+
if (token <= this.canceledQueuedStartToken) {
|
|
2592
|
+
throw new Error(
|
|
2593
|
+
"CrossTabDataBus start was canceled by a later stop(); ready() cannot report readiness. Call start() again after stop() resolves."
|
|
2594
|
+
);
|
|
2595
|
+
}
|
|
2596
|
+
if (!this.started || !this.transportReady) {
|
|
2597
|
+
throw new Error("CrossTabDataBus restart completed without a ready transport.");
|
|
2598
|
+
}
|
|
2599
|
+
});
|
|
2600
|
+
return this.queuedStartReady;
|
|
2601
|
+
}
|
|
2602
|
+
/** Queue exactly one fresh start after an in-flight explicit stop settles. */
|
|
2603
|
+
queueStartAfterStop(config) {
|
|
2604
|
+
if (this.queuedStart) return this.queuedStart;
|
|
2605
|
+
const stop = this.stopPromise ?? Promise.resolve();
|
|
2606
|
+
const token = ++this.queuedStartToken;
|
|
2607
|
+
const queued = stop.catch(() => void 0).then(() => {
|
|
2608
|
+
if (this.queuedStart === queued) this.queuedStart = null;
|
|
2609
|
+
if (token <= this.canceledQueuedStartToken) return;
|
|
2610
|
+
return this.start(config);
|
|
2611
|
+
});
|
|
2612
|
+
this.queuedStart = queued;
|
|
2613
|
+
return queued;
|
|
2614
|
+
}
|
|
2615
|
+
/** Reset failure and recovery diagnostics for a new explicit start session. */
|
|
2616
|
+
resetFailureState() {
|
|
2617
|
+
this.lastError = null;
|
|
2618
|
+
this.lastErrorAt = null;
|
|
2619
|
+
this.lastFailure = null;
|
|
2620
|
+
this.persistenceFailureCount = 0;
|
|
2621
|
+
this.persistenceLastFailureAt = null;
|
|
2622
|
+
this.persistenceLastErrorMessage = null;
|
|
2623
|
+
this.recoveryAttempt = 0;
|
|
2624
|
+
this.recoveryExhausted = false;
|
|
2625
|
+
this.lastRecoveryAt = 0;
|
|
2626
|
+
}
|
|
2506
2627
|
/**
|
|
2507
2628
|
* Open the transport, chained after `before` to ensure lifecycle ordering.
|
|
2508
2629
|
* When `stopClusterOnFailure` is true (initial start), a transport failure
|
|
2509
2630
|
* tears down the cluster as well.
|
|
2510
2631
|
*/
|
|
2511
|
-
openTransport(config, before, stopClusterOnFailure) {
|
|
2632
|
+
openTransport(config, before, stopClusterOnFailure, lifecycleEpoch) {
|
|
2512
2633
|
this.transportReady = false;
|
|
2513
2634
|
const chainedPendingStop = this.pendingStop;
|
|
2635
|
+
const isCurrentLifecycle = () => lifecycleEpoch === this.lifecycleEpoch;
|
|
2514
2636
|
return before.catch(() => void 0).then(() => {
|
|
2515
|
-
if (this.stopping || this.suspended) return;
|
|
2637
|
+
if (!isCurrentLifecycle() || this.stopping || this.suspended) return;
|
|
2516
2638
|
if (this.pendingStop === chainedPendingStop) this.pendingStop = null;
|
|
2517
2639
|
return Promise.resolve(
|
|
2518
2640
|
this.transport.start(config, {
|
|
2519
|
-
onMessage: (message) =>
|
|
2520
|
-
|
|
2521
|
-
|
|
2641
|
+
onMessage: (message) => {
|
|
2642
|
+
if (isCurrentLifecycle()) this.handleTransportMessage(message);
|
|
2643
|
+
},
|
|
2644
|
+
onStatus: (status) => {
|
|
2645
|
+
if (isCurrentLifecycle()) this.updateStatus(status);
|
|
2646
|
+
},
|
|
2647
|
+
onError: (error) => {
|
|
2648
|
+
if (isCurrentLifecycle()) this.reportError(error);
|
|
2649
|
+
}
|
|
2522
2650
|
})
|
|
2523
2651
|
).then(() => {
|
|
2652
|
+
if (!isCurrentLifecycle()) return;
|
|
2524
2653
|
if (this.status === WORKER_STATUS.ERROR) {
|
|
2525
2654
|
throw new Error("Transport failed during startup.");
|
|
2526
2655
|
}
|
|
@@ -2531,6 +2660,7 @@ var CrossTabDataBus = class {
|
|
|
2531
2660
|
}
|
|
2532
2661
|
});
|
|
2533
2662
|
}).catch((error) => {
|
|
2663
|
+
if (!isCurrentLifecycle()) throw error;
|
|
2534
2664
|
if (stopClusterOnFailure) this.started = false;
|
|
2535
2665
|
if (!this.pendingStop) {
|
|
2536
2666
|
this.pendingStop = this.createStopPromise();
|
|
@@ -2545,16 +2675,26 @@ var CrossTabDataBus = class {
|
|
|
2545
2675
|
this.cluster.stop();
|
|
2546
2676
|
this.stopping = false;
|
|
2547
2677
|
}
|
|
2548
|
-
this.startPromise = null;
|
|
2549
2678
|
throw error;
|
|
2550
2679
|
});
|
|
2551
2680
|
}
|
|
2552
2681
|
/**
|
|
2553
2682
|
* Await the DataBus to be fully started (lazy init when using initialConfig).
|
|
2554
2683
|
* Returns a rejected promise when the transport has failed and no start is in
|
|
2555
|
-
* flight — the caller can retry by calling start() or ready() again.
|
|
2684
|
+
* flight — the caller can retry by calling start() or ready() again. While an
|
|
2685
|
+
* explicit stop() is settling, this rejects unless a restart is queued behind
|
|
2686
|
+
* it; false readiness during teardown is never reported.
|
|
2556
2687
|
*/
|
|
2557
2688
|
ready() {
|
|
2689
|
+
if (this.queuedStart) return this.getQueuedStartReady();
|
|
2690
|
+
if (this.stopping) {
|
|
2691
|
+
return Promise.reject(new Error(
|
|
2692
|
+
"CrossTabDataBus is stopping; ready() cannot report readiness until stop() resolves. Wait for stop() to resolve, then call start() before awaiting ready()."
|
|
2693
|
+
));
|
|
2694
|
+
}
|
|
2695
|
+
if (!this.started && !this.hasInitialConfig && this.lastError !== null) {
|
|
2696
|
+
return Promise.reject(this.lastError);
|
|
2697
|
+
}
|
|
2558
2698
|
try {
|
|
2559
2699
|
this.ensureStarted();
|
|
2560
2700
|
} catch (error) {
|
|
@@ -2570,9 +2710,18 @@ var CrossTabDataBus = class {
|
|
|
2570
2710
|
/**
|
|
2571
2711
|
* Register a handler for `topic`. The handler fires on every publication
|
|
2572
2712
|
* delivered to this tab, regardless of which tab published it. Returns an
|
|
2573
|
-
* unsubscribe function for convenience.
|
|
2713
|
+
* unsubscribe function for convenience. During an explicit stop() the
|
|
2714
|
+
* registration is rejected through onError and a no-op cleanup is returned,
|
|
2715
|
+
* so a late subscriber cannot leak into a future restart.
|
|
2574
2716
|
*/
|
|
2575
2717
|
subscribe(topic, handler, options) {
|
|
2718
|
+
if (this.stopping) {
|
|
2719
|
+
this.reportError(new Error(
|
|
2720
|
+
"CrossTabDataBus is stopping; subscribe() was not registered. Wait for stop() to resolve, then call start() before subscribing again."
|
|
2721
|
+
));
|
|
2722
|
+
return () => {
|
|
2723
|
+
};
|
|
2724
|
+
}
|
|
2576
2725
|
this.ensureStarted();
|
|
2577
2726
|
const handlers = this.topicHandlers.get(topic) ?? /* @__PURE__ */ new Set();
|
|
2578
2727
|
const wasUnused = handlers.size === 0;
|
|
@@ -2626,6 +2775,7 @@ var CrossTabDataBus = class {
|
|
|
2626
2775
|
/** Publish a message to `topic`. The owning Worker delivers it to the transport. */
|
|
2627
2776
|
publish(topic, data, options) {
|
|
2628
2777
|
this.ensureStarted();
|
|
2778
|
+
if (this.rejectPublishDuringStop("publish")) return;
|
|
2629
2779
|
if (!this.cluster.publish(topic, data, options)) {
|
|
2630
2780
|
this.reportError(
|
|
2631
2781
|
new Error("Failed to send the publish control message to the owning worker.")
|
|
@@ -2642,6 +2792,7 @@ var CrossTabDataBus = class {
|
|
|
2642
2792
|
publishBatch(topic, items) {
|
|
2643
2793
|
this.ensureStarted();
|
|
2644
2794
|
if (items.length === 0) return;
|
|
2795
|
+
if (this.rejectPublishDuringStop("publishBatch")) return;
|
|
2645
2796
|
if (items.length === 1) {
|
|
2646
2797
|
const first = items[0];
|
|
2647
2798
|
this.publish(topic, first.data, first.options);
|
|
@@ -2770,10 +2921,33 @@ var CrossTabDataBus = class {
|
|
|
2770
2921
|
}
|
|
2771
2922
|
/**
|
|
2772
2923
|
* Gracefully stop the DataBus: unsubscribe all topics, stop the cluster,
|
|
2773
|
-
* and close the transport.
|
|
2924
|
+
* and close the transport. Concurrent and repeated calls share the in-flight
|
|
2925
|
+
* stop promise. A start() received while stopping runs after this completes,
|
|
2926
|
+
* unless another stop() arrives first and cancels that queued restart.
|
|
2774
2927
|
*/
|
|
2775
|
-
|
|
2776
|
-
if (
|
|
2928
|
+
stop() {
|
|
2929
|
+
if (this.queuedStart) {
|
|
2930
|
+
this.canceledQueuedStartToken = this.queuedStartToken;
|
|
2931
|
+
this.queuedStart = null;
|
|
2932
|
+
}
|
|
2933
|
+
if (this.stopPromise) return this.stopPromise;
|
|
2934
|
+
if (!this.started && !this.startPromise && !this.pendingStop && !this.transportReady) {
|
|
2935
|
+
return Promise.resolve();
|
|
2936
|
+
}
|
|
2937
|
+
const stopPromise = this.performStop();
|
|
2938
|
+
this.stopPromise = stopPromise;
|
|
2939
|
+
void stopPromise.then(
|
|
2940
|
+
() => {
|
|
2941
|
+
if (this.stopPromise === stopPromise) this.stopPromise = null;
|
|
2942
|
+
},
|
|
2943
|
+
() => {
|
|
2944
|
+
if (this.stopPromise === stopPromise) this.stopPromise = null;
|
|
2945
|
+
}
|
|
2946
|
+
);
|
|
2947
|
+
return stopPromise;
|
|
2948
|
+
}
|
|
2949
|
+
async performStop() {
|
|
2950
|
+
this.lifecycleEpoch += 1;
|
|
2777
2951
|
this.stopping = true;
|
|
2778
2952
|
this.replayManager.suspend();
|
|
2779
2953
|
this.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.STOP });
|
|
@@ -2965,6 +3139,7 @@ var CrossTabDataBus = class {
|
|
|
2965
3139
|
*/
|
|
2966
3140
|
suspendTransport() {
|
|
2967
3141
|
if (this.stopping) return;
|
|
3142
|
+
this.lifecycleEpoch += 1;
|
|
2968
3143
|
this.suspended = true;
|
|
2969
3144
|
this.transportReady = false;
|
|
2970
3145
|
this.transportSubscribedTopics.clear();
|
|
@@ -3003,19 +3178,23 @@ var CrossTabDataBus = class {
|
|
|
3003
3178
|
this.started = true;
|
|
3004
3179
|
this.suspended = false;
|
|
3005
3180
|
this.updateStatus(WORKER_STATUS.CONNECTING);
|
|
3181
|
+
const lifecycleEpoch = ++this.lifecycleEpoch;
|
|
3006
3182
|
const pending = this.startPromise ?? this.pendingStop ?? Promise.resolve();
|
|
3007
|
-
const opening = pending.catch(() => void 0).then(() => this.openTransport(config, Promise.resolve(), false));
|
|
3183
|
+
const opening = pending.catch(() => void 0).then(() => this.openTransport(config, Promise.resolve(), false, lifecycleEpoch));
|
|
3008
3184
|
this.startPromise = opening;
|
|
3009
3185
|
void opening.then(
|
|
3010
3186
|
() => {
|
|
3187
|
+
if (this.startPromise === opening) this.startPromise = null;
|
|
3188
|
+
if (lifecycleEpoch !== this.lifecycleEpoch) return;
|
|
3011
3189
|
if (traceAttempt !== void 0) {
|
|
3012
3190
|
this.trace.event({ type: TRACE_EVENT_TYPE.RELIABILITY, operation: RELIABILITY_OPERATION.TRANSPORT_RECOVERY, attempt: traceAttempt, outcome: RECOVERY_OUTCOME.SUCCEEDED });
|
|
3013
3191
|
this.recoveryAttempt = 0;
|
|
3014
3192
|
this.recoveryExhausted = false;
|
|
3015
3193
|
}
|
|
3016
|
-
if (this.startPromise === opening) this.startPromise = null;
|
|
3017
3194
|
},
|
|
3018
3195
|
() => {
|
|
3196
|
+
if (this.startPromise === opening) this.startPromise = null;
|
|
3197
|
+
if (lifecycleEpoch !== this.lifecycleEpoch) return;
|
|
3019
3198
|
if (traceAttempt !== void 0) {
|
|
3020
3199
|
this.trace.event({ type: TRACE_EVENT_TYPE.RELIABILITY, operation: RELIABILITY_OPERATION.TRANSPORT_RECOVERY, attempt: traceAttempt, outcome: RECOVERY_OUTCOME.FAILED });
|
|
3021
3200
|
}
|
|
@@ -3049,6 +3228,19 @@ var CrossTabDataBus = class {
|
|
|
3049
3228
|
return operation();
|
|
3050
3229
|
}).catch((error) => this.reportError(error));
|
|
3051
3230
|
}
|
|
3231
|
+
/**
|
|
3232
|
+
* Publications started after teardown begins cannot reach any transport.
|
|
3233
|
+
* Surface that as a normal asynchronous API failure instead of letting
|
|
3234
|
+
* runTransport() return silently. Empty publishBatch() calls remain a no-op
|
|
3235
|
+
* and are filtered by the caller before this check.
|
|
3236
|
+
*/
|
|
3237
|
+
rejectPublishDuringStop(operation) {
|
|
3238
|
+
if (!this.stopping) return false;
|
|
3239
|
+
this.reportError(new Error(
|
|
3240
|
+
`CrossTabDataBus is stopping; ${operation}() was not sent. Wait for stop() to resolve, then call start() before publishing again.`
|
|
3241
|
+
));
|
|
3242
|
+
return true;
|
|
3243
|
+
}
|
|
3052
3244
|
/**
|
|
3053
3245
|
* Ensure the DataBus is started, throwing if no initialConfig was provided.
|
|
3054
3246
|
* Called automatically by subscribe/publish/ready when autoStart is true.
|