cross-tab-worker-databus 0.8.0 → 0.11.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 +34 -0
- package/README.md +2 -0
- package/README.zh.md +2 -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-NX76TAV3.js} +137 -26
- package/dist/chunk-NX76TAV3.js.map +7 -0
- package/dist/cjs/centrifuge.cjs +164 -35
- package/dist/cjs/centrifuge.cjs.map +4 -4
- package/dist/cjs/index.cjs +172 -38
- 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 +19 -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/trace.d.ts +7 -0
- package/dist/core/trace.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 +27 -6
- package/docs/architecture.md +7 -1
- package/docs/capabilities.md +2 -2
- package/docs/configuration.md +1 -1
- package/docs/roadmap.md +15 -1
- package/docs/transports.md +9 -5
- package/docs/zh/api.md +27 -6
- package/docs/zh/architecture.md +7 -1
- package/docs/zh/capabilities.md +2 -2
- package/docs/zh/configuration.md +1 -1
- package/docs/zh/transports.md +8 -4
- package/package.json +1 -1
- package/dist/chunk-WWUY2FV2.js.map +0 -7
|
@@ -298,6 +298,13 @@ function writeJson(storage, key, value) {
|
|
|
298
298
|
} catch {
|
|
299
299
|
}
|
|
300
300
|
}
|
|
301
|
+
function publicationMetadata(messageId, timestamp) {
|
|
302
|
+
if (messageId === void 0 && timestamp === void 0) return void 0;
|
|
303
|
+
return {
|
|
304
|
+
...messageId === void 0 ? {} : { messageId },
|
|
305
|
+
...timestamp === void 0 ? {} : { timestamp }
|
|
306
|
+
};
|
|
307
|
+
}
|
|
301
308
|
function listKeys(storage, prefix) {
|
|
302
309
|
try {
|
|
303
310
|
return Array.from({ length: storage.length }, (_, index) => storage.key(index)).filter(
|
|
@@ -527,26 +534,21 @@ var WorkerClusterRuntime = class {
|
|
|
527
534
|
this.sendRouteReleased(owner.workerId, topic, topicKey, generation);
|
|
528
535
|
}
|
|
529
536
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
* no owner is found). Returns false when the control message could not be
|
|
533
|
-
* posted to a remote owner, so the caller can surface the failure instead of
|
|
534
|
-
* silently dropping the publication.
|
|
535
|
-
*/
|
|
536
|
-
publish(topic, data, messageId) {
|
|
537
|
+
publish(topic, data, metadataOrMessageId) {
|
|
538
|
+
const metadata = typeof metadataOrMessageId === "string" ? { messageId: metadataOrMessageId } : metadataOrMessageId;
|
|
537
539
|
const topicKey = this.rememberTopic(topic);
|
|
538
540
|
if (this.assignedTopics.has(topicKey)) {
|
|
539
|
-
return this.sendControl(this.workerId, "PUBLISH", topic, topicKey, data,
|
|
541
|
+
return this.sendControl(this.workerId, "PUBLISH", topic, topicKey, data, metadata);
|
|
540
542
|
}
|
|
541
543
|
for (const pattern of this.assignedTopics.values()) {
|
|
542
544
|
if (pattern !== topic && topicMatchesPattern(pattern, topic)) {
|
|
543
|
-
return this.sendControl(this.workerId, "PUBLISH", topic, topicKey, data,
|
|
545
|
+
return this.sendControl(this.workerId, "PUBLISH", topic, topicKey, data, metadata);
|
|
544
546
|
}
|
|
545
547
|
}
|
|
546
548
|
const workers = this.readWorkers();
|
|
547
549
|
const route = this.readRoute(topicKey);
|
|
548
550
|
const target = this.routeOwnerIsLive(route, workers) ? route?.workerId ?? this.workerId : this.workerId;
|
|
549
|
-
return this.sendControl(target, "PUBLISH", topic, topicKey, data,
|
|
551
|
+
return this.sendControl(target, "PUBLISH", topic, topicKey, data, metadata);
|
|
550
552
|
}
|
|
551
553
|
/** True when `route` exists and its owner worker is among `workers`.
|
|
552
554
|
* Shared by subscribe (skip re-assignment) and publish (route to owner).
|
|
@@ -668,11 +670,15 @@ var WorkerClusterRuntime = class {
|
|
|
668
670
|
default:
|
|
669
671
|
break;
|
|
670
672
|
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
673
|
+
const metadata = publicationMetadata(message.messageId, message.timestamp);
|
|
674
|
+
if (metadata) this.handlers.onControl(
|
|
675
|
+
message.action,
|
|
676
|
+
message.topic,
|
|
677
|
+
message.data,
|
|
678
|
+
metadata.messageId,
|
|
679
|
+
metadata.timestamp
|
|
680
|
+
);
|
|
681
|
+
else this.handlers.onControl(message.action, message.topic, message.data);
|
|
676
682
|
if (message.action !== "PUBLISH") this.updateLoad();
|
|
677
683
|
}
|
|
678
684
|
/**
|
|
@@ -789,7 +795,7 @@ var WorkerClusterRuntime = class {
|
|
|
789
795
|
* Local execution updates the assignment map and route synchronously, bypassing
|
|
790
796
|
* the BroadcastChannel latency.
|
|
791
797
|
*/
|
|
792
|
-
sendControl(targetWorkerId, action, topic, topicKey, data,
|
|
798
|
+
sendControl(targetWorkerId, action, topic, topicKey, data, metadata) {
|
|
793
799
|
if (targetWorkerId === this.workerId) {
|
|
794
800
|
switch (action) {
|
|
795
801
|
case "SUBSCRIBE":
|
|
@@ -803,8 +809,14 @@ var WorkerClusterRuntime = class {
|
|
|
803
809
|
default:
|
|
804
810
|
break;
|
|
805
811
|
}
|
|
806
|
-
if (
|
|
807
|
-
|
|
812
|
+
if (metadata) this.handlers.onControl(
|
|
813
|
+
action,
|
|
814
|
+
topic,
|
|
815
|
+
data,
|
|
816
|
+
metadata.messageId,
|
|
817
|
+
metadata.timestamp
|
|
818
|
+
);
|
|
819
|
+
else this.handlers.onControl(action, topic, data);
|
|
808
820
|
if (action !== "PUBLISH") this.updateLoad();
|
|
809
821
|
return true;
|
|
810
822
|
}
|
|
@@ -816,7 +828,8 @@ var WorkerClusterRuntime = class {
|
|
|
816
828
|
topic,
|
|
817
829
|
topicKey,
|
|
818
830
|
...data === void 0 ? {} : { data },
|
|
819
|
-
...messageId === void 0 ? {} : { messageId }
|
|
831
|
+
...metadata?.messageId === void 0 ? {} : { messageId: metadata.messageId },
|
|
832
|
+
...metadata?.timestamp === void 0 ? {} : { timestamp: metadata.timestamp }
|
|
820
833
|
});
|
|
821
834
|
}
|
|
822
835
|
/** Post a message on the BroadcastChannel. Returns false on postMessage failure. */
|
|
@@ -1037,6 +1050,8 @@ var DataBusTraceReporter = class {
|
|
|
1037
1050
|
// Bucketed histogram: bucket index = floor(delayMs / 50), capped at 19.
|
|
1038
1051
|
latencyBuckets = new Array(LATENCY_BUCKET_COUNT).fill(0);
|
|
1039
1052
|
latencySumMs = 0;
|
|
1053
|
+
dedupAccepted = 0;
|
|
1054
|
+
dedupSuppressed = 0;
|
|
1040
1055
|
constructor(options, now = Date.now) {
|
|
1041
1056
|
this.enabled = options?.enabled ?? false;
|
|
1042
1057
|
this.mode = options?.mode ?? "all";
|
|
@@ -1112,6 +1127,13 @@ var DataBusTraceReporter = class {
|
|
|
1112
1127
|
this.latencyBuckets[bucketIndex] = (this.latencyBuckets[bucketIndex] ?? 0) + 1;
|
|
1113
1128
|
this.latencySumMs += delayMs;
|
|
1114
1129
|
}
|
|
1130
|
+
/** Record deduplication outcomes for the next metrics window. */
|
|
1131
|
+
recordDedupAccepted() {
|
|
1132
|
+
if (this.metricsActive) this.dedupAccepted += 1;
|
|
1133
|
+
}
|
|
1134
|
+
recordDedupSuppressed() {
|
|
1135
|
+
if (this.metricsActive) this.dedupSuppressed += 1;
|
|
1136
|
+
}
|
|
1115
1137
|
/** True when metrics recording is active: enabled and mode includes metrics.
|
|
1116
1138
|
* Extracted so the four record / flush methods share one guard expression
|
|
1117
1139
|
* instead of repeating `!this.enabled || this.mode === 'events'` at each. */
|
|
@@ -1125,7 +1147,7 @@ var DataBusTraceReporter = class {
|
|
|
1125
1147
|
}
|
|
1126
1148
|
flushNow() {
|
|
1127
1149
|
const timestamp = this.now();
|
|
1128
|
-
if (this.received > 0 || this.dispatched > 0) {
|
|
1150
|
+
if (this.received > 0 || this.dispatched > 0 || this.dedupAccepted > 0 || this.dedupSuppressed > 0) {
|
|
1129
1151
|
const samples = this.latencySamples;
|
|
1130
1152
|
this.emit({
|
|
1131
1153
|
type: "message_metrics",
|
|
@@ -1139,6 +1161,8 @@ var DataBusTraceReporter = class {
|
|
|
1139
1161
|
dispatchP50Ms: roundMs(percentileMs(this.latencyBuckets, samples, 0.5)),
|
|
1140
1162
|
dispatchP95Ms: roundMs(percentileMs(this.latencyBuckets, samples, 0.95)),
|
|
1141
1163
|
dispatchMaxMs: roundMs(percentileMs(this.latencyBuckets, samples, 1)),
|
|
1164
|
+
dedupAccepted: this.dedupAccepted,
|
|
1165
|
+
dedupSuppressed: this.dedupSuppressed,
|
|
1142
1166
|
timestamp
|
|
1143
1167
|
});
|
|
1144
1168
|
this.resetMetrics();
|
|
@@ -1153,6 +1177,8 @@ var DataBusTraceReporter = class {
|
|
|
1153
1177
|
this.receivedAt.clear();
|
|
1154
1178
|
this.latencyBuckets.fill(0);
|
|
1155
1179
|
this.latencySumMs = 0;
|
|
1180
|
+
this.dedupAccepted = 0;
|
|
1181
|
+
this.dedupSuppressed = 0;
|
|
1156
1182
|
}
|
|
1157
1183
|
emit(event) {
|
|
1158
1184
|
try {
|
|
@@ -1203,6 +1229,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1203
1229
|
replayBuffers;
|
|
1204
1230
|
replayMaxPerTopic;
|
|
1205
1231
|
replayPersistence;
|
|
1232
|
+
replayRetentionMs;
|
|
1206
1233
|
replayHydration;
|
|
1207
1234
|
initialConfig;
|
|
1208
1235
|
hasInitialConfig;
|
|
@@ -1211,6 +1238,8 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1211
1238
|
dedupTtlMs;
|
|
1212
1239
|
dedupEnabled;
|
|
1213
1240
|
seenMessageIds = /* @__PURE__ */ new Map();
|
|
1241
|
+
dedupSuppressed = 0;
|
|
1242
|
+
dedupAccepted = 0;
|
|
1214
1243
|
activeConfig;
|
|
1215
1244
|
status = "disconnected";
|
|
1216
1245
|
started = false;
|
|
@@ -1248,6 +1277,10 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1248
1277
|
this.replayMaxPerTopic = replay?.maxPerTopic ?? DEFAULT_REPLAY_MAX_PER_TOPIC;
|
|
1249
1278
|
this.replayBuffers = replay ? /* @__PURE__ */ new Map() : null;
|
|
1250
1279
|
this.replayPersistence = replay?.persistence ?? null;
|
|
1280
|
+
this.replayRetentionMs = replay?.retentionMs;
|
|
1281
|
+
if (this.replayRetentionMs !== void 0 && (!Number.isFinite(this.replayRetentionMs) || this.replayRetentionMs <= 0)) {
|
|
1282
|
+
throw new TypeError("replay.retentionMs must be a positive finite number.");
|
|
1283
|
+
}
|
|
1251
1284
|
this.replayHydration = this.hydrateReplay();
|
|
1252
1285
|
const { autoStart, initialConfig, trace, transport, dedup, ...clusterOptions } = options;
|
|
1253
1286
|
this.transport = transport;
|
|
@@ -1268,7 +1301,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1268
1301
|
handlers: {
|
|
1269
1302
|
// The cluster calls `onControl` when it receives a SUBSCRIBE/UNSUBSCRIBE/PUBLISH
|
|
1270
1303
|
// control message — meaning the owning Worker has delegated the action to us.
|
|
1271
|
-
onControl: (action, topic, data, messageId) => {
|
|
1304
|
+
onControl: (action, topic, data, messageId, timestamp) => {
|
|
1272
1305
|
switch (action) {
|
|
1273
1306
|
case "SUBSCRIBE":
|
|
1274
1307
|
if (this.subscribeTransport(topic)) this.traceSubscription("subscribe", topic);
|
|
@@ -1277,7 +1310,14 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1277
1310
|
if (this.unsubscribeTransport(topic)) this.traceSubscription("unsubscribe", topic);
|
|
1278
1311
|
break;
|
|
1279
1312
|
case "PUBLISH":
|
|
1280
|
-
this.runTransport(() => this.transport.publish(
|
|
1313
|
+
this.runTransport(() => this.transport.publish(
|
|
1314
|
+
topic,
|
|
1315
|
+
data,
|
|
1316
|
+
messageId === void 0 && timestamp === void 0 ? void 0 : {
|
|
1317
|
+
...messageId === void 0 ? {} : { messageId },
|
|
1318
|
+
...timestamp === void 0 ? {} : { timestamp }
|
|
1319
|
+
}
|
|
1320
|
+
));
|
|
1281
1321
|
break;
|
|
1282
1322
|
default:
|
|
1283
1323
|
break;
|
|
@@ -1468,10 +1508,49 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1468
1508
|
}
|
|
1469
1509
|
}
|
|
1470
1510
|
}
|
|
1511
|
+
/** Clear replay history for one exact topic, including durable storage. */
|
|
1512
|
+
async clearReplayTopic(topic) {
|
|
1513
|
+
this.replayBuffers?.delete(topic);
|
|
1514
|
+
if (this.replayPersistence?.clearTopic) {
|
|
1515
|
+
try {
|
|
1516
|
+
await this.replayPersistence.clearTopic(topic);
|
|
1517
|
+
} catch (error) {
|
|
1518
|
+
this.reportError(error);
|
|
1519
|
+
throw error;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
/** Remove replay entries older than an epoch-millisecond cutoff. */
|
|
1524
|
+
async clearReplayBefore(timestamp) {
|
|
1525
|
+
if (!Number.isFinite(timestamp)) throw new TypeError("timestamp must be finite.");
|
|
1526
|
+
if (this.replayBuffers) {
|
|
1527
|
+
for (const [topic, messages] of this.replayBuffers) {
|
|
1528
|
+
const kept = messages.filter((message) => (message.timestamp ?? 0) >= timestamp);
|
|
1529
|
+
if (kept.length) this.replayBuffers.set(topic, kept);
|
|
1530
|
+
else this.replayBuffers.delete(topic);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
if (this.replayPersistence?.clearBefore) await this.replayPersistence.clearBefore(timestamp);
|
|
1534
|
+
}
|
|
1535
|
+
/** Return bounded deduplication counters for diagnostics and health checks. */
|
|
1536
|
+
getDedupStats() {
|
|
1537
|
+
return {
|
|
1538
|
+
enabled: this.dedupEnabled,
|
|
1539
|
+
tracked: this.seenMessageIds.size,
|
|
1540
|
+
suppressed: this.dedupSuppressed,
|
|
1541
|
+
accepted: this.dedupAccepted
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
/** Drop all remembered IDs and reset dedup counters. */
|
|
1545
|
+
resetDedup() {
|
|
1546
|
+
this.seenMessageIds.clear();
|
|
1547
|
+
this.dedupSuppressed = 0;
|
|
1548
|
+
this.dedupAccepted = 0;
|
|
1549
|
+
}
|
|
1471
1550
|
/** Publish a message to `topic`. The owning Worker delivers it to the transport. */
|
|
1472
1551
|
publish(topic, data, options) {
|
|
1473
1552
|
this.ensureStarted();
|
|
1474
|
-
if (!this.cluster.publish(topic, data, options
|
|
1553
|
+
if (!this.cluster.publish(topic, data, options)) {
|
|
1475
1554
|
this.reportError(
|
|
1476
1555
|
new Error("Failed to send the publish control message to the owning worker.")
|
|
1477
1556
|
);
|
|
@@ -1560,9 +1639,13 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1560
1639
|
}
|
|
1561
1640
|
if (this.seenMessageIds.has(message.messageId)) {
|
|
1562
1641
|
this.trace.event({ type: "reliability", operation: "dedup_suppressed", topic: message.topic });
|
|
1642
|
+
this.dedupSuppressed += 1;
|
|
1643
|
+
this.trace.recordDedupSuppressed();
|
|
1563
1644
|
return true;
|
|
1564
1645
|
}
|
|
1565
1646
|
this.seenMessageIds.set(message.messageId, now);
|
|
1647
|
+
this.dedupAccepted += 1;
|
|
1648
|
+
this.trace.recordDedupAccepted();
|
|
1566
1649
|
while (this.seenMessageIds.size > this.dedupMaxEntries) {
|
|
1567
1650
|
const oldest = this.seenMessageIds.keys().next().value;
|
|
1568
1651
|
if (oldest === void 0) break;
|
|
@@ -1592,10 +1675,14 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1592
1675
|
buffer = [];
|
|
1593
1676
|
this.replayBuffers.set(message.topic, buffer);
|
|
1594
1677
|
}
|
|
1595
|
-
|
|
1678
|
+
const storedMessage = message;
|
|
1679
|
+
buffer.push(storedMessage);
|
|
1596
1680
|
if (buffer.length > this.replayMaxPerTopic) buffer.shift();
|
|
1597
1681
|
if (this.replayPersistence) {
|
|
1598
|
-
void this.replayPersistence.append(
|
|
1682
|
+
void this.replayPersistence.append(storedMessage).catch((error) => this.reportError(error));
|
|
1683
|
+
if (this.replayRetentionMs !== void 0 && this.replayPersistence.clearBefore) {
|
|
1684
|
+
void this.replayPersistence.clearBefore(Date.now() - this.replayRetentionMs).catch((error) => this.reportError(error));
|
|
1685
|
+
}
|
|
1599
1686
|
}
|
|
1600
1687
|
}
|
|
1601
1688
|
async hydrateReplay() {
|
|
@@ -1603,6 +1690,9 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1603
1690
|
return;
|
|
1604
1691
|
}
|
|
1605
1692
|
try {
|
|
1693
|
+
if (this.replayRetentionMs !== void 0 && this.replayPersistence.clearBefore) {
|
|
1694
|
+
await this.replayPersistence.clearBefore(Date.now() - this.replayRetentionMs);
|
|
1695
|
+
}
|
|
1606
1696
|
for (const message of await this.replayPersistence.load()) {
|
|
1607
1697
|
let buffer = this.replayBuffers.get(message.topic);
|
|
1608
1698
|
if (!buffer) {
|
|
@@ -1820,6 +1910,26 @@ function selectWorkerBackend(mode, availability = {}) {
|
|
|
1820
1910
|
return hasDedicated ? "dedicated" : hasShared ? "shared" : "local";
|
|
1821
1911
|
}
|
|
1822
1912
|
|
|
1913
|
+
// src/core/publication.ts
|
|
1914
|
+
function parseDataBusPublication(value, fallbackTopic) {
|
|
1915
|
+
if (!value || typeof value !== "object") {
|
|
1916
|
+
return fallbackTopic ? { topic: fallbackTopic, data: value } : null;
|
|
1917
|
+
}
|
|
1918
|
+
const frame = value;
|
|
1919
|
+
const nested = frame.publication && typeof frame.publication === "object" ? frame.publication : null;
|
|
1920
|
+
const publication = nested ?? frame;
|
|
1921
|
+
const topic = typeof publication.topic === "string" ? publication.topic : fallbackTopic;
|
|
1922
|
+
if (!topic) return null;
|
|
1923
|
+
const hasMetadataEnvelope = fallbackTopic !== void 0 && Object.prototype.hasOwnProperty.call(publication, "data") && (typeof publication.messageId === "string" || typeof publication.timestamp === "number");
|
|
1924
|
+
const data = nested || fallbackTopic === void 0 || hasMetadataEnvelope ? publication.data : value;
|
|
1925
|
+
return {
|
|
1926
|
+
topic,
|
|
1927
|
+
data,
|
|
1928
|
+
...typeof publication.messageId === "string" ? { messageId: publication.messageId } : {},
|
|
1929
|
+
...typeof publication.timestamp === "number" ? { timestamp: publication.timestamp } : {}
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1823
1933
|
export {
|
|
1824
1934
|
createBrowserEnvironment,
|
|
1825
1935
|
getOrCreateTabId,
|
|
@@ -1833,6 +1943,7 @@ export {
|
|
|
1833
1943
|
topicMatchesPattern,
|
|
1834
1944
|
WorkerClusterRuntime,
|
|
1835
1945
|
CrossTabDataBus,
|
|
1946
|
+
parseDataBusPublication,
|
|
1836
1947
|
selectWorkerBackend
|
|
1837
1948
|
};
|
|
1838
|
-
//# sourceMappingURL=chunk-
|
|
1949
|
+
//# sourceMappingURL=chunk-NX76TAV3.js.map
|