@secondlayer/subgraphs 3.12.0 → 3.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/index.d.ts +54 -1
- package/dist/src/index.js +952 -199
- package/dist/src/index.js.map +12 -11
- package/dist/src/runtime/block-processor.d.ts +36 -1
- package/dist/src/runtime/block-processor.js +568 -86
- package/dist/src/runtime/block-processor.js.map +9 -8
- package/dist/src/runtime/catchup.d.ts +7 -0
- package/dist/src/runtime/catchup.js +587 -132
- package/dist/src/runtime/catchup.js.map +10 -9
- package/dist/src/runtime/context.d.ts +65 -3
- package/dist/src/runtime/context.js +390 -8
- package/dist/src/runtime/context.js.map +6 -4
- package/dist/src/runtime/processor.js +665 -248
- package/dist/src/runtime/processor.js.map +13 -13
- package/dist/src/runtime/reindex.d.ts +7 -0
- package/dist/src/runtime/reindex.js +618 -198
- package/dist/src/runtime/reindex.js.map +10 -10
- package/dist/src/runtime/reorg.d.ts +7 -0
- package/dist/src/runtime/reorg.js +588 -87
- package/dist/src/runtime/reorg.js.map +10 -9
- package/dist/src/runtime/replay.js.map +2 -2
- package/dist/src/runtime/runner.d.ts +70 -2
- package/dist/src/runtime/runner.js +56 -58
- package/dist/src/runtime/runner.js.map +3 -3
- package/dist/src/runtime/source-matcher.d.ts +2 -0
- package/dist/src/runtime/source-matcher.js.map +2 -2
- package/dist/src/schema/index.d.ts +7 -0
- package/dist/src/schema/index.js +20 -2
- package/dist/src/schema/index.js.map +4 -4
- package/dist/src/service.js +665 -248
- package/dist/src/service.js.map +13 -13
- package/dist/src/types.d.ts +7 -0
- package/dist/src/validate.d.ts +7 -0
- package/dist/src/validate.js +3 -2
- package/dist/src/validate.js.map +3 -3
- package/package.json +2 -2
package/dist/src/index.js
CHANGED
|
@@ -62,7 +62,8 @@ var SubgraphFilterSchema = z.object({
|
|
|
62
62
|
topic: z.string().optional(),
|
|
63
63
|
lockedAddress: z.string().optional(),
|
|
64
64
|
abi: z.record(z.string(), z.any()).optional(),
|
|
65
|
-
trait: z.string().optional()
|
|
65
|
+
trait: z.string().optional(),
|
|
66
|
+
prints: z.record(z.string(), z.record(z.string(), ColumnTypeSchema)).optional()
|
|
66
67
|
}).strict();
|
|
67
68
|
var SubgraphDefinitionSchema = z.object({
|
|
68
69
|
name: SubgraphNameSchema,
|
|
@@ -82,6 +83,134 @@ function validateSubgraphDefinition(def) {
|
|
|
82
83
|
import { logger } from "@secondlayer/shared/logger";
|
|
83
84
|
import { formatUnits } from "@secondlayer/stacks/utils";
|
|
84
85
|
import { sql } from "kysely";
|
|
86
|
+
|
|
87
|
+
// src/schema/generator.ts
|
|
88
|
+
import { createHash } from "node:crypto";
|
|
89
|
+
|
|
90
|
+
// src/schema/utils.ts
|
|
91
|
+
import { pgSchemaName } from "@secondlayer/shared/db/queries/subgraphs";
|
|
92
|
+
|
|
93
|
+
// src/schema/generator.ts
|
|
94
|
+
var TYPE_MAP = {
|
|
95
|
+
text: "TEXT",
|
|
96
|
+
uint: "NUMERIC",
|
|
97
|
+
int: "NUMERIC",
|
|
98
|
+
principal: "TEXT",
|
|
99
|
+
boolean: "BOOLEAN",
|
|
100
|
+
timestamp: "TIMESTAMPTZ",
|
|
101
|
+
jsonb: "JSONB"
|
|
102
|
+
};
|
|
103
|
+
function escapeLiteralDefault(value) {
|
|
104
|
+
if (value === null || value === undefined)
|
|
105
|
+
return "NULL";
|
|
106
|
+
if (typeof value === "number" || typeof value === "bigint")
|
|
107
|
+
return String(value);
|
|
108
|
+
if (typeof value === "boolean")
|
|
109
|
+
return value ? "TRUE" : "FALSE";
|
|
110
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
111
|
+
}
|
|
112
|
+
function tableNeedsTrgm(tableDef) {
|
|
113
|
+
return Object.values(tableDef.columns).some((col) => col.search);
|
|
114
|
+
}
|
|
115
|
+
function emitTableDDL(schemaName, tableName, tableDef) {
|
|
116
|
+
const qualifiedName = `${schemaName}.${tableName}`;
|
|
117
|
+
const statements = [];
|
|
118
|
+
const columnDefs = [
|
|
119
|
+
"_id BIGSERIAL PRIMARY KEY",
|
|
120
|
+
"_block_height BIGINT NOT NULL",
|
|
121
|
+
"_tx_id TEXT NOT NULL",
|
|
122
|
+
"_created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()"
|
|
123
|
+
];
|
|
124
|
+
for (const [colName, col] of Object.entries(tableDef.columns)) {
|
|
125
|
+
const sqlType = TYPE_MAP[col.type];
|
|
126
|
+
const nullable = col.nullable ? "" : " NOT NULL";
|
|
127
|
+
let colDef = `${colName} ${sqlType}${nullable}`;
|
|
128
|
+
if (col.default !== undefined) {
|
|
129
|
+
colDef += ` DEFAULT ${escapeLiteralDefault(col.default)}`;
|
|
130
|
+
}
|
|
131
|
+
if (col.type === "uint") {
|
|
132
|
+
colDef += ` CHECK (${colName} >= 0)`;
|
|
133
|
+
}
|
|
134
|
+
columnDefs.push(colDef);
|
|
135
|
+
}
|
|
136
|
+
statements.push(`CREATE TABLE IF NOT EXISTS ${qualifiedName} (
|
|
137
|
+
${columnDefs.join(`,
|
|
138
|
+
`)}
|
|
139
|
+
)`);
|
|
140
|
+
statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_block_height ON ${qualifiedName} (_block_height)`);
|
|
141
|
+
statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_tx_id ON ${qualifiedName} (_tx_id)`);
|
|
142
|
+
for (const [colName, col] of Object.entries(tableDef.columns)) {
|
|
143
|
+
if (col.indexed) {
|
|
144
|
+
statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName} ON ${qualifiedName} (${colName})`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
for (const [colName, col] of Object.entries(tableDef.columns)) {
|
|
148
|
+
if (col.search) {
|
|
149
|
+
statements.push(`CREATE INDEX IF NOT EXISTS idx_${schemaName}_${tableName}_${colName}_trgm ON ${qualifiedName} USING gin (${colName} gin_trgm_ops)`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (tableDef.indexes) {
|
|
153
|
+
for (let i = 0;i < tableDef.indexes.length; i++) {
|
|
154
|
+
const cols = tableDef.indexes[i];
|
|
155
|
+
const idxName = `idx_${schemaName}_${tableName}_composite_${i}`;
|
|
156
|
+
statements.push(`CREATE INDEX IF NOT EXISTS ${idxName} ON ${qualifiedName} (${cols.join(", ")})`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (tableDef.uniqueKeys) {
|
|
160
|
+
for (let i = 0;i < tableDef.uniqueKeys.length; i++) {
|
|
161
|
+
const cols = tableDef.uniqueKeys[i];
|
|
162
|
+
const constraintName = `uq_${schemaName}_${tableName}_${cols.join("_")}`;
|
|
163
|
+
statements.push(`ALTER TABLE ${qualifiedName} ADD CONSTRAINT ${constraintName} UNIQUE (${cols.join(", ")})`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return statements;
|
|
167
|
+
}
|
|
168
|
+
function emitJournalDDL(schemaName) {
|
|
169
|
+
return [
|
|
170
|
+
`CREATE TABLE IF NOT EXISTS ${schemaName}._journal (
|
|
171
|
+
_jid BIGSERIAL PRIMARY KEY,
|
|
172
|
+
block_height BIGINT NOT NULL,
|
|
173
|
+
table_name TEXT NOT NULL,
|
|
174
|
+
row_key JSONB NOT NULL,
|
|
175
|
+
prev_row JSONB,
|
|
176
|
+
_created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
177
|
+
)`,
|
|
178
|
+
`CREATE INDEX IF NOT EXISTS idx_${schemaName}_journal_height ON ${schemaName}._journal (block_height)`
|
|
179
|
+
];
|
|
180
|
+
}
|
|
181
|
+
function emitForeignKeyDDL(schemaName, tableName, tableDef) {
|
|
182
|
+
return (tableDef.relations ?? []).map((rel) => {
|
|
183
|
+
const constraintName = `fk_${schemaName}_${tableName}_${rel.name}`;
|
|
184
|
+
return `ALTER TABLE ${schemaName}.${tableName} ADD CONSTRAINT ${constraintName} ` + `FOREIGN KEY (${rel.fields.join(", ")}) ` + `REFERENCES ${schemaName}.${rel.references} (${rel.referencedColumns.join(", ")})`;
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
function generateSubgraphSQL(def, schemaNameOverride) {
|
|
188
|
+
const schemaName = schemaNameOverride ?? pgSchemaName(def.name);
|
|
189
|
+
const statements = [];
|
|
190
|
+
const needsTrgm = Object.values(def.schema).some((table) => Object.values(table.columns).some((col) => col.search));
|
|
191
|
+
if (needsTrgm) {
|
|
192
|
+
statements.push("CREATE EXTENSION IF NOT EXISTS pg_trgm");
|
|
193
|
+
}
|
|
194
|
+
statements.push(`CREATE SCHEMA IF NOT EXISTS ${schemaName}`);
|
|
195
|
+
for (const [tableName, tableDef] of Object.entries(def.schema)) {
|
|
196
|
+
statements.push(...emitTableDDL(schemaName, tableName, tableDef));
|
|
197
|
+
}
|
|
198
|
+
statements.push(...emitJournalDDL(schemaName));
|
|
199
|
+
for (const [tableName, tableDef] of Object.entries(def.schema)) {
|
|
200
|
+
statements.push(...emitForeignKeyDDL(schemaName, tableName, tableDef));
|
|
201
|
+
}
|
|
202
|
+
const hashInput = JSON.stringify({
|
|
203
|
+
name: def.name,
|
|
204
|
+
schema: def.schema,
|
|
205
|
+
sources: def.sources
|
|
206
|
+
}, (_key, value) => typeof value === "bigint" ? value.toString() : value);
|
|
207
|
+
const hash = createHash("sha256").update(hashInput).digest("hex");
|
|
208
|
+
return { statements, hash };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/runtime/context.ts
|
|
212
|
+
var JOURNAL_RETENTION_BLOCKS = 300;
|
|
213
|
+
var journalEnsured = new Set;
|
|
85
214
|
function validateColumnName(name) {
|
|
86
215
|
if (!/^[a-z_][a-z0-9_]*$/i.test(name)) {
|
|
87
216
|
throw new Error(`Invalid column name: ${name}`);
|
|
@@ -96,13 +225,15 @@ class SubgraphContext {
|
|
|
96
225
|
subgraphSchema;
|
|
97
226
|
ops = [];
|
|
98
227
|
byo;
|
|
99
|
-
|
|
228
|
+
journal;
|
|
229
|
+
constructor(db, pgSchemaName2, subgraphSchema, block, tx, byo = false, journal = false) {
|
|
100
230
|
this.db = db;
|
|
101
|
-
this.pgSchemaName =
|
|
231
|
+
this.pgSchemaName = pgSchemaName2;
|
|
102
232
|
this.subgraphSchema = subgraphSchema;
|
|
103
233
|
this.block = block;
|
|
104
234
|
this._tx = tx;
|
|
105
235
|
this.byo = byo;
|
|
236
|
+
this.journal = journal;
|
|
106
237
|
}
|
|
107
238
|
get tx() {
|
|
108
239
|
return this._tx;
|
|
@@ -158,6 +289,43 @@ class SubgraphContext {
|
|
|
158
289
|
this.validateTable(table);
|
|
159
290
|
this.ops.push({ kind: "delete", table, data: where });
|
|
160
291
|
}
|
|
292
|
+
increment(table, key, deltas) {
|
|
293
|
+
this.validateTable(table);
|
|
294
|
+
const tableDef = this.subgraphSchema[table];
|
|
295
|
+
const keyColumns = Object.keys(key);
|
|
296
|
+
const hasUniqueConstraint = tableDef?.uniqueKeys?.some((uk) => uk.length === keyColumns.length && uk.every((c) => keyColumns.includes(c)));
|
|
297
|
+
if (!hasUniqueConstraint) {
|
|
298
|
+
throw new Error(`increment("${table}") requires a uniqueKeys constraint on [${keyColumns.join(", ")}]`);
|
|
299
|
+
}
|
|
300
|
+
for (const [col, v] of Object.entries(deltas)) {
|
|
301
|
+
validateColumnName(col);
|
|
302
|
+
if (keyColumns.includes(col)) {
|
|
303
|
+
throw new Error(`increment("${table}"): "${col}" is a key column`);
|
|
304
|
+
}
|
|
305
|
+
if (typeof v !== "bigint" && typeof v !== "number") {
|
|
306
|
+
throw new Error(`increment("${table}"): delta for "${col}" must be bigint or number`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
this.ops.push({
|
|
310
|
+
kind: "increment",
|
|
311
|
+
table,
|
|
312
|
+
data: {
|
|
313
|
+
...key,
|
|
314
|
+
_block_height: this.block.height,
|
|
315
|
+
_tx_id: this._tx.txId,
|
|
316
|
+
_upsert_keys: keyColumns
|
|
317
|
+
},
|
|
318
|
+
set: { ...deltas }
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
opsCheckpoint() {
|
|
322
|
+
return this.ops.length;
|
|
323
|
+
}
|
|
324
|
+
rollbackTo(checkpoint) {
|
|
325
|
+
if (checkpoint < 0 || checkpoint > this.ops.length)
|
|
326
|
+
return;
|
|
327
|
+
this.ops.length = checkpoint;
|
|
328
|
+
}
|
|
161
329
|
patch(table, where, set) {
|
|
162
330
|
this.update(table, where, set);
|
|
163
331
|
}
|
|
@@ -179,7 +347,7 @@ class SubgraphContext {
|
|
|
179
347
|
const query = `SELECT * FROM ${qualifiedTable} WHERE ${clause} LIMIT 1`;
|
|
180
348
|
const { rows } = await sql.raw(query).execute(this.db);
|
|
181
349
|
const row = rows[0] ?? null;
|
|
182
|
-
return row ? this.coerceRow(table, row) : null;
|
|
350
|
+
return this.overlayOne(table, where, row ? this.coerceRow(table, row) : null);
|
|
183
351
|
}
|
|
184
352
|
async findMany(table, where) {
|
|
185
353
|
this.validateTable(table);
|
|
@@ -187,7 +355,85 @@ class SubgraphContext {
|
|
|
187
355
|
const { clause } = buildWhereClause(where);
|
|
188
356
|
const query = `SELECT * FROM ${qualifiedTable} WHERE ${clause}`;
|
|
189
357
|
const { rows } = await sql.raw(query).execute(this.db);
|
|
190
|
-
|
|
358
|
+
const dbRows = rows.map((r) => this.coerceRow(table, r));
|
|
359
|
+
return this.overlayMany(table, where, dbRows);
|
|
360
|
+
}
|
|
361
|
+
overlayOne(table, where, dbRow) {
|
|
362
|
+
let row = dbRow;
|
|
363
|
+
for (const op of this.ops) {
|
|
364
|
+
if (op.table !== table)
|
|
365
|
+
continue;
|
|
366
|
+
row = this.applyOpToRow(op, row, where);
|
|
367
|
+
}
|
|
368
|
+
return row;
|
|
369
|
+
}
|
|
370
|
+
overlayMany(table, where, dbRows) {
|
|
371
|
+
let result = [...dbRows];
|
|
372
|
+
for (const op of this.ops) {
|
|
373
|
+
if (op.table !== table)
|
|
374
|
+
continue;
|
|
375
|
+
if (op.kind === "update") {
|
|
376
|
+
result = result.map((r) => rowMatches(r, op.data) ? { ...r, ...op.set ?? {} } : r);
|
|
377
|
+
} else if (op.kind === "delete") {
|
|
378
|
+
result = result.filter((r) => !rowMatches(r, op.data));
|
|
379
|
+
} else {
|
|
380
|
+
const upsertKeys = op.data._upsert_keys;
|
|
381
|
+
const clean = stripControlKeys(op.data);
|
|
382
|
+
const idx = upsertKeys ? result.findIndex((r) => upsertKeys.every((k) => valEq(r[k], clean[k]))) : -1;
|
|
383
|
+
if (idx >= 0) {
|
|
384
|
+
result[idx] = this.applyOpToRow(op, result[idx], where) ?? result[idx];
|
|
385
|
+
} else {
|
|
386
|
+
const created = this.applyOpToRow(op, null, where);
|
|
387
|
+
if (created)
|
|
388
|
+
result.push(created);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return result;
|
|
393
|
+
}
|
|
394
|
+
applyOpToRow(op, row, where) {
|
|
395
|
+
const upsertKeys = op.data._upsert_keys;
|
|
396
|
+
const clean = stripControlKeys(op.data);
|
|
397
|
+
switch (op.kind) {
|
|
398
|
+
case "insert": {
|
|
399
|
+
if (row) {
|
|
400
|
+
if (upsertKeys?.every((k) => valEq(row[k], clean[k]))) {
|
|
401
|
+
const merged = { ...row };
|
|
402
|
+
for (const [k, v] of Object.entries(clean)) {
|
|
403
|
+
if (!upsertKeys.includes(k) && !k.startsWith("_"))
|
|
404
|
+
merged[k] = v;
|
|
405
|
+
}
|
|
406
|
+
return merged;
|
|
407
|
+
}
|
|
408
|
+
return row;
|
|
409
|
+
}
|
|
410
|
+
return rowMatches(clean, where) ? { ...clean } : null;
|
|
411
|
+
}
|
|
412
|
+
case "increment": {
|
|
413
|
+
const deltas = op.set ?? {};
|
|
414
|
+
if (row) {
|
|
415
|
+
if (upsertKeys.every((k) => valEq(row[k], clean[k]))) {
|
|
416
|
+
const merged = { ...row };
|
|
417
|
+
for (const [col, d] of Object.entries(deltas)) {
|
|
418
|
+
merged[col] = toBigIntOr0(merged[col]) + toBigIntOr0(d);
|
|
419
|
+
}
|
|
420
|
+
return merged;
|
|
421
|
+
}
|
|
422
|
+
return row;
|
|
423
|
+
}
|
|
424
|
+
if (!rowMatches(clean, where))
|
|
425
|
+
return null;
|
|
426
|
+
const created = { ...clean };
|
|
427
|
+
for (const [col, d] of Object.entries(deltas)) {
|
|
428
|
+
created[col] = toBigIntOr0(d);
|
|
429
|
+
}
|
|
430
|
+
return created;
|
|
431
|
+
}
|
|
432
|
+
case "update":
|
|
433
|
+
return row && rowMatches(row, op.data) ? { ...row, ...op.set ?? {} } : row;
|
|
434
|
+
case "delete":
|
|
435
|
+
return row && rowMatches(row, op.data) ? null : row;
|
|
436
|
+
}
|
|
191
437
|
}
|
|
192
438
|
async count(table, where) {
|
|
193
439
|
this.validateTable(table);
|
|
@@ -248,6 +494,7 @@ class SubgraphContext {
|
|
|
248
494
|
async flush() {
|
|
249
495
|
if (this.ops.length === 0)
|
|
250
496
|
return { count: 0, writes: [] };
|
|
497
|
+
await this.ensureJournalTable();
|
|
251
498
|
const opsToFlush = [...this.ops];
|
|
252
499
|
this.ops.length = 0;
|
|
253
500
|
const statements = this.buildStatements(opsToFlush);
|
|
@@ -265,12 +512,12 @@ class SubgraphContext {
|
|
|
265
512
|
const writes = opsToFlush.map((op, rowIndex) => {
|
|
266
513
|
const blockHeight = op.data._block_height ?? this.block.height;
|
|
267
514
|
const txId = op.data._tx_id ?? this._tx.txId;
|
|
268
|
-
const baseRow = op.kind === "update" ? { ...op.data, ...op.set ?? {} } : { ...op.data };
|
|
515
|
+
const baseRow = op.kind === "update" || op.kind === "increment" ? { ...op.data, ...op.set ?? {} } : { ...op.data };
|
|
269
516
|
baseRow._upsert_keys = undefined;
|
|
270
517
|
baseRow._upsert_fallback_keys = undefined;
|
|
271
518
|
baseRow._upsert_fallback_set = undefined;
|
|
272
519
|
return {
|
|
273
|
-
op: op.kind,
|
|
520
|
+
op: op.kind === "increment" ? "update" : op.kind,
|
|
274
521
|
table: op.table,
|
|
275
522
|
row: jsonSafe(baseRow),
|
|
276
523
|
pk: { blockHeight, txId, rowIndex }
|
|
@@ -295,6 +542,35 @@ class SubgraphContext {
|
|
|
295
542
|
const batchKey = `${op.table}:${[...cols].sort().join(",")}:${upsertKeys ? [...upsertKeys].sort().join(",") : ""}`;
|
|
296
543
|
return { data, cols, vals, upsertKeys, batchKey };
|
|
297
544
|
}
|
|
545
|
+
async ensureJournalTable() {
|
|
546
|
+
if (!this.journal || journalEnsured.has(this.pgSchemaName))
|
|
547
|
+
return;
|
|
548
|
+
const { rows } = await sql.raw(`SELECT to_regclass('"${this.pgSchemaName}"."_journal"') AS r`).execute(this.db);
|
|
549
|
+
if (rows[0]?.r) {
|
|
550
|
+
journalEnsured.add(this.pgSchemaName);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
for (const stmt of emitJournalDDL(this.pgSchemaName)) {
|
|
554
|
+
await sql.raw(stmt).execute(this.db);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
columnSqlType(table, col) {
|
|
558
|
+
const def = this.subgraphSchema[table]?.columns?.[col];
|
|
559
|
+
return def ? TYPE_MAP[def.type] : undefined;
|
|
560
|
+
}
|
|
561
|
+
journalCaptureSQL(table, keyCols, keyLiteralRows) {
|
|
562
|
+
const cast = (col, expr) => {
|
|
563
|
+
const t = this.columnSqlType(table, col);
|
|
564
|
+
return t ? `CAST(${expr} AS ${t})` : expr;
|
|
565
|
+
};
|
|
566
|
+
const keyObj = keyCols.map((k) => `'${k}', ${cast(k, `v."${k}"`)}`).join(", ");
|
|
567
|
+
const joinCond = keyCols.map((k) => `t."${k}" = ${cast(k, `v."${k}"`)}`).join(" AND ");
|
|
568
|
+
const valuesList = keyLiteralRows.map((r) => `(${r.join(", ")})`).join(", ");
|
|
569
|
+
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}`;
|
|
570
|
+
}
|
|
571
|
+
journalCaptureByWhereSQL(table, clause) {
|
|
572
|
+
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}`;
|
|
573
|
+
}
|
|
298
574
|
buildStatements(ops) {
|
|
299
575
|
const statements = [];
|
|
300
576
|
if (this.byo) {
|
|
@@ -308,6 +584,38 @@ class SubgraphContext {
|
|
|
308
584
|
}
|
|
309
585
|
let currentBatch = null;
|
|
310
586
|
let currentBatchKey = "";
|
|
587
|
+
let incBatch = null;
|
|
588
|
+
let incBatchKey = "";
|
|
589
|
+
const flushIncrementBatch = () => {
|
|
590
|
+
if (!incBatch)
|
|
591
|
+
return;
|
|
592
|
+
const batch = incBatch;
|
|
593
|
+
const qualifiedTable = `"${this.pgSchemaName}"."${batch.table}"`;
|
|
594
|
+
const cols = [
|
|
595
|
+
...batch.keyCols,
|
|
596
|
+
...batch.deltaCols,
|
|
597
|
+
"_block_height",
|
|
598
|
+
"_tx_id",
|
|
599
|
+
"_created_at"
|
|
600
|
+
];
|
|
601
|
+
const valuesList = Array.from(batch.rows.values()).map((r) => {
|
|
602
|
+
const vals = [
|
|
603
|
+
...batch.keyCols.map((k) => escapeLiteral(r.keys[k])),
|
|
604
|
+
...batch.deltaCols.map((c) => String(r.deltas[c] ?? 0n)),
|
|
605
|
+
escapeLiteral(r.meta.blockHeight),
|
|
606
|
+
escapeLiteral(r.meta.txId),
|
|
607
|
+
"NOW()"
|
|
608
|
+
];
|
|
609
|
+
return `(${vals.join(", ")})`;
|
|
610
|
+
}).join(", ");
|
|
611
|
+
const setClauses = batch.deltaCols.map((c) => `"${c}" = COALESCE("${batch.table}"."${c}", 0) + EXCLUDED."${c}"`);
|
|
612
|
+
if (this.journal) {
|
|
613
|
+
statements.push(this.journalCaptureSQL(batch.table, batch.keyCols, Array.from(batch.rows.values()).map((r) => batch.keyCols.map((k) => escapeLiteral(r.keys[k])))));
|
|
614
|
+
}
|
|
615
|
+
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(", ")}`);
|
|
616
|
+
incBatch = null;
|
|
617
|
+
incBatchKey = "";
|
|
618
|
+
};
|
|
311
619
|
const flushInsertBatch = () => {
|
|
312
620
|
if (!currentBatch)
|
|
313
621
|
return;
|
|
@@ -329,6 +637,11 @@ class SubgraphContext {
|
|
|
329
637
|
}
|
|
330
638
|
const valuesList = rows.map((r) => `(${r.join(", ")})`).join(", ");
|
|
331
639
|
let stmt = `INSERT INTO ${qualifiedTable} (${colList}) VALUES ${valuesList}`;
|
|
640
|
+
if (this.journal && batch.upsertKeys && batch.upsertKeys.length > 0) {
|
|
641
|
+
const uKeys = batch.upsertKeys;
|
|
642
|
+
const keyIndices = uKeys.map((k) => batch.cols.indexOf(k));
|
|
643
|
+
statements.push(this.journalCaptureSQL(batch.table, uKeys, rows.map((r) => keyIndices.map((ki) => r[ki]))));
|
|
644
|
+
}
|
|
332
645
|
if (batch.upsertKeys && batch.upsertKeys.length > 0) {
|
|
333
646
|
const batchKeys = batch.upsertKeys;
|
|
334
647
|
const updateCols = batch.cols.filter((c) => !batchKeys.includes(c) && !c.startsWith("_"));
|
|
@@ -346,6 +659,7 @@ class SubgraphContext {
|
|
|
346
659
|
for (const op of ops) {
|
|
347
660
|
const qualifiedTable = `"${this.pgSchemaName}"."${op.table}"`;
|
|
348
661
|
if (op.kind === "insert") {
|
|
662
|
+
flushIncrementBatch();
|
|
349
663
|
const { cols, vals, upsertKeys, batchKey } = this.prepareInsert(op);
|
|
350
664
|
if (batchKey === currentBatchKey && currentBatch) {
|
|
351
665
|
currentBatch.rows.push(vals);
|
|
@@ -354,22 +668,60 @@ class SubgraphContext {
|
|
|
354
668
|
currentBatch = { table: op.table, cols, rows: [vals], upsertKeys };
|
|
355
669
|
currentBatchKey = batchKey;
|
|
356
670
|
}
|
|
671
|
+
} else if (op.kind === "increment") {
|
|
672
|
+
flushInsertBatch();
|
|
673
|
+
const keyCols = [...op.data._upsert_keys].sort();
|
|
674
|
+
const deltaCols = Object.keys(op.set ?? {}).sort();
|
|
675
|
+
const batchKey = `inc:${op.table}:${keyCols.join(",")}:${deltaCols.join(",")}`;
|
|
676
|
+
if (batchKey !== incBatchKey || !incBatch) {
|
|
677
|
+
flushIncrementBatch();
|
|
678
|
+
incBatch = { table: op.table, keyCols, deltaCols, rows: new Map };
|
|
679
|
+
incBatchKey = batchKey;
|
|
680
|
+
}
|
|
681
|
+
const clean = stripControlKeys(op.data);
|
|
682
|
+
const keySig = keyCols.map((k) => escapeLiteral(clean[k])).join("\x00");
|
|
683
|
+
const existing = incBatch.rows.get(keySig);
|
|
684
|
+
if (existing) {
|
|
685
|
+
for (const c of deltaCols) {
|
|
686
|
+
existing.deltas[c] = (existing.deltas[c] ?? 0n) + toBigIntOr0(op.set?.[c]);
|
|
687
|
+
}
|
|
688
|
+
} else {
|
|
689
|
+
const deltas = {};
|
|
690
|
+
for (const c of deltaCols)
|
|
691
|
+
deltas[c] = toBigIntOr0(op.set?.[c]);
|
|
692
|
+
incBatch.rows.set(keySig, {
|
|
693
|
+
keys: clean,
|
|
694
|
+
deltas,
|
|
695
|
+
meta: {
|
|
696
|
+
blockHeight: op.data._block_height ?? this.block.height,
|
|
697
|
+
txId: op.data._tx_id ?? this._tx.txId
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
}
|
|
357
701
|
} else {
|
|
358
702
|
flushInsertBatch();
|
|
703
|
+
flushIncrementBatch();
|
|
359
704
|
if (op.kind === "update") {
|
|
360
705
|
const setEntries = Object.entries(op.set ?? {});
|
|
361
706
|
for (const [k] of setEntries)
|
|
362
707
|
validateColumnName(k);
|
|
363
708
|
const setClauses = setEntries.map(([k, v]) => `"${k}" = ${escapeLiteral(v)}`);
|
|
364
709
|
const { clause } = buildWhereClause(op.data);
|
|
710
|
+
if (this.journal) {
|
|
711
|
+
statements.push(this.journalCaptureByWhereSQL(op.table, clause));
|
|
712
|
+
}
|
|
365
713
|
statements.push(`UPDATE ${qualifiedTable} SET ${setClauses.join(", ")} WHERE ${clause}`);
|
|
366
714
|
} else if (op.kind === "delete") {
|
|
367
715
|
const { clause } = buildWhereClause(op.data);
|
|
716
|
+
if (this.journal) {
|
|
717
|
+
statements.push(this.journalCaptureByWhereSQL(op.table, clause));
|
|
718
|
+
}
|
|
368
719
|
statements.push(`DELETE FROM ${qualifiedTable} WHERE ${clause}`);
|
|
369
720
|
}
|
|
370
721
|
}
|
|
371
722
|
}
|
|
372
723
|
flushInsertBatch();
|
|
724
|
+
flushIncrementBatch();
|
|
373
725
|
return statements;
|
|
374
726
|
}
|
|
375
727
|
validateTable(table) {
|
|
@@ -378,6 +730,36 @@ class SubgraphContext {
|
|
|
378
730
|
}
|
|
379
731
|
}
|
|
380
732
|
}
|
|
733
|
+
function stripControlKeys(data) {
|
|
734
|
+
const {
|
|
735
|
+
_upsert_keys: _a,
|
|
736
|
+
_upsert_fallback_keys: _b,
|
|
737
|
+
_upsert_fallback_set: _c,
|
|
738
|
+
...clean
|
|
739
|
+
} = data;
|
|
740
|
+
return clean;
|
|
741
|
+
}
|
|
742
|
+
function valEq(a, b) {
|
|
743
|
+
if (a === b)
|
|
744
|
+
return true;
|
|
745
|
+
if (a == null || b == null)
|
|
746
|
+
return false;
|
|
747
|
+
return String(a) === String(b);
|
|
748
|
+
}
|
|
749
|
+
function rowMatches(row, where) {
|
|
750
|
+
return Object.entries(where).every(([k, v]) => valEq(row[k], v));
|
|
751
|
+
}
|
|
752
|
+
function toBigIntOr0(v) {
|
|
753
|
+
if (typeof v === "bigint")
|
|
754
|
+
return v;
|
|
755
|
+
if (v == null)
|
|
756
|
+
return 0n;
|
|
757
|
+
try {
|
|
758
|
+
return BigInt(String(v));
|
|
759
|
+
} catch {
|
|
760
|
+
return 0n;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
381
763
|
function jsonSafe(row) {
|
|
382
764
|
const out = {};
|
|
383
765
|
for (const [k, v] of Object.entries(row)) {
|
|
@@ -696,7 +1078,25 @@ async function runHandlers(subgraph, matched, ctx, opts) {
|
|
|
696
1078
|
filterLookup.set(name, filter);
|
|
697
1079
|
}
|
|
698
1080
|
}
|
|
1081
|
+
const units = [];
|
|
699
1082
|
for (const { tx, events, sourceName } of matched) {
|
|
1083
|
+
if (events.length === 0) {
|
|
1084
|
+
units.push({ tx, sourceName, event: null });
|
|
1085
|
+
} else {
|
|
1086
|
+
for (const event of events)
|
|
1087
|
+
units.push({ tx, sourceName, event });
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
units.sort((a, b) => (a.tx.tx_index ?? 0) - (b.tx.tx_index ?? 0) || (a.event?.event_index ?? -1) - (b.event?.event_index ?? -1));
|
|
1091
|
+
for (const { tx, event, sourceName } of units) {
|
|
1092
|
+
if (errors >= threshold) {
|
|
1093
|
+
logger2.error("Subgraph error threshold reached, skipping remaining events", {
|
|
1094
|
+
subgraph: subgraph.name,
|
|
1095
|
+
errors,
|
|
1096
|
+
threshold
|
|
1097
|
+
});
|
|
1098
|
+
return { processed, errors };
|
|
1099
|
+
}
|
|
700
1100
|
const handler = subgraph.handlers[sourceName] ?? subgraph.handlers["*"] ?? null;
|
|
701
1101
|
if (!handler) {
|
|
702
1102
|
logger2.warn("No handler found for source", {
|
|
@@ -715,9 +1115,29 @@ async function runHandlers(subgraph, matched, ctx, opts) {
|
|
|
715
1115
|
functionName: tx.function_name ?? null
|
|
716
1116
|
});
|
|
717
1117
|
const filter = filterLookup.get(sourceName);
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
1118
|
+
const checkpoint = ctx.opsCheckpoint();
|
|
1119
|
+
try {
|
|
1120
|
+
let payload;
|
|
1121
|
+
if (event === null) {
|
|
1122
|
+
payload = filter ? buildEventPayload(filter, tx, null) : {
|
|
1123
|
+
tx: {
|
|
1124
|
+
txId: tx.tx_id,
|
|
1125
|
+
sender: tx.sender,
|
|
1126
|
+
type: tx.type,
|
|
1127
|
+
status: tx.status,
|
|
1128
|
+
contractId: tx.contract_id,
|
|
1129
|
+
functionName: tx.function_name
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
} else if (filter) {
|
|
1133
|
+
payload = buildEventPayload(filter, tx, event);
|
|
1134
|
+
} else {
|
|
1135
|
+
const decoded = decodeEventData(event.data);
|
|
1136
|
+
payload = {
|
|
1137
|
+
...decoded,
|
|
1138
|
+
_eventId: event.id,
|
|
1139
|
+
_eventType: event.type,
|
|
1140
|
+
_eventIndex: event.event_index,
|
|
721
1141
|
tx: {
|
|
722
1142
|
txId: tx.tx_id,
|
|
723
1143
|
sender: tx.sender,
|
|
@@ -727,62 +1147,22 @@ async function runHandlers(subgraph, matched, ctx, opts) {
|
|
|
727
1147
|
functionName: tx.function_name
|
|
728
1148
|
}
|
|
729
1149
|
};
|
|
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
1150
|
}
|
|
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
|
-
});
|
|
1151
|
+
if (event !== null && filter?.type === "print_event" && filter.topic && payload.topic !== filter.topic) {
|
|
1152
|
+
continue;
|
|
785
1153
|
}
|
|
1154
|
+
await handler(payload, ctx);
|
|
1155
|
+
processed++;
|
|
1156
|
+
} catch (err) {
|
|
1157
|
+
ctx.rollbackTo(checkpoint);
|
|
1158
|
+
errors++;
|
|
1159
|
+
logger2.error("Subgraph handler error", {
|
|
1160
|
+
subgraph: subgraph.name,
|
|
1161
|
+
sourceName,
|
|
1162
|
+
txId: tx.tx_id,
|
|
1163
|
+
...event !== null ? { eventId: event.id, eventType: event.type } : {},
|
|
1164
|
+
error: getErrorMessage(err)
|
|
1165
|
+
});
|
|
786
1166
|
}
|
|
787
1167
|
}
|
|
788
1168
|
return { processed, errors };
|
|
@@ -1030,6 +1410,7 @@ function matchSources(sources, transactions, events, traitContracts = new Map) {
|
|
|
1030
1410
|
// src/runtime/block-processor.ts
|
|
1031
1411
|
import { getTargetDb } from "@secondlayer/shared/db";
|
|
1032
1412
|
import { resolveTraitContractIds } from "@secondlayer/shared/db/queries/contracts";
|
|
1413
|
+
import { advanceOperationCursor } from "@secondlayer/shared/db/queries/subgraph-operations";
|
|
1033
1414
|
import {
|
|
1034
1415
|
isByoSubgraph,
|
|
1035
1416
|
recordSubgraphProcessed,
|
|
@@ -1039,9 +1420,6 @@ import {
|
|
|
1039
1420
|
import { logger as logger5 } from "@secondlayer/shared/logger";
|
|
1040
1421
|
import { sql as sql3 } from "kysely";
|
|
1041
1422
|
|
|
1042
|
-
// src/schema/utils.ts
|
|
1043
|
-
import { pgSchemaName } from "@secondlayer/shared/db/queries/subgraphs";
|
|
1044
|
-
|
|
1045
1423
|
// src/runtime/block-source.ts
|
|
1046
1424
|
import { getSourceDb } from "@secondlayer/shared/db";
|
|
1047
1425
|
import { IndexHttpClient } from "@secondlayer/shared/index-http";
|
|
@@ -1387,7 +1765,7 @@ function resolveBlockSource(subgraph) {
|
|
|
1387
1765
|
}
|
|
1388
1766
|
|
|
1389
1767
|
// src/runtime/outbox-emit.ts
|
|
1390
|
-
import { createHash } from "node:crypto";
|
|
1768
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1391
1769
|
import { logger as logger4 } from "@secondlayer/shared/logger";
|
|
1392
1770
|
var loggedKillSwitch = false;
|
|
1393
1771
|
var OP_VERB = {
|
|
@@ -1400,7 +1778,7 @@ function isEmitOutboxEnabled() {
|
|
|
1400
1778
|
}
|
|
1401
1779
|
function dedupKey(subgraphName, tableName, blockHeight, txId, rowIndex, row) {
|
|
1402
1780
|
const canonical = `${subgraphName}:${tableName}:${blockHeight}:${txId}:${rowIndex}:${stableStringify(row)}`;
|
|
1403
|
-
return
|
|
1781
|
+
return createHash2("sha256").update(canonical).digest("hex").slice(0, 32);
|
|
1404
1782
|
}
|
|
1405
1783
|
function stableStringify(obj) {
|
|
1406
1784
|
const keys = Object.keys(obj).sort();
|
|
@@ -1652,6 +2030,47 @@ async function resolveTraitContracts(subgraph, blockHeight, db) {
|
|
|
1652
2030
|
}
|
|
1653
2031
|
return resolved;
|
|
1654
2032
|
}
|
|
2033
|
+
|
|
2034
|
+
class CursorRaceLostError extends Error {
|
|
2035
|
+
constructor(operationId, height) {
|
|
2036
|
+
super(`op ${operationId} lost cursor race at block ${height}`);
|
|
2037
|
+
this.name = "CursorRaceLostError";
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
function opCursorMode(opts) {
|
|
2041
|
+
const ap = opts?.atomicProgress;
|
|
2042
|
+
return ap && "operationId" in ap ? ap : undefined;
|
|
2043
|
+
}
|
|
2044
|
+
function statusMode(opts) {
|
|
2045
|
+
const ap = opts?.atomicProgress;
|
|
2046
|
+
return ap && "status" in ap ? ap : undefined;
|
|
2047
|
+
}
|
|
2048
|
+
var BLOCK_RETRY_DELAYS_MS = [500, 2000, 5000];
|
|
2049
|
+
function journalEnabled(opts) {
|
|
2050
|
+
return !opts?.skipProgressUpdate;
|
|
2051
|
+
}
|
|
2052
|
+
async function processBlockWithRetry(subgraph, subgraphName, blockHeight, opts, retryDelaysMs = BLOCK_RETRY_DELAYS_MS) {
|
|
2053
|
+
let lastError;
|
|
2054
|
+
for (let attempt = 0;attempt <= retryDelaysMs.length; attempt++) {
|
|
2055
|
+
try {
|
|
2056
|
+
return await processBlock(subgraph, subgraphName, blockHeight, opts);
|
|
2057
|
+
} catch (err) {
|
|
2058
|
+
lastError = err;
|
|
2059
|
+
const delay = retryDelaysMs[attempt];
|
|
2060
|
+
if (delay === undefined)
|
|
2061
|
+
break;
|
|
2062
|
+
logger5.warn("Block processing failed, retrying", {
|
|
2063
|
+
subgraph: subgraphName,
|
|
2064
|
+
blockHeight,
|
|
2065
|
+
attempt: attempt + 1,
|
|
2066
|
+
retryInMs: delay,
|
|
2067
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2068
|
+
});
|
|
2069
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
throw lastError;
|
|
2073
|
+
}
|
|
1655
2074
|
async function processBlock(subgraph, subgraphName, blockHeight, opts) {
|
|
1656
2075
|
const targetDb = getTargetDb();
|
|
1657
2076
|
const blockStart = performance.now();
|
|
@@ -1719,10 +2138,24 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
|
|
|
1719
2138
|
}
|
|
1720
2139
|
};
|
|
1721
2140
|
if (route.byo) {
|
|
2141
|
+
if (statusMode(opts)) {
|
|
2142
|
+
const row = await targetDb.selectFrom("subgraphs").select("last_processed_block").where("name", "=", subgraphName).executeTakeFirst();
|
|
2143
|
+
if (row && Number(row.last_processed_block) >= blockHeight) {
|
|
2144
|
+
result.skipped = true;
|
|
2145
|
+
return result;
|
|
2146
|
+
}
|
|
2147
|
+
} else if (opCursorMode(opts)) {
|
|
2148
|
+
const om = opCursorMode(opts);
|
|
2149
|
+
const row = await targetDb.selectFrom("subgraph_operations").select("cursor_block").where("id", "=", om.operationId).executeTakeFirst();
|
|
2150
|
+
if (row?.cursor_block != null && Number(row.cursor_block) >= blockHeight) {
|
|
2151
|
+
result.skipped = true;
|
|
2152
|
+
return result;
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
1722
2155
|
let runResult = { processed: 0, errors: 0 };
|
|
1723
2156
|
let manifest;
|
|
1724
2157
|
await route.dataDb.transaction().execute(async (tx) => {
|
|
1725
|
-
const ctx = new SubgraphContext(tx, schemaName, subgraph.schema, blockMeta, initialTx, true);
|
|
2158
|
+
const ctx = new SubgraphContext(tx, schemaName, subgraph.schema, blockMeta, initialTx, true, journalEnabled(opts));
|
|
1726
2159
|
const handlerStart = performance.now();
|
|
1727
2160
|
runResult = await runHandlers(subgraph, matched, ctx);
|
|
1728
2161
|
handlerMs = performance.now() - handlerStart;
|
|
@@ -1738,26 +2171,71 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
|
|
|
1738
2171
|
if (manifest && manifest.count > 0) {
|
|
1739
2172
|
await emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, block.height);
|
|
1740
2173
|
}
|
|
2174
|
+
const byoSm = statusMode(opts);
|
|
2175
|
+
const byoOm = opCursorMode(opts);
|
|
2176
|
+
if (byoSm && manifest && manifest.count > 0) {
|
|
2177
|
+
await updateSubgraphStatus(tx, subgraphName, byoSm.status, blockHeight);
|
|
2178
|
+
} else if (byoOm && manifest && manifest.count > 0) {
|
|
2179
|
+
await advanceOperationCursor(tx, byoOm.operationId, blockHeight);
|
|
2180
|
+
}
|
|
1741
2181
|
await applyProgress(tx, runResult);
|
|
1742
2182
|
});
|
|
1743
2183
|
} else {
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
2184
|
+
try {
|
|
2185
|
+
await targetDb.transaction().execute(async (tx) => {
|
|
2186
|
+
const opMode = opCursorMode(opts);
|
|
2187
|
+
if (statusMode(opts)) {
|
|
2188
|
+
const row = await tx.selectFrom("subgraphs").select("last_processed_block").where("name", "=", subgraphName).executeTakeFirst();
|
|
2189
|
+
if (row && Number(row.last_processed_block) >= blockHeight) {
|
|
2190
|
+
result.skipped = true;
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
} else if (opMode) {
|
|
2194
|
+
const row = await tx.selectFrom("subgraph_operations").select("cursor_block").where("id", "=", opMode.operationId).executeTakeFirst();
|
|
2195
|
+
if (row?.cursor_block != null && Number(row.cursor_block) >= blockHeight) {
|
|
2196
|
+
result.skipped = true;
|
|
2197
|
+
return;
|
|
2198
|
+
}
|
|
1756
2199
|
}
|
|
1757
|
-
|
|
2200
|
+
const ctx = new SubgraphContext(tx, schemaName, subgraph.schema, blockMeta, initialTx, false, journalEnabled(opts));
|
|
2201
|
+
const handlerStart = performance.now();
|
|
2202
|
+
const runResult = await runHandlers(subgraph, matched, ctx);
|
|
2203
|
+
handlerMs = performance.now() - handlerStart;
|
|
2204
|
+
result.processed = runResult.processed;
|
|
2205
|
+
result.errors = runResult.errors;
|
|
2206
|
+
let flushedWrites = false;
|
|
2207
|
+
if (ctx.pendingOps > 0) {
|
|
2208
|
+
const flushStart = performance.now();
|
|
2209
|
+
const manifest = await ctx.flush();
|
|
2210
|
+
flushedWrites = manifest.count > 0;
|
|
2211
|
+
if (manifest.count > 0) {
|
|
2212
|
+
await emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, block.height);
|
|
2213
|
+
}
|
|
2214
|
+
flushMs = performance.now() - flushStart;
|
|
2215
|
+
}
|
|
2216
|
+
const sm = statusMode(opts);
|
|
2217
|
+
if (sm && flushedWrites) {
|
|
2218
|
+
await updateSubgraphStatus(tx, subgraphName, sm.status, blockHeight);
|
|
2219
|
+
} else if (opMode && flushedWrites) {
|
|
2220
|
+
const advanced = await advanceOperationCursor(tx, opMode.operationId, blockHeight);
|
|
2221
|
+
if (!advanced) {
|
|
2222
|
+
throw new CursorRaceLostError(opMode.operationId, blockHeight);
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
await applyProgress(tx, runResult);
|
|
2226
|
+
});
|
|
2227
|
+
} catch (err) {
|
|
2228
|
+
if (err instanceof CursorRaceLostError) {
|
|
2229
|
+
logger5.warn("cursor race lost — block already covered", {
|
|
2230
|
+
subgraph: subgraphName,
|
|
2231
|
+
blockHeight,
|
|
2232
|
+
error: err.message
|
|
2233
|
+
});
|
|
2234
|
+
result.skipped = true;
|
|
2235
|
+
return result;
|
|
1758
2236
|
}
|
|
1759
|
-
|
|
1760
|
-
}
|
|
2237
|
+
throw err;
|
|
2238
|
+
}
|
|
1761
2239
|
}
|
|
1762
2240
|
const totalMs = performance.now() - blockStart;
|
|
1763
2241
|
result.timing = {
|
|
@@ -1785,6 +2263,9 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
|
|
|
1785
2263
|
error: err instanceof Error ? err.message : String(err)
|
|
1786
2264
|
});
|
|
1787
2265
|
}
|
|
2266
|
+
if (journalEnabled(opts)) {
|
|
2267
|
+
await sql3.raw(`DELETE FROM "${schemaName}"."_journal" WHERE "block_height" < ${blockHeight - JOURNAL_RETENTION_BLOCKS}`).execute(route.dataDb).catch(() => {});
|
|
2268
|
+
}
|
|
1788
2269
|
}
|
|
1789
2270
|
return result;
|
|
1790
2271
|
}
|
|
@@ -1859,119 +2340,19 @@ import {
|
|
|
1859
2340
|
recordGapBatch,
|
|
1860
2341
|
resolveGaps
|
|
1861
2342
|
} from "@secondlayer/shared/db/queries/subgraph-gaps";
|
|
1862
|
-
import {
|
|
2343
|
+
import {
|
|
2344
|
+
advanceOperationCursor as advanceOperationCursor2,
|
|
2345
|
+
updateOperationProcessedEvents
|
|
2346
|
+
} from "@secondlayer/shared/db/queries/subgraph-operations";
|
|
1863
2347
|
import {
|
|
1864
2348
|
recordSubgraphProcessed as recordSubgraphProcessed2,
|
|
1865
2349
|
updateSubgraphStatus as updateSubgraphStatus2
|
|
1866
2350
|
} from "@secondlayer/shared/db/queries/subgraphs";
|
|
1867
2351
|
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
2352
|
var LOG_INTERVAL = 1000;
|
|
1973
2353
|
var HEALTH_FLUSH_INTERVAL = 1000;
|
|
1974
2354
|
var PROGRESS_FLUSH_INTERVAL_MS = 5000;
|
|
2355
|
+
var EMPTY_BATCH_HALT_THRESHOLD = 3;
|
|
1975
2356
|
var STANDARD_REINDEX_BATCH_CONFIG = {
|
|
1976
2357
|
defaultBatchSize: 500,
|
|
1977
2358
|
minBatchSize: 100,
|
|
@@ -2035,6 +2416,7 @@ async function processBlockRange(def, opts) {
|
|
|
2035
2416
|
const totalBlocks = toBlock - fromBlock + 1;
|
|
2036
2417
|
const stats = new StatsAccumulator(subgraphName, opts.isCatchup);
|
|
2037
2418
|
let blocksProcessed = 0;
|
|
2419
|
+
let blocksSkippedByCursor = 0;
|
|
2038
2420
|
let totalEventsProcessed = 0;
|
|
2039
2421
|
let totalErrors = 0;
|
|
2040
2422
|
let pendingEventsProcessed = 0;
|
|
@@ -2047,6 +2429,7 @@ async function processBlockRange(def, opts) {
|
|
|
2047
2429
|
let batchSize = batchConfig.defaultBatchSize;
|
|
2048
2430
|
let currentHeight = fromBlock;
|
|
2049
2431
|
let aborted = false;
|
|
2432
|
+
let consecutiveEmptyBatches = 0;
|
|
2050
2433
|
const sparse = Boolean(source.nextDataHeight && canSparseScan(def));
|
|
2051
2434
|
const flushHealth = async () => {
|
|
2052
2435
|
if (pendingEventsProcessed === 0 && pendingErrors === 0)
|
|
@@ -2058,6 +2441,13 @@ async function processBlockRange(def, opts) {
|
|
|
2058
2441
|
lastHealthFlushBlock = blocksProcessed;
|
|
2059
2442
|
lastHealthFlushAt = Date.now();
|
|
2060
2443
|
};
|
|
2444
|
+
const haltRange = async (errorMsg, height) => {
|
|
2445
|
+
pendingErrors++;
|
|
2446
|
+
pendingLastError = errorMsg;
|
|
2447
|
+
await flushHealth().catch(() => {});
|
|
2448
|
+
await updateSubgraphStatus2(targetDb, subgraphName, "error").catch(() => {});
|
|
2449
|
+
throw new Error(`${subgraphName}: halted at block ${height}: ${errorMsg}`);
|
|
2450
|
+
};
|
|
2061
2451
|
let nextBatchEnd = Math.min(currentHeight + batchSize - 1, toBlock);
|
|
2062
2452
|
let nextBatchPromise = source.loadBlockRange(currentHeight, nextBatchEnd);
|
|
2063
2453
|
while (currentHeight <= toBlock) {
|
|
@@ -2072,6 +2462,14 @@ async function processBlockRange(def, opts) {
|
|
|
2072
2462
|
}
|
|
2073
2463
|
const batch = await nextBatchPromise;
|
|
2074
2464
|
const batchEnd = nextBatchEnd;
|
|
2465
|
+
if (batch.size === 0 && batchEnd >= currentHeight) {
|
|
2466
|
+
consecutiveEmptyBatches++;
|
|
2467
|
+
if (consecutiveEmptyBatches >= EMPTY_BATCH_HALT_THRESHOLD) {
|
|
2468
|
+
await haltRange(`block source returned ${consecutiveEmptyBatches} consecutive empty batches (ending ${currentHeight}..${batchEnd}) — source degraded`, currentHeight);
|
|
2469
|
+
}
|
|
2470
|
+
} else {
|
|
2471
|
+
consecutiveEmptyBatches = 0;
|
|
2472
|
+
}
|
|
2075
2473
|
const nextStart = batchEnd + 1;
|
|
2076
2474
|
if (nextStart <= toBlock) {
|
|
2077
2475
|
nextBatchEnd = Math.min(nextStart + batchSize - 1, toBlock);
|
|
@@ -2079,28 +2477,40 @@ async function processBlockRange(def, opts) {
|
|
|
2079
2477
|
}
|
|
2080
2478
|
const batchFailedBlocks = [];
|
|
2081
2479
|
let batchMatched = 0;
|
|
2480
|
+
const opCursor = status === "active" && opts.operationId ? { operationId: opts.operationId } : undefined;
|
|
2481
|
+
const atomicProgress = status === "reindexing" ? { status } : opCursor;
|
|
2082
2482
|
for (let height = currentHeight;height <= batchEnd; height++) {
|
|
2083
|
-
|
|
2483
|
+
let blockData = batch.get(height);
|
|
2084
2484
|
if (!blockData) {
|
|
2485
|
+
blockData = (await source.loadBlockRange(height, height)).get(height);
|
|
2486
|
+
}
|
|
2487
|
+
if (!blockData) {
|
|
2488
|
+
if (status === "reindexing") {
|
|
2489
|
+
const errorMsg = `block ${height} missing from source — halting reindex (cursor stays at ${height - 1})`;
|
|
2490
|
+
await haltRange(errorMsg, height);
|
|
2491
|
+
}
|
|
2085
2492
|
batchFailedBlocks.push({ height, reason: "block_missing" });
|
|
2086
2493
|
blocksProcessed++;
|
|
2087
2494
|
continue;
|
|
2088
2495
|
}
|
|
2089
2496
|
let result;
|
|
2090
2497
|
try {
|
|
2091
|
-
result = await
|
|
2498
|
+
result = await processBlockWithRetry(def, subgraphName, height, {
|
|
2092
2499
|
skipProgressUpdate: true,
|
|
2500
|
+
atomicProgress,
|
|
2093
2501
|
preloaded: blockData
|
|
2094
2502
|
});
|
|
2095
2503
|
} catch (err) {
|
|
2096
|
-
const errorMsg =
|
|
2097
|
-
logger6.error("Block processing
|
|
2504
|
+
const errorMsg = getErrorMessage2(err);
|
|
2505
|
+
logger6.error("Block processing failed persistently", {
|
|
2098
2506
|
subgraph: subgraphName,
|
|
2099
2507
|
blockHeight: height,
|
|
2100
2508
|
error: errorMsg
|
|
2101
2509
|
});
|
|
2510
|
+
if (status === "reindexing") {
|
|
2511
|
+
await haltRange(`block ${height} failed persistently: ${errorMsg}`, height);
|
|
2512
|
+
}
|
|
2102
2513
|
batchFailedBlocks.push({ height, reason: "processing_error" });
|
|
2103
|
-
await updateSubgraphStatus2(targetDb, subgraphName, status, height).catch(() => {});
|
|
2104
2514
|
blocksProcessed++;
|
|
2105
2515
|
totalErrors++;
|
|
2106
2516
|
pendingErrors++;
|
|
@@ -2108,6 +2518,8 @@ async function processBlockRange(def, opts) {
|
|
|
2108
2518
|
continue;
|
|
2109
2519
|
}
|
|
2110
2520
|
blocksProcessed++;
|
|
2521
|
+
if (result.skipped)
|
|
2522
|
+
blocksSkippedByCursor++;
|
|
2111
2523
|
batchMatched += result.matched;
|
|
2112
2524
|
totalEventsProcessed += result.processed;
|
|
2113
2525
|
totalErrors += result.errors;
|
|
@@ -2125,7 +2537,11 @@ async function processBlockRange(def, opts) {
|
|
|
2125
2537
|
const now = Date.now();
|
|
2126
2538
|
const shouldFlushProgress = blocksProcessed % 100 === 0 || now - lastProgressFlushAt >= PROGRESS_FLUSH_INTERVAL_MS;
|
|
2127
2539
|
if (shouldFlushProgress) {
|
|
2128
|
-
|
|
2540
|
+
if (opCursor) {
|
|
2541
|
+
await advanceOperationCursor2(targetDb, opCursor.operationId, height);
|
|
2542
|
+
} else {
|
|
2543
|
+
await updateSubgraphStatus2(targetDb, subgraphName, status, height);
|
|
2544
|
+
}
|
|
2129
2545
|
if (opts.operationId) {
|
|
2130
2546
|
await updateOperationProcessedEvents(targetDb, opts.operationId, totalEventsProcessed).catch(() => {});
|
|
2131
2547
|
}
|
|
@@ -2137,7 +2553,8 @@ async function processBlockRange(def, opts) {
|
|
|
2137
2553
|
processed: blocksProcessed,
|
|
2138
2554
|
total: totalBlocks,
|
|
2139
2555
|
currentBlock: height,
|
|
2140
|
-
pct: Math.round(blocksProcessed / totalBlocks * 100)
|
|
2556
|
+
pct: Math.round(blocksProcessed / totalBlocks * 100),
|
|
2557
|
+
...blocksSkippedByCursor > 0 ? { skippedByCursor: blocksSkippedByCursor } : {}
|
|
2141
2558
|
});
|
|
2142
2559
|
}
|
|
2143
2560
|
}
|
|
@@ -2163,7 +2580,11 @@ async function processBlockRange(def, opts) {
|
|
|
2163
2580
|
if (jumpTo > batchEnd + 1) {
|
|
2164
2581
|
const skipped = Math.min(jumpTo, toBlock + 1) - (batchEnd + 1);
|
|
2165
2582
|
blocksProcessed += skipped;
|
|
2166
|
-
|
|
2583
|
+
if (opCursor) {
|
|
2584
|
+
await advanceOperationCursor2(targetDb, opCursor.operationId, jumpTo - 1);
|
|
2585
|
+
} else {
|
|
2586
|
+
await updateSubgraphStatus2(targetDb, subgraphName, status, jumpTo - 1);
|
|
2587
|
+
}
|
|
2167
2588
|
logger6.info("Sparse skip", {
|
|
2168
2589
|
subgraph: subgraphName,
|
|
2169
2590
|
from: batchEnd + 1,
|
|
@@ -2405,6 +2826,336 @@ async function backfillSubgraph(def, opts) {
|
|
|
2405
2826
|
function defineSubgraph(def) {
|
|
2406
2827
|
return def;
|
|
2407
2828
|
}
|
|
2829
|
+
// src/print-schema.ts
|
|
2830
|
+
import {
|
|
2831
|
+
deserializeCV as deserializeCV3
|
|
2832
|
+
} from "@secondlayer/stacks/clarity";
|
|
2833
|
+
function camelizeDataKey(str) {
|
|
2834
|
+
return str.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
2835
|
+
}
|
|
2836
|
+
function cvToTree(cv) {
|
|
2837
|
+
switch (cv.type) {
|
|
2838
|
+
case "uint":
|
|
2839
|
+
case "int":
|
|
2840
|
+
return { kind: cv.type };
|
|
2841
|
+
case "true":
|
|
2842
|
+
case "false":
|
|
2843
|
+
return { kind: "bool" };
|
|
2844
|
+
case "address":
|
|
2845
|
+
case "contract":
|
|
2846
|
+
return { kind: "principal" };
|
|
2847
|
+
case "buffer":
|
|
2848
|
+
return { kind: "buffer", len: cv.value.length / 2 };
|
|
2849
|
+
case "ascii":
|
|
2850
|
+
return { kind: "ascii", len: cv.value.length };
|
|
2851
|
+
case "utf8":
|
|
2852
|
+
return { kind: "utf8", len: new TextEncoder().encode(cv.value).length };
|
|
2853
|
+
case "none":
|
|
2854
|
+
return { kind: "optional", inner: null };
|
|
2855
|
+
case "some":
|
|
2856
|
+
return { kind: "optional", inner: cvToTree(cv.value) };
|
|
2857
|
+
case "ok":
|
|
2858
|
+
return { kind: "response", ok: cvToTree(cv.value), err: null };
|
|
2859
|
+
case "err":
|
|
2860
|
+
return { kind: "response", ok: null, err: cvToTree(cv.value) };
|
|
2861
|
+
case "list": {
|
|
2862
|
+
let inner = null;
|
|
2863
|
+
for (const el of cv.value) {
|
|
2864
|
+
const t = cvToTree(el);
|
|
2865
|
+
inner = inner ? unify(inner, t) : t;
|
|
2866
|
+
}
|
|
2867
|
+
return { kind: "list", inner };
|
|
2868
|
+
}
|
|
2869
|
+
case "tuple": {
|
|
2870
|
+
const entries = new Map;
|
|
2871
|
+
for (const [k, v] of Object.entries(cv.value)) {
|
|
2872
|
+
entries.set(k, { tree: cvToTree(v), present: 1 });
|
|
2873
|
+
}
|
|
2874
|
+
return { kind: "tuple", count: 1, entries };
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
function unifyNullable(a, b) {
|
|
2879
|
+
if (!a)
|
|
2880
|
+
return b;
|
|
2881
|
+
if (!b)
|
|
2882
|
+
return a;
|
|
2883
|
+
return unify(a, b);
|
|
2884
|
+
}
|
|
2885
|
+
var UNION_KIND_ORDER = {
|
|
2886
|
+
uint: 0,
|
|
2887
|
+
int: 1,
|
|
2888
|
+
bool: 2,
|
|
2889
|
+
principal: 3,
|
|
2890
|
+
buffer: 4,
|
|
2891
|
+
ascii: 5,
|
|
2892
|
+
utf8: 6,
|
|
2893
|
+
list: 7,
|
|
2894
|
+
tuple: 8,
|
|
2895
|
+
response: 9
|
|
2896
|
+
};
|
|
2897
|
+
function makeUnion(members) {
|
|
2898
|
+
const sorted = [...members].sort((a, b) => (UNION_KIND_ORDER[a.kind] ?? 99) - (UNION_KIND_ORDER[b.kind] ?? 99));
|
|
2899
|
+
return { kind: "union", members: sorted };
|
|
2900
|
+
}
|
|
2901
|
+
function unify(a, b) {
|
|
2902
|
+
if (a.kind === "optional" || b.kind === "optional") {
|
|
2903
|
+
const ai = a.kind === "optional" ? a.inner : a;
|
|
2904
|
+
const bi = b.kind === "optional" ? b.inner : b;
|
|
2905
|
+
return { kind: "optional", inner: unifyNullable(ai, bi) };
|
|
2906
|
+
}
|
|
2907
|
+
if (a.kind === "union")
|
|
2908
|
+
return unionAdd(a.members, b);
|
|
2909
|
+
if (b.kind === "union")
|
|
2910
|
+
return unionAdd(b.members, a);
|
|
2911
|
+
if (a.kind !== b.kind)
|
|
2912
|
+
return makeUnion([a, b]);
|
|
2913
|
+
switch (a.kind) {
|
|
2914
|
+
case "uint":
|
|
2915
|
+
case "int":
|
|
2916
|
+
case "bool":
|
|
2917
|
+
case "principal":
|
|
2918
|
+
return a;
|
|
2919
|
+
case "buffer":
|
|
2920
|
+
case "ascii":
|
|
2921
|
+
case "utf8":
|
|
2922
|
+
return { kind: a.kind, len: Math.max(a.len, b.len) };
|
|
2923
|
+
case "list":
|
|
2924
|
+
return {
|
|
2925
|
+
kind: "list",
|
|
2926
|
+
inner: unifyNullable(a.inner, b.inner)
|
|
2927
|
+
};
|
|
2928
|
+
case "response": {
|
|
2929
|
+
const rb = b;
|
|
2930
|
+
return {
|
|
2931
|
+
kind: "response",
|
|
2932
|
+
ok: unifyNullable(a.ok, rb.ok),
|
|
2933
|
+
err: unifyNullable(a.err, rb.err)
|
|
2934
|
+
};
|
|
2935
|
+
}
|
|
2936
|
+
case "tuple": {
|
|
2937
|
+
const tb = b;
|
|
2938
|
+
const entries = new Map([...a.entries].map(([k, e]) => [k, { ...e }]));
|
|
2939
|
+
for (const [k, e] of tb.entries) {
|
|
2940
|
+
const existing = entries.get(k);
|
|
2941
|
+
entries.set(k, existing ? {
|
|
2942
|
+
tree: unify(existing.tree, e.tree),
|
|
2943
|
+
present: existing.present + e.present
|
|
2944
|
+
} : { ...e });
|
|
2945
|
+
}
|
|
2946
|
+
return { kind: "tuple", count: a.count + tb.count, entries };
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
function unionAdd(members, t) {
|
|
2951
|
+
if (t.kind === "union") {
|
|
2952
|
+
let acc = { kind: "union", members };
|
|
2953
|
+
for (const m of t.members)
|
|
2954
|
+
acc = unify(acc, m);
|
|
2955
|
+
return acc;
|
|
2956
|
+
}
|
|
2957
|
+
const next = [...members];
|
|
2958
|
+
for (let i = 0;i < next.length; i++) {
|
|
2959
|
+
const member = next[i];
|
|
2960
|
+
if (!member)
|
|
2961
|
+
continue;
|
|
2962
|
+
const merged = unify(member, t);
|
|
2963
|
+
if (merged.kind !== "union") {
|
|
2964
|
+
next[i] = merged;
|
|
2965
|
+
return makeUnion(next);
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
next.push(t);
|
|
2969
|
+
return makeUnion(next);
|
|
2970
|
+
}
|
|
2971
|
+
function wrapOptional(t) {
|
|
2972
|
+
return t.kind === "optional" ? t : { kind: "optional", inner: t };
|
|
2973
|
+
}
|
|
2974
|
+
function renderClarity(t) {
|
|
2975
|
+
if (!t)
|
|
2976
|
+
return "?";
|
|
2977
|
+
switch (t.kind) {
|
|
2978
|
+
case "uint":
|
|
2979
|
+
case "int":
|
|
2980
|
+
case "bool":
|
|
2981
|
+
case "principal":
|
|
2982
|
+
return t.kind;
|
|
2983
|
+
case "buffer":
|
|
2984
|
+
return `(buff ${t.len})`;
|
|
2985
|
+
case "ascii":
|
|
2986
|
+
return `(string-ascii ${t.len})`;
|
|
2987
|
+
case "utf8":
|
|
2988
|
+
return `(string-utf8 ${t.len})`;
|
|
2989
|
+
case "optional":
|
|
2990
|
+
return `(optional ${renderClarity(t.inner)})`;
|
|
2991
|
+
case "list":
|
|
2992
|
+
return `(list ${renderClarity(t.inner)})`;
|
|
2993
|
+
case "response":
|
|
2994
|
+
return `(response ${renderClarity(t.ok)} ${renderClarity(t.err)})`;
|
|
2995
|
+
case "tuple": {
|
|
2996
|
+
const parts = [...t.entries].map(([k, e]) => {
|
|
2997
|
+
const tree = e.present < t.count ? wrapOptional(e.tree) : e.tree;
|
|
2998
|
+
return `(${k} ${renderClarity(tree)})`;
|
|
2999
|
+
});
|
|
3000
|
+
return `(tuple ${parts.join(" ")})`;
|
|
3001
|
+
}
|
|
3002
|
+
case "union":
|
|
3003
|
+
return t.members.map(renderClarity).join(" | ");
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
function renderTs(t) {
|
|
3007
|
+
if (!t)
|
|
3008
|
+
return "unknown";
|
|
3009
|
+
switch (t.kind) {
|
|
3010
|
+
case "uint":
|
|
3011
|
+
case "int":
|
|
3012
|
+
return "bigint";
|
|
3013
|
+
case "bool":
|
|
3014
|
+
return "boolean";
|
|
3015
|
+
case "principal":
|
|
3016
|
+
case "buffer":
|
|
3017
|
+
case "ascii":
|
|
3018
|
+
case "utf8":
|
|
3019
|
+
return "string";
|
|
3020
|
+
case "optional":
|
|
3021
|
+
return t.inner ? `${renderTs(t.inner)} | null` : "unknown | null";
|
|
3022
|
+
case "list": {
|
|
3023
|
+
const inner = renderTs(t.inner);
|
|
3024
|
+
return inner.includes(" | ") ? `(${inner})[]` : `${inner}[]`;
|
|
3025
|
+
}
|
|
3026
|
+
case "response": {
|
|
3027
|
+
const sides = [...new Set([renderTs(t.ok), renderTs(t.err)])];
|
|
3028
|
+
return sides.join(" | ");
|
|
3029
|
+
}
|
|
3030
|
+
case "tuple": {
|
|
3031
|
+
const parts = [...t.entries].map(([k, e]) => {
|
|
3032
|
+
const opt = e.present < t.count ? "?" : "";
|
|
3033
|
+
return `${camelizeDataKey(k)}${opt}: ${renderTs(e.tree)}`;
|
|
3034
|
+
});
|
|
3035
|
+
return `{ ${parts.join("; ")} }`;
|
|
3036
|
+
}
|
|
3037
|
+
case "union":
|
|
3038
|
+
return [...new Set(t.members.map((m) => renderTs(m)))].join(" | ");
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
function toColumnType(t) {
|
|
3042
|
+
if (!t)
|
|
3043
|
+
return "jsonb";
|
|
3044
|
+
switch (t.kind) {
|
|
3045
|
+
case "uint":
|
|
3046
|
+
return "uint";
|
|
3047
|
+
case "int":
|
|
3048
|
+
return "int";
|
|
3049
|
+
case "bool":
|
|
3050
|
+
return "boolean";
|
|
3051
|
+
case "principal":
|
|
3052
|
+
return "principal";
|
|
3053
|
+
case "buffer":
|
|
3054
|
+
case "ascii":
|
|
3055
|
+
case "utf8":
|
|
3056
|
+
return "text";
|
|
3057
|
+
case "list":
|
|
3058
|
+
case "tuple":
|
|
3059
|
+
return "jsonb";
|
|
3060
|
+
case "optional":
|
|
3061
|
+
return toColumnType(t.inner);
|
|
3062
|
+
case "response":
|
|
3063
|
+
return t.ok ? toColumnType(t.ok) : "jsonb";
|
|
3064
|
+
case "union":
|
|
3065
|
+
return "jsonb";
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
var MAX_DECODED_PER_TOPIC_NEWEST = 75;
|
|
3069
|
+
var MAX_DECODED_PER_TOPIC_OLDEST = 25;
|
|
3070
|
+
function inferPrintTopics(samples) {
|
|
3071
|
+
const groups = new Map;
|
|
3072
|
+
for (const s of samples) {
|
|
3073
|
+
const group = groups.get(s.topic);
|
|
3074
|
+
if (group)
|
|
3075
|
+
group.push(s);
|
|
3076
|
+
else
|
|
3077
|
+
groups.set(s.topic, [s]);
|
|
3078
|
+
}
|
|
3079
|
+
const out = [];
|
|
3080
|
+
for (const [topic, rows] of groups) {
|
|
3081
|
+
let first = Number.POSITIVE_INFINITY;
|
|
3082
|
+
let last = Number.NEGATIVE_INFINITY;
|
|
3083
|
+
for (const r of rows) {
|
|
3084
|
+
if (r.blockHeight < first)
|
|
3085
|
+
first = r.blockHeight;
|
|
3086
|
+
if (r.blockHeight > last)
|
|
3087
|
+
last = r.blockHeight;
|
|
3088
|
+
}
|
|
3089
|
+
const withHex = [...rows].filter((r) => r.rawHex !== null).sort((a, b) => b.blockHeight - a.blockHeight);
|
|
3090
|
+
const budget = MAX_DECODED_PER_TOPIC_NEWEST + MAX_DECODED_PER_TOPIC_OLDEST;
|
|
3091
|
+
const picked = withHex.length <= budget ? withHex : [
|
|
3092
|
+
...withHex.slice(0, MAX_DECODED_PER_TOPIC_NEWEST),
|
|
3093
|
+
...withHex.slice(-MAX_DECODED_PER_TOPIC_OLDEST)
|
|
3094
|
+
];
|
|
3095
|
+
const tuples = [];
|
|
3096
|
+
let decoded = 0;
|
|
3097
|
+
for (const p of picked) {
|
|
3098
|
+
try {
|
|
3099
|
+
const cv = deserializeCV3(p.rawHex);
|
|
3100
|
+
decoded++;
|
|
3101
|
+
if (cv.type === "tuple")
|
|
3102
|
+
tuples.push(cv);
|
|
3103
|
+
} catch {}
|
|
3104
|
+
}
|
|
3105
|
+
const nonTuple = decoded > 0 && tuples.length === 0;
|
|
3106
|
+
const fields = [];
|
|
3107
|
+
if (!nonTuple) {
|
|
3108
|
+
const stats = new Map;
|
|
3109
|
+
for (const t of tuples) {
|
|
3110
|
+
for (const [key2, value] of Object.entries(t.value)) {
|
|
3111
|
+
if (key2 === "topic")
|
|
3112
|
+
continue;
|
|
3113
|
+
const tree = cvToTree(value);
|
|
3114
|
+
const existing = stats.get(key2);
|
|
3115
|
+
if (existing) {
|
|
3116
|
+
existing.present++;
|
|
3117
|
+
existing.tree = unify(existing.tree, tree);
|
|
3118
|
+
if (value.type === "none")
|
|
3119
|
+
existing.noneCount++;
|
|
3120
|
+
if (value.type === "none" || value.type === "some") {
|
|
3121
|
+
existing.optionalSeen = true;
|
|
3122
|
+
}
|
|
3123
|
+
} else {
|
|
3124
|
+
stats.set(key2, {
|
|
3125
|
+
tree,
|
|
3126
|
+
present: 1,
|
|
3127
|
+
noneCount: value.type === "none" ? 1 : 0,
|
|
3128
|
+
optionalSeen: value.type === "none" || value.type === "some"
|
|
3129
|
+
});
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
for (const [name, st] of [...stats].sort(([a], [b]) => a.localeCompare(b))) {
|
|
3134
|
+
const field = {
|
|
3135
|
+
name,
|
|
3136
|
+
camel_name: camelizeDataKey(name),
|
|
3137
|
+
clarity_type: renderClarity(st.tree),
|
|
3138
|
+
ts_type: renderTs(st.tree),
|
|
3139
|
+
column_type: toColumnType(st.tree),
|
|
3140
|
+
always_present: st.present === tuples.length
|
|
3141
|
+
};
|
|
3142
|
+
if (st.optionalSeen) {
|
|
3143
|
+
field.optional_some_rate = (st.present - st.noneCount) / st.present;
|
|
3144
|
+
}
|
|
3145
|
+
fields.push(field);
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
out.push({
|
|
3149
|
+
topic,
|
|
3150
|
+
count: rows.length,
|
|
3151
|
+
first_height: first,
|
|
3152
|
+
last_height: last,
|
|
3153
|
+
non_tuple: nonTuple,
|
|
3154
|
+
fields
|
|
3155
|
+
});
|
|
3156
|
+
}
|
|
3157
|
+
return out.sort((a, b) => b.count - a.count);
|
|
3158
|
+
}
|
|
2408
3159
|
// src/schema/prisma.ts
|
|
2409
3160
|
var PRISMA_TYPE = {
|
|
2410
3161
|
uint: { type: "Decimal", db: "@db.Numeric" },
|
|
@@ -3251,6 +4002,7 @@ export {
|
|
|
3251
4002
|
renderDeployPlan,
|
|
3252
4003
|
reindexSubgraph,
|
|
3253
4004
|
pgSchemaName,
|
|
4005
|
+
inferPrintTopics,
|
|
3254
4006
|
hasBreakingChanges,
|
|
3255
4007
|
generateSubgraphSQL,
|
|
3256
4008
|
generatePrismaSchema,
|
|
@@ -3261,10 +4013,11 @@ export {
|
|
|
3261
4013
|
deploySchema,
|
|
3262
4014
|
defineSubgraph,
|
|
3263
4015
|
canSparseScan,
|
|
4016
|
+
camelizeDataKey,
|
|
3264
4017
|
backfillSubgraph,
|
|
3265
4018
|
INDEX_CODEGEN_TABLES,
|
|
3266
4019
|
ByoBreakingChangeError
|
|
3267
4020
|
};
|
|
3268
4021
|
|
|
3269
|
-
//# debugId=
|
|
4022
|
+
//# debugId=4F9187B8B213FB0764756E2164756E21
|
|
3270
4023
|
//# sourceMappingURL=index.js.map
|