@secondlayer/subgraphs 3.23.1 → 3.24.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.
@@ -2,7 +2,7 @@ import { createRequire } from "node:module";
2
2
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
3
 
4
4
  // src/runtime/context.ts
5
- import { logger } from "@secondlayer/shared/logger";
5
+ import { logger as logger2 } from "@secondlayer/shared/logger";
6
6
  import { formatUnits } from "@secondlayer/stacks/utils";
7
7
  import { sql } from "kysely";
8
8
 
@@ -147,6 +147,7 @@ function generateSubgraphSQL(def, schemaNameOverride) {
147
147
 
148
148
  // src/runtime/chain-read.ts
149
149
  import { getSourceDb } from "@secondlayer/shared/db";
150
+ import { logger } from "@secondlayer/shared/logger";
150
151
  import { http, createPublicClient } from "@secondlayer/stacks";
151
152
  import {
152
153
  ContractResponseError,
@@ -232,6 +233,14 @@ function createChainReadClient(ctx) {
232
233
  const pinnedTo = constant ? null : ctx.indexBlockHash;
233
234
  const cached = await db.selectFrom("chain_read_cache").select("result_hex").where("contract_id", "=", contractId).where("function_name", "=", fn.name).where("args_hash", "=", argsHash).where((eb) => pinnedTo === null ? eb("index_block_hash", "is", null) : eb("index_block_hash", "=", pinnedTo)).executeTakeFirst();
234
235
  const resultHex = cached?.result_hex ?? await gate.run(async () => {
236
+ if (!ctx.indexBlockHash) {
237
+ logger.warn("Chain read is not pinned to a block (no index_block_hash)", {
238
+ contractId,
239
+ functionName: fn.name,
240
+ blockHeight: ctx.blockHeight,
241
+ note: 'cache: "contract-constant" asserts this value cannot change; the node answered at its own tip'
242
+ });
243
+ }
235
244
  const value2 = await readContract(nodeClient(rpcUrl), {
236
245
  contract: contractId,
237
246
  functionName: fn.name,
@@ -346,7 +355,7 @@ class SubgraphContext {
346
355
  data: { ...key, ...row, ...meta, _upsert_keys: keyColumns }
347
356
  });
348
357
  } else {
349
- logger.warn("upsert called without matching uniqueKeys constraint, using fallback", {
358
+ logger2.warn("upsert called without matching uniqueKeys constraint, using fallback", {
350
359
  table,
351
360
  keys: keyColumns
352
361
  });
@@ -906,7 +915,7 @@ function decodeFunctionArgs(args) {
906
915
 
907
916
  // src/runtime/runner.ts
908
917
  import { getErrorMessage } from "@secondlayer/shared";
909
- import { logger as logger2 } from "@secondlayer/shared/logger";
918
+ import { logger as logger3 } from "@secondlayer/shared/logger";
910
919
  import {
911
920
  clarityValueToJS,
912
921
  deserializeCV as deserializeCV3,
@@ -1257,7 +1266,7 @@ async function runHandlers(subgraph, matched, ctx, opts) {
1257
1266
  units.sort((a, b) => (a.tx.tx_index ?? 0) - (b.tx.tx_index ?? 0) || (a.event?.event_index ?? -1) - (b.event?.event_index ?? -1));
1258
1267
  for (const { tx, event, sourceName } of units) {
1259
1268
  if (errors >= threshold) {
1260
- logger2.error("Subgraph error threshold reached, skipping remaining events", {
1269
+ logger3.error("Subgraph error threshold reached, skipping remaining events", {
1261
1270
  subgraph: subgraph.name,
1262
1271
  errors,
1263
1272
  threshold
@@ -1266,7 +1275,7 @@ async function runHandlers(subgraph, matched, ctx, opts) {
1266
1275
  }
1267
1276
  const handler = subgraph.handlers[sourceName] ?? subgraph.handlers["*"] ?? null;
1268
1277
  if (!handler) {
1269
- logger2.warn("No handler found for source", {
1278
+ logger3.warn("No handler found for source", {
1270
1279
  subgraph: subgraph.name,
1271
1280
  sourceName,
1272
1281
  txId: tx.tx_id
@@ -1323,7 +1332,7 @@ async function runHandlers(subgraph, matched, ctx, opts) {
1323
1332
  const verdict = validatePrintPayload(filter.prints, printPayload.topic ?? "", printPayload.data ?? {});
1324
1333
  if (!verdict.ok) {
1325
1334
  skipped++;
1326
- logger2.warn("Print payload does not match declared prints schema", {
1335
+ logger3.warn("Print payload does not match declared prints schema", {
1327
1336
  subgraph: subgraph.name,
1328
1337
  sourceName,
1329
1338
  topic: printPayload.topic,
@@ -1338,7 +1347,7 @@ async function runHandlers(subgraph, matched, ctx, opts) {
1338
1347
  } catch (err) {
1339
1348
  ctx.rollbackTo(checkpoint);
1340
1349
  errors++;
1341
- logger2.error("Subgraph handler error", {
1350
+ logger3.error("Subgraph handler error", {
1342
1351
  subgraph: subgraph.name,
1343
1352
  sourceName,
1344
1353
  txId: tx.tx_id,
@@ -1618,12 +1627,14 @@ import { resolveTraitContractIds } from "@secondlayer/shared/db/queries/contract
1618
1627
  import { advanceOperationCursor } from "@secondlayer/shared/db/queries/subgraph-operations";
1619
1628
  import {
1620
1629
  isByoSubgraph,
1630
+ recordLiveError,
1621
1631
  recordLiveProgress,
1622
1632
  recordSubgraphProcessed,
1623
1633
  resolveSubgraphDb,
1634
+ rewindLiveProgress,
1624
1635
  updateSubgraphStatus
1625
1636
  } from "@secondlayer/shared/db/queries/subgraphs";
1626
- import { logger as logger5 } from "@secondlayer/shared/logger";
1637
+ import { logger as logger6 } from "@secondlayer/shared/logger";
1627
1638
  import { sql as sql3 } from "kysely";
1628
1639
 
1629
1640
  // src/runtime/block-source.ts
@@ -1631,7 +1642,7 @@ import { getSourceDb as getSourceDb2 } from "@secondlayer/shared/db";
1631
1642
  import {
1632
1643
  IndexHttpClient
1633
1644
  } from "@secondlayer/shared/index-http";
1634
- import { logger as logger3 } from "@secondlayer/shared/logger";
1645
+ import { logger as logger4 } from "@secondlayer/shared/logger";
1635
1646
 
1636
1647
  // src/runtime/batch-loader.ts
1637
1648
  async function loadBlockRange(db, fromHeight, toHeight) {
@@ -1955,7 +1966,7 @@ class FallbackBlockSource {
1955
1966
  try {
1956
1967
  return await this.primary.getTip();
1957
1968
  } catch (err) {
1958
- logger3.warn("block source primary getTip failed — using DB tap", {
1969
+ logger4.warn("block source primary getTip failed — using DB tap", {
1959
1970
  error: err instanceof Error ? err.message : String(err)
1960
1971
  });
1961
1972
  return this.fallback.getTip();
@@ -1965,7 +1976,7 @@ class FallbackBlockSource {
1965
1976
  try {
1966
1977
  return await this.primary.loadBlockRange(fromHeight, toHeight);
1967
1978
  } catch (err) {
1968
- logger3.warn("block source primary loadBlockRange failed — using DB tap", {
1979
+ logger4.warn("block source primary loadBlockRange failed — using DB tap", {
1969
1980
  from: fromHeight,
1970
1981
  to: toHeight,
1971
1982
  error: err instanceof Error ? err.message : String(err)
@@ -1997,7 +2008,7 @@ function resolveBlockSource(subgraph) {
1997
2008
  return new FallbackBlockSource(new PublicApiBlockSource(buildHttpClient(), referencedIndexEventTypes(subgraph), canSparseScan(subgraph) ? sparseProbeTargets(subgraph) : undefined, needsTransactionData(subgraph)), postgresBlockSource);
1998
2009
  }
1999
2010
  if (process.env.SUBGRAPH_SOURCE === "streams-index" && subgraph) {
2000
- logger3.debug("Subgraph not streams-index eligible, using DB tap", {
2011
+ logger4.debug("Subgraph not streams-index eligible, using DB tap", {
2001
2012
  subgraph: subgraph.name
2002
2013
  });
2003
2014
  }
@@ -2006,7 +2017,7 @@ function resolveBlockSource(subgraph) {
2006
2017
 
2007
2018
  // src/runtime/outbox-emit.ts
2008
2019
  import { createHash as createHash2 } from "node:crypto";
2009
- import { logger as logger4 } from "@secondlayer/shared/logger";
2020
+ import { logger as logger5 } from "@secondlayer/shared/logger";
2010
2021
  var loggedKillSwitch = false;
2011
2022
  var OP_VERB = {
2012
2023
  insert: "created",
@@ -2030,7 +2041,7 @@ function stableStringify(obj) {
2030
2041
  async function emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, blockHeight) {
2031
2042
  if (!isEmitOutboxEnabled()) {
2032
2043
  if (!loggedKillSwitch) {
2033
- logger4.warn("SECONDLAYER_EMIT_OUTBOX=false — outbox emission bypassed");
2044
+ logger5.warn("SECONDLAYER_EMIT_OUTBOX=false — outbox emission bypassed");
2034
2045
  loggedKillSwitch = true;
2035
2046
  }
2036
2047
  return 0;
@@ -2328,10 +2339,20 @@ class CursorRaceLostError extends Error {
2328
2339
  this.name = "CursorRaceLostError";
2329
2340
  }
2330
2341
  }
2342
+
2343
+ class LiveCursorRaceLostError extends Error {
2344
+ constructor(subgraphName, height) {
2345
+ super(`live cursor race lost for ${subgraphName} at block ${height}`);
2346
+ this.name = "LiveCursorRaceLostError";
2347
+ }
2348
+ }
2331
2349
  function opCursorMode(opts) {
2332
2350
  const ap = opts?.atomicProgress;
2333
2351
  return ap && "operationId" in ap ? ap : undefined;
2334
2352
  }
2353
+ function isReorgRewind(opts) {
2354
+ return opts?.reorgRewind === true;
2355
+ }
2335
2356
  function statusMode(opts) {
2336
2357
  const ap = opts?.atomicProgress;
2337
2358
  return ap && "status" in ap ? ap : undefined;
@@ -2350,7 +2371,7 @@ async function processBlockWithRetry(subgraph, subgraphName, blockHeight, opts,
2350
2371
  const delay = retryDelaysMs[attempt];
2351
2372
  if (delay === undefined)
2352
2373
  break;
2353
- logger5.warn("Block processing failed, retrying", {
2374
+ logger6.warn("Block processing failed, retrying", {
2354
2375
  subgraph: subgraphName,
2355
2376
  blockHeight,
2356
2377
  attempt: attempt + 1,
@@ -2382,7 +2403,7 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
2382
2403
  } else {
2383
2404
  const data = (await resolveBlockSource(subgraph).loadBlockRange(blockHeight, blockHeight)).get(blockHeight);
2384
2405
  if (!data) {
2385
- logger5.debug("Block not found or non-canonical for subgraph processing", {
2406
+ logger6.debug("Block not found or non-canonical for subgraph processing", {
2386
2407
  subgraph: subgraphName,
2387
2408
  blockHeight
2388
2409
  });
@@ -2401,7 +2422,11 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
2401
2422
  result.matched = matched.length;
2402
2423
  if (matched.length === 0) {
2403
2424
  if (!opts?.skipProgressUpdate) {
2404
- await recordLiveProgress(targetDb, subgraphName, blockHeight);
2425
+ if (isReorgRewind(opts)) {
2426
+ await rewindLiveProgress(targetDb, subgraphName, blockHeight);
2427
+ } else {
2428
+ await recordLiveProgress(targetDb, subgraphName, blockHeight);
2429
+ }
2405
2430
  }
2406
2431
  return result;
2407
2432
  }
@@ -2420,13 +2445,23 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
2420
2445
  };
2421
2446
  let handlerMs = 0;
2422
2447
  let flushMs = 0;
2423
- const applyProgress = async (tx, rr) => {
2448
+ const applyProgress = async (tx, rr, flushedWrites) => {
2424
2449
  if (opts?.skipProgressUpdate)
2425
2450
  return;
2451
+ const rewind = isReorgRewind(opts);
2426
2452
  if (rr.errors > 0 && rr.processed === 0) {
2427
- await updateSubgraphStatus(tx, subgraphName, "error", blockHeight);
2453
+ if (rewind) {
2454
+ await updateSubgraphStatus(tx, subgraphName, "error", blockHeight);
2455
+ } else {
2456
+ await recordLiveError(tx, subgraphName, blockHeight);
2457
+ }
2458
+ } else if (rewind) {
2459
+ await rewindLiveProgress(tx, subgraphName, blockHeight);
2428
2460
  } else {
2429
- await recordLiveProgress(tx, subgraphName, blockHeight);
2461
+ const advanced = await recordLiveProgress(tx, subgraphName, blockHeight);
2462
+ if (!advanced && flushedWrites && !route.byo) {
2463
+ throw new LiveCursorRaceLostError(subgraphName, blockHeight);
2464
+ }
2430
2465
  }
2431
2466
  if (rr.processed > 0 || rr.errors > 0) {
2432
2467
  const lastError = rr.errors > 0 ? `${rr.errors} error(s) at block ${blockHeight}` : undefined;
@@ -2475,13 +2510,15 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
2475
2510
  } else if (byoOm && manifest && manifest.count > 0) {
2476
2511
  await advanceOperationCursor(tx, byoOm.operationId, blockHeight);
2477
2512
  }
2478
- await applyProgress(tx, runResult);
2513
+ await applyProgress(tx, runResult, manifest ? manifest.count > 0 : false);
2479
2514
  });
2480
2515
  } else {
2481
2516
  try {
2482
2517
  await targetDb.transaction().execute(async (tx) => {
2483
2518
  const opMode = opCursorMode(opts);
2484
- if (statusMode(opts)) {
2519
+ const sm = statusMode(opts);
2520
+ const rewind = isReorgRewind(opts);
2521
+ if (sm) {
2485
2522
  const row = await tx.selectFrom("subgraphs").select("last_processed_block").where("name", "=", subgraphName).executeTakeFirst();
2486
2523
  if (row && Number(row.last_processed_block) >= blockHeight) {
2487
2524
  result.skipped = true;
@@ -2493,6 +2530,12 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
2493
2530
  result.skipped = true;
2494
2531
  return;
2495
2532
  }
2533
+ } else if (!rewind) {
2534
+ const row = await tx.selectFrom("subgraphs").select("last_processed_block").where("name", "=", subgraphName).executeTakeFirst();
2535
+ if (row && Number(row.last_processed_block) >= blockHeight) {
2536
+ result.skipped = true;
2537
+ return;
2538
+ }
2496
2539
  }
2497
2540
  const ctx = new SubgraphContext(tx, schemaName, subgraph.schema, blockMeta, initialTx, false, journalEnabled(opts));
2498
2541
  const handlerStart = performance.now();
@@ -2510,7 +2553,6 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
2510
2553
  }
2511
2554
  flushMs = performance.now() - flushStart;
2512
2555
  }
2513
- const sm = statusMode(opts);
2514
2556
  if (sm && flushedWrites) {
2515
2557
  await updateSubgraphStatus(tx, subgraphName, sm.status, blockHeight);
2516
2558
  } else if (opMode && flushedWrites) {
@@ -2519,11 +2561,11 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
2519
2561
  throw new CursorRaceLostError(opMode.operationId, blockHeight);
2520
2562
  }
2521
2563
  }
2522
- await applyProgress(tx, runResult);
2564
+ await applyProgress(tx, runResult, flushedWrites);
2523
2565
  });
2524
2566
  } catch (err) {
2525
- if (err instanceof CursorRaceLostError) {
2526
- logger5.warn("cursor race lost — block already covered", {
2567
+ if (err instanceof CursorRaceLostError || err instanceof LiveCursorRaceLostError) {
2568
+ logger6.warn("cursor race lost — block already covered", {
2527
2569
  subgraph: subgraphName,
2528
2570
  blockHeight,
2529
2571
  error: err.message
@@ -2535,11 +2577,13 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
2535
2577
  }
2536
2578
  }
2537
2579
  const totalMs = performance.now() - blockStart;
2538
- result.timing = {
2539
- totalMs: Math.round(totalMs),
2540
- handlerMs: Math.round(handlerMs),
2541
- flushMs: Math.round(flushMs)
2542
- };
2580
+ if (!result.skipped) {
2581
+ result.timing = {
2582
+ totalMs: Math.round(totalMs),
2583
+ handlerMs: Math.round(handlerMs),
2584
+ flushMs: Math.round(flushMs)
2585
+ };
2586
+ }
2543
2587
  if (blockHeight % 1000 === 0) {
2544
2588
  try {
2545
2589
  const tables = Object.keys(subgraph.schema);
@@ -2547,7 +2591,7 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
2547
2591
  const { rows } = await sql3.raw(`SELECT n_live_tup AS count FROM pg_stat_user_tables WHERE schemaname = '${schemaName}' AND relname = '${table}'`).execute(route.dataDb);
2548
2592
  const count = Number(rows[0]?.count ?? 0);
2549
2593
  if (count >= 1e7) {
2550
- logger5.warn("Subgraph table exceeds 10M rows (estimate)", {
2594
+ logger6.warn("Subgraph table exceeds 10M rows (estimate)", {
2551
2595
  subgraph: subgraphName,
2552
2596
  table,
2553
2597
  count
@@ -2555,7 +2599,7 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
2555
2599
  }
2556
2600
  }
2557
2601
  } catch (err) {
2558
- logger5.debug("Row count sample failed", {
2602
+ logger6.debug("Row count sample failed", {
2559
2603
  subgraph: subgraphName,
2560
2604
  error: err instanceof Error ? err.message : String(err)
2561
2605
  });
@@ -2633,7 +2677,34 @@ class StatsAccumulator {
2633
2677
  // src/runtime/catchup.ts
2634
2678
  import { getTargetDb as getTargetDb2 } from "@secondlayer/shared/db";
2635
2679
  import { getSubgraph } from "@secondlayer/shared/db/queries/subgraphs";
2636
- import { logger as logger6 } from "@secondlayer/shared/logger";
2680
+ import { logger as logger7 } from "@secondlayer/shared/logger";
2681
+
2682
+ // src/runtime/catchup-leader.ts
2683
+ import {
2684
+ SUBGRAPH_CATCHUP_LOCK_KEY,
2685
+ createPostgresLeaderBackend,
2686
+ withLeaderLock
2687
+ } from "@secondlayer/shared/leader";
2688
+ import { targetListenerUrl } from "@secondlayer/shared/queue/listener";
2689
+ var catchUpLeader = false;
2690
+ function isCatchUpLeader() {
2691
+ return catchUpLeader;
2692
+ }
2693
+ function startCatchUpLeader(opts = {}) {
2694
+ return withLeaderLock(SUBGRAPH_CATCHUP_LOCK_KEY, async () => {
2695
+ catchUpLeader = true;
2696
+ await opts.onAcquire?.();
2697
+ return () => {
2698
+ catchUpLeader = false;
2699
+ };
2700
+ }, {
2701
+ pollMs: opts.pollMs,
2702
+ heartbeatMs: opts.heartbeatMs,
2703
+ createBackend: opts.createBackend ?? (() => createPostgresLeaderBackend(targetListenerUrl()))
2704
+ });
2705
+ }
2706
+
2707
+ // src/runtime/catchup.ts
2637
2708
  var LOG_INTERVAL = 1000;
2638
2709
  var STANDARD_CATCHUP_BATCH_CONFIG = {
2639
2710
  defaultBatchSize: 500,
@@ -2740,7 +2811,7 @@ async function catchUpSubgraph(subgraph, subgraphName) {
2740
2811
  const subgraphStart = Number(subgraphRow.start_block) || 1;
2741
2812
  const startBlock = Math.max(lastProcessedBlock + 1, subgraphStart);
2742
2813
  const totalBlocks = chainTip - lastProcessedBlock;
2743
- logger6.info("Subgraph catch-up starting", {
2814
+ logger7.info("Subgraph catch-up starting", {
2744
2815
  subgraph: subgraphName,
2745
2816
  from: startBlock,
2746
2817
  to: chainTip,
@@ -2755,9 +2826,16 @@ async function catchUpSubgraph(subgraph, subgraphName) {
2755
2826
  let prefetchedBatchEnd = Math.min(currentHeight + batchSize - 1, chainTip);
2756
2827
  let nextBatchPromise = batchConfig.prefetch ? source.loadBlockRange(currentHeight, prefetchedBatchEnd) : undefined;
2757
2828
  while (currentHeight <= chainTip) {
2829
+ if (!isCatchUpLeader()) {
2830
+ logger7.info("Lost catch-up leadership, stopping catch-up", {
2831
+ subgraph: subgraphName,
2832
+ processed
2833
+ });
2834
+ break;
2835
+ }
2758
2836
  const currentRow = await getSubgraph(targetDb, subgraphName);
2759
2837
  if (!currentRow || currentRow.status !== "active") {
2760
- logger6.info("Subgraph status changed, stopping catch-up", {
2838
+ logger7.info("Subgraph status changed, stopping catch-up", {
2761
2839
  subgraph: subgraphName,
2762
2840
  status: currentRow?.status ?? "deleted"
2763
2841
  });
@@ -2786,7 +2864,7 @@ async function catchUpSubgraph(subgraph, subgraphName) {
2786
2864
  blockData = (await source.loadBlockRange(height, height)).get(height);
2787
2865
  }
2788
2866
  if (!blockData) {
2789
- logger6.warn("Block missing during catch-up, deferring to next tick", {
2867
+ logger7.warn("Block missing during catch-up, deferring to next tick", {
2790
2868
  subgraph: subgraphName,
2791
2869
  blockHeight: height
2792
2870
  });
@@ -2806,12 +2884,12 @@ async function catchUpSubgraph(subgraph, subgraphName) {
2806
2884
  });
2807
2885
  } catch (err) {
2808
2886
  if (err instanceof ReorgEpochChangedError) {
2809
- logger6.info("Reorg detected mid catch-up, aborting walk without writing further progress", { subgraph: subgraphName, blockHeight: height });
2887
+ logger7.info("Reorg detected mid catch-up, aborting walk without writing further progress", { subgraph: subgraphName, blockHeight: height });
2810
2888
  stopCatchup = true;
2811
2889
  break;
2812
2890
  }
2813
2891
  const errorMsg = err instanceof Error ? err.message : String(err);
2814
- logger6.error("Block processing failed persistently during catch-up", {
2892
+ logger7.error("Block processing failed persistently during catch-up", {
2815
2893
  subgraph: subgraphName,
2816
2894
  blockHeight: height,
2817
2895
  error: errorMsg
@@ -2830,7 +2908,7 @@ async function catchUpSubgraph(subgraph, subgraphName) {
2830
2908
  }
2831
2909
  }
2832
2910
  if (processed % LOG_INTERVAL === 0) {
2833
- logger6.info("Subgraph catch-up progress", {
2911
+ logger7.info("Subgraph catch-up progress", {
2834
2912
  subgraph: subgraphName,
2835
2913
  processed,
2836
2914
  total: totalBlocks,
@@ -2846,7 +2924,7 @@ async function catchUpSubgraph(subgraph, subgraphName) {
2846
2924
  currentHeight = batchEnd + 1;
2847
2925
  }
2848
2926
  await stats.flush(targetDb);
2849
- logger6.info("Subgraph catch-up complete", {
2927
+ logger7.info("Subgraph catch-up complete", {
2850
2928
  subgraph: subgraphName,
2851
2929
  processed
2852
2930
  });
@@ -2863,13 +2941,14 @@ import {
2863
2941
  listSubgraphs,
2864
2942
  resolveSubgraphRawClient
2865
2943
  } from "@secondlayer/shared/db/queries/subgraphs";
2866
- import { logger as logger7 } from "@secondlayer/shared/logger";
2944
+ import { logger as logger8 } from "@secondlayer/shared/logger";
2945
+ import { sql as sql4 } from "kysely";
2867
2946
  async function handleSubgraphReorg(blockHeight, loadSubgraphDef) {
2868
2947
  const targetDb = getTargetDb3();
2869
2948
  const activeSubgraphs = (await listSubgraphs(targetDb)).filter((v) => v.status === "active");
2870
2949
  if (activeSubgraphs.length === 0)
2871
2950
  return;
2872
- logger7.info("Propagating reorg to subgraphs", {
2951
+ logger8.info("Propagating reorg to subgraphs", {
2873
2952
  blockHeight,
2874
2953
  subgraphCount: activeSubgraphs.length
2875
2954
  });
@@ -2880,100 +2959,13 @@ async function handleSubgraphReorg(blockHeight, loadSubgraphDef) {
2880
2959
  continue;
2881
2960
  bumpReorgEpoch(sg.name);
2882
2961
  await withSubgraphBlockLock(sg.name, async () => {
2883
- const schemaName = sg.schema_name ?? pgSchemaName(sg.name);
2884
- const client = resolveSubgraphRawClient(sg);
2885
- const tableNames = Object.keys(schema);
2886
- const revertedByTable = {};
2887
- for (const tableName of tableNames) {
2888
- const rows = await client.unsafe(`SELECT * FROM "${schemaName}"."${tableName}" WHERE "_block_height" >= $1 LIMIT 1000`, [blockHeight]);
2889
- if (rows.length > 0)
2890
- revertedByTable[tableName] = rows;
2891
- }
2892
- const hasJournal = (await client.unsafe(`SELECT to_regclass('"${schemaName}"."_journal"') AS r`))[0]?.r;
2893
- if (hasJournal) {
2894
- await client.begin(async (tx) => {
2895
- for (const tableName of tableNames) {
2896
- const earliest = `
2897
- SELECT DISTINCT ON (row_key) row_key, prev_row
2898
- FROM "${schemaName}"."_journal"
2899
- WHERE block_height >= $1 AND table_name = $2
2900
- ORDER BY row_key, _jid ASC`;
2901
- await tx.unsafe(`DELETE FROM "${schemaName}"."${tableName}" t USING (${earliest}) e WHERE to_jsonb(t.*) @> e.row_key`, [blockHeight, tableName]);
2902
- await tx.unsafe(`DELETE FROM "${schemaName}"."${tableName}" WHERE "_block_height" >= $1`, [blockHeight]);
2903
- await tx.unsafe(`INSERT INTO "${schemaName}"."${tableName}"
2904
- SELECT r.* FROM (${earliest}) e
2905
- CROSS JOIN LATERAL jsonb_populate_record(NULL::"${schemaName}"."${tableName}", e.prev_row) r
2906
- WHERE e.prev_row IS NOT NULL`, [blockHeight, tableName]);
2907
- }
2908
- await tx.unsafe(`DELETE FROM "${schemaName}"."_journal" WHERE block_height >= $1`, [blockHeight]);
2909
- });
2910
- } else {
2911
- logger7.warn("Subgraph has no revert journal — falling back to height delete (accumulator rows may lose history)", { subgraph: sg.name, blockHeight });
2912
- for (const tableName of tableNames) {
2913
- await client.unsafe(`DELETE FROM "${schemaName}"."${tableName}" WHERE "_block_height" >= $1`, [blockHeight]);
2914
- }
2915
- }
2916
- await client.unsafe(`DO $$ BEGIN
2917
- IF to_regclass('"${schemaName}"."_factory_addresses"') IS NOT NULL THEN
2918
- DELETE FROM "${schemaName}"."_factory_addresses" WHERE block_height >= ${Number(blockHeight)};
2919
- END IF;
2920
- END $$`);
2921
- for (const [tableName, rows] of Object.entries(revertedByTable)) {
2922
- if (rows.length === 0)
2923
- continue;
2924
- await targetDb.insertInto("subscription_outbox").columns([
2925
- "subscription_id",
2926
- "subgraph_name",
2927
- "table_name",
2928
- "block_height",
2929
- "event_type",
2930
- "payload",
2931
- "dedup_key",
2932
- "row_pk",
2933
- "status"
2934
- ]).expression((eb) => eb.selectFrom("subscriptions").select((eb2) => [
2935
- "id as subscription_id",
2936
- eb2.val(sg.name).as("subgraph_name"),
2937
- eb2.val(tableName).as("table_name"),
2938
- eb2.val(blockHeight).as("block_height"),
2939
- eb2.val(`${sg.name}.${tableName}.reverted`).as("event_type"),
2940
- eb2.val(JSON.stringify({
2941
- subgraph: sg.name,
2942
- table: tableName,
2943
- reorgFromHeight: blockHeight,
2944
- rows
2945
- })).as("payload"),
2946
- eb2.val(`reorg:${sg.name}:${tableName}:${blockHeight}`).as("dedup_key"),
2947
- eb2.val({
2948
- subgraph: sg.name,
2949
- table: tableName,
2950
- reorgRoot: blockHeight
2951
- }).as("row_pk"),
2952
- eb2.val("pending").as("status")
2953
- ]).where("subgraph_name", "=", sg.name).where("table_name", "=", tableName).where("status", "=", "active")).onConflict((oc) => oc.columns(["subscription_id", "dedup_key"]).doNothing()).execute().catch((err) => {
2954
- logger7.error("Failed to emit revert event for subscriptions", {
2955
- event: "reorg_revert_emit_dropped",
2956
- subgraph: sg.name,
2957
- table: tableName,
2958
- blockHeight,
2959
- error: getErrorMessage2(err)
2960
- });
2961
- });
2962
- }
2963
- logger7.info("Subgraph reorg cleanup done", {
2964
- subgraph: sg.name,
2965
- blockHeight,
2966
- tablesAffected: Object.keys(revertedByTable).length
2967
- });
2968
- const def = await loadSubgraphDef(sg);
2969
- await processBlock(def, sg.name, blockHeight);
2970
- logger7.info("Subgraph reorg reprocessed", {
2971
- subgraph: sg.name,
2972
- blockHeight
2962
+ await getTargetDb3().transaction().execute(async (lockTx) => {
2963
+ await sql4`SELECT pg_advisory_xact_lock(hashtext(${`subgraph-reorg:${sg.name}`}))`.execute(lockTx);
2964
+ await reorgOneSubgraph(sg, schema, blockHeight, loadSubgraphDef);
2973
2965
  });
2974
2966
  });
2975
2967
  } catch (err) {
2976
- logger7.error("Subgraph reorg handling failed", {
2968
+ logger8.error("Subgraph reorg handling failed", {
2977
2969
  subgraph: sg.name,
2978
2970
  blockHeight,
2979
2971
  error: getErrorMessage2(err)
@@ -2981,9 +2973,103 @@ async function handleSubgraphReorg(blockHeight, loadSubgraphDef) {
2981
2973
  }
2982
2974
  }
2983
2975
  }
2976
+ async function reorgOneSubgraph(sg, schema, blockHeight, loadSubgraphDef) {
2977
+ const targetDb = getTargetDb3();
2978
+ const schemaName = sg.schema_name ?? pgSchemaName(sg.name);
2979
+ const client = resolveSubgraphRawClient(sg);
2980
+ const tableNames = Object.keys(schema);
2981
+ const revertedByTable = {};
2982
+ for (const tableName of tableNames) {
2983
+ const rows = await client.unsafe(`SELECT * FROM "${schemaName}"."${tableName}" WHERE "_block_height" >= $1 LIMIT 1000`, [blockHeight]);
2984
+ if (rows.length > 0)
2985
+ revertedByTable[tableName] = rows;
2986
+ }
2987
+ const hasJournal = (await client.unsafe(`SELECT to_regclass('"${schemaName}"."_journal"') AS r`))[0]?.r;
2988
+ if (hasJournal) {
2989
+ await client.begin(async (tx) => {
2990
+ for (const tableName of tableNames) {
2991
+ const earliest = `
2992
+ SELECT DISTINCT ON (row_key) row_key, prev_row
2993
+ FROM "${schemaName}"."_journal"
2994
+ WHERE block_height >= $1 AND table_name = $2
2995
+ ORDER BY row_key, _jid ASC`;
2996
+ await tx.unsafe(`DELETE FROM "${schemaName}"."${tableName}" t USING (${earliest}) e WHERE to_jsonb(t.*) @> e.row_key`, [blockHeight, tableName]);
2997
+ await tx.unsafe(`DELETE FROM "${schemaName}"."${tableName}" WHERE "_block_height" >= $1`, [blockHeight]);
2998
+ await tx.unsafe(`INSERT INTO "${schemaName}"."${tableName}"
2999
+ SELECT r.* FROM (${earliest}) e
3000
+ CROSS JOIN LATERAL jsonb_populate_record(NULL::"${schemaName}"."${tableName}", e.prev_row) r
3001
+ WHERE e.prev_row IS NOT NULL`, [blockHeight, tableName]);
3002
+ }
3003
+ await tx.unsafe(`DELETE FROM "${schemaName}"."_journal" WHERE block_height >= $1`, [blockHeight]);
3004
+ });
3005
+ } else {
3006
+ logger8.warn("Subgraph has no revert journal — falling back to height delete (accumulator rows may lose history)", { subgraph: sg.name, blockHeight });
3007
+ for (const tableName of tableNames) {
3008
+ await client.unsafe(`DELETE FROM "${schemaName}"."${tableName}" WHERE "_block_height" >= $1`, [blockHeight]);
3009
+ }
3010
+ }
3011
+ await client.unsafe(`DO $$ BEGIN
3012
+ IF to_regclass('"${schemaName}"."_factory_addresses"') IS NOT NULL THEN
3013
+ DELETE FROM "${schemaName}"."_factory_addresses" WHERE block_height >= ${Number(blockHeight)};
3014
+ END IF;
3015
+ END $$`);
3016
+ for (const [tableName, rows] of Object.entries(revertedByTable)) {
3017
+ if (rows.length === 0)
3018
+ continue;
3019
+ await targetDb.insertInto("subscription_outbox").columns([
3020
+ "subscription_id",
3021
+ "subgraph_name",
3022
+ "table_name",
3023
+ "block_height",
3024
+ "event_type",
3025
+ "payload",
3026
+ "dedup_key",
3027
+ "row_pk",
3028
+ "status"
3029
+ ]).expression((eb) => eb.selectFrom("subscriptions").select((eb2) => [
3030
+ "id as subscription_id",
3031
+ eb2.val(sg.name).as("subgraph_name"),
3032
+ eb2.val(tableName).as("table_name"),
3033
+ eb2.val(blockHeight).as("block_height"),
3034
+ eb2.val(`${sg.name}.${tableName}.reverted`).as("event_type"),
3035
+ eb2.val(JSON.stringify({
3036
+ subgraph: sg.name,
3037
+ table: tableName,
3038
+ reorgFromHeight: blockHeight,
3039
+ rows
3040
+ })).as("payload"),
3041
+ eb2.val(`reorg:${sg.name}:${tableName}:${blockHeight}`).as("dedup_key"),
3042
+ eb2.val({
3043
+ subgraph: sg.name,
3044
+ table: tableName,
3045
+ reorgRoot: blockHeight
3046
+ }).as("row_pk"),
3047
+ eb2.val("pending").as("status")
3048
+ ]).where("subgraph_name", "=", sg.name).where("table_name", "=", tableName).where("status", "=", "active")).onConflict((oc) => oc.columns(["subscription_id", "dedup_key"]).doNothing()).execute().catch((err) => {
3049
+ logger8.error("Failed to emit revert event for subscriptions", {
3050
+ event: "reorg_revert_emit_dropped",
3051
+ subgraph: sg.name,
3052
+ table: tableName,
3053
+ blockHeight,
3054
+ error: getErrorMessage2(err)
3055
+ });
3056
+ });
3057
+ }
3058
+ logger8.info("Subgraph reorg cleanup done", {
3059
+ subgraph: sg.name,
3060
+ blockHeight,
3061
+ tablesAffected: Object.keys(revertedByTable).length
3062
+ });
3063
+ const def = await loadSubgraphDef(sg);
3064
+ await processBlock(def, sg.name, blockHeight, { reorgRewind: true });
3065
+ logger8.info("Subgraph reorg reprocessed", {
3066
+ subgraph: sg.name,
3067
+ blockHeight
3068
+ });
3069
+ }
2984
3070
  export {
2985
3071
  handleSubgraphReorg
2986
3072
  };
2987
3073
 
2988
- //# debugId=05228F28851AE4EE64756E2164756E21
3074
+ //# debugId=314C75DB8B9D518F64756E2164756E21
2989
3075
  //# sourceMappingURL=reorg.js.map