cross-tab-worker-databus 0.10.0 → 0.20.6
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 +222 -0
- package/README.md +1 -0
- package/README.zh.md +1 -0
- package/dist/centrifuge.js +1 -1
- package/dist/centrifuge.shared.worker.js +4 -2
- package/dist/centrifuge.shared.worker.js.map +2 -2
- package/dist/centrifuge.worker.js +4 -2
- package/dist/centrifuge.worker.js.map +2 -2
- package/dist/{chunk-ZOPNTR4E.js → chunk-LPS4XOK4.js} +177 -19
- package/dist/{chunk-ZOPNTR4E.js.map → chunk-LPS4XOK4.js.map} +2 -2
- package/dist/cjs/centrifuge.cjs +176 -18
- package/dist/cjs/centrifuge.cjs.map +2 -2
- package/dist/cjs/hooks.cjs +4 -1
- package/dist/cjs/hooks.cjs.map +2 -2
- package/dist/cjs/index.cjs +326 -67
- package/dist/cjs/index.cjs.map +2 -2
- package/dist/cjs/vue.cjs +7 -1
- package/dist/cjs/vue.cjs.map +2 -2
- package/dist/core/data-bus.d.ts +35 -0
- package/dist/core/data-bus.d.ts.map +1 -1
- package/dist/core/publication.d.ts.map +1 -1
- package/dist/core/replay-persistence.d.ts.map +1 -1
- package/dist/core/trace.d.ts +11 -1
- package/dist/core/trace.d.ts.map +1 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +4 -1
- package/dist/hooks.js.map +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +151 -50
- package/dist/index.js.map +2 -2
- package/dist/vue.d.ts.map +1 -1
- package/dist/vue.js +7 -1
- package/dist/vue.js.map +2 -2
- package/dist/websocket.d.ts.map +1 -1
- package/docs/README.md +1 -0
- package/docs/api.md +11 -4
- package/docs/architecture.md +4 -0
- package/docs/capabilities.md +2 -2
- package/docs/configuration.md +21 -1
- package/docs/release-checklist.md +20 -0
- package/docs/roadmap.md +144 -2
- package/docs/zh/README.md +1 -0
- package/docs/zh/api.md +11 -4
- package/docs/zh/architecture.md +4 -0
- package/docs/zh/capabilities.md +2 -2
- package/docs/zh/configuration.md +21 -1
- package/docs/zh/release-checklist.md +20 -0
- package/docs/zh/roadmap.md +132 -2
- package/package.json +2 -1
package/dist/cjs/centrifuge.cjs
CHANGED
|
@@ -1066,7 +1066,9 @@ var DataBusTraceReporter = class {
|
|
|
1066
1066
|
// Bucketed histogram: bucket index = floor(delayMs / 50), capped at 19.
|
|
1067
1067
|
latencyBuckets = new Array(LATENCY_BUCKET_COUNT).fill(0);
|
|
1068
1068
|
latencySumMs = 0;
|
|
1069
|
-
|
|
1069
|
+
dedupAccepted = 0;
|
|
1070
|
+
dedupSuppressed = 0;
|
|
1071
|
+
constructor(options, now = options?.now ?? Date.now) {
|
|
1070
1072
|
this.enabled = options?.enabled ?? false;
|
|
1071
1073
|
this.mode = options?.mode ?? "all";
|
|
1072
1074
|
this.metricsIntervalMs = normalizeInterval(options?.metricsIntervalMs);
|
|
@@ -1141,6 +1143,13 @@ var DataBusTraceReporter = class {
|
|
|
1141
1143
|
this.latencyBuckets[bucketIndex] = (this.latencyBuckets[bucketIndex] ?? 0) + 1;
|
|
1142
1144
|
this.latencySumMs += delayMs;
|
|
1143
1145
|
}
|
|
1146
|
+
/** Record deduplication outcomes for the next metrics window. */
|
|
1147
|
+
recordDedupAccepted() {
|
|
1148
|
+
if (this.metricsActive) this.dedupAccepted += 1;
|
|
1149
|
+
}
|
|
1150
|
+
recordDedupSuppressed() {
|
|
1151
|
+
if (this.metricsActive) this.dedupSuppressed += 1;
|
|
1152
|
+
}
|
|
1144
1153
|
/** True when metrics recording is active: enabled and mode includes metrics.
|
|
1145
1154
|
* Extracted so the four record / flush methods share one guard expression
|
|
1146
1155
|
* instead of repeating `!this.enabled || this.mode === 'events'` at each. */
|
|
@@ -1154,7 +1163,7 @@ var DataBusTraceReporter = class {
|
|
|
1154
1163
|
}
|
|
1155
1164
|
flushNow() {
|
|
1156
1165
|
const timestamp = this.now();
|
|
1157
|
-
if (this.received > 0 || this.dispatched > 0) {
|
|
1166
|
+
if (this.received > 0 || this.dispatched > 0 || this.dedupAccepted > 0 || this.dedupSuppressed > 0) {
|
|
1158
1167
|
const samples = this.latencySamples;
|
|
1159
1168
|
this.emit({
|
|
1160
1169
|
type: "message_metrics",
|
|
@@ -1168,6 +1177,8 @@ var DataBusTraceReporter = class {
|
|
|
1168
1177
|
dispatchP50Ms: roundMs(percentileMs(this.latencyBuckets, samples, 0.5)),
|
|
1169
1178
|
dispatchP95Ms: roundMs(percentileMs(this.latencyBuckets, samples, 0.95)),
|
|
1170
1179
|
dispatchMaxMs: roundMs(percentileMs(this.latencyBuckets, samples, 1)),
|
|
1180
|
+
dedupAccepted: this.dedupAccepted,
|
|
1181
|
+
dedupSuppressed: this.dedupSuppressed,
|
|
1171
1182
|
timestamp
|
|
1172
1183
|
});
|
|
1173
1184
|
this.resetMetrics();
|
|
@@ -1182,6 +1193,8 @@ var DataBusTraceReporter = class {
|
|
|
1182
1193
|
this.receivedAt.clear();
|
|
1183
1194
|
this.latencyBuckets.fill(0);
|
|
1184
1195
|
this.latencySumMs = 0;
|
|
1196
|
+
this.dedupAccepted = 0;
|
|
1197
|
+
this.dedupSuppressed = 0;
|
|
1185
1198
|
}
|
|
1186
1199
|
emit(event) {
|
|
1187
1200
|
try {
|
|
@@ -1217,6 +1230,12 @@ function roundMs(value) {
|
|
|
1217
1230
|
// src/core/data-bus.ts
|
|
1218
1231
|
var PUBLICATION_EVENT = "DATABUS_PUBLICATION";
|
|
1219
1232
|
var DEFAULT_REPLAY_MAX_PER_TOPIC = 100;
|
|
1233
|
+
var PersistenceRetryCancelledError = class extends Error {
|
|
1234
|
+
constructor() {
|
|
1235
|
+
super("Persistence retry cancelled by lifecycle transition.");
|
|
1236
|
+
this.name = "PersistenceRetryCancelledError";
|
|
1237
|
+
}
|
|
1238
|
+
};
|
|
1220
1239
|
var CrossTabDataBus = class _CrossTabDataBus {
|
|
1221
1240
|
transport;
|
|
1222
1241
|
cluster;
|
|
@@ -1232,13 +1251,26 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1232
1251
|
replayBuffers;
|
|
1233
1252
|
replayMaxPerTopic;
|
|
1234
1253
|
replayPersistence;
|
|
1254
|
+
replayRetentionMs;
|
|
1255
|
+
replayRetentionSweepMs;
|
|
1256
|
+
persistenceRetryMaxAttempts;
|
|
1257
|
+
persistenceRetryBackoffMs;
|
|
1258
|
+
persistenceRetryGeneration = 0;
|
|
1235
1259
|
replayHydration;
|
|
1260
|
+
// Retention cleanup is coalesced so a burst of publications does not issue
|
|
1261
|
+
// one IndexedDB read/write transaction per message. The newest cutoff wins.
|
|
1262
|
+
replayRetentionCleanup = null;
|
|
1263
|
+
replayRetentionCutoff = null;
|
|
1264
|
+
replayRetentionTimer = null;
|
|
1236
1265
|
initialConfig;
|
|
1237
1266
|
hasInitialConfig;
|
|
1238
1267
|
trace;
|
|
1239
1268
|
dedupMaxEntries;
|
|
1240
1269
|
dedupTtlMs;
|
|
1270
|
+
dedupSweepMs;
|
|
1271
|
+
dedupSweepTimer = null;
|
|
1241
1272
|
dedupEnabled;
|
|
1273
|
+
now;
|
|
1242
1274
|
seenMessageIds = /* @__PURE__ */ new Map();
|
|
1243
1275
|
dedupSuppressed = 0;
|
|
1244
1276
|
dedupAccepted = 0;
|
|
@@ -1279,14 +1311,32 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1279
1311
|
this.replayMaxPerTopic = replay?.maxPerTopic ?? DEFAULT_REPLAY_MAX_PER_TOPIC;
|
|
1280
1312
|
this.replayBuffers = replay ? /* @__PURE__ */ new Map() : null;
|
|
1281
1313
|
this.replayPersistence = replay?.persistence ?? null;
|
|
1282
|
-
this.
|
|
1314
|
+
this.replayRetentionMs = replay?.retentionMs;
|
|
1315
|
+
if (this.replayRetentionMs !== void 0 && (!Number.isFinite(this.replayRetentionMs) || this.replayRetentionMs <= 0)) {
|
|
1316
|
+
throw new TypeError("replay.retentionMs must be a positive finite number.");
|
|
1317
|
+
}
|
|
1318
|
+
this.replayRetentionSweepMs = replay?.retentionSweepMs;
|
|
1319
|
+
if (this.replayRetentionSweepMs !== void 0 && (!Number.isFinite(this.replayRetentionSweepMs) || this.replayRetentionSweepMs <= 0)) {
|
|
1320
|
+
throw new TypeError("replay.retentionSweepMs must be a positive finite number.");
|
|
1321
|
+
}
|
|
1322
|
+
this.persistenceRetryMaxAttempts = replay?.persistenceRetry?.maxAttempts ?? 1;
|
|
1323
|
+
this.persistenceRetryBackoffMs = replay?.persistenceRetry?.backoffMs ?? 50;
|
|
1324
|
+
if (!Number.isSafeInteger(this.persistenceRetryMaxAttempts) || this.persistenceRetryMaxAttempts <= 0) {
|
|
1325
|
+
throw new TypeError("replay.persistenceRetry.maxAttempts must be a positive safe integer.");
|
|
1326
|
+
}
|
|
1327
|
+
if (!Number.isFinite(this.persistenceRetryBackoffMs) || this.persistenceRetryBackoffMs < 0) {
|
|
1328
|
+
throw new TypeError("replay.persistenceRetry.backoffMs must be a non-negative finite number.");
|
|
1329
|
+
}
|
|
1283
1330
|
const { autoStart, initialConfig, trace, transport, dedup, ...clusterOptions } = options;
|
|
1331
|
+
this.now = dedup?.now ?? Date.now;
|
|
1332
|
+
this.replayHydration = this.hydrateReplay();
|
|
1284
1333
|
this.transport = transport;
|
|
1285
1334
|
this.initialConfig = initialConfig;
|
|
1286
1335
|
this.hasInitialConfig = "initialConfig" in options;
|
|
1287
1336
|
this.trace = new DataBusTraceReporter(trace);
|
|
1288
1337
|
this.dedupMaxEntries = dedup?.maxEntries ?? 1e3;
|
|
1289
1338
|
this.dedupTtlMs = dedup?.ttlMs ?? 6e4;
|
|
1339
|
+
this.dedupSweepMs = dedup?.sweepMs;
|
|
1290
1340
|
this.dedupEnabled = dedup !== void 0;
|
|
1291
1341
|
if (!Number.isSafeInteger(this.dedupMaxEntries) || this.dedupMaxEntries <= 0) {
|
|
1292
1342
|
throw new TypeError("dedup.maxEntries must be a positive safe integer.");
|
|
@@ -1294,6 +1344,9 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1294
1344
|
if (!Number.isFinite(this.dedupTtlMs) || this.dedupTtlMs <= 0) {
|
|
1295
1345
|
throw new TypeError("dedup.ttlMs must be a positive finite number.");
|
|
1296
1346
|
}
|
|
1347
|
+
if (this.dedupSweepMs !== void 0 && (!Number.isFinite(this.dedupSweepMs) || this.dedupSweepMs <= 0)) {
|
|
1348
|
+
throw new TypeError("dedup.sweepMs must be a positive finite number.");
|
|
1349
|
+
}
|
|
1297
1350
|
this.cluster = new WorkerClusterRuntime({
|
|
1298
1351
|
...clusterOptions,
|
|
1299
1352
|
handlers: {
|
|
@@ -1334,11 +1387,16 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1334
1387
|
onSuspend: () => {
|
|
1335
1388
|
if (!this.stopping) this.trace.event({ type: "lifecycle", action: "suspend" });
|
|
1336
1389
|
this.trace.pause();
|
|
1390
|
+
this.persistenceRetryGeneration += 1;
|
|
1391
|
+
this.stopDedupSweep();
|
|
1392
|
+
this.stopReplayRetentionSweep();
|
|
1337
1393
|
this.suspendTransport();
|
|
1338
1394
|
},
|
|
1339
1395
|
onResume: () => {
|
|
1340
1396
|
this.trace.event({ type: "lifecycle", action: "resume" });
|
|
1341
1397
|
this.trace.start();
|
|
1398
|
+
this.startDedupSweep();
|
|
1399
|
+
this.startReplayRetentionSweep();
|
|
1342
1400
|
this.resumeTransport();
|
|
1343
1401
|
},
|
|
1344
1402
|
onDiagnostic: (event) => {
|
|
@@ -1366,6 +1424,8 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1366
1424
|
this.lastError = null;
|
|
1367
1425
|
this.trace.event({ type: "lifecycle", action: "start" });
|
|
1368
1426
|
this.trace.start();
|
|
1427
|
+
this.startDedupSweep();
|
|
1428
|
+
this.startReplayRetentionSweep();
|
|
1369
1429
|
this.updateStatus("connecting");
|
|
1370
1430
|
this.cluster.start();
|
|
1371
1431
|
const opening = this.openTransport(config, this.pendingStop ?? Promise.resolve(), true);
|
|
@@ -1490,7 +1550,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1490
1550
|
this.topicHandlers.delete(topic);
|
|
1491
1551
|
this.replayBuffers?.delete(topic);
|
|
1492
1552
|
if (this.replayPersistence?.clearTopic) {
|
|
1493
|
-
void this.replayPersistence.clearTopic(topic).catch((error) => this.
|
|
1553
|
+
void this.withPersistenceRetry("clearTopic", () => this.replayPersistence.clearTopic(topic)).catch((error) => this.reportPersistenceError(error));
|
|
1494
1554
|
}
|
|
1495
1555
|
this.cluster.unsubscribe(topic);
|
|
1496
1556
|
}
|
|
@@ -1499,9 +1559,9 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1499
1559
|
this.replayBuffers?.clear();
|
|
1500
1560
|
if (this.replayPersistence?.clear) {
|
|
1501
1561
|
try {
|
|
1502
|
-
await this.replayPersistence.clear();
|
|
1562
|
+
await this.withPersistenceRetry("clear", () => this.replayPersistence.clear());
|
|
1503
1563
|
} catch (error) {
|
|
1504
|
-
this.
|
|
1564
|
+
this.reportPersistenceError(error);
|
|
1505
1565
|
throw error;
|
|
1506
1566
|
}
|
|
1507
1567
|
}
|
|
@@ -1511,9 +1571,9 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1511
1571
|
this.replayBuffers?.delete(topic);
|
|
1512
1572
|
if (this.replayPersistence?.clearTopic) {
|
|
1513
1573
|
try {
|
|
1514
|
-
await this.replayPersistence.clearTopic(topic);
|
|
1574
|
+
await this.withPersistenceRetry("clearTopic", () => this.replayPersistence.clearTopic(topic));
|
|
1515
1575
|
} catch (error) {
|
|
1516
|
-
this.
|
|
1576
|
+
this.reportPersistenceError(error);
|
|
1517
1577
|
throw error;
|
|
1518
1578
|
}
|
|
1519
1579
|
}
|
|
@@ -1523,12 +1583,19 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1523
1583
|
if (!Number.isFinite(timestamp)) throw new TypeError("timestamp must be finite.");
|
|
1524
1584
|
if (this.replayBuffers) {
|
|
1525
1585
|
for (const [topic, messages] of this.replayBuffers) {
|
|
1526
|
-
const kept = messages.filter((message) =>
|
|
1586
|
+
const kept = messages.filter((message) => message.timestamp === void 0 || message.timestamp >= timestamp);
|
|
1527
1587
|
if (kept.length) this.replayBuffers.set(topic, kept);
|
|
1528
1588
|
else this.replayBuffers.delete(topic);
|
|
1529
1589
|
}
|
|
1530
1590
|
}
|
|
1531
|
-
if (this.replayPersistence?.clearBefore)
|
|
1591
|
+
if (this.replayPersistence?.clearBefore) {
|
|
1592
|
+
try {
|
|
1593
|
+
await this.withPersistenceRetry("clearBefore", () => this.replayPersistence.clearBefore(timestamp));
|
|
1594
|
+
} catch (error) {
|
|
1595
|
+
this.reportPersistenceError(error);
|
|
1596
|
+
throw error;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1532
1599
|
}
|
|
1533
1600
|
/** Return bounded deduplication counters for diagnostics and health checks. */
|
|
1534
1601
|
getDedupStats() {
|
|
@@ -1586,8 +1653,11 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1586
1653
|
async stop() {
|
|
1587
1654
|
if (!this.started) return;
|
|
1588
1655
|
this.stopping = true;
|
|
1656
|
+
this.persistenceRetryGeneration += 1;
|
|
1589
1657
|
this.trace.event({ type: "lifecycle", action: "stop" });
|
|
1590
1658
|
this.trace.stop();
|
|
1659
|
+
this.stopDedupSweep();
|
|
1660
|
+
this.stopReplayRetentionSweep();
|
|
1591
1661
|
this.topicHandlers.clear();
|
|
1592
1662
|
this.replayBuffers?.clear();
|
|
1593
1663
|
this.cluster.stop();
|
|
@@ -1598,7 +1668,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1598
1668
|
else await this.transport.stop();
|
|
1599
1669
|
} finally {
|
|
1600
1670
|
this.transportSubscribedTopics.clear();
|
|
1601
|
-
this.
|
|
1671
|
+
this.resetDedup();
|
|
1602
1672
|
this.started = false;
|
|
1603
1673
|
this.stopping = false;
|
|
1604
1674
|
this.suspended = false;
|
|
@@ -1631,17 +1701,19 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1631
1701
|
}
|
|
1632
1702
|
isDuplicate(message) {
|
|
1633
1703
|
if (!this.dedupEnabled || !message.messageId) return false;
|
|
1634
|
-
const now =
|
|
1704
|
+
const now = this.now();
|
|
1635
1705
|
for (const [id, timestamp] of this.seenMessageIds) {
|
|
1636
1706
|
if (now - timestamp > this.dedupTtlMs) this.seenMessageIds.delete(id);
|
|
1637
1707
|
}
|
|
1638
1708
|
if (this.seenMessageIds.has(message.messageId)) {
|
|
1639
1709
|
this.trace.event({ type: "reliability", operation: "dedup_suppressed", topic: message.topic });
|
|
1640
1710
|
this.dedupSuppressed += 1;
|
|
1711
|
+
this.trace.recordDedupSuppressed();
|
|
1641
1712
|
return true;
|
|
1642
1713
|
}
|
|
1643
1714
|
this.seenMessageIds.set(message.messageId, now);
|
|
1644
1715
|
this.dedupAccepted += 1;
|
|
1716
|
+
this.trace.recordDedupAccepted();
|
|
1645
1717
|
while (this.seenMessageIds.size > this.dedupMaxEntries) {
|
|
1646
1718
|
const oldest = this.seenMessageIds.keys().next().value;
|
|
1647
1719
|
if (oldest === void 0) break;
|
|
@@ -1649,6 +1721,20 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1649
1721
|
}
|
|
1650
1722
|
return false;
|
|
1651
1723
|
}
|
|
1724
|
+
startDedupSweep() {
|
|
1725
|
+
if (this.dedupSweepTimer || !this.dedupEnabled || !this.dedupSweepMs) return;
|
|
1726
|
+
this.dedupSweepTimer = setInterval(() => this.pruneExpiredDedup(), this.dedupSweepMs);
|
|
1727
|
+
}
|
|
1728
|
+
stopDedupSweep() {
|
|
1729
|
+
if (this.dedupSweepTimer) clearInterval(this.dedupSweepTimer);
|
|
1730
|
+
this.dedupSweepTimer = null;
|
|
1731
|
+
}
|
|
1732
|
+
pruneExpiredDedup() {
|
|
1733
|
+
const cutoff = this.now() - this.dedupTtlMs;
|
|
1734
|
+
for (const [id, timestamp] of this.seenMessageIds) {
|
|
1735
|
+
if (timestamp < cutoff) this.seenMessageIds.delete(id);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1652
1738
|
/** Deliver a message to every local handler registered for its topic,
|
|
1653
1739
|
* plus every handler registered with a wildcard subscription that matches
|
|
1654
1740
|
* (e.g. a handler subscribed to "chat.*" receives "chat.room.1"). */
|
|
@@ -1675,7 +1761,10 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1675
1761
|
buffer.push(storedMessage);
|
|
1676
1762
|
if (buffer.length > this.replayMaxPerTopic) buffer.shift();
|
|
1677
1763
|
if (this.replayPersistence) {
|
|
1678
|
-
void this.replayPersistence.append(storedMessage).catch((error) => this.
|
|
1764
|
+
void this.withPersistenceRetry("append", () => this.replayPersistence.append(storedMessage)).catch((error) => this.reportPersistenceError(error));
|
|
1765
|
+
if (this.replayRetentionMs !== void 0 && this.replayPersistence.clearBefore) {
|
|
1766
|
+
this.scheduleReplayRetentionCleanup(this.now() - this.replayRetentionMs);
|
|
1767
|
+
}
|
|
1679
1768
|
}
|
|
1680
1769
|
}
|
|
1681
1770
|
async hydrateReplay() {
|
|
@@ -1683,7 +1772,10 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1683
1772
|
return;
|
|
1684
1773
|
}
|
|
1685
1774
|
try {
|
|
1686
|
-
|
|
1775
|
+
if (this.replayRetentionMs !== void 0 && this.replayPersistence.clearBefore) {
|
|
1776
|
+
await this.withPersistenceRetry("clearBefore", () => this.replayPersistence.clearBefore(this.now() - this.replayRetentionMs));
|
|
1777
|
+
}
|
|
1778
|
+
for (const message of await this.withPersistenceRetry("load", () => this.replayPersistence.load())) {
|
|
1687
1779
|
let buffer = this.replayBuffers.get(message.topic);
|
|
1688
1780
|
if (!buffer) {
|
|
1689
1781
|
buffer = [];
|
|
@@ -1693,7 +1785,66 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1693
1785
|
if (buffer.length > this.replayMaxPerTopic) buffer.shift();
|
|
1694
1786
|
}
|
|
1695
1787
|
} catch (error) {
|
|
1696
|
-
this.
|
|
1788
|
+
this.reportPersistenceError(error);
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
scheduleReplayRetentionCleanup(cutoff) {
|
|
1792
|
+
if (!this.replayPersistence?.clearBefore) return;
|
|
1793
|
+
if (this.replayRetentionCutoff === null || cutoff > this.replayRetentionCutoff) {
|
|
1794
|
+
this.replayRetentionCutoff = cutoff;
|
|
1795
|
+
}
|
|
1796
|
+
if (this.replayRetentionCleanup) return;
|
|
1797
|
+
this.replayRetentionCleanup = (async () => {
|
|
1798
|
+
while (this.replayRetentionCutoff !== null) {
|
|
1799
|
+
const nextCutoff = this.replayRetentionCutoff;
|
|
1800
|
+
this.replayRetentionCutoff = null;
|
|
1801
|
+
try {
|
|
1802
|
+
await this.replayPersistence.clearBefore(nextCutoff);
|
|
1803
|
+
} catch (error) {
|
|
1804
|
+
this.reportPersistenceError(error);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
})().finally(() => {
|
|
1808
|
+
this.replayRetentionCleanup = null;
|
|
1809
|
+
if (this.replayRetentionCutoff !== null) {
|
|
1810
|
+
this.scheduleReplayRetentionCleanup(this.replayRetentionCutoff);
|
|
1811
|
+
}
|
|
1812
|
+
});
|
|
1813
|
+
}
|
|
1814
|
+
startReplayRetentionSweep() {
|
|
1815
|
+
if (this.replayRetentionTimer || !this.replayRetentionMs || !this.replayRetentionSweepMs || !this.replayPersistence?.clearBefore) return;
|
|
1816
|
+
this.replayRetentionTimer = setInterval(() => {
|
|
1817
|
+
this.scheduleReplayRetentionCleanup(this.now() - this.replayRetentionMs);
|
|
1818
|
+
}, this.replayRetentionSweepMs);
|
|
1819
|
+
}
|
|
1820
|
+
stopReplayRetentionSweep() {
|
|
1821
|
+
if (this.replayRetentionTimer) clearInterval(this.replayRetentionTimer);
|
|
1822
|
+
this.replayRetentionTimer = null;
|
|
1823
|
+
}
|
|
1824
|
+
async withPersistenceRetry(persistenceOperation, operation) {
|
|
1825
|
+
const generation = this.persistenceRetryGeneration;
|
|
1826
|
+
let attempt = 0;
|
|
1827
|
+
let delay = this.persistenceRetryBackoffMs;
|
|
1828
|
+
while (true) {
|
|
1829
|
+
attempt += 1;
|
|
1830
|
+
try {
|
|
1831
|
+
if (generation !== this.persistenceRetryGeneration) throw new PersistenceRetryCancelledError();
|
|
1832
|
+
return await operation();
|
|
1833
|
+
} catch (error) {
|
|
1834
|
+
if (error instanceof PersistenceRetryCancelledError || generation !== this.persistenceRetryGeneration) {
|
|
1835
|
+
throw new PersistenceRetryCancelledError();
|
|
1836
|
+
}
|
|
1837
|
+
if (attempt >= this.persistenceRetryMaxAttempts) throw error;
|
|
1838
|
+
this.trace.event({
|
|
1839
|
+
type: "reliability",
|
|
1840
|
+
operation: "persistence_retry",
|
|
1841
|
+
persistenceOperation,
|
|
1842
|
+
attempt
|
|
1843
|
+
});
|
|
1844
|
+
if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1845
|
+
if (generation !== this.persistenceRetryGeneration) throw new PersistenceRetryCancelledError();
|
|
1846
|
+
delay = Math.min(delay * 2, 1600);
|
|
1847
|
+
}
|
|
1697
1848
|
}
|
|
1698
1849
|
}
|
|
1699
1850
|
/** Deliver buffered history to a newly-registered handler. For an exact
|
|
@@ -1731,7 +1882,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1731
1882
|
for (const topic of this.cluster.getSnapshot().assignedTopics) this.subscribeTransport(topic);
|
|
1732
1883
|
}
|
|
1733
1884
|
if (status === "error" && this.started && !this.stopping) {
|
|
1734
|
-
const now =
|
|
1885
|
+
const now = this.now();
|
|
1735
1886
|
if (now - this.lastRecoveryAt >= _CrossTabDataBus.RECOVERY_COOLDOWN_MS) {
|
|
1736
1887
|
this.lastRecoveryAt = now;
|
|
1737
1888
|
this.trace.event({ type: "reliability", operation: "transport_recovery", attempt: 1 });
|
|
@@ -1748,6 +1899,11 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1748
1899
|
this.trace.event({ type: "error", source: "transport" });
|
|
1749
1900
|
this.invokeHandlers(this.errorHandlers, (handler) => handler(error), "error handler");
|
|
1750
1901
|
}
|
|
1902
|
+
reportPersistenceError(error) {
|
|
1903
|
+
if (error instanceof PersistenceRetryCancelledError) return;
|
|
1904
|
+
this.trace.event({ type: "reliability", operation: "persistence_cleanup" });
|
|
1905
|
+
this.reportError(error);
|
|
1906
|
+
}
|
|
1751
1907
|
traceSubscription(action, topic) {
|
|
1752
1908
|
this.trace.event({
|
|
1753
1909
|
type: "subscription",
|
|
@@ -1905,11 +2061,13 @@ function parseDataBusPublication(value, fallbackTopic) {
|
|
|
1905
2061
|
if (!topic) return null;
|
|
1906
2062
|
const hasMetadataEnvelope = fallbackTopic !== void 0 && Object.prototype.hasOwnProperty.call(publication, "data") && (typeof publication.messageId === "string" || typeof publication.timestamp === "number");
|
|
1907
2063
|
const data = nested || fallbackTopic === void 0 || hasMetadataEnvelope ? publication.data : value;
|
|
2064
|
+
const messageId = typeof publication.messageId === "string" && publication.messageId.length > 0 ? publication.messageId : void 0;
|
|
2065
|
+
const timestamp = typeof publication.timestamp === "number" && Number.isFinite(publication.timestamp) ? publication.timestamp : void 0;
|
|
1908
2066
|
return {
|
|
1909
2067
|
topic,
|
|
1910
2068
|
data,
|
|
1911
|
-
...
|
|
1912
|
-
...
|
|
2069
|
+
...messageId === void 0 ? {} : { messageId },
|
|
2070
|
+
...timestamp === void 0 ? {} : { timestamp }
|
|
1913
2071
|
};
|
|
1914
2072
|
}
|
|
1915
2073
|
|