@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
  }
@@ -576,10 +648,89 @@ class StatsAccumulator {
576
648
 
577
649
  // src/runtime/catchup.ts
578
650
  import { getDb as getDb2 } from "@secondlayer/shared/db";
579
- import { logger as logger4 } from "@secondlayer/shared/logger";
651
+ import {
652
+ recordGapBatch
653
+ } from "@secondlayer/shared/db/queries/subgraph-gaps";
580
654
  import { getSubgraph } from "@secondlayer/shared/db/queries/subgraphs";
655
+ import { logger as logger4 } from "@secondlayer/shared/logger";
656
+
657
+ // src/runtime/batch-loader.ts
658
+ async function loadBlockRange(db, fromHeight, toHeight) {
659
+ const [blocks, txs, events] = await Promise.all([
660
+ db.selectFrom("blocks").selectAll().where("height", ">=", fromHeight).where("height", "<=", toHeight).where("canonical", "=", true).execute(),
661
+ db.selectFrom("transactions").selectAll().where("block_height", ">=", fromHeight).where("block_height", "<=", toHeight).execute(),
662
+ db.selectFrom("events").selectAll().where("block_height", ">=", fromHeight).where("block_height", "<=", toHeight).execute()
663
+ ]);
664
+ const txsByHeight = new Map;
665
+ for (const tx of txs) {
666
+ const h = Number(tx.block_height);
667
+ const list = txsByHeight.get(h) ?? [];
668
+ list.push(tx);
669
+ txsByHeight.set(h, list);
670
+ }
671
+ const eventsByHeight = new Map;
672
+ for (const evt of events) {
673
+ const h = Number(evt.block_height);
674
+ const list = eventsByHeight.get(h) ?? [];
675
+ list.push(evt);
676
+ eventsByHeight.set(h, list);
677
+ }
678
+ const result = new Map;
679
+ for (const block of blocks) {
680
+ const h = Number(block.height);
681
+ result.set(h, {
682
+ block,
683
+ txs: txsByHeight.get(h) ?? [],
684
+ events: eventsByHeight.get(h) ?? []
685
+ });
686
+ }
687
+ return result;
688
+ }
689
+ function avgEventsPerBlock(batch) {
690
+ if (batch.size === 0)
691
+ return 0;
692
+ let totalEvents = 0;
693
+ for (const data of batch.values()) {
694
+ totalEvents += data.events.length;
695
+ }
696
+ return totalEvents / batch.size;
697
+ }
698
+
699
+ // src/runtime/catchup.ts
581
700
  var LOG_INTERVAL = 1000;
701
+ var DEFAULT_BATCH_SIZE = 500;
702
+ var MIN_BATCH_SIZE = 100;
703
+ var MAX_BATCH_SIZE = 1000;
582
704
  var catchingUp = new Set;
705
+ function coalesceGaps(blocks) {
706
+ if (blocks.length === 0)
707
+ return [];
708
+ blocks.sort((a, b) => a.height - b.height);
709
+ const ranges = [];
710
+ let start = blocks[0].height;
711
+ let end = blocks[0].height;
712
+ let reason = blocks[0].reason;
713
+ for (let i = 1;i < blocks.length; i++) {
714
+ const b = blocks[i];
715
+ if (b.height === end + 1 && b.reason === reason) {
716
+ end = b.height;
717
+ } else {
718
+ ranges.push({ start, end, reason });
719
+ start = b.height;
720
+ end = b.height;
721
+ reason = b.reason;
722
+ }
723
+ }
724
+ ranges.push({ start, end, reason });
725
+ return ranges;
726
+ }
727
+ function adjustBatchSize(current, avgEvents) {
728
+ if (avgEvents > 50)
729
+ return Math.max(Math.round(current * 0.5), MIN_BATCH_SIZE);
730
+ if (avgEvents < 10)
731
+ return Math.min(Math.round(current * 1.5), MAX_BATCH_SIZE);
732
+ return current;
733
+ }
583
734
  async function catchUpSubgraph(subgraph, subgraphName) {
584
735
  if (catchingUp.has(subgraphName))
585
736
  return 0;
@@ -606,24 +757,79 @@ async function catchUpSubgraph(subgraph, subgraphName) {
606
757
  });
607
758
  const stats = new StatsAccumulator(subgraphName, subgraphRow.api_key_id, true);
608
759
  let processed = 0;
609
- for (let height = startBlock;height <= chainTip; height++) {
610
- const result = await processBlock(subgraph, subgraphName, height);
611
- processed++;
612
- if (result.timing) {
613
- stats.record(result.timing, result.processed);
614
- if (stats.shouldFlush()) {
615
- await stats.flush(db);
760
+ let batchSize = DEFAULT_BATCH_SIZE;
761
+ let currentHeight = startBlock;
762
+ let nextBatchPromise = loadBlockRange(db, currentHeight, Math.min(currentHeight + batchSize - 1, chainTip));
763
+ while (currentHeight <= chainTip) {
764
+ const currentRow = await getSubgraph(db, subgraphName);
765
+ if (!currentRow || currentRow.status !== "active") {
766
+ logger4.info("Subgraph status changed, stopping catch-up", {
767
+ subgraph: subgraphName,
768
+ status: currentRow?.status ?? "deleted"
769
+ });
770
+ break;
771
+ }
772
+ const batch = await nextBatchPromise;
773
+ const batchEnd = Math.min(currentHeight + batchSize - 1, chainTip);
774
+ const nextStart = batchEnd + 1;
775
+ if (nextStart <= chainTip) {
776
+ const nextEnd = Math.min(nextStart + batchSize - 1, chainTip);
777
+ nextBatchPromise = loadBlockRange(db, nextStart, nextEnd);
778
+ }
779
+ const batchFailedBlocks = [];
780
+ for (let height = currentHeight;height <= batchEnd; height++) {
781
+ const blockData = batch.get(height);
782
+ if (!blockData) {
783
+ batchFailedBlocks.push({ height, reason: "block_missing" });
784
+ processed++;
785
+ continue;
786
+ }
787
+ let result;
788
+ try {
789
+ result = await processBlock(subgraph, subgraphName, height, {
790
+ preloaded: blockData
791
+ });
792
+ } catch (err) {
793
+ logger4.error("Block processing error during catch-up", {
794
+ subgraph: subgraphName,
795
+ blockHeight: height,
796
+ error: err instanceof Error ? err.message : String(err)
797
+ });
798
+ batchFailedBlocks.push({ height, reason: "processing_error" });
799
+ const { updateSubgraphStatus: updateSubgraphStatus2 } = await import("@secondlayer/shared/db/queries/subgraphs");
800
+ await updateSubgraphStatus2(db, subgraphName, "active", height).catch(() => {});
801
+ processed++;
802
+ continue;
803
+ }
804
+ processed++;
805
+ if (result.timing) {
806
+ stats.record(result.timing, result.processed);
807
+ if (stats.shouldFlush()) {
808
+ await stats.flush(db);
809
+ }
810
+ }
811
+ if (processed % LOG_INTERVAL === 0) {
812
+ logger4.info("Subgraph catch-up progress", {
813
+ subgraph: subgraphName,
814
+ processed,
815
+ total: totalBlocks,
816
+ currentBlock: height,
817
+ pct: Math.round(processed / totalBlocks * 100)
818
+ });
616
819
  }
617
820
  }
618
- if (processed % LOG_INTERVAL === 0) {
619
- logger4.info("Subgraph catch-up progress", {
620
- subgraph: subgraphName,
621
- processed,
622
- total: totalBlocks,
623
- currentBlock: height,
624
- pct: Math.round(processed / totalBlocks * 100)
821
+ if (batchFailedBlocks.length > 0) {
822
+ const gaps = coalesceGaps(batchFailedBlocks);
823
+ await recordGapBatch(db, subgraphRow.id, subgraphName, gaps).catch((err) => {
824
+ logger4.warn("Failed to record subgraph gaps", {
825
+ subgraph: subgraphName,
826
+ error: err instanceof Error ? err.message : String(err)
827
+ });
625
828
  });
626
829
  }
830
+ const avg = avgEventsPerBlock(batch);
831
+ batchSize = adjustBatchSize(batchSize, avg);
832
+ currentHeight = batchEnd + 1;
627
833
  }
628
834
  await stats.flush(db);
629
835
  logger4.info("Subgraph catch-up complete", {
@@ -639,5 +845,5 @@ export {
639
845
  catchUpSubgraph
640
846
  };
641
847
 
642
- //# debugId=2F1E08A57DC75EE464756E2164756E21
848
+ //# debugId=03A3F5AF1821816364756E2164756E21
643
849
  //# sourceMappingURL=catchup.js.map