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,5 +1,6 @@
1
1
  import { Webhooks } from 'tapimo'
2
2
  import * as TestApp from '../../test/App.js'
3
+ import * as Metrics from '../Metrics.js'
3
4
  import * as Cursor from './Cursor.js'
4
5
 
5
6
  const apiKeyOwner = { orgId: 'org_1', type: 'api_key' } as const
@@ -327,6 +328,78 @@ describe('listActive', () => {
327
328
  })
328
329
  })
329
330
 
331
+ describe('listActiveWithCursors', () => {
332
+ test('pairs each subscription with its stored cursor from one read', async () => {
333
+ const db = TestApp.database()
334
+ const anchored = await Webhooks.createSubscription(db, createInput())
335
+ const fresh = await Webhooks.createSubscription(db, createInput({ owner: mppOwner }))
336
+ await Webhooks.setCursor(db, anchored.id, Cursor.encode([100, 5]))
337
+
338
+ const pairs = await Webhooks.listActiveWithCursors(db, 4217, 'token:transfer')
339
+ const byId = new Map(pairs.map((pair) => [pair.subscription.id, pair.cursor]))
340
+ expect(Cursor.decode(byId.get(anchored.id)!, ['int', 'int'])).toEqual([100, 5])
341
+ expect(byId.get(fresh.id)).toBeNull()
342
+ })
343
+ })
344
+
345
+ describe('listActiveForChain', () => {
346
+ test('returns active subscriptions across event types in one read', async () => {
347
+ const db = TestApp.database()
348
+ const transfer = await Webhooks.createSubscription(db, createInput())
349
+ const transaction = await Webhooks.createSubscription(
350
+ db,
351
+ createInput({ eventType: 'transaction:included' }),
352
+ )
353
+ await Webhooks.createSubscription(
354
+ db,
355
+ createInput({ eventType: 'block:created', owner: mppOwner }),
356
+ )
357
+ await Webhooks.createSubscription(db, createInput({ chainId: 42431, owner: mppOwner }))
358
+ await Webhooks.updateSubscription(db, apiKeyOwner, transfer.id, { status: 'paused' })
359
+
360
+ const pairs = await Webhooks.listActiveForChain(db, {
361
+ chainId: 4217,
362
+ eventTypes: ['transaction:included'],
363
+ })
364
+ expect(pairs.map(({ subscription }) => subscription.id)).toEqual([transaction.id])
365
+ })
366
+ })
367
+
368
+ describe('maxCursorBlock', () => {
369
+ test('returns the freshest cursor block across event types', async () => {
370
+ const db = TestApp.database()
371
+ const transfer = await Webhooks.createSubscription(db, createInput())
372
+ const tx = await Webhooks.createSubscription(
373
+ db,
374
+ createInput({ eventType: 'transaction:included' }),
375
+ )
376
+ await Webhooks.setCursor(db, transfer.id, Cursor.encode([100, 5]))
377
+ await Webhooks.setCursor(db, tx.id, Cursor.encode([250, 0]))
378
+
379
+ expect(await Webhooks.maxCursorBlock(db, 4217)).toBe(250)
380
+ })
381
+
382
+ test('counts creation checkpoints at their recorded head', async () => {
383
+ const db = TestApp.database()
384
+ await Webhooks.createSubscription(db, createInput(), { startBlockNumber: 900 })
385
+
386
+ expect(await Webhooks.maxCursorBlock(db, 4217)).toBe(900)
387
+ })
388
+
389
+ test('ignores other chains and returns null without cursors', async () => {
390
+ const db = TestApp.database()
391
+ expect(await Webhooks.maxCursorBlock(db, 4217)).toBeNull()
392
+
393
+ const other = await Webhooks.createSubscription(db, createInput({ chainId: 42431 }))
394
+ await Webhooks.setCursor(db, other.id, Cursor.encode([999, 0]))
395
+ // A cursorless subscription on the queried chain still reads as null.
396
+ await Webhooks.createSubscription(db, createInput())
397
+
398
+ expect(await Webhooks.maxCursorBlock(db, 4217)).toBeNull()
399
+ expect(await Webhooks.maxCursorBlock(db, 42431)).toBe(999)
400
+ })
401
+ })
402
+
330
403
  describe('cursors', () => {
331
404
  test('round-trips a per-subscription cursor', async () => {
332
405
  const db = TestApp.database()
@@ -362,6 +435,32 @@ describe('cursors', () => {
362
435
  })
363
436
  })
364
437
 
438
+ describe('compareAndSetCursors', () => {
439
+ test('atomically advances a batch and reports stale rows', async () => {
440
+ const db = TestApp.database()
441
+ const initialized = await Webhooks.createSubscription(db, createInput())
442
+ const stale = await Webhooks.createSubscription(db, createInput())
443
+ const uninitialized = await Webhooks.createSubscription(db, createInput())
444
+ const initial = Cursor.encode([100, 1])
445
+ const next = Cursor.encode([101, 1])
446
+ await Webhooks.setCursor(db, initialized.id, initial)
447
+ await Webhooks.setCursor(db, stale.id, initial)
448
+
449
+ const committed = await Webhooks.compareAndSetCursors(db, {
450
+ cursors: [
451
+ { cursor: next, expectedCursor: initial, subscriptionId: initialized.id },
452
+ { cursor: next, expectedCursor: null, subscriptionId: stale.id },
453
+ { cursor: next, expectedCursor: null, subscriptionId: uninitialized.id },
454
+ ],
455
+ })
456
+
457
+ expect([...committed].toSorted()).toEqual([initialized.id, uninitialized.id].toSorted())
458
+ expect(await Webhooks.getCursor(db, initialized.id)).toBe(next)
459
+ expect(await Webhooks.getCursor(db, stale.id)).toBe(initial)
460
+ expect(await Webhooks.getCursor(db, uninitialized.id)).toBe(next)
461
+ })
462
+ })
463
+
365
464
  describe('recordSuccess / recordFailure', () => {
366
465
  test('recordSuccess resets failures and stamps lastDeliveryAt', async () => {
367
466
  const db = TestApp.database()
@@ -624,6 +723,44 @@ describe('delivery log', () => {
624
723
  `)
625
724
  })
626
725
 
726
+ test('deliverAndRecord partitions event, enqueue, and queue latency', async () => {
727
+ const db = TestApp.database()
728
+ const created = await Webhooks.createSubscription(db, createInput())
729
+ const histograms = new Map<string, number>()
730
+ const metrics = Metrics.from({
731
+ count() {},
732
+ flush() {},
733
+ gauge() {},
734
+ histogram(name, value) {
735
+ histograms.set(name, value)
736
+ },
737
+ })
738
+ const envelope = {
739
+ chainId: 4217,
740
+ createdAt: '1970-01-01T00:00:00.500Z',
741
+ data: { timestamp: '1970-01-01T00:00:00.000Z' },
742
+ id: 'evt_null',
743
+ subscriptionId: created.id,
744
+ type: 'token:transfer',
745
+ } satisfies Webhooks.Envelope
746
+ const fetch = (async () =>
747
+ new Response(null, { status: 200 })) satisfies typeof globalThis.fetch
748
+
749
+ await expect(
750
+ Webhooks.deliverAndRecord(db, created, envelope, {
751
+ dequeuedAt: 1_250,
752
+ fetch,
753
+ metrics,
754
+ now: fixedNow,
755
+ queueAttempt: 2,
756
+ queuedAt: 1_000,
757
+ }),
758
+ ).resolves.toMatchObject({ ok: true })
759
+ expect(histograms.get('webhook_delivery_envelope_to_queue_ms')).toBe(500)
760
+ expect(histograms.get('webhook_delivery_event_to_envelope_ms')).toBe(500)
761
+ expect(histograms.get('webhook_delivery_queue_wait_ms')).toBe(250)
762
+ })
763
+
627
764
  test('deliverAndRecord appends a failed row with the error and status', async () => {
628
765
  const db = TestApp.database()
629
766
  const created = await Webhooks.createSubscription(db, createInput())
@@ -667,6 +804,25 @@ describe('delivery log', () => {
667
804
  `)
668
805
  })
669
806
 
807
+ test('deliverAndRecord records envelope serialization failures', async () => {
808
+ const db = TestApp.database()
809
+ const created = await Webhooks.createSubscription(db, createInput())
810
+ const envelope = {
811
+ chainId: 4217,
812
+ createdAt: '2026-01-01T00:00:00.000Z',
813
+ data: { amount: 1n },
814
+ id: 'evt_invalid',
815
+ subscriptionId: created.id,
816
+ type: 'token:transfer',
817
+ } satisfies Webhooks.Envelope
818
+
819
+ const result = await Webhooks.deliverAndRecord(db, created, envelope, { now: fixedNow })
820
+
821
+ expect(result).toEqual({ error: 'delivery failed before request', ok: false })
822
+ const after = await Webhooks.getSubscription(db, apiKeyOwner, created.id)
823
+ expect(after?.failureCount).toBe(1)
824
+ })
825
+
670
826
  test('deliverCurrentAndRecord skips paused and deleted queued subscriptions', async () => {
671
827
  const db = TestApp.database()
672
828
  const paused = await Webhooks.createSubscription(db, createInput())
@@ -772,6 +928,466 @@ describe('delivery log', () => {
772
928
  })
773
929
  })
774
930
 
931
+ async function stagedObligation(db: ReturnType<typeof TestApp.database>, logIndex = 4) {
932
+ const subscription = await Webhooks.createSubscription(db, createInput())
933
+ const envelope = Webhooks.buildEnvelope({
934
+ blockNumber: 123,
935
+ createdAt: new Date(),
936
+ data: { amount: '1' },
937
+ logIndex,
938
+ subscription,
939
+ })
940
+ const { created, references } = await Webhooks.ensureQueueEvents(db, [{ envelope, subscription }])
941
+ return { created, envelope, reference: references[0]!, subscription }
942
+ }
943
+
944
+ describe('ensureQueueEvents', () => {
945
+ test('keeps oversized envelopes behind compact idempotent Queue references', async () => {
946
+ const db = TestApp.database()
947
+ const subscription = await Webhooks.createSubscription(
948
+ db,
949
+ createInput({ eventType: 'transaction:included' }),
950
+ )
951
+ const input = `0x${'ab'.repeat(70_000)}`
952
+ const envelope = Webhooks.buildEnvelope({
953
+ blockNumber: 123,
954
+ createdAt: fixedNow(),
955
+ data: { input, meta: { rpc: { input } } },
956
+ logIndex: 4,
957
+ subscription,
958
+ })
959
+ const dispatchable = { envelope, subscription }
960
+
961
+ const { references: [reference] } = await Webhooks.ensureQueueEvents(db, [dispatchable]) // prettier-ignore
962
+ await Webhooks.ensureQueueEvents(db, [dispatchable])
963
+
964
+ expect(
965
+ new TextEncoder().encode(JSON.stringify({ body: dispatchable })).byteLength,
966
+ ).toBeGreaterThan(128_000)
967
+ expect(new TextEncoder().encode(JSON.stringify({ body: reference })).byteLength).toBeLessThan(
968
+ 1_000,
969
+ )
970
+ expect((await Webhooks.getQueueEvent(db, reference!))?.envelope).toEqual(envelope)
971
+ })
972
+
973
+ test('delivers a staged oversized envelope from its compact reference', async () => {
974
+ const db = TestApp.database()
975
+ const subscription = await Webhooks.createSubscription(
976
+ db,
977
+ createInput({ eventType: 'transaction:included' }),
978
+ )
979
+ const input = `0x${'ab'.repeat(70_000)}`
980
+ const envelope = Webhooks.buildEnvelope({
981
+ blockNumber: 123,
982
+ data: { input, meta: { rpc: { input } } },
983
+ logIndex: 4,
984
+ subscription,
985
+ })
986
+ const { references: [reference] } = await Webhooks.ensureQueueEvents(db, [{ envelope, subscription }]) // prettier-ignore
987
+ const histograms = new Map<string, number>()
988
+ const metrics = Metrics.from({
989
+ count() {},
990
+ flush() {},
991
+ gauge() {},
992
+ histogram(name, value) {
993
+ histograms.set(name, value)
994
+ },
995
+ })
996
+ let deliveredBytes = 0
997
+ const fetch = (async (_url, init) => {
998
+ deliveredBytes = new TextEncoder().encode(
999
+ typeof init?.body === 'string' ? init.body : '',
1000
+ ).byteLength
1001
+ return new Response(null, { status: 200 })
1002
+ }) satisfies typeof globalThis.fetch
1003
+
1004
+ const result = await Webhooks.deliverQueueEventAndRecord(db, reference!, {
1005
+ fetch,
1006
+ metrics,
1007
+ queuedAt: Date.now(),
1008
+ })
1009
+
1010
+ expect(result.status).toBe('delivered')
1011
+ expect(deliveredBytes).toBeGreaterThan(128_000)
1012
+ expect(histograms.has('webhook_delivery_processing_ms')).toBe(true)
1013
+ expect(histograms.has('webhook_delivery_queue_wait_ms')).toBe(true)
1014
+ expect(histograms.has('webhook_delivery_subscription_read_ms')).toBe(true)
1015
+ })
1016
+
1017
+ test('skips a staged event after its subscription expires', async () => {
1018
+ vi.useFakeTimers({ now: fixedNow(), toFake: ['Date'] })
1019
+ try {
1020
+ const db = TestApp.database()
1021
+ const subscription = await Webhooks.createSubscription(
1022
+ db,
1023
+ createInput({ eventType: 'transaction:included', owner: mppOwner, ttl: 60_000 }),
1024
+ { now: fixedNow },
1025
+ )
1026
+ const envelope = Webhooks.buildEnvelope({
1027
+ blockNumber: 123,
1028
+ data: {},
1029
+ logIndex: 4,
1030
+ subscription,
1031
+ })
1032
+ const { references: [reference] } = await Webhooks.ensureQueueEvents(db, [{ envelope, subscription }]) // prettier-ignore
1033
+
1034
+ vi.setSystemTime(new Date('2026-01-01T00:01:00.000Z'))
1035
+
1036
+ await expect(Webhooks.deliverQueueEventAndRecord(db, reference!)).resolves
1037
+ .toMatchInlineSnapshot(`
1038
+ {
1039
+ "status": "skipped",
1040
+ }
1041
+ `)
1042
+ } finally {
1043
+ vi.useRealTimers()
1044
+ }
1045
+ })
1046
+
1047
+ test('stages once and keeps the first envelope', async () => {
1048
+ const db = TestApp.database()
1049
+ const { created, envelope, reference, subscription } = await stagedObligation(db)
1050
+ const replacement = { ...envelope, data: { amount: '2' } }
1051
+
1052
+ const second = await Webhooks.ensureQueueEvents(db, [{ envelope: replacement, subscription }])
1053
+
1054
+ expect(created).toBe(1)
1055
+ expect(second.created).toBe(0)
1056
+ expect((await Webhooks.getQueueEvent(db, reference))?.envelope).toEqual(envelope)
1057
+ })
1058
+
1059
+ test('never resurrects a terminal obligation', async () => {
1060
+ const db = TestApp.database()
1061
+ const { envelope, reference, subscription } = await stagedObligation(db)
1062
+ const claim = await Webhooks.claimQueueEvent(db, reference)
1063
+ expect(claim.type).toBe('claimed')
1064
+ if (claim.type !== 'claimed') return
1065
+ expect(
1066
+ await Webhooks.completeQueueEvent(db, reference, {
1067
+ claimedAt: claim.record.attemptingAt!,
1068
+ status: 'succeeded',
1069
+ }),
1070
+ ).toBe(true)
1071
+
1072
+ const again = await Webhooks.ensureQueueEvents(db, [{ envelope, subscription }])
1073
+ expect(again.created).toBe(0)
1074
+ await expect(Webhooks.claimQueueEvent(db, reference)).resolves.toEqual({
1075
+ status: 'succeeded',
1076
+ type: 'terminal',
1077
+ })
1078
+ // Terminal rows read as absent, so no consumer path can redeliver them.
1079
+ expect(await Webhooks.getQueueEvent(db, reference)).toBeNull()
1080
+ })
1081
+
1082
+ test('replaces an expired terminal row so replays stay deliverable', async () => {
1083
+ vi.useFakeTimers({ toFake: ['Date'] })
1084
+ vi.setSystemTime(fixedNow())
1085
+ try {
1086
+ const db = TestApp.database()
1087
+ const { envelope, reference, subscription } = await stagedObligation(db)
1088
+ const claim = await Webhooks.claimQueueEvent(db, reference)
1089
+ expect(claim.type).toBe('claimed')
1090
+ if (claim.type !== 'claimed') return
1091
+ expect(
1092
+ await Webhooks.completeQueueEvent(db, reference, {
1093
+ claimedAt: claim.record.attemptingAt!,
1094
+ status: 'succeeded',
1095
+ }),
1096
+ ).toBe(true)
1097
+
1098
+ // Past the dedupe window an unpruned terminal row must not suppress a
1099
+ // replay.
1100
+ vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.deliveryRetentionMs + 1_000))
1101
+ const replay = await Webhooks.ensureQueueEvents(db, [{ envelope, subscription }])
1102
+ expect(replay.created).toBe(1)
1103
+ await expect(Webhooks.claimQueueEvent(db, reference)).resolves.toMatchObject({
1104
+ type: 'claimed',
1105
+ })
1106
+ } finally {
1107
+ vi.useRealTimers()
1108
+ }
1109
+ })
1110
+ })
1111
+
1112
+ describe('claimQueueEvent', () => {
1113
+ beforeEach(() => {
1114
+ vi.useFakeTimers({ toFake: ['Date'] })
1115
+ vi.setSystemTime(fixedNow())
1116
+ })
1117
+
1118
+ afterEach(() => {
1119
+ vi.useRealTimers()
1120
+ })
1121
+
1122
+ test('claims a due obligation once and reclaims after staleness', async () => {
1123
+ const db = TestApp.database()
1124
+ const { reference } = await stagedObligation(db)
1125
+
1126
+ const first = await Webhooks.claimQueueEvent(db, reference)
1127
+ expect(first.type).toBe('claimed')
1128
+ if (first.type !== 'claimed') return
1129
+ expect(first.record.attemptCount).toBe(1)
1130
+
1131
+ await expect(Webhooks.claimQueueEvent(db, reference)).resolves.toEqual({
1132
+ type: 'attempting',
1133
+ })
1134
+
1135
+ vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.deliveryAttemptStaleMs + 1))
1136
+ const reclaimed = await Webhooks.claimQueueEvent(db, reference)
1137
+ expect(reclaimed.type).toBe('claimed')
1138
+ if (reclaimed.type !== 'claimed') return
1139
+ expect(reclaimed.record.attemptCount).toBe(2)
1140
+ })
1141
+
1142
+ test('classifies missing, scheduled, and terminal obligations', async () => {
1143
+ const db = TestApp.database()
1144
+ const { reference } = await stagedObligation(db)
1145
+
1146
+ await expect(
1147
+ Webhooks.claimQueueEvent(db, { eventId: 'evt_absent', subscriptionId: 'wh_absent' }),
1148
+ ).resolves.toEqual({ type: 'missing' })
1149
+
1150
+ const claim = await Webhooks.claimQueueEvent(db, reference)
1151
+ expect(claim.type).toBe('claimed')
1152
+ if (claim.type !== 'claimed') return
1153
+ expect(
1154
+ await Webhooks.scheduleQueueEventRetry(db, reference, {
1155
+ attemptCount: 1,
1156
+ claimedAt: claim.record.attemptingAt!,
1157
+ }),
1158
+ ).toBe(true)
1159
+ await expect(Webhooks.claimQueueEvent(db, reference)).resolves.toEqual({
1160
+ nextAttemptAt: new Date(fixedNow().getTime() + Webhooks.retryDelayMs(1)).toISOString(),
1161
+ type: 'scheduled',
1162
+ })
1163
+
1164
+ vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.retryDelayMs(1) + 1))
1165
+ const retried = await Webhooks.claimQueueEvent(db, reference)
1166
+ expect(retried.type).toBe('claimed')
1167
+ if (retried.type !== 'claimed') return
1168
+ expect(
1169
+ await Webhooks.completeQueueEvent(db, reference, {
1170
+ claimedAt: retried.record.attemptingAt!,
1171
+ status: 'failed',
1172
+ }),
1173
+ ).toBe(true)
1174
+ await expect(Webhooks.claimQueueEvent(db, reference)).resolves.toEqual({
1175
+ status: 'failed',
1176
+ type: 'terminal',
1177
+ })
1178
+ })
1179
+
1180
+ test('keeps owed work claimable indefinitely', async () => {
1181
+ const db = TestApp.database()
1182
+ const { reference } = await stagedObligation(db)
1183
+
1184
+ // Pending obligations never expire; only terminal transitions stamp a
1185
+ // dedupe deadline.
1186
+ vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.deliveryRetentionMs * 3))
1187
+ await expect(Webhooks.claimQueueEvent(db, reference)).resolves.toMatchObject({
1188
+ type: 'claimed',
1189
+ })
1190
+ })
1191
+
1192
+ test('never prunes pending work', async () => {
1193
+ const db = TestApp.database()
1194
+ const { reference } = await stagedObligation(db)
1195
+
1196
+ vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.deliveryRetentionMs + 1_000))
1197
+ await Webhooks.pruneExpired(db)
1198
+ const claim = await Webhooks.claimQueueEvent(db, reference)
1199
+ expect(claim.type).toBe('claimed')
1200
+ if (claim.type !== 'claimed') return
1201
+ expect(
1202
+ await Webhooks.completeQueueEvent(db, reference, {
1203
+ claimedAt: claim.record.attemptingAt!,
1204
+ status: 'succeeded',
1205
+ }),
1206
+ ).toBe(true)
1207
+ })
1208
+
1209
+ test('fences a stale claimant after a reclaim', async () => {
1210
+ const db = TestApp.database()
1211
+ const { reference } = await stagedObligation(db)
1212
+ const stale = await Webhooks.claimQueueEvent(db, reference)
1213
+ expect(stale.type).toBe('claimed')
1214
+ if (stale.type !== 'claimed') return
1215
+
1216
+ vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.deliveryAttemptStaleMs + 1))
1217
+ const current = await Webhooks.claimQueueEvent(db, reference)
1218
+ expect(current.type).toBe('claimed')
1219
+ if (current.type !== 'claimed') return
1220
+
1221
+ const staleClaimedAt = stale.record.attemptingAt!
1222
+ expect(
1223
+ await Webhooks.completeQueueEvent(db, reference, {
1224
+ claimedAt: staleClaimedAt,
1225
+ status: 'failed',
1226
+ }),
1227
+ ).toBe(false)
1228
+ expect(
1229
+ await Webhooks.scheduleQueueEventRetry(db, reference, {
1230
+ attemptCount: 1,
1231
+ claimedAt: staleClaimedAt,
1232
+ }),
1233
+ ).toBe(false)
1234
+ expect(
1235
+ await Webhooks.completeQueueEvent(db, reference, {
1236
+ claimedAt: current.record.attemptingAt!,
1237
+ status: 'succeeded',
1238
+ }),
1239
+ ).toBe(true)
1240
+ })
1241
+ })
1242
+
1243
+ describe('retryDelayMs', () => {
1244
+ test('backs off exponentially and caps hourly', () => {
1245
+ expect([1, 2, 3, 4, 5, 6, 10].map(Webhooks.retryDelayMs)).toMatchInlineSnapshot(`
1246
+ [
1247
+ 5000,
1248
+ 25000,
1249
+ 125000,
1250
+ 625000,
1251
+ 3125000,
1252
+ 3600000,
1253
+ 3600000,
1254
+ ]
1255
+ `)
1256
+ })
1257
+ })
1258
+
1259
+ describe('dueQueueEvents', () => {
1260
+ beforeEach(() => {
1261
+ vi.useFakeTimers({ toFake: ['Date'] })
1262
+ vi.setSystemTime(fixedNow())
1263
+ })
1264
+
1265
+ afterEach(() => {
1266
+ vi.useRealTimers()
1267
+ })
1268
+
1269
+ test('lists due pending work and skips in-flight, scheduled, and terminal rows', async () => {
1270
+ const db = TestApp.database()
1271
+ // Distinct staging times so the due order (next_attempt_at) is deterministic.
1272
+ const due = await stagedObligation(db, 1)
1273
+ vi.setSystemTime(new Date(fixedNow().getTime() + 1))
1274
+ const inFlight = await stagedObligation(db, 2)
1275
+ vi.setSystemTime(new Date(fixedNow().getTime() + 2))
1276
+ const scheduled = await stagedObligation(db, 3)
1277
+ vi.setSystemTime(new Date(fixedNow().getTime() + 3))
1278
+ const terminal = await stagedObligation(db, 4)
1279
+
1280
+ expect((await Webhooks.claimQueueEvent(db, inFlight.reference)).type).toBe('claimed')
1281
+ const scheduledClaim = await Webhooks.claimQueueEvent(db, scheduled.reference)
1282
+ expect(scheduledClaim.type).toBe('claimed')
1283
+ if (scheduledClaim.type !== 'claimed') return
1284
+ expect(
1285
+ await Webhooks.scheduleQueueEventRetry(db, scheduled.reference, {
1286
+ attemptCount: 3,
1287
+ claimedAt: scheduledClaim.record.attemptingAt!,
1288
+ }),
1289
+ ).toBe(true)
1290
+ const terminalClaim = await Webhooks.claimQueueEvent(db, terminal.reference)
1291
+ expect(terminalClaim.type).toBe('claimed')
1292
+ if (terminalClaim.type !== 'claimed') return
1293
+ expect(
1294
+ await Webhooks.completeQueueEvent(db, terminal.reference, {
1295
+ claimedAt: terminalClaim.record.attemptingAt!,
1296
+ status: 'succeeded',
1297
+ }),
1298
+ ).toBe(true)
1299
+
1300
+ await expect(Webhooks.dueQueueEvents(db, 10)).resolves.toEqual([due.reference])
1301
+
1302
+ vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.deliveryAttemptStaleMs + 10))
1303
+ await expect(Webhooks.dueQueueEvents(db, 10)).resolves.toEqual([
1304
+ due.reference,
1305
+ inFlight.reference,
1306
+ ])
1307
+ })
1308
+ })
1309
+
1310
+ describe('pendingQueueEvents', () => {
1311
+ beforeEach(() => {
1312
+ vi.useFakeTimers({ toFake: ['Date'] })
1313
+ vi.setSystemTime(fixedNow())
1314
+ })
1315
+
1316
+ afterEach(() => {
1317
+ vi.useRealTimers()
1318
+ })
1319
+
1320
+ test('counts pending obligations and reports the oldest staging time', async () => {
1321
+ const db = TestApp.database()
1322
+ await stagedObligation(db, 1)
1323
+ vi.setSystemTime(new Date(fixedNow().getTime() + 1_000))
1324
+ const done = await stagedObligation(db, 2)
1325
+ const doneClaim = await Webhooks.claimQueueEvent(db, done.reference)
1326
+ expect(doneClaim.type).toBe('claimed')
1327
+ if (doneClaim.type !== 'claimed') return
1328
+ expect(
1329
+ await Webhooks.completeQueueEvent(db, done.reference, {
1330
+ claimedAt: doneClaim.record.attemptingAt!,
1331
+ status: 'succeeded',
1332
+ }),
1333
+ ).toBe(true)
1334
+
1335
+ await expect(Webhooks.pendingQueueEvents(db)).resolves.toEqual({
1336
+ count: 1,
1337
+ dueCount: 1,
1338
+ oldestAt: fixedNow().toISOString(),
1339
+ oldestDueAt: fixedNow().toISOString(),
1340
+ })
1341
+ })
1342
+
1343
+ test('excludes in-flight claims and scheduled retries from due work', async () => {
1344
+ const db = TestApp.database()
1345
+ const inFlight = await stagedObligation(db, 1)
1346
+ const claim = await Webhooks.claimQueueEvent(db, inFlight.reference)
1347
+ expect(claim.type).toBe('claimed')
1348
+
1349
+ await expect(Webhooks.pendingQueueEvents(db)).resolves.toMatchObject({
1350
+ count: 1,
1351
+ dueCount: 0,
1352
+ oldestDueAt: null,
1353
+ })
1354
+ })
1355
+ })
1356
+
1357
+ describe('deliverClaimedAndRecord', () => {
1358
+ test('delivers a claimed envelope and skips an inactive subscription', async () => {
1359
+ const db = TestApp.database()
1360
+ const { envelope, reference, subscription } = await stagedObligation(db)
1361
+ const claim = await Webhooks.claimQueueEvent(db, reference)
1362
+ expect(claim.type).toBe('claimed')
1363
+ if (claim.type !== 'claimed') return
1364
+
1365
+ let delivered: unknown
1366
+ const fetch = (async (_url, init) => {
1367
+ delivered = JSON.parse(typeof init?.body === 'string' ? init.body : '{}')
1368
+ return new Response(null, { status: 200 })
1369
+ }) satisfies typeof globalThis.fetch
1370
+
1371
+ const delivery = await Webhooks.deliverClaimedAndRecord(
1372
+ db,
1373
+ { envelope: claim.record.envelope, subscriptionId: reference.subscriptionId },
1374
+ { fetch },
1375
+ )
1376
+ expect(delivery.status).toBe('delivered')
1377
+ if (delivery.status !== 'delivered') return
1378
+ expect(delivery.result.ok).toBe(true)
1379
+ expect(delivered).toEqual(envelope)
1380
+
1381
+ await Webhooks.deleteSubscription(db, apiKeyOwner, subscription.id)
1382
+ await expect(
1383
+ Webhooks.deliverClaimedAndRecord(db, {
1384
+ envelope: claim.record.envelope,
1385
+ subscriptionId: reference.subscriptionId,
1386
+ }),
1387
+ ).resolves.toEqual({ status: 'skipped' })
1388
+ })
1389
+ })
1390
+
775
1391
  describe('buildPingEnvelope', () => {
776
1392
  test('builds a synthetic ping envelope with a deterministic id from the nonce', async () => {
777
1393
  const db = TestApp.database()