@secondlayer/subgraphs 3.13.0 → 3.14.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.
@@ -250,23 +250,28 @@ interface ProcessBlockOptions {
250
250
  /** Pre-loaded block data — skips DB reads when provided (used by batch catch-up). */
251
251
  preloaded?: PreloadedBlockData;
252
252
  /**
253
- * Crash-safe sequential processing (the reindex path). When set:
254
- * - a block whose writes flush commits `last_processed_block = blockHeight`
255
- * (with this status) in the SAME transaction, so a crash can never leave
256
- * committed writes ahead of the checkpoint;
257
- * - a block at or below the checkpoint is skipped entirely, so a replay
258
- * (crash-resume overshoot, duplicate dispatch) can never double-apply
259
- * deltas.
260
- * Only valid for strictly ascending block walks over the subgraph's own
261
- * cursor backfill/reorg paths that legitimately revisit heights below
262
- * the cursor must not set this.
253
+ * Crash-safe sequential processing. Two checkpoint scopes:
254
+ * - `{ status }` (reindex): a written block commits
255
+ * `subgraphs.last_processed_block = blockHeight` in the SAME transaction
256
+ * as its writes; replays skip at/below the cursor. Only for strictly
257
+ * ascending walks over the subgraph's own cursor.
258
+ * - `{ operationId }` (backfill): same guarantee against the OPERATION's
259
+ * own `cursor_block` — backfills legitimately revisit heights below the
260
+ * live cursor, so they must never checkpoint (or read) the subgraph
261
+ * cursor. The advance is a CONDITIONAL monotonic UPDATE: concurrent
262
+ * writers serialize on it, and the loser's whole block tx rolls back
263
+ * (surfaced as `skipped`, never an error/gap).
264
+ * Either way: a crash can never leave committed deltas ahead of the
265
+ * relevant checkpoint.
263
266
  */
264
267
  atomicProgress?: {
265
268
  status: string
269
+ } | {
270
+ operationId: string
266
271
  };
267
272
  }
268
273
  /** Default per-block retry schedule before a failure counts as persistent. */
269
- declare const BLOCK_RETRY_DELAYS_MS: unknown;
274
+ declare const BLOCK_RETRY_DELAYS_MS: number[];
270
275
  /**
271
276
  * processBlock with bounded retries. Throws the last error once the schedule
272
277
  * is exhausted — callers decide whether that halts the walk (strict paths) or
@@ -1332,6 +1332,7 @@ function matchSources(sources, transactions, events, traitContracts = new Map) {
1332
1332
  // src/runtime/block-processor.ts
1333
1333
  import { getTargetDb } from "@secondlayer/shared/db";
1334
1334
  import { resolveTraitContractIds } from "@secondlayer/shared/db/queries/contracts";
1335
+ import { advanceOperationCursor } from "@secondlayer/shared/db/queries/subgraph-operations";
1335
1336
  import {
1336
1337
  isByoSubgraph,
1337
1338
  recordSubgraphProcessed,
@@ -1951,6 +1952,21 @@ async function resolveTraitContracts(subgraph, blockHeight, db) {
1951
1952
  }
1952
1953
  return resolved;
1953
1954
  }
1955
+
1956
+ class CursorRaceLostError extends Error {
1957
+ constructor(operationId, height) {
1958
+ super(`op ${operationId} lost cursor race at block ${height}`);
1959
+ this.name = "CursorRaceLostError";
1960
+ }
1961
+ }
1962
+ function opCursorMode(opts) {
1963
+ const ap = opts?.atomicProgress;
1964
+ return ap && "operationId" in ap ? ap : undefined;
1965
+ }
1966
+ function statusMode(opts) {
1967
+ const ap = opts?.atomicProgress;
1968
+ return ap && "status" in ap ? ap : undefined;
1969
+ }
1954
1970
  var BLOCK_RETRY_DELAYS_MS = [500, 2000, 5000];
1955
1971
  function journalEnabled(opts) {
1956
1972
  return !opts?.skipProgressUpdate;
@@ -2044,12 +2060,19 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
2044
2060
  }
2045
2061
  };
2046
2062
  if (route.byo) {
2047
- if (opts?.atomicProgress) {
2063
+ if (statusMode(opts)) {
2048
2064
  const row = await targetDb.selectFrom("subgraphs").select("last_processed_block").where("name", "=", subgraphName).executeTakeFirst();
2049
2065
  if (row && Number(row.last_processed_block) >= blockHeight) {
2050
2066
  result.skipped = true;
2051
2067
  return result;
2052
2068
  }
2069
+ } else if (opCursorMode(opts)) {
2070
+ const om = opCursorMode(opts);
2071
+ const row = await targetDb.selectFrom("subgraph_operations").select("cursor_block").where("id", "=", om.operationId).executeTakeFirst();
2072
+ if (row?.cursor_block != null && Number(row.cursor_block) >= blockHeight) {
2073
+ result.skipped = true;
2074
+ return result;
2075
+ }
2053
2076
  }
2054
2077
  let runResult = { processed: 0, errors: 0 };
2055
2078
  let manifest;
@@ -2070,41 +2093,71 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
2070
2093
  if (manifest && manifest.count > 0) {
2071
2094
  await emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, block.height);
2072
2095
  }
2073
- if (opts?.atomicProgress && manifest && manifest.count > 0) {
2074
- await updateSubgraphStatus(tx, subgraphName, opts.atomicProgress.status, blockHeight);
2096
+ const byoSm = statusMode(opts);
2097
+ const byoOm = opCursorMode(opts);
2098
+ if (byoSm && manifest && manifest.count > 0) {
2099
+ await updateSubgraphStatus(tx, subgraphName, byoSm.status, blockHeight);
2100
+ } else if (byoOm && manifest && manifest.count > 0) {
2101
+ await advanceOperationCursor(tx, byoOm.operationId, blockHeight);
2075
2102
  }
2076
2103
  await applyProgress(tx, runResult);
2077
2104
  });
2078
2105
  } else {
2079
- await targetDb.transaction().execute(async (tx) => {
2080
- if (opts?.atomicProgress) {
2081
- const row = await tx.selectFrom("subgraphs").select("last_processed_block").where("name", "=", subgraphName).executeTakeFirst();
2082
- if (row && Number(row.last_processed_block) >= blockHeight) {
2083
- result.skipped = true;
2084
- return;
2106
+ try {
2107
+ await targetDb.transaction().execute(async (tx) => {
2108
+ const opMode = opCursorMode(opts);
2109
+ if (statusMode(opts)) {
2110
+ const row = await tx.selectFrom("subgraphs").select("last_processed_block").where("name", "=", subgraphName).executeTakeFirst();
2111
+ if (row && Number(row.last_processed_block) >= blockHeight) {
2112
+ result.skipped = true;
2113
+ return;
2114
+ }
2115
+ } else if (opMode) {
2116
+ const row = await tx.selectFrom("subgraph_operations").select("cursor_block").where("id", "=", opMode.operationId).executeTakeFirst();
2117
+ if (row?.cursor_block != null && Number(row.cursor_block) >= blockHeight) {
2118
+ result.skipped = true;
2119
+ return;
2120
+ }
2085
2121
  }
2086
- }
2087
- const ctx = new SubgraphContext(tx, schemaName, subgraph.schema, blockMeta, initialTx, false, journalEnabled(opts));
2088
- const handlerStart = performance.now();
2089
- const runResult = await runHandlers(subgraph, matched, ctx);
2090
- handlerMs = performance.now() - handlerStart;
2091
- result.processed = runResult.processed;
2092
- result.errors = runResult.errors;
2093
- let flushedWrites = false;
2094
- if (ctx.pendingOps > 0) {
2095
- const flushStart = performance.now();
2096
- const manifest = await ctx.flush();
2097
- flushedWrites = manifest.count > 0;
2098
- if (manifest.count > 0) {
2099
- await emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, block.height);
2122
+ const ctx = new SubgraphContext(tx, schemaName, subgraph.schema, blockMeta, initialTx, false, journalEnabled(opts));
2123
+ const handlerStart = performance.now();
2124
+ const runResult = await runHandlers(subgraph, matched, ctx);
2125
+ handlerMs = performance.now() - handlerStart;
2126
+ result.processed = runResult.processed;
2127
+ result.errors = runResult.errors;
2128
+ let flushedWrites = false;
2129
+ if (ctx.pendingOps > 0) {
2130
+ const flushStart = performance.now();
2131
+ const manifest = await ctx.flush();
2132
+ flushedWrites = manifest.count > 0;
2133
+ if (manifest.count > 0) {
2134
+ await emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, block.height);
2135
+ }
2136
+ flushMs = performance.now() - flushStart;
2100
2137
  }
2101
- flushMs = performance.now() - flushStart;
2102
- }
2103
- if (opts?.atomicProgress && flushedWrites) {
2104
- await updateSubgraphStatus(tx, subgraphName, opts.atomicProgress.status, blockHeight);
2138
+ const sm = statusMode(opts);
2139
+ if (sm && flushedWrites) {
2140
+ await updateSubgraphStatus(tx, subgraphName, sm.status, blockHeight);
2141
+ } else if (opMode && flushedWrites) {
2142
+ const advanced = await advanceOperationCursor(tx, opMode.operationId, blockHeight);
2143
+ if (!advanced) {
2144
+ throw new CursorRaceLostError(opMode.operationId, blockHeight);
2145
+ }
2146
+ }
2147
+ await applyProgress(tx, runResult);
2148
+ });
2149
+ } catch (err) {
2150
+ if (err instanceof CursorRaceLostError) {
2151
+ logger5.warn("cursor race lost — block already covered", {
2152
+ subgraph: subgraphName,
2153
+ blockHeight,
2154
+ error: err.message
2155
+ });
2156
+ result.skipped = true;
2157
+ return result;
2105
2158
  }
2106
- await applyProgress(tx, runResult);
2107
- });
2159
+ throw err;
2160
+ }
2108
2161
  }
2109
2162
  const totalMs = performance.now() - blockStart;
2110
2163
  result.timing = {
@@ -2145,5 +2198,5 @@ export {
2145
2198
  BLOCK_RETRY_DELAYS_MS
2146
2199
  };
2147
2200
 
2148
- //# debugId=4E6EBDA2299DFCFC64756E2164756E21
2201
+ //# debugId=85BBF80E16AD899B64756E2164756E21
2149
2202
  //# sourceMappingURL=block-processor.js.map