@secondlayer/subgraphs 0.6.0 → 0.7.1

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 (39) hide show
  1. package/dist/src/index.d.ts +3 -1
  2. package/dist/src/index.js +233 -119
  3. package/dist/src/index.js.map +16 -16
  4. package/dist/src/runtime/block-processor.d.ts +2 -0
  5. package/dist/src/runtime/block-processor.js +130 -100
  6. package/dist/src/runtime/block-processor.js.map +9 -9
  7. package/dist/src/runtime/catchup.d.ts +2 -0
  8. package/dist/src/runtime/catchup.js +180 -104
  9. package/dist/src/runtime/catchup.js.map +12 -12
  10. package/dist/src/runtime/clarity.js.map +2 -2
  11. package/dist/src/runtime/context.d.ts +1 -1
  12. package/dist/src/runtime/context.js +17 -4
  13. package/dist/src/runtime/context.js.map +3 -3
  14. package/dist/src/runtime/processor.js +198 -110
  15. package/dist/src/runtime/processor.js.map +14 -14
  16. package/dist/src/runtime/reindex.d.ts +2 -0
  17. package/dist/src/runtime/reindex.js +223 -117
  18. package/dist/src/runtime/reindex.js.map +13 -13
  19. package/dist/src/runtime/reorg.d.ts +2 -0
  20. package/dist/src/runtime/reorg.js +143 -104
  21. package/dist/src/runtime/reorg.js.map +10 -10
  22. package/dist/src/runtime/runner.d.ts +3 -1
  23. package/dist/src/runtime/runner.js +7 -3
  24. package/dist/src/runtime/runner.js.map +4 -4
  25. package/dist/src/runtime/source-matcher.js.map +3 -3
  26. package/dist/src/runtime/stats.d.ts +1 -1
  27. package/dist/src/runtime/stats.js +2 -2
  28. package/dist/src/runtime/stats.js.map +3 -3
  29. package/dist/src/schema/index.d.ts +3 -1
  30. package/dist/src/schema/index.js +11 -3
  31. package/dist/src/schema/index.js.map +5 -5
  32. package/dist/src/service.js +199 -111
  33. package/dist/src/service.js.map +15 -15
  34. package/dist/src/templates.js.map +2 -2
  35. package/dist/src/types.d.ts +2 -0
  36. package/dist/src/types.js.map +2 -2
  37. package/dist/src/validate.d.ts +2 -0
  38. package/dist/src/validate.js.map +2 -2
  39. 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) {
@@ -227,95 +240,6 @@ function buildWhereClause(where) {
227
240
  return { clause: parts.join(" AND "), values: [] };
228
241
  }
229
242
 
230
- // src/runtime/source-matcher.ts
231
- function matchPattern(value, pattern) {
232
- if (!pattern.includes("*"))
233
- return value === pattern;
234
- const regex = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
235
- return new RegExp(`^${regex}$`).test(value);
236
- }
237
- function matchSource(source, transactions, eventsByTx) {
238
- const results = [];
239
- const key = sourceKey(source);
240
- for (const tx of transactions) {
241
- if (source.type) {
242
- if (!matchPattern(tx.type, source.type))
243
- continue;
244
- const txEvents = eventsByTx.get(tx.tx_id) ?? [];
245
- let matchedEvents = txEvents;
246
- if (source.minAmount !== undefined) {
247
- const amountEvents = matchedEvents.filter((e) => {
248
- const data = e.data;
249
- const rawAmount = data?.amount;
250
- if (rawAmount === undefined)
251
- return false;
252
- const amount = BigInt(rawAmount);
253
- return amount >= source.minAmount;
254
- });
255
- if (amountEvents.length === 0)
256
- continue;
257
- matchedEvents = amountEvents;
258
- }
259
- results.push({ tx, events: matchedEvents, sourceKey: key });
260
- continue;
261
- }
262
- if (source.contract) {
263
- const txContractMatch = tx.contract_id && matchPattern(tx.contract_id, source.contract);
264
- if (source.function && tx.function_name) {
265
- if (!matchPattern(tx.function_name, source.function))
266
- continue;
267
- } else if (source.function && !tx.function_name) {
268
- continue;
269
- }
270
- const txEvents = eventsByTx.get(tx.tx_id) ?? [];
271
- let matchedEvents = txEvents;
272
- if (!txContractMatch) {
273
- matchedEvents = txEvents.filter((e) => {
274
- const data = e.data;
275
- const contractIdentifier = data?.contract_identifier;
276
- return contractIdentifier && matchPattern(contractIdentifier, source.contract);
277
- });
278
- if (matchedEvents.length === 0)
279
- continue;
280
- }
281
- if (source.event) {
282
- matchedEvents = matchedEvents.filter((e) => {
283
- if (matchPattern(e.type, source.event))
284
- return true;
285
- const data = e.data;
286
- const topic = data?.topic;
287
- return topic ? matchPattern(topic, source.event) : false;
288
- });
289
- }
290
- if (txContractMatch || matchedEvents.length > 0) {
291
- results.push({ tx, events: matchedEvents, sourceKey: key });
292
- }
293
- }
294
- }
295
- return results;
296
- }
297
- function matchSources(sources, transactions, events) {
298
- const eventsByTx = new Map;
299
- for (const event of events) {
300
- const list = eventsByTx.get(event.tx_id) ?? [];
301
- list.push(event);
302
- eventsByTx.set(event.tx_id, list);
303
- }
304
- const seen = new Set;
305
- const results = [];
306
- for (const source of sources) {
307
- const matches = matchSource(source, transactions, eventsByTx);
308
- for (const match of matches) {
309
- const dedupeKey = `${match.tx.tx_id}:${match.sourceKey}`;
310
- if (!seen.has(dedupeKey)) {
311
- seen.add(dedupeKey);
312
- results.push(match);
313
- }
314
- }
315
- }
316
- return results;
317
- }
318
-
319
243
  // src/runtime/clarity.ts
320
244
  import { cvToJSON, deserializeCV } from "@secondlayer/stacks/clarity";
321
245
  function decodeClarityValue(hex) {
@@ -348,8 +272,8 @@ function decodeFunctionArgs(args) {
348
272
  }
349
273
 
350
274
  // src/runtime/runner.ts
351
- import { logger as logger2 } from "@secondlayer/shared/logger";
352
275
  import { getErrorMessage } from "@secondlayer/shared";
276
+ import { logger as logger2 } from "@secondlayer/shared/logger";
353
277
  var DEFAULT_ERROR_THRESHOLD = 50;
354
278
  function resolveHandler(handlers, key) {
355
279
  return handlers[key] ?? handlers["*"] ?? null;
@@ -361,7 +285,11 @@ async function runHandlers(subgraph, matched, ctx, opts) {
361
285
  for (const { tx, events, sourceKey: sourceKey2 } of matched) {
362
286
  const handler = resolveHandler(subgraph.handlers, sourceKey2);
363
287
  if (!handler) {
364
- 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
+ });
365
293
  continue;
366
294
  }
367
295
  ctx.setTx({
@@ -438,11 +366,103 @@ async function runHandlers(subgraph, matched, ctx, opts) {
438
366
  return { processed, errors };
439
367
  }
440
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
+
441
458
  // src/runtime/block-processor.ts
442
- import { sql as sql2 } from "kysely";
443
459
  import { getDb } from "@secondlayer/shared/db";
460
+ import {
461
+ recordSubgraphProcessed,
462
+ updateSubgraphStatus
463
+ } from "@secondlayer/shared/db/queries/subgraphs";
444
464
  import { logger as logger3 } from "@secondlayer/shared/logger";
445
- import { updateSubgraphStatus, recordSubgraphProcessed } from "@secondlayer/shared/db/queries/subgraphs";
465
+ import { sql as sql2 } from "kysely";
446
466
 
447
467
  // src/schema/utils.ts
448
468
  import { pgSchemaName } from "@secondlayer/shared/db/queries/subgraphs";
@@ -467,12 +487,18 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
467
487
  } else {
468
488
  block = await db.selectFrom("blocks").selectAll().where("height", "=", blockHeight).executeTakeFirst();
469
489
  if (!block) {
470
- logger3.warn("Block not found for subgraph processing", { subgraph: subgraphName, blockHeight });
490
+ logger3.warn("Block not found for subgraph processing", {
491
+ subgraph: subgraphName,
492
+ blockHeight
493
+ });
471
494
  result.skipped = true;
472
495
  return result;
473
496
  }
474
497
  if (!block.canonical) {
475
- logger3.debug("Skipping non-canonical block", { subgraph: subgraphName, blockHeight });
498
+ logger3.debug("Skipping non-canonical block", {
499
+ subgraph: subgraphName,
500
+ blockHeight
501
+ });
476
502
  result.skipped = true;
477
503
  return result;
478
504
  }
@@ -541,7 +567,11 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
541
567
  const { rows } = await sql2.raw(`SELECT COUNT(*) as count FROM "${schemaName}"."${table}"`).execute(db);
542
568
  const count = Number(rows[0].count);
543
569
  if (count >= 1e7) {
544
- 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
+ });
545
575
  }
546
576
  }
547
577
  } catch {}
@@ -595,7 +625,7 @@ class StatsAccumulator {
595
625
  flush_time_ms: Math.round(entry.flushTimeMs),
596
626
  max_block_time_ms: Math.round(entry.maxBlockTimeMs),
597
627
  max_handler_time_ms: Math.round(entry.maxHandlerTimeMs),
598
- avg_ops_per_block: parseFloat(avgOpsPerBlock.toFixed(2)),
628
+ avg_ops_per_block: Number.parseFloat(avgOpsPerBlock.toFixed(2)),
599
629
  is_catchup: entry.isCatchup
600
630
  }).execute();
601
631
  }
@@ -618,8 +648,11 @@ class StatsAccumulator {
618
648
 
619
649
  // src/runtime/catchup.ts
620
650
  import { getDb as getDb2 } from "@secondlayer/shared/db";
621
- import { logger as logger4 } from "@secondlayer/shared/logger";
651
+ import {
652
+ recordGapBatch
653
+ } from "@secondlayer/shared/db/queries/subgraph-gaps";
622
654
  import { getSubgraph } from "@secondlayer/shared/db/queries/subgraphs";
655
+ import { logger as logger4 } from "@secondlayer/shared/logger";
623
656
 
624
657
  // src/runtime/batch-loader.ts
625
658
  async function loadBlockRange(db, fromHeight, toHeight) {
@@ -669,6 +702,28 @@ var DEFAULT_BATCH_SIZE = 500;
669
702
  var MIN_BATCH_SIZE = 100;
670
703
  var MAX_BATCH_SIZE = 1000;
671
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
+ }
672
727
  function adjustBatchSize(current, avgEvents) {
673
728
  if (avgEvents > 50)
674
729
  return Math.max(Math.round(current * 0.5), MIN_BATCH_SIZE);
@@ -689,10 +744,11 @@ async function catchUpSubgraph(subgraph, subgraphName) {
689
744
  const progress = await db.selectFrom("index_progress").selectAll().where("network", "=", process.env.NETWORK ?? "mainnet").executeTakeFirst();
690
745
  if (!progress)
691
746
  return 0;
692
- const chainTip = Number(progress.last_contiguous_block);
747
+ const chainTip = Number(progress.highest_seen_block);
693
748
  if (lastProcessedBlock >= chainTip)
694
749
  return 0;
695
- const startBlock = lastProcessedBlock + 1;
750
+ const subgraphStart = Number(subgraphRow.start_block) || 1;
751
+ const startBlock = Math.max(lastProcessedBlock + 1, subgraphStart);
696
752
  const totalBlocks = chainTip - lastProcessedBlock;
697
753
  logger4.info("Subgraph catch-up starting", {
698
754
  subgraph: subgraphName,
@@ -706,6 +762,14 @@ async function catchUpSubgraph(subgraph, subgraphName) {
706
762
  let currentHeight = startBlock;
707
763
  let nextBatchPromise = loadBlockRange(db, currentHeight, Math.min(currentHeight + batchSize - 1, chainTip));
708
764
  while (currentHeight <= chainTip) {
765
+ const currentRow = await getSubgraph(db, subgraphName);
766
+ if (!currentRow || currentRow.status !== "active") {
767
+ logger4.info("Subgraph status changed, stopping catch-up", {
768
+ subgraph: subgraphName,
769
+ status: currentRow?.status ?? "deleted"
770
+ });
771
+ break;
772
+ }
709
773
  const batch = await nextBatchPromise;
710
774
  const batchEnd = Math.min(currentHeight + batchSize - 1, chainTip);
711
775
  const nextStart = batchEnd + 1;
@@ -713,9 +777,11 @@ async function catchUpSubgraph(subgraph, subgraphName) {
713
777
  const nextEnd = Math.min(nextStart + batchSize - 1, chainTip);
714
778
  nextBatchPromise = loadBlockRange(db, nextStart, nextEnd);
715
779
  }
780
+ const batchFailedBlocks = [];
716
781
  for (let height = currentHeight;height <= batchEnd; height++) {
717
782
  const blockData = batch.get(height);
718
783
  if (!blockData) {
784
+ batchFailedBlocks.push({ height, reason: "block_missing" });
719
785
  processed++;
720
786
  continue;
721
787
  }
@@ -730,6 +796,7 @@ async function catchUpSubgraph(subgraph, subgraphName) {
730
796
  blockHeight: height,
731
797
  error: err instanceof Error ? err.message : String(err)
732
798
  });
799
+ batchFailedBlocks.push({ height, reason: "processing_error" });
733
800
  const { updateSubgraphStatus: updateSubgraphStatus2 } = await import("@secondlayer/shared/db/queries/subgraphs");
734
801
  await updateSubgraphStatus2(db, subgraphName, "active", height).catch(() => {});
735
802
  processed++;
@@ -752,6 +819,15 @@ async function catchUpSubgraph(subgraph, subgraphName) {
752
819
  });
753
820
  }
754
821
  }
822
+ if (batchFailedBlocks.length > 0) {
823
+ const gaps = coalesceGaps(batchFailedBlocks);
824
+ await recordGapBatch(db, subgraphRow.id, subgraphName, gaps).catch((err) => {
825
+ logger4.warn("Failed to record subgraph gaps", {
826
+ subgraph: subgraphName,
827
+ error: err instanceof Error ? err.message : String(err)
828
+ });
829
+ });
830
+ }
755
831
  const avg = avgEventsPerBlock(batch);
756
832
  batchSize = adjustBatchSize(batchSize, avg);
757
833
  currentHeight = batchEnd + 1;
@@ -770,5 +846,5 @@ export {
770
846
  catchUpSubgraph
771
847
  };
772
848
 
773
- //# debugId=ECEE17CC2A59B3A164756E2164756E21
849
+ //# debugId=953B5DBF1473343764756E2164756E21
774
850
  //# sourceMappingURL=catchup.js.map