cross-tab-worker-databus 0.7.0 → 0.10.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 (46) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/README.md +1 -0
  3. package/README.zh.md +1 -0
  4. package/dist/centrifuge-protocol.d.ts +8 -0
  5. package/dist/centrifuge-protocol.d.ts.map +1 -1
  6. package/dist/centrifuge-session.d.ts.map +1 -1
  7. package/dist/centrifuge.d.ts +2 -1
  8. package/dist/centrifuge.d.ts.map +1 -1
  9. package/dist/centrifuge.js +31 -10
  10. package/dist/centrifuge.js.map +2 -2
  11. package/dist/centrifuge.shared.worker.js +38 -4
  12. package/dist/centrifuge.shared.worker.js.map +3 -3
  13. package/dist/centrifuge.worker.js +38 -4
  14. package/dist/centrifuge.worker.js.map +3 -3
  15. package/dist/{chunk-SI7N5KNJ.js → chunk-ZOPNTR4E.js} +128 -22
  16. package/dist/chunk-ZOPNTR4E.js.map +7 -0
  17. package/dist/cjs/centrifuge.cjs +157 -30
  18. package/dist/cjs/centrifuge.cjs.map +4 -4
  19. package/dist/cjs/index.cjs +166 -28
  20. package/dist/cjs/index.cjs.map +3 -3
  21. package/dist/core/cluster.d.ts +4 -3
  22. package/dist/core/cluster.d.ts.map +1 -1
  23. package/dist/core/data-bus.d.ts +20 -2
  24. package/dist/core/data-bus.d.ts.map +1 -1
  25. package/dist/core/publication.d.ts +10 -0
  26. package/dist/core/publication.d.ts.map +1 -0
  27. package/dist/core/replay-persistence.d.ts +2 -0
  28. package/dist/core/replay-persistence.d.ts.map +1 -1
  29. package/dist/core/trace.d.ts +2 -11
  30. package/dist/core/trace.d.ts.map +1 -1
  31. package/dist/core/types.d.ts +25 -6
  32. package/dist/core/types.d.ts.map +1 -1
  33. package/dist/index.d.ts +2 -2
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +42 -8
  36. package/dist/index.js.map +2 -2
  37. package/dist/websocket.d.ts +2 -2
  38. package/dist/websocket.d.ts.map +1 -1
  39. package/docs/api.md +35 -4
  40. package/docs/architecture.md +3 -1
  41. package/docs/transports.md +9 -5
  42. package/docs/zh/api.md +35 -4
  43. package/docs/zh/architecture.md +3 -1
  44. package/docs/zh/transports.md +8 -4
  45. package/package.json +1 -1
  46. package/dist/chunk-SI7N5KNJ.js.map +0 -7
@@ -339,6 +339,13 @@ function writeJson(storage, key, value) {
339
339
  } catch {
340
340
  }
341
341
  }
342
+ function publicationMetadata(messageId, timestamp) {
343
+ if (messageId === void 0 && timestamp === void 0) return void 0;
344
+ return {
345
+ ...messageId === void 0 ? {} : { messageId },
346
+ ...timestamp === void 0 ? {} : { timestamp }
347
+ };
348
+ }
342
349
  function listKeys(storage, prefix) {
343
350
  try {
344
351
  return Array.from({ length: storage.length }, (_, index) => storage.key(index)).filter(
@@ -568,26 +575,21 @@ var WorkerClusterRuntime = class {
568
575
  this.sendRouteReleased(owner.workerId, topic, topicKey, generation);
569
576
  }
570
577
  }
571
- /**
572
- * Publish a message to `topic`, routing through the owning Worker (or self if
573
- * no owner is found). Returns false when the control message could not be
574
- * posted to a remote owner, so the caller can surface the failure instead of
575
- * silently dropping the publication.
576
- */
577
- publish(topic, data) {
578
+ publish(topic, data, metadataOrMessageId) {
579
+ const metadata = typeof metadataOrMessageId === "string" ? { messageId: metadataOrMessageId } : metadataOrMessageId;
578
580
  const topicKey = this.rememberTopic(topic);
579
581
  if (this.assignedTopics.has(topicKey)) {
580
- return this.sendControl(this.workerId, "PUBLISH", topic, topicKey, data);
582
+ return this.sendControl(this.workerId, "PUBLISH", topic, topicKey, data, metadata);
581
583
  }
582
584
  for (const pattern of this.assignedTopics.values()) {
583
585
  if (pattern !== topic && topicMatchesPattern(pattern, topic)) {
584
- return this.sendControl(this.workerId, "PUBLISH", topic, topicKey, data);
586
+ return this.sendControl(this.workerId, "PUBLISH", topic, topicKey, data, metadata);
585
587
  }
586
588
  }
587
589
  const workers = this.readWorkers();
588
590
  const route = this.readRoute(topicKey);
589
591
  const target = this.routeOwnerIsLive(route, workers) ? route?.workerId ?? this.workerId : this.workerId;
590
- return this.sendControl(target, "PUBLISH", topic, topicKey, data);
592
+ return this.sendControl(target, "PUBLISH", topic, topicKey, data, metadata);
591
593
  }
592
594
  /** True when `route` exists and its owner worker is among `workers`.
593
595
  * Shared by subscribe (skip re-assignment) and publish (route to owner).
@@ -709,7 +711,15 @@ var WorkerClusterRuntime = class {
709
711
  default:
710
712
  break;
711
713
  }
712
- this.handlers.onControl(message.action, message.topic, message.data);
714
+ const metadata = publicationMetadata(message.messageId, message.timestamp);
715
+ if (metadata) this.handlers.onControl(
716
+ message.action,
717
+ message.topic,
718
+ message.data,
719
+ metadata.messageId,
720
+ metadata.timestamp
721
+ );
722
+ else this.handlers.onControl(message.action, message.topic, message.data);
713
723
  if (message.action !== "PUBLISH") this.updateLoad();
714
724
  }
715
725
  /**
@@ -826,7 +836,7 @@ var WorkerClusterRuntime = class {
826
836
  * Local execution updates the assignment map and route synchronously, bypassing
827
837
  * the BroadcastChannel latency.
828
838
  */
829
- sendControl(targetWorkerId, action, topic, topicKey, data) {
839
+ sendControl(targetWorkerId, action, topic, topicKey, data, metadata) {
830
840
  if (targetWorkerId === this.workerId) {
831
841
  switch (action) {
832
842
  case "SUBSCRIBE":
@@ -840,7 +850,14 @@ var WorkerClusterRuntime = class {
840
850
  default:
841
851
  break;
842
852
  }
843
- this.handlers.onControl(action, topic, data);
853
+ if (metadata) this.handlers.onControl(
854
+ action,
855
+ topic,
856
+ data,
857
+ metadata.messageId,
858
+ metadata.timestamp
859
+ );
860
+ else this.handlers.onControl(action, topic, data);
844
861
  if (action !== "PUBLISH") this.updateLoad();
845
862
  return true;
846
863
  }
@@ -851,7 +868,9 @@ var WorkerClusterRuntime = class {
851
868
  action,
852
869
  topic,
853
870
  topicKey,
854
- ...data === void 0 ? {} : { data }
871
+ ...data === void 0 ? {} : { data },
872
+ ...metadata?.messageId === void 0 ? {} : { messageId: metadata.messageId },
873
+ ...metadata?.timestamp === void 0 ? {} : { timestamp: metadata.timestamp }
855
874
  });
856
875
  }
857
876
  /** Post a message on the BroadcastChannel. Returns false on postMessage failure. */
@@ -1246,6 +1265,8 @@ var CrossTabDataBus = class _CrossTabDataBus {
1246
1265
  dedupTtlMs;
1247
1266
  dedupEnabled;
1248
1267
  seenMessageIds = /* @__PURE__ */ new Map();
1268
+ dedupSuppressed = 0;
1269
+ dedupAccepted = 0;
1249
1270
  activeConfig;
1250
1271
  status = "disconnected";
1251
1272
  started = false;
@@ -1303,7 +1324,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
1303
1324
  handlers: {
1304
1325
  // The cluster calls `onControl` when it receives a SUBSCRIBE/UNSUBSCRIBE/PUBLISH
1305
1326
  // control message — meaning the owning Worker has delegated the action to us.
1306
- onControl: (action, topic, data) => {
1327
+ onControl: (action, topic, data, messageId, timestamp) => {
1307
1328
  switch (action) {
1308
1329
  case "SUBSCRIBE":
1309
1330
  if (this.subscribeTransport(topic)) this.traceSubscription("subscribe", topic);
@@ -1312,7 +1333,14 @@ var CrossTabDataBus = class _CrossTabDataBus {
1312
1333
  if (this.unsubscribeTransport(topic)) this.traceSubscription("unsubscribe", topic);
1313
1334
  break;
1314
1335
  case "PUBLISH":
1315
- this.runTransport(() => this.transport.publish(topic, data));
1336
+ this.runTransport(() => this.transport.publish(
1337
+ topic,
1338
+ data,
1339
+ messageId === void 0 && timestamp === void 0 ? void 0 : {
1340
+ ...messageId === void 0 ? {} : { messageId },
1341
+ ...timestamp === void 0 ? {} : { timestamp }
1342
+ }
1343
+ ));
1316
1344
  break;
1317
1345
  default:
1318
1346
  break;
@@ -1491,10 +1519,61 @@ var CrossTabDataBus = class _CrossTabDataBus {
1491
1519
  }
1492
1520
  this.cluster.unsubscribe(topic);
1493
1521
  }
1522
+ /** Clear all in-memory replay buffers and, when supported, durable history. */
1523
+ async clearReplay() {
1524
+ this.replayBuffers?.clear();
1525
+ if (this.replayPersistence?.clear) {
1526
+ try {
1527
+ await this.replayPersistence.clear();
1528
+ } catch (error) {
1529
+ this.reportError(error);
1530
+ throw error;
1531
+ }
1532
+ }
1533
+ }
1534
+ /** Clear replay history for one exact topic, including durable storage. */
1535
+ async clearReplayTopic(topic) {
1536
+ this.replayBuffers?.delete(topic);
1537
+ if (this.replayPersistence?.clearTopic) {
1538
+ try {
1539
+ await this.replayPersistence.clearTopic(topic);
1540
+ } catch (error) {
1541
+ this.reportError(error);
1542
+ throw error;
1543
+ }
1544
+ }
1545
+ }
1546
+ /** Remove replay entries older than an epoch-millisecond cutoff. */
1547
+ async clearReplayBefore(timestamp) {
1548
+ if (!Number.isFinite(timestamp)) throw new TypeError("timestamp must be finite.");
1549
+ if (this.replayBuffers) {
1550
+ for (const [topic, messages] of this.replayBuffers) {
1551
+ const kept = messages.filter((message) => (message.timestamp ?? 0) >= timestamp);
1552
+ if (kept.length) this.replayBuffers.set(topic, kept);
1553
+ else this.replayBuffers.delete(topic);
1554
+ }
1555
+ }
1556
+ if (this.replayPersistence?.clearBefore) await this.replayPersistence.clearBefore(timestamp);
1557
+ }
1558
+ /** Return bounded deduplication counters for diagnostics and health checks. */
1559
+ getDedupStats() {
1560
+ return {
1561
+ enabled: this.dedupEnabled,
1562
+ tracked: this.seenMessageIds.size,
1563
+ suppressed: this.dedupSuppressed,
1564
+ accepted: this.dedupAccepted
1565
+ };
1566
+ }
1567
+ /** Drop all remembered IDs and reset dedup counters. */
1568
+ resetDedup() {
1569
+ this.seenMessageIds.clear();
1570
+ this.dedupSuppressed = 0;
1571
+ this.dedupAccepted = 0;
1572
+ }
1494
1573
  /** Publish a message to `topic`. The owning Worker delivers it to the transport. */
1495
- publish(topic, data) {
1574
+ publish(topic, data, options) {
1496
1575
  this.ensureStarted();
1497
- if (!this.cluster.publish(topic, data)) {
1576
+ if (!this.cluster.publish(topic, data, options)) {
1498
1577
  this.reportError(
1499
1578
  new Error("Failed to send the publish control message to the owning worker.")
1500
1579
  );
@@ -1581,8 +1660,13 @@ var CrossTabDataBus = class _CrossTabDataBus {
1581
1660
  for (const [id, timestamp] of this.seenMessageIds) {
1582
1661
  if (now - timestamp > this.dedupTtlMs) this.seenMessageIds.delete(id);
1583
1662
  }
1584
- if (this.seenMessageIds.has(message.messageId)) return true;
1663
+ if (this.seenMessageIds.has(message.messageId)) {
1664
+ this.trace.event({ type: "reliability", operation: "dedup_suppressed", topic: message.topic });
1665
+ this.dedupSuppressed += 1;
1666
+ return true;
1667
+ }
1585
1668
  this.seenMessageIds.set(message.messageId, now);
1669
+ this.dedupAccepted += 1;
1586
1670
  while (this.seenMessageIds.size > this.dedupMaxEntries) {
1587
1671
  const oldest = this.seenMessageIds.keys().next().value;
1588
1672
  if (oldest === void 0) break;
@@ -1612,10 +1696,11 @@ var CrossTabDataBus = class _CrossTabDataBus {
1612
1696
  buffer = [];
1613
1697
  this.replayBuffers.set(message.topic, buffer);
1614
1698
  }
1615
- buffer.push(message);
1699
+ const storedMessage = message;
1700
+ buffer.push(storedMessage);
1616
1701
  if (buffer.length > this.replayMaxPerTopic) buffer.shift();
1617
1702
  if (this.replayPersistence) {
1618
- void this.replayPersistence.append(message).catch((error) => this.reportError(error));
1703
+ void this.replayPersistence.append(storedMessage).catch((error) => this.reportError(error));
1619
1704
  }
1620
1705
  }
1621
1706
  async hydrateReplay() {
@@ -1892,10 +1977,48 @@ function createIndexedDbReplayPersistence(options) {
1892
1977
  transaction.oncomplete = () => resolve();
1893
1978
  transaction.onerror = () => reject(transaction.error ?? new Error("Failed to clear topic replay history."));
1894
1979
  });
1980
+ },
1981
+ async clearBefore(timestamp) {
1982
+ const db = await open();
1983
+ await new Promise((resolve, reject) => {
1984
+ const transaction = db.transaction(storeName, "readwrite");
1985
+ const store = transaction.objectStore(storeName);
1986
+ const request = store.getAll();
1987
+ request.onsuccess = () => {
1988
+ for (const record of request.result) {
1989
+ const messages = record.messages.filter((message) => (message.timestamp ?? 0) >= timestamp);
1990
+ if (messages.length === 0) store.delete(record.topic);
1991
+ else if (messages.length !== record.messages.length) store.put({ topic: record.topic, messages });
1992
+ }
1993
+ };
1994
+ request.onerror = () => reject(request.error ?? new Error("Failed to read replay history."));
1995
+ transaction.oncomplete = () => resolve();
1996
+ transaction.onerror = () => reject(transaction.error ?? new Error("Failed to prune replay history."));
1997
+ });
1895
1998
  }
1896
1999
  };
1897
2000
  }
1898
2001
 
2002
+ // src/core/publication.ts
2003
+ function parseDataBusPublication(value, fallbackTopic) {
2004
+ if (!value || typeof value !== "object") {
2005
+ return fallbackTopic ? { topic: fallbackTopic, data: value } : null;
2006
+ }
2007
+ const frame = value;
2008
+ const nested = frame.publication && typeof frame.publication === "object" ? frame.publication : null;
2009
+ const publication = nested ?? frame;
2010
+ const topic = typeof publication.topic === "string" ? publication.topic : fallbackTopic;
2011
+ if (!topic) return null;
2012
+ const hasMetadataEnvelope = fallbackTopic !== void 0 && Object.prototype.hasOwnProperty.call(publication, "data") && (typeof publication.messageId === "string" || typeof publication.timestamp === "number");
2013
+ const data = nested || fallbackTopic === void 0 || hasMetadataEnvelope ? publication.data : value;
2014
+ return {
2015
+ topic,
2016
+ data,
2017
+ ...typeof publication.messageId === "string" ? { messageId: publication.messageId } : {},
2018
+ ...typeof publication.timestamp === "number" ? { timestamp: publication.timestamp } : {}
2019
+ };
2020
+ }
2021
+
1899
2022
  // src/websocket.ts
1900
2023
  var WS_OPEN = 1;
1901
2024
  var WebSocketTransport = class {
@@ -1943,12 +2066,18 @@ var WebSocketTransport = class {
1943
2066
  this.sendFrame({ op: "unsubscribe", topic });
1944
2067
  }
1945
2068
  /** Publish `data` to `topic` as a JSON frame. Requires an open socket. */
1946
- publish(topic, data) {
2069
+ publish(topic, data, options) {
1947
2070
  if (data instanceof ArrayBuffer) {
1948
- this.sendBinaryFrame(topic, data);
2071
+ this.sendBinaryFrame(topic, data, options?.messageId, options?.timestamp);
1949
2072
  return;
1950
2073
  }
1951
- this.sendFrame({ op: "publish", topic, data });
2074
+ this.sendFrame({
2075
+ op: "publish",
2076
+ topic,
2077
+ data,
2078
+ ...options?.messageId === void 0 ? {} : { messageId: options.messageId },
2079
+ ...options?.timestamp === void 0 ? {} : { timestamp: options.timestamp }
2080
+ });
1952
2081
  }
1953
2082
  /** Close the socket and drop all state. Safe to call multiple times. */
1954
2083
  stop() {
@@ -1968,7 +2097,17 @@ var WebSocketTransport = class {
1968
2097
  }
1969
2098
  this.socket.send(JSON.stringify(payload));
1970
2099
  }
1971
- sendBinaryFrame(topic, data) {
2100
+ sendBinaryFrame(topic, data, messageId, timestamp) {
2101
+ if (messageId !== void 0 || timestamp !== void 0) {
2102
+ this.sendFrame({
2103
+ op: "publish",
2104
+ topic,
2105
+ data: Array.from(new Uint8Array(data)),
2106
+ ...messageId === void 0 ? {} : { messageId },
2107
+ ...timestamp === void 0 ? {} : { timestamp }
2108
+ });
2109
+ return;
2110
+ }
1972
2111
  if (this.socket?.readyState !== WS_OPEN) {
1973
2112
  this.handlers?.onError(new Error('WebSocket is not open; dropped "publish" frame.'));
1974
2113
  return;
@@ -2008,9 +2147,8 @@ var WebSocketTransport = class {
2008
2147
  return;
2009
2148
  }
2010
2149
  if (!parsed || typeof parsed !== "object") return;
2011
- const frame = parsed;
2012
- if (typeof frame.topic !== "string") return;
2013
- this.handlers?.onMessage({ topic: frame.topic, data: frame.data });
2150
+ const publication = parseDataBusPublication(parsed);
2151
+ if (publication) this.handlers?.onMessage(publication);
2014
2152
  }
2015
2153
  };
2016
2154
  function defaultWebSocketFactory(url, protocols) {