tapimo 0.9.0 → 0.10.1
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 +4292 -3708
- 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 +21 -20
- package/dist/Webhooks.d.ts.map +1 -1
- package/dist/Webhooks.js +66 -23
- 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 +26 -17
- 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/apps/verified-tokens.d.ts +168 -168
- 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 +26 -0
- package/dist/analytics/Analytics.js.map +1 -1
- package/dist/apps/Zones.d.ts +4280 -84
- package/dist/apps/Zones.d.ts.map +1 -1
- package/dist/apps/Zones.js +30 -10
- package/dist/apps/Zones.js.map +1 -1
- package/dist/apps/data/App.d.ts +5799 -4929
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/FxOracle.d.ts +4 -5
- package/dist/apps/data/FxOracle.d.ts.map +1 -1
- package/dist/apps/data/FxOracle.js +19 -4
- package/dist/apps/data/FxOracle.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +3391 -492
- package/dist/apps/data/routes/activities.d.ts.map +1 -1
- package/dist/apps/data/routes/activities.js +372 -29
- package/dist/apps/data/routes/activities.js.map +1 -1
- package/dist/apps/data/routes/addresses.d.ts +853 -339
- package/dist/apps/data/routes/addresses.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.d.ts +10 -10
- package/dist/apps/data/routes/balances.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.js +145 -83
- package/dist/apps/data/routes/balances.js.map +1 -1
- package/dist/apps/data/routes/blocks.d.ts +123 -123
- package/dist/apps/data/routes/coingecko.d.ts +169 -169
- package/dist/apps/data/routes/earn.d.ts +455 -1
- package/dist/apps/data/routes/earn.d.ts.map +1 -1
- package/dist/apps/data/routes/earn.js +369 -45
- package/dist/apps/data/routes/earn.js.map +1 -1
- package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
- package/dist/apps/data/routes/exchanges.js +114 -118
- 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 +35 -34
- 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.map +1 -1
- package/dist/apps/data/routes/receipts.js +68 -47
- package/dist/apps/data/routes/receipts.js.map +1 -1
- package/dist/apps/data/routes/rpc.d.ts +31 -31
- 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 +461 -459
- package/dist/apps/data/routes/tokens.d.ts.map +1 -1
- package/dist/apps/data/routes/transactions.d.ts +313 -305
- package/dist/apps/data/routes/transactions.d.ts.map +1 -1
- package/dist/apps/data/routes/transactions.js +25 -23
- package/dist/apps/data/routes/transactions.js.map +1 -1
- package/dist/apps/data/routes/transfers.d.ts +108 -86
- package/dist/apps/data/routes/transfers.d.ts.map +1 -1
- package/dist/apps/data/routes/transfers.js +618 -105
- package/dist/apps/data/routes/transfers.js.map +1 -1
- package/dist/apps/data/routes/valuation.d.ts +21 -21
- package/dist/apps/data/routes/valuation.d.ts.map +1 -1
- package/dist/apps/data/routes/valuation.js +2 -2
- package/dist/apps/data/routes/valuation.js.map +1 -1
- package/dist/apps/data/routes/webhooks.d.ts +519 -868
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +642 -353
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/data/routes/zones.d.ts +31 -31
- package/dist/apps/data/routes/zones.d.ts.map +1 -1
- package/dist/apps/data/routes/zones.js +4 -2
- package/dist/apps/data/routes/zones.js.map +1 -1
- package/dist/apps/funding/routes/deposit-addresses.js +1 -1
- package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
- package/dist/apps/funding/routes/providers.d.ts +13 -13
- package/dist/apps/funding/routes/quotes.d.ts +89 -89
- package/dist/apps/funding/routes/transfers.d.ts +36 -36
- 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 +11 -309
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +74 -74
- 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 +35 -35
- package/dist/apps/management/routes/faucet.js +2 -2
- package/dist/apps/management/routes/faucet.js.map +1 -1
- package/dist/apps/management/routes/invitations.d.ts +134 -134
- package/dist/apps/management/routes/invite-links.d.ts +80 -80
- package/dist/apps/management/routes/members.d.ts +69 -69
- package/dist/apps/management/routes/orgs.d.ts +98 -98
- package/dist/apps/management/routes/projects.d.ts +60 -60
- package/dist/apps/management/routes/usage.d.ts +72 -72
- package/dist/apps/management/routes/verified-token-requests.d.ts +83 -83
- package/dist/apps/management/routes/webhooks.d.ts +203 -618
- package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
- 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/apps/mpp/App.d.ts +56 -44
- package/dist/apps/mpp/App.d.ts.map +1 -1
- package/dist/apps/mpp/Mpp.d.ts +3 -0
- package/dist/apps/mpp/Mpp.d.ts.map +1 -1
- package/dist/apps/mpp/Mpp.js +3 -0
- package/dist/apps/mpp/Mpp.js.map +1 -1
- package/dist/apps/mpp/internal/Adapter.d.ts.map +1 -1
- package/dist/apps/mpp/internal/Adapter.js +9 -2
- package/dist/apps/mpp/internal/Adapter.js.map +1 -1
- package/dist/db/Db.d.ts +4 -2
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +75 -15
- 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/sponsoredTransactions.d.ts.map +1 -1
- package/dist/db/tables/sponsoredTransactions.js +4 -1
- package/dist/db/tables/sponsoredTransactions.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 +58 -3
- package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
- package/dist/db/tables/webhookSubscriptions.js +86 -3
- package/dist/db/tables/webhookSubscriptions.js.map +1 -1
- package/dist/internal/Auth.d.ts +3 -0
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +6 -4
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/MetricSink.d.ts +139 -3
- package/dist/internal/MetricSink.d.ts.map +1 -1
- package/dist/internal/MetricSink.js +144 -4
- 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/Schema.js +1 -1
- package/dist/internal/Schema.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 +1 -3
- package/dist/internal/Tidx.d.ts.map +1 -1
- package/dist/internal/Tidx.js +1 -3
- 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.map +1 -1
- package/dist/internal/Viem.js +2 -1
- package/dist/internal/Viem.js.map +1 -1
- package/dist/internal/WebhookDestination.d.ts +2 -0
- package/dist/internal/WebhookDestination.d.ts.map +1 -1
- package/dist/internal/WebhookDestination.js +11 -0
- package/dist/internal/WebhookDestination.js.map +1 -1
- package/dist/internal/WebhookTransfer.d.ts +75 -0
- package/dist/internal/WebhookTransfer.d.ts.map +1 -0
- package/dist/internal/WebhookTransfer.js +299 -0
- package/dist/internal/WebhookTransfer.js.map +1 -0
- package/dist/internal/Webhooks.d.ts +176 -0
- package/dist/internal/Webhooks.d.ts.map +1 -1
- package/dist/internal/Webhooks.js +216 -20
- package/dist/internal/Webhooks.js.map +1 -1
- package/dist/internal/funding/Provider.d.ts +4 -0
- package/dist/internal/funding/Provider.d.ts.map +1 -1
- package/dist/internal/funding/Provider.js +11 -7
- package/dist/internal/funding/Provider.js.map +1 -1
- package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
- package/dist/internal/funding/Reconciliation.js +128 -0
- package/dist/internal/funding/Reconciliation.js.map +1 -1
- package/dist/internal/funding/providers/relay.d.ts +6 -4
- package/dist/internal/funding/providers/relay.d.ts.map +1 -1
- package/dist/internal/funding/providers/relay.js +71 -9
- package/dist/internal/funding/providers/relay.js.map +1 -1
- package/package.json +4 -4
- package/src/ApiKeys.test.ts +20 -0
- package/src/ApiKeys.ts +28 -1
- package/src/App.test.ts +10 -8
- package/src/App.ts +1 -0
- package/src/Client.test-d.ts +61 -5
- package/src/Webhooks.bench.ts +714 -0
- package/src/Webhooks.memory.bench.ts +250 -0
- package/src/Webhooks.test.ts +77 -6
- package/src/Webhooks.ts +85 -29
- 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 +8 -0
- package/src/analytics/Analytics.ts +26 -0
- package/src/apps/Zones.test.ts +23 -5
- package/src/apps/Zones.ts +31 -10
- package/src/apps/composition.test.ts +1 -0
- package/src/apps/data/App.test.ts +2 -2
- package/src/apps/data/FxOracle.test.ts +18 -0
- package/src/apps/data/FxOracle.ts +28 -5
- package/src/apps/data/routes/activities.bench.ts +41 -0
- package/src/apps/data/routes/activities.test.ts +122 -2
- package/src/apps/data/routes/activities.ts +435 -32
- package/src/apps/data/routes/balances.bench.ts +21 -0
- package/src/apps/data/routes/balances.test.ts +119 -10
- package/src/apps/data/routes/balances.ts +164 -97
- package/src/apps/data/routes/blocks.bench.ts +34 -0
- package/src/apps/data/routes/blocks.test.ts +12 -6
- 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.test.ts +197 -0
- package/src/apps/data/routes/earn.ts +487 -45
- package/src/apps/data/routes/exchanges.bench.ts +85 -0
- package/src/apps/data/routes/exchanges.test.ts +7 -6
- package/src/apps/data/routes/exchanges.ts +124 -117
- 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.test.ts +24 -7
- package/src/apps/data/routes/receipts.ts +67 -50
- 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.test.ts +51 -0
- package/src/apps/data/routes/transactions.bench.ts +40 -0
- package/src/apps/data/routes/transactions.test.ts +42 -0
- package/src/apps/data/routes/transactions.ts +44 -39
- package/src/apps/data/routes/transfers.bench.ts +28 -0
- package/src/apps/data/routes/transfers.test.ts +402 -24
- package/src/apps/data/routes/transfers.ts +708 -101
- package/src/apps/data/routes/valuation.bench.ts +14 -0
- package/src/apps/data/routes/valuation.test.ts +26 -0
- package/src/apps/data/routes/valuation.ts +6 -2
- 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 +466 -135
- package/src/apps/data/routes/webhooks.ts +820 -429
- 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 +7 -2
- package/src/apps/funding/routes/chains.test.ts +1 -1
- package/src/apps/funding/routes/deposit-addresses.test.ts +1 -1
- package/src/apps/funding/routes/deposit-addresses.ts +1 -1
- package/src/apps/funding/routes/providers.test.ts +1 -1
- package/src/apps/funding/routes/quotes.test.ts +2 -2
- package/src/apps/funding/routes/transfers.test.ts +3 -6
- 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/apps/mpp/Mpp.test.ts +13 -1
- package/src/apps/mpp/Mpp.ts +8 -0
- package/src/apps/mpp/internal/Adapter.ts +10 -2
- package/src/db/Db.test.ts +40 -6
- package/src/db/Db.ts +114 -19
- package/src/db/Schema.ts +32 -0
- package/src/db/tables/sponsoredTransactions.ts +9 -2
- package/src/db/tables/webhookQueueEvents.ts +285 -0
- package/src/db/tables/webhookSubscriptions.ts +131 -4
- package/src/internal/Auth.test.ts +61 -50
- package/src/internal/Auth.ts +9 -4
- package/src/internal/EarnVaults.test.ts +2 -2
- package/src/internal/MetricSink.test.ts +575 -0
- package/src/internal/MetricSink.ts +325 -10
- package/src/internal/RateLimit.test.ts +23 -18
- package/src/internal/RateLimit.ts +23 -15
- package/src/internal/Schema.ts +1 -1
- 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 +1 -3
- package/src/internal/Value.test.ts +25 -0
- package/src/internal/Value.ts +29 -0
- package/src/internal/Viem.test.ts +8 -0
- package/src/internal/Viem.ts +2 -1
- package/src/internal/WebhookDestination.test.ts +20 -0
- package/src/internal/WebhookDestination.ts +10 -0
- package/src/internal/WebhookTransfer.test.ts +89 -0
- package/src/internal/WebhookTransfer.ts +430 -0
- package/src/internal/Webhooks.test.ts +616 -0
- package/src/internal/Webhooks.ts +412 -24
- package/src/internal/funding/Provider.test.ts +48 -8
- package/src/internal/funding/Provider.ts +12 -7
- package/src/internal/funding/Reconciliation.local.test.ts +3 -1
- package/src/internal/funding/Reconciliation.test.ts +1240 -0
- package/src/internal/funding/Reconciliation.ts +136 -0
- package/src/internal/funding/SourceObservation.test.ts +1 -0
- package/src/internal/funding/providers/index.test.ts +73 -3
- package/src/internal/funding/providers/relay.test.ts +2 -9
- package/src/internal/funding/providers/relay.ts +72 -13
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/** @module-tag localnet */
|
|
2
|
+
/**
|
|
3
|
+
* Measures retained database-factory memory and dense transfer-scan allocations.
|
|
4
|
+
* Set `WEBHOOK_MEMORY_SNAPSHOT_DIR` to write V8 heap snapshots for retained objects.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'node:fs'
|
|
7
|
+
import * as path from 'node:path'
|
|
8
|
+
import * as v8 from 'node:v8'
|
|
9
|
+
import { nanoid } from 'nanoid'
|
|
10
|
+
import { sendTransactionSync } from 'viem/actions'
|
|
11
|
+
import { Actions, Addresses } from 'viem/tempo'
|
|
12
|
+
import { Webhooks } from 'tapimo'
|
|
13
|
+
import { bench } from 'vite-plus/test'
|
|
14
|
+
|
|
15
|
+
import * as Db from './db/Db.js'
|
|
16
|
+
import * as Cursor from './internal/Cursor.js'
|
|
17
|
+
import * as Runtime from '../test/runtime.js'
|
|
18
|
+
import * as TestApp from '../test/App.js'
|
|
19
|
+
import * as Tempo from '../test/Tempo.js'
|
|
20
|
+
import * as TestViem from '../test/Viem.js'
|
|
21
|
+
|
|
22
|
+
const batchSize = 200
|
|
23
|
+
const endOfBlockIndex = 2_147_483_647
|
|
24
|
+
const factoryTicks = positiveInteger('WEBHOOK_MEMORY_FACTORY_TICKS', 25)
|
|
25
|
+
const mebibyte = 1024 * 1024
|
|
26
|
+
const runtime = Runtime.get()
|
|
27
|
+
const transferCallsPerTransaction = 32
|
|
28
|
+
const transferCount = positiveInteger('WEBHOOK_MEMORY_TRANSFER_COUNT', 3_600)
|
|
29
|
+
|
|
30
|
+
type Memory = {
|
|
31
|
+
arrayBuffers: number
|
|
32
|
+
external: number
|
|
33
|
+
heapUsed: number
|
|
34
|
+
rss: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type Report = {
|
|
38
|
+
after: Memory
|
|
39
|
+
afterOperation: Memory
|
|
40
|
+
afterOperationDelta: Memory
|
|
41
|
+
before: Memory
|
|
42
|
+
delta: Memory
|
|
43
|
+
details?: Record<string, number> | undefined
|
|
44
|
+
label: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const reports: Report[] = []
|
|
48
|
+
|
|
49
|
+
beforeAll(() => {
|
|
50
|
+
if (runtime.mode !== 'localnet')
|
|
51
|
+
throw new Error('Run webhook memory benchmarks with TEMPO_ENV=localnet.')
|
|
52
|
+
if (!globalThis.gc)
|
|
53
|
+
throw new Error('Run webhook memory benchmarks with NODE_OPTIONS=--expose-gc.')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
afterAll(() => {
|
|
57
|
+
process.stdout.write(`WEBHOOK_MEMORY_REPORT ${JSON.stringify(reports)}\n`)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('webhook poller memory', () => {
|
|
61
|
+
bench(
|
|
62
|
+
`factory-created databases / ${factoryTicks} ticks`,
|
|
63
|
+
async () => {
|
|
64
|
+
reports.push(await measureFactoryRetention())
|
|
65
|
+
},
|
|
66
|
+
{ iterations: 1, time: 0, warmupIterations: 0, warmupTime: 0 },
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
bench(
|
|
70
|
+
`transfer firehose / ${transferCount} generated transfers`,
|
|
71
|
+
async () => {
|
|
72
|
+
reports.push(await measureTransferFirehose())
|
|
73
|
+
},
|
|
74
|
+
{ iterations: 1, time: 0, warmupIterations: 0, warmupTime: 0 },
|
|
75
|
+
)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
async function measureFactoryRetention(): Promise<Report> {
|
|
79
|
+
const databases: Db.Db[] = []
|
|
80
|
+
const schema = `memory_${nanoid()}`
|
|
81
|
+
const database = Db.postgres({ connectionString: Runtime.postgresUrl, schema })
|
|
82
|
+
await database.migrate()
|
|
83
|
+
await Webhooks.createSubscription(database, {
|
|
84
|
+
chainId: runtime.chainId,
|
|
85
|
+
destination: { type: 'url', url: 'https://hooks.example.com/bench/memory/factory' },
|
|
86
|
+
eventType: 'block:created',
|
|
87
|
+
owner: { orgId: 'org_benchmark', type: 'api_key' },
|
|
88
|
+
})
|
|
89
|
+
const headBlockNumber = Number(await TestViem.getClient().getBlockNumber())
|
|
90
|
+
const poller = Webhooks.createPoller({
|
|
91
|
+
db: () => {
|
|
92
|
+
const database = Db.postgres({ connectionString: Runtime.postgresUrl, schema })
|
|
93
|
+
databases.push(database)
|
|
94
|
+
return database
|
|
95
|
+
},
|
|
96
|
+
defaultChainId: runtime.chainId,
|
|
97
|
+
dispatchBatch: async () => ({ ok: true }),
|
|
98
|
+
rpc: TestApp.rpc,
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const report = await measure('factory-retained', async () => {
|
|
103
|
+
for (let index = 0; index < factoryTicks; index++)
|
|
104
|
+
await poller.tick({
|
|
105
|
+
confirmations: 0,
|
|
106
|
+
eventTypes: ['block:created'],
|
|
107
|
+
headBlockNumber,
|
|
108
|
+
maxBlocks: 1,
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
writeHeapSnapshot('factory-retained')
|
|
112
|
+
return { ...report, details: { databases: databases.length, ticks: factoryTicks } }
|
|
113
|
+
} finally {
|
|
114
|
+
await Promise.allSettled(databases.map((database) => database.close()))
|
|
115
|
+
await database.close()
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function measureTransferFirehose(): Promise<Report> {
|
|
120
|
+
const database = TestApp.database()
|
|
121
|
+
const client = Tempo.getClient({ account: Tempo.accounts[0] })
|
|
122
|
+
const startBlockNumber = Number(await TestViem.getClient().getBlockNumber())
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
for (let offset = 0; offset < transferCount; offset += transferCallsPerTransaction) {
|
|
126
|
+
const length = Math.min(transferCallsPerTransaction, transferCount - offset)
|
|
127
|
+
await sendTransactionSync(client, {
|
|
128
|
+
account: Tempo.accounts[0],
|
|
129
|
+
calls: Array.from({ length }, () =>
|
|
130
|
+
Actions.token.transfer.call(client, {
|
|
131
|
+
amount: 1n,
|
|
132
|
+
to: Tempo.accounts[1].address,
|
|
133
|
+
token: Addresses.pathUsd,
|
|
134
|
+
}),
|
|
135
|
+
),
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const headBlockNumber = Number(await TestViem.getClient().getBlockNumber())
|
|
140
|
+
const subscription = await Webhooks.createSubscription(database, {
|
|
141
|
+
chainId: runtime.chainId,
|
|
142
|
+
destination: { type: 'url', url: 'https://hooks.example.com/bench/memory/firehose' },
|
|
143
|
+
eventType: 'token:transfer',
|
|
144
|
+
filters: {},
|
|
145
|
+
owner: { orgId: 'org_benchmark', type: 'api_key' },
|
|
146
|
+
})
|
|
147
|
+
await Webhooks.setCursor(
|
|
148
|
+
database,
|
|
149
|
+
subscription.id,
|
|
150
|
+
Cursor.encode([startBlockNumber, endOfBlockIndex]),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
let dispatched = 0
|
|
154
|
+
let serializedBytes = 0
|
|
155
|
+
const poller = Webhooks.createPoller({
|
|
156
|
+
db: database,
|
|
157
|
+
defaultChainId: runtime.chainId,
|
|
158
|
+
dispatchBatch: async (dispatchables) => {
|
|
159
|
+
dispatched += dispatchables.length
|
|
160
|
+
for (const body of dispatchables)
|
|
161
|
+
serializedBytes += new TextEncoder().encode(JSON.stringify({ body })).byteLength
|
|
162
|
+
return { ok: true }
|
|
163
|
+
},
|
|
164
|
+
rpc: TestApp.rpc,
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
const report = await measure('transfer-firehose', async () => {
|
|
168
|
+
const result = await poller.tick({
|
|
169
|
+
batchSize,
|
|
170
|
+
confirmations: 0,
|
|
171
|
+
eventTypes: ['token:transfer'],
|
|
172
|
+
headBlockNumber,
|
|
173
|
+
maxBlocks: 500,
|
|
174
|
+
})
|
|
175
|
+
if (result.dispatched === 0 || result.dispatched !== dispatched)
|
|
176
|
+
throw new Error(`Unexpected dispatch count: ${result.dispatched} / ${dispatched}.`)
|
|
177
|
+
if (result.dispatched > batchSize)
|
|
178
|
+
throw new Error(`Dispatch count ${result.dispatched} exceeded batch size ${batchSize}.`)
|
|
179
|
+
if (serializedBytes === 0) throw new Error('Expected serialized webhook queue messages.')
|
|
180
|
+
})
|
|
181
|
+
writeHeapSnapshot('transfer-firehose-retained')
|
|
182
|
+
return {
|
|
183
|
+
...report,
|
|
184
|
+
details: { dispatched, serializedBytes, transferCount },
|
|
185
|
+
}
|
|
186
|
+
} finally {
|
|
187
|
+
await database.close()
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function measure(label: string, fn: () => Promise<void>): Promise<Report> {
|
|
192
|
+
collectGarbage()
|
|
193
|
+
const before = memoryUsage()
|
|
194
|
+
await fn()
|
|
195
|
+
const afterOperation = memoryUsage()
|
|
196
|
+
collectGarbage()
|
|
197
|
+
const after = memoryUsage()
|
|
198
|
+
return {
|
|
199
|
+
after,
|
|
200
|
+
afterOperation,
|
|
201
|
+
afterOperationDelta: subtract(afterOperation, before),
|
|
202
|
+
before,
|
|
203
|
+
delta: subtract(after, before),
|
|
204
|
+
label,
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function collectGarbage(): void {
|
|
209
|
+
globalThis.gc?.()
|
|
210
|
+
globalThis.gc?.()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function memoryUsage(): Memory {
|
|
214
|
+
const usage = process.memoryUsage()
|
|
215
|
+
return {
|
|
216
|
+
arrayBuffers: mebibytes(usage.arrayBuffers),
|
|
217
|
+
external: mebibytes(usage.external),
|
|
218
|
+
heapUsed: mebibytes(usage.heapUsed),
|
|
219
|
+
rss: mebibytes(usage.rss),
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function mebibytes(bytes: number): number {
|
|
224
|
+
return Math.round((bytes / mebibyte) * 100) / 100
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function positiveInteger(name: string, fallback: number): number {
|
|
228
|
+
const input = process.env[name]
|
|
229
|
+
if (!input) return fallback
|
|
230
|
+
const value = Number(input)
|
|
231
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
232
|
+
throw new Error(`${name} must be a positive integer.`)
|
|
233
|
+
return value
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function subtract(after: Memory, before: Memory): Memory {
|
|
237
|
+
return {
|
|
238
|
+
arrayBuffers: after.arrayBuffers - before.arrayBuffers,
|
|
239
|
+
external: after.external - before.external,
|
|
240
|
+
heapUsed: after.heapUsed - before.heapUsed,
|
|
241
|
+
rss: after.rss - before.rss,
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function writeHeapSnapshot(label: string): void {
|
|
246
|
+
const directory = process.env.WEBHOOK_MEMORY_SNAPSHOT_DIR
|
|
247
|
+
if (!directory) return
|
|
248
|
+
fs.mkdirSync(directory, { recursive: true })
|
|
249
|
+
v8.writeHeapSnapshot(path.join(directory, `${label}.heapsnapshot`))
|
|
250
|
+
}
|
package/src/Webhooks.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Webhooks } from 'tapimo'
|
|
1
|
+
import { Metrics, Webhooks } from 'tapimo'
|
|
2
2
|
import * as TestApp from '../test/App.js'
|
|
3
3
|
|
|
4
4
|
describe('createPoller', () => {
|
|
@@ -25,9 +25,8 @@ describe('createPoller', () => {
|
|
|
25
25
|
|
|
26
26
|
// A brand-new subscription anchors at the confirmed head on its first tick
|
|
27
27
|
// (no backfill), so this exercises the createPoller → poll wiring (db
|
|
28
|
-
// binding, default chain id, injected head, result shape) without
|
|
29
|
-
//
|
|
30
|
-
// src/apps/webhooks.test.ts.
|
|
28
|
+
// binding, default chain id, injected head, and result shape) without an
|
|
29
|
+
// upstream request. Detailed scanner behavior lives in the route test.
|
|
31
30
|
const result = await poller.tick({ headBlockNumber: 200 })
|
|
32
31
|
|
|
33
32
|
expect(result).toMatchInlineSnapshot(`
|
|
@@ -35,19 +34,70 @@ describe('createPoller', () => {
|
|
|
35
34
|
"chainId": 42431,
|
|
36
35
|
"dispatched": 0,
|
|
37
36
|
"failed": 0,
|
|
38
|
-
"safeBlock":
|
|
37
|
+
"safeBlock": 199,
|
|
39
38
|
"scanned": 0,
|
|
40
39
|
"subscriptions": 1,
|
|
41
40
|
}
|
|
42
41
|
`)
|
|
43
42
|
expect(decodeCursor(await Webhooks.getCursor(db, subscription.id))).toMatchInlineSnapshot(`
|
|
44
43
|
[
|
|
45
|
-
|
|
44
|
+
199,
|
|
46
45
|
2147483647,
|
|
47
46
|
]
|
|
48
47
|
`)
|
|
49
48
|
})
|
|
50
49
|
|
|
50
|
+
test('records active subscriptions before scanning', async () => {
|
|
51
|
+
const db = TestApp.database()
|
|
52
|
+
await Webhooks.createSubscription(db, {
|
|
53
|
+
chainId: 42431,
|
|
54
|
+
destination: { type: 'url', url: 'https://hooks.example.com/x' },
|
|
55
|
+
eventType: 'block:created',
|
|
56
|
+
owner: { orgId: 'org_test', type: 'api_key' },
|
|
57
|
+
})
|
|
58
|
+
const gauges: { name: string; tags: Metrics.Tags; value: number }[] = []
|
|
59
|
+
const poller = Webhooks.createPoller({
|
|
60
|
+
db,
|
|
61
|
+
defaultChainId: 42431,
|
|
62
|
+
metrics: Metrics.from({
|
|
63
|
+
count() {},
|
|
64
|
+
flush() {},
|
|
65
|
+
gauge(name, value, tags = {}) {
|
|
66
|
+
gauges.push({ name, tags, value })
|
|
67
|
+
},
|
|
68
|
+
histogram() {},
|
|
69
|
+
}),
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
await poller.tick({ eventTypes: ['block:created'], headBlockNumber: 200 })
|
|
73
|
+
|
|
74
|
+
expect(gauges).toMatchInlineSnapshot(`
|
|
75
|
+
[
|
|
76
|
+
{
|
|
77
|
+
"name": "webhook_poller_active_subscription_count",
|
|
78
|
+
"tags": {
|
|
79
|
+
"chain_id": "42431",
|
|
80
|
+
},
|
|
81
|
+
"value": 1,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"name": "webhook_subscription_cursor_lag_blocks",
|
|
85
|
+
"tags": {
|
|
86
|
+
"chain_id": "42431",
|
|
87
|
+
},
|
|
88
|
+
"value": 0,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"name": "webhook_poller_safe_block",
|
|
92
|
+
"tags": {
|
|
93
|
+
"chain_id": "42431",
|
|
94
|
+
},
|
|
95
|
+
"value": 199,
|
|
96
|
+
},
|
|
97
|
+
]
|
|
98
|
+
`)
|
|
99
|
+
})
|
|
100
|
+
|
|
51
101
|
test('clamps a creation checkpoint to genesis', async () => {
|
|
52
102
|
const db = TestApp.database()
|
|
53
103
|
const subscription = await Webhooks.createSubscription(
|
|
@@ -72,4 +122,25 @@ describe('createPoller', () => {
|
|
|
72
122
|
|
|
73
123
|
expect(decodeCursor(await Webhooks.getCursor(db, subscription.id))).toEqual([0, 2_147_483_647])
|
|
74
124
|
})
|
|
125
|
+
|
|
126
|
+
test('keeps a factory-created database open after each tick', async () => {
|
|
127
|
+
const db = TestApp.database()
|
|
128
|
+
await Webhooks.createSubscription(db, {
|
|
129
|
+
chainId: 42431,
|
|
130
|
+
destination: { type: 'url', url: 'https://hooks.example.com/x' },
|
|
131
|
+
eventType: 'block:created',
|
|
132
|
+
owner: { orgId: 'org_test', type: 'api_key' },
|
|
133
|
+
})
|
|
134
|
+
const poller = Webhooks.createPoller({
|
|
135
|
+
db: () => db,
|
|
136
|
+
defaultChainId: 42431,
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
await poller.tick({
|
|
140
|
+
eventTypes: ['block:created'],
|
|
141
|
+
headBlockNumber: 1,
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
await expect(Webhooks.maxCursorBlock(db, 42431)).resolves.toBe(0)
|
|
145
|
+
})
|
|
75
146
|
})
|
package/src/Webhooks.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import * as AppWebhooks from './apps/data/routes/webhooks.js'
|
|
2
2
|
import * as Db from './db/Db.js'
|
|
3
|
+
import * as core_Webhooks from './internal/Webhooks.js'
|
|
3
4
|
import * as MetricSink from './internal/MetricSink.js'
|
|
4
5
|
import * as Store from './internal/Store.js'
|
|
5
6
|
import type * as Metrics from './Metrics.js'
|
|
6
|
-
import * as Tidx from './internal/Tidx.js'
|
|
7
7
|
import * as Viem from './internal/Viem.js'
|
|
8
8
|
|
|
9
|
+
/** How often a poller reclaims expired rows, independent of tick cadence. */
|
|
10
|
+
const pruneIntervalMs = 60_000
|
|
11
|
+
|
|
9
12
|
// Re-export the runtime-agnostic primitives (subscriptions, signing, delivery,
|
|
10
13
|
// `deliverAndRecord`, cursors, …) so consumers reach everything through one
|
|
11
14
|
// `Webhooks` namespace. The apps-layer poller stays private behind
|
|
@@ -19,33 +22,29 @@ export * from './internal/Webhooks.js'
|
|
|
19
22
|
export { eventTypes, schema } from './apps/data/routes/webhooks.js'
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
|
-
* Builds a
|
|
25
|
+
* Builds a webhook poller bound to a state store and chain config.
|
|
23
26
|
*
|
|
24
|
-
* This is the entry point a host runtime drives
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* confirmations` and dispatches matching events.
|
|
27
|
+
* This is the entry point a host runtime drives from chain-head notifications
|
|
28
|
+
* or a timer. Each {@link Poller.tick} scans active subscriptions from their
|
|
29
|
+
* cursors up to `head - confirmations` and dispatches matching events.
|
|
28
30
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* poller
|
|
32
|
-
*
|
|
33
|
-
* also builds memoized TIDX clients (via {@link Tidx.createGetClient}) when a
|
|
34
|
-
* `tidx` config is supplied. The poller keeps `App.create` a pure Hono app: it
|
|
35
|
-
* constructs its own clients instead of reaching into app internals, and owns no
|
|
36
|
-
* request state.
|
|
31
|
+
* Every onchain event type scans a confirmed window directly over JSON-RPC, so
|
|
32
|
+
* the poller builds memoized viem clients via {@link Viem.createGetClient}. The
|
|
33
|
+
* poller keeps `App.create` a pure Hono app: it constructs its own clients
|
|
34
|
+
* instead of reaching into app internals, and owns no request state.
|
|
37
35
|
*/
|
|
38
36
|
export function createPoller(options: createPoller.Options): createPoller.Poller {
|
|
39
37
|
// Persistent memoize cache for token metadata, shared across ticks (the
|
|
40
38
|
// webhook state store used to double-duty as this cache).
|
|
41
|
-
const cache = Store.memory()
|
|
42
|
-
const getTidx = options.tidx
|
|
43
|
-
? Tidx.createGetClient({ defaultChainId: options.defaultChainId, tidx: options.tidx })
|
|
44
|
-
: undefined
|
|
39
|
+
const cache = options.cache ?? Store.memory()
|
|
45
40
|
const getClient = Viem.createGetClient({
|
|
46
41
|
defaultChainId: options.defaultChainId,
|
|
47
42
|
rpc: options.rpc,
|
|
48
43
|
})
|
|
44
|
+
// Chain-head ticks run about once per block, so the janitor is throttled here
|
|
45
|
+
// rather than issuing deletes per block. Reads filter expiry at query time, so
|
|
46
|
+
// reclaiming late only costs space.
|
|
47
|
+
let prunedAt: number | undefined
|
|
49
48
|
|
|
50
49
|
const metrics = options.metrics ? MetricSink.webhooks(options.metrics) : undefined
|
|
51
50
|
|
|
@@ -58,6 +57,8 @@ export function createPoller(options: createPoller.Options): createPoller.Poller
|
|
|
58
57
|
// returns the underlying result unchanged and is a no-op when no metrics
|
|
59
58
|
// backend is configured.
|
|
60
59
|
const inner = input.dispatch ?? options.dispatch
|
|
60
|
+
const innerBatch =
|
|
61
|
+
input.dispatchBatch ?? (input.dispatch === undefined ? options.dispatchBatch : undefined)
|
|
61
62
|
const dispatch =
|
|
62
63
|
metrics && inner
|
|
63
64
|
? async (dispatchable: AppWebhooks.poll.Dispatchable) => {
|
|
@@ -73,8 +74,37 @@ export function createPoller(options: createPoller.Options): createPoller.Poller
|
|
|
73
74
|
return result
|
|
74
75
|
}
|
|
75
76
|
: inner
|
|
77
|
+
const dispatchBatch = innerBatch
|
|
78
|
+
? async (
|
|
79
|
+
dispatchables: readonly AppWebhooks.poll.Dispatchable[],
|
|
80
|
+
context: AppWebhooks.poll.DispatchContext,
|
|
81
|
+
) => {
|
|
82
|
+
const start = Date.now()
|
|
83
|
+
const result = await innerBatch(dispatchables, context)
|
|
84
|
+
const durationMs = Date.now() - start
|
|
85
|
+
for (const dispatchable of dispatchables)
|
|
86
|
+
metrics?.record({
|
|
87
|
+
chainId: dispatchable.envelope.chainId,
|
|
88
|
+
durationMs,
|
|
89
|
+
eventType: dispatchable.envelope.type,
|
|
90
|
+
ok: result.ok,
|
|
91
|
+
type: 'webhook:enqueue',
|
|
92
|
+
})
|
|
93
|
+
return result
|
|
94
|
+
}
|
|
95
|
+
: undefined
|
|
96
|
+
|
|
97
|
+
// Resolve per tick so Workers factories build a fresh Hyperdrive-pooled
|
|
98
|
+
// connection each run.
|
|
99
|
+
const db = Db.get(options.db)
|
|
100
|
+
const now = input.now ?? options.now
|
|
101
|
+
if (prunedAt === undefined || Date.now() - prunedAt >= pruneIntervalMs) {
|
|
102
|
+
prunedAt = Date.now()
|
|
103
|
+
await core_Webhooks.pruneExpired(db, { now }).catch(() => {})
|
|
104
|
+
}
|
|
76
105
|
|
|
77
106
|
const start = Date.now()
|
|
107
|
+
let cursorLagBlocks: number | undefined
|
|
78
108
|
try {
|
|
79
109
|
const result = await AppWebhooks.poll({
|
|
80
110
|
batchSize: input.batchSize ?? options.batchSize,
|
|
@@ -82,19 +112,37 @@ export function createPoller(options: createPoller.Options): createPoller.Poller
|
|
|
82
112
|
chainId,
|
|
83
113
|
client: getClient(chainId),
|
|
84
114
|
confirmations: input.confirmations ?? options.confirmations,
|
|
85
|
-
|
|
86
|
-
// Hyperdrive-pooled connection each run.
|
|
87
|
-
db: Db.get(options.db),
|
|
115
|
+
db,
|
|
88
116
|
dispatch,
|
|
117
|
+
dispatchBatch,
|
|
89
118
|
eventTypes: input.eventTypes ?? options.eventTypes,
|
|
90
119
|
...(input.headBlockNumber === undefined
|
|
91
120
|
? {}
|
|
92
121
|
: { headBlockNumber: input.headBlockNumber }),
|
|
93
122
|
maxBlocks: input.maxBlocks ?? options.maxBlocks,
|
|
94
|
-
now
|
|
95
|
-
|
|
123
|
+
now,
|
|
124
|
+
signal: input.signal,
|
|
125
|
+
...(metrics
|
|
126
|
+
? {
|
|
127
|
+
onCursorLag(blocks: number) {
|
|
128
|
+
cursorLagBlocks = blocks
|
|
129
|
+
},
|
|
130
|
+
onScanCheckpoint(checkpoint: AppWebhooks.poll.ScanCheckpoint) {
|
|
131
|
+
metrics.record({ chainId, type: 'webhook:poll-scan', ...checkpoint })
|
|
132
|
+
// Ship the start boundary before the RPC allocation can terminate the isolate.
|
|
133
|
+
metrics.flush()
|
|
134
|
+
},
|
|
135
|
+
onStage(stage: AppWebhooks.poll.Stage) {
|
|
136
|
+
metrics.record({ chainId, type: 'webhook:poll', ...stage })
|
|
137
|
+
},
|
|
138
|
+
onSubscriptions(count: number) {
|
|
139
|
+
metrics.record({ chainId, count, type: 'webhook:poll-subscriptions' })
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
: {}),
|
|
96
143
|
})
|
|
97
144
|
metrics?.record({
|
|
145
|
+
cursorLagBlocks,
|
|
98
146
|
meta: { durationMs: Date.now() - start, outcome: 'ok' },
|
|
99
147
|
result,
|
|
100
148
|
type: 'webhook:tick',
|
|
@@ -102,6 +150,7 @@ export function createPoller(options: createPoller.Options): createPoller.Poller
|
|
|
102
150
|
return result
|
|
103
151
|
} catch (error) {
|
|
104
152
|
metrics?.record({
|
|
153
|
+
cursorLagBlocks,
|
|
105
154
|
meta: { chainId, durationMs: Date.now() - start, outcome: 'error' },
|
|
106
155
|
result: undefined,
|
|
107
156
|
type: 'webhook:tick',
|
|
@@ -121,6 +170,9 @@ export declare namespace createPoller {
|
|
|
121
170
|
/** Delivery transport: hands a matched event to a queue or delivers inline. */
|
|
122
171
|
type Dispatch = NonNullable<AppWebhooks.poll.Options['dispatch']>
|
|
123
172
|
|
|
173
|
+
/** Batch delivery transport: confirms all rows together or leaves their cursor unchanged. */
|
|
174
|
+
type DispatchBatch = NonNullable<AppWebhooks.poll.Options['dispatchBatch']>
|
|
175
|
+
|
|
124
176
|
/** Summary counters returned by {@link Poller.tick}. */
|
|
125
177
|
type Result = AppWebhooks.poll.Result
|
|
126
178
|
|
|
@@ -128,13 +180,15 @@ export declare namespace createPoller {
|
|
|
128
180
|
type Defaults = {
|
|
129
181
|
/** Max matching rows scanned per subscription per tick (default 200). */
|
|
130
182
|
batchSize?: number | undefined
|
|
131
|
-
/** Reorg-safety window: only emit blocks this far behind head (default
|
|
183
|
+
/** Reorg-safety window: only emit blocks this far behind head (default 1). */
|
|
132
184
|
confirmations?: number | undefined
|
|
133
185
|
/**
|
|
134
186
|
* Delivery transport. Defaults to inline `deliverAndRecord` (self-host); on
|
|
135
187
|
* Cloudflare pass a Queue producer so retry/backoff/DLQ are native.
|
|
136
188
|
*/
|
|
137
189
|
dispatch?: Dispatch | undefined
|
|
190
|
+
/** Batch delivery transport used when the runtime can enqueue multiple rows atomically. */
|
|
191
|
+
dispatchBatch?: DispatchBatch | undefined
|
|
138
192
|
/** Event types to poll (default all). */
|
|
139
193
|
eventTypes?: AppWebhooks.poll.Options['eventTypes']
|
|
140
194
|
/** Backfill cap: most blocks an RPC scanner inspects per tick (default 1000). */
|
|
@@ -145,16 +199,16 @@ export declare namespace createPoller {
|
|
|
145
199
|
|
|
146
200
|
/** Options for {@link createPoller}. */
|
|
147
201
|
type Options = Defaults & {
|
|
202
|
+
/** Cache for token metadata shared across ticks. */
|
|
203
|
+
cache?: Store.Store | undefined
|
|
148
204
|
/** Database holding subscriptions + cursors, or a factory resolved per tick (Workers/Hyperdrive). */
|
|
149
205
|
db: Db.Source
|
|
150
206
|
/** Tempo chain id polled when a tick omits one. */
|
|
151
207
|
defaultChainId?: Viem.ChainId | undefined
|
|
152
208
|
/** Metrics backend; emits `webhook:tick` per tick and `webhook:enqueue` per dispatch. */
|
|
153
209
|
metrics?: Metrics.Metrics | undefined
|
|
154
|
-
/** RPC options for the viem clients
|
|
210
|
+
/** RPC options for the viem clients that scan chain events. */
|
|
155
211
|
rpc?: Viem.getClient.Rpc | undefined
|
|
156
|
-
/** TIDX query client options for the legacy `token:transfer` type. */
|
|
157
|
-
tidx?: Tidx.getClient.Tidx | undefined
|
|
158
212
|
}
|
|
159
213
|
|
|
160
214
|
/** Per-tick overrides; falls back to the {@link Options} defaults. */
|
|
@@ -163,11 +217,13 @@ export declare namespace createPoller {
|
|
|
163
217
|
chainId?: Viem.ChainId | undefined
|
|
164
218
|
/** Chain head to work back from; resolved from RPC when omitted. */
|
|
165
219
|
headBlockNumber?: bigint | number | undefined
|
|
220
|
+
/** Stops scanning and dispatch when the caller loses its execution lease. */
|
|
221
|
+
signal?: AbortSignal | undefined
|
|
166
222
|
}
|
|
167
223
|
|
|
168
|
-
/** A
|
|
224
|
+
/** A chain-head-driven webhook poller. */
|
|
169
225
|
type Poller = {
|
|
170
|
-
/** Runs one poll tick, resolving the
|
|
226
|
+
/** Runs one poll tick, resolving the RPC head when not supplied. */
|
|
171
227
|
tick(options?: TickOptions): Promise<Result>
|
|
172
228
|
}
|
|
173
229
|
}
|
|
@@ -105,6 +105,15 @@ describe('POST /api-keys', () => {
|
|
|
105
105
|
expect((await core_ApiKeys.list(store))[0]?.expiresAt).toBe(expiresAt)
|
|
106
106
|
})
|
|
107
107
|
|
|
108
|
+
test('rejects an expiry without the minimum storage lead time', async () => {
|
|
109
|
+
const { client } = await setup()
|
|
110
|
+
const response = await client['api-keys'].$post({
|
|
111
|
+
json: { ...validBody, expiresAt: new Date(Date.now() + 60_000).toISOString() },
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
expect(response.status).toBe(400)
|
|
115
|
+
})
|
|
116
|
+
|
|
108
117
|
test('rejects an unknown scope with 400', async () => {
|
|
109
118
|
const { client } = await setup()
|
|
110
119
|
const response = await client['api-keys'].$post({
|
|
@@ -15,6 +15,9 @@ import type * as App from '../App.js'
|
|
|
15
15
|
|
|
16
16
|
/** Zod schemas owned by the admin API-keys handler. */
|
|
17
17
|
export namespace schema {
|
|
18
|
+
// Leave time for validation and database lookups before KV writes its minimum 60-second TTL.
|
|
19
|
+
const minimumExpiryLeadMs = 2 * 60_000
|
|
20
|
+
|
|
18
21
|
const Name = z
|
|
19
22
|
.pipe(
|
|
20
23
|
z.string(),
|
|
@@ -27,6 +30,17 @@ export namespace schema {
|
|
|
27
30
|
z.meta({ examples: ['Explorer testnet'] }),
|
|
28
31
|
)
|
|
29
32
|
|
|
33
|
+
const ExpiresAt = z.optional(z.iso.datetime()).check(
|
|
34
|
+
z.refine(
|
|
35
|
+
(value) => value === undefined || Date.parse(value) >= Date.now() + minimumExpiryLeadMs,
|
|
36
|
+
{
|
|
37
|
+
error: 'Expiry must be at least two minutes in the future.',
|
|
38
|
+
},
|
|
39
|
+
),
|
|
40
|
+
z.describe('ISO 8601 expiry timestamp. Omit for a non-expiring key.'),
|
|
41
|
+
z.meta({ examples: ['2100-01-01T00:00:00.000Z'] }),
|
|
42
|
+
)
|
|
43
|
+
|
|
30
44
|
const ApiKey = Schema.describe(
|
|
31
45
|
z.extend(ApiKeys.schema.Record, {
|
|
32
46
|
createdByLabel: z
|
|
@@ -59,6 +73,7 @@ export namespace schema {
|
|
|
59
73
|
*/
|
|
60
74
|
export const Body = Schema.describe(
|
|
61
75
|
z.extend(z.omit(ApiKeys.schema.MintInput, { createdBy: true, name: true }), {
|
|
76
|
+
expiresAt: ExpiresAt,
|
|
62
77
|
name: Name,
|
|
63
78
|
scopes: z
|
|
64
79
|
.readonly(
|
|
@@ -17,12 +17,13 @@ const persister = createAsyncStoragePersister({
|
|
|
17
17
|
})
|
|
18
18
|
|
|
19
19
|
export const persistOptions: Omit<PersistQueryClientOptions, 'queryClient'> = {
|
|
20
|
-
|
|
21
|
-
persister,
|
|
20
|
+
buster: 'rate-limit-periods',
|
|
22
21
|
dehydrateOptions: {
|
|
23
22
|
shouldDehydrateQuery: (query) =>
|
|
24
23
|
query.queryKey[0] !== 'config' &&
|
|
25
24
|
query.queryKey[0] !== 'organizations' &&
|
|
26
25
|
defaultShouldDehydrateQuery(query),
|
|
27
26
|
},
|
|
27
|
+
maxAge: 1000 * 60 * 60 * 24,
|
|
28
|
+
persister,
|
|
28
29
|
}
|