cross-tab-worker-databus 0.11.0 → 0.20.7

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +216 -0
  2. package/dist/centrifuge.js +1 -1
  3. package/dist/centrifuge.shared.worker.js +4 -2
  4. package/dist/centrifuge.shared.worker.js.map +2 -2
  5. package/dist/centrifuge.worker.js +4 -2
  6. package/dist/centrifuge.worker.js.map +2 -2
  7. package/dist/{chunk-NX76TAV3.js → chunk-LPS4XOK4.js} +152 -20
  8. package/dist/{chunk-NX76TAV3.js.map → chunk-LPS4XOK4.js.map} +2 -2
  9. package/dist/cjs/centrifuge.cjs +151 -19
  10. package/dist/cjs/centrifuge.cjs.map +2 -2
  11. package/dist/cjs/hooks.cjs +4 -1
  12. package/dist/cjs/hooks.cjs.map +2 -2
  13. package/dist/cjs/index.cjs +301 -68
  14. package/dist/cjs/index.cjs.map +2 -2
  15. package/dist/cjs/vue.cjs +7 -1
  16. package/dist/cjs/vue.cjs.map +2 -2
  17. package/dist/core/data-bus.d.ts +32 -0
  18. package/dist/core/data-bus.d.ts.map +1 -1
  19. package/dist/core/publication.d.ts.map +1 -1
  20. package/dist/core/replay-persistence.d.ts.map +1 -1
  21. package/dist/core/trace.d.ts +4 -1
  22. package/dist/core/trace.d.ts.map +1 -1
  23. package/dist/hooks.d.ts.map +1 -1
  24. package/dist/hooks.js +4 -1
  25. package/dist/hooks.js.map +2 -2
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +151 -50
  29. package/dist/index.js.map +2 -2
  30. package/dist/vue.d.ts.map +1 -1
  31. package/dist/vue.js +7 -1
  32. package/dist/vue.js.map +2 -2
  33. package/dist/websocket.d.ts.map +1 -1
  34. package/docs/README.md +1 -0
  35. package/docs/api.md +10 -3
  36. package/docs/configuration.md +21 -1
  37. package/docs/release-checklist.md +22 -0
  38. package/docs/roadmap.md +136 -3
  39. package/docs/zh/README.md +1 -0
  40. package/docs/zh/api.md +10 -3
  41. package/docs/zh/configuration.md +21 -1
  42. package/docs/zh/release-checklist.md +22 -0
  43. package/docs/zh/roadmap.md +137 -2
  44. package/package.json +3 -1
@@ -1068,7 +1068,7 @@ var DataBusTraceReporter = class {
1068
1068
  latencySumMs = 0;
1069
1069
  dedupAccepted = 0;
1070
1070
  dedupSuppressed = 0;
1071
- constructor(options, now = Date.now) {
1071
+ constructor(options, now = options?.now ?? Date.now) {
1072
1072
  this.enabled = options?.enabled ?? false;
1073
1073
  this.mode = options?.mode ?? "all";
1074
1074
  this.metricsIntervalMs = normalizeInterval(options?.metricsIntervalMs);
@@ -1230,6 +1230,12 @@ function roundMs(value) {
1230
1230
  // src/core/data-bus.ts
1231
1231
  var PUBLICATION_EVENT = "DATABUS_PUBLICATION";
1232
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
+ };
1233
1239
  var CrossTabDataBus = class _CrossTabDataBus {
1234
1240
  transport;
1235
1241
  cluster;
@@ -1246,13 +1252,25 @@ var CrossTabDataBus = class _CrossTabDataBus {
1246
1252
  replayMaxPerTopic;
1247
1253
  replayPersistence;
1248
1254
  replayRetentionMs;
1255
+ replayRetentionSweepMs;
1256
+ persistenceRetryMaxAttempts;
1257
+ persistenceRetryBackoffMs;
1258
+ persistenceRetryGeneration = 0;
1249
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;
1250
1265
  initialConfig;
1251
1266
  hasInitialConfig;
1252
1267
  trace;
1253
1268
  dedupMaxEntries;
1254
1269
  dedupTtlMs;
1270
+ dedupSweepMs;
1271
+ dedupSweepTimer = null;
1255
1272
  dedupEnabled;
1273
+ now;
1256
1274
  seenMessageIds = /* @__PURE__ */ new Map();
1257
1275
  dedupSuppressed = 0;
1258
1276
  dedupAccepted = 0;
@@ -1297,14 +1315,28 @@ var CrossTabDataBus = class _CrossTabDataBus {
1297
1315
  if (this.replayRetentionMs !== void 0 && (!Number.isFinite(this.replayRetentionMs) || this.replayRetentionMs <= 0)) {
1298
1316
  throw new TypeError("replay.retentionMs must be a positive finite number.");
1299
1317
  }
1300
- this.replayHydration = this.hydrateReplay();
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
+ }
1301
1330
  const { autoStart, initialConfig, trace, transport, dedup, ...clusterOptions } = options;
1331
+ this.now = dedup?.now ?? Date.now;
1332
+ this.replayHydration = this.hydrateReplay();
1302
1333
  this.transport = transport;
1303
1334
  this.initialConfig = initialConfig;
1304
1335
  this.hasInitialConfig = "initialConfig" in options;
1305
1336
  this.trace = new DataBusTraceReporter(trace);
1306
1337
  this.dedupMaxEntries = dedup?.maxEntries ?? 1e3;
1307
1338
  this.dedupTtlMs = dedup?.ttlMs ?? 6e4;
1339
+ this.dedupSweepMs = dedup?.sweepMs;
1308
1340
  this.dedupEnabled = dedup !== void 0;
1309
1341
  if (!Number.isSafeInteger(this.dedupMaxEntries) || this.dedupMaxEntries <= 0) {
1310
1342
  throw new TypeError("dedup.maxEntries must be a positive safe integer.");
@@ -1312,6 +1344,9 @@ var CrossTabDataBus = class _CrossTabDataBus {
1312
1344
  if (!Number.isFinite(this.dedupTtlMs) || this.dedupTtlMs <= 0) {
1313
1345
  throw new TypeError("dedup.ttlMs must be a positive finite number.");
1314
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
+ }
1315
1350
  this.cluster = new WorkerClusterRuntime({
1316
1351
  ...clusterOptions,
1317
1352
  handlers: {
@@ -1352,11 +1387,16 @@ var CrossTabDataBus = class _CrossTabDataBus {
1352
1387
  onSuspend: () => {
1353
1388
  if (!this.stopping) this.trace.event({ type: "lifecycle", action: "suspend" });
1354
1389
  this.trace.pause();
1390
+ this.persistenceRetryGeneration += 1;
1391
+ this.stopDedupSweep();
1392
+ this.stopReplayRetentionSweep();
1355
1393
  this.suspendTransport();
1356
1394
  },
1357
1395
  onResume: () => {
1358
1396
  this.trace.event({ type: "lifecycle", action: "resume" });
1359
1397
  this.trace.start();
1398
+ this.startDedupSweep();
1399
+ this.startReplayRetentionSweep();
1360
1400
  this.resumeTransport();
1361
1401
  },
1362
1402
  onDiagnostic: (event) => {
@@ -1384,6 +1424,8 @@ var CrossTabDataBus = class _CrossTabDataBus {
1384
1424
  this.lastError = null;
1385
1425
  this.trace.event({ type: "lifecycle", action: "start" });
1386
1426
  this.trace.start();
1427
+ this.startDedupSweep();
1428
+ this.startReplayRetentionSweep();
1387
1429
  this.updateStatus("connecting");
1388
1430
  this.cluster.start();
1389
1431
  const opening = this.openTransport(config, this.pendingStop ?? Promise.resolve(), true);
@@ -1508,7 +1550,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
1508
1550
  this.topicHandlers.delete(topic);
1509
1551
  this.replayBuffers?.delete(topic);
1510
1552
  if (this.replayPersistence?.clearTopic) {
1511
- void this.replayPersistence.clearTopic(topic).catch((error) => this.reportError(error));
1553
+ void this.withPersistenceRetry("clearTopic", () => this.replayPersistence.clearTopic(topic)).catch((error) => this.reportPersistenceError(error));
1512
1554
  }
1513
1555
  this.cluster.unsubscribe(topic);
1514
1556
  }
@@ -1517,9 +1559,9 @@ var CrossTabDataBus = class _CrossTabDataBus {
1517
1559
  this.replayBuffers?.clear();
1518
1560
  if (this.replayPersistence?.clear) {
1519
1561
  try {
1520
- await this.replayPersistence.clear();
1562
+ await this.withPersistenceRetry("clear", () => this.replayPersistence.clear());
1521
1563
  } catch (error) {
1522
- this.reportError(error);
1564
+ this.reportPersistenceError(error);
1523
1565
  throw error;
1524
1566
  }
1525
1567
  }
@@ -1529,9 +1571,9 @@ var CrossTabDataBus = class _CrossTabDataBus {
1529
1571
  this.replayBuffers?.delete(topic);
1530
1572
  if (this.replayPersistence?.clearTopic) {
1531
1573
  try {
1532
- await this.replayPersistence.clearTopic(topic);
1574
+ await this.withPersistenceRetry("clearTopic", () => this.replayPersistence.clearTopic(topic));
1533
1575
  } catch (error) {
1534
- this.reportError(error);
1576
+ this.reportPersistenceError(error);
1535
1577
  throw error;
1536
1578
  }
1537
1579
  }
@@ -1541,12 +1583,19 @@ var CrossTabDataBus = class _CrossTabDataBus {
1541
1583
  if (!Number.isFinite(timestamp)) throw new TypeError("timestamp must be finite.");
1542
1584
  if (this.replayBuffers) {
1543
1585
  for (const [topic, messages] of this.replayBuffers) {
1544
- const kept = messages.filter((message) => (message.timestamp ?? 0) >= timestamp);
1586
+ const kept = messages.filter((message) => message.timestamp === void 0 || message.timestamp >= timestamp);
1545
1587
  if (kept.length) this.replayBuffers.set(topic, kept);
1546
1588
  else this.replayBuffers.delete(topic);
1547
1589
  }
1548
1590
  }
1549
- if (this.replayPersistence?.clearBefore) await this.replayPersistence.clearBefore(timestamp);
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
+ }
1550
1599
  }
1551
1600
  /** Return bounded deduplication counters for diagnostics and health checks. */
1552
1601
  getDedupStats() {
@@ -1604,8 +1653,11 @@ var CrossTabDataBus = class _CrossTabDataBus {
1604
1653
  async stop() {
1605
1654
  if (!this.started) return;
1606
1655
  this.stopping = true;
1656
+ this.persistenceRetryGeneration += 1;
1607
1657
  this.trace.event({ type: "lifecycle", action: "stop" });
1608
1658
  this.trace.stop();
1659
+ this.stopDedupSweep();
1660
+ this.stopReplayRetentionSweep();
1609
1661
  this.topicHandlers.clear();
1610
1662
  this.replayBuffers?.clear();
1611
1663
  this.cluster.stop();
@@ -1616,7 +1668,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
1616
1668
  else await this.transport.stop();
1617
1669
  } finally {
1618
1670
  this.transportSubscribedTopics.clear();
1619
- this.seenMessageIds.clear();
1671
+ this.resetDedup();
1620
1672
  this.started = false;
1621
1673
  this.stopping = false;
1622
1674
  this.suspended = false;
@@ -1649,7 +1701,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
1649
1701
  }
1650
1702
  isDuplicate(message) {
1651
1703
  if (!this.dedupEnabled || !message.messageId) return false;
1652
- const now = Date.now();
1704
+ const now = this.now();
1653
1705
  for (const [id, timestamp] of this.seenMessageIds) {
1654
1706
  if (now - timestamp > this.dedupTtlMs) this.seenMessageIds.delete(id);
1655
1707
  }
@@ -1669,6 +1721,20 @@ var CrossTabDataBus = class _CrossTabDataBus {
1669
1721
  }
1670
1722
  return false;
1671
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
+ }
1672
1738
  /** Deliver a message to every local handler registered for its topic,
1673
1739
  * plus every handler registered with a wildcard subscription that matches
1674
1740
  * (e.g. a handler subscribed to "chat.*" receives "chat.room.1"). */
@@ -1695,9 +1761,9 @@ var CrossTabDataBus = class _CrossTabDataBus {
1695
1761
  buffer.push(storedMessage);
1696
1762
  if (buffer.length > this.replayMaxPerTopic) buffer.shift();
1697
1763
  if (this.replayPersistence) {
1698
- void this.replayPersistence.append(storedMessage).catch((error) => this.reportError(error));
1764
+ void this.withPersistenceRetry("append", () => this.replayPersistence.append(storedMessage)).catch((error) => this.reportPersistenceError(error));
1699
1765
  if (this.replayRetentionMs !== void 0 && this.replayPersistence.clearBefore) {
1700
- void this.replayPersistence.clearBefore(Date.now() - this.replayRetentionMs).catch((error) => this.reportError(error));
1766
+ this.scheduleReplayRetentionCleanup(this.now() - this.replayRetentionMs);
1701
1767
  }
1702
1768
  }
1703
1769
  }
@@ -1707,9 +1773,9 @@ var CrossTabDataBus = class _CrossTabDataBus {
1707
1773
  }
1708
1774
  try {
1709
1775
  if (this.replayRetentionMs !== void 0 && this.replayPersistence.clearBefore) {
1710
- await this.replayPersistence.clearBefore(Date.now() - this.replayRetentionMs);
1776
+ await this.withPersistenceRetry("clearBefore", () => this.replayPersistence.clearBefore(this.now() - this.replayRetentionMs));
1711
1777
  }
1712
- for (const message of await this.replayPersistence.load()) {
1778
+ for (const message of await this.withPersistenceRetry("load", () => this.replayPersistence.load())) {
1713
1779
  let buffer = this.replayBuffers.get(message.topic);
1714
1780
  if (!buffer) {
1715
1781
  buffer = [];
@@ -1719,7 +1785,66 @@ var CrossTabDataBus = class _CrossTabDataBus {
1719
1785
  if (buffer.length > this.replayMaxPerTopic) buffer.shift();
1720
1786
  }
1721
1787
  } catch (error) {
1722
- this.reportError(error);
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
+ }
1723
1848
  }
1724
1849
  }
1725
1850
  /** Deliver buffered history to a newly-registered handler. For an exact
@@ -1757,7 +1882,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
1757
1882
  for (const topic of this.cluster.getSnapshot().assignedTopics) this.subscribeTransport(topic);
1758
1883
  }
1759
1884
  if (status === "error" && this.started && !this.stopping) {
1760
- const now = Date.now();
1885
+ const now = this.now();
1761
1886
  if (now - this.lastRecoveryAt >= _CrossTabDataBus.RECOVERY_COOLDOWN_MS) {
1762
1887
  this.lastRecoveryAt = now;
1763
1888
  this.trace.event({ type: "reliability", operation: "transport_recovery", attempt: 1 });
@@ -1774,6 +1899,11 @@ var CrossTabDataBus = class _CrossTabDataBus {
1774
1899
  this.trace.event({ type: "error", source: "transport" });
1775
1900
  this.invokeHandlers(this.errorHandlers, (handler) => handler(error), "error handler");
1776
1901
  }
1902
+ reportPersistenceError(error) {
1903
+ if (error instanceof PersistenceRetryCancelledError) return;
1904
+ this.trace.event({ type: "reliability", operation: "persistence_cleanup" });
1905
+ this.reportError(error);
1906
+ }
1777
1907
  traceSubscription(action, topic) {
1778
1908
  this.trace.event({
1779
1909
  type: "subscription",
@@ -1931,11 +2061,13 @@ function parseDataBusPublication(value, fallbackTopic) {
1931
2061
  if (!topic) return null;
1932
2062
  const hasMetadataEnvelope = fallbackTopic !== void 0 && Object.prototype.hasOwnProperty.call(publication, "data") && (typeof publication.messageId === "string" || typeof publication.timestamp === "number");
1933
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;
1934
2066
  return {
1935
2067
  topic,
1936
2068
  data,
1937
- ...typeof publication.messageId === "string" ? { messageId: publication.messageId } : {},
1938
- ...typeof publication.timestamp === "number" ? { timestamp: publication.timestamp } : {}
2069
+ ...messageId === void 0 ? {} : { messageId },
2070
+ ...timestamp === void 0 ? {} : { timestamp }
1939
2071
  };
1940
2072
  }
1941
2073