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
@@ -298,6 +298,13 @@ function writeJson(storage, key, value) {
298
298
  } catch {
299
299
  }
300
300
  }
301
+ function publicationMetadata(messageId, timestamp) {
302
+ if (messageId === void 0 && timestamp === void 0) return void 0;
303
+ return {
304
+ ...messageId === void 0 ? {} : { messageId },
305
+ ...timestamp === void 0 ? {} : { timestamp }
306
+ };
307
+ }
301
308
  function listKeys(storage, prefix) {
302
309
  try {
303
310
  return Array.from({ length: storage.length }, (_, index) => storage.key(index)).filter(
@@ -527,26 +534,21 @@ var WorkerClusterRuntime = class {
527
534
  this.sendRouteReleased(owner.workerId, topic, topicKey, generation);
528
535
  }
529
536
  }
530
- /**
531
- * Publish a message to `topic`, routing through the owning Worker (or self if
532
- * no owner is found). Returns false when the control message could not be
533
- * posted to a remote owner, so the caller can surface the failure instead of
534
- * silently dropping the publication.
535
- */
536
- publish(topic, data, messageId) {
537
+ publish(topic, data, metadataOrMessageId) {
538
+ const metadata = typeof metadataOrMessageId === "string" ? { messageId: metadataOrMessageId } : metadataOrMessageId;
537
539
  const topicKey = this.rememberTopic(topic);
538
540
  if (this.assignedTopics.has(topicKey)) {
539
- return this.sendControl(this.workerId, "PUBLISH", topic, topicKey, data, messageId);
541
+ return this.sendControl(this.workerId, "PUBLISH", topic, topicKey, data, metadata);
540
542
  }
541
543
  for (const pattern of this.assignedTopics.values()) {
542
544
  if (pattern !== topic && topicMatchesPattern(pattern, topic)) {
543
- return this.sendControl(this.workerId, "PUBLISH", topic, topicKey, data, messageId);
545
+ return this.sendControl(this.workerId, "PUBLISH", topic, topicKey, data, metadata);
544
546
  }
545
547
  }
546
548
  const workers = this.readWorkers();
547
549
  const route = this.readRoute(topicKey);
548
550
  const target = this.routeOwnerIsLive(route, workers) ? route?.workerId ?? this.workerId : this.workerId;
549
- return this.sendControl(target, "PUBLISH", topic, topicKey, data, messageId);
551
+ return this.sendControl(target, "PUBLISH", topic, topicKey, data, metadata);
550
552
  }
551
553
  /** True when `route` exists and its owner worker is among `workers`.
552
554
  * Shared by subscribe (skip re-assignment) and publish (route to owner).
@@ -668,11 +670,15 @@ var WorkerClusterRuntime = class {
668
670
  default:
669
671
  break;
670
672
  }
671
- if (message.messageId === void 0) {
672
- this.handlers.onControl(message.action, message.topic, message.data);
673
- } else {
674
- this.handlers.onControl(message.action, message.topic, message.data, message.messageId);
675
- }
673
+ const metadata = publicationMetadata(message.messageId, message.timestamp);
674
+ if (metadata) this.handlers.onControl(
675
+ message.action,
676
+ message.topic,
677
+ message.data,
678
+ metadata.messageId,
679
+ metadata.timestamp
680
+ );
681
+ else this.handlers.onControl(message.action, message.topic, message.data);
676
682
  if (message.action !== "PUBLISH") this.updateLoad();
677
683
  }
678
684
  /**
@@ -789,7 +795,7 @@ var WorkerClusterRuntime = class {
789
795
  * Local execution updates the assignment map and route synchronously, bypassing
790
796
  * the BroadcastChannel latency.
791
797
  */
792
- sendControl(targetWorkerId, action, topic, topicKey, data, messageId) {
798
+ sendControl(targetWorkerId, action, topic, topicKey, data, metadata) {
793
799
  if (targetWorkerId === this.workerId) {
794
800
  switch (action) {
795
801
  case "SUBSCRIBE":
@@ -803,8 +809,14 @@ var WorkerClusterRuntime = class {
803
809
  default:
804
810
  break;
805
811
  }
806
- if (messageId === void 0) this.handlers.onControl(action, topic, data);
807
- else this.handlers.onControl(action, topic, data, messageId);
812
+ if (metadata) this.handlers.onControl(
813
+ action,
814
+ topic,
815
+ data,
816
+ metadata.messageId,
817
+ metadata.timestamp
818
+ );
819
+ else this.handlers.onControl(action, topic, data);
808
820
  if (action !== "PUBLISH") this.updateLoad();
809
821
  return true;
810
822
  }
@@ -816,7 +828,8 @@ var WorkerClusterRuntime = class {
816
828
  topic,
817
829
  topicKey,
818
830
  ...data === void 0 ? {} : { data },
819
- ...messageId === void 0 ? {} : { messageId }
831
+ ...metadata?.messageId === void 0 ? {} : { messageId: metadata.messageId },
832
+ ...metadata?.timestamp === void 0 ? {} : { timestamp: metadata.timestamp }
820
833
  });
821
834
  }
822
835
  /** Post a message on the BroadcastChannel. Returns false on postMessage failure. */
@@ -1211,6 +1224,8 @@ var CrossTabDataBus = class _CrossTabDataBus {
1211
1224
  dedupTtlMs;
1212
1225
  dedupEnabled;
1213
1226
  seenMessageIds = /* @__PURE__ */ new Map();
1227
+ dedupSuppressed = 0;
1228
+ dedupAccepted = 0;
1214
1229
  activeConfig;
1215
1230
  status = "disconnected";
1216
1231
  started = false;
@@ -1268,7 +1283,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
1268
1283
  handlers: {
1269
1284
  // The cluster calls `onControl` when it receives a SUBSCRIBE/UNSUBSCRIBE/PUBLISH
1270
1285
  // control message — meaning the owning Worker has delegated the action to us.
1271
- onControl: (action, topic, data, messageId) => {
1286
+ onControl: (action, topic, data, messageId, timestamp) => {
1272
1287
  switch (action) {
1273
1288
  case "SUBSCRIBE":
1274
1289
  if (this.subscribeTransport(topic)) this.traceSubscription("subscribe", topic);
@@ -1277,7 +1292,14 @@ var CrossTabDataBus = class _CrossTabDataBus {
1277
1292
  if (this.unsubscribeTransport(topic)) this.traceSubscription("unsubscribe", topic);
1278
1293
  break;
1279
1294
  case "PUBLISH":
1280
- this.runTransport(() => this.transport.publish(topic, data, messageId ? { messageId } : void 0));
1295
+ this.runTransport(() => this.transport.publish(
1296
+ topic,
1297
+ data,
1298
+ messageId === void 0 && timestamp === void 0 ? void 0 : {
1299
+ ...messageId === void 0 ? {} : { messageId },
1300
+ ...timestamp === void 0 ? {} : { timestamp }
1301
+ }
1302
+ ));
1281
1303
  break;
1282
1304
  default:
1283
1305
  break;
@@ -1468,10 +1490,49 @@ var CrossTabDataBus = class _CrossTabDataBus {
1468
1490
  }
1469
1491
  }
1470
1492
  }
1493
+ /** Clear replay history for one exact topic, including durable storage. */
1494
+ async clearReplayTopic(topic) {
1495
+ this.replayBuffers?.delete(topic);
1496
+ if (this.replayPersistence?.clearTopic) {
1497
+ try {
1498
+ await this.replayPersistence.clearTopic(topic);
1499
+ } catch (error) {
1500
+ this.reportError(error);
1501
+ throw error;
1502
+ }
1503
+ }
1504
+ }
1505
+ /** Remove replay entries older than an epoch-millisecond cutoff. */
1506
+ async clearReplayBefore(timestamp) {
1507
+ if (!Number.isFinite(timestamp)) throw new TypeError("timestamp must be finite.");
1508
+ if (this.replayBuffers) {
1509
+ for (const [topic, messages] of this.replayBuffers) {
1510
+ const kept = messages.filter((message) => (message.timestamp ?? 0) >= timestamp);
1511
+ if (kept.length) this.replayBuffers.set(topic, kept);
1512
+ else this.replayBuffers.delete(topic);
1513
+ }
1514
+ }
1515
+ if (this.replayPersistence?.clearBefore) await this.replayPersistence.clearBefore(timestamp);
1516
+ }
1517
+ /** Return bounded deduplication counters for diagnostics and health checks. */
1518
+ getDedupStats() {
1519
+ return {
1520
+ enabled: this.dedupEnabled,
1521
+ tracked: this.seenMessageIds.size,
1522
+ suppressed: this.dedupSuppressed,
1523
+ accepted: this.dedupAccepted
1524
+ };
1525
+ }
1526
+ /** Drop all remembered IDs and reset dedup counters. */
1527
+ resetDedup() {
1528
+ this.seenMessageIds.clear();
1529
+ this.dedupSuppressed = 0;
1530
+ this.dedupAccepted = 0;
1531
+ }
1471
1532
  /** Publish a message to `topic`. The owning Worker delivers it to the transport. */
1472
1533
  publish(topic, data, options) {
1473
1534
  this.ensureStarted();
1474
- if (!this.cluster.publish(topic, data, options?.messageId)) {
1535
+ if (!this.cluster.publish(topic, data, options)) {
1475
1536
  this.reportError(
1476
1537
  new Error("Failed to send the publish control message to the owning worker.")
1477
1538
  );
@@ -1560,9 +1621,11 @@ var CrossTabDataBus = class _CrossTabDataBus {
1560
1621
  }
1561
1622
  if (this.seenMessageIds.has(message.messageId)) {
1562
1623
  this.trace.event({ type: "reliability", operation: "dedup_suppressed", topic: message.topic });
1624
+ this.dedupSuppressed += 1;
1563
1625
  return true;
1564
1626
  }
1565
1627
  this.seenMessageIds.set(message.messageId, now);
1628
+ this.dedupAccepted += 1;
1566
1629
  while (this.seenMessageIds.size > this.dedupMaxEntries) {
1567
1630
  const oldest = this.seenMessageIds.keys().next().value;
1568
1631
  if (oldest === void 0) break;
@@ -1592,10 +1655,11 @@ var CrossTabDataBus = class _CrossTabDataBus {
1592
1655
  buffer = [];
1593
1656
  this.replayBuffers.set(message.topic, buffer);
1594
1657
  }
1595
- buffer.push(message);
1658
+ const storedMessage = message;
1659
+ buffer.push(storedMessage);
1596
1660
  if (buffer.length > this.replayMaxPerTopic) buffer.shift();
1597
1661
  if (this.replayPersistence) {
1598
- void this.replayPersistence.append(message).catch((error) => this.reportError(error));
1662
+ void this.replayPersistence.append(storedMessage).catch((error) => this.reportError(error));
1599
1663
  }
1600
1664
  }
1601
1665
  async hydrateReplay() {
@@ -1820,6 +1884,26 @@ function selectWorkerBackend(mode, availability = {}) {
1820
1884
  return hasDedicated ? "dedicated" : hasShared ? "shared" : "local";
1821
1885
  }
1822
1886
 
1887
+ // src/core/publication.ts
1888
+ function parseDataBusPublication(value, fallbackTopic) {
1889
+ if (!value || typeof value !== "object") {
1890
+ return fallbackTopic ? { topic: fallbackTopic, data: value } : null;
1891
+ }
1892
+ const frame = value;
1893
+ const nested = frame.publication && typeof frame.publication === "object" ? frame.publication : null;
1894
+ const publication = nested ?? frame;
1895
+ const topic = typeof publication.topic === "string" ? publication.topic : fallbackTopic;
1896
+ if (!topic) return null;
1897
+ const hasMetadataEnvelope = fallbackTopic !== void 0 && Object.prototype.hasOwnProperty.call(publication, "data") && (typeof publication.messageId === "string" || typeof publication.timestamp === "number");
1898
+ const data = nested || fallbackTopic === void 0 || hasMetadataEnvelope ? publication.data : value;
1899
+ return {
1900
+ topic,
1901
+ data,
1902
+ ...typeof publication.messageId === "string" ? { messageId: publication.messageId } : {},
1903
+ ...typeof publication.timestamp === "number" ? { timestamp: publication.timestamp } : {}
1904
+ };
1905
+ }
1906
+
1823
1907
  export {
1824
1908
  createBrowserEnvironment,
1825
1909
  getOrCreateTabId,
@@ -1833,6 +1917,7 @@ export {
1833
1917
  topicMatchesPattern,
1834
1918
  WorkerClusterRuntime,
1835
1919
  CrossTabDataBus,
1920
+ parseDataBusPublication,
1836
1921
  selectWorkerBackend
1837
1922
  };
1838
- //# sourceMappingURL=chunk-WWUY2FV2.js.map
1923
+ //# sourceMappingURL=chunk-ZOPNTR4E.js.map