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/index.cjs
CHANGED
|
@@ -562,6 +562,7 @@ var WorkerClusterRuntime = class {
|
|
|
562
562
|
projectedLoads.set(owner.workerId, (projectedLoads.get(owner.workerId) ?? owner.load) + 1);
|
|
563
563
|
const generation = (previous?.generation ?? 0) + 1;
|
|
564
564
|
this.writeRoute(topicKey, owner, previous?.workerId, generation);
|
|
565
|
+
this.handlers.onDiagnostic?.({ operation: "route_migration", topic });
|
|
565
566
|
this.flushStorage();
|
|
566
567
|
this.handlers.onControl("UNSUBSCRIBE", topic);
|
|
567
568
|
this.sendRouteReleased(owner.workerId, topic, topicKey, generation);
|
|
@@ -573,20 +574,20 @@ var WorkerClusterRuntime = class {
|
|
|
573
574
|
* posted to a remote owner, so the caller can surface the failure instead of
|
|
574
575
|
* silently dropping the publication.
|
|
575
576
|
*/
|
|
576
|
-
publish(topic, data) {
|
|
577
|
+
publish(topic, data, messageId) {
|
|
577
578
|
const topicKey = this.rememberTopic(topic);
|
|
578
579
|
if (this.assignedTopics.has(topicKey)) {
|
|
579
|
-
return this.sendControl(this.workerId, "PUBLISH", topic, topicKey, data);
|
|
580
|
+
return this.sendControl(this.workerId, "PUBLISH", topic, topicKey, data, messageId);
|
|
580
581
|
}
|
|
581
582
|
for (const pattern of this.assignedTopics.values()) {
|
|
582
583
|
if (pattern !== topic && topicMatchesPattern(pattern, topic)) {
|
|
583
|
-
return this.sendControl(this.workerId, "PUBLISH", topic, topicKey, data);
|
|
584
|
+
return this.sendControl(this.workerId, "PUBLISH", topic, topicKey, data, messageId);
|
|
584
585
|
}
|
|
585
586
|
}
|
|
586
587
|
const workers = this.readWorkers();
|
|
587
588
|
const route = this.readRoute(topicKey);
|
|
588
589
|
const target = this.routeOwnerIsLive(route, workers) ? route?.workerId ?? this.workerId : this.workerId;
|
|
589
|
-
return this.sendControl(target, "PUBLISH", topic, topicKey, data);
|
|
590
|
+
return this.sendControl(target, "PUBLISH", topic, topicKey, data, messageId);
|
|
590
591
|
}
|
|
591
592
|
/** True when `route` exists and its owner worker is among `workers`.
|
|
592
593
|
* Shared by subscribe (skip re-assignment) and publish (route to owner).
|
|
@@ -708,7 +709,11 @@ var WorkerClusterRuntime = class {
|
|
|
708
709
|
default:
|
|
709
710
|
break;
|
|
710
711
|
}
|
|
711
|
-
|
|
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
|
+
}
|
|
712
717
|
if (message.action !== "PUBLISH") this.updateLoad();
|
|
713
718
|
}
|
|
714
719
|
/**
|
|
@@ -825,7 +830,7 @@ var WorkerClusterRuntime = class {
|
|
|
825
830
|
* Local execution updates the assignment map and route synchronously, bypassing
|
|
826
831
|
* the BroadcastChannel latency.
|
|
827
832
|
*/
|
|
828
|
-
sendControl(targetWorkerId, action, topic, topicKey, data) {
|
|
833
|
+
sendControl(targetWorkerId, action, topic, topicKey, data, messageId) {
|
|
829
834
|
if (targetWorkerId === this.workerId) {
|
|
830
835
|
switch (action) {
|
|
831
836
|
case "SUBSCRIBE":
|
|
@@ -839,7 +844,8 @@ var WorkerClusterRuntime = class {
|
|
|
839
844
|
default:
|
|
840
845
|
break;
|
|
841
846
|
}
|
|
842
|
-
this.handlers.onControl(action, topic, data);
|
|
847
|
+
if (messageId === void 0) this.handlers.onControl(action, topic, data);
|
|
848
|
+
else this.handlers.onControl(action, topic, data, messageId);
|
|
843
849
|
if (action !== "PUBLISH") this.updateLoad();
|
|
844
850
|
return true;
|
|
845
851
|
}
|
|
@@ -850,7 +856,8 @@ var WorkerClusterRuntime = class {
|
|
|
850
856
|
action,
|
|
851
857
|
topic,
|
|
852
858
|
topicKey,
|
|
853
|
-
...data === void 0 ? {} : { data }
|
|
859
|
+
...data === void 0 ? {} : { data },
|
|
860
|
+
...messageId === void 0 ? {} : { messageId }
|
|
854
861
|
});
|
|
855
862
|
}
|
|
856
863
|
/** Post a message on the BroadcastChannel. Returns false on postMessage failure. */
|
|
@@ -946,6 +953,8 @@ var WorkerClusterRuntime = class {
|
|
|
946
953
|
...route,
|
|
947
954
|
confirmedAt: this.environment.now()
|
|
948
955
|
});
|
|
956
|
+
const topic = this.knownTopics.get(topicKey);
|
|
957
|
+
if (topic) this.handlers.onDiagnostic?.({ operation: "route_ack", topic });
|
|
949
958
|
}
|
|
950
959
|
/** Remove routes whose topic has no subscribers and whose TTL has expired. */
|
|
951
960
|
cleanupOrphanedRoutes(workers) {
|
|
@@ -1239,6 +1248,10 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1239
1248
|
initialConfig;
|
|
1240
1249
|
hasInitialConfig;
|
|
1241
1250
|
trace;
|
|
1251
|
+
dedupMaxEntries;
|
|
1252
|
+
dedupTtlMs;
|
|
1253
|
+
dedupEnabled;
|
|
1254
|
+
seenMessageIds = /* @__PURE__ */ new Map();
|
|
1242
1255
|
activeConfig;
|
|
1243
1256
|
status = "disconnected";
|
|
1244
1257
|
started = false;
|
|
@@ -1277,17 +1290,26 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1277
1290
|
this.replayBuffers = replay ? /* @__PURE__ */ new Map() : null;
|
|
1278
1291
|
this.replayPersistence = replay?.persistence ?? null;
|
|
1279
1292
|
this.replayHydration = this.hydrateReplay();
|
|
1280
|
-
const { autoStart, initialConfig, trace, transport, ...clusterOptions } = options;
|
|
1293
|
+
const { autoStart, initialConfig, trace, transport, dedup, ...clusterOptions } = options;
|
|
1281
1294
|
this.transport = transport;
|
|
1282
1295
|
this.initialConfig = initialConfig;
|
|
1283
1296
|
this.hasInitialConfig = "initialConfig" in options;
|
|
1284
1297
|
this.trace = new DataBusTraceReporter(trace);
|
|
1298
|
+
this.dedupMaxEntries = dedup?.maxEntries ?? 1e3;
|
|
1299
|
+
this.dedupTtlMs = dedup?.ttlMs ?? 6e4;
|
|
1300
|
+
this.dedupEnabled = dedup !== void 0;
|
|
1301
|
+
if (!Number.isSafeInteger(this.dedupMaxEntries) || this.dedupMaxEntries <= 0) {
|
|
1302
|
+
throw new TypeError("dedup.maxEntries must be a positive safe integer.");
|
|
1303
|
+
}
|
|
1304
|
+
if (!Number.isFinite(this.dedupTtlMs) || this.dedupTtlMs <= 0) {
|
|
1305
|
+
throw new TypeError("dedup.ttlMs must be a positive finite number.");
|
|
1306
|
+
}
|
|
1285
1307
|
this.cluster = new WorkerClusterRuntime({
|
|
1286
1308
|
...clusterOptions,
|
|
1287
1309
|
handlers: {
|
|
1288
1310
|
// The cluster calls `onControl` when it receives a SUBSCRIBE/UNSUBSCRIBE/PUBLISH
|
|
1289
1311
|
// control message — meaning the owning Worker has delegated the action to us.
|
|
1290
|
-
onControl: (action, topic, data) => {
|
|
1312
|
+
onControl: (action, topic, data, messageId) => {
|
|
1291
1313
|
switch (action) {
|
|
1292
1314
|
case "SUBSCRIBE":
|
|
1293
1315
|
if (this.subscribeTransport(topic)) this.traceSubscription("subscribe", topic);
|
|
@@ -1296,7 +1318,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1296
1318
|
if (this.unsubscribeTransport(topic)) this.traceSubscription("unsubscribe", topic);
|
|
1297
1319
|
break;
|
|
1298
1320
|
case "PUBLISH":
|
|
1299
|
-
this.runTransport(() => this.transport.publish(topic, data));
|
|
1321
|
+
this.runTransport(() => this.transport.publish(topic, data, messageId ? { messageId } : void 0));
|
|
1300
1322
|
break;
|
|
1301
1323
|
default:
|
|
1302
1324
|
break;
|
|
@@ -1321,6 +1343,9 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1321
1343
|
this.trace.event({ type: "lifecycle", action: "resume" });
|
|
1322
1344
|
this.trace.start();
|
|
1323
1345
|
this.resumeTransport();
|
|
1346
|
+
},
|
|
1347
|
+
onDiagnostic: (event) => {
|
|
1348
|
+
this.trace.event({ type: "reliability", ...event });
|
|
1324
1349
|
}
|
|
1325
1350
|
}
|
|
1326
1351
|
});
|
|
@@ -1467,12 +1492,27 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1467
1492
|
if (handlers.size > 0) return;
|
|
1468
1493
|
this.topicHandlers.delete(topic);
|
|
1469
1494
|
this.replayBuffers?.delete(topic);
|
|
1495
|
+
if (this.replayPersistence?.clearTopic) {
|
|
1496
|
+
void this.replayPersistence.clearTopic(topic).catch((error) => this.reportError(error));
|
|
1497
|
+
}
|
|
1470
1498
|
this.cluster.unsubscribe(topic);
|
|
1471
1499
|
}
|
|
1500
|
+
/** Clear all in-memory replay buffers and, when supported, durable history. */
|
|
1501
|
+
async clearReplay() {
|
|
1502
|
+
this.replayBuffers?.clear();
|
|
1503
|
+
if (this.replayPersistence?.clear) {
|
|
1504
|
+
try {
|
|
1505
|
+
await this.replayPersistence.clear();
|
|
1506
|
+
} catch (error) {
|
|
1507
|
+
this.reportError(error);
|
|
1508
|
+
throw error;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1472
1512
|
/** Publish a message to `topic`. The owning Worker delivers it to the transport. */
|
|
1473
|
-
publish(topic, data) {
|
|
1513
|
+
publish(topic, data, options) {
|
|
1474
1514
|
this.ensureStarted();
|
|
1475
|
-
if (!this.cluster.publish(topic, data)) {
|
|
1515
|
+
if (!this.cluster.publish(topic, data, options?.messageId)) {
|
|
1476
1516
|
this.reportError(
|
|
1477
1517
|
new Error("Failed to send the publish control message to the owning worker.")
|
|
1478
1518
|
);
|
|
@@ -1522,6 +1562,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1522
1562
|
else await this.transport.stop();
|
|
1523
1563
|
} finally {
|
|
1524
1564
|
this.transportSubscribedTopics.clear();
|
|
1565
|
+
this.seenMessageIds.clear();
|
|
1525
1566
|
this.started = false;
|
|
1526
1567
|
this.stopping = false;
|
|
1527
1568
|
this.suspended = false;
|
|
@@ -1539,6 +1580,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1539
1580
|
* and dispatches locally.
|
|
1540
1581
|
*/
|
|
1541
1582
|
handleTransportMessage(message) {
|
|
1583
|
+
if (this.isDuplicate(message)) return;
|
|
1542
1584
|
this.trace.recordReceived(message.topic);
|
|
1543
1585
|
if (!this.cluster.isAssigned(message.topic)) {
|
|
1544
1586
|
this.trace.recordDiscarded(message.topic);
|
|
@@ -1551,6 +1593,24 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1551
1593
|
}
|
|
1552
1594
|
this.trace.recordDiscarded(message.topic);
|
|
1553
1595
|
}
|
|
1596
|
+
isDuplicate(message) {
|
|
1597
|
+
if (!this.dedupEnabled || !message.messageId) return false;
|
|
1598
|
+
const now = Date.now();
|
|
1599
|
+
for (const [id, timestamp] of this.seenMessageIds) {
|
|
1600
|
+
if (now - timestamp > this.dedupTtlMs) this.seenMessageIds.delete(id);
|
|
1601
|
+
}
|
|
1602
|
+
if (this.seenMessageIds.has(message.messageId)) {
|
|
1603
|
+
this.trace.event({ type: "reliability", operation: "dedup_suppressed", topic: message.topic });
|
|
1604
|
+
return true;
|
|
1605
|
+
}
|
|
1606
|
+
this.seenMessageIds.set(message.messageId, now);
|
|
1607
|
+
while (this.seenMessageIds.size > this.dedupMaxEntries) {
|
|
1608
|
+
const oldest = this.seenMessageIds.keys().next().value;
|
|
1609
|
+
if (oldest === void 0) break;
|
|
1610
|
+
this.seenMessageIds.delete(oldest);
|
|
1611
|
+
}
|
|
1612
|
+
return false;
|
|
1613
|
+
}
|
|
1554
1614
|
/** Deliver a message to every local handler registered for its topic,
|
|
1555
1615
|
* plus every handler registered with a wildcard subscription that matches
|
|
1556
1616
|
* (e.g. a handler subscribed to "chat.*" receives "chat.room.1"). */
|
|
@@ -1635,6 +1695,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1635
1695
|
const now = Date.now();
|
|
1636
1696
|
if (now - this.lastRecoveryAt >= _CrossTabDataBus.RECOVERY_COOLDOWN_MS) {
|
|
1637
1697
|
this.lastRecoveryAt = now;
|
|
1698
|
+
this.trace.event({ type: "reliability", operation: "transport_recovery", attempt: 1 });
|
|
1638
1699
|
setTimeout(() => {
|
|
1639
1700
|
if (this.stopping || !this.started || this.suspended) return;
|
|
1640
1701
|
if (this.status !== "error") return;
|
|
@@ -1834,6 +1895,24 @@ function createIndexedDbReplayPersistence(options) {
|
|
|
1834
1895
|
transaction.oncomplete = () => resolve();
|
|
1835
1896
|
transaction.onerror = () => reject(transaction.error ?? new Error("Failed to persist replay history."));
|
|
1836
1897
|
});
|
|
1898
|
+
},
|
|
1899
|
+
async clear() {
|
|
1900
|
+
const db = await open();
|
|
1901
|
+
await new Promise((resolve, reject) => {
|
|
1902
|
+
const transaction = db.transaction(storeName, "readwrite");
|
|
1903
|
+
transaction.objectStore(storeName).clear();
|
|
1904
|
+
transaction.oncomplete = () => resolve();
|
|
1905
|
+
transaction.onerror = () => reject(transaction.error ?? new Error("Failed to clear replay history."));
|
|
1906
|
+
});
|
|
1907
|
+
},
|
|
1908
|
+
async clearTopic(topic) {
|
|
1909
|
+
const db = await open();
|
|
1910
|
+
await new Promise((resolve, reject) => {
|
|
1911
|
+
const transaction = db.transaction(storeName, "readwrite");
|
|
1912
|
+
transaction.objectStore(storeName).delete(topic);
|
|
1913
|
+
transaction.oncomplete = () => resolve();
|
|
1914
|
+
transaction.onerror = () => reject(transaction.error ?? new Error("Failed to clear topic replay history."));
|
|
1915
|
+
});
|
|
1837
1916
|
}
|
|
1838
1917
|
};
|
|
1839
1918
|
}
|
|
@@ -1885,12 +1964,12 @@ var WebSocketTransport = class {
|
|
|
1885
1964
|
this.sendFrame({ op: "unsubscribe", topic });
|
|
1886
1965
|
}
|
|
1887
1966
|
/** Publish `data` to `topic` as a JSON frame. Requires an open socket. */
|
|
1888
|
-
publish(topic, data) {
|
|
1967
|
+
publish(topic, data, options) {
|
|
1889
1968
|
if (data instanceof ArrayBuffer) {
|
|
1890
|
-
this.sendBinaryFrame(topic, data);
|
|
1969
|
+
this.sendBinaryFrame(topic, data, options?.messageId);
|
|
1891
1970
|
return;
|
|
1892
1971
|
}
|
|
1893
|
-
this.sendFrame({ op: "publish", topic, data });
|
|
1972
|
+
this.sendFrame({ op: "publish", topic, data, ...options?.messageId ? { messageId: options.messageId } : {} });
|
|
1894
1973
|
}
|
|
1895
1974
|
/** Close the socket and drop all state. Safe to call multiple times. */
|
|
1896
1975
|
stop() {
|
|
@@ -1910,7 +1989,11 @@ var WebSocketTransport = class {
|
|
|
1910
1989
|
}
|
|
1911
1990
|
this.socket.send(JSON.stringify(payload));
|
|
1912
1991
|
}
|
|
1913
|
-
sendBinaryFrame(topic, data) {
|
|
1992
|
+
sendBinaryFrame(topic, data, messageId) {
|
|
1993
|
+
if (messageId) {
|
|
1994
|
+
this.sendFrame({ op: "publish", topic, data: Array.from(new Uint8Array(data)), messageId });
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1914
1997
|
if (this.socket?.readyState !== WS_OPEN) {
|
|
1915
1998
|
this.handlers?.onError(new Error('WebSocket is not open; dropped "publish" frame.'));
|
|
1916
1999
|
return;
|
|
@@ -1952,7 +2035,12 @@ var WebSocketTransport = class {
|
|
|
1952
2035
|
if (!parsed || typeof parsed !== "object") return;
|
|
1953
2036
|
const frame = parsed;
|
|
1954
2037
|
if (typeof frame.topic !== "string") return;
|
|
1955
|
-
|
|
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);
|
|
1956
2044
|
}
|
|
1957
2045
|
};
|
|
1958
2046
|
function defaultWebSocketFactory(url, protocols) {
|