@thru/replay 0.2.34 → 0.2.35

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/dist/index.cjs CHANGED
@@ -1350,6 +1350,15 @@ var PageAssembler = class {
1350
1350
 
1351
1351
  // src/account-replay.ts
1352
1352
  var DEFAULT_RECONNECT_CLEANUP_TIMEOUT_MS = 3e4;
1353
+ var REPLAY_IDLE_TIMEOUT_ERROR = "ReplayIdleTimeoutError";
1354
+ function createIdleTimeoutError(timeoutMs) {
1355
+ const error = new Error(`Operation timed out after ${timeoutMs}ms`);
1356
+ error.name = REPLAY_IDLE_TIMEOUT_ERROR;
1357
+ return error;
1358
+ }
1359
+ function isIdleTimeoutError(error) {
1360
+ return error.name === REPLAY_IDLE_TIMEOUT_ERROR;
1361
+ }
1353
1362
  async function closeAsyncIterator(iterator) {
1354
1363
  if (!iterator || typeof iterator.return !== "function") {
1355
1364
  return;
@@ -1510,9 +1519,11 @@ async function* createAccountsByOwnerReplay(options) {
1510
1519
  const seenFromStream = /* @__PURE__ */ new Set();
1511
1520
  const fetchQueue = [];
1512
1521
  let highestSlotSeen = minUpdatedSlot ?? 0n;
1522
+ const lastEmittedAccounts = /* @__PURE__ */ new Map();
1513
1523
  const assembler = new PageAssembler(pageAssemblerOptions);
1514
1524
  let cleanupTimer = null;
1515
1525
  const streamBuffer = [];
1526
+ const deferredStreamBuffer = [];
1516
1527
  let streamDone = false;
1517
1528
  let streamError = null;
1518
1529
  let lastActivityTime = Date.now();
@@ -1520,6 +1531,71 @@ async function* createAccountsByOwnerReplay(options) {
1520
1531
  let activeStreamProcessor = null;
1521
1532
  let streamGeneration = 0;
1522
1533
  let retryAttempt = 0;
1534
+ let pendingCatchUpFromSlot = null;
1535
+ const pendingCleanupTasks = /* @__PURE__ */ new Set();
1536
+ const shouldEmitAccountState = (account) => {
1537
+ const previous = lastEmittedAccounts.get(account.addressHex);
1538
+ if (previous && (account.slot < previous.slot || account.slot === previous.slot && account.seq <= previous.seq)) {
1539
+ return false;
1540
+ }
1541
+ lastEmittedAccounts.set(account.addressHex, {
1542
+ slot: account.slot,
1543
+ seq: account.seq
1544
+ });
1545
+ if (account.slot > highestSlotSeen) {
1546
+ highestSlotSeen = account.slot;
1547
+ }
1548
+ return true;
1549
+ };
1550
+ const queueReplayEvent = (event) => {
1551
+ if (event.type === "account") {
1552
+ if (!shouldEmitAccountState(event.account)) {
1553
+ return false;
1554
+ }
1555
+ seenFromStream.add(event.account.addressHex);
1556
+ }
1557
+ streamBuffer.push(event);
1558
+ return true;
1559
+ };
1560
+ const queueStreamEvent = (event) => {
1561
+ if (pendingCatchUpFromSlot !== null) {
1562
+ deferredStreamBuffer.push(event);
1563
+ return true;
1564
+ }
1565
+ return queueReplayEvent(event);
1566
+ };
1567
+ const flushDeferredStreamEvents = () => {
1568
+ while (deferredStreamBuffer.length > 0) {
1569
+ const event = deferredStreamBuffer.shift();
1570
+ queueReplayEvent(event);
1571
+ }
1572
+ };
1573
+ const getReconnectFromSlot = () => {
1574
+ if (pendingCatchUpFromSlot === null) {
1575
+ return highestSlotSeen;
1576
+ }
1577
+ if (highestSlotSeen <= 0n) {
1578
+ return pendingCatchUpFromSlot;
1579
+ }
1580
+ return pendingCatchUpFromSlot < highestSlotSeen ? pendingCatchUpFromSlot : highestSlotSeen;
1581
+ };
1582
+ const markCatchUpPending = (fromSlot) => {
1583
+ if (fromSlot <= 0n) {
1584
+ return fromSlot;
1585
+ }
1586
+ if (pendingCatchUpFromSlot === null || fromSlot < pendingCatchUpFromSlot) {
1587
+ pendingCatchUpFromSlot = fromSlot;
1588
+ }
1589
+ return pendingCatchUpFromSlot;
1590
+ };
1591
+ const markCatchUpCompleted = (fromSlot) => {
1592
+ if (fromSlot <= 0n) {
1593
+ return;
1594
+ }
1595
+ if (pendingCatchUpFromSlot !== null && fromSlot <= pendingCatchUpFromSlot) {
1596
+ pendingCatchUpFromSlot = null;
1597
+ }
1598
+ };
1523
1599
  const retireActiveStream = () => {
1524
1600
  const iterator = activeStreamIterator;
1525
1601
  const processor = activeStreamProcessor;
@@ -1549,6 +1625,18 @@ async function* createAccountsByOwnerReplay(options) {
1549
1625
  );
1550
1626
  }
1551
1627
  };
1628
+ const cleanupRetiredStreamInBackground = (retired, iteratorLabel, processorLabel) => {
1629
+ const cleanupTask = cleanupRetiredStream(retired, iteratorLabel, processorLabel).catch((err) => {
1630
+ logger.warn("Replay stream reconnect cleanup failed", {
1631
+ event: "replay.stream.reconnect.cleanup_failed",
1632
+ error: err
1633
+ });
1634
+ });
1635
+ pendingCleanupTasks.add(cleanupTask);
1636
+ cleanupTask.finally(() => {
1637
+ pendingCleanupTasks.delete(cleanupTask);
1638
+ });
1639
+ };
1552
1640
  const createFreshClient = () => {
1553
1641
  if (!clientFactory) {
1554
1642
  return;
@@ -1571,8 +1659,8 @@ async function* createAccountsByOwnerReplay(options) {
1571
1659
  });
1572
1660
  }
1573
1661
  };
1574
- const createStreamProcessor = (reason = "initial") => {
1575
- const minSlot = highestSlotSeen > 0n ? highestSlotSeen : minUpdatedSlot;
1662
+ const createStreamProcessor = (reason = "initial", minSlotOverride) => {
1663
+ const minSlot = minSlotOverride ?? (highestSlotSeen > 0n ? highestSlotSeen : minUpdatedSlot);
1576
1664
  const generation = ++streamGeneration;
1577
1665
  const streamStartedAtMs = Date.now();
1578
1666
  let firstMessageSeen = false;
@@ -1580,7 +1668,8 @@ async function* createAccountsByOwnerReplay(options) {
1580
1668
  logger.info("Replay stream waiting for first event", {
1581
1669
  event: "replay.stream.waiting_first_event",
1582
1670
  generation,
1583
- min_slot: minSlot?.toString()
1671
+ min_slot: minSlot?.toString(),
1672
+ highest_slot_seen: highestSlotSeen.toString()
1584
1673
  });
1585
1674
  }
1586
1675
  const newStreamFilter = buildOwnerFilterWithMinSlot(owner, dataSizes, minSlot);
@@ -1612,13 +1701,7 @@ async function* createAccountsByOwnerReplay(options) {
1612
1701
  }
1613
1702
  const event = processResponseMulti(response, assembler);
1614
1703
  if (event) {
1615
- if (event.type === "account") {
1616
- seenFromStream.add(event.account.addressHex);
1617
- if (event.account.slot > highestSlotSeen) {
1618
- highestSlotSeen = event.account.slot;
1619
- }
1620
- }
1621
- streamBuffer.push(event);
1704
+ queueStreamEvent(event);
1622
1705
  }
1623
1706
  }
1624
1707
  } catch (err) {
@@ -1633,6 +1716,141 @@ async function* createAccountsByOwnerReplay(options) {
1633
1716
  })();
1634
1717
  activeStreamProcessor = newProcessor;
1635
1718
  };
1719
+ const queueCatchUpAccounts = async (fromSlot, reason) => {
1720
+ if (fromSlot <= 0n) {
1721
+ return 0;
1722
+ }
1723
+ const startedAtMs = Date.now();
1724
+ let accountsListed = 0;
1725
+ let accountsFetched = 0;
1726
+ let accountsQueued = 0;
1727
+ let accountsSkipped = 0;
1728
+ logger.info("Replay stream reconnect catch-up started", {
1729
+ event: "replay.stream.reconnect.catch_up_started",
1730
+ reason,
1731
+ from_slot: fromSlot.toString()
1732
+ });
1733
+ const catchUpFilter = buildListAccountsOwnerFilter(owner, dataSizes, fromSlot);
1734
+ let pageToken;
1735
+ do {
1736
+ if (shouldStop()) return accountsQueued;
1737
+ const request = {
1738
+ view: proto.AccountView.META_ONLY,
1739
+ filter: catchUpFilter,
1740
+ page: protobuf.create(proto.PageRequestSchema, {
1741
+ pageSize,
1742
+ pageToken
1743
+ })
1744
+ };
1745
+ let response = null;
1746
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
1747
+ try {
1748
+ response = await client.listAccounts(request);
1749
+ break;
1750
+ } catch (err) {
1751
+ if (shouldStop(err)) return accountsQueued;
1752
+ if (attempt === maxRetries - 1) {
1753
+ logger.error("Replay stream reconnect catch-up list accounts failed", {
1754
+ event: "replay.stream.reconnect.catch_up_list_failed",
1755
+ reason,
1756
+ from_slot: fromSlot.toString(),
1757
+ page_token: pageToken,
1758
+ attempts: maxRetries,
1759
+ error: err
1760
+ });
1761
+ throw err;
1762
+ }
1763
+ logger.warn("Replay stream reconnect catch-up list accounts retrying", {
1764
+ event: "replay.stream.reconnect.catch_up_list_retry",
1765
+ reason,
1766
+ from_slot: fromSlot.toString(),
1767
+ page_token: pageToken,
1768
+ attempt: attempt + 1,
1769
+ max_retries: maxRetries,
1770
+ error: err
1771
+ });
1772
+ await abortableDelay(100 * (attempt + 1), signal);
1773
+ }
1774
+ }
1775
+ if (!response) {
1776
+ return accountsQueued;
1777
+ }
1778
+ for (const listedAccount of response.accounts) {
1779
+ if (shouldStop()) return accountsQueued;
1780
+ const address = listedAccount.address?.value;
1781
+ if (!address) {
1782
+ accountsSkipped++;
1783
+ continue;
1784
+ }
1785
+ accountsListed++;
1786
+ const addressHex = bytesToHex2(address);
1787
+ let account = null;
1788
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
1789
+ try {
1790
+ account = await client.getAccount({
1791
+ address: protobuf.create(proto.PubkeySchema, { value: address }),
1792
+ view: proto.AccountView.FULL
1793
+ });
1794
+ break;
1795
+ } catch (err) {
1796
+ if (shouldStop(err)) return accountsQueued;
1797
+ if (attempt === maxRetries - 1) {
1798
+ logger.error(`[catch-up] failed to fetch account ${addressHex} after ${maxRetries} attempts`, { error: err });
1799
+ } else {
1800
+ await abortableDelay(100 * (attempt + 1), signal);
1801
+ }
1802
+ }
1803
+ }
1804
+ if (!account) {
1805
+ accountsSkipped++;
1806
+ continue;
1807
+ }
1808
+ accountsFetched++;
1809
+ const state = getAccountToState(account);
1810
+ if (!state || state.slot < fromSlot) {
1811
+ accountsSkipped++;
1812
+ continue;
1813
+ }
1814
+ if (queueReplayEvent({ type: "account", account: state })) {
1815
+ accountsQueued++;
1816
+ } else {
1817
+ accountsSkipped++;
1818
+ }
1819
+ }
1820
+ pageToken = response.page?.nextPageToken;
1821
+ } while (pageToken);
1822
+ logger.info("Replay stream reconnect catch-up completed", {
1823
+ event: "replay.stream.reconnect.catch_up_completed",
1824
+ reason,
1825
+ from_slot: fromSlot.toString(),
1826
+ duration_ms: Date.now() - startedAtMs,
1827
+ accounts_listed: accountsListed,
1828
+ accounts_fetched: accountsFetched,
1829
+ accounts_queued: accountsQueued,
1830
+ accounts_skipped: accountsSkipped
1831
+ });
1832
+ return accountsQueued;
1833
+ };
1834
+ const queueCatchUpAccountsForReconnect = async (fromSlot, reason) => {
1835
+ const catchUpFromSlot = markCatchUpPending(fromSlot);
1836
+ try {
1837
+ await queueCatchUpAccounts(catchUpFromSlot, reason);
1838
+ if (shouldStop()) return;
1839
+ markCatchUpCompleted(catchUpFromSlot);
1840
+ flushDeferredStreamEvents();
1841
+ } catch (err) {
1842
+ if (shouldStop(err)) return;
1843
+ deferredStreamBuffer.length = 0;
1844
+ logger.warn("Replay stream reconnect catch-up failed", {
1845
+ event: "replay.stream.reconnect.catch_up_failed",
1846
+ reason,
1847
+ from_slot: catchUpFromSlot.toString(),
1848
+ error: err
1849
+ });
1850
+ streamDone = true;
1851
+ streamError = err instanceof Error ? err : new Error(String(err));
1852
+ }
1853
+ };
1636
1854
  try {
1637
1855
  if (shouldStop()) return;
1638
1856
  cleanupTimer = setInterval(() => {
@@ -1643,9 +1861,6 @@ async function* createAccountsByOwnerReplay(options) {
1643
1861
  const yieldStreamBuffer = function* () {
1644
1862
  while (streamBuffer.length > 0) {
1645
1863
  const event = streamBuffer.shift();
1646
- if (event.type === "account") {
1647
- seenFromStream.add(event.account.addressHex);
1648
- }
1649
1864
  yield event;
1650
1865
  }
1651
1866
  };
@@ -1706,10 +1921,7 @@ async function* createAccountsByOwnerReplay(options) {
1706
1921
  }
1707
1922
  if (account) {
1708
1923
  const state = getAccountToState(account);
1709
- if (state) {
1710
- if (state.slot > highestSlotSeen) {
1711
- highestSlotSeen = state.slot;
1712
- }
1924
+ if (state && shouldEmitAccountState(state)) {
1713
1925
  yield { type: "account", account: state };
1714
1926
  }
1715
1927
  }
@@ -1734,32 +1946,42 @@ async function* createAccountsByOwnerReplay(options) {
1734
1946
  connectionTimeoutMs: retryConfig.connectionTimeoutMs
1735
1947
  });
1736
1948
  streamDone = true;
1737
- streamError = new Error(`Operation timed out after ${retryConfig.connectionTimeoutMs}ms`);
1949
+ streamError = createIdleTimeoutError(retryConfig.connectionTimeoutMs);
1738
1950
  }
1739
1951
  if (streamDone) {
1740
1952
  if (streamError) {
1741
1953
  if (shouldStop(streamError)) return;
1742
- const backoffMs = calculateBackoff(retryAttempt, retryConfig);
1954
+ const idleTimeout = isIdleTimeoutError(streamError);
1955
+ const backoffMs = idleTimeout ? 0 : calculateBackoff(retryAttempt, retryConfig);
1743
1956
  logger.warn("Replay stream reconnect started", {
1744
1957
  event: "replay.stream.reconnect.started",
1745
1958
  reason: "stream_error",
1746
1959
  error: streamError.message,
1747
1960
  backoffMs,
1748
1961
  attempt: retryAttempt + 1,
1962
+ idle_timeout: idleTimeout,
1749
1963
  highestSlotSeen: highestSlotSeen.toString()
1750
1964
  });
1751
- await abortableDelay(backoffMs, signal);
1752
- if (shouldStop()) return;
1753
- retryAttempt++;
1965
+ if (backoffMs > 0) {
1966
+ await abortableDelay(backoffMs, signal);
1967
+ if (shouldStop()) return;
1968
+ }
1969
+ if (idleTimeout) {
1970
+ retryAttempt = 0;
1971
+ } else {
1972
+ retryAttempt++;
1973
+ }
1974
+ const reconnectFromSlot = getReconnectFromSlot();
1975
+ markCatchUpPending(reconnectFromSlot);
1754
1976
  const retired = retireActiveStream();
1755
1977
  streamDone = false;
1756
1978
  streamError = null;
1757
1979
  streamBuffer.length = 0;
1758
1980
  lastActivityTime = Date.now();
1759
- await cleanupRetiredStream(retired, "old iterator close", "old processor drain");
1760
- if (shouldStop()) return;
1761
1981
  createFreshClient();
1762
- createStreamProcessor("reconnect");
1982
+ createStreamProcessor("reconnect", reconnectFromSlot > 0n ? reconnectFromSlot : void 0);
1983
+ cleanupRetiredStreamInBackground(retired, "old iterator close", "old processor drain");
1984
+ await queueCatchUpAccountsForReconnect(reconnectFromSlot, "stream_error");
1763
1985
  continue;
1764
1986
  } else {
1765
1987
  if (shouldStop()) return;
@@ -1769,15 +1991,18 @@ async function* createAccountsByOwnerReplay(options) {
1769
1991
  attempt: retryAttempt + 1,
1770
1992
  highestSlotSeen: highestSlotSeen.toString()
1771
1993
  });
1994
+ retryAttempt++;
1995
+ const reconnectFromSlot = getReconnectFromSlot();
1996
+ markCatchUpPending(reconnectFromSlot);
1772
1997
  const retired = retireActiveStream();
1773
1998
  streamDone = false;
1774
1999
  streamError = null;
1775
2000
  streamBuffer.length = 0;
1776
2001
  lastActivityTime = Date.now();
1777
- await cleanupRetiredStream(retired, "old iterator close", "old processor drain");
1778
- if (shouldStop()) return;
1779
2002
  createFreshClient();
1780
- createStreamProcessor("reconnect");
2003
+ createStreamProcessor("reconnect", reconnectFromSlot > 0n ? reconnectFromSlot : void 0);
2004
+ cleanupRetiredStreamInBackground(retired, "old iterator close", "old processor drain");
2005
+ await queueCatchUpAccountsForReconnect(reconnectFromSlot, "stream_ended");
1781
2006
  continue;
1782
2007
  }
1783
2008
  }
@@ -1792,6 +2017,9 @@ async function* createAccountsByOwnerReplay(options) {
1792
2017
  closeIfCloseable(client);
1793
2018
  }
1794
2019
  await cleanupRetiredStream(retired, "final iterator close", "final processor drain");
2020
+ if (pendingCleanupTasks.size > 0) {
2021
+ await Promise.allSettled([...pendingCleanupTasks]);
2022
+ }
1795
2023
  assembler.clear();
1796
2024
  }
1797
2025
  }