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.
- package/CHANGELOG.md +22 -1
- package/dist/centrifuge-protocol.d.ts +4 -0
- package/dist/centrifuge-protocol.d.ts.map +1 -1
- package/dist/centrifuge-session.d.ts.map +1 -1
- package/dist/centrifuge.d.ts +2 -1
- package/dist/centrifuge.d.ts.map +1 -1
- package/dist/centrifuge.js +13 -10
- package/dist/centrifuge.js.map +2 -2
- package/dist/centrifuge.shared.worker.js +7 -4
- package/dist/centrifuge.shared.worker.js.map +2 -2
- package/dist/centrifuge.worker.js +7 -4
- package/dist/centrifuge.worker.js.map +2 -2
- package/dist/{chunk-YUSQSNCX.js → chunk-WWUY2FV2.js} +75 -14
- package/dist/chunk-WWUY2FV2.js.map +7 -0
- package/dist/cjs/centrifuge.cjs +86 -22
- package/dist/cjs/centrifuge.cjs.map +2 -2
- package/dist/cjs/index.cjs +106 -18
- package/dist/cjs/index.cjs.map +2 -2
- package/dist/core/cluster.d.ts +7 -2
- package/dist/core/cluster.d.ts.map +1 -1
- package/dist/core/data-bus.d.ts +16 -2
- package/dist/core/data-bus.d.ts.map +1 -1
- package/dist/core/replay-persistence.d.ts +4 -0
- package/dist/core/replay-persistence.d.ts.map +1 -1
- package/dist/core/trace.d.ts +10 -1
- package/dist/core/trace.d.ts.map +1 -1
- package/dist/core/types.d.ts +10 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +33 -6
- package/dist/index.js.map +2 -2
- package/dist/websocket.d.ts +2 -2
- package/dist/websocket.d.ts.map +1 -1
- package/docs/api.md +15 -2
- package/docs/capabilities.md +3 -3
- package/docs/roadmap.md +7 -7
- package/docs/zh/api.md +13 -2
- package/docs/zh/capabilities.md +3 -3
- package/docs/zh/roadmap.md +7 -7
- package/package.json +1 -1
- package/dist/chunk-YUSQSNCX.js.map +0 -7
package/dist/cjs/centrifuge.cjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|