@secondlayer/subgraphs 0.5.7 → 0.7.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.
Files changed (37) hide show
  1. package/dist/src/index.d.ts +17 -5
  2. package/dist/src/index.js +467 -191
  3. package/dist/src/index.js.map +16 -15
  4. package/dist/src/runtime/block-processor.d.ts +9 -1
  5. package/dist/src/runtime/block-processor.js +216 -144
  6. package/dist/src/runtime/block-processor.js.map +9 -9
  7. package/dist/src/runtime/catchup.d.ts +2 -2
  8. package/dist/src/runtime/catchup.js +366 -160
  9. package/dist/src/runtime/catchup.js.map +12 -11
  10. package/dist/src/runtime/clarity.js.map +2 -2
  11. package/dist/src/runtime/context.d.ts +4 -2
  12. package/dist/src/runtime/context.js +79 -36
  13. package/dist/src/runtime/context.js.map +3 -3
  14. package/dist/src/runtime/processor.js +384 -166
  15. package/dist/src/runtime/processor.js.map +14 -13
  16. package/dist/src/runtime/reindex.d.ts +13 -1
  17. package/dist/src/runtime/reindex.js +459 -189
  18. package/dist/src/runtime/reindex.js.map +13 -12
  19. package/dist/src/runtime/reorg.js +229 -148
  20. package/dist/src/runtime/reorg.js.map +10 -10
  21. package/dist/src/runtime/runner.d.ts +4 -2
  22. package/dist/src/runtime/runner.js +7 -3
  23. package/dist/src/runtime/runner.js.map +4 -4
  24. package/dist/src/runtime/source-matcher.js.map +3 -3
  25. package/dist/src/runtime/stats.d.ts +1 -1
  26. package/dist/src/runtime/stats.js +2 -2
  27. package/dist/src/runtime/stats.js.map +3 -3
  28. package/dist/src/schema/index.d.ts +1 -1
  29. package/dist/src/schema/index.js +9 -3
  30. package/dist/src/schema/index.js.map +5 -5
  31. package/dist/src/service.js +385 -167
  32. package/dist/src/service.js.map +15 -14
  33. package/dist/src/templates.js.map +2 -2
  34. package/dist/src/types.js.map +2 -2
  35. package/dist/src/validate.js +2 -2
  36. package/dist/src/validate.js.map +3 -3
  37. package/package.json +51 -51
@@ -16,8 +16,8 @@ function sourceKey(source) {
16
16
  }
17
17
 
18
18
  // src/runtime/context.ts
19
- import { sql } from "kysely";
20
19
  import { logger } from "@secondlayer/shared/logger";
20
+ import { sql } from "kysely";
21
21
  function validateColumnName(name) {
22
22
  if (!/^[a-z_][a-z0-9_]*$/i.test(name)) {
23
23
  throw new Error(`Invalid column name: ${name}`);
@@ -58,13 +58,26 @@ class SubgraphContext {
58
58
  const keyColumns = Object.keys(key);
59
59
  const hasUniqueConstraint = tableDef.uniqueKeys?.some((uk) => uk.length === keyColumns.length && uk.every((c) => keyColumns.includes(c)));
60
60
  if (hasUniqueConstraint) {
61
- this.ops.push({ kind: "insert", table, data: { ...key, ...row, _upsert_keys: keyColumns } });
61
+ this.ops.push({
62
+ kind: "insert",
63
+ table,
64
+ data: { ...key, ...row, _upsert_keys: keyColumns }
65
+ });
62
66
  } else {
63
67
  logger.warn("upsert called without matching uniqueKeys constraint, using fallback", {
64
68
  table,
65
69
  keys: keyColumns
66
70
  });
67
- this.ops.push({ kind: "insert", table, data: { ...key, ...row, _upsert_fallback_keys: keyColumns, _upsert_fallback_set: row } });
71
+ this.ops.push({
72
+ kind: "insert",
73
+ table,
74
+ data: {
75
+ ...key,
76
+ ...row,
77
+ _upsert_fallback_keys: keyColumns,
78
+ _upsert_fallback_set: row
79
+ }
80
+ });
68
81
  }
69
82
  }
70
83
  delete(table, where) {
@@ -122,53 +135,83 @@ class SubgraphContext {
122
135
  }
123
136
  return opsToFlush.length;
124
137
  }
138
+ prepareInsert(op) {
139
+ const upsertKeys = op.data._upsert_keys;
140
+ const data = { ...op.data };
141
+ delete data._upsert_keys;
142
+ delete data._upsert_fallback_keys;
143
+ delete data._upsert_fallback_set;
144
+ data._block_height = this.block.height;
145
+ data._tx_id = this._tx.txId;
146
+ data._created_at = "NOW()";
147
+ const cols = Object.keys(data);
148
+ cols.forEach(validateColumnName);
149
+ const vals = cols.map((c) => data[c] === "NOW()" ? "NOW()" : escapeLiteral(data[c]));
150
+ const batchKey = `${op.table}:${[...cols].sort().join(",")}:${upsertKeys ? [...upsertKeys].sort().join(",") : ""}`;
151
+ return { data, cols, vals, upsertKeys, batchKey };
152
+ }
125
153
  buildStatements(ops) {
126
154
  const statements = [];
155
+ let currentBatch = null;
156
+ let currentBatchKey = "";
157
+ const flushInsertBatch = () => {
158
+ if (!currentBatch)
159
+ return;
160
+ const qualifiedTable = `"${this.pgSchemaName}"."${currentBatch.table}"`;
161
+ const colList = currentBatch.cols.map((c) => `"${c}"`).join(", ");
162
+ let rows = currentBatch.rows;
163
+ if (currentBatch.upsertKeys && currentBatch.upsertKeys.length > 0) {
164
+ const keyIndices = currentBatch.upsertKeys.map((k) => currentBatch.cols.indexOf(k));
165
+ const seen = new Map;
166
+ for (let i = 0;i < rows.length; i++) {
167
+ const key = keyIndices.map((ki) => rows[i][ki]).join("\x00");
168
+ seen.set(key, i);
169
+ }
170
+ if (seen.size < rows.length) {
171
+ rows = Array.from(seen.values()).map((i) => rows[i]);
172
+ }
173
+ }
174
+ const valuesList = rows.map((r) => `(${r.join(", ")})`).join(", ");
175
+ let stmt = `INSERT INTO ${qualifiedTable} (${colList}) VALUES ${valuesList}`;
176
+ if (currentBatch.upsertKeys && currentBatch.upsertKeys.length > 0) {
177
+ const updateCols = currentBatch.cols.filter((c) => !currentBatch.upsertKeys.includes(c) && !c.startsWith("_"));
178
+ if (updateCols.length > 0) {
179
+ const setClauses = updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`);
180
+ stmt += ` ON CONFLICT (${currentBatch.upsertKeys.map((k) => `"${k}"`).join(", ")}) DO UPDATE SET ${setClauses.join(", ")}`;
181
+ } else {
182
+ stmt += ` ON CONFLICT (${currentBatch.upsertKeys.map((k) => `"${k}"`).join(", ")}) DO NOTHING`;
183
+ }
184
+ }
185
+ statements.push(stmt);
186
+ currentBatch = null;
187
+ currentBatchKey = "";
188
+ };
127
189
  for (const op of ops) {
128
190
  const qualifiedTable = `"${this.pgSchemaName}"."${op.table}"`;
129
- switch (op.kind) {
130
- case "insert": {
131
- const upsertKeys = op.data._upsert_keys;
132
- const fallbackKeys = op.data._upsert_fallback_keys;
133
- const fallbackSet = op.data._upsert_fallback_set;
134
- const data = { ...op.data };
135
- delete data._upsert_keys;
136
- delete data._upsert_fallback_keys;
137
- delete data._upsert_fallback_set;
138
- data._block_height = this.block.height;
139
- data._tx_id = this._tx.txId;
140
- data._created_at = "NOW()";
141
- const cols = Object.keys(data);
142
- cols.forEach(validateColumnName);
143
- const vals = cols.map((c) => data[c] === "NOW()" ? "NOW()" : escapeLiteral(data[c]));
144
- let stmt = `INSERT INTO ${qualifiedTable} (${cols.map((c) => `"${c}"`).join(", ")}) VALUES (${vals.join(", ")})`;
145
- if (upsertKeys && upsertKeys.length > 0) {
146
- const updateCols = cols.filter((c) => !upsertKeys.includes(c) && !c.startsWith("_"));
147
- if (updateCols.length > 0) {
148
- const setClauses = updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`);
149
- stmt += ` ON CONFLICT (${upsertKeys.map((k) => `"${k}"`).join(", ")}) DO UPDATE SET ${setClauses.join(", ")}`;
150
- } else {
151
- stmt += ` ON CONFLICT (${upsertKeys.map((k) => `"${k}"`).join(", ")}) DO NOTHING`;
152
- }
153
- } else if (fallbackKeys && fallbackSet) {}
154
- statements.push(stmt);
155
- break;
191
+ if (op.kind === "insert") {
192
+ const { cols, vals, upsertKeys, batchKey } = this.prepareInsert(op);
193
+ if (batchKey === currentBatchKey && currentBatch) {
194
+ currentBatch.rows.push(vals);
195
+ } else {
196
+ flushInsertBatch();
197
+ currentBatch = { table: op.table, cols, rows: [vals], upsertKeys };
198
+ currentBatchKey = batchKey;
156
199
  }
157
- case "update": {
200
+ } else {
201
+ flushInsertBatch();
202
+ if (op.kind === "update") {
158
203
  const setEntries = Object.entries(op.set);
159
204
  setEntries.forEach(([k]) => validateColumnName(k));
160
205
  const setClauses = setEntries.map(([k, v]) => `"${k}" = ${escapeLiteral(v)}`);
161
206
  const { clause } = buildWhereClause(op.data);
162
207
  statements.push(`UPDATE ${qualifiedTable} SET ${setClauses.join(", ")} WHERE ${clause}`);
163
- break;
164
- }
165
- case "delete": {
208
+ } else if (op.kind === "delete") {
166
209
  const { clause } = buildWhereClause(op.data);
167
210
  statements.push(`DELETE FROM ${qualifiedTable} WHERE ${clause}`);
168
- break;
169
211
  }
170
212
  }
171
213
  }
214
+ flushInsertBatch();
172
215
  return statements;
173
216
  }
174
217
  validateTable(table) {
@@ -197,95 +240,6 @@ function buildWhereClause(where) {
197
240
  return { clause: parts.join(" AND "), values: [] };
198
241
  }
199
242
 
200
- // src/runtime/source-matcher.ts
201
- function matchPattern(value, pattern) {
202
- if (!pattern.includes("*"))
203
- return value === pattern;
204
- const regex = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
205
- return new RegExp(`^${regex}$`).test(value);
206
- }
207
- function matchSource(source, transactions, eventsByTx) {
208
- const results = [];
209
- const key = sourceKey(source);
210
- for (const tx of transactions) {
211
- if (source.type) {
212
- if (!matchPattern(tx.type, source.type))
213
- continue;
214
- const txEvents = eventsByTx.get(tx.tx_id) ?? [];
215
- let matchedEvents = txEvents;
216
- if (source.minAmount !== undefined) {
217
- const amountEvents = matchedEvents.filter((e) => {
218
- const data = e.data;
219
- const rawAmount = data?.amount;
220
- if (rawAmount === undefined)
221
- return false;
222
- const amount = BigInt(rawAmount);
223
- return amount >= source.minAmount;
224
- });
225
- if (amountEvents.length === 0)
226
- continue;
227
- matchedEvents = amountEvents;
228
- }
229
- results.push({ tx, events: matchedEvents, sourceKey: key });
230
- continue;
231
- }
232
- if (source.contract) {
233
- const txContractMatch = tx.contract_id && matchPattern(tx.contract_id, source.contract);
234
- if (source.function && tx.function_name) {
235
- if (!matchPattern(tx.function_name, source.function))
236
- continue;
237
- } else if (source.function && !tx.function_name) {
238
- continue;
239
- }
240
- const txEvents = eventsByTx.get(tx.tx_id) ?? [];
241
- let matchedEvents = txEvents;
242
- if (!txContractMatch) {
243
- matchedEvents = txEvents.filter((e) => {
244
- const data = e.data;
245
- const contractIdentifier = data?.contract_identifier;
246
- return contractIdentifier && matchPattern(contractIdentifier, source.contract);
247
- });
248
- if (matchedEvents.length === 0)
249
- continue;
250
- }
251
- if (source.event) {
252
- matchedEvents = matchedEvents.filter((e) => {
253
- if (matchPattern(e.type, source.event))
254
- return true;
255
- const data = e.data;
256
- const topic = data?.topic;
257
- return topic ? matchPattern(topic, source.event) : false;
258
- });
259
- }
260
- if (txContractMatch || matchedEvents.length > 0) {
261
- results.push({ tx, events: matchedEvents, sourceKey: key });
262
- }
263
- }
264
- }
265
- return results;
266
- }
267
- function matchSources(sources, transactions, events) {
268
- const eventsByTx = new Map;
269
- for (const event of events) {
270
- const list = eventsByTx.get(event.tx_id) ?? [];
271
- list.push(event);
272
- eventsByTx.set(event.tx_id, list);
273
- }
274
- const seen = new Set;
275
- const results = [];
276
- for (const source of sources) {
277
- const matches = matchSource(source, transactions, eventsByTx);
278
- for (const match of matches) {
279
- const dedupeKey = `${match.tx.tx_id}:${match.sourceKey}`;
280
- if (!seen.has(dedupeKey)) {
281
- seen.add(dedupeKey);
282
- results.push(match);
283
- }
284
- }
285
- }
286
- return results;
287
- }
288
-
289
243
  // src/runtime/clarity.ts
290
244
  import { cvToJSON, deserializeCV } from "@secondlayer/stacks/clarity";
291
245
  function decodeClarityValue(hex) {
@@ -318,8 +272,8 @@ function decodeFunctionArgs(args) {
318
272
  }
319
273
 
320
274
  // src/runtime/runner.ts
321
- import { logger as logger2 } from "@secondlayer/shared/logger";
322
275
  import { getErrorMessage } from "@secondlayer/shared";
276
+ import { logger as logger2 } from "@secondlayer/shared/logger";
323
277
  var DEFAULT_ERROR_THRESHOLD = 50;
324
278
  function resolveHandler(handlers, key) {
325
279
  return handlers[key] ?? handlers["*"] ?? null;
@@ -331,7 +285,11 @@ async function runHandlers(subgraph, matched, ctx, opts) {
331
285
  for (const { tx, events, sourceKey: sourceKey2 } of matched) {
332
286
  const handler = resolveHandler(subgraph.handlers, sourceKey2);
333
287
  if (!handler) {
334
- logger2.warn("No handler found for source key", { subgraph: subgraph.name, sourceKey: sourceKey2, txId: tx.tx_id });
288
+ logger2.warn("No handler found for source key", {
289
+ subgraph: subgraph.name,
290
+ sourceKey: sourceKey2,
291
+ txId: tx.tx_id
292
+ });
335
293
  continue;
336
294
  }
337
295
  ctx.setTx({
@@ -408,16 +366,109 @@ async function runHandlers(subgraph, matched, ctx, opts) {
408
366
  return { processed, errors };
409
367
  }
410
368
 
369
+ // src/runtime/source-matcher.ts
370
+ function matchPattern(value, pattern) {
371
+ if (!pattern.includes("*"))
372
+ return value === pattern;
373
+ const regex = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
374
+ return new RegExp(`^${regex}$`).test(value);
375
+ }
376
+ function matchSource(source, transactions, eventsByTx) {
377
+ const results = [];
378
+ const key = sourceKey(source);
379
+ for (const tx of transactions) {
380
+ if (source.type) {
381
+ if (!matchPattern(tx.type, source.type))
382
+ continue;
383
+ const txEvents = eventsByTx.get(tx.tx_id) ?? [];
384
+ let matchedEvents = txEvents;
385
+ if (source.minAmount !== undefined) {
386
+ const amountEvents = matchedEvents.filter((e) => {
387
+ const data = e.data;
388
+ const rawAmount = data?.amount;
389
+ if (rawAmount === undefined)
390
+ return false;
391
+ const amount = BigInt(rawAmount);
392
+ return amount >= source.minAmount;
393
+ });
394
+ if (amountEvents.length === 0)
395
+ continue;
396
+ matchedEvents = amountEvents;
397
+ }
398
+ results.push({ tx, events: matchedEvents, sourceKey: key });
399
+ continue;
400
+ }
401
+ if (source.contract) {
402
+ const txContractMatch = tx.contract_id && matchPattern(tx.contract_id, source.contract);
403
+ if (source.function && tx.function_name) {
404
+ if (!matchPattern(tx.function_name, source.function))
405
+ continue;
406
+ } else if (source.function && !tx.function_name) {
407
+ continue;
408
+ }
409
+ const txEvents = eventsByTx.get(tx.tx_id) ?? [];
410
+ let matchedEvents = txEvents;
411
+ if (!txContractMatch) {
412
+ matchedEvents = txEvents.filter((e) => {
413
+ const data = e.data;
414
+ const contractIdentifier = data?.contract_identifier;
415
+ return contractIdentifier && matchPattern(contractIdentifier, source.contract);
416
+ });
417
+ if (matchedEvents.length === 0)
418
+ continue;
419
+ }
420
+ if (source.event) {
421
+ matchedEvents = matchedEvents.filter((e) => {
422
+ if (matchPattern(e.type, source.event))
423
+ return true;
424
+ const data = e.data;
425
+ const topic = data?.topic;
426
+ return topic ? matchPattern(topic, source.event) : false;
427
+ });
428
+ }
429
+ if (txContractMatch || matchedEvents.length > 0) {
430
+ results.push({ tx, events: matchedEvents, sourceKey: key });
431
+ }
432
+ }
433
+ }
434
+ return results;
435
+ }
436
+ function matchSources(sources, transactions, events) {
437
+ const eventsByTx = new Map;
438
+ for (const event of events) {
439
+ const list = eventsByTx.get(event.tx_id) ?? [];
440
+ list.push(event);
441
+ eventsByTx.set(event.tx_id, list);
442
+ }
443
+ const seen = new Set;
444
+ const results = [];
445
+ for (const source of sources) {
446
+ const matches = matchSource(source, transactions, eventsByTx);
447
+ for (const match of matches) {
448
+ const dedupeKey = `${match.tx.tx_id}:${match.sourceKey}`;
449
+ if (!seen.has(dedupeKey)) {
450
+ seen.add(dedupeKey);
451
+ results.push(match);
452
+ }
453
+ }
454
+ }
455
+ return results;
456
+ }
457
+
411
458
  // src/runtime/block-processor.ts
412
- import { sql as sql2 } from "kysely";
413
459
  import { getDb } from "@secondlayer/shared/db";
460
+ import {
461
+ recordSubgraphProcessed,
462
+ updateSubgraphStatus
463
+ } from "@secondlayer/shared/db/queries/subgraphs";
414
464
  import { logger as logger3 } from "@secondlayer/shared/logger";
415
- import { updateSubgraphStatus, recordSubgraphProcessed } from "@secondlayer/shared/db/queries/subgraphs";
465
+ import { sql as sql2 } from "kysely";
416
466
 
417
467
  // src/schema/utils.ts
418
468
  import { pgSchemaName } from "@secondlayer/shared/db/queries/subgraphs";
419
469
 
420
470
  // src/runtime/block-processor.ts
471
+ var schemaNameCache = new Map;
421
472
  async function processBlock(subgraph, subgraphName, blockHeight, opts) {
422
473
  const db = getDb();
423
474
  const blockStart = performance.now();
@@ -428,19 +479,32 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
428
479
  errors: 0,
429
480
  skipped: false
430
481
  };
431
- const block = await db.selectFrom("blocks").selectAll().where("height", "=", blockHeight).executeTakeFirst();
432
- if (!block) {
433
- logger3.warn("Block not found for subgraph processing", { subgraph: subgraphName, blockHeight });
434
- result.skipped = true;
435
- return result;
436
- }
437
- if (!block.canonical) {
438
- logger3.debug("Skipping non-canonical block", { subgraph: subgraphName, blockHeight });
439
- result.skipped = true;
440
- return result;
482
+ let block, txs, evts;
483
+ if (opts?.preloaded) {
484
+ block = opts.preloaded.block;
485
+ txs = opts.preloaded.txs;
486
+ evts = opts.preloaded.events;
487
+ } else {
488
+ block = await db.selectFrom("blocks").selectAll().where("height", "=", blockHeight).executeTakeFirst();
489
+ if (!block) {
490
+ logger3.warn("Block not found for subgraph processing", {
491
+ subgraph: subgraphName,
492
+ blockHeight
493
+ });
494
+ result.skipped = true;
495
+ return result;
496
+ }
497
+ if (!block.canonical) {
498
+ logger3.debug("Skipping non-canonical block", {
499
+ subgraph: subgraphName,
500
+ blockHeight
501
+ });
502
+ result.skipped = true;
503
+ return result;
504
+ }
505
+ txs = await db.selectFrom("transactions").selectAll().where("block_height", "=", blockHeight).execute();
506
+ evts = await db.selectFrom("events").selectAll().where("block_height", "=", blockHeight).execute();
441
507
  }
442
- const txs = await db.selectFrom("transactions").selectAll().where("block_height", "=", blockHeight).execute();
443
- const evts = await db.selectFrom("events").selectAll().where("block_height", "=", blockHeight).execute();
444
508
  const matched = matchSources(subgraph.sources, txs, evts);
445
509
  result.matched = matched.length;
446
510
  if (matched.length === 0) {
@@ -449,8 +513,12 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
449
513
  }
450
514
  return result;
451
515
  }
452
- const subgraphRecord = await db.selectFrom("subgraphs").select("schema_name").where("name", "=", subgraphName).executeTakeFirst();
453
- const schemaName = subgraphRecord?.schema_name ?? pgSchemaName(subgraphName);
516
+ let schemaName = schemaNameCache.get(subgraphName);
517
+ if (!schemaName) {
518
+ const subgraphRecord = await db.selectFrom("subgraphs").select("schema_name").where("name", "=", subgraphName).executeTakeFirst();
519
+ schemaName = subgraphRecord?.schema_name ?? pgSchemaName(subgraphName);
520
+ schemaNameCache.set(subgraphName, schemaName);
521
+ }
454
522
  const blockMeta = {
455
523
  height: block.height,
456
524
  hash: block.hash,
@@ -499,7 +567,11 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
499
567
  const { rows } = await sql2.raw(`SELECT COUNT(*) as count FROM "${schemaName}"."${table}"`).execute(db);
500
568
  const count = Number(rows[0].count);
501
569
  if (count >= 1e7) {
502
- logger3.warn("Subgraph table exceeds 10M rows", { subgraph: subgraphName, table, count });
570
+ logger3.warn("Subgraph table exceeds 10M rows", {
571
+ subgraph: subgraphName,
572
+ table,
573
+ count
574
+ });
503
575
  }
504
576
  }
505
577
  } catch {}
@@ -553,7 +625,7 @@ class StatsAccumulator {
553
625
  flush_time_ms: Math.round(entry.flushTimeMs),
554
626
  max_block_time_ms: Math.round(entry.maxBlockTimeMs),
555
627
  max_handler_time_ms: Math.round(entry.maxHandlerTimeMs),
556
- avg_ops_per_block: parseFloat(avgOpsPerBlock.toFixed(2)),
628
+ avg_ops_per_block: Number.parseFloat(avgOpsPerBlock.toFixed(2)),
557
629
  is_catchup: entry.isCatchup
558
630
  }).execute();
559
631
  }
@@ -575,10 +647,14 @@ class StatsAccumulator {
575
647
  }
576
648
 
577
649
  // src/runtime/reindex.ts
650
+ import { getErrorMessage as getErrorMessage2 } from "@secondlayer/shared";
578
651
  import { getDb as getDb2, getRawClient } from "@secondlayer/shared/db";
652
+ import {
653
+ recordGapBatch,
654
+ resolveGaps
655
+ } from "@secondlayer/shared/db/queries/subgraph-gaps";
579
656
  import { updateSubgraphStatus as updateSubgraphStatus2 } from "@secondlayer/shared/db/queries/subgraphs";
580
657
  import { logger as logger4 } from "@secondlayer/shared/logger";
581
- import { getErrorMessage as getErrorMessage2 } from "@secondlayer/shared";
582
658
 
583
659
  // src/schema/generator.ts
584
660
  var TYPE_MAP = {
@@ -665,45 +741,121 @@ function generateSubgraphSQL(def, schemaNameOverride) {
665
741
  return { statements, hash };
666
742
  }
667
743
 
744
+ // src/runtime/batch-loader.ts
745
+ async function loadBlockRange(db, fromHeight, toHeight) {
746
+ const [blocks, txs, events] = await Promise.all([
747
+ db.selectFrom("blocks").selectAll().where("height", ">=", fromHeight).where("height", "<=", toHeight).where("canonical", "=", true).execute(),
748
+ db.selectFrom("transactions").selectAll().where("block_height", ">=", fromHeight).where("block_height", "<=", toHeight).execute(),
749
+ db.selectFrom("events").selectAll().where("block_height", ">=", fromHeight).where("block_height", "<=", toHeight).execute()
750
+ ]);
751
+ const txsByHeight = new Map;
752
+ for (const tx of txs) {
753
+ const h = Number(tx.block_height);
754
+ const list = txsByHeight.get(h) ?? [];
755
+ list.push(tx);
756
+ txsByHeight.set(h, list);
757
+ }
758
+ const eventsByHeight = new Map;
759
+ for (const evt of events) {
760
+ const h = Number(evt.block_height);
761
+ const list = eventsByHeight.get(h) ?? [];
762
+ list.push(evt);
763
+ eventsByHeight.set(h, list);
764
+ }
765
+ const result = new Map;
766
+ for (const block of blocks) {
767
+ const h = Number(block.height);
768
+ result.set(h, {
769
+ block,
770
+ txs: txsByHeight.get(h) ?? [],
771
+ events: eventsByHeight.get(h) ?? []
772
+ });
773
+ }
774
+ return result;
775
+ }
776
+ function avgEventsPerBlock(batch) {
777
+ if (batch.size === 0)
778
+ return 0;
779
+ let totalEvents = 0;
780
+ for (const data of batch.values()) {
781
+ totalEvents += data.events.length;
782
+ }
783
+ return totalEvents / batch.size;
784
+ }
785
+
668
786
  // src/runtime/reindex.ts
669
787
  var LOG_INTERVAL = 1000;
670
- async function reindexSubgraph(def, opts) {
671
- const db = getDb2();
672
- const client = getRawClient();
673
- const subgraphName = def.name;
674
- const schemaName = opts?.schemaName ?? pgSchemaName(subgraphName);
675
- await updateSubgraphStatus2(db, subgraphName, "reindexing");
676
- logger4.info("Reindex starting", { subgraph: subgraphName });
677
- try {
678
- await client.unsafe(`DROP SCHEMA IF EXISTS "${schemaName}" CASCADE`);
679
- const { statements } = generateSubgraphSQL(def, schemaName);
680
- for (const stmt of statements) {
681
- await client.unsafe(stmt);
682
- }
683
- logger4.info("Schema recreated for reindex", { subgraph: subgraphName });
684
- const fromBlock = opts?.fromBlock ?? 1;
685
- let toBlock;
686
- if (opts?.toBlock != null) {
687
- toBlock = opts.toBlock;
788
+ var DEFAULT_BATCH_SIZE = 500;
789
+ var MIN_BATCH_SIZE = 100;
790
+ var MAX_BATCH_SIZE = 1000;
791
+ function coalesceFailedBlocks(blocks) {
792
+ if (blocks.length === 0)
793
+ return [];
794
+ blocks.sort((a, b) => a.height - b.height);
795
+ const ranges = [];
796
+ let start = blocks[0].height;
797
+ let end = blocks[0].height;
798
+ let reason = blocks[0].reason;
799
+ for (let i = 1;i < blocks.length; i++) {
800
+ const b = blocks[i];
801
+ if (b.height === end + 1 && b.reason === reason) {
802
+ end = b.height;
688
803
  } else {
689
- const progress = await db.selectFrom("index_progress").selectAll().where("network", "=", process.env.NETWORK ?? "mainnet").executeTakeFirst();
690
- toBlock = progress?.last_indexed_block ?? progress?.last_contiguous_block ?? 0;
804
+ ranges.push({ start, end, reason });
805
+ start = b.height;
806
+ end = b.height;
807
+ reason = b.reason;
691
808
  }
692
- if (fromBlock > toBlock) {
693
- logger4.info("No blocks to reindex", { subgraph: subgraphName, fromBlock, toBlock });
694
- await updateSubgraphStatus2(db, subgraphName, "active", 0);
695
- return { processed: 0 };
809
+ }
810
+ ranges.push({ start, end, reason });
811
+ return ranges;
812
+ }
813
+ async function processBlockRange(def, opts) {
814
+ const db = getDb2();
815
+ const subgraphName = def.name;
816
+ const { fromBlock, toBlock, status } = opts;
817
+ const totalBlocks = toBlock - fromBlock + 1;
818
+ const stats = new StatsAccumulator(subgraphName, opts.apiKeyId, opts.isCatchup);
819
+ let blocksProcessed = 0;
820
+ let totalEventsProcessed = 0;
821
+ let totalErrors = 0;
822
+ let batchSize = DEFAULT_BATCH_SIZE;
823
+ let currentHeight = fromBlock;
824
+ let nextBatchPromise = loadBlockRange(db, currentHeight, Math.min(currentHeight + batchSize - 1, toBlock));
825
+ while (currentHeight <= toBlock) {
826
+ const batch = await nextBatchPromise;
827
+ const batchEnd = Math.min(currentHeight + batchSize - 1, toBlock);
828
+ const nextStart = batchEnd + 1;
829
+ if (nextStart <= toBlock) {
830
+ const nextEnd = Math.min(nextStart + batchSize - 1, toBlock);
831
+ nextBatchPromise = loadBlockRange(db, nextStart, nextEnd);
696
832
  }
697
- const totalBlocks = toBlock - fromBlock + 1;
698
- logger4.info("Reindexing blocks", { subgraph: subgraphName, fromBlock, toBlock, totalBlocks });
699
- const subgraphRow = await db.selectFrom("subgraphs").select("api_key_id").where("name", "=", subgraphName).executeTakeFirst();
700
- const stats = new StatsAccumulator(subgraphName, subgraphRow?.api_key_id ?? null, false);
701
- let blocksProcessed = 0;
702
- let totalEventsProcessed = 0;
703
- let totalErrors = 0;
704
- const PROGRESS_INTERVAL = 100;
705
- for (let height = fromBlock;height <= toBlock; height++) {
706
- const result = await processBlock(def, subgraphName, height, { skipProgressUpdate: true });
833
+ const batchFailedBlocks = [];
834
+ for (let height = currentHeight;height <= batchEnd; height++) {
835
+ const blockData = batch.get(height);
836
+ if (!blockData) {
837
+ batchFailedBlocks.push({ height, reason: "block_missing" });
838
+ blocksProcessed++;
839
+ continue;
840
+ }
841
+ let result;
842
+ try {
843
+ result = await processBlock(def, subgraphName, height, {
844
+ skipProgressUpdate: true,
845
+ preloaded: blockData
846
+ });
847
+ } catch (err) {
848
+ logger4.error("Block processing error", {
849
+ subgraph: subgraphName,
850
+ blockHeight: height,
851
+ error: err instanceof Error ? err.message : String(err)
852
+ });
853
+ batchFailedBlocks.push({ height, reason: "processing_error" });
854
+ await updateSubgraphStatus2(db, subgraphName, status, height).catch(() => {});
855
+ blocksProcessed++;
856
+ totalErrors++;
857
+ continue;
858
+ }
707
859
  blocksProcessed++;
708
860
  totalEventsProcessed += result.processed;
709
861
  totalErrors += result.errors;
@@ -713,11 +865,11 @@ async function reindexSubgraph(def, opts) {
713
865
  await stats.flush(db);
714
866
  }
715
867
  }
716
- if (blocksProcessed % PROGRESS_INTERVAL === 0) {
717
- await updateSubgraphStatus2(db, subgraphName, "reindexing", height);
868
+ if (blocksProcessed % 100 === 0) {
869
+ await updateSubgraphStatus2(db, subgraphName, status, height);
718
870
  }
719
871
  if (blocksProcessed % LOG_INTERVAL === 0) {
720
- logger4.info("Reindex progress", {
872
+ logger4.info(`${status === "reindexing" ? "Reindex" : "Backfill"} progress`, {
721
873
  subgraph: subgraphName,
722
874
  processed: blocksProcessed,
723
875
  total: totalBlocks,
@@ -726,14 +878,87 @@ async function reindexSubgraph(def, opts) {
726
878
  });
727
879
  }
728
880
  }
729
- await stats.flush(db);
881
+ if (batchFailedBlocks.length > 0 && opts.subgraphId) {
882
+ const gaps = coalesceFailedBlocks(batchFailedBlocks);
883
+ await recordGapBatch(db, opts.subgraphId, subgraphName, gaps).catch((err) => {
884
+ logger4.warn("Failed to record subgraph gaps", {
885
+ subgraph: subgraphName,
886
+ error: err instanceof Error ? err.message : String(err)
887
+ });
888
+ });
889
+ }
890
+ const avg = avgEventsPerBlock(batch);
891
+ if (avg > 50)
892
+ batchSize = Math.max(Math.round(batchSize * 0.5), MIN_BATCH_SIZE);
893
+ else if (avg < 10)
894
+ batchSize = Math.min(Math.round(batchSize * 1.5), MAX_BATCH_SIZE);
895
+ currentHeight = batchEnd + 1;
896
+ }
897
+ await stats.flush(db);
898
+ return { blocksProcessed, totalEventsProcessed, totalErrors };
899
+ }
900
+ async function resolveBlockRange(db, opts) {
901
+ const fromBlock = opts?.fromBlock ?? 1;
902
+ let toBlock;
903
+ if (opts?.toBlock != null) {
904
+ toBlock = opts.toBlock;
905
+ } else {
906
+ const progress = await db.selectFrom("index_progress").selectAll().where("network", "=", process.env.NETWORK ?? "mainnet").executeTakeFirst();
907
+ toBlock = progress?.last_indexed_block ?? progress?.last_contiguous_block ?? 0;
908
+ }
909
+ return { fromBlock, toBlock };
910
+ }
911
+ async function reindexSubgraph(def, opts) {
912
+ const db = getDb2();
913
+ const client = getRawClient();
914
+ const subgraphName = def.name;
915
+ const schemaName = opts?.schemaName ?? pgSchemaName(subgraphName);
916
+ await updateSubgraphStatus2(db, subgraphName, "reindexing");
917
+ logger4.info("Reindex starting", { subgraph: subgraphName });
918
+ try {
919
+ await client.unsafe(`DROP SCHEMA IF EXISTS "${schemaName}" CASCADE`);
920
+ const { statements } = generateSubgraphSQL(def, schemaName);
921
+ for (const stmt of statements) {
922
+ await client.unsafe(stmt);
923
+ }
924
+ logger4.info("Schema recreated for reindex", { subgraph: subgraphName });
925
+ const { fromBlock, toBlock } = await resolveBlockRange(db, opts);
926
+ if (fromBlock > toBlock) {
927
+ logger4.info("No blocks to reindex", {
928
+ subgraph: subgraphName,
929
+ fromBlock,
930
+ toBlock
931
+ });
932
+ await updateSubgraphStatus2(db, subgraphName, "active", 0);
933
+ return { processed: 0 };
934
+ }
935
+ logger4.info("Reindexing blocks", {
936
+ subgraph: subgraphName,
937
+ fromBlock,
938
+ toBlock,
939
+ totalBlocks: toBlock - fromBlock + 1
940
+ });
941
+ const subgraphRow = await db.selectFrom("subgraphs").select(["id", "api_key_id"]).where("name", "=", subgraphName).executeTakeFirst();
942
+ const result = await processBlockRange(def, {
943
+ fromBlock,
944
+ toBlock,
945
+ status: "reindexing",
946
+ isCatchup: false,
947
+ apiKeyId: subgraphRow?.api_key_id ?? null,
948
+ subgraphId: subgraphRow?.id
949
+ });
730
950
  const { recordSubgraphProcessed: recordSubgraphProcessed2 } = await import("@secondlayer/shared/db/queries/subgraphs");
731
- if (totalEventsProcessed > 0 || totalErrors > 0) {
732
- await recordSubgraphProcessed2(db, subgraphName, totalEventsProcessed, totalErrors, totalErrors > 0 ? `${totalErrors} error(s) during reindex` : undefined);
951
+ if (result.totalEventsProcessed > 0 || result.totalErrors > 0) {
952
+ await recordSubgraphProcessed2(db, subgraphName, result.totalEventsProcessed, result.totalErrors, result.totalErrors > 0 ? `${result.totalErrors} error(s) during reindex` : undefined);
733
953
  }
734
954
  await updateSubgraphStatus2(db, subgraphName, "active", toBlock);
735
- logger4.info("Reindex complete", { subgraph: subgraphName, blocks: blocksProcessed, events: totalEventsProcessed, errors: totalErrors });
736
- return { processed: blocksProcessed };
955
+ logger4.info("Reindex complete", {
956
+ subgraph: subgraphName,
957
+ blocks: result.blocksProcessed,
958
+ events: result.totalEventsProcessed,
959
+ errors: result.totalErrors
960
+ });
961
+ return { processed: result.blocksProcessed };
737
962
  } catch (err) {
738
963
  logger4.error("Reindex failed", {
739
964
  subgraph: subgraphName,
@@ -743,9 +968,54 @@ async function reindexSubgraph(def, opts) {
743
968
  throw err;
744
969
  }
745
970
  }
971
+ async function backfillSubgraph(def, opts) {
972
+ const db = getDb2();
973
+ const subgraphName = def.name;
974
+ logger4.info("Backfill starting", {
975
+ subgraph: subgraphName,
976
+ from: opts.fromBlock,
977
+ to: opts.toBlock
978
+ });
979
+ try {
980
+ const subgraphRow = await db.selectFrom("subgraphs").select(["id", "api_key_id"]).where("name", "=", subgraphName).executeTakeFirst();
981
+ const result = await processBlockRange(def, {
982
+ fromBlock: opts.fromBlock,
983
+ toBlock: opts.toBlock,
984
+ status: "active",
985
+ isCatchup: false,
986
+ apiKeyId: subgraphRow?.api_key_id ?? null,
987
+ subgraphId: subgraphRow?.id
988
+ });
989
+ const resolved = await resolveGaps(db, subgraphName, opts.fromBlock, opts.toBlock).catch(() => 0);
990
+ if (resolved > 0) {
991
+ logger4.info("Resolved subgraph gaps via backfill", {
992
+ subgraph: subgraphName,
993
+ resolved
994
+ });
995
+ }
996
+ const { recordSubgraphProcessed: recordSubgraphProcessed2 } = await import("@secondlayer/shared/db/queries/subgraphs");
997
+ if (result.totalEventsProcessed > 0 || result.totalErrors > 0) {
998
+ await recordSubgraphProcessed2(db, subgraphName, result.totalEventsProcessed, result.totalErrors, result.totalErrors > 0 ? `${result.totalErrors} error(s) during backfill` : undefined);
999
+ }
1000
+ logger4.info("Backfill complete", {
1001
+ subgraph: subgraphName,
1002
+ blocks: result.blocksProcessed,
1003
+ events: result.totalEventsProcessed,
1004
+ errors: result.totalErrors
1005
+ });
1006
+ return { processed: result.blocksProcessed };
1007
+ } catch (err) {
1008
+ logger4.error("Backfill failed", {
1009
+ subgraph: subgraphName,
1010
+ error: getErrorMessage2(err)
1011
+ });
1012
+ throw err;
1013
+ }
1014
+ }
746
1015
  export {
747
- reindexSubgraph
1016
+ reindexSubgraph,
1017
+ backfillSubgraph
748
1018
  };
749
1019
 
750
- //# debugId=3876D18534CDDF5864756E2164756E21
1020
+ //# debugId=114013EAB31F34E164756E2164756E21
751
1021
  //# sourceMappingURL=reindex.js.map