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
@@ -1052,7 +1052,7 @@ var DataBusTraceReporter = class {
1052
1052
  latencySumMs = 0;
1053
1053
  dedupAccepted = 0;
1054
1054
  dedupSuppressed = 0;
1055
- constructor(options, now = Date.now) {
1055
+ constructor(options, now = options?.now ?? Date.now) {
1056
1056
  this.enabled = options?.enabled ?? false;
1057
1057
  this.mode = options?.mode ?? "all";
1058
1058
  this.metricsIntervalMs = normalizeInterval(options?.metricsIntervalMs);
@@ -1214,6 +1214,12 @@ function roundMs(value) {
1214
1214
  // src/core/data-bus.ts
1215
1215
  var PUBLICATION_EVENT = "DATABUS_PUBLICATION";
1216
1216
  var DEFAULT_REPLAY_MAX_PER_TOPIC = 100;
1217
+ var PersistenceRetryCancelledError = class extends Error {
1218
+ constructor() {
1219
+ super("Persistence retry cancelled by lifecycle transition.");
1220
+ this.name = "PersistenceRetryCancelledError";
1221
+ }
1222
+ };
1217
1223
  var CrossTabDataBus = class _CrossTabDataBus {
1218
1224
  transport;
1219
1225
  cluster;
@@ -1230,13 +1236,25 @@ var CrossTabDataBus = class _CrossTabDataBus {
1230
1236
  replayMaxPerTopic;
1231
1237
  replayPersistence;
1232
1238
  replayRetentionMs;
1239
+ replayRetentionSweepMs;
1240
+ persistenceRetryMaxAttempts;
1241
+ persistenceRetryBackoffMs;
1242
+ persistenceRetryGeneration = 0;
1233
1243
  replayHydration;
1244
+ // Retention cleanup is coalesced so a burst of publications does not issue
1245
+ // one IndexedDB read/write transaction per message. The newest cutoff wins.
1246
+ replayRetentionCleanup = null;
1247
+ replayRetentionCutoff = null;
1248
+ replayRetentionTimer = null;
1234
1249
  initialConfig;
1235
1250
  hasInitialConfig;
1236
1251
  trace;
1237
1252
  dedupMaxEntries;
1238
1253
  dedupTtlMs;
1254
+ dedupSweepMs;
1255
+ dedupSweepTimer = null;
1239
1256
  dedupEnabled;
1257
+ now;
1240
1258
  seenMessageIds = /* @__PURE__ */ new Map();
1241
1259
  dedupSuppressed = 0;
1242
1260
  dedupAccepted = 0;
@@ -1281,14 +1299,28 @@ var CrossTabDataBus = class _CrossTabDataBus {
1281
1299
  if (this.replayRetentionMs !== void 0 && (!Number.isFinite(this.replayRetentionMs) || this.replayRetentionMs <= 0)) {
1282
1300
  throw new TypeError("replay.retentionMs must be a positive finite number.");
1283
1301
  }
1284
- this.replayHydration = this.hydrateReplay();
1302
+ this.replayRetentionSweepMs = replay?.retentionSweepMs;
1303
+ if (this.replayRetentionSweepMs !== void 0 && (!Number.isFinite(this.replayRetentionSweepMs) || this.replayRetentionSweepMs <= 0)) {
1304
+ throw new TypeError("replay.retentionSweepMs must be a positive finite number.");
1305
+ }
1306
+ this.persistenceRetryMaxAttempts = replay?.persistenceRetry?.maxAttempts ?? 1;
1307
+ this.persistenceRetryBackoffMs = replay?.persistenceRetry?.backoffMs ?? 50;
1308
+ if (!Number.isSafeInteger(this.persistenceRetryMaxAttempts) || this.persistenceRetryMaxAttempts <= 0) {
1309
+ throw new TypeError("replay.persistenceRetry.maxAttempts must be a positive safe integer.");
1310
+ }
1311
+ if (!Number.isFinite(this.persistenceRetryBackoffMs) || this.persistenceRetryBackoffMs < 0) {
1312
+ throw new TypeError("replay.persistenceRetry.backoffMs must be a non-negative finite number.");
1313
+ }
1285
1314
  const { autoStart, initialConfig, trace, transport, dedup, ...clusterOptions } = options;
1315
+ this.now = dedup?.now ?? Date.now;
1316
+ this.replayHydration = this.hydrateReplay();
1286
1317
  this.transport = transport;
1287
1318
  this.initialConfig = initialConfig;
1288
1319
  this.hasInitialConfig = "initialConfig" in options;
1289
1320
  this.trace = new DataBusTraceReporter(trace);
1290
1321
  this.dedupMaxEntries = dedup?.maxEntries ?? 1e3;
1291
1322
  this.dedupTtlMs = dedup?.ttlMs ?? 6e4;
1323
+ this.dedupSweepMs = dedup?.sweepMs;
1292
1324
  this.dedupEnabled = dedup !== void 0;
1293
1325
  if (!Number.isSafeInteger(this.dedupMaxEntries) || this.dedupMaxEntries <= 0) {
1294
1326
  throw new TypeError("dedup.maxEntries must be a positive safe integer.");
@@ -1296,6 +1328,9 @@ var CrossTabDataBus = class _CrossTabDataBus {
1296
1328
  if (!Number.isFinite(this.dedupTtlMs) || this.dedupTtlMs <= 0) {
1297
1329
  throw new TypeError("dedup.ttlMs must be a positive finite number.");
1298
1330
  }
1331
+ if (this.dedupSweepMs !== void 0 && (!Number.isFinite(this.dedupSweepMs) || this.dedupSweepMs <= 0)) {
1332
+ throw new TypeError("dedup.sweepMs must be a positive finite number.");
1333
+ }
1299
1334
  this.cluster = new WorkerClusterRuntime({
1300
1335
  ...clusterOptions,
1301
1336
  handlers: {
@@ -1336,11 +1371,16 @@ var CrossTabDataBus = class _CrossTabDataBus {
1336
1371
  onSuspend: () => {
1337
1372
  if (!this.stopping) this.trace.event({ type: "lifecycle", action: "suspend" });
1338
1373
  this.trace.pause();
1374
+ this.persistenceRetryGeneration += 1;
1375
+ this.stopDedupSweep();
1376
+ this.stopReplayRetentionSweep();
1339
1377
  this.suspendTransport();
1340
1378
  },
1341
1379
  onResume: () => {
1342
1380
  this.trace.event({ type: "lifecycle", action: "resume" });
1343
1381
  this.trace.start();
1382
+ this.startDedupSweep();
1383
+ this.startReplayRetentionSweep();
1344
1384
  this.resumeTransport();
1345
1385
  },
1346
1386
  onDiagnostic: (event) => {
@@ -1368,6 +1408,8 @@ var CrossTabDataBus = class _CrossTabDataBus {
1368
1408
  this.lastError = null;
1369
1409
  this.trace.event({ type: "lifecycle", action: "start" });
1370
1410
  this.trace.start();
1411
+ this.startDedupSweep();
1412
+ this.startReplayRetentionSweep();
1371
1413
  this.updateStatus("connecting");
1372
1414
  this.cluster.start();
1373
1415
  const opening = this.openTransport(config, this.pendingStop ?? Promise.resolve(), true);
@@ -1492,7 +1534,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
1492
1534
  this.topicHandlers.delete(topic);
1493
1535
  this.replayBuffers?.delete(topic);
1494
1536
  if (this.replayPersistence?.clearTopic) {
1495
- void this.replayPersistence.clearTopic(topic).catch((error) => this.reportError(error));
1537
+ void this.withPersistenceRetry("clearTopic", () => this.replayPersistence.clearTopic(topic)).catch((error) => this.reportPersistenceError(error));
1496
1538
  }
1497
1539
  this.cluster.unsubscribe(topic);
1498
1540
  }
@@ -1501,9 +1543,9 @@ var CrossTabDataBus = class _CrossTabDataBus {
1501
1543
  this.replayBuffers?.clear();
1502
1544
  if (this.replayPersistence?.clear) {
1503
1545
  try {
1504
- await this.replayPersistence.clear();
1546
+ await this.withPersistenceRetry("clear", () => this.replayPersistence.clear());
1505
1547
  } catch (error) {
1506
- this.reportError(error);
1548
+ this.reportPersistenceError(error);
1507
1549
  throw error;
1508
1550
  }
1509
1551
  }
@@ -1513,9 +1555,9 @@ var CrossTabDataBus = class _CrossTabDataBus {
1513
1555
  this.replayBuffers?.delete(topic);
1514
1556
  if (this.replayPersistence?.clearTopic) {
1515
1557
  try {
1516
- await this.replayPersistence.clearTopic(topic);
1558
+ await this.withPersistenceRetry("clearTopic", () => this.replayPersistence.clearTopic(topic));
1517
1559
  } catch (error) {
1518
- this.reportError(error);
1560
+ this.reportPersistenceError(error);
1519
1561
  throw error;
1520
1562
  }
1521
1563
  }
@@ -1525,12 +1567,19 @@ var CrossTabDataBus = class _CrossTabDataBus {
1525
1567
  if (!Number.isFinite(timestamp)) throw new TypeError("timestamp must be finite.");
1526
1568
  if (this.replayBuffers) {
1527
1569
  for (const [topic, messages] of this.replayBuffers) {
1528
- const kept = messages.filter((message) => (message.timestamp ?? 0) >= timestamp);
1570
+ const kept = messages.filter((message) => message.timestamp === void 0 || message.timestamp >= timestamp);
1529
1571
  if (kept.length) this.replayBuffers.set(topic, kept);
1530
1572
  else this.replayBuffers.delete(topic);
1531
1573
  }
1532
1574
  }
1533
- if (this.replayPersistence?.clearBefore) await this.replayPersistence.clearBefore(timestamp);
1575
+ if (this.replayPersistence?.clearBefore) {
1576
+ try {
1577
+ await this.withPersistenceRetry("clearBefore", () => this.replayPersistence.clearBefore(timestamp));
1578
+ } catch (error) {
1579
+ this.reportPersistenceError(error);
1580
+ throw error;
1581
+ }
1582
+ }
1534
1583
  }
1535
1584
  /** Return bounded deduplication counters for diagnostics and health checks. */
1536
1585
  getDedupStats() {
@@ -1588,8 +1637,11 @@ var CrossTabDataBus = class _CrossTabDataBus {
1588
1637
  async stop() {
1589
1638
  if (!this.started) return;
1590
1639
  this.stopping = true;
1640
+ this.persistenceRetryGeneration += 1;
1591
1641
  this.trace.event({ type: "lifecycle", action: "stop" });
1592
1642
  this.trace.stop();
1643
+ this.stopDedupSweep();
1644
+ this.stopReplayRetentionSweep();
1593
1645
  this.topicHandlers.clear();
1594
1646
  this.replayBuffers?.clear();
1595
1647
  this.cluster.stop();
@@ -1600,7 +1652,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
1600
1652
  else await this.transport.stop();
1601
1653
  } finally {
1602
1654
  this.transportSubscribedTopics.clear();
1603
- this.seenMessageIds.clear();
1655
+ this.resetDedup();
1604
1656
  this.started = false;
1605
1657
  this.stopping = false;
1606
1658
  this.suspended = false;
@@ -1633,7 +1685,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
1633
1685
  }
1634
1686
  isDuplicate(message) {
1635
1687
  if (!this.dedupEnabled || !message.messageId) return false;
1636
- const now = Date.now();
1688
+ const now = this.now();
1637
1689
  for (const [id, timestamp] of this.seenMessageIds) {
1638
1690
  if (now - timestamp > this.dedupTtlMs) this.seenMessageIds.delete(id);
1639
1691
  }
@@ -1653,6 +1705,20 @@ var CrossTabDataBus = class _CrossTabDataBus {
1653
1705
  }
1654
1706
  return false;
1655
1707
  }
1708
+ startDedupSweep() {
1709
+ if (this.dedupSweepTimer || !this.dedupEnabled || !this.dedupSweepMs) return;
1710
+ this.dedupSweepTimer = setInterval(() => this.pruneExpiredDedup(), this.dedupSweepMs);
1711
+ }
1712
+ stopDedupSweep() {
1713
+ if (this.dedupSweepTimer) clearInterval(this.dedupSweepTimer);
1714
+ this.dedupSweepTimer = null;
1715
+ }
1716
+ pruneExpiredDedup() {
1717
+ const cutoff = this.now() - this.dedupTtlMs;
1718
+ for (const [id, timestamp] of this.seenMessageIds) {
1719
+ if (timestamp < cutoff) this.seenMessageIds.delete(id);
1720
+ }
1721
+ }
1656
1722
  /** Deliver a message to every local handler registered for its topic,
1657
1723
  * plus every handler registered with a wildcard subscription that matches
1658
1724
  * (e.g. a handler subscribed to "chat.*" receives "chat.room.1"). */
@@ -1679,9 +1745,9 @@ var CrossTabDataBus = class _CrossTabDataBus {
1679
1745
  buffer.push(storedMessage);
1680
1746
  if (buffer.length > this.replayMaxPerTopic) buffer.shift();
1681
1747
  if (this.replayPersistence) {
1682
- void this.replayPersistence.append(storedMessage).catch((error) => this.reportError(error));
1748
+ void this.withPersistenceRetry("append", () => this.replayPersistence.append(storedMessage)).catch((error) => this.reportPersistenceError(error));
1683
1749
  if (this.replayRetentionMs !== void 0 && this.replayPersistence.clearBefore) {
1684
- void this.replayPersistence.clearBefore(Date.now() - this.replayRetentionMs).catch((error) => this.reportError(error));
1750
+ this.scheduleReplayRetentionCleanup(this.now() - this.replayRetentionMs);
1685
1751
  }
1686
1752
  }
1687
1753
  }
@@ -1691,9 +1757,9 @@ var CrossTabDataBus = class _CrossTabDataBus {
1691
1757
  }
1692
1758
  try {
1693
1759
  if (this.replayRetentionMs !== void 0 && this.replayPersistence.clearBefore) {
1694
- await this.replayPersistence.clearBefore(Date.now() - this.replayRetentionMs);
1760
+ await this.withPersistenceRetry("clearBefore", () => this.replayPersistence.clearBefore(this.now() - this.replayRetentionMs));
1695
1761
  }
1696
- for (const message of await this.replayPersistence.load()) {
1762
+ for (const message of await this.withPersistenceRetry("load", () => this.replayPersistence.load())) {
1697
1763
  let buffer = this.replayBuffers.get(message.topic);
1698
1764
  if (!buffer) {
1699
1765
  buffer = [];
@@ -1703,7 +1769,66 @@ var CrossTabDataBus = class _CrossTabDataBus {
1703
1769
  if (buffer.length > this.replayMaxPerTopic) buffer.shift();
1704
1770
  }
1705
1771
  } catch (error) {
1706
- this.reportError(error);
1772
+ this.reportPersistenceError(error);
1773
+ }
1774
+ }
1775
+ scheduleReplayRetentionCleanup(cutoff) {
1776
+ if (!this.replayPersistence?.clearBefore) return;
1777
+ if (this.replayRetentionCutoff === null || cutoff > this.replayRetentionCutoff) {
1778
+ this.replayRetentionCutoff = cutoff;
1779
+ }
1780
+ if (this.replayRetentionCleanup) return;
1781
+ this.replayRetentionCleanup = (async () => {
1782
+ while (this.replayRetentionCutoff !== null) {
1783
+ const nextCutoff = this.replayRetentionCutoff;
1784
+ this.replayRetentionCutoff = null;
1785
+ try {
1786
+ await this.replayPersistence.clearBefore(nextCutoff);
1787
+ } catch (error) {
1788
+ this.reportPersistenceError(error);
1789
+ }
1790
+ }
1791
+ })().finally(() => {
1792
+ this.replayRetentionCleanup = null;
1793
+ if (this.replayRetentionCutoff !== null) {
1794
+ this.scheduleReplayRetentionCleanup(this.replayRetentionCutoff);
1795
+ }
1796
+ });
1797
+ }
1798
+ startReplayRetentionSweep() {
1799
+ if (this.replayRetentionTimer || !this.replayRetentionMs || !this.replayRetentionSweepMs || !this.replayPersistence?.clearBefore) return;
1800
+ this.replayRetentionTimer = setInterval(() => {
1801
+ this.scheduleReplayRetentionCleanup(this.now() - this.replayRetentionMs);
1802
+ }, this.replayRetentionSweepMs);
1803
+ }
1804
+ stopReplayRetentionSweep() {
1805
+ if (this.replayRetentionTimer) clearInterval(this.replayRetentionTimer);
1806
+ this.replayRetentionTimer = null;
1807
+ }
1808
+ async withPersistenceRetry(persistenceOperation, operation) {
1809
+ const generation = this.persistenceRetryGeneration;
1810
+ let attempt = 0;
1811
+ let delay = this.persistenceRetryBackoffMs;
1812
+ while (true) {
1813
+ attempt += 1;
1814
+ try {
1815
+ if (generation !== this.persistenceRetryGeneration) throw new PersistenceRetryCancelledError();
1816
+ return await operation();
1817
+ } catch (error) {
1818
+ if (error instanceof PersistenceRetryCancelledError || generation !== this.persistenceRetryGeneration) {
1819
+ throw new PersistenceRetryCancelledError();
1820
+ }
1821
+ if (attempt >= this.persistenceRetryMaxAttempts) throw error;
1822
+ this.trace.event({
1823
+ type: "reliability",
1824
+ operation: "persistence_retry",
1825
+ persistenceOperation,
1826
+ attempt
1827
+ });
1828
+ if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay));
1829
+ if (generation !== this.persistenceRetryGeneration) throw new PersistenceRetryCancelledError();
1830
+ delay = Math.min(delay * 2, 1600);
1831
+ }
1707
1832
  }
1708
1833
  }
1709
1834
  /** Deliver buffered history to a newly-registered handler. For an exact
@@ -1741,7 +1866,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
1741
1866
  for (const topic of this.cluster.getSnapshot().assignedTopics) this.subscribeTransport(topic);
1742
1867
  }
1743
1868
  if (status === "error" && this.started && !this.stopping) {
1744
- const now = Date.now();
1869
+ const now = this.now();
1745
1870
  if (now - this.lastRecoveryAt >= _CrossTabDataBus.RECOVERY_COOLDOWN_MS) {
1746
1871
  this.lastRecoveryAt = now;
1747
1872
  this.trace.event({ type: "reliability", operation: "transport_recovery", attempt: 1 });
@@ -1758,6 +1883,11 @@ var CrossTabDataBus = class _CrossTabDataBus {
1758
1883
  this.trace.event({ type: "error", source: "transport" });
1759
1884
  this.invokeHandlers(this.errorHandlers, (handler) => handler(error), "error handler");
1760
1885
  }
1886
+ reportPersistenceError(error) {
1887
+ if (error instanceof PersistenceRetryCancelledError) return;
1888
+ this.trace.event({ type: "reliability", operation: "persistence_cleanup" });
1889
+ this.reportError(error);
1890
+ }
1761
1891
  traceSubscription(action, topic) {
1762
1892
  this.trace.event({
1763
1893
  type: "subscription",
@@ -1922,11 +2052,13 @@ function parseDataBusPublication(value, fallbackTopic) {
1922
2052
  if (!topic) return null;
1923
2053
  const hasMetadataEnvelope = fallbackTopic !== void 0 && Object.prototype.hasOwnProperty.call(publication, "data") && (typeof publication.messageId === "string" || typeof publication.timestamp === "number");
1924
2054
  const data = nested || fallbackTopic === void 0 || hasMetadataEnvelope ? publication.data : value;
2055
+ const messageId = typeof publication.messageId === "string" && publication.messageId.length > 0 ? publication.messageId : void 0;
2056
+ const timestamp = typeof publication.timestamp === "number" && Number.isFinite(publication.timestamp) ? publication.timestamp : void 0;
1925
2057
  return {
1926
2058
  topic,
1927
2059
  data,
1928
- ...typeof publication.messageId === "string" ? { messageId: publication.messageId } : {},
1929
- ...typeof publication.timestamp === "number" ? { timestamp: publication.timestamp } : {}
2060
+ ...messageId === void 0 ? {} : { messageId },
2061
+ ...timestamp === void 0 ? {} : { timestamp }
1930
2062
  };
1931
2063
  }
1932
2064
 
@@ -1946,4 +2078,4 @@ export {
1946
2078
  parseDataBusPublication,
1947
2079
  selectWorkerBackend
1948
2080
  };
1949
- //# sourceMappingURL=chunk-NX76TAV3.js.map
2081
+ //# sourceMappingURL=chunk-LPS4XOK4.js.map