cross-tab-worker-databus 0.3.0 → 0.5.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 +30 -0
- package/dist/centrifuge.js +19 -3
- package/dist/centrifuge.js.map +2 -2
- package/dist/{chunk-PNYWDVTH.js → chunk-W7DAXK4D.js} +92 -2
- package/dist/{chunk-PNYWDVTH.js.map → chunk-W7DAXK4D.js.map} +3 -3
- package/dist/cjs/centrifuge.cjs +112 -3
- package/dist/cjs/centrifuge.cjs.map +3 -3
- package/dist/cjs/index.cjs +140 -1
- package/dist/cjs/index.cjs.map +4 -4
- package/dist/core/data-bus.d.ts +32 -1
- package/dist/core/data-bus.d.ts.map +1 -1
- package/dist/core/replay-persistence.d.ts +13 -0
- package/dist/core/replay-persistence.d.ts.map +1 -0
- package/dist/core/types.d.ts +3 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +50 -1
- package/dist/index.js.map +3 -3
- package/docs/README.md +2 -1
- package/docs/api.md +1 -0
- package/docs/getting-started.md +2 -0
- package/docs/roadmap.md +18 -0
- package/docs/zh/README.md +2 -1
- package/docs/zh/api.md +1 -0
- package/docs/zh/getting-started.md +2 -0
- package/docs/zh/roadmap.md +18 -0
- package/package.json +4 -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,12 @@ 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;
|
|
1204
|
+
replayPersistence;
|
|
1205
|
+
replayHydration;
|
|
1196
1206
|
initialConfig;
|
|
1197
1207
|
hasInitialConfig;
|
|
1198
1208
|
trace;
|
|
@@ -1221,6 +1231,19 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1221
1231
|
// Minimum interval in ms between automatic recovery attempts.
|
|
1222
1232
|
static RECOVERY_COOLDOWN_MS = 1e3;
|
|
1223
1233
|
constructor(options) {
|
|
1234
|
+
const replay = options.replay;
|
|
1235
|
+
if (replay) {
|
|
1236
|
+
const maxPerTopic = replay.maxPerTopic ?? DEFAULT_REPLAY_MAX_PER_TOPIC;
|
|
1237
|
+
if (!Number.isSafeInteger(maxPerTopic) || maxPerTopic <= 0) {
|
|
1238
|
+
throw new TypeError(
|
|
1239
|
+
`replay.maxPerTopic must be a positive safe integer, got ${String(maxPerTopic)}.`
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
this.replayMaxPerTopic = replay?.maxPerTopic ?? DEFAULT_REPLAY_MAX_PER_TOPIC;
|
|
1244
|
+
this.replayBuffers = replay ? /* @__PURE__ */ new Map() : null;
|
|
1245
|
+
this.replayPersistence = replay?.persistence ?? null;
|
|
1246
|
+
this.replayHydration = this.hydrateReplay();
|
|
1224
1247
|
const { autoStart, initialConfig, trace, transport, ...clusterOptions } = options;
|
|
1225
1248
|
this.transport = transport;
|
|
1226
1249
|
this.initialConfig = initialConfig;
|
|
@@ -1377,13 +1400,26 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1377
1400
|
* delivered to this tab, regardless of which tab published it. Returns an
|
|
1378
1401
|
* unsubscribe function for convenience.
|
|
1379
1402
|
*/
|
|
1380
|
-
subscribe(topic, handler) {
|
|
1403
|
+
subscribe(topic, handler, options) {
|
|
1381
1404
|
this.ensureStarted();
|
|
1382
1405
|
const handlers = this.topicHandlers.get(topic) ?? /* @__PURE__ */ new Set();
|
|
1383
1406
|
const wasUnused = handlers.size === 0;
|
|
1384
1407
|
handlers.add(handler);
|
|
1385
1408
|
this.topicHandlers.set(topic, handlers);
|
|
1386
1409
|
if (wasUnused) this.cluster.subscribe(topic);
|
|
1410
|
+
if (options?.replay) {
|
|
1411
|
+
const limit = Math.min(
|
|
1412
|
+
typeof options.replay === "number" ? Math.floor(options.replay) : this.replayMaxPerTopic,
|
|
1413
|
+
this.replayMaxPerTopic
|
|
1414
|
+
);
|
|
1415
|
+
if (this.replayPersistence) {
|
|
1416
|
+
void this.replayHydration.then(() => {
|
|
1417
|
+
if (this.topicHandlers.get(topic)?.has(handler)) this.deliverReplay(topic, limit, handler);
|
|
1418
|
+
});
|
|
1419
|
+
} else {
|
|
1420
|
+
this.deliverReplay(topic, limit, handler);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1387
1423
|
return () => this.unsubscribe(topic, handler);
|
|
1388
1424
|
}
|
|
1389
1425
|
/** Remove a specific handler, or all handlers for `topic`.
|
|
@@ -1397,6 +1433,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1397
1433
|
else handlers.clear();
|
|
1398
1434
|
if (handlers.size > 0) return;
|
|
1399
1435
|
this.topicHandlers.delete(topic);
|
|
1436
|
+
this.replayBuffers?.delete(topic);
|
|
1400
1437
|
this.cluster.unsubscribe(topic);
|
|
1401
1438
|
}
|
|
1402
1439
|
/** Publish a message to `topic`. The owning Worker delivers it to the transport. */
|
|
@@ -1443,6 +1480,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1443
1480
|
this.trace.event({ type: "lifecycle", action: "stop" });
|
|
1444
1481
|
this.trace.stop();
|
|
1445
1482
|
this.topicHandlers.clear();
|
|
1483
|
+
this.replayBuffers?.clear();
|
|
1446
1484
|
this.cluster.stop();
|
|
1447
1485
|
try {
|
|
1448
1486
|
await this.startPromise?.catch(() => void 0);
|
|
@@ -1491,6 +1529,61 @@ var CrossTabDataBus = class _CrossTabDataBus {
|
|
|
1491
1529
|
this.invokeHandlers(handlers, (handler) => handler(message));
|
|
1492
1530
|
}
|
|
1493
1531
|
}
|
|
1532
|
+
this.recordReplay(message);
|
|
1533
|
+
}
|
|
1534
|
+
/** Append a dispatched publication to the topic's replay ring buffer.
|
|
1535
|
+
* No-op when replay is disabled. */
|
|
1536
|
+
recordReplay(message) {
|
|
1537
|
+
if (!this.replayBuffers) return;
|
|
1538
|
+
let buffer = this.replayBuffers.get(message.topic);
|
|
1539
|
+
if (!buffer) {
|
|
1540
|
+
buffer = [];
|
|
1541
|
+
this.replayBuffers.set(message.topic, buffer);
|
|
1542
|
+
}
|
|
1543
|
+
buffer.push(message);
|
|
1544
|
+
if (buffer.length > this.replayMaxPerTopic) buffer.shift();
|
|
1545
|
+
if (this.replayPersistence) {
|
|
1546
|
+
void this.replayPersistence.append(message).catch((error) => this.reportError(error));
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
async hydrateReplay() {
|
|
1550
|
+
if (!this.replayBuffers || !this.replayPersistence) {
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
try {
|
|
1554
|
+
for (const message of await this.replayPersistence.load()) {
|
|
1555
|
+
let buffer = this.replayBuffers.get(message.topic);
|
|
1556
|
+
if (!buffer) {
|
|
1557
|
+
buffer = [];
|
|
1558
|
+
this.replayBuffers.set(message.topic, buffer);
|
|
1559
|
+
}
|
|
1560
|
+
buffer.push(message);
|
|
1561
|
+
if (buffer.length > this.replayMaxPerTopic) buffer.shift();
|
|
1562
|
+
}
|
|
1563
|
+
} catch (error) {
|
|
1564
|
+
this.reportError(error);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
/** Deliver buffered history to a newly-registered handler. For an exact
|
|
1568
|
+
* topic this is that topic's ring; for a wildcard subscription every
|
|
1569
|
+
* buffered topic matching the pattern contributes (in buffer insertion
|
|
1570
|
+
* order). Replay deliveries are marked `replayed: true` and are not
|
|
1571
|
+
* counted into trace metrics. */
|
|
1572
|
+
deliverReplay(topic, limit, handler) {
|
|
1573
|
+
if (!this.replayBuffers || limit <= 0) return;
|
|
1574
|
+
const deliver = (buffer2) => {
|
|
1575
|
+
for (const message of buffer2.slice(-limit)) {
|
|
1576
|
+
this.invokeHandlers([handler], (h) => h({ ...message, replayed: true }));
|
|
1577
|
+
}
|
|
1578
|
+
};
|
|
1579
|
+
if (isWildcardTopic(topic)) {
|
|
1580
|
+
for (const [bufferedTopic, buffer2] of this.replayBuffers) {
|
|
1581
|
+
if (topicMatchesPattern(topic, bufferedTopic)) deliver(buffer2);
|
|
1582
|
+
}
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
const buffer = this.replayBuffers.get(topic);
|
|
1586
|
+
if (buffer) deliver(buffer);
|
|
1494
1587
|
}
|
|
1495
1588
|
/**
|
|
1496
1589
|
* Propagate a status change to the cluster, trace, and all registered
|
|
@@ -2081,7 +2174,15 @@ function createDefaultWorker() {
|
|
|
2081
2174
|
if (typeof Worker === "undefined") {
|
|
2082
2175
|
throw new Error("CentrifugeWorkerTransport requires a browser Worker implementation.");
|
|
2083
2176
|
}
|
|
2084
|
-
|
|
2177
|
+
let workerUrl;
|
|
2178
|
+
try {
|
|
2179
|
+
workerUrl = new URL("./centrifuge.worker.js", import_meta.url);
|
|
2180
|
+
} catch {
|
|
2181
|
+
throw new Error(
|
|
2182
|
+
"The default Centrifuge Worker URL is unavailable in this module format; provide workerFactory explicitly."
|
|
2183
|
+
);
|
|
2184
|
+
}
|
|
2185
|
+
return new Worker(workerUrl, {
|
|
2085
2186
|
name: "cross-tab-worker-databus",
|
|
2086
2187
|
type: "module"
|
|
2087
2188
|
});
|
|
@@ -2090,7 +2191,15 @@ function createDefaultSharedWorker() {
|
|
|
2090
2191
|
if (typeof SharedWorker === "undefined") {
|
|
2091
2192
|
throw new Error("CentrifugeWorkerTransport requires a browser SharedWorker implementation.");
|
|
2092
2193
|
}
|
|
2093
|
-
|
|
2194
|
+
let workerUrl;
|
|
2195
|
+
try {
|
|
2196
|
+
workerUrl = new URL("./centrifuge.shared.worker.js", import_meta.url);
|
|
2197
|
+
} catch {
|
|
2198
|
+
throw new Error(
|
|
2199
|
+
"The default Centrifuge SharedWorker URL is unavailable in this module format; provide sharedWorkerFactory explicitly."
|
|
2200
|
+
);
|
|
2201
|
+
}
|
|
2202
|
+
return new SharedWorker(workerUrl, {
|
|
2094
2203
|
name: "cross-tab-worker-databus-shared",
|
|
2095
2204
|
type: "module"
|
|
2096
2205
|
});
|