@thru/replay 0.2.20 → 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 +448 -181
- 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 +450 -183
- 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 = {
|
|
@@ -359,6 +447,27 @@ function compareBigint(a, b) {
|
|
|
359
447
|
if (a === b) return 0;
|
|
360
448
|
return a < b ? -1 : 1;
|
|
361
449
|
}
|
|
450
|
+
function isNonDecreasing(items, extractSlot) {
|
|
451
|
+
for (let idx = 1; idx < items.length; idx += 1) {
|
|
452
|
+
if (extractSlot(items[idx]) < extractSlot(items[idx - 1])) return false;
|
|
453
|
+
}
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
function assertBackfillPageOrder(previousPage, currentPage, extractSlot) {
|
|
457
|
+
if (!isNonDecreasing(currentPage, extractSlot)) {
|
|
458
|
+
throw new Error(
|
|
459
|
+
"backfill source returned a page that is not ordered by ascending slot"
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
if (!previousPage?.length || !currentPage.length) return;
|
|
463
|
+
const previousMaxSlot = extractSlot(previousPage[previousPage.length - 1]);
|
|
464
|
+
const currentMinSlot = extractSlot(currentPage[0]);
|
|
465
|
+
if (currentMinSlot < previousMaxSlot) {
|
|
466
|
+
throw new Error(
|
|
467
|
+
`backfill source returned pages out of ascending slot order: page minimum slot ${currentMinSlot} is before previous page maximum slot ${previousMaxSlot}`
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
362
471
|
var ReplayStream = class {
|
|
363
472
|
config;
|
|
364
473
|
logger;
|
|
@@ -382,12 +491,17 @@ var ReplayStream = class {
|
|
|
382
491
|
extractKey,
|
|
383
492
|
safetyMargin,
|
|
384
493
|
resubscribeOnEnd,
|
|
385
|
-
onReconnect
|
|
494
|
+
onReconnect,
|
|
495
|
+
signal,
|
|
496
|
+
dispose
|
|
386
497
|
} = this.config;
|
|
387
498
|
const shouldResubscribeOnEnd = resubscribeOnEnd ?? true;
|
|
388
499
|
const keyOf = extractKey ?? ((item) => extractSlot(item).toString());
|
|
500
|
+
const shouldStop = (err) => signal?.aborted === true || isAbortError(err);
|
|
389
501
|
let currentSubscribeLive = subscribeLive;
|
|
390
502
|
let currentFetchBackfill = fetchBackfill;
|
|
503
|
+
let currentDispose = dispose ?? (() => {
|
|
504
|
+
});
|
|
391
505
|
const createLivePump = (slot, startStreaming = false, emitFloor) => new LivePump({
|
|
392
506
|
source: currentSubscribeLive(slot),
|
|
393
507
|
slotOf: extractSlot,
|
|
@@ -419,91 +533,103 @@ var ReplayStream = class {
|
|
|
419
533
|
this.logger.info(
|
|
420
534
|
`replay entering BACKFILLING state (startSlot=${startSlot}, safetyMargin=${safetyMargin})`
|
|
421
535
|
);
|
|
422
|
-
let
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
const page = await currentFetchBackfill({ startSlot, cursor });
|
|
426
|
-
if (!page.items.length && !page.cursor && !page.done) {
|
|
427
|
-
emptyPageRetries++;
|
|
428
|
-
if (emptyPageRetries > MAX_EMPTY_PAGE_RETRIES) {
|
|
429
|
-
this.logger.error(
|
|
430
|
-
`backfill returned ${MAX_EMPTY_PAGE_RETRIES} consecutive empty pages; treating as done`
|
|
431
|
-
);
|
|
432
|
-
break;
|
|
433
|
-
}
|
|
434
|
-
const backoffMs = calculateBackoff(emptyPageRetries - 1, DEFAULT_RETRY_CONFIG);
|
|
435
|
-
this.logger.warn(
|
|
436
|
-
`empty backfill page without cursor; retrying in ${backoffMs}ms (${emptyPageRetries}/${MAX_EMPTY_PAGE_RETRIES})`
|
|
437
|
-
);
|
|
438
|
-
await delay(backoffMs);
|
|
439
|
-
continue;
|
|
440
|
-
}
|
|
441
|
-
emptyPageRetries = 0;
|
|
442
|
-
const sorted = [...page.items].sort(
|
|
443
|
-
(a, b) => compareBigint(extractSlot(a), extractSlot(b))
|
|
444
|
-
);
|
|
445
|
-
for (const item of sorted) {
|
|
536
|
+
let pendingOrderedPage = null;
|
|
537
|
+
const emitBackfillItems = async function* (self, items) {
|
|
538
|
+
for (const item of items) {
|
|
446
539
|
const slot = extractSlot(item);
|
|
447
540
|
const key = keyOf(item);
|
|
448
541
|
if (slot < startSlot) continue;
|
|
449
542
|
if (seenItem(slot, key)) {
|
|
450
|
-
|
|
543
|
+
self.metrics.discardedDuplicates += 1;
|
|
451
544
|
continue;
|
|
452
545
|
}
|
|
453
546
|
currentSlot = slot;
|
|
454
547
|
recordEmission(slot, key);
|
|
455
|
-
|
|
548
|
+
self.metrics.emittedBackfill += 1;
|
|
549
|
+
yield item;
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
const flushPendingBackfill = async function* (self) {
|
|
553
|
+
if (!pendingOrderedPage) return;
|
|
554
|
+
for await (const item of emitBackfillItems(self, pendingOrderedPage)) {
|
|
456
555
|
yield item;
|
|
457
556
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
if (
|
|
465
|
-
|
|
466
|
-
|
|
557
|
+
pendingOrderedPage = null;
|
|
558
|
+
};
|
|
559
|
+
let emptyPageRetries = 0;
|
|
560
|
+
const MAX_EMPTY_PAGE_RETRIES = 10;
|
|
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})`
|
|
467
585
|
);
|
|
468
|
-
|
|
586
|
+
await abortableDelay(backoffMs, signal);
|
|
587
|
+
continue;
|
|
469
588
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
recordEmission(slot, key);
|
|
486
|
-
this.metrics.emittedLive += 1;
|
|
487
|
-
yield item;
|
|
488
|
-
livePump.updateEmitFloor(currentSlot);
|
|
489
|
-
}
|
|
490
|
-
if (!drained.length) livePump.updateEmitFloor(currentSlot);
|
|
491
|
-
this.logger.info("replay entering STREAMING state");
|
|
492
|
-
const retryConfig = DEFAULT_RETRY_CONFIG;
|
|
493
|
-
let retryAttempt = 0;
|
|
494
|
-
while (true) {
|
|
495
|
-
try {
|
|
496
|
-
const next = await withTimeout(
|
|
497
|
-
livePump.next(),
|
|
498
|
-
retryConfig.connectionTimeoutMs
|
|
499
|
-
);
|
|
500
|
-
retryAttempt = 0;
|
|
501
|
-
if (next.done) {
|
|
502
|
-
if (!shouldResubscribeOnEnd) break;
|
|
503
|
-
throw new Error("stream ended");
|
|
589
|
+
emptyPageRetries = 0;
|
|
590
|
+
assertBackfillPageOrder(pendingOrderedPage, page.items, extractSlot);
|
|
591
|
+
if (pendingOrderedPage !== null) {
|
|
592
|
+
for await (const item of flushPendingBackfill(this)) {
|
|
593
|
+
if (shouldStop()) return;
|
|
594
|
+
yield item;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
pendingOrderedPage = [...page.items];
|
|
598
|
+
const reachedEnd = page.done || page.cursor === void 0;
|
|
599
|
+
if (reachedEnd) {
|
|
600
|
+
for await (const item of flushPendingBackfill(this)) {
|
|
601
|
+
if (shouldStop()) return;
|
|
602
|
+
yield item;
|
|
603
|
+
}
|
|
504
604
|
}
|
|
505
|
-
const
|
|
506
|
-
|
|
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
|
+
}
|
|
621
|
+
}
|
|
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);
|
|
507
633
|
if (seenItem(slot, key)) {
|
|
508
634
|
this.metrics.discardedDuplicates += 1;
|
|
509
635
|
continue;
|
|
@@ -511,57 +637,97 @@ var ReplayStream = class {
|
|
|
511
637
|
currentSlot = slot;
|
|
512
638
|
recordEmission(slot, key);
|
|
513
639
|
this.metrics.emittedLive += 1;
|
|
514
|
-
yield
|
|
640
|
+
yield item;
|
|
515
641
|
livePump.updateEmitFloor(currentSlot);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
+
);
|
|
531
697
|
}
|
|
532
|
-
this.logger.info("created fresh client for reconnection");
|
|
533
|
-
} catch (factoryErr) {
|
|
534
|
-
this.logger.error(
|
|
535
|
-
`failed to create fresh client: ${factoryErr instanceof Error ? factoryErr.message : String(factoryErr)}; using existing`
|
|
536
|
-
);
|
|
537
698
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
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;
|
|
551
715
|
}
|
|
552
|
-
yield item;
|
|
553
716
|
}
|
|
717
|
+
const resumeSlot = currentSlot > 0n ? currentSlot : 0n;
|
|
718
|
+
livePump = createLivePump(resumeSlot, true, currentSlot);
|
|
554
719
|
}
|
|
555
|
-
const resumeSlot = currentSlot > 0n ? currentSlot : 0n;
|
|
556
|
-
livePump = createLivePump(resumeSlot, true, currentSlot);
|
|
557
720
|
}
|
|
721
|
+
} finally {
|
|
722
|
+
currentDispose();
|
|
723
|
+
await safeClose(livePump);
|
|
558
724
|
}
|
|
559
725
|
}
|
|
560
726
|
/**
|
|
561
727
|
* Perform mini-backfill from lastProcessedSlot to catch up after reconnection.
|
|
562
728
|
* Ensures no data gaps from events that occurred during disconnection.
|
|
563
729
|
*/
|
|
564
|
-
async *miniBackfill(fromSlot, fetchBackfill, extractSlot, keyOf, seenItem, recordEmission) {
|
|
730
|
+
async *miniBackfill(fromSlot, fetchBackfill, extractSlot, keyOf, seenItem, recordEmission, signal) {
|
|
565
731
|
this.logger.info(`mini-backfill starting from slot ${fromSlot}`);
|
|
566
732
|
const MINI_BACKFILL_TIMEOUT = 3e4;
|
|
567
733
|
let lastProgressTime = Date.now();
|
|
@@ -569,6 +735,7 @@ var ReplayStream = class {
|
|
|
569
735
|
let itemsYielded = 0;
|
|
570
736
|
try {
|
|
571
737
|
while (true) {
|
|
738
|
+
if (signal?.aborted) return;
|
|
572
739
|
if (Date.now() - lastProgressTime > MINI_BACKFILL_TIMEOUT) {
|
|
573
740
|
this.logger.warn(`mini-backfill timed out after ${MINI_BACKFILL_TIMEOUT}ms with no progress`);
|
|
574
741
|
break;
|
|
@@ -579,6 +746,7 @@ var ReplayStream = class {
|
|
|
579
746
|
);
|
|
580
747
|
let pageYielded = 0;
|
|
581
748
|
for (const item of sorted) {
|
|
749
|
+
if (signal?.aborted) return;
|
|
582
750
|
const slot = extractSlot(item);
|
|
583
751
|
const key = keyOf(item);
|
|
584
752
|
if (seenItem(slot, key)) {
|
|
@@ -597,6 +765,9 @@ var ReplayStream = class {
|
|
|
597
765
|
}
|
|
598
766
|
this.logger.info(`mini-backfill complete: ${itemsYielded} items yielded`);
|
|
599
767
|
} catch (err) {
|
|
768
|
+
if (signal?.aborted || isAbortError(err)) {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
600
771
|
this.logger.warn(
|
|
601
772
|
`mini-backfill failed: ${err instanceof Error ? err.message : String(err)}; proceeding with live stream`
|
|
602
773
|
);
|
|
@@ -604,14 +775,22 @@ var ReplayStream = class {
|
|
|
604
775
|
}
|
|
605
776
|
};
|
|
606
777
|
async function safeClose(pump) {
|
|
778
|
+
let timeoutId;
|
|
607
779
|
try {
|
|
780
|
+
const timeout = new Promise((resolve) => {
|
|
781
|
+
timeoutId = setTimeout(() => resolve("timeout"), 5e3);
|
|
782
|
+
});
|
|
608
783
|
const result = await Promise.race([
|
|
609
784
|
pump.close().then(() => "closed"),
|
|
610
|
-
|
|
785
|
+
timeout
|
|
611
786
|
]);
|
|
612
787
|
if (result === "timeout") {
|
|
613
788
|
}
|
|
614
789
|
} catch {
|
|
790
|
+
} finally {
|
|
791
|
+
if (timeoutId !== void 0) {
|
|
792
|
+
clearTimeout(timeoutId);
|
|
793
|
+
}
|
|
615
794
|
}
|
|
616
795
|
}
|
|
617
796
|
function combineFilters(base, user) {
|
|
@@ -707,15 +886,38 @@ function createBlockReplay(options) {
|
|
|
707
886
|
subscribeLive,
|
|
708
887
|
extractSlot: (block) => block.header?.slot ?? 0n,
|
|
709
888
|
logger: options.logger,
|
|
710
|
-
resubscribeOnEnd: options.resubscribeOnEnd
|
|
889
|
+
resubscribeOnEnd: options.resubscribeOnEnd,
|
|
890
|
+
signal: options.signal
|
|
711
891
|
});
|
|
712
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
|
|
713
914
|
var DEFAULT_PAGE_SIZE2 = 256;
|
|
714
915
|
var DEFAULT_SAFETY_MARGIN2 = 64n;
|
|
715
916
|
var PAGE_ORDER_ASC2 = "slot asc";
|
|
716
917
|
function createTransactionReplay(options) {
|
|
717
918
|
const safetyMargin = options.safetyMargin ?? DEFAULT_SAFETY_MARGIN2;
|
|
718
|
-
|
|
919
|
+
let currentClient = resolveClient(options, "TransactionReplayOptions");
|
|
920
|
+
const createFetchBackfill = (client) => async ({
|
|
719
921
|
startSlot,
|
|
720
922
|
cursor
|
|
721
923
|
}) => {
|
|
@@ -725,7 +927,7 @@ function createTransactionReplay(options) {
|
|
|
725
927
|
pageToken: cursor
|
|
726
928
|
});
|
|
727
929
|
const mergedFilter = combineFilters(slotLiteralFilter("transaction.slot", startSlot), options.filter);
|
|
728
|
-
const response = await
|
|
930
|
+
const response = await client.listTransactions(
|
|
729
931
|
protobuf.create(proto.ListTransactionsRequestSchema, {
|
|
730
932
|
filter: mergedFilter,
|
|
731
933
|
page,
|
|
@@ -735,26 +937,38 @@ function createTransactionReplay(options) {
|
|
|
735
937
|
);
|
|
736
938
|
return backfillPage(response.transactions, response.page);
|
|
737
939
|
};
|
|
738
|
-
const
|
|
940
|
+
const createSubscribeLive = (client) => (startSlot) => {
|
|
739
941
|
const mergedFilter = combineFilters(slotLiteralFilter("transaction.slot", startSlot), options.filter);
|
|
740
942
|
const request = protobuf.create(proto.StreamTransactionsRequestSchema, {
|
|
741
943
|
filter: mergedFilter,
|
|
742
944
|
minConsensus: options.minConsensus
|
|
743
945
|
});
|
|
744
946
|
return mapAsyncIterable(
|
|
745
|
-
|
|
947
|
+
client.streamTransactions(request),
|
|
746
948
|
(resp) => resp.transaction
|
|
747
949
|
);
|
|
748
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;
|
|
749
960
|
return new ReplayStream({
|
|
750
961
|
startSlot: options.startSlot,
|
|
751
962
|
safetyMargin,
|
|
752
|
-
fetchBackfill,
|
|
753
|
-
subscribeLive,
|
|
963
|
+
fetchBackfill: createFetchBackfill(currentClient),
|
|
964
|
+
subscribeLive: createSubscribeLive(currentClient),
|
|
754
965
|
extractSlot: (tx) => tx.slot ?? 0n,
|
|
755
966
|
extractKey: transactionKey,
|
|
756
967
|
logger: options.logger,
|
|
757
|
-
resubscribeOnEnd: options.resubscribeOnEnd
|
|
968
|
+
resubscribeOnEnd: options.resubscribeOnEnd,
|
|
969
|
+
signal: options.signal,
|
|
970
|
+
onReconnect,
|
|
971
|
+
dispose: options.clientFactory ? () => closeIfCloseable(currentClient) : void 0
|
|
758
972
|
});
|
|
759
973
|
}
|
|
760
974
|
function transactionKey(tx) {
|
|
@@ -769,19 +983,6 @@ function bytesToHex(bytes) {
|
|
|
769
983
|
for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
|
|
770
984
|
return hex;
|
|
771
985
|
}
|
|
772
|
-
|
|
773
|
-
// src/types.ts
|
|
774
|
-
function resolveClient(opts, optionsName) {
|
|
775
|
-
if (opts.clientFactory) {
|
|
776
|
-
return opts.clientFactory();
|
|
777
|
-
}
|
|
778
|
-
if (!opts.client) {
|
|
779
|
-
throw new Error(`${optionsName} requires either client or clientFactory`);
|
|
780
|
-
}
|
|
781
|
-
return opts.client;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
// src/replay/event-replay.ts
|
|
785
986
|
var DEFAULT_PAGE_SIZE3 = 512;
|
|
786
987
|
var DEFAULT_SAFETY_MARGIN3 = 64n;
|
|
787
988
|
var PAGE_ORDER_ASC3 = "slot asc";
|
|
@@ -824,10 +1025,12 @@ function createEventReplay(options) {
|
|
|
824
1025
|
);
|
|
825
1026
|
};
|
|
826
1027
|
const onReconnect = options.clientFactory ? () => {
|
|
827
|
-
|
|
1028
|
+
const newClient = options.clientFactory();
|
|
1029
|
+
currentClient = newClient;
|
|
828
1030
|
return {
|
|
829
1031
|
subscribeLive: createSubscribeLive(currentClient),
|
|
830
|
-
fetchBackfill: createFetchBackfill(currentClient)
|
|
1032
|
+
fetchBackfill: createFetchBackfill(currentClient),
|
|
1033
|
+
dispose: () => closeIfCloseable(newClient)
|
|
831
1034
|
};
|
|
832
1035
|
} : void 0;
|
|
833
1036
|
return new ReplayStream({
|
|
@@ -839,7 +1042,9 @@ function createEventReplay(options) {
|
|
|
839
1042
|
extractKey: eventKey,
|
|
840
1043
|
logger: options.logger,
|
|
841
1044
|
resubscribeOnEnd: options.resubscribeOnEnd,
|
|
842
|
-
|
|
1045
|
+
signal: options.signal,
|
|
1046
|
+
onReconnect,
|
|
1047
|
+
dispose: options.clientFactory ? () => closeIfCloseable(currentClient) : void 0
|
|
843
1048
|
});
|
|
844
1049
|
}
|
|
845
1050
|
function streamResponseToEvent(resp) {
|
|
@@ -864,7 +1069,7 @@ function eventBackfillFilter(startSlot, resumeAfter) {
|
|
|
864
1069
|
return slotLiteralFilter("event.slot", startSlot);
|
|
865
1070
|
}
|
|
866
1071
|
return protobuf.create(proto.FilterSchema, {
|
|
867
|
-
expression: `event.slot > uint(${boundary.slot.toString()}) || (event.slot == uint(${boundary.slot.toString()}) && (event.block_offset > uint(${boundary.blockOffset.toString()}) || (event.block_offset == uint(${boundary.blockOffset.toString()}) && event.call_idx
|
|
1072
|
+
expression: `event.slot > uint(${boundary.slot.toString()}) || (event.slot == uint(${boundary.slot.toString()}) && (event.block_offset > uint(${boundary.blockOffset.toString()}) || (event.block_offset == uint(${boundary.blockOffset.toString()}) && event.call_idx >= uint(${boundary.callIdx.toString()}))))`
|
|
868
1073
|
});
|
|
869
1074
|
}
|
|
870
1075
|
function parseEventId(resumeAfter) {
|
|
@@ -873,23 +1078,27 @@ function parseEventId(resumeAfter) {
|
|
|
873
1078
|
}
|
|
874
1079
|
function parseCanonicalEventId(eventId, expectedSlot) {
|
|
875
1080
|
if (!eventId) return null;
|
|
876
|
-
const
|
|
877
|
-
if (
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
1081
|
+
const parts = eventId.split(":");
|
|
1082
|
+
if (parts.length === 5) {
|
|
1083
|
+
const [, slotPart, blockOffsetPart, callIdxPart, eventIdxPart] = parts;
|
|
1084
|
+
const slot = BigInt(slotPart);
|
|
1085
|
+
if (slot !== expectedSlot) return null;
|
|
1086
|
+
return {
|
|
1087
|
+
slot,
|
|
1088
|
+
blockOffset: BigInt(blockOffsetPart),
|
|
1089
|
+
callIdx: BigInt(callIdxPart),
|
|
1090
|
+
eventIdx: BigInt(eventIdxPart)
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
return null;
|
|
886
1094
|
}
|
|
887
1095
|
function isAfterBoundary(event, boundary) {
|
|
888
1096
|
if (event.slot !== boundary.slot) return event.slot > boundary.slot;
|
|
889
1097
|
if (event.blockOffset !== boundary.blockOffset) {
|
|
890
1098
|
return event.blockOffset > boundary.blockOffset;
|
|
891
1099
|
}
|
|
892
|
-
return event.callIdx > boundary.callIdx;
|
|
1100
|
+
if (event.callIdx !== boundary.callIdx) return event.callIdx > boundary.callIdx;
|
|
1101
|
+
return event.eventIdx > boundary.eventIdx;
|
|
893
1102
|
}
|
|
894
1103
|
function shouldEmitLiveEvent(event, startSlot, resumeAfter) {
|
|
895
1104
|
const boundary = parseEventId(resumeAfter);
|
|
@@ -1060,6 +1269,15 @@ var PageAssembler = class {
|
|
|
1060
1269
|
};
|
|
1061
1270
|
|
|
1062
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
|
+
}
|
|
1063
1281
|
function bytesToHex2(bytes) {
|
|
1064
1282
|
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1065
1283
|
}
|
|
@@ -1134,9 +1352,12 @@ async function* createAccountsByOwnerReplay(options) {
|
|
|
1134
1352
|
cleanupInterval = 1e4,
|
|
1135
1353
|
onBackfillComplete,
|
|
1136
1354
|
clientFactory,
|
|
1137
|
-
logger = NOOP_LOGGER
|
|
1355
|
+
logger = NOOP_LOGGER,
|
|
1356
|
+
signal
|
|
1138
1357
|
} = options;
|
|
1358
|
+
const shouldStop = (err) => signal?.aborted === true || isAbortError(err);
|
|
1139
1359
|
let client = resolveClient(options, "AccountsByOwnerReplayOptions");
|
|
1360
|
+
const ownsClient = Boolean(clientFactory);
|
|
1140
1361
|
const seenFromStream = /* @__PURE__ */ new Set();
|
|
1141
1362
|
const fetchQueue = [];
|
|
1142
1363
|
let highestSlotSeen = minUpdatedSlot ?? 0n;
|
|
@@ -1146,15 +1367,24 @@ async function* createAccountsByOwnerReplay(options) {
|
|
|
1146
1367
|
let streamDone = false;
|
|
1147
1368
|
let streamError = null;
|
|
1148
1369
|
let lastActivityTime = Date.now();
|
|
1370
|
+
let activeStreamIterator = null;
|
|
1371
|
+
let activeStreamProcessor = null;
|
|
1149
1372
|
try {
|
|
1373
|
+
if (shouldStop()) return;
|
|
1150
1374
|
cleanupTimer = setInterval(() => {
|
|
1151
1375
|
assembler.cleanup();
|
|
1152
1376
|
}, cleanupInterval);
|
|
1377
|
+
cleanupTimer.unref?.();
|
|
1153
1378
|
const streamFilter = buildOwnerFilterWithMinSlot(owner, dataSizes, minUpdatedSlot);
|
|
1154
1379
|
const stream = client.streamAccountUpdates({ view, filter: streamFilter });
|
|
1155
|
-
const
|
|
1380
|
+
const streamIterator = stream[Symbol.asyncIterator]();
|
|
1381
|
+
activeStreamIterator = streamIterator;
|
|
1382
|
+
activeStreamProcessor = (async () => {
|
|
1156
1383
|
try {
|
|
1157
|
-
|
|
1384
|
+
while (true) {
|
|
1385
|
+
const next = await streamIterator.next();
|
|
1386
|
+
if (next.done) break;
|
|
1387
|
+
const response = next.value;
|
|
1158
1388
|
lastActivityTime = Date.now();
|
|
1159
1389
|
const event = processResponseMulti(response, assembler);
|
|
1160
1390
|
if (event) {
|
|
@@ -1185,6 +1415,7 @@ async function* createAccountsByOwnerReplay(options) {
|
|
|
1185
1415
|
const backfillFilter = buildListAccountsOwnerFilter(owner, dataSizes, minUpdatedSlot);
|
|
1186
1416
|
let pageToken;
|
|
1187
1417
|
do {
|
|
1418
|
+
if (shouldStop()) return;
|
|
1188
1419
|
const request = {
|
|
1189
1420
|
view: proto.AccountView.META_ONLY,
|
|
1190
1421
|
// Address + metadata only, no data
|
|
@@ -1194,7 +1425,13 @@ async function* createAccountsByOwnerReplay(options) {
|
|
|
1194
1425
|
pageToken
|
|
1195
1426
|
})
|
|
1196
1427
|
};
|
|
1197
|
-
|
|
1428
|
+
let response;
|
|
1429
|
+
try {
|
|
1430
|
+
response = await client.listAccounts(request);
|
|
1431
|
+
} catch (err) {
|
|
1432
|
+
if (shouldStop(err)) return;
|
|
1433
|
+
throw err;
|
|
1434
|
+
}
|
|
1198
1435
|
for (const account of response.accounts) {
|
|
1199
1436
|
if (account.address?.value) {
|
|
1200
1437
|
fetchQueue.push(account.address.value);
|
|
@@ -1204,6 +1441,7 @@ async function* createAccountsByOwnerReplay(options) {
|
|
|
1204
1441
|
yield* yieldStreamBuffer();
|
|
1205
1442
|
} while (pageToken);
|
|
1206
1443
|
for (const address of fetchQueue) {
|
|
1444
|
+
if (shouldStop()) return;
|
|
1207
1445
|
const addressHex = bytesToHex2(address);
|
|
1208
1446
|
if (seenFromStream.has(addressHex)) {
|
|
1209
1447
|
continue;
|
|
@@ -1221,10 +1459,11 @@ async function* createAccountsByOwnerReplay(options) {
|
|
|
1221
1459
|
});
|
|
1222
1460
|
break;
|
|
1223
1461
|
} catch (err) {
|
|
1462
|
+
if (shouldStop(err)) return;
|
|
1224
1463
|
if (attempt === maxRetries - 1) {
|
|
1225
1464
|
logger.error(`[backfill] failed to fetch account ${addressHex} after ${maxRetries} attempts`, { error: err });
|
|
1226
1465
|
} else {
|
|
1227
|
-
await
|
|
1466
|
+
await abortableDelay(100 * (attempt + 1), signal);
|
|
1228
1467
|
}
|
|
1229
1468
|
}
|
|
1230
1469
|
}
|
|
@@ -1243,13 +1482,13 @@ async function* createAccountsByOwnerReplay(options) {
|
|
|
1243
1482
|
}
|
|
1244
1483
|
const retryConfig = DEFAULT_RETRY_CONFIG;
|
|
1245
1484
|
let retryAttempt = 0;
|
|
1246
|
-
let currentStream = stream;
|
|
1247
|
-
let currentStreamProcessor = streamProcessor;
|
|
1248
1485
|
lastActivityTime = Date.now();
|
|
1249
1486
|
const createStreamProcessor = () => {
|
|
1250
1487
|
if (clientFactory) {
|
|
1251
1488
|
try {
|
|
1252
|
-
|
|
1489
|
+
const newClient = clientFactory();
|
|
1490
|
+
closeIfCloseable(client);
|
|
1491
|
+
client = newClient;
|
|
1253
1492
|
logger.info("[account-stream] created fresh client for reconnection");
|
|
1254
1493
|
} catch (err) {
|
|
1255
1494
|
logger.error("[account-stream] failed to create fresh client", { error: err });
|
|
@@ -1257,9 +1496,14 @@ async function* createAccountsByOwnerReplay(options) {
|
|
|
1257
1496
|
}
|
|
1258
1497
|
const newStreamFilter = buildOwnerFilterWithMinSlot(owner, dataSizes, highestSlotSeen > 0n ? highestSlotSeen : minUpdatedSlot);
|
|
1259
1498
|
const newStream = client.streamAccountUpdates({ view, filter: newStreamFilter });
|
|
1499
|
+
const newStreamIterator = newStream[Symbol.asyncIterator]();
|
|
1500
|
+
activeStreamIterator = newStreamIterator;
|
|
1260
1501
|
const newProcessor = (async () => {
|
|
1261
1502
|
try {
|
|
1262
|
-
|
|
1503
|
+
while (true) {
|
|
1504
|
+
const next = await newStreamIterator.next();
|
|
1505
|
+
if (next.done) break;
|
|
1506
|
+
const response = next.value;
|
|
1263
1507
|
retryAttempt = 0;
|
|
1264
1508
|
lastActivityTime = Date.now();
|
|
1265
1509
|
const event = processResponseMulti(response, assembler);
|
|
@@ -1279,9 +1523,10 @@ async function* createAccountsByOwnerReplay(options) {
|
|
|
1279
1523
|
streamDone = true;
|
|
1280
1524
|
}
|
|
1281
1525
|
})();
|
|
1282
|
-
return {
|
|
1526
|
+
return { iterator: newStreamIterator, processor: newProcessor };
|
|
1283
1527
|
};
|
|
1284
1528
|
while (true) {
|
|
1529
|
+
if (shouldStop()) return;
|
|
1285
1530
|
const hadEvents = streamBuffer.length > 0;
|
|
1286
1531
|
yield* yieldStreamBuffer();
|
|
1287
1532
|
if (hadEvents) {
|
|
@@ -1296,36 +1541,56 @@ async function* createAccountsByOwnerReplay(options) {
|
|
|
1296
1541
|
}
|
|
1297
1542
|
if (streamDone) {
|
|
1298
1543
|
if (streamError) {
|
|
1544
|
+
if (shouldStop(streamError)) return;
|
|
1299
1545
|
const backoffMs = calculateBackoff(retryAttempt, retryConfig);
|
|
1300
1546
|
logger.warn(
|
|
1301
1547
|
`[account-stream] disconnected (${streamError.message}); reconnecting in ${backoffMs}ms (attempt ${retryAttempt + 1})`
|
|
1302
1548
|
);
|
|
1303
|
-
await
|
|
1549
|
+
await abortableDelay(backoffMs, signal);
|
|
1550
|
+
if (shouldStop()) return;
|
|
1304
1551
|
retryAttempt++;
|
|
1305
1552
|
streamDone = false;
|
|
1306
1553
|
streamError = null;
|
|
1307
1554
|
streamBuffer.length = 0;
|
|
1308
1555
|
lastActivityTime = Date.now();
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
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;
|
|
1312
1563
|
continue;
|
|
1313
1564
|
} else {
|
|
1565
|
+
if (shouldStop()) return;
|
|
1314
1566
|
logger.warn("[account-stream] stream ended unexpectedly; reconnecting...");
|
|
1315
1567
|
streamDone = false;
|
|
1316
1568
|
lastActivityTime = Date.now();
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
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;
|
|
1320
1576
|
continue;
|
|
1321
1577
|
}
|
|
1322
1578
|
}
|
|
1323
|
-
await
|
|
1579
|
+
await abortableDelay(10, signal);
|
|
1324
1580
|
}
|
|
1325
1581
|
} finally {
|
|
1326
1582
|
if (cleanupTimer) {
|
|
1327
1583
|
clearInterval(cleanupTimer);
|
|
1328
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
|
+
}
|
|
1329
1594
|
assembler.clear();
|
|
1330
1595
|
}
|
|
1331
1596
|
}
|
|
@@ -1361,6 +1626,7 @@ async function* createAccountReplay(options) {
|
|
|
1361
1626
|
cleanupTimer = setInterval(() => {
|
|
1362
1627
|
assembler.cleanup();
|
|
1363
1628
|
}, cleanupInterval);
|
|
1629
|
+
cleanupTimer.unref?.();
|
|
1364
1630
|
const filterParams = {
|
|
1365
1631
|
address: protobuf.create(proto.FilterParamValueSchema, { kind: { case: "bytesValue", value: new Uint8Array(address) } })
|
|
1366
1632
|
};
|
|
@@ -1567,6 +1833,7 @@ var ConsoleSink = class {
|
|
|
1567
1833
|
constructor(prefix = "ReplaySink") {
|
|
1568
1834
|
this.prefix = prefix;
|
|
1569
1835
|
}
|
|
1836
|
+
prefix;
|
|
1570
1837
|
open(meta) {
|
|
1571
1838
|
const suffix = meta?.stream ? ` (${meta.stream})` : "";
|
|
1572
1839
|
console.info(`${this.prefix}${suffix} opened`, meta?.label ?? "");
|