@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.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
- return;
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
- await db.transaction(async (tx) => {
437
- committedEvents = await tx.insert(stream.table).values(eventsToCommit).onConflictDoNothing().returning();
438
- await updateCheckpoint(
439
- tx,
440
- stream.name,
441
- batch.slot,
442
- lastEventToCommitId
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
- try {
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
- const parsed = stream.parse(event);
483
- if (!parsed) continue;
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
- const parsed = stream.parse(account);
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 eventProcessorOptions = {
819
- clientFactory,
820
- db,
821
- defaultStartSlot,
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 eventStreamPromises = eventStreams.map(
834
- (stream) => runEventStreamProcessor(
835
- stream,
836
- eventProcessorOptions,
837
- this.abortController.signal
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 accountStreamPromises = accountStreams.map(
841
- (stream) => runAccountStreamProcessor(
842
- stream,
843
- accountProcessorOptions,
844
- this.abortController.signal
845
- )
971
+ const accountSupervisors = accountStreams.map(
972
+ (stream) => this.runAccountStreamSupervisor(stream, {
973
+ clientFactory,
974
+ db,
975
+ logLevel,
976
+ validateParse
977
+ }, supervisorOptions)
846
978
  );
847
- const [eventResults, accountResults] = await Promise.all([
848
- Promise.allSettled(eventStreamPromises),
849
- Promise.allSettled(accountStreamPromises)
850
- ]);
979
+ await Promise.all([...eventSupervisors, ...accountSupervisors]);
851
980
  const result = {
852
- eventStreams: eventStreams.map((stream, i) => {
853
- const r = eventResults[i];
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 };