@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.
@@ -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
- return opsToFlush.length;
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 logger3 } from "@secondlayer/shared/logger";
861
- import { sql as sql2 } from "kysely";
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
- logger3.warn("Block not found for subgraph processing", {
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
- logger3.debug("Skipping non-canonical block", {
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 sql2.raw(`SELECT n_live_tup AS count FROM pg_stat_user_tables WHERE schemaname = '${schemaName}' AND relname = '${table}'`).execute(targetDb);
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
- logger3.warn("Subgraph table exceeds 10M rows (estimate)", {
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 logger4 } from "@secondlayer/shared/logger";
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
- logger4.info("Subgraph catch-up starting", {
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
- logger4.info("Subgraph status changed, stopping catch-up", {
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
- logger4.error("Block processing error during catch-up", {
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
- logger4.info("Subgraph catch-up progress", {
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
- logger4.warn("Failed to record subgraph gaps", {
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
- logger4.info("Subgraph catch-up complete", {
1445
+ logger5.info("Subgraph catch-up complete", {
1234
1446
  subgraph: subgraphName,
1235
1447
  processed
1236
1448
  });
@@ -1243,5 +1455,5 @@ export {
1243
1455
  catchUpSubgraph
1244
1456
  };
1245
1457
 
1246
- //# debugId=6C16DAE24C1ADF9B64756E2164756E21
1458
+ //# debugId=CC931C2EA467385764756E2164756E21
1247
1459
  //# sourceMappingURL=catchup.js.map