@thru/replay 0.2.21 → 0.2.22
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 +389 -173
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +33 -4
- package/dist/index.d.ts +33 -4
- package/dist/index.mjs +391 -175
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -12
package/dist/index.cjs
CHANGED
|
@@ -3,20 +3,49 @@
|
|
|
3
3
|
var protobuf = require('@bufbuild/protobuf');
|
|
4
4
|
var connect = require('@connectrpc/connect');
|
|
5
5
|
var connectNode = require('@connectrpc/connect-node');
|
|
6
|
-
var proto = require('@thru/proto');
|
|
6
|
+
var proto = require('@thru/sdk/proto');
|
|
7
7
|
|
|
8
8
|
// src/chain-client.ts
|
|
9
9
|
var ChainClient = class {
|
|
10
10
|
constructor(options) {
|
|
11
11
|
this.options = options;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
if (options.transport) {
|
|
13
|
+
this.sessionManager = null;
|
|
14
|
+
this.query = connect.createClient(proto.QueryService, options.transport);
|
|
15
|
+
this.streaming = connect.createClient(proto.StreamingService, options.transport);
|
|
16
|
+
} else {
|
|
17
|
+
const { transport, sessionManager } = this.createOwnedTransport();
|
|
18
|
+
this.sessionManager = sessionManager;
|
|
19
|
+
this.query = connect.createClient(proto.QueryService, transport);
|
|
20
|
+
this.streaming = connect.createClient(proto.StreamingService, transport);
|
|
21
|
+
}
|
|
15
22
|
this.callOptions = options.callOptions;
|
|
16
23
|
}
|
|
24
|
+
options;
|
|
17
25
|
query;
|
|
18
26
|
streaming;
|
|
19
27
|
callOptions;
|
|
28
|
+
/**
|
|
29
|
+
* The HTTP/2 session manager owned by this client. Only set when the client
|
|
30
|
+
* created its own gRPC transport (i.e., `options.transport` was not provided).
|
|
31
|
+
* `close()` uses this to tear down the underlying persistent connection.
|
|
32
|
+
*/
|
|
33
|
+
sessionManager;
|
|
34
|
+
closed = false;
|
|
35
|
+
/**
|
|
36
|
+
* Close the underlying HTTP/2 session, if this client owns one. Idempotent.
|
|
37
|
+
*
|
|
38
|
+
* Callers are responsible for ensuring that no in-flight RPCs or streams
|
|
39
|
+
* are still being awaited on this client — pending requests will fail.
|
|
40
|
+
*
|
|
41
|
+
* If the client was constructed with an externally-supplied `transport`,
|
|
42
|
+
* `close()` is a no-op; the caller owns the transport's lifecycle.
|
|
43
|
+
*/
|
|
44
|
+
close() {
|
|
45
|
+
if (this.closed) return;
|
|
46
|
+
this.closed = true;
|
|
47
|
+
this.sessionManager?.abort();
|
|
48
|
+
}
|
|
20
49
|
getAccount(request) {
|
|
21
50
|
return this.query.getAccount(protobuf.create(proto.GetAccountRequestSchema, request), this.callOptions);
|
|
22
51
|
}
|
|
@@ -41,7 +70,7 @@ var ChainClient = class {
|
|
|
41
70
|
streamEvents(request) {
|
|
42
71
|
return this.streaming.streamEvents(protobuf.create(proto.StreamEventsRequestSchema, request), this.callOptions);
|
|
43
72
|
}
|
|
44
|
-
|
|
73
|
+
createOwnedTransport() {
|
|
45
74
|
if (!this.options.baseUrl) {
|
|
46
75
|
throw new Error("ChainClient requires baseUrl when no transport is provided");
|
|
47
76
|
}
|
|
@@ -51,15 +80,19 @@ var ChainClient = class {
|
|
|
51
80
|
...userInterceptors,
|
|
52
81
|
...headerInterceptor ? [headerInterceptor] : []
|
|
53
82
|
];
|
|
54
|
-
|
|
55
|
-
baseUrl: this.options.baseUrl,
|
|
56
|
-
useBinaryFormat: this.options.useBinaryFormat ?? true,
|
|
57
|
-
interceptors: mergedInterceptors.length ? mergedInterceptors : void 0,
|
|
83
|
+
const sessionManager = new connectNode.Http2SessionManager(this.options.baseUrl, {
|
|
58
84
|
pingIntervalMs: 3e4,
|
|
59
85
|
pingIdleConnection: true,
|
|
60
86
|
pingTimeoutMs: 1e4,
|
|
61
87
|
idleConnectionTimeoutMs: 0
|
|
62
88
|
});
|
|
89
|
+
const transport = connectNode.createGrpcTransport({
|
|
90
|
+
baseUrl: this.options.baseUrl,
|
|
91
|
+
useBinaryFormat: this.options.useBinaryFormat ?? true,
|
|
92
|
+
interceptors: mergedInterceptors.length ? mergedInterceptors : void 0,
|
|
93
|
+
sessionManager
|
|
94
|
+
});
|
|
95
|
+
return { transport, sessionManager };
|
|
63
96
|
}
|
|
64
97
|
createHeaderInterceptor() {
|
|
65
98
|
const headers = {};
|
|
@@ -77,6 +110,9 @@ var ChainClient = class {
|
|
|
77
110
|
getHeight() {
|
|
78
111
|
return this.query.getHeight(protobuf.create(proto.GetHeightRequestSchema, {}), this.callOptions);
|
|
79
112
|
}
|
|
113
|
+
getChainInfo(request = {}) {
|
|
114
|
+
return this.query.getChainInfo(protobuf.create(proto.GetChainInfoRequestSchema, request), this.callOptions);
|
|
115
|
+
}
|
|
80
116
|
};
|
|
81
117
|
|
|
82
118
|
// src/async-queue.ts
|
|
@@ -239,15 +275,16 @@ var LivePump = class {
|
|
|
239
275
|
buffer;
|
|
240
276
|
slotOf;
|
|
241
277
|
keyOf;
|
|
242
|
-
|
|
278
|
+
sourceIterator;
|
|
243
279
|
logger;
|
|
244
280
|
mode;
|
|
245
281
|
minSlotSeen = null;
|
|
246
282
|
maxSlotSeen = null;
|
|
247
283
|
minEmitSlot = null;
|
|
248
284
|
pumpPromise;
|
|
285
|
+
closing = false;
|
|
249
286
|
constructor(options) {
|
|
250
|
-
this.
|
|
287
|
+
this.sourceIterator = options.source[Symbol.asyncIterator]();
|
|
251
288
|
this.slotOf = options.slotOf;
|
|
252
289
|
this.keyOf = options.keyOf ?? ((item) => options.slotOf(item).toString());
|
|
253
290
|
this.logger = options.logger ?? NOOP_LOGGER;
|
|
@@ -291,12 +328,19 @@ var LivePump = class {
|
|
|
291
328
|
return this.queue.next();
|
|
292
329
|
}
|
|
293
330
|
async close() {
|
|
331
|
+
this.closing = true;
|
|
294
332
|
this.queue.close();
|
|
295
|
-
await
|
|
333
|
+
await Promise.allSettled([
|
|
334
|
+
this.closeSourceIterator(),
|
|
335
|
+
this.pumpPromise
|
|
336
|
+
]);
|
|
296
337
|
}
|
|
297
338
|
async start() {
|
|
298
339
|
try {
|
|
299
|
-
|
|
340
|
+
while (!this.closing) {
|
|
341
|
+
const next = await this.sourceIterator.next();
|
|
342
|
+
if (next.done || this.closing) break;
|
|
343
|
+
const item = next.value;
|
|
300
344
|
const slot = this.slotOf(item);
|
|
301
345
|
if (this.minSlotSeen === null || slot < this.minSlotSeen) this.minSlotSeen = slot;
|
|
302
346
|
if (this.maxSlotSeen === null || slot > this.maxSlotSeen) this.maxSlotSeen = slot;
|
|
@@ -306,9 +350,21 @@ var LivePump = class {
|
|
|
306
350
|
this.queue.push(item);
|
|
307
351
|
}
|
|
308
352
|
}
|
|
309
|
-
this.queue.close();
|
|
310
353
|
} catch (err) {
|
|
311
|
-
this.
|
|
354
|
+
if (!this.closing) {
|
|
355
|
+
this.queue.fail(err);
|
|
356
|
+
}
|
|
357
|
+
} finally {
|
|
358
|
+
this.queue.close();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async closeSourceIterator() {
|
|
362
|
+
if (typeof this.sourceIterator.return !== "function") {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
await this.sourceIterator.return();
|
|
367
|
+
} catch {
|
|
312
368
|
}
|
|
313
369
|
}
|
|
314
370
|
};
|
|
@@ -328,6 +384,7 @@ function withTimeout(promise, timeoutMs) {
|
|
|
328
384
|
const timer = setTimeout(() => {
|
|
329
385
|
reject(new TimeoutError(`Operation timed out after ${timeoutMs}ms`));
|
|
330
386
|
}, timeoutMs);
|
|
387
|
+
timer.unref?.();
|
|
331
388
|
promise.then((value) => {
|
|
332
389
|
clearTimeout(timer);
|
|
333
390
|
resolve(value);
|
|
@@ -346,6 +403,37 @@ var TimeoutError = class extends Error {
|
|
|
346
403
|
function delay(ms) {
|
|
347
404
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
348
405
|
}
|
|
406
|
+
function abortableDelay(ms, signal) {
|
|
407
|
+
if (ms <= 0 || signal?.aborted) {
|
|
408
|
+
return Promise.resolve();
|
|
409
|
+
}
|
|
410
|
+
const abortSignal = signal;
|
|
411
|
+
return new Promise((resolve) => {
|
|
412
|
+
if (!abortSignal) {
|
|
413
|
+
const timer2 = setTimeout(resolve, ms);
|
|
414
|
+
timer2.unref?.();
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const onAbort = () => {
|
|
418
|
+
clearTimeout(timer);
|
|
419
|
+
abortSignal.removeEventListener("abort", onAbort);
|
|
420
|
+
resolve();
|
|
421
|
+
};
|
|
422
|
+
const timer = setTimeout(() => {
|
|
423
|
+
abortSignal.removeEventListener("abort", onAbort);
|
|
424
|
+
resolve();
|
|
425
|
+
}, ms);
|
|
426
|
+
timer.unref?.();
|
|
427
|
+
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
function isAbortError(err) {
|
|
431
|
+
if (!err || typeof err !== "object") return false;
|
|
432
|
+
const name = typeof err.name === "string" ? err.name.toLowerCase() : "";
|
|
433
|
+
const message = typeof err.message === "string" ? err.message.toLowerCase() : "";
|
|
434
|
+
const code = String(err.code ?? "").toLowerCase();
|
|
435
|
+
return name.includes("abort") || name.includes("cancel") || code === "canceled" || code === "cancelled" || message.includes("aborted") || message.includes("canceled") || message.includes("cancelled");
|
|
436
|
+
}
|
|
349
437
|
|
|
350
438
|
// src/replay-stream.ts
|
|
351
439
|
var DEFAULT_METRICS = {
|
|
@@ -403,12 +491,17 @@ var ReplayStream = class {
|
|
|
403
491
|
extractKey,
|
|
404
492
|
safetyMargin,
|
|
405
493
|
resubscribeOnEnd,
|
|
406
|
-
onReconnect
|
|
494
|
+
onReconnect,
|
|
495
|
+
signal,
|
|
496
|
+
dispose
|
|
407
497
|
} = this.config;
|
|
408
498
|
const shouldResubscribeOnEnd = resubscribeOnEnd ?? true;
|
|
409
499
|
const keyOf = extractKey ?? ((item) => extractSlot(item).toString());
|
|
500
|
+
const shouldStop = (err) => signal?.aborted === true || isAbortError(err);
|
|
410
501
|
let currentSubscribeLive = subscribeLive;
|
|
411
502
|
let currentFetchBackfill = fetchBackfill;
|
|
503
|
+
let currentDispose = dispose ?? (() => {
|
|
504
|
+
});
|
|
412
505
|
const createLivePump = (slot, startStreaming = false, emitFloor) => new LivePump({
|
|
413
506
|
source: currentSubscribeLive(slot),
|
|
414
507
|
slotOf: extractSlot,
|
|
@@ -465,92 +558,78 @@ var ReplayStream = class {
|
|
|
465
558
|
};
|
|
466
559
|
let emptyPageRetries = 0;
|
|
467
560
|
const MAX_EMPTY_PAGE_RETRIES = 10;
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
561
|
+
try {
|
|
562
|
+
while (!backfillDone) {
|
|
563
|
+
if (shouldStop()) return;
|
|
564
|
+
let page;
|
|
565
|
+
try {
|
|
566
|
+
page = await currentFetchBackfill({ startSlot, cursor });
|
|
567
|
+
} catch (err) {
|
|
568
|
+
if (shouldStop(err)) return;
|
|
569
|
+
throw err;
|
|
570
|
+
}
|
|
571
|
+
if (!page.items.length && !page.cursor && !page.done) {
|
|
572
|
+
emptyPageRetries++;
|
|
573
|
+
if (emptyPageRetries > MAX_EMPTY_PAGE_RETRIES) {
|
|
574
|
+
this.logger.error(
|
|
575
|
+
`backfill returned ${MAX_EMPTY_PAGE_RETRIES} consecutive empty pages; treating as done`
|
|
576
|
+
);
|
|
577
|
+
for await (const item of flushPendingBackfill(this)) {
|
|
578
|
+
yield item;
|
|
579
|
+
}
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
const backoffMs = calculateBackoff(emptyPageRetries - 1, DEFAULT_RETRY_CONFIG);
|
|
583
|
+
this.logger.warn(
|
|
584
|
+
`empty backfill page without cursor; retrying in ${backoffMs}ms (${emptyPageRetries}/${MAX_EMPTY_PAGE_RETRIES})`
|
|
475
585
|
);
|
|
586
|
+
await abortableDelay(backoffMs, signal);
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
emptyPageRetries = 0;
|
|
590
|
+
assertBackfillPageOrder(pendingOrderedPage, page.items, extractSlot);
|
|
591
|
+
if (pendingOrderedPage !== null) {
|
|
476
592
|
for await (const item of flushPendingBackfill(this)) {
|
|
593
|
+
if (shouldStop()) return;
|
|
477
594
|
yield item;
|
|
478
595
|
}
|
|
479
|
-
break;
|
|
480
|
-
}
|
|
481
|
-
const backoffMs = calculateBackoff(emptyPageRetries - 1, DEFAULT_RETRY_CONFIG);
|
|
482
|
-
this.logger.warn(
|
|
483
|
-
`empty backfill page without cursor; retrying in ${backoffMs}ms (${emptyPageRetries}/${MAX_EMPTY_PAGE_RETRIES})`
|
|
484
|
-
);
|
|
485
|
-
await delay(backoffMs);
|
|
486
|
-
continue;
|
|
487
|
-
}
|
|
488
|
-
emptyPageRetries = 0;
|
|
489
|
-
assertBackfillPageOrder(pendingOrderedPage, page.items, extractSlot);
|
|
490
|
-
if (pendingOrderedPage !== null) {
|
|
491
|
-
for await (const item of flushPendingBackfill(this)) {
|
|
492
|
-
yield item;
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
pendingOrderedPage = [...page.items];
|
|
496
|
-
const reachedEnd = page.done || page.cursor === void 0;
|
|
497
|
-
if (reachedEnd) {
|
|
498
|
-
for await (const item of flushPendingBackfill(this)) {
|
|
499
|
-
yield item;
|
|
500
596
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
cursor = page.cursor;
|
|
505
|
-
const maxStreamSlot = livePump.maxSlot();
|
|
506
|
-
if (maxStreamSlot !== null) {
|
|
507
|
-
const catchUpSlot = maxStreamSlot > safetyMargin ? maxStreamSlot - safetyMargin : 0n;
|
|
508
|
-
if (currentSlot >= catchUpSlot) {
|
|
597
|
+
pendingOrderedPage = [...page.items];
|
|
598
|
+
const reachedEnd = page.done || page.cursor === void 0;
|
|
599
|
+
if (reachedEnd) {
|
|
509
600
|
for await (const item of flushPendingBackfill(this)) {
|
|
601
|
+
if (shouldStop()) return;
|
|
510
602
|
yield item;
|
|
511
603
|
}
|
|
512
|
-
this.logger.info(
|
|
513
|
-
`replay reached SWITCHING threshold (currentSlot=${currentSlot}, maxStreamSlot=${maxStreamSlot}, catchUpSlot=${catchUpSlot})`
|
|
514
|
-
);
|
|
515
|
-
backfillDone = true;
|
|
516
604
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
this.metrics.emittedLive += 1;
|
|
534
|
-
yield item;
|
|
535
|
-
livePump.updateEmitFloor(currentSlot);
|
|
536
|
-
}
|
|
537
|
-
if (!drained.length) livePump.updateEmitFloor(currentSlot);
|
|
538
|
-
this.logger.info("replay entering STREAMING state");
|
|
539
|
-
const retryConfig = DEFAULT_RETRY_CONFIG;
|
|
540
|
-
let retryAttempt = 0;
|
|
541
|
-
while (true) {
|
|
542
|
-
try {
|
|
543
|
-
const next = await withTimeout(
|
|
544
|
-
livePump.next(),
|
|
545
|
-
retryConfig.connectionTimeoutMs
|
|
546
|
-
);
|
|
547
|
-
retryAttempt = 0;
|
|
548
|
-
if (next.done) {
|
|
549
|
-
if (!shouldResubscribeOnEnd) break;
|
|
550
|
-
throw new Error("stream ended");
|
|
605
|
+
const duplicatesTrimmed = livePump.discardBufferedUpTo(currentSlot);
|
|
606
|
+
this.metrics.discardedDuplicates += duplicatesTrimmed;
|
|
607
|
+
cursor = page.cursor;
|
|
608
|
+
const maxStreamSlot = livePump.maxSlot();
|
|
609
|
+
if (maxStreamSlot !== null) {
|
|
610
|
+
const catchUpSlot = maxStreamSlot > safetyMargin ? maxStreamSlot - safetyMargin : 0n;
|
|
611
|
+
if (currentSlot >= catchUpSlot) {
|
|
612
|
+
for await (const item of flushPendingBackfill(this)) {
|
|
613
|
+
if (shouldStop()) return;
|
|
614
|
+
yield item;
|
|
615
|
+
}
|
|
616
|
+
this.logger.info(
|
|
617
|
+
`replay reached SWITCHING threshold (currentSlot=${currentSlot}, maxStreamSlot=${maxStreamSlot}, catchUpSlot=${catchUpSlot})`
|
|
618
|
+
);
|
|
619
|
+
backfillDone = true;
|
|
620
|
+
}
|
|
551
621
|
}
|
|
552
|
-
|
|
553
|
-
|
|
622
|
+
if (reachedEnd) backfillDone = true;
|
|
623
|
+
}
|
|
624
|
+
if (shouldStop()) return;
|
|
625
|
+
this.logger.info(`replay entering SWITCHING state (currentSlot=${currentSlot})`);
|
|
626
|
+
const { drained, discarded } = livePump.enableStreaming(currentSlot);
|
|
627
|
+
this.metrics.bufferedItems = drained.length;
|
|
628
|
+
this.metrics.discardedDuplicates += discarded;
|
|
629
|
+
for (const item of drained) {
|
|
630
|
+
if (shouldStop()) return;
|
|
631
|
+
const slot = extractSlot(item);
|
|
632
|
+
const key = keyOf(item);
|
|
554
633
|
if (seenItem(slot, key)) {
|
|
555
634
|
this.metrics.discardedDuplicates += 1;
|
|
556
635
|
continue;
|
|
@@ -558,57 +637,97 @@ var ReplayStream = class {
|
|
|
558
637
|
currentSlot = slot;
|
|
559
638
|
recordEmission(slot, key);
|
|
560
639
|
this.metrics.emittedLive += 1;
|
|
561
|
-
yield
|
|
640
|
+
yield item;
|
|
562
641
|
livePump.updateEmitFloor(currentSlot);
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
642
|
+
}
|
|
643
|
+
if (!drained.length) livePump.updateEmitFloor(currentSlot);
|
|
644
|
+
this.logger.info("replay entering STREAMING state");
|
|
645
|
+
const retryConfig = DEFAULT_RETRY_CONFIG;
|
|
646
|
+
let retryAttempt = 0;
|
|
647
|
+
while (true) {
|
|
648
|
+
if (shouldStop()) return;
|
|
649
|
+
try {
|
|
650
|
+
const next = await withTimeout(
|
|
651
|
+
livePump.next(),
|
|
652
|
+
retryConfig.connectionTimeoutMs
|
|
653
|
+
);
|
|
654
|
+
retryAttempt = 0;
|
|
655
|
+
if (next.done) {
|
|
656
|
+
if (shouldStop()) return;
|
|
657
|
+
if (!shouldResubscribeOnEnd) break;
|
|
658
|
+
throw new Error("stream ended");
|
|
659
|
+
}
|
|
660
|
+
const slot = extractSlot(next.value);
|
|
661
|
+
const key = keyOf(next.value);
|
|
662
|
+
if (seenItem(slot, key)) {
|
|
663
|
+
this.metrics.discardedDuplicates += 1;
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
currentSlot = slot;
|
|
667
|
+
recordEmission(slot, key);
|
|
668
|
+
this.metrics.emittedLive += 1;
|
|
669
|
+
yield next.value;
|
|
670
|
+
livePump.updateEmitFloor(currentSlot);
|
|
671
|
+
} catch (err) {
|
|
672
|
+
if (shouldStop(err)) return;
|
|
673
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
674
|
+
const backoffMs = calculateBackoff(retryAttempt, retryConfig);
|
|
675
|
+
this.logger.warn(
|
|
676
|
+
`live stream disconnected (${errMsg}); reconnecting in ${backoffMs}ms from slot ${currentSlot} (attempt ${retryAttempt + 1})`
|
|
677
|
+
);
|
|
678
|
+
await abortableDelay(backoffMs, signal);
|
|
679
|
+
if (shouldStop()) return;
|
|
680
|
+
currentDispose();
|
|
681
|
+
await safeClose(livePump);
|
|
682
|
+
retryAttempt++;
|
|
683
|
+
if (onReconnect) {
|
|
684
|
+
try {
|
|
685
|
+
const fresh = onReconnect();
|
|
686
|
+
currentSubscribeLive = fresh.subscribeLive;
|
|
687
|
+
if (fresh.fetchBackfill) {
|
|
688
|
+
currentFetchBackfill = fresh.fetchBackfill;
|
|
689
|
+
}
|
|
690
|
+
currentDispose = fresh.dispose ?? (() => {
|
|
691
|
+
});
|
|
692
|
+
this.logger.info("created fresh client for reconnection");
|
|
693
|
+
} catch (factoryErr) {
|
|
694
|
+
this.logger.error(
|
|
695
|
+
`failed to create fresh client: ${factoryErr instanceof Error ? factoryErr.message : String(factoryErr)}; using existing`
|
|
696
|
+
);
|
|
578
697
|
}
|
|
579
|
-
this.logger.info("created fresh client for reconnection");
|
|
580
|
-
} catch (factoryErr) {
|
|
581
|
-
this.logger.error(
|
|
582
|
-
`failed to create fresh client: ${factoryErr instanceof Error ? factoryErr.message : String(factoryErr)}; using existing`
|
|
583
|
-
);
|
|
584
698
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
currentSlot
|
|
699
|
+
if (onReconnect && currentSlot > 0n) {
|
|
700
|
+
for await (const item of this.miniBackfill(
|
|
701
|
+
currentSlot,
|
|
702
|
+
currentFetchBackfill,
|
|
703
|
+
extractSlot,
|
|
704
|
+
keyOf,
|
|
705
|
+
seenItem,
|
|
706
|
+
recordEmission,
|
|
707
|
+
signal
|
|
708
|
+
)) {
|
|
709
|
+
if (shouldStop()) return;
|
|
710
|
+
const itemSlot = extractSlot(item);
|
|
711
|
+
if (itemSlot > currentSlot) {
|
|
712
|
+
currentSlot = itemSlot;
|
|
713
|
+
}
|
|
714
|
+
yield item;
|
|
598
715
|
}
|
|
599
|
-
yield item;
|
|
600
716
|
}
|
|
717
|
+
const resumeSlot = currentSlot > 0n ? currentSlot : 0n;
|
|
718
|
+
livePump = createLivePump(resumeSlot, true, currentSlot);
|
|
601
719
|
}
|
|
602
|
-
const resumeSlot = currentSlot > 0n ? currentSlot : 0n;
|
|
603
|
-
livePump = createLivePump(resumeSlot, true, currentSlot);
|
|
604
720
|
}
|
|
721
|
+
} finally {
|
|
722
|
+
currentDispose();
|
|
723
|
+
await safeClose(livePump);
|
|
605
724
|
}
|
|
606
725
|
}
|
|
607
726
|
/**
|
|
608
727
|
* Perform mini-backfill from lastProcessedSlot to catch up after reconnection.
|
|
609
728
|
* Ensures no data gaps from events that occurred during disconnection.
|
|
610
729
|
*/
|
|
611
|
-
async *miniBackfill(fromSlot, fetchBackfill, extractSlot, keyOf, seenItem, recordEmission) {
|
|
730
|
+
async *miniBackfill(fromSlot, fetchBackfill, extractSlot, keyOf, seenItem, recordEmission, signal) {
|
|
612
731
|
this.logger.info(`mini-backfill starting from slot ${fromSlot}`);
|
|
613
732
|
const MINI_BACKFILL_TIMEOUT = 3e4;
|
|
614
733
|
let lastProgressTime = Date.now();
|
|
@@ -616,6 +735,7 @@ var ReplayStream = class {
|
|
|
616
735
|
let itemsYielded = 0;
|
|
617
736
|
try {
|
|
618
737
|
while (true) {
|
|
738
|
+
if (signal?.aborted) return;
|
|
619
739
|
if (Date.now() - lastProgressTime > MINI_BACKFILL_TIMEOUT) {
|
|
620
740
|
this.logger.warn(`mini-backfill timed out after ${MINI_BACKFILL_TIMEOUT}ms with no progress`);
|
|
621
741
|
break;
|
|
@@ -626,6 +746,7 @@ var ReplayStream = class {
|
|
|
626
746
|
);
|
|
627
747
|
let pageYielded = 0;
|
|
628
748
|
for (const item of sorted) {
|
|
749
|
+
if (signal?.aborted) return;
|
|
629
750
|
const slot = extractSlot(item);
|
|
630
751
|
const key = keyOf(item);
|
|
631
752
|
if (seenItem(slot, key)) {
|
|
@@ -644,6 +765,9 @@ var ReplayStream = class {
|
|
|
644
765
|
}
|
|
645
766
|
this.logger.info(`mini-backfill complete: ${itemsYielded} items yielded`);
|
|
646
767
|
} catch (err) {
|
|
768
|
+
if (signal?.aborted || isAbortError(err)) {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
647
771
|
this.logger.warn(
|
|
648
772
|
`mini-backfill failed: ${err instanceof Error ? err.message : String(err)}; proceeding with live stream`
|
|
649
773
|
);
|
|
@@ -651,14 +775,22 @@ var ReplayStream = class {
|
|
|
651
775
|
}
|
|
652
776
|
};
|
|
653
777
|
async function safeClose(pump) {
|
|
778
|
+
let timeoutId;
|
|
654
779
|
try {
|
|
780
|
+
const timeout = new Promise((resolve) => {
|
|
781
|
+
timeoutId = setTimeout(() => resolve("timeout"), 5e3);
|
|
782
|
+
});
|
|
655
783
|
const result = await Promise.race([
|
|
656
784
|
pump.close().then(() => "closed"),
|
|
657
|
-
|
|
785
|
+
timeout
|
|
658
786
|
]);
|
|
659
787
|
if (result === "timeout") {
|
|
660
788
|
}
|
|
661
789
|
} catch {
|
|
790
|
+
} finally {
|
|
791
|
+
if (timeoutId !== void 0) {
|
|
792
|
+
clearTimeout(timeoutId);
|
|
793
|
+
}
|
|
662
794
|
}
|
|
663
795
|
}
|
|
664
796
|
function combineFilters(base, user) {
|
|
@@ -754,15 +886,38 @@ function createBlockReplay(options) {
|
|
|
754
886
|
subscribeLive,
|
|
755
887
|
extractSlot: (block) => block.header?.slot ?? 0n,
|
|
756
888
|
logger: options.logger,
|
|
757
|
-
resubscribeOnEnd: options.resubscribeOnEnd
|
|
889
|
+
resubscribeOnEnd: options.resubscribeOnEnd,
|
|
890
|
+
signal: options.signal
|
|
758
891
|
});
|
|
759
892
|
}
|
|
893
|
+
|
|
894
|
+
// src/types.ts
|
|
895
|
+
function resolveClient(opts, optionsName) {
|
|
896
|
+
if (opts.clientFactory) {
|
|
897
|
+
return opts.clientFactory();
|
|
898
|
+
}
|
|
899
|
+
if (!opts.client) {
|
|
900
|
+
throw new Error(`${optionsName} requires either client or clientFactory`);
|
|
901
|
+
}
|
|
902
|
+
return opts.client;
|
|
903
|
+
}
|
|
904
|
+
function closeIfCloseable(value) {
|
|
905
|
+
if (value && typeof value.close === "function") {
|
|
906
|
+
try {
|
|
907
|
+
value.close();
|
|
908
|
+
} catch {
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// src/replay/transaction-replay.ts
|
|
760
914
|
var DEFAULT_PAGE_SIZE2 = 256;
|
|
761
915
|
var DEFAULT_SAFETY_MARGIN2 = 64n;
|
|
762
916
|
var PAGE_ORDER_ASC2 = "slot asc";
|
|
763
917
|
function createTransactionReplay(options) {
|
|
764
918
|
const safetyMargin = options.safetyMargin ?? DEFAULT_SAFETY_MARGIN2;
|
|
765
|
-
|
|
919
|
+
let currentClient = resolveClient(options, "TransactionReplayOptions");
|
|
920
|
+
const createFetchBackfill = (client) => async ({
|
|
766
921
|
startSlot,
|
|
767
922
|
cursor
|
|
768
923
|
}) => {
|
|
@@ -772,7 +927,7 @@ function createTransactionReplay(options) {
|
|
|
772
927
|
pageToken: cursor
|
|
773
928
|
});
|
|
774
929
|
const mergedFilter = combineFilters(slotLiteralFilter("transaction.slot", startSlot), options.filter);
|
|
775
|
-
const response = await
|
|
930
|
+
const response = await client.listTransactions(
|
|
776
931
|
protobuf.create(proto.ListTransactionsRequestSchema, {
|
|
777
932
|
filter: mergedFilter,
|
|
778
933
|
page,
|
|
@@ -782,26 +937,38 @@ function createTransactionReplay(options) {
|
|
|
782
937
|
);
|
|
783
938
|
return backfillPage(response.transactions, response.page);
|
|
784
939
|
};
|
|
785
|
-
const
|
|
940
|
+
const createSubscribeLive = (client) => (startSlot) => {
|
|
786
941
|
const mergedFilter = combineFilters(slotLiteralFilter("transaction.slot", startSlot), options.filter);
|
|
787
942
|
const request = protobuf.create(proto.StreamTransactionsRequestSchema, {
|
|
788
943
|
filter: mergedFilter,
|
|
789
944
|
minConsensus: options.minConsensus
|
|
790
945
|
});
|
|
791
946
|
return mapAsyncIterable(
|
|
792
|
-
|
|
947
|
+
client.streamTransactions(request),
|
|
793
948
|
(resp) => resp.transaction
|
|
794
949
|
);
|
|
795
950
|
};
|
|
951
|
+
const onReconnect = options.clientFactory ? () => {
|
|
952
|
+
const newClient = options.clientFactory();
|
|
953
|
+
currentClient = newClient;
|
|
954
|
+
return {
|
|
955
|
+
subscribeLive: createSubscribeLive(currentClient),
|
|
956
|
+
fetchBackfill: createFetchBackfill(currentClient),
|
|
957
|
+
dispose: () => closeIfCloseable(newClient)
|
|
958
|
+
};
|
|
959
|
+
} : void 0;
|
|
796
960
|
return new ReplayStream({
|
|
797
961
|
startSlot: options.startSlot,
|
|
798
962
|
safetyMargin,
|
|
799
|
-
fetchBackfill,
|
|
800
|
-
subscribeLive,
|
|
963
|
+
fetchBackfill: createFetchBackfill(currentClient),
|
|
964
|
+
subscribeLive: createSubscribeLive(currentClient),
|
|
801
965
|
extractSlot: (tx) => tx.slot ?? 0n,
|
|
802
966
|
extractKey: transactionKey,
|
|
803
967
|
logger: options.logger,
|
|
804
|
-
resubscribeOnEnd: options.resubscribeOnEnd
|
|
968
|
+
resubscribeOnEnd: options.resubscribeOnEnd,
|
|
969
|
+
signal: options.signal,
|
|
970
|
+
onReconnect,
|
|
971
|
+
dispose: options.clientFactory ? () => closeIfCloseable(currentClient) : void 0
|
|
805
972
|
});
|
|
806
973
|
}
|
|
807
974
|
function transactionKey(tx) {
|
|
@@ -816,19 +983,6 @@ function bytesToHex(bytes) {
|
|
|
816
983
|
for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
|
|
817
984
|
return hex;
|
|
818
985
|
}
|
|
819
|
-
|
|
820
|
-
// src/types.ts
|
|
821
|
-
function resolveClient(opts, optionsName) {
|
|
822
|
-
if (opts.clientFactory) {
|
|
823
|
-
return opts.clientFactory();
|
|
824
|
-
}
|
|
825
|
-
if (!opts.client) {
|
|
826
|
-
throw new Error(`${optionsName} requires either client or clientFactory`);
|
|
827
|
-
}
|
|
828
|
-
return opts.client;
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
// src/replay/event-replay.ts
|
|
832
986
|
var DEFAULT_PAGE_SIZE3 = 512;
|
|
833
987
|
var DEFAULT_SAFETY_MARGIN3 = 64n;
|
|
834
988
|
var PAGE_ORDER_ASC3 = "slot asc";
|
|
@@ -871,10 +1025,12 @@ function createEventReplay(options) {
|
|
|
871
1025
|
);
|
|
872
1026
|
};
|
|
873
1027
|
const onReconnect = options.clientFactory ? () => {
|
|
874
|
-
|
|
1028
|
+
const newClient = options.clientFactory();
|
|
1029
|
+
currentClient = newClient;
|
|
875
1030
|
return {
|
|
876
1031
|
subscribeLive: createSubscribeLive(currentClient),
|
|
877
|
-
fetchBackfill: createFetchBackfill(currentClient)
|
|
1032
|
+
fetchBackfill: createFetchBackfill(currentClient),
|
|
1033
|
+
dispose: () => closeIfCloseable(newClient)
|
|
878
1034
|
};
|
|
879
1035
|
} : void 0;
|
|
880
1036
|
return new ReplayStream({
|
|
@@ -886,7 +1042,9 @@ function createEventReplay(options) {
|
|
|
886
1042
|
extractKey: eventKey,
|
|
887
1043
|
logger: options.logger,
|
|
888
1044
|
resubscribeOnEnd: options.resubscribeOnEnd,
|
|
889
|
-
|
|
1045
|
+
signal: options.signal,
|
|
1046
|
+
onReconnect,
|
|
1047
|
+
dispose: options.clientFactory ? () => closeIfCloseable(currentClient) : void 0
|
|
890
1048
|
});
|
|
891
1049
|
}
|
|
892
1050
|
function streamResponseToEvent(resp) {
|
|
@@ -1111,6 +1269,15 @@ var PageAssembler = class {
|
|
|
1111
1269
|
};
|
|
1112
1270
|
|
|
1113
1271
|
// src/account-replay.ts
|
|
1272
|
+
async function closeAsyncIterator(iterator) {
|
|
1273
|
+
if (!iterator || typeof iterator.return !== "function") {
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
try {
|
|
1277
|
+
await iterator.return();
|
|
1278
|
+
} catch {
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1114
1281
|
function bytesToHex2(bytes) {
|
|
1115
1282
|
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1116
1283
|
}
|
|
@@ -1185,9 +1352,12 @@ async function* createAccountsByOwnerReplay(options) {
|
|
|
1185
1352
|
cleanupInterval = 1e4,
|
|
1186
1353
|
onBackfillComplete,
|
|
1187
1354
|
clientFactory,
|
|
1188
|
-
logger = NOOP_LOGGER
|
|
1355
|
+
logger = NOOP_LOGGER,
|
|
1356
|
+
signal
|
|
1189
1357
|
} = options;
|
|
1358
|
+
const shouldStop = (err) => signal?.aborted === true || isAbortError(err);
|
|
1190
1359
|
let client = resolveClient(options, "AccountsByOwnerReplayOptions");
|
|
1360
|
+
const ownsClient = Boolean(clientFactory);
|
|
1191
1361
|
const seenFromStream = /* @__PURE__ */ new Set();
|
|
1192
1362
|
const fetchQueue = [];
|
|
1193
1363
|
let highestSlotSeen = minUpdatedSlot ?? 0n;
|
|
@@ -1197,15 +1367,24 @@ async function* createAccountsByOwnerReplay(options) {
|
|
|
1197
1367
|
let streamDone = false;
|
|
1198
1368
|
let streamError = null;
|
|
1199
1369
|
let lastActivityTime = Date.now();
|
|
1370
|
+
let activeStreamIterator = null;
|
|
1371
|
+
let activeStreamProcessor = null;
|
|
1200
1372
|
try {
|
|
1373
|
+
if (shouldStop()) return;
|
|
1201
1374
|
cleanupTimer = setInterval(() => {
|
|
1202
1375
|
assembler.cleanup();
|
|
1203
1376
|
}, cleanupInterval);
|
|
1377
|
+
cleanupTimer.unref?.();
|
|
1204
1378
|
const streamFilter = buildOwnerFilterWithMinSlot(owner, dataSizes, minUpdatedSlot);
|
|
1205
1379
|
const stream = client.streamAccountUpdates({ view, filter: streamFilter });
|
|
1206
|
-
const
|
|
1380
|
+
const streamIterator = stream[Symbol.asyncIterator]();
|
|
1381
|
+
activeStreamIterator = streamIterator;
|
|
1382
|
+
activeStreamProcessor = (async () => {
|
|
1207
1383
|
try {
|
|
1208
|
-
|
|
1384
|
+
while (true) {
|
|
1385
|
+
const next = await streamIterator.next();
|
|
1386
|
+
if (next.done) break;
|
|
1387
|
+
const response = next.value;
|
|
1209
1388
|
lastActivityTime = Date.now();
|
|
1210
1389
|
const event = processResponseMulti(response, assembler);
|
|
1211
1390
|
if (event) {
|
|
@@ -1236,6 +1415,7 @@ async function* createAccountsByOwnerReplay(options) {
|
|
|
1236
1415
|
const backfillFilter = buildListAccountsOwnerFilter(owner, dataSizes, minUpdatedSlot);
|
|
1237
1416
|
let pageToken;
|
|
1238
1417
|
do {
|
|
1418
|
+
if (shouldStop()) return;
|
|
1239
1419
|
const request = {
|
|
1240
1420
|
view: proto.AccountView.META_ONLY,
|
|
1241
1421
|
// Address + metadata only, no data
|
|
@@ -1245,7 +1425,13 @@ async function* createAccountsByOwnerReplay(options) {
|
|
|
1245
1425
|
pageToken
|
|
1246
1426
|
})
|
|
1247
1427
|
};
|
|
1248
|
-
|
|
1428
|
+
let response;
|
|
1429
|
+
try {
|
|
1430
|
+
response = await client.listAccounts(request);
|
|
1431
|
+
} catch (err) {
|
|
1432
|
+
if (shouldStop(err)) return;
|
|
1433
|
+
throw err;
|
|
1434
|
+
}
|
|
1249
1435
|
for (const account of response.accounts) {
|
|
1250
1436
|
if (account.address?.value) {
|
|
1251
1437
|
fetchQueue.push(account.address.value);
|
|
@@ -1255,6 +1441,7 @@ async function* createAccountsByOwnerReplay(options) {
|
|
|
1255
1441
|
yield* yieldStreamBuffer();
|
|
1256
1442
|
} while (pageToken);
|
|
1257
1443
|
for (const address of fetchQueue) {
|
|
1444
|
+
if (shouldStop()) return;
|
|
1258
1445
|
const addressHex = bytesToHex2(address);
|
|
1259
1446
|
if (seenFromStream.has(addressHex)) {
|
|
1260
1447
|
continue;
|
|
@@ -1272,10 +1459,11 @@ async function* createAccountsByOwnerReplay(options) {
|
|
|
1272
1459
|
});
|
|
1273
1460
|
break;
|
|
1274
1461
|
} catch (err) {
|
|
1462
|
+
if (shouldStop(err)) return;
|
|
1275
1463
|
if (attempt === maxRetries - 1) {
|
|
1276
1464
|
logger.error(`[backfill] failed to fetch account ${addressHex} after ${maxRetries} attempts`, { error: err });
|
|
1277
1465
|
} else {
|
|
1278
|
-
await
|
|
1466
|
+
await abortableDelay(100 * (attempt + 1), signal);
|
|
1279
1467
|
}
|
|
1280
1468
|
}
|
|
1281
1469
|
}
|
|
@@ -1294,13 +1482,13 @@ async function* createAccountsByOwnerReplay(options) {
|
|
|
1294
1482
|
}
|
|
1295
1483
|
const retryConfig = DEFAULT_RETRY_CONFIG;
|
|
1296
1484
|
let retryAttempt = 0;
|
|
1297
|
-
let currentStream = stream;
|
|
1298
|
-
let currentStreamProcessor = streamProcessor;
|
|
1299
1485
|
lastActivityTime = Date.now();
|
|
1300
1486
|
const createStreamProcessor = () => {
|
|
1301
1487
|
if (clientFactory) {
|
|
1302
1488
|
try {
|
|
1303
|
-
|
|
1489
|
+
const newClient = clientFactory();
|
|
1490
|
+
closeIfCloseable(client);
|
|
1491
|
+
client = newClient;
|
|
1304
1492
|
logger.info("[account-stream] created fresh client for reconnection");
|
|
1305
1493
|
} catch (err) {
|
|
1306
1494
|
logger.error("[account-stream] failed to create fresh client", { error: err });
|
|
@@ -1308,9 +1496,14 @@ async function* createAccountsByOwnerReplay(options) {
|
|
|
1308
1496
|
}
|
|
1309
1497
|
const newStreamFilter = buildOwnerFilterWithMinSlot(owner, dataSizes, highestSlotSeen > 0n ? highestSlotSeen : minUpdatedSlot);
|
|
1310
1498
|
const newStream = client.streamAccountUpdates({ view, filter: newStreamFilter });
|
|
1499
|
+
const newStreamIterator = newStream[Symbol.asyncIterator]();
|
|
1500
|
+
activeStreamIterator = newStreamIterator;
|
|
1311
1501
|
const newProcessor = (async () => {
|
|
1312
1502
|
try {
|
|
1313
|
-
|
|
1503
|
+
while (true) {
|
|
1504
|
+
const next = await newStreamIterator.next();
|
|
1505
|
+
if (next.done) break;
|
|
1506
|
+
const response = next.value;
|
|
1314
1507
|
retryAttempt = 0;
|
|
1315
1508
|
lastActivityTime = Date.now();
|
|
1316
1509
|
const event = processResponseMulti(response, assembler);
|
|
@@ -1330,9 +1523,10 @@ async function* createAccountsByOwnerReplay(options) {
|
|
|
1330
1523
|
streamDone = true;
|
|
1331
1524
|
}
|
|
1332
1525
|
})();
|
|
1333
|
-
return {
|
|
1526
|
+
return { iterator: newStreamIterator, processor: newProcessor };
|
|
1334
1527
|
};
|
|
1335
1528
|
while (true) {
|
|
1529
|
+
if (shouldStop()) return;
|
|
1336
1530
|
const hadEvents = streamBuffer.length > 0;
|
|
1337
1531
|
yield* yieldStreamBuffer();
|
|
1338
1532
|
if (hadEvents) {
|
|
@@ -1347,36 +1541,56 @@ async function* createAccountsByOwnerReplay(options) {
|
|
|
1347
1541
|
}
|
|
1348
1542
|
if (streamDone) {
|
|
1349
1543
|
if (streamError) {
|
|
1544
|
+
if (shouldStop(streamError)) return;
|
|
1350
1545
|
const backoffMs = calculateBackoff(retryAttempt, retryConfig);
|
|
1351
1546
|
logger.warn(
|
|
1352
1547
|
`[account-stream] disconnected (${streamError.message}); reconnecting in ${backoffMs}ms (attempt ${retryAttempt + 1})`
|
|
1353
1548
|
);
|
|
1354
|
-
await
|
|
1549
|
+
await abortableDelay(backoffMs, signal);
|
|
1550
|
+
if (shouldStop()) return;
|
|
1355
1551
|
retryAttempt++;
|
|
1356
1552
|
streamDone = false;
|
|
1357
1553
|
streamError = null;
|
|
1358
1554
|
streamBuffer.length = 0;
|
|
1359
1555
|
lastActivityTime = Date.now();
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1556
|
+
await closeAsyncIterator(activeStreamIterator);
|
|
1557
|
+
if (activeStreamProcessor) {
|
|
1558
|
+
await Promise.allSettled([activeStreamProcessor]);
|
|
1559
|
+
}
|
|
1560
|
+
const { iterator: newIterator, processor: newProcessor } = createStreamProcessor();
|
|
1561
|
+
activeStreamIterator = newIterator;
|
|
1562
|
+
activeStreamProcessor = newProcessor;
|
|
1363
1563
|
continue;
|
|
1364
1564
|
} else {
|
|
1565
|
+
if (shouldStop()) return;
|
|
1365
1566
|
logger.warn("[account-stream] stream ended unexpectedly; reconnecting...");
|
|
1366
1567
|
streamDone = false;
|
|
1367
1568
|
lastActivityTime = Date.now();
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1569
|
+
await closeAsyncIterator(activeStreamIterator);
|
|
1570
|
+
if (activeStreamProcessor) {
|
|
1571
|
+
await Promise.allSettled([activeStreamProcessor]);
|
|
1572
|
+
}
|
|
1573
|
+
const { iterator: newIterator, processor: newProcessor } = createStreamProcessor();
|
|
1574
|
+
activeStreamIterator = newIterator;
|
|
1575
|
+
activeStreamProcessor = newProcessor;
|
|
1371
1576
|
continue;
|
|
1372
1577
|
}
|
|
1373
1578
|
}
|
|
1374
|
-
await
|
|
1579
|
+
await abortableDelay(10, signal);
|
|
1375
1580
|
}
|
|
1376
1581
|
} finally {
|
|
1377
1582
|
if (cleanupTimer) {
|
|
1378
1583
|
clearInterval(cleanupTimer);
|
|
1379
1584
|
}
|
|
1585
|
+
const closeIteratorPromise = closeAsyncIterator(activeStreamIterator);
|
|
1586
|
+
if (ownsClient) {
|
|
1587
|
+
closeIfCloseable(client);
|
|
1588
|
+
}
|
|
1589
|
+
if (activeStreamProcessor) {
|
|
1590
|
+
await Promise.allSettled([closeIteratorPromise, activeStreamProcessor]);
|
|
1591
|
+
} else {
|
|
1592
|
+
await closeIteratorPromise;
|
|
1593
|
+
}
|
|
1380
1594
|
assembler.clear();
|
|
1381
1595
|
}
|
|
1382
1596
|
}
|
|
@@ -1412,6 +1626,7 @@ async function* createAccountReplay(options) {
|
|
|
1412
1626
|
cleanupTimer = setInterval(() => {
|
|
1413
1627
|
assembler.cleanup();
|
|
1414
1628
|
}, cleanupInterval);
|
|
1629
|
+
cleanupTimer.unref?.();
|
|
1415
1630
|
const filterParams = {
|
|
1416
1631
|
address: protobuf.create(proto.FilterParamValueSchema, { kind: { case: "bytesValue", value: new Uint8Array(address) } })
|
|
1417
1632
|
};
|
|
@@ -1618,6 +1833,7 @@ var ConsoleSink = class {
|
|
|
1618
1833
|
constructor(prefix = "ReplaySink") {
|
|
1619
1834
|
this.prefix = prefix;
|
|
1620
1835
|
}
|
|
1836
|
+
prefix;
|
|
1621
1837
|
open(meta) {
|
|
1622
1838
|
const suffix = meta?.stream ? ` (${meta.stream})` : "";
|
|
1623
1839
|
console.info(`${this.prefix}${suffix} opened`, meta?.label ?? "");
|