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
@@ -537,6 +537,7 @@ var WorkerClusterRuntime = class {
537
537
  projectedLoads.set(owner.workerId, (projectedLoads.get(owner.workerId) ?? owner.load) + 1);
538
538
  const generation = (previous?.generation ?? 0) + 1;
539
539
  this.writeRoute(topicKey, owner, previous?.workerId, generation);
540
+ this.handlers.onDiagnostic?.({ operation: "route_migration", topic });
540
541
  this.flushStorage();
541
542
  this.handlers.onControl("UNSUBSCRIBE", topic);
542
543
  this.sendRouteReleased(owner.workerId, topic, topicKey, generation);
@@ -548,20 +549,20 @@ var WorkerClusterRuntime = class {
548
549
  * posted to a remote owner, so the caller can surface the failure instead of
549
550
  * silently dropping the publication.
550
551
  */
551
- publish(topic, data) {
552
+ publish(topic, data, messageId) {
552
553
  const topicKey = this.rememberTopic(topic);
553
554
  if (this.assignedTopics.has(topicKey)) {
554
- return this.sendControl(this.workerId, "PUBLISH", topic, topicKey, data);
555
+ return this.sendControl(this.workerId, "PUBLISH", topic, topicKey, data, messageId);
555
556
  }
556
557
  for (const pattern of this.assignedTopics.values()) {
557
558
  if (pattern !== topic && topicMatchesPattern(pattern, topic)) {
558
- return this.sendControl(this.workerId, "PUBLISH", topic, topicKey, data);
559
+ return this.sendControl(this.workerId, "PUBLISH", topic, topicKey, data, messageId);
559
560
  }
560
561
  }
561
562
  const workers = this.readWorkers();
562
563
  const route = this.readRoute(topicKey);
563
564
  const target = this.routeOwnerIsLive(route, workers) ? route?.workerId ?? this.workerId : this.workerId;
564
- return this.sendControl(target, "PUBLISH", topic, topicKey, data);
565
+ return this.sendControl(target, "PUBLISH", topic, topicKey, data, messageId);
565
566
  }
566
567
  /** True when `route` exists and its owner worker is among `workers`.
567
568
  * Shared by subscribe (skip re-assignment) and publish (route to owner).
@@ -683,7 +684,11 @@ var WorkerClusterRuntime = class {
683
684
  default:
684
685
  break;
685
686
  }
686
- this.handlers.onControl(message.action, message.topic, message.data);
687
+ if (message.messageId === void 0) {
688
+ this.handlers.onControl(message.action, message.topic, message.data);
689
+ } else {
690
+ this.handlers.onControl(message.action, message.topic, message.data, message.messageId);
691
+ }
687
692
  if (message.action !== "PUBLISH") this.updateLoad();
688
693
  }
689
694
  /**
@@ -800,7 +805,7 @@ var WorkerClusterRuntime = class {
800
805
  * Local execution updates the assignment map and route synchronously, bypassing
801
806
  * the BroadcastChannel latency.
802
807
  */
803
- sendControl(targetWorkerId, action, topic, topicKey, data) {
808
+ sendControl(targetWorkerId, action, topic, topicKey, data, messageId) {
804
809
  if (targetWorkerId === this.workerId) {
805
810
  switch (action) {
806
811
  case "SUBSCRIBE":
@@ -814,7 +819,8 @@ var WorkerClusterRuntime = class {
814
819
  default:
815
820
  break;
816
821
  }
817
- this.handlers.onControl(action, topic, data);
822
+ if (messageId === void 0) this.handlers.onControl(action, topic, data);
823
+ else this.handlers.onControl(action, topic, data, messageId);
818
824
  if (action !== "PUBLISH") this.updateLoad();
819
825
  return true;
820
826
  }
@@ -825,7 +831,8 @@ var WorkerClusterRuntime = class {
825
831
  action,
826
832
  topic,
827
833
  topicKey,
828
- ...data === void 0 ? {} : { data }
834
+ ...data === void 0 ? {} : { data },
835
+ ...messageId === void 0 ? {} : { messageId }
829
836
  });
830
837
  }
831
838
  /** Post a message on the BroadcastChannel. Returns false on postMessage failure. */
@@ -921,6 +928,8 @@ var WorkerClusterRuntime = class {
921
928
  ...route,
922
929
  confirmedAt: this.environment.now()
923
930
  });
931
+ const topic = this.knownTopics.get(topicKey);
932
+ if (topic) this.handlers.onDiagnostic?.({ operation: "route_ack", topic });
924
933
  }
925
934
  /** Remove routes whose topic has no subscribers and whose TTL has expired. */
926
935
  cleanupOrphanedRoutes(workers) {
@@ -1214,6 +1223,10 @@ var CrossTabDataBus = class _CrossTabDataBus {
1214
1223
  initialConfig;
1215
1224
  hasInitialConfig;
1216
1225
  trace;
1226
+ dedupMaxEntries;
1227
+ dedupTtlMs;
1228
+ dedupEnabled;
1229
+ seenMessageIds = /* @__PURE__ */ new Map();
1217
1230
  activeConfig;
1218
1231
  status = "disconnected";
1219
1232
  started = false;
@@ -1252,17 +1265,26 @@ var CrossTabDataBus = class _CrossTabDataBus {
1252
1265
  this.replayBuffers = replay ? /* @__PURE__ */ new Map() : null;
1253
1266
  this.replayPersistence = replay?.persistence ?? null;
1254
1267
  this.replayHydration = this.hydrateReplay();
1255
- const { autoStart, initialConfig, trace, transport, ...clusterOptions } = options;
1268
+ const { autoStart, initialConfig, trace, transport, dedup, ...clusterOptions } = options;
1256
1269
  this.transport = transport;
1257
1270
  this.initialConfig = initialConfig;
1258
1271
  this.hasInitialConfig = "initialConfig" in options;
1259
1272
  this.trace = new DataBusTraceReporter(trace);
1273
+ this.dedupMaxEntries = dedup?.maxEntries ?? 1e3;
1274
+ this.dedupTtlMs = dedup?.ttlMs ?? 6e4;
1275
+ this.dedupEnabled = dedup !== void 0;
1276
+ if (!Number.isSafeInteger(this.dedupMaxEntries) || this.dedupMaxEntries <= 0) {
1277
+ throw new TypeError("dedup.maxEntries must be a positive safe integer.");
1278
+ }
1279
+ if (!Number.isFinite(this.dedupTtlMs) || this.dedupTtlMs <= 0) {
1280
+ throw new TypeError("dedup.ttlMs must be a positive finite number.");
1281
+ }
1260
1282
  this.cluster = new WorkerClusterRuntime({
1261
1283
  ...clusterOptions,
1262
1284
  handlers: {
1263
1285
  // The cluster calls `onControl` when it receives a SUBSCRIBE/UNSUBSCRIBE/PUBLISH
1264
1286
  // control message — meaning the owning Worker has delegated the action to us.
1265
- onControl: (action, topic, data) => {
1287
+ onControl: (action, topic, data, messageId) => {
1266
1288
  switch (action) {
1267
1289
  case "SUBSCRIBE":
1268
1290
  if (this.subscribeTransport(topic)) this.traceSubscription("subscribe", topic);
@@ -1271,7 +1293,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
1271
1293
  if (this.unsubscribeTransport(topic)) this.traceSubscription("unsubscribe", topic);
1272
1294
  break;
1273
1295
  case "PUBLISH":
1274
- this.runTransport(() => this.transport.publish(topic, data));
1296
+ this.runTransport(() => this.transport.publish(topic, data, messageId ? { messageId } : void 0));
1275
1297
  break;
1276
1298
  default:
1277
1299
  break;
@@ -1296,6 +1318,9 @@ var CrossTabDataBus = class _CrossTabDataBus {
1296
1318
  this.trace.event({ type: "lifecycle", action: "resume" });
1297
1319
  this.trace.start();
1298
1320
  this.resumeTransport();
1321
+ },
1322
+ onDiagnostic: (event) => {
1323
+ this.trace.event({ type: "reliability", ...event });
1299
1324
  }
1300
1325
  }
1301
1326
  });
@@ -1442,12 +1467,27 @@ var CrossTabDataBus = class _CrossTabDataBus {
1442
1467
  if (handlers.size > 0) return;
1443
1468
  this.topicHandlers.delete(topic);
1444
1469
  this.replayBuffers?.delete(topic);
1470
+ if (this.replayPersistence?.clearTopic) {
1471
+ void this.replayPersistence.clearTopic(topic).catch((error) => this.reportError(error));
1472
+ }
1445
1473
  this.cluster.unsubscribe(topic);
1446
1474
  }
1475
+ /** Clear all in-memory replay buffers and, when supported, durable history. */
1476
+ async clearReplay() {
1477
+ this.replayBuffers?.clear();
1478
+ if (this.replayPersistence?.clear) {
1479
+ try {
1480
+ await this.replayPersistence.clear();
1481
+ } catch (error) {
1482
+ this.reportError(error);
1483
+ throw error;
1484
+ }
1485
+ }
1486
+ }
1447
1487
  /** Publish a message to `topic`. The owning Worker delivers it to the transport. */
1448
- publish(topic, data) {
1488
+ publish(topic, data, options) {
1449
1489
  this.ensureStarted();
1450
- if (!this.cluster.publish(topic, data)) {
1490
+ if (!this.cluster.publish(topic, data, options?.messageId)) {
1451
1491
  this.reportError(
1452
1492
  new Error("Failed to send the publish control message to the owning worker.")
1453
1493
  );
@@ -1497,6 +1537,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
1497
1537
  else await this.transport.stop();
1498
1538
  } finally {
1499
1539
  this.transportSubscribedTopics.clear();
1540
+ this.seenMessageIds.clear();
1500
1541
  this.started = false;
1501
1542
  this.stopping = false;
1502
1543
  this.suspended = false;
@@ -1514,6 +1555,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
1514
1555
  * and dispatches locally.
1515
1556
  */
1516
1557
  handleTransportMessage(message) {
1558
+ if (this.isDuplicate(message)) return;
1517
1559
  this.trace.recordReceived(message.topic);
1518
1560
  if (!this.cluster.isAssigned(message.topic)) {
1519
1561
  this.trace.recordDiscarded(message.topic);
@@ -1526,6 +1568,24 @@ var CrossTabDataBus = class _CrossTabDataBus {
1526
1568
  }
1527
1569
  this.trace.recordDiscarded(message.topic);
1528
1570
  }
1571
+ isDuplicate(message) {
1572
+ if (!this.dedupEnabled || !message.messageId) return false;
1573
+ const now = Date.now();
1574
+ for (const [id, timestamp] of this.seenMessageIds) {
1575
+ if (now - timestamp > this.dedupTtlMs) this.seenMessageIds.delete(id);
1576
+ }
1577
+ if (this.seenMessageIds.has(message.messageId)) {
1578
+ this.trace.event({ type: "reliability", operation: "dedup_suppressed", topic: message.topic });
1579
+ return true;
1580
+ }
1581
+ this.seenMessageIds.set(message.messageId, now);
1582
+ while (this.seenMessageIds.size > this.dedupMaxEntries) {
1583
+ const oldest = this.seenMessageIds.keys().next().value;
1584
+ if (oldest === void 0) break;
1585
+ this.seenMessageIds.delete(oldest);
1586
+ }
1587
+ return false;
1588
+ }
1529
1589
  /** Deliver a message to every local handler registered for its topic,
1530
1590
  * plus every handler registered with a wildcard subscription that matches
1531
1591
  * (e.g. a handler subscribed to "chat.*" receives "chat.room.1"). */
@@ -1610,6 +1670,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
1610
1670
  const now = Date.now();
1611
1671
  if (now - this.lastRecoveryAt >= _CrossTabDataBus.RECOVERY_COOLDOWN_MS) {
1612
1672
  this.lastRecoveryAt = now;
1673
+ this.trace.event({ type: "reliability", operation: "transport_recovery", attempt: 1 });
1613
1674
  setTimeout(() => {
1614
1675
  if (this.stopping || !this.started || this.suspended) return;
1615
1676
  if (this.status !== "error") return;
@@ -1790,7 +1851,7 @@ var CentrifugeSession = class {
1790
1851
  return;
1791
1852
  case "PUBLISH":
1792
1853
  case "PUBLISH_BIN":
1793
- this.publish(message.topic, message.data);
1854
+ this.publish(message.topic, message.data, message.messageId);
1794
1855
  return;
1795
1856
  case "STOP":
1796
1857
  this.stop();
@@ -1855,9 +1916,10 @@ var CentrifugeSession = class {
1855
1916
  subscription.unsubscribe();
1856
1917
  }
1857
1918
  /** Publish a message to the Centrifuge channel. */
1858
- publish(topic, data) {
1919
+ publish(topic, data, messageId) {
1859
1920
  if (!this.client) return this.postError(new Error("Centrifuge client is not initialized."));
1860
- void this.client.publish(topic, data).catch((error) => this.postError(error));
1921
+ const payload = messageId ? { data, messageId } : data;
1922
+ void this.client.publish(topic, payload).catch((error) => this.postError(error));
1861
1923
  }
1862
1924
  /** Forward a publication to the transport. Binary payloads take the
1863
1925
  * zero-copy `MESSAGE_BIN` path when `transferable` is enabled; everything
@@ -1869,7 +1931,9 @@ var CentrifugeSession = class {
1869
1931
  this.post({ type: "MESSAGE_BIN", topic, data }, [data]);
1870
1932
  return;
1871
1933
  }
1872
- this.post({ type: "MESSAGE", topic, data });
1934
+ const messageId = data && typeof data === "object" && typeof data.messageId === "string" ? data.messageId : void 0;
1935
+ const payload = messageId && Object.prototype.hasOwnProperty.call(data, "data") ? data.data : data;
1936
+ this.post({ type: "MESSAGE", topic, data: payload, ...messageId ? { messageId } : {} });
1873
1937
  }
1874
1938
  /** Disconnect the client and clear all subscriptions. */
1875
1939
  stop() {
@@ -1999,12 +2063,12 @@ var CentrifugeWorkerTransport = class {
1999
2063
  * Publish data to `topic`. Binary data (ArrayBuffer) is sent via Transferable
2000
2064
  * when `transferable` is enabled, avoiding a structured-clone cycle.
2001
2065
  */
2002
- publish(topic, data) {
2066
+ publish(topic, data, options) {
2003
2067
  if (this.transferable && data instanceof ArrayBuffer) {
2004
- this.post({ type: "PUBLISH_BIN", topic, data }, [data]);
2068
+ this.post({ type: "PUBLISH_BIN", topic, data, ...options?.messageId ? { messageId: options.messageId } : {} }, [data]);
2005
2069
  return;
2006
2070
  }
2007
- this.post({ type: "PUBLISH", topic, data });
2071
+ this.post({ type: "PUBLISH", topic, data, ...options?.messageId ? { messageId: options.messageId } : {} });
2008
2072
  }
2009
2073
  /**
2010
2074
  * Gracefully stop the transport: send STOP, clean up event listeners, and
@@ -2075,8 +2139,8 @@ var CentrifugeWorkerTransport = class {
2075
2139
  * and the local-session sink — all three feed into this single dispatcher. */
2076
2140
  handleOutput(message) {
2077
2141
  if (message.type === "STATUS") this.handlers?.onStatus(message.status);
2078
- if (message.type === "MESSAGE") this.handlers?.onMessage({ topic: message.topic, data: message.data });
2079
- if (message.type === "MESSAGE_BIN") this.handlers?.onMessage({ topic: message.topic, data: message.data });
2142
+ if (message.type === "MESSAGE") this.handlers?.onMessage({ topic: message.topic, data: message.data, ...message.messageId ? { messageId: message.messageId } : {} });
2143
+ if (message.type === "MESSAGE_BIN") this.handlers?.onMessage({ topic: message.topic, data: message.data, ...message.messageId ? { messageId: message.messageId } : {} });
2080
2144
  if (message.type === "ERROR") this.handlers?.onError(deserializeWorkerError(message.error));
2081
2145
  }
2082
2146
  /** Handle a Worker-level failure (crash, message decode error). Discards the