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.
- package/CHANGELOG.md +28 -1
- package/README.md +1 -0
- package/README.zh.md +1 -0
- package/dist/centrifuge-protocol.d.ts +8 -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 +31 -10
- package/dist/centrifuge.js.map +2 -2
- package/dist/centrifuge.shared.worker.js +38 -4
- package/dist/centrifuge.shared.worker.js.map +3 -3
- package/dist/centrifuge.worker.js +38 -4
- package/dist/centrifuge.worker.js.map +3 -3
- package/dist/{chunk-SI7N5KNJ.js → chunk-ZOPNTR4E.js} +128 -22
- package/dist/chunk-ZOPNTR4E.js.map +7 -0
- package/dist/cjs/centrifuge.cjs +157 -30
- package/dist/cjs/centrifuge.cjs.map +4 -4
- package/dist/cjs/index.cjs +166 -28
- package/dist/cjs/index.cjs.map +3 -3
- package/dist/core/cluster.d.ts +4 -3
- package/dist/core/cluster.d.ts.map +1 -1
- package/dist/core/data-bus.d.ts +20 -2
- package/dist/core/data-bus.d.ts.map +1 -1
- package/dist/core/publication.d.ts +10 -0
- package/dist/core/publication.d.ts.map +1 -0
- package/dist/core/replay-persistence.d.ts +2 -0
- package/dist/core/replay-persistence.d.ts.map +1 -1
- package/dist/core/trace.d.ts +2 -11
- package/dist/core/trace.d.ts.map +1 -1
- package/dist/core/types.d.ts +25 -6
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +42 -8
- 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 +35 -4
- package/docs/architecture.md +3 -1
- package/docs/transports.md +9 -5
- package/docs/zh/api.md +35 -4
- package/docs/zh/architecture.md +3 -1
- package/docs/zh/transports.md +8 -4
- package/package.json +1 -1
- package/dist/chunk-SI7N5KNJ.js.map +0 -7
package/dist/cjs/index.cjs
CHANGED
|
@@ -339,6 +339,13 @@ function writeJson(storage, key, value) {
|
|
|
339
339
|
} catch {
|
|
340
340
|
}
|
|
341
341
|
}
|
|
342
|
+
function publicationMetadata(messageId, timestamp) {
|
|
343
|
+
if (messageId === void 0 && timestamp === void 0) return void 0;
|
|
344
|
+
return {
|
|
345
|
+
...messageId === void 0 ? {} : { messageId },
|
|
346
|
+
...timestamp === void 0 ? {} : { timestamp }
|
|
347
|
+
};
|
|
348
|
+
}
|
|
342
349
|
function listKeys(storage, prefix) {
|
|
343
350
|
try {
|
|
344
351
|
return Array.from({ length: storage.length }, (_, index) => storage.key(index)).filter(
|
|
@@ -568,26 +575,21 @@ var WorkerClusterRuntime = class {
|
|
|
568
575
|
this.sendRouteReleased(owner.workerId, topic, topicKey, generation);
|
|
569
576
|
}
|
|
570
577
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
* no owner is found). Returns false when the control message could not be
|
|
574
|
-
* posted to a remote owner, so the caller can surface the failure instead of
|
|
575
|
-
* silently dropping the publication.
|
|
576
|
-
*/
|
|
577
|
-
publish(topic, data) {
|
|
578
|
+
publish(topic, data, metadataOrMessageId) {
|
|
579
|
+
const metadata = typeof metadataOrMessageId === "string" ? { messageId: metadataOrMessageId } : metadataOrMessageId;
|
|
578
580
|
const topicKey = this.rememberTopic(topic);
|
|
579
581
|
if (this.assignedTopics.has(topicKey)) {
|
|
580
|
-
return this.sendControl(this.workerId, "PUBLISH", topic, topicKey, data);
|
|
582
|
+
return this.sendControl(this.workerId, "PUBLISH", topic, topicKey, data, metadata);
|
|
581
583
|
}
|
|
582
584
|
for (const pattern of this.assignedTopics.values()) {
|
|
583
585
|
if (pattern !== topic && topicMatchesPattern(pattern, topic)) {
|
|
584
|
-
return this.sendControl(this.workerId, "PUBLISH", topic, topicKey, data);
|
|
586
|
+
return this.sendControl(this.workerId, "PUBLISH", topic, topicKey, data, metadata);
|
|
585
587
|
}
|
|
586
588
|
}
|
|
587
589
|
const workers = this.readWorkers();
|
|
588
590
|
const route = this.readRoute(topicKey);
|
|
589
591
|
const target = this.routeOwnerIsLive(route, workers) ? route?.workerId ?? this.workerId : this.workerId;
|
|
590
|
-
return this.sendControl(target, "PUBLISH", topic, topicKey, data);
|
|
592
|
+
return this.sendControl(target, "PUBLISH", topic, topicKey, data, metadata);
|
|
591
593
|
}
|
|
592
594
|
/** True when `route` exists and its owner worker is among `workers`.
|
|
593
595
|
* Shared by subscribe (skip re-assignment) and publish (route to owner).
|
|
@@ -709,7 +711,15 @@ var WorkerClusterRuntime = class {
|
|
|
709
711
|
default:
|
|
710
712
|
break;
|
|
711
713
|
}
|
|
712
|
-
|
|
714
|
+
const metadata = publicationMetadata(message.messageId, message.timestamp);
|
|
715
|
+
if (metadata) this.handlers.onControl(
|
|
716
|
+
message.action,
|
|
717
|
+
message.topic,
|
|
718
|
+
message.data,
|
|
719
|
+
metadata.messageId,
|
|
720
|
+
metadata.timestamp
|
|
721
|
+
);
|
|
722
|
+
else this.handlers.onControl(message.action, message.topic, message.data);
|
|
713
723
|
if (message.action !== "PUBLISH") this.updateLoad();
|
|
714
724
|
}
|
|
715
725
|
/**
|
|
@@ -826,7 +836,7 @@ var WorkerClusterRuntime = class {
|
|
|
826
836
|
* Local execution updates the assignment map and route synchronously, bypassing
|
|
827
837
|
* the BroadcastChannel latency.
|
|
828
838
|
*/
|
|
829
|
-
sendControl(targetWorkerId, action, topic, topicKey, data) {
|
|
839
|
+
sendControl(targetWorkerId, action, topic, topicKey, data, metadata) {
|
|
830
840
|
if (targetWorkerId === this.workerId) {
|
|
831
841
|
switch (action) {
|
|
832
842
|
case "SUBSCRIBE":
|
|
@@ -840,7 +850,14 @@ var WorkerClusterRuntime = class {
|
|
|
840
850
|
default:
|
|
841
851
|
break;
|
|
842
852
|
}
|
|
843
|
-
this.handlers.onControl(
|
|
853
|
+
if (metadata) this.handlers.onControl(
|
|
854
|
+
action,
|
|
855
|
+
topic,
|
|
856
|
+
data,
|
|
857
|
+
metadata.messageId,
|
|
858
|
+
metadata.timestamp
|
|
859
|
+
);
|
|
860
|
+
else this.handlers.onControl(action, topic, data);
|
|
844
861
|
if (action !== "PUBLISH") this.updateLoad();
|
|
845
862
|
return true;
|
|
846
863
|
}
|
|
@@ -851,7 +868,9 @@ var WorkerClusterRuntime = class {
|
|
|
851
868
|
action,
|
|
852
869
|
topic,
|
|
853
870
|
topicKey,
|
|
854
|
-
...data === void 0 ? {} : { data }
|
|
871
|
+
...data === void 0 ? {} : { data },
|
|
872
|
+
...metadata?.messageId === void 0 ? {} : { messageId: metadata.messageId },
|
|
873
|
+
...metadata?.timestamp === void 0 ? {} : { timestamp: metadata.timestamp }
|
|
855
874
|
});
|
|
856
875
|
}
|
|
857
876
|
/** Post a message on the BroadcastChannel. Returns false on postMessage failure. */
|
|
@@ -1246,6 +1265,8 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1246
1265
|
dedupTtlMs;
|
|
1247
1266
|
dedupEnabled;
|
|
1248
1267
|
seenMessageIds = /* @__PURE__ */ new Map();
|
|
1268
|
+
dedupSuppressed = 0;
|
|
1269
|
+
dedupAccepted = 0;
|
|
1249
1270
|
activeConfig;
|
|
1250
1271
|
status = "disconnected";
|
|
1251
1272
|
started = false;
|
|
@@ -1303,7 +1324,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1303
1324
|
handlers: {
|
|
1304
1325
|
// The cluster calls `onControl` when it receives a SUBSCRIBE/UNSUBSCRIBE/PUBLISH
|
|
1305
1326
|
// control message — meaning the owning Worker has delegated the action to us.
|
|
1306
|
-
onControl: (action, topic, data) => {
|
|
1327
|
+
onControl: (action, topic, data, messageId, timestamp) => {
|
|
1307
1328
|
switch (action) {
|
|
1308
1329
|
case "SUBSCRIBE":
|
|
1309
1330
|
if (this.subscribeTransport(topic)) this.traceSubscription("subscribe", topic);
|
|
@@ -1312,7 +1333,14 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1312
1333
|
if (this.unsubscribeTransport(topic)) this.traceSubscription("unsubscribe", topic);
|
|
1313
1334
|
break;
|
|
1314
1335
|
case "PUBLISH":
|
|
1315
|
-
this.runTransport(() => this.transport.publish(
|
|
1336
|
+
this.runTransport(() => this.transport.publish(
|
|
1337
|
+
topic,
|
|
1338
|
+
data,
|
|
1339
|
+
messageId === void 0 && timestamp === void 0 ? void 0 : {
|
|
1340
|
+
...messageId === void 0 ? {} : { messageId },
|
|
1341
|
+
...timestamp === void 0 ? {} : { timestamp }
|
|
1342
|
+
}
|
|
1343
|
+
));
|
|
1316
1344
|
break;
|
|
1317
1345
|
default:
|
|
1318
1346
|
break;
|
|
@@ -1491,10 +1519,61 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1491
1519
|
}
|
|
1492
1520
|
this.cluster.unsubscribe(topic);
|
|
1493
1521
|
}
|
|
1522
|
+
/** Clear all in-memory replay buffers and, when supported, durable history. */
|
|
1523
|
+
async clearReplay() {
|
|
1524
|
+
this.replayBuffers?.clear();
|
|
1525
|
+
if (this.replayPersistence?.clear) {
|
|
1526
|
+
try {
|
|
1527
|
+
await this.replayPersistence.clear();
|
|
1528
|
+
} catch (error) {
|
|
1529
|
+
this.reportError(error);
|
|
1530
|
+
throw error;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
/** Clear replay history for one exact topic, including durable storage. */
|
|
1535
|
+
async clearReplayTopic(topic) {
|
|
1536
|
+
this.replayBuffers?.delete(topic);
|
|
1537
|
+
if (this.replayPersistence?.clearTopic) {
|
|
1538
|
+
try {
|
|
1539
|
+
await this.replayPersistence.clearTopic(topic);
|
|
1540
|
+
} catch (error) {
|
|
1541
|
+
this.reportError(error);
|
|
1542
|
+
throw error;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
/** Remove replay entries older than an epoch-millisecond cutoff. */
|
|
1547
|
+
async clearReplayBefore(timestamp) {
|
|
1548
|
+
if (!Number.isFinite(timestamp)) throw new TypeError("timestamp must be finite.");
|
|
1549
|
+
if (this.replayBuffers) {
|
|
1550
|
+
for (const [topic, messages] of this.replayBuffers) {
|
|
1551
|
+
const kept = messages.filter((message) => (message.timestamp ?? 0) >= timestamp);
|
|
1552
|
+
if (kept.length) this.replayBuffers.set(topic, kept);
|
|
1553
|
+
else this.replayBuffers.delete(topic);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
if (this.replayPersistence?.clearBefore) await this.replayPersistence.clearBefore(timestamp);
|
|
1557
|
+
}
|
|
1558
|
+
/** Return bounded deduplication counters for diagnostics and health checks. */
|
|
1559
|
+
getDedupStats() {
|
|
1560
|
+
return {
|
|
1561
|
+
enabled: this.dedupEnabled,
|
|
1562
|
+
tracked: this.seenMessageIds.size,
|
|
1563
|
+
suppressed: this.dedupSuppressed,
|
|
1564
|
+
accepted: this.dedupAccepted
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
/** Drop all remembered IDs and reset dedup counters. */
|
|
1568
|
+
resetDedup() {
|
|
1569
|
+
this.seenMessageIds.clear();
|
|
1570
|
+
this.dedupSuppressed = 0;
|
|
1571
|
+
this.dedupAccepted = 0;
|
|
1572
|
+
}
|
|
1494
1573
|
/** Publish a message to `topic`. The owning Worker delivers it to the transport. */
|
|
1495
|
-
publish(topic, data) {
|
|
1574
|
+
publish(topic, data, options) {
|
|
1496
1575
|
this.ensureStarted();
|
|
1497
|
-
if (!this.cluster.publish(topic, data)) {
|
|
1576
|
+
if (!this.cluster.publish(topic, data, options)) {
|
|
1498
1577
|
this.reportError(
|
|
1499
1578
|
new Error("Failed to send the publish control message to the owning worker.")
|
|
1500
1579
|
);
|
|
@@ -1581,8 +1660,13 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1581
1660
|
for (const [id, timestamp] of this.seenMessageIds) {
|
|
1582
1661
|
if (now - timestamp > this.dedupTtlMs) this.seenMessageIds.delete(id);
|
|
1583
1662
|
}
|
|
1584
|
-
if (this.seenMessageIds.has(message.messageId))
|
|
1663
|
+
if (this.seenMessageIds.has(message.messageId)) {
|
|
1664
|
+
this.trace.event({ type: "reliability", operation: "dedup_suppressed", topic: message.topic });
|
|
1665
|
+
this.dedupSuppressed += 1;
|
|
1666
|
+
return true;
|
|
1667
|
+
}
|
|
1585
1668
|
this.seenMessageIds.set(message.messageId, now);
|
|
1669
|
+
this.dedupAccepted += 1;
|
|
1586
1670
|
while (this.seenMessageIds.size > this.dedupMaxEntries) {
|
|
1587
1671
|
const oldest = this.seenMessageIds.keys().next().value;
|
|
1588
1672
|
if (oldest === void 0) break;
|
|
@@ -1612,10 +1696,11 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1612
1696
|
buffer = [];
|
|
1613
1697
|
this.replayBuffers.set(message.topic, buffer);
|
|
1614
1698
|
}
|
|
1615
|
-
|
|
1699
|
+
const storedMessage = message;
|
|
1700
|
+
buffer.push(storedMessage);
|
|
1616
1701
|
if (buffer.length > this.replayMaxPerTopic) buffer.shift();
|
|
1617
1702
|
if (this.replayPersistence) {
|
|
1618
|
-
void this.replayPersistence.append(
|
|
1703
|
+
void this.replayPersistence.append(storedMessage).catch((error) => this.reportError(error));
|
|
1619
1704
|
}
|
|
1620
1705
|
}
|
|
1621
1706
|
async hydrateReplay() {
|
|
@@ -1892,10 +1977,48 @@ function createIndexedDbReplayPersistence(options) {
|
|
|
1892
1977
|
transaction.oncomplete = () => resolve();
|
|
1893
1978
|
transaction.onerror = () => reject(transaction.error ?? new Error("Failed to clear topic replay history."));
|
|
1894
1979
|
});
|
|
1980
|
+
},
|
|
1981
|
+
async clearBefore(timestamp) {
|
|
1982
|
+
const db = await open();
|
|
1983
|
+
await new Promise((resolve, reject) => {
|
|
1984
|
+
const transaction = db.transaction(storeName, "readwrite");
|
|
1985
|
+
const store = transaction.objectStore(storeName);
|
|
1986
|
+
const request = store.getAll();
|
|
1987
|
+
request.onsuccess = () => {
|
|
1988
|
+
for (const record of request.result) {
|
|
1989
|
+
const messages = record.messages.filter((message) => (message.timestamp ?? 0) >= timestamp);
|
|
1990
|
+
if (messages.length === 0) store.delete(record.topic);
|
|
1991
|
+
else if (messages.length !== record.messages.length) store.put({ topic: record.topic, messages });
|
|
1992
|
+
}
|
|
1993
|
+
};
|
|
1994
|
+
request.onerror = () => reject(request.error ?? new Error("Failed to read replay history."));
|
|
1995
|
+
transaction.oncomplete = () => resolve();
|
|
1996
|
+
transaction.onerror = () => reject(transaction.error ?? new Error("Failed to prune replay history."));
|
|
1997
|
+
});
|
|
1895
1998
|
}
|
|
1896
1999
|
};
|
|
1897
2000
|
}
|
|
1898
2001
|
|
|
2002
|
+
// src/core/publication.ts
|
|
2003
|
+
function parseDataBusPublication(value, fallbackTopic) {
|
|
2004
|
+
if (!value || typeof value !== "object") {
|
|
2005
|
+
return fallbackTopic ? { topic: fallbackTopic, data: value } : null;
|
|
2006
|
+
}
|
|
2007
|
+
const frame = value;
|
|
2008
|
+
const nested = frame.publication && typeof frame.publication === "object" ? frame.publication : null;
|
|
2009
|
+
const publication = nested ?? frame;
|
|
2010
|
+
const topic = typeof publication.topic === "string" ? publication.topic : fallbackTopic;
|
|
2011
|
+
if (!topic) return null;
|
|
2012
|
+
const hasMetadataEnvelope = fallbackTopic !== void 0 && Object.prototype.hasOwnProperty.call(publication, "data") && (typeof publication.messageId === "string" || typeof publication.timestamp === "number");
|
|
2013
|
+
const data = nested || fallbackTopic === void 0 || hasMetadataEnvelope ? publication.data : value;
|
|
2014
|
+
return {
|
|
2015
|
+
topic,
|
|
2016
|
+
data,
|
|
2017
|
+
...typeof publication.messageId === "string" ? { messageId: publication.messageId } : {},
|
|
2018
|
+
...typeof publication.timestamp === "number" ? { timestamp: publication.timestamp } : {}
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
|
|
1899
2022
|
// src/websocket.ts
|
|
1900
2023
|
var WS_OPEN = 1;
|
|
1901
2024
|
var WebSocketTransport = class {
|
|
@@ -1943,12 +2066,18 @@ var WebSocketTransport = class {
|
|
|
1943
2066
|
this.sendFrame({ op: "unsubscribe", topic });
|
|
1944
2067
|
}
|
|
1945
2068
|
/** Publish `data` to `topic` as a JSON frame. Requires an open socket. */
|
|
1946
|
-
publish(topic, data) {
|
|
2069
|
+
publish(topic, data, options) {
|
|
1947
2070
|
if (data instanceof ArrayBuffer) {
|
|
1948
|
-
this.sendBinaryFrame(topic, data);
|
|
2071
|
+
this.sendBinaryFrame(topic, data, options?.messageId, options?.timestamp);
|
|
1949
2072
|
return;
|
|
1950
2073
|
}
|
|
1951
|
-
this.sendFrame({
|
|
2074
|
+
this.sendFrame({
|
|
2075
|
+
op: "publish",
|
|
2076
|
+
topic,
|
|
2077
|
+
data,
|
|
2078
|
+
...options?.messageId === void 0 ? {} : { messageId: options.messageId },
|
|
2079
|
+
...options?.timestamp === void 0 ? {} : { timestamp: options.timestamp }
|
|
2080
|
+
});
|
|
1952
2081
|
}
|
|
1953
2082
|
/** Close the socket and drop all state. Safe to call multiple times. */
|
|
1954
2083
|
stop() {
|
|
@@ -1968,7 +2097,17 @@ var WebSocketTransport = class {
|
|
|
1968
2097
|
}
|
|
1969
2098
|
this.socket.send(JSON.stringify(payload));
|
|
1970
2099
|
}
|
|
1971
|
-
sendBinaryFrame(topic, data) {
|
|
2100
|
+
sendBinaryFrame(topic, data, messageId, timestamp) {
|
|
2101
|
+
if (messageId !== void 0 || timestamp !== void 0) {
|
|
2102
|
+
this.sendFrame({
|
|
2103
|
+
op: "publish",
|
|
2104
|
+
topic,
|
|
2105
|
+
data: Array.from(new Uint8Array(data)),
|
|
2106
|
+
...messageId === void 0 ? {} : { messageId },
|
|
2107
|
+
...timestamp === void 0 ? {} : { timestamp }
|
|
2108
|
+
});
|
|
2109
|
+
return;
|
|
2110
|
+
}
|
|
1972
2111
|
if (this.socket?.readyState !== WS_OPEN) {
|
|
1973
2112
|
this.handlers?.onError(new Error('WebSocket is not open; dropped "publish" frame.'));
|
|
1974
2113
|
return;
|
|
@@ -2008,9 +2147,8 @@ var WebSocketTransport = class {
|
|
|
2008
2147
|
return;
|
|
2009
2148
|
}
|
|
2010
2149
|
if (!parsed || typeof parsed !== "object") return;
|
|
2011
|
-
const
|
|
2012
|
-
if (
|
|
2013
|
-
this.handlers?.onMessage({ topic: frame.topic, data: frame.data });
|
|
2150
|
+
const publication = parseDataBusPublication(parsed);
|
|
2151
|
+
if (publication) this.handlers?.onMessage(publication);
|
|
2014
2152
|
}
|
|
2015
2153
|
};
|
|
2016
2154
|
function defaultWebSocketFactory(url, protocols) {
|