@secondlayer/subgraphs 3.12.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 (34) hide show
  1. package/dist/src/index.d.ts +7 -0
  2. package/dist/src/index.js +531 -180
  3. package/dist/src/index.js.map +10 -10
  4. package/dist/src/runtime/block-processor.d.ts +31 -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 +7 -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 +576 -229
  14. package/dist/src/runtime/processor.js.map +12 -12
  15. package/dist/src/runtime/reindex.d.ts +7 -0
  16. package/dist/src/runtime/reindex.js +531 -180
  17. package/dist/src/runtime/reindex.js.map +10 -10
  18. package/dist/src/runtime/reorg.d.ts +7 -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 +70 -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 +7 -0
  28. package/dist/src/schema/index.js +18 -1
  29. package/dist/src/schema/index.js.map +3 -3
  30. package/dist/src/service.js +576 -229
  31. package/dist/src/service.js.map +12 -12
  32. package/dist/src/types.d.ts +7 -0
  33. package/dist/src/validate.d.ts +7 -0
  34. package/package.json +1 -1
package/dist/src/index.js CHANGED
@@ -82,6 +82,134 @@ function validateSubgraphDefinition(def) {
82
82
  import { logger } from "@secondlayer/shared/logger";
83
83
  import { formatUnits } from "@secondlayer/stacks/utils";
84
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;
85
213
  function validateColumnName(name) {
86
214
  if (!/^[a-z_][a-z0-9_]*$/i.test(name)) {
87
215
  throw new Error(`Invalid column name: ${name}`);
@@ -96,13 +224,15 @@ class SubgraphContext {
96
224
  subgraphSchema;
97
225
  ops = [];
98
226
  byo;
99
- constructor(db, pgSchemaName, subgraphSchema, block, tx, byo = false) {
227
+ journal;
228
+ constructor(db, pgSchemaName2, subgraphSchema, block, tx, byo = false, journal = false) {
100
229
  this.db = db;
101
- this.pgSchemaName = pgSchemaName;
230
+ this.pgSchemaName = pgSchemaName2;
102
231
  this.subgraphSchema = subgraphSchema;
103
232
  this.block = block;
104
233
  this._tx = tx;
105
234
  this.byo = byo;
235
+ this.journal = journal;
106
236
  }
107
237
  get tx() {
108
238
  return this._tx;
@@ -158,6 +288,43 @@ class SubgraphContext {
158
288
  this.validateTable(table);
159
289
  this.ops.push({ kind: "delete", table, data: where });
160
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
+ }
161
328
  patch(table, where, set) {
162
329
  this.update(table, where, set);
163
330
  }
@@ -179,7 +346,7 @@ class SubgraphContext {
179
346
  const query = `SELECT * FROM ${qualifiedTable} WHERE ${clause} LIMIT 1`;
180
347
  const { rows } = await sql.raw(query).execute(this.db);
181
348
  const row = rows[0] ?? null;
182
- return row ? this.coerceRow(table, row) : null;
349
+ return this.overlayOne(table, where, row ? this.coerceRow(table, row) : null);
183
350
  }
184
351
  async findMany(table, where) {
185
352
  this.validateTable(table);
@@ -187,7 +354,85 @@ class SubgraphContext {
187
354
  const { clause } = buildWhereClause(where);
188
355
  const query = `SELECT * FROM ${qualifiedTable} WHERE ${clause}`;
189
356
  const { rows } = await sql.raw(query).execute(this.db);
190
- 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
+ }
191
436
  }
192
437
  async count(table, where) {
193
438
  this.validateTable(table);
@@ -248,6 +493,7 @@ class SubgraphContext {
248
493
  async flush() {
249
494
  if (this.ops.length === 0)
250
495
  return { count: 0, writes: [] };
496
+ await this.ensureJournalTable();
251
497
  const opsToFlush = [...this.ops];
252
498
  this.ops.length = 0;
253
499
  const statements = this.buildStatements(opsToFlush);
@@ -265,12 +511,12 @@ class SubgraphContext {
265
511
  const writes = opsToFlush.map((op, rowIndex) => {
266
512
  const blockHeight = op.data._block_height ?? this.block.height;
267
513
  const txId = op.data._tx_id ?? this._tx.txId;
268
- 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 };
269
515
  baseRow._upsert_keys = undefined;
270
516
  baseRow._upsert_fallback_keys = undefined;
271
517
  baseRow._upsert_fallback_set = undefined;
272
518
  return {
273
- op: op.kind,
519
+ op: op.kind === "increment" ? "update" : op.kind,
274
520
  table: op.table,
275
521
  row: jsonSafe(baseRow),
276
522
  pk: { blockHeight, txId, rowIndex }
@@ -295,6 +541,35 @@ class SubgraphContext {
295
541
  const batchKey = `${op.table}:${[...cols].sort().join(",")}:${upsertKeys ? [...upsertKeys].sort().join(",") : ""}`;
296
542
  return { data, cols, vals, upsertKeys, batchKey };
297
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
+ }
298
573
  buildStatements(ops) {
299
574
  const statements = [];
300
575
  if (this.byo) {
@@ -308,6 +583,38 @@ class SubgraphContext {
308
583
  }
309
584
  let currentBatch = null;
310
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
+ };
311
618
  const flushInsertBatch = () => {
312
619
  if (!currentBatch)
313
620
  return;
@@ -329,6 +636,11 @@ class SubgraphContext {
329
636
  }
330
637
  const valuesList = rows.map((r) => `(${r.join(", ")})`).join(", ");
331
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
+ }
332
644
  if (batch.upsertKeys && batch.upsertKeys.length > 0) {
333
645
  const batchKeys = batch.upsertKeys;
334
646
  const updateCols = batch.cols.filter((c) => !batchKeys.includes(c) && !c.startsWith("_"));
@@ -346,6 +658,7 @@ class SubgraphContext {
346
658
  for (const op of ops) {
347
659
  const qualifiedTable = `"${this.pgSchemaName}"."${op.table}"`;
348
660
  if (op.kind === "insert") {
661
+ flushIncrementBatch();
349
662
  const { cols, vals, upsertKeys, batchKey } = this.prepareInsert(op);
350
663
  if (batchKey === currentBatchKey && currentBatch) {
351
664
  currentBatch.rows.push(vals);
@@ -354,22 +667,60 @@ class SubgraphContext {
354
667
  currentBatch = { table: op.table, cols, rows: [vals], upsertKeys };
355
668
  currentBatchKey = batchKey;
356
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
+ }
357
700
  } else {
358
701
  flushInsertBatch();
702
+ flushIncrementBatch();
359
703
  if (op.kind === "update") {
360
704
  const setEntries = Object.entries(op.set ?? {});
361
705
  for (const [k] of setEntries)
362
706
  validateColumnName(k);
363
707
  const setClauses = setEntries.map(([k, v]) => `"${k}" = ${escapeLiteral(v)}`);
364
708
  const { clause } = buildWhereClause(op.data);
709
+ if (this.journal) {
710
+ statements.push(this.journalCaptureByWhereSQL(op.table, clause));
711
+ }
365
712
  statements.push(`UPDATE ${qualifiedTable} SET ${setClauses.join(", ")} WHERE ${clause}`);
366
713
  } else if (op.kind === "delete") {
367
714
  const { clause } = buildWhereClause(op.data);
715
+ if (this.journal) {
716
+ statements.push(this.journalCaptureByWhereSQL(op.table, clause));
717
+ }
368
718
  statements.push(`DELETE FROM ${qualifiedTable} WHERE ${clause}`);
369
719
  }
370
720
  }
371
721
  }
372
722
  flushInsertBatch();
723
+ flushIncrementBatch();
373
724
  return statements;
374
725
  }
375
726
  validateTable(table) {
@@ -378,6 +729,36 @@ class SubgraphContext {
378
729
  }
379
730
  }
380
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
+ }
381
762
  function jsonSafe(row) {
382
763
  const out = {};
383
764
  for (const [k, v] of Object.entries(row)) {
@@ -696,7 +1077,25 @@ async function runHandlers(subgraph, matched, ctx, opts) {
696
1077
  filterLookup.set(name, filter);
697
1078
  }
698
1079
  }
1080
+ const units = [];
699
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
+ }
700
1099
  const handler = subgraph.handlers[sourceName] ?? subgraph.handlers["*"] ?? null;
701
1100
  if (!handler) {
702
1101
  logger2.warn("No handler found for source", {
@@ -715,9 +1114,29 @@ async function runHandlers(subgraph, matched, ctx, opts) {
715
1114
  functionName: tx.function_name ?? null
716
1115
  });
717
1116
  const filter = filterLookup.get(sourceName);
718
- if (events.length === 0) {
719
- try {
720
- 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,
721
1140
  tx: {
722
1141
  txId: tx.tx_id,
723
1142
  sender: tx.sender,
@@ -727,62 +1146,22 @@ async function runHandlers(subgraph, matched, ctx, opts) {
727
1146
  functionName: tx.function_name
728
1147
  }
729
1148
  };
730
- await handler(payload, ctx);
731
- processed++;
732
- } catch (err) {
733
- errors++;
734
- logger2.error("Subgraph handler error", {
735
- subgraph: subgraph.name,
736
- sourceName,
737
- txId: tx.tx_id,
738
- error: getErrorMessage(err)
739
- });
740
- }
741
- continue;
742
- }
743
- for (const event of events) {
744
- if (errors >= threshold) {
745
- logger2.error("Subgraph error threshold reached, skipping remaining events", {
746
- subgraph: subgraph.name,
747
- errors,
748
- threshold
749
- });
750
- return { processed, errors };
751
1149
  }
752
- try {
753
- const payload = filter ? buildEventPayload(filter, tx, event) : (() => {
754
- const decoded = decodeEventData(event.data);
755
- return {
756
- ...decoded,
757
- _eventId: event.id,
758
- _eventType: event.type,
759
- _eventIndex: event.event_index,
760
- tx: {
761
- txId: tx.tx_id,
762
- sender: tx.sender,
763
- type: tx.type,
764
- status: tx.status,
765
- contractId: tx.contract_id,
766
- functionName: tx.function_name
767
- }
768
- };
769
- })();
770
- if (filter?.type === "print_event" && filter.topic && payload.topic !== filter.topic) {
771
- continue;
772
- }
773
- await handler(payload, ctx);
774
- processed++;
775
- } catch (err) {
776
- errors++;
777
- logger2.error("Subgraph handler error", {
778
- subgraph: subgraph.name,
779
- sourceName,
780
- txId: tx.tx_id,
781
- eventId: event.id,
782
- eventType: event.type,
783
- error: getErrorMessage(err)
784
- });
1150
+ if (event !== null && filter?.type === "print_event" && filter.topic && payload.topic !== filter.topic) {
1151
+ continue;
785
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
+ });
786
1165
  }
787
1166
  }
788
1167
  return { processed, errors };
@@ -1039,9 +1418,6 @@ import {
1039
1418
  import { logger as logger5 } from "@secondlayer/shared/logger";
1040
1419
  import { sql as sql3 } from "kysely";
1041
1420
 
1042
- // src/schema/utils.ts
1043
- import { pgSchemaName } from "@secondlayer/shared/db/queries/subgraphs";
1044
-
1045
1421
  // src/runtime/block-source.ts
1046
1422
  import { getSourceDb } from "@secondlayer/shared/db";
1047
1423
  import { IndexHttpClient } from "@secondlayer/shared/index-http";
@@ -1387,7 +1763,7 @@ function resolveBlockSource(subgraph) {
1387
1763
  }
1388
1764
 
1389
1765
  // src/runtime/outbox-emit.ts
1390
- import { createHash } from "node:crypto";
1766
+ import { createHash as createHash2 } from "node:crypto";
1391
1767
  import { logger as logger4 } from "@secondlayer/shared/logger";
1392
1768
  var loggedKillSwitch = false;
1393
1769
  var OP_VERB = {
@@ -1400,7 +1776,7 @@ function isEmitOutboxEnabled() {
1400
1776
  }
1401
1777
  function dedupKey(subgraphName, tableName, blockHeight, txId, rowIndex, row) {
1402
1778
  const canonical = `${subgraphName}:${tableName}:${blockHeight}:${txId}:${rowIndex}:${stableStringify(row)}`;
1403
- return createHash("sha256").update(canonical).digest("hex").slice(0, 32);
1779
+ return createHash2("sha256").update(canonical).digest("hex").slice(0, 32);
1404
1780
  }
1405
1781
  function stableStringify(obj) {
1406
1782
  const keys = Object.keys(obj).sort();
@@ -1652,6 +2028,32 @@ async function resolveTraitContracts(subgraph, blockHeight, db) {
1652
2028
  }
1653
2029
  return resolved;
1654
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
+ }
1655
2057
  async function processBlock(subgraph, subgraphName, blockHeight, opts) {
1656
2058
  const targetDb = getTargetDb();
1657
2059
  const blockStart = performance.now();
@@ -1719,10 +2121,17 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
1719
2121
  }
1720
2122
  };
1721
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
+ }
1722
2131
  let runResult = { processed: 0, errors: 0 };
1723
2132
  let manifest;
1724
2133
  await route.dataDb.transaction().execute(async (tx) => {
1725
- 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));
1726
2135
  const handlerStart = performance.now();
1727
2136
  runResult = await runHandlers(subgraph, matched, ctx);
1728
2137
  handlerMs = performance.now() - handlerStart;
@@ -1738,24 +2147,39 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
1738
2147
  if (manifest && manifest.count > 0) {
1739
2148
  await emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, block.height);
1740
2149
  }
2150
+ if (opts?.atomicProgress && manifest && manifest.count > 0) {
2151
+ await updateSubgraphStatus(tx, subgraphName, opts.atomicProgress.status, blockHeight);
2152
+ }
1741
2153
  await applyProgress(tx, runResult);
1742
2154
  });
1743
2155
  } else {
1744
2156
  await targetDb.transaction().execute(async (tx) => {
1745
- 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));
1746
2165
  const handlerStart = performance.now();
1747
2166
  const runResult = await runHandlers(subgraph, matched, ctx);
1748
2167
  handlerMs = performance.now() - handlerStart;
1749
2168
  result.processed = runResult.processed;
1750
2169
  result.errors = runResult.errors;
2170
+ let flushedWrites = false;
1751
2171
  if (ctx.pendingOps > 0) {
1752
2172
  const flushStart = performance.now();
1753
2173
  const manifest = await ctx.flush();
2174
+ flushedWrites = manifest.count > 0;
1754
2175
  if (manifest.count > 0) {
1755
2176
  await emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, block.height);
1756
2177
  }
1757
2178
  flushMs = performance.now() - flushStart;
1758
2179
  }
2180
+ if (opts?.atomicProgress && flushedWrites) {
2181
+ await updateSubgraphStatus(tx, subgraphName, opts.atomicProgress.status, blockHeight);
2182
+ }
1759
2183
  await applyProgress(tx, runResult);
1760
2184
  });
1761
2185
  }
@@ -1785,6 +2209,9 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
1785
2209
  error: err instanceof Error ? err.message : String(err)
1786
2210
  });
1787
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
+ }
1788
2215
  }
1789
2216
  return result;
1790
2217
  }
@@ -1865,113 +2292,10 @@ import {
1865
2292
  updateSubgraphStatus as updateSubgraphStatus2
1866
2293
  } from "@secondlayer/shared/db/queries/subgraphs";
1867
2294
  import { logger as logger6 } from "@secondlayer/shared/logger";
1868
-
1869
- // src/schema/generator.ts
1870
- import { createHash as createHash2 } from "node:crypto";
1871
- var TYPE_MAP = {
1872
- text: "TEXT",
1873
- uint: "NUMERIC",
1874
- int: "NUMERIC",
1875
- principal: "TEXT",
1876
- boolean: "BOOLEAN",
1877
- timestamp: "TIMESTAMPTZ",
1878
- jsonb: "JSONB"
1879
- };
1880
- function escapeLiteralDefault(value) {
1881
- if (value === null || value === undefined)
1882
- return "NULL";
1883
- if (typeof value === "number" || typeof value === "bigint")
1884
- return String(value);
1885
- if (typeof value === "boolean")
1886
- return value ? "TRUE" : "FALSE";
1887
- return `'${String(value).replace(/'/g, "''")}'`;
1888
- }
1889
- function tableNeedsTrgm(tableDef) {
1890
- return Object.values(tableDef.columns).some((col) => col.search);
1891
- }
1892
- function emitTableDDL(schemaName, tableName, tableDef) {
1893
- const qualifiedName = `${schemaName}.${tableName}`;
1894
- const statements = [];
1895
- const columnDefs = [
1896
- "_id BIGSERIAL PRIMARY KEY",
1897
- "_block_height BIGINT NOT NULL",
1898
- "_tx_id TEXT NOT NULL",
1899
- "_created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()"
1900
- ];
1901
- for (const [colName, col] of Object.entries(tableDef.columns)) {
1902
- const sqlType = TYPE_MAP[col.type];
1903
- const nullable = col.nullable ? "" : " NOT NULL";
1904
- let colDef = `${colName} ${sqlType}${nullable}`;
1905
- if (col.default !== undefined) {
1906
- colDef += ` DEFAULT ${escapeLiteralDefault(col.default)}`;
1907
- }
1908
- columnDefs.push(colDef);
1909
- }
1910
- statements.push(`CREATE TABLE IF NOT EXISTS ${qualifiedName} (
1911
- ${columnDefs.join(`,
1912
- `)}
1913
- )`);
1914
- statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_block_height ON ${qualifiedName} (_block_height)`);
1915
- statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_tx_id ON ${qualifiedName} (_tx_id)`);
1916
- for (const [colName, col] of Object.entries(tableDef.columns)) {
1917
- if (col.indexed) {
1918
- statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName} ON ${qualifiedName} (${colName})`);
1919
- }
1920
- }
1921
- for (const [colName, col] of Object.entries(tableDef.columns)) {
1922
- if (col.search) {
1923
- statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName}_trgm ON ${qualifiedName} USING gin (${colName} gin_trgm_ops)`);
1924
- }
1925
- }
1926
- if (tableDef.indexes) {
1927
- for (let i = 0;i < tableDef.indexes.length; i++) {
1928
- const cols = tableDef.indexes[i];
1929
- const idxName = `idx_${schemaName}_${tableName}_composite_${i}`;
1930
- statements.push(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${qualifiedName} (${cols.join(", ")})`);
1931
- }
1932
- }
1933
- if (tableDef.uniqueKeys) {
1934
- for (let i = 0;i < tableDef.uniqueKeys.length; i++) {
1935
- const cols = tableDef.uniqueKeys[i];
1936
- const constraintName = `uq_${schemaName}_${tableName}_${cols.join("_")}`;
1937
- statements.push(`ALTER TABLE ${qualifiedName} ADD CONSTRAINT ${constraintName} UNIQUE (${cols.join(", ")})`);
1938
- }
1939
- }
1940
- return statements;
1941
- }
1942
- function emitForeignKeyDDL(schemaName, tableName, tableDef) {
1943
- return (tableDef.relations ?? []).map((rel) => {
1944
- const constraintName = `fk_${schemaName}_${tableName}_${rel.name}`;
1945
- return `ALTER TABLE ${schemaName}.${tableName} ADD CONSTRAINT ${constraintName} ` + `FOREIGN KEY (${rel.fields.join(", ")}) ` + `REFERENCES ${schemaName}.${rel.references} (${rel.referencedColumns.join(", ")})`;
1946
- });
1947
- }
1948
- function generateSubgraphSQL(def, schemaNameOverride) {
1949
- const schemaName = schemaNameOverride ?? pgSchemaName(def.name);
1950
- const statements = [];
1951
- const needsTrgm = Object.values(def.schema).some((table) => Object.values(table.columns).some((col) => col.search));
1952
- if (needsTrgm) {
1953
- statements.push("CREATE EXTENSION IF NOT EXISTS pg_trgm");
1954
- }
1955
- statements.push(`CREATE SCHEMA IF NOT EXISTS ${schemaName}`);
1956
- for (const [tableName, tableDef] of Object.entries(def.schema)) {
1957
- statements.push(...emitTableDDL(schemaName, tableName, tableDef));
1958
- }
1959
- for (const [tableName, tableDef] of Object.entries(def.schema)) {
1960
- statements.push(...emitForeignKeyDDL(schemaName, tableName, tableDef));
1961
- }
1962
- const hashInput = JSON.stringify({
1963
- name: def.name,
1964
- schema: def.schema,
1965
- sources: def.sources
1966
- }, (_key, value) => typeof value === "bigint" ? value.toString() : value);
1967
- const hash = createHash2("sha256").update(hashInput).digest("hex");
1968
- return { statements, hash };
1969
- }
1970
-
1971
- // src/runtime/reindex.ts
1972
2295
  var LOG_INTERVAL = 1000;
1973
2296
  var HEALTH_FLUSH_INTERVAL = 1000;
1974
2297
  var PROGRESS_FLUSH_INTERVAL_MS = 5000;
2298
+ var EMPTY_BATCH_HALT_THRESHOLD = 3;
1975
2299
  var STANDARD_REINDEX_BATCH_CONFIG = {
1976
2300
  defaultBatchSize: 500,
1977
2301
  minBatchSize: 100,
@@ -2047,6 +2371,7 @@ async function processBlockRange(def, opts) {
2047
2371
  let batchSize = batchConfig.defaultBatchSize;
2048
2372
  let currentHeight = fromBlock;
2049
2373
  let aborted = false;
2374
+ let consecutiveEmptyBatches = 0;
2050
2375
  const sparse = Boolean(source.nextDataHeight && canSparseScan(def));
2051
2376
  const flushHealth = async () => {
2052
2377
  if (pendingEventsProcessed === 0 && pendingErrors === 0)
@@ -2058,6 +2383,13 @@ async function processBlockRange(def, opts) {
2058
2383
  lastHealthFlushBlock = blocksProcessed;
2059
2384
  lastHealthFlushAt = Date.now();
2060
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
+ };
2061
2393
  let nextBatchEnd = Math.min(currentHeight + batchSize - 1, toBlock);
2062
2394
  let nextBatchPromise = source.loadBlockRange(currentHeight, nextBatchEnd);
2063
2395
  while (currentHeight <= toBlock) {
@@ -2072,6 +2404,14 @@ async function processBlockRange(def, opts) {
2072
2404
  }
2073
2405
  const batch = await nextBatchPromise;
2074
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
+ }
2075
2415
  const nextStart = batchEnd + 1;
2076
2416
  if (nextStart <= toBlock) {
2077
2417
  nextBatchEnd = Math.min(nextStart + batchSize - 1, toBlock);
@@ -2079,28 +2419,39 @@ async function processBlockRange(def, opts) {
2079
2419
  }
2080
2420
  const batchFailedBlocks = [];
2081
2421
  let batchMatched = 0;
2422
+ const atomicProgress = status === "reindexing" ? { status } : undefined;
2082
2423
  for (let height = currentHeight;height <= batchEnd; height++) {
2083
- const blockData = batch.get(height);
2424
+ let blockData = batch.get(height);
2425
+ if (!blockData) {
2426
+ blockData = (await source.loadBlockRange(height, height)).get(height);
2427
+ }
2084
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
+ }
2085
2433
  batchFailedBlocks.push({ height, reason: "block_missing" });
2086
2434
  blocksProcessed++;
2087
2435
  continue;
2088
2436
  }
2089
2437
  let result;
2090
2438
  try {
2091
- result = await processBlock(def, subgraphName, height, {
2439
+ result = await processBlockWithRetry(def, subgraphName, height, {
2092
2440
  skipProgressUpdate: true,
2441
+ atomicProgress,
2093
2442
  preloaded: blockData
2094
2443
  });
2095
2444
  } catch (err) {
2096
- const errorMsg = err instanceof Error ? err.message : String(err);
2097
- logger6.error("Block processing error", {
2445
+ const errorMsg = getErrorMessage2(err);
2446
+ logger6.error("Block processing failed persistently", {
2098
2447
  subgraph: subgraphName,
2099
2448
  blockHeight: height,
2100
2449
  error: errorMsg
2101
2450
  });
2451
+ if (status === "reindexing") {
2452
+ await haltRange(`block ${height} failed persistently: ${errorMsg}`, height);
2453
+ }
2102
2454
  batchFailedBlocks.push({ height, reason: "processing_error" });
2103
- await updateSubgraphStatus2(targetDb, subgraphName, status, height).catch(() => {});
2104
2455
  blocksProcessed++;
2105
2456
  totalErrors++;
2106
2457
  pendingErrors++;
@@ -3266,5 +3617,5 @@ export {
3266
3617
  ByoBreakingChangeError
3267
3618
  };
3268
3619
 
3269
- //# debugId=415665ECDA2A436C64756E2164756E21
3620
+ //# debugId=A7DC4F802366B29864756E2164756E21
3270
3621
  //# sourceMappingURL=index.js.map