@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
package/dist/src/index.js CHANGED
@@ -53,15 +53,15 @@ var SubgraphDefinitionSchema = z.object({
53
53
  description: z.string().optional(),
54
54
  sources: z.array(SubgraphSourceSchema).min(1, "Must have at least one source"),
55
55
  schema: SubgraphSchemaSchema,
56
- handlers: z.record(z.string(), z.function())
56
+ handlers: z.record(z.string(), z.any())
57
57
  });
58
58
  function validateSubgraphDefinition(def) {
59
59
  return SubgraphDefinitionSchema.parse(def);
60
60
  }
61
61
 
62
62
  // src/runtime/context.ts
63
- import { sql } from "kysely";
64
63
  import { logger } from "@secondlayer/shared/logger";
64
+ import { sql } from "kysely";
65
65
  function validateColumnName(name) {
66
66
  if (!/^[a-z_][a-z0-9_]*$/i.test(name)) {
67
67
  throw new Error(`Invalid column name: ${name}`);
@@ -102,13 +102,26 @@ class SubgraphContext {
102
102
  const keyColumns = Object.keys(key);
103
103
  const hasUniqueConstraint = tableDef.uniqueKeys?.some((uk) => uk.length === keyColumns.length && uk.every((c) => keyColumns.includes(c)));
104
104
  if (hasUniqueConstraint) {
105
- this.ops.push({ kind: "insert", table, data: { ...key, ...row, _upsert_keys: keyColumns } });
105
+ this.ops.push({
106
+ kind: "insert",
107
+ table,
108
+ data: { ...key, ...row, _upsert_keys: keyColumns }
109
+ });
106
110
  } else {
107
111
  logger.warn("upsert called without matching uniqueKeys constraint, using fallback", {
108
112
  table,
109
113
  keys: keyColumns
110
114
  });
111
- this.ops.push({ kind: "insert", table, data: { ...key, ...row, _upsert_fallback_keys: keyColumns, _upsert_fallback_set: row } });
115
+ this.ops.push({
116
+ kind: "insert",
117
+ table,
118
+ data: {
119
+ ...key,
120
+ ...row,
121
+ _upsert_fallback_keys: keyColumns,
122
+ _upsert_fallback_set: row
123
+ }
124
+ });
112
125
  }
113
126
  }
114
127
  delete(table, where) {
@@ -166,53 +179,83 @@ class SubgraphContext {
166
179
  }
167
180
  return opsToFlush.length;
168
181
  }
182
+ prepareInsert(op) {
183
+ const upsertKeys = op.data._upsert_keys;
184
+ const data = { ...op.data };
185
+ delete data._upsert_keys;
186
+ delete data._upsert_fallback_keys;
187
+ delete data._upsert_fallback_set;
188
+ data._block_height = this.block.height;
189
+ data._tx_id = this._tx.txId;
190
+ data._created_at = "NOW()";
191
+ const cols = Object.keys(data);
192
+ cols.forEach(validateColumnName);
193
+ const vals = cols.map((c) => data[c] === "NOW()" ? "NOW()" : escapeLiteral(data[c]));
194
+ const batchKey = `${op.table}:${[...cols].sort().join(",")}:${upsertKeys ? [...upsertKeys].sort().join(",") : ""}`;
195
+ return { data, cols, vals, upsertKeys, batchKey };
196
+ }
169
197
  buildStatements(ops) {
170
198
  const statements = [];
199
+ let currentBatch = null;
200
+ let currentBatchKey = "";
201
+ const flushInsertBatch = () => {
202
+ if (!currentBatch)
203
+ return;
204
+ const qualifiedTable = `"${this.pgSchemaName}"."${currentBatch.table}"`;
205
+ const colList = currentBatch.cols.map((c) => `"${c}"`).join(", ");
206
+ let rows = currentBatch.rows;
207
+ if (currentBatch.upsertKeys && currentBatch.upsertKeys.length > 0) {
208
+ const keyIndices = currentBatch.upsertKeys.map((k) => currentBatch.cols.indexOf(k));
209
+ const seen = new Map;
210
+ for (let i = 0;i < rows.length; i++) {
211
+ const key = keyIndices.map((ki) => rows[i][ki]).join("\x00");
212
+ seen.set(key, i);
213
+ }
214
+ if (seen.size < rows.length) {
215
+ rows = Array.from(seen.values()).map((i) => rows[i]);
216
+ }
217
+ }
218
+ const valuesList = rows.map((r) => `(${r.join(", ")})`).join(", ");
219
+ let stmt = `INSERT INTO ${qualifiedTable} (${colList}) VALUES ${valuesList}`;
220
+ if (currentBatch.upsertKeys && currentBatch.upsertKeys.length > 0) {
221
+ const updateCols = currentBatch.cols.filter((c) => !currentBatch.upsertKeys.includes(c) && !c.startsWith("_"));
222
+ if (updateCols.length > 0) {
223
+ const setClauses = updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`);
224
+ stmt += ` ON CONFLICT (${currentBatch.upsertKeys.map((k) => `"${k}"`).join(", ")}) DO UPDATE SET ${setClauses.join(", ")}`;
225
+ } else {
226
+ stmt += ` ON CONFLICT (${currentBatch.upsertKeys.map((k) => `"${k}"`).join(", ")}) DO NOTHING`;
227
+ }
228
+ }
229
+ statements.push(stmt);
230
+ currentBatch = null;
231
+ currentBatchKey = "";
232
+ };
171
233
  for (const op of ops) {
172
234
  const qualifiedTable = `"${this.pgSchemaName}"."${op.table}"`;
173
- switch (op.kind) {
174
- case "insert": {
175
- const upsertKeys = op.data._upsert_keys;
176
- const fallbackKeys = op.data._upsert_fallback_keys;
177
- const fallbackSet = op.data._upsert_fallback_set;
178
- const data = { ...op.data };
179
- delete data._upsert_keys;
180
- delete data._upsert_fallback_keys;
181
- delete data._upsert_fallback_set;
182
- data._block_height = this.block.height;
183
- data._tx_id = this._tx.txId;
184
- data._created_at = "NOW()";
185
- const cols = Object.keys(data);
186
- cols.forEach(validateColumnName);
187
- const vals = cols.map((c) => data[c] === "NOW()" ? "NOW()" : escapeLiteral(data[c]));
188
- let stmt = `INSERT INTO ${qualifiedTable} (${cols.map((c) => `"${c}"`).join(", ")}) VALUES (${vals.join(", ")})`;
189
- if (upsertKeys && upsertKeys.length > 0) {
190
- const updateCols = cols.filter((c) => !upsertKeys.includes(c) && !c.startsWith("_"));
191
- if (updateCols.length > 0) {
192
- const setClauses = updateCols.map((c) => `"${c}" = EXCLUDED."${c}"`);
193
- stmt += ` ON CONFLICT (${upsertKeys.map((k) => `"${k}"`).join(", ")}) DO UPDATE SET ${setClauses.join(", ")}`;
194
- } else {
195
- stmt += ` ON CONFLICT (${upsertKeys.map((k) => `"${k}"`).join(", ")}) DO NOTHING`;
196
- }
197
- } else if (fallbackKeys && fallbackSet) {}
198
- statements.push(stmt);
199
- break;
235
+ if (op.kind === "insert") {
236
+ const { cols, vals, upsertKeys, batchKey } = this.prepareInsert(op);
237
+ if (batchKey === currentBatchKey && currentBatch) {
238
+ currentBatch.rows.push(vals);
239
+ } else {
240
+ flushInsertBatch();
241
+ currentBatch = { table: op.table, cols, rows: [vals], upsertKeys };
242
+ currentBatchKey = batchKey;
200
243
  }
201
- case "update": {
244
+ } else {
245
+ flushInsertBatch();
246
+ if (op.kind === "update") {
202
247
  const setEntries = Object.entries(op.set);
203
248
  setEntries.forEach(([k]) => validateColumnName(k));
204
249
  const setClauses = setEntries.map(([k, v]) => `"${k}" = ${escapeLiteral(v)}`);
205
250
  const { clause } = buildWhereClause(op.data);
206
251
  statements.push(`UPDATE ${qualifiedTable} SET ${setClauses.join(", ")} WHERE ${clause}`);
207
- break;
208
- }
209
- case "delete": {
252
+ } else if (op.kind === "delete") {
210
253
  const { clause } = buildWhereClause(op.data);
211
254
  statements.push(`DELETE FROM ${qualifiedTable} WHERE ${clause}`);
212
- break;
213
255
  }
214
256
  }
215
257
  }
258
+ flushInsertBatch();
216
259
  return statements;
217
260
  }
218
261
  validateTable(table) {
@@ -241,95 +284,6 @@ function buildWhereClause(where) {
241
284
  return { clause: parts.join(" AND "), values: [] };
242
285
  }
243
286
 
244
- // src/runtime/source-matcher.ts
245
- function matchPattern(value, pattern) {
246
- if (!pattern.includes("*"))
247
- return value === pattern;
248
- const regex = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
249
- return new RegExp(`^${regex}$`).test(value);
250
- }
251
- function matchSource(source, transactions, eventsByTx) {
252
- const results = [];
253
- const key = sourceKey(source);
254
- for (const tx of transactions) {
255
- if (source.type) {
256
- if (!matchPattern(tx.type, source.type))
257
- continue;
258
- const txEvents = eventsByTx.get(tx.tx_id) ?? [];
259
- let matchedEvents = txEvents;
260
- if (source.minAmount !== undefined) {
261
- const amountEvents = matchedEvents.filter((e) => {
262
- const data = e.data;
263
- const rawAmount = data?.amount;
264
- if (rawAmount === undefined)
265
- return false;
266
- const amount = BigInt(rawAmount);
267
- return amount >= source.minAmount;
268
- });
269
- if (amountEvents.length === 0)
270
- continue;
271
- matchedEvents = amountEvents;
272
- }
273
- results.push({ tx, events: matchedEvents, sourceKey: key });
274
- continue;
275
- }
276
- if (source.contract) {
277
- const txContractMatch = tx.contract_id && matchPattern(tx.contract_id, source.contract);
278
- if (source.function && tx.function_name) {
279
- if (!matchPattern(tx.function_name, source.function))
280
- continue;
281
- } else if (source.function && !tx.function_name) {
282
- continue;
283
- }
284
- const txEvents = eventsByTx.get(tx.tx_id) ?? [];
285
- let matchedEvents = txEvents;
286
- if (!txContractMatch) {
287
- matchedEvents = txEvents.filter((e) => {
288
- const data = e.data;
289
- const contractIdentifier = data?.contract_identifier;
290
- return contractIdentifier && matchPattern(contractIdentifier, source.contract);
291
- });
292
- if (matchedEvents.length === 0)
293
- continue;
294
- }
295
- if (source.event) {
296
- matchedEvents = matchedEvents.filter((e) => {
297
- if (matchPattern(e.type, source.event))
298
- return true;
299
- const data = e.data;
300
- const topic = data?.topic;
301
- return topic ? matchPattern(topic, source.event) : false;
302
- });
303
- }
304
- if (txContractMatch || matchedEvents.length > 0) {
305
- results.push({ tx, events: matchedEvents, sourceKey: key });
306
- }
307
- }
308
- }
309
- return results;
310
- }
311
- function matchSources(sources, transactions, events) {
312
- const eventsByTx = new Map;
313
- for (const event of events) {
314
- const list = eventsByTx.get(event.tx_id) ?? [];
315
- list.push(event);
316
- eventsByTx.set(event.tx_id, list);
317
- }
318
- const seen = new Set;
319
- const results = [];
320
- for (const source of sources) {
321
- const matches = matchSource(source, transactions, eventsByTx);
322
- for (const match of matches) {
323
- const dedupeKey = `${match.tx.tx_id}:${match.sourceKey}`;
324
- if (!seen.has(dedupeKey)) {
325
- seen.add(dedupeKey);
326
- results.push(match);
327
- }
328
- }
329
- }
330
- return results;
331
- }
332
-
333
287
  // src/runtime/clarity.ts
334
288
  import { cvToJSON, deserializeCV } from "@secondlayer/stacks/clarity";
335
289
  function decodeClarityValue(hex) {
@@ -362,8 +316,8 @@ function decodeFunctionArgs(args) {
362
316
  }
363
317
 
364
318
  // src/runtime/runner.ts
365
- import { logger as logger2 } from "@secondlayer/shared/logger";
366
319
  import { getErrorMessage } from "@secondlayer/shared";
320
+ import { logger as logger2 } from "@secondlayer/shared/logger";
367
321
  var DEFAULT_ERROR_THRESHOLD = 50;
368
322
  function resolveHandler(handlers, key) {
369
323
  return handlers[key] ?? handlers["*"] ?? null;
@@ -375,7 +329,11 @@ async function runHandlers(subgraph, matched, ctx, opts) {
375
329
  for (const { tx, events, sourceKey: sourceKey2 } of matched) {
376
330
  const handler = resolveHandler(subgraph.handlers, sourceKey2);
377
331
  if (!handler) {
378
- logger2.warn("No handler found for source key", { subgraph: subgraph.name, sourceKey: sourceKey2, txId: tx.tx_id });
332
+ logger2.warn("No handler found for source key", {
333
+ subgraph: subgraph.name,
334
+ sourceKey: sourceKey2,
335
+ txId: tx.tx_id
336
+ });
379
337
  continue;
380
338
  }
381
339
  ctx.setTx({
@@ -452,16 +410,109 @@ async function runHandlers(subgraph, matched, ctx, opts) {
452
410
  return { processed, errors };
453
411
  }
454
412
 
413
+ // src/runtime/source-matcher.ts
414
+ function matchPattern(value, pattern) {
415
+ if (!pattern.includes("*"))
416
+ return value === pattern;
417
+ const regex = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
418
+ return new RegExp(`^${regex}$`).test(value);
419
+ }
420
+ function matchSource(source, transactions, eventsByTx) {
421
+ const results = [];
422
+ const key = sourceKey(source);
423
+ for (const tx of transactions) {
424
+ if (source.type) {
425
+ if (!matchPattern(tx.type, source.type))
426
+ continue;
427
+ const txEvents = eventsByTx.get(tx.tx_id) ?? [];
428
+ let matchedEvents = txEvents;
429
+ if (source.minAmount !== undefined) {
430
+ const amountEvents = matchedEvents.filter((e) => {
431
+ const data = e.data;
432
+ const rawAmount = data?.amount;
433
+ if (rawAmount === undefined)
434
+ return false;
435
+ const amount = BigInt(rawAmount);
436
+ return amount >= source.minAmount;
437
+ });
438
+ if (amountEvents.length === 0)
439
+ continue;
440
+ matchedEvents = amountEvents;
441
+ }
442
+ results.push({ tx, events: matchedEvents, sourceKey: key });
443
+ continue;
444
+ }
445
+ if (source.contract) {
446
+ const txContractMatch = tx.contract_id && matchPattern(tx.contract_id, source.contract);
447
+ if (source.function && tx.function_name) {
448
+ if (!matchPattern(tx.function_name, source.function))
449
+ continue;
450
+ } else if (source.function && !tx.function_name) {
451
+ continue;
452
+ }
453
+ const txEvents = eventsByTx.get(tx.tx_id) ?? [];
454
+ let matchedEvents = txEvents;
455
+ if (!txContractMatch) {
456
+ matchedEvents = txEvents.filter((e) => {
457
+ const data = e.data;
458
+ const contractIdentifier = data?.contract_identifier;
459
+ return contractIdentifier && matchPattern(contractIdentifier, source.contract);
460
+ });
461
+ if (matchedEvents.length === 0)
462
+ continue;
463
+ }
464
+ if (source.event) {
465
+ matchedEvents = matchedEvents.filter((e) => {
466
+ if (matchPattern(e.type, source.event))
467
+ return true;
468
+ const data = e.data;
469
+ const topic = data?.topic;
470
+ return topic ? matchPattern(topic, source.event) : false;
471
+ });
472
+ }
473
+ if (txContractMatch || matchedEvents.length > 0) {
474
+ results.push({ tx, events: matchedEvents, sourceKey: key });
475
+ }
476
+ }
477
+ }
478
+ return results;
479
+ }
480
+ function matchSources(sources, transactions, events) {
481
+ const eventsByTx = new Map;
482
+ for (const event of events) {
483
+ const list = eventsByTx.get(event.tx_id) ?? [];
484
+ list.push(event);
485
+ eventsByTx.set(event.tx_id, list);
486
+ }
487
+ const seen = new Set;
488
+ const results = [];
489
+ for (const source of sources) {
490
+ const matches = matchSource(source, transactions, eventsByTx);
491
+ for (const match of matches) {
492
+ const dedupeKey = `${match.tx.tx_id}:${match.sourceKey}`;
493
+ if (!seen.has(dedupeKey)) {
494
+ seen.add(dedupeKey);
495
+ results.push(match);
496
+ }
497
+ }
498
+ }
499
+ return results;
500
+ }
501
+
455
502
  // src/runtime/block-processor.ts
456
- import { sql as sql2 } from "kysely";
457
503
  import { getDb } from "@secondlayer/shared/db";
504
+ import {
505
+ recordSubgraphProcessed,
506
+ updateSubgraphStatus
507
+ } from "@secondlayer/shared/db/queries/subgraphs";
458
508
  import { logger as logger3 } from "@secondlayer/shared/logger";
459
- import { updateSubgraphStatus, recordSubgraphProcessed } from "@secondlayer/shared/db/queries/subgraphs";
509
+ import { sql as sql2 } from "kysely";
460
510
 
461
511
  // src/schema/utils.ts
462
512
  import { pgSchemaName } from "@secondlayer/shared/db/queries/subgraphs";
463
513
 
464
514
  // src/runtime/block-processor.ts
515
+ var schemaNameCache = new Map;
465
516
  async function processBlock(subgraph, subgraphName, blockHeight, opts) {
466
517
  const db = getDb();
467
518
  const blockStart = performance.now();
@@ -472,19 +523,32 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
472
523
  errors: 0,
473
524
  skipped: false
474
525
  };
475
- const block = await db.selectFrom("blocks").selectAll().where("height", "=", blockHeight).executeTakeFirst();
476
- if (!block) {
477
- logger3.warn("Block not found for subgraph processing", { subgraph: subgraphName, blockHeight });
478
- result.skipped = true;
479
- return result;
480
- }
481
- if (!block.canonical) {
482
- logger3.debug("Skipping non-canonical block", { subgraph: subgraphName, blockHeight });
483
- result.skipped = true;
484
- return result;
526
+ let block, txs, evts;
527
+ if (opts?.preloaded) {
528
+ block = opts.preloaded.block;
529
+ txs = opts.preloaded.txs;
530
+ evts = opts.preloaded.events;
531
+ } else {
532
+ block = await db.selectFrom("blocks").selectAll().where("height", "=", blockHeight).executeTakeFirst();
533
+ if (!block) {
534
+ logger3.warn("Block not found for subgraph processing", {
535
+ subgraph: subgraphName,
536
+ blockHeight
537
+ });
538
+ result.skipped = true;
539
+ return result;
540
+ }
541
+ if (!block.canonical) {
542
+ logger3.debug("Skipping non-canonical block", {
543
+ subgraph: subgraphName,
544
+ blockHeight
545
+ });
546
+ result.skipped = true;
547
+ return result;
548
+ }
549
+ txs = await db.selectFrom("transactions").selectAll().where("block_height", "=", blockHeight).execute();
550
+ evts = await db.selectFrom("events").selectAll().where("block_height", "=", blockHeight).execute();
485
551
  }
486
- const txs = await db.selectFrom("transactions").selectAll().where("block_height", "=", blockHeight).execute();
487
- const evts = await db.selectFrom("events").selectAll().where("block_height", "=", blockHeight).execute();
488
552
  const matched = matchSources(subgraph.sources, txs, evts);
489
553
  result.matched = matched.length;
490
554
  if (matched.length === 0) {
@@ -493,8 +557,12 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
493
557
  }
494
558
  return result;
495
559
  }
496
- const subgraphRecord = await db.selectFrom("subgraphs").select("schema_name").where("name", "=", subgraphName).executeTakeFirst();
497
- const schemaName = subgraphRecord?.schema_name ?? pgSchemaName(subgraphName);
560
+ let schemaName = schemaNameCache.get(subgraphName);
561
+ if (!schemaName) {
562
+ const subgraphRecord = await db.selectFrom("subgraphs").select("schema_name").where("name", "=", subgraphName).executeTakeFirst();
563
+ schemaName = subgraphRecord?.schema_name ?? pgSchemaName(subgraphName);
564
+ schemaNameCache.set(subgraphName, schemaName);
565
+ }
498
566
  const blockMeta = {
499
567
  height: block.height,
500
568
  hash: block.hash,
@@ -543,7 +611,11 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
543
611
  const { rows } = await sql2.raw(`SELECT COUNT(*) as count FROM "${schemaName}"."${table}"`).execute(db);
544
612
  const count = Number(rows[0].count);
545
613
  if (count >= 1e7) {
546
- logger3.warn("Subgraph table exceeds 10M rows", { subgraph: subgraphName, table, count });
614
+ logger3.warn("Subgraph table exceeds 10M rows", {
615
+ subgraph: subgraphName,
616
+ table,
617
+ count
618
+ });
547
619
  }
548
620
  }
549
621
  } catch {}
@@ -597,7 +669,7 @@ class StatsAccumulator {
597
669
  flush_time_ms: Math.round(entry.flushTimeMs),
598
670
  max_block_time_ms: Math.round(entry.maxBlockTimeMs),
599
671
  max_handler_time_ms: Math.round(entry.maxHandlerTimeMs),
600
- avg_ops_per_block: parseFloat(avgOpsPerBlock.toFixed(2)),
672
+ avg_ops_per_block: Number.parseFloat(avgOpsPerBlock.toFixed(2)),
601
673
  is_catchup: entry.isCatchup
602
674
  }).execute();
603
675
  }
@@ -619,10 +691,14 @@ class StatsAccumulator {
619
691
  }
620
692
 
621
693
  // src/runtime/reindex.ts
694
+ import { getErrorMessage as getErrorMessage2 } from "@secondlayer/shared";
622
695
  import { getDb as getDb2, getRawClient } from "@secondlayer/shared/db";
696
+ import {
697
+ recordGapBatch,
698
+ resolveGaps
699
+ } from "@secondlayer/shared/db/queries/subgraph-gaps";
623
700
  import { updateSubgraphStatus as updateSubgraphStatus2 } from "@secondlayer/shared/db/queries/subgraphs";
624
701
  import { logger as logger4 } from "@secondlayer/shared/logger";
625
- import { getErrorMessage as getErrorMessage2 } from "@secondlayer/shared";
626
702
 
627
703
  // src/schema/generator.ts
628
704
  var TYPE_MAP = {
@@ -709,45 +785,121 @@ function generateSubgraphSQL(def, schemaNameOverride) {
709
785
  return { statements, hash };
710
786
  }
711
787
 
788
+ // src/runtime/batch-loader.ts
789
+ async function loadBlockRange(db, fromHeight, toHeight) {
790
+ const [blocks, txs, events] = await Promise.all([
791
+ db.selectFrom("blocks").selectAll().where("height", ">=", fromHeight).where("height", "<=", toHeight).where("canonical", "=", true).execute(),
792
+ db.selectFrom("transactions").selectAll().where("block_height", ">=", fromHeight).where("block_height", "<=", toHeight).execute(),
793
+ db.selectFrom("events").selectAll().where("block_height", ">=", fromHeight).where("block_height", "<=", toHeight).execute()
794
+ ]);
795
+ const txsByHeight = new Map;
796
+ for (const tx of txs) {
797
+ const h = Number(tx.block_height);
798
+ const list = txsByHeight.get(h) ?? [];
799
+ list.push(tx);
800
+ txsByHeight.set(h, list);
801
+ }
802
+ const eventsByHeight = new Map;
803
+ for (const evt of events) {
804
+ const h = Number(evt.block_height);
805
+ const list = eventsByHeight.get(h) ?? [];
806
+ list.push(evt);
807
+ eventsByHeight.set(h, list);
808
+ }
809
+ const result = new Map;
810
+ for (const block of blocks) {
811
+ const h = Number(block.height);
812
+ result.set(h, {
813
+ block,
814
+ txs: txsByHeight.get(h) ?? [],
815
+ events: eventsByHeight.get(h) ?? []
816
+ });
817
+ }
818
+ return result;
819
+ }
820
+ function avgEventsPerBlock(batch) {
821
+ if (batch.size === 0)
822
+ return 0;
823
+ let totalEvents = 0;
824
+ for (const data of batch.values()) {
825
+ totalEvents += data.events.length;
826
+ }
827
+ return totalEvents / batch.size;
828
+ }
829
+
712
830
  // src/runtime/reindex.ts
713
831
  var LOG_INTERVAL = 1000;
714
- async function reindexSubgraph(def, opts) {
715
- const db = getDb2();
716
- const client = getRawClient();
717
- const subgraphName = def.name;
718
- const schemaName = opts?.schemaName ?? pgSchemaName(subgraphName);
719
- await updateSubgraphStatus2(db, subgraphName, "reindexing");
720
- logger4.info("Reindex starting", { subgraph: subgraphName });
721
- try {
722
- await client.unsafe(`DROP SCHEMA IF EXISTS "${schemaName}" CASCADE`);
723
- const { statements } = generateSubgraphSQL(def, schemaName);
724
- for (const stmt of statements) {
725
- await client.unsafe(stmt);
726
- }
727
- logger4.info("Schema recreated for reindex", { subgraph: subgraphName });
728
- const fromBlock = opts?.fromBlock ?? 1;
729
- let toBlock;
730
- if (opts?.toBlock != null) {
731
- toBlock = opts.toBlock;
832
+ var DEFAULT_BATCH_SIZE = 500;
833
+ var MIN_BATCH_SIZE = 100;
834
+ var MAX_BATCH_SIZE = 1000;
835
+ function coalesceFailedBlocks(blocks) {
836
+ if (blocks.length === 0)
837
+ return [];
838
+ blocks.sort((a, b) => a.height - b.height);
839
+ const ranges = [];
840
+ let start = blocks[0].height;
841
+ let end = blocks[0].height;
842
+ let reason = blocks[0].reason;
843
+ for (let i = 1;i < blocks.length; i++) {
844
+ const b = blocks[i];
845
+ if (b.height === end + 1 && b.reason === reason) {
846
+ end = b.height;
732
847
  } else {
733
- const progress = await db.selectFrom("index_progress").selectAll().where("network", "=", process.env.NETWORK ?? "mainnet").executeTakeFirst();
734
- toBlock = progress?.last_indexed_block ?? progress?.last_contiguous_block ?? 0;
848
+ ranges.push({ start, end, reason });
849
+ start = b.height;
850
+ end = b.height;
851
+ reason = b.reason;
735
852
  }
736
- if (fromBlock > toBlock) {
737
- logger4.info("No blocks to reindex", { subgraph: subgraphName, fromBlock, toBlock });
738
- await updateSubgraphStatus2(db, subgraphName, "active", 0);
739
- return { processed: 0 };
853
+ }
854
+ ranges.push({ start, end, reason });
855
+ return ranges;
856
+ }
857
+ async function processBlockRange(def, opts) {
858
+ const db = getDb2();
859
+ const subgraphName = def.name;
860
+ const { fromBlock, toBlock, status } = opts;
861
+ const totalBlocks = toBlock - fromBlock + 1;
862
+ const stats = new StatsAccumulator(subgraphName, opts.apiKeyId, opts.isCatchup);
863
+ let blocksProcessed = 0;
864
+ let totalEventsProcessed = 0;
865
+ let totalErrors = 0;
866
+ let batchSize = DEFAULT_BATCH_SIZE;
867
+ let currentHeight = fromBlock;
868
+ let nextBatchPromise = loadBlockRange(db, currentHeight, Math.min(currentHeight + batchSize - 1, toBlock));
869
+ while (currentHeight <= toBlock) {
870
+ const batch = await nextBatchPromise;
871
+ const batchEnd = Math.min(currentHeight + batchSize - 1, toBlock);
872
+ const nextStart = batchEnd + 1;
873
+ if (nextStart <= toBlock) {
874
+ const nextEnd = Math.min(nextStart + batchSize - 1, toBlock);
875
+ nextBatchPromise = loadBlockRange(db, nextStart, nextEnd);
740
876
  }
741
- const totalBlocks = toBlock - fromBlock + 1;
742
- logger4.info("Reindexing blocks", { subgraph: subgraphName, fromBlock, toBlock, totalBlocks });
743
- const subgraphRow = await db.selectFrom("subgraphs").select("api_key_id").where("name", "=", subgraphName).executeTakeFirst();
744
- const stats = new StatsAccumulator(subgraphName, subgraphRow?.api_key_id ?? null, false);
745
- let blocksProcessed = 0;
746
- let totalEventsProcessed = 0;
747
- let totalErrors = 0;
748
- const PROGRESS_INTERVAL = 100;
749
- for (let height = fromBlock;height <= toBlock; height++) {
750
- const result = await processBlock(def, subgraphName, height, { skipProgressUpdate: true });
877
+ const batchFailedBlocks = [];
878
+ for (let height = currentHeight;height <= batchEnd; height++) {
879
+ const blockData = batch.get(height);
880
+ if (!blockData) {
881
+ batchFailedBlocks.push({ height, reason: "block_missing" });
882
+ blocksProcessed++;
883
+ continue;
884
+ }
885
+ let result;
886
+ try {
887
+ result = await processBlock(def, subgraphName, height, {
888
+ skipProgressUpdate: true,
889
+ preloaded: blockData
890
+ });
891
+ } catch (err) {
892
+ logger4.error("Block processing error", {
893
+ subgraph: subgraphName,
894
+ blockHeight: height,
895
+ error: err instanceof Error ? err.message : String(err)
896
+ });
897
+ batchFailedBlocks.push({ height, reason: "processing_error" });
898
+ await updateSubgraphStatus2(db, subgraphName, status, height).catch(() => {});
899
+ blocksProcessed++;
900
+ totalErrors++;
901
+ continue;
902
+ }
751
903
  blocksProcessed++;
752
904
  totalEventsProcessed += result.processed;
753
905
  totalErrors += result.errors;
@@ -757,11 +909,11 @@ async function reindexSubgraph(def, opts) {
757
909
  await stats.flush(db);
758
910
  }
759
911
  }
760
- if (blocksProcessed % PROGRESS_INTERVAL === 0) {
761
- await updateSubgraphStatus2(db, subgraphName, "reindexing", height);
912
+ if (blocksProcessed % 100 === 0) {
913
+ await updateSubgraphStatus2(db, subgraphName, status, height);
762
914
  }
763
915
  if (blocksProcessed % LOG_INTERVAL === 0) {
764
- logger4.info("Reindex progress", {
916
+ logger4.info(`${status === "reindexing" ? "Reindex" : "Backfill"} progress`, {
765
917
  subgraph: subgraphName,
766
918
  processed: blocksProcessed,
767
919
  total: totalBlocks,
@@ -770,14 +922,87 @@ async function reindexSubgraph(def, opts) {
770
922
  });
771
923
  }
772
924
  }
773
- await stats.flush(db);
925
+ if (batchFailedBlocks.length > 0 && opts.subgraphId) {
926
+ const gaps = coalesceFailedBlocks(batchFailedBlocks);
927
+ await recordGapBatch(db, opts.subgraphId, subgraphName, gaps).catch((err) => {
928
+ logger4.warn("Failed to record subgraph gaps", {
929
+ subgraph: subgraphName,
930
+ error: err instanceof Error ? err.message : String(err)
931
+ });
932
+ });
933
+ }
934
+ const avg = avgEventsPerBlock(batch);
935
+ if (avg > 50)
936
+ batchSize = Math.max(Math.round(batchSize * 0.5), MIN_BATCH_SIZE);
937
+ else if (avg < 10)
938
+ batchSize = Math.min(Math.round(batchSize * 1.5), MAX_BATCH_SIZE);
939
+ currentHeight = batchEnd + 1;
940
+ }
941
+ await stats.flush(db);
942
+ return { blocksProcessed, totalEventsProcessed, totalErrors };
943
+ }
944
+ async function resolveBlockRange(db, opts) {
945
+ const fromBlock = opts?.fromBlock ?? 1;
946
+ let toBlock;
947
+ if (opts?.toBlock != null) {
948
+ toBlock = opts.toBlock;
949
+ } else {
950
+ const progress = await db.selectFrom("index_progress").selectAll().where("network", "=", process.env.NETWORK ?? "mainnet").executeTakeFirst();
951
+ toBlock = progress?.last_indexed_block ?? progress?.last_contiguous_block ?? 0;
952
+ }
953
+ return { fromBlock, toBlock };
954
+ }
955
+ async function reindexSubgraph(def, opts) {
956
+ const db = getDb2();
957
+ const client = getRawClient();
958
+ const subgraphName = def.name;
959
+ const schemaName = opts?.schemaName ?? pgSchemaName(subgraphName);
960
+ await updateSubgraphStatus2(db, subgraphName, "reindexing");
961
+ logger4.info("Reindex starting", { subgraph: subgraphName });
962
+ 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
+ const { fromBlock, toBlock } = await resolveBlockRange(db, opts);
970
+ if (fromBlock > toBlock) {
971
+ logger4.info("No blocks to reindex", {
972
+ subgraph: subgraphName,
973
+ fromBlock,
974
+ toBlock
975
+ });
976
+ await updateSubgraphStatus2(db, subgraphName, "active", 0);
977
+ return { processed: 0 };
978
+ }
979
+ logger4.info("Reindexing blocks", {
980
+ subgraph: subgraphName,
981
+ fromBlock,
982
+ toBlock,
983
+ totalBlocks: toBlock - fromBlock + 1
984
+ });
985
+ const subgraphRow = await db.selectFrom("subgraphs").select(["id", "api_key_id"]).where("name", "=", subgraphName).executeTakeFirst();
986
+ const result = await processBlockRange(def, {
987
+ fromBlock,
988
+ toBlock,
989
+ status: "reindexing",
990
+ isCatchup: false,
991
+ apiKeyId: subgraphRow?.api_key_id ?? null,
992
+ subgraphId: subgraphRow?.id
993
+ });
774
994
  const { recordSubgraphProcessed: recordSubgraphProcessed2 } = await import("@secondlayer/shared/db/queries/subgraphs");
775
- if (totalEventsProcessed > 0 || totalErrors > 0) {
776
- await recordSubgraphProcessed2(db, subgraphName, totalEventsProcessed, totalErrors, totalErrors > 0 ? `${totalErrors} error(s) during reindex` : undefined);
995
+ if (result.totalEventsProcessed > 0 || result.totalErrors > 0) {
996
+ await recordSubgraphProcessed2(db, subgraphName, result.totalEventsProcessed, result.totalErrors, result.totalErrors > 0 ? `${result.totalErrors} error(s) during reindex` : undefined);
777
997
  }
778
998
  await updateSubgraphStatus2(db, subgraphName, "active", toBlock);
779
- logger4.info("Reindex complete", { subgraph: subgraphName, blocks: blocksProcessed, events: totalEventsProcessed, errors: totalErrors });
780
- return { processed: blocksProcessed };
999
+ logger4.info("Reindex complete", {
1000
+ subgraph: subgraphName,
1001
+ blocks: result.blocksProcessed,
1002
+ events: result.totalEventsProcessed,
1003
+ errors: result.totalErrors
1004
+ });
1005
+ return { processed: result.blocksProcessed };
781
1006
  } catch (err) {
782
1007
  logger4.error("Reindex failed", {
783
1008
  subgraph: subgraphName,
@@ -787,6 +1012,50 @@ async function reindexSubgraph(def, opts) {
787
1012
  throw err;
788
1013
  }
789
1014
  }
1015
+ async function backfillSubgraph(def, opts) {
1016
+ const db = getDb2();
1017
+ const subgraphName = def.name;
1018
+ logger4.info("Backfill starting", {
1019
+ subgraph: subgraphName,
1020
+ from: opts.fromBlock,
1021
+ to: opts.toBlock
1022
+ });
1023
+ try {
1024
+ const subgraphRow = await db.selectFrom("subgraphs").select(["id", "api_key_id"]).where("name", "=", subgraphName).executeTakeFirst();
1025
+ const result = await processBlockRange(def, {
1026
+ fromBlock: opts.fromBlock,
1027
+ toBlock: opts.toBlock,
1028
+ status: "active",
1029
+ isCatchup: false,
1030
+ apiKeyId: subgraphRow?.api_key_id ?? null,
1031
+ subgraphId: subgraphRow?.id
1032
+ });
1033
+ const resolved = await resolveGaps(db, subgraphName, opts.fromBlock, opts.toBlock).catch(() => 0);
1034
+ if (resolved > 0) {
1035
+ logger4.info("Resolved subgraph gaps via backfill", {
1036
+ subgraph: subgraphName,
1037
+ resolved
1038
+ });
1039
+ }
1040
+ const { recordSubgraphProcessed: recordSubgraphProcessed2 } = await import("@secondlayer/shared/db/queries/subgraphs");
1041
+ if (result.totalEventsProcessed > 0 || result.totalErrors > 0) {
1042
+ await recordSubgraphProcessed2(db, subgraphName, result.totalEventsProcessed, result.totalErrors, result.totalErrors > 0 ? `${result.totalErrors} error(s) during backfill` : undefined);
1043
+ }
1044
+ logger4.info("Backfill complete", {
1045
+ subgraph: subgraphName,
1046
+ blocks: result.blocksProcessed,
1047
+ events: result.totalEventsProcessed,
1048
+ errors: result.totalErrors
1049
+ });
1050
+ return { processed: result.blocksProcessed };
1051
+ } catch (err) {
1052
+ logger4.error("Backfill failed", {
1053
+ subgraph: subgraphName,
1054
+ error: getErrorMessage2(err)
1055
+ });
1056
+ throw err;
1057
+ }
1058
+ }
790
1059
  // src/define.ts
791
1060
  function defineSubgraph(def) {
792
1061
  return def;
@@ -845,7 +1114,13 @@ async function deploySchema(db, def, handlerPath, opts) {
845
1114
  const regData = {
846
1115
  name: def.name,
847
1116
  version: def.version || "1.0.0",
848
- definition: toJsonSafe({ name: def.name, version: def.version, description: def.description, sources: def.sources, schema: def.schema }),
1117
+ definition: toJsonSafe({
1118
+ name: def.name,
1119
+ version: def.version,
1120
+ description: def.description,
1121
+ sources: def.sources,
1122
+ schema: def.schema
1123
+ }),
849
1124
  schemaHash: hash,
850
1125
  handlerPath,
851
1126
  apiKeyId: opts?.apiKeyId,
@@ -961,8 +1236,9 @@ export {
961
1236
  generateSubgraphSQL,
962
1237
  diffSchema,
963
1238
  deploySchema,
964
- defineSubgraph
1239
+ defineSubgraph,
1240
+ backfillSubgraph
965
1241
  };
966
1242
 
967
- //# debugId=B83B1361C4912AAD64756E2164756E21
1243
+ //# debugId=5A0D359BE0D619B264756E2164756E21
968
1244
  //# sourceMappingURL=index.js.map