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.
Files changed (319) hide show
  1. package/dist/ApiKey.d.ts +2 -1
  2. package/dist/ApiKey.d.ts.map +1 -1
  3. package/dist/ApiKeys.d.ts +4 -2
  4. package/dist/ApiKeys.d.ts.map +1 -1
  5. package/dist/ApiKeys.js +26 -1
  6. package/dist/ApiKeys.js.map +1 -1
  7. package/dist/App.d.ts +4292 -3708
  8. package/dist/App.d.ts.map +1 -1
  9. package/dist/App.js +1 -0
  10. package/dist/App.js.map +1 -1
  11. package/dist/Webhooks.d.ts +21 -20
  12. package/dist/Webhooks.d.ts.map +1 -1
  13. package/dist/Webhooks.js +66 -23
  14. package/dist/Webhooks.js.map +1 -1
  15. package/dist/admin/App.d.ts +91 -84
  16. package/dist/admin/App.d.ts.map +1 -1
  17. package/dist/admin/apps/api-keys.d.ts +26 -17
  18. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  19. package/dist/admin/apps/api-keys.js +6 -0
  20. package/dist/admin/apps/api-keys.js.map +1 -1
  21. package/dist/admin/apps/organizations.d.ts +4 -2
  22. package/dist/admin/apps/organizations.d.ts.map +1 -1
  23. package/dist/admin/apps/verified-tokens.d.ts +168 -168
  24. package/dist/admin/ui.generated.d.ts +1 -1
  25. package/dist/admin/ui.generated.js +69 -69
  26. package/dist/admin/ui.generated.js.map +1 -1
  27. package/dist/analytics/Analytics.d.ts.map +1 -1
  28. package/dist/analytics/Analytics.js +26 -0
  29. package/dist/analytics/Analytics.js.map +1 -1
  30. package/dist/apps/Zones.d.ts +4280 -84
  31. package/dist/apps/Zones.d.ts.map +1 -1
  32. package/dist/apps/Zones.js +30 -10
  33. package/dist/apps/Zones.js.map +1 -1
  34. package/dist/apps/data/App.d.ts +5799 -4929
  35. package/dist/apps/data/App.d.ts.map +1 -1
  36. package/dist/apps/data/FxOracle.d.ts +4 -5
  37. package/dist/apps/data/FxOracle.d.ts.map +1 -1
  38. package/dist/apps/data/FxOracle.js +19 -4
  39. package/dist/apps/data/FxOracle.js.map +1 -1
  40. package/dist/apps/data/routes/activities.d.ts +3391 -492
  41. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  42. package/dist/apps/data/routes/activities.js +372 -29
  43. package/dist/apps/data/routes/activities.js.map +1 -1
  44. package/dist/apps/data/routes/addresses.d.ts +853 -339
  45. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  46. package/dist/apps/data/routes/balances.d.ts +10 -10
  47. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  48. package/dist/apps/data/routes/balances.js +145 -83
  49. package/dist/apps/data/routes/balances.js.map +1 -1
  50. package/dist/apps/data/routes/blocks.d.ts +123 -123
  51. package/dist/apps/data/routes/coingecko.d.ts +169 -169
  52. package/dist/apps/data/routes/earn.d.ts +455 -1
  53. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  54. package/dist/apps/data/routes/earn.js +369 -45
  55. package/dist/apps/data/routes/earn.js.map +1 -1
  56. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  57. package/dist/apps/data/routes/exchanges.js +114 -118
  58. package/dist/apps/data/routes/exchanges.js.map +1 -1
  59. package/dist/apps/data/routes/fee-amm.d.ts +24 -24
  60. package/dist/apps/data/routes/indexer.d.ts +35 -34
  61. package/dist/apps/data/routes/indexer.d.ts.map +1 -1
  62. package/dist/apps/data/routes/indexer.js +3 -2
  63. package/dist/apps/data/routes/indexer.js.map +1 -1
  64. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  65. package/dist/apps/data/routes/receipts.js +68 -47
  66. package/dist/apps/data/routes/receipts.js.map +1 -1
  67. package/dist/apps/data/routes/rpc.d.ts +31 -31
  68. package/dist/apps/data/routes/rpc.d.ts.map +1 -1
  69. package/dist/apps/data/routes/rpc.js +5 -1
  70. package/dist/apps/data/routes/rpc.js.map +1 -1
  71. package/dist/apps/data/routes/tokenlist.d.ts +12 -12
  72. package/dist/apps/data/routes/tokens.d.ts +461 -459
  73. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  74. package/dist/apps/data/routes/transactions.d.ts +313 -305
  75. package/dist/apps/data/routes/transactions.d.ts.map +1 -1
  76. package/dist/apps/data/routes/transactions.js +25 -23
  77. package/dist/apps/data/routes/transactions.js.map +1 -1
  78. package/dist/apps/data/routes/transfers.d.ts +108 -86
  79. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  80. package/dist/apps/data/routes/transfers.js +618 -105
  81. package/dist/apps/data/routes/transfers.js.map +1 -1
  82. package/dist/apps/data/routes/valuation.d.ts +21 -21
  83. package/dist/apps/data/routes/valuation.d.ts.map +1 -1
  84. package/dist/apps/data/routes/valuation.js +2 -2
  85. package/dist/apps/data/routes/valuation.js.map +1 -1
  86. package/dist/apps/data/routes/webhooks.d.ts +519 -868
  87. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  88. package/dist/apps/data/routes/webhooks.js +642 -353
  89. package/dist/apps/data/routes/webhooks.js.map +1 -1
  90. package/dist/apps/data/routes/zones.d.ts +31 -31
  91. package/dist/apps/data/routes/zones.d.ts.map +1 -1
  92. package/dist/apps/data/routes/zones.js +4 -2
  93. package/dist/apps/data/routes/zones.js.map +1 -1
  94. package/dist/apps/funding/routes/deposit-addresses.js +1 -1
  95. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  96. package/dist/apps/funding/routes/providers.d.ts +13 -13
  97. package/dist/apps/funding/routes/quotes.d.ts +89 -89
  98. package/dist/apps/funding/routes/transfers.d.ts +36 -36
  99. package/dist/apps/funding/routes/transfers.js +1 -1
  100. package/dist/apps/funding/routes/transfers.js.map +1 -1
  101. package/dist/apps/management/App.d.ts +11 -309
  102. package/dist/apps/management/App.d.ts.map +1 -1
  103. package/dist/apps/management/routes/api-keys.d.ts +74 -74
  104. package/dist/apps/management/routes/billing.js +1 -1
  105. package/dist/apps/management/routes/billing.js.map +1 -1
  106. package/dist/apps/management/routes/faucet.d.ts +35 -35
  107. package/dist/apps/management/routes/faucet.js +2 -2
  108. package/dist/apps/management/routes/faucet.js.map +1 -1
  109. package/dist/apps/management/routes/invitations.d.ts +134 -134
  110. package/dist/apps/management/routes/invite-links.d.ts +80 -80
  111. package/dist/apps/management/routes/members.d.ts +69 -69
  112. package/dist/apps/management/routes/orgs.d.ts +98 -98
  113. package/dist/apps/management/routes/projects.d.ts +60 -60
  114. package/dist/apps/management/routes/usage.d.ts +72 -72
  115. package/dist/apps/management/routes/verified-token-requests.d.ts +83 -83
  116. package/dist/apps/management/routes/webhooks.d.ts +203 -618
  117. package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
  118. package/dist/apps/mcp.d.ts.map +1 -1
  119. package/dist/apps/mcp.js +4 -1
  120. package/dist/apps/mcp.js.map +1 -1
  121. package/dist/apps/mpp/App.d.ts +56 -44
  122. package/dist/apps/mpp/App.d.ts.map +1 -1
  123. package/dist/apps/mpp/Mpp.d.ts +3 -0
  124. package/dist/apps/mpp/Mpp.d.ts.map +1 -1
  125. package/dist/apps/mpp/Mpp.js +3 -0
  126. package/dist/apps/mpp/Mpp.js.map +1 -1
  127. package/dist/apps/mpp/internal/Adapter.d.ts.map +1 -1
  128. package/dist/apps/mpp/internal/Adapter.js +9 -2
  129. package/dist/apps/mpp/internal/Adapter.js.map +1 -1
  130. package/dist/db/Db.d.ts +4 -2
  131. package/dist/db/Db.d.ts.map +1 -1
  132. package/dist/db/Db.js +75 -15
  133. package/dist/db/Db.js.map +1 -1
  134. package/dist/db/Schema.d.ts +19 -0
  135. package/dist/db/Schema.d.ts.map +1 -1
  136. package/dist/db/Schema.js +24 -0
  137. package/dist/db/Schema.js.map +1 -1
  138. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  139. package/dist/db/tables/sponsoredTransactions.js +4 -1
  140. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  141. package/dist/db/tables/webhookQueueEvents.d.ts +122 -0
  142. package/dist/db/tables/webhookQueueEvents.d.ts.map +1 -0
  143. package/dist/db/tables/webhookQueueEvents.js +160 -0
  144. package/dist/db/tables/webhookQueueEvents.js.map +1 -0
  145. package/dist/db/tables/webhookSubscriptions.d.ts +58 -3
  146. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  147. package/dist/db/tables/webhookSubscriptions.js +86 -3
  148. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  149. package/dist/internal/Auth.d.ts +3 -0
  150. package/dist/internal/Auth.d.ts.map +1 -1
  151. package/dist/internal/Auth.js +6 -4
  152. package/dist/internal/Auth.js.map +1 -1
  153. package/dist/internal/MetricSink.d.ts +139 -3
  154. package/dist/internal/MetricSink.d.ts.map +1 -1
  155. package/dist/internal/MetricSink.js +144 -4
  156. package/dist/internal/MetricSink.js.map +1 -1
  157. package/dist/internal/RateLimit.d.ts +4 -6
  158. package/dist/internal/RateLimit.d.ts.map +1 -1
  159. package/dist/internal/RateLimit.js +15 -14
  160. package/dist/internal/RateLimit.js.map +1 -1
  161. package/dist/internal/Schema.js +1 -1
  162. package/dist/internal/Schema.js.map +1 -1
  163. package/dist/internal/Store.d.ts +2 -1
  164. package/dist/internal/Store.d.ts.map +1 -1
  165. package/dist/internal/Store.js +57 -18
  166. package/dist/internal/Store.js.map +1 -1
  167. package/dist/internal/Tidx.d.ts +1 -3
  168. package/dist/internal/Tidx.d.ts.map +1 -1
  169. package/dist/internal/Tidx.js +1 -3
  170. package/dist/internal/Tidx.js.map +1 -1
  171. package/dist/internal/Value.d.ts +2 -0
  172. package/dist/internal/Value.d.ts.map +1 -1
  173. package/dist/internal/Value.js +25 -0
  174. package/dist/internal/Value.js.map +1 -1
  175. package/dist/internal/Viem.d.ts.map +1 -1
  176. package/dist/internal/Viem.js +2 -1
  177. package/dist/internal/Viem.js.map +1 -1
  178. package/dist/internal/WebhookDestination.d.ts +2 -0
  179. package/dist/internal/WebhookDestination.d.ts.map +1 -1
  180. package/dist/internal/WebhookDestination.js +11 -0
  181. package/dist/internal/WebhookDestination.js.map +1 -1
  182. package/dist/internal/WebhookTransfer.d.ts +75 -0
  183. package/dist/internal/WebhookTransfer.d.ts.map +1 -0
  184. package/dist/internal/WebhookTransfer.js +299 -0
  185. package/dist/internal/WebhookTransfer.js.map +1 -0
  186. package/dist/internal/Webhooks.d.ts +176 -0
  187. package/dist/internal/Webhooks.d.ts.map +1 -1
  188. package/dist/internal/Webhooks.js +216 -20
  189. package/dist/internal/Webhooks.js.map +1 -1
  190. package/dist/internal/funding/Provider.d.ts +4 -0
  191. package/dist/internal/funding/Provider.d.ts.map +1 -1
  192. package/dist/internal/funding/Provider.js +11 -7
  193. package/dist/internal/funding/Provider.js.map +1 -1
  194. package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
  195. package/dist/internal/funding/Reconciliation.js +128 -0
  196. package/dist/internal/funding/Reconciliation.js.map +1 -1
  197. package/dist/internal/funding/providers/relay.d.ts +6 -4
  198. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  199. package/dist/internal/funding/providers/relay.js +71 -9
  200. package/dist/internal/funding/providers/relay.js.map +1 -1
  201. package/package.json +4 -4
  202. package/src/ApiKeys.test.ts +20 -0
  203. package/src/ApiKeys.ts +28 -1
  204. package/src/App.test.ts +10 -8
  205. package/src/App.ts +1 -0
  206. package/src/Client.test-d.ts +61 -5
  207. package/src/Webhooks.bench.ts +714 -0
  208. package/src/Webhooks.memory.bench.ts +250 -0
  209. package/src/Webhooks.test.ts +77 -6
  210. package/src/Webhooks.ts +85 -29
  211. package/src/admin/apps/api-keys.test.ts +9 -0
  212. package/src/admin/apps/api-keys.ts +15 -0
  213. package/src/admin/ui/src/lib/query-client.ts +3 -2
  214. package/src/admin/ui/src/routes/api-keys.tsx +31 -9
  215. package/src/admin/ui.generated.ts +69 -69
  216. package/src/analytics/Analytics.test.ts +8 -0
  217. package/src/analytics/Analytics.ts +26 -0
  218. package/src/apps/Zones.test.ts +23 -5
  219. package/src/apps/Zones.ts +31 -10
  220. package/src/apps/composition.test.ts +1 -0
  221. package/src/apps/data/App.test.ts +2 -2
  222. package/src/apps/data/FxOracle.test.ts +18 -0
  223. package/src/apps/data/FxOracle.ts +28 -5
  224. package/src/apps/data/routes/activities.bench.ts +41 -0
  225. package/src/apps/data/routes/activities.test.ts +122 -2
  226. package/src/apps/data/routes/activities.ts +435 -32
  227. package/src/apps/data/routes/balances.bench.ts +21 -0
  228. package/src/apps/data/routes/balances.test.ts +119 -10
  229. package/src/apps/data/routes/balances.ts +164 -97
  230. package/src/apps/data/routes/blocks.bench.ts +34 -0
  231. package/src/apps/data/routes/blocks.test.ts +12 -6
  232. package/src/apps/data/routes/coingecko.bench.ts +53 -0
  233. package/src/apps/data/routes/earn.bench.ts +71 -0
  234. package/src/apps/data/routes/earn.test.ts +197 -0
  235. package/src/apps/data/routes/earn.ts +487 -45
  236. package/src/apps/data/routes/exchanges.bench.ts +85 -0
  237. package/src/apps/data/routes/exchanges.test.ts +7 -6
  238. package/src/apps/data/routes/exchanges.ts +124 -117
  239. package/src/apps/data/routes/fee-amm.bench.ts +13 -0
  240. package/src/apps/data/routes/indexer.bench.ts +8 -0
  241. package/src/apps/data/routes/indexer.test.ts +2 -1
  242. package/src/apps/data/routes/indexer.ts +3 -2
  243. package/src/apps/data/routes/receipts.bench.ts +39 -0
  244. package/src/apps/data/routes/receipts.test.ts +24 -7
  245. package/src/apps/data/routes/receipts.ts +67 -50
  246. package/src/apps/data/routes/rpc.bench.ts +14 -0
  247. package/src/apps/data/routes/rpc.test.ts +27 -0
  248. package/src/apps/data/routes/rpc.ts +5 -1
  249. package/src/apps/data/routes/tokenlist.bench.ts +3 -0
  250. package/src/apps/data/routes/tokens.bench.ts +76 -0
  251. package/src/apps/data/routes/tokens.test.ts +51 -0
  252. package/src/apps/data/routes/transactions.bench.ts +40 -0
  253. package/src/apps/data/routes/transactions.test.ts +42 -0
  254. package/src/apps/data/routes/transactions.ts +44 -39
  255. package/src/apps/data/routes/transfers.bench.ts +28 -0
  256. package/src/apps/data/routes/transfers.test.ts +402 -24
  257. package/src/apps/data/routes/transfers.ts +708 -101
  258. package/src/apps/data/routes/valuation.bench.ts +14 -0
  259. package/src/apps/data/routes/valuation.test.ts +26 -0
  260. package/src/apps/data/routes/valuation.ts +6 -2
  261. package/src/apps/data/routes/verified-tokens.bench.ts +8 -0
  262. package/src/apps/data/routes/webhooks.bench.ts +26 -0
  263. package/src/apps/data/routes/webhooks.scan.test.ts +326 -0
  264. package/src/apps/data/routes/webhooks.test.ts +466 -135
  265. package/src/apps/data/routes/webhooks.ts +820 -429
  266. package/src/apps/data/routes/zones.bench.ts +32 -0
  267. package/src/apps/data/routes/zones.test.ts +1 -1
  268. package/src/apps/data/routes/zones.ts +7 -2
  269. package/src/apps/funding/routes/chains.test.ts +1 -1
  270. package/src/apps/funding/routes/deposit-addresses.test.ts +1 -1
  271. package/src/apps/funding/routes/deposit-addresses.ts +1 -1
  272. package/src/apps/funding/routes/providers.test.ts +1 -1
  273. package/src/apps/funding/routes/quotes.test.ts +2 -2
  274. package/src/apps/funding/routes/transfers.test.ts +3 -6
  275. package/src/apps/funding/routes/transfers.ts +1 -1
  276. package/src/apps/management/routes/billing.ts +1 -1
  277. package/src/apps/management/routes/faucet.ts +2 -2
  278. package/src/apps/management/routes/me.test.ts +31 -0
  279. package/src/apps/mcp.ts +4 -1
  280. package/src/apps/mpp/Mpp.test.ts +13 -1
  281. package/src/apps/mpp/Mpp.ts +8 -0
  282. package/src/apps/mpp/internal/Adapter.ts +10 -2
  283. package/src/db/Db.test.ts +40 -6
  284. package/src/db/Db.ts +114 -19
  285. package/src/db/Schema.ts +32 -0
  286. package/src/db/tables/sponsoredTransactions.ts +9 -2
  287. package/src/db/tables/webhookQueueEvents.ts +285 -0
  288. package/src/db/tables/webhookSubscriptions.ts +131 -4
  289. package/src/internal/Auth.test.ts +61 -50
  290. package/src/internal/Auth.ts +9 -4
  291. package/src/internal/EarnVaults.test.ts +2 -2
  292. package/src/internal/MetricSink.test.ts +575 -0
  293. package/src/internal/MetricSink.ts +325 -10
  294. package/src/internal/RateLimit.test.ts +23 -18
  295. package/src/internal/RateLimit.ts +23 -15
  296. package/src/internal/Schema.ts +1 -1
  297. package/src/internal/Store.test.ts +99 -13
  298. package/src/internal/Store.ts +52 -17
  299. package/src/internal/Tidx.test.ts +4 -4
  300. package/src/internal/Tidx.ts +1 -3
  301. package/src/internal/Value.test.ts +25 -0
  302. package/src/internal/Value.ts +29 -0
  303. package/src/internal/Viem.test.ts +8 -0
  304. package/src/internal/Viem.ts +2 -1
  305. package/src/internal/WebhookDestination.test.ts +20 -0
  306. package/src/internal/WebhookDestination.ts +10 -0
  307. package/src/internal/WebhookTransfer.test.ts +89 -0
  308. package/src/internal/WebhookTransfer.ts +430 -0
  309. package/src/internal/Webhooks.test.ts +616 -0
  310. package/src/internal/Webhooks.ts +412 -24
  311. package/src/internal/funding/Provider.test.ts +48 -8
  312. package/src/internal/funding/Provider.ts +12 -7
  313. package/src/internal/funding/Reconciliation.local.test.ts +3 -1
  314. package/src/internal/funding/Reconciliation.test.ts +1240 -0
  315. package/src/internal/funding/Reconciliation.ts +136 -0
  316. package/src/internal/funding/SourceObservation.test.ts +1 -0
  317. package/src/internal/funding/providers/index.test.ts +73 -3
  318. package/src/internal/funding/providers/relay.test.ts +2 -9
  319. package/src/internal/funding/providers/relay.ts +72 -13
@@ -1,6 +1,7 @@
1
1
  import type { Metrics } from '../Metrics.js'
2
2
  import type * as Webhooks from '../Webhooks.js'
3
3
  import type * as Log from './Log.js'
4
+ import * as WebhookDestination from './WebhookDestination.js'
4
5
 
5
6
  /** Webhook event type, including the synthetic `ping` carried on test envelopes. */
6
7
  export type EventType = Webhooks.Envelope['type']
@@ -8,6 +9,12 @@ export type EventType = Webhooks.Envelope['type']
8
9
  /** Where a delivery attempt originated. */
9
10
  export type Trigger = 'manual_retry' | 'ping' | 'queue'
10
11
 
12
+ /** Bounded poller stage names used by `webhook:poll`. */
13
+ export type PollStage = 'cursor_write' | 'enrich' | 'rpc_head' | 'scan' | 'subscription_read'
14
+
15
+ /** Bounded Durable Object or transport failure classes used by `webhook:poll-failure`. */
16
+ export type PollFailure = 'other' | 'overloaded' | 'remote' | 'retryable'
17
+
11
18
  /** Context for one poller tick that the {@link Webhooks.createPoller.Result} omits. */
12
19
  export type TickMeta = {
13
20
  /** Chain polled this tick; used when the tick errored before a result existed. */
@@ -20,11 +27,127 @@ export type TickMeta = {
20
27
 
21
28
  /**
22
29
  * A webhook metric event, recorded by the {@link webhooks} sink. The poller
23
- * emits `webhook:tick`/`webhook:enqueue` and the delivery path emits
24
- * `webhook:delivery`.
30
+ * emits head recovery, poll, tick, and enqueue measurements; the delivery path
31
+ * emits `webhook:delivery`.
25
32
  */
26
33
  export type WebhookEvent =
27
34
  | {
35
+ /** Tempo chain whose poll cycle completed. */
36
+ chainId: number
37
+ /** Whether the poll cycle completed without scan or dispatch failures. */
38
+ outcome: 'error' | 'ok'
39
+ /** Requested boundary movement since the preceding successful poll, in blocks. */
40
+ requestedBoundaryAdvanceBlocks?: number | undefined
41
+ /** Execution path that acquired this poll claim. */
42
+ trigger: 'alarm' | 'cron' | 'direct' | 'queue'
43
+ /** Identifies a transport-neutral poll-cycle measurement. */
44
+ type: 'webhook:poll-cycle'
45
+ }
46
+ | {
47
+ /** Tempo chain whose durable progress was read. */
48
+ chainId: number
49
+ /** Blocks between the desired and fully drained boundaries. */
50
+ lagBlocks: number
51
+ /** Identifies durable coordinator progress. */
52
+ type: 'webhook:head-progress'
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
+ }
69
+ | {
70
+ /** Tempo chain whose recovery poke was dequeued. */
71
+ chainId: number
72
+ /** When the recovery poke was dequeued, as epoch milliseconds. */
73
+ dequeuedAt: number
74
+ /** When the recovery poke was enqueued, as epoch milliseconds. */
75
+ enqueuedAt: number
76
+ /** Recovery source carried through the Queue handoff. */
77
+ trigger: 'alarm' | 'queue'
78
+ /** Identifies recovery Queue latency, not observed head volume. */
79
+ type: 'webhook:head-recovery'
80
+ }
81
+ | {
82
+ /** Tempo chain whose poll claim was acquired. */
83
+ chainId: number
84
+ /** Delay from observing the desired boundary to acquiring the claim. */
85
+ delayMs?: number | undefined
86
+ /** Execution path that acquired the claim. */
87
+ trigger: 'alarm' | 'cron' | 'direct' | 'queue'
88
+ /** Identifies a poll beginning, before scan work runs. */
89
+ type: 'webhook:poll-start'
90
+ }
91
+ | {
92
+ /** Tempo chain whose poll failed. */
93
+ chainId: number
94
+ /** Bounded class of the thrown failure. */
95
+ failure: PollFailure
96
+ /** Operation boundary that rejected the poll. */
97
+ stage: 'claim' | 'continuation_enqueue' | 'finish' | 'scan'
98
+ /** Execution path that failed before recovery. */
99
+ trigger: 'alarm' | 'cron' | 'direct' | 'queue'
100
+ /** Identifies a transport-neutral poll failure. */
101
+ type: 'webhook:poll-failure'
102
+ }
103
+ | {
104
+ chainId: number
105
+ durationMs: number
106
+ eventType?: EventType | undefined
107
+ outcome: 'error' | 'ok'
108
+ stage: PollStage
109
+ type: 'webhook:poll'
110
+ }
111
+ | {
112
+ /** Tempo chain whose eligible subscription count was read. */
113
+ chainId: number
114
+ /** Active, unexpired subscriptions selected for the poll. */
115
+ count: number
116
+ /** Identifies a pre-scan subscription eligibility measurement. */
117
+ type: 'webhook:poll-subscriptions'
118
+ }
119
+ | {
120
+ /** Chain scanned by this poll batch. */
121
+ chainId: number
122
+ /** Shared upper bound for concurrent upstream scans. */
123
+ concurrency: number
124
+ /** Scan duration, present only after completion. */
125
+ durationMs?: number | undefined
126
+ /** Subscription event type scanned by this batch. */
127
+ eventType: EventType
128
+ /** Subscriptions whose scan rejected, present only after completion. */
129
+ failedSubscriptions?: number | undefined
130
+ /** Matching rows retained by completed pages, present only after completion. */
131
+ matchedItems?: number | undefined
132
+ /** Largest block window requested by one subscription. */
133
+ maxWindowBlocks: number
134
+ /** Whether the checkpoint precedes allocation or follows scan completion. */
135
+ phase: 'complete' | 'start'
136
+ /** Outcome of the completed RPC-backed scan. */
137
+ outcome?: 'empty' | 'error' | 'missing' | 'ok' | undefined
138
+ /** JSON-RPC method used by this event type's scanner. */
139
+ rpcMethod: 'eth_getBlockByNumber' | 'eth_getLogs'
140
+ /** Subscriptions retained in this scan batch. */
141
+ subscriptionCount: number
142
+ /** Maximum subscriptions retained by this event-type batch. */
143
+ subscriptionLimit: number
144
+ /** Sum of subscription block windows in this scan batch. */
145
+ totalWindowBlocks: number
146
+ /** Identifies a memory-attribution scan boundary. */
147
+ type: 'webhook:poll-scan'
148
+ }
149
+ | {
150
+ cursorLagBlocks?: number | undefined
28
151
  meta: TickMeta
29
152
  result: Webhooks.createPoller.Result | undefined
30
153
  type: 'webhook:tick'
@@ -37,9 +160,37 @@ export type WebhookEvent =
37
160
  type: 'webhook:enqueue'
38
161
  }
39
162
  | {
163
+ /** Atomic ledger claim outcome. */
164
+ outcome: 'attempting' | 'claimed' | 'missing' | 'scheduled' | 'terminal'
165
+ /** Identifies a delivery-obligation claim. */
166
+ type: 'webhook:delivery-job-claim'
167
+ }
168
+ | {
169
+ /** Obligations represented by this outcome. */
170
+ count: number
171
+ /** Whether staging created or found the obligation. */
172
+ outcome: 'created' | 'existing'
173
+ /** Identifies obligation staging ahead of Queue handoff. */
174
+ type: 'webhook:delivery-job-ensure'
175
+ }
176
+ | {
177
+ /** Delivery attempt ordinal: the ledger claim count on the queue path. */
178
+ attempt: number
40
179
  envelope: Webhooks.Envelope
180
+ /** Cloudflare Queue delivery attempt, absent outside the Queue path. */
181
+ queueAttempt?: number | undefined
41
182
  result: Webhooks.Result
42
183
  subscription: Webhooks.Subscription
184
+ timings?: {
185
+ deliveryLogMs?: number | undefined
186
+ envelopeToQueueMs?: number | undefined
187
+ endToEndMs?: number | undefined
188
+ eventToEnvelopeMs?: number | undefined
189
+ processingMs?: number | undefined
190
+ queueWaitMs?: number | undefined
191
+ stateWriteMs?: number | undefined
192
+ subscriptionReadMs?: number | undefined
193
+ }
43
194
  trigger: Trigger
44
195
  type: 'webhook:delivery'
45
196
  }
@@ -195,7 +346,7 @@ type StatusClass = '2xx' | '4xx' | '5xx' | 'network' | 'timeout' | 'unknown' | '
195
346
  /**
196
347
  * Builds a webhook delivery-reliability sink over `backend`. Tags stay
197
348
  * low-cardinality (`chain_id`, `event_type`, `destination_type`, `status_class`,
198
- * `attempt_bucket`, `outcome`) — never subscription/org/event ids, which belong
349
+ * `attempt_bucket`, `failure`, `queue_attempt_bucket`, `outcome`, `stage`) — never subscription/org/event ids, which belong
199
350
  * in logs or analytics, not StatsD-style series.
200
351
  */
201
352
  export function webhooks(backend: Metrics): WebhookSink {
@@ -205,6 +356,111 @@ export function webhooks(backend: Metrics): WebhookSink {
205
356
  },
206
357
  record(event) {
207
358
  switch (event.type) {
359
+ case 'webhook:head-recovery': {
360
+ const tags = { chain_id: String(event.chainId), trigger: event.trigger }
361
+ backend.count('webhook_head_recovery_queue_count', 1, tags)
362
+ backend.histogram(
363
+ 'webhook_head_recovery_queue_wait_ms',
364
+ Math.max(0, event.dequeuedAt - event.enqueuedAt),
365
+ tags,
366
+ )
367
+ return
368
+ }
369
+ case 'webhook:head-progress':
370
+ backend.gauge('webhook_head_progress_lag_blocks', event.lagBlocks, {
371
+ chain_id: String(event.chainId),
372
+ })
373
+ return
374
+ case 'webhook:head-skipped':
375
+ backend.gauge('webhook_head_skipped_count', event.skippedHeads, {
376
+ chain_id: String(event.chainId),
377
+ })
378
+ return
379
+ case 'webhook:listener-head-lag':
380
+ backend.gauge('webhook_listener_head_lag_blocks', event.lagBlocks, {
381
+ chain_id: String(event.chainId),
382
+ })
383
+ return
384
+ case 'webhook:poll-failure':
385
+ backend.count('webhook_poll_failure_count', 1, {
386
+ chain_id: String(event.chainId),
387
+ failure: event.failure,
388
+ stage: event.stage,
389
+ trigger: event.trigger,
390
+ })
391
+ return
392
+ case 'webhook:poll': {
393
+ const tags = {
394
+ chain_id: String(event.chainId),
395
+ ...(event.eventType === undefined ? {} : { event_type: event.eventType }),
396
+ outcome: event.outcome,
397
+ stage: event.stage,
398
+ }
399
+ backend.histogram('webhook_poller_stage_duration_ms', event.durationMs, tags)
400
+ return
401
+ }
402
+ case 'webhook:poll-scan': {
403
+ const tags = {
404
+ chain_id: String(event.chainId),
405
+ event_type: event.eventType,
406
+ phase: event.phase,
407
+ }
408
+ backend.count('webhook_poller_scan_checkpoint_count', 1, tags)
409
+ backend.histogram('webhook_poller_scan_concurrency', event.concurrency, tags)
410
+ backend.histogram('webhook_poller_scan_subscription_count', event.subscriptionCount, tags)
411
+ backend.histogram('webhook_poller_scan_subscription_limit', event.subscriptionLimit, tags)
412
+ backend.histogram('webhook_poller_scan_max_window_blocks', event.maxWindowBlocks, tags)
413
+ backend.histogram(
414
+ 'webhook_poller_scan_total_window_blocks',
415
+ event.totalWindowBlocks,
416
+ tags,
417
+ )
418
+ if (event.durationMs !== undefined)
419
+ backend.histogram('webhook_poller_scan_duration_ms', event.durationMs, tags)
420
+ if (event.failedSubscriptions !== undefined)
421
+ backend.count(
422
+ 'webhook_poller_scan_failed_subscription_count',
423
+ event.failedSubscriptions,
424
+ tags,
425
+ )
426
+ if (event.matchedItems !== undefined)
427
+ backend.count('webhook_poller_scan_matched_item_count', event.matchedItems, tags)
428
+ if (event.outcome !== undefined)
429
+ backend.count('webhook_poller_rpc_scan_count', 1, {
430
+ chain_id: String(event.chainId),
431
+ event_type: event.eventType,
432
+ outcome: event.outcome,
433
+ rpc_method: event.rpcMethod,
434
+ })
435
+ return
436
+ }
437
+ case 'webhook:poll-subscriptions':
438
+ backend.gauge('webhook_poller_active_subscription_count', event.count, {
439
+ chain_id: String(event.chainId),
440
+ })
441
+ return
442
+ case 'webhook:poll-cycle': {
443
+ const tags = {
444
+ chain_id: String(event.chainId),
445
+ outcome: event.outcome,
446
+ trigger: event.trigger,
447
+ }
448
+ backend.count('webhook_poll_cycle_count', 1, tags)
449
+ if (event.requestedBoundaryAdvanceBlocks !== undefined)
450
+ backend.histogram(
451
+ 'webhook_poll_requested_boundary_advance_blocks',
452
+ event.requestedBoundaryAdvanceBlocks,
453
+ tags,
454
+ )
455
+ return
456
+ }
457
+ case 'webhook:poll-start': {
458
+ const tags = { chain_id: String(event.chainId), trigger: event.trigger }
459
+ backend.count('webhook_poll_start_count', 1, tags)
460
+ if (event.delayMs !== undefined)
461
+ backend.histogram('webhook_poll_start_delay_ms', event.delayMs, tags)
462
+ return
463
+ }
208
464
  case 'webhook:tick': {
209
465
  const { meta, result } = event
210
466
  const chain_id = String(result?.chainId ?? meta.chainId ?? 'unknown')
@@ -213,6 +469,10 @@ export function webhooks(backend: Metrics): WebhookSink {
213
469
  chain_id,
214
470
  outcome: meta.outcome,
215
471
  })
472
+ if (event.cursorLagBlocks !== undefined)
473
+ backend.gauge('webhook_subscription_cursor_lag_blocks', event.cursorLagBlocks, {
474
+ chain_id,
475
+ })
216
476
  if (!result) return
217
477
  backend.histogram('webhook_poller_subscription_count', result.subscriptions, { chain_id })
218
478
  backend.count('webhook_poller_scanned_count', result.scanned, { chain_id })
@@ -237,8 +497,29 @@ export function webhooks(backend: Metrics): WebhookSink {
237
497
  })
238
498
  return
239
499
  }
500
+ case 'webhook:delivery-job-claim': {
501
+ backend.count('webhook_delivery_job_claim_count', 1, { outcome: event.outcome })
502
+ return
503
+ }
504
+ case 'webhook:delivery-job-ensure': {
505
+ backend.count('webhook_delivery_job_ensure_count', event.count, {
506
+ outcome: event.outcome,
507
+ })
508
+ return
509
+ }
240
510
  case 'webhook:delivery': {
241
- const { envelope, result, subscription } = event
511
+ const { envelope, result, subscription, timings } = event
512
+ const queueTags =
513
+ event.queueAttempt === undefined
514
+ ? {}
515
+ : {
516
+ queue_attempt_bucket: attemptBucket(event.queueAttempt),
517
+ // Retryable results ack the message and schedule a ledger
518
+ // retry; the Queue transport never redelivers them.
519
+ queue_disposition: WebhookDestination.isRetryable(result)
520
+ ? 'scheduled_retry'
521
+ : 'acked',
522
+ }
242
523
  const tags = {
243
524
  chain_id: String(envelope.chainId),
244
525
  destination_type: subscription.destination.type satisfies DestinationType,
@@ -247,8 +528,9 @@ export function webhooks(backend: Metrics): WebhookSink {
247
528
  const outcome = result.ok ? 'succeeded' : 'failed'
248
529
  backend.count('webhook_delivery_attempt_count', 1, {
249
530
  ...tags,
250
- attempt_bucket: attemptBucket(subscription.failureCount + 1),
531
+ attempt_bucket: attemptBucket(event.attempt),
251
532
  outcome,
533
+ ...queueTags,
252
534
  status_class: statusClass(result),
253
535
  trigger: event.trigger,
254
536
  })
@@ -257,11 +539,44 @@ export function webhooks(backend: Metrics): WebhookSink {
257
539
  ...tags,
258
540
  outcome,
259
541
  })
260
- backend.histogram(
261
- 'webhook_delivery_queue_latency_ms',
262
- Math.max(0, Date.now() - Date.parse(envelope.createdAt)),
263
- tags,
264
- )
542
+ if (timings?.deliveryLogMs !== undefined)
543
+ backend.histogram('webhook_delivery_log_write_ms', timings.deliveryLogMs, tags)
544
+ if (timings?.envelopeToQueueMs !== undefined)
545
+ backend.histogram(
546
+ 'webhook_delivery_envelope_to_queue_ms',
547
+ timings.envelopeToQueueMs,
548
+ tags,
549
+ )
550
+ if (timings?.endToEndMs !== undefined)
551
+ backend.histogram('webhook_delivery_end_to_end_ms', timings.endToEndMs, {
552
+ ...tags,
553
+ // Unlimited-horizon ledger retries emit hour-plus samples;
554
+ // the bucket keeps first-attempt percentiles interpretable.
555
+ attempt_bucket: attemptBucket(event.attempt),
556
+ outcome,
557
+ })
558
+ if (timings?.eventToEnvelopeMs !== undefined)
559
+ backend.histogram(
560
+ 'webhook_delivery_event_to_envelope_ms',
561
+ timings.eventToEnvelopeMs,
562
+ tags,
563
+ )
564
+ if (timings?.processingMs !== undefined)
565
+ backend.histogram('webhook_delivery_processing_ms', timings.processingMs, tags)
566
+ if (timings?.queueWaitMs !== undefined)
567
+ backend.histogram('webhook_delivery_queue_wait_ms', timings.queueWaitMs, {
568
+ ...tags,
569
+ outcome,
570
+ ...queueTags,
571
+ })
572
+ if (timings?.stateWriteMs !== undefined)
573
+ backend.histogram('webhook_delivery_state_write_ms', timings.stateWriteMs, tags)
574
+ if (timings?.subscriptionReadMs !== undefined)
575
+ backend.histogram(
576
+ 'webhook_delivery_subscription_read_ms',
577
+ timings.subscriptionReadMs,
578
+ tags,
579
+ )
265
580
  return
266
581
  }
267
582
  }
@@ -1,13 +1,14 @@
1
1
  import { RateLimit, Store } from 'tapimo'
2
2
 
3
3
  describe('memory', () => {
4
- test('tracks fixed-window quota in a store', async () => {
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 first = await store.consume({ key: 'public:anonymous', limit: { perMinute: 2 } })
9
- const second = await store.consume({ key: 'public:anonymous', limit: { perMinute: 2 } })
10
- const third = await store.consume({ key: 'public:anonymous', limit: { perMinute: 2 } })
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 window opens', async () => {
39
- // The window reset rides on counter expiry (the key is stable, not
40
- // bucket-suffixed), so the store clock and the consume clock must agree.
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.000Z'))
44
+ vi.setSystemTime(new Date('2026-05-25T00:00:30.250Z'))
44
45
  const store = RateLimit.memory()
45
- await store.consume({ key: 'public:anonymous', limit: { perMinute: 2 } })
46
- await store.consume({ key: 'public:anonymous', limit: { perMinute: 2 } })
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:01:00.001Z'))
49
- const next = await store.consume({ key: 'public:anonymous', limit: { perMinute: 2 } })
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": 1779667320,
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
- // 30s into the minute, so 30s of the window remains.
76
- now: () => new Date('2026-05-25T00:00:30.000Z'),
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({ key: 'public:anonymous', limit: { perMinute: 2 } })
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": 30000,
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
- perMinute: z
9
+ limit: z
10
10
  .number()
11
- .check(z.int(), z.nonnegative(), z.describe('Number of requests allowed per minute.')),
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 is stable per quota key the
53
- * window lives in the TTL: the counter expires exactly at the end of the
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 bucket = Math.floor(time / 60_000)
66
- // Every increment within a window recomputes the TTL against the same
67
- // window end, so the counter's absolute expiry never moves under load.
68
- const count = await Store.increment(store, `ratelimit:${options.key}`, {
69
- ttl: (bucket + 1) * 60_000 - time,
70
- })
71
- const limit = options.limit.perMinute
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) * 60_000) / 1_000),
85
+ reset: Math.ceil(((bucket + 1) * window) / 1_000),
78
86
  }
79
87
  },
80
88
  }
@@ -356,7 +356,7 @@ export const Denomination = z
356
356
  .check(
357
357
  z.describe(
358
358
  'Currency to denominate values in (case-insensitive, e.g. `AUD`). ' +
359
- 'Must be priced by the configured FX oracle. Defaults to `USD`.',
359
+ 'Must be priced by the configured FX oracle.',
360
360
  ),
361
361
  z.meta({ examples: ['AUD'] }),
362
362
  )