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