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