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
@@ -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) {
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);
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);
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);
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,7 +670,15 @@ var WorkerClusterRuntime = class {
668
670
  default:
669
671
  break;
670
672
  }
671
- this.handlers.onControl(message.action, message.topic, message.data);
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);
672
682
  if (message.action !== "PUBLISH") this.updateLoad();
673
683
  }
674
684
  /**
@@ -785,7 +795,7 @@ var WorkerClusterRuntime = class {
785
795
  * Local execution updates the assignment map and route synchronously, bypassing
786
796
  * the BroadcastChannel latency.
787
797
  */
788
- sendControl(targetWorkerId, action, topic, topicKey, data) {
798
+ sendControl(targetWorkerId, action, topic, topicKey, data, metadata) {
789
799
  if (targetWorkerId === this.workerId) {
790
800
  switch (action) {
791
801
  case "SUBSCRIBE":
@@ -799,7 +809,14 @@ var WorkerClusterRuntime = class {
799
809
  default:
800
810
  break;
801
811
  }
802
- this.handlers.onControl(action, topic, data);
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);
803
820
  if (action !== "PUBLISH") this.updateLoad();
804
821
  return true;
805
822
  }
@@ -810,7 +827,9 @@ var WorkerClusterRuntime = class {
810
827
  action,
811
828
  topic,
812
829
  topicKey,
813
- ...data === void 0 ? {} : { data }
830
+ ...data === void 0 ? {} : { data },
831
+ ...metadata?.messageId === void 0 ? {} : { messageId: metadata.messageId },
832
+ ...metadata?.timestamp === void 0 ? {} : { timestamp: metadata.timestamp }
814
833
  });
815
834
  }
816
835
  /** Post a message on the BroadcastChannel. Returns false on postMessage failure. */
@@ -1205,6 +1224,8 @@ var CrossTabDataBus = class _CrossTabDataBus {
1205
1224
  dedupTtlMs;
1206
1225
  dedupEnabled;
1207
1226
  seenMessageIds = /* @__PURE__ */ new Map();
1227
+ dedupSuppressed = 0;
1228
+ dedupAccepted = 0;
1208
1229
  activeConfig;
1209
1230
  status = "disconnected";
1210
1231
  started = false;
@@ -1262,7 +1283,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
1262
1283
  handlers: {
1263
1284
  // The cluster calls `onControl` when it receives a SUBSCRIBE/UNSUBSCRIBE/PUBLISH
1264
1285
  // control message — meaning the owning Worker has delegated the action to us.
1265
- onControl: (action, topic, data) => {
1286
+ onControl: (action, topic, data, messageId, timestamp) => {
1266
1287
  switch (action) {
1267
1288
  case "SUBSCRIBE":
1268
1289
  if (this.subscribeTransport(topic)) this.traceSubscription("subscribe", topic);
@@ -1271,7 +1292,14 @@ var CrossTabDataBus = class _CrossTabDataBus {
1271
1292
  if (this.unsubscribeTransport(topic)) this.traceSubscription("unsubscribe", topic);
1272
1293
  break;
1273
1294
  case "PUBLISH":
1274
- this.runTransport(() => this.transport.publish(topic, data));
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
+ ));
1275
1303
  break;
1276
1304
  default:
1277
1305
  break;
@@ -1450,10 +1478,61 @@ var CrossTabDataBus = class _CrossTabDataBus {
1450
1478
  }
1451
1479
  this.cluster.unsubscribe(topic);
1452
1480
  }
1481
+ /** Clear all in-memory replay buffers and, when supported, durable history. */
1482
+ async clearReplay() {
1483
+ this.replayBuffers?.clear();
1484
+ if (this.replayPersistence?.clear) {
1485
+ try {
1486
+ await this.replayPersistence.clear();
1487
+ } catch (error) {
1488
+ this.reportError(error);
1489
+ throw error;
1490
+ }
1491
+ }
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
+ }
1453
1532
  /** Publish a message to `topic`. The owning Worker delivers it to the transport. */
1454
- publish(topic, data) {
1533
+ publish(topic, data, options) {
1455
1534
  this.ensureStarted();
1456
- if (!this.cluster.publish(topic, data)) {
1535
+ if (!this.cluster.publish(topic, data, options)) {
1457
1536
  this.reportError(
1458
1537
  new Error("Failed to send the publish control message to the owning worker.")
1459
1538
  );
@@ -1540,8 +1619,13 @@ var CrossTabDataBus = class _CrossTabDataBus {
1540
1619
  for (const [id, timestamp] of this.seenMessageIds) {
1541
1620
  if (now - timestamp > this.dedupTtlMs) this.seenMessageIds.delete(id);
1542
1621
  }
1543
- if (this.seenMessageIds.has(message.messageId)) return true;
1622
+ if (this.seenMessageIds.has(message.messageId)) {
1623
+ this.trace.event({ type: "reliability", operation: "dedup_suppressed", topic: message.topic });
1624
+ this.dedupSuppressed += 1;
1625
+ return true;
1626
+ }
1544
1627
  this.seenMessageIds.set(message.messageId, now);
1628
+ this.dedupAccepted += 1;
1545
1629
  while (this.seenMessageIds.size > this.dedupMaxEntries) {
1546
1630
  const oldest = this.seenMessageIds.keys().next().value;
1547
1631
  if (oldest === void 0) break;
@@ -1571,10 +1655,11 @@ var CrossTabDataBus = class _CrossTabDataBus {
1571
1655
  buffer = [];
1572
1656
  this.replayBuffers.set(message.topic, buffer);
1573
1657
  }
1574
- buffer.push(message);
1658
+ const storedMessage = message;
1659
+ buffer.push(storedMessage);
1575
1660
  if (buffer.length > this.replayMaxPerTopic) buffer.shift();
1576
1661
  if (this.replayPersistence) {
1577
- void this.replayPersistence.append(message).catch((error) => this.reportError(error));
1662
+ void this.replayPersistence.append(storedMessage).catch((error) => this.reportError(error));
1578
1663
  }
1579
1664
  }
1580
1665
  async hydrateReplay() {
@@ -1799,6 +1884,26 @@ function selectWorkerBackend(mode, availability = {}) {
1799
1884
  return hasDedicated ? "dedicated" : hasShared ? "shared" : "local";
1800
1885
  }
1801
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
+
1802
1907
  export {
1803
1908
  createBrowserEnvironment,
1804
1909
  getOrCreateTabId,
@@ -1812,6 +1917,7 @@ export {
1812
1917
  topicMatchesPattern,
1813
1918
  WorkerClusterRuntime,
1814
1919
  CrossTabDataBus,
1920
+ parseDataBusPublication,
1815
1921
  selectWorkerBackend
1816
1922
  };
1817
- //# sourceMappingURL=chunk-SI7N5KNJ.js.map
1923
+ //# sourceMappingURL=chunk-ZOPNTR4E.js.map