@secondlayer/subgraphs 1.0.0-beta.1 → 1.0.0-beta.3
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/dist/src/runtime/processor.js +183 -27
- package/dist/src/runtime/processor.js.map +11 -5
- package/dist/src/runtime/replay.d.ts +13 -0
- package/dist/src/runtime/replay.js +93 -0
- package/dist/src/runtime/replay.js.map +11 -0
- package/dist/src/service.js +185 -29
- package/dist/src/service.js.map +10 -4
- package/package.json +5 -1
|
@@ -1503,7 +1503,7 @@ import {
|
|
|
1503
1503
|
listSubgraphs as listSubgraphs2,
|
|
1504
1504
|
updateSubgraphStatus as updateSubgraphStatus2
|
|
1505
1505
|
} from "@secondlayer/shared/db/queries/subgraphs";
|
|
1506
|
-
import { logger as
|
|
1506
|
+
import { logger as logger9 } from "@secondlayer/shared/logger";
|
|
1507
1507
|
import { listen as listen2 } from "@secondlayer/shared/queue/listener";
|
|
1508
1508
|
|
|
1509
1509
|
// src/runtime/emitter.ts
|
|
@@ -1513,10 +1513,98 @@ import {
|
|
|
1513
1513
|
import {
|
|
1514
1514
|
getSubscriptionSigningSecret
|
|
1515
1515
|
} from "@secondlayer/shared/db/queries/subscriptions";
|
|
1516
|
-
import { logger as
|
|
1516
|
+
import { logger as logger8 } from "@secondlayer/shared/logger";
|
|
1517
1517
|
import { listen } from "@secondlayer/shared/queue/listener";
|
|
1518
1518
|
import { sql as sql4 } from "kysely";
|
|
1519
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
|
+
|
|
1520
1608
|
// src/runtime/formats/standard-webhooks.ts
|
|
1521
1609
|
import { sign } from "@secondlayer/shared/crypto/standard-webhooks";
|
|
1522
1610
|
function buildStandardWebhooks(outboxRow, signingSecret) {
|
|
@@ -1539,15 +1627,69 @@ function buildStandardWebhooks(outboxRow, signingSecret) {
|
|
|
1539
1627
|
};
|
|
1540
1628
|
}
|
|
1541
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
|
+
|
|
1542
1683
|
// src/runtime/emitter.ts
|
|
1543
1684
|
var BATCH_SIZE = 50;
|
|
1685
|
+
var LIVE_SHARE = 0.9;
|
|
1544
1686
|
var BACKOFF_SECONDS = [30, 120, 600, 3600, 21600, 86400, 259200];
|
|
1545
1687
|
var CIRCUIT_THRESHOLD = 20;
|
|
1546
1688
|
function nextDelaySeconds(attempt) {
|
|
1547
1689
|
return BACKOFF_SECONDS[Math.min(attempt, BACKOFF_SECONDS.length - 1)];
|
|
1548
1690
|
}
|
|
1549
1691
|
async function dispatchOne(db, outboxRow, sub) {
|
|
1550
|
-
const { body, headers } =
|
|
1692
|
+
const { body, headers } = buildForFormat(outboxRow, sub, getSubscriptionSigningSecret(sub));
|
|
1551
1693
|
const start = performance.now();
|
|
1552
1694
|
let statusCode = null;
|
|
1553
1695
|
let error = null;
|
|
@@ -1625,7 +1767,7 @@ async function settleFailed(db, outboxRow, sub, errText) {
|
|
|
1625
1767
|
updated_at: new Date
|
|
1626
1768
|
}).where("id", "=", outboxRow.subscription_id).execute();
|
|
1627
1769
|
if (shouldTripCircuit) {
|
|
1628
|
-
|
|
1770
|
+
logger8.warn("Subscription circuit tripped — paused after consecutive failures", {
|
|
1629
1771
|
subscription: sub.name,
|
|
1630
1772
|
failures: newFailures
|
|
1631
1773
|
});
|
|
@@ -1637,20 +1779,34 @@ async function claimAndDrain(db, state, emitterId) {
|
|
|
1637
1779
|
return 0;
|
|
1638
1780
|
state.claimInFlight = true;
|
|
1639
1781
|
try {
|
|
1782
|
+
const liveLimit = Math.max(1, Math.round(BATCH_SIZE * LIVE_SHARE));
|
|
1783
|
+
const replayLimit = BATCH_SIZE - liveLimit;
|
|
1640
1784
|
const claimed = await db.transaction().execute(async (tx) => {
|
|
1641
|
-
const
|
|
1785
|
+
const live = await sql4`
|
|
1786
|
+
SELECT * FROM subscription_outbox
|
|
1787
|
+
WHERE status = 'pending'
|
|
1788
|
+
AND next_attempt_at <= NOW()
|
|
1789
|
+
AND is_replay = FALSE
|
|
1790
|
+
ORDER BY next_attempt_at ASC
|
|
1791
|
+
FOR UPDATE SKIP LOCKED
|
|
1792
|
+
LIMIT ${sql4.lit(liveLimit)}
|
|
1793
|
+
`.execute(tx);
|
|
1794
|
+
const replay = await sql4`
|
|
1642
1795
|
SELECT * FROM subscription_outbox
|
|
1643
|
-
WHERE status = 'pending'
|
|
1796
|
+
WHERE status = 'pending'
|
|
1797
|
+
AND next_attempt_at <= NOW()
|
|
1798
|
+
AND is_replay = TRUE
|
|
1644
1799
|
ORDER BY next_attempt_at ASC
|
|
1645
1800
|
FOR UPDATE SKIP LOCKED
|
|
1646
|
-
LIMIT ${sql4.lit(
|
|
1801
|
+
LIMIT ${sql4.lit(replayLimit)}
|
|
1647
1802
|
`.execute(tx);
|
|
1648
|
-
|
|
1803
|
+
const combined = [...live.rows, ...replay.rows];
|
|
1804
|
+
if (combined.length === 0)
|
|
1649
1805
|
return [];
|
|
1650
1806
|
const now = new Date;
|
|
1651
1807
|
const lockUntil = new Date(now.getTime() + 60000);
|
|
1652
|
-
await tx.updateTable("subscription_outbox").set({ locked_by: emitterId, locked_until: lockUntil }).where("id", "in",
|
|
1653
|
-
return
|
|
1808
|
+
await tx.updateTable("subscription_outbox").set({ locked_by: emitterId, locked_until: lockUntil }).where("id", "in", combined.map((r) => r.id)).execute();
|
|
1809
|
+
return combined;
|
|
1654
1810
|
});
|
|
1655
1811
|
if (claimed.length === 0)
|
|
1656
1812
|
return 0;
|
|
@@ -1695,7 +1851,7 @@ async function drainForSub(db, state, sub, rows) {
|
|
|
1695
1851
|
await settleFailed(db, row, sub, err);
|
|
1696
1852
|
}
|
|
1697
1853
|
} catch (err) {
|
|
1698
|
-
|
|
1854
|
+
logger8.error("Emitter dispatch crashed", {
|
|
1699
1855
|
outboxId: row.id,
|
|
1700
1856
|
error: err instanceof Error ? err.message : String(err)
|
|
1701
1857
|
});
|
|
@@ -1732,30 +1888,30 @@ async function startEmitter(opts) {
|
|
|
1732
1888
|
};
|
|
1733
1889
|
const pollIntervalMs = opts?.pollIntervalMs ?? 120000;
|
|
1734
1890
|
const retentionIntervalMs = opts?.retentionIntervalMs ?? 60 * 60000;
|
|
1735
|
-
|
|
1891
|
+
logger8.info("[emitter] started", { id: emitterId });
|
|
1736
1892
|
await refreshMatcher(db).catch((err) => {
|
|
1737
|
-
|
|
1893
|
+
logger8.error("[emitter] initial matcher refresh failed", {
|
|
1738
1894
|
error: err instanceof Error ? err.message : String(err)
|
|
1739
1895
|
});
|
|
1740
1896
|
});
|
|
1741
1897
|
const stopNew = await listen("subscriptions:new_outbox", () => {
|
|
1742
1898
|
if (!state.running)
|
|
1743
1899
|
return;
|
|
1744
|
-
claimAndDrain(db, state, emitterId).catch((err) =>
|
|
1900
|
+
claimAndDrain(db, state, emitterId).catch((err) => logger8.error("[emitter] claim failed", {
|
|
1745
1901
|
error: err instanceof Error ? err.message : String(err)
|
|
1746
1902
|
}));
|
|
1747
1903
|
});
|
|
1748
1904
|
const stopChanged = await listen("subscriptions:changed", () => {
|
|
1749
1905
|
if (!state.running)
|
|
1750
1906
|
return;
|
|
1751
|
-
refreshMatcher(db).catch((err) =>
|
|
1907
|
+
refreshMatcher(db).catch((err) => logger8.error("[emitter] matcher refresh failed", {
|
|
1752
1908
|
error: err instanceof Error ? err.message : String(err)
|
|
1753
1909
|
}));
|
|
1754
1910
|
});
|
|
1755
1911
|
const poll = setInterval(() => {
|
|
1756
1912
|
if (!state.running)
|
|
1757
1913
|
return;
|
|
1758
|
-
claimAndDrain(db, state, emitterId).catch((err) =>
|
|
1914
|
+
claimAndDrain(db, state, emitterId).catch((err) => logger8.error("[emitter] poll claim failed", {
|
|
1759
1915
|
error: err instanceof Error ? err.message : String(err)
|
|
1760
1916
|
}));
|
|
1761
1917
|
}, pollIntervalMs);
|
|
@@ -1763,7 +1919,7 @@ async function startEmitter(opts) {
|
|
|
1763
1919
|
const retention = setInterval(() => {
|
|
1764
1920
|
if (!state.running)
|
|
1765
1921
|
return;
|
|
1766
|
-
runRetention(db).catch((err) =>
|
|
1922
|
+
runRetention(db).catch((err) => logger8.error("[emitter] retention failed", {
|
|
1767
1923
|
error: err instanceof Error ? err.message : String(err)
|
|
1768
1924
|
}));
|
|
1769
1925
|
}, retentionIntervalMs);
|
|
@@ -1773,7 +1929,7 @@ async function startEmitter(opts) {
|
|
|
1773
1929
|
clearInterval(retention);
|
|
1774
1930
|
await stopNew();
|
|
1775
1931
|
await stopChanged();
|
|
1776
|
-
|
|
1932
|
+
logger8.info("[emitter] stopped", { id: emitterId });
|
|
1777
1933
|
};
|
|
1778
1934
|
}
|
|
1779
1935
|
|
|
@@ -1811,7 +1967,7 @@ async function loadSubgraphDefinition(sg) {
|
|
|
1811
1967
|
knownVersions.set(sg.name, sg.version);
|
|
1812
1968
|
definitionCache.set(sg.name, def);
|
|
1813
1969
|
if (prevVersion && prevVersion !== sg.version) {
|
|
1814
|
-
|
|
1970
|
+
logger9.info("Subgraph handler reloaded", {
|
|
1815
1971
|
subgraph: sg.name,
|
|
1816
1972
|
from: prevVersion,
|
|
1817
1973
|
to: sg.version
|
|
@@ -1831,7 +1987,7 @@ function cleanupCaches(active) {
|
|
|
1831
1987
|
async function startSubgraphProcessor(opts) {
|
|
1832
1988
|
const concurrency = opts?.concurrency ?? DEFAULT_CONCURRENCY;
|
|
1833
1989
|
let running = true;
|
|
1834
|
-
|
|
1990
|
+
logger9.info("Starting subgraph processor", { concurrency });
|
|
1835
1991
|
const targetDb = getTargetDb5();
|
|
1836
1992
|
const activeSubgraphs = (await listSubgraphs2(targetDb)).filter((v) => v.status === "active");
|
|
1837
1993
|
for (const sg of activeSubgraphs) {
|
|
@@ -1843,7 +1999,7 @@ async function startSubgraphProcessor(opts) {
|
|
|
1843
1999
|
if (isHandlerNotFoundError(err)) {
|
|
1844
2000
|
await updateSubgraphStatus2(targetDb, sg.name, "error");
|
|
1845
2001
|
}
|
|
1846
|
-
|
|
2002
|
+
logger9.error("Subgraph catch-up failed on startup", {
|
|
1847
2003
|
subgraph: sg.name,
|
|
1848
2004
|
error: msg
|
|
1849
2005
|
});
|
|
@@ -1864,7 +2020,7 @@ async function startSubgraphProcessor(opts) {
|
|
|
1864
2020
|
if (isHandlerNotFoundError(err)) {
|
|
1865
2021
|
await updateSubgraphStatus2(db, sg.name, "error");
|
|
1866
2022
|
}
|
|
1867
|
-
|
|
2023
|
+
logger9.error("Subgraph processing failed", {
|
|
1868
2024
|
subgraph: sg.name,
|
|
1869
2025
|
error: msg
|
|
1870
2026
|
});
|
|
@@ -1881,7 +2037,7 @@ async function startSubgraphProcessor(opts) {
|
|
|
1881
2037
|
await handleSubgraphReorg(blockHeight, loadSubgraphDefinition);
|
|
1882
2038
|
}
|
|
1883
2039
|
} catch (err) {
|
|
1884
|
-
|
|
2040
|
+
logger9.error("Subgraph reorg handling failed", {
|
|
1885
2041
|
error: getErrorMessage3(err)
|
|
1886
2042
|
});
|
|
1887
2043
|
}
|
|
@@ -1897,7 +2053,7 @@ async function startSubgraphProcessor(opts) {
|
|
|
1897
2053
|
const def = await loadSubgraphDefinition(sg);
|
|
1898
2054
|
await catchUpSubgraph(def, sg.name);
|
|
1899
2055
|
} catch (err) {
|
|
1900
|
-
|
|
2056
|
+
logger9.error("Subgraph poll processing failed", {
|
|
1901
2057
|
subgraph: sg.name,
|
|
1902
2058
|
error: getErrorMessage3(err)
|
|
1903
2059
|
});
|
|
@@ -1905,19 +2061,19 @@ async function startSubgraphProcessor(opts) {
|
|
|
1905
2061
|
}
|
|
1906
2062
|
}, POLL_INTERVAL_MS);
|
|
1907
2063
|
const stopEmitter = await startEmitter();
|
|
1908
|
-
|
|
2064
|
+
logger9.info("Subgraph processor ready");
|
|
1909
2065
|
return async () => {
|
|
1910
2066
|
running = false;
|
|
1911
2067
|
clearInterval(pollInterval);
|
|
1912
2068
|
await stopListening();
|
|
1913
2069
|
await stopReorgListening();
|
|
1914
2070
|
await stopEmitter();
|
|
1915
|
-
|
|
2071
|
+
logger9.info("Subgraph processor stopped");
|
|
1916
2072
|
};
|
|
1917
2073
|
}
|
|
1918
2074
|
export {
|
|
1919
2075
|
startSubgraphProcessor
|
|
1920
2076
|
};
|
|
1921
2077
|
|
|
1922
|
-
//# debugId=
|
|
2078
|
+
//# debugId=273D92EC37433C7164756E2164756E21
|
|
1923
2079
|
//# sourceMappingURL=processor.js.map
|