@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
@@ -1052,7 +1264,7 @@ import {
1052
1264
  recordSubgraphProcessed as recordSubgraphProcessed2,
1053
1265
  updateSubgraphStatus as updateSubgraphStatus2
1054
1266
  } from "@secondlayer/shared/db/queries/subgraphs";
1055
- import { logger as logger4 } from "@secondlayer/shared/logger";
1267
+ import { logger as logger5 } from "@secondlayer/shared/logger";
1056
1268
 
1057
1269
  // src/schema/generator.ts
1058
1270
  var TYPE_MAP = {
@@ -1225,7 +1437,7 @@ async function processBlockRange(def, opts) {
1225
1437
  while (currentHeight <= toBlock) {
1226
1438
  if (opts.signal?.aborted) {
1227
1439
  aborted = true;
1228
- logger4.info("Block processing aborted", {
1440
+ logger5.info("Block processing aborted", {
1229
1441
  subgraph: subgraphName,
1230
1442
  currentBlock: currentHeight,
1231
1443
  reason: String(opts.signal.reason ?? "unknown")
@@ -1255,7 +1467,7 @@ async function processBlockRange(def, opts) {
1255
1467
  });
1256
1468
  } catch (err) {
1257
1469
  const errorMsg = err instanceof Error ? err.message : String(err);
1258
- logger4.error("Block processing error", {
1470
+ logger5.error("Block processing error", {
1259
1471
  subgraph: subgraphName,
1260
1472
  blockHeight: height,
1261
1473
  error: errorMsg
@@ -1280,7 +1492,7 @@ async function processBlockRange(def, opts) {
1280
1492
  await updateSubgraphStatus2(targetDb, subgraphName, status, height);
1281
1493
  }
1282
1494
  if (blocksProcessed % LOG_INTERVAL === 0) {
1283
- logger4.info(`${status === "reindexing" ? "Reindex" : "Backfill"} progress`, {
1495
+ logger5.info(`${status === "reindexing" ? "Reindex" : "Backfill"} progress`, {
1284
1496
  subgraph: subgraphName,
1285
1497
  processed: blocksProcessed,
1286
1498
  total: totalBlocks,
@@ -1292,7 +1504,7 @@ async function processBlockRange(def, opts) {
1292
1504
  if (batchFailedBlocks.length > 0 && opts.subgraphId) {
1293
1505
  const gaps = coalesceFailedBlocks(batchFailedBlocks);
1294
1506
  await recordGapBatch(targetDb, opts.subgraphId, subgraphName, gaps).catch((err) => {
1295
- logger4.warn("Failed to record subgraph gaps", {
1507
+ logger5.warn("Failed to record subgraph gaps", {
1296
1508
  subgraph: subgraphName,
1297
1509
  error: err instanceof Error ? err.message : String(err)
1298
1510
  });
@@ -1329,11 +1541,11 @@ async function reindexSubgraph(def, opts) {
1329
1541
  const subgraphName = def.name;
1330
1542
  const schemaName = opts?.schemaName ?? pgSchemaName(subgraphName);
1331
1543
  await updateSubgraphStatus2(targetDb, subgraphName, "reindexing");
1332
- logger4.info("Reindex starting", { subgraph: subgraphName });
1544
+ logger5.info("Reindex starting", { subgraph: subgraphName });
1333
1545
  try {
1334
1546
  const { fromBlock, toBlock } = await resolveBlockRange(sourceDb, def, opts);
1335
1547
  if (fromBlock > toBlock) {
1336
- logger4.info("No blocks to reindex", {
1548
+ logger5.info("No blocks to reindex", {
1337
1549
  subgraph: subgraphName,
1338
1550
  fromBlock,
1339
1551
  toBlock
@@ -1347,8 +1559,8 @@ async function reindexSubgraph(def, opts) {
1347
1559
  for (const stmt of statements) {
1348
1560
  await client.unsafe(stmt);
1349
1561
  }
1350
- logger4.info("Schema recreated for reindex", { subgraph: subgraphName });
1351
- logger4.info("Reindexing blocks", {
1562
+ logger5.info("Schema recreated for reindex", { subgraph: subgraphName });
1563
+ logger5.info("Reindexing blocks", {
1352
1564
  subgraph: subgraphName,
1353
1565
  fromBlock,
1354
1566
  toBlock,
@@ -1369,9 +1581,9 @@ async function reindexSubgraph(def, opts) {
1369
1581
  if (reason === "user-cancelled") {
1370
1582
  await updateSubgraphStatus2(targetDb, subgraphName, "active");
1371
1583
  await clearReindexMetadata(targetDb, subgraphName);
1372
- logger4.info("Reindex cancelled by user", { subgraph: subgraphName });
1584
+ logger5.info("Reindex cancelled by user", { subgraph: subgraphName });
1373
1585
  } else {
1374
- logger4.info("Reindex interrupted by shutdown, will resume", {
1586
+ logger5.info("Reindex interrupted by shutdown, will resume", {
1375
1587
  subgraph: subgraphName
1376
1588
  });
1377
1589
  }
@@ -1383,7 +1595,7 @@ async function reindexSubgraph(def, opts) {
1383
1595
  }
1384
1596
  await updateSubgraphStatus2(targetDb, subgraphName, "active", toBlock);
1385
1597
  await clearReindexMetadata(targetDb, subgraphName);
1386
- logger4.info("Reindex complete", {
1598
+ logger5.info("Reindex complete", {
1387
1599
  subgraph: subgraphName,
1388
1600
  blocks: result.blocksProcessed,
1389
1601
  events: result.totalEventsProcessed,
@@ -1391,7 +1603,7 @@ async function reindexSubgraph(def, opts) {
1391
1603
  });
1392
1604
  return { processed: result.blocksProcessed };
1393
1605
  } catch (err) {
1394
- logger4.error("Reindex failed", {
1606
+ logger5.error("Reindex failed", {
1395
1607
  subgraph: subgraphName,
1396
1608
  error: getErrorMessage2(err)
1397
1609
  });
@@ -1411,7 +1623,7 @@ async function resumeReindex(def, opts) {
1411
1623
  if (!row)
1412
1624
  throw new Error(`Subgraph "${subgraphName}" not found`);
1413
1625
  if (row.reindex_from_block == null || row.reindex_to_block == null) {
1414
- logger4.info("No reindex metadata, starting fresh reindex", {
1626
+ logger5.info("No reindex metadata, starting fresh reindex", {
1415
1627
  subgraph: subgraphName
1416
1628
  });
1417
1629
  return reindexSubgraph(def, {
@@ -1422,12 +1634,12 @@ async function resumeReindex(def, opts) {
1422
1634
  const fromBlock = Math.max(row.last_processed_block + 1, row.reindex_from_block);
1423
1635
  const toBlock = row.reindex_to_block;
1424
1636
  if (fromBlock > toBlock) {
1425
- logger4.info("Resume: no remaining blocks", { subgraph: subgraphName });
1637
+ logger5.info("Resume: no remaining blocks", { subgraph: subgraphName });
1426
1638
  await updateSubgraphStatus2(targetDb, subgraphName, "active", toBlock);
1427
1639
  await clearReindexMetadata(targetDb, subgraphName);
1428
1640
  return { processed: 0 };
1429
1641
  }
1430
- logger4.info("Resuming reindex", {
1642
+ logger5.info("Resuming reindex", {
1431
1643
  subgraph: subgraphName,
1432
1644
  fromBlock,
1433
1645
  toBlock,
@@ -1448,9 +1660,9 @@ async function resumeReindex(def, opts) {
1448
1660
  if (reason === "user-cancelled") {
1449
1661
  await updateSubgraphStatus2(targetDb, subgraphName, "active");
1450
1662
  await clearReindexMetadata(targetDb, subgraphName);
1451
- logger4.info("Resume cancelled by user", { subgraph: subgraphName });
1663
+ logger5.info("Resume cancelled by user", { subgraph: subgraphName });
1452
1664
  } else {
1453
- logger4.info("Resume interrupted by shutdown, will resume again", {
1665
+ logger5.info("Resume interrupted by shutdown, will resume again", {
1454
1666
  subgraph: subgraphName
1455
1667
  });
1456
1668
  }
@@ -1462,13 +1674,13 @@ async function resumeReindex(def, opts) {
1462
1674
  }
1463
1675
  await updateSubgraphStatus2(targetDb, subgraphName, "active", toBlock);
1464
1676
  await clearReindexMetadata(targetDb, subgraphName);
1465
- logger4.info("Resumed reindex complete", {
1677
+ logger5.info("Resumed reindex complete", {
1466
1678
  subgraph: subgraphName,
1467
1679
  blocks: result.blocksProcessed
1468
1680
  });
1469
1681
  return { processed: result.blocksProcessed };
1470
1682
  } catch (err) {
1471
- logger4.error("Resumed reindex failed", {
1683
+ logger5.error("Resumed reindex failed", {
1472
1684
  subgraph: subgraphName,
1473
1685
  error: getErrorMessage2(err)
1474
1686
  });
@@ -1479,7 +1691,7 @@ async function resumeReindex(def, opts) {
1479
1691
  async function backfillSubgraph(def, opts) {
1480
1692
  const targetDb = getTargetDb2();
1481
1693
  const subgraphName = def.name;
1482
- logger4.info("Backfill starting", {
1694
+ logger5.info("Backfill starting", {
1483
1695
  subgraph: subgraphName,
1484
1696
  from: opts.fromBlock,
1485
1697
  to: opts.toBlock
@@ -1496,12 +1708,12 @@ async function backfillSubgraph(def, opts) {
1496
1708
  signal: opts.signal
1497
1709
  });
1498
1710
  if (result.aborted) {
1499
- logger4.info("Backfill aborted", { subgraph: subgraphName });
1711
+ logger5.info("Backfill aborted", { subgraph: subgraphName });
1500
1712
  return { processed: result.blocksProcessed };
1501
1713
  }
1502
1714
  const resolved = await resolveGaps(targetDb, subgraphName, opts.fromBlock, opts.toBlock).catch(() => 0);
1503
1715
  if (resolved > 0) {
1504
- logger4.info("Resolved subgraph gaps via backfill", {
1716
+ logger5.info("Resolved subgraph gaps via backfill", {
1505
1717
  subgraph: subgraphName,
1506
1718
  resolved
1507
1719
  });
@@ -1510,7 +1722,7 @@ async function backfillSubgraph(def, opts) {
1510
1722
  if (result.totalEventsProcessed > 0 || result.totalErrors > 0) {
1511
1723
  await recordSubgraphProcessed3(targetDb, subgraphName, result.totalEventsProcessed, result.totalErrors, result.totalErrors > 0 ? `${result.totalErrors} error(s) during backfill` : undefined);
1512
1724
  }
1513
- logger4.info("Backfill complete", {
1725
+ logger5.info("Backfill complete", {
1514
1726
  subgraph: subgraphName,
1515
1727
  blocks: result.blocksProcessed,
1516
1728
  events: result.totalEventsProcessed,
@@ -1518,7 +1730,7 @@ async function backfillSubgraph(def, opts) {
1518
1730
  });
1519
1731
  return { processed: result.blocksProcessed };
1520
1732
  } catch (err) {
1521
- logger4.error("Backfill failed", {
1733
+ logger5.error("Backfill failed", {
1522
1734
  subgraph: subgraphName,
1523
1735
  error: getErrorMessage2(err)
1524
1736
  });
@@ -1531,5 +1743,5 @@ export {
1531
1743
  backfillSubgraph
1532
1744
  };
1533
1745
 
1534
- //# debugId=CA98B860698EE36C64756E2164756E21
1746
+ //# debugId=A2112359A7EA0BBD64756E2164756E21
1535
1747
  //# sourceMappingURL=reindex.js.map