@secondlayer/subgraphs 3.11.0 → 3.13.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.
Files changed (36) hide show
  1. package/dist/src/index.d.ts +36 -1
  2. package/dist/src/index.js +542 -180
  3. package/dist/src/index.js.map +12 -12
  4. package/dist/src/runtime/block-processor.d.ts +34 -1
  5. package/dist/src/runtime/block-processor.js +501 -72
  6. package/dist/src/runtime/block-processor.js.map +9 -8
  7. package/dist/src/runtime/catchup.d.ts +10 -0
  8. package/dist/src/runtime/catchup.js +520 -118
  9. package/dist/src/runtime/catchup.js.map +10 -9
  10. package/dist/src/runtime/context.d.ts +65 -3
  11. package/dist/src/runtime/context.js +390 -8
  12. package/dist/src/runtime/context.js.map +6 -4
  13. package/dist/src/runtime/processor.js +590 -230
  14. package/dist/src/runtime/processor.js.map +13 -13
  15. package/dist/src/runtime/reindex.d.ts +14 -0
  16. package/dist/src/runtime/reindex.js +538 -180
  17. package/dist/src/runtime/reindex.js.map +10 -10
  18. package/dist/src/runtime/reorg.d.ts +10 -0
  19. package/dist/src/runtime/reorg.js +521 -73
  20. package/dist/src/runtime/reorg.js.map +10 -9
  21. package/dist/src/runtime/replay.js.map +2 -2
  22. package/dist/src/runtime/runner.d.ts +73 -2
  23. package/dist/src/runtime/runner.js +56 -58
  24. package/dist/src/runtime/runner.js.map +3 -3
  25. package/dist/src/runtime/source-matcher.d.ts +2 -0
  26. package/dist/src/runtime/source-matcher.js.map +2 -2
  27. package/dist/src/schema/index.d.ts +10 -0
  28. package/dist/src/schema/index.js +19 -1
  29. package/dist/src/schema/index.js.map +5 -5
  30. package/dist/src/service.js +590 -230
  31. package/dist/src/service.js.map +13 -13
  32. package/dist/src/types.d.ts +10 -0
  33. package/dist/src/validate.d.ts +10 -0
  34. package/dist/src/validate.js +2 -1
  35. package/dist/src/validate.js.map +3 -3
  36. package/package.json +2 -2
package/dist/src/index.js CHANGED
@@ -69,6 +69,7 @@ var SubgraphDefinitionSchema = z.object({
69
69
  version: z.string().optional(),
70
70
  description: z.string().optional(),
71
71
  startBlock: z.number().int().nonnegative().optional(),
72
+ backfillMode: z.enum(["blocking", "concurrent"]).optional(),
72
73
  sources: z.record(z.string(), SubgraphFilterSchema).refine((s) => Object.keys(s).length > 0, "Must have at least one source"),
73
74
  schema: SubgraphSchemaSchema,
74
75
  handlers: z.record(z.string(), z.any())
@@ -81,6 +82,134 @@ function validateSubgraphDefinition(def) {
81
82
  import { logger } from "@secondlayer/shared/logger";
82
83
  import { formatUnits } from "@secondlayer/stacks/utils";
83
84
  import { sql } from "kysely";
85
+
86
+ // src/schema/generator.ts
87
+ import { createHash } from "node:crypto";
88
+
89
+ // src/schema/utils.ts
90
+ import { pgSchemaName } from "@secondlayer/shared/db/queries/subgraphs";
91
+
92
+ // src/schema/generator.ts
93
+ var TYPE_MAP = {
94
+ text: "TEXT",
95
+ uint: "NUMERIC",
96
+ int: "NUMERIC",
97
+ principal: "TEXT",
98
+ boolean: "BOOLEAN",
99
+ timestamp: "TIMESTAMPTZ",
100
+ jsonb: "JSONB"
101
+ };
102
+ function escapeLiteralDefault(value) {
103
+ if (value === null || value === undefined)
104
+ return "NULL";
105
+ if (typeof value === "number" || typeof value === "bigint")
106
+ return String(value);
107
+ if (typeof value === "boolean")
108
+ return value ? "TRUE" : "FALSE";
109
+ return `'${String(value).replace(/'/g, "''")}'`;
110
+ }
111
+ function tableNeedsTrgm(tableDef) {
112
+ return Object.values(tableDef.columns).some((col) => col.search);
113
+ }
114
+ function emitTableDDL(schemaName, tableName, tableDef) {
115
+ const qualifiedName = `${schemaName}.${tableName}`;
116
+ const statements = [];
117
+ const columnDefs = [
118
+ "_id BIGSERIAL PRIMARY KEY",
119
+ "_block_height BIGINT NOT NULL",
120
+ "_tx_id TEXT NOT NULL",
121
+ "_created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()"
122
+ ];
123
+ for (const [colName, col] of Object.entries(tableDef.columns)) {
124
+ const sqlType = TYPE_MAP[col.type];
125
+ const nullable = col.nullable ? "" : " NOT NULL";
126
+ let colDef = `${colName} ${sqlType}${nullable}`;
127
+ if (col.default !== undefined) {
128
+ colDef += ` DEFAULT ${escapeLiteralDefault(col.default)}`;
129
+ }
130
+ if (col.type === "uint") {
131
+ colDef += ` CHECK (${colName} >= 0)`;
132
+ }
133
+ columnDefs.push(colDef);
134
+ }
135
+ statements.push(`CREATE TABLE IF NOT EXISTS ${qualifiedName} (
136
+ ${columnDefs.join(`,
137
+ `)}
138
+ )`);
139
+ statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_block_height ON ${qualifiedName} (_block_height)`);
140
+ statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_tx_id ON ${qualifiedName} (_tx_id)`);
141
+ for (const [colName, col] of Object.entries(tableDef.columns)) {
142
+ if (col.indexed) {
143
+ statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName} ON ${qualifiedName} (${colName})`);
144
+ }
145
+ }
146
+ for (const [colName, col] of Object.entries(tableDef.columns)) {
147
+ if (col.search) {
148
+ statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName}_trgm ON ${qualifiedName} USING gin (${colName} gin_trgm_ops)`);
149
+ }
150
+ }
151
+ if (tableDef.indexes) {
152
+ for (let i = 0;i < tableDef.indexes.length; i++) {
153
+ const cols = tableDef.indexes[i];
154
+ const idxName = `idx_${schemaName}_${tableName}_composite_${i}`;
155
+ statements.push(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${qualifiedName} (${cols.join(", ")})`);
156
+ }
157
+ }
158
+ if (tableDef.uniqueKeys) {
159
+ for (let i = 0;i < tableDef.uniqueKeys.length; i++) {
160
+ const cols = tableDef.uniqueKeys[i];
161
+ const constraintName = `uq_${schemaName}_${tableName}_${cols.join("_")}`;
162
+ statements.push(`ALTER TABLE ${qualifiedName} ADD CONSTRAINT ${constraintName} UNIQUE (${cols.join(", ")})`);
163
+ }
164
+ }
165
+ return statements;
166
+ }
167
+ function emitJournalDDL(schemaName) {
168
+ return [
169
+ `CREATE TABLE IF NOT EXISTS ${schemaName}._journal (
170
+ _jid BIGSERIAL PRIMARY KEY,
171
+ block_height BIGINT NOT NULL,
172
+ table_name TEXT NOT NULL,
173
+ row_key JSONB NOT NULL,
174
+ prev_row JSONB,
175
+ _created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
176
+ )`,
177
+ `CREATE INDEX IF NOT EXISTS idx_${schemaName}_journal_height ON ${schemaName}._journal (block_height)`
178
+ ];
179
+ }
180
+ function emitForeignKeyDDL(schemaName, tableName, tableDef) {
181
+ return (tableDef.relations ?? []).map((rel) => {
182
+ const constraintName = `fk_${schemaName}_${tableName}_${rel.name}`;
183
+ return `ALTER TABLE ${schemaName}.${tableName} ADD CONSTRAINT ${constraintName} ` + `FOREIGN KEY (${rel.fields.join(", ")}) ` + `REFERENCES ${schemaName}.${rel.references} (${rel.referencedColumns.join(", ")})`;
184
+ });
185
+ }
186
+ function generateSubgraphSQL(def, schemaNameOverride) {
187
+ const schemaName = schemaNameOverride ?? pgSchemaName(def.name);
188
+ const statements = [];
189
+ const needsTrgm = Object.values(def.schema).some((table) => Object.values(table.columns).some((col) => col.search));
190
+ if (needsTrgm) {
191
+ statements.push("CREATE EXTENSION IF NOT EXISTS pg_trgm");
192
+ }
193
+ statements.push(`CREATE SCHEMA IF NOT EXISTS ${schemaName}`);
194
+ for (const [tableName, tableDef] of Object.entries(def.schema)) {
195
+ statements.push(...emitTableDDL(schemaName, tableName, tableDef));
196
+ }
197
+ statements.push(...emitJournalDDL(schemaName));
198
+ for (const [tableName, tableDef] of Object.entries(def.schema)) {
199
+ statements.push(...emitForeignKeyDDL(schemaName, tableName, tableDef));
200
+ }
201
+ const hashInput = JSON.stringify({
202
+ name: def.name,
203
+ schema: def.schema,
204
+ sources: def.sources
205
+ }, (_key, value) => typeof value === "bigint" ? value.toString() : value);
206
+ const hash = createHash("sha256").update(hashInput).digest("hex");
207
+ return { statements, hash };
208
+ }
209
+
210
+ // src/runtime/context.ts
211
+ var JOURNAL_RETENTION_BLOCKS = 300;
212
+ var journalEnsured = new Set;
84
213
  function validateColumnName(name) {
85
214
  if (!/^[a-z_][a-z0-9_]*$/i.test(name)) {
86
215
  throw new Error(`Invalid column name: ${name}`);
@@ -95,13 +224,15 @@ class SubgraphContext {
95
224
  subgraphSchema;
96
225
  ops = [];
97
226
  byo;
98
- constructor(db, pgSchemaName, subgraphSchema, block, tx, byo = false) {
227
+ journal;
228
+ constructor(db, pgSchemaName2, subgraphSchema, block, tx, byo = false, journal = false) {
99
229
  this.db = db;
100
- this.pgSchemaName = pgSchemaName;
230
+ this.pgSchemaName = pgSchemaName2;
101
231
  this.subgraphSchema = subgraphSchema;
102
232
  this.block = block;
103
233
  this._tx = tx;
104
234
  this.byo = byo;
235
+ this.journal = journal;
105
236
  }
106
237
  get tx() {
107
238
  return this._tx;
@@ -157,6 +288,43 @@ class SubgraphContext {
157
288
  this.validateTable(table);
158
289
  this.ops.push({ kind: "delete", table, data: where });
159
290
  }
291
+ increment(table, key, deltas) {
292
+ this.validateTable(table);
293
+ const tableDef = this.subgraphSchema[table];
294
+ const keyColumns = Object.keys(key);
295
+ const hasUniqueConstraint = tableDef?.uniqueKeys?.some((uk) => uk.length === keyColumns.length && uk.every((c) => keyColumns.includes(c)));
296
+ if (!hasUniqueConstraint) {
297
+ throw new Error(`increment("${table}") requires a uniqueKeys constraint on [${keyColumns.join(", ")}]`);
298
+ }
299
+ for (const [col, v] of Object.entries(deltas)) {
300
+ validateColumnName(col);
301
+ if (keyColumns.includes(col)) {
302
+ throw new Error(`increment("${table}"): "${col}" is a key column`);
303
+ }
304
+ if (typeof v !== "bigint" && typeof v !== "number") {
305
+ throw new Error(`increment("${table}"): delta for "${col}" must be bigint or number`);
306
+ }
307
+ }
308
+ this.ops.push({
309
+ kind: "increment",
310
+ table,
311
+ data: {
312
+ ...key,
313
+ _block_height: this.block.height,
314
+ _tx_id: this._tx.txId,
315
+ _upsert_keys: keyColumns
316
+ },
317
+ set: { ...deltas }
318
+ });
319
+ }
320
+ opsCheckpoint() {
321
+ return this.ops.length;
322
+ }
323
+ rollbackTo(checkpoint) {
324
+ if (checkpoint < 0 || checkpoint > this.ops.length)
325
+ return;
326
+ this.ops.length = checkpoint;
327
+ }
160
328
  patch(table, where, set) {
161
329
  this.update(table, where, set);
162
330
  }
@@ -178,7 +346,7 @@ class SubgraphContext {
178
346
  const query = `SELECT * FROM ${qualifiedTable} WHERE ${clause} LIMIT 1`;
179
347
  const { rows } = await sql.raw(query).execute(this.db);
180
348
  const row = rows[0] ?? null;
181
- return row ? this.coerceRow(table, row) : null;
349
+ return this.overlayOne(table, where, row ? this.coerceRow(table, row) : null);
182
350
  }
183
351
  async findMany(table, where) {
184
352
  this.validateTable(table);
@@ -186,7 +354,85 @@ class SubgraphContext {
186
354
  const { clause } = buildWhereClause(where);
187
355
  const query = `SELECT * FROM ${qualifiedTable} WHERE ${clause}`;
188
356
  const { rows } = await sql.raw(query).execute(this.db);
189
- return rows.map((r) => this.coerceRow(table, r));
357
+ const dbRows = rows.map((r) => this.coerceRow(table, r));
358
+ return this.overlayMany(table, where, dbRows);
359
+ }
360
+ overlayOne(table, where, dbRow) {
361
+ let row = dbRow;
362
+ for (const op of this.ops) {
363
+ if (op.table !== table)
364
+ continue;
365
+ row = this.applyOpToRow(op, row, where);
366
+ }
367
+ return row;
368
+ }
369
+ overlayMany(table, where, dbRows) {
370
+ let result = [...dbRows];
371
+ for (const op of this.ops) {
372
+ if (op.table !== table)
373
+ continue;
374
+ if (op.kind === "update") {
375
+ result = result.map((r) => rowMatches(r, op.data) ? { ...r, ...op.set ?? {} } : r);
376
+ } else if (op.kind === "delete") {
377
+ result = result.filter((r) => !rowMatches(r, op.data));
378
+ } else {
379
+ const upsertKeys = op.data._upsert_keys;
380
+ const clean = stripControlKeys(op.data);
381
+ const idx = upsertKeys ? result.findIndex((r) => upsertKeys.every((k) => valEq(r[k], clean[k]))) : -1;
382
+ if (idx >= 0) {
383
+ result[idx] = this.applyOpToRow(op, result[idx], where) ?? result[idx];
384
+ } else {
385
+ const created = this.applyOpToRow(op, null, where);
386
+ if (created)
387
+ result.push(created);
388
+ }
389
+ }
390
+ }
391
+ return result;
392
+ }
393
+ applyOpToRow(op, row, where) {
394
+ const upsertKeys = op.data._upsert_keys;
395
+ const clean = stripControlKeys(op.data);
396
+ switch (op.kind) {
397
+ case "insert": {
398
+ if (row) {
399
+ if (upsertKeys?.every((k) => valEq(row[k], clean[k]))) {
400
+ const merged = { ...row };
401
+ for (const [k, v] of Object.entries(clean)) {
402
+ if (!upsertKeys.includes(k) && !k.startsWith("_"))
403
+ merged[k] = v;
404
+ }
405
+ return merged;
406
+ }
407
+ return row;
408
+ }
409
+ return rowMatches(clean, where) ? { ...clean } : null;
410
+ }
411
+ case "increment": {
412
+ const deltas = op.set ?? {};
413
+ if (row) {
414
+ if (upsertKeys.every((k) => valEq(row[k], clean[k]))) {
415
+ const merged = { ...row };
416
+ for (const [col, d] of Object.entries(deltas)) {
417
+ merged[col] = toBigIntOr0(merged[col]) + toBigIntOr0(d);
418
+ }
419
+ return merged;
420
+ }
421
+ return row;
422
+ }
423
+ if (!rowMatches(clean, where))
424
+ return null;
425
+ const created = { ...clean };
426
+ for (const [col, d] of Object.entries(deltas)) {
427
+ created[col] = toBigIntOr0(d);
428
+ }
429
+ return created;
430
+ }
431
+ case "update":
432
+ return row && rowMatches(row, op.data) ? { ...row, ...op.set ?? {} } : row;
433
+ case "delete":
434
+ return row && rowMatches(row, op.data) ? null : row;
435
+ }
190
436
  }
191
437
  async count(table, where) {
192
438
  this.validateTable(table);
@@ -247,6 +493,7 @@ class SubgraphContext {
247
493
  async flush() {
248
494
  if (this.ops.length === 0)
249
495
  return { count: 0, writes: [] };
496
+ await this.ensureJournalTable();
250
497
  const opsToFlush = [...this.ops];
251
498
  this.ops.length = 0;
252
499
  const statements = this.buildStatements(opsToFlush);
@@ -264,12 +511,12 @@ class SubgraphContext {
264
511
  const writes = opsToFlush.map((op, rowIndex) => {
265
512
  const blockHeight = op.data._block_height ?? this.block.height;
266
513
  const txId = op.data._tx_id ?? this._tx.txId;
267
- const baseRow = op.kind === "update" ? { ...op.data, ...op.set ?? {} } : { ...op.data };
514
+ const baseRow = op.kind === "update" || op.kind === "increment" ? { ...op.data, ...op.set ?? {} } : { ...op.data };
268
515
  baseRow._upsert_keys = undefined;
269
516
  baseRow._upsert_fallback_keys = undefined;
270
517
  baseRow._upsert_fallback_set = undefined;
271
518
  return {
272
- op: op.kind,
519
+ op: op.kind === "increment" ? "update" : op.kind,
273
520
  table: op.table,
274
521
  row: jsonSafe(baseRow),
275
522
  pk: { blockHeight, txId, rowIndex }
@@ -294,6 +541,35 @@ class SubgraphContext {
294
541
  const batchKey = `${op.table}:${[...cols].sort().join(",")}:${upsertKeys ? [...upsertKeys].sort().join(",") : ""}`;
295
542
  return { data, cols, vals, upsertKeys, batchKey };
296
543
  }
544
+ async ensureJournalTable() {
545
+ if (!this.journal || journalEnsured.has(this.pgSchemaName))
546
+ return;
547
+ const { rows } = await sql.raw(`SELECT to_regclass('"${this.pgSchemaName}"."_journal"') AS r`).execute(this.db);
548
+ if (rows[0]?.r) {
549
+ journalEnsured.add(this.pgSchemaName);
550
+ return;
551
+ }
552
+ for (const stmt of emitJournalDDL(this.pgSchemaName)) {
553
+ await sql.raw(stmt).execute(this.db);
554
+ }
555
+ }
556
+ columnSqlType(table, col) {
557
+ const def = this.subgraphSchema[table]?.columns?.[col];
558
+ return def ? TYPE_MAP[def.type] : undefined;
559
+ }
560
+ journalCaptureSQL(table, keyCols, keyLiteralRows) {
561
+ const cast = (col, expr) => {
562
+ const t = this.columnSqlType(table, col);
563
+ return t ? `CAST(${expr} AS ${t})` : expr;
564
+ };
565
+ const keyObj = keyCols.map((k) => `'${k}', ${cast(k, `v."${k}"`)}`).join(", ");
566
+ const joinCond = keyCols.map((k) => `t."${k}" = ${cast(k, `v."${k}"`)}`).join(" AND ");
567
+ const valuesList = keyLiteralRows.map((r) => `(${r.join(", ")})`).join(", ");
568
+ return `INSERT INTO "${this.pgSchemaName}"."_journal" ("block_height", "table_name", "row_key", "prev_row") ` + `SELECT ${this.block.height}, '${table}', jsonb_build_object(${keyObj}), to_jsonb(t.*) ` + `FROM (VALUES ${valuesList}) AS v(${keyCols.map((k) => `"${k}"`).join(", ")}) ` + `LEFT JOIN "${this.pgSchemaName}"."${table}" t ON ${joinCond}`;
569
+ }
570
+ journalCaptureByWhereSQL(table, clause) {
571
+ return `INSERT INTO "${this.pgSchemaName}"."_journal" ("block_height", "table_name", "row_key", "prev_row") ` + `SELECT ${this.block.height}, '${table}', jsonb_build_object('_id', t."_id"), to_jsonb(t.*) ` + `FROM "${this.pgSchemaName}"."${table}" t WHERE ${clause}`;
572
+ }
297
573
  buildStatements(ops) {
298
574
  const statements = [];
299
575
  if (this.byo) {
@@ -307,6 +583,38 @@ class SubgraphContext {
307
583
  }
308
584
  let currentBatch = null;
309
585
  let currentBatchKey = "";
586
+ let incBatch = null;
587
+ let incBatchKey = "";
588
+ const flushIncrementBatch = () => {
589
+ if (!incBatch)
590
+ return;
591
+ const batch = incBatch;
592
+ const qualifiedTable = `"${this.pgSchemaName}"."${batch.table}"`;
593
+ const cols = [
594
+ ...batch.keyCols,
595
+ ...batch.deltaCols,
596
+ "_block_height",
597
+ "_tx_id",
598
+ "_created_at"
599
+ ];
600
+ const valuesList = Array.from(batch.rows.values()).map((r) => {
601
+ const vals = [
602
+ ...batch.keyCols.map((k) => escapeLiteral(r.keys[k])),
603
+ ...batch.deltaCols.map((c) => String(r.deltas[c] ?? 0n)),
604
+ escapeLiteral(r.meta.blockHeight),
605
+ escapeLiteral(r.meta.txId),
606
+ "NOW()"
607
+ ];
608
+ return `(${vals.join(", ")})`;
609
+ }).join(", ");
610
+ const setClauses = batch.deltaCols.map((c) => `"${c}" = COALESCE("${batch.table}"."${c}", 0) + EXCLUDED."${c}"`);
611
+ if (this.journal) {
612
+ statements.push(this.journalCaptureSQL(batch.table, batch.keyCols, Array.from(batch.rows.values()).map((r) => batch.keyCols.map((k) => escapeLiteral(r.keys[k])))));
613
+ }
614
+ statements.push(`INSERT INTO ${qualifiedTable} (${cols.map((c) => `"${c}"`).join(", ")}) VALUES ${valuesList} ` + `ON CONFLICT (${batch.keyCols.map((k) => `"${k}"`).join(", ")}) DO UPDATE SET ${setClauses.join(", ")}`);
615
+ incBatch = null;
616
+ incBatchKey = "";
617
+ };
310
618
  const flushInsertBatch = () => {
311
619
  if (!currentBatch)
312
620
  return;
@@ -328,6 +636,11 @@ class SubgraphContext {
328
636
  }
329
637
  const valuesList = rows.map((r) => `(${r.join(", ")})`).join(", ");
330
638
  let stmt = `INSERT INTO ${qualifiedTable} (${colList}) VALUES ${valuesList}`;
639
+ if (this.journal && batch.upsertKeys && batch.upsertKeys.length > 0) {
640
+ const uKeys = batch.upsertKeys;
641
+ const keyIndices = uKeys.map((k) => batch.cols.indexOf(k));
642
+ statements.push(this.journalCaptureSQL(batch.table, uKeys, rows.map((r) => keyIndices.map((ki) => r[ki]))));
643
+ }
331
644
  if (batch.upsertKeys && batch.upsertKeys.length > 0) {
332
645
  const batchKeys = batch.upsertKeys;
333
646
  const updateCols = batch.cols.filter((c) => !batchKeys.includes(c) && !c.startsWith("_"));
@@ -345,6 +658,7 @@ class SubgraphContext {
345
658
  for (const op of ops) {
346
659
  const qualifiedTable = `"${this.pgSchemaName}"."${op.table}"`;
347
660
  if (op.kind === "insert") {
661
+ flushIncrementBatch();
348
662
  const { cols, vals, upsertKeys, batchKey } = this.prepareInsert(op);
349
663
  if (batchKey === currentBatchKey && currentBatch) {
350
664
  currentBatch.rows.push(vals);
@@ -353,22 +667,60 @@ class SubgraphContext {
353
667
  currentBatch = { table: op.table, cols, rows: [vals], upsertKeys };
354
668
  currentBatchKey = batchKey;
355
669
  }
670
+ } else if (op.kind === "increment") {
671
+ flushInsertBatch();
672
+ const keyCols = [...op.data._upsert_keys].sort();
673
+ const deltaCols = Object.keys(op.set ?? {}).sort();
674
+ const batchKey = `inc:${op.table}:${keyCols.join(",")}:${deltaCols.join(",")}`;
675
+ if (batchKey !== incBatchKey || !incBatch) {
676
+ flushIncrementBatch();
677
+ incBatch = { table: op.table, keyCols, deltaCols, rows: new Map };
678
+ incBatchKey = batchKey;
679
+ }
680
+ const clean = stripControlKeys(op.data);
681
+ const keySig = keyCols.map((k) => escapeLiteral(clean[k])).join("\x00");
682
+ const existing = incBatch.rows.get(keySig);
683
+ if (existing) {
684
+ for (const c of deltaCols) {
685
+ existing.deltas[c] = (existing.deltas[c] ?? 0n) + toBigIntOr0(op.set?.[c]);
686
+ }
687
+ } else {
688
+ const deltas = {};
689
+ for (const c of deltaCols)
690
+ deltas[c] = toBigIntOr0(op.set?.[c]);
691
+ incBatch.rows.set(keySig, {
692
+ keys: clean,
693
+ deltas,
694
+ meta: {
695
+ blockHeight: op.data._block_height ?? this.block.height,
696
+ txId: op.data._tx_id ?? this._tx.txId
697
+ }
698
+ });
699
+ }
356
700
  } else {
357
701
  flushInsertBatch();
702
+ flushIncrementBatch();
358
703
  if (op.kind === "update") {
359
704
  const setEntries = Object.entries(op.set ?? {});
360
705
  for (const [k] of setEntries)
361
706
  validateColumnName(k);
362
707
  const setClauses = setEntries.map(([k, v]) => `"${k}" = ${escapeLiteral(v)}`);
363
708
  const { clause } = buildWhereClause(op.data);
709
+ if (this.journal) {
710
+ statements.push(this.journalCaptureByWhereSQL(op.table, clause));
711
+ }
364
712
  statements.push(`UPDATE ${qualifiedTable} SET ${setClauses.join(", ")} WHERE ${clause}`);
365
713
  } else if (op.kind === "delete") {
366
714
  const { clause } = buildWhereClause(op.data);
715
+ if (this.journal) {
716
+ statements.push(this.journalCaptureByWhereSQL(op.table, clause));
717
+ }
367
718
  statements.push(`DELETE FROM ${qualifiedTable} WHERE ${clause}`);
368
719
  }
369
720
  }
370
721
  }
371
722
  flushInsertBatch();
723
+ flushIncrementBatch();
372
724
  return statements;
373
725
  }
374
726
  validateTable(table) {
@@ -377,6 +729,36 @@ class SubgraphContext {
377
729
  }
378
730
  }
379
731
  }
732
+ function stripControlKeys(data) {
733
+ const {
734
+ _upsert_keys: _a,
735
+ _upsert_fallback_keys: _b,
736
+ _upsert_fallback_set: _c,
737
+ ...clean
738
+ } = data;
739
+ return clean;
740
+ }
741
+ function valEq(a, b) {
742
+ if (a === b)
743
+ return true;
744
+ if (a == null || b == null)
745
+ return false;
746
+ return String(a) === String(b);
747
+ }
748
+ function rowMatches(row, where) {
749
+ return Object.entries(where).every(([k, v]) => valEq(row[k], v));
750
+ }
751
+ function toBigIntOr0(v) {
752
+ if (typeof v === "bigint")
753
+ return v;
754
+ if (v == null)
755
+ return 0n;
756
+ try {
757
+ return BigInt(String(v));
758
+ } catch {
759
+ return 0n;
760
+ }
761
+ }
380
762
  function jsonSafe(row) {
381
763
  const out = {};
382
764
  for (const [k, v] of Object.entries(row)) {
@@ -695,7 +1077,25 @@ async function runHandlers(subgraph, matched, ctx, opts) {
695
1077
  filterLookup.set(name, filter);
696
1078
  }
697
1079
  }
1080
+ const units = [];
698
1081
  for (const { tx, events, sourceName } of matched) {
1082
+ if (events.length === 0) {
1083
+ units.push({ tx, sourceName, event: null });
1084
+ } else {
1085
+ for (const event of events)
1086
+ units.push({ tx, sourceName, event });
1087
+ }
1088
+ }
1089
+ units.sort((a, b) => (a.tx.tx_index ?? 0) - (b.tx.tx_index ?? 0) || (a.event?.event_index ?? -1) - (b.event?.event_index ?? -1));
1090
+ for (const { tx, event, sourceName } of units) {
1091
+ if (errors >= threshold) {
1092
+ logger2.error("Subgraph error threshold reached, skipping remaining events", {
1093
+ subgraph: subgraph.name,
1094
+ errors,
1095
+ threshold
1096
+ });
1097
+ return { processed, errors };
1098
+ }
699
1099
  const handler = subgraph.handlers[sourceName] ?? subgraph.handlers["*"] ?? null;
700
1100
  if (!handler) {
701
1101
  logger2.warn("No handler found for source", {
@@ -714,9 +1114,29 @@ async function runHandlers(subgraph, matched, ctx, opts) {
714
1114
  functionName: tx.function_name ?? null
715
1115
  });
716
1116
  const filter = filterLookup.get(sourceName);
717
- if (events.length === 0) {
718
- try {
719
- const payload = filter ? buildEventPayload(filter, tx, null) : {
1117
+ const checkpoint = ctx.opsCheckpoint();
1118
+ try {
1119
+ let payload;
1120
+ if (event === null) {
1121
+ payload = filter ? buildEventPayload(filter, tx, null) : {
1122
+ tx: {
1123
+ txId: tx.tx_id,
1124
+ sender: tx.sender,
1125
+ type: tx.type,
1126
+ status: tx.status,
1127
+ contractId: tx.contract_id,
1128
+ functionName: tx.function_name
1129
+ }
1130
+ };
1131
+ } else if (filter) {
1132
+ payload = buildEventPayload(filter, tx, event);
1133
+ } else {
1134
+ const decoded = decodeEventData(event.data);
1135
+ payload = {
1136
+ ...decoded,
1137
+ _eventId: event.id,
1138
+ _eventType: event.type,
1139
+ _eventIndex: event.event_index,
720
1140
  tx: {
721
1141
  txId: tx.tx_id,
722
1142
  sender: tx.sender,
@@ -726,62 +1146,22 @@ async function runHandlers(subgraph, matched, ctx, opts) {
726
1146
  functionName: tx.function_name
727
1147
  }
728
1148
  };
729
- await handler(payload, ctx);
730
- processed++;
731
- } catch (err) {
732
- errors++;
733
- logger2.error("Subgraph handler error", {
734
- subgraph: subgraph.name,
735
- sourceName,
736
- txId: tx.tx_id,
737
- error: getErrorMessage(err)
738
- });
739
- }
740
- continue;
741
- }
742
- for (const event of events) {
743
- if (errors >= threshold) {
744
- logger2.error("Subgraph error threshold reached, skipping remaining events", {
745
- subgraph: subgraph.name,
746
- errors,
747
- threshold
748
- });
749
- return { processed, errors };
750
1149
  }
751
- try {
752
- const payload = filter ? buildEventPayload(filter, tx, event) : (() => {
753
- const decoded = decodeEventData(event.data);
754
- return {
755
- ...decoded,
756
- _eventId: event.id,
757
- _eventType: event.type,
758
- _eventIndex: event.event_index,
759
- tx: {
760
- txId: tx.tx_id,
761
- sender: tx.sender,
762
- type: tx.type,
763
- status: tx.status,
764
- contractId: tx.contract_id,
765
- functionName: tx.function_name
766
- }
767
- };
768
- })();
769
- if (filter?.type === "print_event" && filter.topic && payload.topic !== filter.topic) {
770
- continue;
771
- }
772
- await handler(payload, ctx);
773
- processed++;
774
- } catch (err) {
775
- errors++;
776
- logger2.error("Subgraph handler error", {
777
- subgraph: subgraph.name,
778
- sourceName,
779
- txId: tx.tx_id,
780
- eventId: event.id,
781
- eventType: event.type,
782
- error: getErrorMessage(err)
783
- });
1150
+ if (event !== null && filter?.type === "print_event" && filter.topic && payload.topic !== filter.topic) {
1151
+ continue;
784
1152
  }
1153
+ await handler(payload, ctx);
1154
+ processed++;
1155
+ } catch (err) {
1156
+ ctx.rollbackTo(checkpoint);
1157
+ errors++;
1158
+ logger2.error("Subgraph handler error", {
1159
+ subgraph: subgraph.name,
1160
+ sourceName,
1161
+ txId: tx.tx_id,
1162
+ ...event !== null ? { eventId: event.id, eventType: event.type } : {},
1163
+ error: getErrorMessage(err)
1164
+ });
785
1165
  }
786
1166
  }
787
1167
  return { processed, errors };
@@ -1038,9 +1418,6 @@ import {
1038
1418
  import { logger as logger5 } from "@secondlayer/shared/logger";
1039
1419
  import { sql as sql3 } from "kysely";
1040
1420
 
1041
- // src/schema/utils.ts
1042
- import { pgSchemaName } from "@secondlayer/shared/db/queries/subgraphs";
1043
-
1044
1421
  // src/runtime/block-source.ts
1045
1422
  import { getSourceDb } from "@secondlayer/shared/db";
1046
1423
  import { IndexHttpClient } from "@secondlayer/shared/index-http";
@@ -1386,7 +1763,7 @@ function resolveBlockSource(subgraph) {
1386
1763
  }
1387
1764
 
1388
1765
  // src/runtime/outbox-emit.ts
1389
- import { createHash } from "node:crypto";
1766
+ import { createHash as createHash2 } from "node:crypto";
1390
1767
  import { logger as logger4 } from "@secondlayer/shared/logger";
1391
1768
  var loggedKillSwitch = false;
1392
1769
  var OP_VERB = {
@@ -1399,7 +1776,7 @@ function isEmitOutboxEnabled() {
1399
1776
  }
1400
1777
  function dedupKey(subgraphName, tableName, blockHeight, txId, rowIndex, row) {
1401
1778
  const canonical = `${subgraphName}:${tableName}:${blockHeight}:${txId}:${rowIndex}:${stableStringify(row)}`;
1402
- return createHash("sha256").update(canonical).digest("hex").slice(0, 32);
1779
+ return createHash2("sha256").update(canonical).digest("hex").slice(0, 32);
1403
1780
  }
1404
1781
  function stableStringify(obj) {
1405
1782
  const keys = Object.keys(obj).sort();
@@ -1651,6 +2028,32 @@ async function resolveTraitContracts(subgraph, blockHeight, db) {
1651
2028
  }
1652
2029
  return resolved;
1653
2030
  }
2031
+ var BLOCK_RETRY_DELAYS_MS = [500, 2000, 5000];
2032
+ function journalEnabled(opts) {
2033
+ return !opts?.skipProgressUpdate;
2034
+ }
2035
+ async function processBlockWithRetry(subgraph, subgraphName, blockHeight, opts, retryDelaysMs = BLOCK_RETRY_DELAYS_MS) {
2036
+ let lastError;
2037
+ for (let attempt = 0;attempt <= retryDelaysMs.length; attempt++) {
2038
+ try {
2039
+ return await processBlock(subgraph, subgraphName, blockHeight, opts);
2040
+ } catch (err) {
2041
+ lastError = err;
2042
+ const delay = retryDelaysMs[attempt];
2043
+ if (delay === undefined)
2044
+ break;
2045
+ logger5.warn("Block processing failed, retrying", {
2046
+ subgraph: subgraphName,
2047
+ blockHeight,
2048
+ attempt: attempt + 1,
2049
+ retryInMs: delay,
2050
+ error: err instanceof Error ? err.message : String(err)
2051
+ });
2052
+ await new Promise((r) => setTimeout(r, delay));
2053
+ }
2054
+ }
2055
+ throw lastError;
2056
+ }
1654
2057
  async function processBlock(subgraph, subgraphName, blockHeight, opts) {
1655
2058
  const targetDb = getTargetDb();
1656
2059
  const blockStart = performance.now();
@@ -1718,10 +2121,17 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
1718
2121
  }
1719
2122
  };
1720
2123
  if (route.byo) {
2124
+ if (opts?.atomicProgress) {
2125
+ const row = await targetDb.selectFrom("subgraphs").select("last_processed_block").where("name", "=", subgraphName).executeTakeFirst();
2126
+ if (row && Number(row.last_processed_block) >= blockHeight) {
2127
+ result.skipped = true;
2128
+ return result;
2129
+ }
2130
+ }
1721
2131
  let runResult = { processed: 0, errors: 0 };
1722
2132
  let manifest;
1723
2133
  await route.dataDb.transaction().execute(async (tx) => {
1724
- const ctx = new SubgraphContext(tx, schemaName, subgraph.schema, blockMeta, initialTx, true);
2134
+ const ctx = new SubgraphContext(tx, schemaName, subgraph.schema, blockMeta, initialTx, true, journalEnabled(opts));
1725
2135
  const handlerStart = performance.now();
1726
2136
  runResult = await runHandlers(subgraph, matched, ctx);
1727
2137
  handlerMs = performance.now() - handlerStart;
@@ -1737,24 +2147,39 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
1737
2147
  if (manifest && manifest.count > 0) {
1738
2148
  await emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, block.height);
1739
2149
  }
2150
+ if (opts?.atomicProgress && manifest && manifest.count > 0) {
2151
+ await updateSubgraphStatus(tx, subgraphName, opts.atomicProgress.status, blockHeight);
2152
+ }
1740
2153
  await applyProgress(tx, runResult);
1741
2154
  });
1742
2155
  } else {
1743
2156
  await targetDb.transaction().execute(async (tx) => {
1744
- const ctx = new SubgraphContext(tx, schemaName, subgraph.schema, blockMeta, initialTx);
2157
+ if (opts?.atomicProgress) {
2158
+ const row = await tx.selectFrom("subgraphs").select("last_processed_block").where("name", "=", subgraphName).executeTakeFirst();
2159
+ if (row && Number(row.last_processed_block) >= blockHeight) {
2160
+ result.skipped = true;
2161
+ return;
2162
+ }
2163
+ }
2164
+ const ctx = new SubgraphContext(tx, schemaName, subgraph.schema, blockMeta, initialTx, false, journalEnabled(opts));
1745
2165
  const handlerStart = performance.now();
1746
2166
  const runResult = await runHandlers(subgraph, matched, ctx);
1747
2167
  handlerMs = performance.now() - handlerStart;
1748
2168
  result.processed = runResult.processed;
1749
2169
  result.errors = runResult.errors;
2170
+ let flushedWrites = false;
1750
2171
  if (ctx.pendingOps > 0) {
1751
2172
  const flushStart = performance.now();
1752
2173
  const manifest = await ctx.flush();
2174
+ flushedWrites = manifest.count > 0;
1753
2175
  if (manifest.count > 0) {
1754
2176
  await emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, block.height);
1755
2177
  }
1756
2178
  flushMs = performance.now() - flushStart;
1757
2179
  }
2180
+ if (opts?.atomicProgress && flushedWrites) {
2181
+ await updateSubgraphStatus(tx, subgraphName, opts.atomicProgress.status, blockHeight);
2182
+ }
1758
2183
  await applyProgress(tx, runResult);
1759
2184
  });
1760
2185
  }
@@ -1784,6 +2209,9 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
1784
2209
  error: err instanceof Error ? err.message : String(err)
1785
2210
  });
1786
2211
  }
2212
+ if (journalEnabled(opts)) {
2213
+ await sql3.raw(`DELETE FROM "${schemaName}"."_journal" WHERE "block_height" < ${blockHeight - JOURNAL_RETENTION_BLOCKS}`).execute(route.dataDb).catch(() => {});
2214
+ }
1787
2215
  }
1788
2216
  return result;
1789
2217
  }
@@ -1858,118 +2286,16 @@ import {
1858
2286
  recordGapBatch,
1859
2287
  resolveGaps
1860
2288
  } from "@secondlayer/shared/db/queries/subgraph-gaps";
2289
+ import { updateOperationProcessedEvents } from "@secondlayer/shared/db/queries/subgraph-operations";
1861
2290
  import {
1862
2291
  recordSubgraphProcessed as recordSubgraphProcessed2,
1863
2292
  updateSubgraphStatus as updateSubgraphStatus2
1864
2293
  } from "@secondlayer/shared/db/queries/subgraphs";
1865
2294
  import { logger as logger6 } from "@secondlayer/shared/logger";
1866
-
1867
- // src/schema/generator.ts
1868
- import { createHash as createHash2 } from "node:crypto";
1869
- var TYPE_MAP = {
1870
- text: "TEXT",
1871
- uint: "NUMERIC",
1872
- int: "NUMERIC",
1873
- principal: "TEXT",
1874
- boolean: "BOOLEAN",
1875
- timestamp: "TIMESTAMPTZ",
1876
- jsonb: "JSONB"
1877
- };
1878
- function escapeLiteralDefault(value) {
1879
- if (value === null || value === undefined)
1880
- return "NULL";
1881
- if (typeof value === "number" || typeof value === "bigint")
1882
- return String(value);
1883
- if (typeof value === "boolean")
1884
- return value ? "TRUE" : "FALSE";
1885
- return `'${String(value).replace(/'/g, "''")}'`;
1886
- }
1887
- function tableNeedsTrgm(tableDef) {
1888
- return Object.values(tableDef.columns).some((col) => col.search);
1889
- }
1890
- function emitTableDDL(schemaName, tableName, tableDef) {
1891
- const qualifiedName = `${schemaName}.${tableName}`;
1892
- const statements = [];
1893
- const columnDefs = [
1894
- "_id BIGSERIAL PRIMARY KEY",
1895
- "_block_height BIGINT NOT NULL",
1896
- "_tx_id TEXT NOT NULL",
1897
- "_created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()"
1898
- ];
1899
- for (const [colName, col] of Object.entries(tableDef.columns)) {
1900
- const sqlType = TYPE_MAP[col.type];
1901
- const nullable = col.nullable ? "" : " NOT NULL";
1902
- let colDef = `${colName} ${sqlType}${nullable}`;
1903
- if (col.default !== undefined) {
1904
- colDef += ` DEFAULT ${escapeLiteralDefault(col.default)}`;
1905
- }
1906
- columnDefs.push(colDef);
1907
- }
1908
- statements.push(`CREATE TABLE IF NOT EXISTS ${qualifiedName} (
1909
- ${columnDefs.join(`,
1910
- `)}
1911
- )`);
1912
- statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_block_height ON ${qualifiedName} (_block_height)`);
1913
- statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_tx_id ON ${qualifiedName} (_tx_id)`);
1914
- for (const [colName, col] of Object.entries(tableDef.columns)) {
1915
- if (col.indexed) {
1916
- statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName} ON ${qualifiedName} (${colName})`);
1917
- }
1918
- }
1919
- for (const [colName, col] of Object.entries(tableDef.columns)) {
1920
- if (col.search) {
1921
- statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName}_trgm ON ${qualifiedName} USING gin (${colName} gin_trgm_ops)`);
1922
- }
1923
- }
1924
- if (tableDef.indexes) {
1925
- for (let i = 0;i < tableDef.indexes.length; i++) {
1926
- const cols = tableDef.indexes[i];
1927
- const idxName = `idx_${schemaName}_${tableName}_composite_${i}`;
1928
- statements.push(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${qualifiedName} (${cols.join(", ")})`);
1929
- }
1930
- }
1931
- if (tableDef.uniqueKeys) {
1932
- for (let i = 0;i < tableDef.uniqueKeys.length; i++) {
1933
- const cols = tableDef.uniqueKeys[i];
1934
- const constraintName = `uq_${schemaName}_${tableName}_${cols.join("_")}`;
1935
- statements.push(`ALTER TABLE ${qualifiedName} ADD CONSTRAINT ${constraintName} UNIQUE (${cols.join(", ")})`);
1936
- }
1937
- }
1938
- return statements;
1939
- }
1940
- function emitForeignKeyDDL(schemaName, tableName, tableDef) {
1941
- return (tableDef.relations ?? []).map((rel) => {
1942
- const constraintName = `fk_${schemaName}_${tableName}_${rel.name}`;
1943
- return `ALTER TABLE ${schemaName}.${tableName} ADD CONSTRAINT ${constraintName} ` + `FOREIGN KEY (${rel.fields.join(", ")}) ` + `REFERENCES ${schemaName}.${rel.references} (${rel.referencedColumns.join(", ")})`;
1944
- });
1945
- }
1946
- function generateSubgraphSQL(def, schemaNameOverride) {
1947
- const schemaName = schemaNameOverride ?? pgSchemaName(def.name);
1948
- const statements = [];
1949
- const needsTrgm = Object.values(def.schema).some((table) => Object.values(table.columns).some((col) => col.search));
1950
- if (needsTrgm) {
1951
- statements.push("CREATE EXTENSION IF NOT EXISTS pg_trgm");
1952
- }
1953
- statements.push(`CREATE SCHEMA IF NOT EXISTS ${schemaName}`);
1954
- for (const [tableName, tableDef] of Object.entries(def.schema)) {
1955
- statements.push(...emitTableDDL(schemaName, tableName, tableDef));
1956
- }
1957
- for (const [tableName, tableDef] of Object.entries(def.schema)) {
1958
- statements.push(...emitForeignKeyDDL(schemaName, tableName, tableDef));
1959
- }
1960
- const hashInput = JSON.stringify({
1961
- name: def.name,
1962
- schema: def.schema,
1963
- sources: def.sources
1964
- }, (_key, value) => typeof value === "bigint" ? value.toString() : value);
1965
- const hash = createHash2("sha256").update(hashInput).digest("hex");
1966
- return { statements, hash };
1967
- }
1968
-
1969
- // src/runtime/reindex.ts
1970
2295
  var LOG_INTERVAL = 1000;
1971
2296
  var HEALTH_FLUSH_INTERVAL = 1000;
1972
2297
  var PROGRESS_FLUSH_INTERVAL_MS = 5000;
2298
+ var EMPTY_BATCH_HALT_THRESHOLD = 3;
1973
2299
  var STANDARD_REINDEX_BATCH_CONFIG = {
1974
2300
  defaultBatchSize: 500,
1975
2301
  minBatchSize: 100,
@@ -2045,6 +2371,7 @@ async function processBlockRange(def, opts) {
2045
2371
  let batchSize = batchConfig.defaultBatchSize;
2046
2372
  let currentHeight = fromBlock;
2047
2373
  let aborted = false;
2374
+ let consecutiveEmptyBatches = 0;
2048
2375
  const sparse = Boolean(source.nextDataHeight && canSparseScan(def));
2049
2376
  const flushHealth = async () => {
2050
2377
  if (pendingEventsProcessed === 0 && pendingErrors === 0)
@@ -2056,6 +2383,13 @@ async function processBlockRange(def, opts) {
2056
2383
  lastHealthFlushBlock = blocksProcessed;
2057
2384
  lastHealthFlushAt = Date.now();
2058
2385
  };
2386
+ const haltRange = async (errorMsg, height) => {
2387
+ pendingErrors++;
2388
+ pendingLastError = errorMsg;
2389
+ await flushHealth().catch(() => {});
2390
+ await updateSubgraphStatus2(targetDb, subgraphName, "error").catch(() => {});
2391
+ throw new Error(`${subgraphName}: halted at block ${height}: ${errorMsg}`);
2392
+ };
2059
2393
  let nextBatchEnd = Math.min(currentHeight + batchSize - 1, toBlock);
2060
2394
  let nextBatchPromise = source.loadBlockRange(currentHeight, nextBatchEnd);
2061
2395
  while (currentHeight <= toBlock) {
@@ -2070,6 +2404,14 @@ async function processBlockRange(def, opts) {
2070
2404
  }
2071
2405
  const batch = await nextBatchPromise;
2072
2406
  const batchEnd = nextBatchEnd;
2407
+ if (batch.size === 0 && batchEnd >= currentHeight) {
2408
+ consecutiveEmptyBatches++;
2409
+ if (consecutiveEmptyBatches >= EMPTY_BATCH_HALT_THRESHOLD) {
2410
+ await haltRange(`block source returned ${consecutiveEmptyBatches} consecutive empty batches (ending ${currentHeight}..${batchEnd}) — source degraded`, currentHeight);
2411
+ }
2412
+ } else {
2413
+ consecutiveEmptyBatches = 0;
2414
+ }
2073
2415
  const nextStart = batchEnd + 1;
2074
2416
  if (nextStart <= toBlock) {
2075
2417
  nextBatchEnd = Math.min(nextStart + batchSize - 1, toBlock);
@@ -2077,28 +2419,39 @@ async function processBlockRange(def, opts) {
2077
2419
  }
2078
2420
  const batchFailedBlocks = [];
2079
2421
  let batchMatched = 0;
2422
+ const atomicProgress = status === "reindexing" ? { status } : undefined;
2080
2423
  for (let height = currentHeight;height <= batchEnd; height++) {
2081
- const blockData = batch.get(height);
2424
+ let blockData = batch.get(height);
2082
2425
  if (!blockData) {
2426
+ blockData = (await source.loadBlockRange(height, height)).get(height);
2427
+ }
2428
+ if (!blockData) {
2429
+ if (status === "reindexing") {
2430
+ const errorMsg = `block ${height} missing from source — halting reindex (cursor stays at ${height - 1})`;
2431
+ await haltRange(errorMsg, height);
2432
+ }
2083
2433
  batchFailedBlocks.push({ height, reason: "block_missing" });
2084
2434
  blocksProcessed++;
2085
2435
  continue;
2086
2436
  }
2087
2437
  let result;
2088
2438
  try {
2089
- result = await processBlock(def, subgraphName, height, {
2439
+ result = await processBlockWithRetry(def, subgraphName, height, {
2090
2440
  skipProgressUpdate: true,
2441
+ atomicProgress,
2091
2442
  preloaded: blockData
2092
2443
  });
2093
2444
  } catch (err) {
2094
- const errorMsg = err instanceof Error ? err.message : String(err);
2095
- logger6.error("Block processing error", {
2445
+ const errorMsg = getErrorMessage2(err);
2446
+ logger6.error("Block processing failed persistently", {
2096
2447
  subgraph: subgraphName,
2097
2448
  blockHeight: height,
2098
2449
  error: errorMsg
2099
2450
  });
2451
+ if (status === "reindexing") {
2452
+ await haltRange(`block ${height} failed persistently: ${errorMsg}`, height);
2453
+ }
2100
2454
  batchFailedBlocks.push({ height, reason: "processing_error" });
2101
- await updateSubgraphStatus2(targetDb, subgraphName, status, height).catch(() => {});
2102
2455
  blocksProcessed++;
2103
2456
  totalErrors++;
2104
2457
  pendingErrors++;
@@ -2124,6 +2477,9 @@ async function processBlockRange(def, opts) {
2124
2477
  const shouldFlushProgress = blocksProcessed % 100 === 0 || now - lastProgressFlushAt >= PROGRESS_FLUSH_INTERVAL_MS;
2125
2478
  if (shouldFlushProgress) {
2126
2479
  await updateSubgraphStatus2(targetDb, subgraphName, status, height);
2480
+ if (opts.operationId) {
2481
+ await updateOperationProcessedEvents(targetDb, opts.operationId, totalEventsProcessed).catch(() => {});
2482
+ }
2127
2483
  lastProgressFlushAt = now;
2128
2484
  }
2129
2485
  if (blocksProcessed % LOG_INTERVAL === 0) {
@@ -2241,6 +2597,7 @@ async function reindexSubgraph(def, opts) {
2241
2597
  isCatchup: false,
2242
2598
  apiKeyId: null,
2243
2599
  subgraphId: subgraphRow?.id,
2600
+ operationId: opts?.operationId,
2244
2601
  signal: opts?.signal
2245
2602
  });
2246
2603
  if (result.aborted) {
@@ -2316,6 +2673,7 @@ async function resumeReindex(def, opts) {
2316
2673
  isCatchup: false,
2317
2674
  apiKeyId: null,
2318
2675
  subgraphId: row.id,
2676
+ operationId: opts.operationId,
2319
2677
  signal: opts.signal
2320
2678
  });
2321
2679
  if (result.aborted) {
@@ -2364,6 +2722,7 @@ async function backfillSubgraph(def, opts) {
2364
2722
  isCatchup: false,
2365
2723
  apiKeyId: null,
2366
2724
  subgraphId: subgraphRow?.id,
2725
+ operationId: opts.operationId,
2367
2726
  signal: opts.signal
2368
2727
  });
2369
2728
  if (result.aborted) {
@@ -3238,10 +3597,12 @@ function getDefault(type) {
3238
3597
  }
3239
3598
  export {
3240
3599
  validateSubgraphDefinition,
3600
+ sparseProbeTargets,
3241
3601
  resumeReindex,
3242
3602
  renderDeployPlan,
3243
3603
  reindexSubgraph,
3244
3604
  pgSchemaName,
3605
+ hasBreakingChanges,
3245
3606
  generateSubgraphSQL,
3246
3607
  generatePrismaSchema,
3247
3608
  generateKyselySchema,
@@ -3250,10 +3611,11 @@ export {
3250
3611
  diffSchema,
3251
3612
  deploySchema,
3252
3613
  defineSubgraph,
3614
+ canSparseScan,
3253
3615
  backfillSubgraph,
3254
3616
  INDEX_CODEGEN_TABLES,
3255
3617
  ByoBreakingChangeError
3256
3618
  };
3257
3619
 
3258
- //# debugId=9A94DC6E9FCF6CD964756E2164756E21
3620
+ //# debugId=A7DC4F802366B29864756E2164756E21
3259
3621
  //# sourceMappingURL=index.js.map