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.
@@ -1210,6 +1210,7 @@ function roundMs(value) {
1210
1210
 
1211
1211
  // src/core/data-bus.ts
1212
1212
  var PUBLICATION_EVENT = "DATABUS_PUBLICATION";
1213
+ var DEFAULT_REPLAY_MAX_PER_TOPIC = 100;
1213
1214
  var CrossTabDataBus = class _CrossTabDataBus {
1214
1215
  transport;
1215
1216
  cluster;
@@ -1220,6 +1221,10 @@ var CrossTabDataBus = class _CrossTabDataBus {
1220
1221
  transportSubscribedTopics = /* @__PURE__ */ new Set();
1221
1222
  statusHandlers = /* @__PURE__ */ new Set();
1222
1223
  errorHandlers = /* @__PURE__ */ new Set();
1224
+ // Bounded per-topic ring of recent dispatched publications. Null unless
1225
+ // replay is enabled — buffering is opt-in and must cost nothing otherwise.
1226
+ replayBuffers;
1227
+ replayMaxPerTopic;
1223
1228
  initialConfig;
1224
1229
  hasInitialConfig;
1225
1230
  trace;
@@ -1248,6 +1253,17 @@ var CrossTabDataBus = class _CrossTabDataBus {
1248
1253
  // Minimum interval in ms between automatic recovery attempts.
1249
1254
  static RECOVERY_COOLDOWN_MS = 1e3;
1250
1255
  constructor(options) {
1256
+ const replay = options.replay;
1257
+ if (replay) {
1258
+ const maxPerTopic = replay.maxPerTopic ?? DEFAULT_REPLAY_MAX_PER_TOPIC;
1259
+ if (!Number.isSafeInteger(maxPerTopic) || maxPerTopic <= 0) {
1260
+ throw new TypeError(
1261
+ `replay.maxPerTopic must be a positive safe integer, got ${String(maxPerTopic)}.`
1262
+ );
1263
+ }
1264
+ }
1265
+ this.replayMaxPerTopic = replay?.maxPerTopic ?? DEFAULT_REPLAY_MAX_PER_TOPIC;
1266
+ this.replayBuffers = replay ? /* @__PURE__ */ new Map() : null;
1251
1267
  const { autoStart, initialConfig, trace, transport, ...clusterOptions } = options;
1252
1268
  this.transport = transport;
1253
1269
  this.initialConfig = initialConfig;
@@ -1404,13 +1420,20 @@ var CrossTabDataBus = class _CrossTabDataBus {
1404
1420
  * delivered to this tab, regardless of which tab published it. Returns an
1405
1421
  * unsubscribe function for convenience.
1406
1422
  */
1407
- subscribe(topic, handler) {
1423
+ subscribe(topic, handler, options) {
1408
1424
  this.ensureStarted();
1409
1425
  const handlers = this.topicHandlers.get(topic) ?? /* @__PURE__ */ new Set();
1410
1426
  const wasUnused = handlers.size === 0;
1411
1427
  handlers.add(handler);
1412
1428
  this.topicHandlers.set(topic, handlers);
1413
1429
  if (wasUnused) this.cluster.subscribe(topic);
1430
+ if (options?.replay) {
1431
+ const limit = Math.min(
1432
+ typeof options.replay === "number" ? Math.floor(options.replay) : this.replayMaxPerTopic,
1433
+ this.replayMaxPerTopic
1434
+ );
1435
+ this.deliverReplay(topic, limit, handler);
1436
+ }
1414
1437
  return () => this.unsubscribe(topic, handler);
1415
1438
  }
1416
1439
  /** Remove a specific handler, or all handlers for `topic`.
@@ -1424,6 +1447,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
1424
1447
  else handlers.clear();
1425
1448
  if (handlers.size > 0) return;
1426
1449
  this.topicHandlers.delete(topic);
1450
+ this.replayBuffers?.delete(topic);
1427
1451
  this.cluster.unsubscribe(topic);
1428
1452
  }
1429
1453
  /** Publish a message to `topic`. The owning Worker delivers it to the transport. */
@@ -1470,6 +1494,7 @@ var CrossTabDataBus = class _CrossTabDataBus {
1470
1494
  this.trace.event({ type: "lifecycle", action: "stop" });
1471
1495
  this.trace.stop();
1472
1496
  this.topicHandlers.clear();
1497
+ this.replayBuffers?.clear();
1473
1498
  this.cluster.stop();
1474
1499
  try {
1475
1500
  await this.startPromise?.catch(() => void 0);
@@ -1518,6 +1543,40 @@ var CrossTabDataBus = class _CrossTabDataBus {
1518
1543
  this.invokeHandlers(handlers, (handler) => handler(message));
1519
1544
  }
1520
1545
  }
1546
+ this.recordReplay(message);
1547
+ }
1548
+ /** Append a dispatched publication to the topic's replay ring buffer.
1549
+ * No-op when replay is disabled. */
1550
+ recordReplay(message) {
1551
+ if (!this.replayBuffers) return;
1552
+ let buffer = this.replayBuffers.get(message.topic);
1553
+ if (!buffer) {
1554
+ buffer = [];
1555
+ this.replayBuffers.set(message.topic, buffer);
1556
+ }
1557
+ buffer.push(message);
1558
+ if (buffer.length > this.replayMaxPerTopic) buffer.shift();
1559
+ }
1560
+ /** Deliver buffered history to a newly-registered handler. For an exact
1561
+ * topic this is that topic's ring; for a wildcard subscription every
1562
+ * buffered topic matching the pattern contributes (in buffer insertion
1563
+ * order). Replay deliveries are marked `replayed: true` and are not
1564
+ * counted into trace metrics. */
1565
+ deliverReplay(topic, limit, handler) {
1566
+ if (!this.replayBuffers || limit <= 0) return;
1567
+ const deliver = (buffer2) => {
1568
+ for (const message of buffer2.slice(-limit)) {
1569
+ this.invokeHandlers([handler], (h) => h({ ...message, replayed: true }));
1570
+ }
1571
+ };
1572
+ if (isWildcardTopic(topic)) {
1573
+ for (const [bufferedTopic, buffer2] of this.replayBuffers) {
1574
+ if (topicMatchesPattern(topic, bufferedTopic)) deliver(buffer2);
1575
+ }
1576
+ return;
1577
+ }
1578
+ const buffer = this.replayBuffers.get(topic);
1579
+ if (buffer) deliver(buffer);
1521
1580
  }
1522
1581
  /**
1523
1582
  * Propagate a status change to the cluster, trace, and all registered