@thru/replay 0.2.34 → 0.2.36

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