@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,859 @@
1
+ /**
2
+ * REST resource composable for CRUD operations with optimistic updates,
3
+ * form submission, and reactive caching built on the bQuery fetch layer.
4
+ *
5
+ * @module bquery/reactive
6
+ */
7
+
8
+ import { computed } from './computed';
9
+ import { Signal, signal } from './core';
10
+ import { useFetch, type AsyncDataStatus, type UseFetchOptions } from './async-data';
11
+ import { createHttp, type HttpClient, type HttpRequestConfig, type HttpResponse } from './http';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // useResource — full CRUD composable
15
+ // ---------------------------------------------------------------------------
16
+
17
+ /** HTTP method shortcuts available on a resource. */
18
+ export interface ResourceActions<T> {
19
+ /** Fetch the resource (GET). */
20
+ fetch: () => Promise<T | undefined>;
21
+ /** Create a new item (POST). */
22
+ create: (body: Partial<T> | Record<string, unknown>) => Promise<T | undefined>;
23
+ /** Replace the resource (PUT). */
24
+ update: (body: Partial<T> | Record<string, unknown>) => Promise<T | undefined>;
25
+ /** Partially update the resource (PATCH). */
26
+ patch: (body: Partial<T> | Record<string, unknown>) => Promise<T | undefined>;
27
+ /** Delete the resource (DELETE). */
28
+ remove: () => Promise<void>;
29
+ }
30
+
31
+ /** Options for `useResource()`. */
32
+ export interface UseResourceOptions<T = unknown> extends Omit<
33
+ UseFetchOptions<T>,
34
+ 'method' | 'body'
35
+ > {
36
+ /** Enable optimistic updates for mutating operations (default: false). */
37
+ optimistic?: boolean;
38
+ /** Called after any successful mutation (create / update / patch / remove). */
39
+ onMutationSuccess?: (data: T | undefined, action: string) => void;
40
+ /** Called after a failed mutation, receives the error and action name. */
41
+ onMutationError?: (error: Error, action: string) => void;
42
+ }
43
+
44
+ /** Return value of `useResource()`. */
45
+ export interface UseResourceReturn<T> {
46
+ /** Reactive resource data. */
47
+ data: Signal<T | undefined>;
48
+ /** Last error. */
49
+ error: Signal<Error | null>;
50
+ /** Lifecycle status for the initial fetch. */
51
+ status: Signal<AsyncDataStatus>;
52
+ /** Whether the initial fetch is pending. */
53
+ pending: { readonly value: boolean; peek(): boolean };
54
+ /** Whether any mutation is in progress. */
55
+ isMutating: { readonly value: boolean; peek(): boolean };
56
+ /** CRUD actions. */
57
+ actions: ResourceActions<T>;
58
+ /** Refresh the resource (re-GET). */
59
+ refresh: () => Promise<T | undefined>;
60
+ /** Clear data, error, and status. */
61
+ clear: () => void;
62
+ /** Dispose all reactive state and prevent future operations. */
63
+ dispose: () => void;
64
+ }
65
+
66
+ /**
67
+ * Reactive REST resource composable providing CRUD operations.
68
+ *
69
+ * Binds a base URL to a resource and exposes `fetch`, `create`, `update`,
70
+ * `patch`, and `remove` helpers with optional optimistic updates.
71
+ *
72
+ * @template T - Resource data type
73
+ * @param url - Resource endpoint URL or getter
74
+ * @param options - Fetch and resource options
75
+ * @returns Reactive resource state with CRUD actions
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * import { useResource } from '@bquery/bquery/reactive';
80
+ *
81
+ * const user = useResource<User>('/api/users/1', {
82
+ * baseUrl: 'https://api.example.com',
83
+ * optimistic: true,
84
+ * });
85
+ *
86
+ * // Read
87
+ * await user.actions.fetch();
88
+ *
89
+ * // Update
90
+ * await user.actions.patch({ name: 'Ada' });
91
+ *
92
+ * // Delete
93
+ * await user.actions.remove();
94
+ * ```
95
+ */
96
+ export const useResource = <T = unknown>(
97
+ url: string | URL | (() => string | URL),
98
+ options: UseResourceOptions<T> = {}
99
+ ): UseResourceReturn<T> => {
100
+ const { optimistic = false, onMutationSuccess, onMutationError, ...fetchOptions } = options;
101
+
102
+ // Internal fetch state for the GET
103
+ const fetchState = useFetch<T>(url, {
104
+ ...fetchOptions,
105
+ });
106
+
107
+ const mutating = signal(false);
108
+ const isMutating = computed(() => mutating.value);
109
+
110
+ let disposed = false;
111
+
112
+ const resolveUrl = (): string => {
113
+ const resolved = typeof url === 'function' ? url() : url;
114
+ return resolved instanceof URL ? resolved.toString() : resolved;
115
+ };
116
+
117
+ const stripGetLifecycleOptions = <TResult>(): Omit<
118
+ UseFetchOptions<TResult>,
119
+ 'method' | 'body' | 'defaultValue' | 'transform' | 'onSuccess' | 'onError'
120
+ > => {
121
+ const {
122
+ defaultValue: _defaultValue,
123
+ transform: _transform,
124
+ onSuccess: _onSuccess,
125
+ onError: _onError,
126
+ ...remainingOpts
127
+ } = fetchOptions;
128
+ return remainingOpts as Omit<
129
+ UseFetchOptions<TResult>,
130
+ 'method' | 'body' | 'defaultValue' | 'transform' | 'onSuccess' | 'onError'
131
+ >;
132
+ };
133
+
134
+ const executeMutation = async (
135
+ action: string,
136
+ method: string,
137
+ body?: Partial<T> | Record<string, unknown>,
138
+ optimisticData?: T | undefined
139
+ ): Promise<T | undefined> => {
140
+ if (disposed) return fetchState.data.peek();
141
+
142
+ const previousData = fetchState.data.peek();
143
+
144
+ // Optimistic update
145
+ if (optimistic && optimisticData !== undefined) {
146
+ fetchState.data.value = optimisticData;
147
+ }
148
+
149
+ mutating.value = true;
150
+ fetchState.error.value = null;
151
+
152
+ try {
153
+ const mutationState = useFetch<T>(resolveUrl(), {
154
+ ...stripGetLifecycleOptions<T>(),
155
+ method,
156
+ body: body ?? undefined,
157
+ immediate: false,
158
+ watch: undefined,
159
+ });
160
+
161
+ const result = await mutationState.execute();
162
+ const mutationError = mutationState.error.peek();
163
+ mutationState.dispose();
164
+
165
+ if (disposed) return fetchState.data.peek();
166
+
167
+ // Check if the inner fetch encountered an error
168
+ if (mutationError) {
169
+ // Rollback on optimistic failure
170
+ if (optimistic && optimisticData !== undefined) {
171
+ fetchState.data.value = previousData;
172
+ }
173
+
174
+ fetchState.error.value = mutationError;
175
+ fetchState.status.value = 'error';
176
+ mutating.value = false;
177
+ onMutationError?.(mutationError, action);
178
+ return fetchState.data.peek();
179
+ }
180
+
181
+ // For non-DELETE mutations, update data with server response
182
+ if (method !== 'DELETE' && result !== undefined) {
183
+ fetchState.data.value = result;
184
+ }
185
+
186
+ mutating.value = false;
187
+ fetchState.status.value = 'success';
188
+ onMutationSuccess?.(result, action);
189
+ return result;
190
+ } catch (caught) {
191
+ if (disposed) return fetchState.data.peek();
192
+
193
+ // Rollback on optimistic failure
194
+ if (optimistic && optimisticData !== undefined) {
195
+ fetchState.data.value = previousData;
196
+ }
197
+
198
+ const normalizedError = caught instanceof Error ? caught : new Error(String(caught));
199
+ fetchState.error.value = normalizedError;
200
+ fetchState.status.value = 'error';
201
+ mutating.value = false;
202
+ onMutationError?.(normalizedError, action);
203
+ return fetchState.data.peek();
204
+ }
205
+ };
206
+
207
+ const actions: ResourceActions<T> = {
208
+ fetch: () => fetchState.execute(),
209
+ create: (body) => executeMutation('create', 'POST', body),
210
+ update: (body) => {
211
+ const base = fetchState.data.peek();
212
+ return executeMutation(
213
+ 'update',
214
+ 'PUT',
215
+ body,
216
+ optimistic && base !== undefined ? ({ ...base, ...body } as T) : undefined
217
+ );
218
+ },
219
+ patch: (body) => {
220
+ const base = fetchState.data.peek();
221
+ return executeMutation(
222
+ 'patch',
223
+ 'PATCH',
224
+ body,
225
+ optimistic && base !== undefined ? ({ ...base, ...body } as T) : undefined
226
+ );
227
+ },
228
+ remove: async () => {
229
+ await executeMutation('remove', 'DELETE');
230
+ if (!disposed && fetchState.error.peek() == null) {
231
+ fetchState.data.value = undefined;
232
+ }
233
+ },
234
+ };
235
+
236
+ const originalDispose = fetchState.dispose;
237
+ const dispose = (): void => {
238
+ if (disposed) return;
239
+ disposed = true;
240
+ originalDispose();
241
+ };
242
+
243
+ return {
244
+ data: fetchState.data,
245
+ error: fetchState.error,
246
+ status: fetchState.status,
247
+ pending: fetchState.pending,
248
+ isMutating,
249
+ actions,
250
+ refresh: fetchState.execute,
251
+ clear: fetchState.clear,
252
+ dispose,
253
+ };
254
+ };
255
+
256
+ // ---------------------------------------------------------------------------
257
+ // useSubmit — form submission composable
258
+ // ---------------------------------------------------------------------------
259
+
260
+ /** Options for `useSubmit()`. */
261
+ export interface UseSubmitOptions<TResponse = unknown> extends Omit<
262
+ UseFetchOptions<TResponse>,
263
+ 'body' | 'immediate'
264
+ > {
265
+ /** HTTP method (default: `'POST'`). */
266
+ method?: string;
267
+ }
268
+
269
+ /** Return value of `useSubmit()`. */
270
+ export interface UseSubmitReturn<TResponse = unknown> {
271
+ /** Last response data. */
272
+ data: Signal<TResponse | undefined>;
273
+ /** Last error. */
274
+ error: Signal<Error | null>;
275
+ /** Current status. */
276
+ status: Signal<AsyncDataStatus>;
277
+ /** Whether the submission is pending. */
278
+ pending: { readonly value: boolean; peek(): boolean };
279
+ /** Submit data to the endpoint. */
280
+ submit: (body: Record<string, unknown> | FormData | BodyInit) => Promise<TResponse | undefined>;
281
+ /** Reset state. */
282
+ clear: () => void;
283
+ }
284
+
285
+ /**
286
+ * Reactive form submission composable.
287
+ *
288
+ * Provides a `submit()` function that sends data to an endpoint with
289
+ * reactive status, data, and error signals.
290
+ *
291
+ * @template TResponse - Response data type
292
+ * @param url - Submission endpoint URL
293
+ * @param options - Fetch options (method defaults to POST)
294
+ * @returns Reactive submission state with `submit()` and `clear()`
295
+ *
296
+ * @example
297
+ * ```ts
298
+ * import { useSubmit } from '@bquery/bquery/reactive';
299
+ *
300
+ * const form = useSubmit<{ id: number }>('/api/users', {
301
+ * baseUrl: 'https://api.example.com',
302
+ * headers: { 'x-csrf': token },
303
+ * });
304
+ *
305
+ * const result = await form.submit({ name: 'Ada', email: 'ada@example.com' });
306
+ * console.log(form.status.value); // 'success'
307
+ * ```
308
+ */
309
+ export const useSubmit = <TResponse = unknown>(
310
+ url: string | URL,
311
+ options: UseSubmitOptions<TResponse> = {}
312
+ ): UseSubmitReturn<TResponse> => {
313
+ const { method = 'POST', ...fetchOptions } = options;
314
+
315
+ const data = signal<TResponse | undefined>(undefined);
316
+ const error = signal<Error | null>(null);
317
+ const status = signal<AsyncDataStatus>('idle');
318
+ const pending = computed(() => status.value === 'pending');
319
+
320
+ const submit = async (
321
+ body: Record<string, unknown> | FormData | BodyInit
322
+ ): Promise<TResponse | undefined> => {
323
+ status.value = 'pending';
324
+ error.value = null;
325
+
326
+ try {
327
+ const state = useFetch<TResponse>(url, {
328
+ ...fetchOptions,
329
+ method,
330
+ body: body as UseFetchOptions['body'],
331
+ immediate: false,
332
+ watch: undefined,
333
+ });
334
+
335
+ const result = await state.execute();
336
+ const fetchError = state.error.peek();
337
+ state.dispose();
338
+
339
+ if (fetchError) {
340
+ error.value = fetchError;
341
+ status.value = 'error';
342
+ return undefined;
343
+ }
344
+
345
+ data.value = result;
346
+ status.value = 'success';
347
+ return result;
348
+ } catch (caught) {
349
+ const normalizedError = caught instanceof Error ? caught : new Error(String(caught));
350
+ error.value = normalizedError;
351
+ status.value = 'error';
352
+ return undefined;
353
+ }
354
+ };
355
+
356
+ const clear = (): void => {
357
+ data.value = undefined;
358
+ error.value = null;
359
+ status.value = 'idle';
360
+ };
361
+
362
+ return {
363
+ data,
364
+ error,
365
+ status,
366
+ pending,
367
+ submit,
368
+ clear,
369
+ };
370
+ };
371
+
372
+ // ---------------------------------------------------------------------------
373
+ // createRestClient — imperative REST client
374
+ // ---------------------------------------------------------------------------
375
+
376
+ /** Typed CRUD methods for a REST endpoint. */
377
+ export interface RestClient<T = unknown> {
378
+ /** GET all items. */
379
+ list: (config?: HttpRequestConfig) => Promise<HttpResponse<T[]>>;
380
+ /** GET a single item by ID. */
381
+ get: (id: string | number, config?: HttpRequestConfig) => Promise<HttpResponse<T>>;
382
+ /** POST a new item. */
383
+ create: (
384
+ body: Partial<T> | Record<string, unknown>,
385
+ config?: HttpRequestConfig
386
+ ) => Promise<HttpResponse<T>>;
387
+ /** PUT (full replace) an item by ID. */
388
+ update: (
389
+ id: string | number,
390
+ body: Partial<T> | Record<string, unknown>,
391
+ config?: HttpRequestConfig
392
+ ) => Promise<HttpResponse<T>>;
393
+ /** PATCH (partial update) an item by ID. */
394
+ patch: (
395
+ id: string | number,
396
+ body: Partial<T> | Record<string, unknown>,
397
+ config?: HttpRequestConfig
398
+ ) => Promise<HttpResponse<T>>;
399
+ /** DELETE an item by ID. */
400
+ remove: (id: string | number, config?: HttpRequestConfig) => Promise<HttpResponse<void>>;
401
+ /** The underlying HttpClient instance. */
402
+ http: HttpClient;
403
+ }
404
+
405
+ /**
406
+ * Create a typed REST client for a specific API resource.
407
+ *
408
+ * Wraps `createHttp()` and maps standard CRUD operations to their
409
+ * conventional REST endpoints (`GET /`, `GET /:id`, `POST /`, `PUT /:id`,
410
+ * `PATCH /:id`, `DELETE /:id`).
411
+ *
412
+ * @template T - Resource item type
413
+ * @param baseUrl - Base URL of the resource (e.g. `https://api.example.com/users`)
414
+ * @param defaults - Default request configuration merged into every call
415
+ * @returns Typed REST client with `list`, `get`, `create`, `update`, `patch`, `remove`
416
+ *
417
+ * @example
418
+ * ```ts
419
+ * import { createRestClient } from '@bquery/bquery/reactive';
420
+ *
421
+ * interface User { id: number; name: string; email: string }
422
+ *
423
+ * const users = createRestClient<User>('https://api.example.com/users', {
424
+ * headers: { authorization: '******' },
425
+ * timeout: 10_000,
426
+ * });
427
+ *
428
+ * const { data: allUsers } = await users.list();
429
+ * const { data: user } = await users.get(1);
430
+ * const { data: created } = await users.create({ name: 'Ada' });
431
+ * await users.update(1, { name: 'Ada', email: 'ada@example.com' });
432
+ * await users.patch(1, { email: 'new@example.com' });
433
+ * await users.remove(1);
434
+ * ```
435
+ */
436
+ export const createRestClient = <T = unknown>(
437
+ baseUrl: string,
438
+ defaults: HttpRequestConfig = {}
439
+ ): RestClient<T> => {
440
+ const httpClient = createHttp({ ...defaults });
441
+
442
+ // Ensure the base URL ends without a trailing slash for consistent joining
443
+ let base = baseUrl;
444
+ while (base.endsWith('/')) base = base.slice(0, -1);
445
+
446
+ return {
447
+ list: (config) => httpClient.get<T[]>(base, config),
448
+ get: (id, config) => httpClient.get<T>(`${base}/${encodeURIComponent(String(id))}`, config),
449
+ create: (body, config) => httpClient.post<T>(base, body as HttpRequestConfig['body'], config),
450
+ update: (id, body, config) =>
451
+ httpClient.put<T>(
452
+ `${base}/${encodeURIComponent(String(id))}`,
453
+ body as HttpRequestConfig['body'],
454
+ config
455
+ ),
456
+ patch: (id, body, config) =>
457
+ httpClient.patch<T>(
458
+ `${base}/${encodeURIComponent(String(id))}`,
459
+ body as HttpRequestConfig['body'],
460
+ config
461
+ ),
462
+ remove: (id, config) =>
463
+ httpClient.delete<void>(`${base}/${encodeURIComponent(String(id))}`, config),
464
+ http: httpClient,
465
+ };
466
+ };
467
+
468
+ // ---------------------------------------------------------------------------
469
+ // useResourceList — reactive collection CRUD
470
+ // ---------------------------------------------------------------------------
471
+
472
+ /** Extract a unique identifier from an item. */
473
+ export type IdExtractor<T> = (item: T) => string | number;
474
+
475
+ /** Options for `useResourceList()`. */
476
+ export interface UseResourceListOptions<T = unknown> extends Omit<
477
+ UseFetchOptions<T[]>,
478
+ 'method' | 'body'
479
+ > {
480
+ /** Extract the unique ID from each item (default: `item.id`). */
481
+ getId?: IdExtractor<T>;
482
+ /** Enable optimistic list mutations (default: false). */
483
+ optimistic?: boolean;
484
+ /** Called after a successful list mutation. */
485
+ onMutationSuccess?: (action: string) => void;
486
+ /** Called after a failed list mutation. */
487
+ onMutationError?: (error: Error, action: string) => void;
488
+ }
489
+
490
+ /** CRUD actions for a list resource. */
491
+ export interface ResourceListActions<T> {
492
+ /** Refresh the list (GET). */
493
+ fetch: () => Promise<T[] | undefined>;
494
+ /** Add a new item to the list (POST). */
495
+ add: (body: Partial<T> | Record<string, unknown>) => Promise<T | undefined>;
496
+ /** Update an existing item (PUT) by ID. */
497
+ update: (
498
+ id: string | number,
499
+ body: Partial<T> | Record<string, unknown>
500
+ ) => Promise<T | undefined>;
501
+ /** Partially update an existing item (PATCH) by ID. */
502
+ patch: (
503
+ id: string | number,
504
+ body: Partial<T> | Record<string, unknown>
505
+ ) => Promise<T | undefined>;
506
+ /** Remove an item from the list (DELETE) by ID. */
507
+ remove: (id: string | number) => Promise<void>;
508
+ }
509
+
510
+ /** Return value of `useResourceList()`. */
511
+ export interface UseResourceListReturn<T> {
512
+ /** Reactive list data. */
513
+ data: Signal<T[] | undefined>;
514
+ /** Last error. */
515
+ error: Signal<Error | null>;
516
+ /** Lifecycle status. */
517
+ status: Signal<AsyncDataStatus>;
518
+ /** Whether the list fetch is pending. */
519
+ pending: { readonly value: boolean; peek(): boolean };
520
+ /** Whether any mutation is in progress. */
521
+ isMutating: { readonly value: boolean; peek(): boolean };
522
+ /** CRUD actions. */
523
+ actions: ResourceListActions<T>;
524
+ /** Refresh the list. */
525
+ refresh: () => Promise<T[] | undefined>;
526
+ /** Clear data, error, and status. */
527
+ clear: () => void;
528
+ /** Dispose all reactive state. */
529
+ dispose: () => void;
530
+ }
531
+
532
+ /**
533
+ * Reactive list/collection CRUD composable with optimistic add, remove, and update.
534
+ *
535
+ * Fetches a list of items and provides typed CRUD helpers that update the
536
+ * reactive array optimistically or after server confirmation.
537
+ *
538
+ * @template T - Item type
539
+ * @param url - List endpoint URL or getter
540
+ * @param options - Fetch and list options
541
+ * @returns Reactive list state with CRUD actions
542
+ *
543
+ * @example
544
+ * ```ts
545
+ * import { useResourceList } from '@bquery/bquery/reactive';
546
+ *
547
+ * interface Todo { id: number; title: string; done: boolean }
548
+ *
549
+ * const todos = useResourceList<Todo>('/api/todos', {
550
+ * baseUrl: 'https://api.example.com',
551
+ * optimistic: true,
552
+ * getId: (t) => t.id,
553
+ * });
554
+ *
555
+ * await todos.actions.add({ title: 'Buy milk', done: false });
556
+ * await todos.actions.patch(1, { done: true });
557
+ * await todos.actions.remove(1);
558
+ * ```
559
+ */
560
+ export const useResourceList = <T = unknown>(
561
+ url: string | URL | (() => string | URL),
562
+ options: UseResourceListOptions<T> = {}
563
+ ): UseResourceListReturn<T> => {
564
+ const {
565
+ getId = (item: T) => (item as Record<string, unknown>).id as string | number,
566
+ optimistic = false,
567
+ onMutationSuccess,
568
+ onMutationError,
569
+ ...fetchOptions
570
+ } = options;
571
+
572
+ const fetchState = useFetch<T[]>(url, { ...fetchOptions });
573
+
574
+ const mutating = signal(false);
575
+ const isMutating = computed(() => mutating.value);
576
+
577
+ let disposed = false;
578
+
579
+ const resolveUrl = (): string => {
580
+ const resolved = typeof url === 'function' ? url() : url;
581
+ return resolved instanceof URL ? resolved.toString() : resolved;
582
+ };
583
+
584
+ const baseUrl = (): string => {
585
+ let base = resolveUrl();
586
+ while (base.endsWith('/')) base = base.slice(0, -1);
587
+ return base;
588
+ };
589
+
590
+ const toMutationFetchOptions = <TResult>(): Omit<
591
+ UseFetchOptions<TResult>,
592
+ 'method' | 'body' | 'defaultValue' | 'transform' | 'onSuccess' | 'onError'
593
+ > => {
594
+ // Strip list-level async-data defaults/callbacks; mutations operate on item payloads instead.
595
+ const {
596
+ defaultValue: _defaultValue,
597
+ transform: _transform,
598
+ onSuccess: _onSuccess,
599
+ onError: _onError,
600
+ ...transportOpts
601
+ } = fetchOptions;
602
+ return transportOpts as Omit<
603
+ UseFetchOptions<TResult>,
604
+ 'method' | 'body' | 'defaultValue' | 'transform' | 'onSuccess' | 'onError'
605
+ >;
606
+ };
607
+
608
+ const runMutation = async <TResult>(
609
+ action: string,
610
+ method: string,
611
+ urlSuffix: string,
612
+ body: Record<string, unknown> | Partial<T> | undefined,
613
+ applyOptimistic: (() => void) | undefined,
614
+ rollback: (() => void) | undefined
615
+ ): Promise<TResult | undefined> => {
616
+ if (disposed) return undefined;
617
+
618
+ if (optimistic && applyOptimistic) applyOptimistic();
619
+
620
+ mutating.value = true;
621
+ fetchState.error.value = null;
622
+
623
+ try {
624
+ const mutationUrl = `${baseUrl()}${urlSuffix}`;
625
+ const mutationState = useFetch<TResult>(mutationUrl, {
626
+ ...toMutationFetchOptions<TResult>(),
627
+ method,
628
+ body: body ?? undefined,
629
+ immediate: false,
630
+ watch: undefined,
631
+ });
632
+
633
+ const result = await mutationState.execute();
634
+ const mutationError = mutationState.error.peek();
635
+ mutationState.dispose();
636
+
637
+ if (disposed) return undefined;
638
+
639
+ if (mutationError) {
640
+ if (optimistic && rollback) rollback();
641
+ fetchState.error.value = mutationError;
642
+ fetchState.status.value = 'error';
643
+ mutating.value = false;
644
+ onMutationError?.(mutationError, action);
645
+ return undefined;
646
+ }
647
+
648
+ mutating.value = false;
649
+ fetchState.status.value = 'success';
650
+ onMutationSuccess?.(action);
651
+ return result as TResult | undefined;
652
+ } catch (caught) {
653
+ if (disposed) return undefined;
654
+ if (optimistic && rollback) rollback();
655
+ const normalizedError = caught instanceof Error ? caught : new Error(String(caught));
656
+ fetchState.error.value = normalizedError;
657
+ fetchState.status.value = 'error';
658
+ mutating.value = false;
659
+ onMutationError?.(normalizedError, action);
660
+ return undefined;
661
+ }
662
+ };
663
+
664
+ const actions: ResourceListActions<T> = {
665
+ fetch: () => fetchState.execute(),
666
+
667
+ add: async (body) => {
668
+ const previousList = fetchState.data.peek();
669
+ const optimisticItem = body as T;
670
+ const optimisticInsertionIndex = previousList?.length ?? 0;
671
+
672
+ const result = await runMutation<T>(
673
+ 'add',
674
+ 'POST',
675
+ '',
676
+ body as Record<string, unknown>,
677
+ optimistic
678
+ ? () => {
679
+ fetchState.data.value = [...(previousList ?? []), optimisticItem];
680
+ }
681
+ : undefined,
682
+ optimistic
683
+ ? () => {
684
+ fetchState.data.value = previousList;
685
+ }
686
+ : undefined
687
+ );
688
+
689
+ if (result !== undefined && !disposed) {
690
+ const current = fetchState.data.peek() ?? [];
691
+ if (optimistic) {
692
+ const next = [...current];
693
+ // Replace the optimistic placeholder when it is still present; otherwise append.
694
+ if (
695
+ optimisticInsertionIndex < next.length &&
696
+ next[optimisticInsertionIndex] === optimisticItem
697
+ ) {
698
+ next[optimisticInsertionIndex] = result;
699
+ } else {
700
+ next.push(result);
701
+ }
702
+ fetchState.data.value = next;
703
+ } else {
704
+ fetchState.data.value = [...current, result];
705
+ }
706
+ }
707
+
708
+ return result;
709
+ },
710
+
711
+ update: async (id, body) => {
712
+ const previousList = fetchState.data.peek();
713
+
714
+ const result = await runMutation<T>(
715
+ 'update',
716
+ 'PUT',
717
+ `/${encodeURIComponent(String(id))}`,
718
+ body as Record<string, unknown>,
719
+ optimistic && previousList
720
+ ? () => {
721
+ fetchState.data.value = previousList.map((item) =>
722
+ getId(item) === id ? ({ ...item, ...body } as T) : item
723
+ );
724
+ }
725
+ : undefined,
726
+ optimistic
727
+ ? () => {
728
+ fetchState.data.value = previousList;
729
+ }
730
+ : undefined
731
+ );
732
+
733
+ if (result !== undefined && !disposed) {
734
+ const current = fetchState.data.peek() ?? [];
735
+ fetchState.data.value = current.map((item) => (getId(item) === id ? result : item));
736
+ }
737
+
738
+ return result;
739
+ },
740
+
741
+ patch: async (id, body) => {
742
+ const previousList = fetchState.data.peek();
743
+
744
+ const result = await runMutation<T>(
745
+ 'patch',
746
+ 'PATCH',
747
+ `/${encodeURIComponent(String(id))}`,
748
+ body as Record<string, unknown>,
749
+ optimistic && previousList
750
+ ? () => {
751
+ fetchState.data.value = previousList.map((item) =>
752
+ getId(item) === id ? ({ ...item, ...body } as T) : item
753
+ );
754
+ }
755
+ : undefined,
756
+ optimistic
757
+ ? () => {
758
+ fetchState.data.value = previousList;
759
+ }
760
+ : undefined
761
+ );
762
+
763
+ if (result !== undefined && !disposed) {
764
+ const current = fetchState.data.peek() ?? [];
765
+ fetchState.data.value = current.map((item) => (getId(item) === id ? result : item));
766
+ }
767
+
768
+ return result;
769
+ },
770
+
771
+ remove: async (id) => {
772
+ const previousList = fetchState.data.peek();
773
+
774
+ await runMutation<void>(
775
+ 'remove',
776
+ 'DELETE',
777
+ `/${encodeURIComponent(String(id))}`,
778
+ undefined,
779
+ optimistic && previousList
780
+ ? () => {
781
+ fetchState.data.value = previousList.filter((item) => getId(item) !== id);
782
+ }
783
+ : undefined,
784
+ optimistic
785
+ ? () => {
786
+ fetchState.data.value = previousList;
787
+ }
788
+ : undefined
789
+ );
790
+
791
+ // If not optimistic, remove from the list after server confirms
792
+ if (!optimistic && !disposed && fetchState.error.peek() == null) {
793
+ const current = fetchState.data.peek() ?? [];
794
+ fetchState.data.value = current.filter((item) => getId(item) !== id);
795
+ }
796
+ },
797
+ };
798
+
799
+ const originalDispose = fetchState.dispose;
800
+ const dispose = (): void => {
801
+ if (disposed) return;
802
+ disposed = true;
803
+ originalDispose();
804
+ };
805
+
806
+ return {
807
+ data: fetchState.data as Signal<T[] | undefined>,
808
+ error: fetchState.error,
809
+ status: fetchState.status,
810
+ pending: fetchState.pending,
811
+ isMutating,
812
+ actions,
813
+ refresh: fetchState.execute,
814
+ clear: fetchState.clear,
815
+ dispose,
816
+ };
817
+ };
818
+
819
+ // ---------------------------------------------------------------------------
820
+ // Request deduplication
821
+ // ---------------------------------------------------------------------------
822
+
823
+ /** @internal In-flight request/operation cache for deduplication. */
824
+ const inflightRequests = new Map<string, Promise<unknown>>();
825
+
826
+ /**
827
+ * Deduplicate identical in-flight requests or operations keyed by `key`.
828
+ *
829
+ * If an operation with the same key is already in flight, reuse its promise
830
+ * instead of starting a new one. Once the operation completes, the entry is removed.
831
+ *
832
+ * @param key - Cache key for the in-flight operation (for HTTP, typically URL + serialized query)
833
+ * @param execute - The operation function to run if no duplicate is in flight
834
+ * @returns The shared result promise for callers using the same key concurrently
835
+ *
836
+ * @example
837
+ * ```ts
838
+ * import { deduplicateRequest, createHttp } from '@bquery/bquery/reactive';
839
+ *
840
+ * const api = createHttp({ baseUrl: 'https://api.example.com' });
841
+ *
842
+ * // Both calls share the same in-flight operation
843
+ * const [a, b] = await Promise.all([
844
+ * deduplicateRequest('/users', () => api.get('/users')),
845
+ * deduplicateRequest('/users', () => api.get('/users')),
846
+ * ]);
847
+ * ```
848
+ */
849
+ export function deduplicateRequest<T>(key: string, execute: () => Promise<T>): Promise<T> {
850
+ const existing = inflightRequests.get(key);
851
+ if (existing) return existing as Promise<T>;
852
+
853
+ const promise = execute().finally(() => {
854
+ inflightRequests.delete(key);
855
+ });
856
+
857
+ inflightRequests.set(key, promise);
858
+ return promise;
859
+ }