@secondlayer/subgraphs 3.12.0 → 3.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/index.d.ts +54 -1
- package/dist/src/index.js +952 -199
- package/dist/src/index.js.map +12 -11
- package/dist/src/runtime/block-processor.d.ts +36 -1
- package/dist/src/runtime/block-processor.js +568 -86
- package/dist/src/runtime/block-processor.js.map +9 -8
- package/dist/src/runtime/catchup.d.ts +7 -0
- package/dist/src/runtime/catchup.js +587 -132
- package/dist/src/runtime/catchup.js.map +10 -9
- package/dist/src/runtime/context.d.ts +65 -3
- package/dist/src/runtime/context.js +390 -8
- package/dist/src/runtime/context.js.map +6 -4
- package/dist/src/runtime/processor.js +665 -248
- package/dist/src/runtime/processor.js.map +13 -13
- package/dist/src/runtime/reindex.d.ts +7 -0
- package/dist/src/runtime/reindex.js +618 -198
- package/dist/src/runtime/reindex.js.map +10 -10
- package/dist/src/runtime/reorg.d.ts +7 -0
- package/dist/src/runtime/reorg.js +588 -87
- package/dist/src/runtime/reorg.js.map +10 -9
- package/dist/src/runtime/replay.js.map +2 -2
- package/dist/src/runtime/runner.d.ts +70 -2
- package/dist/src/runtime/runner.js +56 -58
- package/dist/src/runtime/runner.js.map +3 -3
- package/dist/src/runtime/source-matcher.d.ts +2 -0
- package/dist/src/runtime/source-matcher.js.map +2 -2
- package/dist/src/schema/index.d.ts +7 -0
- package/dist/src/schema/index.js +20 -2
- package/dist/src/schema/index.js.map +4 -4
- package/dist/src/service.js +665 -248
- package/dist/src/service.js.map +13 -13
- package/dist/src/types.d.ts +7 -0
- package/dist/src/validate.d.ts +7 -0
- package/dist/src/validate.js +3 -2
- package/dist/src/validate.js.map +3 -3
- 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
|
-
|
|
150
|
+
journal;
|
|
151
|
+
constructor(db, pgSchemaName2, subgraphSchema, block, tx, byo = false, journal = false) {
|
|
23
152
|
this.db = db;
|
|
24
|
-
this.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
|
-
|
|
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
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
-
|
|
676
|
-
|
|
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 };
|
|
@@ -953,6 +1332,7 @@ function matchSources(sources, transactions, events, traitContracts = new Map) {
|
|
|
953
1332
|
// src/runtime/block-processor.ts
|
|
954
1333
|
import { getTargetDb } from "@secondlayer/shared/db";
|
|
955
1334
|
import { resolveTraitContractIds } from "@secondlayer/shared/db/queries/contracts";
|
|
1335
|
+
import { advanceOperationCursor } from "@secondlayer/shared/db/queries/subgraph-operations";
|
|
956
1336
|
import {
|
|
957
1337
|
isByoSubgraph,
|
|
958
1338
|
recordSubgraphProcessed,
|
|
@@ -962,9 +1342,6 @@ import {
|
|
|
962
1342
|
import { logger as logger5 } from "@secondlayer/shared/logger";
|
|
963
1343
|
import { sql as sql3 } from "kysely";
|
|
964
1344
|
|
|
965
|
-
// src/schema/utils.ts
|
|
966
|
-
import { pgSchemaName } from "@secondlayer/shared/db/queries/subgraphs";
|
|
967
|
-
|
|
968
1345
|
// src/runtime/block-source.ts
|
|
969
1346
|
import { getSourceDb } from "@secondlayer/shared/db";
|
|
970
1347
|
import { IndexHttpClient } from "@secondlayer/shared/index-http";
|
|
@@ -1310,7 +1687,7 @@ function resolveBlockSource(subgraph) {
|
|
|
1310
1687
|
}
|
|
1311
1688
|
|
|
1312
1689
|
// src/runtime/outbox-emit.ts
|
|
1313
|
-
import { createHash } from "node:crypto";
|
|
1690
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1314
1691
|
import { logger as logger4 } from "@secondlayer/shared/logger";
|
|
1315
1692
|
var loggedKillSwitch = false;
|
|
1316
1693
|
var OP_VERB = {
|
|
@@ -1323,7 +1700,7 @@ function isEmitOutboxEnabled() {
|
|
|
1323
1700
|
}
|
|
1324
1701
|
function dedupKey(subgraphName, tableName, blockHeight, txId, rowIndex, row) {
|
|
1325
1702
|
const canonical = `${subgraphName}:${tableName}:${blockHeight}:${txId}:${rowIndex}:${stableStringify(row)}`;
|
|
1326
|
-
return
|
|
1703
|
+
return createHash2("sha256").update(canonical).digest("hex").slice(0, 32);
|
|
1327
1704
|
}
|
|
1328
1705
|
function stableStringify(obj) {
|
|
1329
1706
|
const keys = Object.keys(obj).sort();
|
|
@@ -1575,6 +1952,47 @@ async function resolveTraitContracts(subgraph, blockHeight, db) {
|
|
|
1575
1952
|
}
|
|
1576
1953
|
return resolved;
|
|
1577
1954
|
}
|
|
1955
|
+
|
|
1956
|
+
class CursorRaceLostError extends Error {
|
|
1957
|
+
constructor(operationId, height) {
|
|
1958
|
+
super(`op ${operationId} lost cursor race at block ${height}`);
|
|
1959
|
+
this.name = "CursorRaceLostError";
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
function opCursorMode(opts) {
|
|
1963
|
+
const ap = opts?.atomicProgress;
|
|
1964
|
+
return ap && "operationId" in ap ? ap : undefined;
|
|
1965
|
+
}
|
|
1966
|
+
function statusMode(opts) {
|
|
1967
|
+
const ap = opts?.atomicProgress;
|
|
1968
|
+
return ap && "status" in ap ? ap : undefined;
|
|
1969
|
+
}
|
|
1970
|
+
var BLOCK_RETRY_DELAYS_MS = [500, 2000, 5000];
|
|
1971
|
+
function journalEnabled(opts) {
|
|
1972
|
+
return !opts?.skipProgressUpdate;
|
|
1973
|
+
}
|
|
1974
|
+
async function processBlockWithRetry(subgraph, subgraphName, blockHeight, opts, retryDelaysMs = BLOCK_RETRY_DELAYS_MS) {
|
|
1975
|
+
let lastError;
|
|
1976
|
+
for (let attempt = 0;attempt <= retryDelaysMs.length; attempt++) {
|
|
1977
|
+
try {
|
|
1978
|
+
return await processBlock(subgraph, subgraphName, blockHeight, opts);
|
|
1979
|
+
} catch (err) {
|
|
1980
|
+
lastError = err;
|
|
1981
|
+
const delay = retryDelaysMs[attempt];
|
|
1982
|
+
if (delay === undefined)
|
|
1983
|
+
break;
|
|
1984
|
+
logger5.warn("Block processing failed, retrying", {
|
|
1985
|
+
subgraph: subgraphName,
|
|
1986
|
+
blockHeight,
|
|
1987
|
+
attempt: attempt + 1,
|
|
1988
|
+
retryInMs: delay,
|
|
1989
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1990
|
+
});
|
|
1991
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
throw lastError;
|
|
1995
|
+
}
|
|
1578
1996
|
async function processBlock(subgraph, subgraphName, blockHeight, opts) {
|
|
1579
1997
|
const targetDb = getTargetDb();
|
|
1580
1998
|
const blockStart = performance.now();
|
|
@@ -1642,10 +2060,24 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
|
|
|
1642
2060
|
}
|
|
1643
2061
|
};
|
|
1644
2062
|
if (route.byo) {
|
|
2063
|
+
if (statusMode(opts)) {
|
|
2064
|
+
const row = await targetDb.selectFrom("subgraphs").select("last_processed_block").where("name", "=", subgraphName).executeTakeFirst();
|
|
2065
|
+
if (row && Number(row.last_processed_block) >= blockHeight) {
|
|
2066
|
+
result.skipped = true;
|
|
2067
|
+
return result;
|
|
2068
|
+
}
|
|
2069
|
+
} else if (opCursorMode(opts)) {
|
|
2070
|
+
const om = opCursorMode(opts);
|
|
2071
|
+
const row = await targetDb.selectFrom("subgraph_operations").select("cursor_block").where("id", "=", om.operationId).executeTakeFirst();
|
|
2072
|
+
if (row?.cursor_block != null && Number(row.cursor_block) >= blockHeight) {
|
|
2073
|
+
result.skipped = true;
|
|
2074
|
+
return result;
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
1645
2077
|
let runResult = { processed: 0, errors: 0 };
|
|
1646
2078
|
let manifest;
|
|
1647
2079
|
await route.dataDb.transaction().execute(async (tx) => {
|
|
1648
|
-
const ctx = new SubgraphContext(tx, schemaName, subgraph.schema, blockMeta, initialTx, true);
|
|
2080
|
+
const ctx = new SubgraphContext(tx, schemaName, subgraph.schema, blockMeta, initialTx, true, journalEnabled(opts));
|
|
1649
2081
|
const handlerStart = performance.now();
|
|
1650
2082
|
runResult = await runHandlers(subgraph, matched, ctx);
|
|
1651
2083
|
handlerMs = performance.now() - handlerStart;
|
|
@@ -1661,26 +2093,71 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
|
|
|
1661
2093
|
if (manifest && manifest.count > 0) {
|
|
1662
2094
|
await emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, block.height);
|
|
1663
2095
|
}
|
|
2096
|
+
const byoSm = statusMode(opts);
|
|
2097
|
+
const byoOm = opCursorMode(opts);
|
|
2098
|
+
if (byoSm && manifest && manifest.count > 0) {
|
|
2099
|
+
await updateSubgraphStatus(tx, subgraphName, byoSm.status, blockHeight);
|
|
2100
|
+
} else if (byoOm && manifest && manifest.count > 0) {
|
|
2101
|
+
await advanceOperationCursor(tx, byoOm.operationId, blockHeight);
|
|
2102
|
+
}
|
|
1664
2103
|
await applyProgress(tx, runResult);
|
|
1665
2104
|
});
|
|
1666
2105
|
} else {
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
2106
|
+
try {
|
|
2107
|
+
await targetDb.transaction().execute(async (tx) => {
|
|
2108
|
+
const opMode = opCursorMode(opts);
|
|
2109
|
+
if (statusMode(opts)) {
|
|
2110
|
+
const row = await tx.selectFrom("subgraphs").select("last_processed_block").where("name", "=", subgraphName).executeTakeFirst();
|
|
2111
|
+
if (row && Number(row.last_processed_block) >= blockHeight) {
|
|
2112
|
+
result.skipped = true;
|
|
2113
|
+
return;
|
|
2114
|
+
}
|
|
2115
|
+
} else if (opMode) {
|
|
2116
|
+
const row = await tx.selectFrom("subgraph_operations").select("cursor_block").where("id", "=", opMode.operationId).executeTakeFirst();
|
|
2117
|
+
if (row?.cursor_block != null && Number(row.cursor_block) >= blockHeight) {
|
|
2118
|
+
result.skipped = true;
|
|
2119
|
+
return;
|
|
2120
|
+
}
|
|
1679
2121
|
}
|
|
1680
|
-
|
|
2122
|
+
const ctx = new SubgraphContext(tx, schemaName, subgraph.schema, blockMeta, initialTx, false, journalEnabled(opts));
|
|
2123
|
+
const handlerStart = performance.now();
|
|
2124
|
+
const runResult = await runHandlers(subgraph, matched, ctx);
|
|
2125
|
+
handlerMs = performance.now() - handlerStart;
|
|
2126
|
+
result.processed = runResult.processed;
|
|
2127
|
+
result.errors = runResult.errors;
|
|
2128
|
+
let flushedWrites = false;
|
|
2129
|
+
if (ctx.pendingOps > 0) {
|
|
2130
|
+
const flushStart = performance.now();
|
|
2131
|
+
const manifest = await ctx.flush();
|
|
2132
|
+
flushedWrites = manifest.count > 0;
|
|
2133
|
+
if (manifest.count > 0) {
|
|
2134
|
+
await emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, block.height);
|
|
2135
|
+
}
|
|
2136
|
+
flushMs = performance.now() - flushStart;
|
|
2137
|
+
}
|
|
2138
|
+
const sm = statusMode(opts);
|
|
2139
|
+
if (sm && flushedWrites) {
|
|
2140
|
+
await updateSubgraphStatus(tx, subgraphName, sm.status, blockHeight);
|
|
2141
|
+
} else if (opMode && flushedWrites) {
|
|
2142
|
+
const advanced = await advanceOperationCursor(tx, opMode.operationId, blockHeight);
|
|
2143
|
+
if (!advanced) {
|
|
2144
|
+
throw new CursorRaceLostError(opMode.operationId, blockHeight);
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
await applyProgress(tx, runResult);
|
|
2148
|
+
});
|
|
2149
|
+
} catch (err) {
|
|
2150
|
+
if (err instanceof CursorRaceLostError) {
|
|
2151
|
+
logger5.warn("cursor race lost — block already covered", {
|
|
2152
|
+
subgraph: subgraphName,
|
|
2153
|
+
blockHeight,
|
|
2154
|
+
error: err.message
|
|
2155
|
+
});
|
|
2156
|
+
result.skipped = true;
|
|
2157
|
+
return result;
|
|
1681
2158
|
}
|
|
1682
|
-
|
|
1683
|
-
}
|
|
2159
|
+
throw err;
|
|
2160
|
+
}
|
|
1684
2161
|
}
|
|
1685
2162
|
const totalMs = performance.now() - blockStart;
|
|
1686
2163
|
result.timing = {
|
|
@@ -1708,6 +2185,9 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
|
|
|
1708
2185
|
error: err instanceof Error ? err.message : String(err)
|
|
1709
2186
|
});
|
|
1710
2187
|
}
|
|
2188
|
+
if (journalEnabled(opts)) {
|
|
2189
|
+
await sql3.raw(`DELETE FROM "${schemaName}"."_journal" WHERE "block_height" < ${blockHeight - JOURNAL_RETENTION_BLOCKS}`).execute(route.dataDb).catch(() => {});
|
|
2190
|
+
}
|
|
1711
2191
|
}
|
|
1712
2192
|
return result;
|
|
1713
2193
|
}
|
|
@@ -1782,119 +2262,19 @@ import {
|
|
|
1782
2262
|
recordGapBatch,
|
|
1783
2263
|
resolveGaps
|
|
1784
2264
|
} from "@secondlayer/shared/db/queries/subgraph-gaps";
|
|
1785
|
-
import {
|
|
2265
|
+
import {
|
|
2266
|
+
advanceOperationCursor as advanceOperationCursor2,
|
|
2267
|
+
updateOperationProcessedEvents
|
|
2268
|
+
} from "@secondlayer/shared/db/queries/subgraph-operations";
|
|
1786
2269
|
import {
|
|
1787
2270
|
recordSubgraphProcessed as recordSubgraphProcessed2,
|
|
1788
2271
|
updateSubgraphStatus as updateSubgraphStatus2
|
|
1789
2272
|
} from "@secondlayer/shared/db/queries/subgraphs";
|
|
1790
2273
|
import { logger as logger6 } from "@secondlayer/shared/logger";
|
|
1791
|
-
|
|
1792
|
-
// src/schema/generator.ts
|
|
1793
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
1794
|
-
var TYPE_MAP = {
|
|
1795
|
-
text: "TEXT",
|
|
1796
|
-
uint: "NUMERIC",
|
|
1797
|
-
int: "NUMERIC",
|
|
1798
|
-
principal: "TEXT",
|
|
1799
|
-
boolean: "BOOLEAN",
|
|
1800
|
-
timestamp: "TIMESTAMPTZ",
|
|
1801
|
-
jsonb: "JSONB"
|
|
1802
|
-
};
|
|
1803
|
-
function escapeLiteralDefault(value) {
|
|
1804
|
-
if (value === null || value === undefined)
|
|
1805
|
-
return "NULL";
|
|
1806
|
-
if (typeof value === "number" || typeof value === "bigint")
|
|
1807
|
-
return String(value);
|
|
1808
|
-
if (typeof value === "boolean")
|
|
1809
|
-
return value ? "TRUE" : "FALSE";
|
|
1810
|
-
return `'${String(value).replace(/'/g, "''")}'`;
|
|
1811
|
-
}
|
|
1812
|
-
function tableNeedsTrgm(tableDef) {
|
|
1813
|
-
return Object.values(tableDef.columns).some((col) => col.search);
|
|
1814
|
-
}
|
|
1815
|
-
function emitTableDDL(schemaName, tableName, tableDef) {
|
|
1816
|
-
const qualifiedName = `${schemaName}.${tableName}`;
|
|
1817
|
-
const statements = [];
|
|
1818
|
-
const columnDefs = [
|
|
1819
|
-
"_id BIGSERIAL PRIMARY KEY",
|
|
1820
|
-
"_block_height BIGINT NOT NULL",
|
|
1821
|
-
"_tx_id TEXT NOT NULL",
|
|
1822
|
-
"_created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()"
|
|
1823
|
-
];
|
|
1824
|
-
for (const [colName, col] of Object.entries(tableDef.columns)) {
|
|
1825
|
-
const sqlType = TYPE_MAP[col.type];
|
|
1826
|
-
const nullable = col.nullable ? "" : " NOT NULL";
|
|
1827
|
-
let colDef = `${colName} ${sqlType}${nullable}`;
|
|
1828
|
-
if (col.default !== undefined) {
|
|
1829
|
-
colDef += ` DEFAULT ${escapeLiteralDefault(col.default)}`;
|
|
1830
|
-
}
|
|
1831
|
-
columnDefs.push(colDef);
|
|
1832
|
-
}
|
|
1833
|
-
statements.push(`CREATE TABLE IF NOT EXISTS ${qualifiedName} (
|
|
1834
|
-
${columnDefs.join(`,
|
|
1835
|
-
`)}
|
|
1836
|
-
)`);
|
|
1837
|
-
statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_block_height ON ${qualifiedName} (_block_height)`);
|
|
1838
|
-
statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_tx_id ON ${qualifiedName} (_tx_id)`);
|
|
1839
|
-
for (const [colName, col] of Object.entries(tableDef.columns)) {
|
|
1840
|
-
if (col.indexed) {
|
|
1841
|
-
statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName} ON ${qualifiedName} (${colName})`);
|
|
1842
|
-
}
|
|
1843
|
-
}
|
|
1844
|
-
for (const [colName, col] of Object.entries(tableDef.columns)) {
|
|
1845
|
-
if (col.search) {
|
|
1846
|
-
statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName}_trgm ON ${qualifiedName} USING gin (${colName} gin_trgm_ops)`);
|
|
1847
|
-
}
|
|
1848
|
-
}
|
|
1849
|
-
if (tableDef.indexes) {
|
|
1850
|
-
for (let i = 0;i < tableDef.indexes.length; i++) {
|
|
1851
|
-
const cols = tableDef.indexes[i];
|
|
1852
|
-
const idxName = `idx_${schemaName}_${tableName}_composite_${i}`;
|
|
1853
|
-
statements.push(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${qualifiedName} (${cols.join(", ")})`);
|
|
1854
|
-
}
|
|
1855
|
-
}
|
|
1856
|
-
if (tableDef.uniqueKeys) {
|
|
1857
|
-
for (let i = 0;i < tableDef.uniqueKeys.length; i++) {
|
|
1858
|
-
const cols = tableDef.uniqueKeys[i];
|
|
1859
|
-
const constraintName = `uq_${schemaName}_${tableName}_${cols.join("_")}`;
|
|
1860
|
-
statements.push(`ALTER TABLE ${qualifiedName} ADD CONSTRAINT ${constraintName} UNIQUE (${cols.join(", ")})`);
|
|
1861
|
-
}
|
|
1862
|
-
}
|
|
1863
|
-
return statements;
|
|
1864
|
-
}
|
|
1865
|
-
function emitForeignKeyDDL(schemaName, tableName, tableDef) {
|
|
1866
|
-
return (tableDef.relations ?? []).map((rel) => {
|
|
1867
|
-
const constraintName = `fk_${schemaName}_${tableName}_${rel.name}`;
|
|
1868
|
-
return `ALTER TABLE ${schemaName}.${tableName} ADD CONSTRAINT ${constraintName} ` + `FOREIGN KEY (${rel.fields.join(", ")}) ` + `REFERENCES ${schemaName}.${rel.references} (${rel.referencedColumns.join(", ")})`;
|
|
1869
|
-
});
|
|
1870
|
-
}
|
|
1871
|
-
function generateSubgraphSQL(def, schemaNameOverride) {
|
|
1872
|
-
const schemaName = schemaNameOverride ?? pgSchemaName(def.name);
|
|
1873
|
-
const statements = [];
|
|
1874
|
-
const needsTrgm = Object.values(def.schema).some((table) => Object.values(table.columns).some((col) => col.search));
|
|
1875
|
-
if (needsTrgm) {
|
|
1876
|
-
statements.push("CREATE EXTENSION IF NOT EXISTS pg_trgm");
|
|
1877
|
-
}
|
|
1878
|
-
statements.push(`CREATE SCHEMA IF NOT EXISTS ${schemaName}`);
|
|
1879
|
-
for (const [tableName, tableDef] of Object.entries(def.schema)) {
|
|
1880
|
-
statements.push(...emitTableDDL(schemaName, tableName, tableDef));
|
|
1881
|
-
}
|
|
1882
|
-
for (const [tableName, tableDef] of Object.entries(def.schema)) {
|
|
1883
|
-
statements.push(...emitForeignKeyDDL(schemaName, tableName, tableDef));
|
|
1884
|
-
}
|
|
1885
|
-
const hashInput = JSON.stringify({
|
|
1886
|
-
name: def.name,
|
|
1887
|
-
schema: def.schema,
|
|
1888
|
-
sources: def.sources
|
|
1889
|
-
}, (_key, value) => typeof value === "bigint" ? value.toString() : value);
|
|
1890
|
-
const hash = createHash2("sha256").update(hashInput).digest("hex");
|
|
1891
|
-
return { statements, hash };
|
|
1892
|
-
}
|
|
1893
|
-
|
|
1894
|
-
// src/runtime/reindex.ts
|
|
1895
2274
|
var LOG_INTERVAL = 1000;
|
|
1896
2275
|
var HEALTH_FLUSH_INTERVAL = 1000;
|
|
1897
2276
|
var PROGRESS_FLUSH_INTERVAL_MS = 5000;
|
|
2277
|
+
var EMPTY_BATCH_HALT_THRESHOLD = 3;
|
|
1898
2278
|
var STANDARD_REINDEX_BATCH_CONFIG = {
|
|
1899
2279
|
defaultBatchSize: 500,
|
|
1900
2280
|
minBatchSize: 100,
|
|
@@ -1958,6 +2338,7 @@ async function processBlockRange(def, opts) {
|
|
|
1958
2338
|
const totalBlocks = toBlock - fromBlock + 1;
|
|
1959
2339
|
const stats = new StatsAccumulator(subgraphName, opts.isCatchup);
|
|
1960
2340
|
let blocksProcessed = 0;
|
|
2341
|
+
let blocksSkippedByCursor = 0;
|
|
1961
2342
|
let totalEventsProcessed = 0;
|
|
1962
2343
|
let totalErrors = 0;
|
|
1963
2344
|
let pendingEventsProcessed = 0;
|
|
@@ -1970,6 +2351,7 @@ async function processBlockRange(def, opts) {
|
|
|
1970
2351
|
let batchSize = batchConfig.defaultBatchSize;
|
|
1971
2352
|
let currentHeight = fromBlock;
|
|
1972
2353
|
let aborted = false;
|
|
2354
|
+
let consecutiveEmptyBatches = 0;
|
|
1973
2355
|
const sparse = Boolean(source.nextDataHeight && canSparseScan(def));
|
|
1974
2356
|
const flushHealth = async () => {
|
|
1975
2357
|
if (pendingEventsProcessed === 0 && pendingErrors === 0)
|
|
@@ -1981,6 +2363,13 @@ async function processBlockRange(def, opts) {
|
|
|
1981
2363
|
lastHealthFlushBlock = blocksProcessed;
|
|
1982
2364
|
lastHealthFlushAt = Date.now();
|
|
1983
2365
|
};
|
|
2366
|
+
const haltRange = async (errorMsg, height) => {
|
|
2367
|
+
pendingErrors++;
|
|
2368
|
+
pendingLastError = errorMsg;
|
|
2369
|
+
await flushHealth().catch(() => {});
|
|
2370
|
+
await updateSubgraphStatus2(targetDb, subgraphName, "error").catch(() => {});
|
|
2371
|
+
throw new Error(`${subgraphName}: halted at block ${height}: ${errorMsg}`);
|
|
2372
|
+
};
|
|
1984
2373
|
let nextBatchEnd = Math.min(currentHeight + batchSize - 1, toBlock);
|
|
1985
2374
|
let nextBatchPromise = source.loadBlockRange(currentHeight, nextBatchEnd);
|
|
1986
2375
|
while (currentHeight <= toBlock) {
|
|
@@ -1995,6 +2384,14 @@ async function processBlockRange(def, opts) {
|
|
|
1995
2384
|
}
|
|
1996
2385
|
const batch = await nextBatchPromise;
|
|
1997
2386
|
const batchEnd = nextBatchEnd;
|
|
2387
|
+
if (batch.size === 0 && batchEnd >= currentHeight) {
|
|
2388
|
+
consecutiveEmptyBatches++;
|
|
2389
|
+
if (consecutiveEmptyBatches >= EMPTY_BATCH_HALT_THRESHOLD) {
|
|
2390
|
+
await haltRange(`block source returned ${consecutiveEmptyBatches} consecutive empty batches (ending ${currentHeight}..${batchEnd}) — source degraded`, currentHeight);
|
|
2391
|
+
}
|
|
2392
|
+
} else {
|
|
2393
|
+
consecutiveEmptyBatches = 0;
|
|
2394
|
+
}
|
|
1998
2395
|
const nextStart = batchEnd + 1;
|
|
1999
2396
|
if (nextStart <= toBlock) {
|
|
2000
2397
|
nextBatchEnd = Math.min(nextStart + batchSize - 1, toBlock);
|
|
@@ -2002,28 +2399,40 @@ async function processBlockRange(def, opts) {
|
|
|
2002
2399
|
}
|
|
2003
2400
|
const batchFailedBlocks = [];
|
|
2004
2401
|
let batchMatched = 0;
|
|
2402
|
+
const opCursor = status === "active" && opts.operationId ? { operationId: opts.operationId } : undefined;
|
|
2403
|
+
const atomicProgress = status === "reindexing" ? { status } : opCursor;
|
|
2005
2404
|
for (let height = currentHeight;height <= batchEnd; height++) {
|
|
2006
|
-
|
|
2405
|
+
let blockData = batch.get(height);
|
|
2007
2406
|
if (!blockData) {
|
|
2407
|
+
blockData = (await source.loadBlockRange(height, height)).get(height);
|
|
2408
|
+
}
|
|
2409
|
+
if (!blockData) {
|
|
2410
|
+
if (status === "reindexing") {
|
|
2411
|
+
const errorMsg = `block ${height} missing from source — halting reindex (cursor stays at ${height - 1})`;
|
|
2412
|
+
await haltRange(errorMsg, height);
|
|
2413
|
+
}
|
|
2008
2414
|
batchFailedBlocks.push({ height, reason: "block_missing" });
|
|
2009
2415
|
blocksProcessed++;
|
|
2010
2416
|
continue;
|
|
2011
2417
|
}
|
|
2012
2418
|
let result;
|
|
2013
2419
|
try {
|
|
2014
|
-
result = await
|
|
2420
|
+
result = await processBlockWithRetry(def, subgraphName, height, {
|
|
2015
2421
|
skipProgressUpdate: true,
|
|
2422
|
+
atomicProgress,
|
|
2016
2423
|
preloaded: blockData
|
|
2017
2424
|
});
|
|
2018
2425
|
} catch (err) {
|
|
2019
|
-
const errorMsg =
|
|
2020
|
-
logger6.error("Block processing
|
|
2426
|
+
const errorMsg = getErrorMessage2(err);
|
|
2427
|
+
logger6.error("Block processing failed persistently", {
|
|
2021
2428
|
subgraph: subgraphName,
|
|
2022
2429
|
blockHeight: height,
|
|
2023
2430
|
error: errorMsg
|
|
2024
2431
|
});
|
|
2432
|
+
if (status === "reindexing") {
|
|
2433
|
+
await haltRange(`block ${height} failed persistently: ${errorMsg}`, height);
|
|
2434
|
+
}
|
|
2025
2435
|
batchFailedBlocks.push({ height, reason: "processing_error" });
|
|
2026
|
-
await updateSubgraphStatus2(targetDb, subgraphName, status, height).catch(() => {});
|
|
2027
2436
|
blocksProcessed++;
|
|
2028
2437
|
totalErrors++;
|
|
2029
2438
|
pendingErrors++;
|
|
@@ -2031,6 +2440,8 @@ async function processBlockRange(def, opts) {
|
|
|
2031
2440
|
continue;
|
|
2032
2441
|
}
|
|
2033
2442
|
blocksProcessed++;
|
|
2443
|
+
if (result.skipped)
|
|
2444
|
+
blocksSkippedByCursor++;
|
|
2034
2445
|
batchMatched += result.matched;
|
|
2035
2446
|
totalEventsProcessed += result.processed;
|
|
2036
2447
|
totalErrors += result.errors;
|
|
@@ -2048,7 +2459,11 @@ async function processBlockRange(def, opts) {
|
|
|
2048
2459
|
const now = Date.now();
|
|
2049
2460
|
const shouldFlushProgress = blocksProcessed % 100 === 0 || now - lastProgressFlushAt >= PROGRESS_FLUSH_INTERVAL_MS;
|
|
2050
2461
|
if (shouldFlushProgress) {
|
|
2051
|
-
|
|
2462
|
+
if (opCursor) {
|
|
2463
|
+
await advanceOperationCursor2(targetDb, opCursor.operationId, height);
|
|
2464
|
+
} else {
|
|
2465
|
+
await updateSubgraphStatus2(targetDb, subgraphName, status, height);
|
|
2466
|
+
}
|
|
2052
2467
|
if (opts.operationId) {
|
|
2053
2468
|
await updateOperationProcessedEvents(targetDb, opts.operationId, totalEventsProcessed).catch(() => {});
|
|
2054
2469
|
}
|
|
@@ -2060,7 +2475,8 @@ async function processBlockRange(def, opts) {
|
|
|
2060
2475
|
processed: blocksProcessed,
|
|
2061
2476
|
total: totalBlocks,
|
|
2062
2477
|
currentBlock: height,
|
|
2063
|
-
pct: Math.round(blocksProcessed / totalBlocks * 100)
|
|
2478
|
+
pct: Math.round(blocksProcessed / totalBlocks * 100),
|
|
2479
|
+
...blocksSkippedByCursor > 0 ? { skippedByCursor: blocksSkippedByCursor } : {}
|
|
2064
2480
|
});
|
|
2065
2481
|
}
|
|
2066
2482
|
}
|
|
@@ -2086,7 +2502,11 @@ async function processBlockRange(def, opts) {
|
|
|
2086
2502
|
if (jumpTo > batchEnd + 1) {
|
|
2087
2503
|
const skipped = Math.min(jumpTo, toBlock + 1) - (batchEnd + 1);
|
|
2088
2504
|
blocksProcessed += skipped;
|
|
2089
|
-
|
|
2505
|
+
if (opCursor) {
|
|
2506
|
+
await advanceOperationCursor2(targetDb, opCursor.operationId, jumpTo - 1);
|
|
2507
|
+
} else {
|
|
2508
|
+
await updateSubgraphStatus2(targetDb, subgraphName, status, jumpTo - 1);
|
|
2509
|
+
}
|
|
2090
2510
|
logger6.info("Sparse skip", {
|
|
2091
2511
|
subgraph: subgraphName,
|
|
2092
2512
|
from: batchEnd + 1,
|
|
@@ -2326,9 +2746,6 @@ async function backfillSubgraph(def, opts) {
|
|
|
2326
2746
|
|
|
2327
2747
|
// src/runtime/catchup.ts
|
|
2328
2748
|
import { getTargetDb as getTargetDb3 } from "@secondlayer/shared/db";
|
|
2329
|
-
import {
|
|
2330
|
-
recordGapBatch as recordGapBatch2
|
|
2331
|
-
} from "@secondlayer/shared/db/queries/subgraph-gaps";
|
|
2332
2749
|
import { getSubgraph } from "@secondlayer/shared/db/queries/subgraphs";
|
|
2333
2750
|
import { logger as logger7 } from "@secondlayer/shared/logger";
|
|
2334
2751
|
var LOG_INTERVAL2 = 1000;
|
|
@@ -2367,28 +2784,6 @@ function resolveCatchupBatchConfig(env = process.env) {
|
|
|
2367
2784
|
prefetch: parseBoolean(env.SUBGRAPH_CATCHUP_PREFETCH) ?? base.prefetch
|
|
2368
2785
|
};
|
|
2369
2786
|
}
|
|
2370
|
-
function coalesceGaps(blocks) {
|
|
2371
|
-
if (blocks.length === 0)
|
|
2372
|
-
return [];
|
|
2373
|
-
blocks.sort((a, b) => a.height - b.height);
|
|
2374
|
-
const ranges = [];
|
|
2375
|
-
let start = blocks[0].height;
|
|
2376
|
-
let end = blocks[0].height;
|
|
2377
|
-
let reason = blocks[0].reason;
|
|
2378
|
-
for (let i = 1;i < blocks.length; i++) {
|
|
2379
|
-
const b = blocks[i];
|
|
2380
|
-
if (b.height === end + 1 && b.reason === reason) {
|
|
2381
|
-
end = b.height;
|
|
2382
|
-
} else {
|
|
2383
|
-
ranges.push({ start, end, reason });
|
|
2384
|
-
start = b.height;
|
|
2385
|
-
end = b.height;
|
|
2386
|
-
reason = b.reason;
|
|
2387
|
-
}
|
|
2388
|
-
}
|
|
2389
|
-
ranges.push({ start, end, reason });
|
|
2390
|
-
return ranges;
|
|
2391
|
-
}
|
|
2392
2787
|
function adjustBatchSize(current, avgEvents, config) {
|
|
2393
2788
|
if (avgEvents > 50)
|
|
2394
2789
|
return Math.max(Math.round(current * 0.5), config.minBatchSize);
|
|
@@ -2451,30 +2846,37 @@ async function catchUpSubgraph(subgraph, subgraphName) {
|
|
|
2451
2846
|
batchEnd = Math.min(currentHeight + batchSize - 1, chainTip);
|
|
2452
2847
|
batch = await source.loadBlockRange(currentHeight, batchEnd);
|
|
2453
2848
|
}
|
|
2454
|
-
|
|
2849
|
+
let stopCatchup = false;
|
|
2455
2850
|
for (let height = currentHeight;height <= batchEnd; height++) {
|
|
2456
|
-
|
|
2851
|
+
let blockData = batch.get(height);
|
|
2457
2852
|
if (!blockData) {
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2853
|
+
blockData = (await source.loadBlockRange(height, height)).get(height);
|
|
2854
|
+
}
|
|
2855
|
+
if (!blockData) {
|
|
2856
|
+
logger7.warn("Block missing during catch-up, deferring to next tick", {
|
|
2857
|
+
subgraph: subgraphName,
|
|
2858
|
+
blockHeight: height
|
|
2859
|
+
});
|
|
2860
|
+
stopCatchup = true;
|
|
2861
|
+
break;
|
|
2461
2862
|
}
|
|
2462
2863
|
let result;
|
|
2463
2864
|
try {
|
|
2464
|
-
result = await
|
|
2865
|
+
result = await processBlockWithRetry(subgraph, subgraphName, height, {
|
|
2465
2866
|
preloaded: blockData
|
|
2466
2867
|
});
|
|
2467
2868
|
} catch (err) {
|
|
2468
|
-
|
|
2869
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
2870
|
+
logger7.error("Block processing failed persistently during catch-up", {
|
|
2469
2871
|
subgraph: subgraphName,
|
|
2470
2872
|
blockHeight: height,
|
|
2471
|
-
error:
|
|
2873
|
+
error: errorMsg
|
|
2472
2874
|
});
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
await updateSubgraphStatus3(targetDb, subgraphName, "
|
|
2476
|
-
|
|
2477
|
-
|
|
2875
|
+
const { updateSubgraphStatus: updateSubgraphStatus3, recordSubgraphProcessed: recordSubgraphProcessed3 } = await import("@secondlayer/shared/db/queries/subgraphs");
|
|
2876
|
+
await recordSubgraphProcessed3(targetDb, subgraphName, 0, 1, `catch-up halted at block ${height}: ${errorMsg}`).catch(() => {});
|
|
2877
|
+
await updateSubgraphStatus3(targetDb, subgraphName, "error").catch(() => {});
|
|
2878
|
+
stopCatchup = true;
|
|
2879
|
+
break;
|
|
2478
2880
|
}
|
|
2479
2881
|
processed++;
|
|
2480
2882
|
if (result.timing) {
|
|
@@ -2493,15 +2895,8 @@ async function catchUpSubgraph(subgraph, subgraphName) {
|
|
|
2493
2895
|
});
|
|
2494
2896
|
}
|
|
2495
2897
|
}
|
|
2496
|
-
if (
|
|
2497
|
-
|
|
2498
|
-
await recordGapBatch2(targetDb, subgraphRow.id, subgraphName, gaps).catch((err) => {
|
|
2499
|
-
logger7.warn("Failed to record subgraph gaps", {
|
|
2500
|
-
subgraph: subgraphName,
|
|
2501
|
-
error: err instanceof Error ? err.message : String(err)
|
|
2502
|
-
});
|
|
2503
|
-
});
|
|
2504
|
-
}
|
|
2898
|
+
if (stopCatchup)
|
|
2899
|
+
break;
|
|
2505
2900
|
const avg = avgEventsPerBlock(batch);
|
|
2506
2901
|
batchSize = adjustBatchSize(batchSize, avg, batchConfig);
|
|
2507
2902
|
currentHeight = batchEnd + 1;
|
|
@@ -2548,8 +2943,29 @@ async function handleSubgraphReorg(blockHeight, loadSubgraphDef) {
|
|
|
2548
2943
|
if (rows.length > 0)
|
|
2549
2944
|
revertedByTable[tableName] = rows;
|
|
2550
2945
|
}
|
|
2551
|
-
|
|
2552
|
-
|
|
2946
|
+
const hasJournal = (await client.unsafe(`SELECT to_regclass('"${schemaName}"."_journal"') AS r`))[0]?.r;
|
|
2947
|
+
if (hasJournal) {
|
|
2948
|
+
await client.begin(async (tx) => {
|
|
2949
|
+
for (const tableName of tableNames) {
|
|
2950
|
+
const earliest = `
|
|
2951
|
+
SELECT DISTINCT ON (row_key) row_key, prev_row
|
|
2952
|
+
FROM "${schemaName}"."_journal"
|
|
2953
|
+
WHERE block_height >= $1 AND table_name = $2
|
|
2954
|
+
ORDER BY row_key, _jid ASC`;
|
|
2955
|
+
await tx.unsafe(`DELETE FROM "${schemaName}"."${tableName}" t USING (${earliest}) e WHERE to_jsonb(t.*) @> e.row_key`, [blockHeight, tableName]);
|
|
2956
|
+
await tx.unsafe(`DELETE FROM "${schemaName}"."${tableName}" WHERE "_block_height" >= $1`, [blockHeight]);
|
|
2957
|
+
await tx.unsafe(`INSERT INTO "${schemaName}"."${tableName}"
|
|
2958
|
+
SELECT r.* FROM (${earliest}) e
|
|
2959
|
+
CROSS JOIN LATERAL jsonb_populate_record(NULL::"${schemaName}"."${tableName}", e.prev_row) r
|
|
2960
|
+
WHERE e.prev_row IS NOT NULL`, [blockHeight, tableName]);
|
|
2961
|
+
}
|
|
2962
|
+
await tx.unsafe(`DELETE FROM "${schemaName}"."_journal" WHERE block_height >= $1`, [blockHeight]);
|
|
2963
|
+
});
|
|
2964
|
+
} else {
|
|
2965
|
+
logger8.warn("Subgraph has no revert journal — falling back to height delete (accumulator rows may lose history)", { subgraph: sg.name, blockHeight });
|
|
2966
|
+
for (const tableName of tableNames) {
|
|
2967
|
+
await client.unsafe(`DELETE FROM "${schemaName}"."${tableName}" WHERE "_block_height" >= $1`, [blockHeight]);
|
|
2968
|
+
}
|
|
2553
2969
|
}
|
|
2554
2970
|
for (const [tableName, rows] of Object.entries(revertedByTable)) {
|
|
2555
2971
|
if (rows.length === 0)
|
|
@@ -2833,8 +3249,9 @@ async function runSubgraphOperation(operation, signal) {
|
|
|
2833
3249
|
if (operation.from_block == null || operation.to_block == null) {
|
|
2834
3250
|
throw new Error("Backfill operation is missing from_block or to_block");
|
|
2835
3251
|
}
|
|
3252
|
+
const resumeFrom = operation.cursor_block != null ? Math.max(Number(operation.from_block), Number(operation.cursor_block) + 1) : Number(operation.from_block);
|
|
2836
3253
|
const result2 = await backfillSubgraph(def, {
|
|
2837
|
-
fromBlock:
|
|
3254
|
+
fromBlock: resumeFrom,
|
|
2838
3255
|
toBlock: Number(operation.to_block),
|
|
2839
3256
|
schemaName,
|
|
2840
3257
|
operationId: operation.id,
|
|
@@ -3068,5 +3485,5 @@ export {
|
|
|
3068
3485
|
startSubgraphOperationRunner
|
|
3069
3486
|
};
|
|
3070
3487
|
|
|
3071
|
-
//# debugId=
|
|
3488
|
+
//# debugId=D7F138F6BEB6BA7164756E2164756E21
|
|
3072
3489
|
//# sourceMappingURL=processor.js.map
|