@thru/indexer 0.2.30 → 0.2.31
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 +392 -93
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +79 -1
- package/dist/index.d.ts +79 -1
- package/dist/index.mjs +392 -93
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -360,7 +360,8 @@ async function runEventStreamProcessor(stream, options, abortSignal) {
|
|
|
360
360
|
safetyMargin = 64,
|
|
361
361
|
pageSize = 512,
|
|
362
362
|
logLevel = "info",
|
|
363
|
-
validateParse = false
|
|
363
|
+
validateParse = false,
|
|
364
|
+
observer
|
|
364
365
|
} = options;
|
|
365
366
|
const log = (level, msg) => {
|
|
366
367
|
if (logLevel === "debug" || level !== "debug") {
|
|
@@ -370,6 +371,10 @@ async function runEventStreamProcessor(stream, options, abortSignal) {
|
|
|
370
371
|
log("info", `Starting stream processor: ${stream.description}`);
|
|
371
372
|
const checkpoint = await getCheckpoint(db, stream.name);
|
|
372
373
|
const startSlot = checkpoint ? checkpoint.slot : defaultStartSlot;
|
|
374
|
+
observer?.onStart?.({
|
|
375
|
+
startSlot,
|
|
376
|
+
checkpointSlot: checkpoint?.slot ?? null
|
|
377
|
+
});
|
|
373
378
|
log(
|
|
374
379
|
"info",
|
|
375
380
|
`Starting from slot ${startSlot}${checkpoint ? " (resuming)" : " (fresh start)"}`
|
|
@@ -425,31 +430,43 @@ async function runEventStreamProcessor(stream, options, abortSignal) {
|
|
|
425
430
|
);
|
|
426
431
|
}
|
|
427
432
|
} catch (filterErr) {
|
|
433
|
+
observer?.onError?.("filterBatch", filterErr);
|
|
428
434
|
log(
|
|
429
435
|
"error",
|
|
430
436
|
`filterBatch hook failed: ${filterErr instanceof Error ? filterErr.message : String(filterErr)}`
|
|
431
437
|
);
|
|
432
|
-
|
|
438
|
+
throw filterErr;
|
|
433
439
|
}
|
|
434
440
|
}
|
|
435
441
|
const lastEventToCommit = eventsToCommit[eventsToCommit.length - 1];
|
|
436
442
|
const lastEventToCommitId = lastEventToCommit?.id ?? null;
|
|
437
443
|
let committedEvents = eventsToCommit;
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
444
|
+
try {
|
|
445
|
+
await db.transaction(async (tx) => {
|
|
446
|
+
committedEvents = await tx.insert(stream.table).values(eventsToCommit).onConflictDoNothing().returning();
|
|
447
|
+
await updateCheckpoint(
|
|
448
|
+
tx,
|
|
449
|
+
stream.name,
|
|
450
|
+
batch.slot,
|
|
451
|
+
lastEventToCommitId
|
|
452
|
+
);
|
|
453
|
+
});
|
|
454
|
+
} catch (commitErr) {
|
|
455
|
+
observer?.onError?.("commit", commitErr);
|
|
456
|
+
throw commitErr;
|
|
457
|
+
}
|
|
447
458
|
stats.batchesCommitted++;
|
|
448
459
|
stats.lastSlot = batch.slot;
|
|
460
|
+
observer?.onBatchCommitted?.({
|
|
461
|
+
slot: batch.slot,
|
|
462
|
+
count: eventsToCommit.length
|
|
463
|
+
});
|
|
464
|
+
observer?.onCheckpoint?.({ slot: batch.slot });
|
|
449
465
|
if (stream.onCommit && committedEvents.length > 0) {
|
|
450
466
|
try {
|
|
451
467
|
await stream.onCommit({ ...batch, events: committedEvents }, { db });
|
|
452
468
|
} catch (hookErr) {
|
|
469
|
+
observer?.onError?.("onCommit", hookErr);
|
|
453
470
|
log(
|
|
454
471
|
"error",
|
|
455
472
|
`onCommit hook failed: ${hookErr instanceof Error ? hookErr.message : String(hookErr)}`
|
|
@@ -457,6 +474,11 @@ async function runEventStreamProcessor(stream, options, abortSignal) {
|
|
|
457
474
|
}
|
|
458
475
|
}
|
|
459
476
|
};
|
|
477
|
+
let rejectFlushFailure = () => {
|
|
478
|
+
};
|
|
479
|
+
const flushFailure = new Promise((_, reject) => {
|
|
480
|
+
rejectFlushFailure = reject;
|
|
481
|
+
});
|
|
460
482
|
const flushInterval = setInterval(async () => {
|
|
461
483
|
const batch = batcher.flushIfStale();
|
|
462
484
|
if (batch) {
|
|
@@ -471,21 +493,38 @@ async function runEventStreamProcessor(stream, options, abortSignal) {
|
|
|
471
493
|
"error",
|
|
472
494
|
`Timeout flush failed: ${err instanceof Error ? err.message : String(err)}`
|
|
473
495
|
);
|
|
496
|
+
clearInterval(flushInterval);
|
|
497
|
+
rejectFlushFailure(err);
|
|
474
498
|
}
|
|
475
499
|
}
|
|
476
500
|
}, 1e3);
|
|
477
|
-
|
|
501
|
+
const processReplay = async () => {
|
|
478
502
|
for await (const event of replay$1) {
|
|
479
503
|
if (abortSignal?.aborted) {
|
|
480
504
|
log("info", "Abort signal received, stopping...");
|
|
481
505
|
break;
|
|
482
506
|
}
|
|
483
507
|
eventsReceivedSinceLastLog++;
|
|
484
|
-
|
|
485
|
-
|
|
508
|
+
observer?.onRecord?.({
|
|
509
|
+
slot: event.slot ?? null,
|
|
510
|
+
id: event.eventId ?? null
|
|
511
|
+
});
|
|
512
|
+
let parsed;
|
|
513
|
+
try {
|
|
514
|
+
parsed = stream.parse(event);
|
|
515
|
+
} catch (parseErr) {
|
|
516
|
+
observer?.onParserError?.(parseErr);
|
|
517
|
+
observer?.onError?.("parse", parseErr);
|
|
518
|
+
throw parseErr;
|
|
519
|
+
}
|
|
520
|
+
if (!parsed) {
|
|
521
|
+
observer?.onParserNull?.();
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
486
524
|
if (validateParse) {
|
|
487
525
|
const validation = validateParsedData(stream.schema, parsed, stream.name);
|
|
488
526
|
if (!validation.success) {
|
|
527
|
+
observer?.onParseValidationError?.(validation.error);
|
|
489
528
|
log("error", validation.error);
|
|
490
529
|
continue;
|
|
491
530
|
}
|
|
@@ -517,6 +556,9 @@ async function runEventStreamProcessor(stream, options, abortSignal) {
|
|
|
517
556
|
`Final flush: ${finalBatch.events.length} event(s) at slot ${finalBatch.slot}`
|
|
518
557
|
);
|
|
519
558
|
}
|
|
559
|
+
};
|
|
560
|
+
try {
|
|
561
|
+
await Promise.race([processReplay(), flushFailure]);
|
|
520
562
|
} catch (err) {
|
|
521
563
|
log(
|
|
522
564
|
"error",
|
|
@@ -525,6 +567,8 @@ async function runEventStreamProcessor(stream, options, abortSignal) {
|
|
|
525
567
|
throw err;
|
|
526
568
|
} finally {
|
|
527
569
|
clearInterval(flushInterval);
|
|
570
|
+
rejectFlushFailure = () => {
|
|
571
|
+
};
|
|
528
572
|
}
|
|
529
573
|
log(
|
|
530
574
|
"info",
|
|
@@ -573,7 +617,7 @@ function shouldLog(level, minLevel) {
|
|
|
573
617
|
return levels.indexOf(level) >= levels.indexOf(minLevel);
|
|
574
618
|
}
|
|
575
619
|
async function runAccountStreamProcessor(stream, options, abortSignal) {
|
|
576
|
-
const { clientFactory, db, logLevel = "info", validateParse = false } = options;
|
|
620
|
+
const { clientFactory, db, logLevel = "info", validateParse = false, observer } = options;
|
|
577
621
|
const checkpointName = `account:${stream.name}`;
|
|
578
622
|
const log = (level, msg, meta) => {
|
|
579
623
|
if (shouldLog(level, logLevel)) {
|
|
@@ -589,6 +633,10 @@ async function runAccountStreamProcessor(stream, options, abortSignal) {
|
|
|
589
633
|
};
|
|
590
634
|
const checkpoint = await getCheckpoint(db, checkpointName);
|
|
591
635
|
const minUpdatedSlot = checkpoint?.slot;
|
|
636
|
+
observer?.onStart?.({
|
|
637
|
+
startSlot: minUpdatedSlot,
|
|
638
|
+
checkpointSlot: checkpoint?.slot ?? null
|
|
639
|
+
});
|
|
592
640
|
if (minUpdatedSlot) {
|
|
593
641
|
log("info", `Resuming from checkpoint: slot ${minUpdatedSlot}`);
|
|
594
642
|
}
|
|
@@ -632,6 +680,10 @@ async function runAccountStreamProcessor(stream, options, abortSignal) {
|
|
|
632
680
|
if (event.type === "account") {
|
|
633
681
|
const account = event.account;
|
|
634
682
|
stats.accountsProcessed++;
|
|
683
|
+
observer?.onRecord?.({
|
|
684
|
+
slot: account.slot,
|
|
685
|
+
id: account.addressHex
|
|
686
|
+
});
|
|
635
687
|
if (stats.accountsProcessed <= 3) {
|
|
636
688
|
log(
|
|
637
689
|
"info",
|
|
@@ -646,17 +698,28 @@ async function runAccountStreamProcessor(stream, options, abortSignal) {
|
|
|
646
698
|
try {
|
|
647
699
|
await db.delete(stream.table).where(drizzleOrm.eq(table[idField], idValue));
|
|
648
700
|
stats.accountsDeleted++;
|
|
701
|
+
observer?.onCheckpoint?.({ slot: account.slot });
|
|
649
702
|
log("info", `Deleted row for account ${account.addressHex}`);
|
|
650
703
|
if (account.slot > lastProcessedSlot) {
|
|
651
704
|
lastProcessedSlot = account.slot;
|
|
652
705
|
}
|
|
653
706
|
} catch (err) {
|
|
707
|
+
observer?.onError?.("commit", err);
|
|
654
708
|
log("error", `Failed to delete account ${account.addressHex}: ${err}`);
|
|
709
|
+
throw err;
|
|
655
710
|
}
|
|
656
711
|
continue;
|
|
657
712
|
}
|
|
658
|
-
|
|
713
|
+
let parsed;
|
|
714
|
+
try {
|
|
715
|
+
parsed = stream.parse(account);
|
|
716
|
+
} catch (parseErr) {
|
|
717
|
+
observer?.onParserError?.(parseErr);
|
|
718
|
+
observer?.onError?.("parse", parseErr);
|
|
719
|
+
throw parseErr;
|
|
720
|
+
}
|
|
659
721
|
if (!parsed) {
|
|
722
|
+
observer?.onParserNull?.();
|
|
660
723
|
log(
|
|
661
724
|
"debug",
|
|
662
725
|
`Skipped account ${account.addressHex} - parser returned null (dataLen=${account.data.length})`
|
|
@@ -669,6 +732,7 @@ async function runAccountStreamProcessor(stream, options, abortSignal) {
|
|
|
669
732
|
if (validateParse) {
|
|
670
733
|
const validation = validateParsedData(stream.schema, parsed, stream.name);
|
|
671
734
|
if (!validation.success) {
|
|
735
|
+
observer?.onParseValidationError?.(validation.error);
|
|
672
736
|
log("error", validation.error);
|
|
673
737
|
continue;
|
|
674
738
|
}
|
|
@@ -691,10 +755,12 @@ async function runAccountStreamProcessor(stream, options, abortSignal) {
|
|
|
691
755
|
);
|
|
692
756
|
}
|
|
693
757
|
} catch (err) {
|
|
758
|
+
observer?.onError?.("commit", err);
|
|
694
759
|
log(
|
|
695
760
|
"error",
|
|
696
761
|
`Failed to upsert account ${account.addressHex}: ${err}`
|
|
697
762
|
);
|
|
763
|
+
throw err;
|
|
698
764
|
}
|
|
699
765
|
if (upserted && account.slot > lastProcessedSlot) {
|
|
700
766
|
lastProcessedSlot = account.slot;
|
|
@@ -709,6 +775,7 @@ async function runAccountStreamProcessor(stream, options, abortSignal) {
|
|
|
709
775
|
const slot = event.block.slot;
|
|
710
776
|
if (lastProcessedSlot > 0n) {
|
|
711
777
|
await updateCheckpoint(db, checkpointName, lastProcessedSlot, null);
|
|
778
|
+
observer?.onCheckpoint?.({ slot: lastProcessedSlot });
|
|
712
779
|
log(
|
|
713
780
|
"debug",
|
|
714
781
|
`Block finished: slot ${slot}, checkpoint saved at account slot ${lastProcessedSlot}`
|
|
@@ -723,6 +790,7 @@ async function runAccountStreamProcessor(stream, options, abortSignal) {
|
|
|
723
790
|
}
|
|
724
791
|
if (lastProcessedSlot > 0n) {
|
|
725
792
|
await updateCheckpoint(db, checkpointName, lastProcessedSlot, null);
|
|
793
|
+
observer?.onCheckpoint?.({ slot: lastProcessedSlot });
|
|
726
794
|
log("info", `Final checkpoint saved: slot ${lastProcessedSlot}`);
|
|
727
795
|
}
|
|
728
796
|
} catch (err) {
|
|
@@ -742,19 +810,83 @@ async function runAccountStreamProcessor(stream, options, abortSignal) {
|
|
|
742
810
|
);
|
|
743
811
|
return stats;
|
|
744
812
|
}
|
|
813
|
+
|
|
814
|
+
// src/runtime/status.ts
|
|
815
|
+
function emptyStreamCounters() {
|
|
816
|
+
return {
|
|
817
|
+
eventsReceived: 0,
|
|
818
|
+
parserNulls: 0,
|
|
819
|
+
parserErrors: 0,
|
|
820
|
+
parseValidationErrors: 0,
|
|
821
|
+
commitErrors: 0,
|
|
822
|
+
filterBatchErrors: 0,
|
|
823
|
+
onCommitErrors: 0,
|
|
824
|
+
recordsProcessed: 0,
|
|
825
|
+
batchesCommitted: 0
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
function cloneStreamStatus(status) {
|
|
829
|
+
return {
|
|
830
|
+
...status,
|
|
831
|
+
lastError: status.lastError ? { ...status.lastError } : null,
|
|
832
|
+
counters: { ...status.counters }
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
function normalizeIndexerError(input) {
|
|
836
|
+
const err = input.error;
|
|
837
|
+
const maybeRecord = err && typeof err === "object" ? err : {};
|
|
838
|
+
const name = err instanceof Error ? err.name : typeof maybeRecord.name === "string" ? maybeRecord.name : "Error";
|
|
839
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
840
|
+
const code = typeof maybeRecord.code === "string" || typeof maybeRecord.code === "number" ? maybeRecord.code : void 0;
|
|
841
|
+
const retryable = isRetryablePhase(input.phase);
|
|
842
|
+
return {
|
|
843
|
+
name,
|
|
844
|
+
message,
|
|
845
|
+
code,
|
|
846
|
+
phase: input.phase,
|
|
847
|
+
retryable,
|
|
848
|
+
streamName: input.streamName,
|
|
849
|
+
streamKind: input.streamKind,
|
|
850
|
+
startSlot: input.startSlot === void 0 || input.startSlot === null ? void 0 : input.startSlot.toString(),
|
|
851
|
+
checkpointSlot: input.checkpointSlot === void 0 || input.checkpointSlot === null ? void 0 : input.checkpointSlot.toString(),
|
|
852
|
+
endpointLabel: input.endpointLabel
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
function isRetryablePhase(phase) {
|
|
856
|
+
switch (phase) {
|
|
857
|
+
case "starting":
|
|
858
|
+
case "backfill":
|
|
859
|
+
case "live":
|
|
860
|
+
case "commit":
|
|
861
|
+
case "supervisor":
|
|
862
|
+
return true;
|
|
863
|
+
case "parse":
|
|
864
|
+
case "filterBatch":
|
|
865
|
+
case "onCommit":
|
|
866
|
+
return false;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// src/runtime/indexer.ts
|
|
745
871
|
var Indexer = class {
|
|
746
872
|
config;
|
|
747
873
|
abortController = null;
|
|
748
874
|
running = false;
|
|
749
875
|
shutdownRequested = false;
|
|
876
|
+
startedAtMs = null;
|
|
877
|
+
streamStatuses = /* @__PURE__ */ new Map();
|
|
750
878
|
constructor(config) {
|
|
751
879
|
this.config = {
|
|
752
880
|
defaultStartSlot: 0n,
|
|
753
881
|
safetyMargin: 64,
|
|
754
882
|
pageSize: 512,
|
|
755
883
|
logLevel: "info",
|
|
884
|
+
supervisorInitialBackoffMs: 1e3,
|
|
885
|
+
supervisorMaxBackoffMs: 3e4,
|
|
886
|
+
streamStaleMs: 3e5,
|
|
756
887
|
...config
|
|
757
888
|
};
|
|
889
|
+
this.initializeStreamStatuses();
|
|
758
890
|
}
|
|
759
891
|
/**
|
|
760
892
|
* Check if the checkpoint table exists in the database.
|
|
@@ -797,7 +929,9 @@ var Indexer = class {
|
|
|
797
929
|
await this.checkCheckpointTable();
|
|
798
930
|
this.running = true;
|
|
799
931
|
this.shutdownRequested = false;
|
|
932
|
+
this.startedAtMs = Date.now();
|
|
800
933
|
this.abortController = new AbortController();
|
|
934
|
+
this.initializeStreamStatuses();
|
|
801
935
|
const {
|
|
802
936
|
db,
|
|
803
937
|
clientFactory,
|
|
@@ -807,7 +941,10 @@ var Indexer = class {
|
|
|
807
941
|
safetyMargin,
|
|
808
942
|
pageSize,
|
|
809
943
|
logLevel,
|
|
810
|
-
validateParse
|
|
944
|
+
validateParse,
|
|
945
|
+
endpointLabel,
|
|
946
|
+
supervisorInitialBackoffMs = 1e3,
|
|
947
|
+
supervisorMaxBackoffMs = 3e4
|
|
811
948
|
} = this.config;
|
|
812
949
|
console.log("[indexer] Starting indexer...");
|
|
813
950
|
console.log(
|
|
@@ -817,92 +954,41 @@ var Indexer = class {
|
|
|
817
954
|
`[indexer] Running ${accountStreams.length} account stream(s): ${accountStreams.map((s) => s.name).join(", ") || "none"}`
|
|
818
955
|
);
|
|
819
956
|
try {
|
|
820
|
-
const
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
safetyMargin,
|
|
825
|
-
pageSize,
|
|
826
|
-
logLevel,
|
|
827
|
-
validateParse
|
|
828
|
-
};
|
|
829
|
-
const accountProcessorOptions = {
|
|
830
|
-
clientFactory,
|
|
831
|
-
db,
|
|
832
|
-
logLevel,
|
|
833
|
-
validateParse
|
|
957
|
+
const supervisorOptions = {
|
|
958
|
+
endpointLabel,
|
|
959
|
+
initialBackoffMs: supervisorInitialBackoffMs,
|
|
960
|
+
maxBackoffMs: supervisorMaxBackoffMs
|
|
834
961
|
};
|
|
835
|
-
const
|
|
836
|
-
(stream) =>
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
962
|
+
const eventSupervisors = eventStreams.map(
|
|
963
|
+
(stream) => this.runEventStreamSupervisor(stream, {
|
|
964
|
+
clientFactory,
|
|
965
|
+
db,
|
|
966
|
+
defaultStartSlot,
|
|
967
|
+
safetyMargin,
|
|
968
|
+
pageSize,
|
|
969
|
+
logLevel,
|
|
970
|
+
validateParse
|
|
971
|
+
}, supervisorOptions)
|
|
841
972
|
);
|
|
842
|
-
const
|
|
843
|
-
(stream) =>
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
973
|
+
const accountSupervisors = accountStreams.map(
|
|
974
|
+
(stream) => this.runAccountStreamSupervisor(stream, {
|
|
975
|
+
clientFactory,
|
|
976
|
+
db,
|
|
977
|
+
logLevel,
|
|
978
|
+
validateParse
|
|
979
|
+
}, supervisorOptions)
|
|
848
980
|
);
|
|
849
|
-
|
|
850
|
-
Promise.allSettled(eventStreamPromises),
|
|
851
|
-
Promise.allSettled(accountStreamPromises)
|
|
852
|
-
]);
|
|
981
|
+
await Promise.all([...eventSupervisors, ...accountSupervisors]);
|
|
853
982
|
const result = {
|
|
854
|
-
eventStreams: eventStreams.map((stream
|
|
855
|
-
|
|
856
|
-
if (r.status === "fulfilled") {
|
|
857
|
-
console.log(
|
|
858
|
-
`[indexer] Event stream "${stream.name}" completed: ${r.value.eventsProcessed} events in ${r.value.batchesCommitted} batches`
|
|
859
|
-
);
|
|
860
|
-
return {
|
|
861
|
-
name: stream.name,
|
|
862
|
-
status: "fulfilled",
|
|
863
|
-
result: r.value
|
|
864
|
-
};
|
|
865
|
-
} else {
|
|
866
|
-
console.error(
|
|
867
|
-
`[indexer] Event stream "${stream.name}" failed:`,
|
|
868
|
-
r.reason
|
|
869
|
-
);
|
|
870
|
-
return {
|
|
871
|
-
name: stream.name,
|
|
872
|
-
status: "rejected",
|
|
873
|
-
error: r.reason
|
|
874
|
-
};
|
|
875
|
-
}
|
|
876
|
-
}),
|
|
877
|
-
accountStreams: accountStreams.map((stream, i) => {
|
|
878
|
-
const r = accountResults[i];
|
|
879
|
-
if (r.status === "fulfilled") {
|
|
880
|
-
console.log(
|
|
881
|
-
`[indexer] Account stream "${stream.name}" completed: ${r.value.accountsUpdated} accounts updated, ${r.value.accountsDeleted} deleted`
|
|
882
|
-
);
|
|
883
|
-
return {
|
|
884
|
-
name: stream.name,
|
|
885
|
-
status: "fulfilled",
|
|
886
|
-
result: r.value
|
|
887
|
-
};
|
|
888
|
-
} else {
|
|
889
|
-
console.error(
|
|
890
|
-
`[indexer] Account stream "${stream.name}" failed:`,
|
|
891
|
-
r.reason
|
|
892
|
-
);
|
|
893
|
-
return {
|
|
894
|
-
name: stream.name,
|
|
895
|
-
status: "rejected",
|
|
896
|
-
error: r.reason
|
|
897
|
-
};
|
|
898
|
-
}
|
|
899
|
-
})
|
|
983
|
+
eventStreams: eventStreams.map((stream) => this.resultForStream(stream.name)),
|
|
984
|
+
accountStreams: accountStreams.map((stream) => this.resultForStream(stream.name))
|
|
900
985
|
};
|
|
901
986
|
console.log("[indexer] All streams stopped.");
|
|
902
987
|
return result;
|
|
903
988
|
} finally {
|
|
904
989
|
this.running = false;
|
|
905
990
|
this.abortController = null;
|
|
991
|
+
this.startedAtMs = null;
|
|
906
992
|
}
|
|
907
993
|
}
|
|
908
994
|
/**
|
|
@@ -930,6 +1016,219 @@ var Indexer = class {
|
|
|
930
1016
|
isRunning() {
|
|
931
1017
|
return this.running;
|
|
932
1018
|
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Get the current in-memory runtime status for every configured stream.
|
|
1021
|
+
*/
|
|
1022
|
+
getStatus() {
|
|
1023
|
+
const now = Date.now();
|
|
1024
|
+
const streams = Array.from(this.streamStatuses.values()).map((status) => {
|
|
1025
|
+
const stream = cloneStreamStatus(status);
|
|
1026
|
+
stream.stale = this.isStreamStale(stream, now);
|
|
1027
|
+
return stream;
|
|
1028
|
+
});
|
|
1029
|
+
const healthy = this.running && !this.shutdownRequested && streams.length > 0 && streams.every((stream) => stream.state === "running" && !stream.stale);
|
|
1030
|
+
return {
|
|
1031
|
+
running: this.running,
|
|
1032
|
+
shutdownRequested: this.shutdownRequested,
|
|
1033
|
+
startedAt: this.startedAtMs === null ? null : new Date(this.startedAtMs).toISOString(),
|
|
1034
|
+
uptimeMs: this.startedAtMs === null ? 0 : now - this.startedAtMs,
|
|
1035
|
+
healthy,
|
|
1036
|
+
streams
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
initializeStreamStatuses() {
|
|
1040
|
+
this.streamStatuses = /* @__PURE__ */ new Map();
|
|
1041
|
+
for (const stream of this.config.eventStreams ?? []) {
|
|
1042
|
+
this.streamStatuses.set(this.statusKey("event", stream.name), this.createInitialStreamStatus("event", stream.name));
|
|
1043
|
+
}
|
|
1044
|
+
for (const stream of this.config.accountStreams ?? []) {
|
|
1045
|
+
this.streamStatuses.set(this.statusKey("account", stream.name), this.createInitialStreamStatus("account", stream.name));
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
createInitialStreamStatus(kind, name) {
|
|
1049
|
+
return {
|
|
1050
|
+
name,
|
|
1051
|
+
kind,
|
|
1052
|
+
state: "idle",
|
|
1053
|
+
checkpointSlot: null,
|
|
1054
|
+
lastProcessedSlot: null,
|
|
1055
|
+
lastEventAt: null,
|
|
1056
|
+
stale: false,
|
|
1057
|
+
restartCount: 0,
|
|
1058
|
+
lastStartedAt: null,
|
|
1059
|
+
lastErrorAt: null,
|
|
1060
|
+
lastError: null,
|
|
1061
|
+
counters: emptyStreamCounters()
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
statusKey(kind, name) {
|
|
1065
|
+
return `${kind}:${name}`;
|
|
1066
|
+
}
|
|
1067
|
+
statusFor(kind, name) {
|
|
1068
|
+
const key = this.statusKey(kind, name);
|
|
1069
|
+
let status = this.streamStatuses.get(key);
|
|
1070
|
+
if (!status) {
|
|
1071
|
+
status = this.createInitialStreamStatus(kind, name);
|
|
1072
|
+
this.streamStatuses.set(key, status);
|
|
1073
|
+
}
|
|
1074
|
+
return status;
|
|
1075
|
+
}
|
|
1076
|
+
resultForStream(name) {
|
|
1077
|
+
return {
|
|
1078
|
+
name,
|
|
1079
|
+
status: "fulfilled"
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
createObserver(kind, name, endpointLabel) {
|
|
1083
|
+
const status = this.statusFor(kind, name);
|
|
1084
|
+
let startSlot = null;
|
|
1085
|
+
let checkpointSlot = null;
|
|
1086
|
+
return {
|
|
1087
|
+
onStart: (info) => {
|
|
1088
|
+
startSlot = info.startSlot ?? null;
|
|
1089
|
+
checkpointSlot = info.checkpointSlot ?? null;
|
|
1090
|
+
status.state = "running";
|
|
1091
|
+
status.checkpointSlot = checkpointSlot === null ? null : checkpointSlot.toString();
|
|
1092
|
+
status.lastStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1093
|
+
},
|
|
1094
|
+
onRecord: (info) => {
|
|
1095
|
+
status.counters.eventsReceived++;
|
|
1096
|
+
status.lastEventAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1097
|
+
if (info.slot !== void 0 && info.slot !== null) {
|
|
1098
|
+
status.lastProcessedSlot = info.slot.toString();
|
|
1099
|
+
}
|
|
1100
|
+
},
|
|
1101
|
+
onParserNull: () => {
|
|
1102
|
+
status.counters.parserNulls++;
|
|
1103
|
+
},
|
|
1104
|
+
onParserError: () => {
|
|
1105
|
+
status.counters.parserErrors++;
|
|
1106
|
+
},
|
|
1107
|
+
onParseValidationError: () => {
|
|
1108
|
+
status.counters.parseValidationErrors++;
|
|
1109
|
+
},
|
|
1110
|
+
onBatchCommitted: (info) => {
|
|
1111
|
+
status.counters.batchesCommitted++;
|
|
1112
|
+
status.counters.recordsProcessed += info.count;
|
|
1113
|
+
status.lastProcessedSlot = info.slot.toString();
|
|
1114
|
+
status.lastEventAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1115
|
+
},
|
|
1116
|
+
onCheckpoint: (info) => {
|
|
1117
|
+
status.checkpointSlot = info.slot.toString();
|
|
1118
|
+
status.lastProcessedSlot = info.slot.toString();
|
|
1119
|
+
},
|
|
1120
|
+
onError: (phase, error) => {
|
|
1121
|
+
if (phase === "commit") status.counters.commitErrors++;
|
|
1122
|
+
if (phase === "filterBatch") status.counters.filterBatchErrors++;
|
|
1123
|
+
if (phase === "onCommit") status.counters.onCommitErrors++;
|
|
1124
|
+
status.lastErrorAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1125
|
+
status.lastError = normalizeIndexerError({
|
|
1126
|
+
error,
|
|
1127
|
+
phase,
|
|
1128
|
+
streamName: name,
|
|
1129
|
+
streamKind: kind,
|
|
1130
|
+
startSlot,
|
|
1131
|
+
checkpointSlot,
|
|
1132
|
+
endpointLabel
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
async runEventStreamSupervisor(stream, processorOptions, supervisorOptions) {
|
|
1138
|
+
await this.runStreamSupervisor("event", stream.name, supervisorOptions, async (observer) => {
|
|
1139
|
+
const result = await runEventStreamProcessor(
|
|
1140
|
+
stream,
|
|
1141
|
+
{ ...processorOptions, observer },
|
|
1142
|
+
this.abortController.signal
|
|
1143
|
+
);
|
|
1144
|
+
return `processed ${result.eventsProcessed} event(s) in ${result.batchesCommitted} batch(es)`;
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
async runAccountStreamSupervisor(stream, processorOptions, supervisorOptions) {
|
|
1148
|
+
await this.runStreamSupervisor("account", stream.name, supervisorOptions, async (observer) => {
|
|
1149
|
+
const result = await runAccountStreamProcessor(
|
|
1150
|
+
stream,
|
|
1151
|
+
{ ...processorOptions, observer },
|
|
1152
|
+
this.abortController.signal
|
|
1153
|
+
);
|
|
1154
|
+
return `processed ${result.accountsProcessed} account event(s), updated ${result.accountsUpdated}, deleted ${result.accountsDeleted}`;
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
async runStreamSupervisor(kind, name, options, runOnce) {
|
|
1158
|
+
let attempt = 0;
|
|
1159
|
+
const status = this.statusFor(kind, name);
|
|
1160
|
+
while (!this.abortController?.signal.aborted) {
|
|
1161
|
+
const observer = this.createObserver(kind, name, options.endpointLabel);
|
|
1162
|
+
status.state = attempt === 0 ? "starting" : "retrying";
|
|
1163
|
+
status.lastStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1164
|
+
try {
|
|
1165
|
+
const summary = await runOnce(observer);
|
|
1166
|
+
if (this.abortController?.signal.aborted) {
|
|
1167
|
+
status.state = "stopped";
|
|
1168
|
+
console.log(`[indexer] ${kind} stream "${name}" stopped: ${summary}`);
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
throw new Error(`${kind} stream "${name}" completed unexpectedly: ${summary}`);
|
|
1172
|
+
} catch (error) {
|
|
1173
|
+
if (this.abortController?.signal.aborted) {
|
|
1174
|
+
status.state = "stopped";
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
status.restartCount++;
|
|
1178
|
+
status.state = "retrying";
|
|
1179
|
+
status.lastErrorAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1180
|
+
if (!status.lastError || status.lastError.phase === "supervisor") {
|
|
1181
|
+
status.lastError = normalizeIndexerError({
|
|
1182
|
+
error,
|
|
1183
|
+
phase: "supervisor",
|
|
1184
|
+
streamName: name,
|
|
1185
|
+
streamKind: kind,
|
|
1186
|
+
endpointLabel: options.endpointLabel
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
const backoffMs = this.supervisorBackoffMs(attempt, options.initialBackoffMs, options.maxBackoffMs);
|
|
1190
|
+
console.error(
|
|
1191
|
+
`[indexer] ${kind} stream "${name}" failed; restarting in ${backoffMs}ms:`,
|
|
1192
|
+
error
|
|
1193
|
+
);
|
|
1194
|
+
attempt++;
|
|
1195
|
+
await this.delay(backoffMs, this.abortController.signal);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
status.state = "stopped";
|
|
1199
|
+
}
|
|
1200
|
+
supervisorBackoffMs(attempt, initialMs, maxMs) {
|
|
1201
|
+
const base = Math.min(maxMs, initialMs * Math.pow(2, attempt));
|
|
1202
|
+
const jitter = Math.floor(base * 0.2 * Math.random());
|
|
1203
|
+
return base + jitter;
|
|
1204
|
+
}
|
|
1205
|
+
isStreamStale(stream, nowMs) {
|
|
1206
|
+
const staleMs = this.config.streamStaleMs ?? 3e5;
|
|
1207
|
+
if (staleMs <= 0 || stream.state !== "running") {
|
|
1208
|
+
return false;
|
|
1209
|
+
}
|
|
1210
|
+
const activityAt = stream.lastEventAt ?? stream.lastStartedAt;
|
|
1211
|
+
if (!activityAt) {
|
|
1212
|
+
return true;
|
|
1213
|
+
}
|
|
1214
|
+
const activityMs = Date.parse(activityAt);
|
|
1215
|
+
return !Number.isFinite(activityMs) || nowMs - activityMs > staleMs;
|
|
1216
|
+
}
|
|
1217
|
+
delay(ms, signal) {
|
|
1218
|
+
if (ms <= 0 || signal.aborted) return Promise.resolve();
|
|
1219
|
+
return new Promise((resolve) => {
|
|
1220
|
+
const onAbort = () => {
|
|
1221
|
+
clearTimeout(timer);
|
|
1222
|
+
resolve();
|
|
1223
|
+
};
|
|
1224
|
+
const timer = setTimeout(() => {
|
|
1225
|
+
signal.removeEventListener("abort", onAbort);
|
|
1226
|
+
resolve();
|
|
1227
|
+
}, ms);
|
|
1228
|
+
timer.unref?.();
|
|
1229
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
933
1232
|
};
|
|
934
1233
|
|
|
935
1234
|
exports.Indexer = Indexer;
|