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/index.cjs
CHANGED
|
@@ -1855,6 +1855,36 @@ function roundMs(value) {
|
|
|
1855
1855
|
return Math.round(value * 10) / 10;
|
|
1856
1856
|
}
|
|
1857
1857
|
|
|
1858
|
+
// src/core/replay-pruning.ts
|
|
1859
|
+
function pruneReplayHistory(messages, options) {
|
|
1860
|
+
const { maxPerTopic, pruneStrategy, retentionMs, now } = options;
|
|
1861
|
+
const ageEnabled = pruneStrategy !== PRUNE_STRATEGY.COUNT && retentionMs !== void 0;
|
|
1862
|
+
if (!ageEnabled) {
|
|
1863
|
+
return messages.length > maxPerTopic ? messages.slice(-maxPerTopic) : messages;
|
|
1864
|
+
}
|
|
1865
|
+
const cutoff = now - retentionMs;
|
|
1866
|
+
let hasExpired = false;
|
|
1867
|
+
let timestamplessCount = 0;
|
|
1868
|
+
for (const message of messages) {
|
|
1869
|
+
if (message.timestamp === void 0) timestamplessCount += 1;
|
|
1870
|
+
else if (message.timestamp < cutoff) hasExpired = true;
|
|
1871
|
+
}
|
|
1872
|
+
let pruned = hasExpired ? messages.filter((message) => message.timestamp === void 0 || message.timestamp >= cutoff) : messages;
|
|
1873
|
+
if (pruneStrategy === PRUNE_STRATEGY.BOTH) {
|
|
1874
|
+
return pruned.length > maxPerTopic ? pruned.slice(-maxPerTopic) : pruned;
|
|
1875
|
+
}
|
|
1876
|
+
if (timestamplessCount <= maxPerTopic) return pruned;
|
|
1877
|
+
let timestamplessToDrop = timestamplessCount - maxPerTopic;
|
|
1878
|
+
pruned = pruned.filter((message) => {
|
|
1879
|
+
if (message.timestamp === void 0 && timestamplessToDrop > 0) {
|
|
1880
|
+
timestamplessToDrop -= 1;
|
|
1881
|
+
return false;
|
|
1882
|
+
}
|
|
1883
|
+
return true;
|
|
1884
|
+
});
|
|
1885
|
+
return pruned;
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1858
1888
|
// src/core/replay-manager.ts
|
|
1859
1889
|
var PersistenceRetryCancelledError = class extends Error {
|
|
1860
1890
|
constructor() {
|
|
@@ -1916,17 +1946,15 @@ var ReplayManager = class {
|
|
|
1916
1946
|
this.buffers.set(message.topic, buffer);
|
|
1917
1947
|
}
|
|
1918
1948
|
buffer.push(message);
|
|
1919
|
-
const
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
buffer.shift();
|
|
1929
|
-
}
|
|
1949
|
+
const pruned = pruneReplayHistory(buffer, {
|
|
1950
|
+
maxPerTopic: this.maxPerTopic,
|
|
1951
|
+
pruneStrategy: this.pruneStrategy,
|
|
1952
|
+
retentionMs: this.retentionMs,
|
|
1953
|
+
now: this.now()
|
|
1954
|
+
});
|
|
1955
|
+
if (pruned !== buffer) {
|
|
1956
|
+
buffer = pruned;
|
|
1957
|
+
this.buffers.set(message.topic, buffer);
|
|
1930
1958
|
}
|
|
1931
1959
|
if (!this.persistence) return;
|
|
1932
1960
|
if (this.persistence.appendBatch) {
|
|
@@ -2107,14 +2135,24 @@ var ReplayManager = class {
|
|
|
2107
2135
|
if (this.retentionMs !== void 0 && this.persistence.clearBefore) {
|
|
2108
2136
|
await this.withPersistenceRetry(PERSISTENCE_OPERATION.CLEAR_BEFORE, () => this.persistence.clearBefore(this.now() - this.retentionMs));
|
|
2109
2137
|
}
|
|
2110
|
-
|
|
2138
|
+
const loaded = await this.withPersistenceRetry(PERSISTENCE_OPERATION.LOAD, () => this.persistence.load());
|
|
2139
|
+
for (const message of loaded) {
|
|
2111
2140
|
let buffer = this.buffers.get(message.topic);
|
|
2112
2141
|
if (!buffer) {
|
|
2113
2142
|
buffer = [];
|
|
2114
2143
|
this.buffers.set(message.topic, buffer);
|
|
2115
2144
|
}
|
|
2116
2145
|
buffer.push(message);
|
|
2117
|
-
|
|
2146
|
+
}
|
|
2147
|
+
const hydrationNow = this.now();
|
|
2148
|
+
for (const [topic, buffer] of this.buffers) {
|
|
2149
|
+
const pruned = pruneReplayHistory(buffer, {
|
|
2150
|
+
maxPerTopic: this.maxPerTopic,
|
|
2151
|
+
pruneStrategy: this.pruneStrategy,
|
|
2152
|
+
retentionMs: this.retentionMs,
|
|
2153
|
+
now: hydrationNow
|
|
2154
|
+
});
|
|
2155
|
+
if (pruned !== buffer) this.buffers.set(topic, pruned);
|
|
2118
2156
|
}
|
|
2119
2157
|
} catch (error) {
|
|
2120
2158
|
this.onPersistenceError(error);
|
|
@@ -2291,7 +2329,7 @@ var DedupManager = class {
|
|
|
2291
2329
|
};
|
|
2292
2330
|
|
|
2293
2331
|
// src/core/version.ts
|
|
2294
|
-
var SDK_VERSION = true ? "0.20.
|
|
2332
|
+
var SDK_VERSION = true ? "0.20.86" : "";
|
|
2295
2333
|
|
|
2296
2334
|
// src/core/data-bus.ts
|
|
2297
2335
|
var DEFAULT_REPLAY_MAX_PER_TOPIC = 100;
|
|
@@ -2329,6 +2367,24 @@ var CrossTabDataBus = class {
|
|
|
2329
2367
|
// Gate that serialises start/stop/suspend/resume — only one lifecycle
|
|
2330
2368
|
// transition at a time. Resets to null once the operation settles.
|
|
2331
2369
|
startPromise = null;
|
|
2370
|
+
// Gate for an explicit stop(). Concurrent stop() calls share it, and a
|
|
2371
|
+
// start() received while stopping chains a fresh start after it.
|
|
2372
|
+
stopPromise = null;
|
|
2373
|
+
// A start() requested while an explicit stop() is still settling. Kept
|
|
2374
|
+
// separate from startPromise because stop()'s finally block clears the
|
|
2375
|
+
// ordinary lifecycle gate before the queued start is allowed to run.
|
|
2376
|
+
queuedStart = null;
|
|
2377
|
+
// Lazy readiness view of queuedStart. start() keeps its documented
|
|
2378
|
+
// resolve-on-cancellation contract, while ready() must reject when the
|
|
2379
|
+
// queued intent was superseded by a later stop().
|
|
2380
|
+
queuedStartReady = null;
|
|
2381
|
+
queuedStartReadyToken = 0;
|
|
2382
|
+
// The queued continuation is chained to the stop promise and cannot be
|
|
2383
|
+
// un-scheduled once scheduled. A later stop() therefore invalidates the
|
|
2384
|
+
// current intent by recording its token; a subsequent start() issues a
|
|
2385
|
+
// higher token so the latest lifecycle request still wins.
|
|
2386
|
+
queuedStartToken = 0;
|
|
2387
|
+
canceledQueuedStartToken = 0;
|
|
2332
2388
|
// Timestamp of the last automatic transport recovery attempt.
|
|
2333
2389
|
// Used to avoid a tight retry loop when the transport fails repeatedly.
|
|
2334
2390
|
lastRecoveryAt = 0;
|
|
@@ -2351,6 +2407,9 @@ var CrossTabDataBus = class {
|
|
|
2351
2407
|
// surfaces a failure while later opens and automatic recovery wait for the
|
|
2352
2408
|
// stop to settle.
|
|
2353
2409
|
pendingStop = null;
|
|
2410
|
+
// Ownership token for asynchronous transport opens. Every lifecycle
|
|
2411
|
+
// transition invalidates callbacks and failure cleanup from older opens.
|
|
2412
|
+
lifecycleEpoch = 0;
|
|
2354
2413
|
// Minimum interval in ms between automatic recovery attempts.
|
|
2355
2414
|
recoveryCooldownMs;
|
|
2356
2415
|
recoveryMaxAttempts;
|
|
@@ -2463,37 +2522,51 @@ var CrossTabDataBus = class {
|
|
|
2463
2522
|
* Start the DataBus with the given transport config.
|
|
2464
2523
|
*
|
|
2465
2524
|
* The first call starts the cluster and opens the transport. Concurrent calls
|
|
2466
|
-
* during an in-flight
|
|
2467
|
-
*
|
|
2468
|
-
*
|
|
2525
|
+
* during an in-flight open return the same promise. A call received while an
|
|
2526
|
+
* explicit stop() is settling queues one fresh start after cleanup; a later
|
|
2527
|
+
* stop() before that queued start runs cancels it, so the latest lifecycle
|
|
2528
|
+
* intent wins. Once an operation settles (success or failure) its promise
|
|
2529
|
+
* gate is cleared so a subsequent start() or resumeTransport() can open a
|
|
2530
|
+
* fresh lifecycle.
|
|
2469
2531
|
*/
|
|
2470
2532
|
start(config) {
|
|
2471
|
-
if (this.
|
|
2472
|
-
if (this.
|
|
2533
|
+
if (this.queuedStart) return this.queuedStart;
|
|
2534
|
+
if (this.stopping) return this.queueStartAfterStop(config);
|
|
2535
|
+
if (this.startPromise && this.startPromise !== this.pendingStop) return this.startPromise;
|
|
2536
|
+
if (this.started) {
|
|
2537
|
+
const transportDown = !this.transportReady || this.status === WORKER_STATUS.ERROR || this.status === WORKER_STATUS.DISCONNECTED;
|
|
2538
|
+
if (!transportDown) return Promise.resolve();
|
|
2539
|
+
this.activeConfig = config;
|
|
2540
|
+
this.resetFailureState();
|
|
2541
|
+
return this.reopenTransport();
|
|
2542
|
+
}
|
|
2473
2543
|
this.started = true;
|
|
2474
2544
|
this.stopping = false;
|
|
2475
2545
|
this.suspended = false;
|
|
2476
2546
|
this.activeConfig = config;
|
|
2477
|
-
this.
|
|
2478
|
-
this.lastFailure = null;
|
|
2479
|
-
this.persistenceFailureCount = 0;
|
|
2480
|
-
this.persistenceLastFailureAt = null;
|
|
2481
|
-
this.persistenceLastErrorMessage = null;
|
|
2547
|
+
this.resetFailureState();
|
|
2482
2548
|
this.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.START });
|
|
2483
2549
|
this.trace.start();
|
|
2484
2550
|
this.startDedupSweep();
|
|
2485
2551
|
this.replayManager.start();
|
|
2486
2552
|
this.updateStatus(WORKER_STATUS.CONNECTING);
|
|
2487
2553
|
this.cluster.start();
|
|
2488
|
-
const
|
|
2554
|
+
const lifecycleEpoch = ++this.lifecycleEpoch;
|
|
2555
|
+
const opening = this.openTransport(
|
|
2556
|
+
config,
|
|
2557
|
+
this.pendingStop ?? Promise.resolve(),
|
|
2558
|
+
true,
|
|
2559
|
+
lifecycleEpoch
|
|
2560
|
+
);
|
|
2489
2561
|
this.startPromise = opening;
|
|
2490
2562
|
for (const topic of this.topicHandlers.keys()) {
|
|
2491
2563
|
this.cluster.subscribe(topic);
|
|
2492
2564
|
}
|
|
2493
2565
|
void opening.then(
|
|
2494
2566
|
() => {
|
|
2567
|
+
if (this.startPromise !== opening) return;
|
|
2495
2568
|
this.emitCoordinationTrace();
|
|
2496
|
-
|
|
2569
|
+
this.startPromise = null;
|
|
2497
2570
|
},
|
|
2498
2571
|
() => {
|
|
2499
2572
|
if (this.startPromise === opening) this.startPromise = null;
|
|
@@ -2501,24 +2574,80 @@ var CrossTabDataBus = class {
|
|
|
2501
2574
|
);
|
|
2502
2575
|
return opening;
|
|
2503
2576
|
}
|
|
2577
|
+
/** Return a cancellation-aware readiness view of the current queued start. */
|
|
2578
|
+
getQueuedStartReady() {
|
|
2579
|
+
const queued = this.queuedStart;
|
|
2580
|
+
if (!queued) {
|
|
2581
|
+
return Promise.reject(new Error("No queued start is in flight."));
|
|
2582
|
+
}
|
|
2583
|
+
const token = this.queuedStartToken;
|
|
2584
|
+
if (this.queuedStartReady && this.queuedStartReadyToken === token) {
|
|
2585
|
+
return this.queuedStartReady;
|
|
2586
|
+
}
|
|
2587
|
+
this.queuedStartReadyToken = token;
|
|
2588
|
+
this.queuedStartReady = queued.then(() => {
|
|
2589
|
+
if (token <= this.canceledQueuedStartToken) {
|
|
2590
|
+
throw new Error(
|
|
2591
|
+
"CrossTabDataBus start was canceled by a later stop(); ready() cannot report readiness. Call start() again after stop() resolves."
|
|
2592
|
+
);
|
|
2593
|
+
}
|
|
2594
|
+
if (!this.started || !this.transportReady) {
|
|
2595
|
+
throw new Error("CrossTabDataBus restart completed without a ready transport.");
|
|
2596
|
+
}
|
|
2597
|
+
});
|
|
2598
|
+
return this.queuedStartReady;
|
|
2599
|
+
}
|
|
2600
|
+
/** Queue exactly one fresh start after an in-flight explicit stop settles. */
|
|
2601
|
+
queueStartAfterStop(config) {
|
|
2602
|
+
if (this.queuedStart) return this.queuedStart;
|
|
2603
|
+
const stop = this.stopPromise ?? Promise.resolve();
|
|
2604
|
+
const token = ++this.queuedStartToken;
|
|
2605
|
+
const queued = stop.catch(() => void 0).then(() => {
|
|
2606
|
+
if (this.queuedStart === queued) this.queuedStart = null;
|
|
2607
|
+
if (token <= this.canceledQueuedStartToken) return;
|
|
2608
|
+
return this.start(config);
|
|
2609
|
+
});
|
|
2610
|
+
this.queuedStart = queued;
|
|
2611
|
+
return queued;
|
|
2612
|
+
}
|
|
2613
|
+
/** Reset failure and recovery diagnostics for a new explicit start session. */
|
|
2614
|
+
resetFailureState() {
|
|
2615
|
+
this.lastError = null;
|
|
2616
|
+
this.lastErrorAt = null;
|
|
2617
|
+
this.lastFailure = null;
|
|
2618
|
+
this.persistenceFailureCount = 0;
|
|
2619
|
+
this.persistenceLastFailureAt = null;
|
|
2620
|
+
this.persistenceLastErrorMessage = null;
|
|
2621
|
+
this.recoveryAttempt = 0;
|
|
2622
|
+
this.recoveryExhausted = false;
|
|
2623
|
+
this.lastRecoveryAt = 0;
|
|
2624
|
+
}
|
|
2504
2625
|
/**
|
|
2505
2626
|
* Open the transport, chained after `before` to ensure lifecycle ordering.
|
|
2506
2627
|
* When `stopClusterOnFailure` is true (initial start), a transport failure
|
|
2507
2628
|
* tears down the cluster as well.
|
|
2508
2629
|
*/
|
|
2509
|
-
openTransport(config, before, stopClusterOnFailure) {
|
|
2630
|
+
openTransport(config, before, stopClusterOnFailure, lifecycleEpoch) {
|
|
2510
2631
|
this.transportReady = false;
|
|
2511
2632
|
const chainedPendingStop = this.pendingStop;
|
|
2633
|
+
const isCurrentLifecycle = () => lifecycleEpoch === this.lifecycleEpoch;
|
|
2512
2634
|
return before.catch(() => void 0).then(() => {
|
|
2513
|
-
if (this.stopping || this.suspended) return;
|
|
2635
|
+
if (!isCurrentLifecycle() || this.stopping || this.suspended) return;
|
|
2514
2636
|
if (this.pendingStop === chainedPendingStop) this.pendingStop = null;
|
|
2515
2637
|
return Promise.resolve(
|
|
2516
2638
|
this.transport.start(config, {
|
|
2517
|
-
onMessage: (message) =>
|
|
2518
|
-
|
|
2519
|
-
|
|
2639
|
+
onMessage: (message) => {
|
|
2640
|
+
if (isCurrentLifecycle()) this.handleTransportMessage(message);
|
|
2641
|
+
},
|
|
2642
|
+
onStatus: (status) => {
|
|
2643
|
+
if (isCurrentLifecycle()) this.updateStatus(status);
|
|
2644
|
+
},
|
|
2645
|
+
onError: (error) => {
|
|
2646
|
+
if (isCurrentLifecycle()) this.reportError(error);
|
|
2647
|
+
}
|
|
2520
2648
|
})
|
|
2521
2649
|
).then(() => {
|
|
2650
|
+
if (!isCurrentLifecycle()) return;
|
|
2522
2651
|
if (this.status === WORKER_STATUS.ERROR) {
|
|
2523
2652
|
throw new Error("Transport failed during startup.");
|
|
2524
2653
|
}
|
|
@@ -2529,6 +2658,7 @@ var CrossTabDataBus = class {
|
|
|
2529
2658
|
}
|
|
2530
2659
|
});
|
|
2531
2660
|
}).catch((error) => {
|
|
2661
|
+
if (!isCurrentLifecycle()) throw error;
|
|
2532
2662
|
if (stopClusterOnFailure) this.started = false;
|
|
2533
2663
|
if (!this.pendingStop) {
|
|
2534
2664
|
this.pendingStop = this.createStopPromise();
|
|
@@ -2543,16 +2673,26 @@ var CrossTabDataBus = class {
|
|
|
2543
2673
|
this.cluster.stop();
|
|
2544
2674
|
this.stopping = false;
|
|
2545
2675
|
}
|
|
2546
|
-
this.startPromise = null;
|
|
2547
2676
|
throw error;
|
|
2548
2677
|
});
|
|
2549
2678
|
}
|
|
2550
2679
|
/**
|
|
2551
2680
|
* Await the DataBus to be fully started (lazy init when using initialConfig).
|
|
2552
2681
|
* Returns a rejected promise when the transport has failed and no start is in
|
|
2553
|
-
* flight — the caller can retry by calling start() or ready() again.
|
|
2682
|
+
* flight — the caller can retry by calling start() or ready() again. While an
|
|
2683
|
+
* explicit stop() is settling, this rejects unless a restart is queued behind
|
|
2684
|
+
* it; false readiness during teardown is never reported.
|
|
2554
2685
|
*/
|
|
2555
2686
|
ready() {
|
|
2687
|
+
if (this.queuedStart) return this.getQueuedStartReady();
|
|
2688
|
+
if (this.stopping) {
|
|
2689
|
+
return Promise.reject(new Error(
|
|
2690
|
+
"CrossTabDataBus is stopping; ready() cannot report readiness until stop() resolves. Wait for stop() to resolve, then call start() before awaiting ready()."
|
|
2691
|
+
));
|
|
2692
|
+
}
|
|
2693
|
+
if (!this.started && !this.hasInitialConfig && this.lastError !== null) {
|
|
2694
|
+
return Promise.reject(this.lastError);
|
|
2695
|
+
}
|
|
2556
2696
|
try {
|
|
2557
2697
|
this.ensureStarted();
|
|
2558
2698
|
} catch (error) {
|
|
@@ -2568,9 +2708,18 @@ var CrossTabDataBus = class {
|
|
|
2568
2708
|
/**
|
|
2569
2709
|
* Register a handler for `topic`. The handler fires on every publication
|
|
2570
2710
|
* delivered to this tab, regardless of which tab published it. Returns an
|
|
2571
|
-
* unsubscribe function for convenience.
|
|
2711
|
+
* unsubscribe function for convenience. During an explicit stop() the
|
|
2712
|
+
* registration is rejected through onError and a no-op cleanup is returned,
|
|
2713
|
+
* so a late subscriber cannot leak into a future restart.
|
|
2572
2714
|
*/
|
|
2573
2715
|
subscribe(topic, handler, options) {
|
|
2716
|
+
if (this.stopping) {
|
|
2717
|
+
this.reportError(new Error(
|
|
2718
|
+
"CrossTabDataBus is stopping; subscribe() was not registered. Wait for stop() to resolve, then call start() before subscribing again."
|
|
2719
|
+
));
|
|
2720
|
+
return () => {
|
|
2721
|
+
};
|
|
2722
|
+
}
|
|
2574
2723
|
this.ensureStarted();
|
|
2575
2724
|
const handlers = this.topicHandlers.get(topic) ?? /* @__PURE__ */ new Set();
|
|
2576
2725
|
const wasUnused = handlers.size === 0;
|
|
@@ -2624,6 +2773,7 @@ var CrossTabDataBus = class {
|
|
|
2624
2773
|
/** Publish a message to `topic`. The owning Worker delivers it to the transport. */
|
|
2625
2774
|
publish(topic, data, options) {
|
|
2626
2775
|
this.ensureStarted();
|
|
2776
|
+
if (this.rejectPublishDuringStop("publish")) return;
|
|
2627
2777
|
if (!this.cluster.publish(topic, data, options)) {
|
|
2628
2778
|
this.reportError(
|
|
2629
2779
|
new Error("Failed to send the publish control message to the owning worker.")
|
|
@@ -2640,6 +2790,7 @@ var CrossTabDataBus = class {
|
|
|
2640
2790
|
publishBatch(topic, items) {
|
|
2641
2791
|
this.ensureStarted();
|
|
2642
2792
|
if (items.length === 0) return;
|
|
2793
|
+
if (this.rejectPublishDuringStop("publishBatch")) return;
|
|
2643
2794
|
if (items.length === 1) {
|
|
2644
2795
|
const first = items[0];
|
|
2645
2796
|
this.publish(topic, first.data, first.options);
|
|
@@ -2768,10 +2919,33 @@ var CrossTabDataBus = class {
|
|
|
2768
2919
|
}
|
|
2769
2920
|
/**
|
|
2770
2921
|
* Gracefully stop the DataBus: unsubscribe all topics, stop the cluster,
|
|
2771
|
-
* and close the transport.
|
|
2922
|
+
* and close the transport. Concurrent and repeated calls share the in-flight
|
|
2923
|
+
* stop promise. A start() received while stopping runs after this completes,
|
|
2924
|
+
* unless another stop() arrives first and cancels that queued restart.
|
|
2772
2925
|
*/
|
|
2773
|
-
|
|
2774
|
-
if (
|
|
2926
|
+
stop() {
|
|
2927
|
+
if (this.queuedStart) {
|
|
2928
|
+
this.canceledQueuedStartToken = this.queuedStartToken;
|
|
2929
|
+
this.queuedStart = null;
|
|
2930
|
+
}
|
|
2931
|
+
if (this.stopPromise) return this.stopPromise;
|
|
2932
|
+
if (!this.started && !this.startPromise && !this.pendingStop && !this.transportReady) {
|
|
2933
|
+
return Promise.resolve();
|
|
2934
|
+
}
|
|
2935
|
+
const stopPromise = this.performStop();
|
|
2936
|
+
this.stopPromise = stopPromise;
|
|
2937
|
+
void stopPromise.then(
|
|
2938
|
+
() => {
|
|
2939
|
+
if (this.stopPromise === stopPromise) this.stopPromise = null;
|
|
2940
|
+
},
|
|
2941
|
+
() => {
|
|
2942
|
+
if (this.stopPromise === stopPromise) this.stopPromise = null;
|
|
2943
|
+
}
|
|
2944
|
+
);
|
|
2945
|
+
return stopPromise;
|
|
2946
|
+
}
|
|
2947
|
+
async performStop() {
|
|
2948
|
+
this.lifecycleEpoch += 1;
|
|
2775
2949
|
this.stopping = true;
|
|
2776
2950
|
this.replayManager.suspend();
|
|
2777
2951
|
this.trace.event({ type: TRACE_EVENT_TYPE.LIFECYCLE, action: TRACE_LIFECYCLE_ACTION.STOP });
|
|
@@ -2963,6 +3137,7 @@ var CrossTabDataBus = class {
|
|
|
2963
3137
|
*/
|
|
2964
3138
|
suspendTransport() {
|
|
2965
3139
|
if (this.stopping) return;
|
|
3140
|
+
this.lifecycleEpoch += 1;
|
|
2966
3141
|
this.suspended = true;
|
|
2967
3142
|
this.transportReady = false;
|
|
2968
3143
|
this.transportSubscribedTopics.clear();
|
|
@@ -3001,19 +3176,23 @@ var CrossTabDataBus = class {
|
|
|
3001
3176
|
this.started = true;
|
|
3002
3177
|
this.suspended = false;
|
|
3003
3178
|
this.updateStatus(WORKER_STATUS.CONNECTING);
|
|
3179
|
+
const lifecycleEpoch = ++this.lifecycleEpoch;
|
|
3004
3180
|
const pending = this.startPromise ?? this.pendingStop ?? Promise.resolve();
|
|
3005
|
-
const opening = pending.catch(() => void 0).then(() => this.openTransport(config, Promise.resolve(), false));
|
|
3181
|
+
const opening = pending.catch(() => void 0).then(() => this.openTransport(config, Promise.resolve(), false, lifecycleEpoch));
|
|
3006
3182
|
this.startPromise = opening;
|
|
3007
3183
|
void opening.then(
|
|
3008
3184
|
() => {
|
|
3185
|
+
if (this.startPromise === opening) this.startPromise = null;
|
|
3186
|
+
if (lifecycleEpoch !== this.lifecycleEpoch) return;
|
|
3009
3187
|
if (traceAttempt !== void 0) {
|
|
3010
3188
|
this.trace.event({ type: TRACE_EVENT_TYPE.RELIABILITY, operation: RELIABILITY_OPERATION.TRANSPORT_RECOVERY, attempt: traceAttempt, outcome: RECOVERY_OUTCOME.SUCCEEDED });
|
|
3011
3189
|
this.recoveryAttempt = 0;
|
|
3012
3190
|
this.recoveryExhausted = false;
|
|
3013
3191
|
}
|
|
3014
|
-
if (this.startPromise === opening) this.startPromise = null;
|
|
3015
3192
|
},
|
|
3016
3193
|
() => {
|
|
3194
|
+
if (this.startPromise === opening) this.startPromise = null;
|
|
3195
|
+
if (lifecycleEpoch !== this.lifecycleEpoch) return;
|
|
3017
3196
|
if (traceAttempt !== void 0) {
|
|
3018
3197
|
this.trace.event({ type: TRACE_EVENT_TYPE.RELIABILITY, operation: RELIABILITY_OPERATION.TRANSPORT_RECOVERY, attempt: traceAttempt, outcome: RECOVERY_OUTCOME.FAILED });
|
|
3019
3198
|
}
|
|
@@ -3047,6 +3226,19 @@ var CrossTabDataBus = class {
|
|
|
3047
3226
|
return operation();
|
|
3048
3227
|
}).catch((error) => this.reportError(error));
|
|
3049
3228
|
}
|
|
3229
|
+
/**
|
|
3230
|
+
* Publications started after teardown begins cannot reach any transport.
|
|
3231
|
+
* Surface that as a normal asynchronous API failure instead of letting
|
|
3232
|
+
* runTransport() return silently. Empty publishBatch() calls remain a no-op
|
|
3233
|
+
* and are filtered by the caller before this check.
|
|
3234
|
+
*/
|
|
3235
|
+
rejectPublishDuringStop(operation) {
|
|
3236
|
+
if (!this.stopping) return false;
|
|
3237
|
+
this.reportError(new Error(
|
|
3238
|
+
`CrossTabDataBus is stopping; ${operation}() was not sent. Wait for stop() to resolve, then call start() before publishing again.`
|
|
3239
|
+
));
|
|
3240
|
+
return true;
|
|
3241
|
+
}
|
|
3050
3242
|
/**
|
|
3051
3243
|
* Ensure the DataBus is started, throwing if no initialConfig was provided.
|
|
3052
3244
|
* Called automatically by subscribe/publish/ready when autoStart is true.
|
|
@@ -3152,35 +3344,29 @@ function createIndexedDbReplayPersistence(options) {
|
|
|
3152
3344
|
grouped.set(message.topic, [...grouped.get(message.topic) ?? [], message]);
|
|
3153
3345
|
}
|
|
3154
3346
|
let hasError = false;
|
|
3347
|
+
const fail = (error) => {
|
|
3348
|
+
if (hasError) return;
|
|
3349
|
+
hasError = true;
|
|
3350
|
+
invalidate(db);
|
|
3351
|
+
reject(error);
|
|
3352
|
+
};
|
|
3155
3353
|
for (const [topic, topicMessages] of grouped) {
|
|
3156
3354
|
const request = store.get(topic);
|
|
3157
3355
|
request.onsuccess = () => {
|
|
3158
3356
|
if (hasError) return;
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
history = history.filter((item) => item.timestamp === void 0 || item.timestamp >= cutoff);
|
|
3164
|
-
}
|
|
3165
|
-
if (pruneStrategy !== PRUNE_STRATEGY.AGE || !ageBounded) history = history.slice(-maxPerTopic);
|
|
3357
|
+
const history = pruneReplayHistory(
|
|
3358
|
+
(request.result?.messages ?? []).concat(topicMessages),
|
|
3359
|
+
{ maxPerTopic, pruneStrategy, retentionMs, now: Date.now() }
|
|
3360
|
+
);
|
|
3166
3361
|
store.put({ topic, messages: history });
|
|
3167
3362
|
};
|
|
3168
|
-
request.onerror = () =>
|
|
3169
|
-
if (hasError) return;
|
|
3170
|
-
hasError = true;
|
|
3171
|
-
invalidate(db);
|
|
3172
|
-
reject(request.error ?? new Error("Failed to read replay history."));
|
|
3173
|
-
};
|
|
3363
|
+
request.onerror = () => fail(request.error ?? new Error("Failed to read replay history."));
|
|
3174
3364
|
}
|
|
3175
3365
|
transaction.oncomplete = () => {
|
|
3176
3366
|
if (!hasError) resolve();
|
|
3177
3367
|
};
|
|
3178
|
-
transaction.onerror = () =>
|
|
3179
|
-
|
|
3180
|
-
hasError = true;
|
|
3181
|
-
invalidate(db);
|
|
3182
|
-
reject(transaction.error ?? new Error("Failed to persist replay history."));
|
|
3183
|
-
};
|
|
3368
|
+
transaction.onerror = () => fail(transaction.error ?? new Error("Failed to persist replay history."));
|
|
3369
|
+
transaction.onabort = () => fail(transaction.error ?? new Error("Failed to persist replay history."));
|
|
3184
3370
|
});
|
|
3185
3371
|
})();
|
|
3186
3372
|
const open = () => {
|
|
@@ -3208,19 +3394,34 @@ function createIndexedDbReplayPersistence(options) {
|
|
|
3208
3394
|
async load() {
|
|
3209
3395
|
const db = await open();
|
|
3210
3396
|
return new Promise((resolve, reject) => {
|
|
3397
|
+
let transaction;
|
|
3211
3398
|
let request;
|
|
3212
3399
|
try {
|
|
3213
|
-
|
|
3400
|
+
transaction = db.transaction(storeName, "readonly");
|
|
3401
|
+
request = transaction.objectStore(storeName).getAll();
|
|
3214
3402
|
} catch (error) {
|
|
3215
3403
|
invalidate(db);
|
|
3216
3404
|
reject(error);
|
|
3217
3405
|
return;
|
|
3218
3406
|
}
|
|
3219
|
-
|
|
3220
|
-
|
|
3407
|
+
let settled = false;
|
|
3408
|
+
const fail = (error) => {
|
|
3409
|
+
if (settled) return;
|
|
3410
|
+
settled = true;
|
|
3221
3411
|
invalidate(db);
|
|
3222
|
-
reject(
|
|
3412
|
+
reject(error);
|
|
3413
|
+
};
|
|
3414
|
+
let records = [];
|
|
3415
|
+
request.onsuccess = () => {
|
|
3416
|
+
records = request.result;
|
|
3223
3417
|
};
|
|
3418
|
+
request.onerror = () => fail(request.error ?? new Error("Failed to load replay history."));
|
|
3419
|
+
transaction.oncomplete = () => {
|
|
3420
|
+
if (settled) return;
|
|
3421
|
+
settled = true;
|
|
3422
|
+
resolve(records.flatMap((record) => record.messages));
|
|
3423
|
+
};
|
|
3424
|
+
transaction.onabort = () => fail(transaction.error ?? new Error("Failed to load replay history."));
|
|
3224
3425
|
});
|
|
3225
3426
|
},
|
|
3226
3427
|
append(message) {
|
|
@@ -3244,12 +3445,20 @@ function createIndexedDbReplayPersistence(options) {
|
|
|
3244
3445
|
reject(error);
|
|
3245
3446
|
return;
|
|
3246
3447
|
}
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3448
|
+
let settled = false;
|
|
3449
|
+
const fail = (error) => {
|
|
3450
|
+
if (settled) return;
|
|
3451
|
+
settled = true;
|
|
3250
3452
|
invalidate(db);
|
|
3251
|
-
reject(
|
|
3453
|
+
reject(error);
|
|
3454
|
+
};
|
|
3455
|
+
transaction.objectStore(storeName).clear();
|
|
3456
|
+
transaction.oncomplete = () => {
|
|
3457
|
+
settled = true;
|
|
3458
|
+
resolve();
|
|
3252
3459
|
};
|
|
3460
|
+
transaction.onerror = () => fail(transaction.error ?? new Error("Failed to clear replay history."));
|
|
3461
|
+
transaction.onabort = () => fail(transaction.error ?? new Error("Failed to clear replay history."));
|
|
3253
3462
|
});
|
|
3254
3463
|
})()
|
|
3255
3464
|
});
|
|
@@ -3268,12 +3477,20 @@ function createIndexedDbReplayPersistence(options) {
|
|
|
3268
3477
|
reject(error);
|
|
3269
3478
|
return;
|
|
3270
3479
|
}
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3480
|
+
let settled = false;
|
|
3481
|
+
const fail = (error) => {
|
|
3482
|
+
if (settled) return;
|
|
3483
|
+
settled = true;
|
|
3274
3484
|
invalidate(db);
|
|
3275
|
-
reject(
|
|
3485
|
+
reject(error);
|
|
3276
3486
|
};
|
|
3487
|
+
transaction.objectStore(storeName).delete(topic);
|
|
3488
|
+
transaction.oncomplete = () => {
|
|
3489
|
+
settled = true;
|
|
3490
|
+
resolve();
|
|
3491
|
+
};
|
|
3492
|
+
transaction.onerror = () => fail(transaction.error ?? new Error("Failed to clear topic replay history."));
|
|
3493
|
+
transaction.onabort = () => fail(transaction.error ?? new Error("Failed to clear topic replay history."));
|
|
3277
3494
|
});
|
|
3278
3495
|
})()
|
|
3279
3496
|
});
|
|
@@ -3292,6 +3509,13 @@ function createIndexedDbReplayPersistence(options) {
|
|
|
3292
3509
|
reject(error);
|
|
3293
3510
|
return;
|
|
3294
3511
|
}
|
|
3512
|
+
let settled = false;
|
|
3513
|
+
const fail = (error) => {
|
|
3514
|
+
if (settled) return;
|
|
3515
|
+
settled = true;
|
|
3516
|
+
invalidate(db);
|
|
3517
|
+
reject(error);
|
|
3518
|
+
};
|
|
3295
3519
|
const store = transaction.objectStore(storeName);
|
|
3296
3520
|
const request = store.getAll();
|
|
3297
3521
|
request.onsuccess = () => {
|
|
@@ -3301,15 +3525,13 @@ function createIndexedDbReplayPersistence(options) {
|
|
|
3301
3525
|
else if (messages.length !== record.messages.length) store.put({ topic: record.topic, messages });
|
|
3302
3526
|
}
|
|
3303
3527
|
};
|
|
3304
|
-
request.onerror = () =>
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
transaction.oncomplete = () => resolve();
|
|
3309
|
-
transaction.onerror = () => {
|
|
3310
|
-
invalidate(db);
|
|
3311
|
-
reject(transaction.error ?? new Error("Failed to prune replay history."));
|
|
3528
|
+
request.onerror = () => fail(request.error ?? new Error("Failed to read replay history."));
|
|
3529
|
+
transaction.oncomplete = () => {
|
|
3530
|
+
settled = true;
|
|
3531
|
+
resolve();
|
|
3312
3532
|
};
|
|
3533
|
+
transaction.onerror = () => fail(transaction.error ?? new Error("Failed to prune replay history."));
|
|
3534
|
+
transaction.onabort = () => fail(transaction.error ?? new Error("Failed to prune replay history."));
|
|
3313
3535
|
});
|
|
3314
3536
|
})()
|
|
3315
3537
|
});
|