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