@thru/replay 0.1.36 → 0.2.0
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 +405 -72
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +137 -49
- package/dist/index.d.ts +137 -49
- package/dist/index.mjs +401 -74
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -3
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { create } from '@bufbuild/protobuf';
|
|
2
2
|
import { createClient } from '@connectrpc/connect';
|
|
3
3
|
import { createGrpcTransport } from '@connectrpc/connect-node';
|
|
4
|
-
import { QueryService, StreamingService, ListBlocksRequestSchema, StreamBlocksRequestSchema, ListTransactionsRequestSchema, StreamTransactionsRequestSchema, ListEventsRequestSchema, StreamEventsRequestSchema, StreamAccountUpdatesRequestSchema, GetHeightRequestSchema, AccountView, FilterParamValueSchema, FilterSchema,
|
|
4
|
+
import { QueryService, StreamingService, GetAccountRequestSchema, ListAccountsRequestSchema, ListBlocksRequestSchema, StreamBlocksRequestSchema, ListTransactionsRequestSchema, StreamTransactionsRequestSchema, ListEventsRequestSchema, StreamEventsRequestSchema, StreamAccountUpdatesRequestSchema, GetHeightRequestSchema, AccountView, PageRequestSchema, PubkeySchema, FilterParamValueSchema, FilterSchema, EventSchema } from '@thru/proto';
|
|
5
5
|
export { AccountView, FilterParamValueSchema, FilterSchema } from '@thru/proto';
|
|
6
6
|
|
|
7
7
|
// src/chain-client.ts
|
|
@@ -16,6 +16,12 @@ var ChainClient = class {
|
|
|
16
16
|
query;
|
|
17
17
|
streaming;
|
|
18
18
|
callOptions;
|
|
19
|
+
getAccount(request) {
|
|
20
|
+
return this.query.getAccount(create(GetAccountRequestSchema, request), this.callOptions);
|
|
21
|
+
}
|
|
22
|
+
listAccounts(request) {
|
|
23
|
+
return this.query.listAccounts(create(ListAccountsRequestSchema, request), this.callOptions);
|
|
24
|
+
}
|
|
19
25
|
listBlocks(request) {
|
|
20
26
|
return this.query.listBlocks(create(ListBlocksRequestSchema, request), this.callOptions);
|
|
21
27
|
}
|
|
@@ -302,18 +308,52 @@ var LivePump = class {
|
|
|
302
308
|
}
|
|
303
309
|
};
|
|
304
310
|
|
|
311
|
+
// src/retry.ts
|
|
312
|
+
var DEFAULT_RETRY_CONFIG = {
|
|
313
|
+
initialDelayMs: 1e3,
|
|
314
|
+
maxDelayMs: 3e4,
|
|
315
|
+
connectionTimeoutMs: 3e4
|
|
316
|
+
};
|
|
317
|
+
function calculateBackoff(attempt, config) {
|
|
318
|
+
const delay2 = config.initialDelayMs * Math.pow(2, attempt);
|
|
319
|
+
return Math.min(delay2, config.maxDelayMs);
|
|
320
|
+
}
|
|
321
|
+
function withTimeout(promise, timeoutMs) {
|
|
322
|
+
return new Promise((resolve, reject) => {
|
|
323
|
+
const timer = setTimeout(() => {
|
|
324
|
+
reject(new TimeoutError(`Operation timed out after ${timeoutMs}ms`));
|
|
325
|
+
}, timeoutMs);
|
|
326
|
+
promise.then((value) => {
|
|
327
|
+
clearTimeout(timer);
|
|
328
|
+
resolve(value);
|
|
329
|
+
}).catch((err) => {
|
|
330
|
+
clearTimeout(timer);
|
|
331
|
+
reject(err);
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
var TimeoutError = class extends Error {
|
|
336
|
+
constructor(message) {
|
|
337
|
+
super(message);
|
|
338
|
+
this.name = "TimeoutError";
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
function delay(ms) {
|
|
342
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
343
|
+
}
|
|
344
|
+
|
|
305
345
|
// src/replay-stream.ts
|
|
306
346
|
var DEFAULT_METRICS = {
|
|
307
347
|
bufferedItems: 0,
|
|
308
348
|
emittedBackfill: 0,
|
|
309
349
|
emittedLive: 0,
|
|
350
|
+
emittedReconnect: 0,
|
|
310
351
|
discardedDuplicates: 0
|
|
311
352
|
};
|
|
312
353
|
function compareBigint(a, b) {
|
|
313
354
|
if (a === b) return 0;
|
|
314
355
|
return a < b ? -1 : 1;
|
|
315
356
|
}
|
|
316
|
-
var RETRY_DELAY_MS = 1e3;
|
|
317
357
|
var ReplayStream = class {
|
|
318
358
|
config;
|
|
319
359
|
logger;
|
|
@@ -336,12 +376,15 @@ var ReplayStream = class {
|
|
|
336
376
|
extractSlot,
|
|
337
377
|
extractKey,
|
|
338
378
|
safetyMargin,
|
|
339
|
-
resubscribeOnEnd
|
|
379
|
+
resubscribeOnEnd,
|
|
380
|
+
onReconnect
|
|
340
381
|
} = this.config;
|
|
341
382
|
const shouldResubscribeOnEnd = resubscribeOnEnd ?? true;
|
|
342
383
|
const keyOf = extractKey ?? ((item) => extractSlot(item).toString());
|
|
384
|
+
let currentSubscribeLive = subscribeLive;
|
|
385
|
+
let currentFetchBackfill = fetchBackfill;
|
|
343
386
|
const createLivePump = (slot, startStreaming = false, emitFloor) => new LivePump({
|
|
344
|
-
source:
|
|
387
|
+
source: currentSubscribeLive(slot),
|
|
345
388
|
slotOf: extractSlot,
|
|
346
389
|
keyOf,
|
|
347
390
|
logger: this.logger,
|
|
@@ -427,9 +470,15 @@ var ReplayStream = class {
|
|
|
427
470
|
}
|
|
428
471
|
if (!drained.length) livePump.updateEmitFloor(currentSlot);
|
|
429
472
|
this.logger.info("replay entering STREAMING state");
|
|
473
|
+
const retryConfig = DEFAULT_RETRY_CONFIG;
|
|
474
|
+
let retryAttempt = 0;
|
|
430
475
|
while (true) {
|
|
431
476
|
try {
|
|
432
|
-
const next = await
|
|
477
|
+
const next = await withTimeout(
|
|
478
|
+
livePump.next(),
|
|
479
|
+
retryConfig.connectionTimeoutMs
|
|
480
|
+
);
|
|
481
|
+
retryAttempt = 0;
|
|
433
482
|
if (next.done) {
|
|
434
483
|
if (!shouldResubscribeOnEnd) break;
|
|
435
484
|
throw new Error("stream ended");
|
|
@@ -447,20 +496,91 @@ var ReplayStream = class {
|
|
|
447
496
|
livePump.updateEmitFloor(currentSlot);
|
|
448
497
|
} catch (err) {
|
|
449
498
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
499
|
+
const backoffMs = calculateBackoff(retryAttempt, retryConfig);
|
|
450
500
|
this.logger.warn(
|
|
451
|
-
`live stream disconnected (${errMsg}); reconnecting in ${
|
|
501
|
+
`live stream disconnected (${errMsg}); reconnecting in ${backoffMs}ms from slot ${currentSlot} (attempt ${retryAttempt + 1})`
|
|
452
502
|
);
|
|
453
|
-
await delay(
|
|
503
|
+
await delay(backoffMs);
|
|
454
504
|
await safeClose(livePump);
|
|
505
|
+
retryAttempt++;
|
|
506
|
+
if (onReconnect) {
|
|
507
|
+
try {
|
|
508
|
+
const fresh = onReconnect();
|
|
509
|
+
currentSubscribeLive = fresh.subscribeLive;
|
|
510
|
+
if (fresh.fetchBackfill) {
|
|
511
|
+
currentFetchBackfill = fresh.fetchBackfill;
|
|
512
|
+
}
|
|
513
|
+
this.logger.info("created fresh client for reconnection");
|
|
514
|
+
} catch (factoryErr) {
|
|
515
|
+
this.logger.error(
|
|
516
|
+
`failed to create fresh client: ${factoryErr instanceof Error ? factoryErr.message : String(factoryErr)}; using existing`
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (onReconnect && currentSlot > 0n) {
|
|
521
|
+
for await (const item of this.miniBackfill(
|
|
522
|
+
currentSlot,
|
|
523
|
+
currentFetchBackfill,
|
|
524
|
+
extractSlot,
|
|
525
|
+
keyOf,
|
|
526
|
+
seenItem,
|
|
527
|
+
recordEmission
|
|
528
|
+
)) {
|
|
529
|
+
const itemSlot = extractSlot(item);
|
|
530
|
+
if (itemSlot > currentSlot) {
|
|
531
|
+
currentSlot = itemSlot;
|
|
532
|
+
}
|
|
533
|
+
yield item;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
455
536
|
const resumeSlot = currentSlot > 0n ? currentSlot : 0n;
|
|
456
537
|
livePump = createLivePump(resumeSlot, true, currentSlot);
|
|
457
538
|
}
|
|
458
539
|
}
|
|
459
540
|
}
|
|
541
|
+
/**
|
|
542
|
+
* Perform mini-backfill from lastProcessedSlot to catch up after reconnection.
|
|
543
|
+
* Ensures no data gaps from events that occurred during disconnection.
|
|
544
|
+
*/
|
|
545
|
+
async *miniBackfill(fromSlot, fetchBackfill, extractSlot, keyOf, seenItem, recordEmission) {
|
|
546
|
+
this.logger.info(`mini-backfill starting from slot ${fromSlot}`);
|
|
547
|
+
const MINI_BACKFILL_TIMEOUT = 3e4;
|
|
548
|
+
const startTime = Date.now();
|
|
549
|
+
let cursor;
|
|
550
|
+
let itemsYielded = 0;
|
|
551
|
+
try {
|
|
552
|
+
while (true) {
|
|
553
|
+
if (Date.now() - startTime > MINI_BACKFILL_TIMEOUT) {
|
|
554
|
+
this.logger.warn(`mini-backfill timed out after ${MINI_BACKFILL_TIMEOUT}ms`);
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
const page = await fetchBackfill({ startSlot: fromSlot, cursor });
|
|
558
|
+
const sorted = [...page.items].sort(
|
|
559
|
+
(a, b) => compareBigint(extractSlot(a), extractSlot(b))
|
|
560
|
+
);
|
|
561
|
+
for (const item of sorted) {
|
|
562
|
+
const slot = extractSlot(item);
|
|
563
|
+
const key = keyOf(item);
|
|
564
|
+
if (seenItem(slot, key)) {
|
|
565
|
+
this.metrics.discardedDuplicates += 1;
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
recordEmission(slot, key);
|
|
569
|
+
itemsYielded++;
|
|
570
|
+
this.metrics.emittedReconnect += 1;
|
|
571
|
+
yield item;
|
|
572
|
+
}
|
|
573
|
+
cursor = page.cursor;
|
|
574
|
+
if (page.done || cursor === void 0) break;
|
|
575
|
+
}
|
|
576
|
+
this.logger.info(`mini-backfill complete: ${itemsYielded} items yielded`);
|
|
577
|
+
} catch (err) {
|
|
578
|
+
this.logger.warn(
|
|
579
|
+
`mini-backfill failed: ${err instanceof Error ? err.message : String(err)}; proceeding with live stream`
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
460
583
|
};
|
|
461
|
-
function delay(ms) {
|
|
462
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
463
|
-
}
|
|
464
584
|
async function safeClose(pump) {
|
|
465
585
|
try {
|
|
466
586
|
await pump.close();
|
|
@@ -622,12 +742,26 @@ function bytesToHex(bytes) {
|
|
|
622
742
|
for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
|
|
623
743
|
return hex;
|
|
624
744
|
}
|
|
745
|
+
|
|
746
|
+
// src/types.ts
|
|
747
|
+
function resolveClient(opts, optionsName) {
|
|
748
|
+
if (opts.clientFactory) {
|
|
749
|
+
return opts.clientFactory();
|
|
750
|
+
}
|
|
751
|
+
if (!opts.client) {
|
|
752
|
+
throw new Error(`${optionsName} requires either client or clientFactory`);
|
|
753
|
+
}
|
|
754
|
+
return opts.client;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// src/replay/event-replay.ts
|
|
625
758
|
var DEFAULT_PAGE_SIZE3 = 512;
|
|
626
759
|
var DEFAULT_SAFETY_MARGIN3 = 64n;
|
|
627
760
|
var PAGE_ORDER_ASC3 = "slot asc";
|
|
628
761
|
function createEventReplay(options) {
|
|
629
762
|
const safetyMargin = options.safetyMargin ?? DEFAULT_SAFETY_MARGIN3;
|
|
630
|
-
|
|
763
|
+
let currentClient = resolveClient(options, "EventReplayOptions");
|
|
764
|
+
const createFetchBackfill = (client) => async ({
|
|
631
765
|
startSlot,
|
|
632
766
|
cursor
|
|
633
767
|
}) => {
|
|
@@ -638,7 +772,7 @@ function createEventReplay(options) {
|
|
|
638
772
|
});
|
|
639
773
|
const baseFilter = slotLiteralFilter("event.slot", startSlot);
|
|
640
774
|
const mergedFilter = combineFilters(baseFilter, options.filter);
|
|
641
|
-
const response = await
|
|
775
|
+
const response = await client.listEvents(
|
|
642
776
|
create(ListEventsRequestSchema, {
|
|
643
777
|
filter: mergedFilter,
|
|
644
778
|
page
|
|
@@ -646,25 +780,33 @@ function createEventReplay(options) {
|
|
|
646
780
|
);
|
|
647
781
|
return backfillPage(response.events, response.page);
|
|
648
782
|
};
|
|
649
|
-
const
|
|
783
|
+
const createSubscribeLive = (client) => (startSlot) => {
|
|
650
784
|
const mergedFilter = combineFilters(slotLiteralFilter("event.slot", startSlot), options.filter);
|
|
651
785
|
const request = create(StreamEventsRequestSchema, {
|
|
652
786
|
filter: mergedFilter
|
|
653
787
|
});
|
|
654
788
|
return mapAsyncIterable(
|
|
655
|
-
|
|
789
|
+
client.streamEvents(request),
|
|
656
790
|
(resp) => streamResponseToEvent(resp)
|
|
657
791
|
);
|
|
658
792
|
};
|
|
793
|
+
const onReconnect = options.clientFactory ? () => {
|
|
794
|
+
currentClient = options.clientFactory();
|
|
795
|
+
return {
|
|
796
|
+
subscribeLive: createSubscribeLive(currentClient),
|
|
797
|
+
fetchBackfill: createFetchBackfill(currentClient)
|
|
798
|
+
};
|
|
799
|
+
} : void 0;
|
|
659
800
|
return new ReplayStream({
|
|
660
801
|
startSlot: options.startSlot,
|
|
661
802
|
safetyMargin,
|
|
662
|
-
fetchBackfill,
|
|
663
|
-
subscribeLive,
|
|
803
|
+
fetchBackfill: createFetchBackfill(currentClient),
|
|
804
|
+
subscribeLive: createSubscribeLive(currentClient),
|
|
664
805
|
extractSlot: (event) => event.slot ?? 0n,
|
|
665
806
|
extractKey: eventKey,
|
|
666
807
|
logger: options.logger,
|
|
667
|
-
resubscribeOnEnd: options.resubscribeOnEnd
|
|
808
|
+
resubscribeOnEnd: options.resubscribeOnEnd,
|
|
809
|
+
onReconnect
|
|
668
810
|
});
|
|
669
811
|
}
|
|
670
812
|
function streamResponseToEvent(resp) {
|
|
@@ -858,7 +1000,7 @@ function snapshotToState(account) {
|
|
|
858
1000
|
meta: account.meta,
|
|
859
1001
|
data: account.data?.data ?? new Uint8Array(0),
|
|
860
1002
|
isDelete: account.meta.flags?.isDeleted ?? false,
|
|
861
|
-
source: "
|
|
1003
|
+
source: "stream"
|
|
862
1004
|
};
|
|
863
1005
|
}
|
|
864
1006
|
function assembledToState(assembled) {
|
|
@@ -870,9 +1012,248 @@ function assembledToState(assembled) {
|
|
|
870
1012
|
meta: assembled.meta,
|
|
871
1013
|
data: assembled.data,
|
|
872
1014
|
isDelete: assembled.isDelete,
|
|
873
|
-
source: "
|
|
1015
|
+
source: "stream"
|
|
874
1016
|
};
|
|
875
1017
|
}
|
|
1018
|
+
function getAccountToState(account) {
|
|
1019
|
+
if (!account.address?.value || !account.meta) {
|
|
1020
|
+
return null;
|
|
1021
|
+
}
|
|
1022
|
+
return {
|
|
1023
|
+
address: account.address.value,
|
|
1024
|
+
addressHex: bytesToHex2(account.address.value),
|
|
1025
|
+
slot: account.meta.lastUpdatedSlot ?? account.versionContext?.slot ?? 0n,
|
|
1026
|
+
seq: BigInt(account.meta.seq.toString()),
|
|
1027
|
+
meta: account.meta,
|
|
1028
|
+
data: account.data?.data ?? new Uint8Array(0),
|
|
1029
|
+
isDelete: account.meta.flags?.isDeleted ?? false,
|
|
1030
|
+
source: "backfill"
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
function buildListAccountsOwnerFilter(owner, dataSizes, minUpdatedSlot) {
|
|
1034
|
+
let expression = "account.meta.owner.value == params.owner_bytes";
|
|
1035
|
+
if (dataSizes && dataSizes.length > 0) {
|
|
1036
|
+
const sizeConditions = dataSizes.map((size) => `account.meta.data_size == uint(${size})`).join(" || ");
|
|
1037
|
+
expression = `(${expression}) && (${sizeConditions})`;
|
|
1038
|
+
}
|
|
1039
|
+
if (minUpdatedSlot !== void 0 && minUpdatedSlot > 0n) {
|
|
1040
|
+
expression = `(${expression}) && account.meta.last_updated_slot >= params.min_updated_slot`;
|
|
1041
|
+
}
|
|
1042
|
+
const params = {
|
|
1043
|
+
owner_bytes: create(FilterParamValueSchema, { kind: { case: "bytesValue", value: new Uint8Array(owner) } })
|
|
1044
|
+
};
|
|
1045
|
+
if (minUpdatedSlot !== void 0 && minUpdatedSlot > 0n) {
|
|
1046
|
+
params["min_updated_slot"] = create(FilterParamValueSchema, { kind: { case: "uintValue", value: minUpdatedSlot } });
|
|
1047
|
+
}
|
|
1048
|
+
return create(FilterSchema, { expression, params });
|
|
1049
|
+
}
|
|
1050
|
+
async function* createAccountsByOwnerReplay(options) {
|
|
1051
|
+
const {
|
|
1052
|
+
owner,
|
|
1053
|
+
view = AccountView.FULL,
|
|
1054
|
+
dataSizes,
|
|
1055
|
+
minUpdatedSlot,
|
|
1056
|
+
pageSize = 100,
|
|
1057
|
+
maxRetries = 3,
|
|
1058
|
+
pageAssemblerOptions,
|
|
1059
|
+
cleanupInterval = 1e4,
|
|
1060
|
+
onBackfillComplete,
|
|
1061
|
+
clientFactory,
|
|
1062
|
+
logger = NOOP_LOGGER
|
|
1063
|
+
} = options;
|
|
1064
|
+
let client = resolveClient(options, "AccountsByOwnerReplayOptions");
|
|
1065
|
+
const seenFromStream = /* @__PURE__ */ new Set();
|
|
1066
|
+
const fetchQueue = [];
|
|
1067
|
+
let highestSlotSeen = minUpdatedSlot ?? 0n;
|
|
1068
|
+
const assembler = new PageAssembler(pageAssemblerOptions);
|
|
1069
|
+
let cleanupTimer = null;
|
|
1070
|
+
const streamBuffer = [];
|
|
1071
|
+
let streamDone = false;
|
|
1072
|
+
let streamError = null;
|
|
1073
|
+
try {
|
|
1074
|
+
cleanupTimer = setInterval(() => {
|
|
1075
|
+
assembler.cleanup();
|
|
1076
|
+
}, cleanupInterval);
|
|
1077
|
+
const streamFilter = buildOwnerFilterWithMinSlot(owner, dataSizes, minUpdatedSlot);
|
|
1078
|
+
const stream = client.streamAccountUpdates({ view, filter: streamFilter });
|
|
1079
|
+
const streamProcessor = (async () => {
|
|
1080
|
+
try {
|
|
1081
|
+
for await (const response of stream) {
|
|
1082
|
+
const event = processResponseMulti(response, assembler);
|
|
1083
|
+
if (event) {
|
|
1084
|
+
if (event.type === "account") {
|
|
1085
|
+
seenFromStream.add(event.account.addressHex);
|
|
1086
|
+
if (event.account.slot > highestSlotSeen) {
|
|
1087
|
+
highestSlotSeen = event.account.slot;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
streamBuffer.push(event);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
} catch (err) {
|
|
1094
|
+
streamError = err;
|
|
1095
|
+
} finally {
|
|
1096
|
+
streamDone = true;
|
|
1097
|
+
}
|
|
1098
|
+
})();
|
|
1099
|
+
const yieldStreamBuffer = function* () {
|
|
1100
|
+
while (streamBuffer.length > 0) {
|
|
1101
|
+
const event = streamBuffer.shift();
|
|
1102
|
+
if (event.type === "account") {
|
|
1103
|
+
seenFromStream.add(event.account.addressHex);
|
|
1104
|
+
}
|
|
1105
|
+
yield event;
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
const backfillFilter = buildListAccountsOwnerFilter(owner, dataSizes, minUpdatedSlot);
|
|
1109
|
+
let pageToken;
|
|
1110
|
+
do {
|
|
1111
|
+
const request = {
|
|
1112
|
+
view: AccountView.META_ONLY,
|
|
1113
|
+
// Address + metadata only, no data
|
|
1114
|
+
filter: backfillFilter,
|
|
1115
|
+
page: create(PageRequestSchema, {
|
|
1116
|
+
pageSize,
|
|
1117
|
+
pageToken
|
|
1118
|
+
})
|
|
1119
|
+
};
|
|
1120
|
+
const response = await client.listAccounts(request);
|
|
1121
|
+
for (const account of response.accounts) {
|
|
1122
|
+
if (account.address?.value) {
|
|
1123
|
+
fetchQueue.push(account.address.value);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
pageToken = response.page?.nextPageToken;
|
|
1127
|
+
yield* yieldStreamBuffer();
|
|
1128
|
+
} while (pageToken);
|
|
1129
|
+
for (const address of fetchQueue) {
|
|
1130
|
+
const addressHex = bytesToHex2(address);
|
|
1131
|
+
if (seenFromStream.has(addressHex)) {
|
|
1132
|
+
continue;
|
|
1133
|
+
}
|
|
1134
|
+
yield* yieldStreamBuffer();
|
|
1135
|
+
if (seenFromStream.has(addressHex)) {
|
|
1136
|
+
continue;
|
|
1137
|
+
}
|
|
1138
|
+
let account = null;
|
|
1139
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
1140
|
+
try {
|
|
1141
|
+
account = await client.getAccount({
|
|
1142
|
+
address: create(PubkeySchema, { value: address }),
|
|
1143
|
+
view: AccountView.FULL
|
|
1144
|
+
});
|
|
1145
|
+
break;
|
|
1146
|
+
} catch (err) {
|
|
1147
|
+
if (attempt === maxRetries - 1) {
|
|
1148
|
+
logger.error(`[backfill] failed to fetch account ${addressHex} after ${maxRetries} attempts`, { error: err });
|
|
1149
|
+
} else {
|
|
1150
|
+
await new Promise((resolve) => setTimeout(resolve, 100 * (attempt + 1)));
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
if (account) {
|
|
1155
|
+
const state = getAccountToState(account);
|
|
1156
|
+
if (state) {
|
|
1157
|
+
if (state.slot > highestSlotSeen) {
|
|
1158
|
+
highestSlotSeen = state.slot;
|
|
1159
|
+
}
|
|
1160
|
+
yield { type: "account", account: state };
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
if (onBackfillComplete) {
|
|
1165
|
+
onBackfillComplete(highestSlotSeen);
|
|
1166
|
+
}
|
|
1167
|
+
const retryConfig = DEFAULT_RETRY_CONFIG;
|
|
1168
|
+
let retryAttempt = 0;
|
|
1169
|
+
let currentStream = stream;
|
|
1170
|
+
let currentStreamProcessor = streamProcessor;
|
|
1171
|
+
const createStreamProcessor = () => {
|
|
1172
|
+
if (clientFactory) {
|
|
1173
|
+
try {
|
|
1174
|
+
client = clientFactory();
|
|
1175
|
+
logger.info("[account-stream] created fresh client for reconnection");
|
|
1176
|
+
} catch (err) {
|
|
1177
|
+
logger.error("[account-stream] failed to create fresh client", { error: err });
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
const newStreamFilter = buildOwnerFilterWithMinSlot(owner, dataSizes, highestSlotSeen > 0n ? highestSlotSeen : minUpdatedSlot);
|
|
1181
|
+
const newStream = client.streamAccountUpdates({ view, filter: newStreamFilter });
|
|
1182
|
+
const newProcessor = (async () => {
|
|
1183
|
+
try {
|
|
1184
|
+
for await (const response of newStream) {
|
|
1185
|
+
retryAttempt = 0;
|
|
1186
|
+
const event = processResponseMulti(response, assembler);
|
|
1187
|
+
if (event) {
|
|
1188
|
+
if (event.type === "account") {
|
|
1189
|
+
seenFromStream.add(event.account.addressHex);
|
|
1190
|
+
if (event.account.slot > highestSlotSeen) {
|
|
1191
|
+
highestSlotSeen = event.account.slot;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
streamBuffer.push(event);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
} catch (err) {
|
|
1198
|
+
streamError = err;
|
|
1199
|
+
} finally {
|
|
1200
|
+
streamDone = true;
|
|
1201
|
+
}
|
|
1202
|
+
})();
|
|
1203
|
+
return { stream: newStream, processor: newProcessor };
|
|
1204
|
+
};
|
|
1205
|
+
while (true) {
|
|
1206
|
+
yield* yieldStreamBuffer();
|
|
1207
|
+
if (streamDone) {
|
|
1208
|
+
if (streamError) {
|
|
1209
|
+
const backoffMs = calculateBackoff(retryAttempt, retryConfig);
|
|
1210
|
+
logger.warn(
|
|
1211
|
+
`[account-stream] disconnected (${streamError.message}); reconnecting in ${backoffMs}ms (attempt ${retryAttempt + 1})`
|
|
1212
|
+
);
|
|
1213
|
+
await delay(backoffMs);
|
|
1214
|
+
retryAttempt++;
|
|
1215
|
+
streamDone = false;
|
|
1216
|
+
streamError = null;
|
|
1217
|
+
streamBuffer.length = 0;
|
|
1218
|
+
const { stream: newStream, processor: newProcessor } = createStreamProcessor();
|
|
1219
|
+
currentStream = newStream;
|
|
1220
|
+
currentStreamProcessor = newProcessor;
|
|
1221
|
+
continue;
|
|
1222
|
+
} else {
|
|
1223
|
+
logger.warn("[account-stream] stream ended unexpectedly; reconnecting...");
|
|
1224
|
+
streamDone = false;
|
|
1225
|
+
const { stream: newStream, processor: newProcessor } = createStreamProcessor();
|
|
1226
|
+
currentStream = newStream;
|
|
1227
|
+
currentStreamProcessor = newProcessor;
|
|
1228
|
+
continue;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
await delay(10);
|
|
1232
|
+
}
|
|
1233
|
+
} finally {
|
|
1234
|
+
if (cleanupTimer) {
|
|
1235
|
+
clearInterval(cleanupTimer);
|
|
1236
|
+
}
|
|
1237
|
+
assembler.clear();
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
function buildOwnerFilterWithMinSlot(owner, dataSizes, minSlot) {
|
|
1241
|
+
let expression = "(has(snapshot.meta.owner) && snapshot.meta.owner.value == params.owner) || (has(account_update.meta.owner) && account_update.meta.owner.value == params.owner)";
|
|
1242
|
+
if (dataSizes && dataSizes.length > 0) {
|
|
1243
|
+
const sizeConditions = dataSizes.map((size) => `snapshot.meta.data_size == uint(${size}) || account_update.meta.data_size == uint(${size})`).join(" || ");
|
|
1244
|
+
expression = `(${expression}) && (${sizeConditions})`;
|
|
1245
|
+
}
|
|
1246
|
+
if (minSlot !== void 0 && minSlot > 0n) {
|
|
1247
|
+
expression = `(${expression}) && (has(snapshot.address) || (has(account_update.slot) && account_update.slot >= params.min_slot))`;
|
|
1248
|
+
}
|
|
1249
|
+
const params = {
|
|
1250
|
+
owner: create(FilterParamValueSchema, { kind: { case: "bytesValue", value: new Uint8Array(owner) } })
|
|
1251
|
+
};
|
|
1252
|
+
if (minSlot !== void 0 && minSlot > 0n) {
|
|
1253
|
+
params["min_slot"] = create(FilterParamValueSchema, { kind: { case: "uintValue", value: minSlot } });
|
|
1254
|
+
}
|
|
1255
|
+
return create(FilterSchema, { expression, params });
|
|
1256
|
+
}
|
|
876
1257
|
async function* createAccountReplay(options) {
|
|
877
1258
|
const {
|
|
878
1259
|
client,
|
|
@@ -974,60 +1355,6 @@ function processResponseMulti(response, assembler) {
|
|
|
974
1355
|
return null;
|
|
975
1356
|
}
|
|
976
1357
|
}
|
|
977
|
-
function buildOwnerFilter(owner, dataSizes, additionalFilter) {
|
|
978
|
-
let expression = "(has(snapshot.meta.owner) && snapshot.meta.owner.value == params.owner) || (has(account_update.meta.owner) && account_update.meta.owner.value == params.owner)";
|
|
979
|
-
if (dataSizes && dataSizes.length > 0) {
|
|
980
|
-
const sizeConditions = dataSizes.map((size) => `snapshot.meta.data_size == uint(${size}) || account_update.meta.data_size == uint(${size})`).join(" || ");
|
|
981
|
-
expression = `(${expression}) && (${sizeConditions})`;
|
|
982
|
-
}
|
|
983
|
-
if (additionalFilter?.expression) {
|
|
984
|
-
expression = `(${expression}) && (${additionalFilter.expression})`;
|
|
985
|
-
}
|
|
986
|
-
const params = {
|
|
987
|
-
owner: create(FilterParamValueSchema, { kind: { case: "bytesValue", value: new Uint8Array(owner) } })
|
|
988
|
-
};
|
|
989
|
-
if (additionalFilter?.params) {
|
|
990
|
-
for (const [key, value] of Object.entries(additionalFilter.params)) {
|
|
991
|
-
params[key] = create(FilterParamValueSchema, value);
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
return create(FilterSchema, { expression, params });
|
|
995
|
-
}
|
|
996
|
-
async function* createAccountsByOwnerReplay(options) {
|
|
997
|
-
const {
|
|
998
|
-
client,
|
|
999
|
-
owner,
|
|
1000
|
-
view = AccountView.FULL,
|
|
1001
|
-
dataSizes,
|
|
1002
|
-
filter,
|
|
1003
|
-
pageAssemblerOptions,
|
|
1004
|
-
cleanupInterval = 1e4
|
|
1005
|
-
} = options;
|
|
1006
|
-
const assembler = new PageAssembler(pageAssemblerOptions);
|
|
1007
|
-
let cleanupTimer = null;
|
|
1008
|
-
try {
|
|
1009
|
-
cleanupTimer = setInterval(() => {
|
|
1010
|
-
assembler.cleanup();
|
|
1011
|
-
}, cleanupInterval);
|
|
1012
|
-
const ownerFilter = buildOwnerFilter(owner, dataSizes, filter);
|
|
1013
|
-
const request = {
|
|
1014
|
-
view,
|
|
1015
|
-
filter: ownerFilter
|
|
1016
|
-
};
|
|
1017
|
-
const stream = client.streamAccountUpdates(request);
|
|
1018
|
-
for await (const response of stream) {
|
|
1019
|
-
const event = processResponseMulti(response, assembler);
|
|
1020
|
-
if (event) {
|
|
1021
|
-
yield event;
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
} finally {
|
|
1025
|
-
if (cleanupTimer) {
|
|
1026
|
-
clearInterval(cleanupTimer);
|
|
1027
|
-
}
|
|
1028
|
-
assembler.clear();
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
1358
|
var AccountSeqTracker = class {
|
|
1032
1359
|
seqs = /* @__PURE__ */ new Map();
|
|
1033
1360
|
/**
|
|
@@ -1165,6 +1492,6 @@ var ConsoleSink = class {
|
|
|
1165
1492
|
}
|
|
1166
1493
|
};
|
|
1167
1494
|
|
|
1168
|
-
export { AccountSeqTracker, ChainClient, ConsoleSink, MultiAccountReplay, NOOP_LOGGER, PAGE_SIZE, PageAssembler, ReplayStream, createAccountReplay, createAccountsByOwnerReplay, createBlockReplay, createConsoleLogger, createEventReplay, createTransactionReplay };
|
|
1495
|
+
export { AccountSeqTracker, ChainClient, ConsoleSink, DEFAULT_RETRY_CONFIG, MultiAccountReplay, NOOP_LOGGER, PAGE_SIZE, PageAssembler, ReplayStream, TimeoutError, calculateBackoff, createAccountReplay, createAccountsByOwnerReplay, createBlockReplay, createConsoleLogger, createEventReplay, createTransactionReplay, delay, resolveClient, withTimeout };
|
|
1169
1496
|
//# sourceMappingURL=index.mjs.map
|
|
1170
1497
|
//# sourceMappingURL=index.mjs.map
|