@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.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
|
-
|
|
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 =
|
|
1948
|
+
streamError = createIdleTimeoutError(retryConfig.connectionTimeoutMs);
|
|
1737
1949
|
}
|
|
1738
1950
|
if (streamDone) {
|
|
1739
1951
|
if (streamError) {
|
|
1740
1952
|
if (shouldStop(streamError)) return;
|
|
1741
|
-
const
|
|
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
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
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
|
}
|