@secondlayer/subgraphs 0.7.2 → 0.8.0

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.
@@ -87,15 +87,28 @@ interface ReindexOptions {
87
87
  fromBlock?: number;
88
88
  toBlock?: number;
89
89
  schemaName?: string;
90
+ signal?: AbortSignal;
90
91
  }
91
92
  /**
92
93
  * Reindex a subgraph by dropping its tables, recreating them, and reprocessing
93
94
  * all historical blocks through the handler pipeline.
95
+ * Supports cancellation via AbortSignal — on shutdown abort, status stays
96
+ * "reindexing" for auto-resume; on user cancel, status resets to "active".
94
97
  */
95
98
  declare function reindexSubgraph(def: SubgraphDefinition, opts?: ReindexOptions): Promise<{
96
99
  processed: number
97
100
  }>;
98
101
  /**
102
+ * Resume a previously interrupted reindex. Skips schema drop (already done)
103
+ * and continues from last_processed_block + 1.
104
+ */
105
+ declare function resumeReindex(def: SubgraphDefinition, opts: {
106
+ schemaName: string
107
+ signal?: AbortSignal
108
+ }): Promise<{
109
+ processed: number
110
+ }>;
111
+ /**
99
112
  * Backfill a subgraph by re-processing a block range WITHOUT dropping the schema.
100
113
  * Uses upserts so existing data is updated, not duplicated. Safe to run while
101
114
  * the subgraph is actively syncing.
@@ -104,6 +117,7 @@ declare function backfillSubgraph(def: SubgraphDefinition, opts: {
104
117
  fromBlock: number
105
118
  toBlock: number
106
119
  schemaName?: string
120
+ signal?: AbortSignal
107
121
  }): Promise<{
108
122
  processed: number
109
123
  }>;
@@ -230,4 +244,4 @@ interface SubgraphTableClient<TRow> {
230
244
  type InferSubgraphClient<T> = T extends {
231
245
  schema: infer S
232
246
  } ? { [K in keyof S] : S[K] extends SubgraphTable ? SubgraphTableClient<InferTableRow<S[K]>> : never } : never;
233
- export { validateSubgraphDefinition, sourceKey, reindexSubgraph, pgSchemaName, generateSubgraphSQL, diffSchema, deploySchema, defineSubgraph, backfillSubgraph, WhereInput, TableDiff, SystemRow, SubgraphTableClient, SubgraphTable, SubgraphSource, SubgraphSchema, SubgraphHandler, SubgraphDefinition, SubgraphContext, SubgraphColumn, ReindexOptions, InferTableRow, InferSubgraphClient, InferColumnType, GeneratedSQL, FindManyOptions, ComparisonFilter, ColumnType, ColumnToTS, ColumnDiff };
247
+ export { validateSubgraphDefinition, sourceKey, resumeReindex, reindexSubgraph, pgSchemaName, generateSubgraphSQL, diffSchema, deploySchema, defineSubgraph, backfillSubgraph, WhereInput, TableDiff, SystemRow, SubgraphTableClient, SubgraphTable, SubgraphSource, SubgraphSchema, SubgraphHandler, SubgraphDefinition, SubgraphContext, SubgraphColumn, ReindexOptions, InferTableRow, InferSubgraphClient, InferColumnType, GeneratedSQL, FindManyOptions, ComparisonFilter, ColumnType, ColumnToTS, ColumnDiff };
package/dist/src/index.js CHANGED
@@ -411,11 +411,17 @@ async function runHandlers(subgraph, matched, ctx, opts) {
411
411
  }
412
412
 
413
413
  // src/runtime/source-matcher.ts
414
+ var patternCache = new Map;
414
415
  function matchPattern(value, pattern) {
415
416
  if (!pattern.includes("*"))
416
417
  return value === pattern;
417
- const regex = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
418
- return new RegExp(`^${regex}$`).test(value);
418
+ let re = patternCache.get(pattern);
419
+ if (!re) {
420
+ const regex = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
421
+ re = new RegExp(`^${regex}$`);
422
+ patternCache.set(pattern, re);
423
+ }
424
+ return re.test(value);
419
425
  }
420
426
  function matchSource(source, transactions, eventsByTx) {
421
427
  const results = [];
@@ -608,10 +614,10 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
608
614
  try {
609
615
  const tables = Object.keys(subgraph.schema);
610
616
  for (const table of tables) {
611
- const { rows } = await sql2.raw(`SELECT COUNT(*) as count FROM "${schemaName}"."${table}"`).execute(db);
612
- const count = Number(rows[0].count);
617
+ const { rows } = await sql2.raw(`SELECT n_live_tup AS count FROM pg_stat_user_tables WHERE schemaname = '${schemaName}' AND relname = '${table}'`).execute(db);
618
+ const count = Number(rows[0]?.count ?? 0);
613
619
  if (count >= 1e7) {
614
- logger3.warn("Subgraph table exceeds 10M rows", {
620
+ logger3.warn("Subgraph table exceeds 10M rows (estimate)", {
615
621
  subgraph: subgraphName,
616
622
  table,
617
623
  count
@@ -865,8 +871,18 @@ async function processBlockRange(def, opts) {
865
871
  let totalErrors = 0;
866
872
  let batchSize = DEFAULT_BATCH_SIZE;
867
873
  let currentHeight = fromBlock;
874
+ let aborted = false;
868
875
  let nextBatchPromise = loadBlockRange(db, currentHeight, Math.min(currentHeight + batchSize - 1, toBlock));
869
876
  while (currentHeight <= toBlock) {
877
+ if (opts.signal?.aborted) {
878
+ aborted = true;
879
+ logger4.info("Block processing aborted", {
880
+ subgraph: subgraphName,
881
+ currentBlock: currentHeight,
882
+ reason: String(opts.signal.reason ?? "unknown")
883
+ });
884
+ break;
885
+ }
870
886
  const batch = await nextBatchPromise;
871
887
  const batchEnd = Math.min(currentHeight + batchSize - 1, toBlock);
872
888
  const nextStart = batchEnd + 1;
@@ -939,7 +955,7 @@ async function processBlockRange(def, opts) {
939
955
  currentHeight = batchEnd + 1;
940
956
  }
941
957
  await stats.flush(db);
942
- return { blocksProcessed, totalEventsProcessed, totalErrors };
958
+ return { blocksProcessed, totalEventsProcessed, totalErrors, aborted };
943
959
  }
944
960
  async function resolveBlockRange(db, def, opts) {
945
961
  const fromBlock = opts?.fromBlock ?? def.startBlock ?? 1;
@@ -952,6 +968,9 @@ async function resolveBlockRange(db, def, opts) {
952
968
  }
953
969
  return { fromBlock, toBlock };
954
970
  }
971
+ async function clearReindexMetadata(db, subgraphName) {
972
+ await db.updateTable("subgraphs").set({ reindex_from_block: null, reindex_to_block: null }).where("name", "=", subgraphName).execute();
973
+ }
955
974
  async function reindexSubgraph(def, opts) {
956
975
  const db = getDb2();
957
976
  const client = getRawClient();
@@ -960,12 +979,6 @@ async function reindexSubgraph(def, opts) {
960
979
  await updateSubgraphStatus2(db, subgraphName, "reindexing");
961
980
  logger4.info("Reindex starting", { subgraph: subgraphName });
962
981
  try {
963
- await client.unsafe(`DROP SCHEMA IF EXISTS "${schemaName}" CASCADE`);
964
- const { statements } = generateSubgraphSQL(def, schemaName);
965
- for (const stmt of statements) {
966
- await client.unsafe(stmt);
967
- }
968
- logger4.info("Schema recreated for reindex", { subgraph: subgraphName });
969
982
  const { fromBlock, toBlock } = await resolveBlockRange(db, def, opts);
970
983
  if (fromBlock > toBlock) {
971
984
  logger4.info("No blocks to reindex", {
@@ -976,6 +989,13 @@ async function reindexSubgraph(def, opts) {
976
989
  await updateSubgraphStatus2(db, subgraphName, "active", 0);
977
990
  return { processed: 0 };
978
991
  }
992
+ await db.updateTable("subgraphs").set({ reindex_from_block: fromBlock, reindex_to_block: toBlock }).where("name", "=", subgraphName).execute();
993
+ await client.unsafe(`DROP SCHEMA IF EXISTS "${schemaName}" CASCADE`);
994
+ const { statements } = generateSubgraphSQL(def, schemaName);
995
+ for (const stmt of statements) {
996
+ await client.unsafe(stmt);
997
+ }
998
+ logger4.info("Schema recreated for reindex", { subgraph: subgraphName });
979
999
  logger4.info("Reindexing blocks", {
980
1000
  subgraph: subgraphName,
981
1001
  fromBlock,
@@ -989,13 +1009,28 @@ async function reindexSubgraph(def, opts) {
989
1009
  status: "reindexing",
990
1010
  isCatchup: false,
991
1011
  apiKeyId: subgraphRow?.api_key_id ?? null,
992
- subgraphId: subgraphRow?.id
1012
+ subgraphId: subgraphRow?.id,
1013
+ signal: opts?.signal
993
1014
  });
1015
+ if (result.aborted) {
1016
+ const reason = String(opts?.signal?.reason ?? "unknown");
1017
+ if (reason === "user-cancelled") {
1018
+ await updateSubgraphStatus2(db, subgraphName, "active");
1019
+ await clearReindexMetadata(db, subgraphName);
1020
+ logger4.info("Reindex cancelled by user", { subgraph: subgraphName });
1021
+ } else {
1022
+ logger4.info("Reindex interrupted by shutdown, will resume", {
1023
+ subgraph: subgraphName
1024
+ });
1025
+ }
1026
+ return { processed: result.blocksProcessed };
1027
+ }
994
1028
  const { recordSubgraphProcessed: recordSubgraphProcessed2 } = await import("@secondlayer/shared/db/queries/subgraphs");
995
1029
  if (result.totalEventsProcessed > 0 || result.totalErrors > 0) {
996
1030
  await recordSubgraphProcessed2(db, subgraphName, result.totalEventsProcessed, result.totalErrors, result.totalErrors > 0 ? `${result.totalErrors} error(s) during reindex` : undefined);
997
1031
  }
998
1032
  await updateSubgraphStatus2(db, subgraphName, "active", toBlock);
1033
+ await clearReindexMetadata(db, subgraphName);
999
1034
  logger4.info("Reindex complete", {
1000
1035
  subgraph: subgraphName,
1001
1036
  blocks: result.blocksProcessed,
@@ -1012,6 +1047,84 @@ async function reindexSubgraph(def, opts) {
1012
1047
  throw err;
1013
1048
  }
1014
1049
  }
1050
+ async function resumeReindex(def, opts) {
1051
+ const db = getDb2();
1052
+ const subgraphName = def.name;
1053
+ const row = await db.selectFrom("subgraphs").select([
1054
+ "id",
1055
+ "api_key_id",
1056
+ "last_processed_block",
1057
+ "reindex_from_block",
1058
+ "reindex_to_block"
1059
+ ]).where("name", "=", subgraphName).executeTakeFirst();
1060
+ if (!row)
1061
+ throw new Error(`Subgraph "${subgraphName}" not found`);
1062
+ if (row.reindex_from_block == null || row.reindex_to_block == null) {
1063
+ logger4.info("No reindex metadata, starting fresh reindex", {
1064
+ subgraph: subgraphName
1065
+ });
1066
+ return reindexSubgraph(def, {
1067
+ schemaName: opts.schemaName,
1068
+ signal: opts.signal
1069
+ });
1070
+ }
1071
+ const fromBlock = Math.max(row.last_processed_block + 1, row.reindex_from_block);
1072
+ const toBlock = row.reindex_to_block;
1073
+ if (fromBlock > toBlock) {
1074
+ logger4.info("Resume: no remaining blocks", { subgraph: subgraphName });
1075
+ await updateSubgraphStatus2(db, subgraphName, "active", toBlock);
1076
+ await clearReindexMetadata(db, subgraphName);
1077
+ return { processed: 0 };
1078
+ }
1079
+ logger4.info("Resuming reindex", {
1080
+ subgraph: subgraphName,
1081
+ fromBlock,
1082
+ toBlock,
1083
+ remaining: toBlock - fromBlock + 1
1084
+ });
1085
+ try {
1086
+ const result = await processBlockRange(def, {
1087
+ fromBlock,
1088
+ toBlock,
1089
+ status: "reindexing",
1090
+ isCatchup: false,
1091
+ apiKeyId: row.api_key_id ?? null,
1092
+ subgraphId: row.id,
1093
+ signal: opts.signal
1094
+ });
1095
+ if (result.aborted) {
1096
+ const reason = String(opts.signal?.reason ?? "unknown");
1097
+ if (reason === "user-cancelled") {
1098
+ await updateSubgraphStatus2(db, subgraphName, "active");
1099
+ await clearReindexMetadata(db, subgraphName);
1100
+ logger4.info("Resume cancelled by user", { subgraph: subgraphName });
1101
+ } else {
1102
+ logger4.info("Resume interrupted by shutdown, will resume again", {
1103
+ subgraph: subgraphName
1104
+ });
1105
+ }
1106
+ return { processed: result.blocksProcessed };
1107
+ }
1108
+ const { recordSubgraphProcessed: recordSubgraphProcessed2 } = await import("@secondlayer/shared/db/queries/subgraphs");
1109
+ if (result.totalEventsProcessed > 0 || result.totalErrors > 0) {
1110
+ await recordSubgraphProcessed2(db, subgraphName, result.totalEventsProcessed, result.totalErrors, result.totalErrors > 0 ? `${result.totalErrors} error(s) during resumed reindex` : undefined);
1111
+ }
1112
+ await updateSubgraphStatus2(db, subgraphName, "active", toBlock);
1113
+ await clearReindexMetadata(db, subgraphName);
1114
+ logger4.info("Resumed reindex complete", {
1115
+ subgraph: subgraphName,
1116
+ blocks: result.blocksProcessed
1117
+ });
1118
+ return { processed: result.blocksProcessed };
1119
+ } catch (err) {
1120
+ logger4.error("Resumed reindex failed", {
1121
+ subgraph: subgraphName,
1122
+ error: getErrorMessage2(err)
1123
+ });
1124
+ await updateSubgraphStatus2(db, subgraphName, "error");
1125
+ throw err;
1126
+ }
1127
+ }
1015
1128
  async function backfillSubgraph(def, opts) {
1016
1129
  const db = getDb2();
1017
1130
  const subgraphName = def.name;
@@ -1028,8 +1141,13 @@ async function backfillSubgraph(def, opts) {
1028
1141
  status: "active",
1029
1142
  isCatchup: false,
1030
1143
  apiKeyId: subgraphRow?.api_key_id ?? null,
1031
- subgraphId: subgraphRow?.id
1144
+ subgraphId: subgraphRow?.id,
1145
+ signal: opts.signal
1032
1146
  });
1147
+ if (result.aborted) {
1148
+ logger4.info("Backfill aborted", { subgraph: subgraphName });
1149
+ return { processed: result.blocksProcessed };
1150
+ }
1033
1151
  const resolved = await resolveGaps(db, subgraphName, opts.fromBlock, opts.toBlock).catch(() => 0);
1034
1152
  if (resolved > 0) {
1035
1153
  logger4.info("Resolved subgraph gaps via backfill", {
@@ -1233,6 +1351,7 @@ function getDefault(type) {
1233
1351
  export {
1234
1352
  validateSubgraphDefinition,
1235
1353
  sourceKey,
1354
+ resumeReindex,
1236
1355
  reindexSubgraph,
1237
1356
  pgSchemaName,
1238
1357
  generateSubgraphSQL,
@@ -1242,5 +1361,5 @@ export {
1242
1361
  backfillSubgraph
1243
1362
  };
1244
1363
 
1245
- //# debugId=DED4A221B27D85F264756E2164756E21
1364
+ //# debugId=7A65C6186191C40964756E2164756E21
1246
1365
  //# sourceMappingURL=index.js.map