cross-tab-worker-databus 0.6.0 → 0.7.0

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.
@@ -562,6 +562,7 @@ var WorkerClusterRuntime = class {
562
562
  projectedLoads.set(owner.workerId, (projectedLoads.get(owner.workerId) ?? owner.load) + 1);
563
563
  const generation = (previous?.generation ?? 0) + 1;
564
564
  this.writeRoute(topicKey, owner, previous?.workerId, generation);
565
+ this.handlers.onDiagnostic?.({ operation: "route_migration", topic });
565
566
  this.flushStorage();
566
567
  this.handlers.onControl("UNSUBSCRIBE", topic);
567
568
  this.sendRouteReleased(owner.workerId, topic, topicKey, generation);
@@ -946,6 +947,8 @@ var WorkerClusterRuntime = class {
946
947
  ...route,
947
948
  confirmedAt: this.environment.now()
948
949
  });
950
+ const topic = this.knownTopics.get(topicKey);
951
+ if (topic) this.handlers.onDiagnostic?.({ operation: "route_ack", topic });
949
952
  }
950
953
  /** Remove routes whose topic has no subscribers and whose TTL has expired. */
951
954
  cleanupOrphanedRoutes(workers) {
@@ -1239,6 +1242,10 @@ var CrossTabDataBus = class _CrossTabDataBus {
1239
1242
  initialConfig;
1240
1243
  hasInitialConfig;
1241
1244
  trace;
1245
+ dedupMaxEntries;
1246
+ dedupTtlMs;
1247
+ dedupEnabled;
1248
+ seenMessageIds = /* @__PURE__ */ new Map();
1242
1249
  activeConfig;
1243
1250
  status = "disconnected";
1244
1251
  started = false;
@@ -1277,11 +1284,20 @@ var CrossTabDataBus = class _CrossTabDataBus {
1277
1284
  this.replayBuffers = replay ? /* @__PURE__ */ new Map() : null;
1278
1285
  this.replayPersistence = replay?.persistence ?? null;
1279
1286
  this.replayHydration = this.hydrateReplay();
1280
- const { autoStart, initialConfig, trace, transport, ...clusterOptions } = options;
1287
+ const { autoStart, initialConfig, trace, transport, dedup, ...clusterOptions } = options;
1281
1288
  this.transport = transport;
1282
1289
  this.initialConfig = initialConfig;
1283
1290
  this.hasInitialConfig = "initialConfig" in options;
1284
1291
  this.trace = new DataBusTraceReporter(trace);
1292
+ this.dedupMaxEntries = dedup?.maxEntries ?? 1e3;
1293
+ this.dedupTtlMs = dedup?.ttlMs ?? 6e4;
1294
+ this.dedupEnabled = dedup !== void 0;
1295
+ if (!Number.isSafeInteger(this.dedupMaxEntries) || this.dedupMaxEntries <= 0) {
1296
+ throw new TypeError("dedup.maxEntries must be a positive safe integer.");
1297
+ }
1298
+ if (!Number.isFinite(this.dedupTtlMs) || this.dedupTtlMs <= 0) {
1299
+ throw new TypeError("dedup.ttlMs must be a positive finite number.");
1300
+ }
1285
1301
  this.cluster = new WorkerClusterRuntime({
1286
1302
  ...clusterOptions,
1287
1303
  handlers: {
@@ -1321,6 +1337,9 @@ var CrossTabDataBus = class _CrossTabDataBus {
1321
1337
  this.trace.event({ type: "lifecycle", action: "resume" });
1322
1338
  this.trace.start();
1323
1339
  this.resumeTransport();
1340
+ },
1341
+ onDiagnostic: (event) => {
1342
+ this.trace.event({ type: "reliability", ...event });
1324
1343
  }
1325
1344
  }
1326
1345
  });
@@ -1467,6 +1486,9 @@ var CrossTabDataBus = class _CrossTabDataBus {
1467
1486
  if (handlers.size > 0) return;
1468
1487
  this.topicHandlers.delete(topic);
1469
1488
  this.replayBuffers?.delete(topic);
1489
+ if (this.replayPersistence?.clearTopic) {
1490
+ void this.replayPersistence.clearTopic(topic).catch((error) => this.reportError(error));
1491
+ }
1470
1492
  this.cluster.unsubscribe(topic);
1471
1493
  }
1472
1494
  /** Publish a message to `topic`. The owning Worker delivers it to the transport. */
@@ -1522,6 +1544,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
1522
1544
  else await this.transport.stop();
1523
1545
  } finally {
1524
1546
  this.transportSubscribedTopics.clear();
1547
+ this.seenMessageIds.clear();
1525
1548
  this.started = false;
1526
1549
  this.stopping = false;
1527
1550
  this.suspended = false;
@@ -1539,6 +1562,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
1539
1562
  * and dispatches locally.
1540
1563
  */
1541
1564
  handleTransportMessage(message) {
1565
+ if (this.isDuplicate(message)) return;
1542
1566
  this.trace.recordReceived(message.topic);
1543
1567
  if (!this.cluster.isAssigned(message.topic)) {
1544
1568
  this.trace.recordDiscarded(message.topic);
@@ -1551,6 +1575,21 @@ var CrossTabDataBus = class _CrossTabDataBus {
1551
1575
  }
1552
1576
  this.trace.recordDiscarded(message.topic);
1553
1577
  }
1578
+ isDuplicate(message) {
1579
+ if (!this.dedupEnabled || !message.messageId) return false;
1580
+ const now = Date.now();
1581
+ for (const [id, timestamp] of this.seenMessageIds) {
1582
+ if (now - timestamp > this.dedupTtlMs) this.seenMessageIds.delete(id);
1583
+ }
1584
+ if (this.seenMessageIds.has(message.messageId)) return true;
1585
+ this.seenMessageIds.set(message.messageId, now);
1586
+ while (this.seenMessageIds.size > this.dedupMaxEntries) {
1587
+ const oldest = this.seenMessageIds.keys().next().value;
1588
+ if (oldest === void 0) break;
1589
+ this.seenMessageIds.delete(oldest);
1590
+ }
1591
+ return false;
1592
+ }
1554
1593
  /** Deliver a message to every local handler registered for its topic,
1555
1594
  * plus every handler registered with a wildcard subscription that matches
1556
1595
  * (e.g. a handler subscribed to "chat.*" receives "chat.room.1"). */
@@ -1635,6 +1674,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
1635
1674
  const now = Date.now();
1636
1675
  if (now - this.lastRecoveryAt >= _CrossTabDataBus.RECOVERY_COOLDOWN_MS) {
1637
1676
  this.lastRecoveryAt = now;
1677
+ this.trace.event({ type: "reliability", operation: "transport_recovery", attempt: 1 });
1638
1678
  setTimeout(() => {
1639
1679
  if (this.stopping || !this.started || this.suspended) return;
1640
1680
  if (this.status !== "error") return;
@@ -1834,6 +1874,24 @@ function createIndexedDbReplayPersistence(options) {
1834
1874
  transaction.oncomplete = () => resolve();
1835
1875
  transaction.onerror = () => reject(transaction.error ?? new Error("Failed to persist replay history."));
1836
1876
  });
1877
+ },
1878
+ async clear() {
1879
+ const db = await open();
1880
+ await new Promise((resolve, reject) => {
1881
+ const transaction = db.transaction(storeName, "readwrite");
1882
+ transaction.objectStore(storeName).clear();
1883
+ transaction.oncomplete = () => resolve();
1884
+ transaction.onerror = () => reject(transaction.error ?? new Error("Failed to clear replay history."));
1885
+ });
1886
+ },
1887
+ async clearTopic(topic) {
1888
+ const db = await open();
1889
+ await new Promise((resolve, reject) => {
1890
+ const transaction = db.transaction(storeName, "readwrite");
1891
+ transaction.objectStore(storeName).delete(topic);
1892
+ transaction.oncomplete = () => resolve();
1893
+ transaction.onerror = () => reject(transaction.error ?? new Error("Failed to clear topic replay history."));
1894
+ });
1837
1895
  }
1838
1896
  };
1839
1897
  }