@secondlayer/subgraphs 1.0.0-alpha.0 → 1.0.0-beta.2
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.js +261 -49
- package/dist/src/index.js.map +8 -5
- package/dist/src/runtime/block-processor.js +222 -10
- package/dist/src/runtime/block-processor.js.map +9 -6
- package/dist/src/runtime/catchup.js +229 -17
- package/dist/src/runtime/catchup.js.map +8 -5
- package/dist/src/runtime/context.d.ts +22 -2
- package/dist/src/runtime/context.js +24 -3
- package/dist/src/runtime/context.js.map +3 -3
- package/dist/src/runtime/processor.js +665 -37
- package/dist/src/runtime/processor.js.map +17 -6
- package/dist/src/runtime/reindex.js +247 -35
- package/dist/src/runtime/reindex.js.map +8 -5
- package/dist/src/runtime/reorg.js +227 -15
- package/dist/src/runtime/reorg.js.map +8 -5
- package/dist/src/runtime/runner.d.ts +21 -1
- package/dist/src/service.js +667 -39
- package/dist/src/service.js.map +17 -6
- package/package.json +2 -2
|
@@ -168,7 +168,7 @@ class SubgraphContext {
|
|
|
168
168
|
}
|
|
169
169
|
async flush() {
|
|
170
170
|
if (this.ops.length === 0)
|
|
171
|
-
return 0;
|
|
171
|
+
return { count: 0, writes: [] };
|
|
172
172
|
const opsToFlush = [...this.ops];
|
|
173
173
|
this.ops.length = 0;
|
|
174
174
|
const statements = this.buildStatements(opsToFlush);
|
|
@@ -183,7 +183,21 @@ class SubgraphContext {
|
|
|
183
183
|
}
|
|
184
184
|
});
|
|
185
185
|
}
|
|
186
|
-
|
|
186
|
+
const writes = opsToFlush.map((op, rowIndex) => {
|
|
187
|
+
const blockHeight = op.data._block_height ?? this.block.height;
|
|
188
|
+
const txId = op.data._tx_id ?? this._tx.txId;
|
|
189
|
+
const baseRow = op.kind === "update" ? { ...op.data, ...op.set ?? {} } : { ...op.data };
|
|
190
|
+
delete baseRow._upsert_keys;
|
|
191
|
+
delete baseRow._upsert_fallback_keys;
|
|
192
|
+
delete baseRow._upsert_fallback_set;
|
|
193
|
+
return {
|
|
194
|
+
op: op.kind,
|
|
195
|
+
table: op.table,
|
|
196
|
+
row: jsonSafe(baseRow),
|
|
197
|
+
pk: { blockHeight, txId, rowIndex }
|
|
198
|
+
};
|
|
199
|
+
});
|
|
200
|
+
return { count: opsToFlush.length, writes };
|
|
187
201
|
}
|
|
188
202
|
prepareInsert(op) {
|
|
189
203
|
const upsertKeys = op.data._upsert_keys;
|
|
@@ -276,6 +290,13 @@ class SubgraphContext {
|
|
|
276
290
|
}
|
|
277
291
|
}
|
|
278
292
|
}
|
|
293
|
+
function jsonSafe(row) {
|
|
294
|
+
const out = {};
|
|
295
|
+
for (const [k, v] of Object.entries(row)) {
|
|
296
|
+
out[k] = typeof v === "bigint" ? v.toString() : v;
|
|
297
|
+
}
|
|
298
|
+
return out;
|
|
299
|
+
}
|
|
279
300
|
function escapeLiteral(value) {
|
|
280
301
|
if (value === null || value === undefined)
|
|
281
302
|
return "NULL";
|
|
@@ -857,12 +878,200 @@ import {
|
|
|
857
878
|
recordSubgraphProcessed,
|
|
858
879
|
updateSubgraphStatus
|
|
859
880
|
} from "@secondlayer/shared/db/queries/subgraphs";
|
|
860
|
-
import { logger as
|
|
861
|
-
import { sql as
|
|
881
|
+
import { logger as logger4 } from "@secondlayer/shared/logger";
|
|
882
|
+
import { sql as sql3 } from "kysely";
|
|
862
883
|
|
|
863
884
|
// src/schema/utils.ts
|
|
864
885
|
import { pgSchemaName } from "@secondlayer/shared/db/queries/subgraphs";
|
|
865
886
|
|
|
887
|
+
// src/runtime/outbox-emit.ts
|
|
888
|
+
import { createHash } from "node:crypto";
|
|
889
|
+
import { logger as logger3 } from "@secondlayer/shared/logger";
|
|
890
|
+
var loggedKillSwitch = false;
|
|
891
|
+
function isEmitOutboxEnabled() {
|
|
892
|
+
return process.env.SECONDLAYER_EMIT_OUTBOX !== "false";
|
|
893
|
+
}
|
|
894
|
+
function dedupKey(subgraphName, tableName, blockHeight, txId, rowIndex, row) {
|
|
895
|
+
const canonical = `${subgraphName}:${tableName}:${blockHeight}:${txId}:${rowIndex}:${stableStringify(row)}`;
|
|
896
|
+
return createHash("sha256").update(canonical).digest("hex").slice(0, 32);
|
|
897
|
+
}
|
|
898
|
+
function stableStringify(obj) {
|
|
899
|
+
const keys = Object.keys(obj).sort();
|
|
900
|
+
return JSON.stringify(keys.reduce((acc, k) => {
|
|
901
|
+
acc[k] = obj[k];
|
|
902
|
+
return acc;
|
|
903
|
+
}, {}));
|
|
904
|
+
}
|
|
905
|
+
async function emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, blockHeight) {
|
|
906
|
+
if (!isEmitOutboxEnabled()) {
|
|
907
|
+
if (!loggedKillSwitch) {
|
|
908
|
+
logger3.warn("SECONDLAYER_EMIT_OUTBOX=false — outbox emission bypassed");
|
|
909
|
+
loggedKillSwitch = true;
|
|
910
|
+
}
|
|
911
|
+
return 0;
|
|
912
|
+
}
|
|
913
|
+
loggedKillSwitch = false;
|
|
914
|
+
if (manifest.count === 0 || matcher.size() === 0)
|
|
915
|
+
return 0;
|
|
916
|
+
const rows = [];
|
|
917
|
+
for (const write of manifest.writes) {
|
|
918
|
+
if (write.op !== "insert")
|
|
919
|
+
continue;
|
|
920
|
+
const subs = matcher.match(subgraphName, write.table, write.row);
|
|
921
|
+
if (subs.length === 0)
|
|
922
|
+
continue;
|
|
923
|
+
const eventType = `${subgraphName}.${write.table}.created`;
|
|
924
|
+
for (const s of subs) {
|
|
925
|
+
rows.push({
|
|
926
|
+
subscription_id: s.id,
|
|
927
|
+
subgraph_name: subgraphName,
|
|
928
|
+
table_name: write.table,
|
|
929
|
+
block_height: blockHeight,
|
|
930
|
+
tx_id: write.pk.txId || null,
|
|
931
|
+
row_pk: write.pk,
|
|
932
|
+
event_type: eventType,
|
|
933
|
+
payload: write.row,
|
|
934
|
+
dedup_key: dedupKey(subgraphName, write.table, write.pk.blockHeight, write.pk.txId, write.pk.rowIndex, write.row)
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
if (rows.length === 0)
|
|
939
|
+
return 0;
|
|
940
|
+
await tx.insertInto("subscription_outbox").values(rows).onConflict((oc) => oc.columns(["subscription_id", "dedup_key"]).doNothing()).execute();
|
|
941
|
+
return rows.length;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// src/runtime/subscription-state.ts
|
|
945
|
+
import { listSubscriptions } from "@secondlayer/shared/db/queries/subscriptions";
|
|
946
|
+
import { sql as sql2 } from "kysely";
|
|
947
|
+
|
|
948
|
+
// src/runtime/emitter-matcher.ts
|
|
949
|
+
function isPrimitive(v) {
|
|
950
|
+
const t = typeof v;
|
|
951
|
+
return t === "string" || t === "number" || t === "boolean";
|
|
952
|
+
}
|
|
953
|
+
function coerceNumeric(v) {
|
|
954
|
+
if (typeof v === "number" && Number.isFinite(v))
|
|
955
|
+
return v;
|
|
956
|
+
if (typeof v === "bigint")
|
|
957
|
+
return Number(v);
|
|
958
|
+
if (typeof v === "string" && v !== "" && !Number.isNaN(Number(v))) {
|
|
959
|
+
return Number(v);
|
|
960
|
+
}
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
963
|
+
function matchClause(rowValue, clause) {
|
|
964
|
+
if (isPrimitive(clause)) {
|
|
965
|
+
if (typeof clause === "number" || typeof rowValue === "number") {
|
|
966
|
+
const a = coerceNumeric(rowValue);
|
|
967
|
+
const b = coerceNumeric(clause);
|
|
968
|
+
if (a !== null && b !== null)
|
|
969
|
+
return a === b;
|
|
970
|
+
}
|
|
971
|
+
return rowValue === clause || String(rowValue) === String(clause);
|
|
972
|
+
}
|
|
973
|
+
if (clause === null || typeof clause !== "object" || Array.isArray(clause)) {
|
|
974
|
+
return false;
|
|
975
|
+
}
|
|
976
|
+
const keys = Object.keys(clause);
|
|
977
|
+
if (keys.length !== 1)
|
|
978
|
+
return false;
|
|
979
|
+
const op = keys[0];
|
|
980
|
+
const c = clause;
|
|
981
|
+
switch (op) {
|
|
982
|
+
case "eq":
|
|
983
|
+
return matchClause(rowValue, c.eq);
|
|
984
|
+
case "neq":
|
|
985
|
+
return !matchClause(rowValue, c.neq);
|
|
986
|
+
case "gt":
|
|
987
|
+
case "gte":
|
|
988
|
+
case "lt":
|
|
989
|
+
case "lte": {
|
|
990
|
+
const a = coerceNumeric(rowValue);
|
|
991
|
+
const b = coerceNumeric(c[op]);
|
|
992
|
+
if (a === null || b === null)
|
|
993
|
+
return false;
|
|
994
|
+
if (op === "gt")
|
|
995
|
+
return a > b;
|
|
996
|
+
if (op === "gte")
|
|
997
|
+
return a >= b;
|
|
998
|
+
if (op === "lt")
|
|
999
|
+
return a < b;
|
|
1000
|
+
return a <= b;
|
|
1001
|
+
}
|
|
1002
|
+
case "in": {
|
|
1003
|
+
const list = c.in;
|
|
1004
|
+
if (!Array.isArray(list))
|
|
1005
|
+
return false;
|
|
1006
|
+
return list.some((item) => isPrimitive(item) && matchClause(rowValue, item));
|
|
1007
|
+
}
|
|
1008
|
+
default:
|
|
1009
|
+
return false;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
function matchesFilter(filter, row) {
|
|
1013
|
+
if (!filter || Object.keys(filter).length === 0)
|
|
1014
|
+
return true;
|
|
1015
|
+
for (const [col, clause] of Object.entries(filter)) {
|
|
1016
|
+
if (!matchClause(row[col], clause))
|
|
1017
|
+
return false;
|
|
1018
|
+
}
|
|
1019
|
+
return true;
|
|
1020
|
+
}
|
|
1021
|
+
function key(subgraphName, tableName) {
|
|
1022
|
+
return `${subgraphName}\x00${tableName}`;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
class SubscriptionMatcher {
|
|
1026
|
+
byKey = new Map;
|
|
1027
|
+
byId = new Map;
|
|
1028
|
+
setAll(subs) {
|
|
1029
|
+
this.byKey.clear();
|
|
1030
|
+
this.byId.clear();
|
|
1031
|
+
for (const sub of subs) {
|
|
1032
|
+
if (sub.status !== "active")
|
|
1033
|
+
continue;
|
|
1034
|
+
this.byId.set(sub.id, sub);
|
|
1035
|
+
const k = key(sub.subgraph_name, sub.table_name);
|
|
1036
|
+
const arr = this.byKey.get(k);
|
|
1037
|
+
if (arr)
|
|
1038
|
+
arr.push(sub);
|
|
1039
|
+
else
|
|
1040
|
+
this.byKey.set(k, [sub]);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
match(subgraphName, tableName, row) {
|
|
1044
|
+
const bucket = this.byKey.get(key(subgraphName, tableName));
|
|
1045
|
+
if (!bucket)
|
|
1046
|
+
return [];
|
|
1047
|
+
const hits = [];
|
|
1048
|
+
for (const sub of bucket) {
|
|
1049
|
+
if (matchesFilter(sub.filter, row))
|
|
1050
|
+
hits.push(sub);
|
|
1051
|
+
}
|
|
1052
|
+
return hits;
|
|
1053
|
+
}
|
|
1054
|
+
has(subgraphName, tableName) {
|
|
1055
|
+
return this.byKey.has(key(subgraphName, tableName));
|
|
1056
|
+
}
|
|
1057
|
+
size() {
|
|
1058
|
+
return this.byId.size;
|
|
1059
|
+
}
|
|
1060
|
+
get(id) {
|
|
1061
|
+
return this.byId.get(id);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// src/runtime/subscription-state.ts
|
|
1066
|
+
var matcher = new SubscriptionMatcher;
|
|
1067
|
+
async function refreshMatcher(db) {
|
|
1068
|
+
const rows = await sql2`
|
|
1069
|
+
SELECT * FROM subscriptions WHERE status = 'active'
|
|
1070
|
+
`.execute(db);
|
|
1071
|
+
matcher.setAll(rows.rows);
|
|
1072
|
+
return matcher.size();
|
|
1073
|
+
}
|
|
1074
|
+
|
|
866
1075
|
// src/runtime/block-processor.ts
|
|
867
1076
|
var schemaNameCache = new Map;
|
|
868
1077
|
async function processBlock(subgraph, subgraphName, blockHeight, opts) {
|
|
@@ -886,7 +1095,7 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
|
|
|
886
1095
|
} else {
|
|
887
1096
|
block = await sourceDb.selectFrom("blocks").selectAll().where("height", "=", blockHeight).executeTakeFirst();
|
|
888
1097
|
if (!block) {
|
|
889
|
-
|
|
1098
|
+
logger4.warn("Block not found for subgraph processing", {
|
|
890
1099
|
subgraph: subgraphName,
|
|
891
1100
|
blockHeight
|
|
892
1101
|
});
|
|
@@ -894,7 +1103,7 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
|
|
|
894
1103
|
return result;
|
|
895
1104
|
}
|
|
896
1105
|
if (!block.canonical) {
|
|
897
|
-
|
|
1106
|
+
logger4.debug("Skipping non-canonical block", {
|
|
898
1107
|
subgraph: subgraphName,
|
|
899
1108
|
blockHeight
|
|
900
1109
|
});
|
|
@@ -941,7 +1150,10 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
|
|
|
941
1150
|
result.errors = runResult.errors;
|
|
942
1151
|
if (ctx.pendingOps > 0) {
|
|
943
1152
|
const flushStart = performance.now();
|
|
944
|
-
await ctx.flush();
|
|
1153
|
+
const manifest = await ctx.flush();
|
|
1154
|
+
if (manifest.count > 0) {
|
|
1155
|
+
await emitSubscriptionOutbox(tx, subgraphName, manifest, matcher, block.height);
|
|
1156
|
+
}
|
|
945
1157
|
flushMs = performance.now() - flushStart;
|
|
946
1158
|
}
|
|
947
1159
|
if (!opts?.skipProgressUpdate) {
|
|
@@ -963,10 +1175,10 @@ async function processBlock(subgraph, subgraphName, blockHeight, opts) {
|
|
|
963
1175
|
try {
|
|
964
1176
|
const tables = Object.keys(subgraph.schema);
|
|
965
1177
|
for (const table of tables) {
|
|
966
|
-
const { rows } = await
|
|
1178
|
+
const { rows } = await sql3.raw(`SELECT n_live_tup AS count FROM pg_stat_user_tables WHERE schemaname = '${schemaName}' AND relname = '${table}'`).execute(targetDb);
|
|
967
1179
|
const count = Number(rows[0]?.count ?? 0);
|
|
968
1180
|
if (count >= 1e7) {
|
|
969
|
-
|
|
1181
|
+
logger4.warn("Subgraph table exceeds 10M rows (estimate)", {
|
|
970
1182
|
subgraph: subgraphName,
|
|
971
1183
|
table,
|
|
972
1184
|
count
|
|
@@ -1047,7 +1259,7 @@ import {
|
|
|
1047
1259
|
recordGapBatch
|
|
1048
1260
|
} from "@secondlayer/shared/db/queries/subgraph-gaps";
|
|
1049
1261
|
import { getSubgraph } from "@secondlayer/shared/db/queries/subgraphs";
|
|
1050
|
-
import { logger as
|
|
1262
|
+
import { logger as logger5 } from "@secondlayer/shared/logger";
|
|
1051
1263
|
|
|
1052
1264
|
// src/runtime/batch-loader.ts
|
|
1053
1265
|
async function loadBlockRange(db, fromHeight, toHeight) {
|
|
@@ -1146,7 +1358,7 @@ async function catchUpSubgraph(subgraph, subgraphName) {
|
|
|
1146
1358
|
const subgraphStart = Number(subgraphRow.start_block) || 1;
|
|
1147
1359
|
const startBlock = Math.max(lastProcessedBlock + 1, subgraphStart);
|
|
1148
1360
|
const totalBlocks = chainTip - lastProcessedBlock;
|
|
1149
|
-
|
|
1361
|
+
logger5.info("Subgraph catch-up starting", {
|
|
1150
1362
|
subgraph: subgraphName,
|
|
1151
1363
|
from: startBlock,
|
|
1152
1364
|
to: chainTip,
|
|
@@ -1161,7 +1373,7 @@ async function catchUpSubgraph(subgraph, subgraphName) {
|
|
|
1161
1373
|
while (currentHeight <= chainTip) {
|
|
1162
1374
|
const currentRow = await getSubgraph(targetDb, subgraphName);
|
|
1163
1375
|
if (!currentRow || currentRow.status !== "active") {
|
|
1164
|
-
|
|
1376
|
+
logger5.info("Subgraph status changed, stopping catch-up", {
|
|
1165
1377
|
subgraph: subgraphName,
|
|
1166
1378
|
status: currentRow?.status ?? "deleted"
|
|
1167
1379
|
});
|
|
@@ -1188,7 +1400,7 @@ async function catchUpSubgraph(subgraph, subgraphName) {
|
|
|
1188
1400
|
preloaded: blockData
|
|
1189
1401
|
});
|
|
1190
1402
|
} catch (err) {
|
|
1191
|
-
|
|
1403
|
+
logger5.error("Block processing error during catch-up", {
|
|
1192
1404
|
subgraph: subgraphName,
|
|
1193
1405
|
blockHeight: height,
|
|
1194
1406
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -1207,7 +1419,7 @@ async function catchUpSubgraph(subgraph, subgraphName) {
|
|
|
1207
1419
|
}
|
|
1208
1420
|
}
|
|
1209
1421
|
if (processed % LOG_INTERVAL === 0) {
|
|
1210
|
-
|
|
1422
|
+
logger5.info("Subgraph catch-up progress", {
|
|
1211
1423
|
subgraph: subgraphName,
|
|
1212
1424
|
processed,
|
|
1213
1425
|
total: totalBlocks,
|
|
@@ -1219,7 +1431,7 @@ async function catchUpSubgraph(subgraph, subgraphName) {
|
|
|
1219
1431
|
if (batchFailedBlocks.length > 0) {
|
|
1220
1432
|
const gaps = coalesceGaps(batchFailedBlocks);
|
|
1221
1433
|
await recordGapBatch(targetDb, subgraphRow.id, subgraphName, gaps).catch((err) => {
|
|
1222
|
-
|
|
1434
|
+
logger5.warn("Failed to record subgraph gaps", {
|
|
1223
1435
|
subgraph: subgraphName,
|
|
1224
1436
|
error: err instanceof Error ? err.message : String(err)
|
|
1225
1437
|
});
|
|
@@ -1230,7 +1442,7 @@ async function catchUpSubgraph(subgraph, subgraphName) {
|
|
|
1230
1442
|
currentHeight = batchEnd + 1;
|
|
1231
1443
|
}
|
|
1232
1444
|
await stats.flush(targetDb);
|
|
1233
|
-
|
|
1445
|
+
logger5.info("Subgraph catch-up complete", {
|
|
1234
1446
|
subgraph: subgraphName,
|
|
1235
1447
|
processed
|
|
1236
1448
|
});
|
|
@@ -1244,14 +1456,14 @@ async function catchUpSubgraph(subgraph, subgraphName) {
|
|
|
1244
1456
|
import { getErrorMessage as getErrorMessage2 } from "@secondlayer/shared";
|
|
1245
1457
|
import { getRawClient, getTargetDb as getTargetDb3 } from "@secondlayer/shared/db";
|
|
1246
1458
|
import { listSubgraphs } from "@secondlayer/shared/db/queries/subgraphs";
|
|
1247
|
-
import { logger as
|
|
1459
|
+
import { logger as logger6 } from "@secondlayer/shared/logger";
|
|
1248
1460
|
async function handleSubgraphReorg(blockHeight, loadSubgraphDef) {
|
|
1249
1461
|
const targetDb = getTargetDb3();
|
|
1250
1462
|
const client = getRawClient("target");
|
|
1251
1463
|
const activeSubgraphs = (await listSubgraphs(targetDb)).filter((v) => v.status === "active");
|
|
1252
1464
|
if (activeSubgraphs.length === 0)
|
|
1253
1465
|
return;
|
|
1254
|
-
|
|
1466
|
+
logger6.info("Propagating reorg to subgraphs", {
|
|
1255
1467
|
blockHeight,
|
|
1256
1468
|
subgraphCount: activeSubgraphs.length
|
|
1257
1469
|
});
|
|
@@ -1264,18 +1476,18 @@ async function handleSubgraphReorg(blockHeight, loadSubgraphDef) {
|
|
|
1264
1476
|
for (const tableName of Object.keys(schema)) {
|
|
1265
1477
|
await client.unsafe(`DELETE FROM "${schemaName}"."${tableName}" WHERE "_block_height" = $1`, [blockHeight]);
|
|
1266
1478
|
}
|
|
1267
|
-
|
|
1479
|
+
logger6.info("Subgraph reorg cleanup done", {
|
|
1268
1480
|
subgraph: sg.name,
|
|
1269
1481
|
blockHeight
|
|
1270
1482
|
});
|
|
1271
1483
|
const def = await loadSubgraphDef(sg);
|
|
1272
1484
|
await processBlock(def, sg.name, blockHeight);
|
|
1273
|
-
|
|
1485
|
+
logger6.info("Subgraph reorg reprocessed", {
|
|
1274
1486
|
subgraph: sg.name,
|
|
1275
1487
|
blockHeight
|
|
1276
1488
|
});
|
|
1277
1489
|
} catch (err) {
|
|
1278
|
-
|
|
1490
|
+
logger6.error("Subgraph reorg handling failed", {
|
|
1279
1491
|
subgraph: sg.name,
|
|
1280
1492
|
blockHeight,
|
|
1281
1493
|
error: getErrorMessage2(err)
|
|
@@ -1286,13 +1498,427 @@ async function handleSubgraphReorg(blockHeight, loadSubgraphDef) {
|
|
|
1286
1498
|
|
|
1287
1499
|
// src/runtime/processor.ts
|
|
1288
1500
|
import { getErrorMessage as getErrorMessage3 } from "@secondlayer/shared";
|
|
1289
|
-
import { getTargetDb as
|
|
1501
|
+
import { getTargetDb as getTargetDb5 } from "@secondlayer/shared/db";
|
|
1290
1502
|
import {
|
|
1291
1503
|
listSubgraphs as listSubgraphs2,
|
|
1292
1504
|
updateSubgraphStatus as updateSubgraphStatus2
|
|
1293
1505
|
} from "@secondlayer/shared/db/queries/subgraphs";
|
|
1294
|
-
import { logger as
|
|
1506
|
+
import { logger as logger9 } from "@secondlayer/shared/logger";
|
|
1507
|
+
import { listen as listen2 } from "@secondlayer/shared/queue/listener";
|
|
1508
|
+
|
|
1509
|
+
// src/runtime/emitter.ts
|
|
1510
|
+
import {
|
|
1511
|
+
getTargetDb as getTargetDb4
|
|
1512
|
+
} from "@secondlayer/shared/db";
|
|
1513
|
+
import {
|
|
1514
|
+
getSubscriptionSigningSecret
|
|
1515
|
+
} from "@secondlayer/shared/db/queries/subscriptions";
|
|
1516
|
+
import { logger as logger8 } from "@secondlayer/shared/logger";
|
|
1295
1517
|
import { listen } from "@secondlayer/shared/queue/listener";
|
|
1518
|
+
import { sql as sql4 } from "kysely";
|
|
1519
|
+
|
|
1520
|
+
// src/runtime/formats/index.ts
|
|
1521
|
+
import { logger as logger7 } from "@secondlayer/shared/logger";
|
|
1522
|
+
|
|
1523
|
+
// src/runtime/formats/cloudevents.ts
|
|
1524
|
+
function buildCloudEvents(outboxRow, _sub) {
|
|
1525
|
+
const event = {
|
|
1526
|
+
specversion: "1.0",
|
|
1527
|
+
type: outboxRow.event_type,
|
|
1528
|
+
source: `secondlayer:${outboxRow.subgraph_name}`,
|
|
1529
|
+
id: outboxRow.id,
|
|
1530
|
+
time: new Date(outboxRow.created_at).toISOString(),
|
|
1531
|
+
datacontenttype: "application/json",
|
|
1532
|
+
data: outboxRow.payload
|
|
1533
|
+
};
|
|
1534
|
+
return {
|
|
1535
|
+
body: JSON.stringify(event),
|
|
1536
|
+
headers: {
|
|
1537
|
+
"content-type": "application/cloudevents+json; charset=utf-8"
|
|
1538
|
+
}
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// src/runtime/formats/cloudflare.ts
|
|
1543
|
+
import { decryptSecret } from "@secondlayer/shared/crypto/secrets";
|
|
1544
|
+
function resolveBearer(sub) {
|
|
1545
|
+
const cfg = sub.auth_config;
|
|
1546
|
+
if (cfg.tokenEnc) {
|
|
1547
|
+
try {
|
|
1548
|
+
return decryptSecret(Buffer.from(cfg.tokenEnc, "base64"));
|
|
1549
|
+
} catch {
|
|
1550
|
+
return null;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
return cfg.token ?? null;
|
|
1554
|
+
}
|
|
1555
|
+
function buildCloudflare(outboxRow, sub) {
|
|
1556
|
+
const body = JSON.stringify({
|
|
1557
|
+
params: {
|
|
1558
|
+
...outboxRow.payload,
|
|
1559
|
+
_type: outboxRow.event_type,
|
|
1560
|
+
_outboxId: outboxRow.id
|
|
1561
|
+
}
|
|
1562
|
+
});
|
|
1563
|
+
const headers = {
|
|
1564
|
+
"content-type": "application/json"
|
|
1565
|
+
};
|
|
1566
|
+
const token = resolveBearer(sub);
|
|
1567
|
+
if (token)
|
|
1568
|
+
headers.authorization = `Bearer ${token}`;
|
|
1569
|
+
return { body, headers };
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// src/runtime/formats/inngest.ts
|
|
1573
|
+
var INNGEST_VERSION = "2026-04-23.v1";
|
|
1574
|
+
function buildInngest(outboxRow) {
|
|
1575
|
+
const event = {
|
|
1576
|
+
name: outboxRow.event_type,
|
|
1577
|
+
data: outboxRow.payload,
|
|
1578
|
+
id: outboxRow.id,
|
|
1579
|
+
ts: new Date(outboxRow.created_at).getTime(),
|
|
1580
|
+
v: INNGEST_VERSION
|
|
1581
|
+
};
|
|
1582
|
+
return {
|
|
1583
|
+
body: JSON.stringify([event]),
|
|
1584
|
+
headers: {
|
|
1585
|
+
"content-type": "application/json"
|
|
1586
|
+
}
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
// src/runtime/formats/raw.ts
|
|
1591
|
+
function buildRaw(outboxRow, sub) {
|
|
1592
|
+
const cfg = sub.auth_config;
|
|
1593
|
+
const headers = {
|
|
1594
|
+
"content-type": cfg.contentType ?? "application/json",
|
|
1595
|
+
...cfg.headers ?? {}
|
|
1596
|
+
};
|
|
1597
|
+
if (cfg.authType === "bearer" && cfg.token) {
|
|
1598
|
+
headers.authorization = `Bearer ${cfg.token}`;
|
|
1599
|
+
} else if (cfg.authType === "basic" && cfg.basicAuth) {
|
|
1600
|
+
headers.authorization = `Basic ${cfg.basicAuth}`;
|
|
1601
|
+
}
|
|
1602
|
+
return {
|
|
1603
|
+
body: JSON.stringify(outboxRow.payload),
|
|
1604
|
+
headers
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// src/runtime/formats/standard-webhooks.ts
|
|
1609
|
+
import { sign } from "@secondlayer/shared/crypto/standard-webhooks";
|
|
1610
|
+
function buildStandardWebhooks(outboxRow, signingSecret) {
|
|
1611
|
+
const payload = {
|
|
1612
|
+
type: outboxRow.event_type,
|
|
1613
|
+
timestamp: new Date(outboxRow.created_at).toISOString(),
|
|
1614
|
+
data: outboxRow.payload
|
|
1615
|
+
};
|
|
1616
|
+
const body = JSON.stringify(payload);
|
|
1617
|
+
const sigHeaders = sign(body, signingSecret, {
|
|
1618
|
+
id: outboxRow.id,
|
|
1619
|
+
timestampSeconds: Math.floor(new Date(outboxRow.created_at).getTime() / 1000)
|
|
1620
|
+
});
|
|
1621
|
+
return {
|
|
1622
|
+
body,
|
|
1623
|
+
headers: {
|
|
1624
|
+
"content-type": "application/json",
|
|
1625
|
+
...sigHeaders
|
|
1626
|
+
}
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// src/runtime/formats/trigger.ts
|
|
1631
|
+
import { decryptSecret as decryptSecret2 } from "@secondlayer/shared/crypto/secrets";
|
|
1632
|
+
function resolveBearer2(sub) {
|
|
1633
|
+
const cfg = sub.auth_config;
|
|
1634
|
+
if (cfg.tokenEnc) {
|
|
1635
|
+
try {
|
|
1636
|
+
return decryptSecret2(Buffer.from(cfg.tokenEnc, "base64"));
|
|
1637
|
+
} catch {
|
|
1638
|
+
return null;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
return cfg.token ?? null;
|
|
1642
|
+
}
|
|
1643
|
+
function buildTrigger(outboxRow, sub) {
|
|
1644
|
+
const body = JSON.stringify({
|
|
1645
|
+
payload: outboxRow.payload,
|
|
1646
|
+
options: {
|
|
1647
|
+
idempotencyKey: outboxRow.id
|
|
1648
|
+
}
|
|
1649
|
+
});
|
|
1650
|
+
const headers = {
|
|
1651
|
+
"content-type": "application/json"
|
|
1652
|
+
};
|
|
1653
|
+
const token = resolveBearer2(sub);
|
|
1654
|
+
if (token)
|
|
1655
|
+
headers.authorization = `Bearer ${token}`;
|
|
1656
|
+
return { body, headers };
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
// src/runtime/formats/index.ts
|
|
1660
|
+
function buildForFormat(outboxRow, sub, signingSecret) {
|
|
1661
|
+
switch (sub.format) {
|
|
1662
|
+
case "inngest":
|
|
1663
|
+
return buildInngest(outboxRow);
|
|
1664
|
+
case "trigger":
|
|
1665
|
+
return buildTrigger(outboxRow, sub);
|
|
1666
|
+
case "cloudflare":
|
|
1667
|
+
return buildCloudflare(outboxRow, sub);
|
|
1668
|
+
case "cloudevents":
|
|
1669
|
+
return buildCloudEvents(outboxRow, sub);
|
|
1670
|
+
case "raw":
|
|
1671
|
+
return buildRaw(outboxRow, sub);
|
|
1672
|
+
case "standard-webhooks":
|
|
1673
|
+
return buildStandardWebhooks(outboxRow, signingSecret);
|
|
1674
|
+
default:
|
|
1675
|
+
logger7.warn("Unknown subscription format, falling back to standard-webhooks", {
|
|
1676
|
+
format: sub.format,
|
|
1677
|
+
subscriptionId: sub.id
|
|
1678
|
+
});
|
|
1679
|
+
return buildStandardWebhooks(outboxRow, signingSecret);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
// src/runtime/emitter.ts
|
|
1684
|
+
var BATCH_SIZE = 50;
|
|
1685
|
+
var BACKOFF_SECONDS = [30, 120, 600, 3600, 21600, 86400, 259200];
|
|
1686
|
+
var CIRCUIT_THRESHOLD = 20;
|
|
1687
|
+
function nextDelaySeconds(attempt) {
|
|
1688
|
+
return BACKOFF_SECONDS[Math.min(attempt, BACKOFF_SECONDS.length - 1)];
|
|
1689
|
+
}
|
|
1690
|
+
async function dispatchOne(db, outboxRow, sub) {
|
|
1691
|
+
const { body, headers } = buildForFormat(outboxRow, sub, getSubscriptionSigningSecret(sub));
|
|
1692
|
+
const start = performance.now();
|
|
1693
|
+
let statusCode = null;
|
|
1694
|
+
let error = null;
|
|
1695
|
+
let ok = false;
|
|
1696
|
+
let responseBody = "";
|
|
1697
|
+
let responseHeaders = {};
|
|
1698
|
+
try {
|
|
1699
|
+
const res = await fetch(sub.url, {
|
|
1700
|
+
method: "POST",
|
|
1701
|
+
headers,
|
|
1702
|
+
body,
|
|
1703
|
+
signal: AbortSignal.timeout(sub.timeout_ms)
|
|
1704
|
+
});
|
|
1705
|
+
statusCode = res.status;
|
|
1706
|
+
ok = res.ok;
|
|
1707
|
+
const buf = await res.arrayBuffer();
|
|
1708
|
+
const truncated = buf.byteLength > 8192 ? buf.slice(0, 8192) : buf;
|
|
1709
|
+
responseBody = Buffer.from(truncated).toString("utf8");
|
|
1710
|
+
responseHeaders = Object.fromEntries(res.headers.entries());
|
|
1711
|
+
} catch (err) {
|
|
1712
|
+
error = err instanceof Error ? err.message : String(err);
|
|
1713
|
+
}
|
|
1714
|
+
const durationMs = Math.round(performance.now() - start);
|
|
1715
|
+
const attempt = outboxRow.attempt + 1;
|
|
1716
|
+
await db.insertInto("subscription_deliveries").values({
|
|
1717
|
+
outbox_id: outboxRow.id,
|
|
1718
|
+
subscription_id: outboxRow.subscription_id,
|
|
1719
|
+
attempt,
|
|
1720
|
+
status_code: statusCode,
|
|
1721
|
+
response_headers: responseHeaders,
|
|
1722
|
+
response_body: responseBody || null,
|
|
1723
|
+
error_message: error,
|
|
1724
|
+
duration_ms: durationMs
|
|
1725
|
+
}).execute();
|
|
1726
|
+
return { ok, statusCode, error, durationMs };
|
|
1727
|
+
}
|
|
1728
|
+
async function settleDelivered(db, outboxRow) {
|
|
1729
|
+
await db.transaction().execute(async (tx) => {
|
|
1730
|
+
await tx.updateTable("subscription_outbox").set({
|
|
1731
|
+
status: "delivered",
|
|
1732
|
+
delivered_at: new Date,
|
|
1733
|
+
attempt: outboxRow.attempt + 1,
|
|
1734
|
+
locked_by: null,
|
|
1735
|
+
locked_until: null
|
|
1736
|
+
}).where("id", "=", outboxRow.id).execute();
|
|
1737
|
+
await tx.updateTable("subscriptions").set({
|
|
1738
|
+
last_delivery_at: new Date,
|
|
1739
|
+
last_success_at: new Date,
|
|
1740
|
+
circuit_failures: 0,
|
|
1741
|
+
last_error: null,
|
|
1742
|
+
updated_at: new Date
|
|
1743
|
+
}).where("id", "=", outboxRow.subscription_id).execute();
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
async function settleFailed(db, outboxRow, sub, errText) {
|
|
1747
|
+
const attempt = outboxRow.attempt + 1;
|
|
1748
|
+
const isDead = attempt >= sub.max_retries;
|
|
1749
|
+
const nextAt = isDead ? null : new Date(Date.now() + nextDelaySeconds(outboxRow.attempt) * 1000);
|
|
1750
|
+
await db.transaction().execute(async (tx) => {
|
|
1751
|
+
await tx.updateTable("subscription_outbox").set({
|
|
1752
|
+
attempt,
|
|
1753
|
+
next_attempt_at: nextAt ?? new Date,
|
|
1754
|
+
status: isDead ? "dead" : "pending",
|
|
1755
|
+
failed_at: isDead ? new Date : null,
|
|
1756
|
+
locked_by: null,
|
|
1757
|
+
locked_until: null
|
|
1758
|
+
}).where("id", "=", outboxRow.id).execute();
|
|
1759
|
+
const newFailures = sub.circuit_failures + 1;
|
|
1760
|
+
const shouldTripCircuit = newFailures >= CIRCUIT_THRESHOLD;
|
|
1761
|
+
await tx.updateTable("subscriptions").set({
|
|
1762
|
+
last_delivery_at: new Date,
|
|
1763
|
+
last_error: errText.slice(0, 500),
|
|
1764
|
+
circuit_failures: newFailures,
|
|
1765
|
+
...shouldTripCircuit ? { status: "paused", circuit_opened_at: new Date } : {},
|
|
1766
|
+
updated_at: new Date
|
|
1767
|
+
}).where("id", "=", outboxRow.subscription_id).execute();
|
|
1768
|
+
if (shouldTripCircuit) {
|
|
1769
|
+
logger8.warn("Subscription circuit tripped — paused after consecutive failures", {
|
|
1770
|
+
subscription: sub.name,
|
|
1771
|
+
failures: newFailures
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
async function claimAndDrain(db, state, emitterId) {
|
|
1777
|
+
if (state.claimInFlight)
|
|
1778
|
+
return 0;
|
|
1779
|
+
state.claimInFlight = true;
|
|
1780
|
+
try {
|
|
1781
|
+
const claimed = await db.transaction().execute(async (tx) => {
|
|
1782
|
+
const rows = await sql4`
|
|
1783
|
+
SELECT * FROM subscription_outbox
|
|
1784
|
+
WHERE status = 'pending' AND next_attempt_at <= NOW()
|
|
1785
|
+
ORDER BY next_attempt_at ASC
|
|
1786
|
+
FOR UPDATE SKIP LOCKED
|
|
1787
|
+
LIMIT ${sql4.lit(BATCH_SIZE)}
|
|
1788
|
+
`.execute(tx);
|
|
1789
|
+
if (rows.rows.length === 0)
|
|
1790
|
+
return [];
|
|
1791
|
+
const now = new Date;
|
|
1792
|
+
const lockUntil = new Date(now.getTime() + 60000);
|
|
1793
|
+
await tx.updateTable("subscription_outbox").set({ locked_by: emitterId, locked_until: lockUntil }).where("id", "in", rows.rows.map((r) => r.id)).execute();
|
|
1794
|
+
return rows.rows;
|
|
1795
|
+
});
|
|
1796
|
+
if (claimed.length === 0)
|
|
1797
|
+
return 0;
|
|
1798
|
+
const bySubId = new Map;
|
|
1799
|
+
for (const row of claimed) {
|
|
1800
|
+
const arr = bySubId.get(row.subscription_id);
|
|
1801
|
+
if (arr)
|
|
1802
|
+
arr.push(row);
|
|
1803
|
+
else
|
|
1804
|
+
bySubId.set(row.subscription_id, [row]);
|
|
1805
|
+
}
|
|
1806
|
+
const subIds = Array.from(bySubId.keys());
|
|
1807
|
+
const subs = await db.selectFrom("subscriptions").selectAll().where("id", "in", subIds).execute();
|
|
1808
|
+
const subById = new Map(subs.map((s) => [s.id, s]));
|
|
1809
|
+
await Promise.all(subIds.map((subId) => drainForSub(db, state, subById.get(subId), bySubId.get(subId))));
|
|
1810
|
+
return claimed.length;
|
|
1811
|
+
} finally {
|
|
1812
|
+
state.claimInFlight = false;
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
async function drainForSub(db, state, sub, rows) {
|
|
1816
|
+
const cap = sub.concurrency || 4;
|
|
1817
|
+
const counter = () => state.inFlightBySub.get(sub.id) ?? 0;
|
|
1818
|
+
const inc = () => state.inFlightBySub.set(sub.id, counter() + 1);
|
|
1819
|
+
const dec = () => state.inFlightBySub.set(sub.id, Math.max(0, counter() - 1));
|
|
1820
|
+
const queue = [...rows];
|
|
1821
|
+
const workers = [];
|
|
1822
|
+
const slots = Math.min(cap, queue.length);
|
|
1823
|
+
for (let i = 0;i < slots; i++) {
|
|
1824
|
+
workers.push((async () => {
|
|
1825
|
+
while (state.running && queue.length > 0) {
|
|
1826
|
+
const row = queue.shift();
|
|
1827
|
+
if (!row)
|
|
1828
|
+
break;
|
|
1829
|
+
inc();
|
|
1830
|
+
try {
|
|
1831
|
+
const result = await dispatchOne(db, row, sub);
|
|
1832
|
+
if (result.ok) {
|
|
1833
|
+
await settleDelivered(db, row);
|
|
1834
|
+
} else {
|
|
1835
|
+
const err = result.error ?? `HTTP ${result.statusCode ?? "?"}`;
|
|
1836
|
+
await settleFailed(db, row, sub, err);
|
|
1837
|
+
}
|
|
1838
|
+
} catch (err) {
|
|
1839
|
+
logger8.error("Emitter dispatch crashed", {
|
|
1840
|
+
outboxId: row.id,
|
|
1841
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1842
|
+
});
|
|
1843
|
+
await settleFailed(db, row, sub, err instanceof Error ? err.message : String(err));
|
|
1844
|
+
} finally {
|
|
1845
|
+
dec();
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
})());
|
|
1849
|
+
}
|
|
1850
|
+
await Promise.all(workers);
|
|
1851
|
+
}
|
|
1852
|
+
async function runRetention(db) {
|
|
1853
|
+
await sql4`
|
|
1854
|
+
DELETE FROM subscription_outbox
|
|
1855
|
+
WHERE status = 'delivered' AND delivered_at < NOW() - interval '7 days'
|
|
1856
|
+
`.execute(db);
|
|
1857
|
+
await sql4`
|
|
1858
|
+
DELETE FROM subscription_deliveries
|
|
1859
|
+
WHERE dispatched_at < NOW() - interval '30 days'
|
|
1860
|
+
`.execute(db);
|
|
1861
|
+
await sql4`
|
|
1862
|
+
DELETE FROM subscription_outbox
|
|
1863
|
+
WHERE status = 'dead' AND failed_at < NOW() - interval '90 days'
|
|
1864
|
+
`.execute(db);
|
|
1865
|
+
}
|
|
1866
|
+
async function startEmitter(opts) {
|
|
1867
|
+
const emitterId = `emitter-${Math.random().toString(36).slice(2, 10)}`;
|
|
1868
|
+
const db = getTargetDb4();
|
|
1869
|
+
const state = {
|
|
1870
|
+
running: true,
|
|
1871
|
+
inFlightBySub: new Map,
|
|
1872
|
+
claimInFlight: false
|
|
1873
|
+
};
|
|
1874
|
+
const pollIntervalMs = opts?.pollIntervalMs ?? 120000;
|
|
1875
|
+
const retentionIntervalMs = opts?.retentionIntervalMs ?? 60 * 60000;
|
|
1876
|
+
logger8.info("[emitter] started", { id: emitterId });
|
|
1877
|
+
await refreshMatcher(db).catch((err) => {
|
|
1878
|
+
logger8.error("[emitter] initial matcher refresh failed", {
|
|
1879
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1880
|
+
});
|
|
1881
|
+
});
|
|
1882
|
+
const stopNew = await listen("subscriptions:new_outbox", () => {
|
|
1883
|
+
if (!state.running)
|
|
1884
|
+
return;
|
|
1885
|
+
claimAndDrain(db, state, emitterId).catch((err) => logger8.error("[emitter] claim failed", {
|
|
1886
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1887
|
+
}));
|
|
1888
|
+
});
|
|
1889
|
+
const stopChanged = await listen("subscriptions:changed", () => {
|
|
1890
|
+
if (!state.running)
|
|
1891
|
+
return;
|
|
1892
|
+
refreshMatcher(db).catch((err) => logger8.error("[emitter] matcher refresh failed", {
|
|
1893
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1894
|
+
}));
|
|
1895
|
+
});
|
|
1896
|
+
const poll = setInterval(() => {
|
|
1897
|
+
if (!state.running)
|
|
1898
|
+
return;
|
|
1899
|
+
claimAndDrain(db, state, emitterId).catch((err) => logger8.error("[emitter] poll claim failed", {
|
|
1900
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1901
|
+
}));
|
|
1902
|
+
}, pollIntervalMs);
|
|
1903
|
+
claimAndDrain(db, state, emitterId);
|
|
1904
|
+
const retention = setInterval(() => {
|
|
1905
|
+
if (!state.running)
|
|
1906
|
+
return;
|
|
1907
|
+
runRetention(db).catch((err) => logger8.error("[emitter] retention failed", {
|
|
1908
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1909
|
+
}));
|
|
1910
|
+
}, retentionIntervalMs);
|
|
1911
|
+
return async () => {
|
|
1912
|
+
state.running = false;
|
|
1913
|
+
clearInterval(poll);
|
|
1914
|
+
clearInterval(retention);
|
|
1915
|
+
await stopNew();
|
|
1916
|
+
await stopChanged();
|
|
1917
|
+
logger8.info("[emitter] stopped", { id: emitterId });
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// src/runtime/processor.ts
|
|
1296
1922
|
var CHANNEL_NEW_BLOCK = "indexer:new_block";
|
|
1297
1923
|
var DEFAULT_CONCURRENCY = 5;
|
|
1298
1924
|
var POLL_INTERVAL_MS = 5000;
|
|
@@ -1326,7 +1952,7 @@ async function loadSubgraphDefinition(sg) {
|
|
|
1326
1952
|
knownVersions.set(sg.name, sg.version);
|
|
1327
1953
|
definitionCache.set(sg.name, def);
|
|
1328
1954
|
if (prevVersion && prevVersion !== sg.version) {
|
|
1329
|
-
|
|
1955
|
+
logger9.info("Subgraph handler reloaded", {
|
|
1330
1956
|
subgraph: sg.name,
|
|
1331
1957
|
from: prevVersion,
|
|
1332
1958
|
to: sg.version
|
|
@@ -1346,8 +1972,8 @@ function cleanupCaches(active) {
|
|
|
1346
1972
|
async function startSubgraphProcessor(opts) {
|
|
1347
1973
|
const concurrency = opts?.concurrency ?? DEFAULT_CONCURRENCY;
|
|
1348
1974
|
let running = true;
|
|
1349
|
-
|
|
1350
|
-
const targetDb =
|
|
1975
|
+
logger9.info("Starting subgraph processor", { concurrency });
|
|
1976
|
+
const targetDb = getTargetDb5();
|
|
1351
1977
|
const activeSubgraphs = (await listSubgraphs2(targetDb)).filter((v) => v.status === "active");
|
|
1352
1978
|
for (const sg of activeSubgraphs) {
|
|
1353
1979
|
try {
|
|
@@ -1358,16 +1984,16 @@ async function startSubgraphProcessor(opts) {
|
|
|
1358
1984
|
if (isHandlerNotFoundError(err)) {
|
|
1359
1985
|
await updateSubgraphStatus2(targetDb, sg.name, "error");
|
|
1360
1986
|
}
|
|
1361
|
-
|
|
1987
|
+
logger9.error("Subgraph catch-up failed on startup", {
|
|
1362
1988
|
subgraph: sg.name,
|
|
1363
1989
|
error: msg
|
|
1364
1990
|
});
|
|
1365
1991
|
}
|
|
1366
1992
|
}
|
|
1367
|
-
const stopListening = await
|
|
1993
|
+
const stopListening = await listen2(CHANNEL_NEW_BLOCK, async () => {
|
|
1368
1994
|
if (!running)
|
|
1369
1995
|
return;
|
|
1370
|
-
const db =
|
|
1996
|
+
const db = getTargetDb5();
|
|
1371
1997
|
const subgraphs = (await listSubgraphs2(db)).filter((v) => v.status === "active");
|
|
1372
1998
|
cleanupCaches(subgraphs);
|
|
1373
1999
|
for (const sg of subgraphs) {
|
|
@@ -1379,14 +2005,14 @@ async function startSubgraphProcessor(opts) {
|
|
|
1379
2005
|
if (isHandlerNotFoundError(err)) {
|
|
1380
2006
|
await updateSubgraphStatus2(db, sg.name, "error");
|
|
1381
2007
|
}
|
|
1382
|
-
|
|
2008
|
+
logger9.error("Subgraph processing failed", {
|
|
1383
2009
|
subgraph: sg.name,
|
|
1384
2010
|
error: msg
|
|
1385
2011
|
});
|
|
1386
2012
|
}
|
|
1387
2013
|
}
|
|
1388
2014
|
}, { connectionString: sourceListenerUrl() });
|
|
1389
|
-
const stopReorgListening = await
|
|
2015
|
+
const stopReorgListening = await listen2("subgraph_reorg", async (payload) => {
|
|
1390
2016
|
if (!running)
|
|
1391
2017
|
return;
|
|
1392
2018
|
try {
|
|
@@ -1396,7 +2022,7 @@ async function startSubgraphProcessor(opts) {
|
|
|
1396
2022
|
await handleSubgraphReorg(blockHeight, loadSubgraphDefinition);
|
|
1397
2023
|
}
|
|
1398
2024
|
} catch (err) {
|
|
1399
|
-
|
|
2025
|
+
logger9.error("Subgraph reorg handling failed", {
|
|
1400
2026
|
error: getErrorMessage3(err)
|
|
1401
2027
|
});
|
|
1402
2028
|
}
|
|
@@ -1404,7 +2030,7 @@ async function startSubgraphProcessor(opts) {
|
|
|
1404
2030
|
const pollInterval = setInterval(async () => {
|
|
1405
2031
|
if (!running)
|
|
1406
2032
|
return;
|
|
1407
|
-
const db =
|
|
2033
|
+
const db = getTargetDb5();
|
|
1408
2034
|
const subgraphs = (await listSubgraphs2(db)).filter((v) => v.status === "active");
|
|
1409
2035
|
cleanupCaches(subgraphs);
|
|
1410
2036
|
for (const sg of subgraphs) {
|
|
@@ -1412,25 +2038,27 @@ async function startSubgraphProcessor(opts) {
|
|
|
1412
2038
|
const def = await loadSubgraphDefinition(sg);
|
|
1413
2039
|
await catchUpSubgraph(def, sg.name);
|
|
1414
2040
|
} catch (err) {
|
|
1415
|
-
|
|
2041
|
+
logger9.error("Subgraph poll processing failed", {
|
|
1416
2042
|
subgraph: sg.name,
|
|
1417
2043
|
error: getErrorMessage3(err)
|
|
1418
2044
|
});
|
|
1419
2045
|
}
|
|
1420
2046
|
}
|
|
1421
2047
|
}, POLL_INTERVAL_MS);
|
|
1422
|
-
|
|
2048
|
+
const stopEmitter = await startEmitter();
|
|
2049
|
+
logger9.info("Subgraph processor ready");
|
|
1423
2050
|
return async () => {
|
|
1424
2051
|
running = false;
|
|
1425
2052
|
clearInterval(pollInterval);
|
|
1426
2053
|
await stopListening();
|
|
1427
2054
|
await stopReorgListening();
|
|
1428
|
-
|
|
2055
|
+
await stopEmitter();
|
|
2056
|
+
logger9.info("Subgraph processor stopped");
|
|
1429
2057
|
};
|
|
1430
2058
|
}
|
|
1431
2059
|
export {
|
|
1432
2060
|
startSubgraphProcessor
|
|
1433
2061
|
};
|
|
1434
2062
|
|
|
1435
|
-
//# debugId=
|
|
2063
|
+
//# debugId=F94CB61F26C58C5F64756E2164756E21
|
|
1436
2064
|
//# sourceMappingURL=processor.js.map
|