@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
  }
@@ -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);
@@ -706,6 +761,14 @@ async function catchUpSubgraph(subgraph, subgraphName) {
706
761
  let currentHeight = startBlock;
707
762
  let nextBatchPromise = loadBlockRange(db, currentHeight, Math.min(currentHeight + batchSize - 1, chainTip));
708
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
+ }
709
772
  const batch = await nextBatchPromise;
710
773
  const batchEnd = Math.min(currentHeight + batchSize - 1, chainTip);
711
774
  const nextStart = batchEnd + 1;
@@ -713,9 +776,11 @@ async function catchUpSubgraph(subgraph, subgraphName) {
713
776
  const nextEnd = Math.min(nextStart + batchSize - 1, chainTip);
714
777
  nextBatchPromise = loadBlockRange(db, nextStart, nextEnd);
715
778
  }
779
+ const batchFailedBlocks = [];
716
780
  for (let height = currentHeight;height <= batchEnd; height++) {
717
781
  const blockData = batch.get(height);
718
782
  if (!blockData) {
783
+ batchFailedBlocks.push({ height, reason: "block_missing" });
719
784
  processed++;
720
785
  continue;
721
786
  }
@@ -730,6 +795,7 @@ async function catchUpSubgraph(subgraph, subgraphName) {
730
795
  blockHeight: height,
731
796
  error: err instanceof Error ? err.message : String(err)
732
797
  });
798
+ batchFailedBlocks.push({ height, reason: "processing_error" });
733
799
  const { updateSubgraphStatus: updateSubgraphStatus2 } = await import("@secondlayer/shared/db/queries/subgraphs");
734
800
  await updateSubgraphStatus2(db, subgraphName, "active", height).catch(() => {});
735
801
  processed++;
@@ -752,6 +818,15 @@ async function catchUpSubgraph(subgraph, subgraphName) {
752
818
  });
753
819
  }
754
820
  }
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
+ });
828
+ });
829
+ }
755
830
  const avg = avgEventsPerBlock(batch);
756
831
  batchSize = adjustBatchSize(batchSize, avg);
757
832
  currentHeight = batchEnd + 1;
@@ -768,17 +843,20 @@ async function catchUpSubgraph(subgraph, subgraphName) {
768
843
  }
769
844
 
770
845
  // src/runtime/reorg.ts
846
+ import { getErrorMessage as getErrorMessage2 } from "@secondlayer/shared";
771
847
  import { getDb as getDb3, getRawClient } from "@secondlayer/shared/db";
772
848
  import { listSubgraphs } from "@secondlayer/shared/db/queries/subgraphs";
773
849
  import { logger as logger5 } from "@secondlayer/shared/logger";
774
- import { getErrorMessage as getErrorMessage2 } from "@secondlayer/shared";
775
850
  async function handleSubgraphReorg(blockHeight, loadSubgraphDef) {
776
851
  const db = getDb3();
777
852
  const client = getRawClient();
778
853
  const activeSubgraphs = (await listSubgraphs(db)).filter((v) => v.status === "active");
779
854
  if (activeSubgraphs.length === 0)
780
855
  return;
781
- logger5.info("Propagating reorg to subgraphs", { blockHeight, subgraphCount: activeSubgraphs.length });
856
+ logger5.info("Propagating reorg to subgraphs", {
857
+ blockHeight,
858
+ subgraphCount: activeSubgraphs.length
859
+ });
782
860
  for (const sg of activeSubgraphs) {
783
861
  try {
784
862
  const schema = sg.definition.schema;
@@ -788,10 +866,16 @@ async function handleSubgraphReorg(blockHeight, loadSubgraphDef) {
788
866
  for (const tableName of Object.keys(schema)) {
789
867
  await client.unsafe(`DELETE FROM "${schemaName}"."${tableName}" WHERE "_block_height" = $1`, [blockHeight]);
790
868
  }
791
- logger5.info("Subgraph reorg cleanup done", { subgraph: sg.name, blockHeight });
869
+ logger5.info("Subgraph reorg cleanup done", {
870
+ subgraph: sg.name,
871
+ blockHeight
872
+ });
792
873
  const def = await loadSubgraphDef(sg.handler_path);
793
874
  await processBlock(def, sg.name, blockHeight);
794
- logger5.info("Subgraph reorg reprocessed", { subgraph: sg.name, blockHeight });
875
+ logger5.info("Subgraph reorg reprocessed", {
876
+ subgraph: sg.name,
877
+ blockHeight
878
+ });
795
879
  } catch (err) {
796
880
  logger5.error("Subgraph reorg handling failed", {
797
881
  subgraph: sg.name,
@@ -803,11 +887,14 @@ async function handleSubgraphReorg(blockHeight, loadSubgraphDef) {
803
887
  }
804
888
 
805
889
  // src/runtime/processor.ts
890
+ import { getErrorMessage as getErrorMessage3 } from "@secondlayer/shared";
806
891
  import { getDb as getDb4 } from "@secondlayer/shared/db";
892
+ import {
893
+ listSubgraphs as listSubgraphs2,
894
+ updateSubgraphStatus as updateSubgraphStatus2
895
+ } from "@secondlayer/shared/db/queries/subgraphs";
807
896
  import { logger as logger6 } from "@secondlayer/shared/logger";
808
- import { getErrorMessage as getErrorMessage3 } from "@secondlayer/shared";
809
897
  import { listen } from "@secondlayer/shared/queue/listener";
810
- import { listSubgraphs as listSubgraphs2, updateSubgraphStatus as updateSubgraphStatus2 } from "@secondlayer/shared/db/queries/subgraphs";
811
898
  var CHANNEL_NEW_BLOCK = "streams:new_job";
812
899
  var DEFAULT_CONCURRENCY = 5;
813
900
  var POLL_INTERVAL_MS = 5000;
@@ -910,5 +997,5 @@ export {
910
997
  startSubgraphProcessor
911
998
  };
912
999
 
913
- //# debugId=584F9781CF28817C64756E2164756E21
1000
+ //# debugId=01869B6EFB08774364756E2164756E21
914
1001
  //# sourceMappingURL=processor.js.map