@secondlayer/subgraphs 3.11.0 → 3.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/index.d.ts +36 -1
- package/dist/src/index.js +542 -180
- package/dist/src/index.js.map +12 -12
- package/dist/src/runtime/block-processor.d.ts +34 -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 +10 -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 +590 -230
- package/dist/src/runtime/processor.js.map +13 -13
- package/dist/src/runtime/reindex.d.ts +14 -0
- package/dist/src/runtime/reindex.js +538 -180
- package/dist/src/runtime/reindex.js.map +10 -10
- package/dist/src/runtime/reorg.d.ts +10 -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 +73 -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 +10 -0
- package/dist/src/schema/index.js +19 -1
- package/dist/src/schema/index.js.map +5 -5
- package/dist/src/service.js +590 -230
- package/dist/src/service.js.map +13 -13
- package/dist/src/types.d.ts +10 -0
- package/dist/src/validate.d.ts +10 -0
- package/dist/src/validate.js +2 -1
- package/dist/src/validate.js.map +3 -3
- package/package.json +2 -2
package/dist/src/index.js
CHANGED
|
@@ -69,6 +69,7 @@ var SubgraphDefinitionSchema = z.object({
|
|
|
69
69
|
version: z.string().optional(),
|
|
70
70
|
description: z.string().optional(),
|
|
71
71
|
startBlock: z.number().int().nonnegative().optional(),
|
|
72
|
+
backfillMode: z.enum(["blocking", "concurrent"]).optional(),
|
|
72
73
|
sources: z.record(z.string(), SubgraphFilterSchema).refine((s) => Object.keys(s).length > 0, "Must have at least one source"),
|
|
73
74
|
schema: SubgraphSchemaSchema,
|
|
74
75
|
handlers: z.record(z.string(), z.any())
|
|
@@ -81,6 +82,134 @@ function validateSubgraphDefinition(def) {
|
|
|
81
82
|
import { logger } from "@secondlayer/shared/logger";
|
|
82
83
|
import { formatUnits } from "@secondlayer/stacks/utils";
|
|
83
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;
|
|
84
213
|
function validateColumnName(name) {
|
|
85
214
|
if (!/^[a-z_][a-z0-9_]*$/i.test(name)) {
|
|
86
215
|
throw new Error(`Invalid column name: ${name}`);
|
|
@@ -95,13 +224,15 @@ class SubgraphContext {
|
|
|
95
224
|
subgraphSchema;
|
|
96
225
|
ops = [];
|
|
97
226
|
byo;
|
|
98
|
-
|
|
227
|
+
journal;
|
|
228
|
+
constructor(db, pgSchemaName2, subgraphSchema, block, tx, byo = false, journal = false) {
|
|
99
229
|
this.db = db;
|
|
100
|
-
this.pgSchemaName =
|
|
230
|
+
this.pgSchemaName = pgSchemaName2;
|
|
101
231
|
this.subgraphSchema = subgraphSchema;
|
|
102
232
|
this.block = block;
|
|
103
233
|
this._tx = tx;
|
|
104
234
|
this.byo = byo;
|
|
235
|
+
this.journal = journal;
|
|
105
236
|
}
|
|
106
237
|
get tx() {
|
|
107
238
|
return this._tx;
|
|
@@ -157,6 +288,43 @@ class SubgraphContext {
|
|
|
157
288
|
this.validateTable(table);
|
|
158
289
|
this.ops.push({ kind: "delete", table, data: where });
|
|
159
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
|
+
}
|
|
160
328
|
patch(table, where, set) {
|
|
161
329
|
this.update(table, where, set);
|
|
162
330
|
}
|
|
@@ -178,7 +346,7 @@ class SubgraphContext {
|
|
|
178
346
|
const query = `SELECT * FROM ${qualifiedTable} WHERE ${clause} LIMIT 1`;
|
|
179
347
|
const { rows } = await sql.raw(query).execute(this.db);
|
|
180
348
|
const row = rows[0] ?? null;
|
|
181
|
-
return row ? this.coerceRow(table, row) : null;
|
|
349
|
+
return this.overlayOne(table, where, row ? this.coerceRow(table, row) : null);
|
|
182
350
|
}
|
|
183
351
|
async findMany(table, where) {
|
|
184
352
|
this.validateTable(table);
|
|
@@ -186,7 +354,85 @@ class SubgraphContext {
|
|
|
186
354
|
const { clause } = buildWhereClause(where);
|
|
187
355
|
const query = `SELECT * FROM ${qualifiedTable} WHERE ${clause}`;
|
|
188
356
|
const { rows } = await sql.raw(query).execute(this.db);
|
|
189
|
-
|
|
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
|
+
}
|
|
190
436
|
}
|
|
191
437
|
async count(table, where) {
|
|
192
438
|
this.validateTable(table);
|
|
@@ -247,6 +493,7 @@ class SubgraphContext {
|
|
|
247
493
|
async flush() {
|
|
248
494
|
if (this.ops.length === 0)
|
|
249
495
|
return { count: 0, writes: [] };
|
|
496
|
+
await this.ensureJournalTable();
|
|
250
497
|
const opsToFlush = [...this.ops];
|
|
251
498
|
this.ops.length = 0;
|
|
252
499
|
const statements = this.buildStatements(opsToFlush);
|
|
@@ -264,12 +511,12 @@ class SubgraphContext {
|
|
|
264
511
|
const writes = opsToFlush.map((op, rowIndex) => {
|
|
265
512
|
const blockHeight = op.data._block_height ?? this.block.height;
|
|
266
513
|
const txId = op.data._tx_id ?? this._tx.txId;
|
|
267
|
-
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 };
|
|
268
515
|
baseRow._upsert_keys = undefined;
|
|
269
516
|
baseRow._upsert_fallback_keys = undefined;
|
|
270
517
|
baseRow._upsert_fallback_set = undefined;
|
|
271
518
|
return {
|
|
272
|
-
op: op.kind,
|
|
519
|
+
op: op.kind === "increment" ? "update" : op.kind,
|
|
273
520
|
table: op.table,
|
|
274
521
|
row: jsonSafe(baseRow),
|
|
275
522
|
pk: { blockHeight, txId, rowIndex }
|
|
@@ -294,6 +541,35 @@ class SubgraphContext {
|
|
|
294
541
|
const batchKey = `${op.table}:${[...cols].sort().join(",")}:${upsertKeys ? [...upsertKeys].sort().join(",") : ""}`;
|
|
295
542
|
return { data, cols, vals, upsertKeys, batchKey };
|
|
296
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
|
+
}
|
|
297
573
|
buildStatements(ops) {
|
|
298
574
|
const statements = [];
|
|
299
575
|
if (this.byo) {
|
|
@@ -307,6 +583,38 @@ class SubgraphContext {
|
|
|
307
583
|
}
|
|
308
584
|
let currentBatch = null;
|
|
309
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
|
+
};
|
|
310
618
|
const flushInsertBatch = () => {
|
|
311
619
|
if (!currentBatch)
|
|
312
620
|
return;
|
|
@@ -328,6 +636,11 @@ class SubgraphContext {
|
|
|
328
636
|
}
|
|
329
637
|
const valuesList = rows.map((r) => `(${r.join(", ")})`).join(", ");
|
|
330
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
|
+
}
|
|
331
644
|
if (batch.upsertKeys && batch.upsertKeys.length > 0) {
|
|
332
645
|
const batchKeys = batch.upsertKeys;
|
|
333
646
|
const updateCols = batch.cols.filter((c) => !batchKeys.includes(c) && !c.startsWith("_"));
|
|
@@ -345,6 +658,7 @@ class SubgraphContext {
|
|
|
345
658
|
for (const op of ops) {
|
|
346
659
|
const qualifiedTable = `"${this.pgSchemaName}"."${op.table}"`;
|
|
347
660
|
if (op.kind === "insert") {
|
|
661
|
+
flushIncrementBatch();
|
|
348
662
|
const { cols, vals, upsertKeys, batchKey } = this.prepareInsert(op);
|
|
349
663
|
if (batchKey === currentBatchKey && currentBatch) {
|
|
350
664
|
currentBatch.rows.push(vals);
|
|
@@ -353,22 +667,60 @@ class SubgraphContext {
|
|
|
353
667
|
currentBatch = { table: op.table, cols, rows: [vals], upsertKeys };
|
|
354
668
|
currentBatchKey = batchKey;
|
|
355
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
|
+
}
|
|
356
700
|
} else {
|
|
357
701
|
flushInsertBatch();
|
|
702
|
+
flushIncrementBatch();
|
|
358
703
|
if (op.kind === "update") {
|
|
359
704
|
const setEntries = Object.entries(op.set ?? {});
|
|
360
705
|
for (const [k] of setEntries)
|
|
361
706
|
validateColumnName(k);
|
|
362
707
|
const setClauses = setEntries.map(([k, v]) => `"${k}" = ${escapeLiteral(v)}`);
|
|
363
708
|
const { clause } = buildWhereClause(op.data);
|
|
709
|
+
if (this.journal) {
|
|
710
|
+
statements.push(this.journalCaptureByWhereSQL(op.table, clause));
|
|
711
|
+
}
|
|
364
712
|
statements.push(`UPDATE ${qualifiedTable} SET ${setClauses.join(", ")} WHERE ${clause}`);
|
|
365
713
|
} else if (op.kind === "delete") {
|
|
366
714
|
const { clause } = buildWhereClause(op.data);
|
|
715
|
+
if (this.journal) {
|
|
716
|
+
statements.push(this.journalCaptureByWhereSQL(op.table, clause));
|
|
717
|
+
}
|
|
367
718
|
statements.push(`DELETE FROM ${qualifiedTable} WHERE ${clause}`);
|
|
368
719
|
}
|
|
369
720
|
}
|
|
370
721
|
}
|
|
371
722
|
flushInsertBatch();
|
|
723
|
+
flushIncrementBatch();
|
|
372
724
|
return statements;
|
|
373
725
|
}
|
|
374
726
|
validateTable(table) {
|
|
@@ -377,6 +729,36 @@ class SubgraphContext {
|
|
|
377
729
|
}
|
|
378
730
|
}
|
|
379
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
|
+
}
|
|
380
762
|
function jsonSafe(row) {
|
|
381
763
|
const out = {};
|
|
382
764
|
for (const [k, v] of Object.entries(row)) {
|
|
@@ -695,7 +1077,25 @@ async function runHandlers(subgraph, matched, ctx, opts) {
|
|
|
695
1077
|
filterLookup.set(name, filter);
|
|
696
1078
|
}
|
|
697
1079
|
}
|
|
1080
|
+
const units = [];
|
|
698
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
|
+
}
|
|
699
1099
|
const handler = subgraph.handlers[sourceName] ?? subgraph.handlers["*"] ?? null;
|
|
700
1100
|
if (!handler) {
|
|
701
1101
|
logger2.warn("No handler found for source", {
|
|
@@ -714,9 +1114,29 @@ async function runHandlers(subgraph, matched, ctx, opts) {
|
|
|
714
1114
|
functionName: tx.function_name ?? null
|
|
715
1115
|
});
|
|
716
1116
|
const filter = filterLookup.get(sourceName);
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
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,
|
|
720
1140
|
tx: {
|
|
721
1141
|
txId: tx.tx_id,
|
|
722
1142
|
sender: tx.sender,
|
|
@@ -726,62 +1146,22 @@ async function runHandlers(subgraph, matched, ctx, opts) {
|
|
|
726
1146
|
functionName: tx.function_name
|
|
727
1147
|
}
|
|
728
1148
|
};
|
|
729
|
-
await handler(payload, ctx);
|
|
730
|
-
processed++;
|
|
731
|
-
} catch (err) {
|
|
732
|
-
errors++;
|
|
733
|
-
logger2.error("Subgraph handler error", {
|
|
734
|
-
subgraph: subgraph.name,
|
|
735
|
-
sourceName,
|
|
736
|
-
txId: tx.tx_id,
|
|
737
|
-
error: getErrorMessage(err)
|
|
738
|
-
});
|
|
739
|
-
}
|
|
740
|
-
continue;
|
|
741
|
-
}
|
|
742
|
-
for (const event of events) {
|
|
743
|
-
if (errors >= threshold) {
|
|
744
|
-
logger2.error("Subgraph error threshold reached, skipping remaining events", {
|
|
745
|
-
subgraph: subgraph.name,
|
|
746
|
-
errors,
|
|
747
|
-
threshold
|
|
748
|
-
});
|
|
749
|
-
return { processed, errors };
|
|
750
1149
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
const decoded = decodeEventData(event.data);
|
|
754
|
-
return {
|
|
755
|
-
...decoded,
|
|
756
|
-
_eventId: event.id,
|
|
757
|
-
_eventType: event.type,
|
|
758
|
-
_eventIndex: event.event_index,
|
|
759
|
-
tx: {
|
|
760
|
-
txId: tx.tx_id,
|
|
761
|
-
sender: tx.sender,
|
|
762
|
-
type: tx.type,
|
|
763
|
-
status: tx.status,
|
|
764
|
-
contractId: tx.contract_id,
|
|
765
|
-
functionName: tx.function_name
|
|
766
|
-
}
|
|
767
|
-
};
|
|
768
|
-
})();
|
|
769
|
-
if (filter?.type === "print_event" && filter.topic && payload.topic !== filter.topic) {
|
|
770
|
-
continue;
|
|
771
|
-
}
|
|
772
|
-
await handler(payload, ctx);
|
|
773
|
-
processed++;
|
|
774
|
-
} catch (err) {
|
|
775
|
-
errors++;
|
|
776
|
-
logger2.error("Subgraph handler error", {
|
|
777
|
-
subgraph: subgraph.name,
|
|
778
|
-
sourceName,
|
|
779
|
-
txId: tx.tx_id,
|
|
780
|
-
eventId: event.id,
|
|
781
|
-
eventType: event.type,
|
|
782
|
-
error: getErrorMessage(err)
|
|
783
|
-
});
|
|
1150
|
+
if (event !== null && filter?.type === "print_event" && filter.topic && payload.topic !== filter.topic) {
|
|
1151
|
+
continue;
|
|
784
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
|
+
});
|
|
785
1165
|
}
|
|
786
1166
|
}
|
|
787
1167
|
return { processed, errors };
|
|
@@ -1038,9 +1418,6 @@ import {
|
|
|
1038
1418
|
import { logger as logger5 } from "@secondlayer/shared/logger";
|
|
1039
1419
|
import { sql as sql3 } from "kysely";
|
|
1040
1420
|
|
|
1041
|
-
// src/schema/utils.ts
|
|
1042
|
-
import { pgSchemaName } from "@secondlayer/shared/db/queries/subgraphs";
|
|
1043
|
-
|
|
1044
1421
|
// src/runtime/block-source.ts
|
|
1045
1422
|
import { getSourceDb } from "@secondlayer/shared/db";
|
|
1046
1423
|
import { IndexHttpClient } from "@secondlayer/shared/index-http";
|
|
@@ -1386,7 +1763,7 @@ function resolveBlockSource(subgraph) {
|
|
|
1386
1763
|
}
|
|
1387
1764
|
|
|
1388
1765
|
// src/runtime/outbox-emit.ts
|
|
1389
|
-
import { createHash } from "node:crypto";
|
|
1766
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1390
1767
|
import { logger as logger4 } from "@secondlayer/shared/logger";
|
|
1391
1768
|
var loggedKillSwitch = false;
|
|
1392
1769
|
var OP_VERB = {
|
|
@@ -1399,7 +1776,7 @@ function isEmitOutboxEnabled() {
|
|
|
1399
1776
|
}
|
|
1400
1777
|
function dedupKey(subgraphName, tableName, blockHeight, txId, rowIndex, row) {
|
|
1401
1778
|
const canonical = `${subgraphName}:${tableName}:${blockHeight}:${txId}:${rowIndex}:${stableStringify(row)}`;
|
|
1402
|
-
return
|
|
1779
|
+
return createHash2("sha256").update(canonical).digest("hex").slice(0, 32);
|
|
1403
1780
|
}
|
|
1404
1781
|
function stableStringify(obj) {
|
|
1405
1782
|
const keys = Object.keys(obj).sort();
|
|
@@ -1651,6 +2028,32 @@ async function resolveTraitContracts(subgraph, blockHeight, db) {
|
|
|
1651
2028
|
}
|
|
1652
2029
|
return resolved;
|
|
1653
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
|
+
}
|
|
1654
2057
|
async function processBlock(subgraph, subgraphName, blockHeight, opts) {
|
|
1655
2058
|
const targetDb = getTargetDb();
|
|
1656
2059
|
const blockStart = performance.now();
|
|
@@ -1718,10 +2121,17 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
|
|
|
1718
2121
|
}
|
|
1719
2122
|
};
|
|
1720
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
|
+
}
|
|
1721
2131
|
let runResult = { processed: 0, errors: 0 };
|
|
1722
2132
|
let manifest;
|
|
1723
2133
|
await route.dataDb.transaction().execute(async (tx) => {
|
|
1724
|
-
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));
|
|
1725
2135
|
const handlerStart = performance.now();
|
|
1726
2136
|
runResult = await runHandlers(subgraph, matched, ctx);
|
|
1727
2137
|
handlerMs = performance.now() - handlerStart;
|
|
@@ -1737,24 +2147,39 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
|
|
|
1737
2147
|
if (manifest && manifest.count > 0) {
|
|
1738
2148
|
await emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, block.height);
|
|
1739
2149
|
}
|
|
2150
|
+
if (opts?.atomicProgress && manifest && manifest.count > 0) {
|
|
2151
|
+
await updateSubgraphStatus(tx, subgraphName, opts.atomicProgress.status, blockHeight);
|
|
2152
|
+
}
|
|
1740
2153
|
await applyProgress(tx, runResult);
|
|
1741
2154
|
});
|
|
1742
2155
|
} else {
|
|
1743
2156
|
await targetDb.transaction().execute(async (tx) => {
|
|
1744
|
-
|
|
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));
|
|
1745
2165
|
const handlerStart = performance.now();
|
|
1746
2166
|
const runResult = await runHandlers(subgraph, matched, ctx);
|
|
1747
2167
|
handlerMs = performance.now() - handlerStart;
|
|
1748
2168
|
result.processed = runResult.processed;
|
|
1749
2169
|
result.errors = runResult.errors;
|
|
2170
|
+
let flushedWrites = false;
|
|
1750
2171
|
if (ctx.pendingOps > 0) {
|
|
1751
2172
|
const flushStart = performance.now();
|
|
1752
2173
|
const manifest = await ctx.flush();
|
|
2174
|
+
flushedWrites = manifest.count > 0;
|
|
1753
2175
|
if (manifest.count > 0) {
|
|
1754
2176
|
await emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, block.height);
|
|
1755
2177
|
}
|
|
1756
2178
|
flushMs = performance.now() - flushStart;
|
|
1757
2179
|
}
|
|
2180
|
+
if (opts?.atomicProgress && flushedWrites) {
|
|
2181
|
+
await updateSubgraphStatus(tx, subgraphName, opts.atomicProgress.status, blockHeight);
|
|
2182
|
+
}
|
|
1758
2183
|
await applyProgress(tx, runResult);
|
|
1759
2184
|
});
|
|
1760
2185
|
}
|
|
@@ -1784,6 +2209,9 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
|
|
|
1784
2209
|
error: err instanceof Error ? err.message : String(err)
|
|
1785
2210
|
});
|
|
1786
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
|
+
}
|
|
1787
2215
|
}
|
|
1788
2216
|
return result;
|
|
1789
2217
|
}
|
|
@@ -1858,118 +2286,16 @@ import {
|
|
|
1858
2286
|
recordGapBatch,
|
|
1859
2287
|
resolveGaps
|
|
1860
2288
|
} from "@secondlayer/shared/db/queries/subgraph-gaps";
|
|
2289
|
+
import { updateOperationProcessedEvents } from "@secondlayer/shared/db/queries/subgraph-operations";
|
|
1861
2290
|
import {
|
|
1862
2291
|
recordSubgraphProcessed as recordSubgraphProcessed2,
|
|
1863
2292
|
updateSubgraphStatus as updateSubgraphStatus2
|
|
1864
2293
|
} from "@secondlayer/shared/db/queries/subgraphs";
|
|
1865
2294
|
import { logger as logger6 } from "@secondlayer/shared/logger";
|
|
1866
|
-
|
|
1867
|
-
// src/schema/generator.ts
|
|
1868
|
-
import { createHash as createHash2 } from "node:crypto";
|
|
1869
|
-
var TYPE_MAP = {
|
|
1870
|
-
text: "TEXT",
|
|
1871
|
-
uint: "NUMERIC",
|
|
1872
|
-
int: "NUMERIC",
|
|
1873
|
-
principal: "TEXT",
|
|
1874
|
-
boolean: "BOOLEAN",
|
|
1875
|
-
timestamp: "TIMESTAMPTZ",
|
|
1876
|
-
jsonb: "JSONB"
|
|
1877
|
-
};
|
|
1878
|
-
function escapeLiteralDefault(value) {
|
|
1879
|
-
if (value === null || value === undefined)
|
|
1880
|
-
return "NULL";
|
|
1881
|
-
if (typeof value === "number" || typeof value === "bigint")
|
|
1882
|
-
return String(value);
|
|
1883
|
-
if (typeof value === "boolean")
|
|
1884
|
-
return value ? "TRUE" : "FALSE";
|
|
1885
|
-
return `'${String(value).replace(/'/g, "''")}'`;
|
|
1886
|
-
}
|
|
1887
|
-
function tableNeedsTrgm(tableDef) {
|
|
1888
|
-
return Object.values(tableDef.columns).some((col) => col.search);
|
|
1889
|
-
}
|
|
1890
|
-
function emitTableDDL(schemaName, tableName, tableDef) {
|
|
1891
|
-
const qualifiedName = `${schemaName}.${tableName}`;
|
|
1892
|
-
const statements = [];
|
|
1893
|
-
const columnDefs = [
|
|
1894
|
-
"_id BIGSERIAL PRIMARY KEY",
|
|
1895
|
-
"_block_height BIGINT NOT NULL",
|
|
1896
|
-
"_tx_id TEXT NOT NULL",
|
|
1897
|
-
"_created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()"
|
|
1898
|
-
];
|
|
1899
|
-
for (const [colName, col] of Object.entries(tableDef.columns)) {
|
|
1900
|
-
const sqlType = TYPE_MAP[col.type];
|
|
1901
|
-
const nullable = col.nullable ? "" : " NOT NULL";
|
|
1902
|
-
let colDef = `${colName} ${sqlType}${nullable}`;
|
|
1903
|
-
if (col.default !== undefined) {
|
|
1904
|
-
colDef += ` DEFAULT ${escapeLiteralDefault(col.default)}`;
|
|
1905
|
-
}
|
|
1906
|
-
columnDefs.push(colDef);
|
|
1907
|
-
}
|
|
1908
|
-
statements.push(`CREATE TABLE IF NOT EXISTS ${qualifiedName} (
|
|
1909
|
-
${columnDefs.join(`,
|
|
1910
|
-
`)}
|
|
1911
|
-
)`);
|
|
1912
|
-
statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_block_height ON ${qualifiedName} (_block_height)`);
|
|
1913
|
-
statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_tx_id ON ${qualifiedName} (_tx_id)`);
|
|
1914
|
-
for (const [colName, col] of Object.entries(tableDef.columns)) {
|
|
1915
|
-
if (col.indexed) {
|
|
1916
|
-
statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName} ON ${qualifiedName} (${colName})`);
|
|
1917
|
-
}
|
|
1918
|
-
}
|
|
1919
|
-
for (const [colName, col] of Object.entries(tableDef.columns)) {
|
|
1920
|
-
if (col.search) {
|
|
1921
|
-
statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName}_trgm ON ${qualifiedName} USING gin (${colName} gin_trgm_ops)`);
|
|
1922
|
-
}
|
|
1923
|
-
}
|
|
1924
|
-
if (tableDef.indexes) {
|
|
1925
|
-
for (let i = 0;i < tableDef.indexes.length; i++) {
|
|
1926
|
-
const cols = tableDef.indexes[i];
|
|
1927
|
-
const idxName = `idx_${schemaName}_${tableName}_composite_${i}`;
|
|
1928
|
-
statements.push(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${qualifiedName} (${cols.join(", ")})`);
|
|
1929
|
-
}
|
|
1930
|
-
}
|
|
1931
|
-
if (tableDef.uniqueKeys) {
|
|
1932
|
-
for (let i = 0;i < tableDef.uniqueKeys.length; i++) {
|
|
1933
|
-
const cols = tableDef.uniqueKeys[i];
|
|
1934
|
-
const constraintName = `uq_${schemaName}_${tableName}_${cols.join("_")}`;
|
|
1935
|
-
statements.push(`ALTER TABLE ${qualifiedName} ADD CONSTRAINT ${constraintName} UNIQUE (${cols.join(", ")})`);
|
|
1936
|
-
}
|
|
1937
|
-
}
|
|
1938
|
-
return statements;
|
|
1939
|
-
}
|
|
1940
|
-
function emitForeignKeyDDL(schemaName, tableName, tableDef) {
|
|
1941
|
-
return (tableDef.relations ?? []).map((rel) => {
|
|
1942
|
-
const constraintName = `fk_${schemaName}_${tableName}_${rel.name}`;
|
|
1943
|
-
return `ALTER TABLE ${schemaName}.${tableName} ADD CONSTRAINT ${constraintName} ` + `FOREIGN KEY (${rel.fields.join(", ")}) ` + `REFERENCES ${schemaName}.${rel.references} (${rel.referencedColumns.join(", ")})`;
|
|
1944
|
-
});
|
|
1945
|
-
}
|
|
1946
|
-
function generateSubgraphSQL(def, schemaNameOverride) {
|
|
1947
|
-
const schemaName = schemaNameOverride ?? pgSchemaName(def.name);
|
|
1948
|
-
const statements = [];
|
|
1949
|
-
const needsTrgm = Object.values(def.schema).some((table) => Object.values(table.columns).some((col) => col.search));
|
|
1950
|
-
if (needsTrgm) {
|
|
1951
|
-
statements.push("CREATE EXTENSION IF NOT EXISTS pg_trgm");
|
|
1952
|
-
}
|
|
1953
|
-
statements.push(`CREATE SCHEMA IF NOT EXISTS ${schemaName}`);
|
|
1954
|
-
for (const [tableName, tableDef] of Object.entries(def.schema)) {
|
|
1955
|
-
statements.push(...emitTableDDL(schemaName, tableName, tableDef));
|
|
1956
|
-
}
|
|
1957
|
-
for (const [tableName, tableDef] of Object.entries(def.schema)) {
|
|
1958
|
-
statements.push(...emitForeignKeyDDL(schemaName, tableName, tableDef));
|
|
1959
|
-
}
|
|
1960
|
-
const hashInput = JSON.stringify({
|
|
1961
|
-
name: def.name,
|
|
1962
|
-
schema: def.schema,
|
|
1963
|
-
sources: def.sources
|
|
1964
|
-
}, (_key, value) => typeof value === "bigint" ? value.toString() : value);
|
|
1965
|
-
const hash = createHash2("sha256").update(hashInput).digest("hex");
|
|
1966
|
-
return { statements, hash };
|
|
1967
|
-
}
|
|
1968
|
-
|
|
1969
|
-
// src/runtime/reindex.ts
|
|
1970
2295
|
var LOG_INTERVAL = 1000;
|
|
1971
2296
|
var HEALTH_FLUSH_INTERVAL = 1000;
|
|
1972
2297
|
var PROGRESS_FLUSH_INTERVAL_MS = 5000;
|
|
2298
|
+
var EMPTY_BATCH_HALT_THRESHOLD = 3;
|
|
1973
2299
|
var STANDARD_REINDEX_BATCH_CONFIG = {
|
|
1974
2300
|
defaultBatchSize: 500,
|
|
1975
2301
|
minBatchSize: 100,
|
|
@@ -2045,6 +2371,7 @@ async function processBlockRange(def, opts) {
|
|
|
2045
2371
|
let batchSize = batchConfig.defaultBatchSize;
|
|
2046
2372
|
let currentHeight = fromBlock;
|
|
2047
2373
|
let aborted = false;
|
|
2374
|
+
let consecutiveEmptyBatches = 0;
|
|
2048
2375
|
const sparse = Boolean(source.nextDataHeight && canSparseScan(def));
|
|
2049
2376
|
const flushHealth = async () => {
|
|
2050
2377
|
if (pendingEventsProcessed === 0 && pendingErrors === 0)
|
|
@@ -2056,6 +2383,13 @@ async function processBlockRange(def, opts) {
|
|
|
2056
2383
|
lastHealthFlushBlock = blocksProcessed;
|
|
2057
2384
|
lastHealthFlushAt = Date.now();
|
|
2058
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
|
+
};
|
|
2059
2393
|
let nextBatchEnd = Math.min(currentHeight + batchSize - 1, toBlock);
|
|
2060
2394
|
let nextBatchPromise = source.loadBlockRange(currentHeight, nextBatchEnd);
|
|
2061
2395
|
while (currentHeight <= toBlock) {
|
|
@@ -2070,6 +2404,14 @@ async function processBlockRange(def, opts) {
|
|
|
2070
2404
|
}
|
|
2071
2405
|
const batch = await nextBatchPromise;
|
|
2072
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
|
+
}
|
|
2073
2415
|
const nextStart = batchEnd + 1;
|
|
2074
2416
|
if (nextStart <= toBlock) {
|
|
2075
2417
|
nextBatchEnd = Math.min(nextStart + batchSize - 1, toBlock);
|
|
@@ -2077,28 +2419,39 @@ async function processBlockRange(def, opts) {
|
|
|
2077
2419
|
}
|
|
2078
2420
|
const batchFailedBlocks = [];
|
|
2079
2421
|
let batchMatched = 0;
|
|
2422
|
+
const atomicProgress = status === "reindexing" ? { status } : undefined;
|
|
2080
2423
|
for (let height = currentHeight;height <= batchEnd; height++) {
|
|
2081
|
-
|
|
2424
|
+
let blockData = batch.get(height);
|
|
2082
2425
|
if (!blockData) {
|
|
2426
|
+
blockData = (await source.loadBlockRange(height, height)).get(height);
|
|
2427
|
+
}
|
|
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
|
+
}
|
|
2083
2433
|
batchFailedBlocks.push({ height, reason: "block_missing" });
|
|
2084
2434
|
blocksProcessed++;
|
|
2085
2435
|
continue;
|
|
2086
2436
|
}
|
|
2087
2437
|
let result;
|
|
2088
2438
|
try {
|
|
2089
|
-
result = await
|
|
2439
|
+
result = await processBlockWithRetry(def, subgraphName, height, {
|
|
2090
2440
|
skipProgressUpdate: true,
|
|
2441
|
+
atomicProgress,
|
|
2091
2442
|
preloaded: blockData
|
|
2092
2443
|
});
|
|
2093
2444
|
} catch (err) {
|
|
2094
|
-
const errorMsg =
|
|
2095
|
-
logger6.error("Block processing
|
|
2445
|
+
const errorMsg = getErrorMessage2(err);
|
|
2446
|
+
logger6.error("Block processing failed persistently", {
|
|
2096
2447
|
subgraph: subgraphName,
|
|
2097
2448
|
blockHeight: height,
|
|
2098
2449
|
error: errorMsg
|
|
2099
2450
|
});
|
|
2451
|
+
if (status === "reindexing") {
|
|
2452
|
+
await haltRange(`block ${height} failed persistently: ${errorMsg}`, height);
|
|
2453
|
+
}
|
|
2100
2454
|
batchFailedBlocks.push({ height, reason: "processing_error" });
|
|
2101
|
-
await updateSubgraphStatus2(targetDb, subgraphName, status, height).catch(() => {});
|
|
2102
2455
|
blocksProcessed++;
|
|
2103
2456
|
totalErrors++;
|
|
2104
2457
|
pendingErrors++;
|
|
@@ -2124,6 +2477,9 @@ async function processBlockRange(def, opts) {
|
|
|
2124
2477
|
const shouldFlushProgress = blocksProcessed % 100 === 0 || now - lastProgressFlushAt >= PROGRESS_FLUSH_INTERVAL_MS;
|
|
2125
2478
|
if (shouldFlushProgress) {
|
|
2126
2479
|
await updateSubgraphStatus2(targetDb, subgraphName, status, height);
|
|
2480
|
+
if (opts.operationId) {
|
|
2481
|
+
await updateOperationProcessedEvents(targetDb, opts.operationId, totalEventsProcessed).catch(() => {});
|
|
2482
|
+
}
|
|
2127
2483
|
lastProgressFlushAt = now;
|
|
2128
2484
|
}
|
|
2129
2485
|
if (blocksProcessed % LOG_INTERVAL === 0) {
|
|
@@ -2241,6 +2597,7 @@ async function reindexSubgraph(def, opts) {
|
|
|
2241
2597
|
isCatchup: false,
|
|
2242
2598
|
apiKeyId: null,
|
|
2243
2599
|
subgraphId: subgraphRow?.id,
|
|
2600
|
+
operationId: opts?.operationId,
|
|
2244
2601
|
signal: opts?.signal
|
|
2245
2602
|
});
|
|
2246
2603
|
if (result.aborted) {
|
|
@@ -2316,6 +2673,7 @@ async function resumeReindex(def, opts) {
|
|
|
2316
2673
|
isCatchup: false,
|
|
2317
2674
|
apiKeyId: null,
|
|
2318
2675
|
subgraphId: row.id,
|
|
2676
|
+
operationId: opts.operationId,
|
|
2319
2677
|
signal: opts.signal
|
|
2320
2678
|
});
|
|
2321
2679
|
if (result.aborted) {
|
|
@@ -2364,6 +2722,7 @@ async function backfillSubgraph(def, opts) {
|
|
|
2364
2722
|
isCatchup: false,
|
|
2365
2723
|
apiKeyId: null,
|
|
2366
2724
|
subgraphId: subgraphRow?.id,
|
|
2725
|
+
operationId: opts.operationId,
|
|
2367
2726
|
signal: opts.signal
|
|
2368
2727
|
});
|
|
2369
2728
|
if (result.aborted) {
|
|
@@ -3238,10 +3597,12 @@ function getDefault(type) {
|
|
|
3238
3597
|
}
|
|
3239
3598
|
export {
|
|
3240
3599
|
validateSubgraphDefinition,
|
|
3600
|
+
sparseProbeTargets,
|
|
3241
3601
|
resumeReindex,
|
|
3242
3602
|
renderDeployPlan,
|
|
3243
3603
|
reindexSubgraph,
|
|
3244
3604
|
pgSchemaName,
|
|
3605
|
+
hasBreakingChanges,
|
|
3245
3606
|
generateSubgraphSQL,
|
|
3246
3607
|
generatePrismaSchema,
|
|
3247
3608
|
generateKyselySchema,
|
|
@@ -3250,10 +3611,11 @@ export {
|
|
|
3250
3611
|
diffSchema,
|
|
3251
3612
|
deploySchema,
|
|
3252
3613
|
defineSubgraph,
|
|
3614
|
+
canSparseScan,
|
|
3253
3615
|
backfillSubgraph,
|
|
3254
3616
|
INDEX_CODEGEN_TABLES,
|
|
3255
3617
|
ByoBreakingChangeError
|
|
3256
3618
|
};
|
|
3257
3619
|
|
|
3258
|
-
//# debugId=
|
|
3620
|
+
//# debugId=A7DC4F802366B29864756E2164756E21
|
|
3259
3621
|
//# sourceMappingURL=index.js.map
|