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.
Files changed (50) hide show
  1. package/CHANGELOG.md +222 -0
  2. package/README.md +1 -0
  3. package/README.zh.md +1 -0
  4. package/dist/centrifuge.js +1 -1
  5. package/dist/centrifuge.shared.worker.js +4 -2
  6. package/dist/centrifuge.shared.worker.js.map +2 -2
  7. package/dist/centrifuge.worker.js +4 -2
  8. package/dist/centrifuge.worker.js.map +2 -2
  9. package/dist/{chunk-ZOPNTR4E.js → chunk-LPS4XOK4.js} +177 -19
  10. package/dist/{chunk-ZOPNTR4E.js.map → chunk-LPS4XOK4.js.map} +2 -2
  11. package/dist/cjs/centrifuge.cjs +176 -18
  12. package/dist/cjs/centrifuge.cjs.map +2 -2
  13. package/dist/cjs/hooks.cjs +4 -1
  14. package/dist/cjs/hooks.cjs.map +2 -2
  15. package/dist/cjs/index.cjs +326 -67
  16. package/dist/cjs/index.cjs.map +2 -2
  17. package/dist/cjs/vue.cjs +7 -1
  18. package/dist/cjs/vue.cjs.map +2 -2
  19. package/dist/core/data-bus.d.ts +35 -0
  20. package/dist/core/data-bus.d.ts.map +1 -1
  21. package/dist/core/publication.d.ts.map +1 -1
  22. package/dist/core/replay-persistence.d.ts.map +1 -1
  23. package/dist/core/trace.d.ts +11 -1
  24. package/dist/core/trace.d.ts.map +1 -1
  25. package/dist/hooks.d.ts.map +1 -1
  26. package/dist/hooks.js +4 -1
  27. package/dist/hooks.js.map +2 -2
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +151 -50
  31. package/dist/index.js.map +2 -2
  32. package/dist/vue.d.ts.map +1 -1
  33. package/dist/vue.js +7 -1
  34. package/dist/vue.js.map +2 -2
  35. package/dist/websocket.d.ts.map +1 -1
  36. package/docs/README.md +1 -0
  37. package/docs/api.md +11 -4
  38. package/docs/architecture.md +4 -0
  39. package/docs/capabilities.md +2 -2
  40. package/docs/configuration.md +21 -1
  41. package/docs/release-checklist.md +20 -0
  42. package/docs/roadmap.md +144 -2
  43. package/docs/zh/README.md +1 -0
  44. package/docs/zh/api.md +11 -4
  45. package/docs/zh/architecture.md +4 -0
  46. package/docs/zh/capabilities.md +2 -2
  47. package/docs/zh/configuration.md +21 -1
  48. package/docs/zh/release-checklist.md +20 -0
  49. package/docs/zh/roadmap.md +132 -2
  50. package/package.json +2 -1
@@ -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
- constructor(options, now = Date.now) {
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.replayHydration = this.hydrateReplay();
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.reportError(error));
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.reportError(error);
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.reportError(error);
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) => (message.timestamp ?? 0) >= timestamp);
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) 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
+ }
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.seenMessageIds.clear();
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 = Date.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.reportError(error));
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
- for (const message of await this.replayPersistence.load()) {
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.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
+ }
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 = Date.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
- ...typeof publication.messageId === "string" ? { messageId: publication.messageId } : {},
1912
- ...typeof publication.timestamp === "number" ? { timestamp: publication.timestamp } : {}
2069
+ ...messageId === void 0 ? {} : { messageId },
2070
+ ...timestamp === void 0 ? {} : { timestamp }
1913
2071
  };
1914
2072
  }
1915
2073