tapimo 0.10.0 → 0.10.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.
- package/dist/ApiKey.d.ts +2 -1
- package/dist/ApiKey.d.ts.map +1 -1
- package/dist/ApiKeys.d.ts +4 -2
- package/dist/ApiKeys.d.ts.map +1 -1
- package/dist/ApiKeys.js +26 -1
- package/dist/ApiKeys.js.map +1 -1
- package/dist/App.d.ts +6007 -5003
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +1 -0
- package/dist/App.js.map +1 -1
- package/dist/Webhooks.d.ts +1 -1
- package/dist/Webhooks.d.ts.map +1 -1
- package/dist/Webhooks.js +6 -6
- package/dist/Webhooks.js.map +1 -1
- package/dist/admin/App.d.ts +91 -84
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +22 -13
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.js +6 -0
- package/dist/admin/apps/api-keys.js.map +1 -1
- package/dist/admin/apps/organizations.d.ts +4 -2
- package/dist/admin/apps/organizations.d.ts.map +1 -1
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.js +69 -69
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/analytics/Analytics.d.ts.map +1 -1
- package/dist/analytics/Analytics.js +232 -0
- package/dist/analytics/Analytics.js.map +1 -1
- package/dist/analytics/tables/requestEvents.d.ts +4 -0
- package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
- package/dist/analytics/tables/requestEvents.js +2 -0
- package/dist/analytics/tables/requestEvents.js.map +1 -1
- package/dist/apps/Zones.d.ts +4266 -82
- package/dist/apps/Zones.d.ts.map +1 -1
- package/dist/apps/Zones.js +27 -8
- package/dist/apps/Zones.js.map +1 -1
- package/dist/apps/data/App.d.ts +3467 -2696
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +2 -0
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +2999 -100
- package/dist/apps/data/routes/activities.d.ts.map +1 -1
- package/dist/apps/data/routes/activities.js +340 -2
- package/dist/apps/data/routes/activities.js.map +1 -1
- package/dist/apps/data/routes/addresses.d.ts +514 -0
- package/dist/apps/data/routes/addresses.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.d.ts +26 -26
- package/dist/apps/data/routes/balances.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.js +7 -3
- package/dist/apps/data/routes/balances.js.map +1 -1
- package/dist/apps/data/routes/blocks.d.ts +4 -4
- package/dist/apps/data/routes/earn.d.ts +75 -75
- package/dist/apps/data/routes/earn.d.ts.map +1 -1
- package/dist/apps/data/routes/earn.js +65 -36
- package/dist/apps/data/routes/earn.js.map +1 -1
- package/dist/apps/data/routes/exchanges.d.ts +124 -120
- package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
- package/dist/apps/data/routes/exchanges.js +115 -77
- package/dist/apps/data/routes/exchanges.js.map +1 -1
- package/dist/apps/data/routes/fee-amm.d.ts +24 -24
- package/dist/apps/data/routes/indexer.d.ts +2 -1
- package/dist/apps/data/routes/indexer.d.ts.map +1 -1
- package/dist/apps/data/routes/indexer.js +3 -2
- package/dist/apps/data/routes/indexer.js.map +1 -1
- package/dist/apps/data/routes/receipts.d.ts +4 -3
- package/dist/apps/data/routes/receipts.d.ts.map +1 -1
- package/dist/apps/data/routes/receipts.js +15 -14
- package/dist/apps/data/routes/receipts.js.map +1 -1
- package/dist/apps/data/routes/rpc.d.ts.map +1 -1
- package/dist/apps/data/routes/rpc.js +5 -1
- package/dist/apps/data/routes/rpc.js.map +1 -1
- package/dist/apps/data/routes/tokenlist.d.ts +12 -12
- package/dist/apps/data/routes/tokens.d.ts +1 -1
- package/dist/apps/data/routes/tokens.d.ts.map +1 -1
- package/dist/apps/data/routes/tokens.js +4 -3
- package/dist/apps/data/routes/tokens.js.map +1 -1
- package/dist/apps/data/routes/transfers.d.ts.map +1 -1
- package/dist/apps/data/routes/transfers.js +25 -7
- package/dist/apps/data/routes/transfers.js.map +1 -1
- package/dist/apps/data/routes/verified-tokens.d.ts +37 -37
- package/dist/apps/data/routes/webhooks.d.ts +71 -1
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +177 -23
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/data/routes/zones.js +1 -1
- package/dist/apps/data/routes/zones.js.map +1 -1
- package/dist/apps/funding/App.d.ts +322 -89
- package/dist/apps/funding/App.d.ts.map +1 -1
- package/dist/apps/funding/routes/deposit-addresses.d.ts +375 -45
- package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
- package/dist/apps/funding/routes/deposit-addresses.js +208 -12
- package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
- package/dist/apps/funding/routes/transfers.d.ts +32 -32
- package/dist/apps/funding/routes/transfers.js +1 -1
- package/dist/apps/funding/routes/transfers.js.map +1 -1
- package/dist/apps/management/App.d.ts +418 -418
- package/dist/apps/management/routes/api-keys.d.ts +50 -50
- package/dist/apps/management/routes/billing.d.ts +72 -72
- package/dist/apps/management/routes/billing.js +1 -1
- package/dist/apps/management/routes/billing.js.map +1 -1
- package/dist/apps/management/routes/faucet.d.ts +8 -8
- package/dist/apps/management/routes/faucet.js +2 -2
- package/dist/apps/management/routes/faucet.js.map +1 -1
- package/dist/apps/management/routes/invite-links.d.ts +42 -42
- package/dist/apps/management/routes/me.d.ts +6 -6
- package/dist/apps/management/routes/members.d.ts +14 -14
- package/dist/apps/management/routes/projects.d.ts +40 -40
- package/dist/apps/management/routes/usage.d.ts +8 -8
- package/dist/apps/management/routes/webhooks.d.ts +49 -49
- package/dist/apps/mcp.d.ts.map +1 -1
- package/dist/apps/mcp.js +4 -1
- package/dist/apps/mcp.js.map +1 -1
- package/dist/db/Db.d.ts +2 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +90 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +19 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +24 -0
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/fundingDepositAddresses.d.ts +20 -0
- package/dist/db/tables/fundingDepositAddresses.d.ts.map +1 -1
- package/dist/db/tables/fundingDepositAddresses.js +17 -0
- package/dist/db/tables/fundingDepositAddresses.js.map +1 -1
- package/dist/db/tables/fundingDeposits.d.ts +41 -0
- package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
- package/dist/db/tables/fundingDeposits.js +69 -0
- package/dist/db/tables/fundingDeposits.js.map +1 -1
- package/dist/db/tables/webhookQueueEvents.d.ts +122 -0
- package/dist/db/tables/webhookQueueEvents.d.ts.map +1 -0
- package/dist/db/tables/webhookQueueEvents.js +160 -0
- package/dist/db/tables/webhookQueueEvents.js.map +1 -0
- package/dist/db/tables/webhookSubscriptions.d.ts +2 -0
- package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
- package/dist/db/tables/webhookSubscriptions.js +10 -0
- package/dist/db/tables/webhookSubscriptions.js.map +1 -1
- package/dist/internal/Auth.d.ts +3 -26
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +6 -37
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/Credential.d.ts +27 -0
- package/dist/internal/Credential.d.ts.map +1 -0
- package/dist/internal/Credential.js +34 -0
- package/dist/internal/Credential.js.map +1 -0
- package/dist/internal/EarnRates.js +11 -7
- package/dist/internal/EarnRates.js.map +1 -1
- package/dist/internal/Log.d.ts +8 -0
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +4 -0
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/MetricSink.d.ts +51 -0
- package/dist/internal/MetricSink.d.ts.map +1 -1
- package/dist/internal/MetricSink.js +54 -2
- package/dist/internal/MetricSink.js.map +1 -1
- package/dist/internal/RateLimit.d.ts +4 -6
- package/dist/internal/RateLimit.d.ts.map +1 -1
- package/dist/internal/RateLimit.js +15 -14
- package/dist/internal/RateLimit.js.map +1 -1
- package/dist/internal/Store.d.ts +2 -1
- package/dist/internal/Store.d.ts.map +1 -1
- package/dist/internal/Store.js +57 -18
- package/dist/internal/Store.js.map +1 -1
- package/dist/internal/Tidx.d.ts +4 -3
- package/dist/internal/Tidx.d.ts.map +1 -1
- package/dist/internal/Tidx.js +2 -2
- package/dist/internal/Tidx.js.map +1 -1
- package/dist/internal/Value.d.ts +2 -0
- package/dist/internal/Value.d.ts.map +1 -1
- package/dist/internal/Value.js +25 -0
- package/dist/internal/Value.js.map +1 -1
- package/dist/internal/Viem.d.ts +4 -3
- package/dist/internal/Viem.d.ts.map +1 -1
- package/dist/internal/Viem.js +3 -2
- package/dist/internal/Viem.js.map +1 -1
- package/dist/internal/WebhookTransfer.d.ts +20 -0
- package/dist/internal/WebhookTransfer.d.ts.map +1 -1
- package/dist/internal/WebhookTransfer.js +7 -4
- package/dist/internal/WebhookTransfer.js.map +1 -1
- package/dist/internal/Webhooks.d.ts +141 -0
- package/dist/internal/Webhooks.d.ts.map +1 -1
- package/dist/internal/Webhooks.js +139 -10
- package/dist/internal/Webhooks.js.map +1 -1
- package/dist/internal/funding/providers/relay.d.ts +3 -4
- package/dist/internal/funding/providers/relay.d.ts.map +1 -1
- package/dist/internal/funding/providers/relay.js +59 -8
- package/dist/internal/funding/providers/relay.js.map +1 -1
- package/dist/internal/index.d.ts +1 -0
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +1 -0
- package/dist/internal/index.js.map +1 -1
- package/package.json +3 -3
- package/src/ApiKeys.test.ts +20 -0
- package/src/ApiKeys.ts +28 -1
- package/src/App.test.ts +40 -8
- package/src/App.ts +1 -0
- package/src/Client.test-d.ts +34 -1
- package/src/Webhooks.ts +20 -18
- package/src/admin/apps/api-keys.test.ts +9 -0
- package/src/admin/apps/api-keys.ts +15 -0
- package/src/admin/ui/src/lib/query-client.ts +3 -2
- package/src/admin/ui/src/routes/api-keys.tsx +31 -9
- package/src/admin/ui.generated.ts +69 -69
- package/src/analytics/Analytics.test.ts +42 -0
- package/src/analytics/Analytics.ts +232 -0
- package/src/analytics/tables/requestEvents.ts +6 -0
- package/src/apps/Zones.test.ts +20 -3
- package/src/apps/Zones.ts +28 -8
- package/src/apps/composition.test.ts +2 -0
- package/src/apps/data/App.test.ts +2 -2
- package/src/apps/data/App.ts +2 -0
- package/src/apps/data/routes/activities.bench.ts +41 -0
- package/src/apps/data/routes/activities.test.ts +103 -2
- package/src/apps/data/routes/activities.ts +401 -2
- package/src/apps/data/routes/balances.bench.ts +21 -0
- package/src/apps/data/routes/balances.ts +7 -3
- package/src/apps/data/routes/blocks.bench.ts +34 -0
- package/src/apps/data/routes/coingecko.bench.ts +53 -0
- package/src/apps/data/routes/earn.bench.ts +71 -0
- package/src/apps/data/routes/earn.ts +66 -36
- package/src/apps/data/routes/exchanges.bench.ts +85 -0
- package/src/apps/data/routes/exchanges.test.ts +8 -3
- package/src/apps/data/routes/exchanges.ts +134 -82
- package/src/apps/data/routes/fee-amm.bench.ts +13 -0
- package/src/apps/data/routes/indexer.bench.ts +8 -0
- package/src/apps/data/routes/indexer.test.ts +2 -1
- package/src/apps/data/routes/indexer.ts +3 -2
- package/src/apps/data/routes/receipts.bench.ts +39 -0
- package/src/apps/data/routes/receipts.ts +16 -14
- package/src/apps/data/routes/rpc.bench.ts +14 -0
- package/src/apps/data/routes/rpc.test.ts +27 -0
- package/src/apps/data/routes/rpc.ts +5 -1
- package/src/apps/data/routes/tokenlist.bench.ts +3 -0
- package/src/apps/data/routes/tokens.bench.ts +76 -0
- package/src/apps/data/routes/tokens.ts +3 -2
- package/src/apps/data/routes/transactions.bench.ts +48 -0
- package/src/apps/data/routes/transfers.bench.ts +28 -0
- package/src/apps/data/routes/transfers.ts +36 -10
- package/src/apps/data/routes/valuation.bench.ts +14 -0
- package/src/apps/data/routes/verified-tokens.bench.ts +8 -0
- package/src/apps/data/routes/webhooks.bench.ts +26 -0
- package/src/apps/data/routes/webhooks.scan.test.ts +326 -0
- package/src/apps/data/routes/webhooks.test.ts +53 -6
- package/src/apps/data/routes/webhooks.ts +252 -24
- package/src/apps/data/routes/zones.bench.ts +32 -0
- package/src/apps/data/routes/zones.test.ts +1 -1
- package/src/apps/data/routes/zones.ts +1 -1
- package/src/apps/funding/routes/chains.test.ts +1 -1
- package/src/apps/funding/routes/deposit-addresses.test.ts +217 -3
- package/src/apps/funding/routes/deposit-addresses.ts +255 -19
- package/src/apps/funding/routes/providers.test.ts +1 -1
- package/src/apps/funding/routes/quotes.test.ts +1 -1
- package/src/apps/funding/routes/transfers.test.ts +20 -8
- package/src/apps/funding/routes/transfers.ts +1 -1
- package/src/apps/management/routes/billing.ts +1 -1
- package/src/apps/management/routes/faucet.ts +2 -2
- package/src/apps/management/routes/me.test.ts +31 -0
- package/src/apps/mcp.ts +4 -1
- package/src/db/Db.ts +117 -0
- package/src/db/Schema.ts +32 -0
- package/src/db/tables/fundingDepositAddresses.test.ts +28 -0
- package/src/db/tables/fundingDepositAddresses.ts +39 -0
- package/src/db/tables/fundingDeposits.test.ts +82 -0
- package/src/db/tables/fundingDeposits.ts +130 -0
- package/src/db/tables/webhookQueueEvents.ts +285 -0
- package/src/db/tables/webhookSubscriptions.ts +15 -0
- package/src/internal/Auth.test.ts +61 -98
- package/src/internal/Auth.ts +9 -63
- package/src/internal/Credential.test.ts +51 -0
- package/src/internal/Credential.ts +58 -0
- package/src/internal/EarnRates.ts +13 -7
- package/src/internal/EarnVaults.test.ts +2 -2
- package/src/internal/Log.test.ts +3 -1
- package/src/internal/Log.ts +14 -0
- package/src/internal/MetricSink.test.ts +108 -2
- package/src/internal/MetricSink.ts +112 -2
- package/src/internal/RateLimit.test.ts +23 -18
- package/src/internal/RateLimit.ts +23 -15
- package/src/internal/Store.test.ts +99 -13
- package/src/internal/Store.ts +52 -17
- package/src/internal/Tidx.test.ts +4 -4
- package/src/internal/Tidx.ts +5 -4
- package/src/internal/Value.test.ts +25 -0
- package/src/internal/Value.ts +29 -0
- package/src/internal/Viem.test.ts +4 -0
- package/src/internal/Viem.ts +6 -4
- package/src/internal/WebhookTransfer.test.ts +89 -0
- package/src/internal/WebhookTransfer.ts +11 -7
- package/src/internal/Webhooks.test.ts +488 -0
- package/src/internal/Webhooks.ts +291 -10
- package/src/internal/funding/Provider.test.ts +1 -1
- package/src/internal/funding/providers/index.test.ts +73 -3
- package/src/internal/funding/providers/relay.test.ts +1 -5
- package/src/internal/funding/providers/relay.ts +56 -12
- package/src/internal/index.test.ts +1 -0
- package/src/internal/index.ts +1 -0
|
@@ -804,6 +804,25 @@ describe('delivery log', () => {
|
|
|
804
804
|
`)
|
|
805
805
|
})
|
|
806
806
|
|
|
807
|
+
test('deliverAndRecord records envelope serialization failures', async () => {
|
|
808
|
+
const db = TestApp.database()
|
|
809
|
+
const created = await Webhooks.createSubscription(db, createInput())
|
|
810
|
+
const envelope = {
|
|
811
|
+
chainId: 4217,
|
|
812
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
813
|
+
data: { amount: 1n },
|
|
814
|
+
id: 'evt_invalid',
|
|
815
|
+
subscriptionId: created.id,
|
|
816
|
+
type: 'token:transfer',
|
|
817
|
+
} satisfies Webhooks.Envelope
|
|
818
|
+
|
|
819
|
+
const result = await Webhooks.deliverAndRecord(db, created, envelope, { now: fixedNow })
|
|
820
|
+
|
|
821
|
+
expect(result).toEqual({ error: 'delivery failed before request', ok: false })
|
|
822
|
+
const after = await Webhooks.getSubscription(db, apiKeyOwner, created.id)
|
|
823
|
+
expect(after?.failureCount).toBe(1)
|
|
824
|
+
})
|
|
825
|
+
|
|
807
826
|
test('deliverCurrentAndRecord skips paused and deleted queued subscriptions', async () => {
|
|
808
827
|
const db = TestApp.database()
|
|
809
828
|
const paused = await Webhooks.createSubscription(db, createInput())
|
|
@@ -909,6 +928,475 @@ describe('delivery log', () => {
|
|
|
909
928
|
})
|
|
910
929
|
})
|
|
911
930
|
|
|
931
|
+
async function stagedObligation(db: ReturnType<typeof TestApp.database>, logIndex = 4) {
|
|
932
|
+
const subscription = await Webhooks.createSubscription(db, createInput())
|
|
933
|
+
const envelope = Webhooks.buildEnvelope({
|
|
934
|
+
blockNumber: 123,
|
|
935
|
+
createdAt: new Date(),
|
|
936
|
+
data: { amount: '1' },
|
|
937
|
+
logIndex,
|
|
938
|
+
subscription,
|
|
939
|
+
})
|
|
940
|
+
const { created, references } = await Webhooks.ensureQueueEvents(db, [{ envelope, subscription }])
|
|
941
|
+
return { created, envelope, reference: references[0]!, subscription }
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
describe('ensureQueueEvents', () => {
|
|
945
|
+
test('keeps oversized envelopes behind compact idempotent Queue references', async () => {
|
|
946
|
+
const db = TestApp.database()
|
|
947
|
+
const subscription = await Webhooks.createSubscription(
|
|
948
|
+
db,
|
|
949
|
+
createInput({ eventType: 'transaction:included' }),
|
|
950
|
+
)
|
|
951
|
+
const input = `0x${'ab'.repeat(70_000)}`
|
|
952
|
+
const envelope = Webhooks.buildEnvelope({
|
|
953
|
+
blockNumber: 123,
|
|
954
|
+
createdAt: fixedNow(),
|
|
955
|
+
data: { input, meta: { rpc: { input } } },
|
|
956
|
+
logIndex: 4,
|
|
957
|
+
subscription,
|
|
958
|
+
})
|
|
959
|
+
const dispatchable = { envelope, subscription }
|
|
960
|
+
|
|
961
|
+
const { references: [reference] } = await Webhooks.ensureQueueEvents(db, [dispatchable]) // prettier-ignore
|
|
962
|
+
await Webhooks.ensureQueueEvents(db, [dispatchable])
|
|
963
|
+
|
|
964
|
+
expect(
|
|
965
|
+
new TextEncoder().encode(JSON.stringify({ body: dispatchable })).byteLength,
|
|
966
|
+
).toBeGreaterThan(128_000)
|
|
967
|
+
expect(new TextEncoder().encode(JSON.stringify({ body: reference })).byteLength).toBeLessThan(
|
|
968
|
+
1_000,
|
|
969
|
+
)
|
|
970
|
+
expect((await Webhooks.getQueueEvent(db, reference!))?.envelope).toEqual(envelope)
|
|
971
|
+
})
|
|
972
|
+
|
|
973
|
+
test('delivers a staged oversized envelope from its compact reference', async () => {
|
|
974
|
+
const db = TestApp.database()
|
|
975
|
+
const subscription = await Webhooks.createSubscription(
|
|
976
|
+
db,
|
|
977
|
+
createInput({ eventType: 'transaction:included' }),
|
|
978
|
+
)
|
|
979
|
+
const input = `0x${'ab'.repeat(70_000)}`
|
|
980
|
+
const envelope = Webhooks.buildEnvelope({
|
|
981
|
+
blockNumber: 123,
|
|
982
|
+
data: { input, meta: { rpc: { input } } },
|
|
983
|
+
logIndex: 4,
|
|
984
|
+
subscription,
|
|
985
|
+
})
|
|
986
|
+
const { references: [reference] } = await Webhooks.ensureQueueEvents(db, [{ envelope, subscription }]) // prettier-ignore
|
|
987
|
+
const histograms = new Map<string, number>()
|
|
988
|
+
const metrics = Metrics.from({
|
|
989
|
+
count() {},
|
|
990
|
+
flush() {},
|
|
991
|
+
gauge() {},
|
|
992
|
+
histogram(name, value) {
|
|
993
|
+
histograms.set(name, value)
|
|
994
|
+
},
|
|
995
|
+
})
|
|
996
|
+
let deliveredBytes = 0
|
|
997
|
+
const fetch = (async (_url, init) => {
|
|
998
|
+
deliveredBytes = new TextEncoder().encode(
|
|
999
|
+
typeof init?.body === 'string' ? init.body : '',
|
|
1000
|
+
).byteLength
|
|
1001
|
+
return new Response(null, { status: 200 })
|
|
1002
|
+
}) satisfies typeof globalThis.fetch
|
|
1003
|
+
|
|
1004
|
+
const result = await Webhooks.deliverQueueEventAndRecord(db, reference!, {
|
|
1005
|
+
fetch,
|
|
1006
|
+
metrics,
|
|
1007
|
+
queuedAt: Date.now(),
|
|
1008
|
+
})
|
|
1009
|
+
|
|
1010
|
+
expect(result.status).toBe('delivered')
|
|
1011
|
+
expect(deliveredBytes).toBeGreaterThan(128_000)
|
|
1012
|
+
expect(histograms.has('webhook_delivery_processing_ms')).toBe(true)
|
|
1013
|
+
expect(histograms.has('webhook_delivery_queue_wait_ms')).toBe(true)
|
|
1014
|
+
expect(histograms.has('webhook_delivery_subscription_read_ms')).toBe(true)
|
|
1015
|
+
})
|
|
1016
|
+
|
|
1017
|
+
test('skips a staged event after its subscription expires', async () => {
|
|
1018
|
+
vi.useFakeTimers({ now: fixedNow(), toFake: ['Date'] })
|
|
1019
|
+
try {
|
|
1020
|
+
const db = TestApp.database()
|
|
1021
|
+
const subscription = await Webhooks.createSubscription(
|
|
1022
|
+
db,
|
|
1023
|
+
createInput({ eventType: 'transaction:included', owner: mppOwner, ttl: 60_000 }),
|
|
1024
|
+
{ now: fixedNow },
|
|
1025
|
+
)
|
|
1026
|
+
const envelope = Webhooks.buildEnvelope({
|
|
1027
|
+
blockNumber: 123,
|
|
1028
|
+
data: {},
|
|
1029
|
+
logIndex: 4,
|
|
1030
|
+
subscription,
|
|
1031
|
+
})
|
|
1032
|
+
const { references: [reference] } = await Webhooks.ensureQueueEvents(db, [{ envelope, subscription }]) // prettier-ignore
|
|
1033
|
+
|
|
1034
|
+
vi.setSystemTime(new Date('2026-01-01T00:01:00.000Z'))
|
|
1035
|
+
|
|
1036
|
+
await expect(Webhooks.deliverQueueEventAndRecord(db, reference!)).resolves
|
|
1037
|
+
.toMatchInlineSnapshot(`
|
|
1038
|
+
{
|
|
1039
|
+
"status": "skipped",
|
|
1040
|
+
}
|
|
1041
|
+
`)
|
|
1042
|
+
} finally {
|
|
1043
|
+
vi.useRealTimers()
|
|
1044
|
+
}
|
|
1045
|
+
})
|
|
1046
|
+
|
|
1047
|
+
test('stages once and keeps the first envelope', async () => {
|
|
1048
|
+
const db = TestApp.database()
|
|
1049
|
+
const { created, envelope, reference, subscription } = await stagedObligation(db)
|
|
1050
|
+
const replacement = { ...envelope, data: { amount: '2' } }
|
|
1051
|
+
|
|
1052
|
+
const second = await Webhooks.ensureQueueEvents(db, [{ envelope: replacement, subscription }])
|
|
1053
|
+
|
|
1054
|
+
expect(created).toBe(1)
|
|
1055
|
+
expect(second.created).toBe(0)
|
|
1056
|
+
expect((await Webhooks.getQueueEvent(db, reference))?.envelope).toEqual(envelope)
|
|
1057
|
+
})
|
|
1058
|
+
|
|
1059
|
+
test('never resurrects a terminal obligation', async () => {
|
|
1060
|
+
const db = TestApp.database()
|
|
1061
|
+
const { envelope, reference, subscription } = await stagedObligation(db)
|
|
1062
|
+
const claim = await Webhooks.claimQueueEvent(db, reference)
|
|
1063
|
+
expect(claim.type).toBe('claimed')
|
|
1064
|
+
if (claim.type !== 'claimed') return
|
|
1065
|
+
expect(
|
|
1066
|
+
await Webhooks.completeQueueEvent(db, reference, {
|
|
1067
|
+
claimedAt: claim.record.attemptingAt!,
|
|
1068
|
+
status: 'succeeded',
|
|
1069
|
+
}),
|
|
1070
|
+
).toBe(true)
|
|
1071
|
+
|
|
1072
|
+
const again = await Webhooks.ensureQueueEvents(db, [{ envelope, subscription }])
|
|
1073
|
+
expect(again.created).toBe(0)
|
|
1074
|
+
await expect(Webhooks.claimQueueEvent(db, reference)).resolves.toEqual({
|
|
1075
|
+
status: 'succeeded',
|
|
1076
|
+
type: 'terminal',
|
|
1077
|
+
})
|
|
1078
|
+
// Terminal rows read as absent, so no consumer path can redeliver them.
|
|
1079
|
+
expect(await Webhooks.getQueueEvent(db, reference)).toBeNull()
|
|
1080
|
+
})
|
|
1081
|
+
|
|
1082
|
+
test('replaces an expired terminal row so replays stay deliverable', async () => {
|
|
1083
|
+
vi.useFakeTimers({ toFake: ['Date'] })
|
|
1084
|
+
vi.setSystemTime(fixedNow())
|
|
1085
|
+
try {
|
|
1086
|
+
const db = TestApp.database()
|
|
1087
|
+
const { envelope, reference, subscription } = await stagedObligation(db)
|
|
1088
|
+
const claim = await Webhooks.claimQueueEvent(db, reference)
|
|
1089
|
+
expect(claim.type).toBe('claimed')
|
|
1090
|
+
if (claim.type !== 'claimed') return
|
|
1091
|
+
expect(
|
|
1092
|
+
await Webhooks.completeQueueEvent(db, reference, {
|
|
1093
|
+
claimedAt: claim.record.attemptingAt!,
|
|
1094
|
+
status: 'succeeded',
|
|
1095
|
+
}),
|
|
1096
|
+
).toBe(true)
|
|
1097
|
+
|
|
1098
|
+
// Past the dedupe window an unpruned terminal row must not suppress a
|
|
1099
|
+
// replay.
|
|
1100
|
+
vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.deliveryRetentionMs + 1_000))
|
|
1101
|
+
const replay = await Webhooks.ensureQueueEvents(db, [{ envelope, subscription }])
|
|
1102
|
+
expect(replay.created).toBe(1)
|
|
1103
|
+
await expect(Webhooks.claimQueueEvent(db, reference)).resolves.toMatchObject({
|
|
1104
|
+
type: 'claimed',
|
|
1105
|
+
})
|
|
1106
|
+
} finally {
|
|
1107
|
+
vi.useRealTimers()
|
|
1108
|
+
}
|
|
1109
|
+
})
|
|
1110
|
+
})
|
|
1111
|
+
|
|
1112
|
+
describe('claimQueueEvent', () => {
|
|
1113
|
+
beforeEach(() => {
|
|
1114
|
+
vi.useFakeTimers({ toFake: ['Date'] })
|
|
1115
|
+
vi.setSystemTime(fixedNow())
|
|
1116
|
+
})
|
|
1117
|
+
|
|
1118
|
+
afterEach(() => {
|
|
1119
|
+
vi.useRealTimers()
|
|
1120
|
+
})
|
|
1121
|
+
|
|
1122
|
+
test('claims a due obligation once and reclaims after staleness', async () => {
|
|
1123
|
+
const db = TestApp.database()
|
|
1124
|
+
const { reference } = await stagedObligation(db)
|
|
1125
|
+
|
|
1126
|
+
const first = await Webhooks.claimQueueEvent(db, reference)
|
|
1127
|
+
expect(first.type).toBe('claimed')
|
|
1128
|
+
if (first.type !== 'claimed') return
|
|
1129
|
+
expect(first.record.attemptCount).toBe(1)
|
|
1130
|
+
|
|
1131
|
+
await expect(Webhooks.claimQueueEvent(db, reference)).resolves.toEqual({
|
|
1132
|
+
type: 'attempting',
|
|
1133
|
+
})
|
|
1134
|
+
|
|
1135
|
+
vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.deliveryAttemptStaleMs + 1))
|
|
1136
|
+
const reclaimed = await Webhooks.claimQueueEvent(db, reference)
|
|
1137
|
+
expect(reclaimed.type).toBe('claimed')
|
|
1138
|
+
if (reclaimed.type !== 'claimed') return
|
|
1139
|
+
expect(reclaimed.record.attemptCount).toBe(2)
|
|
1140
|
+
})
|
|
1141
|
+
|
|
1142
|
+
test('classifies missing, scheduled, and terminal obligations', async () => {
|
|
1143
|
+
const db = TestApp.database()
|
|
1144
|
+
const { reference } = await stagedObligation(db)
|
|
1145
|
+
|
|
1146
|
+
await expect(
|
|
1147
|
+
Webhooks.claimQueueEvent(db, { eventId: 'evt_absent', subscriptionId: 'wh_absent' }),
|
|
1148
|
+
).resolves.toEqual({ type: 'missing' })
|
|
1149
|
+
|
|
1150
|
+
const claim = await Webhooks.claimQueueEvent(db, reference)
|
|
1151
|
+
expect(claim.type).toBe('claimed')
|
|
1152
|
+
if (claim.type !== 'claimed') return
|
|
1153
|
+
expect(
|
|
1154
|
+
await Webhooks.scheduleQueueEventRetry(db, reference, {
|
|
1155
|
+
attemptCount: 1,
|
|
1156
|
+
claimedAt: claim.record.attemptingAt!,
|
|
1157
|
+
}),
|
|
1158
|
+
).toBe(true)
|
|
1159
|
+
await expect(Webhooks.claimQueueEvent(db, reference)).resolves.toEqual({
|
|
1160
|
+
nextAttemptAt: new Date(fixedNow().getTime() + Webhooks.retryDelayMs(1)).toISOString(),
|
|
1161
|
+
type: 'scheduled',
|
|
1162
|
+
})
|
|
1163
|
+
|
|
1164
|
+
vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.retryDelayMs(1) + 1))
|
|
1165
|
+
const retried = await Webhooks.claimQueueEvent(db, reference)
|
|
1166
|
+
expect(retried.type).toBe('claimed')
|
|
1167
|
+
if (retried.type !== 'claimed') return
|
|
1168
|
+
expect(
|
|
1169
|
+
await Webhooks.completeQueueEvent(db, reference, {
|
|
1170
|
+
claimedAt: retried.record.attemptingAt!,
|
|
1171
|
+
status: 'failed',
|
|
1172
|
+
}),
|
|
1173
|
+
).toBe(true)
|
|
1174
|
+
await expect(Webhooks.claimQueueEvent(db, reference)).resolves.toEqual({
|
|
1175
|
+
status: 'failed',
|
|
1176
|
+
type: 'terminal',
|
|
1177
|
+
})
|
|
1178
|
+
})
|
|
1179
|
+
|
|
1180
|
+
test('keeps owed work claimable indefinitely', async () => {
|
|
1181
|
+
const db = TestApp.database()
|
|
1182
|
+
const { reference } = await stagedObligation(db)
|
|
1183
|
+
|
|
1184
|
+
// Pending obligations never expire; only terminal transitions stamp a
|
|
1185
|
+
// dedupe deadline.
|
|
1186
|
+
vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.deliveryRetentionMs * 3))
|
|
1187
|
+
await expect(Webhooks.claimQueueEvent(db, reference)).resolves.toMatchObject({
|
|
1188
|
+
type: 'claimed',
|
|
1189
|
+
})
|
|
1190
|
+
})
|
|
1191
|
+
|
|
1192
|
+
test('never prunes pending work', async () => {
|
|
1193
|
+
const db = TestApp.database()
|
|
1194
|
+
const { reference } = await stagedObligation(db)
|
|
1195
|
+
|
|
1196
|
+
vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.deliveryRetentionMs + 1_000))
|
|
1197
|
+
await Webhooks.pruneExpired(db)
|
|
1198
|
+
const claim = await Webhooks.claimQueueEvent(db, reference)
|
|
1199
|
+
expect(claim.type).toBe('claimed')
|
|
1200
|
+
if (claim.type !== 'claimed') return
|
|
1201
|
+
expect(
|
|
1202
|
+
await Webhooks.completeQueueEvent(db, reference, {
|
|
1203
|
+
claimedAt: claim.record.attemptingAt!,
|
|
1204
|
+
status: 'succeeded',
|
|
1205
|
+
}),
|
|
1206
|
+
).toBe(true)
|
|
1207
|
+
})
|
|
1208
|
+
|
|
1209
|
+
test('fences a stale claimant after a reclaim', async () => {
|
|
1210
|
+
const db = TestApp.database()
|
|
1211
|
+
const { reference } = await stagedObligation(db)
|
|
1212
|
+
const stale = await Webhooks.claimQueueEvent(db, reference)
|
|
1213
|
+
expect(stale.type).toBe('claimed')
|
|
1214
|
+
if (stale.type !== 'claimed') return
|
|
1215
|
+
|
|
1216
|
+
vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.deliveryAttemptStaleMs + 1))
|
|
1217
|
+
const current = await Webhooks.claimQueueEvent(db, reference)
|
|
1218
|
+
expect(current.type).toBe('claimed')
|
|
1219
|
+
if (current.type !== 'claimed') return
|
|
1220
|
+
|
|
1221
|
+
const staleClaimedAt = stale.record.attemptingAt!
|
|
1222
|
+
expect(
|
|
1223
|
+
await Webhooks.completeQueueEvent(db, reference, {
|
|
1224
|
+
claimedAt: staleClaimedAt,
|
|
1225
|
+
status: 'failed',
|
|
1226
|
+
}),
|
|
1227
|
+
).toBe(false)
|
|
1228
|
+
expect(
|
|
1229
|
+
await Webhooks.scheduleQueueEventRetry(db, reference, {
|
|
1230
|
+
attemptCount: 1,
|
|
1231
|
+
claimedAt: staleClaimedAt,
|
|
1232
|
+
}),
|
|
1233
|
+
).toBe(false)
|
|
1234
|
+
expect(
|
|
1235
|
+
await Webhooks.completeQueueEvent(db, reference, {
|
|
1236
|
+
claimedAt: current.record.attemptingAt!,
|
|
1237
|
+
status: 'succeeded',
|
|
1238
|
+
}),
|
|
1239
|
+
).toBe(true)
|
|
1240
|
+
})
|
|
1241
|
+
})
|
|
1242
|
+
|
|
1243
|
+
describe('retryDelayMs', () => {
|
|
1244
|
+
test('backs off exponentially and caps hourly', () => {
|
|
1245
|
+
expect([1, 2, 3, 4, 5, 6, 10].map(Webhooks.retryDelayMs)).toMatchInlineSnapshot(`
|
|
1246
|
+
[
|
|
1247
|
+
5000,
|
|
1248
|
+
25000,
|
|
1249
|
+
125000,
|
|
1250
|
+
625000,
|
|
1251
|
+
3125000,
|
|
1252
|
+
3600000,
|
|
1253
|
+
3600000,
|
|
1254
|
+
]
|
|
1255
|
+
`)
|
|
1256
|
+
})
|
|
1257
|
+
})
|
|
1258
|
+
|
|
1259
|
+
describe('cursorBlock', () => {
|
|
1260
|
+
test('decodes one- and two-part cursors and rejects the rest', () => {
|
|
1261
|
+
expect(Webhooks.cursorBlock(null)).toBeUndefined()
|
|
1262
|
+
expect(Webhooks.cursorBlock('not-a-cursor')).toBeUndefined()
|
|
1263
|
+
expect(Webhooks.cursorBlock(Cursor.encode([123]))).toBe(123)
|
|
1264
|
+
expect(Webhooks.cursorBlock(Cursor.encode([456, 7]))).toBe(456)
|
|
1265
|
+
})
|
|
1266
|
+
})
|
|
1267
|
+
|
|
1268
|
+
describe('dueQueueEvents', () => {
|
|
1269
|
+
beforeEach(() => {
|
|
1270
|
+
vi.useFakeTimers({ toFake: ['Date'] })
|
|
1271
|
+
vi.setSystemTime(fixedNow())
|
|
1272
|
+
})
|
|
1273
|
+
|
|
1274
|
+
afterEach(() => {
|
|
1275
|
+
vi.useRealTimers()
|
|
1276
|
+
})
|
|
1277
|
+
|
|
1278
|
+
test('lists due pending work and skips in-flight, scheduled, and terminal rows', async () => {
|
|
1279
|
+
const db = TestApp.database()
|
|
1280
|
+
// Distinct staging times so the due order (next_attempt_at) is deterministic.
|
|
1281
|
+
const due = await stagedObligation(db, 1)
|
|
1282
|
+
vi.setSystemTime(new Date(fixedNow().getTime() + 1))
|
|
1283
|
+
const inFlight = await stagedObligation(db, 2)
|
|
1284
|
+
vi.setSystemTime(new Date(fixedNow().getTime() + 2))
|
|
1285
|
+
const scheduled = await stagedObligation(db, 3)
|
|
1286
|
+
vi.setSystemTime(new Date(fixedNow().getTime() + 3))
|
|
1287
|
+
const terminal = await stagedObligation(db, 4)
|
|
1288
|
+
|
|
1289
|
+
expect((await Webhooks.claimQueueEvent(db, inFlight.reference)).type).toBe('claimed')
|
|
1290
|
+
const scheduledClaim = await Webhooks.claimQueueEvent(db, scheduled.reference)
|
|
1291
|
+
expect(scheduledClaim.type).toBe('claimed')
|
|
1292
|
+
if (scheduledClaim.type !== 'claimed') return
|
|
1293
|
+
expect(
|
|
1294
|
+
await Webhooks.scheduleQueueEventRetry(db, scheduled.reference, {
|
|
1295
|
+
attemptCount: 3,
|
|
1296
|
+
claimedAt: scheduledClaim.record.attemptingAt!,
|
|
1297
|
+
}),
|
|
1298
|
+
).toBe(true)
|
|
1299
|
+
const terminalClaim = await Webhooks.claimQueueEvent(db, terminal.reference)
|
|
1300
|
+
expect(terminalClaim.type).toBe('claimed')
|
|
1301
|
+
if (terminalClaim.type !== 'claimed') return
|
|
1302
|
+
expect(
|
|
1303
|
+
await Webhooks.completeQueueEvent(db, terminal.reference, {
|
|
1304
|
+
claimedAt: terminalClaim.record.attemptingAt!,
|
|
1305
|
+
status: 'succeeded',
|
|
1306
|
+
}),
|
|
1307
|
+
).toBe(true)
|
|
1308
|
+
|
|
1309
|
+
await expect(Webhooks.dueQueueEvents(db, 10)).resolves.toEqual([due.reference])
|
|
1310
|
+
|
|
1311
|
+
vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.deliveryAttemptStaleMs + 10))
|
|
1312
|
+
await expect(Webhooks.dueQueueEvents(db, 10)).resolves.toEqual([
|
|
1313
|
+
due.reference,
|
|
1314
|
+
inFlight.reference,
|
|
1315
|
+
])
|
|
1316
|
+
})
|
|
1317
|
+
})
|
|
1318
|
+
|
|
1319
|
+
describe('pendingQueueEvents', () => {
|
|
1320
|
+
beforeEach(() => {
|
|
1321
|
+
vi.useFakeTimers({ toFake: ['Date'] })
|
|
1322
|
+
vi.setSystemTime(fixedNow())
|
|
1323
|
+
})
|
|
1324
|
+
|
|
1325
|
+
afterEach(() => {
|
|
1326
|
+
vi.useRealTimers()
|
|
1327
|
+
})
|
|
1328
|
+
|
|
1329
|
+
test('counts pending obligations and reports the oldest staging time', async () => {
|
|
1330
|
+
const db = TestApp.database()
|
|
1331
|
+
await stagedObligation(db, 1)
|
|
1332
|
+
vi.setSystemTime(new Date(fixedNow().getTime() + 1_000))
|
|
1333
|
+
const done = await stagedObligation(db, 2)
|
|
1334
|
+
const doneClaim = await Webhooks.claimQueueEvent(db, done.reference)
|
|
1335
|
+
expect(doneClaim.type).toBe('claimed')
|
|
1336
|
+
if (doneClaim.type !== 'claimed') return
|
|
1337
|
+
expect(
|
|
1338
|
+
await Webhooks.completeQueueEvent(db, done.reference, {
|
|
1339
|
+
claimedAt: doneClaim.record.attemptingAt!,
|
|
1340
|
+
status: 'succeeded',
|
|
1341
|
+
}),
|
|
1342
|
+
).toBe(true)
|
|
1343
|
+
|
|
1344
|
+
await expect(Webhooks.pendingQueueEvents(db)).resolves.toEqual({
|
|
1345
|
+
count: 1,
|
|
1346
|
+
dueCount: 1,
|
|
1347
|
+
oldestAt: fixedNow().toISOString(),
|
|
1348
|
+
oldestDueAt: fixedNow().toISOString(),
|
|
1349
|
+
})
|
|
1350
|
+
})
|
|
1351
|
+
|
|
1352
|
+
test('excludes in-flight claims and scheduled retries from due work', async () => {
|
|
1353
|
+
const db = TestApp.database()
|
|
1354
|
+
const inFlight = await stagedObligation(db, 1)
|
|
1355
|
+
const claim = await Webhooks.claimQueueEvent(db, inFlight.reference)
|
|
1356
|
+
expect(claim.type).toBe('claimed')
|
|
1357
|
+
|
|
1358
|
+
await expect(Webhooks.pendingQueueEvents(db)).resolves.toMatchObject({
|
|
1359
|
+
count: 1,
|
|
1360
|
+
dueCount: 0,
|
|
1361
|
+
oldestDueAt: null,
|
|
1362
|
+
})
|
|
1363
|
+
})
|
|
1364
|
+
})
|
|
1365
|
+
|
|
1366
|
+
describe('deliverClaimedAndRecord', () => {
|
|
1367
|
+
test('delivers a claimed envelope and skips an inactive subscription', async () => {
|
|
1368
|
+
const db = TestApp.database()
|
|
1369
|
+
const { envelope, reference, subscription } = await stagedObligation(db)
|
|
1370
|
+
const claim = await Webhooks.claimQueueEvent(db, reference)
|
|
1371
|
+
expect(claim.type).toBe('claimed')
|
|
1372
|
+
if (claim.type !== 'claimed') return
|
|
1373
|
+
|
|
1374
|
+
let delivered: unknown
|
|
1375
|
+
const fetch = (async (_url, init) => {
|
|
1376
|
+
delivered = JSON.parse(typeof init?.body === 'string' ? init.body : '{}')
|
|
1377
|
+
return new Response(null, { status: 200 })
|
|
1378
|
+
}) satisfies typeof globalThis.fetch
|
|
1379
|
+
|
|
1380
|
+
const delivery = await Webhooks.deliverClaimedAndRecord(
|
|
1381
|
+
db,
|
|
1382
|
+
{ envelope: claim.record.envelope, subscriptionId: reference.subscriptionId },
|
|
1383
|
+
{ fetch },
|
|
1384
|
+
)
|
|
1385
|
+
expect(delivery.status).toBe('delivered')
|
|
1386
|
+
if (delivery.status !== 'delivered') return
|
|
1387
|
+
expect(delivery.result.ok).toBe(true)
|
|
1388
|
+
expect(delivered).toEqual(envelope)
|
|
1389
|
+
|
|
1390
|
+
await Webhooks.deleteSubscription(db, apiKeyOwner, subscription.id)
|
|
1391
|
+
await expect(
|
|
1392
|
+
Webhooks.deliverClaimedAndRecord(db, {
|
|
1393
|
+
envelope: claim.record.envelope,
|
|
1394
|
+
subscriptionId: reference.subscriptionId,
|
|
1395
|
+
}),
|
|
1396
|
+
).resolves.toEqual({ status: 'skipped' })
|
|
1397
|
+
})
|
|
1398
|
+
})
|
|
1399
|
+
|
|
912
1400
|
describe('buildPingEnvelope', () => {
|
|
913
1401
|
test('builds a synthetic ping envelope with a deterministic id from the nonce', async () => {
|
|
914
1402
|
const db = TestApp.database()
|