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
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Shared or per-chain credentials. */
|
|
2
|
+
export type ChainCredential = string | Record<number, string>
|
|
3
|
+
|
|
4
|
+
/** Parses shared or per-chain credentials from an environment value. */
|
|
5
|
+
export function parseCredential(value: ChainCredential | undefined): parseCredential.ReturnType {
|
|
6
|
+
if (!value) return undefined
|
|
7
|
+
if (typeof value !== 'string') return value
|
|
8
|
+
const credentials = (() => {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(value) as unknown
|
|
11
|
+
} catch {
|
|
12
|
+
return value
|
|
13
|
+
}
|
|
14
|
+
})()
|
|
15
|
+
if (typeof credentials === 'string') return credentials
|
|
16
|
+
if (
|
|
17
|
+
!credentials ||
|
|
18
|
+
typeof credentials !== 'object' ||
|
|
19
|
+
Array.isArray(credentials) ||
|
|
20
|
+
Object.values(credentials).some((credential) => typeof credential !== 'string')
|
|
21
|
+
)
|
|
22
|
+
throw new Error('Expected a credential or a chain-id-to-credential JSON object.')
|
|
23
|
+
return credentials as Record<number, string>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export declare namespace parseCredential {
|
|
27
|
+
/** Parsed shared or per-chain credentials. */
|
|
28
|
+
type ReturnType = ChainCredential | undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Resolves a chain credential and infers its HTTP authorization scheme. */
|
|
32
|
+
export function resolveCredential(
|
|
33
|
+
options: resolveCredential.Options,
|
|
34
|
+
): resolveCredential.ReturnType {
|
|
35
|
+
const credentials = parseCredential(options.value)
|
|
36
|
+
const credential = typeof credentials === 'string' ? credentials : credentials?.[options.chainId]
|
|
37
|
+
if (!credential) return { basicAuth: undefined }
|
|
38
|
+
if (credential.includes(':')) return { basicAuth: credential }
|
|
39
|
+
return { basicAuth: undefined, bearerAuth: credential }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export declare namespace resolveCredential {
|
|
43
|
+
/** Options for resolving a chain credential. */
|
|
44
|
+
type Options = {
|
|
45
|
+
/** Chain id selecting a per-chain credential. */
|
|
46
|
+
chainId: number
|
|
47
|
+
/** Shared or per-chain credentials. */
|
|
48
|
+
value?: ChainCredential | undefined
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Resolved HTTP authorization credentials. */
|
|
52
|
+
type ReturnType = {
|
|
53
|
+
/** Basic credentials inferred from a value containing a colon. */
|
|
54
|
+
basicAuth: string | undefined
|
|
55
|
+
/** Bearer credential inferred from a value without a colon. */
|
|
56
|
+
bearerAuth?: string | undefined
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -246,6 +246,12 @@ async function measurePage(options: measurePage.Options): Promise<Measurement |
|
|
|
246
246
|
() => undefined,
|
|
247
247
|
)
|
|
248
248
|
|
|
249
|
+
// The migration scan is independent of historical quotes, so keep its TIDX
|
|
250
|
+
// round trip off the RPC critical path.
|
|
251
|
+
const migrations = recentMigrations(options).then(
|
|
252
|
+
(value) => ({ status: 'fulfilled', value }) as const,
|
|
253
|
+
(reason: unknown) => ({ reason, status: 'rejected' }) as const,
|
|
254
|
+
)
|
|
249
255
|
const rates = new Map<string, string>()
|
|
250
256
|
await Promise.all(
|
|
251
257
|
options.vaults.map(async (vault) => {
|
|
@@ -263,13 +269,13 @@ async function measurePage(options: measurePage.Options): Promise<Measurement |
|
|
|
263
269
|
}),
|
|
264
270
|
)
|
|
265
271
|
if (rates.size === 0) return { measuredAt, rates }
|
|
272
|
+
const migrationResult = await migrations
|
|
266
273
|
|
|
267
274
|
// A migration inside the window bridges two different yield engines, so the
|
|
268
|
-
// window resets instead of reporting growth across the swap.
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
for (const { blockNumber, vault } of migrations)
|
|
275
|
+
// window resets instead of reporting growth across the swap.
|
|
276
|
+
if (migrationResult.status === 'rejected') throw migrationResult.reason
|
|
277
|
+
if (!migrationResult.value) return undefined
|
|
278
|
+
for (const { blockNumber, vault } of migrationResult.value)
|
|
273
279
|
if (blockNumber > start.number && blockNumber <= end.number) rates.delete(vault)
|
|
274
280
|
return { measuredAt, rates }
|
|
275
281
|
}
|
|
@@ -306,9 +312,9 @@ async function boundaryBlocks(options: boundaryBlocks.Options): Promise<Boundari
|
|
|
306
312
|
const result = await options.getTidx(chainId).fetch({
|
|
307
313
|
chainId,
|
|
308
314
|
query: `
|
|
309
|
-
(SELECT num, timestamp FROM blocks WHERE timestamp <= '${endInstant}' ORDER BY num DESC LIMIT 1)
|
|
315
|
+
(SELECT num, timestamp FROM blocks WHERE timestamp <= '${endInstant}' ORDER BY timestamp DESC, num DESC LIMIT 1)
|
|
310
316
|
UNION ALL
|
|
311
|
-
(SELECT num, timestamp FROM blocks WHERE timestamp <= '${startInstant}' ORDER BY num DESC LIMIT 1)
|
|
317
|
+
(SELECT num, timestamp FROM blocks WHERE timestamp <= '${startInstant}' ORDER BY timestamp DESC, num DESC LIMIT 1)
|
|
312
318
|
` as string,
|
|
313
319
|
})
|
|
314
320
|
const rows = result.rows.flatMap((row) => {
|
|
@@ -76,7 +76,7 @@ describe('resolve', () => {
|
|
|
76
76
|
const getClient = Viem.createGetClient({
|
|
77
77
|
defaultChainId: tempoTestnet.id,
|
|
78
78
|
rpc: TestViem.rpc,
|
|
79
|
-
zones: [Zones.
|
|
79
|
+
zones: [Zones.zoneModeratoInternal],
|
|
80
80
|
})
|
|
81
81
|
const { instantLiquidity, sharePrice, state, ...discovery } = await EarnVaults.resolve({
|
|
82
82
|
getClient,
|
|
@@ -84,7 +84,7 @@ describe('resolve', () => {
|
|
|
84
84
|
chainId: tempoTestnet.id,
|
|
85
85
|
vaultAddress,
|
|
86
86
|
},
|
|
87
|
-
zones: [Zones.
|
|
87
|
+
zones: [Zones.zoneModeratoInternal],
|
|
88
88
|
})
|
|
89
89
|
expect({
|
|
90
90
|
depositsPaused: state.depositsPaused,
|
package/src/internal/Log.test.ts
CHANGED
|
@@ -51,10 +51,11 @@ describe('middleware', () => {
|
|
|
51
51
|
`)
|
|
52
52
|
})
|
|
53
53
|
|
|
54
|
-
test('includes
|
|
54
|
+
test('includes failed optional funding counts', async () => {
|
|
55
55
|
const entries: Log.Entry[] = []
|
|
56
56
|
const api = app({ emit: (entry) => void entries.push(entry) })
|
|
57
57
|
api.get('/v1/funding/transfers', (c) => {
|
|
58
|
+
c.set('fundingDepositCountFailed', true)
|
|
58
59
|
c.set('fundingTransferCountFailed', true)
|
|
59
60
|
return c.json({ data: [], nextCursor: null }, 200)
|
|
60
61
|
})
|
|
@@ -63,6 +64,7 @@ describe('middleware', () => {
|
|
|
63
64
|
|
|
64
65
|
expect(stable(entries[0]!)).toMatchInlineSnapshot(`
|
|
65
66
|
{
|
|
67
|
+
"fundingDepositCountFailed": true,
|
|
66
68
|
"fundingTransferCountFailed": true,
|
|
67
69
|
"level": "info",
|
|
68
70
|
"method": "GET",
|
package/src/internal/Log.ts
CHANGED
|
@@ -10,10 +10,14 @@ import type * as Timing from './Timing.js'
|
|
|
10
10
|
|
|
11
11
|
/** Hono variables read by the request logger and written by other layers. */
|
|
12
12
|
export type Variables = {
|
|
13
|
+
/** Tempo chain selected for the request. */
|
|
14
|
+
chainId?: number | undefined
|
|
13
15
|
/** Whether the original request exception was already reported. */
|
|
14
16
|
errorCaptured?: boolean | undefined
|
|
15
17
|
/** Stable error code recorded by `Response.error` for the request log entry. */
|
|
16
18
|
errorCode?: string | undefined
|
|
19
|
+
/** Whether the optional funding deposit count query failed. */
|
|
20
|
+
fundingDepositCountFailed?: true | undefined
|
|
17
21
|
/** Bounded MPP relay outcome emitted by the credential routes. */
|
|
18
22
|
mpp?: Entry['mpp'] | undefined
|
|
19
23
|
/** Bounded outcomes for funding providers attempted by the request. */
|
|
@@ -125,12 +129,16 @@ export type Sponsorship = {
|
|
|
125
129
|
export type Entry = {
|
|
126
130
|
/** `'hit'` when the edge response cache served the request without reaching the origin handlers. */
|
|
127
131
|
cache?: 'hit' | undefined
|
|
132
|
+
/** Tempo chain selected for the request. */
|
|
133
|
+
chainId?: number | undefined
|
|
128
134
|
/** Total request duration in milliseconds. */
|
|
129
135
|
duration: number
|
|
130
136
|
/** Whether the original request exception was already reported. */
|
|
131
137
|
errorCaptured?: boolean | undefined
|
|
132
138
|
/** Stable error code when the response is a JSON error envelope (e.g. `not_found`, `api_key_invalid`). */
|
|
133
139
|
errorCode?: string | undefined
|
|
140
|
+
/** Whether the optional funding deposit count query failed. */
|
|
141
|
+
fundingDepositCountFailed?: true | undefined
|
|
134
142
|
/** Bounded funding provider attempts made while serving the request. */
|
|
135
143
|
fundingProviderAttempts?: readonly FundingProviderAttempt[] | undefined
|
|
136
144
|
/** Whether the optional funding transfer count query failed. */
|
|
@@ -253,7 +261,11 @@ export function middleware<environment extends Environment = Environment>(
|
|
|
253
261
|
const error = c.error as ErrorMetadata | undefined
|
|
254
262
|
const principal = c.get('principal' as never) as Auth.Principal | undefined
|
|
255
263
|
const errorCaptured = c.get('errorCaptured' as never) as boolean | undefined
|
|
264
|
+
const chainId = c.get('chainId' as never) as number | undefined
|
|
256
265
|
const errorCode = c.get('errorCode' as never) as string | undefined
|
|
266
|
+
const fundingDepositCountFailed = c.get('fundingDepositCountFailed' as never) as
|
|
267
|
+
| true
|
|
268
|
+
| undefined
|
|
257
269
|
const fundingProviderAttempts =
|
|
258
270
|
error?.fundingProviderAttempts ??
|
|
259
271
|
(c.get('fundingProviderAttempts' as never) as Entry['fundingProviderAttempts'] | undefined)
|
|
@@ -300,9 +312,11 @@ export function middleware<environment extends Environment = Environment>(
|
|
|
300
312
|
|
|
301
313
|
const result = fn({
|
|
302
314
|
...(c.get('edgeCacheStatus' as never) === 'hit' ? { cache: 'hit' as const } : {}),
|
|
315
|
+
...(chainId === undefined ? {} : { chainId }),
|
|
303
316
|
duration: round(duration),
|
|
304
317
|
...(errorCaptured === undefined ? {} : { errorCaptured }),
|
|
305
318
|
...(errorCode === undefined ? {} : { errorCode }),
|
|
319
|
+
...(fundingDepositCountFailed === undefined ? {} : { fundingDepositCountFailed }),
|
|
306
320
|
...(fundingProviderAttempts === undefined ? {} : { fundingProviderAttempts }),
|
|
307
321
|
...(fundingTransferCountFailed === undefined ? {} : { fundingTransferCountFailed }),
|
|
308
322
|
level:
|
|
@@ -11,7 +11,7 @@ type MetricEvent = {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
describe('requests', () => {
|
|
14
|
-
test('records optional funding
|
|
14
|
+
test('records optional funding count failures on successful pages', () => {
|
|
15
15
|
const events: MetricEvent[] = []
|
|
16
16
|
const metrics = Metrics.from({
|
|
17
17
|
count(name, value, tags) {
|
|
@@ -25,6 +25,7 @@ describe('requests', () => {
|
|
|
25
25
|
|
|
26
26
|
record({
|
|
27
27
|
duration: 10,
|
|
28
|
+
fundingDepositCountFailed: true,
|
|
28
29
|
fundingTransferCountFailed: true,
|
|
29
30
|
level: 'info',
|
|
30
31
|
method: 'GET',
|
|
@@ -34,6 +35,22 @@ describe('requests', () => {
|
|
|
34
35
|
status: 200,
|
|
35
36
|
})
|
|
36
37
|
|
|
38
|
+
expect(events.find((event) => event.name === 'funding_deposit_count_failure_count'))
|
|
39
|
+
.toMatchInlineSnapshot(`
|
|
40
|
+
{
|
|
41
|
+
"name": "funding_deposit_count_failure_count",
|
|
42
|
+
"tags": {
|
|
43
|
+
"cache": "miss",
|
|
44
|
+
"method": "GET",
|
|
45
|
+
"principal_environment": "none",
|
|
46
|
+
"principal_type": "unknown",
|
|
47
|
+
"route": "/v1/funding/transfers",
|
|
48
|
+
},
|
|
49
|
+
"type": "count",
|
|
50
|
+
"value": 1,
|
|
51
|
+
}
|
|
52
|
+
`)
|
|
53
|
+
|
|
37
54
|
expect(events.find((event) => event.name === 'funding_transfer_count_failure_count'))
|
|
38
55
|
.toMatchInlineSnapshot(`
|
|
39
56
|
{
|
|
@@ -637,6 +654,93 @@ describe('webhooks', () => {
|
|
|
637
654
|
`)
|
|
638
655
|
})
|
|
639
656
|
|
|
657
|
+
test('records scan pipeline blocks, backlog, and finality lag', () => {
|
|
658
|
+
const events: MetricEvent[] = []
|
|
659
|
+
const metrics = Metrics.from({
|
|
660
|
+
count(name, value, tags) {
|
|
661
|
+
events.push({ name, tags, type: 'count', value })
|
|
662
|
+
},
|
|
663
|
+
flush() {},
|
|
664
|
+
gauge(name, value, tags) {
|
|
665
|
+
events.push({ name, tags, type: 'gauge', value })
|
|
666
|
+
},
|
|
667
|
+
histogram() {},
|
|
668
|
+
})
|
|
669
|
+
const sink = MetricSink.webhooks(metrics)
|
|
670
|
+
|
|
671
|
+
sink.record({ chainId: 42431, matches: 3, origin: 'socket', outcome: 'dispatched', type: 'webhook:scan-block' }) // prettier-ignore
|
|
672
|
+
sink.record({ chainId: 42431, matches: 0, origin: 'poll', outcome: 'deferred', type: 'webhook:scan-block' }) // prettier-ignore
|
|
673
|
+
sink.record({ chainId: 42431, oldestPendingAgeMs: 5_000, pendingCount: 2, type: 'webhook:scan-backlog' }) // prettier-ignore
|
|
674
|
+
sink.record({ chainId: 42431, lagBlocks: 1, type: 'webhook:scan-finality' })
|
|
675
|
+
|
|
676
|
+
expect(events.map(({ name, tags, value }) => `${name}=${value} ${JSON.stringify(tags)}`))
|
|
677
|
+
.toMatchInlineSnapshot(`
|
|
678
|
+
[
|
|
679
|
+
"webhook_scan_block_count=1 {"chain_id":"42431","origin":"socket","outcome":"dispatched"}",
|
|
680
|
+
"webhook_scan_match_count=3 {"chain_id":"42431","origin":"socket","outcome":"dispatched"}",
|
|
681
|
+
"webhook_scan_block_count=1 {"chain_id":"42431","origin":"poll","outcome":"deferred"}",
|
|
682
|
+
"webhook_scan_pending_count=2 {"chain_id":"42431"}",
|
|
683
|
+
"webhook_scan_oldest_pending_age_ms=5000 {"chain_id":"42431"}",
|
|
684
|
+
"webhook_scan_finality_lag_blocks=1 {"chain_id":"42431"}",
|
|
685
|
+
]
|
|
686
|
+
`)
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
test('records obligation staging and claim outcomes', () => {
|
|
690
|
+
const events: MetricEvent[] = []
|
|
691
|
+
const metrics = Metrics.from({
|
|
692
|
+
count(name, value, tags) {
|
|
693
|
+
events.push({ name, tags, type: 'count', value })
|
|
694
|
+
},
|
|
695
|
+
flush() {},
|
|
696
|
+
gauge() {},
|
|
697
|
+
histogram() {},
|
|
698
|
+
})
|
|
699
|
+
const sink = MetricSink.webhooks(metrics)
|
|
700
|
+
|
|
701
|
+
sink.record({ count: 3, outcome: 'created', type: 'webhook:delivery-job-ensure' })
|
|
702
|
+
sink.record({ count: 2, outcome: 'existing', type: 'webhook:delivery-job-ensure' })
|
|
703
|
+
sink.record({ outcome: 'claimed', type: 'webhook:delivery-job-claim' })
|
|
704
|
+
sink.record({ outcome: 'terminal', type: 'webhook:delivery-job-claim' })
|
|
705
|
+
|
|
706
|
+
expect(events).toMatchInlineSnapshot(`
|
|
707
|
+
[
|
|
708
|
+
{
|
|
709
|
+
"name": "webhook_delivery_job_ensure_count",
|
|
710
|
+
"tags": {
|
|
711
|
+
"outcome": "created",
|
|
712
|
+
},
|
|
713
|
+
"type": "count",
|
|
714
|
+
"value": 3,
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
"name": "webhook_delivery_job_ensure_count",
|
|
718
|
+
"tags": {
|
|
719
|
+
"outcome": "existing",
|
|
720
|
+
},
|
|
721
|
+
"type": "count",
|
|
722
|
+
"value": 2,
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
"name": "webhook_delivery_job_claim_count",
|
|
726
|
+
"tags": {
|
|
727
|
+
"outcome": "claimed",
|
|
728
|
+
},
|
|
729
|
+
"type": "count",
|
|
730
|
+
"value": 1,
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
"name": "webhook_delivery_job_claim_count",
|
|
734
|
+
"tags": {
|
|
735
|
+
"outcome": "terminal",
|
|
736
|
+
},
|
|
737
|
+
"type": "count",
|
|
738
|
+
"value": 1,
|
|
739
|
+
},
|
|
740
|
+
]
|
|
741
|
+
`)
|
|
742
|
+
})
|
|
743
|
+
|
|
640
744
|
test('records scan boundaries with bounded workload dimensions', () => {
|
|
641
745
|
const events: MetricEvent[] = []
|
|
642
746
|
const metrics = Metrics.from({
|
|
@@ -867,6 +971,7 @@ describe('webhooks', () => {
|
|
|
867
971
|
} satisfies Webhooks.Envelope
|
|
868
972
|
|
|
869
973
|
sink.record({
|
|
974
|
+
attempt: 1,
|
|
870
975
|
envelope,
|
|
871
976
|
queueAttempt: 2,
|
|
872
977
|
result: { durationMs: 25, ok: true, status: 204 },
|
|
@@ -957,6 +1062,7 @@ describe('webhooks', () => {
|
|
|
957
1062
|
expect(JSON.stringify(events)).not.toMatch(/evt_private|org_private|wh_private|whsec_private/)
|
|
958
1063
|
|
|
959
1064
|
sink.record({
|
|
1065
|
+
attempt: 3,
|
|
960
1066
|
envelope,
|
|
961
1067
|
queueAttempt: 3,
|
|
962
1068
|
result: { error: 'upstream unavailable', ok: false, status: 503 },
|
|
@@ -967,7 +1073,7 @@ describe('webhooks', () => {
|
|
|
967
1073
|
|
|
968
1074
|
expect(events.at(-1)?.tags).toMatchObject({
|
|
969
1075
|
queue_attempt_bucket: '3-5',
|
|
970
|
-
queue_disposition: '
|
|
1076
|
+
queue_disposition: 'scheduled_retry',
|
|
971
1077
|
})
|
|
972
1078
|
})
|
|
973
1079
|
})
|
|
@@ -51,6 +51,21 @@ export type WebhookEvent =
|
|
|
51
51
|
/** Identifies durable coordinator progress. */
|
|
52
52
|
type: 'webhook:head-progress'
|
|
53
53
|
}
|
|
54
|
+
| {
|
|
55
|
+
/** Tempo chain whose listener was checked. */
|
|
56
|
+
chainId: number
|
|
57
|
+
/** Heads refused in a row by the monotonic guard; zero on a delivering listener. */
|
|
58
|
+
skippedHeads: number
|
|
59
|
+
/** Identifies heads received and discarded rather than heads not received. */
|
|
60
|
+
type: 'webhook:head-skipped'
|
|
61
|
+
}
|
|
62
|
+
| {
|
|
63
|
+
chainId: number
|
|
64
|
+
/** Blocks the probed chain head leads the listener's newest observation. */
|
|
65
|
+
lagBlocks: number
|
|
66
|
+
/** Identifies the out-of-band listener head probe. */
|
|
67
|
+
type: 'webhook:listener-head-lag'
|
|
68
|
+
}
|
|
54
69
|
| {
|
|
55
70
|
/** Tempo chain whose recovery poke was dequeued. */
|
|
56
71
|
chainId: number
|
|
@@ -145,6 +160,49 @@ export type WebhookEvent =
|
|
|
145
160
|
type: 'webhook:enqueue'
|
|
146
161
|
}
|
|
147
162
|
| {
|
|
163
|
+
chainId: number
|
|
164
|
+
/** Rows matched when dispatched; zero otherwise. */
|
|
165
|
+
matches: number
|
|
166
|
+
/** How the head that enqueued this block was observed. */
|
|
167
|
+
origin: string
|
|
168
|
+
/** One scanned block's outcome. */
|
|
169
|
+
outcome: 'deferred' | 'dispatched' | 'empty'
|
|
170
|
+
/** Identifies one block-fanout scan. */
|
|
171
|
+
type: 'webhook:scan-block'
|
|
172
|
+
}
|
|
173
|
+
| {
|
|
174
|
+
chainId: number
|
|
175
|
+
/** Age of the oldest tracked block, absent when none are pending. */
|
|
176
|
+
oldestPendingAgeMs?: number | undefined
|
|
177
|
+
/** Tracked blocks awaiting completion reports. */
|
|
178
|
+
pendingCount: number
|
|
179
|
+
/** Identifies the coordinator's tracked-scan backlog. */
|
|
180
|
+
type: 'webhook:scan-backlog'
|
|
181
|
+
}
|
|
182
|
+
| {
|
|
183
|
+
chainId: number
|
|
184
|
+
/** Blocks between the observed head and the finalized head. */
|
|
185
|
+
lagBlocks: number
|
|
186
|
+
/** Identifies the finality gate's settling distance. */
|
|
187
|
+
type: 'webhook:scan-finality'
|
|
188
|
+
}
|
|
189
|
+
| {
|
|
190
|
+
/** Atomic ledger claim outcome. */
|
|
191
|
+
outcome: 'attempting' | 'claimed' | 'missing' | 'scheduled' | 'terminal'
|
|
192
|
+
/** Identifies a delivery-obligation claim. */
|
|
193
|
+
type: 'webhook:delivery-job-claim'
|
|
194
|
+
}
|
|
195
|
+
| {
|
|
196
|
+
/** Obligations represented by this outcome. */
|
|
197
|
+
count: number
|
|
198
|
+
/** Whether staging created or found the obligation. */
|
|
199
|
+
outcome: 'created' | 'existing'
|
|
200
|
+
/** Identifies obligation staging ahead of Queue handoff. */
|
|
201
|
+
type: 'webhook:delivery-job-ensure'
|
|
202
|
+
}
|
|
203
|
+
| {
|
|
204
|
+
/** Delivery attempt ordinal: the ledger claim count on the queue path. */
|
|
205
|
+
attempt: number
|
|
148
206
|
envelope: Webhooks.Envelope
|
|
149
207
|
/** Cloudflare Queue delivery attempt, absent outside the Queue path. */
|
|
150
208
|
queueAttempt?: number | undefined
|
|
@@ -244,6 +302,9 @@ export function requests(backend: Metrics): RequestSink {
|
|
|
244
302
|
backend.histogram('funding_quote_provider_duration_ms', attempt.durationMs, tags_attempt)
|
|
245
303
|
}
|
|
246
304
|
|
|
305
|
+
if (entry.fundingDepositCountFailed)
|
|
306
|
+
backend.count('funding_deposit_count_failure_count', 1, tags)
|
|
307
|
+
|
|
247
308
|
if (entry.fundingTransferCountFailed)
|
|
248
309
|
backend.count('funding_transfer_count_failure_count', 1, tags)
|
|
249
310
|
|
|
@@ -340,6 +401,16 @@ export function webhooks(backend: Metrics): WebhookSink {
|
|
|
340
401
|
chain_id: String(event.chainId),
|
|
341
402
|
})
|
|
342
403
|
return
|
|
404
|
+
case 'webhook:head-skipped':
|
|
405
|
+
backend.gauge('webhook_head_skipped_count', event.skippedHeads, {
|
|
406
|
+
chain_id: String(event.chainId),
|
|
407
|
+
})
|
|
408
|
+
return
|
|
409
|
+
case 'webhook:listener-head-lag':
|
|
410
|
+
backend.gauge('webhook_listener_head_lag_blocks', event.lagBlocks, {
|
|
411
|
+
chain_id: String(event.chainId),
|
|
412
|
+
})
|
|
413
|
+
return
|
|
343
414
|
case 'webhook:poll-failure':
|
|
344
415
|
backend.count('webhook_poll_failure_count', 1, {
|
|
345
416
|
chain_id: String(event.chainId),
|
|
@@ -456,6 +527,38 @@ export function webhooks(backend: Metrics): WebhookSink {
|
|
|
456
527
|
})
|
|
457
528
|
return
|
|
458
529
|
}
|
|
530
|
+
case 'webhook:scan-block': {
|
|
531
|
+
const tags = {
|
|
532
|
+
chain_id: String(event.chainId),
|
|
533
|
+
origin: event.origin,
|
|
534
|
+
outcome: event.outcome,
|
|
535
|
+
}
|
|
536
|
+
backend.count('webhook_scan_block_count', 1, tags)
|
|
537
|
+
if (event.matches > 0) backend.count('webhook_scan_match_count', event.matches, tags)
|
|
538
|
+
return
|
|
539
|
+
}
|
|
540
|
+
case 'webhook:scan-backlog': {
|
|
541
|
+
const tags = { chain_id: String(event.chainId) }
|
|
542
|
+
backend.gauge('webhook_scan_pending_count', event.pendingCount, tags)
|
|
543
|
+
backend.gauge('webhook_scan_oldest_pending_age_ms', event.oldestPendingAgeMs ?? 0, tags)
|
|
544
|
+
return
|
|
545
|
+
}
|
|
546
|
+
case 'webhook:scan-finality': {
|
|
547
|
+
backend.gauge('webhook_scan_finality_lag_blocks', event.lagBlocks, {
|
|
548
|
+
chain_id: String(event.chainId),
|
|
549
|
+
})
|
|
550
|
+
return
|
|
551
|
+
}
|
|
552
|
+
case 'webhook:delivery-job-claim': {
|
|
553
|
+
backend.count('webhook_delivery_job_claim_count', 1, { outcome: event.outcome })
|
|
554
|
+
return
|
|
555
|
+
}
|
|
556
|
+
case 'webhook:delivery-job-ensure': {
|
|
557
|
+
backend.count('webhook_delivery_job_ensure_count', event.count, {
|
|
558
|
+
outcome: event.outcome,
|
|
559
|
+
})
|
|
560
|
+
return
|
|
561
|
+
}
|
|
459
562
|
case 'webhook:delivery': {
|
|
460
563
|
const { envelope, result, subscription, timings } = event
|
|
461
564
|
const queueTags =
|
|
@@ -463,7 +566,11 @@ export function webhooks(backend: Metrics): WebhookSink {
|
|
|
463
566
|
? {}
|
|
464
567
|
: {
|
|
465
568
|
queue_attempt_bucket: attemptBucket(event.queueAttempt),
|
|
466
|
-
|
|
569
|
+
// Retryable results ack the message and schedule a ledger
|
|
570
|
+
// retry; the Queue transport never redelivers them.
|
|
571
|
+
queue_disposition: WebhookDestination.isRetryable(result)
|
|
572
|
+
? 'scheduled_retry'
|
|
573
|
+
: 'acked',
|
|
467
574
|
}
|
|
468
575
|
const tags = {
|
|
469
576
|
chain_id: String(envelope.chainId),
|
|
@@ -473,7 +580,7 @@ export function webhooks(backend: Metrics): WebhookSink {
|
|
|
473
580
|
const outcome = result.ok ? 'succeeded' : 'failed'
|
|
474
581
|
backend.count('webhook_delivery_attempt_count', 1, {
|
|
475
582
|
...tags,
|
|
476
|
-
attempt_bucket: attemptBucket(
|
|
583
|
+
attempt_bucket: attemptBucket(event.attempt),
|
|
477
584
|
outcome,
|
|
478
585
|
...queueTags,
|
|
479
586
|
status_class: statusClass(result),
|
|
@@ -495,6 +602,9 @@ export function webhooks(backend: Metrics): WebhookSink {
|
|
|
495
602
|
if (timings?.endToEndMs !== undefined)
|
|
496
603
|
backend.histogram('webhook_delivery_end_to_end_ms', timings.endToEndMs, {
|
|
497
604
|
...tags,
|
|
605
|
+
// Unlimited-horizon ledger retries emit hour-plus samples;
|
|
606
|
+
// the bucket keeps first-attempt percentiles interpretable.
|
|
607
|
+
attempt_bucket: attemptBucket(event.attempt),
|
|
498
608
|
outcome,
|
|
499
609
|
})
|
|
500
610
|
if (timings?.eventToEnvelopeMs !== undefined)
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { RateLimit, Store } from 'tapimo'
|
|
2
2
|
|
|
3
3
|
describe('memory', () => {
|
|
4
|
-
test('tracks fixed-
|
|
4
|
+
test('tracks fixed-minute quota in a store', async () => {
|
|
5
5
|
const store = RateLimit.memory({
|
|
6
6
|
now: () => new Date('2026-05-25T00:00:30.000Z'),
|
|
7
7
|
})
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
8
|
+
const limit = { limit: 2, period: 'minute' } as const
|
|
9
|
+
const first = await store.consume({ key: 'public:anonymous', limit })
|
|
10
|
+
const second = await store.consume({ key: 'public:anonymous', limit })
|
|
11
|
+
const third = await store.consume({ key: 'public:anonymous', limit })
|
|
11
12
|
|
|
12
13
|
expect(first).toMatchInlineSnapshot(`
|
|
13
14
|
{
|
|
@@ -35,25 +36,26 @@ describe('memory', () => {
|
|
|
35
36
|
`)
|
|
36
37
|
})
|
|
37
38
|
|
|
38
|
-
test('restarts the count when the next
|
|
39
|
-
// The window reset rides on counter expiry
|
|
40
|
-
//
|
|
39
|
+
test('restarts the count when the next second opens', async () => {
|
|
40
|
+
// The window reset rides on counter expiry, so the store clock and the
|
|
41
|
+
// consume clock must agree.
|
|
41
42
|
vi.useFakeTimers()
|
|
42
43
|
try {
|
|
43
|
-
vi.setSystemTime(new Date('2026-05-25T00:00:30.
|
|
44
|
+
vi.setSystemTime(new Date('2026-05-25T00:00:30.250Z'))
|
|
44
45
|
const store = RateLimit.memory()
|
|
45
|
-
|
|
46
|
-
await store.consume({ key: 'public:anonymous', limit
|
|
46
|
+
const limit = { limit: 2, period: 'second' } as const
|
|
47
|
+
await store.consume({ key: 'public:anonymous', limit })
|
|
48
|
+
await store.consume({ key: 'public:anonymous', limit })
|
|
47
49
|
|
|
48
|
-
vi.setSystemTime(new Date('2026-05-25T00:
|
|
49
|
-
const next = await store.consume({ key: 'public:anonymous', limit
|
|
50
|
+
vi.setSystemTime(new Date('2026-05-25T00:00:31.001Z'))
|
|
51
|
+
const next = await store.consume({ key: 'public:anonymous', limit })
|
|
50
52
|
|
|
51
53
|
expect(next).toMatchInlineSnapshot(`
|
|
52
54
|
{
|
|
53
55
|
"allowed": true,
|
|
54
56
|
"limit": 2,
|
|
55
57
|
"remaining": 1,
|
|
56
|
-
"reset":
|
|
58
|
+
"reset": 1779667232,
|
|
57
59
|
}
|
|
58
60
|
`)
|
|
59
61
|
} finally {
|
|
@@ -72,19 +74,22 @@ describe('memory', () => {
|
|
|
72
74
|
},
|
|
73
75
|
})
|
|
74
76
|
const store = RateLimit.memory({
|
|
75
|
-
//
|
|
76
|
-
now: () => new Date('2026-05-25T00:00:30.
|
|
77
|
+
// 250ms into the second, so 750ms of the window remains.
|
|
78
|
+
now: () => new Date('2026-05-25T00:00:30.250Z'),
|
|
77
79
|
store: recording,
|
|
78
80
|
})
|
|
79
81
|
|
|
80
|
-
await store.consume({
|
|
82
|
+
await store.consume({
|
|
83
|
+
key: 'public:anonymous',
|
|
84
|
+
limit: { limit: 2, period: 'second' },
|
|
85
|
+
})
|
|
81
86
|
|
|
82
87
|
expect(increments).toMatchInlineSnapshot(`
|
|
83
88
|
[
|
|
84
89
|
{
|
|
85
|
-
"key": "ratelimit:public:anonymous",
|
|
90
|
+
"key": "ratelimit:second:1779667230:public:anonymous",
|
|
86
91
|
"options": {
|
|
87
|
-
"ttl":
|
|
92
|
+
"ttl": 750,
|
|
88
93
|
},
|
|
89
94
|
},
|
|
90
95
|
]
|
|
@@ -6,9 +6,15 @@ import * as Store from './Store.js'
|
|
|
6
6
|
export namespace schema {
|
|
7
7
|
/** A rate-limit policy; the source of truth for {@link Limit}. */
|
|
8
8
|
export const Limit = z.object({
|
|
9
|
-
|
|
9
|
+
limit: z
|
|
10
10
|
.number()
|
|
11
|
-
.check(z.int(), z.nonnegative(), z.describe('Number of requests allowed per
|
|
11
|
+
.check(z.int(), z.nonnegative(), z.describe('Number of requests allowed per period.')),
|
|
12
|
+
period: z
|
|
13
|
+
.union([z.literal('minute'), z.literal('second')])
|
|
14
|
+
.check(
|
|
15
|
+
z.describe('Fixed window used to enforce the request limit.'),
|
|
16
|
+
z.meta({ examples: ['minute'] }),
|
|
17
|
+
),
|
|
12
18
|
})
|
|
13
19
|
}
|
|
14
20
|
|
|
@@ -49,11 +55,8 @@ export namespace RateLimitStore {
|
|
|
49
55
|
* Each consume is a single `Store.increment`, so a backend with a native
|
|
50
56
|
* counter (e.g. a Durable Object) is one atomic round trip; backends without
|
|
51
57
|
* one fall back to get + put, acceptable for dev/in-process stores only.
|
|
52
|
-
* The storage key
|
|
53
|
-
*
|
|
54
|
-
* current minute, so the next window's first hit restarts at 1 and idle keys
|
|
55
|
-
* hold no storage. The sub-minute TTL rules out Cloudflare KV as a backend,
|
|
56
|
-
* which is unsuitable for enforced counters anyway.
|
|
58
|
+
* The storage key includes the fixed-window bucket, so backend latency cannot
|
|
59
|
+
* carry a previous count into a later window.
|
|
57
60
|
*/
|
|
58
61
|
export function memory(options: memory.Options = {}): RateLimitStore {
|
|
59
62
|
const store = options.store ?? Store.memory()
|
|
@@ -62,19 +65,24 @@ export function memory(options: memory.Options = {}): RateLimitStore {
|
|
|
62
65
|
return {
|
|
63
66
|
async consume(options) {
|
|
64
67
|
const time = now().getTime()
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
// window
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
const window = options.limit.period === 'second' ? 1_000 : 60_000
|
|
69
|
+
const bucket = Math.floor(time / window)
|
|
70
|
+
// The bucket isolates window counts if the backing store applies the TTL
|
|
71
|
+
// after the next window starts.
|
|
72
|
+
const count = await Store.increment(
|
|
73
|
+
store,
|
|
74
|
+
`ratelimit:${options.limit.period}:${bucket}:${options.key}`,
|
|
75
|
+
{
|
|
76
|
+
ttl: (bucket + 1) * window - time,
|
|
77
|
+
},
|
|
78
|
+
)
|
|
79
|
+
const limit = options.limit.limit
|
|
72
80
|
|
|
73
81
|
return {
|
|
74
82
|
allowed: count <= limit,
|
|
75
83
|
limit,
|
|
76
84
|
remaining: Math.max(limit - count, 0),
|
|
77
|
-
reset: Math.ceil(((bucket + 1) *
|
|
85
|
+
reset: Math.ceil(((bucket + 1) * window) / 1_000),
|
|
78
86
|
}
|
|
79
87
|
},
|
|
80
88
|
}
|