@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
@@ -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
  }
@@ -1743,8 +2170,29 @@ async function handleSubgraphReorg(blockHeight, loadSubgraphDef) {
1743
2170
  if (rows.length > 0)
1744
2171
  revertedByTable[tableName] = rows;
1745
2172
  }
1746
- for (const tableName of tableNames) {
1747
- await client.unsafe(`DELETE FROM "${schemaName}"."${tableName}" WHERE "_block_height" >= $1`, [blockHeight]);
2173
+ const hasJournal = (await client.unsafe(`SELECT to_regclass('"${schemaName}"."_journal"') AS r`))[0]?.r;
2174
+ if (hasJournal) {
2175
+ await client.begin(async (tx) => {
2176
+ for (const tableName of tableNames) {
2177
+ const earliest = `
2178
+ SELECT DISTINCT ON (row_key) row_key, prev_row
2179
+ FROM "${schemaName}"."_journal"
2180
+ WHERE block_height >= $1 AND table_name = $2
2181
+ ORDER BY row_key, _jid ASC`;
2182
+ await tx.unsafe(`DELETE FROM "${schemaName}"."${tableName}" t USING (${earliest}) e WHERE to_jsonb(t.*) @> e.row_key`, [blockHeight, tableName]);
2183
+ await tx.unsafe(`DELETE FROM "${schemaName}"."${tableName}" WHERE "_block_height" >= $1`, [blockHeight]);
2184
+ await tx.unsafe(`INSERT INTO "${schemaName}"."${tableName}"
2185
+ SELECT r.* FROM (${earliest}) e
2186
+ CROSS JOIN LATERAL jsonb_populate_record(NULL::"${schemaName}"."${tableName}", e.prev_row) r
2187
+ WHERE e.prev_row IS NOT NULL`, [blockHeight, tableName]);
2188
+ }
2189
+ await tx.unsafe(`DELETE FROM "${schemaName}"."_journal" WHERE block_height >= $1`, [blockHeight]);
2190
+ });
2191
+ } else {
2192
+ logger6.warn("Subgraph has no revert journal — falling back to height delete (accumulator rows may lose history)", { subgraph: sg.name, blockHeight });
2193
+ for (const tableName of tableNames) {
2194
+ await client.unsafe(`DELETE FROM "${schemaName}"."${tableName}" WHERE "_block_height" >= $1`, [blockHeight]);
2195
+ }
1748
2196
  }
1749
2197
  for (const [tableName, rows] of Object.entries(revertedByTable)) {
1750
2198
  if (rows.length === 0)
@@ -1805,5 +2253,5 @@ export {
1805
2253
  handleSubgraphReorg
1806
2254
  };
1807
2255
 
1808
- //# debugId=379C74DD237845D364756E2164756E21
2256
+ //# debugId=9E3057C106B5756964756E2164756E21
1809
2257
  //# sourceMappingURL=reorg.js.map