@secondlayer/subgraphs 4.2.0 → 4.3.0

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.
@@ -7,7 +7,7 @@ import { lookup as dnsLookup } from "node:dns/promises";
7
7
  import {
8
8
  getTargetDb
9
9
  } from "@secondlayer/shared/db";
10
- import { getSubscriptionSigningSecret } from "@secondlayer/shared/db/queries/subscriptions";
10
+ import { getWebhookSigningSecret } from "@secondlayer/shared/db/queries/webhooks";
11
11
  import { logger as logger2 } from "@secondlayer/shared/logger";
12
12
  import { listen, targetListenerUrl } from "@secondlayer/shared/queue/listener";
13
13
  import { sql as sql2 } from "kysely";
@@ -161,9 +161,9 @@ function buildBody(outboxRow, sub, signingSecret) {
161
161
  case "standard-webhooks":
162
162
  return buildStandardWebhooks(outboxRow, signingSecret);
163
163
  default:
164
- logger.warn("Unknown subscription format, falling back to standard-webhooks", {
164
+ logger.warn("Unknown webhook format, falling back to standard-webhooks", {
165
165
  format: sub.format,
166
- subscriptionId: sub.id
166
+ webhookId: sub.id
167
167
  });
168
168
  return buildStandardWebhooks(outboxRow, signingSecret);
169
169
  }
@@ -177,8 +177,28 @@ function buildForFormat(outboxRow, sub, signingSecret) {
177
177
  return result;
178
178
  }
179
179
 
180
- // src/runtime/subscription-state.ts
181
- import { listSubscriptions } from "@secondlayer/shared/db/queries/subscriptions";
180
+ // src/runtime/hosted-meter.ts
181
+ var hooks = {};
182
+ function setHostedMeterHooks(next) {
183
+ hooks = next;
184
+ }
185
+ async function meterBlocksProcessed(accountId, blocks, subgraphName) {
186
+ if (!accountId || blocks <= 0)
187
+ return true;
188
+ if (!hooks.onBlocksProcessed)
189
+ return true;
190
+ return hooks.onBlocksProcessed(accountId, blocks, subgraphName);
191
+ }
192
+ async function meterDeliveryAttempt(accountId, webhookId) {
193
+ if (!accountId)
194
+ return true;
195
+ if (!hooks.onDeliveryAttempt)
196
+ return true;
197
+ return hooks.onDeliveryAttempt(accountId, webhookId);
198
+ }
199
+
200
+ // src/runtime/webhook-state.ts
201
+ import { listWebhooks } from "@secondlayer/shared/db/queries/webhooks";
182
202
  import { sql } from "kysely";
183
203
 
184
204
  // src/runtime/emitter-matcher.ts
@@ -298,7 +318,7 @@ function key(subgraphName, tableName) {
298
318
  return `${subgraphName}\x00${tableName}`;
299
319
  }
300
320
 
301
- class SubscriptionMatcher {
321
+ class WebhookMatcher {
302
322
  byKey = new Map;
303
323
  byId = new Map;
304
324
  setAll(subs) {
@@ -340,11 +360,11 @@ class SubscriptionMatcher {
340
360
  }
341
361
  }
342
362
 
343
- // src/runtime/subscription-state.ts
344
- var matcher = new SubscriptionMatcher;
363
+ // src/runtime/webhook-state.ts
364
+ var matcher = new WebhookMatcher;
345
365
  async function refreshMatcher(db) {
346
366
  const rows = await sql`
347
- SELECT * FROM subscriptions WHERE status = 'active'
367
+ SELECT * FROM webhooks WHERE status = 'active'
348
368
  `.execute(db);
349
369
  matcher.setAll(rows.rows);
350
370
  return matcher.size();
@@ -355,8 +375,8 @@ var BATCH_SIZE = 50;
355
375
  var LIVE_SHARE = 0.9;
356
376
  var BACKOFF_SECONDS = [30, 120, 600, 3600, 21600, 86400, 259200];
357
377
  var CIRCUIT_THRESHOLD = 20;
358
- var MAX_SUBSCRIPTION_TIMEOUT_MS = 300000;
359
- var LOCK_WINDOW_MS = MAX_SUBSCRIPTION_TIMEOUT_MS + 60000;
378
+ var MAX_WEBHOOK_TIMEOUT_MS = 300000;
379
+ var LOCK_WINDOW_MS = MAX_WEBHOOK_TIMEOUT_MS + 60000;
360
380
  function nextDelaySeconds(attempt) {
361
381
  return BACKOFF_SECONDS[Math.min(attempt, BACKOFF_SECONDS.length - 1)];
362
382
  }
@@ -464,7 +484,7 @@ async function checkEgressAllowed(url) {
464
484
  return null;
465
485
  }
466
486
  var MAX_REDIRECT_HOPS = 3;
467
- async function postToSubscription(url, body, headers, timeoutMs) {
487
+ async function postToWebhook(url, body, headers, timeoutMs) {
468
488
  const start = performance.now();
469
489
  let target = url;
470
490
  let statusCode = null;
@@ -533,14 +553,14 @@ async function postToSubscription(url, body, headers, timeoutMs) {
533
553
  responseHeaders
534
554
  };
535
555
  }
536
- var __postToSubscriptionForTest = postToSubscription;
556
+ var __postToWebhookForTest = postToWebhook;
537
557
  async function dispatchOne(db, outboxRow, sub) {
538
- const { body, headers } = buildForFormat(outboxRow, sub, getSubscriptionSigningSecret(sub));
539
- const r = await postToSubscription(sub.url, body, headers, sub.timeout_ms);
558
+ const { body, headers } = buildForFormat(outboxRow, sub, getWebhookSigningSecret(sub));
559
+ const r = await postToWebhook(sub.url, body, headers, sub.timeout_ms);
540
560
  const attempt = outboxRow.attempt + 1;
541
- await db.insertInto("subscription_deliveries").values({
561
+ await db.insertInto("webhook_deliveries").values({
542
562
  outbox_id: outboxRow.id,
543
- subscription_id: outboxRow.subscription_id,
563
+ webhook_id: outboxRow.webhook_id,
544
564
  attempt,
545
565
  status_code: r.statusCode,
546
566
  response_headers: r.responseHeaders,
@@ -548,6 +568,7 @@ async function dispatchOne(db, outboxRow, sub) {
548
568
  error_message: r.error,
549
569
  duration_ms: r.durationMs
550
570
  }).execute();
571
+ await meterDeliveryAttempt(sub.account_id, sub.id);
551
572
  return {
552
573
  ok: r.ok,
553
574
  statusCode: r.statusCode,
@@ -559,7 +580,7 @@ function buildTestOutboxRow(sub) {
559
580
  const now = new Date;
560
581
  return {
561
582
  id: randomUUID(),
562
- subscription_id: sub.id,
583
+ webhook_id: sub.id,
563
584
  kind: sub.kind,
564
585
  subgraph_name: sub.subgraph_name ?? null,
565
586
  table_name: sub.table_name ?? null,
@@ -570,6 +591,7 @@ function buildTestOutboxRow(sub) {
570
591
  payload: {
571
592
  test: true,
572
593
  message: "Secondlayer test delivery",
594
+ webhook_id: sub.id,
573
595
  subscription_id: sub.id,
574
596
  sent_at: now.toISOString()
575
597
  },
@@ -587,11 +609,11 @@ function buildTestOutboxRow(sub) {
587
609
  }
588
610
  async function deliverTestEvent(db, sub) {
589
611
  const testRow = buildTestOutboxRow(sub);
590
- const { body, headers } = buildForFormat(testRow, sub, getSubscriptionSigningSecret(sub));
591
- const r = await postToSubscription(sub.url, body, headers, sub.timeout_ms);
592
- const inserted = await db.insertInto("subscription_deliveries").values({
612
+ const { body, headers } = buildForFormat(testRow, sub, getWebhookSigningSecret(sub));
613
+ const r = await postToWebhook(sub.url, body, headers, sub.timeout_ms);
614
+ const inserted = await db.insertInto("webhook_deliveries").values({
593
615
  outbox_id: null,
594
- subscription_id: sub.id,
616
+ webhook_id: sub.id,
595
617
  attempt: 1,
596
618
  status_code: r.statusCode,
597
619
  response_headers: r.responseHeaders,
@@ -609,20 +631,20 @@ async function deliverTestEvent(db, sub) {
609
631
  }
610
632
  async function settleDelivered(db, outboxRow) {
611
633
  await db.transaction().execute(async (tx) => {
612
- await tx.updateTable("subscription_outbox").set({
634
+ await tx.updateTable("webhook_outbox").set({
613
635
  status: "delivered",
614
636
  delivered_at: new Date,
615
637
  attempt: outboxRow.attempt + 1,
616
638
  locked_by: null,
617
639
  locked_until: null
618
640
  }).where("id", "=", outboxRow.id).execute();
619
- await tx.updateTable("subscriptions").set({
641
+ await tx.updateTable("webhooks").set({
620
642
  last_delivery_at: new Date,
621
643
  last_success_at: new Date,
622
644
  circuit_failures: 0,
623
645
  last_error: null,
624
646
  updated_at: new Date
625
- }).where("id", "=", outboxRow.subscription_id).execute();
647
+ }).where("id", "=", outboxRow.webhook_id).execute();
626
648
  });
627
649
  }
628
650
  async function settleFailed(db, outboxRow, sub, errText) {
@@ -630,7 +652,7 @@ async function settleFailed(db, outboxRow, sub, errText) {
630
652
  const isDead = attempt >= sub.max_retries;
631
653
  const nextAt = isDead ? null : new Date(Date.now() + nextDelaySeconds(outboxRow.attempt) * 1000);
632
654
  await db.transaction().execute(async (tx) => {
633
- await tx.updateTable("subscription_outbox").set({
655
+ await tx.updateTable("webhook_outbox").set({
634
656
  attempt,
635
657
  next_attempt_at: nextAt ?? new Date,
636
658
  status: isDead ? "dead" : "pending",
@@ -639,7 +661,7 @@ async function settleFailed(db, outboxRow, sub, errText) {
639
661
  locked_until: null
640
662
  }).where("id", "=", outboxRow.id).execute();
641
663
  const incResult = await sql2`
642
- UPDATE subscriptions
664
+ UPDATE webhooks
643
665
  SET circuit_failures = circuit_failures + 1,
644
666
  last_delivery_at = NOW(),
645
667
  last_error = ${errText.slice(0, 500)},
@@ -650,13 +672,13 @@ async function settleFailed(db, outboxRow, sub, errText) {
650
672
  const newFailures = incResult.rows[0]?.circuit_failures ?? sub.circuit_failures + 1;
651
673
  const shouldTripCircuit = newFailures >= CIRCUIT_THRESHOLD;
652
674
  if (shouldTripCircuit) {
653
- await tx.updateTable("subscriptions").set({
675
+ await tx.updateTable("webhooks").set({
654
676
  status: "paused",
655
677
  circuit_opened_at: new Date,
656
678
  updated_at: new Date
657
679
  }).where("id", "=", sub.id).execute();
658
- logger2.warn("Subscription circuit tripped — paused after consecutive failures", {
659
- subscription: sub.name,
680
+ logger2.warn("Webhook circuit tripped — paused after consecutive failures", {
681
+ webhook: sub.name,
660
682
  failures: newFailures
661
683
  });
662
684
  }
@@ -671,7 +693,7 @@ async function claimAndDrain(db, state, emitterId) {
671
693
  const replayLimit = BATCH_SIZE - liveLimit;
672
694
  const claimed = await db.transaction().execute(async (tx) => {
673
695
  const live = await sql2`
674
- SELECT * FROM subscription_outbox
696
+ SELECT * FROM webhook_outbox
675
697
  WHERE status = 'pending'
676
698
  AND next_attempt_at <= NOW()
677
699
  AND is_replay = FALSE
@@ -680,7 +702,7 @@ async function claimAndDrain(db, state, emitterId) {
680
702
  LIMIT ${sql2.lit(liveLimit)}
681
703
  `.execute(tx);
682
704
  const replay = await sql2`
683
- SELECT * FROM subscription_outbox
705
+ SELECT * FROM webhook_outbox
684
706
  WHERE status = 'pending'
685
707
  AND next_attempt_at <= NOW()
686
708
  AND is_replay = TRUE
@@ -693,7 +715,7 @@ async function claimAndDrain(db, state, emitterId) {
693
715
  return [];
694
716
  const now = new Date;
695
717
  const lockUntil = new Date(now.getTime() + LOCK_WINDOW_MS);
696
- await tx.updateTable("subscription_outbox").set({
718
+ await tx.updateTable("webhook_outbox").set({
697
719
  locked_by: emitterId,
698
720
  locked_until: lockUntil,
699
721
  next_attempt_at: lockUntil
@@ -704,14 +726,14 @@ async function claimAndDrain(db, state, emitterId) {
704
726
  return 0;
705
727
  const bySubId = new Map;
706
728
  for (const row of claimed) {
707
- const arr = bySubId.get(row.subscription_id);
729
+ const arr = bySubId.get(row.webhook_id);
708
730
  if (arr)
709
731
  arr.push(row);
710
732
  else
711
- bySubId.set(row.subscription_id, [row]);
733
+ bySubId.set(row.webhook_id, [row]);
712
734
  }
713
735
  const subIds = Array.from(bySubId.keys());
714
- const subs = await db.selectFrom("subscriptions").selectAll().where("id", "in", subIds).execute();
736
+ const subs = await db.selectFrom("webhooks").selectAll().where("id", "in", subIds).execute();
715
737
  const subById = new Map(subs.map((s) => [s.id, s]));
716
738
  await Promise.all(subIds.map((subId) => drainForSub(db, state, subById.get(subId), bySubId.get(subId))));
717
739
  return claimed.length;
@@ -720,6 +742,8 @@ async function claimAndDrain(db, state, emitterId) {
720
742
  }
721
743
  }
722
744
  async function drainForSub(db, state, sub, rows) {
745
+ if (sub.status !== "active")
746
+ return;
723
747
  const cap = sub.concurrency || 4;
724
748
  const counter = () => state.inFlightBySub.get(sub.id) ?? 0;
725
749
  const inc = () => state.inFlightBySub.set(sub.id, counter() + 1);
@@ -758,15 +782,15 @@ async function drainForSub(db, state, sub, rows) {
758
782
  }
759
783
  async function runRetention(db) {
760
784
  await sql2`
761
- DELETE FROM subscription_outbox
785
+ DELETE FROM webhook_outbox
762
786
  WHERE status = 'delivered' AND delivered_at < NOW() - interval '7 days'
763
787
  `.execute(db);
764
788
  await sql2`
765
- DELETE FROM subscription_deliveries
789
+ DELETE FROM webhook_deliveries
766
790
  WHERE dispatched_at < NOW() - interval '30 days'
767
791
  `.execute(db);
768
792
  await sql2`
769
- DELETE FROM subscription_outbox
793
+ DELETE FROM webhook_outbox
770
794
  WHERE status = 'dead' AND failed_at < NOW() - interval '90 days'
771
795
  `.execute(db);
772
796
  }
@@ -803,14 +827,14 @@ async function startEmitter(opts) {
803
827
  throw new Error(`[emitter] matcher refresh failed ${MATCHER_BOOT_ATTEMPTS}×; aborting boot: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`);
804
828
  }
805
829
  const listenUrl = targetListenerUrl();
806
- const stopNew = await listen("subscriptions:new_outbox", () => {
830
+ const stopNew = await listen("webhooks:new_outbox", () => {
807
831
  if (!state.running)
808
832
  return;
809
833
  claimAndDrain(db, state, emitterId).catch((err) => logger2.error("[emitter] claim failed", {
810
834
  error: err instanceof Error ? err.message : String(err)
811
835
  }));
812
836
  }, { connectionString: listenUrl });
813
- const stopChanged = await listen("subscriptions:changed", () => {
837
+ const stopChanged = await listen("webhooks:changed", () => {
814
838
  if (!state.running)
815
839
  return;
816
840
  refreshMatcher(db).catch((err) => logger2.error("[emitter] matcher refresh failed", {
@@ -846,11 +870,11 @@ export {
846
870
  deliverTestEvent,
847
871
  checkEgressAllowed,
848
872
  __setDnsLookupForTest,
849
- __postToSubscriptionForTest,
850
- MAX_SUBSCRIPTION_TIMEOUT_MS,
873
+ __postToWebhookForTest,
874
+ MAX_WEBHOOK_TIMEOUT_MS,
851
875
  MAX_REDIRECT_HOPS,
852
876
  LOCK_WINDOW_MS
853
877
  };
854
878
 
855
- //# debugId=C6066230FC5B9BFF64756E2164756E21
879
+ //# debugId=51FFE04872B2ADAD64756E2164756E21
856
880
  //# sourceMappingURL=emitter.js.map