@thru/indexer 0.2.31 → 0.2.33

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.d.ts CHANGED
@@ -2,7 +2,7 @@ import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
2
2
  import { PgTableWithColumns, PgColumn } from 'drizzle-orm/pg-core';
3
3
  import { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
4
4
  import { z } from 'zod';
5
- import { Filter, Event, AccountState, ChainClientFactory } from '@thru/replay';
5
+ import { Filter, Event, AccountState, ChainClientFactory, ReplayLogger } from '@thru/replay';
6
6
 
7
7
  /**
8
8
  * Schema type definitions with improved type safety.
@@ -817,6 +817,8 @@ interface IndexerConfig {
817
817
  pageSize?: number;
818
818
  /** Log level (default: "info") */
819
819
  logLevel?: "debug" | "info" | "warn" | "error";
820
+ /** Optional structured logger for runtime, processor, and replay lifecycle logs. */
821
+ logger?: ReplayLogger;
820
822
  /** Human-readable endpoint label included in normalized stream errors */
821
823
  endpointLabel?: string;
822
824
  /** Initial supervisor restart backoff in milliseconds (default: 1000) */
@@ -886,11 +888,13 @@ interface IndexerResult {
886
888
  */
887
889
  declare class Indexer {
888
890
  private config;
891
+ private logger;
889
892
  private abortController;
890
893
  private running;
891
894
  private shutdownRequested;
892
895
  private startedAtMs;
893
896
  private streamStatuses;
897
+ private streamHealthStates;
894
898
  constructor(config: IndexerConfig);
895
899
  /**
896
900
  * Check if the checkpoint table exists in the database.
@@ -926,6 +930,9 @@ declare class Indexer {
926
930
  private statusKey;
927
931
  private statusFor;
928
932
  private resultForStream;
933
+ private setStreamState;
934
+ private streamLogFields;
935
+ private emitHealthTransitions;
929
936
  private createObserver;
930
937
  private runEventStreamSupervisor;
931
938
  private runAccountStreamSupervisor;
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { pgTable, timestamp, text, bigint, boolean, integer, index } from 'drizzle-orm/pg-core';
2
2
  import { z } from 'zod';
3
- import { createConsoleLogger, createEventReplay, createAccountsByOwnerReplay, AccountView } from '@thru/replay';
3
+ import { createEventReplay, createAccountsByOwnerReplay, AccountView } from '@thru/replay';
4
4
  import { eq, sql } from 'drizzle-orm';
5
5
  import { encodeAddress } from '@thru/sdk/helpers';
6
6
 
@@ -299,6 +299,54 @@ function getSchemaExports(config) {
299
299
  return exports;
300
300
  }
301
301
 
302
+ // src/runtime/logger.ts
303
+ var LEVELS = ["debug", "info", "warn", "error"];
304
+ function shouldLog(level, minimum) {
305
+ return LEVELS.indexOf(level) >= LEVELS.indexOf(minimum);
306
+ }
307
+ function writeConsole(prefix, level, message, meta) {
308
+ const text3 = `[${prefix}] ${message}`;
309
+ if (meta && Object.keys(meta).length > 0) {
310
+ if (level === "error") {
311
+ console.error(text3, meta);
312
+ } else if (level === "warn") {
313
+ console.warn(text3, meta);
314
+ } else {
315
+ console.log(text3, meta);
316
+ }
317
+ return;
318
+ }
319
+ if (level === "error") {
320
+ console.error(text3);
321
+ } else if (level === "warn") {
322
+ console.warn(text3);
323
+ } else {
324
+ console.log(text3);
325
+ }
326
+ }
327
+ function createScopedLogger(options) {
328
+ const minimum = options.level ?? "info";
329
+ const bindings = options.bindings ?? {};
330
+ const log = (level, message, meta) => {
331
+ if (!shouldLog(level, minimum)) {
332
+ return;
333
+ }
334
+ const hasMeta = meta !== void 0 && Object.keys(meta).length > 0;
335
+ const fields = { ...bindings, ...meta ?? {} };
336
+ if (options.logger) {
337
+ options.logger[level](message, fields);
338
+ } else {
339
+ writeConsole(options.prefix, level, message, hasMeta ? fields : void 0);
340
+ }
341
+ };
342
+ return {
343
+ debug: (message, meta) => log("debug", message, meta),
344
+ info: (message, meta) => log("info", message, meta),
345
+ warn: (message, meta) => log("warn", message, meta),
346
+ error: (message, meta) => log("error", message, meta)
347
+ };
348
+ }
349
+
302
350
  // src/streams/processor.ts
303
351
  var StreamBatcher = class {
304
352
  currentSlot = null;
@@ -358,14 +406,21 @@ async function runEventStreamProcessor(stream, options, abortSignal) {
358
406
  safetyMargin = 64,
359
407
  pageSize = 512,
360
408
  logLevel = "info",
409
+ logger: baseLogger,
361
410
  validateParse = false,
362
411
  observer
363
412
  } = options;
364
- const log = (level, msg) => {
365
- if (logLevel === "debug" || level !== "debug") {
366
- console.log(`[${stream.name}] ${msg}`);
413
+ const logger = createScopedLogger({
414
+ logger: baseLogger,
415
+ level: logLevel,
416
+ prefix: stream.name,
417
+ bindings: {
418
+ component: "indexer-stream",
419
+ stream: stream.name,
420
+ kind: "event"
367
421
  }
368
- };
422
+ });
423
+ const log = (level, msg, meta) => logger[level](msg, meta);
369
424
  log("info", `Starting stream processor: ${stream.description}`);
370
425
  const checkpoint = await getCheckpoint(db, stream.name);
371
426
  const startSlot = checkpoint ? checkpoint.slot : defaultStartSlot;
@@ -377,13 +432,6 @@ async function runEventStreamProcessor(stream, options, abortSignal) {
377
432
  "info",
378
433
  `Starting from slot ${startSlot}${checkpoint ? " (resuming)" : " (fresh start)"}`
379
434
  );
380
- const logger = logLevel === "debug" ? createConsoleLogger(stream.name) : {
381
- debug: () => {
382
- },
383
- info: (msg) => log("info", msg),
384
- warn: (msg) => log("warn", msg),
385
- error: (msg) => log("error", msg)
386
- };
387
435
  const replay = createEventReplay({
388
436
  clientFactory,
389
437
  startSlot,
@@ -392,7 +440,8 @@ async function runEventStreamProcessor(stream, options, abortSignal) {
392
440
  pageSize,
393
441
  filter: stream.getFilter(),
394
442
  logger,
395
- resubscribeOnEnd: true
443
+ resubscribeOnEnd: true,
444
+ signal: abortSignal
396
445
  });
397
446
  const batcher = new StreamBatcher();
398
447
  const stats = {
@@ -610,20 +659,21 @@ function defineAccountStream(definition) {
610
659
  api: definition.api
611
660
  };
612
661
  }
613
- function shouldLog(level, minLevel) {
614
- const levels = ["debug", "info", "warn", "error"];
615
- return levels.indexOf(level) >= levels.indexOf(minLevel);
616
- }
617
662
  async function runAccountStreamProcessor(stream, options, abortSignal) {
618
- const { clientFactory, db, logLevel = "info", validateParse = false, observer } = options;
663
+ const { clientFactory, db, logLevel = "info", logger: baseLogger, validateParse = false, observer } = options;
619
664
  const checkpointName = `account:${stream.name}`;
620
- const log = (level, msg, meta) => {
621
- if (shouldLog(level, logLevel)) {
622
- {
623
- console.log(`[account-stream:${stream.name}] ${msg}`);
624
- }
665
+ const logger = createScopedLogger({
666
+ logger: baseLogger,
667
+ level: logLevel,
668
+ prefix: `account-stream:${stream.name}`,
669
+ bindings: {
670
+ component: "indexer-stream",
671
+ stream: stream.name,
672
+ kind: "account",
673
+ checkpoint_name: checkpointName
625
674
  }
626
- };
675
+ });
676
+ const log = (level, msg, meta) => logger[level](msg, meta);
627
677
  const stats = {
628
678
  accountsProcessed: 0,
629
679
  accountsUpdated: 0,
@@ -642,18 +692,6 @@ async function runAccountStreamProcessor(stream, options, abortSignal) {
642
692
  if (stream.expectedSize) {
643
693
  log("info", `Expected data size: ${stream.expectedSize} bytes`);
644
694
  }
645
- const replayLogger = logLevel === "debug" ? {
646
- debug: (msg) => log("debug", msg),
647
- info: (msg) => log("info", msg),
648
- warn: (msg) => log("warn", msg),
649
- error: (msg) => log("error", msg)
650
- } : {
651
- debug: () => {
652
- },
653
- info: (msg) => log("info", msg),
654
- warn: (msg) => log("warn", msg),
655
- error: (msg) => log("error", msg)
656
- };
657
695
  let lastProcessedSlot = minUpdatedSlot ?? 0n;
658
696
  try {
659
697
  const replay = createAccountsByOwnerReplay({
@@ -662,7 +700,8 @@ async function runAccountStreamProcessor(stream, options, abortSignal) {
662
700
  view: AccountView.FULL,
663
701
  dataSizes: stream.dataSizes ?? (stream.expectedSize ? [stream.expectedSize] : void 0),
664
702
  minUpdatedSlot,
665
- logger: replayLogger,
703
+ logger,
704
+ signal: abortSignal,
666
705
  onBackfillComplete: (highestSlot) => {
667
706
  log(
668
707
  "info",
@@ -868,11 +907,13 @@ function isRetryablePhase(phase) {
868
907
  // src/runtime/indexer.ts
869
908
  var Indexer = class {
870
909
  config;
910
+ logger;
871
911
  abortController = null;
872
912
  running = false;
873
913
  shutdownRequested = false;
874
914
  startedAtMs = null;
875
915
  streamStatuses = /* @__PURE__ */ new Map();
916
+ streamHealthStates = /* @__PURE__ */ new Map();
876
917
  constructor(config) {
877
918
  this.config = {
878
919
  defaultStartSlot: 0n,
@@ -884,6 +925,12 @@ var Indexer = class {
884
925
  streamStaleMs: 3e5,
885
926
  ...config
886
927
  };
928
+ this.logger = createScopedLogger({
929
+ logger: this.config.logger,
930
+ level: this.config.logLevel,
931
+ prefix: "indexer",
932
+ bindings: { component: "indexer-runtime" }
933
+ });
887
934
  this.initializeStreamStatuses();
888
935
  }
889
936
  /**
@@ -898,14 +945,14 @@ var Indexer = class {
898
945
  } catch (err) {
899
946
  const message = err instanceof Error ? err.cause instanceof Error ? err.cause.message : err.message : String(err);
900
947
  if (message.includes("does not exist") || message.includes("relation")) {
901
- console.warn(
902
- `[indexer] WARNING: Checkpoint table "indexer_checkpoints" not found.
903
- [indexer] Make sure to export checkpointTable from your Drizzle schema:
948
+ this.logger.warn(
949
+ `Checkpoint table "indexer_checkpoints" not found.
950
+ Make sure to export checkpointTable from your Drizzle schema:
904
951
 
905
952
  // db/schema.ts
906
953
  export { checkpointTable } from "@thru/indexer";
907
954
 
908
- [indexer] Then run: pnpm drizzle-kit push (or generate + migrate)
955
+ Then run: pnpm drizzle-kit push (or generate + migrate)
909
956
  `
910
957
  );
911
958
  }
@@ -942,15 +989,14 @@ var Indexer = class {
942
989
  validateParse,
943
990
  endpointLabel,
944
991
  supervisorInitialBackoffMs = 1e3,
945
- supervisorMaxBackoffMs = 3e4
992
+ supervisorMaxBackoffMs = 3e4,
993
+ logger
946
994
  } = this.config;
947
- console.log("[indexer] Starting indexer...");
948
- console.log(
949
- `[indexer] Running ${eventStreams.length} event stream(s): ${eventStreams.map((s) => s.name).join(", ") || "none"}`
950
- );
951
- console.log(
952
- `[indexer] Running ${accountStreams.length} account stream(s): ${accountStreams.map((s) => s.name).join(", ") || "none"}`
953
- );
995
+ this.logger.info("Starting indexer", {
996
+ event: "indexer.started",
997
+ event_streams: eventStreams.map((stream) => stream.name),
998
+ account_streams: accountStreams.map((stream) => stream.name)
999
+ });
954
1000
  try {
955
1001
  const supervisorOptions = {
956
1002
  endpointLabel,
@@ -965,6 +1011,7 @@ var Indexer = class {
965
1011
  safetyMargin,
966
1012
  pageSize,
967
1013
  logLevel,
1014
+ logger,
968
1015
  validateParse
969
1016
  }, supervisorOptions)
970
1017
  );
@@ -973,6 +1020,7 @@ var Indexer = class {
973
1020
  clientFactory,
974
1021
  db,
975
1022
  logLevel,
1023
+ logger,
976
1024
  validateParse
977
1025
  }, supervisorOptions)
978
1026
  );
@@ -981,7 +1029,9 @@ var Indexer = class {
981
1029
  eventStreams: eventStreams.map((stream) => this.resultForStream(stream.name)),
982
1030
  accountStreams: accountStreams.map((stream) => this.resultForStream(stream.name))
983
1031
  };
984
- console.log("[indexer] All streams stopped.");
1032
+ this.logger.info("All indexer streams stopped", {
1033
+ event: "indexer.stopped"
1034
+ });
985
1035
  return result;
986
1036
  } finally {
987
1037
  this.running = false;
@@ -997,14 +1047,20 @@ var Indexer = class {
997
1047
  */
998
1048
  stop() {
999
1049
  if (!this.running || !this.abortController) {
1000
- console.log("[indexer] Not running");
1050
+ this.logger.info("Indexer is not running", {
1051
+ event: "indexer.stop.noop"
1052
+ });
1001
1053
  return;
1002
1054
  }
1003
1055
  if (this.shutdownRequested) {
1004
- console.log("[indexer] Force shutdown...");
1056
+ this.logger.warn("Force shutting down indexer", {
1057
+ event: "indexer.force_shutdown"
1058
+ });
1005
1059
  process.exit(1);
1006
1060
  }
1007
- console.log("[indexer] Shutdown requested, finishing current batches...");
1061
+ this.logger.info("Indexer shutdown requested", {
1062
+ event: "indexer.shutdown_requested"
1063
+ });
1008
1064
  this.shutdownRequested = true;
1009
1065
  this.abortController.abort();
1010
1066
  }
@@ -1025,6 +1081,7 @@ var Indexer = class {
1025
1081
  return stream;
1026
1082
  });
1027
1083
  const healthy = this.running && !this.shutdownRequested && streams.length > 0 && streams.every((stream) => stream.state === "running" && !stream.stale);
1084
+ this.emitHealthTransitions(streams, now);
1028
1085
  return {
1029
1086
  running: this.running,
1030
1087
  shutdownRequested: this.shutdownRequested,
@@ -1036,6 +1093,7 @@ var Indexer = class {
1036
1093
  }
1037
1094
  initializeStreamStatuses() {
1038
1095
  this.streamStatuses = /* @__PURE__ */ new Map();
1096
+ this.streamHealthStates = /* @__PURE__ */ new Map();
1039
1097
  for (const stream of this.config.eventStreams ?? []) {
1040
1098
  this.streamStatuses.set(this.statusKey("event", stream.name), this.createInitialStreamStatus("event", stream.name));
1041
1099
  }
@@ -1077,6 +1135,84 @@ var Indexer = class {
1077
1135
  status: "fulfilled"
1078
1136
  };
1079
1137
  }
1138
+ setStreamState(status, nextState, meta = {}) {
1139
+ const previousState = status.state;
1140
+ status.state = nextState;
1141
+ if (previousState === nextState) {
1142
+ return;
1143
+ }
1144
+ this.logger.info("Indexer stream state changed", {
1145
+ event: "indexer.stream.state_changed",
1146
+ stream: status.name,
1147
+ kind: status.kind,
1148
+ previous_state: previousState,
1149
+ next_state: nextState,
1150
+ restart_count: status.restartCount,
1151
+ ...meta
1152
+ });
1153
+ }
1154
+ streamLogFields(stream) {
1155
+ return {
1156
+ stream: stream.name,
1157
+ kind: stream.kind,
1158
+ state: stream.state,
1159
+ stale: stream.stale,
1160
+ checkpoint_slot: stream.checkpointSlot,
1161
+ last_processed_slot: stream.lastProcessedSlot,
1162
+ last_event_at: stream.lastEventAt,
1163
+ restart_count: stream.restartCount,
1164
+ last_error_at: stream.lastErrorAt,
1165
+ last_error: stream.lastError
1166
+ };
1167
+ }
1168
+ emitHealthTransitions(streams, nowMs) {
1169
+ if (!this.running || this.shutdownRequested) {
1170
+ return;
1171
+ }
1172
+ for (const stream of streams) {
1173
+ const key = this.statusKey(stream.kind, stream.name);
1174
+ const unhealthy = stream.state !== "running" || stream.stale;
1175
+ const previous = this.streamHealthStates.get(key);
1176
+ const initialStarting = !previous && stream.state === "starting" && stream.restartCount === 0 && !stream.lastError;
1177
+ if (initialStarting) {
1178
+ this.streamHealthStates.set(key, {
1179
+ unhealthy: false,
1180
+ unhealthySinceMs: null
1181
+ });
1182
+ continue;
1183
+ }
1184
+ if (unhealthy && previous?.unhealthy !== true) {
1185
+ this.streamHealthStates.set(key, {
1186
+ unhealthy: true,
1187
+ unhealthySinceMs: nowMs
1188
+ });
1189
+ this.logger.warn("Indexer stream unhealthy", {
1190
+ event: "indexer.stream.unhealthy",
1191
+ reason: stream.stale ? "stale" : "state",
1192
+ ...this.streamLogFields(stream)
1193
+ });
1194
+ continue;
1195
+ }
1196
+ if (!unhealthy && previous?.unhealthy === true) {
1197
+ this.streamHealthStates.set(key, {
1198
+ unhealthy: false,
1199
+ unhealthySinceMs: null
1200
+ });
1201
+ this.logger.info("Indexer stream recovered", {
1202
+ event: "indexer.stream.recovered",
1203
+ unhealthy_duration_ms: previous.unhealthySinceMs === null ? null : nowMs - previous.unhealthySinceMs,
1204
+ ...this.streamLogFields(stream)
1205
+ });
1206
+ continue;
1207
+ }
1208
+ if (!previous) {
1209
+ this.streamHealthStates.set(key, {
1210
+ unhealthy,
1211
+ unhealthySinceMs: unhealthy ? nowMs : null
1212
+ });
1213
+ }
1214
+ }
1215
+ }
1080
1216
  createObserver(kind, name, endpointLabel) {
1081
1217
  const status = this.statusFor(kind, name);
1082
1218
  let startSlot = null;
@@ -1085,7 +1221,10 @@ var Indexer = class {
1085
1221
  onStart: (info) => {
1086
1222
  startSlot = info.startSlot ?? null;
1087
1223
  checkpointSlot = info.checkpointSlot ?? null;
1088
- status.state = "running";
1224
+ this.setStreamState(status, "running", {
1225
+ start_slot: startSlot?.toString(),
1226
+ checkpoint_slot: checkpointSlot?.toString() ?? null
1227
+ });
1089
1228
  status.checkpointSlot = checkpointSlot === null ? null : checkpointSlot.toString();
1090
1229
  status.lastStartedAt = (/* @__PURE__ */ new Date()).toISOString();
1091
1230
  },
@@ -1157,23 +1296,26 @@ var Indexer = class {
1157
1296
  const status = this.statusFor(kind, name);
1158
1297
  while (!this.abortController?.signal.aborted) {
1159
1298
  const observer = this.createObserver(kind, name, options.endpointLabel);
1160
- status.state = attempt === 0 ? "starting" : "retrying";
1299
+ this.setStreamState(status, attempt === 0 ? "starting" : "retrying", {
1300
+ attempt
1301
+ });
1161
1302
  status.lastStartedAt = (/* @__PURE__ */ new Date()).toISOString();
1162
1303
  try {
1163
1304
  const summary = await runOnce(observer);
1164
1305
  if (this.abortController?.signal.aborted) {
1165
- status.state = "stopped";
1166
- console.log(`[indexer] ${kind} stream "${name}" stopped: ${summary}`);
1306
+ this.setStreamState(status, "stopped", { summary });
1167
1307
  return;
1168
1308
  }
1169
1309
  throw new Error(`${kind} stream "${name}" completed unexpectedly: ${summary}`);
1170
1310
  } catch (error) {
1171
1311
  if (this.abortController?.signal.aborted) {
1172
- status.state = "stopped";
1312
+ this.setStreamState(status, "stopped");
1173
1313
  return;
1174
1314
  }
1175
1315
  status.restartCount++;
1176
- status.state = "retrying";
1316
+ this.setStreamState(status, "retrying", {
1317
+ restart_count: status.restartCount
1318
+ });
1177
1319
  status.lastErrorAt = (/* @__PURE__ */ new Date()).toISOString();
1178
1320
  if (!status.lastError || status.lastError.phase === "supervisor") {
1179
1321
  status.lastError = normalizeIndexerError({
@@ -1185,15 +1327,21 @@ var Indexer = class {
1185
1327
  });
1186
1328
  }
1187
1329
  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
- );
1330
+ this.logger.warn("Indexer stream supervisor restarting", {
1331
+ event: "indexer.stream.supervisor_restart",
1332
+ stream: name,
1333
+ kind,
1334
+ backoff_ms: backoffMs,
1335
+ attempt: attempt + 1,
1336
+ restart_count: status.restartCount,
1337
+ error,
1338
+ last_error: status.lastError
1339
+ });
1192
1340
  attempt++;
1193
1341
  await this.delay(backoffMs, this.abortController.signal);
1194
1342
  }
1195
1343
  }
1196
- status.state = "stopped";
1344
+ this.setStreamState(status, "stopped");
1197
1345
  }
1198
1346
  supervisorBackoffMs(attempt, initialMs, maxMs) {
1199
1347
  const base = Math.min(maxMs, initialMs * Math.pow(2, attempt));