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.
@@ -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
- return new Worker(new URL("./centrifuge.worker.js", import_meta.url), {
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
- return new SharedWorker(new URL("./centrifuge.shared.worker.js", import_meta.url), {
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
  });