cross-tab-worker-databus 0.8.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 (41) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +1 -0
  3. package/README.zh.md +1 -0
  4. package/dist/centrifuge-protocol.d.ts +4 -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.js +29 -11
  8. package/dist/centrifuge.js.map +2 -2
  9. package/dist/centrifuge.shared.worker.js +37 -6
  10. package/dist/centrifuge.shared.worker.js.map +3 -3
  11. package/dist/centrifuge.worker.js +37 -6
  12. package/dist/centrifuge.worker.js.map +3 -3
  13. package/dist/{chunk-WWUY2FV2.js → chunk-ZOPNTR4E.js} +110 -25
  14. package/dist/chunk-ZOPNTR4E.js.map +7 -0
  15. package/dist/cjs/centrifuge.cjs +137 -34
  16. package/dist/cjs/centrifuge.cjs.map +4 -4
  17. package/dist/cjs/index.cjs +145 -37
  18. package/dist/cjs/index.cjs.map +3 -3
  19. package/dist/core/cluster.d.ts +3 -2
  20. package/dist/core/cluster.d.ts.map +1 -1
  21. package/dist/core/data-bus.d.ts +16 -0
  22. package/dist/core/data-bus.d.ts.map +1 -1
  23. package/dist/core/publication.d.ts +10 -0
  24. package/dist/core/publication.d.ts.map +1 -0
  25. package/dist/core/replay-persistence.d.ts +2 -0
  26. package/dist/core/replay-persistence.d.ts.map +1 -1
  27. package/dist/core/types.d.ts +19 -7
  28. package/dist/core/types.d.ts.map +1 -1
  29. package/dist/index.d.ts +2 -2
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +39 -14
  32. package/dist/index.js.map +2 -2
  33. package/dist/websocket.d.ts.map +1 -1
  34. package/docs/api.md +25 -4
  35. package/docs/architecture.md +3 -1
  36. package/docs/transports.md +9 -5
  37. package/docs/zh/api.md +25 -4
  38. package/docs/zh/architecture.md +3 -1
  39. package/docs/zh/transports.md +8 -4
  40. package/package.json +1 -1
  41. package/dist/chunk-WWUY2FV2.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, messageId) {
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, messageId);
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, messageId);
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, messageId);
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,11 +711,15 @@ var WorkerClusterRuntime = class {
709
711
  default:
710
712
  break;
711
713
  }
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
- }
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);
717
723
  if (message.action !== "PUBLISH") this.updateLoad();
718
724
  }
719
725
  /**
@@ -830,7 +836,7 @@ var WorkerClusterRuntime = class {
830
836
  * Local execution updates the assignment map and route synchronously, bypassing
831
837
  * the BroadcastChannel latency.
832
838
  */
833
- sendControl(targetWorkerId, action, topic, topicKey, data, messageId) {
839
+ sendControl(targetWorkerId, action, topic, topicKey, data, metadata) {
834
840
  if (targetWorkerId === this.workerId) {
835
841
  switch (action) {
836
842
  case "SUBSCRIBE":
@@ -844,8 +850,14 @@ var WorkerClusterRuntime = class {
844
850
  default:
845
851
  break;
846
852
  }
847
- if (messageId === void 0) this.handlers.onControl(action, topic, data);
848
- else this.handlers.onControl(action, topic, data, messageId);
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);
849
861
  if (action !== "PUBLISH") this.updateLoad();
850
862
  return true;
851
863
  }
@@ -857,7 +869,8 @@ var WorkerClusterRuntime = class {
857
869
  topic,
858
870
  topicKey,
859
871
  ...data === void 0 ? {} : { data },
860
- ...messageId === void 0 ? {} : { messageId }
872
+ ...metadata?.messageId === void 0 ? {} : { messageId: metadata.messageId },
873
+ ...metadata?.timestamp === void 0 ? {} : { timestamp: metadata.timestamp }
861
874
  });
862
875
  }
863
876
  /** Post a message on the BroadcastChannel. Returns false on postMessage failure. */
@@ -1252,6 +1265,8 @@ var CrossTabDataBus = class _CrossTabDataBus {
1252
1265
  dedupTtlMs;
1253
1266
  dedupEnabled;
1254
1267
  seenMessageIds = /* @__PURE__ */ new Map();
1268
+ dedupSuppressed = 0;
1269
+ dedupAccepted = 0;
1255
1270
  activeConfig;
1256
1271
  status = "disconnected";
1257
1272
  started = false;
@@ -1309,7 +1324,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
1309
1324
  handlers: {
1310
1325
  // The cluster calls `onControl` when it receives a SUBSCRIBE/UNSUBSCRIBE/PUBLISH
1311
1326
  // control message — meaning the owning Worker has delegated the action to us.
1312
- onControl: (action, topic, data, messageId) => {
1327
+ onControl: (action, topic, data, messageId, timestamp) => {
1313
1328
  switch (action) {
1314
1329
  case "SUBSCRIBE":
1315
1330
  if (this.subscribeTransport(topic)) this.traceSubscription("subscribe", topic);
@@ -1318,7 +1333,14 @@ var CrossTabDataBus = class _CrossTabDataBus {
1318
1333
  if (this.unsubscribeTransport(topic)) this.traceSubscription("unsubscribe", topic);
1319
1334
  break;
1320
1335
  case "PUBLISH":
1321
- this.runTransport(() => this.transport.publish(topic, data, messageId ? { messageId } : void 0));
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
+ ));
1322
1344
  break;
1323
1345
  default:
1324
1346
  break;
@@ -1509,10 +1531,49 @@ var CrossTabDataBus = class _CrossTabDataBus {
1509
1531
  }
1510
1532
  }
1511
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
+ }
1512
1573
  /** Publish a message to `topic`. The owning Worker delivers it to the transport. */
1513
1574
  publish(topic, data, options) {
1514
1575
  this.ensureStarted();
1515
- if (!this.cluster.publish(topic, data, options?.messageId)) {
1576
+ if (!this.cluster.publish(topic, data, options)) {
1516
1577
  this.reportError(
1517
1578
  new Error("Failed to send the publish control message to the owning worker.")
1518
1579
  );
@@ -1601,9 +1662,11 @@ var CrossTabDataBus = class _CrossTabDataBus {
1601
1662
  }
1602
1663
  if (this.seenMessageIds.has(message.messageId)) {
1603
1664
  this.trace.event({ type: "reliability", operation: "dedup_suppressed", topic: message.topic });
1665
+ this.dedupSuppressed += 1;
1604
1666
  return true;
1605
1667
  }
1606
1668
  this.seenMessageIds.set(message.messageId, now);
1669
+ this.dedupAccepted += 1;
1607
1670
  while (this.seenMessageIds.size > this.dedupMaxEntries) {
1608
1671
  const oldest = this.seenMessageIds.keys().next().value;
1609
1672
  if (oldest === void 0) break;
@@ -1633,10 +1696,11 @@ var CrossTabDataBus = class _CrossTabDataBus {
1633
1696
  buffer = [];
1634
1697
  this.replayBuffers.set(message.topic, buffer);
1635
1698
  }
1636
- buffer.push(message);
1699
+ const storedMessage = message;
1700
+ buffer.push(storedMessage);
1637
1701
  if (buffer.length > this.replayMaxPerTopic) buffer.shift();
1638
1702
  if (this.replayPersistence) {
1639
- void this.replayPersistence.append(message).catch((error) => this.reportError(error));
1703
+ void this.replayPersistence.append(storedMessage).catch((error) => this.reportError(error));
1640
1704
  }
1641
1705
  }
1642
1706
  async hydrateReplay() {
@@ -1913,10 +1977,48 @@ function createIndexedDbReplayPersistence(options) {
1913
1977
  transaction.oncomplete = () => resolve();
1914
1978
  transaction.onerror = () => reject(transaction.error ?? new Error("Failed to clear topic replay history."));
1915
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
+ });
1916
1998
  }
1917
1999
  };
1918
2000
  }
1919
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
+
1920
2022
  // src/websocket.ts
1921
2023
  var WS_OPEN = 1;
1922
2024
  var WebSocketTransport = class {
@@ -1966,10 +2068,16 @@ var WebSocketTransport = class {
1966
2068
  /** Publish `data` to `topic` as a JSON frame. Requires an open socket. */
1967
2069
  publish(topic, data, options) {
1968
2070
  if (data instanceof ArrayBuffer) {
1969
- this.sendBinaryFrame(topic, data, options?.messageId);
2071
+ this.sendBinaryFrame(topic, data, options?.messageId, options?.timestamp);
1970
2072
  return;
1971
2073
  }
1972
- this.sendFrame({ op: "publish", topic, data, ...options?.messageId ? { messageId: options.messageId } : {} });
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
+ });
1973
2081
  }
1974
2082
  /** Close the socket and drop all state. Safe to call multiple times. */
1975
2083
  stop() {
@@ -1989,9 +2097,15 @@ var WebSocketTransport = class {
1989
2097
  }
1990
2098
  this.socket.send(JSON.stringify(payload));
1991
2099
  }
1992
- sendBinaryFrame(topic, data, messageId) {
1993
- if (messageId) {
1994
- this.sendFrame({ op: "publish", topic, data: Array.from(new Uint8Array(data)), messageId });
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
+ });
1995
2109
  return;
1996
2110
  }
1997
2111
  if (this.socket?.readyState !== WS_OPEN) {
@@ -2033,14 +2147,8 @@ var WebSocketTransport = class {
2033
2147
  return;
2034
2148
  }
2035
2149
  if (!parsed || typeof parsed !== "object") return;
2036
- const frame = parsed;
2037
- if (typeof frame.topic !== "string") return;
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);
2150
+ const publication = parseDataBusPublication(parsed);
2151
+ if (publication) this.handlers?.onMessage(publication);
2044
2152
  }
2045
2153
  };
2046
2154
  function defaultWebSocketFactory(url, protocols) {