@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.
@@ -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
  });
@@ -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 logger5 } from "@secondlayer/shared/logger";
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
- logger5.info("Propagating reorg to subgraphs", {
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
- logger5.info("Subgraph reorg cleanup done", {
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
- logger5.info("Subgraph reorg reprocessed", {
1485
+ logger6.info("Subgraph reorg reprocessed", {
1274
1486
  subgraph: sg.name,
1275
1487
  blockHeight
1276
1488
  });
1277
1489
  } catch (err) {
1278
- logger5.error("Subgraph reorg handling failed", {
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 getTargetDb4 } from "@secondlayer/shared/db";
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 logger6 } from "@secondlayer/shared/logger";
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
- logger6.info("Subgraph handler reloaded", {
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
- logger6.info("Starting subgraph processor", { concurrency });
1350
- const targetDb = getTargetDb4();
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
- logger6.error("Subgraph catch-up failed on startup", {
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 listen(CHANNEL_NEW_BLOCK, async () => {
1993
+ const stopListening = await listen2(CHANNEL_NEW_BLOCK, async () => {
1368
1994
  if (!running)
1369
1995
  return;
1370
- const db = getTargetDb4();
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
- logger6.error("Subgraph processing failed", {
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 listen("subgraph_reorg", async (payload) => {
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
- logger6.error("Subgraph reorg handling failed", {
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 = getTargetDb4();
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
- logger6.error("Subgraph poll processing failed", {
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
- logger6.info("Subgraph processor ready");
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
- logger6.info("Subgraph processor stopped");
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=F8E2C460E99ED57564756E2164756E21
2063
+ //# debugId=F94CB61F26C58C5F64756E2164756E21
1436
2064
  //# sourceMappingURL=processor.js.map