@thru/replay 0.2.32 → 0.2.34
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 +299 -98
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +45 -41
- package/dist/index.d.ts +45 -41
- package/dist/index.mjs +299 -98
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -442,6 +442,7 @@ var DEFAULT_METRICS = {
|
|
|
442
442
|
emittedReconnect: 0,
|
|
443
443
|
discardedDuplicates: 0
|
|
444
444
|
};
|
|
445
|
+
var RECONNECT_STUCK_THRESHOLD_MS = 3e4;
|
|
445
446
|
function compareBigint(a, b) {
|
|
446
447
|
if (a === b) return 0;
|
|
447
448
|
return a < b ? -1 : 1;
|
|
@@ -643,6 +644,7 @@ var ReplayStream = class {
|
|
|
643
644
|
this.logger.info("replay entering STREAMING state");
|
|
644
645
|
const retryConfig = DEFAULT_RETRY_CONFIG;
|
|
645
646
|
let retryAttempt = 0;
|
|
647
|
+
let waitingFirstReconnectEvent = null;
|
|
646
648
|
while (true) {
|
|
647
649
|
if (shouldStop()) return;
|
|
648
650
|
try {
|
|
@@ -657,6 +659,16 @@ var ReplayStream = class {
|
|
|
657
659
|
throw new Error("stream ended");
|
|
658
660
|
}
|
|
659
661
|
const slot = extractSlot(next.value);
|
|
662
|
+
if (waitingFirstReconnectEvent) {
|
|
663
|
+
this.logger.info("Replay stream reconnect completed", {
|
|
664
|
+
event: "replay.stream.reconnect.completed",
|
|
665
|
+
attempt: waitingFirstReconnectEvent.attempt,
|
|
666
|
+
resume_slot: waitingFirstReconnectEvent.resumeSlot.toString(),
|
|
667
|
+
first_event_slot: slot.toString(),
|
|
668
|
+
duration_ms: Date.now() - waitingFirstReconnectEvent.startedAtMs
|
|
669
|
+
});
|
|
670
|
+
waitingFirstReconnectEvent = null;
|
|
671
|
+
}
|
|
660
672
|
const key = keyOf(next.value);
|
|
661
673
|
if (seenItem(slot, key)) {
|
|
662
674
|
this.metrics.discardedDuplicates += 1;
|
|
@@ -671,13 +683,45 @@ var ReplayStream = class {
|
|
|
671
683
|
if (shouldStop(err)) return;
|
|
672
684
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
673
685
|
const backoffMs = calculateBackoff(retryAttempt, retryConfig);
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
686
|
+
const attempt = retryAttempt + 1;
|
|
687
|
+
const reconnectStartedAtMs = Date.now();
|
|
688
|
+
this.logger.warn("Replay stream reconnect started", {
|
|
689
|
+
event: "replay.stream.reconnect.started",
|
|
690
|
+
reason: errMsg === "stream ended" ? "stream_ended" : "stream_error",
|
|
691
|
+
error: errMsg,
|
|
692
|
+
backoff_ms: backoffMs,
|
|
693
|
+
attempt,
|
|
694
|
+
current_slot: currentSlot.toString()
|
|
695
|
+
});
|
|
677
696
|
await abortableDelay(backoffMs, signal);
|
|
678
697
|
if (shouldStop()) return;
|
|
698
|
+
const cleanupStartedAtMs = Date.now();
|
|
699
|
+
this.logger.info("Replay stream reconnect cleanup started", {
|
|
700
|
+
event: "replay.stream.reconnect.cleanup_started",
|
|
701
|
+
phase: "live_pump_close",
|
|
702
|
+
attempt,
|
|
703
|
+
current_slot: currentSlot.toString()
|
|
704
|
+
});
|
|
679
705
|
currentDispose();
|
|
680
|
-
await safeClose(livePump);
|
|
706
|
+
const closeResult = await safeClose(livePump, signal);
|
|
707
|
+
const cleanupDurationMs = Date.now() - cleanupStartedAtMs;
|
|
708
|
+
if (closeResult === "timed-out") {
|
|
709
|
+
this.logger.warn("Replay stream reconnect cleanup stuck", {
|
|
710
|
+
event: "replay.stream.reconnect.stuck",
|
|
711
|
+
phase: "live_pump_close",
|
|
712
|
+
attempt,
|
|
713
|
+
duration_ms: cleanupDurationMs,
|
|
714
|
+
timeout_ms: RECONNECT_STUCK_THRESHOLD_MS
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
this.logger.info("Replay stream reconnect cleanup completed", {
|
|
718
|
+
event: "replay.stream.reconnect.cleanup_completed",
|
|
719
|
+
phase: "live_pump_close",
|
|
720
|
+
attempt,
|
|
721
|
+
duration_ms: cleanupDurationMs,
|
|
722
|
+
result: closeResult
|
|
723
|
+
});
|
|
724
|
+
if (shouldStop()) return;
|
|
681
725
|
retryAttempt++;
|
|
682
726
|
if (onReconnect) {
|
|
683
727
|
try {
|
|
@@ -688,11 +732,16 @@ var ReplayStream = class {
|
|
|
688
732
|
}
|
|
689
733
|
currentDispose = fresh.dispose ?? (() => {
|
|
690
734
|
});
|
|
691
|
-
this.logger.info("
|
|
735
|
+
this.logger.info("Replay stream fresh reconnect sources created", {
|
|
736
|
+
event: "replay.stream.reconnect.sources_created",
|
|
737
|
+
attempt
|
|
738
|
+
});
|
|
692
739
|
} catch (factoryErr) {
|
|
693
|
-
this.logger.error(
|
|
694
|
-
|
|
695
|
-
|
|
740
|
+
this.logger.error("Replay stream fresh reconnect sources failed", {
|
|
741
|
+
event: "replay.stream.reconnect.sources_failed",
|
|
742
|
+
attempt,
|
|
743
|
+
error: factoryErr
|
|
744
|
+
});
|
|
696
745
|
}
|
|
697
746
|
}
|
|
698
747
|
if (onReconnect && currentSlot > 0n) {
|
|
@@ -715,11 +764,21 @@ var ReplayStream = class {
|
|
|
715
764
|
}
|
|
716
765
|
const resumeSlot = currentSlot > 0n ? currentSlot : 0n;
|
|
717
766
|
livePump = createLivePump(resumeSlot, true, currentSlot);
|
|
767
|
+
waitingFirstReconnectEvent = {
|
|
768
|
+
startedAtMs: reconnectStartedAtMs,
|
|
769
|
+
attempt,
|
|
770
|
+
resumeSlot
|
|
771
|
+
};
|
|
772
|
+
this.logger.info("Replay stream waiting for first event", {
|
|
773
|
+
event: "replay.stream.waiting_first_event",
|
|
774
|
+
attempt,
|
|
775
|
+
resume_slot: resumeSlot.toString()
|
|
776
|
+
});
|
|
718
777
|
}
|
|
719
778
|
}
|
|
720
779
|
} finally {
|
|
721
780
|
currentDispose();
|
|
722
|
-
await safeClose(livePump);
|
|
781
|
+
await safeClose(livePump, signal);
|
|
723
782
|
}
|
|
724
783
|
}
|
|
725
784
|
/**
|
|
@@ -773,24 +832,45 @@ var ReplayStream = class {
|
|
|
773
832
|
}
|
|
774
833
|
}
|
|
775
834
|
};
|
|
776
|
-
async function safeClose(pump) {
|
|
835
|
+
async function safeClose(pump, signal) {
|
|
777
836
|
let timeoutId;
|
|
837
|
+
let onAbort;
|
|
778
838
|
try {
|
|
839
|
+
if (signal?.aborted) {
|
|
840
|
+
return "aborted";
|
|
841
|
+
}
|
|
779
842
|
const timeout = new Promise((resolve) => {
|
|
780
|
-
timeoutId = setTimeout(() => resolve("timeout"),
|
|
843
|
+
timeoutId = setTimeout(() => resolve("timeout"), RECONNECT_STUCK_THRESHOLD_MS);
|
|
781
844
|
});
|
|
845
|
+
const abort = signal ? new Promise((resolve) => {
|
|
846
|
+
onAbort = () => resolve("aborted");
|
|
847
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
848
|
+
}) : null;
|
|
849
|
+
const close = pump.close().then(
|
|
850
|
+
() => "closed",
|
|
851
|
+
() => "closed"
|
|
852
|
+
);
|
|
782
853
|
const result = await Promise.race([
|
|
783
|
-
|
|
784
|
-
timeout
|
|
854
|
+
close,
|
|
855
|
+
timeout,
|
|
856
|
+
...abort ? [abort] : []
|
|
785
857
|
]);
|
|
858
|
+
if (result === "aborted") {
|
|
859
|
+
return "aborted";
|
|
860
|
+
}
|
|
786
861
|
if (result === "timeout") {
|
|
862
|
+
return "timed-out";
|
|
787
863
|
}
|
|
788
864
|
} catch {
|
|
789
865
|
} finally {
|
|
790
866
|
if (timeoutId !== void 0) {
|
|
791
867
|
clearTimeout(timeoutId);
|
|
792
868
|
}
|
|
869
|
+
if (signal && onAbort) {
|
|
870
|
+
signal.removeEventListener("abort", onAbort);
|
|
871
|
+
}
|
|
793
872
|
}
|
|
873
|
+
return "closed";
|
|
794
874
|
}
|
|
795
875
|
function combineFilters(base, user) {
|
|
796
876
|
if (!base && !user) return void 0;
|
|
@@ -1268,6 +1348,7 @@ var PageAssembler = class {
|
|
|
1268
1348
|
};
|
|
1269
1349
|
|
|
1270
1350
|
// src/account-replay.ts
|
|
1351
|
+
var DEFAULT_RECONNECT_CLEANUP_TIMEOUT_MS = 3e4;
|
|
1271
1352
|
async function closeAsyncIterator(iterator) {
|
|
1272
1353
|
if (!iterator || typeof iterator.return !== "function") {
|
|
1273
1354
|
return;
|
|
@@ -1277,6 +1358,72 @@ async function closeAsyncIterator(iterator) {
|
|
|
1277
1358
|
} catch {
|
|
1278
1359
|
}
|
|
1279
1360
|
}
|
|
1361
|
+
async function waitForCleanup(promise, timeoutMs, label, logger, signal) {
|
|
1362
|
+
const startedAtMs = Date.now();
|
|
1363
|
+
logger.info("Replay stream reconnect cleanup started", {
|
|
1364
|
+
event: "replay.stream.reconnect.cleanup_started",
|
|
1365
|
+
phase: label,
|
|
1366
|
+
timeout_ms: timeoutMs
|
|
1367
|
+
});
|
|
1368
|
+
if (signal?.aborted) {
|
|
1369
|
+
logger.debug("Replay stream reconnect cleanup aborted", {
|
|
1370
|
+
event: "replay.stream.reconnect.cleanup_completed",
|
|
1371
|
+
phase: label,
|
|
1372
|
+
timeout_ms: timeoutMs,
|
|
1373
|
+
result: "aborted",
|
|
1374
|
+
duration_ms: 0
|
|
1375
|
+
});
|
|
1376
|
+
return "aborted";
|
|
1377
|
+
}
|
|
1378
|
+
let timer = null;
|
|
1379
|
+
let onAbort = null;
|
|
1380
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
1381
|
+
timer = setTimeout(() => resolve("timed-out"), timeoutMs);
|
|
1382
|
+
timer.unref?.();
|
|
1383
|
+
});
|
|
1384
|
+
const abortPromise = signal ? new Promise((resolve) => {
|
|
1385
|
+
onAbort = () => resolve("aborted");
|
|
1386
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1387
|
+
}) : null;
|
|
1388
|
+
const completionPromise = promise.then(
|
|
1389
|
+
() => "completed",
|
|
1390
|
+
() => "completed"
|
|
1391
|
+
);
|
|
1392
|
+
const result = await Promise.race(
|
|
1393
|
+
abortPromise ? [completionPromise, timeoutPromise, abortPromise] : [completionPromise, timeoutPromise]
|
|
1394
|
+
);
|
|
1395
|
+
if (timer) {
|
|
1396
|
+
clearTimeout(timer);
|
|
1397
|
+
}
|
|
1398
|
+
if (signal && onAbort) {
|
|
1399
|
+
signal.removeEventListener("abort", onAbort);
|
|
1400
|
+
}
|
|
1401
|
+
if (result === "timed-out") {
|
|
1402
|
+
logger.warn("Replay stream reconnect cleanup stuck", {
|
|
1403
|
+
event: "replay.stream.reconnect.stuck",
|
|
1404
|
+
phase: label,
|
|
1405
|
+
timeout_ms: timeoutMs,
|
|
1406
|
+
duration_ms: Date.now() - startedAtMs
|
|
1407
|
+
});
|
|
1408
|
+
} else if (result === "aborted") {
|
|
1409
|
+
logger.debug("Replay stream reconnect cleanup aborted", {
|
|
1410
|
+
event: "replay.stream.reconnect.cleanup_completed",
|
|
1411
|
+
phase: label,
|
|
1412
|
+
timeout_ms: timeoutMs,
|
|
1413
|
+
result,
|
|
1414
|
+
duration_ms: Date.now() - startedAtMs
|
|
1415
|
+
});
|
|
1416
|
+
} else {
|
|
1417
|
+
logger.info("Replay stream reconnect cleanup completed", {
|
|
1418
|
+
event: "replay.stream.reconnect.cleanup_completed",
|
|
1419
|
+
phase: label,
|
|
1420
|
+
timeout_ms: timeoutMs,
|
|
1421
|
+
result,
|
|
1422
|
+
duration_ms: Date.now() - startedAtMs
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
return result;
|
|
1426
|
+
}
|
|
1280
1427
|
function bytesToHex2(bytes) {
|
|
1281
1428
|
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1282
1429
|
}
|
|
@@ -1349,6 +1496,8 @@ async function* createAccountsByOwnerReplay(options) {
|
|
|
1349
1496
|
maxRetries = 3,
|
|
1350
1497
|
pageAssemblerOptions,
|
|
1351
1498
|
cleanupInterval = 1e4,
|
|
1499
|
+
reconnectCleanupTimeoutMs = DEFAULT_RECONNECT_CLEANUP_TIMEOUT_MS,
|
|
1500
|
+
retryConfig = DEFAULT_RETRY_CONFIG,
|
|
1352
1501
|
onBackfillComplete,
|
|
1353
1502
|
clientFactory,
|
|
1354
1503
|
logger = NOOP_LOGGER,
|
|
@@ -1368,23 +1517,98 @@ async function* createAccountsByOwnerReplay(options) {
|
|
|
1368
1517
|
let lastActivityTime = Date.now();
|
|
1369
1518
|
let activeStreamIterator = null;
|
|
1370
1519
|
let activeStreamProcessor = null;
|
|
1371
|
-
|
|
1520
|
+
let streamGeneration = 0;
|
|
1521
|
+
let retryAttempt = 0;
|
|
1522
|
+
const retireActiveStream = () => {
|
|
1523
|
+
const iterator = activeStreamIterator;
|
|
1524
|
+
const processor = activeStreamProcessor;
|
|
1525
|
+
activeStreamIterator = null;
|
|
1526
|
+
activeStreamProcessor = null;
|
|
1527
|
+
streamGeneration++;
|
|
1528
|
+
return { iterator, processor };
|
|
1529
|
+
};
|
|
1530
|
+
const cleanupRetiredStream = async (retired, iteratorLabel, processorLabel) => {
|
|
1531
|
+
if (retired.iterator) {
|
|
1532
|
+
await waitForCleanup(
|
|
1533
|
+
closeAsyncIterator(retired.iterator),
|
|
1534
|
+
reconnectCleanupTimeoutMs,
|
|
1535
|
+
iteratorLabel,
|
|
1536
|
+
logger,
|
|
1537
|
+
signal
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1372
1540
|
if (shouldStop()) return;
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1541
|
+
if (retired.processor) {
|
|
1542
|
+
await waitForCleanup(
|
|
1543
|
+
Promise.allSettled([retired.processor]),
|
|
1544
|
+
reconnectCleanupTimeoutMs,
|
|
1545
|
+
processorLabel,
|
|
1546
|
+
logger,
|
|
1547
|
+
signal
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
};
|
|
1551
|
+
const createFreshClient = () => {
|
|
1552
|
+
if (!clientFactory) {
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
logger.info("Replay stream fresh client creation started", {
|
|
1556
|
+
event: "replay.stream.reconnect.client_started"
|
|
1557
|
+
});
|
|
1558
|
+
try {
|
|
1559
|
+
const previousClient = client;
|
|
1560
|
+
const newClient = clientFactory();
|
|
1561
|
+
closeIfCloseable(previousClient);
|
|
1562
|
+
client = newClient;
|
|
1563
|
+
logger.info("Replay stream fresh client creation completed", {
|
|
1564
|
+
event: "replay.stream.reconnect.client_completed"
|
|
1565
|
+
});
|
|
1566
|
+
} catch (err) {
|
|
1567
|
+
logger.error("Replay stream fresh client creation failed", {
|
|
1568
|
+
event: "replay.stream.reconnect.client_failed",
|
|
1569
|
+
error: err
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
};
|
|
1573
|
+
const createStreamProcessor = (reason = "initial") => {
|
|
1574
|
+
const minSlot = highestSlotSeen > 0n ? highestSlotSeen : minUpdatedSlot;
|
|
1575
|
+
const generation = ++streamGeneration;
|
|
1576
|
+
const streamStartedAtMs = Date.now();
|
|
1577
|
+
let firstMessageSeen = false;
|
|
1578
|
+
if (reason === "reconnect") {
|
|
1579
|
+
logger.info("Replay stream waiting for first event", {
|
|
1580
|
+
event: "replay.stream.waiting_first_event",
|
|
1581
|
+
generation,
|
|
1582
|
+
min_slot: minSlot?.toString()
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
const newStreamFilter = buildOwnerFilterWithMinSlot(owner, dataSizes, minSlot);
|
|
1586
|
+
const newStream = client.streamAccountUpdates({ view, filter: newStreamFilter });
|
|
1587
|
+
const newStreamIterator = newStream[Symbol.asyncIterator]();
|
|
1588
|
+
streamDone = false;
|
|
1589
|
+
streamError = null;
|
|
1590
|
+
lastActivityTime = Date.now();
|
|
1591
|
+
activeStreamIterator = newStreamIterator;
|
|
1592
|
+
const newProcessor = (async () => {
|
|
1382
1593
|
try {
|
|
1383
1594
|
while (true) {
|
|
1384
|
-
const next = await
|
|
1595
|
+
const next = await newStreamIterator.next();
|
|
1596
|
+
if (generation !== streamGeneration) {
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1385
1599
|
if (next.done) break;
|
|
1386
1600
|
const response = next.value;
|
|
1601
|
+
retryAttempt = 0;
|
|
1387
1602
|
lastActivityTime = Date.now();
|
|
1603
|
+
if (reason === "reconnect" && !firstMessageSeen) {
|
|
1604
|
+
firstMessageSeen = true;
|
|
1605
|
+
logger.info("Replay stream reconnect completed", {
|
|
1606
|
+
event: "replay.stream.reconnect.completed",
|
|
1607
|
+
generation,
|
|
1608
|
+
first_message: true,
|
|
1609
|
+
duration_ms: Date.now() - streamStartedAtMs
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1388
1612
|
const event = processResponseMulti(response, assembler);
|
|
1389
1613
|
if (event) {
|
|
1390
1614
|
if (event.type === "account") {
|
|
@@ -1397,11 +1621,24 @@ async function* createAccountsByOwnerReplay(options) {
|
|
|
1397
1621
|
}
|
|
1398
1622
|
}
|
|
1399
1623
|
} catch (err) {
|
|
1400
|
-
|
|
1624
|
+
if (generation === streamGeneration) {
|
|
1625
|
+
streamError = err;
|
|
1626
|
+
}
|
|
1401
1627
|
} finally {
|
|
1402
|
-
|
|
1628
|
+
if (generation === streamGeneration) {
|
|
1629
|
+
streamDone = true;
|
|
1630
|
+
}
|
|
1403
1631
|
}
|
|
1404
1632
|
})();
|
|
1633
|
+
activeStreamProcessor = newProcessor;
|
|
1634
|
+
};
|
|
1635
|
+
try {
|
|
1636
|
+
if (shouldStop()) return;
|
|
1637
|
+
cleanupTimer = setInterval(() => {
|
|
1638
|
+
assembler.cleanup();
|
|
1639
|
+
}, cleanupInterval);
|
|
1640
|
+
cleanupTimer.unref?.();
|
|
1641
|
+
createStreamProcessor();
|
|
1405
1642
|
const yieldStreamBuffer = function* () {
|
|
1406
1643
|
while (streamBuffer.length > 0) {
|
|
1407
1644
|
const event = streamBuffer.shift();
|
|
@@ -1479,51 +1716,8 @@ async function* createAccountsByOwnerReplay(options) {
|
|
|
1479
1716
|
if (onBackfillComplete) {
|
|
1480
1717
|
onBackfillComplete(highestSlotSeen);
|
|
1481
1718
|
}
|
|
1482
|
-
|
|
1483
|
-
let retryAttempt = 0;
|
|
1719
|
+
retryAttempt = 0;
|
|
1484
1720
|
lastActivityTime = Date.now();
|
|
1485
|
-
const createStreamProcessor = () => {
|
|
1486
|
-
if (clientFactory) {
|
|
1487
|
-
try {
|
|
1488
|
-
const newClient = clientFactory();
|
|
1489
|
-
closeIfCloseable(client);
|
|
1490
|
-
client = newClient;
|
|
1491
|
-
logger.info("[account-stream] created fresh client for reconnection");
|
|
1492
|
-
} catch (err) {
|
|
1493
|
-
logger.error("[account-stream] failed to create fresh client", { error: err });
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
const newStreamFilter = buildOwnerFilterWithMinSlot(owner, dataSizes, highestSlotSeen > 0n ? highestSlotSeen : minUpdatedSlot);
|
|
1497
|
-
const newStream = client.streamAccountUpdates({ view, filter: newStreamFilter });
|
|
1498
|
-
const newStreamIterator = newStream[Symbol.asyncIterator]();
|
|
1499
|
-
activeStreamIterator = newStreamIterator;
|
|
1500
|
-
const newProcessor = (async () => {
|
|
1501
|
-
try {
|
|
1502
|
-
while (true) {
|
|
1503
|
-
const next = await newStreamIterator.next();
|
|
1504
|
-
if (next.done) break;
|
|
1505
|
-
const response = next.value;
|
|
1506
|
-
retryAttempt = 0;
|
|
1507
|
-
lastActivityTime = Date.now();
|
|
1508
|
-
const event = processResponseMulti(response, assembler);
|
|
1509
|
-
if (event) {
|
|
1510
|
-
if (event.type === "account") {
|
|
1511
|
-
seenFromStream.add(event.account.addressHex);
|
|
1512
|
-
if (event.account.slot > highestSlotSeen) {
|
|
1513
|
-
highestSlotSeen = event.account.slot;
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
|
-
streamBuffer.push(event);
|
|
1517
|
-
}
|
|
1518
|
-
}
|
|
1519
|
-
} catch (err) {
|
|
1520
|
-
streamError = err;
|
|
1521
|
-
} finally {
|
|
1522
|
-
streamDone = true;
|
|
1523
|
-
}
|
|
1524
|
-
})();
|
|
1525
|
-
return { iterator: newStreamIterator, processor: newProcessor };
|
|
1526
|
-
};
|
|
1527
1721
|
while (true) {
|
|
1528
1722
|
if (shouldStop()) return;
|
|
1529
1723
|
const hadEvents = streamBuffer.length > 0;
|
|
@@ -1531,10 +1725,13 @@ async function* createAccountsByOwnerReplay(options) {
|
|
|
1531
1725
|
if (hadEvents) {
|
|
1532
1726
|
lastActivityTime = Date.now();
|
|
1533
1727
|
}
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1728
|
+
const idleMs = Date.now() - lastActivityTime;
|
|
1729
|
+
if (!streamDone && idleMs > retryConfig.connectionTimeoutMs) {
|
|
1730
|
+
logger.warn("Replay stream idle timeout detected", {
|
|
1731
|
+
event: "replay.stream.idle_timeout",
|
|
1732
|
+
idleMs,
|
|
1733
|
+
connectionTimeoutMs: retryConfig.connectionTimeoutMs
|
|
1734
|
+
});
|
|
1538
1735
|
streamDone = true;
|
|
1539
1736
|
streamError = new Error(`Operation timed out after ${retryConfig.connectionTimeoutMs}ms`);
|
|
1540
1737
|
}
|
|
@@ -1542,36 +1739,44 @@ async function* createAccountsByOwnerReplay(options) {
|
|
|
1542
1739
|
if (streamError) {
|
|
1543
1740
|
if (shouldStop(streamError)) return;
|
|
1544
1741
|
const backoffMs = calculateBackoff(retryAttempt, retryConfig);
|
|
1545
|
-
logger.warn(
|
|
1546
|
-
|
|
1547
|
-
|
|
1742
|
+
logger.warn("Replay stream reconnect started", {
|
|
1743
|
+
event: "replay.stream.reconnect.started",
|
|
1744
|
+
reason: "stream_error",
|
|
1745
|
+
error: streamError.message,
|
|
1746
|
+
backoffMs,
|
|
1747
|
+
attempt: retryAttempt + 1,
|
|
1748
|
+
highestSlotSeen: highestSlotSeen.toString()
|
|
1749
|
+
});
|
|
1548
1750
|
await abortableDelay(backoffMs, signal);
|
|
1549
1751
|
if (shouldStop()) return;
|
|
1550
1752
|
retryAttempt++;
|
|
1753
|
+
const retired = retireActiveStream();
|
|
1551
1754
|
streamDone = false;
|
|
1552
1755
|
streamError = null;
|
|
1553
1756
|
streamBuffer.length = 0;
|
|
1554
1757
|
lastActivityTime = Date.now();
|
|
1555
|
-
await
|
|
1556
|
-
if (
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
const { iterator: newIterator, processor: newProcessor } = createStreamProcessor();
|
|
1560
|
-
activeStreamIterator = newIterator;
|
|
1561
|
-
activeStreamProcessor = newProcessor;
|
|
1758
|
+
await cleanupRetiredStream(retired, "old iterator close", "old processor drain");
|
|
1759
|
+
if (shouldStop()) return;
|
|
1760
|
+
createFreshClient();
|
|
1761
|
+
createStreamProcessor("reconnect");
|
|
1562
1762
|
continue;
|
|
1563
1763
|
} else {
|
|
1564
1764
|
if (shouldStop()) return;
|
|
1565
|
-
logger.warn("
|
|
1765
|
+
logger.warn("Replay stream reconnect started", {
|
|
1766
|
+
event: "replay.stream.reconnect.started",
|
|
1767
|
+
reason: "stream_ended",
|
|
1768
|
+
attempt: retryAttempt + 1,
|
|
1769
|
+
highestSlotSeen: highestSlotSeen.toString()
|
|
1770
|
+
});
|
|
1771
|
+
const retired = retireActiveStream();
|
|
1566
1772
|
streamDone = false;
|
|
1773
|
+
streamError = null;
|
|
1774
|
+
streamBuffer.length = 0;
|
|
1567
1775
|
lastActivityTime = Date.now();
|
|
1568
|
-
await
|
|
1569
|
-
if (
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
const { iterator: newIterator, processor: newProcessor } = createStreamProcessor();
|
|
1573
|
-
activeStreamIterator = newIterator;
|
|
1574
|
-
activeStreamProcessor = newProcessor;
|
|
1776
|
+
await cleanupRetiredStream(retired, "old iterator close", "old processor drain");
|
|
1777
|
+
if (shouldStop()) return;
|
|
1778
|
+
createFreshClient();
|
|
1779
|
+
createStreamProcessor("reconnect");
|
|
1575
1780
|
continue;
|
|
1576
1781
|
}
|
|
1577
1782
|
}
|
|
@@ -1581,15 +1786,11 @@ async function* createAccountsByOwnerReplay(options) {
|
|
|
1581
1786
|
if (cleanupTimer) {
|
|
1582
1787
|
clearInterval(cleanupTimer);
|
|
1583
1788
|
}
|
|
1584
|
-
const
|
|
1789
|
+
const retired = retireActiveStream();
|
|
1585
1790
|
if (ownsClient) {
|
|
1586
1791
|
closeIfCloseable(client);
|
|
1587
1792
|
}
|
|
1588
|
-
|
|
1589
|
-
await Promise.allSettled([closeIteratorPromise, activeStreamProcessor]);
|
|
1590
|
-
} else {
|
|
1591
|
-
await closeIteratorPromise;
|
|
1592
|
-
}
|
|
1793
|
+
await cleanupRetiredStream(retired, "final iterator close", "final processor drain");
|
|
1593
1794
|
assembler.clear();
|
|
1594
1795
|
}
|
|
1595
1796
|
}
|