@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.
- package/README.md +9 -9
- package/dist/src/index.js +11 -11
- package/dist/src/index.js.map +9 -9
- package/dist/src/runtime/block-processor.js +11 -11
- package/dist/src/runtime/block-processor.js.map +9 -9
- package/dist/src/runtime/catchup.js +11 -11
- package/dist/src/runtime/catchup.js.map +9 -9
- package/dist/src/runtime/context.d.ts +1 -1
- package/dist/src/runtime/context.js.map +1 -1
- package/dist/src/runtime/emitter.d.ts +10 -10
- package/dist/src/runtime/emitter.js +68 -44
- package/dist/src/runtime/emitter.js.map +14 -13
- package/dist/src/runtime/processor.js +64 -36
- package/dist/src/runtime/processor.js.map +13 -12
- package/dist/src/runtime/reindex.js +11 -11
- package/dist/src/runtime/reindex.js.map +9 -9
- package/dist/src/runtime/reorg.d.ts +2 -2
- package/dist/src/runtime/reorg.js +17 -17
- package/dist/src/runtime/reorg.js.map +10 -10
- package/dist/src/runtime/replay.d.ts +6 -6
- package/dist/src/runtime/replay.js +26 -26
- package/dist/src/runtime/replay.js.map +4 -4
- package/dist/src/runtime/runner.d.ts +1 -1
- package/dist/src/service.js +79 -36
- package/dist/src/service.js.map +14 -13
- package/dist/src/testing/index.js.map +1 -1
- package/dist/src/triggers/index.d.ts +2 -2
- package/dist/src/triggers/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -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 {
|
|
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
|
|
164
|
+
logger.warn("Unknown webhook format, falling back to standard-webhooks", {
|
|
165
165
|
format: sub.format,
|
|
166
|
-
|
|
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/
|
|
181
|
-
|
|
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
|
|
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/
|
|
344
|
-
var matcher = new
|
|
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
|
|
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
|
|
359
|
-
var LOCK_WINDOW_MS =
|
|
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
|
|
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
|
|
556
|
+
var __postToWebhookForTest = postToWebhook;
|
|
537
557
|
async function dispatchOne(db, outboxRow, sub) {
|
|
538
|
-
const { body, headers } = buildForFormat(outboxRow, sub,
|
|
539
|
-
const r = await
|
|
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("
|
|
561
|
+
await db.insertInto("webhook_deliveries").values({
|
|
542
562
|
outbox_id: outboxRow.id,
|
|
543
|
-
|
|
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
|
-
|
|
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,
|
|
591
|
-
const r = await
|
|
592
|
-
const inserted = await db.insertInto("
|
|
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
|
-
|
|
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("
|
|
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("
|
|
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.
|
|
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("
|
|
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
|
|
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("
|
|
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("
|
|
659
|
-
|
|
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
|
|
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
|
|
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("
|
|
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.
|
|
729
|
+
const arr = bySubId.get(row.webhook_id);
|
|
708
730
|
if (arr)
|
|
709
731
|
arr.push(row);
|
|
710
732
|
else
|
|
711
|
-
bySubId.set(row.
|
|
733
|
+
bySubId.set(row.webhook_id, [row]);
|
|
712
734
|
}
|
|
713
735
|
const subIds = Array.from(bySubId.keys());
|
|
714
|
-
const subs = await db.selectFrom("
|
|
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
|
|
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
|
|
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
|
|
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("
|
|
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("
|
|
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
|
-
|
|
850
|
-
|
|
873
|
+
__postToWebhookForTest,
|
|
874
|
+
MAX_WEBHOOK_TIMEOUT_MS,
|
|
851
875
|
MAX_REDIRECT_HOPS,
|
|
852
876
|
LOCK_WINDOW_MS
|
|
853
877
|
};
|
|
854
878
|
|
|
855
|
-
//# debugId=
|
|
879
|
+
//# debugId=51FFE04872B2ADAD64756E2164756E21
|
|
856
880
|
//# sourceMappingURL=emitter.js.map
|