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/index.cjs
CHANGED
|
@@ -1091,7 +1091,9 @@ var DataBusTraceReporter = class {
|
|
|
1091
1091
|
// Bucketed histogram: bucket index = floor(delayMs / 50), capped at 19.
|
|
1092
1092
|
latencyBuckets = new Array(LATENCY_BUCKET_COUNT).fill(0);
|
|
1093
1093
|
latencySumMs = 0;
|
|
1094
|
-
|
|
1094
|
+
dedupAccepted = 0;
|
|
1095
|
+
dedupSuppressed = 0;
|
|
1096
|
+
constructor(options, now = options?.now ?? Date.now) {
|
|
1095
1097
|
this.enabled = options?.enabled ?? false;
|
|
1096
1098
|
this.mode = options?.mode ?? "all";
|
|
1097
1099
|
this.metricsIntervalMs = normalizeInterval(options?.metricsIntervalMs);
|
|
@@ -1166,6 +1168,13 @@ var DataBusTraceReporter = class {
|
|
|
1166
1168
|
this.latencyBuckets[bucketIndex] = (this.latencyBuckets[bucketIndex] ?? 0) + 1;
|
|
1167
1169
|
this.latencySumMs += delayMs;
|
|
1168
1170
|
}
|
|
1171
|
+
/** Record deduplication outcomes for the next metrics window. */
|
|
1172
|
+
recordDedupAccepted() {
|
|
1173
|
+
if (this.metricsActive) this.dedupAccepted += 1;
|
|
1174
|
+
}
|
|
1175
|
+
recordDedupSuppressed() {
|
|
1176
|
+
if (this.metricsActive) this.dedupSuppressed += 1;
|
|
1177
|
+
}
|
|
1169
1178
|
/** True when metrics recording is active: enabled and mode includes metrics.
|
|
1170
1179
|
* Extracted so the four record / flush methods share one guard expression
|
|
1171
1180
|
* instead of repeating `!this.enabled || this.mode === 'events'` at each. */
|
|
@@ -1179,7 +1188,7 @@ var DataBusTraceReporter = class {
|
|
|
1179
1188
|
}
|
|
1180
1189
|
flushNow() {
|
|
1181
1190
|
const timestamp = this.now();
|
|
1182
|
-
if (this.received > 0 || this.dispatched > 0) {
|
|
1191
|
+
if (this.received > 0 || this.dispatched > 0 || this.dedupAccepted > 0 || this.dedupSuppressed > 0) {
|
|
1183
1192
|
const samples = this.latencySamples;
|
|
1184
1193
|
this.emit({
|
|
1185
1194
|
type: "message_metrics",
|
|
@@ -1193,6 +1202,8 @@ var DataBusTraceReporter = class {
|
|
|
1193
1202
|
dispatchP50Ms: roundMs(percentileMs(this.latencyBuckets, samples, 0.5)),
|
|
1194
1203
|
dispatchP95Ms: roundMs(percentileMs(this.latencyBuckets, samples, 0.95)),
|
|
1195
1204
|
dispatchMaxMs: roundMs(percentileMs(this.latencyBuckets, samples, 1)),
|
|
1205
|
+
dedupAccepted: this.dedupAccepted,
|
|
1206
|
+
dedupSuppressed: this.dedupSuppressed,
|
|
1196
1207
|
timestamp
|
|
1197
1208
|
});
|
|
1198
1209
|
this.resetMetrics();
|
|
@@ -1207,6 +1218,8 @@ var DataBusTraceReporter = class {
|
|
|
1207
1218
|
this.receivedAt.clear();
|
|
1208
1219
|
this.latencyBuckets.fill(0);
|
|
1209
1220
|
this.latencySumMs = 0;
|
|
1221
|
+
this.dedupAccepted = 0;
|
|
1222
|
+
this.dedupSuppressed = 0;
|
|
1210
1223
|
}
|
|
1211
1224
|
emit(event) {
|
|
1212
1225
|
try {
|
|
@@ -1242,6 +1255,12 @@ function roundMs(value) {
|
|
|
1242
1255
|
// src/core/data-bus.ts
|
|
1243
1256
|
var PUBLICATION_EVENT = "DATABUS_PUBLICATION";
|
|
1244
1257
|
var DEFAULT_REPLAY_MAX_PER_TOPIC = 100;
|
|
1258
|
+
var PersistenceRetryCancelledError = class extends Error {
|
|
1259
|
+
constructor() {
|
|
1260
|
+
super("Persistence retry cancelled by lifecycle transition.");
|
|
1261
|
+
this.name = "PersistenceRetryCancelledError";
|
|
1262
|
+
}
|
|
1263
|
+
};
|
|
1245
1264
|
var CrossTabDataBus = class _CrossTabDataBus {
|
|
1246
1265
|
transport;
|
|
1247
1266
|
cluster;
|
|
@@ -1257,13 +1276,26 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1257
1276
|
replayBuffers;
|
|
1258
1277
|
replayMaxPerTopic;
|
|
1259
1278
|
replayPersistence;
|
|
1279
|
+
replayRetentionMs;
|
|
1280
|
+
replayRetentionSweepMs;
|
|
1281
|
+
persistenceRetryMaxAttempts;
|
|
1282
|
+
persistenceRetryBackoffMs;
|
|
1283
|
+
persistenceRetryGeneration = 0;
|
|
1260
1284
|
replayHydration;
|
|
1285
|
+
// Retention cleanup is coalesced so a burst of publications does not issue
|
|
1286
|
+
// one IndexedDB read/write transaction per message. The newest cutoff wins.
|
|
1287
|
+
replayRetentionCleanup = null;
|
|
1288
|
+
replayRetentionCutoff = null;
|
|
1289
|
+
replayRetentionTimer = null;
|
|
1261
1290
|
initialConfig;
|
|
1262
1291
|
hasInitialConfig;
|
|
1263
1292
|
trace;
|
|
1264
1293
|
dedupMaxEntries;
|
|
1265
1294
|
dedupTtlMs;
|
|
1295
|
+
dedupSweepMs;
|
|
1296
|
+
dedupSweepTimer = null;
|
|
1266
1297
|
dedupEnabled;
|
|
1298
|
+
now;
|
|
1267
1299
|
seenMessageIds = /* @__PURE__ */ new Map();
|
|
1268
1300
|
dedupSuppressed = 0;
|
|
1269
1301
|
dedupAccepted = 0;
|
|
@@ -1304,14 +1336,32 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1304
1336
|
this.replayMaxPerTopic = replay?.maxPerTopic ?? DEFAULT_REPLAY_MAX_PER_TOPIC;
|
|
1305
1337
|
this.replayBuffers = replay ? /* @__PURE__ */ new Map() : null;
|
|
1306
1338
|
this.replayPersistence = replay?.persistence ?? null;
|
|
1307
|
-
this.
|
|
1339
|
+
this.replayRetentionMs = replay?.retentionMs;
|
|
1340
|
+
if (this.replayRetentionMs !== void 0 && (!Number.isFinite(this.replayRetentionMs) || this.replayRetentionMs <= 0)) {
|
|
1341
|
+
throw new TypeError("replay.retentionMs must be a positive finite number.");
|
|
1342
|
+
}
|
|
1343
|
+
this.replayRetentionSweepMs = replay?.retentionSweepMs;
|
|
1344
|
+
if (this.replayRetentionSweepMs !== void 0 && (!Number.isFinite(this.replayRetentionSweepMs) || this.replayRetentionSweepMs <= 0)) {
|
|
1345
|
+
throw new TypeError("replay.retentionSweepMs must be a positive finite number.");
|
|
1346
|
+
}
|
|
1347
|
+
this.persistenceRetryMaxAttempts = replay?.persistenceRetry?.maxAttempts ?? 1;
|
|
1348
|
+
this.persistenceRetryBackoffMs = replay?.persistenceRetry?.backoffMs ?? 50;
|
|
1349
|
+
if (!Number.isSafeInteger(this.persistenceRetryMaxAttempts) || this.persistenceRetryMaxAttempts <= 0) {
|
|
1350
|
+
throw new TypeError("replay.persistenceRetry.maxAttempts must be a positive safe integer.");
|
|
1351
|
+
}
|
|
1352
|
+
if (!Number.isFinite(this.persistenceRetryBackoffMs) || this.persistenceRetryBackoffMs < 0) {
|
|
1353
|
+
throw new TypeError("replay.persistenceRetry.backoffMs must be a non-negative finite number.");
|
|
1354
|
+
}
|
|
1308
1355
|
const { autoStart, initialConfig, trace, transport, dedup, ...clusterOptions } = options;
|
|
1356
|
+
this.now = dedup?.now ?? Date.now;
|
|
1357
|
+
this.replayHydration = this.hydrateReplay();
|
|
1309
1358
|
this.transport = transport;
|
|
1310
1359
|
this.initialConfig = initialConfig;
|
|
1311
1360
|
this.hasInitialConfig = "initialConfig" in options;
|
|
1312
1361
|
this.trace = new DataBusTraceReporter(trace);
|
|
1313
1362
|
this.dedupMaxEntries = dedup?.maxEntries ?? 1e3;
|
|
1314
1363
|
this.dedupTtlMs = dedup?.ttlMs ?? 6e4;
|
|
1364
|
+
this.dedupSweepMs = dedup?.sweepMs;
|
|
1315
1365
|
this.dedupEnabled = dedup !== void 0;
|
|
1316
1366
|
if (!Number.isSafeInteger(this.dedupMaxEntries) || this.dedupMaxEntries <= 0) {
|
|
1317
1367
|
throw new TypeError("dedup.maxEntries must be a positive safe integer.");
|
|
@@ -1319,6 +1369,9 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1319
1369
|
if (!Number.isFinite(this.dedupTtlMs) || this.dedupTtlMs <= 0) {
|
|
1320
1370
|
throw new TypeError("dedup.ttlMs must be a positive finite number.");
|
|
1321
1371
|
}
|
|
1372
|
+
if (this.dedupSweepMs !== void 0 && (!Number.isFinite(this.dedupSweepMs) || this.dedupSweepMs <= 0)) {
|
|
1373
|
+
throw new TypeError("dedup.sweepMs must be a positive finite number.");
|
|
1374
|
+
}
|
|
1322
1375
|
this.cluster = new WorkerClusterRuntime({
|
|
1323
1376
|
...clusterOptions,
|
|
1324
1377
|
handlers: {
|
|
@@ -1359,11 +1412,16 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1359
1412
|
onSuspend: () => {
|
|
1360
1413
|
if (!this.stopping) this.trace.event({ type: "lifecycle", action: "suspend" });
|
|
1361
1414
|
this.trace.pause();
|
|
1415
|
+
this.persistenceRetryGeneration += 1;
|
|
1416
|
+
this.stopDedupSweep();
|
|
1417
|
+
this.stopReplayRetentionSweep();
|
|
1362
1418
|
this.suspendTransport();
|
|
1363
1419
|
},
|
|
1364
1420
|
onResume: () => {
|
|
1365
1421
|
this.trace.event({ type: "lifecycle", action: "resume" });
|
|
1366
1422
|
this.trace.start();
|
|
1423
|
+
this.startDedupSweep();
|
|
1424
|
+
this.startReplayRetentionSweep();
|
|
1367
1425
|
this.resumeTransport();
|
|
1368
1426
|
},
|
|
1369
1427
|
onDiagnostic: (event) => {
|
|
@@ -1391,6 +1449,8 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1391
1449
|
this.lastError = null;
|
|
1392
1450
|
this.trace.event({ type: "lifecycle", action: "start" });
|
|
1393
1451
|
this.trace.start();
|
|
1452
|
+
this.startDedupSweep();
|
|
1453
|
+
this.startReplayRetentionSweep();
|
|
1394
1454
|
this.updateStatus("connecting");
|
|
1395
1455
|
this.cluster.start();
|
|
1396
1456
|
const opening = this.openTransport(config, this.pendingStop ?? Promise.resolve(), true);
|
|
@@ -1515,7 +1575,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1515
1575
|
this.topicHandlers.delete(topic);
|
|
1516
1576
|
this.replayBuffers?.delete(topic);
|
|
1517
1577
|
if (this.replayPersistence?.clearTopic) {
|
|
1518
|
-
void this.replayPersistence.clearTopic(topic).catch((error) => this.
|
|
1578
|
+
void this.withPersistenceRetry("clearTopic", () => this.replayPersistence.clearTopic(topic)).catch((error) => this.reportPersistenceError(error));
|
|
1519
1579
|
}
|
|
1520
1580
|
this.cluster.unsubscribe(topic);
|
|
1521
1581
|
}
|
|
@@ -1524,9 +1584,9 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1524
1584
|
this.replayBuffers?.clear();
|
|
1525
1585
|
if (this.replayPersistence?.clear) {
|
|
1526
1586
|
try {
|
|
1527
|
-
await this.replayPersistence.clear();
|
|
1587
|
+
await this.withPersistenceRetry("clear", () => this.replayPersistence.clear());
|
|
1528
1588
|
} catch (error) {
|
|
1529
|
-
this.
|
|
1589
|
+
this.reportPersistenceError(error);
|
|
1530
1590
|
throw error;
|
|
1531
1591
|
}
|
|
1532
1592
|
}
|
|
@@ -1536,9 +1596,9 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1536
1596
|
this.replayBuffers?.delete(topic);
|
|
1537
1597
|
if (this.replayPersistence?.clearTopic) {
|
|
1538
1598
|
try {
|
|
1539
|
-
await this.replayPersistence.clearTopic(topic);
|
|
1599
|
+
await this.withPersistenceRetry("clearTopic", () => this.replayPersistence.clearTopic(topic));
|
|
1540
1600
|
} catch (error) {
|
|
1541
|
-
this.
|
|
1601
|
+
this.reportPersistenceError(error);
|
|
1542
1602
|
throw error;
|
|
1543
1603
|
}
|
|
1544
1604
|
}
|
|
@@ -1548,12 +1608,19 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1548
1608
|
if (!Number.isFinite(timestamp)) throw new TypeError("timestamp must be finite.");
|
|
1549
1609
|
if (this.replayBuffers) {
|
|
1550
1610
|
for (const [topic, messages] of this.replayBuffers) {
|
|
1551
|
-
const kept = messages.filter((message) =>
|
|
1611
|
+
const kept = messages.filter((message) => message.timestamp === void 0 || message.timestamp >= timestamp);
|
|
1552
1612
|
if (kept.length) this.replayBuffers.set(topic, kept);
|
|
1553
1613
|
else this.replayBuffers.delete(topic);
|
|
1554
1614
|
}
|
|
1555
1615
|
}
|
|
1556
|
-
if (this.replayPersistence?.clearBefore)
|
|
1616
|
+
if (this.replayPersistence?.clearBefore) {
|
|
1617
|
+
try {
|
|
1618
|
+
await this.withPersistenceRetry("clearBefore", () => this.replayPersistence.clearBefore(timestamp));
|
|
1619
|
+
} catch (error) {
|
|
1620
|
+
this.reportPersistenceError(error);
|
|
1621
|
+
throw error;
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1557
1624
|
}
|
|
1558
1625
|
/** Return bounded deduplication counters for diagnostics and health checks. */
|
|
1559
1626
|
getDedupStats() {
|
|
@@ -1611,8 +1678,11 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1611
1678
|
async stop() {
|
|
1612
1679
|
if (!this.started) return;
|
|
1613
1680
|
this.stopping = true;
|
|
1681
|
+
this.persistenceRetryGeneration += 1;
|
|
1614
1682
|
this.trace.event({ type: "lifecycle", action: "stop" });
|
|
1615
1683
|
this.trace.stop();
|
|
1684
|
+
this.stopDedupSweep();
|
|
1685
|
+
this.stopReplayRetentionSweep();
|
|
1616
1686
|
this.topicHandlers.clear();
|
|
1617
1687
|
this.replayBuffers?.clear();
|
|
1618
1688
|
this.cluster.stop();
|
|
@@ -1623,7 +1693,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1623
1693
|
else await this.transport.stop();
|
|
1624
1694
|
} finally {
|
|
1625
1695
|
this.transportSubscribedTopics.clear();
|
|
1626
|
-
this.
|
|
1696
|
+
this.resetDedup();
|
|
1627
1697
|
this.started = false;
|
|
1628
1698
|
this.stopping = false;
|
|
1629
1699
|
this.suspended = false;
|
|
@@ -1656,17 +1726,19 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1656
1726
|
}
|
|
1657
1727
|
isDuplicate(message) {
|
|
1658
1728
|
if (!this.dedupEnabled || !message.messageId) return false;
|
|
1659
|
-
const now =
|
|
1729
|
+
const now = this.now();
|
|
1660
1730
|
for (const [id, timestamp] of this.seenMessageIds) {
|
|
1661
1731
|
if (now - timestamp > this.dedupTtlMs) this.seenMessageIds.delete(id);
|
|
1662
1732
|
}
|
|
1663
1733
|
if (this.seenMessageIds.has(message.messageId)) {
|
|
1664
1734
|
this.trace.event({ type: "reliability", operation: "dedup_suppressed", topic: message.topic });
|
|
1665
1735
|
this.dedupSuppressed += 1;
|
|
1736
|
+
this.trace.recordDedupSuppressed();
|
|
1666
1737
|
return true;
|
|
1667
1738
|
}
|
|
1668
1739
|
this.seenMessageIds.set(message.messageId, now);
|
|
1669
1740
|
this.dedupAccepted += 1;
|
|
1741
|
+
this.trace.recordDedupAccepted();
|
|
1670
1742
|
while (this.seenMessageIds.size > this.dedupMaxEntries) {
|
|
1671
1743
|
const oldest = this.seenMessageIds.keys().next().value;
|
|
1672
1744
|
if (oldest === void 0) break;
|
|
@@ -1674,6 +1746,20 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1674
1746
|
}
|
|
1675
1747
|
return false;
|
|
1676
1748
|
}
|
|
1749
|
+
startDedupSweep() {
|
|
1750
|
+
if (this.dedupSweepTimer || !this.dedupEnabled || !this.dedupSweepMs) return;
|
|
1751
|
+
this.dedupSweepTimer = setInterval(() => this.pruneExpiredDedup(), this.dedupSweepMs);
|
|
1752
|
+
}
|
|
1753
|
+
stopDedupSweep() {
|
|
1754
|
+
if (this.dedupSweepTimer) clearInterval(this.dedupSweepTimer);
|
|
1755
|
+
this.dedupSweepTimer = null;
|
|
1756
|
+
}
|
|
1757
|
+
pruneExpiredDedup() {
|
|
1758
|
+
const cutoff = this.now() - this.dedupTtlMs;
|
|
1759
|
+
for (const [id, timestamp] of this.seenMessageIds) {
|
|
1760
|
+
if (timestamp < cutoff) this.seenMessageIds.delete(id);
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1677
1763
|
/** Deliver a message to every local handler registered for its topic,
|
|
1678
1764
|
* plus every handler registered with a wildcard subscription that matches
|
|
1679
1765
|
* (e.g. a handler subscribed to "chat.*" receives "chat.room.1"). */
|
|
@@ -1700,7 +1786,10 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1700
1786
|
buffer.push(storedMessage);
|
|
1701
1787
|
if (buffer.length > this.replayMaxPerTopic) buffer.shift();
|
|
1702
1788
|
if (this.replayPersistence) {
|
|
1703
|
-
void this.replayPersistence.append(storedMessage).catch((error) => this.
|
|
1789
|
+
void this.withPersistenceRetry("append", () => this.replayPersistence.append(storedMessage)).catch((error) => this.reportPersistenceError(error));
|
|
1790
|
+
if (this.replayRetentionMs !== void 0 && this.replayPersistence.clearBefore) {
|
|
1791
|
+
this.scheduleReplayRetentionCleanup(this.now() - this.replayRetentionMs);
|
|
1792
|
+
}
|
|
1704
1793
|
}
|
|
1705
1794
|
}
|
|
1706
1795
|
async hydrateReplay() {
|
|
@@ -1708,7 +1797,10 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1708
1797
|
return;
|
|
1709
1798
|
}
|
|
1710
1799
|
try {
|
|
1711
|
-
|
|
1800
|
+
if (this.replayRetentionMs !== void 0 && this.replayPersistence.clearBefore) {
|
|
1801
|
+
await this.withPersistenceRetry("clearBefore", () => this.replayPersistence.clearBefore(this.now() - this.replayRetentionMs));
|
|
1802
|
+
}
|
|
1803
|
+
for (const message of await this.withPersistenceRetry("load", () => this.replayPersistence.load())) {
|
|
1712
1804
|
let buffer = this.replayBuffers.get(message.topic);
|
|
1713
1805
|
if (!buffer) {
|
|
1714
1806
|
buffer = [];
|
|
@@ -1718,7 +1810,66 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1718
1810
|
if (buffer.length > this.replayMaxPerTopic) buffer.shift();
|
|
1719
1811
|
}
|
|
1720
1812
|
} catch (error) {
|
|
1721
|
-
this.
|
|
1813
|
+
this.reportPersistenceError(error);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
scheduleReplayRetentionCleanup(cutoff) {
|
|
1817
|
+
if (!this.replayPersistence?.clearBefore) return;
|
|
1818
|
+
if (this.replayRetentionCutoff === null || cutoff > this.replayRetentionCutoff) {
|
|
1819
|
+
this.replayRetentionCutoff = cutoff;
|
|
1820
|
+
}
|
|
1821
|
+
if (this.replayRetentionCleanup) return;
|
|
1822
|
+
this.replayRetentionCleanup = (async () => {
|
|
1823
|
+
while (this.replayRetentionCutoff !== null) {
|
|
1824
|
+
const nextCutoff = this.replayRetentionCutoff;
|
|
1825
|
+
this.replayRetentionCutoff = null;
|
|
1826
|
+
try {
|
|
1827
|
+
await this.replayPersistence.clearBefore(nextCutoff);
|
|
1828
|
+
} catch (error) {
|
|
1829
|
+
this.reportPersistenceError(error);
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
})().finally(() => {
|
|
1833
|
+
this.replayRetentionCleanup = null;
|
|
1834
|
+
if (this.replayRetentionCutoff !== null) {
|
|
1835
|
+
this.scheduleReplayRetentionCleanup(this.replayRetentionCutoff);
|
|
1836
|
+
}
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
startReplayRetentionSweep() {
|
|
1840
|
+
if (this.replayRetentionTimer || !this.replayRetentionMs || !this.replayRetentionSweepMs || !this.replayPersistence?.clearBefore) return;
|
|
1841
|
+
this.replayRetentionTimer = setInterval(() => {
|
|
1842
|
+
this.scheduleReplayRetentionCleanup(this.now() - this.replayRetentionMs);
|
|
1843
|
+
}, this.replayRetentionSweepMs);
|
|
1844
|
+
}
|
|
1845
|
+
stopReplayRetentionSweep() {
|
|
1846
|
+
if (this.replayRetentionTimer) clearInterval(this.replayRetentionTimer);
|
|
1847
|
+
this.replayRetentionTimer = null;
|
|
1848
|
+
}
|
|
1849
|
+
async withPersistenceRetry(persistenceOperation, operation) {
|
|
1850
|
+
const generation = this.persistenceRetryGeneration;
|
|
1851
|
+
let attempt = 0;
|
|
1852
|
+
let delay = this.persistenceRetryBackoffMs;
|
|
1853
|
+
while (true) {
|
|
1854
|
+
attempt += 1;
|
|
1855
|
+
try {
|
|
1856
|
+
if (generation !== this.persistenceRetryGeneration) throw new PersistenceRetryCancelledError();
|
|
1857
|
+
return await operation();
|
|
1858
|
+
} catch (error) {
|
|
1859
|
+
if (error instanceof PersistenceRetryCancelledError || generation !== this.persistenceRetryGeneration) {
|
|
1860
|
+
throw new PersistenceRetryCancelledError();
|
|
1861
|
+
}
|
|
1862
|
+
if (attempt >= this.persistenceRetryMaxAttempts) throw error;
|
|
1863
|
+
this.trace.event({
|
|
1864
|
+
type: "reliability",
|
|
1865
|
+
operation: "persistence_retry",
|
|
1866
|
+
persistenceOperation,
|
|
1867
|
+
attempt
|
|
1868
|
+
});
|
|
1869
|
+
if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1870
|
+
if (generation !== this.persistenceRetryGeneration) throw new PersistenceRetryCancelledError();
|
|
1871
|
+
delay = Math.min(delay * 2, 1600);
|
|
1872
|
+
}
|
|
1722
1873
|
}
|
|
1723
1874
|
}
|
|
1724
1875
|
/** Deliver buffered history to a newly-registered handler. For an exact
|
|
@@ -1756,7 +1907,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1756
1907
|
for (const topic of this.cluster.getSnapshot().assignedTopics) this.subscribeTransport(topic);
|
|
1757
1908
|
}
|
|
1758
1909
|
if (status === "error" && this.started && !this.stopping) {
|
|
1759
|
-
const now =
|
|
1910
|
+
const now = this.now();
|
|
1760
1911
|
if (now - this.lastRecoveryAt >= _CrossTabDataBus.RECOVERY_COOLDOWN_MS) {
|
|
1761
1912
|
this.lastRecoveryAt = now;
|
|
1762
1913
|
this.trace.event({ type: "reliability", operation: "transport_recovery", attempt: 1 });
|
|
@@ -1773,6 +1924,11 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1773
1924
|
this.trace.event({ type: "error", source: "transport" });
|
|
1774
1925
|
this.invokeHandlers(this.errorHandlers, (handler) => handler(error), "error handler");
|
|
1775
1926
|
}
|
|
1927
|
+
reportPersistenceError(error) {
|
|
1928
|
+
if (error instanceof PersistenceRetryCancelledError) return;
|
|
1929
|
+
this.trace.event({ type: "reliability", operation: "persistence_cleanup" });
|
|
1930
|
+
this.reportError(error);
|
|
1931
|
+
}
|
|
1776
1932
|
traceSubscription(action, topic) {
|
|
1777
1933
|
this.trace.event({
|
|
1778
1934
|
type: "subscription",
|
|
@@ -1926,74 +2082,165 @@ function createIndexedDbReplayPersistence(options) {
|
|
|
1926
2082
|
throw new TypeError(`maxPerTopic must be a positive safe integer, got ${String(maxPerTopic)}.`);
|
|
1927
2083
|
}
|
|
1928
2084
|
let dbPromise = null;
|
|
2085
|
+
const invalidate = (db) => {
|
|
2086
|
+
if (dbPromise) {
|
|
2087
|
+
void dbPromise.then((current) => {
|
|
2088
|
+
if (current === db) {
|
|
2089
|
+
current.close();
|
|
2090
|
+
dbPromise = null;
|
|
2091
|
+
}
|
|
2092
|
+
}, () => void 0);
|
|
2093
|
+
}
|
|
2094
|
+
};
|
|
2095
|
+
let mutationQueue = Promise.resolve();
|
|
2096
|
+
const serializeMutation = (mutation) => {
|
|
2097
|
+
const next = mutationQueue.then(mutation, mutation);
|
|
2098
|
+
mutationQueue = next.catch(() => void 0);
|
|
2099
|
+
return next;
|
|
2100
|
+
};
|
|
1929
2101
|
const open = () => {
|
|
1930
2102
|
if (dbPromise) return dbPromise;
|
|
1931
|
-
|
|
2103
|
+
const pending = new Promise((resolve, reject) => {
|
|
1932
2104
|
const request = indexedDb.open(dbName, 1);
|
|
1933
2105
|
request.onupgradeneeded = () => request.result.createObjectStore(storeName, { keyPath: "topic" });
|
|
1934
|
-
request.onsuccess = () =>
|
|
2106
|
+
request.onsuccess = () => {
|
|
2107
|
+
const db = request.result;
|
|
2108
|
+
db.onversionchange = () => {
|
|
2109
|
+
db.close();
|
|
2110
|
+
if (dbPromise) dbPromise = null;
|
|
2111
|
+
};
|
|
2112
|
+
resolve(db);
|
|
2113
|
+
};
|
|
1935
2114
|
request.onerror = () => reject(request.error ?? new Error("Failed to open replay database."));
|
|
1936
2115
|
});
|
|
1937
|
-
|
|
2116
|
+
dbPromise = pending;
|
|
2117
|
+
void pending.catch(() => {
|
|
2118
|
+
if (dbPromise === pending) dbPromise = null;
|
|
2119
|
+
});
|
|
2120
|
+
return pending;
|
|
1938
2121
|
};
|
|
1939
2122
|
return {
|
|
1940
2123
|
async load() {
|
|
1941
2124
|
const db = await open();
|
|
1942
2125
|
return new Promise((resolve, reject) => {
|
|
1943
|
-
|
|
2126
|
+
let request;
|
|
2127
|
+
try {
|
|
2128
|
+
request = db.transaction(storeName, "readonly").objectStore(storeName).getAll();
|
|
2129
|
+
} catch (error) {
|
|
2130
|
+
invalidate(db);
|
|
2131
|
+
reject(error);
|
|
2132
|
+
return;
|
|
2133
|
+
}
|
|
1944
2134
|
request.onsuccess = () => resolve(request.result.flatMap((record) => record.messages));
|
|
1945
|
-
request.onerror = () =>
|
|
2135
|
+
request.onerror = () => {
|
|
2136
|
+
invalidate(db);
|
|
2137
|
+
reject(request.error ?? new Error("Failed to load replay history."));
|
|
2138
|
+
};
|
|
1946
2139
|
});
|
|
1947
2140
|
},
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
2141
|
+
append(message) {
|
|
2142
|
+
return serializeMutation(async () => {
|
|
2143
|
+
const db = await open();
|
|
2144
|
+
await new Promise((resolve, reject) => {
|
|
2145
|
+
let transaction;
|
|
2146
|
+
try {
|
|
2147
|
+
transaction = db.transaction(storeName, "readwrite");
|
|
2148
|
+
} catch (error) {
|
|
2149
|
+
invalidate(db);
|
|
2150
|
+
reject(error);
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
const store = transaction.objectStore(storeName);
|
|
2154
|
+
const request = store.get(message.topic);
|
|
2155
|
+
request.onsuccess = () => {
|
|
2156
|
+
const messages = (request.result?.messages ?? []).concat(message).slice(-maxPerTopic);
|
|
2157
|
+
store.put({ topic: message.topic, messages });
|
|
2158
|
+
};
|
|
2159
|
+
request.onerror = () => {
|
|
2160
|
+
invalidate(db);
|
|
2161
|
+
reject(request.error ?? new Error("Failed to read replay history."));
|
|
2162
|
+
};
|
|
2163
|
+
transaction.oncomplete = () => resolve();
|
|
2164
|
+
transaction.onerror = () => {
|
|
2165
|
+
invalidate(db);
|
|
2166
|
+
reject(transaction.error ?? new Error("Failed to persist replay history."));
|
|
2167
|
+
};
|
|
2168
|
+
});
|
|
1961
2169
|
});
|
|
1962
2170
|
},
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
2171
|
+
clear() {
|
|
2172
|
+
return serializeMutation(async () => {
|
|
2173
|
+
const db = await open();
|
|
2174
|
+
await new Promise((resolve, reject) => {
|
|
2175
|
+
let transaction;
|
|
2176
|
+
try {
|
|
2177
|
+
transaction = db.transaction(storeName, "readwrite");
|
|
2178
|
+
} catch (error) {
|
|
2179
|
+
invalidate(db);
|
|
2180
|
+
reject(error);
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
2183
|
+
transaction.objectStore(storeName).clear();
|
|
2184
|
+
transaction.oncomplete = () => resolve();
|
|
2185
|
+
transaction.onerror = () => {
|
|
2186
|
+
invalidate(db);
|
|
2187
|
+
reject(transaction.error ?? new Error("Failed to clear replay history."));
|
|
2188
|
+
};
|
|
2189
|
+
});
|
|
1970
2190
|
});
|
|
1971
2191
|
},
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
2192
|
+
clearTopic(topic) {
|
|
2193
|
+
return serializeMutation(async () => {
|
|
2194
|
+
const db = await open();
|
|
2195
|
+
await new Promise((resolve, reject) => {
|
|
2196
|
+
let transaction;
|
|
2197
|
+
try {
|
|
2198
|
+
transaction = db.transaction(storeName, "readwrite");
|
|
2199
|
+
} catch (error) {
|
|
2200
|
+
invalidate(db);
|
|
2201
|
+
reject(error);
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
transaction.objectStore(storeName).delete(topic);
|
|
2205
|
+
transaction.oncomplete = () => resolve();
|
|
2206
|
+
transaction.onerror = () => {
|
|
2207
|
+
invalidate(db);
|
|
2208
|
+
reject(transaction.error ?? new Error("Failed to clear topic replay history."));
|
|
2209
|
+
};
|
|
2210
|
+
});
|
|
1979
2211
|
});
|
|
1980
2212
|
},
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
2213
|
+
clearBefore(timestamp) {
|
|
2214
|
+
return serializeMutation(async () => {
|
|
2215
|
+
const db = await open();
|
|
2216
|
+
await new Promise((resolve, reject) => {
|
|
2217
|
+
let transaction;
|
|
2218
|
+
try {
|
|
2219
|
+
transaction = db.transaction(storeName, "readwrite");
|
|
2220
|
+
} catch (error) {
|
|
2221
|
+
invalidate(db);
|
|
2222
|
+
reject(error);
|
|
2223
|
+
return;
|
|
1992
2224
|
}
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
2225
|
+
const store = transaction.objectStore(storeName);
|
|
2226
|
+
const request = store.getAll();
|
|
2227
|
+
request.onsuccess = () => {
|
|
2228
|
+
for (const record of request.result) {
|
|
2229
|
+
const messages = record.messages.filter((message) => message.timestamp === void 0 || message.timestamp >= timestamp);
|
|
2230
|
+
if (messages.length === 0) store.delete(record.topic);
|
|
2231
|
+
else if (messages.length !== record.messages.length) store.put({ topic: record.topic, messages });
|
|
2232
|
+
}
|
|
2233
|
+
};
|
|
2234
|
+
request.onerror = () => {
|
|
2235
|
+
invalidate(db);
|
|
2236
|
+
reject(request.error ?? new Error("Failed to read replay history."));
|
|
2237
|
+
};
|
|
2238
|
+
transaction.oncomplete = () => resolve();
|
|
2239
|
+
transaction.onerror = () => {
|
|
2240
|
+
invalidate(db);
|
|
2241
|
+
reject(transaction.error ?? new Error("Failed to prune replay history."));
|
|
2242
|
+
};
|
|
2243
|
+
});
|
|
1997
2244
|
});
|
|
1998
2245
|
}
|
|
1999
2246
|
};
|
|
@@ -2011,11 +2258,13 @@ function parseDataBusPublication(value, fallbackTopic) {
|
|
|
2011
2258
|
if (!topic) return null;
|
|
2012
2259
|
const hasMetadataEnvelope = fallbackTopic !== void 0 && Object.prototype.hasOwnProperty.call(publication, "data") && (typeof publication.messageId === "string" || typeof publication.timestamp === "number");
|
|
2013
2260
|
const data = nested || fallbackTopic === void 0 || hasMetadataEnvelope ? publication.data : value;
|
|
2261
|
+
const messageId = typeof publication.messageId === "string" && publication.messageId.length > 0 ? publication.messageId : void 0;
|
|
2262
|
+
const timestamp = typeof publication.timestamp === "number" && Number.isFinite(publication.timestamp) ? publication.timestamp : void 0;
|
|
2014
2263
|
return {
|
|
2015
2264
|
topic,
|
|
2016
2265
|
data,
|
|
2017
|
-
...
|
|
2018
|
-
...
|
|
2266
|
+
...messageId === void 0 ? {} : { messageId },
|
|
2267
|
+
...timestamp === void 0 ? {} : { timestamp }
|
|
2019
2268
|
};
|
|
2020
2269
|
}
|
|
2021
2270
|
|
|
@@ -2051,7 +2300,9 @@ var WebSocketTransport = class {
|
|
|
2051
2300
|
};
|
|
2052
2301
|
socket.onclose = () => handlers.onStatus("disconnected");
|
|
2053
2302
|
socket.onerror = () => handlers.onStatus("error");
|
|
2054
|
-
socket.onmessage = (event) =>
|
|
2303
|
+
socket.onmessage = (event) => {
|
|
2304
|
+
void this.handleMessage(event.data);
|
|
2305
|
+
};
|
|
2055
2306
|
this.socket = socket;
|
|
2056
2307
|
}
|
|
2057
2308
|
/** Idempotent: re-subscribing an active topic re-sends the frame but does
|
|
@@ -2127,7 +2378,15 @@ var WebSocketTransport = class {
|
|
|
2127
2378
|
/** Parse a server frame. Only objects carrying a string `topic` are
|
|
2128
2379
|
* publications; malformed JSON and unknown shapes are ignored so a chatty
|
|
2129
2380
|
* server cannot crash the message path. */
|
|
2130
|
-
handleMessage(raw) {
|
|
2381
|
+
async handleMessage(raw) {
|
|
2382
|
+
if (typeof Blob !== "undefined" && raw instanceof Blob) {
|
|
2383
|
+
try {
|
|
2384
|
+
await this.handleMessage(await raw.arrayBuffer());
|
|
2385
|
+
} catch (error) {
|
|
2386
|
+
this.handlers?.onError(error);
|
|
2387
|
+
}
|
|
2388
|
+
return;
|
|
2389
|
+
}
|
|
2131
2390
|
let parsed;
|
|
2132
2391
|
if (raw instanceof ArrayBuffer) {
|
|
2133
2392
|
const bytes = new Uint8Array(raw);
|