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
@@ -1050,7 +1050,9 @@ var DataBusTraceReporter = class {
1050
1050
  // Bucketed histogram: bucket index = floor(delayMs / 50), capped at 19.
1051
1051
  latencyBuckets = new Array(LATENCY_BUCKET_COUNT).fill(0);
1052
1052
  latencySumMs = 0;
1053
- constructor(options, now = Date.now) {
1053
+ dedupAccepted = 0;
1054
+ dedupSuppressed = 0;
1055
+ constructor(options, now = options?.now ?? Date.now) {
1054
1056
  this.enabled = options?.enabled ?? false;
1055
1057
  this.mode = options?.mode ?? "all";
1056
1058
  this.metricsIntervalMs = normalizeInterval(options?.metricsIntervalMs);
@@ -1125,6 +1127,13 @@ var DataBusTraceReporter = class {
1125
1127
  this.latencyBuckets[bucketIndex] = (this.latencyBuckets[bucketIndex] ?? 0) + 1;
1126
1128
  this.latencySumMs += delayMs;
1127
1129
  }
1130
+ /** Record deduplication outcomes for the next metrics window. */
1131
+ recordDedupAccepted() {
1132
+ if (this.metricsActive) this.dedupAccepted += 1;
1133
+ }
1134
+ recordDedupSuppressed() {
1135
+ if (this.metricsActive) this.dedupSuppressed += 1;
1136
+ }
1128
1137
  /** True when metrics recording is active: enabled and mode includes metrics.
1129
1138
  * Extracted so the four record / flush methods share one guard expression
1130
1139
  * instead of repeating `!this.enabled || this.mode === 'events'` at each. */
@@ -1138,7 +1147,7 @@ var DataBusTraceReporter = class {
1138
1147
  }
1139
1148
  flushNow() {
1140
1149
  const timestamp = this.now();
1141
- if (this.received > 0 || this.dispatched > 0) {
1150
+ if (this.received > 0 || this.dispatched > 0 || this.dedupAccepted > 0 || this.dedupSuppressed > 0) {
1142
1151
  const samples = this.latencySamples;
1143
1152
  this.emit({
1144
1153
  type: "message_metrics",
@@ -1152,6 +1161,8 @@ var DataBusTraceReporter = class {
1152
1161
  dispatchP50Ms: roundMs(percentileMs(this.latencyBuckets, samples, 0.5)),
1153
1162
  dispatchP95Ms: roundMs(percentileMs(this.latencyBuckets, samples, 0.95)),
1154
1163
  dispatchMaxMs: roundMs(percentileMs(this.latencyBuckets, samples, 1)),
1164
+ dedupAccepted: this.dedupAccepted,
1165
+ dedupSuppressed: this.dedupSuppressed,
1155
1166
  timestamp
1156
1167
  });
1157
1168
  this.resetMetrics();
@@ -1166,6 +1177,8 @@ var DataBusTraceReporter = class {
1166
1177
  this.receivedAt.clear();
1167
1178
  this.latencyBuckets.fill(0);
1168
1179
  this.latencySumMs = 0;
1180
+ this.dedupAccepted = 0;
1181
+ this.dedupSuppressed = 0;
1169
1182
  }
1170
1183
  emit(event) {
1171
1184
  try {
@@ -1201,6 +1214,12 @@ function roundMs(value) {
1201
1214
  // src/core/data-bus.ts
1202
1215
  var PUBLICATION_EVENT = "DATABUS_PUBLICATION";
1203
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
+ };
1204
1223
  var CrossTabDataBus = class _CrossTabDataBus {
1205
1224
  transport;
1206
1225
  cluster;
@@ -1216,13 +1235,26 @@ var CrossTabDataBus = class _CrossTabDataBus {
1216
1235
  replayBuffers;
1217
1236
  replayMaxPerTopic;
1218
1237
  replayPersistence;
1238
+ replayRetentionMs;
1239
+ replayRetentionSweepMs;
1240
+ persistenceRetryMaxAttempts;
1241
+ persistenceRetryBackoffMs;
1242
+ persistenceRetryGeneration = 0;
1219
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;
1220
1249
  initialConfig;
1221
1250
  hasInitialConfig;
1222
1251
  trace;
1223
1252
  dedupMaxEntries;
1224
1253
  dedupTtlMs;
1254
+ dedupSweepMs;
1255
+ dedupSweepTimer = null;
1225
1256
  dedupEnabled;
1257
+ now;
1226
1258
  seenMessageIds = /* @__PURE__ */ new Map();
1227
1259
  dedupSuppressed = 0;
1228
1260
  dedupAccepted = 0;
@@ -1263,14 +1295,32 @@ var CrossTabDataBus = class _CrossTabDataBus {
1263
1295
  this.replayMaxPerTopic = replay?.maxPerTopic ?? DEFAULT_REPLAY_MAX_PER_TOPIC;
1264
1296
  this.replayBuffers = replay ? /* @__PURE__ */ new Map() : null;
1265
1297
  this.replayPersistence = replay?.persistence ?? null;
1266
- this.replayHydration = this.hydrateReplay();
1298
+ this.replayRetentionMs = replay?.retentionMs;
1299
+ if (this.replayRetentionMs !== void 0 && (!Number.isFinite(this.replayRetentionMs) || this.replayRetentionMs <= 0)) {
1300
+ throw new TypeError("replay.retentionMs must be a positive finite number.");
1301
+ }
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
+ }
1267
1314
  const { autoStart, initialConfig, trace, transport, dedup, ...clusterOptions } = options;
1315
+ this.now = dedup?.now ?? Date.now;
1316
+ this.replayHydration = this.hydrateReplay();
1268
1317
  this.transport = transport;
1269
1318
  this.initialConfig = initialConfig;
1270
1319
  this.hasInitialConfig = "initialConfig" in options;
1271
1320
  this.trace = new DataBusTraceReporter(trace);
1272
1321
  this.dedupMaxEntries = dedup?.maxEntries ?? 1e3;
1273
1322
  this.dedupTtlMs = dedup?.ttlMs ?? 6e4;
1323
+ this.dedupSweepMs = dedup?.sweepMs;
1274
1324
  this.dedupEnabled = dedup !== void 0;
1275
1325
  if (!Number.isSafeInteger(this.dedupMaxEntries) || this.dedupMaxEntries <= 0) {
1276
1326
  throw new TypeError("dedup.maxEntries must be a positive safe integer.");
@@ -1278,6 +1328,9 @@ var CrossTabDataBus = class _CrossTabDataBus {
1278
1328
  if (!Number.isFinite(this.dedupTtlMs) || this.dedupTtlMs <= 0) {
1279
1329
  throw new TypeError("dedup.ttlMs must be a positive finite number.");
1280
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
+ }
1281
1334
  this.cluster = new WorkerClusterRuntime({
1282
1335
  ...clusterOptions,
1283
1336
  handlers: {
@@ -1318,11 +1371,16 @@ var CrossTabDataBus = class _CrossTabDataBus {
1318
1371
  onSuspend: () => {
1319
1372
  if (!this.stopping) this.trace.event({ type: "lifecycle", action: "suspend" });
1320
1373
  this.trace.pause();
1374
+ this.persistenceRetryGeneration += 1;
1375
+ this.stopDedupSweep();
1376
+ this.stopReplayRetentionSweep();
1321
1377
  this.suspendTransport();
1322
1378
  },
1323
1379
  onResume: () => {
1324
1380
  this.trace.event({ type: "lifecycle", action: "resume" });
1325
1381
  this.trace.start();
1382
+ this.startDedupSweep();
1383
+ this.startReplayRetentionSweep();
1326
1384
  this.resumeTransport();
1327
1385
  },
1328
1386
  onDiagnostic: (event) => {
@@ -1350,6 +1408,8 @@ var CrossTabDataBus = class _CrossTabDataBus {
1350
1408
  this.lastError = null;
1351
1409
  this.trace.event({ type: "lifecycle", action: "start" });
1352
1410
  this.trace.start();
1411
+ this.startDedupSweep();
1412
+ this.startReplayRetentionSweep();
1353
1413
  this.updateStatus("connecting");
1354
1414
  this.cluster.start();
1355
1415
  const opening = this.openTransport(config, this.pendingStop ?? Promise.resolve(), true);
@@ -1474,7 +1534,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
1474
1534
  this.topicHandlers.delete(topic);
1475
1535
  this.replayBuffers?.delete(topic);
1476
1536
  if (this.replayPersistence?.clearTopic) {
1477
- void this.replayPersistence.clearTopic(topic).catch((error) => this.reportError(error));
1537
+ void this.withPersistenceRetry("clearTopic", () => this.replayPersistence.clearTopic(topic)).catch((error) => this.reportPersistenceError(error));
1478
1538
  }
1479
1539
  this.cluster.unsubscribe(topic);
1480
1540
  }
@@ -1483,9 +1543,9 @@ var CrossTabDataBus = class _CrossTabDataBus {
1483
1543
  this.replayBuffers?.clear();
1484
1544
  if (this.replayPersistence?.clear) {
1485
1545
  try {
1486
- await this.replayPersistence.clear();
1546
+ await this.withPersistenceRetry("clear", () => this.replayPersistence.clear());
1487
1547
  } catch (error) {
1488
- this.reportError(error);
1548
+ this.reportPersistenceError(error);
1489
1549
  throw error;
1490
1550
  }
1491
1551
  }
@@ -1495,9 +1555,9 @@ var CrossTabDataBus = class _CrossTabDataBus {
1495
1555
  this.replayBuffers?.delete(topic);
1496
1556
  if (this.replayPersistence?.clearTopic) {
1497
1557
  try {
1498
- await this.replayPersistence.clearTopic(topic);
1558
+ await this.withPersistenceRetry("clearTopic", () => this.replayPersistence.clearTopic(topic));
1499
1559
  } catch (error) {
1500
- this.reportError(error);
1560
+ this.reportPersistenceError(error);
1501
1561
  throw error;
1502
1562
  }
1503
1563
  }
@@ -1507,12 +1567,19 @@ var CrossTabDataBus = class _CrossTabDataBus {
1507
1567
  if (!Number.isFinite(timestamp)) throw new TypeError("timestamp must be finite.");
1508
1568
  if (this.replayBuffers) {
1509
1569
  for (const [topic, messages] of this.replayBuffers) {
1510
- const kept = messages.filter((message) => (message.timestamp ?? 0) >= timestamp);
1570
+ const kept = messages.filter((message) => message.timestamp === void 0 || message.timestamp >= timestamp);
1511
1571
  if (kept.length) this.replayBuffers.set(topic, kept);
1512
1572
  else this.replayBuffers.delete(topic);
1513
1573
  }
1514
1574
  }
1515
- 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
+ }
1516
1583
  }
1517
1584
  /** Return bounded deduplication counters for diagnostics and health checks. */
1518
1585
  getDedupStats() {
@@ -1570,8 +1637,11 @@ var CrossTabDataBus = class _CrossTabDataBus {
1570
1637
  async stop() {
1571
1638
  if (!this.started) return;
1572
1639
  this.stopping = true;
1640
+ this.persistenceRetryGeneration += 1;
1573
1641
  this.trace.event({ type: "lifecycle", action: "stop" });
1574
1642
  this.trace.stop();
1643
+ this.stopDedupSweep();
1644
+ this.stopReplayRetentionSweep();
1575
1645
  this.topicHandlers.clear();
1576
1646
  this.replayBuffers?.clear();
1577
1647
  this.cluster.stop();
@@ -1582,7 +1652,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
1582
1652
  else await this.transport.stop();
1583
1653
  } finally {
1584
1654
  this.transportSubscribedTopics.clear();
1585
- this.seenMessageIds.clear();
1655
+ this.resetDedup();
1586
1656
  this.started = false;
1587
1657
  this.stopping = false;
1588
1658
  this.suspended = false;
@@ -1615,17 +1685,19 @@ var CrossTabDataBus = class _CrossTabDataBus {
1615
1685
  }
1616
1686
  isDuplicate(message) {
1617
1687
  if (!this.dedupEnabled || !message.messageId) return false;
1618
- const now = Date.now();
1688
+ const now = this.now();
1619
1689
  for (const [id, timestamp] of this.seenMessageIds) {
1620
1690
  if (now - timestamp > this.dedupTtlMs) this.seenMessageIds.delete(id);
1621
1691
  }
1622
1692
  if (this.seenMessageIds.has(message.messageId)) {
1623
1693
  this.trace.event({ type: "reliability", operation: "dedup_suppressed", topic: message.topic });
1624
1694
  this.dedupSuppressed += 1;
1695
+ this.trace.recordDedupSuppressed();
1625
1696
  return true;
1626
1697
  }
1627
1698
  this.seenMessageIds.set(message.messageId, now);
1628
1699
  this.dedupAccepted += 1;
1700
+ this.trace.recordDedupAccepted();
1629
1701
  while (this.seenMessageIds.size > this.dedupMaxEntries) {
1630
1702
  const oldest = this.seenMessageIds.keys().next().value;
1631
1703
  if (oldest === void 0) break;
@@ -1633,6 +1705,20 @@ var CrossTabDataBus = class _CrossTabDataBus {
1633
1705
  }
1634
1706
  return false;
1635
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
+ }
1636
1722
  /** Deliver a message to every local handler registered for its topic,
1637
1723
  * plus every handler registered with a wildcard subscription that matches
1638
1724
  * (e.g. a handler subscribed to "chat.*" receives "chat.room.1"). */
@@ -1659,7 +1745,10 @@ var CrossTabDataBus = class _CrossTabDataBus {
1659
1745
  buffer.push(storedMessage);
1660
1746
  if (buffer.length > this.replayMaxPerTopic) buffer.shift();
1661
1747
  if (this.replayPersistence) {
1662
- void this.replayPersistence.append(storedMessage).catch((error) => this.reportError(error));
1748
+ void this.withPersistenceRetry("append", () => this.replayPersistence.append(storedMessage)).catch((error) => this.reportPersistenceError(error));
1749
+ if (this.replayRetentionMs !== void 0 && this.replayPersistence.clearBefore) {
1750
+ this.scheduleReplayRetentionCleanup(this.now() - this.replayRetentionMs);
1751
+ }
1663
1752
  }
1664
1753
  }
1665
1754
  async hydrateReplay() {
@@ -1667,7 +1756,10 @@ var CrossTabDataBus = class _CrossTabDataBus {
1667
1756
  return;
1668
1757
  }
1669
1758
  try {
1670
- for (const message of await this.replayPersistence.load()) {
1759
+ if (this.replayRetentionMs !== void 0 && this.replayPersistence.clearBefore) {
1760
+ await this.withPersistenceRetry("clearBefore", () => this.replayPersistence.clearBefore(this.now() - this.replayRetentionMs));
1761
+ }
1762
+ for (const message of await this.withPersistenceRetry("load", () => this.replayPersistence.load())) {
1671
1763
  let buffer = this.replayBuffers.get(message.topic);
1672
1764
  if (!buffer) {
1673
1765
  buffer = [];
@@ -1677,7 +1769,66 @@ var CrossTabDataBus = class _CrossTabDataBus {
1677
1769
  if (buffer.length > this.replayMaxPerTopic) buffer.shift();
1678
1770
  }
1679
1771
  } catch (error) {
1680
- 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
+ }
1681
1832
  }
1682
1833
  }
1683
1834
  /** Deliver buffered history to a newly-registered handler. For an exact
@@ -1715,7 +1866,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
1715
1866
  for (const topic of this.cluster.getSnapshot().assignedTopics) this.subscribeTransport(topic);
1716
1867
  }
1717
1868
  if (status === "error" && this.started && !this.stopping) {
1718
- const now = Date.now();
1869
+ const now = this.now();
1719
1870
  if (now - this.lastRecoveryAt >= _CrossTabDataBus.RECOVERY_COOLDOWN_MS) {
1720
1871
  this.lastRecoveryAt = now;
1721
1872
  this.trace.event({ type: "reliability", operation: "transport_recovery", attempt: 1 });
@@ -1732,6 +1883,11 @@ var CrossTabDataBus = class _CrossTabDataBus {
1732
1883
  this.trace.event({ type: "error", source: "transport" });
1733
1884
  this.invokeHandlers(this.errorHandlers, (handler) => handler(error), "error handler");
1734
1885
  }
1886
+ reportPersistenceError(error) {
1887
+ if (error instanceof PersistenceRetryCancelledError) return;
1888
+ this.trace.event({ type: "reliability", operation: "persistence_cleanup" });
1889
+ this.reportError(error);
1890
+ }
1735
1891
  traceSubscription(action, topic) {
1736
1892
  this.trace.event({
1737
1893
  type: "subscription",
@@ -1896,11 +2052,13 @@ function parseDataBusPublication(value, fallbackTopic) {
1896
2052
  if (!topic) return null;
1897
2053
  const hasMetadataEnvelope = fallbackTopic !== void 0 && Object.prototype.hasOwnProperty.call(publication, "data") && (typeof publication.messageId === "string" || typeof publication.timestamp === "number");
1898
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;
1899
2057
  return {
1900
2058
  topic,
1901
2059
  data,
1902
- ...typeof publication.messageId === "string" ? { messageId: publication.messageId } : {},
1903
- ...typeof publication.timestamp === "number" ? { timestamp: publication.timestamp } : {}
2060
+ ...messageId === void 0 ? {} : { messageId },
2061
+ ...timestamp === void 0 ? {} : { timestamp }
1904
2062
  };
1905
2063
  }
1906
2064
 
@@ -1920,4 +2078,4 @@ export {
1920
2078
  parseDataBusPublication,
1921
2079
  selectWorkerBackend
1922
2080
  };
1923
- //# sourceMappingURL=chunk-ZOPNTR4E.js.map
2081
+ //# sourceMappingURL=chunk-LPS4XOK4.js.map