cross-tab-worker-databus 0.8.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 +21 -0
- package/README.md +1 -0
- package/README.zh.md +1 -0
- 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.js +29 -11
- package/dist/centrifuge.js.map +2 -2
- package/dist/centrifuge.shared.worker.js +37 -6
- package/dist/centrifuge.shared.worker.js.map +3 -3
- package/dist/centrifuge.worker.js +37 -6
- package/dist/centrifuge.worker.js.map +3 -3
- package/dist/{chunk-WWUY2FV2.js → chunk-ZOPNTR4E.js} +110 -25
- package/dist/chunk-ZOPNTR4E.js.map +7 -0
- package/dist/cjs/centrifuge.cjs +137 -34
- package/dist/cjs/centrifuge.cjs.map +4 -4
- package/dist/cjs/index.cjs +145 -37
- package/dist/cjs/index.cjs.map +3 -3
- package/dist/core/cluster.d.ts +3 -2
- package/dist/core/cluster.d.ts.map +1 -1
- package/dist/core/data-bus.d.ts +16 -0
- 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/types.d.ts +19 -7
- 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 +39 -14
- package/dist/index.js.map +2 -2
- package/dist/websocket.d.ts.map +1 -1
- package/docs/api.md +25 -4
- package/docs/architecture.md +3 -1
- package/docs/transports.md +9 -5
- package/docs/zh/api.md +25 -4
- package/docs/zh/architecture.md +3 -1
- package/docs/zh/transports.md +8 -4
- package/package.json +1 -1
- package/dist/chunk-WWUY2FV2.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, messageId) {
|
|
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,11 +711,15 @@ var WorkerClusterRuntime = class {
|
|
|
709
711
|
default:
|
|
710
712
|
break;
|
|
711
713
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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);
|
|
717
723
|
if (message.action !== "PUBLISH") this.updateLoad();
|
|
718
724
|
}
|
|
719
725
|
/**
|
|
@@ -830,7 +836,7 @@ var WorkerClusterRuntime = class {
|
|
|
830
836
|
* Local execution updates the assignment map and route synchronously, bypassing
|
|
831
837
|
* the BroadcastChannel latency.
|
|
832
838
|
*/
|
|
833
|
-
sendControl(targetWorkerId, action, topic, topicKey, data,
|
|
839
|
+
sendControl(targetWorkerId, action, topic, topicKey, data, metadata) {
|
|
834
840
|
if (targetWorkerId === this.workerId) {
|
|
835
841
|
switch (action) {
|
|
836
842
|
case "SUBSCRIBE":
|
|
@@ -844,8 +850,14 @@ var WorkerClusterRuntime = class {
|
|
|
844
850
|
default:
|
|
845
851
|
break;
|
|
846
852
|
}
|
|
847
|
-
if (
|
|
848
|
-
|
|
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);
|
|
849
861
|
if (action !== "PUBLISH") this.updateLoad();
|
|
850
862
|
return true;
|
|
851
863
|
}
|
|
@@ -857,7 +869,8 @@ var WorkerClusterRuntime = class {
|
|
|
857
869
|
topic,
|
|
858
870
|
topicKey,
|
|
859
871
|
...data === void 0 ? {} : { data },
|
|
860
|
-
...messageId === void 0 ? {} : { messageId }
|
|
872
|
+
...metadata?.messageId === void 0 ? {} : { messageId: metadata.messageId },
|
|
873
|
+
...metadata?.timestamp === void 0 ? {} : { timestamp: metadata.timestamp }
|
|
861
874
|
});
|
|
862
875
|
}
|
|
863
876
|
/** Post a message on the BroadcastChannel. Returns false on postMessage failure. */
|
|
@@ -1252,6 +1265,8 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1252
1265
|
dedupTtlMs;
|
|
1253
1266
|
dedupEnabled;
|
|
1254
1267
|
seenMessageIds = /* @__PURE__ */ new Map();
|
|
1268
|
+
dedupSuppressed = 0;
|
|
1269
|
+
dedupAccepted = 0;
|
|
1255
1270
|
activeConfig;
|
|
1256
1271
|
status = "disconnected";
|
|
1257
1272
|
started = false;
|
|
@@ -1309,7 +1324,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1309
1324
|
handlers: {
|
|
1310
1325
|
// The cluster calls `onControl` when it receives a SUBSCRIBE/UNSUBSCRIBE/PUBLISH
|
|
1311
1326
|
// control message — meaning the owning Worker has delegated the action to us.
|
|
1312
|
-
onControl: (action, topic, data, messageId) => {
|
|
1327
|
+
onControl: (action, topic, data, messageId, timestamp) => {
|
|
1313
1328
|
switch (action) {
|
|
1314
1329
|
case "SUBSCRIBE":
|
|
1315
1330
|
if (this.subscribeTransport(topic)) this.traceSubscription("subscribe", topic);
|
|
@@ -1318,7 +1333,14 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1318
1333
|
if (this.unsubscribeTransport(topic)) this.traceSubscription("unsubscribe", topic);
|
|
1319
1334
|
break;
|
|
1320
1335
|
case "PUBLISH":
|
|
1321
|
-
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
|
+
));
|
|
1322
1344
|
break;
|
|
1323
1345
|
default:
|
|
1324
1346
|
break;
|
|
@@ -1509,10 +1531,49 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1509
1531
|
}
|
|
1510
1532
|
}
|
|
1511
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
|
+
}
|
|
1512
1573
|
/** Publish a message to `topic`. The owning Worker delivers it to the transport. */
|
|
1513
1574
|
publish(topic, data, options) {
|
|
1514
1575
|
this.ensureStarted();
|
|
1515
|
-
if (!this.cluster.publish(topic, data, options
|
|
1576
|
+
if (!this.cluster.publish(topic, data, options)) {
|
|
1516
1577
|
this.reportError(
|
|
1517
1578
|
new Error("Failed to send the publish control message to the owning worker.")
|
|
1518
1579
|
);
|
|
@@ -1601,9 +1662,11 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1601
1662
|
}
|
|
1602
1663
|
if (this.seenMessageIds.has(message.messageId)) {
|
|
1603
1664
|
this.trace.event({ type: "reliability", operation: "dedup_suppressed", topic: message.topic });
|
|
1665
|
+
this.dedupSuppressed += 1;
|
|
1604
1666
|
return true;
|
|
1605
1667
|
}
|
|
1606
1668
|
this.seenMessageIds.set(message.messageId, now);
|
|
1669
|
+
this.dedupAccepted += 1;
|
|
1607
1670
|
while (this.seenMessageIds.size > this.dedupMaxEntries) {
|
|
1608
1671
|
const oldest = this.seenMessageIds.keys().next().value;
|
|
1609
1672
|
if (oldest === void 0) break;
|
|
@@ -1633,10 +1696,11 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1633
1696
|
buffer = [];
|
|
1634
1697
|
this.replayBuffers.set(message.topic, buffer);
|
|
1635
1698
|
}
|
|
1636
|
-
|
|
1699
|
+
const storedMessage = message;
|
|
1700
|
+
buffer.push(storedMessage);
|
|
1637
1701
|
if (buffer.length > this.replayMaxPerTopic) buffer.shift();
|
|
1638
1702
|
if (this.replayPersistence) {
|
|
1639
|
-
void this.replayPersistence.append(
|
|
1703
|
+
void this.replayPersistence.append(storedMessage).catch((error) => this.reportError(error));
|
|
1640
1704
|
}
|
|
1641
1705
|
}
|
|
1642
1706
|
async hydrateReplay() {
|
|
@@ -1913,10 +1977,48 @@ function createIndexedDbReplayPersistence(options) {
|
|
|
1913
1977
|
transaction.oncomplete = () => resolve();
|
|
1914
1978
|
transaction.onerror = () => reject(transaction.error ?? new Error("Failed to clear topic replay history."));
|
|
1915
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
|
+
});
|
|
1916
1998
|
}
|
|
1917
1999
|
};
|
|
1918
2000
|
}
|
|
1919
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
|
+
|
|
1920
2022
|
// src/websocket.ts
|
|
1921
2023
|
var WS_OPEN = 1;
|
|
1922
2024
|
var WebSocketTransport = class {
|
|
@@ -1966,10 +2068,16 @@ var WebSocketTransport = class {
|
|
|
1966
2068
|
/** Publish `data` to `topic` as a JSON frame. Requires an open socket. */
|
|
1967
2069
|
publish(topic, data, options) {
|
|
1968
2070
|
if (data instanceof ArrayBuffer) {
|
|
1969
|
-
this.sendBinaryFrame(topic, data, options?.messageId);
|
|
2071
|
+
this.sendBinaryFrame(topic, data, options?.messageId, options?.timestamp);
|
|
1970
2072
|
return;
|
|
1971
2073
|
}
|
|
1972
|
-
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
|
+
});
|
|
1973
2081
|
}
|
|
1974
2082
|
/** Close the socket and drop all state. Safe to call multiple times. */
|
|
1975
2083
|
stop() {
|
|
@@ -1989,9 +2097,15 @@ var WebSocketTransport = class {
|
|
|
1989
2097
|
}
|
|
1990
2098
|
this.socket.send(JSON.stringify(payload));
|
|
1991
2099
|
}
|
|
1992
|
-
sendBinaryFrame(topic, data, messageId) {
|
|
1993
|
-
if (messageId) {
|
|
1994
|
-
this.sendFrame({
|
|
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
|
+
});
|
|
1995
2109
|
return;
|
|
1996
2110
|
}
|
|
1997
2111
|
if (this.socket?.readyState !== WS_OPEN) {
|
|
@@ -2033,14 +2147,8 @@ var WebSocketTransport = class {
|
|
|
2033
2147
|
return;
|
|
2034
2148
|
}
|
|
2035
2149
|
if (!parsed || typeof parsed !== "object") return;
|
|
2036
|
-
const
|
|
2037
|
-
if (
|
|
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);
|
|
2150
|
+
const publication = parseDataBusPublication(parsed);
|
|
2151
|
+
if (publication) this.handlers?.onMessage(publication);
|
|
2044
2152
|
}
|
|
2045
2153
|
};
|
|
2046
2154
|
function defaultWebSocketFactory(url, protocols) {
|