@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 +256 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +256 -28
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
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 =
|
|
1949
|
+
streamError = createIdleTimeoutError(retryConfig.connectionTimeoutMs);
|
|
1738
1950
|
}
|
|
1739
1951
|
if (streamDone) {
|
|
1740
1952
|
if (streamError) {
|
|
1741
1953
|
if (shouldStop(streamError)) return;
|
|
1742
|
-
const
|
|
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
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
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
|
}
|