cross-tab-worker-databus 0.3.0 → 0.4.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 +17 -0
- package/dist/centrifuge.js +19 -3
- package/dist/centrifuge.js.map +2 -2
- package/dist/{chunk-PNYWDVTH.js → chunk-ZGQRELIV.js} +61 -2
- package/dist/{chunk-PNYWDVTH.js.map → chunk-ZGQRELIV.js.map} +3 -3
- package/dist/cjs/centrifuge.cjs +81 -3
- package/dist/cjs/centrifuge.cjs.map +3 -3
- package/dist/cjs/index.cjs +60 -1
- package/dist/cjs/index.cjs.map +3 -3
- package/dist/core/data-bus.d.ts +25 -1
- package/dist/core/data-bus.d.ts.map +1 -1
- package/dist/core/types.d.ts +3 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/docs/api.md +1 -0
- package/docs/getting-started.md +2 -0
- package/docs/zh/api.md +1 -0
- package/docs/zh/getting-started.md +2 -0
- package/package.json +3 -2
package/dist/cjs/centrifuge.cjs
CHANGED
|
@@ -160,6 +160,9 @@ function selectActiveWorkers(workers, maxActiveWorkers = DEFAULT_MAX_ACTIVE_WORK
|
|
|
160
160
|
(left, right) => left.registeredAt - right.registeredAt || (left.workerId < right.workerId ? -1 : left.workerId > right.workerId ? 1 : 0)
|
|
161
161
|
).slice(0, maxActiveWorkers);
|
|
162
162
|
}
|
|
163
|
+
function isWildcardTopic(pattern) {
|
|
164
|
+
return pattern === "*" || pattern.endsWith(".*");
|
|
165
|
+
}
|
|
163
166
|
function topicMatchesPattern(pattern, topic) {
|
|
164
167
|
if (!pattern || !topic) return false;
|
|
165
168
|
if (pattern === topic) return true;
|
|
@@ -1183,6 +1186,7 @@ function roundMs(value) {
|
|
|
1183
1186
|
|
|
1184
1187
|
// src/core/data-bus.ts
|
|
1185
1188
|
var PUBLICATION_EVENT = "DATABUS_PUBLICATION";
|
|
1189
|
+
var DEFAULT_REPLAY_MAX_PER_TOPIC = 100;
|
|
1186
1190
|
var CrossTabDataBus = class _CrossTabDataBus {
|
|
1187
1191
|
transport;
|
|
1188
1192
|
cluster;
|
|
@@ -1193,6 +1197,10 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1193
1197
|
transportSubscribedTopics = /* @__PURE__ */ new Set();
|
|
1194
1198
|
statusHandlers = /* @__PURE__ */ new Set();
|
|
1195
1199
|
errorHandlers = /* @__PURE__ */ new Set();
|
|
1200
|
+
// Bounded per-topic ring of recent dispatched publications. Null unless
|
|
1201
|
+
// replay is enabled — buffering is opt-in and must cost nothing otherwise.
|
|
1202
|
+
replayBuffers;
|
|
1203
|
+
replayMaxPerTopic;
|
|
1196
1204
|
initialConfig;
|
|
1197
1205
|
hasInitialConfig;
|
|
1198
1206
|
trace;
|
|
@@ -1221,6 +1229,17 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1221
1229
|
// Minimum interval in ms between automatic recovery attempts.
|
|
1222
1230
|
static RECOVERY_COOLDOWN_MS = 1e3;
|
|
1223
1231
|
constructor(options) {
|
|
1232
|
+
const replay = options.replay;
|
|
1233
|
+
if (replay) {
|
|
1234
|
+
const maxPerTopic = replay.maxPerTopic ?? DEFAULT_REPLAY_MAX_PER_TOPIC;
|
|
1235
|
+
if (!Number.isSafeInteger(maxPerTopic) || maxPerTopic <= 0) {
|
|
1236
|
+
throw new TypeError(
|
|
1237
|
+
`replay.maxPerTopic must be a positive safe integer, got ${String(maxPerTopic)}.`
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
this.replayMaxPerTopic = replay?.maxPerTopic ?? DEFAULT_REPLAY_MAX_PER_TOPIC;
|
|
1242
|
+
this.replayBuffers = replay ? /* @__PURE__ */ new Map() : null;
|
|
1224
1243
|
const { autoStart, initialConfig, trace, transport, ...clusterOptions } = options;
|
|
1225
1244
|
this.transport = transport;
|
|
1226
1245
|
this.initialConfig = initialConfig;
|
|
@@ -1377,13 +1396,20 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1377
1396
|
* delivered to this tab, regardless of which tab published it. Returns an
|
|
1378
1397
|
* unsubscribe function for convenience.
|
|
1379
1398
|
*/
|
|
1380
|
-
subscribe(topic, handler) {
|
|
1399
|
+
subscribe(topic, handler, options) {
|
|
1381
1400
|
this.ensureStarted();
|
|
1382
1401
|
const handlers = this.topicHandlers.get(topic) ?? /* @__PURE__ */ new Set();
|
|
1383
1402
|
const wasUnused = handlers.size === 0;
|
|
1384
1403
|
handlers.add(handler);
|
|
1385
1404
|
this.topicHandlers.set(topic, handlers);
|
|
1386
1405
|
if (wasUnused) this.cluster.subscribe(topic);
|
|
1406
|
+
if (options?.replay) {
|
|
1407
|
+
const limit = Math.min(
|
|
1408
|
+
typeof options.replay === "number" ? Math.floor(options.replay) : this.replayMaxPerTopic,
|
|
1409
|
+
this.replayMaxPerTopic
|
|
1410
|
+
);
|
|
1411
|
+
this.deliverReplay(topic, limit, handler);
|
|
1412
|
+
}
|
|
1387
1413
|
return () => this.unsubscribe(topic, handler);
|
|
1388
1414
|
}
|
|
1389
1415
|
/** Remove a specific handler, or all handlers for `topic`.
|
|
@@ -1397,6 +1423,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1397
1423
|
else handlers.clear();
|
|
1398
1424
|
if (handlers.size > 0) return;
|
|
1399
1425
|
this.topicHandlers.delete(topic);
|
|
1426
|
+
this.replayBuffers?.delete(topic);
|
|
1400
1427
|
this.cluster.unsubscribe(topic);
|
|
1401
1428
|
}
|
|
1402
1429
|
/** Publish a message to `topic`. The owning Worker delivers it to the transport. */
|
|
@@ -1443,6 +1470,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1443
1470
|
this.trace.event({ type: "lifecycle", action: "stop" });
|
|
1444
1471
|
this.trace.stop();
|
|
1445
1472
|
this.topicHandlers.clear();
|
|
1473
|
+
this.replayBuffers?.clear();
|
|
1446
1474
|
this.cluster.stop();
|
|
1447
1475
|
try {
|
|
1448
1476
|
await this.startPromise?.catch(() => void 0);
|
|
@@ -1491,6 +1519,40 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1491
1519
|
this.invokeHandlers(handlers, (handler) => handler(message));
|
|
1492
1520
|
}
|
|
1493
1521
|
}
|
|
1522
|
+
this.recordReplay(message);
|
|
1523
|
+
}
|
|
1524
|
+
/** Append a dispatched publication to the topic's replay ring buffer.
|
|
1525
|
+
* No-op when replay is disabled. */
|
|
1526
|
+
recordReplay(message) {
|
|
1527
|
+
if (!this.replayBuffers) return;
|
|
1528
|
+
let buffer = this.replayBuffers.get(message.topic);
|
|
1529
|
+
if (!buffer) {
|
|
1530
|
+
buffer = [];
|
|
1531
|
+
this.replayBuffers.set(message.topic, buffer);
|
|
1532
|
+
}
|
|
1533
|
+
buffer.push(message);
|
|
1534
|
+
if (buffer.length > this.replayMaxPerTopic) buffer.shift();
|
|
1535
|
+
}
|
|
1536
|
+
/** Deliver buffered history to a newly-registered handler. For an exact
|
|
1537
|
+
* topic this is that topic's ring; for a wildcard subscription every
|
|
1538
|
+
* buffered topic matching the pattern contributes (in buffer insertion
|
|
1539
|
+
* order). Replay deliveries are marked `replayed: true` and are not
|
|
1540
|
+
* counted into trace metrics. */
|
|
1541
|
+
deliverReplay(topic, limit, handler) {
|
|
1542
|
+
if (!this.replayBuffers || limit <= 0) return;
|
|
1543
|
+
const deliver = (buffer2) => {
|
|
1544
|
+
for (const message of buffer2.slice(-limit)) {
|
|
1545
|
+
this.invokeHandlers([handler], (h) => h({ ...message, replayed: true }));
|
|
1546
|
+
}
|
|
1547
|
+
};
|
|
1548
|
+
if (isWildcardTopic(topic)) {
|
|
1549
|
+
for (const [bufferedTopic, buffer2] of this.replayBuffers) {
|
|
1550
|
+
if (topicMatchesPattern(topic, bufferedTopic)) deliver(buffer2);
|
|
1551
|
+
}
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
const buffer = this.replayBuffers.get(topic);
|
|
1555
|
+
if (buffer) deliver(buffer);
|
|
1494
1556
|
}
|
|
1495
1557
|
/**
|
|
1496
1558
|
* Propagate a status change to the cluster, trace, and all registered
|
|
@@ -2081,7 +2143,15 @@ function createDefaultWorker() {
|
|
|
2081
2143
|
if (typeof Worker === "undefined") {
|
|
2082
2144
|
throw new Error("CentrifugeWorkerTransport requires a browser Worker implementation.");
|
|
2083
2145
|
}
|
|
2084
|
-
|
|
2146
|
+
let workerUrl;
|
|
2147
|
+
try {
|
|
2148
|
+
workerUrl = new URL("./centrifuge.worker.js", import_meta.url);
|
|
2149
|
+
} catch {
|
|
2150
|
+
throw new Error(
|
|
2151
|
+
"The default Centrifuge Worker URL is unavailable in this module format; provide workerFactory explicitly."
|
|
2152
|
+
);
|
|
2153
|
+
}
|
|
2154
|
+
return new Worker(workerUrl, {
|
|
2085
2155
|
name: "cross-tab-worker-databus",
|
|
2086
2156
|
type: "module"
|
|
2087
2157
|
});
|
|
@@ -2090,7 +2160,15 @@ function createDefaultSharedWorker() {
|
|
|
2090
2160
|
if (typeof SharedWorker === "undefined") {
|
|
2091
2161
|
throw new Error("CentrifugeWorkerTransport requires a browser SharedWorker implementation.");
|
|
2092
2162
|
}
|
|
2093
|
-
|
|
2163
|
+
let workerUrl;
|
|
2164
|
+
try {
|
|
2165
|
+
workerUrl = new URL("./centrifuge.shared.worker.js", import_meta.url);
|
|
2166
|
+
} catch {
|
|
2167
|
+
throw new Error(
|
|
2168
|
+
"The default Centrifuge SharedWorker URL is unavailable in this module format; provide sharedWorkerFactory explicitly."
|
|
2169
|
+
);
|
|
2170
|
+
}
|
|
2171
|
+
return new SharedWorker(workerUrl, {
|
|
2094
2172
|
name: "cross-tab-worker-databus-shared",
|
|
2095
2173
|
type: "module"
|
|
2096
2174
|
});
|