@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
@@ -5,6 +5,134 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
5
5
  import { logger } from "@secondlayer/shared/logger";
6
6
  import { formatUnits } from "@secondlayer/stacks/utils";
7
7
  import { sql } from "kysely";
8
+
9
+ // src/schema/generator.ts
10
+ import { createHash } from "node:crypto";
11
+
12
+ // src/schema/utils.ts
13
+ import { pgSchemaName } from "@secondlayer/shared/db/queries/subgraphs";
14
+
15
+ // src/schema/generator.ts
16
+ var TYPE_MAP = {
17
+ text: "TEXT",
18
+ uint: "NUMERIC",
19
+ int: "NUMERIC",
20
+ principal: "TEXT",
21
+ boolean: "BOOLEAN",
22
+ timestamp: "TIMESTAMPTZ",
23
+ jsonb: "JSONB"
24
+ };
25
+ function escapeLiteralDefault(value) {
26
+ if (value === null || value === undefined)
27
+ return "NULL";
28
+ if (typeof value === "number" || typeof value === "bigint")
29
+ return String(value);
30
+ if (typeof value === "boolean")
31
+ return value ? "TRUE" : "FALSE";
32
+ return `'${String(value).replace(/'/g, "''")}'`;
33
+ }
34
+ function tableNeedsTrgm(tableDef) {
35
+ return Object.values(tableDef.columns).some((col) => col.search);
36
+ }
37
+ function emitTableDDL(schemaName, tableName, tableDef) {
38
+ const qualifiedName = `${schemaName}.${tableName}`;
39
+ const statements = [];
40
+ const columnDefs = [
41
+ "_id BIGSERIAL PRIMARY KEY",
42
+ "_block_height BIGINT NOT NULL",
43
+ "_tx_id TEXT NOT NULL",
44
+ "_created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()"
45
+ ];
46
+ for (const [colName, col] of Object.entries(tableDef.columns)) {
47
+ const sqlType = TYPE_MAP[col.type];
48
+ const nullable = col.nullable ? "" : " NOT NULL";
49
+ let colDef = `${colName} ${sqlType}${nullable}`;
50
+ if (col.default !== undefined) {
51
+ colDef += ` DEFAULT ${escapeLiteralDefault(col.default)}`;
52
+ }
53
+ if (col.type === "uint") {
54
+ colDef += ` CHECK (${colName} >= 0)`;
55
+ }
56
+ columnDefs.push(colDef);
57
+ }
58
+ statements.push(`CREATE TABLE IF NOT EXISTS ${qualifiedName} (
59
+ ${columnDefs.join(`,
60
+ `)}
61
+ )`);
62
+ statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_block_height ON ${qualifiedName} (_block_height)`);
63
+ statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_tx_id ON ${qualifiedName} (_tx_id)`);
64
+ for (const [colName, col] of Object.entries(tableDef.columns)) {
65
+ if (col.indexed) {
66
+ statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName} ON ${qualifiedName} (${colName})`);
67
+ }
68
+ }
69
+ for (const [colName, col] of Object.entries(tableDef.columns)) {
70
+ if (col.search) {
71
+ statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName}_trgm ON ${qualifiedName} USING gin (${colName} gin_trgm_ops)`);
72
+ }
73
+ }
74
+ if (tableDef.indexes) {
75
+ for (let i = 0;i < tableDef.indexes.length; i++) {
76
+ const cols = tableDef.indexes[i];
77
+ const idxName = `idx_${schemaName}_${tableName}_composite_${i}`;
78
+ statements.push(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${qualifiedName} (${cols.join(", ")})`);
79
+ }
80
+ }
81
+ if (tableDef.uniqueKeys) {
82
+ for (let i = 0;i < tableDef.uniqueKeys.length; i++) {
83
+ const cols = tableDef.uniqueKeys[i];
84
+ const constraintName = `uq_${schemaName}_${tableName}_${cols.join("_")}`;
85
+ statements.push(`ALTER TABLE ${qualifiedName} ADD CONSTRAINT ${constraintName} UNIQUE (${cols.join(", ")})`);
86
+ }
87
+ }
88
+ return statements;
89
+ }
90
+ function emitJournalDDL(schemaName) {
91
+ return [
92
+ `CREATE TABLE IF NOT EXISTS ${schemaName}._journal (
93
+ _jid BIGSERIAL PRIMARY KEY,
94
+ block_height BIGINT NOT NULL,
95
+ table_name TEXT NOT NULL,
96
+ row_key JSONB NOT NULL,
97
+ prev_row JSONB,
98
+ _created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
99
+ )`,
100
+ `CREATE INDEX IF NOT EXISTS idx_${schemaName}_journal_height ON ${schemaName}._journal (block_height)`
101
+ ];
102
+ }
103
+ function emitForeignKeyDDL(schemaName, tableName, tableDef) {
104
+ return (tableDef.relations ?? []).map((rel) => {
105
+ const constraintName = `fk_${schemaName}_${tableName}_${rel.name}`;
106
+ return `ALTER TABLE ${schemaName}.${tableName} ADD CONSTRAINT ${constraintName} ` + `FOREIGN KEY (${rel.fields.join(", ")}) ` + `REFERENCES ${schemaName}.${rel.references} (${rel.referencedColumns.join(", ")})`;
107
+ });
108
+ }
109
+ function generateSubgraphSQL(def, schemaNameOverride) {
110
+ const schemaName = schemaNameOverride ?? pgSchemaName(def.name);
111
+ const statements = [];
112
+ const needsTrgm = Object.values(def.schema).some((table) => Object.values(table.columns).some((col) => col.search));
113
+ if (needsTrgm) {
114
+ statements.push("CREATE EXTENSION IF NOT EXISTS pg_trgm");
115
+ }
116
+ statements.push(`CREATE SCHEMA IF NOT EXISTS ${schemaName}`);
117
+ for (const [tableName, tableDef] of Object.entries(def.schema)) {
118
+ statements.push(...emitTableDDL(schemaName, tableName, tableDef));
119
+ }
120
+ statements.push(...emitJournalDDL(schemaName));
121
+ for (const [tableName, tableDef] of Object.entries(def.schema)) {
122
+ statements.push(...emitForeignKeyDDL(schemaName, tableName, tableDef));
123
+ }
124
+ const hashInput = JSON.stringify({
125
+ name: def.name,
126
+ schema: def.schema,
127
+ sources: def.sources
128
+ }, (_key, value) => typeof value === "bigint" ? value.toString() : value);
129
+ const hash = createHash("sha256").update(hashInput).digest("hex");
130
+ return { statements, hash };
131
+ }
132
+
133
+ // src/runtime/context.ts
134
+ var JOURNAL_RETENTION_BLOCKS = 300;
135
+ var journalEnsured = new Set;
8
136
  function validateColumnName(name) {
9
137
  if (!/^[a-z_][a-z0-9_]*$/i.test(name)) {
10
138
  throw new Error(`Invalid column name: ${name}`);
@@ -19,13 +147,15 @@ class SubgraphContext {
19
147
  subgraphSchema;
20
148
  ops = [];
21
149
  byo;
22
- constructor(db, pgSchemaName, subgraphSchema, block, tx, byo = false) {
150
+ journal;
151
+ constructor(db, pgSchemaName2, subgraphSchema, block, tx, byo = false, journal = false) {
23
152
  this.db = db;
24
- this.pgSchemaName = pgSchemaName;
153
+ this.pgSchemaName = pgSchemaName2;
25
154
  this.subgraphSchema = subgraphSchema;
26
155
  this.block = block;
27
156
  this._tx = tx;
28
157
  this.byo = byo;
158
+ this.journal = journal;
29
159
  }
30
160
  get tx() {
31
161
  return this._tx;
@@ -81,6 +211,43 @@ class SubgraphContext {
81
211
  this.validateTable(table);
82
212
  this.ops.push({ kind: "delete", table, data: where });
83
213
  }
214
+ increment(table, key, deltas) {
215
+ this.validateTable(table);
216
+ const tableDef = this.subgraphSchema[table];
217
+ const keyColumns = Object.keys(key);
218
+ const hasUniqueConstraint = tableDef?.uniqueKeys?.some((uk) => uk.length === keyColumns.length && uk.every((c) => keyColumns.includes(c)));
219
+ if (!hasUniqueConstraint) {
220
+ throw new Error(`increment("${table}") requires a uniqueKeys constraint on [${keyColumns.join(", ")}]`);
221
+ }
222
+ for (const [col, v] of Object.entries(deltas)) {
223
+ validateColumnName(col);
224
+ if (keyColumns.includes(col)) {
225
+ throw new Error(`increment("${table}"): "${col}" is a key column`);
226
+ }
227
+ if (typeof v !== "bigint" && typeof v !== "number") {
228
+ throw new Error(`increment("${table}"): delta for "${col}" must be bigint or number`);
229
+ }
230
+ }
231
+ this.ops.push({
232
+ kind: "increment",
233
+ table,
234
+ data: {
235
+ ...key,
236
+ _block_height: this.block.height,
237
+ _tx_id: this._tx.txId,
238
+ _upsert_keys: keyColumns
239
+ },
240
+ set: { ...deltas }
241
+ });
242
+ }
243
+ opsCheckpoint() {
244
+ return this.ops.length;
245
+ }
246
+ rollbackTo(checkpoint) {
247
+ if (checkpoint < 0 || checkpoint > this.ops.length)
248
+ return;
249
+ this.ops.length = checkpoint;
250
+ }
84
251
  patch(table, where, set) {
85
252
  this.update(table, where, set);
86
253
  }
@@ -102,7 +269,7 @@ class SubgraphContext {
102
269
  const query = `SELECT * FROM ${qualifiedTable} WHERE ${clause} LIMIT 1`;
103
270
  const { rows } = await sql.raw(query).execute(this.db);
104
271
  const row = rows[0] ?? null;
105
- return row ? this.coerceRow(table, row) : null;
272
+ return this.overlayOne(table, where, row ? this.coerceRow(table, row) : null);
106
273
  }
107
274
  async findMany(table, where) {
108
275
  this.validateTable(table);
@@ -110,7 +277,85 @@ class SubgraphContext {
110
277
  const { clause } = buildWhereClause(where);
111
278
  const query = `SELECT * FROM ${qualifiedTable} WHERE ${clause}`;
112
279
  const { rows } = await sql.raw(query).execute(this.db);
113
- return rows.map((r) => this.coerceRow(table, r));
280
+ const dbRows = rows.map((r) => this.coerceRow(table, r));
281
+ return this.overlayMany(table, where, dbRows);
282
+ }
283
+ overlayOne(table, where, dbRow) {
284
+ let row = dbRow;
285
+ for (const op of this.ops) {
286
+ if (op.table !== table)
287
+ continue;
288
+ row = this.applyOpToRow(op, row, where);
289
+ }
290
+ return row;
291
+ }
292
+ overlayMany(table, where, dbRows) {
293
+ let result = [...dbRows];
294
+ for (const op of this.ops) {
295
+ if (op.table !== table)
296
+ continue;
297
+ if (op.kind === "update") {
298
+ result = result.map((r) => rowMatches(r, op.data) ? { ...r, ...op.set ?? {} } : r);
299
+ } else if (op.kind === "delete") {
300
+ result = result.filter((r) => !rowMatches(r, op.data));
301
+ } else {
302
+ const upsertKeys = op.data._upsert_keys;
303
+ const clean = stripControlKeys(op.data);
304
+ const idx = upsertKeys ? result.findIndex((r) => upsertKeys.every((k) => valEq(r[k], clean[k]))) : -1;
305
+ if (idx >= 0) {
306
+ result[idx] = this.applyOpToRow(op, result[idx], where) ?? result[idx];
307
+ } else {
308
+ const created = this.applyOpToRow(op, null, where);
309
+ if (created)
310
+ result.push(created);
311
+ }
312
+ }
313
+ }
314
+ return result;
315
+ }
316
+ applyOpToRow(op, row, where) {
317
+ const upsertKeys = op.data._upsert_keys;
318
+ const clean = stripControlKeys(op.data);
319
+ switch (op.kind) {
320
+ case "insert": {
321
+ if (row) {
322
+ if (upsertKeys?.every((k) => valEq(row[k], clean[k]))) {
323
+ const merged = { ...row };
324
+ for (const [k, v] of Object.entries(clean)) {
325
+ if (!upsertKeys.includes(k) && !k.startsWith("_"))
326
+ merged[k] = v;
327
+ }
328
+ return merged;
329
+ }
330
+ return row;
331
+ }
332
+ return rowMatches(clean, where) ? { ...clean } : null;
333
+ }
334
+ case "increment": {
335
+ const deltas = op.set ?? {};
336
+ if (row) {
337
+ if (upsertKeys.every((k) => valEq(row[k], clean[k]))) {
338
+ const merged = { ...row };
339
+ for (const [col, d] of Object.entries(deltas)) {
340
+ merged[col] = toBigIntOr0(merged[col]) + toBigIntOr0(d);
341
+ }
342
+ return merged;
343
+ }
344
+ return row;
345
+ }
346
+ if (!rowMatches(clean, where))
347
+ return null;
348
+ const created = { ...clean };
349
+ for (const [col, d] of Object.entries(deltas)) {
350
+ created[col] = toBigIntOr0(d);
351
+ }
352
+ return created;
353
+ }
354
+ case "update":
355
+ return row && rowMatches(row, op.data) ? { ...row, ...op.set ?? {} } : row;
356
+ case "delete":
357
+ return row && rowMatches(row, op.data) ? null : row;
358
+ }
114
359
  }
115
360
  async count(table, where) {
116
361
  this.validateTable(table);
@@ -171,6 +416,7 @@ class SubgraphContext {
171
416
  async flush() {
172
417
  if (this.ops.length === 0)
173
418
  return { count: 0, writes: [] };
419
+ await this.ensureJournalTable();
174
420
  const opsToFlush = [...this.ops];
175
421
  this.ops.length = 0;
176
422
  const statements = this.buildStatements(opsToFlush);
@@ -188,12 +434,12 @@ class SubgraphContext {
188
434
  const writes = opsToFlush.map((op, rowIndex) => {
189
435
  const blockHeight = op.data._block_height ?? this.block.height;
190
436
  const txId = op.data._tx_id ?? this._tx.txId;
191
- const baseRow = op.kind === "update" ? { ...op.data, ...op.set ?? {} } : { ...op.data };
437
+ const baseRow = op.kind === "update" || op.kind === "increment" ? { ...op.data, ...op.set ?? {} } : { ...op.data };
192
438
  baseRow._upsert_keys = undefined;
193
439
  baseRow._upsert_fallback_keys = undefined;
194
440
  baseRow._upsert_fallback_set = undefined;
195
441
  return {
196
- op: op.kind,
442
+ op: op.kind === "increment" ? "update" : op.kind,
197
443
  table: op.table,
198
444
  row: jsonSafe(baseRow),
199
445
  pk: { blockHeight, txId, rowIndex }
@@ -218,6 +464,35 @@ class SubgraphContext {
218
464
  const batchKey = `${op.table}:${[...cols].sort().join(",")}:${upsertKeys ? [...upsertKeys].sort().join(",") : ""}`;
219
465
  return { data, cols, vals, upsertKeys, batchKey };
220
466
  }
467
+ async ensureJournalTable() {
468
+ if (!this.journal || journalEnsured.has(this.pgSchemaName))
469
+ return;
470
+ const { rows } = await sql.raw(`SELECT to_regclass('"${this.pgSchemaName}"."_journal"') AS r`).execute(this.db);
471
+ if (rows[0]?.r) {
472
+ journalEnsured.add(this.pgSchemaName);
473
+ return;
474
+ }
475
+ for (const stmt of emitJournalDDL(this.pgSchemaName)) {
476
+ await sql.raw(stmt).execute(this.db);
477
+ }
478
+ }
479
+ columnSqlType(table, col) {
480
+ const def = this.subgraphSchema[table]?.columns?.[col];
481
+ return def ? TYPE_MAP[def.type] : undefined;
482
+ }
483
+ journalCaptureSQL(table, keyCols, keyLiteralRows) {
484
+ const cast = (col, expr) => {
485
+ const t = this.columnSqlType(table, col);
486
+ return t ? `CAST(${expr} AS ${t})` : expr;
487
+ };
488
+ const keyObj = keyCols.map((k) => `'${k}', ${cast(k, `v."${k}"`)}`).join(", ");
489
+ const joinCond = keyCols.map((k) => `t."${k}" = ${cast(k, `v."${k}"`)}`).join(" AND ");
490
+ const valuesList = keyLiteralRows.map((r) => `(${r.join(", ")})`).join(", ");
491
+ 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}`;
492
+ }
493
+ journalCaptureByWhereSQL(table, clause) {
494
+ 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}`;
495
+ }
221
496
  buildStatements(ops) {
222
497
  const statements = [];
223
498
  if (this.byo) {
@@ -231,6 +506,38 @@ class SubgraphContext {
231
506
  }
232
507
  let currentBatch = null;
233
508
  let currentBatchKey = "";
509
+ let incBatch = null;
510
+ let incBatchKey = "";
511
+ const flushIncrementBatch = () => {
512
+ if (!incBatch)
513
+ return;
514
+ const batch = incBatch;
515
+ const qualifiedTable = `"${this.pgSchemaName}"."${batch.table}"`;
516
+ const cols = [
517
+ ...batch.keyCols,
518
+ ...batch.deltaCols,
519
+ "_block_height",
520
+ "_tx_id",
521
+ "_created_at"
522
+ ];
523
+ const valuesList = Array.from(batch.rows.values()).map((r) => {
524
+ const vals = [
525
+ ...batch.keyCols.map((k) => escapeLiteral(r.keys[k])),
526
+ ...batch.deltaCols.map((c) => String(r.deltas[c] ?? 0n)),
527
+ escapeLiteral(r.meta.blockHeight),
528
+ escapeLiteral(r.meta.txId),
529
+ "NOW()"
530
+ ];
531
+ return `(${vals.join(", ")})`;
532
+ }).join(", ");
533
+ const setClauses = batch.deltaCols.map((c) => `"${c}" = COALESCE("${batch.table}"."${c}", 0) + EXCLUDED."${c}"`);
534
+ if (this.journal) {
535
+ statements.push(this.journalCaptureSQL(batch.table, batch.keyCols, Array.from(batch.rows.values()).map((r) => batch.keyCols.map((k) => escapeLiteral(r.keys[k])))));
536
+ }
537
+ 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(", ")}`);
538
+ incBatch = null;
539
+ incBatchKey = "";
540
+ };
234
541
  const flushInsertBatch = () => {
235
542
  if (!currentBatch)
236
543
  return;
@@ -252,6 +559,11 @@ class SubgraphContext {
252
559
  }
253
560
  const valuesList = rows.map((r) => `(${r.join(", ")})`).join(", ");
254
561
  let stmt = `INSERT INTO ${qualifiedTable} (${colList}) VALUES ${valuesList}`;
562
+ if (this.journal && batch.upsertKeys && batch.upsertKeys.length > 0) {
563
+ const uKeys = batch.upsertKeys;
564
+ const keyIndices = uKeys.map((k) => batch.cols.indexOf(k));
565
+ statements.push(this.journalCaptureSQL(batch.table, uKeys, rows.map((r) => keyIndices.map((ki) => r[ki]))));
566
+ }
255
567
  if (batch.upsertKeys && batch.upsertKeys.length > 0) {
256
568
  const batchKeys = batch.upsertKeys;
257
569
  const updateCols = batch.cols.filter((c) => !batchKeys.includes(c) && !c.startsWith("_"));
@@ -269,6 +581,7 @@ class SubgraphContext {
269
581
  for (const op of ops) {
270
582
  const qualifiedTable = `"${this.pgSchemaName}"."${op.table}"`;
271
583
  if (op.kind === "insert") {
584
+ flushIncrementBatch();
272
585
  const { cols, vals, upsertKeys, batchKey } = this.prepareInsert(op);
273
586
  if (batchKey === currentBatchKey && currentBatch) {
274
587
  currentBatch.rows.push(vals);
@@ -277,22 +590,60 @@ class SubgraphContext {
277
590
  currentBatch = { table: op.table, cols, rows: [vals], upsertKeys };
278
591
  currentBatchKey = batchKey;
279
592
  }
593
+ } else if (op.kind === "increment") {
594
+ flushInsertBatch();
595
+ const keyCols = [...op.data._upsert_keys].sort();
596
+ const deltaCols = Object.keys(op.set ?? {}).sort();
597
+ const batchKey = `inc:${op.table}:${keyCols.join(",")}:${deltaCols.join(",")}`;
598
+ if (batchKey !== incBatchKey || !incBatch) {
599
+ flushIncrementBatch();
600
+ incBatch = { table: op.table, keyCols, deltaCols, rows: new Map };
601
+ incBatchKey = batchKey;
602
+ }
603
+ const clean = stripControlKeys(op.data);
604
+ const keySig = keyCols.map((k) => escapeLiteral(clean[k])).join("\x00");
605
+ const existing = incBatch.rows.get(keySig);
606
+ if (existing) {
607
+ for (const c of deltaCols) {
608
+ existing.deltas[c] = (existing.deltas[c] ?? 0n) + toBigIntOr0(op.set?.[c]);
609
+ }
610
+ } else {
611
+ const deltas = {};
612
+ for (const c of deltaCols)
613
+ deltas[c] = toBigIntOr0(op.set?.[c]);
614
+ incBatch.rows.set(keySig, {
615
+ keys: clean,
616
+ deltas,
617
+ meta: {
618
+ blockHeight: op.data._block_height ?? this.block.height,
619
+ txId: op.data._tx_id ?? this._tx.txId
620
+ }
621
+ });
622
+ }
280
623
  } else {
281
624
  flushInsertBatch();
625
+ flushIncrementBatch();
282
626
  if (op.kind === "update") {
283
627
  const setEntries = Object.entries(op.set ?? {});
284
628
  for (const [k] of setEntries)
285
629
  validateColumnName(k);
286
630
  const setClauses = setEntries.map(([k, v]) => `"${k}" = ${escapeLiteral(v)}`);
287
631
  const { clause } = buildWhereClause(op.data);
632
+ if (this.journal) {
633
+ statements.push(this.journalCaptureByWhereSQL(op.table, clause));
634
+ }
288
635
  statements.push(`UPDATE ${qualifiedTable} SET ${setClauses.join(", ")} WHERE ${clause}`);
289
636
  } else if (op.kind === "delete") {
290
637
  const { clause } = buildWhereClause(op.data);
638
+ if (this.journal) {
639
+ statements.push(this.journalCaptureByWhereSQL(op.table, clause));
640
+ }
291
641
  statements.push(`DELETE FROM ${qualifiedTable} WHERE ${clause}`);
292
642
  }
293
643
  }
294
644
  }
295
645
  flushInsertBatch();
646
+ flushIncrementBatch();
296
647
  return statements;
297
648
  }
298
649
  validateTable(table) {
@@ -301,6 +652,36 @@ class SubgraphContext {
301
652
  }
302
653
  }
303
654
  }
655
+ function stripControlKeys(data) {
656
+ const {
657
+ _upsert_keys: _a,
658
+ _upsert_fallback_keys: _b,
659
+ _upsert_fallback_set: _c,
660
+ ...clean
661
+ } = data;
662
+ return clean;
663
+ }
664
+ function valEq(a, b) {
665
+ if (a === b)
666
+ return true;
667
+ if (a == null || b == null)
668
+ return false;
669
+ return String(a) === String(b);
670
+ }
671
+ function rowMatches(row, where) {
672
+ return Object.entries(where).every(([k, v]) => valEq(row[k], v));
673
+ }
674
+ function toBigIntOr0(v) {
675
+ if (typeof v === "bigint")
676
+ return v;
677
+ if (v == null)
678
+ return 0n;
679
+ try {
680
+ return BigInt(String(v));
681
+ } catch {
682
+ return 0n;
683
+ }
684
+ }
304
685
  function jsonSafe(row) {
305
686
  const out = {};
306
687
  for (const [k, v] of Object.entries(row)) {
@@ -619,7 +1000,25 @@ async function runHandlers(subgraph, matched, ctx, opts) {
619
1000
  filterLookup.set(name, filter);
620
1001
  }
621
1002
  }
1003
+ const units = [];
622
1004
  for (const { tx, events, sourceName } of matched) {
1005
+ if (events.length === 0) {
1006
+ units.push({ tx, sourceName, event: null });
1007
+ } else {
1008
+ for (const event of events)
1009
+ units.push({ tx, sourceName, event });
1010
+ }
1011
+ }
1012
+ units.sort((a, b) => (a.tx.tx_index ?? 0) - (b.tx.tx_index ?? 0) || (a.event?.event_index ?? -1) - (b.event?.event_index ?? -1));
1013
+ for (const { tx, event, sourceName } of units) {
1014
+ if (errors >= threshold) {
1015
+ logger2.error("Subgraph error threshold reached, skipping remaining events", {
1016
+ subgraph: subgraph.name,
1017
+ errors,
1018
+ threshold
1019
+ });
1020
+ return { processed, errors };
1021
+ }
623
1022
  const handler = subgraph.handlers[sourceName] ?? subgraph.handlers["*"] ?? null;
624
1023
  if (!handler) {
625
1024
  logger2.warn("No handler found for source", {
@@ -638,9 +1037,29 @@ async function runHandlers(subgraph, matched, ctx, opts) {
638
1037
  functionName: tx.function_name ?? null
639
1038
  });
640
1039
  const filter = filterLookup.get(sourceName);
641
- if (events.length === 0) {
642
- try {
643
- const payload = filter ? buildEventPayload(filter, tx, null) : {
1040
+ const checkpoint = ctx.opsCheckpoint();
1041
+ try {
1042
+ let payload;
1043
+ if (event === null) {
1044
+ payload = filter ? buildEventPayload(filter, tx, null) : {
1045
+ tx: {
1046
+ txId: tx.tx_id,
1047
+ sender: tx.sender,
1048
+ type: tx.type,
1049
+ status: tx.status,
1050
+ contractId: tx.contract_id,
1051
+ functionName: tx.function_name
1052
+ }
1053
+ };
1054
+ } else if (filter) {
1055
+ payload = buildEventPayload(filter, tx, event);
1056
+ } else {
1057
+ const decoded = decodeEventData(event.data);
1058
+ payload = {
1059
+ ...decoded,
1060
+ _eventId: event.id,
1061
+ _eventType: event.type,
1062
+ _eventIndex: event.event_index,
644
1063
  tx: {
645
1064
  txId: tx.tx_id,
646
1065
  sender: tx.sender,
@@ -650,62 +1069,22 @@ async function runHandlers(subgraph, matched, ctx, opts) {
650
1069
  functionName: tx.function_name
651
1070
  }
652
1071
  };
653
- await handler(payload, ctx);
654
- processed++;
655
- } catch (err) {
656
- errors++;
657
- logger2.error("Subgraph handler error", {
658
- subgraph: subgraph.name,
659
- sourceName,
660
- txId: tx.tx_id,
661
- error: getErrorMessage(err)
662
- });
663
- }
664
- continue;
665
- }
666
- for (const event of events) {
667
- if (errors >= threshold) {
668
- logger2.error("Subgraph error threshold reached, skipping remaining events", {
669
- subgraph: subgraph.name,
670
- errors,
671
- threshold
672
- });
673
- return { processed, errors };
674
1072
  }
675
- try {
676
- const payload = filter ? buildEventPayload(filter, tx, event) : (() => {
677
- const decoded = decodeEventData(event.data);
678
- return {
679
- ...decoded,
680
- _eventId: event.id,
681
- _eventType: event.type,
682
- _eventIndex: event.event_index,
683
- tx: {
684
- txId: tx.tx_id,
685
- sender: tx.sender,
686
- type: tx.type,
687
- status: tx.status,
688
- contractId: tx.contract_id,
689
- functionName: tx.function_name
690
- }
691
- };
692
- })();
693
- if (filter?.type === "print_event" && filter.topic && payload.topic !== filter.topic) {
694
- continue;
695
- }
696
- await handler(payload, ctx);
697
- processed++;
698
- } catch (err) {
699
- errors++;
700
- logger2.error("Subgraph handler error", {
701
- subgraph: subgraph.name,
702
- sourceName,
703
- txId: tx.tx_id,
704
- eventId: event.id,
705
- eventType: event.type,
706
- error: getErrorMessage(err)
707
- });
1073
+ if (event !== null && filter?.type === "print_event" && filter.topic && payload.topic !== filter.topic) {
1074
+ continue;
708
1075
  }
1076
+ await handler(payload, ctx);
1077
+ processed++;
1078
+ } catch (err) {
1079
+ ctx.rollbackTo(checkpoint);
1080
+ errors++;
1081
+ logger2.error("Subgraph handler error", {
1082
+ subgraph: subgraph.name,
1083
+ sourceName,
1084
+ txId: tx.tx_id,
1085
+ ...event !== null ? { eventId: event.id, eventType: event.type } : {},
1086
+ error: getErrorMessage(err)
1087
+ });
709
1088
  }
710
1089
  }
711
1090
  return { processed, errors };
@@ -962,9 +1341,6 @@ import {
962
1341
  import { logger as logger5 } from "@secondlayer/shared/logger";
963
1342
  import { sql as sql3 } from "kysely";
964
1343
 
965
- // src/schema/utils.ts
966
- import { pgSchemaName } from "@secondlayer/shared/db/queries/subgraphs";
967
-
968
1344
  // src/runtime/block-source.ts
969
1345
  import { getSourceDb } from "@secondlayer/shared/db";
970
1346
  import { IndexHttpClient } from "@secondlayer/shared/index-http";
@@ -1310,7 +1686,7 @@ function resolveBlockSource(subgraph) {
1310
1686
  }
1311
1687
 
1312
1688
  // src/runtime/outbox-emit.ts
1313
- import { createHash } from "node:crypto";
1689
+ import { createHash as createHash2 } from "node:crypto";
1314
1690
  import { logger as logger4 } from "@secondlayer/shared/logger";
1315
1691
  var loggedKillSwitch = false;
1316
1692
  var OP_VERB = {
@@ -1323,7 +1699,7 @@ function isEmitOutboxEnabled() {
1323
1699
  }
1324
1700
  function dedupKey(subgraphName, tableName, blockHeight, txId, rowIndex, row) {
1325
1701
  const canonical = `${subgraphName}:${tableName}:${blockHeight}:${txId}:${rowIndex}:${stableStringify(row)}`;
1326
- return createHash("sha256").update(canonical).digest("hex").slice(0, 32);
1702
+ return createHash2("sha256").update(canonical).digest("hex").slice(0, 32);
1327
1703
  }
1328
1704
  function stableStringify(obj) {
1329
1705
  const keys = Object.keys(obj).sort();
@@ -1575,6 +1951,32 @@ async function resolveTraitContracts(subgraph, blockHeight, db) {
1575
1951
  }
1576
1952
  return resolved;
1577
1953
  }
1954
+ var BLOCK_RETRY_DELAYS_MS = [500, 2000, 5000];
1955
+ function journalEnabled(opts) {
1956
+ return !opts?.skipProgressUpdate;
1957
+ }
1958
+ async function processBlockWithRetry(subgraph, subgraphName, blockHeight, opts, retryDelaysMs = BLOCK_RETRY_DELAYS_MS) {
1959
+ let lastError;
1960
+ for (let attempt = 0;attempt <= retryDelaysMs.length; attempt++) {
1961
+ try {
1962
+ return await processBlock(subgraph, subgraphName, blockHeight, opts);
1963
+ } catch (err) {
1964
+ lastError = err;
1965
+ const delay = retryDelaysMs[attempt];
1966
+ if (delay === undefined)
1967
+ break;
1968
+ logger5.warn("Block processing failed, retrying", {
1969
+ subgraph: subgraphName,
1970
+ blockHeight,
1971
+ attempt: attempt + 1,
1972
+ retryInMs: delay,
1973
+ error: err instanceof Error ? err.message : String(err)
1974
+ });
1975
+ await new Promise((r) => setTimeout(r, delay));
1976
+ }
1977
+ }
1978
+ throw lastError;
1979
+ }
1578
1980
  async function processBlock(subgraph, subgraphName, blockHeight, opts) {
1579
1981
  const targetDb = getTargetDb();
1580
1982
  const blockStart = performance.now();
@@ -1642,10 +2044,17 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
1642
2044
  }
1643
2045
  };
1644
2046
  if (route.byo) {
2047
+ if (opts?.atomicProgress) {
2048
+ const row = await targetDb.selectFrom("subgraphs").select("last_processed_block").where("name", "=", subgraphName).executeTakeFirst();
2049
+ if (row && Number(row.last_processed_block) >= blockHeight) {
2050
+ result.skipped = true;
2051
+ return result;
2052
+ }
2053
+ }
1645
2054
  let runResult = { processed: 0, errors: 0 };
1646
2055
  let manifest;
1647
2056
  await route.dataDb.transaction().execute(async (tx) => {
1648
- const ctx = new SubgraphContext(tx, schemaName, subgraph.schema, blockMeta, initialTx, true);
2057
+ const ctx = new SubgraphContext(tx, schemaName, subgraph.schema, blockMeta, initialTx, true, journalEnabled(opts));
1649
2058
  const handlerStart = performance.now();
1650
2059
  runResult = await runHandlers(subgraph, matched, ctx);
1651
2060
  handlerMs = performance.now() - handlerStart;
@@ -1661,24 +2070,39 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
1661
2070
  if (manifest && manifest.count > 0) {
1662
2071
  await emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, block.height);
1663
2072
  }
2073
+ if (opts?.atomicProgress && manifest && manifest.count > 0) {
2074
+ await updateSubgraphStatus(tx, subgraphName, opts.atomicProgress.status, blockHeight);
2075
+ }
1664
2076
  await applyProgress(tx, runResult);
1665
2077
  });
1666
2078
  } else {
1667
2079
  await targetDb.transaction().execute(async (tx) => {
1668
- const ctx = new SubgraphContext(tx, schemaName, subgraph.schema, blockMeta, initialTx);
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;
2085
+ }
2086
+ }
2087
+ const ctx = new SubgraphContext(tx, schemaName, subgraph.schema, blockMeta, initialTx, false, journalEnabled(opts));
1669
2088
  const handlerStart = performance.now();
1670
2089
  const runResult = await runHandlers(subgraph, matched, ctx);
1671
2090
  handlerMs = performance.now() - handlerStart;
1672
2091
  result.processed = runResult.processed;
1673
2092
  result.errors = runResult.errors;
2093
+ let flushedWrites = false;
1674
2094
  if (ctx.pendingOps > 0) {
1675
2095
  const flushStart = performance.now();
1676
2096
  const manifest = await ctx.flush();
2097
+ flushedWrites = manifest.count > 0;
1677
2098
  if (manifest.count > 0) {
1678
2099
  await emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, block.height);
1679
2100
  }
1680
2101
  flushMs = performance.now() - flushStart;
1681
2102
  }
2103
+ if (opts?.atomicProgress && flushedWrites) {
2104
+ await updateSubgraphStatus(tx, subgraphName, opts.atomicProgress.status, blockHeight);
2105
+ }
1682
2106
  await applyProgress(tx, runResult);
1683
2107
  });
1684
2108
  }
@@ -1708,6 +2132,9 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
1708
2132
  error: err instanceof Error ? err.message : String(err)
1709
2133
  });
1710
2134
  }
2135
+ if (journalEnabled(opts)) {
2136
+ await sql3.raw(`DELETE FROM "${schemaName}"."_journal" WHERE "block_height" < ${blockHeight - JOURNAL_RETENTION_BLOCKS}`).execute(route.dataDb).catch(() => {});
2137
+ }
1711
2138
  }
1712
2139
  return result;
1713
2140
  }
@@ -1782,118 +2209,16 @@ import {
1782
2209
  recordGapBatch,
1783
2210
  resolveGaps
1784
2211
  } from "@secondlayer/shared/db/queries/subgraph-gaps";
2212
+ import { updateOperationProcessedEvents } from "@secondlayer/shared/db/queries/subgraph-operations";
1785
2213
  import {
1786
2214
  recordSubgraphProcessed as recordSubgraphProcessed2,
1787
2215
  updateSubgraphStatus as updateSubgraphStatus2
1788
2216
  } from "@secondlayer/shared/db/queries/subgraphs";
1789
2217
  import { logger as logger6 } from "@secondlayer/shared/logger";
1790
-
1791
- // src/schema/generator.ts
1792
- import { createHash as createHash2 } from "node:crypto";
1793
- var TYPE_MAP = {
1794
- text: "TEXT",
1795
- uint: "NUMERIC",
1796
- int: "NUMERIC",
1797
- principal: "TEXT",
1798
- boolean: "BOOLEAN",
1799
- timestamp: "TIMESTAMPTZ",
1800
- jsonb: "JSONB"
1801
- };
1802
- function escapeLiteralDefault(value) {
1803
- if (value === null || value === undefined)
1804
- return "NULL";
1805
- if (typeof value === "number" || typeof value === "bigint")
1806
- return String(value);
1807
- if (typeof value === "boolean")
1808
- return value ? "TRUE" : "FALSE";
1809
- return `'${String(value).replace(/'/g, "''")}'`;
1810
- }
1811
- function tableNeedsTrgm(tableDef) {
1812
- return Object.values(tableDef.columns).some((col) => col.search);
1813
- }
1814
- function emitTableDDL(schemaName, tableName, tableDef) {
1815
- const qualifiedName = `${schemaName}.${tableName}`;
1816
- const statements = [];
1817
- const columnDefs = [
1818
- "_id BIGSERIAL PRIMARY KEY",
1819
- "_block_height BIGINT NOT NULL",
1820
- "_tx_id TEXT NOT NULL",
1821
- "_created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()"
1822
- ];
1823
- for (const [colName, col] of Object.entries(tableDef.columns)) {
1824
- const sqlType = TYPE_MAP[col.type];
1825
- const nullable = col.nullable ? "" : " NOT NULL";
1826
- let colDef = `${colName} ${sqlType}${nullable}`;
1827
- if (col.default !== undefined) {
1828
- colDef += ` DEFAULT ${escapeLiteralDefault(col.default)}`;
1829
- }
1830
- columnDefs.push(colDef);
1831
- }
1832
- statements.push(`CREATE TABLE IF NOT EXISTS ${qualifiedName} (
1833
- ${columnDefs.join(`,
1834
- `)}
1835
- )`);
1836
- statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_block_height ON ${qualifiedName} (_block_height)`);
1837
- statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_tx_id ON ${qualifiedName} (_tx_id)`);
1838
- for (const [colName, col] of Object.entries(tableDef.columns)) {
1839
- if (col.indexed) {
1840
- statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName} ON ${qualifiedName} (${colName})`);
1841
- }
1842
- }
1843
- for (const [colName, col] of Object.entries(tableDef.columns)) {
1844
- if (col.search) {
1845
- statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName}_trgm ON ${qualifiedName} USING gin (${colName} gin_trgm_ops)`);
1846
- }
1847
- }
1848
- if (tableDef.indexes) {
1849
- for (let i = 0;i < tableDef.indexes.length; i++) {
1850
- const cols = tableDef.indexes[i];
1851
- const idxName = `idx_${schemaName}_${tableName}_composite_${i}`;
1852
- statements.push(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${qualifiedName} (${cols.join(", ")})`);
1853
- }
1854
- }
1855
- if (tableDef.uniqueKeys) {
1856
- for (let i = 0;i < tableDef.uniqueKeys.length; i++) {
1857
- const cols = tableDef.uniqueKeys[i];
1858
- const constraintName = `uq_${schemaName}_${tableName}_${cols.join("_")}`;
1859
- statements.push(`ALTER TABLE ${qualifiedName} ADD CONSTRAINT ${constraintName} UNIQUE (${cols.join(", ")})`);
1860
- }
1861
- }
1862
- return statements;
1863
- }
1864
- function emitForeignKeyDDL(schemaName, tableName, tableDef) {
1865
- return (tableDef.relations ?? []).map((rel) => {
1866
- const constraintName = `fk_${schemaName}_${tableName}_${rel.name}`;
1867
- return `ALTER TABLE ${schemaName}.${tableName} ADD CONSTRAINT ${constraintName} ` + `FOREIGN KEY (${rel.fields.join(", ")}) ` + `REFERENCES ${schemaName}.${rel.references} (${rel.referencedColumns.join(", ")})`;
1868
- });
1869
- }
1870
- function generateSubgraphSQL(def, schemaNameOverride) {
1871
- const schemaName = schemaNameOverride ?? pgSchemaName(def.name);
1872
- const statements = [];
1873
- const needsTrgm = Object.values(def.schema).some((table) => Object.values(table.columns).some((col) => col.search));
1874
- if (needsTrgm) {
1875
- statements.push("CREATE EXTENSION IF NOT EXISTS pg_trgm");
1876
- }
1877
- statements.push(`CREATE SCHEMA IF NOT EXISTS ${schemaName}`);
1878
- for (const [tableName, tableDef] of Object.entries(def.schema)) {
1879
- statements.push(...emitTableDDL(schemaName, tableName, tableDef));
1880
- }
1881
- for (const [tableName, tableDef] of Object.entries(def.schema)) {
1882
- statements.push(...emitForeignKeyDDL(schemaName, tableName, tableDef));
1883
- }
1884
- const hashInput = JSON.stringify({
1885
- name: def.name,
1886
- schema: def.schema,
1887
- sources: def.sources
1888
- }, (_key, value) => typeof value === "bigint" ? value.toString() : value);
1889
- const hash = createHash2("sha256").update(hashInput).digest("hex");
1890
- return { statements, hash };
1891
- }
1892
-
1893
- // src/runtime/reindex.ts
1894
2218
  var LOG_INTERVAL = 1000;
1895
2219
  var HEALTH_FLUSH_INTERVAL = 1000;
1896
2220
  var PROGRESS_FLUSH_INTERVAL_MS = 5000;
2221
+ var EMPTY_BATCH_HALT_THRESHOLD = 3;
1897
2222
  var STANDARD_REINDEX_BATCH_CONFIG = {
1898
2223
  defaultBatchSize: 500,
1899
2224
  minBatchSize: 100,
@@ -1969,6 +2294,7 @@ async function processBlockRange(def, opts) {
1969
2294
  let batchSize = batchConfig.defaultBatchSize;
1970
2295
  let currentHeight = fromBlock;
1971
2296
  let aborted = false;
2297
+ let consecutiveEmptyBatches = 0;
1972
2298
  const sparse = Boolean(source.nextDataHeight && canSparseScan(def));
1973
2299
  const flushHealth = async () => {
1974
2300
  if (pendingEventsProcessed === 0 && pendingErrors === 0)
@@ -1980,6 +2306,13 @@ async function processBlockRange(def, opts) {
1980
2306
  lastHealthFlushBlock = blocksProcessed;
1981
2307
  lastHealthFlushAt = Date.now();
1982
2308
  };
2309
+ const haltRange = async (errorMsg, height) => {
2310
+ pendingErrors++;
2311
+ pendingLastError = errorMsg;
2312
+ await flushHealth().catch(() => {});
2313
+ await updateSubgraphStatus2(targetDb, subgraphName, "error").catch(() => {});
2314
+ throw new Error(`${subgraphName}: halted at block ${height}: ${errorMsg}`);
2315
+ };
1983
2316
  let nextBatchEnd = Math.min(currentHeight + batchSize - 1, toBlock);
1984
2317
  let nextBatchPromise = source.loadBlockRange(currentHeight, nextBatchEnd);
1985
2318
  while (currentHeight <= toBlock) {
@@ -1994,6 +2327,14 @@ async function processBlockRange(def, opts) {
1994
2327
  }
1995
2328
  const batch = await nextBatchPromise;
1996
2329
  const batchEnd = nextBatchEnd;
2330
+ if (batch.size === 0 && batchEnd >= currentHeight) {
2331
+ consecutiveEmptyBatches++;
2332
+ if (consecutiveEmptyBatches >= EMPTY_BATCH_HALT_THRESHOLD) {
2333
+ await haltRange(`block source returned ${consecutiveEmptyBatches} consecutive empty batches (ending ${currentHeight}..${batchEnd}) — source degraded`, currentHeight);
2334
+ }
2335
+ } else {
2336
+ consecutiveEmptyBatches = 0;
2337
+ }
1997
2338
  const nextStart = batchEnd + 1;
1998
2339
  if (nextStart <= toBlock) {
1999
2340
  nextBatchEnd = Math.min(nextStart + batchSize - 1, toBlock);
@@ -2001,28 +2342,39 @@ async function processBlockRange(def, opts) {
2001
2342
  }
2002
2343
  const batchFailedBlocks = [];
2003
2344
  let batchMatched = 0;
2345
+ const atomicProgress = status === "reindexing" ? { status } : undefined;
2004
2346
  for (let height = currentHeight;height <= batchEnd; height++) {
2005
- const blockData = batch.get(height);
2347
+ let blockData = batch.get(height);
2348
+ if (!blockData) {
2349
+ blockData = (await source.loadBlockRange(height, height)).get(height);
2350
+ }
2006
2351
  if (!blockData) {
2352
+ if (status === "reindexing") {
2353
+ const errorMsg = `block ${height} missing from source — halting reindex (cursor stays at ${height - 1})`;
2354
+ await haltRange(errorMsg, height);
2355
+ }
2007
2356
  batchFailedBlocks.push({ height, reason: "block_missing" });
2008
2357
  blocksProcessed++;
2009
2358
  continue;
2010
2359
  }
2011
2360
  let result;
2012
2361
  try {
2013
- result = await processBlock(def, subgraphName, height, {
2362
+ result = await processBlockWithRetry(def, subgraphName, height, {
2014
2363
  skipProgressUpdate: true,
2364
+ atomicProgress,
2015
2365
  preloaded: blockData
2016
2366
  });
2017
2367
  } catch (err) {
2018
- const errorMsg = err instanceof Error ? err.message : String(err);
2019
- logger6.error("Block processing error", {
2368
+ const errorMsg = getErrorMessage2(err);
2369
+ logger6.error("Block processing failed persistently", {
2020
2370
  subgraph: subgraphName,
2021
2371
  blockHeight: height,
2022
2372
  error: errorMsg
2023
2373
  });
2374
+ if (status === "reindexing") {
2375
+ await haltRange(`block ${height} failed persistently: ${errorMsg}`, height);
2376
+ }
2024
2377
  batchFailedBlocks.push({ height, reason: "processing_error" });
2025
- await updateSubgraphStatus2(targetDb, subgraphName, status, height).catch(() => {});
2026
2378
  blocksProcessed++;
2027
2379
  totalErrors++;
2028
2380
  pendingErrors++;
@@ -2048,6 +2400,9 @@ async function processBlockRange(def, opts) {
2048
2400
  const shouldFlushProgress = blocksProcessed % 100 === 0 || now - lastProgressFlushAt >= PROGRESS_FLUSH_INTERVAL_MS;
2049
2401
  if (shouldFlushProgress) {
2050
2402
  await updateSubgraphStatus2(targetDb, subgraphName, status, height);
2403
+ if (opts.operationId) {
2404
+ await updateOperationProcessedEvents(targetDb, opts.operationId, totalEventsProcessed).catch(() => {});
2405
+ }
2051
2406
  lastProgressFlushAt = now;
2052
2407
  }
2053
2408
  if (blocksProcessed % LOG_INTERVAL === 0) {
@@ -2165,6 +2520,7 @@ async function reindexSubgraph(def, opts) {
2165
2520
  isCatchup: false,
2166
2521
  apiKeyId: null,
2167
2522
  subgraphId: subgraphRow?.id,
2523
+ operationId: opts?.operationId,
2168
2524
  signal: opts?.signal
2169
2525
  });
2170
2526
  if (result.aborted) {
@@ -2240,6 +2596,7 @@ async function resumeReindex(def, opts) {
2240
2596
  isCatchup: false,
2241
2597
  apiKeyId: null,
2242
2598
  subgraphId: row.id,
2599
+ operationId: opts.operationId,
2243
2600
  signal: opts.signal
2244
2601
  });
2245
2602
  if (result.aborted) {
@@ -2288,6 +2645,7 @@ async function backfillSubgraph(def, opts) {
2288
2645
  isCatchup: false,
2289
2646
  apiKeyId: null,
2290
2647
  subgraphId: subgraphRow?.id,
2648
+ operationId: opts.operationId,
2291
2649
  signal: opts.signal
2292
2650
  });
2293
2651
  if (result.aborted) {
@@ -2319,9 +2677,6 @@ async function backfillSubgraph(def, opts) {
2319
2677
 
2320
2678
  // src/runtime/catchup.ts
2321
2679
  import { getTargetDb as getTargetDb3 } from "@secondlayer/shared/db";
2322
- import {
2323
- recordGapBatch as recordGapBatch2
2324
- } from "@secondlayer/shared/db/queries/subgraph-gaps";
2325
2680
  import { getSubgraph } from "@secondlayer/shared/db/queries/subgraphs";
2326
2681
  import { logger as logger7 } from "@secondlayer/shared/logger";
2327
2682
  var LOG_INTERVAL2 = 1000;
@@ -2360,28 +2715,6 @@ function resolveCatchupBatchConfig(env = process.env) {
2360
2715
  prefetch: parseBoolean(env.SUBGRAPH_CATCHUP_PREFETCH) ?? base.prefetch
2361
2716
  };
2362
2717
  }
2363
- function coalesceGaps(blocks) {
2364
- if (blocks.length === 0)
2365
- return [];
2366
- blocks.sort((a, b) => a.height - b.height);
2367
- const ranges = [];
2368
- let start = blocks[0].height;
2369
- let end = blocks[0].height;
2370
- let reason = blocks[0].reason;
2371
- for (let i = 1;i < blocks.length; i++) {
2372
- const b = blocks[i];
2373
- if (b.height === end + 1 && b.reason === reason) {
2374
- end = b.height;
2375
- } else {
2376
- ranges.push({ start, end, reason });
2377
- start = b.height;
2378
- end = b.height;
2379
- reason = b.reason;
2380
- }
2381
- }
2382
- ranges.push({ start, end, reason });
2383
- return ranges;
2384
- }
2385
2718
  function adjustBatchSize(current, avgEvents, config) {
2386
2719
  if (avgEvents > 50)
2387
2720
  return Math.max(Math.round(current * 0.5), config.minBatchSize);
@@ -2444,30 +2777,37 @@ async function catchUpSubgraph(subgraph, subgraphName) {
2444
2777
  batchEnd = Math.min(currentHeight + batchSize - 1, chainTip);
2445
2778
  batch = await source.loadBlockRange(currentHeight, batchEnd);
2446
2779
  }
2447
- const batchFailedBlocks = [];
2780
+ let stopCatchup = false;
2448
2781
  for (let height = currentHeight;height <= batchEnd; height++) {
2449
- const blockData = batch.get(height);
2782
+ let blockData = batch.get(height);
2450
2783
  if (!blockData) {
2451
- batchFailedBlocks.push({ height, reason: "block_missing" });
2452
- processed++;
2453
- continue;
2784
+ blockData = (await source.loadBlockRange(height, height)).get(height);
2785
+ }
2786
+ if (!blockData) {
2787
+ logger7.warn("Block missing during catch-up, deferring to next tick", {
2788
+ subgraph: subgraphName,
2789
+ blockHeight: height
2790
+ });
2791
+ stopCatchup = true;
2792
+ break;
2454
2793
  }
2455
2794
  let result;
2456
2795
  try {
2457
- result = await processBlock(subgraph, subgraphName, height, {
2796
+ result = await processBlockWithRetry(subgraph, subgraphName, height, {
2458
2797
  preloaded: blockData
2459
2798
  });
2460
2799
  } catch (err) {
2461
- logger7.error("Block processing error during catch-up", {
2800
+ const errorMsg = err instanceof Error ? err.message : String(err);
2801
+ logger7.error("Block processing failed persistently during catch-up", {
2462
2802
  subgraph: subgraphName,
2463
2803
  blockHeight: height,
2464
- error: err instanceof Error ? err.message : String(err)
2804
+ error: errorMsg
2465
2805
  });
2466
- batchFailedBlocks.push({ height, reason: "processing_error" });
2467
- const { updateSubgraphStatus: updateSubgraphStatus3 } = await import("@secondlayer/shared/db/queries/subgraphs");
2468
- await updateSubgraphStatus3(targetDb, subgraphName, "active", height).catch(() => {});
2469
- processed++;
2470
- continue;
2806
+ const { updateSubgraphStatus: updateSubgraphStatus3, recordSubgraphProcessed: recordSubgraphProcessed3 } = await import("@secondlayer/shared/db/queries/subgraphs");
2807
+ await recordSubgraphProcessed3(targetDb, subgraphName, 0, 1, `catch-up halted at block ${height}: ${errorMsg}`).catch(() => {});
2808
+ await updateSubgraphStatus3(targetDb, subgraphName, "error").catch(() => {});
2809
+ stopCatchup = true;
2810
+ break;
2471
2811
  }
2472
2812
  processed++;
2473
2813
  if (result.timing) {
@@ -2486,15 +2826,8 @@ async function catchUpSubgraph(subgraph, subgraphName) {
2486
2826
  });
2487
2827
  }
2488
2828
  }
2489
- if (batchFailedBlocks.length > 0) {
2490
- const gaps = coalesceGaps(batchFailedBlocks);
2491
- await recordGapBatch2(targetDb, subgraphRow.id, subgraphName, gaps).catch((err) => {
2492
- logger7.warn("Failed to record subgraph gaps", {
2493
- subgraph: subgraphName,
2494
- error: err instanceof Error ? err.message : String(err)
2495
- });
2496
- });
2497
- }
2829
+ if (stopCatchup)
2830
+ break;
2498
2831
  const avg = avgEventsPerBlock(batch);
2499
2832
  batchSize = adjustBatchSize(batchSize, avg, batchConfig);
2500
2833
  currentHeight = batchEnd + 1;
@@ -2541,8 +2874,29 @@ async function handleSubgraphReorg(blockHeight, loadSubgraphDef) {
2541
2874
  if (rows.length > 0)
2542
2875
  revertedByTable[tableName] = rows;
2543
2876
  }
2544
- for (const tableName of tableNames) {
2545
- await client.unsafe(`DELETE FROM "${schemaName}"."${tableName}" WHERE "_block_height" >= $1`, [blockHeight]);
2877
+ const hasJournal = (await client.unsafe(`SELECT to_regclass('"${schemaName}"."_journal"') AS r`))[0]?.r;
2878
+ if (hasJournal) {
2879
+ await client.begin(async (tx) => {
2880
+ for (const tableName of tableNames) {
2881
+ const earliest = `
2882
+ SELECT DISTINCT ON (row_key) row_key, prev_row
2883
+ FROM "${schemaName}"."_journal"
2884
+ WHERE block_height >= $1 AND table_name = $2
2885
+ ORDER BY row_key, _jid ASC`;
2886
+ await tx.unsafe(`DELETE FROM "${schemaName}"."${tableName}" t USING (${earliest}) e WHERE to_jsonb(t.*) @> e.row_key`, [blockHeight, tableName]);
2887
+ await tx.unsafe(`DELETE FROM "${schemaName}"."${tableName}" WHERE "_block_height" >= $1`, [blockHeight]);
2888
+ await tx.unsafe(`INSERT INTO "${schemaName}"."${tableName}"
2889
+ SELECT r.* FROM (${earliest}) e
2890
+ CROSS JOIN LATERAL jsonb_populate_record(NULL::"${schemaName}"."${tableName}", e.prev_row) r
2891
+ WHERE e.prev_row IS NOT NULL`, [blockHeight, tableName]);
2892
+ }
2893
+ await tx.unsafe(`DELETE FROM "${schemaName}"."_journal" WHERE block_height >= $1`, [blockHeight]);
2894
+ });
2895
+ } else {
2896
+ logger8.warn("Subgraph has no revert journal — falling back to height delete (accumulator rows may lose history)", { subgraph: sg.name, blockHeight });
2897
+ for (const tableName of tableNames) {
2898
+ await client.unsafe(`DELETE FROM "${schemaName}"."${tableName}" WHERE "_block_height" >= $1`, [blockHeight]);
2899
+ }
2546
2900
  }
2547
2901
  for (const [tableName, rows] of Object.entries(revertedByTable)) {
2548
2902
  if (rows.length === 0)
@@ -2830,6 +3184,7 @@ async function runSubgraphOperation(operation, signal) {
2830
3184
  fromBlock: Number(operation.from_block),
2831
3185
  toBlock: Number(operation.to_block),
2832
3186
  schemaName,
3187
+ operationId: operation.id,
2833
3188
  signal
2834
3189
  });
2835
3190
  return result2.processed;
@@ -2839,13 +3194,18 @@ async function runSubgraphOperation(operation, signal) {
2839
3194
  }
2840
3195
  const hasResumeMetadata = subgraph.status === "reindexing" && subgraph.reindex_from_block != null && subgraph.reindex_to_block != null;
2841
3196
  if (hasResumeMetadata) {
2842
- const result2 = await resumeReindex(def, { schemaName, signal });
3197
+ const result2 = await resumeReindex(def, {
3198
+ schemaName,
3199
+ operationId: operation.id,
3200
+ signal
3201
+ });
2843
3202
  return result2.processed;
2844
3203
  }
2845
3204
  const result = await reindexSubgraph(def, {
2846
3205
  fromBlock: operation.from_block == null ? undefined : Number(operation.from_block),
2847
3206
  toBlock: operation.to_block == null ? undefined : Number(operation.to_block),
2848
3207
  schemaName,
3208
+ operationId: operation.id,
2849
3209
  signal
2850
3210
  });
2851
3211
  return result.processed;
@@ -3055,5 +3415,5 @@ export {
3055
3415
  startSubgraphOperationRunner
3056
3416
  };
3057
3417
 
3058
- //# debugId=B5C517086F325E0564756E2164756E21
3418
+ //# debugId=9E7A5CE05C0F87F064756E2164756E21
3059
3419
  //# sourceMappingURL=processor.js.map