@secondlayer/subgraphs 1.0.0-alpha.0 → 1.0.0-beta.1
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 +524 -37
- package/dist/src/runtime/processor.js.map +11 -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 +526 -39
- package/dist/src/service.js.map +11 -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
|
|
@@ -981,5 +1193,5 @@ export {
|
|
|
981
1193
|
processBlock
|
|
982
1194
|
};
|
|
983
1195
|
|
|
984
|
-
//# debugId=
|
|
1196
|
+
//# debugId=940CDB923EE5429B64756E2164756E21
|
|
985
1197
|
//# sourceMappingURL=block-processor.js.map
|