cross-tab-worker-databus 0.6.0 → 0.8.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/dist/centrifuge-protocol.d.ts +4 -0
  3. package/dist/centrifuge-protocol.d.ts.map +1 -1
  4. package/dist/centrifuge-session.d.ts.map +1 -1
  5. package/dist/centrifuge.d.ts +2 -1
  6. package/dist/centrifuge.d.ts.map +1 -1
  7. package/dist/centrifuge.js +13 -10
  8. package/dist/centrifuge.js.map +2 -2
  9. package/dist/centrifuge.shared.worker.js +7 -4
  10. package/dist/centrifuge.shared.worker.js.map +2 -2
  11. package/dist/centrifuge.worker.js +7 -4
  12. package/dist/centrifuge.worker.js.map +2 -2
  13. package/dist/{chunk-YUSQSNCX.js → chunk-WWUY2FV2.js} +75 -14
  14. package/dist/chunk-WWUY2FV2.js.map +7 -0
  15. package/dist/cjs/centrifuge.cjs +86 -22
  16. package/dist/cjs/centrifuge.cjs.map +2 -2
  17. package/dist/cjs/index.cjs +106 -18
  18. package/dist/cjs/index.cjs.map +2 -2
  19. package/dist/core/cluster.d.ts +7 -2
  20. package/dist/core/cluster.d.ts.map +1 -1
  21. package/dist/core/data-bus.d.ts +16 -2
  22. package/dist/core/data-bus.d.ts.map +1 -1
  23. package/dist/core/replay-persistence.d.ts +4 -0
  24. package/dist/core/replay-persistence.d.ts.map +1 -1
  25. package/dist/core/trace.d.ts +10 -1
  26. package/dist/core/trace.d.ts.map +1 -1
  27. package/dist/core/types.d.ts +10 -1
  28. package/dist/core/types.d.ts.map +1 -1
  29. package/dist/index.d.ts +3 -3
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +33 -6
  32. package/dist/index.js.map +2 -2
  33. package/dist/websocket.d.ts +2 -2
  34. package/dist/websocket.d.ts.map +1 -1
  35. package/docs/api.md +15 -2
  36. package/docs/capabilities.md +3 -3
  37. package/docs/roadmap.md +7 -7
  38. package/docs/zh/api.md +13 -2
  39. package/docs/zh/capabilities.md +3 -3
  40. package/docs/zh/roadmap.md +7 -7
  41. package/package.json +1 -1
  42. package/dist/chunk-YUSQSNCX.js.map +0 -7
@@ -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);
@@ -573,20 +574,20 @@ var WorkerClusterRuntime = class {
573
574
  * posted to a remote owner, so the caller can surface the failure instead of
574
575
  * silently dropping the publication.
575
576
  */
576
- publish(topic, data) {
577
+ publish(topic, data, messageId) {
577
578
  const topicKey = this.rememberTopic(topic);
578
579
  if (this.assignedTopics.has(topicKey)) {
579
- return this.sendControl(this.workerId, "PUBLISH", topic, topicKey, data);
580
+ return this.sendControl(this.workerId, "PUBLISH", topic, topicKey, data, messageId);
580
581
  }
581
582
  for (const pattern of this.assignedTopics.values()) {
582
583
  if (pattern !== topic && topicMatchesPattern(pattern, topic)) {
583
- return this.sendControl(this.workerId, "PUBLISH", topic, topicKey, data);
584
+ return this.sendControl(this.workerId, "PUBLISH", topic, topicKey, data, messageId);
584
585
  }
585
586
  }
586
587
  const workers = this.readWorkers();
587
588
  const route = this.readRoute(topicKey);
588
589
  const target = this.routeOwnerIsLive(route, workers) ? route?.workerId ?? this.workerId : this.workerId;
589
- return this.sendControl(target, "PUBLISH", topic, topicKey, data);
590
+ return this.sendControl(target, "PUBLISH", topic, topicKey, data, messageId);
590
591
  }
591
592
  /** True when `route` exists and its owner worker is among `workers`.
592
593
  * Shared by subscribe (skip re-assignment) and publish (route to owner).
@@ -708,7 +709,11 @@ var WorkerClusterRuntime = class {
708
709
  default:
709
710
  break;
710
711
  }
711
- this.handlers.onControl(message.action, message.topic, message.data);
712
+ if (message.messageId === void 0) {
713
+ this.handlers.onControl(message.action, message.topic, message.data);
714
+ } else {
715
+ this.handlers.onControl(message.action, message.topic, message.data, message.messageId);
716
+ }
712
717
  if (message.action !== "PUBLISH") this.updateLoad();
713
718
  }
714
719
  /**
@@ -825,7 +830,7 @@ var WorkerClusterRuntime = class {
825
830
  * Local execution updates the assignment map and route synchronously, bypassing
826
831
  * the BroadcastChannel latency.
827
832
  */
828
- sendControl(targetWorkerId, action, topic, topicKey, data) {
833
+ sendControl(targetWorkerId, action, topic, topicKey, data, messageId) {
829
834
  if (targetWorkerId === this.workerId) {
830
835
  switch (action) {
831
836
  case "SUBSCRIBE":
@@ -839,7 +844,8 @@ var WorkerClusterRuntime = class {
839
844
  default:
840
845
  break;
841
846
  }
842
- this.handlers.onControl(action, topic, data);
847
+ if (messageId === void 0) this.handlers.onControl(action, topic, data);
848
+ else this.handlers.onControl(action, topic, data, messageId);
843
849
  if (action !== "PUBLISH") this.updateLoad();
844
850
  return true;
845
851
  }
@@ -850,7 +856,8 @@ var WorkerClusterRuntime = class {
850
856
  action,
851
857
  topic,
852
858
  topicKey,
853
- ...data === void 0 ? {} : { data }
859
+ ...data === void 0 ? {} : { data },
860
+ ...messageId === void 0 ? {} : { messageId }
854
861
  });
855
862
  }
856
863
  /** Post a message on the BroadcastChannel. Returns false on postMessage failure. */
@@ -946,6 +953,8 @@ var WorkerClusterRuntime = class {
946
953
  ...route,
947
954
  confirmedAt: this.environment.now()
948
955
  });
956
+ const topic = this.knownTopics.get(topicKey);
957
+ if (topic) this.handlers.onDiagnostic?.({ operation: "route_ack", topic });
949
958
  }
950
959
  /** Remove routes whose topic has no subscribers and whose TTL has expired. */
951
960
  cleanupOrphanedRoutes(workers) {
@@ -1239,6 +1248,10 @@ var CrossTabDataBus = class _CrossTabDataBus {
1239
1248
  initialConfig;
1240
1249
  hasInitialConfig;
1241
1250
  trace;
1251
+ dedupMaxEntries;
1252
+ dedupTtlMs;
1253
+ dedupEnabled;
1254
+ seenMessageIds = /* @__PURE__ */ new Map();
1242
1255
  activeConfig;
1243
1256
  status = "disconnected";
1244
1257
  started = false;
@@ -1277,17 +1290,26 @@ var CrossTabDataBus = class _CrossTabDataBus {
1277
1290
  this.replayBuffers = replay ? /* @__PURE__ */ new Map() : null;
1278
1291
  this.replayPersistence = replay?.persistence ?? null;
1279
1292
  this.replayHydration = this.hydrateReplay();
1280
- const { autoStart, initialConfig, trace, transport, ...clusterOptions } = options;
1293
+ const { autoStart, initialConfig, trace, transport, dedup, ...clusterOptions } = options;
1281
1294
  this.transport = transport;
1282
1295
  this.initialConfig = initialConfig;
1283
1296
  this.hasInitialConfig = "initialConfig" in options;
1284
1297
  this.trace = new DataBusTraceReporter(trace);
1298
+ this.dedupMaxEntries = dedup?.maxEntries ?? 1e3;
1299
+ this.dedupTtlMs = dedup?.ttlMs ?? 6e4;
1300
+ this.dedupEnabled = dedup !== void 0;
1301
+ if (!Number.isSafeInteger(this.dedupMaxEntries) || this.dedupMaxEntries <= 0) {
1302
+ throw new TypeError("dedup.maxEntries must be a positive safe integer.");
1303
+ }
1304
+ if (!Number.isFinite(this.dedupTtlMs) || this.dedupTtlMs <= 0) {
1305
+ throw new TypeError("dedup.ttlMs must be a positive finite number.");
1306
+ }
1285
1307
  this.cluster = new WorkerClusterRuntime({
1286
1308
  ...clusterOptions,
1287
1309
  handlers: {
1288
1310
  // The cluster calls `onControl` when it receives a SUBSCRIBE/UNSUBSCRIBE/PUBLISH
1289
1311
  // control message — meaning the owning Worker has delegated the action to us.
1290
- onControl: (action, topic, data) => {
1312
+ onControl: (action, topic, data, messageId) => {
1291
1313
  switch (action) {
1292
1314
  case "SUBSCRIBE":
1293
1315
  if (this.subscribeTransport(topic)) this.traceSubscription("subscribe", topic);
@@ -1296,7 +1318,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
1296
1318
  if (this.unsubscribeTransport(topic)) this.traceSubscription("unsubscribe", topic);
1297
1319
  break;
1298
1320
  case "PUBLISH":
1299
- this.runTransport(() => this.transport.publish(topic, data));
1321
+ this.runTransport(() => this.transport.publish(topic, data, messageId ? { messageId } : void 0));
1300
1322
  break;
1301
1323
  default:
1302
1324
  break;
@@ -1321,6 +1343,9 @@ var CrossTabDataBus = class _CrossTabDataBus {
1321
1343
  this.trace.event({ type: "lifecycle", action: "resume" });
1322
1344
  this.trace.start();
1323
1345
  this.resumeTransport();
1346
+ },
1347
+ onDiagnostic: (event) => {
1348
+ this.trace.event({ type: "reliability", ...event });
1324
1349
  }
1325
1350
  }
1326
1351
  });
@@ -1467,12 +1492,27 @@ var CrossTabDataBus = class _CrossTabDataBus {
1467
1492
  if (handlers.size > 0) return;
1468
1493
  this.topicHandlers.delete(topic);
1469
1494
  this.replayBuffers?.delete(topic);
1495
+ if (this.replayPersistence?.clearTopic) {
1496
+ void this.replayPersistence.clearTopic(topic).catch((error) => this.reportError(error));
1497
+ }
1470
1498
  this.cluster.unsubscribe(topic);
1471
1499
  }
1500
+ /** Clear all in-memory replay buffers and, when supported, durable history. */
1501
+ async clearReplay() {
1502
+ this.replayBuffers?.clear();
1503
+ if (this.replayPersistence?.clear) {
1504
+ try {
1505
+ await this.replayPersistence.clear();
1506
+ } catch (error) {
1507
+ this.reportError(error);
1508
+ throw error;
1509
+ }
1510
+ }
1511
+ }
1472
1512
  /** Publish a message to `topic`. The owning Worker delivers it to the transport. */
1473
- publish(topic, data) {
1513
+ publish(topic, data, options) {
1474
1514
  this.ensureStarted();
1475
- if (!this.cluster.publish(topic, data)) {
1515
+ if (!this.cluster.publish(topic, data, options?.messageId)) {
1476
1516
  this.reportError(
1477
1517
  new Error("Failed to send the publish control message to the owning worker.")
1478
1518
  );
@@ -1522,6 +1562,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
1522
1562
  else await this.transport.stop();
1523
1563
  } finally {
1524
1564
  this.transportSubscribedTopics.clear();
1565
+ this.seenMessageIds.clear();
1525
1566
  this.started = false;
1526
1567
  this.stopping = false;
1527
1568
  this.suspended = false;
@@ -1539,6 +1580,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
1539
1580
  * and dispatches locally.
1540
1581
  */
1541
1582
  handleTransportMessage(message) {
1583
+ if (this.isDuplicate(message)) return;
1542
1584
  this.trace.recordReceived(message.topic);
1543
1585
  if (!this.cluster.isAssigned(message.topic)) {
1544
1586
  this.trace.recordDiscarded(message.topic);
@@ -1551,6 +1593,24 @@ var CrossTabDataBus = class _CrossTabDataBus {
1551
1593
  }
1552
1594
  this.trace.recordDiscarded(message.topic);
1553
1595
  }
1596
+ isDuplicate(message) {
1597
+ if (!this.dedupEnabled || !message.messageId) return false;
1598
+ const now = Date.now();
1599
+ for (const [id, timestamp] of this.seenMessageIds) {
1600
+ if (now - timestamp > this.dedupTtlMs) this.seenMessageIds.delete(id);
1601
+ }
1602
+ if (this.seenMessageIds.has(message.messageId)) {
1603
+ this.trace.event({ type: "reliability", operation: "dedup_suppressed", topic: message.topic });
1604
+ return true;
1605
+ }
1606
+ this.seenMessageIds.set(message.messageId, now);
1607
+ while (this.seenMessageIds.size > this.dedupMaxEntries) {
1608
+ const oldest = this.seenMessageIds.keys().next().value;
1609
+ if (oldest === void 0) break;
1610
+ this.seenMessageIds.delete(oldest);
1611
+ }
1612
+ return false;
1613
+ }
1554
1614
  /** Deliver a message to every local handler registered for its topic,
1555
1615
  * plus every handler registered with a wildcard subscription that matches
1556
1616
  * (e.g. a handler subscribed to "chat.*" receives "chat.room.1"). */
@@ -1635,6 +1695,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
1635
1695
  const now = Date.now();
1636
1696
  if (now - this.lastRecoveryAt >= _CrossTabDataBus.RECOVERY_COOLDOWN_MS) {
1637
1697
  this.lastRecoveryAt = now;
1698
+ this.trace.event({ type: "reliability", operation: "transport_recovery", attempt: 1 });
1638
1699
  setTimeout(() => {
1639
1700
  if (this.stopping || !this.started || this.suspended) return;
1640
1701
  if (this.status !== "error") return;
@@ -1834,6 +1895,24 @@ function createIndexedDbReplayPersistence(options) {
1834
1895
  transaction.oncomplete = () => resolve();
1835
1896
  transaction.onerror = () => reject(transaction.error ?? new Error("Failed to persist replay history."));
1836
1897
  });
1898
+ },
1899
+ async clear() {
1900
+ const db = await open();
1901
+ await new Promise((resolve, reject) => {
1902
+ const transaction = db.transaction(storeName, "readwrite");
1903
+ transaction.objectStore(storeName).clear();
1904
+ transaction.oncomplete = () => resolve();
1905
+ transaction.onerror = () => reject(transaction.error ?? new Error("Failed to clear replay history."));
1906
+ });
1907
+ },
1908
+ async clearTopic(topic) {
1909
+ const db = await open();
1910
+ await new Promise((resolve, reject) => {
1911
+ const transaction = db.transaction(storeName, "readwrite");
1912
+ transaction.objectStore(storeName).delete(topic);
1913
+ transaction.oncomplete = () => resolve();
1914
+ transaction.onerror = () => reject(transaction.error ?? new Error("Failed to clear topic replay history."));
1915
+ });
1837
1916
  }
1838
1917
  };
1839
1918
  }
@@ -1885,12 +1964,12 @@ var WebSocketTransport = class {
1885
1964
  this.sendFrame({ op: "unsubscribe", topic });
1886
1965
  }
1887
1966
  /** Publish `data` to `topic` as a JSON frame. Requires an open socket. */
1888
- publish(topic, data) {
1967
+ publish(topic, data, options) {
1889
1968
  if (data instanceof ArrayBuffer) {
1890
- this.sendBinaryFrame(topic, data);
1969
+ this.sendBinaryFrame(topic, data, options?.messageId);
1891
1970
  return;
1892
1971
  }
1893
- this.sendFrame({ op: "publish", topic, data });
1972
+ this.sendFrame({ op: "publish", topic, data, ...options?.messageId ? { messageId: options.messageId } : {} });
1894
1973
  }
1895
1974
  /** Close the socket and drop all state. Safe to call multiple times. */
1896
1975
  stop() {
@@ -1910,7 +1989,11 @@ var WebSocketTransport = class {
1910
1989
  }
1911
1990
  this.socket.send(JSON.stringify(payload));
1912
1991
  }
1913
- sendBinaryFrame(topic, data) {
1992
+ sendBinaryFrame(topic, data, messageId) {
1993
+ if (messageId) {
1994
+ this.sendFrame({ op: "publish", topic, data: Array.from(new Uint8Array(data)), messageId });
1995
+ return;
1996
+ }
1914
1997
  if (this.socket?.readyState !== WS_OPEN) {
1915
1998
  this.handlers?.onError(new Error('WebSocket is not open; dropped "publish" frame.'));
1916
1999
  return;
@@ -1952,7 +2035,12 @@ var WebSocketTransport = class {
1952
2035
  if (!parsed || typeof parsed !== "object") return;
1953
2036
  const frame = parsed;
1954
2037
  if (typeof frame.topic !== "string") return;
1955
- this.handlers?.onMessage({ topic: frame.topic, data: frame.data });
2038
+ const message = {
2039
+ topic: frame.topic,
2040
+ data: frame.data,
2041
+ ...typeof frame.messageId === "string" ? { messageId: frame.messageId } : {}
2042
+ };
2043
+ this.handlers?.onMessage(message);
1956
2044
  }
1957
2045
  };
1958
2046
  function defaultWebSocketFactory(url, protocols) {