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