@secondlayer/subgraphs 0.6.0 → 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.
@@ -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
  }
@@ -617,10 +647,14 @@ class StatsAccumulator {
617
647
  }
618
648
 
619
649
  // src/runtime/reindex.ts
650
+ import { getErrorMessage as getErrorMessage2 } from "@secondlayer/shared";
620
651
  import { getDb as getDb2, getRawClient } from "@secondlayer/shared/db";
652
+ import {
653
+ recordGapBatch,
654
+ resolveGaps
655
+ } from "@secondlayer/shared/db/queries/subgraph-gaps";
621
656
  import { updateSubgraphStatus as updateSubgraphStatus2 } from "@secondlayer/shared/db/queries/subgraphs";
622
657
  import { logger as logger4 } from "@secondlayer/shared/logger";
623
- import { getErrorMessage as getErrorMessage2 } from "@secondlayer/shared";
624
658
 
625
659
  // src/schema/generator.ts
626
660
  var TYPE_MAP = {
@@ -754,6 +788,28 @@ var LOG_INTERVAL = 1000;
754
788
  var DEFAULT_BATCH_SIZE = 500;
755
789
  var MIN_BATCH_SIZE = 100;
756
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;
803
+ } else {
804
+ ranges.push({ start, end, reason });
805
+ start = b.height;
806
+ end = b.height;
807
+ reason = b.reason;
808
+ }
809
+ }
810
+ ranges.push({ start, end, reason });
811
+ return ranges;
812
+ }
757
813
  async function processBlockRange(def, opts) {
758
814
  const db = getDb2();
759
815
  const subgraphName = def.name;
@@ -774,9 +830,11 @@ async function processBlockRange(def, opts) {
774
830
  const nextEnd = Math.min(nextStart + batchSize - 1, toBlock);
775
831
  nextBatchPromise = loadBlockRange(db, nextStart, nextEnd);
776
832
  }
833
+ const batchFailedBlocks = [];
777
834
  for (let height = currentHeight;height <= batchEnd; height++) {
778
835
  const blockData = batch.get(height);
779
836
  if (!blockData) {
837
+ batchFailedBlocks.push({ height, reason: "block_missing" });
780
838
  blocksProcessed++;
781
839
  continue;
782
840
  }
@@ -792,6 +850,7 @@ async function processBlockRange(def, opts) {
792
850
  blockHeight: height,
793
851
  error: err instanceof Error ? err.message : String(err)
794
852
  });
853
+ batchFailedBlocks.push({ height, reason: "processing_error" });
795
854
  await updateSubgraphStatus2(db, subgraphName, status, height).catch(() => {});
796
855
  blocksProcessed++;
797
856
  totalErrors++;
@@ -819,6 +878,15 @@ async function processBlockRange(def, opts) {
819
878
  });
820
879
  }
821
880
  }
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
+ }
822
890
  const avg = avgEventsPerBlock(batch);
823
891
  if (avg > 50)
824
892
  batchSize = Math.max(Math.round(batchSize * 0.5), MIN_BATCH_SIZE);
@@ -856,28 +924,46 @@ async function reindexSubgraph(def, opts) {
856
924
  logger4.info("Schema recreated for reindex", { subgraph: subgraphName });
857
925
  const { fromBlock, toBlock } = await resolveBlockRange(db, opts);
858
926
  if (fromBlock > toBlock) {
859
- logger4.info("No blocks to reindex", { subgraph: subgraphName, fromBlock, toBlock });
927
+ logger4.info("No blocks to reindex", {
928
+ subgraph: subgraphName,
929
+ fromBlock,
930
+ toBlock
931
+ });
860
932
  await updateSubgraphStatus2(db, subgraphName, "active", 0);
861
933
  return { processed: 0 };
862
934
  }
863
- logger4.info("Reindexing blocks", { subgraph: subgraphName, fromBlock, toBlock, totalBlocks: toBlock - fromBlock + 1 });
864
- const subgraphRow = await db.selectFrom("subgraphs").select("api_key_id").where("name", "=", subgraphName).executeTakeFirst();
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();
865
942
  const result = await processBlockRange(def, {
866
943
  fromBlock,
867
944
  toBlock,
868
945
  status: "reindexing",
869
946
  isCatchup: false,
870
- apiKeyId: subgraphRow?.api_key_id ?? null
947
+ apiKeyId: subgraphRow?.api_key_id ?? null,
948
+ subgraphId: subgraphRow?.id
871
949
  });
872
950
  const { recordSubgraphProcessed: recordSubgraphProcessed2 } = await import("@secondlayer/shared/db/queries/subgraphs");
873
951
  if (result.totalEventsProcessed > 0 || result.totalErrors > 0) {
874
952
  await recordSubgraphProcessed2(db, subgraphName, result.totalEventsProcessed, result.totalErrors, result.totalErrors > 0 ? `${result.totalErrors} error(s) during reindex` : undefined);
875
953
  }
876
954
  await updateSubgraphStatus2(db, subgraphName, "active", toBlock);
877
- logger4.info("Reindex complete", { subgraph: subgraphName, blocks: result.blocksProcessed, events: result.totalEventsProcessed, errors: result.totalErrors });
955
+ logger4.info("Reindex complete", {
956
+ subgraph: subgraphName,
957
+ blocks: result.blocksProcessed,
958
+ events: result.totalEventsProcessed,
959
+ errors: result.totalErrors
960
+ });
878
961
  return { processed: result.blocksProcessed };
879
962
  } catch (err) {
880
- logger4.error("Reindex failed", { subgraph: subgraphName, error: getErrorMessage2(err) });
963
+ logger4.error("Reindex failed", {
964
+ subgraph: subgraphName,
965
+ error: getErrorMessage2(err)
966
+ });
881
967
  await updateSubgraphStatus2(db, subgraphName, "error");
882
968
  throw err;
883
969
  }
@@ -885,24 +971,44 @@ async function reindexSubgraph(def, opts) {
885
971
  async function backfillSubgraph(def, opts) {
886
972
  const db = getDb2();
887
973
  const subgraphName = def.name;
888
- logger4.info("Backfill starting", { subgraph: subgraphName, from: opts.fromBlock, to: opts.toBlock });
974
+ logger4.info("Backfill starting", {
975
+ subgraph: subgraphName,
976
+ from: opts.fromBlock,
977
+ to: opts.toBlock
978
+ });
889
979
  try {
890
- const subgraphRow = await db.selectFrom("subgraphs").select("api_key_id").where("name", "=", subgraphName).executeTakeFirst();
980
+ const subgraphRow = await db.selectFrom("subgraphs").select(["id", "api_key_id"]).where("name", "=", subgraphName).executeTakeFirst();
891
981
  const result = await processBlockRange(def, {
892
982
  fromBlock: opts.fromBlock,
893
983
  toBlock: opts.toBlock,
894
984
  status: "active",
895
985
  isCatchup: false,
896
- apiKeyId: subgraphRow?.api_key_id ?? null
986
+ apiKeyId: subgraphRow?.api_key_id ?? null,
987
+ subgraphId: subgraphRow?.id
897
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
+ }
898
996
  const { recordSubgraphProcessed: recordSubgraphProcessed2 } = await import("@secondlayer/shared/db/queries/subgraphs");
899
997
  if (result.totalEventsProcessed > 0 || result.totalErrors > 0) {
900
998
  await recordSubgraphProcessed2(db, subgraphName, result.totalEventsProcessed, result.totalErrors, result.totalErrors > 0 ? `${result.totalErrors} error(s) during backfill` : undefined);
901
999
  }
902
- logger4.info("Backfill complete", { subgraph: subgraphName, blocks: result.blocksProcessed, events: result.totalEventsProcessed, errors: result.totalErrors });
1000
+ logger4.info("Backfill complete", {
1001
+ subgraph: subgraphName,
1002
+ blocks: result.blocksProcessed,
1003
+ events: result.totalEventsProcessed,
1004
+ errors: result.totalErrors
1005
+ });
903
1006
  return { processed: result.blocksProcessed };
904
1007
  } catch (err) {
905
- logger4.error("Backfill failed", { subgraph: subgraphName, error: getErrorMessage2(err) });
1008
+ logger4.error("Backfill failed", {
1009
+ subgraph: subgraphName,
1010
+ error: getErrorMessage2(err)
1011
+ });
906
1012
  throw err;
907
1013
  }
908
1014
  }
@@ -911,5 +1017,5 @@ export {
911
1017
  backfillSubgraph
912
1018
  };
913
1019
 
914
- //# debugId=15F1B59510B2A45164756E2164756E21
1020
+ //# debugId=114013EAB31F34E164756E2164756E21
915
1021
  //# sourceMappingURL=reindex.js.map