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
@@ -0,0 +1,285 @@
1
+ import { type ExpressionBuilder, type Generated, type Insertable, type Selectable, sql } from 'kysely' // prettier-ignore
2
+
3
+ import type * as Webhooks from '../../internal/Webhooks.js'
4
+ import type * as Db from '../Db.js'
5
+ import type * as db_Schema from '../Schema.js'
6
+
7
+ /** Columns of the `webhook_queue_events` table, derived from `Schema.WebhookQueueEvent`. */
8
+ export type Table = Omit<
9
+ db_Schema.WebhookQueueEvent,
10
+ 'attemptCount' | 'attemptingAt' | 'nextAttemptAt' | 'status'
11
+ > & {
12
+ /** Delivery attempts claimed so far; database default 0. */
13
+ attemptCount: Generated<number>
14
+ /** ISO timestamp of the current claim; database default null. */
15
+ attemptingAt: Generated<string | null>
16
+ /** ISO retry due time; database default null (due immediately). */
17
+ nextAttemptAt: Generated<string | null>
18
+ /** Delivery-obligation lifecycle status; database default `pending`. */
19
+ status: Generated<db_Schema.WebhookQueueEvent['status']>
20
+ }
21
+
22
+ /** A stored webhook Queue event. */
23
+ export type Record = Selectable<Table>
24
+
25
+ /** A terminal delivery-obligation outcome. */
26
+ export type TerminalStatus = Exclude<db_Schema.WebhookQueueEvent['status'], 'pending'>
27
+
28
+ /**
29
+ * Reads one pending envelope. Terminal rows read as absent so no consumer
30
+ * can redeliver a completed obligation or disturb its dedupe row.
31
+ */
32
+ export async function get(
33
+ db: Db.Db,
34
+ reference: Webhooks.QueueReference,
35
+ ): Promise<Webhooks.QueueEvent | null> {
36
+ return (
37
+ (await db.kysely
38
+ .selectFrom('webhook_queue_events')
39
+ .selectAll()
40
+ .where('eventId', '=', reference.eventId)
41
+ .where('status', '=', 'pending')
42
+ .where('subscriptionId', '=', reference.subscriptionId)
43
+ .executeTakeFirst()) ?? null
44
+ )
45
+ }
46
+
47
+ /**
48
+ * Stages envelopes as fresh obligations. Pending conflicts keep their first
49
+ * envelope and backoff position; terminal conflicts inside the dedupe window
50
+ * suppress the replay; terminal conflicts past it are replaced, so an expired
51
+ * dedupe row pruning has not yet reclaimed never blocks a replay.
52
+ */
53
+ export async function ensure(db: Db.Db, rows: readonly Insertable<Table>[]): Promise<number> {
54
+ if (rows.length === 0) return 0
55
+ const result = await db.kysely
56
+ .insertInto('webhook_queue_events')
57
+ .values([...rows])
58
+ .onConflict((oc) =>
59
+ oc
60
+ .columns(['subscriptionId', 'eventId'])
61
+ .doUpdateSet((eb) => ({
62
+ attemptCount: 0,
63
+ attemptingAt: null,
64
+ createdAt: eb.ref('excluded.createdAt'),
65
+ envelope: eb.ref('excluded.envelope'),
66
+ expiresAt: eb.ref('excluded.expiresAt'),
67
+ nextAttemptAt: eb.ref('excluded.nextAttemptAt'),
68
+ status: 'pending',
69
+ }))
70
+ .where((eb) =>
71
+ eb.and([
72
+ eb('webhook_queue_events.status', '!=', 'pending'),
73
+ eb('webhook_queue_events.expiresAt', '<=', eb.ref('excluded.createdAt')),
74
+ ]),
75
+ ),
76
+ )
77
+ .executeTakeFirst()
78
+ // The command tag counts inserted rows plus replaced expired rows; live
79
+ // conflicts fall out of the conditional update and are not counted.
80
+ return Number(result.numInsertedOrUpdatedRows ?? 0n)
81
+ }
82
+
83
+ /** Due, unclaimed pending work: shared by {@link claim} and {@link due}. */
84
+ function whereClaimable(
85
+ eb: ExpressionBuilder<Db.Database, 'webhook_queue_events'>,
86
+ options: { now: string; staleBefore: string },
87
+ ) {
88
+ return eb.and([
89
+ eb('status', '=', 'pending'),
90
+ eb.or([eb('nextAttemptAt', 'is', null), eb('nextAttemptAt', '<=', options.now)]),
91
+ eb.or([eb('attemptingAt', 'is', null), eb('attemptingAt', '<=', options.staleBefore)]),
92
+ ])
93
+ }
94
+
95
+ /** Claims one due pending obligation with a single atomic update. */
96
+ export async function claim(
97
+ db: Db.Db,
98
+ reference: Webhooks.QueueReference,
99
+ options: claim.Options,
100
+ ): Promise<claim.Result> {
101
+ const claimed = await db.kysely
102
+ .updateTable('webhook_queue_events')
103
+ .set((eb) => ({ attemptCount: eb('attemptCount', '+', 1), attemptingAt: options.now }))
104
+ .where('subscriptionId', '=', reference.subscriptionId)
105
+ .where('eventId', '=', reference.eventId)
106
+ .where((eb) => whereClaimable(eb, options))
107
+ .returningAll()
108
+ .executeTakeFirst()
109
+ if (claimed) return { record: claimed, type: 'claimed' }
110
+
111
+ const existing = await db.kysely
112
+ .selectFrom('webhook_queue_events')
113
+ .select(['attemptingAt', 'nextAttemptAt', 'status'])
114
+ .where('subscriptionId', '=', reference.subscriptionId)
115
+ .where('eventId', '=', reference.eventId)
116
+ .executeTakeFirst()
117
+ if (!existing) return { type: 'missing' }
118
+ if (existing.status !== 'pending') return { status: existing.status, type: 'terminal' }
119
+ if (existing.attemptingAt !== null && existing.attemptingAt > options.staleBefore)
120
+ return { type: 'attempting' }
121
+ return { nextAttemptAt: existing.nextAttemptAt, type: 'scheduled' }
122
+ }
123
+
124
+ export declare namespace claim {
125
+ /** Claim clock bounds. */
126
+ type Options = {
127
+ /** Current ISO timestamp; becomes the claim marker. */
128
+ now: string
129
+ /** ISO cutoff below which an existing claim counts as stale. */
130
+ staleBefore: string
131
+ }
132
+
133
+ /** Atomic claim outcome. */
134
+ type Result =
135
+ | { type: 'attempting' }
136
+ | { record: Record; type: 'claimed' }
137
+ | { type: 'missing' }
138
+ | { nextAttemptAt: string | null; type: 'scheduled' }
139
+ | { status: TerminalStatus; type: 'terminal' }
140
+ }
141
+
142
+ /**
143
+ * Marks a claimed obligation terminal, starting its dedupe retention window;
144
+ * pending rows never expire, so this stamp is the only expiry that matters.
145
+ * The `attemptingAt` predicate fences stale claimants.
146
+ */
147
+ export async function complete(
148
+ db: Db.Db,
149
+ reference: Webhooks.QueueReference,
150
+ options: complete.Options,
151
+ ): Promise<boolean> {
152
+ const result = await db.kysely
153
+ .updateTable('webhook_queue_events')
154
+ .set({ attemptingAt: null, expiresAt: options.expiresAt, status: options.status })
155
+ .where('subscriptionId', '=', reference.subscriptionId)
156
+ .where('eventId', '=', reference.eventId)
157
+ .where('status', '=', 'pending')
158
+ .where('attemptingAt', '=', options.claimedAt)
159
+ .executeTakeFirst()
160
+ return result.numUpdatedRows === 1n
161
+ }
162
+
163
+ export declare namespace complete {
164
+ /** Fenced terminal transition. */
165
+ type Options = {
166
+ /** Claim marker returned by {@link claim}; fences stale claimants. */
167
+ claimedAt: string
168
+ /** Replacement ISO retention deadline for the terminal dedupe row. */
169
+ expiresAt: string
170
+ /** Terminal outcome. */
171
+ status: TerminalStatus
172
+ }
173
+ }
174
+
175
+ /** Releases a claim and schedules the next attempt; fenced like {@link complete}. */
176
+ export async function schedule(
177
+ db: Db.Db,
178
+ reference: Webhooks.QueueReference,
179
+ options: schedule.Options,
180
+ ): Promise<boolean> {
181
+ const result = await db.kysely
182
+ .updateTable('webhook_queue_events')
183
+ .set({ attemptingAt: null, nextAttemptAt: options.nextAttemptAt })
184
+ .where('subscriptionId', '=', reference.subscriptionId)
185
+ .where('eventId', '=', reference.eventId)
186
+ .where('status', '=', 'pending')
187
+ .where('attemptingAt', '=', options.claimedAt)
188
+ .executeTakeFirst()
189
+ return result.numUpdatedRows === 1n
190
+ }
191
+
192
+ export declare namespace schedule {
193
+ /** Fenced retry scheduling. */
194
+ type Options = {
195
+ /** Claim marker returned by {@link claim}; fences stale claimants. */
196
+ claimedAt: string
197
+ /** ISO due time of the next attempt. */
198
+ nextAttemptAt: string
199
+ }
200
+ }
201
+
202
+ /** Lists due pending references for the sweeper, most overdue first. */
203
+ export async function due(db: Db.Db, options: due.Options): Promise<Webhooks.QueueReference[]> {
204
+ return (
205
+ db.kysely
206
+ .selectFrom('webhook_queue_events')
207
+ .select(['eventId', 'subscriptionId'])
208
+ .where((eb) => whereClaimable(eb, options))
209
+ // Matches the pending partial index order so the scan needs no sort.
210
+ .orderBy(sql`next_attempt_at nulls first`)
211
+ .limit(options.limit)
212
+ .execute()
213
+ )
214
+ }
215
+
216
+ export declare namespace due {
217
+ /** Sweep bounds. */
218
+ type Options = {
219
+ /** Maximum references to return. */
220
+ limit: number
221
+ /** Current ISO timestamp. */
222
+ now: string
223
+ /** ISO cutoff below which an existing claim counts as stale. */
224
+ staleBefore: string
225
+ }
226
+ }
227
+
228
+ /** Returns the pending-obligation snapshot, split by due-and-unclaimed work. */
229
+ export async function pending(db: Db.Db, options: pending.Options): Promise<pending.Result> {
230
+ const row = await db.kysely
231
+ .selectFrom('webhook_queue_events')
232
+ .select((eb) => [
233
+ eb.fn.countAll<number>().as('count'),
234
+ eb.fn.min('createdAt').as('oldestAt'),
235
+ eb.fn
236
+ .countAll<number>()
237
+ .filterWhere((fb) => whereClaimable(fb, options))
238
+ .as('dueCount'),
239
+ eb.fn
240
+ // Null next attempts (migration-backfilled rows) are due immediately;
241
+ // coalesce to staging time so they age instead of vanishing from MIN.
242
+ .min(eb.fn.coalesce('nextAttemptAt', 'createdAt'))
243
+ .filterWhere((fb) => whereClaimable(fb, options))
244
+ .as('oldestDueAt'),
245
+ ])
246
+ .where('status', '=', 'pending')
247
+ .executeTakeFirstOrThrow()
248
+ return {
249
+ count: Number(row.count),
250
+ dueCount: Number(row.dueCount),
251
+ oldestAt: row.oldestAt,
252
+ oldestDueAt: row.oldestDueAt,
253
+ }
254
+ }
255
+
256
+ export declare namespace pending {
257
+ /** Snapshot clock bounds; the read-only subset of {@link claim.Options}. */
258
+ type Options = Pick<claim.Options, 'now' | 'staleBefore'>
259
+
260
+ /** Pending-work snapshot. */
261
+ type Result = {
262
+ /** Pending obligations, including in-flight claims and scheduled retries. */
263
+ count: number
264
+ /** Obligations due now with no live claim; the sweeper's backlog. */
265
+ dueCount: number
266
+ /** Oldest pending staging timestamp, or null when none. */
267
+ oldestAt: string | null
268
+ /** Oldest due-and-unclaimed retry time, or null when none; drives the stall alert. */
269
+ oldestDueAt: string | null
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Deletes terminal dedupe rows past their retention deadline. Pending rows
275
+ * never expire: owed work drains only through terminal transitions
276
+ * (delivery, permanent failure, or the skip after auto-disable).
277
+ */
278
+ export async function pruneExpired(db: Db.Db, now: string): Promise<number> {
279
+ const result = await db.kysely
280
+ .deleteFrom('webhook_queue_events')
281
+ .where('expiresAt', '<=', now)
282
+ .where('status', '!=', 'pending')
283
+ .executeTakeFirst()
284
+ return Number(result.numDeletedRows)
285
+ }
@@ -13,6 +13,21 @@ export type Table = db_Schema.WebhookSubscription
13
13
  /** A stored webhook-subscription row. */
14
14
  export type Record = Selectable<Table>
15
15
 
16
+ /** Reads one live subscription by id for an internal Queue delivery. */
17
+ export async function getById(
18
+ db: Db.Db,
19
+ id: string,
20
+ now: string,
21
+ ): Promise<Webhooks.Subscription | null> {
22
+ const row = await db.kysely
23
+ .selectFrom('webhook_subscriptions')
24
+ .selectAll()
25
+ .where('id', '=', id)
26
+ .where(live(now))
27
+ .executeTakeFirst()
28
+ return row ? toSubscription(row) : null
29
+ }
30
+
16
31
  /** Maps an owner union to its stored `ownerId` column value. */
17
32
  export function ownerId(owner: Webhooks.Owner): string {
18
33
  return owner.type === 'api_key' ? owner.orgId : owner.payer
@@ -273,20 +288,21 @@ export async function remove(
273
288
 
274
289
  /**
275
290
  * Lists live `active` subscriptions for a `(chainId, eventType)` stream — the
276
- * poller's per-stream lookup, served by the partial status index.
291
+ * poller's per-stream lookup, served by the partial status index. Each row
292
+ * carries its poller cursor so the poller avoids a per-subscription read.
277
293
  *
278
294
  * @param db - The database.
279
295
  * @param chainId - Chain the stream belongs to.
280
296
  * @param eventType - Event type of the stream.
281
297
  * @param now - ISO timestamp expiry is evaluated against.
282
- * @returns The subscriptions, in id (chronological) order.
298
+ * @returns Cursor-and-subscription pairs, in id (chronological) order.
283
299
  */
284
300
  export async function listActive(
285
301
  db: Db.Db,
286
302
  chainId: number,
287
303
  eventType: Webhooks.EventType,
288
304
  now: string,
289
- ): Promise<Webhooks.Subscription[]> {
305
+ ): Promise<{ cursor: string | null; subscription: Webhooks.Subscription }[]> {
290
306
  const rows = await db.kysely
291
307
  .selectFrom('webhook_subscriptions')
292
308
  .selectAll()
@@ -296,7 +312,69 @@ export async function listActive(
296
312
  .where(live(now))
297
313
  .orderBy('id')
298
314
  .execute()
299
- return rows.map(toSubscription)
315
+ return rows.map((row) => ({ cursor: row.pollerCursor, subscription: toSubscription(row) }))
316
+ }
317
+
318
+ /**
319
+ * Lists every live `active` subscription on a chain in one read. The poller
320
+ * groups the rows by event type so empty streams cost no extra database reads.
321
+ *
322
+ * @param db - The database.
323
+ * @param options - Chain, configured event types, and expiry time.
324
+ * @returns Cursor-and-subscription pairs ordered by event type, then id.
325
+ */
326
+ export async function listActiveForChain(
327
+ db: Db.Db,
328
+ options: listActiveForChain.Options,
329
+ ): Promise<{ cursor: string | null; subscription: Webhooks.Subscription }[]> {
330
+ if (options.eventTypes.length === 0) return []
331
+ const rows = await db.kysely
332
+ .selectFrom('webhook_subscriptions')
333
+ .selectAll()
334
+ .where('chainId', '=', options.chainId)
335
+ .where('eventType', 'in', options.eventTypes)
336
+ .where('status', '=', 'active')
337
+ .where(live(options.now))
338
+ .orderBy('eventType')
339
+ .orderBy('id')
340
+ .execute()
341
+ return rows.map((row) => ({ cursor: row.pollerCursor, subscription: toSubscription(row) }))
342
+ }
343
+
344
+ export declare namespace listActiveForChain {
345
+ /** Filters for the poller's chain-level subscription read. */
346
+ type Options = {
347
+ /** Chain whose subscriptions to read. */
348
+ chainId: number
349
+ /** Event types configured for this poller tick. */
350
+ eventTypes: readonly Webhooks.EventType[]
351
+ /** ISO timestamp expiry is evaluated against. */
352
+ now: string
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Reads the poller cursors of every live `active` subscription on a chain,
358
+ * across event types. Feeds the cron's cursor-freshness fallback gate.
359
+ *
360
+ * @param db - The database.
361
+ * @param chainId - Chain whose cursors to read.
362
+ * @param now - ISO timestamp expiry is evaluated against.
363
+ * @returns The non-null cursors, unordered.
364
+ */
365
+ export async function listActiveCursors(
366
+ db: Db.Db,
367
+ chainId: number,
368
+ now: string,
369
+ ): Promise<string[]> {
370
+ const rows = await db.kysely
371
+ .selectFrom('webhook_subscriptions')
372
+ .select('pollerCursor')
373
+ .where('chainId', '=', chainId)
374
+ .where('status', '=', 'active')
375
+ .where(live(now))
376
+ .execute()
377
+ return rows.flatMap((row) => (row.pollerCursor === null ? [] : [row.pollerCursor]))
300
378
  }
301
379
 
302
380
  /**
@@ -367,6 +445,55 @@ export declare namespace compareAndSetCursor {
367
445
  }
368
446
  }
369
447
 
448
+ /**
449
+ * Writes multiple poller cursors when each stored value still matches the
450
+ * caller's observed value.
451
+ *
452
+ * @param db - The database.
453
+ * @param options - Candidate cursor writes.
454
+ * @returns Subscription ids whose cursors were updated.
455
+ */
456
+ export async function compareAndSetCursors(
457
+ db: Db.Db,
458
+ options: compareAndSetCursors.Options,
459
+ ): Promise<Set<string>> {
460
+ if (options.cursors.length === 0) return new Set()
461
+ const values = JSON.stringify(options.cursors)
462
+ type Candidate = {
463
+ cursor: string
464
+ expectedCursor: string | null
465
+ subscriptionId: string
466
+ }
467
+ const result = await db.kysely
468
+ .with(
469
+ 'candidates',
470
+ () => sql<Candidate>`(
471
+ SELECT
472
+ candidate."subscriptionId" AS subscription_id,
473
+ candidate."expectedCursor" AS expected_cursor,
474
+ candidate.cursor
475
+ FROM jsonb_to_recordset(${values}::jsonb)
476
+ AS candidate("subscriptionId" text, "expectedCursor" text, cursor text)
477
+ )`,
478
+ )
479
+ .updateTable('webhook_subscriptions as subscription')
480
+ .from('candidates as candidate')
481
+ .set((eb) => ({ pollerCursor: eb.ref('candidate.cursor') }))
482
+ .whereRef('subscription.id', '=', 'candidate.subscriptionId')
483
+ .whereRef('subscription.pollerCursor', 'is not distinct from', 'candidate.expectedCursor')
484
+ .returning('subscription.id as subscriptionId')
485
+ .execute()
486
+ return new Set(result.map((row) => row.subscriptionId))
487
+ }
488
+
489
+ export declare namespace compareAndSetCursors {
490
+ /** Options for {@link compareAndSetCursors}. */
491
+ type Options = {
492
+ /** Candidate cursor writes. */
493
+ cursors: readonly compareAndSetCursor.Options[]
494
+ }
495
+ }
496
+
370
497
  /**
371
498
  * Records a successful delivery: resets the consecutive-failure counter and
372
499
  * stamps `lastDeliveryAt`.