cross-tab-worker-databus 0.4.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.
@@ -25,6 +25,7 @@ __export(src_exports, {
25
25
  WebSocketTransport: () => WebSocketTransport,
26
26
  WorkerClusterRuntime: () => WorkerClusterRuntime,
27
27
  createBrowserEnvironment: () => createBrowserEnvironment,
28
+ createIndexedDbReplayPersistence: () => createIndexedDbReplayPersistence,
28
29
  createOpaqueKey: () => createOpaqueKey,
29
30
  createWebSocketDataBus: () => createWebSocketDataBus,
30
31
  getOrCreateTabId: () => getOrCreateTabId,
@@ -1225,6 +1226,8 @@ var CrossTabDataBus = class _CrossTabDataBus {
1225
1226
  // replay is enabled — buffering is opt-in and must cost nothing otherwise.
1226
1227
  replayBuffers;
1227
1228
  replayMaxPerTopic;
1229
+ replayPersistence;
1230
+ replayHydration;
1228
1231
  initialConfig;
1229
1232
  hasInitialConfig;
1230
1233
  trace;
@@ -1264,6 +1267,8 @@ var CrossTabDataBus = class _CrossTabDataBus {
1264
1267
  }
1265
1268
  this.replayMaxPerTopic = replay?.maxPerTopic ?? DEFAULT_REPLAY_MAX_PER_TOPIC;
1266
1269
  this.replayBuffers = replay ? /* @__PURE__ */ new Map() : null;
1270
+ this.replayPersistence = replay?.persistence ?? null;
1271
+ this.replayHydration = this.hydrateReplay();
1267
1272
  const { autoStart, initialConfig, trace, transport, ...clusterOptions } = options;
1268
1273
  this.transport = transport;
1269
1274
  this.initialConfig = initialConfig;
@@ -1432,7 +1437,13 @@ var CrossTabDataBus = class _CrossTabDataBus {
1432
1437
  typeof options.replay === "number" ? Math.floor(options.replay) : this.replayMaxPerTopic,
1433
1438
  this.replayMaxPerTopic
1434
1439
  );
1435
- this.deliverReplay(topic, limit, handler);
1440
+ if (this.replayPersistence) {
1441
+ void this.replayHydration.then(() => {
1442
+ if (this.topicHandlers.get(topic)?.has(handler)) this.deliverReplay(topic, limit, handler);
1443
+ });
1444
+ } else {
1445
+ this.deliverReplay(topic, limit, handler);
1446
+ }
1436
1447
  }
1437
1448
  return () => this.unsubscribe(topic, handler);
1438
1449
  }
@@ -1556,6 +1567,27 @@ var CrossTabDataBus = class _CrossTabDataBus {
1556
1567
  }
1557
1568
  buffer.push(message);
1558
1569
  if (buffer.length > this.replayMaxPerTopic) buffer.shift();
1570
+ if (this.replayPersistence) {
1571
+ void this.replayPersistence.append(message).catch((error) => this.reportError(error));
1572
+ }
1573
+ }
1574
+ async hydrateReplay() {
1575
+ if (!this.replayBuffers || !this.replayPersistence) {
1576
+ return;
1577
+ }
1578
+ try {
1579
+ for (const message of await this.replayPersistence.load()) {
1580
+ let buffer = this.replayBuffers.get(message.topic);
1581
+ if (!buffer) {
1582
+ buffer = [];
1583
+ this.replayBuffers.set(message.topic, buffer);
1584
+ }
1585
+ buffer.push(message);
1586
+ if (buffer.length > this.replayMaxPerTopic) buffer.shift();
1587
+ }
1588
+ } catch (error) {
1589
+ this.reportError(error);
1590
+ }
1559
1591
  }
1560
1592
  /** Deliver buffered history to a newly-registered handler. For an exact
1561
1593
  * topic this is that topic's ring; for a wildcard subscription every
@@ -1750,6 +1782,54 @@ function formatRouteTrace(route) {
1750
1782
  return `${route.topicKey}@${route.workerId}|confirmed=${route.confirmedAt !== void 0}`;
1751
1783
  }
1752
1784
 
1785
+ // src/core/replay-persistence.ts
1786
+ function createIndexedDbReplayPersistence(options) {
1787
+ const indexedDb = globalThis.indexedDB;
1788
+ if (!indexedDb) throw new Error("IndexedDB is unavailable in this environment.");
1789
+ const dbName = options.dbName ?? "cross-tab-worker-databus";
1790
+ const storeName = "replay";
1791
+ const maxPerTopic = options.maxPerTopic;
1792
+ if (!Number.isSafeInteger(maxPerTopic) || maxPerTopic <= 0) {
1793
+ throw new TypeError(`maxPerTopic must be a positive safe integer, got ${String(maxPerTopic)}.`);
1794
+ }
1795
+ let dbPromise = null;
1796
+ const open = () => {
1797
+ if (dbPromise) return dbPromise;
1798
+ dbPromise = new Promise((resolve, reject) => {
1799
+ const request = indexedDb.open(dbName, 1);
1800
+ request.onupgradeneeded = () => request.result.createObjectStore(storeName, { keyPath: "topic" });
1801
+ request.onsuccess = () => resolve(request.result);
1802
+ request.onerror = () => reject(request.error ?? new Error("Failed to open replay database."));
1803
+ });
1804
+ return dbPromise;
1805
+ };
1806
+ return {
1807
+ async load() {
1808
+ const db = await open();
1809
+ return new Promise((resolve, reject) => {
1810
+ const request = db.transaction(storeName, "readonly").objectStore(storeName).getAll();
1811
+ request.onsuccess = () => resolve(request.result.flatMap((record) => record.messages));
1812
+ request.onerror = () => reject(request.error ?? new Error("Failed to load replay history."));
1813
+ });
1814
+ },
1815
+ async append(message) {
1816
+ const db = await open();
1817
+ await new Promise((resolve, reject) => {
1818
+ const transaction = db.transaction(storeName, "readwrite");
1819
+ const store = transaction.objectStore(storeName);
1820
+ const request = store.get(message.topic);
1821
+ request.onsuccess = () => {
1822
+ const messages = (request.result?.messages ?? []).concat(message).slice(-maxPerTopic);
1823
+ store.put({ topic: message.topic, messages });
1824
+ };
1825
+ request.onerror = () => reject(request.error ?? new Error("Failed to read replay history."));
1826
+ transaction.oncomplete = () => resolve();
1827
+ transaction.onerror = () => reject(transaction.error ?? new Error("Failed to persist replay history."));
1828
+ });
1829
+ }
1830
+ };
1831
+ }
1832
+
1753
1833
  // src/websocket.ts
1754
1834
  var WS_OPEN = 1;
1755
1835
  var WebSocketTransport = class {