@bquery/bquery 1.6.0 → 1.8.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 (402) hide show
  1. package/README.md +192 -18
  2. package/dist/a11y/announce.d.ts +43 -0
  3. package/dist/a11y/announce.d.ts.map +1 -0
  4. package/dist/a11y/audit.d.ts +42 -0
  5. package/dist/a11y/audit.d.ts.map +1 -0
  6. package/dist/a11y/index.d.ts +53 -0
  7. package/dist/a11y/index.d.ts.map +1 -0
  8. package/dist/a11y/media-preferences.d.ts +77 -0
  9. package/dist/a11y/media-preferences.d.ts.map +1 -0
  10. package/dist/a11y/roving-tab-index.d.ts +38 -0
  11. package/dist/a11y/roving-tab-index.d.ts.map +1 -0
  12. package/dist/a11y/skip-link.d.ts +37 -0
  13. package/dist/a11y/skip-link.d.ts.map +1 -0
  14. package/dist/a11y/trap-focus.d.ts +49 -0
  15. package/dist/a11y/trap-focus.d.ts.map +1 -0
  16. package/dist/a11y/types.d.ts +152 -0
  17. package/dist/a11y/types.d.ts.map +1 -0
  18. package/dist/a11y-DVBCy09c.js +421 -0
  19. package/dist/a11y-DVBCy09c.js.map +1 -0
  20. package/dist/a11y.es.mjs +14 -0
  21. package/dist/component/component.d.ts.map +1 -1
  22. package/dist/component/html.d.ts.map +1 -1
  23. package/dist/component/index.d.ts +2 -1
  24. package/dist/component/index.d.ts.map +1 -1
  25. package/dist/component/library.d.ts.map +1 -1
  26. package/dist/component/scope.d.ts +138 -0
  27. package/dist/component/scope.d.ts.map +1 -0
  28. package/dist/component/types.d.ts +53 -1
  29. package/dist/component/types.d.ts.map +1 -1
  30. package/dist/component-L3-JfOFz.js +684 -0
  31. package/dist/component-L3-JfOFz.js.map +1 -0
  32. package/dist/component.es.mjs +9 -6
  33. package/dist/{config-DRmZZno3.js → config-DhT9auRm.js} +4 -4
  34. package/dist/{config-DRmZZno3.js.map → config-DhT9auRm.js.map} +1 -1
  35. package/dist/constraints-D5RHQLmP.js +100 -0
  36. package/dist/constraints-D5RHQLmP.js.map +1 -0
  37. package/dist/core/collection.d.ts +134 -0
  38. package/dist/core/collection.d.ts.map +1 -1
  39. package/dist/core/element.d.ts +120 -0
  40. package/dist/core/element.d.ts.map +1 -1
  41. package/dist/core/env.d.ts +18 -0
  42. package/dist/core/env.d.ts.map +1 -0
  43. package/dist/core/index.d.ts +1 -0
  44. package/dist/core/index.d.ts.map +1 -1
  45. package/dist/core/shared.d.ts +14 -0
  46. package/dist/core/shared.d.ts.map +1 -1
  47. package/dist/core/utils/index.d.ts +52 -41
  48. package/dist/core/utils/index.d.ts.map +1 -1
  49. package/dist/core-DdtZHzsS.js +168 -0
  50. package/dist/core-DdtZHzsS.js.map +1 -0
  51. package/dist/{core-CCEabVHl.js → core-EMYSLzaT.js} +293 -194
  52. package/dist/core-EMYSLzaT.js.map +1 -0
  53. package/dist/core.es.mjs +48 -46
  54. package/dist/custom-directives-Dr4C5lVV.js +9 -0
  55. package/dist/custom-directives-Dr4C5lVV.js.map +1 -0
  56. package/dist/devtools/devtools.d.ts +212 -0
  57. package/dist/devtools/devtools.d.ts.map +1 -0
  58. package/dist/devtools/index.d.ts +20 -0
  59. package/dist/devtools/index.d.ts.map +1 -0
  60. package/dist/devtools/types.d.ts +69 -0
  61. package/dist/devtools/types.d.ts.map +1 -0
  62. package/dist/devtools-BhB2iDPT.js +122 -0
  63. package/dist/devtools-BhB2iDPT.js.map +1 -0
  64. package/dist/devtools.es.mjs +19 -0
  65. package/dist/dnd/draggable.d.ts +51 -0
  66. package/dist/dnd/draggable.d.ts.map +1 -0
  67. package/dist/dnd/droppable.d.ts +38 -0
  68. package/dist/dnd/droppable.d.ts.map +1 -0
  69. package/dist/dnd/index.d.ts +47 -0
  70. package/dist/dnd/index.d.ts.map +1 -0
  71. package/dist/dnd/sortable.d.ts +43 -0
  72. package/dist/dnd/sortable.d.ts.map +1 -0
  73. package/dist/dnd/types.d.ts +250 -0
  74. package/dist/dnd/types.d.ts.map +1 -0
  75. package/dist/dnd-NwZBYh4l.js +244 -0
  76. package/dist/dnd-NwZBYh4l.js.map +1 -0
  77. package/dist/dnd.es.mjs +6 -0
  78. package/dist/env-CTdvLaH2.js +19 -0
  79. package/dist/env-CTdvLaH2.js.map +1 -0
  80. package/dist/forms/create-form.d.ts +49 -0
  81. package/dist/forms/create-form.d.ts.map +1 -0
  82. package/dist/forms/index.d.ts +40 -0
  83. package/dist/forms/index.d.ts.map +1 -0
  84. package/dist/forms/types.d.ts +185 -0
  85. package/dist/forms/types.d.ts.map +1 -0
  86. package/dist/forms/use-field.d.ts +34 -0
  87. package/dist/forms/use-field.d.ts.map +1 -0
  88. package/dist/forms/validators.d.ts +204 -0
  89. package/dist/forms/validators.d.ts.map +1 -0
  90. package/dist/forms-UcRHsYxC.js +227 -0
  91. package/dist/forms-UcRHsYxC.js.map +1 -0
  92. package/dist/forms.es.mjs +16 -0
  93. package/dist/full.d.ts +30 -11
  94. package/dist/full.d.ts.map +1 -1
  95. package/dist/full.es.mjs +209 -93
  96. package/dist/full.iife.js +47 -31
  97. package/dist/full.iife.js.map +1 -1
  98. package/dist/full.umd.js +47 -31
  99. package/dist/full.umd.js.map +1 -1
  100. package/dist/function-Cybd57JV.js +33 -0
  101. package/dist/function-Cybd57JV.js.map +1 -0
  102. package/dist/i18n/formatting.d.ts +40 -0
  103. package/dist/i18n/formatting.d.ts.map +1 -0
  104. package/dist/i18n/i18n.d.ts +48 -0
  105. package/dist/i18n/i18n.d.ts.map +1 -0
  106. package/dist/i18n/index.d.ts +57 -0
  107. package/dist/i18n/index.d.ts.map +1 -0
  108. package/dist/i18n/translate.d.ts +83 -0
  109. package/dist/i18n/translate.d.ts.map +1 -0
  110. package/dist/i18n/types.d.ts +156 -0
  111. package/dist/i18n/types.d.ts.map +1 -0
  112. package/dist/i18n-kuF6Ekj6.js +89 -0
  113. package/dist/i18n-kuF6Ekj6.js.map +1 -0
  114. package/dist/i18n.es.mjs +6 -0
  115. package/dist/index.d.ts +11 -0
  116. package/dist/index.d.ts.map +1 -1
  117. package/dist/index.es.mjs +257 -143
  118. package/dist/media/battery.d.ts +35 -0
  119. package/dist/media/battery.d.ts.map +1 -0
  120. package/dist/media/breakpoints.d.ts +51 -0
  121. package/dist/media/breakpoints.d.ts.map +1 -0
  122. package/dist/media/clipboard.d.ts +30 -0
  123. package/dist/media/clipboard.d.ts.map +1 -0
  124. package/dist/media/device-sensors.d.ts +54 -0
  125. package/dist/media/device-sensors.d.ts.map +1 -0
  126. package/dist/media/geolocation.d.ts +38 -0
  127. package/dist/media/geolocation.d.ts.map +1 -0
  128. package/dist/media/index.d.ts +42 -0
  129. package/dist/media/index.d.ts.map +1 -0
  130. package/dist/media/media-query.d.ts +36 -0
  131. package/dist/media/media-query.d.ts.map +1 -0
  132. package/dist/media/network.d.ts +35 -0
  133. package/dist/media/network.d.ts.map +1 -0
  134. package/dist/media/types.d.ts +173 -0
  135. package/dist/media/types.d.ts.map +1 -0
  136. package/dist/media/viewport.d.ts +32 -0
  137. package/dist/media/viewport.d.ts.map +1 -0
  138. package/dist/media-i-fB5WxI.js +340 -0
  139. package/dist/media-i-fB5WxI.js.map +1 -0
  140. package/dist/media.es.mjs +12 -0
  141. package/dist/motion/index.d.ts +7 -3
  142. package/dist/motion/index.d.ts.map +1 -1
  143. package/dist/motion/morph.d.ts +27 -0
  144. package/dist/motion/morph.d.ts.map +1 -0
  145. package/dist/motion/parallax.d.ts +30 -0
  146. package/dist/motion/parallax.d.ts.map +1 -0
  147. package/dist/motion/reduced-motion.d.ts +36 -3
  148. package/dist/motion/reduced-motion.d.ts.map +1 -1
  149. package/dist/motion/types.d.ts +58 -0
  150. package/dist/motion/types.d.ts.map +1 -1
  151. package/dist/motion/typewriter.d.ts +31 -0
  152. package/dist/motion/typewriter.d.ts.map +1 -0
  153. package/dist/motion-BJsAuULb.js +530 -0
  154. package/dist/motion-BJsAuULb.js.map +1 -0
  155. package/dist/motion.es.mjs +27 -23
  156. package/dist/{view-C70lA3vf.js → mount-B4Y8bk8Z.js} +166 -160
  157. package/dist/mount-B4Y8bk8Z.js.map +1 -0
  158. package/dist/{object-qGpWr6-J.js → object-BCk-1c8T.js} +5 -4
  159. package/dist/{object-qGpWr6-J.js.map → object-BCk-1c8T.js.map} +1 -1
  160. package/dist/{platform-Dr9b6fsq.js → platform-Dw2gE3zI.js} +21 -22
  161. package/dist/{platform-Dr9b6fsq.js.map → platform-Dw2gE3zI.js.map} +1 -1
  162. package/dist/platform.es.mjs +2 -2
  163. package/dist/plugin/index.d.ts +22 -0
  164. package/dist/plugin/index.d.ts.map +1 -0
  165. package/dist/plugin/registry.d.ts +108 -0
  166. package/dist/plugin/registry.d.ts.map +1 -0
  167. package/dist/plugin/types.d.ts +110 -0
  168. package/dist/plugin/types.d.ts.map +1 -0
  169. package/dist/plugin-C2WuC8SF.js +66 -0
  170. package/dist/plugin-C2WuC8SF.js.map +1 -0
  171. package/dist/plugin.es.mjs +9 -0
  172. package/dist/reactive/async-data.d.ts +28 -3
  173. package/dist/reactive/async-data.d.ts.map +1 -1
  174. package/dist/reactive/computed.d.ts +10 -0
  175. package/dist/reactive/computed.d.ts.map +1 -1
  176. package/dist/reactive/effect.d.ts +3 -0
  177. package/dist/reactive/effect.d.ts.map +1 -1
  178. package/dist/reactive/http.d.ts +194 -0
  179. package/dist/reactive/http.d.ts.map +1 -0
  180. package/dist/reactive/index.d.ts +2 -2
  181. package/dist/reactive/index.d.ts.map +1 -1
  182. package/dist/reactive/pagination.d.ts +126 -0
  183. package/dist/reactive/pagination.d.ts.map +1 -0
  184. package/dist/reactive/polling.d.ts +55 -0
  185. package/dist/reactive/polling.d.ts.map +1 -0
  186. package/dist/reactive/readonly.d.ts +20 -1
  187. package/dist/reactive/readonly.d.ts.map +1 -1
  188. package/dist/reactive/rest.d.ts +293 -0
  189. package/dist/reactive/rest.d.ts.map +1 -0
  190. package/dist/reactive/scope.d.ts +140 -0
  191. package/dist/reactive/scope.d.ts.map +1 -0
  192. package/dist/reactive/signal.d.ts +16 -2
  193. package/dist/reactive/signal.d.ts.map +1 -1
  194. package/dist/reactive/to-value.d.ts +57 -0
  195. package/dist/reactive/to-value.d.ts.map +1 -0
  196. package/dist/reactive/websocket.d.ts +285 -0
  197. package/dist/reactive/websocket.d.ts.map +1 -0
  198. package/dist/reactive-DwkhUJfP.js +1148 -0
  199. package/dist/reactive-DwkhUJfP.js.map +1 -0
  200. package/dist/reactive.es.mjs +38 -20
  201. package/dist/registry-B08iilIh.js +26 -0
  202. package/dist/registry-B08iilIh.js.map +1 -0
  203. package/dist/router/bq-link.d.ts +112 -0
  204. package/dist/router/bq-link.d.ts.map +1 -0
  205. package/dist/router/constraints.d.ts +9 -0
  206. package/dist/router/constraints.d.ts.map +1 -0
  207. package/dist/router/index.d.ts +15 -7
  208. package/dist/router/index.d.ts.map +1 -1
  209. package/dist/router/match.d.ts +0 -1
  210. package/dist/router/match.d.ts.map +1 -1
  211. package/dist/router/path-pattern.d.ts +14 -0
  212. package/dist/router/path-pattern.d.ts.map +1 -0
  213. package/dist/router/query.d.ts.map +1 -1
  214. package/dist/router/router.d.ts +3 -1
  215. package/dist/router/router.d.ts.map +1 -1
  216. package/dist/router/state.d.ts +25 -2
  217. package/dist/router/state.d.ts.map +1 -1
  218. package/dist/router/types.d.ts +48 -4
  219. package/dist/router/types.d.ts.map +1 -1
  220. package/dist/router/use-route.d.ts +50 -0
  221. package/dist/router/use-route.d.ts.map +1 -0
  222. package/dist/router/utils.d.ts +3 -0
  223. package/dist/router/utils.d.ts.map +1 -1
  224. package/dist/router-CQikC9Ed.js +492 -0
  225. package/dist/router-CQikC9Ed.js.map +1 -0
  226. package/dist/router.es.mjs +14 -10
  227. package/dist/{sanitize-Bs2dkMby.js → sanitize-B1V4JswB.js} +2 -1
  228. package/dist/{sanitize-Bs2dkMby.js.map → sanitize-B1V4JswB.js.map} +1 -1
  229. package/dist/security/index.d.ts +2 -2
  230. package/dist/security/index.d.ts.map +1 -1
  231. package/dist/security.es.mjs +1 -1
  232. package/dist/ssr/hydrate.d.ts +65 -0
  233. package/dist/ssr/hydrate.d.ts.map +1 -0
  234. package/dist/ssr/index.d.ts +59 -0
  235. package/dist/ssr/index.d.ts.map +1 -0
  236. package/dist/ssr/render.d.ts +62 -0
  237. package/dist/ssr/render.d.ts.map +1 -0
  238. package/dist/ssr/serialize.d.ts +118 -0
  239. package/dist/ssr/serialize.d.ts.map +1 -0
  240. package/dist/ssr/types.d.ts +70 -0
  241. package/dist/ssr/types.d.ts.map +1 -0
  242. package/dist/ssr-_dAcGdzu.js +248 -0
  243. package/dist/ssr-_dAcGdzu.js.map +1 -0
  244. package/dist/ssr.es.mjs +9 -0
  245. package/dist/store/create-store.d.ts.map +1 -1
  246. package/dist/store/index.d.ts +1 -1
  247. package/dist/store/index.d.ts.map +1 -1
  248. package/dist/store/persisted.d.ts +38 -4
  249. package/dist/store/persisted.d.ts.map +1 -1
  250. package/dist/store/types.d.ts +138 -1
  251. package/dist/store/types.d.ts.map +1 -1
  252. package/dist/store/utils.d.ts +2 -2
  253. package/dist/store/utils.d.ts.map +1 -1
  254. package/dist/store-Cb3gPRve.js +338 -0
  255. package/dist/store-Cb3gPRve.js.map +1 -0
  256. package/dist/store.es.mjs +11 -10
  257. package/dist/storybook/index.d.ts.map +1 -1
  258. package/dist/storybook.es.mjs +1 -1
  259. package/dist/storybook.es.mjs.map +1 -1
  260. package/dist/testing/index.d.ts +23 -0
  261. package/dist/testing/index.d.ts.map +1 -0
  262. package/dist/testing/testing.d.ts +156 -0
  263. package/dist/testing/testing.d.ts.map +1 -0
  264. package/dist/testing/types.d.ts +134 -0
  265. package/dist/testing/types.d.ts.map +1 -0
  266. package/dist/testing-C5Sjfsna.js +224 -0
  267. package/dist/testing-C5Sjfsna.js.map +1 -0
  268. package/dist/testing.es.mjs +9 -0
  269. package/dist/type-guards-BMX2c0LP.js +44 -0
  270. package/dist/type-guards-BMX2c0LP.js.map +1 -0
  271. package/dist/untrack-D0fnO5k2.js +36 -0
  272. package/dist/untrack-D0fnO5k2.js.map +1 -0
  273. package/dist/view/custom-directives.d.ts +20 -0
  274. package/dist/view/custom-directives.d.ts.map +1 -0
  275. package/dist/view/evaluate.d.ts.map +1 -1
  276. package/dist/view/process.d.ts.map +1 -1
  277. package/dist/view.es.mjs +9 -9
  278. package/package.json +47 -11
  279. package/src/a11y/announce.ts +131 -0
  280. package/src/a11y/audit.ts +314 -0
  281. package/src/a11y/index.ts +68 -0
  282. package/src/a11y/media-preferences.ts +255 -0
  283. package/src/a11y/roving-tab-index.ts +164 -0
  284. package/src/a11y/skip-link.ts +255 -0
  285. package/src/a11y/trap-focus.ts +184 -0
  286. package/src/a11y/types.ts +183 -0
  287. package/src/component/component.ts +599 -524
  288. package/src/component/html.ts +153 -153
  289. package/src/component/index.ts +52 -50
  290. package/src/component/library.ts +540 -518
  291. package/src/component/scope.ts +212 -0
  292. package/src/component/types.ts +310 -256
  293. package/src/core/collection.ts +249 -1
  294. package/src/core/element.ts +252 -11
  295. package/src/core/env.ts +60 -0
  296. package/src/core/index.ts +1 -0
  297. package/src/core/shared.ts +64 -0
  298. package/src/core/utils/index.ts +66 -1
  299. package/src/devtools/devtools.ts +410 -0
  300. package/src/devtools/index.ts +48 -0
  301. package/src/devtools/types.ts +104 -0
  302. package/src/dnd/draggable.ts +296 -0
  303. package/src/dnd/droppable.ts +228 -0
  304. package/src/dnd/index.ts +62 -0
  305. package/src/dnd/sortable.ts +307 -0
  306. package/src/dnd/types.ts +293 -0
  307. package/src/forms/create-form.ts +320 -0
  308. package/src/forms/index.ts +70 -0
  309. package/src/forms/types.ts +203 -0
  310. package/src/forms/use-field.ts +231 -0
  311. package/src/forms/validators.ts +294 -0
  312. package/src/full.ts +554 -229
  313. package/src/i18n/formatting.ts +67 -0
  314. package/src/i18n/i18n.ts +200 -0
  315. package/src/i18n/index.ts +67 -0
  316. package/src/i18n/translate.ts +182 -0
  317. package/src/i18n/types.ts +171 -0
  318. package/src/index.ts +72 -0
  319. package/src/media/battery.ts +116 -0
  320. package/src/media/breakpoints.ts +129 -0
  321. package/src/media/clipboard.ts +80 -0
  322. package/src/media/device-sensors.ts +158 -0
  323. package/src/media/geolocation.ts +119 -0
  324. package/src/media/index.ts +76 -0
  325. package/src/media/media-query.ts +92 -0
  326. package/src/media/network.ts +115 -0
  327. package/src/media/types.ts +177 -0
  328. package/src/media/viewport.ts +84 -0
  329. package/src/motion/index.ts +11 -2
  330. package/src/motion/morph.ts +151 -0
  331. package/src/motion/parallax.ts +120 -0
  332. package/src/motion/reduced-motion.ts +52 -3
  333. package/src/motion/types.ts +63 -0
  334. package/src/motion/typewriter.ts +164 -0
  335. package/src/plugin/index.ts +37 -0
  336. package/src/plugin/registry.ts +284 -0
  337. package/src/plugin/types.ts +137 -0
  338. package/src/reactive/async-data.ts +250 -29
  339. package/src/reactive/computed.ts +53 -1
  340. package/src/reactive/effect.ts +29 -6
  341. package/src/reactive/http.ts +790 -0
  342. package/src/reactive/index.ts +60 -0
  343. package/src/reactive/pagination.ts +317 -0
  344. package/src/reactive/polling.ts +179 -0
  345. package/src/reactive/readonly.ts +52 -8
  346. package/src/reactive/rest.ts +859 -0
  347. package/src/reactive/scope.ts +276 -0
  348. package/src/reactive/signal.ts +61 -1
  349. package/src/reactive/to-value.ts +71 -0
  350. package/src/reactive/websocket.ts +849 -0
  351. package/src/router/bq-link.ts +279 -0
  352. package/src/router/constraints.ts +204 -0
  353. package/src/router/index.ts +15 -7
  354. package/src/router/match.ts +255 -49
  355. package/src/router/path-pattern.ts +52 -0
  356. package/src/router/query.ts +3 -0
  357. package/src/router/router.ts +258 -48
  358. package/src/router/state.ts +51 -3
  359. package/src/router/types.ts +50 -4
  360. package/src/router/use-route.ts +68 -0
  361. package/src/router/utils.ts +44 -3
  362. package/src/security/index.ts +12 -17
  363. package/src/security/sanitize.ts +70 -70
  364. package/src/security/trusted-html.ts +71 -71
  365. package/src/ssr/hydrate.ts +84 -0
  366. package/src/ssr/index.ts +70 -0
  367. package/src/ssr/render.ts +508 -0
  368. package/src/ssr/serialize.ts +296 -0
  369. package/src/ssr/types.ts +81 -0
  370. package/src/store/create-store.ts +146 -8
  371. package/src/store/define-store.ts +49 -49
  372. package/src/store/index.ts +5 -0
  373. package/src/store/mapping.ts +74 -74
  374. package/src/store/persisted.ts +245 -62
  375. package/src/store/types.ts +247 -92
  376. package/src/store/utils.ts +4 -10
  377. package/src/store/watch.ts +53 -53
  378. package/src/storybook/index.ts +480 -479
  379. package/src/testing/index.ts +42 -0
  380. package/src/testing/testing.ts +593 -0
  381. package/src/testing/types.ts +170 -0
  382. package/src/view/custom-directives.ts +28 -0
  383. package/src/view/evaluate.ts +2 -0
  384. package/src/view/process.ts +19 -3
  385. package/dist/component-BEQgt5hl.js +0 -600
  386. package/dist/component-BEQgt5hl.js.map +0 -1
  387. package/dist/core-BGQJVw0-.js +0 -35
  388. package/dist/core-BGQJVw0-.js.map +0 -1
  389. package/dist/core-CCEabVHl.js.map +0 -1
  390. package/dist/effect-AFRW_Plg.js +0 -84
  391. package/dist/effect-AFRW_Plg.js.map +0 -1
  392. package/dist/motion-D9TcHxOF.js +0 -415
  393. package/dist/motion-D9TcHxOF.js.map +0 -1
  394. package/dist/reactive-DSkct0dO.js +0 -254
  395. package/dist/reactive-DSkct0dO.js.map +0 -1
  396. package/dist/router-CbDhl8rS.js +0 -188
  397. package/dist/router-CbDhl8rS.js.map +0 -1
  398. package/dist/store-BwDvI45q.js +0 -263
  399. package/dist/store-BwDvI45q.js.map +0 -1
  400. package/dist/untrack-B0rVscTc.js +0 -7
  401. package/dist/untrack-B0rVscTc.js.map +0 -1
  402. package/dist/view-C70lA3vf.js.map +0 -1
@@ -0,0 +1,790 @@
1
+ /**
2
+ * Imperative HTTP client with Axios-like API, interceptors, retry, timeout,
3
+ * cancellation, and progress tracking — built on the native Fetch API.
4
+ *
5
+ * @module bquery/reactive
6
+ */
7
+
8
+ import { merge, isPlainObject } from '../core/utils/object';
9
+ import { getBqueryConfig, type BqueryFetchParseAs } from '../platform/config';
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Types
13
+ // ---------------------------------------------------------------------------
14
+
15
+ /** Configuration for automatic request retries. */
16
+ export interface RetryConfig {
17
+ /** Maximum number of retry attempts (default: 3). */
18
+ count: number;
19
+ /** Delay in ms between retries, or a function receiving the attempt index. */
20
+ delay?: number | ((attempt: number) => number);
21
+ /** Predicate deciding whether to retry a given error. Defaults to network / 5xx errors. */
22
+ retryOn?: (error: HttpError, attempt: number) => boolean;
23
+ /** Called before each retry attempt with the error and 1-indexed attempt number. */
24
+ onRetry?: (error: HttpError, attempt: number) => void;
25
+ }
26
+
27
+ /** Progress information emitted during upload or download. */
28
+ export interface HttpProgressEvent {
29
+ /** Bytes transferred so far. */
30
+ loaded: number;
31
+ /** Total bytes if known, otherwise 0. */
32
+ total: number;
33
+ /** Percentage between 0 and 100, or `undefined` when total is unknown. */
34
+ percent: number | undefined;
35
+ }
36
+
37
+ /** Full request configuration accepted by the HTTP client. */
38
+ export interface HttpRequestConfig extends Omit<RequestInit, 'body' | 'headers' | 'signal'> {
39
+ /** Request URL (resolved against `baseUrl`). */
40
+ url?: string;
41
+ /** Base URL prepended to relative request URLs. */
42
+ baseUrl?: string;
43
+ /** Request headers. */
44
+ headers?: HeadersInit;
45
+ /** Query parameters appended to the URL. */
46
+ query?: Record<string, unknown>;
47
+ /** Request body — plain objects/arrays are JSON-serialised automatically. */
48
+ body?: BodyInit | Record<string, unknown> | unknown[] | null;
49
+ /** Request timeout in milliseconds. 0 means no timeout (default). */
50
+ timeout?: number;
51
+ /** Response parsing strategy. */
52
+ parseAs?: BqueryFetchParseAs;
53
+ /** Custom status validation. Returns `true` for acceptable statuses. Default: `status >= 200 && status < 300`. */
54
+ validateStatus?: (status: number) => boolean;
55
+ /** Custom fetch implementation for testing or adapters. */
56
+ fetcher?: typeof fetch;
57
+ /** External `AbortSignal` for request cancellation. */
58
+ signal?: AbortSignal;
59
+ /** Retry configuration. Pass a number for simple retry count, or a `RetryConfig` object. */
60
+ retry?: number | RetryConfig;
61
+ /** Called repeatedly as response body chunks arrive. */
62
+ onDownloadProgress?: (event: HttpProgressEvent) => void;
63
+ }
64
+
65
+ /** Structured HTTP response returned by every client method. */
66
+ export interface HttpResponse<T = unknown> {
67
+ /** Parsed response data. */
68
+ data: T;
69
+ /** HTTP status code. */
70
+ status: number;
71
+ /** HTTP status text. */
72
+ statusText: string;
73
+ /** Response headers. */
74
+ headers: Headers;
75
+ /** Resolved request configuration used for this call. */
76
+ config: HttpRequestConfig;
77
+ }
78
+
79
+ /** Error subclass thrown on failed HTTP requests with rich metadata. */
80
+ export class HttpError extends Error {
81
+ /** HTTP response (available when the server replied). */
82
+ response?: HttpResponse;
83
+ /** Resolved request configuration. */
84
+ config: HttpRequestConfig;
85
+ /** Original error code string (e.g. `'TIMEOUT'`, `'ABORT'`, `'NETWORK'`). */
86
+ code: string;
87
+
88
+ constructor(message: string, config: HttpRequestConfig, code: string, response?: HttpResponse) {
89
+ super(message);
90
+ this.name = 'HttpError';
91
+ this.config = config;
92
+ this.code = code;
93
+ this.response = response;
94
+ }
95
+ }
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // Interceptors
99
+ // ---------------------------------------------------------------------------
100
+
101
+ /** Single interceptor handler pair. */
102
+ export interface Interceptor<T> {
103
+ fulfilled?: (value: T) => T | Promise<T>;
104
+ rejected?: (error: unknown) => unknown;
105
+ }
106
+
107
+ /** Manager for adding and removing interceptors. */
108
+ export interface InterceptorManager<T> {
109
+ /** Register an interceptor. Returns a numeric id for later removal via `eject()`. */
110
+ use(fulfilled?: (value: T) => T | Promise<T>, rejected?: (error: unknown) => unknown): number;
111
+ /** Remove a previously registered interceptor by id. */
112
+ eject(id: number): void;
113
+ /** Remove all interceptors. */
114
+ clear(): void;
115
+ }
116
+
117
+ /** @internal */
118
+ interface InterceptorEntry<T> {
119
+ id: number;
120
+ fulfilled?: (value: T) => T | Promise<T>;
121
+ rejected?: (error: unknown) => unknown;
122
+ }
123
+
124
+ /** @internal */
125
+ function createInterceptorManager<T>(): InterceptorManager<T> & {
126
+ /** @internal */ forEach(fn: (entry: InterceptorEntry<T>) => void): void;
127
+ } {
128
+ const entries: Array<InterceptorEntry<T> | null> = [];
129
+ let nextId = 0;
130
+
131
+ return {
132
+ use(fulfilled, rejected) {
133
+ const id = nextId++;
134
+ entries.push({ id, fulfilled, rejected });
135
+ return id;
136
+ },
137
+ eject(id) {
138
+ const index = entries.findIndex((e) => e?.id === id);
139
+ if (index !== -1) entries[index] = null;
140
+ },
141
+ clear() {
142
+ entries.length = 0;
143
+ },
144
+ forEach(fn) {
145
+ for (const entry of entries) {
146
+ if (entry) fn(entry);
147
+ }
148
+ },
149
+ };
150
+ }
151
+
152
+ // ---------------------------------------------------------------------------
153
+ // HttpClient interface
154
+ // ---------------------------------------------------------------------------
155
+
156
+ /** Imperative HTTP client with interceptors and convenience method shortcuts. */
157
+ export interface HttpClient {
158
+ /** Send a request using the provided configuration. */
159
+ request<T = unknown>(config: HttpRequestConfig): Promise<HttpResponse<T>>;
160
+ /** Send a GET request. */
161
+ get<T = unknown>(url: string, config?: HttpRequestConfig): Promise<HttpResponse<T>>;
162
+ /** Send a POST request. */
163
+ post<T = unknown>(
164
+ url: string,
165
+ body?: HttpRequestConfig['body'],
166
+ config?: HttpRequestConfig
167
+ ): Promise<HttpResponse<T>>;
168
+ /** Send a PUT request. */
169
+ put<T = unknown>(
170
+ url: string,
171
+ body?: HttpRequestConfig['body'],
172
+ config?: HttpRequestConfig
173
+ ): Promise<HttpResponse<T>>;
174
+ /** Send a PATCH request. */
175
+ patch<T = unknown>(
176
+ url: string,
177
+ body?: HttpRequestConfig['body'],
178
+ config?: HttpRequestConfig
179
+ ): Promise<HttpResponse<T>>;
180
+ /** Send a DELETE request. */
181
+ delete<T = unknown>(url: string, config?: HttpRequestConfig): Promise<HttpResponse<T>>;
182
+ /** Send a HEAD request. */
183
+ head<T = unknown>(url: string, config?: HttpRequestConfig): Promise<HttpResponse<T>>;
184
+ /** Send an OPTIONS request. */
185
+ options<T = unknown>(url: string, config?: HttpRequestConfig): Promise<HttpResponse<T>>;
186
+ /** Request and response interceptors. */
187
+ interceptors: {
188
+ request: InterceptorManager<HttpRequestConfig>;
189
+ response: InterceptorManager<HttpResponse>;
190
+ };
191
+ /** The merged default configuration used by this client. */
192
+ defaults: HttpRequestConfig;
193
+ }
194
+
195
+ // ---------------------------------------------------------------------------
196
+ // Internal helpers
197
+ // ---------------------------------------------------------------------------
198
+
199
+ const DEFAULT_VALIDATE_STATUS = (status: number): boolean => status >= 200 && status < 300;
200
+
201
+ const DEFAULT_RETRY_ON = (error: HttpError): boolean => {
202
+ if (error.code === 'PARSE') return false;
203
+ if (error.code === 'TIMEOUT' || error.code === 'NETWORK') return true;
204
+ const status = error.response?.status;
205
+ return status !== undefined && status >= 500;
206
+ };
207
+
208
+ /** @internal */
209
+ const normalizeRetry = (retry: HttpRequestConfig['retry']): RetryConfig | undefined => {
210
+ if (retry == null) return undefined;
211
+ if (typeof retry === 'number') return { count: retry };
212
+ return retry;
213
+ };
214
+
215
+ /** @internal */
216
+ const resolveRetryDelay = (delay: RetryConfig['delay'], attempt: number): number => {
217
+ if (delay == null) return Math.min(1000 * 2 ** attempt, 30_000);
218
+ if (typeof delay === 'number') return delay;
219
+ return delay(attempt);
220
+ };
221
+
222
+ /** @internal */
223
+ const sleep = (ms: number, signal?: AbortSignal): Promise<void> =>
224
+ new Promise<void>((resolve, reject) => {
225
+ if (signal?.aborted) {
226
+ reject(signal.reason ?? new DOMException('The operation was aborted.', 'AbortError'));
227
+ return;
228
+ }
229
+ let timer: ReturnType<typeof setTimeout>;
230
+ const onAbort = (): void => {
231
+ clearTimeout(timer);
232
+ signal?.removeEventListener('abort', onAbort);
233
+ reject(signal?.reason ?? new DOMException('The operation was aborted.', 'AbortError'));
234
+ };
235
+ timer = setTimeout(() => {
236
+ signal?.removeEventListener('abort', onAbort);
237
+ resolve();
238
+ }, ms);
239
+ if (signal) {
240
+ signal.addEventListener('abort', onAbort, { once: true });
241
+ }
242
+ });
243
+
244
+ /** @internal */
245
+ const toHeaders = (...sources: Array<HeadersInit | undefined>): Headers => {
246
+ const headers = new Headers();
247
+ for (const source of sources) {
248
+ if (!source) continue;
249
+ new Headers(source).forEach((value, key) => {
250
+ headers.set(key, value);
251
+ });
252
+ }
253
+ return headers;
254
+ };
255
+
256
+ /** @internal */
257
+ const isBodyLike = (value: unknown): value is BodyInit => {
258
+ if (typeof value === 'string') return true;
259
+ if (value instanceof Blob || value instanceof FormData || value instanceof URLSearchParams) {
260
+ return true;
261
+ }
262
+ if (typeof ArrayBuffer !== 'undefined' && value instanceof ArrayBuffer) return true;
263
+ if (typeof ReadableStream !== 'undefined' && value instanceof ReadableStream) return true;
264
+ return typeof value === 'object' && value !== null && ArrayBuffer.isView(value);
265
+ };
266
+
267
+ /** @internal */
268
+ const serializeBody = (
269
+ body: HttpRequestConfig['body'],
270
+ headers: Headers
271
+ ): BodyInit | null | undefined => {
272
+ if (body == null) return body;
273
+ if (isBodyLike(body)) return body;
274
+ if (!headers.has('content-type')) {
275
+ headers.set('content-type', 'application/json');
276
+ }
277
+ return JSON.stringify(body);
278
+ };
279
+
280
+ /** @internal */
281
+ const appendQuery = (url: URL, query: Record<string, unknown>): void => {
282
+ for (const [key, value] of Object.entries(query)) {
283
+ if (value == null) continue;
284
+ if (Array.isArray(value)) {
285
+ for (const item of value) {
286
+ if (item != null) url.searchParams.append(key, String(item));
287
+ }
288
+ continue;
289
+ }
290
+ url.searchParams.set(key, String(value));
291
+ }
292
+ };
293
+
294
+ /** @internal */
295
+ const buildUrl = (url: string, baseUrl?: string): URL => {
296
+ const runtimeBase =
297
+ typeof window !== 'undefined' && /^https?:/i.test(window.location.href)
298
+ ? window.location.href
299
+ : 'http://localhost';
300
+ const base = baseUrl ? new URL(baseUrl, runtimeBase).toString() : runtimeBase;
301
+ return new URL(url, base);
302
+ };
303
+
304
+ /** @internal */
305
+ const parseResponseBody = async <T>(
306
+ response: Response,
307
+ parseAs: BqueryFetchParseAs,
308
+ config: HttpRequestConfig
309
+ ): Promise<T> => {
310
+ if (parseAs === 'response') return response as T;
311
+ if (parseAs === 'text') return (await response.text()) as T;
312
+ if (parseAs === 'blob') return (await response.blob()) as T;
313
+ if (parseAs === 'arrayBuffer') return (await response.arrayBuffer()) as T;
314
+ if (parseAs === 'formData') return (await response.formData()) as T;
315
+
316
+ const text = await response.text();
317
+ if (!text) return undefined as T;
318
+
319
+ try {
320
+ return JSON.parse(text) as T;
321
+ } catch (parseError) {
322
+ const detail = response.url ? ` for ${response.url}` : '';
323
+ throw new HttpError(
324
+ `Failed to parse JSON response${detail} (status ${response.status}): ${parseError instanceof Error ? parseError.message : String(parseError)}`,
325
+ config,
326
+ 'PARSE',
327
+ {
328
+ data: text,
329
+ status: response.status,
330
+ statusText: response.statusText,
331
+ headers: response.headers,
332
+ config,
333
+ }
334
+ );
335
+ }
336
+ };
337
+
338
+ /** @internal – wrap a response body stream to report download progress. */
339
+ const wrapDownloadStream = (
340
+ response: Response,
341
+ onProgress: (event: HttpProgressEvent) => void
342
+ ): Response => {
343
+ const body = response.body;
344
+ if (!body) return response;
345
+
346
+ const total = parseInt(response.headers.get('content-length') ?? '0', 10) || 0;
347
+ let loaded = 0;
348
+
349
+ const reader = body.getReader();
350
+ const stream = new ReadableStream({
351
+ async pull(controller) {
352
+ const { done, value } = await reader.read();
353
+ if (done) {
354
+ controller.close();
355
+ return;
356
+ }
357
+ loaded += value.byteLength;
358
+ onProgress({
359
+ loaded,
360
+ total,
361
+ percent: total > 0 ? Math.round((loaded / total) * 100) : undefined,
362
+ });
363
+ controller.enqueue(value);
364
+ },
365
+ cancel(reason) {
366
+ reader.cancel(reason);
367
+ },
368
+ });
369
+
370
+ return new Response(stream, {
371
+ status: response.status,
372
+ statusText: response.statusText,
373
+ headers: response.headers,
374
+ });
375
+ };
376
+
377
+ // ---------------------------------------------------------------------------
378
+ // Core request execution
379
+ // ---------------------------------------------------------------------------
380
+
381
+ /** @internal Execute a single HTTP request (no retry/interceptor logic). */
382
+ const executeRequest = async <T>(config: HttpRequestConfig): Promise<HttpResponse<T>> => {
383
+ const fetchConfig = getBqueryConfig().fetch;
384
+ const parseAs = config.parseAs ?? fetchConfig?.parseAs ?? 'json';
385
+ const fetcher = config.fetcher ?? fetch;
386
+ const validateStatus = config.validateStatus ?? DEFAULT_VALIDATE_STATUS;
387
+
388
+ const urlString = config.url ?? '/';
389
+ const url = buildUrl(urlString, config.baseUrl ?? fetchConfig?.baseUrl);
390
+
391
+ if (config.query) {
392
+ appendQuery(url, config.query);
393
+ }
394
+
395
+ const headers = toHeaders(fetchConfig?.headers, config.headers);
396
+ const serializedBody = serializeBody(config.body, headers);
397
+
398
+ // Build RequestInit, omitting non-standard keys
399
+ const requestInit: RequestInit = {};
400
+ if (config.method) requestInit.method = config.method.toUpperCase();
401
+ requestInit.headers = headers;
402
+ if (serializedBody != null) requestInit.body = serializedBody;
403
+ if (config.cache) requestInit.cache = config.cache;
404
+ if (config.credentials) requestInit.credentials = config.credentials;
405
+ if (config.integrity) requestInit.integrity = config.integrity;
406
+ if (config.keepalive !== undefined) requestInit.keepalive = config.keepalive;
407
+ if (config.mode) requestInit.mode = config.mode;
408
+ if (config.redirect) requestInit.redirect = config.redirect;
409
+ if (config.referrer) requestInit.referrer = config.referrer;
410
+ if (config.referrerPolicy) requestInit.referrerPolicy = config.referrerPolicy;
411
+
412
+ // Abort / timeout
413
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
414
+ let mergedSignal: AbortSignal | undefined = config.signal;
415
+ let externalAbortHandler: (() => void) | undefined;
416
+
417
+ if (config.timeout && config.timeout > 0) {
418
+ const controller = new AbortController();
419
+
420
+ if (config.signal) {
421
+ // Compose: abort when *either* the external signal or the timeout fires
422
+ externalAbortHandler = () => controller.abort(config.signal?.reason);
423
+ config.signal.addEventListener('abort', externalAbortHandler, { once: true });
424
+ }
425
+
426
+ timeoutId = setTimeout(() => {
427
+ controller.abort(new DOMException('Request timeout', 'TimeoutError'));
428
+ }, config.timeout);
429
+
430
+ mergedSignal = controller.signal;
431
+ }
432
+
433
+ if (mergedSignal) requestInit.signal = mergedSignal;
434
+
435
+ try {
436
+ let response = await fetcher(url.toString(), requestInit);
437
+
438
+ if (config.onDownloadProgress) {
439
+ response = wrapDownloadStream(response, config.onDownloadProgress);
440
+ }
441
+
442
+ if (!validateStatus(response.status)) {
443
+ throw new HttpError(
444
+ `Request failed with status ${response.status}`,
445
+ config,
446
+ 'ERR_BAD_RESPONSE',
447
+ {
448
+ data: undefined,
449
+ status: response.status,
450
+ statusText: response.statusText,
451
+ headers: response.headers,
452
+ config,
453
+ }
454
+ );
455
+ }
456
+
457
+ const data = await parseResponseBody<T>(response, parseAs, config);
458
+
459
+ const httpResponse: HttpResponse<T> = {
460
+ data,
461
+ status: response.status,
462
+ statusText: response.statusText,
463
+ headers: response.headers,
464
+ config,
465
+ };
466
+
467
+ return httpResponse;
468
+ } catch (error) {
469
+ if (error instanceof HttpError) throw error;
470
+
471
+ if (error instanceof DOMException) {
472
+ if (error.name === 'AbortError' || error.name === 'TimeoutError') {
473
+ const isTimeout = error.name === 'TimeoutError' || error.message === 'Request timeout';
474
+ throw new HttpError(
475
+ isTimeout ? `Request timeout of ${config.timeout}ms exceeded` : 'Request aborted',
476
+ config,
477
+ isTimeout ? 'TIMEOUT' : 'ABORT'
478
+ );
479
+ }
480
+ }
481
+
482
+ throw new HttpError(error instanceof Error ? error.message : String(error), config, 'NETWORK');
483
+ } finally {
484
+ if (timeoutId !== undefined) clearTimeout(timeoutId);
485
+ if (config.signal && externalAbortHandler) {
486
+ config.signal.removeEventListener('abort', externalAbortHandler);
487
+ }
488
+ }
489
+ };
490
+
491
+ // ---------------------------------------------------------------------------
492
+ // Factory
493
+ // ---------------------------------------------------------------------------
494
+
495
+ /**
496
+ * Create a preconfigured HTTP client instance with interceptors.
497
+ *
498
+ * @param defaults - Default request configuration merged into every request
499
+ * @returns An `HttpClient` with `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()`, `.options()`
500
+ *
501
+ * @example
502
+ * ```ts
503
+ * import { createHttp } from '@bquery/bquery/reactive';
504
+ *
505
+ * const api = createHttp({
506
+ * baseUrl: 'https://api.example.com',
507
+ * headers: { authorization: 'Bearer token' },
508
+ * timeout: 10_000,
509
+ * });
510
+ *
511
+ * api.interceptors.request.use((config) => {
512
+ * config.headers = { ...Object.fromEntries(new Headers(config.headers)), 'x-req-id': crypto.randomUUID() };
513
+ * return config;
514
+ * });
515
+ *
516
+ * const { data } = await api.get<User[]>('/users');
517
+ * ```
518
+ */
519
+ export function createHttp(defaults: HttpRequestConfig = {}): HttpClient {
520
+ const requestInterceptors = createInterceptorManager<HttpRequestConfig>();
521
+ const responseInterceptors = createInterceptorManager<HttpResponse>();
522
+
523
+ const mergeConfig = (perCall: HttpRequestConfig): HttpRequestConfig => {
524
+ const mergedQuery = merge({}, defaults.query ?? {}, perCall.query ?? {}) as Record<
525
+ string,
526
+ unknown
527
+ >;
528
+
529
+ return {
530
+ ...defaults,
531
+ ...perCall,
532
+ headers: toHeaders(defaults.headers, perCall.headers),
533
+ query: Object.keys(mergedQuery).length > 0 ? mergedQuery : undefined,
534
+ };
535
+ };
536
+
537
+ const dispatchRequest = async <T>(config: HttpRequestConfig): Promise<HttpResponse<T>> => {
538
+ // Run request interceptors
539
+ let resolvedConfig = config;
540
+ const requestChain: Array<InterceptorEntry<HttpRequestConfig>> = [];
541
+ requestInterceptors.forEach((entry) => requestChain.push(entry));
542
+
543
+ for (const { fulfilled, rejected } of requestChain) {
544
+ try {
545
+ if (fulfilled) {
546
+ resolvedConfig = await fulfilled(resolvedConfig);
547
+ }
548
+ } catch (err) {
549
+ if (rejected) {
550
+ const result = await rejected(err);
551
+ if (isPlainObject(result)) {
552
+ resolvedConfig = result as unknown as HttpRequestConfig;
553
+ } else {
554
+ throw err;
555
+ }
556
+ } else {
557
+ throw err;
558
+ }
559
+ }
560
+ }
561
+
562
+ // Execute with retry
563
+ const retryConfig = normalizeRetry(resolvedConfig.retry);
564
+ let lastError: HttpError | undefined;
565
+
566
+ const maxAttempts = (retryConfig?.count ?? 0) + 1;
567
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
568
+ try {
569
+ let response = await executeRequest<T>(resolvedConfig);
570
+
571
+ // Run response interceptors
572
+ const responseChain: Array<InterceptorEntry<HttpResponse>> = [];
573
+ responseInterceptors.forEach((entry) => responseChain.push(entry));
574
+
575
+ for (const { fulfilled, rejected } of responseChain) {
576
+ try {
577
+ if (fulfilled) {
578
+ response = (await fulfilled(response as HttpResponse)) as HttpResponse<T>;
579
+ }
580
+ } catch (err) {
581
+ if (rejected) {
582
+ const result = await rejected(err);
583
+ if (result && typeof result === 'object' && 'data' in result) {
584
+ response = result as HttpResponse<T>;
585
+ } else {
586
+ throw err;
587
+ }
588
+ } else {
589
+ throw err;
590
+ }
591
+ }
592
+ }
593
+
594
+ return response;
595
+ } catch (error) {
596
+ const httpError =
597
+ error instanceof HttpError
598
+ ? error
599
+ : new HttpError(
600
+ error instanceof Error ? error.message : String(error),
601
+ resolvedConfig,
602
+ 'NETWORK'
603
+ );
604
+
605
+ lastError = httpError;
606
+
607
+ const shouldRetry = retryConfig
608
+ ? (retryConfig.retryOn ?? DEFAULT_RETRY_ON)(httpError, attempt)
609
+ : false;
610
+
611
+ if (!shouldRetry || attempt >= maxAttempts - 1) {
612
+ // Run response error interceptors before throwing
613
+ const responseChain: Array<InterceptorEntry<HttpResponse>> = [];
614
+ responseInterceptors.forEach((entry) => responseChain.push(entry));
615
+
616
+ let finalError: unknown = httpError;
617
+ for (const { rejected } of responseChain) {
618
+ if (rejected) {
619
+ try {
620
+ const result = await rejected(finalError);
621
+ if (result && typeof result === 'object' && 'data' in result) {
622
+ return result as HttpResponse<T>;
623
+ }
624
+ if (result != null) {
625
+ finalError = result;
626
+ }
627
+ } catch (innerErr) {
628
+ if (innerErr != null) {
629
+ finalError = innerErr;
630
+ }
631
+ }
632
+ }
633
+ }
634
+
635
+ if (!(finalError instanceof Error)) {
636
+ finalError = httpError;
637
+ }
638
+
639
+ throw finalError;
640
+ }
641
+
642
+ const retryDelay = retryConfig ? resolveRetryDelay(retryConfig.delay, attempt) : 0;
643
+ retryConfig?.onRetry?.(httpError, attempt + 1);
644
+ await sleep(retryDelay, resolvedConfig.signal);
645
+ }
646
+ }
647
+
648
+ throw lastError!;
649
+ };
650
+
651
+ const request = <T>(config: HttpRequestConfig): Promise<HttpResponse<T>> =>
652
+ dispatchRequest<T>(mergeConfig(config));
653
+
654
+ const client: HttpClient = {
655
+ request,
656
+ get: <T>(url: string, config: HttpRequestConfig = {}) =>
657
+ request<T>({ ...config, url, method: 'GET' }),
658
+ post: <T>(url: string, body?: HttpRequestConfig['body'], config: HttpRequestConfig = {}) =>
659
+ request<T>({ ...config, url, method: 'POST', body }),
660
+ put: <T>(url: string, body?: HttpRequestConfig['body'], config: HttpRequestConfig = {}) =>
661
+ request<T>({ ...config, url, method: 'PUT', body }),
662
+ patch: <T>(url: string, body?: HttpRequestConfig['body'], config: HttpRequestConfig = {}) =>
663
+ request<T>({ ...config, url, method: 'PATCH', body }),
664
+ delete: <T>(url: string, config: HttpRequestConfig = {}) =>
665
+ request<T>({ ...config, url, method: 'DELETE' }),
666
+ head: <T>(url: string, config: HttpRequestConfig = {}) =>
667
+ request<T>({ ...config, url, method: 'HEAD' }),
668
+ options: <T>(url: string, config: HttpRequestConfig = {}) =>
669
+ request<T>({ ...config, url, method: 'OPTIONS' }),
670
+ interceptors: {
671
+ request: requestInterceptors,
672
+ response: responseInterceptors,
673
+ },
674
+ defaults,
675
+ };
676
+
677
+ return client;
678
+ }
679
+
680
+ /**
681
+ * Default HTTP client instance using global bQuery fetch config.
682
+ *
683
+ * @example
684
+ * ```ts
685
+ * import { http } from '@bquery/bquery/reactive';
686
+ *
687
+ * const { data } = await http.get<User[]>('/api/users');
688
+ * const { data: created } = await http.post('/api/users', { name: 'Ada' });
689
+ * ```
690
+ */
691
+ export const http: HttpClient = createHttp();
692
+
693
+ // ---------------------------------------------------------------------------
694
+ // Request Queue
695
+ // ---------------------------------------------------------------------------
696
+
697
+ /** Options for `createRequestQueue()`. */
698
+ export interface RequestQueueOptions {
699
+ /** Maximum number of concurrent in-flight requests (default: 6). */
700
+ concurrency?: number;
701
+ }
702
+
703
+ /** A queued request entry. */
704
+ interface QueueEntry<T = unknown> {
705
+ execute: () => Promise<HttpResponse<T>>;
706
+ resolve: (value: HttpResponse<T>) => void;
707
+ reject: (reason: unknown) => void;
708
+ }
709
+
710
+ /** Return value of `createRequestQueue()`. */
711
+ export interface RequestQueue {
712
+ /** Enqueue a request. Returns a promise that resolves when the request completes. */
713
+ add: <T = unknown>(execute: () => Promise<HttpResponse<T>>) => Promise<HttpResponse<T>>;
714
+ /** Number of requests currently being processed. */
715
+ readonly pending: number;
716
+ /** Number of requests waiting in the queue. */
717
+ readonly size: number;
718
+ /** Remove all pending (not yet started) requests from the queue. Their promises will reject. */
719
+ clear: () => void;
720
+ }
721
+
722
+ /**
723
+ * Create a request queue with a concurrency limit.
724
+ *
725
+ * Useful for rate-limiting parallel HTTP requests (e.g. browser connection limits,
726
+ * API throttling) while maintaining a simple promise-based interface.
727
+ *
728
+ * @param options - Queue configuration
729
+ * @returns A `RequestQueue` with `.add()`, `.clear()`, `.pending`, and `.size`
730
+ *
731
+ * @example
732
+ * ```ts
733
+ * import { createRequestQueue, createHttp } from '@bquery/bquery/reactive';
734
+ *
735
+ * const api = createHttp({ baseUrl: 'https://api.example.com' });
736
+ * const queue = createRequestQueue({ concurrency: 3 });
737
+ *
738
+ * // These will run at most 3 at a time
739
+ * const results = await Promise.all(
740
+ * ids.map(id => queue.add(() => api.get(`/items/${id}`)))
741
+ * );
742
+ * ```
743
+ */
744
+ export function createRequestQueue(options: RequestQueueOptions = {}): RequestQueue {
745
+ const { concurrency = 6 } = options;
746
+ if (!Number.isInteger(concurrency) || concurrency < 1) {
747
+ throw new Error('Request queue concurrency must be a positive integer');
748
+ }
749
+ const queue: Array<QueueEntry> = [];
750
+ let running = 0;
751
+
752
+ const drain = (): void => {
753
+ while (running < concurrency && queue.length > 0) {
754
+ const entry = queue.shift()!;
755
+ running++;
756
+ Promise.resolve()
757
+ .then(entry.execute)
758
+ .then(entry.resolve, entry.reject)
759
+ .finally(() => {
760
+ running--;
761
+ drain();
762
+ });
763
+ }
764
+ };
765
+
766
+ return {
767
+ add<T = unknown>(execute: () => Promise<HttpResponse<T>>): Promise<HttpResponse<T>> {
768
+ return new Promise<HttpResponse<T>>((resolve, reject) => {
769
+ queue.push({
770
+ execute: execute as () => Promise<HttpResponse>,
771
+ resolve: resolve as (value: HttpResponse) => void,
772
+ reject,
773
+ });
774
+ drain();
775
+ });
776
+ },
777
+ get pending() {
778
+ return running;
779
+ },
780
+ get size() {
781
+ return queue.length;
782
+ },
783
+ clear() {
784
+ const cleared = queue.splice(0);
785
+ for (const entry of cleared) {
786
+ entry.reject(new Error('Request queue cleared'));
787
+ }
788
+ },
789
+ };
790
+ }