@secondlayer/subgraphs 6.6.1 → 7.0.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 +5 -6
- package/dist/index.d.ts +3 -2
- package/dist/index.js +4 -4
- package/dist/index.js.map +2 -2
- package/dist/runtime/emitter.js +15 -210
- package/dist/runtime/emitter.js.map +8 -10
- package/dist/runtime/replay.d.ts +5 -5
- package/dist/runtime/replay.js +5 -95
- package/dist/runtime/replay.js.map +6 -7
- package/dist/schema/index.js +4 -4
- package/dist/schema/index.js.map +1 -1
- package/dist/testing/index.js +4 -4
- package/dist/testing/index.js.map +3 -3
- package/dist/verification.js +4 -4
- package/dist/verification.js.map +1 -1
- package/dist/verify/index.js +4 -4
- package/dist/verify/index.js.map +3 -3
- package/package.json +3 -3
package/dist/runtime/emitter.js
CHANGED
|
@@ -11,18 +11,21 @@ import { listen, targetListenerUrl } from "@secondlayer/shared/queue/listener";
|
|
|
11
11
|
import {
|
|
12
12
|
webhookTimeoutMsCeiling
|
|
13
13
|
} from "@secondlayer/shared/schemas/webhooks";
|
|
14
|
-
import { sql
|
|
14
|
+
import { sql } from "kysely";
|
|
15
15
|
|
|
16
16
|
// src/runtime/formats/index.ts
|
|
17
17
|
import { signSecondlayerWebhook } from "@secondlayer/shared/crypto/secondlayer-webhook";
|
|
18
18
|
import { logger } from "@secondlayer/shared/logger";
|
|
19
19
|
|
|
20
20
|
// src/runtime/formats/cloudevents.ts
|
|
21
|
+
function chainOf(payload) {
|
|
22
|
+
return payload?.chain === "bitcoin" ? "bitcoin" : "stacks";
|
|
23
|
+
}
|
|
21
24
|
function buildCloudEvents(outboxRow, _sub) {
|
|
22
25
|
const event = {
|
|
23
26
|
specversion: "1.0",
|
|
24
27
|
type: outboxRow.event_type,
|
|
25
|
-
source: `secondlayer:${outboxRow.
|
|
28
|
+
source: `secondlayer:${chainOf(outboxRow.payload)}`,
|
|
26
29
|
id: outboxRow.id,
|
|
27
30
|
time: new Date(outboxRow.created_at).toISOString(),
|
|
28
31
|
datacontenttype: "application/json",
|
|
@@ -191,178 +194,6 @@ function recordDelivery() {
|
|
|
191
194
|
pending += 1;
|
|
192
195
|
}
|
|
193
196
|
|
|
194
|
-
// src/runtime/webhook-state.ts
|
|
195
|
-
import { sql } from "kysely";
|
|
196
|
-
|
|
197
|
-
// src/runtime/emitter-matcher.ts
|
|
198
|
-
function isPrimitive(v) {
|
|
199
|
-
const t = typeof v;
|
|
200
|
-
return t === "string" || t === "number" || t === "boolean";
|
|
201
|
-
}
|
|
202
|
-
function coerceBigInt(v) {
|
|
203
|
-
if (typeof v === "bigint")
|
|
204
|
-
return v;
|
|
205
|
-
if (typeof v === "number") {
|
|
206
|
-
if (!Number.isFinite(v))
|
|
207
|
-
return null;
|
|
208
|
-
if (!Number.isInteger(v))
|
|
209
|
-
return null;
|
|
210
|
-
return BigInt(v);
|
|
211
|
-
}
|
|
212
|
-
if (typeof v === "string" && /^-?\d+$/.test(v)) {
|
|
213
|
-
try {
|
|
214
|
-
return BigInt(v);
|
|
215
|
-
} catch {
|
|
216
|
-
return null;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
return null;
|
|
220
|
-
}
|
|
221
|
-
function coerceFloat(v) {
|
|
222
|
-
if (typeof v === "number" && Number.isFinite(v))
|
|
223
|
-
return v;
|
|
224
|
-
if (typeof v === "bigint")
|
|
225
|
-
return Number(v);
|
|
226
|
-
if (typeof v === "string" && v !== "" && !Number.isNaN(Number(v))) {
|
|
227
|
-
return Number(v);
|
|
228
|
-
}
|
|
229
|
-
return null;
|
|
230
|
-
}
|
|
231
|
-
function compareNumeric(a, b) {
|
|
232
|
-
const ba = coerceBigInt(a);
|
|
233
|
-
const bb = coerceBigInt(b);
|
|
234
|
-
if (ba !== null && bb !== null) {
|
|
235
|
-
if (ba === bb)
|
|
236
|
-
return 0;
|
|
237
|
-
return ba > bb ? 1 : -1;
|
|
238
|
-
}
|
|
239
|
-
const fa = coerceFloat(a);
|
|
240
|
-
const fb = coerceFloat(b);
|
|
241
|
-
if (fa === null || fb === null)
|
|
242
|
-
return null;
|
|
243
|
-
if (fa === fb)
|
|
244
|
-
return 0;
|
|
245
|
-
return fa > fb ? 1 : -1;
|
|
246
|
-
}
|
|
247
|
-
function matchClause(rowValue, clause) {
|
|
248
|
-
const rowIsPrimitive = isPrimitive(rowValue) || typeof rowValue === "bigint";
|
|
249
|
-
if (isPrimitive(clause)) {
|
|
250
|
-
if (!rowIsPrimitive)
|
|
251
|
-
return false;
|
|
252
|
-
if (typeof clause === "number" || typeof rowValue === "number" || typeof rowValue === "bigint") {
|
|
253
|
-
const cmp = compareNumeric(rowValue, clause);
|
|
254
|
-
if (cmp !== null)
|
|
255
|
-
return cmp === 0;
|
|
256
|
-
}
|
|
257
|
-
return rowValue === clause || String(rowValue) === String(clause);
|
|
258
|
-
}
|
|
259
|
-
if (clause === null || typeof clause !== "object" || Array.isArray(clause)) {
|
|
260
|
-
return false;
|
|
261
|
-
}
|
|
262
|
-
const keys = Object.keys(clause);
|
|
263
|
-
if (keys.length !== 1)
|
|
264
|
-
return false;
|
|
265
|
-
const op = keys[0];
|
|
266
|
-
const c = clause;
|
|
267
|
-
switch (op) {
|
|
268
|
-
case "eq":
|
|
269
|
-
return matchClause(rowValue, c.eq);
|
|
270
|
-
case "neq":
|
|
271
|
-
return !matchClause(rowValue, c.neq);
|
|
272
|
-
case "gt":
|
|
273
|
-
case "gte":
|
|
274
|
-
case "lt":
|
|
275
|
-
case "lte": {
|
|
276
|
-
if (!rowIsPrimitive)
|
|
277
|
-
return false;
|
|
278
|
-
const cmp = compareNumeric(rowValue, c[op]);
|
|
279
|
-
if (cmp === null)
|
|
280
|
-
return false;
|
|
281
|
-
if (op === "gt")
|
|
282
|
-
return cmp > 0;
|
|
283
|
-
if (op === "gte")
|
|
284
|
-
return cmp >= 0;
|
|
285
|
-
if (op === "lt")
|
|
286
|
-
return cmp < 0;
|
|
287
|
-
return cmp <= 0;
|
|
288
|
-
}
|
|
289
|
-
case "in": {
|
|
290
|
-
const list = c.in;
|
|
291
|
-
if (!Array.isArray(list))
|
|
292
|
-
return false;
|
|
293
|
-
if (!rowIsPrimitive)
|
|
294
|
-
return false;
|
|
295
|
-
return list.some((item) => isPrimitive(item) && matchClause(rowValue, item));
|
|
296
|
-
}
|
|
297
|
-
default:
|
|
298
|
-
return false;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
function matchesFilter(filter, row) {
|
|
302
|
-
if (!filter || Object.keys(filter).length === 0)
|
|
303
|
-
return true;
|
|
304
|
-
for (const [col, clause] of Object.entries(filter)) {
|
|
305
|
-
if (!matchClause(row[col], clause))
|
|
306
|
-
return false;
|
|
307
|
-
}
|
|
308
|
-
return true;
|
|
309
|
-
}
|
|
310
|
-
function key(subgraphName, tableName) {
|
|
311
|
-
return `${subgraphName}\x00${tableName}`;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
class WebhookMatcher {
|
|
315
|
-
byKey = new Map;
|
|
316
|
-
byId = new Map;
|
|
317
|
-
setAll(subs) {
|
|
318
|
-
this.byKey.clear();
|
|
319
|
-
this.byId.clear();
|
|
320
|
-
for (const sub of subs) {
|
|
321
|
-
if (sub.status !== "active")
|
|
322
|
-
continue;
|
|
323
|
-
if (sub.kind !== "subgraph" || !sub.subgraph_name || !sub.table_name)
|
|
324
|
-
continue;
|
|
325
|
-
this.byId.set(sub.id, sub);
|
|
326
|
-
const k = key(sub.subgraph_name, sub.table_name);
|
|
327
|
-
const arr = this.byKey.get(k);
|
|
328
|
-
if (arr)
|
|
329
|
-
arr.push(sub);
|
|
330
|
-
else
|
|
331
|
-
this.byKey.set(k, [sub]);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
match(subgraphName, tableName, row) {
|
|
335
|
-
const bucket = this.byKey.get(key(subgraphName, tableName));
|
|
336
|
-
if (!bucket)
|
|
337
|
-
return [];
|
|
338
|
-
const hits = [];
|
|
339
|
-
for (const sub of bucket) {
|
|
340
|
-
if (matchesFilter(sub.filter, row))
|
|
341
|
-
hits.push(sub);
|
|
342
|
-
}
|
|
343
|
-
return hits;
|
|
344
|
-
}
|
|
345
|
-
has(subgraphName, tableName) {
|
|
346
|
-
return this.byKey.has(key(subgraphName, tableName));
|
|
347
|
-
}
|
|
348
|
-
size() {
|
|
349
|
-
return this.byId.size;
|
|
350
|
-
}
|
|
351
|
-
get(id) {
|
|
352
|
-
return this.byId.get(id);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// src/runtime/webhook-state.ts
|
|
357
|
-
var matcher = new WebhookMatcher;
|
|
358
|
-
async function refreshMatcher(db) {
|
|
359
|
-
const rows = await sql`
|
|
360
|
-
SELECT * FROM webhooks WHERE status = 'active'
|
|
361
|
-
`.execute(db);
|
|
362
|
-
matcher.setAll(rows.rows);
|
|
363
|
-
return matcher.size();
|
|
364
|
-
}
|
|
365
|
-
|
|
366
197
|
// src/runtime/emitter.ts
|
|
367
198
|
var BATCH_SIZE = 50;
|
|
368
199
|
var LIVE_SHARE = 0.9;
|
|
@@ -598,12 +429,10 @@ function buildTestOutboxRow(sub) {
|
|
|
598
429
|
id: randomUUID(),
|
|
599
430
|
webhook_id: sub.id,
|
|
600
431
|
kind: sub.kind,
|
|
601
|
-
subgraph_name: sub.subgraph_name ?? null,
|
|
602
|
-
table_name: sub.table_name ?? null,
|
|
603
432
|
block_height: 0,
|
|
604
433
|
tx_id: null,
|
|
605
434
|
row_pk: null,
|
|
606
|
-
event_type:
|
|
435
|
+
event_type: "chain.test.apply",
|
|
607
436
|
payload: {
|
|
608
437
|
test: true,
|
|
609
438
|
message: "Secondlayer test delivery",
|
|
@@ -694,7 +523,7 @@ async function settleFailed(db, outboxRow, sub, errText) {
|
|
|
694
523
|
});
|
|
695
524
|
return;
|
|
696
525
|
}
|
|
697
|
-
const incResult = await
|
|
526
|
+
const incResult = await sql`
|
|
698
527
|
UPDATE webhooks
|
|
699
528
|
SET circuit_failures = circuit_failures + 1,
|
|
700
529
|
last_delivery_at = NOW(),
|
|
@@ -756,23 +585,23 @@ async function claimAndDispatchOnce(db, state, emitterId, trigger) {
|
|
|
756
585
|
const liveLimit = Math.max(1, Math.round(BATCH_SIZE * LIVE_SHARE));
|
|
757
586
|
const replayLimit = BATCH_SIZE - liveLimit;
|
|
758
587
|
const { combined, liveCount, replayCount } = await db.transaction().execute(async (tx) => {
|
|
759
|
-
const live = await
|
|
588
|
+
const live = await sql`
|
|
760
589
|
SELECT * FROM webhook_outbox
|
|
761
590
|
WHERE status = 'pending'
|
|
762
591
|
AND next_attempt_at <= NOW()
|
|
763
592
|
AND is_replay = FALSE
|
|
764
593
|
ORDER BY next_attempt_at ASC
|
|
765
594
|
FOR UPDATE SKIP LOCKED
|
|
766
|
-
LIMIT ${
|
|
595
|
+
LIMIT ${sql.lit(liveLimit)}
|
|
767
596
|
`.execute(tx);
|
|
768
|
-
const replay = await
|
|
597
|
+
const replay = await sql`
|
|
769
598
|
SELECT * FROM webhook_outbox
|
|
770
599
|
WHERE status = 'pending'
|
|
771
600
|
AND next_attempt_at <= NOW()
|
|
772
601
|
AND is_replay = TRUE
|
|
773
602
|
ORDER BY next_attempt_at ASC
|
|
774
603
|
FOR UPDATE SKIP LOCKED
|
|
775
|
-
LIMIT ${
|
|
604
|
+
LIMIT ${sql.lit(replayLimit)}
|
|
776
605
|
`.execute(tx);
|
|
777
606
|
const rows = [...live.rows, ...replay.rows];
|
|
778
607
|
if (rows.length === 0) {
|
|
@@ -891,15 +720,15 @@ async function runOne(db, state, sq, row) {
|
|
|
891
720
|
}
|
|
892
721
|
}
|
|
893
722
|
async function runRetention(db) {
|
|
894
|
-
await
|
|
723
|
+
await sql`
|
|
895
724
|
DELETE FROM webhook_outbox
|
|
896
725
|
WHERE status = 'delivered' AND delivered_at < NOW() - interval '7 days'
|
|
897
726
|
`.execute(db);
|
|
898
|
-
await
|
|
727
|
+
await sql`
|
|
899
728
|
DELETE FROM webhook_deliveries
|
|
900
729
|
WHERE dispatched_at < NOW() - interval '30 days'
|
|
901
730
|
`.execute(db);
|
|
902
|
-
await
|
|
731
|
+
await sql`
|
|
903
732
|
DELETE FROM webhook_outbox
|
|
904
733
|
WHERE status = 'dead' AND failed_at < NOW() - interval '90 days'
|
|
905
734
|
`.execute(db);
|
|
@@ -918,27 +747,6 @@ async function startEmitter(opts) {
|
|
|
918
747
|
const pollIntervalMs = opts?.pollIntervalMs ?? 120000;
|
|
919
748
|
const retentionIntervalMs = opts?.retentionIntervalMs ?? 60 * 60000;
|
|
920
749
|
logger3.info("[emitter] started", { id: emitterId });
|
|
921
|
-
const MATCHER_BOOT_ATTEMPTS = 5;
|
|
922
|
-
let lastErr = null;
|
|
923
|
-
for (let i = 0;i < MATCHER_BOOT_ATTEMPTS; i++) {
|
|
924
|
-
try {
|
|
925
|
-
await refreshMatcher(db);
|
|
926
|
-
lastErr = null;
|
|
927
|
-
break;
|
|
928
|
-
} catch (err) {
|
|
929
|
-
lastErr = err;
|
|
930
|
-
const delayMs = 500 * 2 ** i;
|
|
931
|
-
logger3.warn("[emitter] matcher refresh failed, retrying", {
|
|
932
|
-
attempt: i + 1,
|
|
933
|
-
delayMs,
|
|
934
|
-
error: err instanceof Error ? err.message : String(err)
|
|
935
|
-
});
|
|
936
|
-
await new Promise((r) => setTimeout(r, delayMs));
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
if (lastErr) {
|
|
940
|
-
throw new Error(`[emitter] matcher refresh failed ${MATCHER_BOOT_ATTEMPTS}×; aborting boot: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`);
|
|
941
|
-
}
|
|
942
750
|
const listenUrl = targetListenerUrl();
|
|
943
751
|
const stopNew = await listen("webhooks:new_outbox", () => {
|
|
944
752
|
if (!state.running)
|
|
@@ -950,9 +758,6 @@ async function startEmitter(opts) {
|
|
|
950
758
|
const stopChanged = await listen("webhooks:changed", () => {
|
|
951
759
|
if (!state.running)
|
|
952
760
|
return;
|
|
953
|
-
refreshMatcher(db).catch((err) => logger3.error("[emitter] matcher refresh failed", {
|
|
954
|
-
error: err instanceof Error ? err.message : String(err)
|
|
955
|
-
}));
|
|
956
761
|
claimAndDrain(db, state, emitterId, "changed").catch((err) => logger3.error("[emitter] claim failed", {
|
|
957
762
|
error: err instanceof Error ? err.message : String(err)
|
|
958
763
|
}));
|
|
@@ -1018,5 +823,5 @@ export {
|
|
|
1018
823
|
startEmitter
|
|
1019
824
|
};
|
|
1020
825
|
|
|
1021
|
-
//# debugId=
|
|
826
|
+
//# debugId=CAA46708A06EDDEB64756E2164756E21
|
|
1022
827
|
//# sourceMappingURL=emitter.js.map
|