@bquery/bquery 1.7.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 (262) hide show
  1. package/README.md +760 -716
  2. package/dist/{a11y-C5QOVvRn.js → a11y-DVBCy09c.js} +3 -3
  3. package/dist/a11y-DVBCy09c.js.map +1 -0
  4. package/dist/a11y.es.mjs +1 -1
  5. package/dist/component/library.d.ts.map +1 -1
  6. package/dist/{component-CuuTijA6.js → component-L3-JfOFz.js} +5 -5
  7. package/dist/component-L3-JfOFz.js.map +1 -0
  8. package/dist/component.es.mjs +1 -1
  9. package/dist/{config-BW35FKuA.js → config-DhT9auRm.js} +1 -1
  10. package/dist/{config-BW35FKuA.js.map → config-DhT9auRm.js.map} +1 -1
  11. package/dist/{constraints-3lV9yyBw.js → constraints-D5RHQLmP.js} +1 -1
  12. package/dist/constraints-D5RHQLmP.js.map +1 -0
  13. package/dist/core/collection.d.ts +86 -0
  14. package/dist/core/collection.d.ts.map +1 -1
  15. package/dist/core/element.d.ts +28 -0
  16. package/dist/core/element.d.ts.map +1 -1
  17. package/dist/core/shared.d.ts +6 -0
  18. package/dist/core/shared.d.ts.map +1 -1
  19. package/dist/core-DdtZHzsS.js +168 -0
  20. package/dist/core-DdtZHzsS.js.map +1 -0
  21. package/dist/{core-Cjl7GUu8.js → core-EMYSLzaT.js} +289 -259
  22. package/dist/core-EMYSLzaT.js.map +1 -0
  23. package/dist/core.es.mjs +48 -47
  24. package/dist/{custom-directives-7wAShnnd.js → custom-directives-Dr4C5lVV.js} +1 -1
  25. package/dist/custom-directives-Dr4C5lVV.js.map +1 -0
  26. package/dist/{devtools-D2fQLhDN.js → devtools-BhB2iDPT.js} +2 -2
  27. package/dist/devtools-BhB2iDPT.js.map +1 -0
  28. package/dist/devtools.es.mjs +1 -1
  29. package/dist/{dnd-B8EgyzaI.js → dnd-NwZBYh4l.js} +1 -1
  30. package/dist/dnd-NwZBYh4l.js.map +1 -0
  31. package/dist/dnd.es.mjs +1 -1
  32. package/dist/{env-NeVmr4Gf.js → env-CTdvLaH2.js} +1 -1
  33. package/dist/env-CTdvLaH2.js.map +1 -0
  34. package/dist/forms/create-form.d.ts.map +1 -1
  35. package/dist/forms/index.d.ts +3 -2
  36. package/dist/forms/index.d.ts.map +1 -1
  37. package/dist/forms/types.d.ts +46 -0
  38. package/dist/forms/types.d.ts.map +1 -1
  39. package/dist/forms/use-field.d.ts +34 -0
  40. package/dist/forms/use-field.d.ts.map +1 -0
  41. package/dist/forms/validators.d.ts +25 -0
  42. package/dist/forms/validators.d.ts.map +1 -1
  43. package/dist/forms-UcRHsYxC.js +227 -0
  44. package/dist/forms-UcRHsYxC.js.map +1 -0
  45. package/dist/forms.es.mjs +14 -12
  46. package/dist/full.d.ts +17 -26
  47. package/dist/full.d.ts.map +1 -1
  48. package/dist/full.es.mjs +206 -181
  49. package/dist/full.iife.js +33 -33
  50. package/dist/full.iife.js.map +1 -1
  51. package/dist/full.umd.js +33 -33
  52. package/dist/full.umd.js.map +1 -1
  53. package/dist/function-Cybd57JV.js +33 -0
  54. package/dist/function-Cybd57JV.js.map +1 -0
  55. package/dist/{i18n-BnnhTFOS.js → i18n-kuF6Ekj6.js} +3 -3
  56. package/dist/i18n-kuF6Ekj6.js.map +1 -0
  57. package/dist/i18n.es.mjs +1 -1
  58. package/dist/index.es.mjs +251 -228
  59. package/dist/media/breakpoints.d.ts.map +1 -1
  60. package/dist/media/types.d.ts +2 -2
  61. package/dist/media/types.d.ts.map +1 -1
  62. package/dist/{media-Di2Ta22s.js → media-i-fB5WxI.js} +3 -3
  63. package/dist/media-i-fB5WxI.js.map +1 -0
  64. package/dist/media.es.mjs +1 -1
  65. package/dist/{motion-qPj_TYGv.js → motion-BJsAuULb.js} +2 -2
  66. package/dist/motion-BJsAuULb.js.map +1 -0
  67. package/dist/motion.es.mjs +1 -1
  68. package/dist/{mount-SM07RUa6.js → mount-B4Y8bk8Z.js} +5 -5
  69. package/dist/mount-B4Y8bk8Z.js.map +1 -0
  70. package/dist/{platform-CPbCprb6.js → platform-Dw2gE3zI.js} +3 -3
  71. package/dist/{platform-CPbCprb6.js.map → platform-Dw2gE3zI.js.map} +1 -1
  72. package/dist/platform.es.mjs +2 -2
  73. package/dist/plugin/registry.d.ts.map +1 -1
  74. package/dist/{plugin-cPoOHFLY.js → plugin-C2WuC8SF.js} +20 -18
  75. package/dist/plugin-C2WuC8SF.js.map +1 -0
  76. package/dist/plugin.es.mjs +1 -1
  77. package/dist/reactive/async-data.d.ts +28 -3
  78. package/dist/reactive/async-data.d.ts.map +1 -1
  79. package/dist/reactive/computed.d.ts +3 -0
  80. package/dist/reactive/computed.d.ts.map +1 -1
  81. package/dist/reactive/effect.d.ts +3 -0
  82. package/dist/reactive/effect.d.ts.map +1 -1
  83. package/dist/reactive/http.d.ts +194 -0
  84. package/dist/reactive/http.d.ts.map +1 -0
  85. package/dist/reactive/index.d.ts +2 -2
  86. package/dist/reactive/index.d.ts.map +1 -1
  87. package/dist/reactive/pagination.d.ts +126 -0
  88. package/dist/reactive/pagination.d.ts.map +1 -0
  89. package/dist/reactive/polling.d.ts +55 -0
  90. package/dist/reactive/polling.d.ts.map +1 -0
  91. package/dist/reactive/readonly.d.ts +20 -1
  92. package/dist/reactive/readonly.d.ts.map +1 -1
  93. package/dist/reactive/rest.d.ts +293 -0
  94. package/dist/reactive/rest.d.ts.map +1 -0
  95. package/dist/reactive/scope.d.ts +140 -0
  96. package/dist/reactive/scope.d.ts.map +1 -0
  97. package/dist/reactive/signal.d.ts +16 -2
  98. package/dist/reactive/signal.d.ts.map +1 -1
  99. package/dist/reactive/to-value.d.ts +57 -0
  100. package/dist/reactive/to-value.d.ts.map +1 -0
  101. package/dist/reactive/websocket.d.ts +285 -0
  102. package/dist/reactive/websocket.d.ts.map +1 -0
  103. package/dist/reactive-DwkhUJfP.js +1148 -0
  104. package/dist/reactive-DwkhUJfP.js.map +1 -0
  105. package/dist/reactive.es.mjs +38 -19
  106. package/dist/{registry-CWf368tT.js → registry-B08iilIh.js} +1 -1
  107. package/dist/{registry-CWf368tT.js.map → registry-B08iilIh.js.map} +1 -1
  108. package/dist/router/constraints.d.ts.map +1 -1
  109. package/dist/router/index.d.ts +1 -1
  110. package/dist/router/index.d.ts.map +1 -1
  111. package/dist/router/router.d.ts.map +1 -1
  112. package/dist/router/state.d.ts +25 -2
  113. package/dist/router/state.d.ts.map +1 -1
  114. package/dist/router-CQikC9Ed.js +492 -0
  115. package/dist/router-CQikC9Ed.js.map +1 -0
  116. package/dist/router.es.mjs +9 -8
  117. package/dist/ssr/hydrate.d.ts.map +1 -1
  118. package/dist/{ssr-B2qd_WBB.js → ssr-_dAcGdzu.js} +4 -4
  119. package/dist/ssr-_dAcGdzu.js.map +1 -0
  120. package/dist/ssr.es.mjs +1 -1
  121. package/dist/store/persisted.d.ts.map +1 -1
  122. package/dist/{store-DWpyH6p5.js → store-Cb3gPRve.js} +7 -7
  123. package/dist/store-Cb3gPRve.js.map +1 -0
  124. package/dist/store.es.mjs +2 -2
  125. package/dist/storybook.es.mjs.map +1 -1
  126. package/dist/{testing-CsqjNUyy.js → testing-C5Sjfsna.js} +8 -8
  127. package/dist/testing-C5Sjfsna.js.map +1 -0
  128. package/dist/testing.es.mjs +1 -1
  129. package/dist/{type-guards-Do9DWgNp.js → type-guards-BMX2c0LP.js} +1 -1
  130. package/dist/{type-guards-Do9DWgNp.js.map → type-guards-BMX2c0LP.js.map} +1 -1
  131. package/dist/untrack-D0fnO5k2.js +36 -0
  132. package/dist/untrack-D0fnO5k2.js.map +1 -0
  133. package/dist/view/custom-directives.d.ts.map +1 -1
  134. package/dist/view.es.mjs +4 -4
  135. package/package.json +177 -177
  136. package/src/a11y/announce.ts +131 -131
  137. package/src/a11y/audit.ts +314 -314
  138. package/src/a11y/index.ts +68 -68
  139. package/src/a11y/media-preferences.ts +255 -255
  140. package/src/a11y/roving-tab-index.ts +164 -164
  141. package/src/a11y/skip-link.ts +255 -255
  142. package/src/a11y/trap-focus.ts +184 -184
  143. package/src/a11y/types.ts +183 -183
  144. package/src/component/component.ts +599 -599
  145. package/src/component/html.ts +153 -153
  146. package/src/component/index.ts +52 -52
  147. package/src/component/library.ts +540 -542
  148. package/src/component/scope.ts +212 -212
  149. package/src/component/types.ts +310 -310
  150. package/src/core/collection.ts +876 -707
  151. package/src/core/element.ts +1015 -981
  152. package/src/core/env.ts +60 -60
  153. package/src/core/index.ts +49 -49
  154. package/src/core/shared.ts +77 -62
  155. package/src/core/utils/index.ts +148 -148
  156. package/src/devtools/devtools.ts +410 -410
  157. package/src/devtools/index.ts +48 -48
  158. package/src/devtools/types.ts +104 -104
  159. package/src/dnd/draggable.ts +296 -296
  160. package/src/dnd/droppable.ts +228 -228
  161. package/src/dnd/index.ts +62 -62
  162. package/src/dnd/sortable.ts +307 -307
  163. package/src/dnd/types.ts +293 -293
  164. package/src/forms/create-form.ts +320 -278
  165. package/src/forms/index.ts +70 -65
  166. package/src/forms/types.ts +203 -154
  167. package/src/forms/use-field.ts +231 -0
  168. package/src/forms/validators.ts +294 -265
  169. package/src/full.ts +554 -480
  170. package/src/i18n/formatting.ts +67 -67
  171. package/src/i18n/i18n.ts +200 -200
  172. package/src/i18n/index.ts +67 -67
  173. package/src/i18n/translate.ts +182 -182
  174. package/src/i18n/types.ts +171 -171
  175. package/src/index.ts +108 -108
  176. package/src/media/battery.ts +116 -116
  177. package/src/media/breakpoints.ts +129 -131
  178. package/src/media/clipboard.ts +80 -80
  179. package/src/media/device-sensors.ts +158 -158
  180. package/src/media/geolocation.ts +119 -119
  181. package/src/media/index.ts +76 -76
  182. package/src/media/media-query.ts +92 -92
  183. package/src/media/network.ts +115 -115
  184. package/src/media/types.ts +177 -177
  185. package/src/media/viewport.ts +84 -84
  186. package/src/motion/index.ts +57 -57
  187. package/src/motion/morph.ts +151 -151
  188. package/src/motion/parallax.ts +120 -120
  189. package/src/motion/reduced-motion.ts +66 -66
  190. package/src/motion/types.ts +271 -271
  191. package/src/motion/typewriter.ts +164 -164
  192. package/src/plugin/index.ts +37 -37
  193. package/src/plugin/registry.ts +284 -269
  194. package/src/plugin/types.ts +137 -137
  195. package/src/reactive/async-data.ts +250 -29
  196. package/src/reactive/computed.ts +144 -130
  197. package/src/reactive/effect.ts +29 -6
  198. package/src/reactive/http.ts +790 -0
  199. package/src/reactive/index.ts +60 -0
  200. package/src/reactive/pagination.ts +317 -0
  201. package/src/reactive/polling.ts +179 -0
  202. package/src/reactive/readonly.ts +52 -8
  203. package/src/reactive/rest.ts +859 -0
  204. package/src/reactive/scope.ts +276 -0
  205. package/src/reactive/signal.ts +61 -1
  206. package/src/reactive/to-value.ts +71 -0
  207. package/src/reactive/websocket.ts +849 -0
  208. package/src/router/bq-link.ts +279 -279
  209. package/src/router/constraints.ts +204 -201
  210. package/src/router/index.ts +49 -49
  211. package/src/router/match.ts +312 -312
  212. package/src/router/path-pattern.ts +52 -52
  213. package/src/router/query.ts +38 -38
  214. package/src/router/router.ts +421 -402
  215. package/src/router/state.ts +51 -3
  216. package/src/router/types.ts +139 -139
  217. package/src/router/use-route.ts +68 -68
  218. package/src/router/utils.ts +157 -157
  219. package/src/security/index.ts +12 -12
  220. package/src/ssr/hydrate.ts +84 -82
  221. package/src/ssr/index.ts +70 -70
  222. package/src/ssr/render.ts +508 -508
  223. package/src/ssr/serialize.ts +296 -296
  224. package/src/ssr/types.ts +81 -81
  225. package/src/store/create-store.ts +467 -467
  226. package/src/store/index.ts +27 -27
  227. package/src/store/persisted.ts +245 -249
  228. package/src/store/types.ts +247 -247
  229. package/src/store/utils.ts +135 -135
  230. package/src/storybook/index.ts +480 -480
  231. package/src/testing/index.ts +42 -42
  232. package/src/testing/testing.ts +593 -593
  233. package/src/testing/types.ts +170 -170
  234. package/src/view/custom-directives.ts +28 -30
  235. package/src/view/evaluate.ts +292 -292
  236. package/src/view/process.ts +108 -108
  237. package/dist/a11y-C5QOVvRn.js.map +0 -1
  238. package/dist/component-CuuTijA6.js.map +0 -1
  239. package/dist/constraints-3lV9yyBw.js.map +0 -1
  240. package/dist/core-Cjl7GUu8.js.map +0 -1
  241. package/dist/core-DnlyjbF2.js +0 -112
  242. package/dist/core-DnlyjbF2.js.map +0 -1
  243. package/dist/custom-directives-7wAShnnd.js.map +0 -1
  244. package/dist/devtools-D2fQLhDN.js.map +0 -1
  245. package/dist/dnd-B8EgyzaI.js.map +0 -1
  246. package/dist/env-NeVmr4Gf.js.map +0 -1
  247. package/dist/forms-C3yovgH9.js +0 -141
  248. package/dist/forms-C3yovgH9.js.map +0 -1
  249. package/dist/i18n-BnnhTFOS.js.map +0 -1
  250. package/dist/media-Di2Ta22s.js.map +0 -1
  251. package/dist/motion-qPj_TYGv.js.map +0 -1
  252. package/dist/mount-SM07RUa6.js.map +0 -1
  253. package/dist/plugin-cPoOHFLY.js.map +0 -1
  254. package/dist/reactive-Cfv0RK6x.js +0 -233
  255. package/dist/reactive-Cfv0RK6x.js.map +0 -1
  256. package/dist/router-BrthaP_z.js +0 -473
  257. package/dist/router-BrthaP_z.js.map +0 -1
  258. package/dist/ssr-B2qd_WBB.js.map +0 -1
  259. package/dist/store-DWpyH6p5.js.map +0 -1
  260. package/dist/testing-CsqjNUyy.js.map +0 -1
  261. package/dist/untrack-DJVQQ2WM.js +0 -33
  262. package/dist/untrack-DJVQQ2WM.js.map +0 -1
@@ -1,137 +1,137 @@
1
- /**
2
- * Public types for the bQuery plugin system.
3
- *
4
- * Plugins extend bQuery by registering custom directives and Web Components
5
- * through a single unified interface.
6
- *
7
- * @module bquery/plugin
8
- */
9
-
10
- import type { CleanupFn } from '../reactive/index';
11
- import type { BindingContext } from '../view/types';
12
-
13
- // ---------------------------------------------------------------------------
14
- // Custom Directive
15
- // ---------------------------------------------------------------------------
16
-
17
- /**
18
- * A custom directive handler that is invoked when the view module encounters
19
- * a `bq-{name}` attribute during mount processing.
20
- *
21
- * @param el - The DOM element carrying the directive attribute
22
- * @param expression - The raw attribute value (expression string)
23
- * @param context - The current binding context (data / signals)
24
- * @param cleanups - Array where the handler should push any cleanup functions
25
- *
26
- * @example
27
- * ```ts
28
- * const tooltipDirective: CustomDirectiveHandler = (el, expression, ctx, cleanups) => {
29
- * const tip = document.createElement('span');
30
- * tip.textContent = String(expression);
31
- * el.appendChild(tip);
32
- * cleanups.push(() => tip.remove());
33
- * };
34
- * ```
35
- */
36
- export type CustomDirectiveHandler = (
37
- el: Element,
38
- expression: string,
39
- context: BindingContext,
40
- cleanups: CleanupFn[]
41
- ) => void;
42
-
43
- /**
44
- * Descriptor for a custom directive registered by a plugin.
45
- */
46
- export interface CustomDirective {
47
- /** The directive name (without prefix). e.g. `'tooltip'` → `bq-tooltip` */
48
- readonly name: string;
49
- /** The handler function called when the directive is encountered. */
50
- readonly handler: CustomDirectiveHandler;
51
- }
52
-
53
- // ---------------------------------------------------------------------------
54
- // Plugin Install Context
55
- // ---------------------------------------------------------------------------
56
-
57
- /**
58
- * Context object provided to a plugin's `install` function.
59
- *
60
- * Plugins use these helpers to register their contributions into bQuery's
61
- * global registries without directly importing internal modules.
62
- */
63
- export interface PluginInstallContext {
64
- /**
65
- * Register a custom view directive that will be recognized during
66
- * `mount()` processing.
67
- *
68
- * @param name - Directive name **without** the `bq-` prefix (e.g. `'tooltip'`)
69
- * @param handler - The handler called for each element with the directive
70
- *
71
- * @example
72
- * ```ts
73
- * ctx.directive('focus', (el) => {
74
- * (el as HTMLElement).focus();
75
- * });
76
- * ```
77
- */
78
- directive(name: string, handler: CustomDirectiveHandler): void;
79
-
80
- /**
81
- * Register a Web Component via the native `customElements.define()` API.
82
- *
83
- * @param tagName - Custom element tag (e.g. `'my-counter'`)
84
- * @param constructor - The `HTMLElement` subclass
85
- * @param options - Optional `ElementDefinitionOptions` (e.g. `{ extends: 'div' }`)
86
- *
87
- * @example
88
- * ```ts
89
- * ctx.component('my-counter', MyCounterElement);
90
- * ```
91
- */
92
- component(
93
- tagName: string,
94
- constructor: CustomElementConstructor,
95
- options?: ElementDefinitionOptions
96
- ): void;
97
- }
98
-
99
- // ---------------------------------------------------------------------------
100
- // Plugin Interface
101
- // ---------------------------------------------------------------------------
102
-
103
- /**
104
- * A bQuery plugin.
105
- *
106
- * Plugins are plain objects with a `name` and an `install` function.
107
- * Call `use(plugin)` to activate a plugin before creating routers, stores,
108
- * or mounting views.
109
- *
110
- * @example
111
- * ```ts
112
- * import { use } from '@bquery/bquery/plugin';
113
- *
114
- * const myPlugin: BQueryPlugin = {
115
- * name: 'my-plugin',
116
- * install(ctx, options) {
117
- * ctx.directive('highlight', (el, expr) => {
118
- * (el as HTMLElement).style.background = String(expr);
119
- * });
120
- * },
121
- * };
122
- *
123
- * use(myPlugin, { color: 'yellow' });
124
- * ```
125
- */
126
- export interface BQueryPlugin<TOptions = unknown> {
127
- /** Unique human-readable name for the plugin. */
128
- readonly name: string;
129
-
130
- /**
131
- * Called once when the plugin is registered via `use()`.
132
- *
133
- * @param context - Helpers for registering directives, components, etc.
134
- * @param options - User-provided options forwarded from `use(plugin, options)`.
135
- */
136
- install(context: PluginInstallContext, options?: TOptions): void;
137
- }
1
+ /**
2
+ * Public types for the bQuery plugin system.
3
+ *
4
+ * Plugins extend bQuery by registering custom directives and Web Components
5
+ * through a single unified interface.
6
+ *
7
+ * @module bquery/plugin
8
+ */
9
+
10
+ import type { CleanupFn } from '../reactive/index';
11
+ import type { BindingContext } from '../view/types';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Custom Directive
15
+ // ---------------------------------------------------------------------------
16
+
17
+ /**
18
+ * A custom directive handler that is invoked when the view module encounters
19
+ * a `bq-{name}` attribute during mount processing.
20
+ *
21
+ * @param el - The DOM element carrying the directive attribute
22
+ * @param expression - The raw attribute value (expression string)
23
+ * @param context - The current binding context (data / signals)
24
+ * @param cleanups - Array where the handler should push any cleanup functions
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * const tooltipDirective: CustomDirectiveHandler = (el, expression, ctx, cleanups) => {
29
+ * const tip = document.createElement('span');
30
+ * tip.textContent = String(expression);
31
+ * el.appendChild(tip);
32
+ * cleanups.push(() => tip.remove());
33
+ * };
34
+ * ```
35
+ */
36
+ export type CustomDirectiveHandler = (
37
+ el: Element,
38
+ expression: string,
39
+ context: BindingContext,
40
+ cleanups: CleanupFn[]
41
+ ) => void;
42
+
43
+ /**
44
+ * Descriptor for a custom directive registered by a plugin.
45
+ */
46
+ export interface CustomDirective {
47
+ /** The directive name (without prefix). e.g. `'tooltip'` → `bq-tooltip` */
48
+ readonly name: string;
49
+ /** The handler function called when the directive is encountered. */
50
+ readonly handler: CustomDirectiveHandler;
51
+ }
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Plugin Install Context
55
+ // ---------------------------------------------------------------------------
56
+
57
+ /**
58
+ * Context object provided to a plugin's `install` function.
59
+ *
60
+ * Plugins use these helpers to register their contributions into bQuery's
61
+ * global registries without directly importing internal modules.
62
+ */
63
+ export interface PluginInstallContext {
64
+ /**
65
+ * Register a custom view directive that will be recognized during
66
+ * `mount()` processing.
67
+ *
68
+ * @param name - Directive name **without** the `bq-` prefix (e.g. `'tooltip'`)
69
+ * @param handler - The handler called for each element with the directive
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * ctx.directive('focus', (el) => {
74
+ * (el as HTMLElement).focus();
75
+ * });
76
+ * ```
77
+ */
78
+ directive(name: string, handler: CustomDirectiveHandler): void;
79
+
80
+ /**
81
+ * Register a Web Component via the native `customElements.define()` API.
82
+ *
83
+ * @param tagName - Custom element tag (e.g. `'my-counter'`)
84
+ * @param constructor - The `HTMLElement` subclass
85
+ * @param options - Optional `ElementDefinitionOptions` (e.g. `{ extends: 'div' }`)
86
+ *
87
+ * @example
88
+ * ```ts
89
+ * ctx.component('my-counter', MyCounterElement);
90
+ * ```
91
+ */
92
+ component(
93
+ tagName: string,
94
+ constructor: CustomElementConstructor,
95
+ options?: ElementDefinitionOptions
96
+ ): void;
97
+ }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // Plugin Interface
101
+ // ---------------------------------------------------------------------------
102
+
103
+ /**
104
+ * A bQuery plugin.
105
+ *
106
+ * Plugins are plain objects with a `name` and an `install` function.
107
+ * Call `use(plugin)` to activate a plugin before creating routers, stores,
108
+ * or mounting views.
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * import { use } from '@bquery/bquery/plugin';
113
+ *
114
+ * const myPlugin: BQueryPlugin = {
115
+ * name: 'my-plugin',
116
+ * install(ctx, options) {
117
+ * ctx.directive('highlight', (el, expr) => {
118
+ * (el as HTMLElement).style.background = String(expr);
119
+ * });
120
+ * },
121
+ * };
122
+ *
123
+ * use(myPlugin, { color: 'yellow' });
124
+ * ```
125
+ */
126
+ export interface BQueryPlugin<TOptions = unknown> {
127
+ /** Unique human-readable name for the plugin. */
128
+ readonly name: string;
129
+
130
+ /**
131
+ * Called once when the plugin is registered via `use()`.
132
+ *
133
+ * @param context - Helpers for registering directives, components, etc.
134
+ * @param options - User-provided options forwarded from `use(plugin, options)`.
135
+ */
136
+ install(context: PluginInstallContext, options?: TOptions): void;
137
+ }
@@ -47,15 +47,27 @@ export interface AsyncDataState<TData> {
47
47
  execute: () => Promise<TData | undefined>;
48
48
  /** Alias for execute(). */
49
49
  refresh: () => Promise<TData | undefined>;
50
+ /** Abort the current in-flight request (useFetch only; no-op for useAsyncData). */
51
+ abort: () => void;
50
52
  /** Clear data, error, and status back to the initial state. */
51
53
  clear: () => void;
52
54
  /** Dispose reactive watchers and prevent future executions. */
53
55
  dispose: () => void;
54
56
  }
55
57
 
58
+ /** Configuration for automatic request retries in useFetch(). */
59
+ export interface UseFetchRetryConfig {
60
+ /** Maximum number of retry attempts (default: 3). */
61
+ count: number;
62
+ /** Delay in ms between retries, or a function receiving the attempt index. */
63
+ delay?: number | ((attempt: number) => number);
64
+ /** Predicate deciding whether to retry. Defaults to network / 5xx errors. */
65
+ retryOn?: (error: Error, attempt: number) => boolean;
66
+ }
67
+
56
68
  /** Options for useFetch(). */
57
69
  export interface UseFetchOptions<TResponse = unknown, TData = TResponse>
58
- extends UseAsyncDataOptions<TResponse, TData>, Omit<RequestInit, 'body' | 'headers'> {
70
+ extends UseAsyncDataOptions<TResponse, TData>, Omit<RequestInit, 'body' | 'headers' | 'signal'> {
59
71
  /** Base URL prepended to relative URLs. */
60
72
  baseUrl?: string;
61
73
  /** Query parameters appended to the request URL. */
@@ -68,6 +80,14 @@ export interface UseFetchOptions<TResponse = unknown, TData = TResponse>
68
80
  parseAs?: BqueryFetchParseAs;
69
81
  /** Custom fetch implementation for testing or adapters. */
70
82
  fetcher?: typeof fetch;
83
+ /** Request timeout in milliseconds. 0 means no timeout. */
84
+ timeout?: number;
85
+ /** External AbortSignal for request cancellation. */
86
+ signal?: AbortSignal;
87
+ /** Retry configuration. Pass a number for simple retry count, or a config object. */
88
+ retry?: number | UseFetchRetryConfig;
89
+ /** Custom status validation. Returns `true` for acceptable statuses. */
90
+ validateStatus?: (status: number) => boolean;
71
91
  }
72
92
 
73
93
  /** Input accepted by useFetch(). */
@@ -340,23 +360,101 @@ export const useAsyncData = <TResult, TData = TResult>(
340
360
  pending,
341
361
  execute,
342
362
  refresh: execute,
363
+ abort: () => {},
343
364
  clear,
344
365
  dispose,
345
366
  };
346
367
  };
347
368
 
369
+ /** @internal */
370
+ const DEFAULT_VALIDATE_STATUS = (status: number): boolean => status >= 200 && status < 300;
371
+
372
+ /** @internal */
373
+ const isDomExceptionNamed = (error: unknown, name: string): error is DOMException =>
374
+ error instanceof DOMException && error.name === name;
375
+
376
+ /** @internal */
377
+ const isTimeoutDomException = (error: unknown): error is DOMException =>
378
+ isDomExceptionNamed(error, 'TimeoutError');
379
+
380
+ /** @internal */
381
+ const isAbortDomException = (error: unknown): error is DOMException =>
382
+ isDomExceptionNamed(error, 'AbortError');
383
+
384
+ /** @internal */
385
+ const DEFAULT_RETRY_ON = (error: Error): boolean => {
386
+ if (
387
+ isAbortDomException(error) ||
388
+ isTimeoutDomException(error) ||
389
+ (error as Error & { code?: string }).code === 'ABORT' ||
390
+ (error as Error & { code?: string }).code === 'TIMEOUT'
391
+ ) {
392
+ return false;
393
+ }
394
+ const status = (error as Error & { status?: number }).status;
395
+ return status === undefined || status >= 500;
396
+ };
397
+
398
+ /** @internal */
399
+ const normalizeRetryConfig = (retry: UseFetchOptions['retry']): UseFetchRetryConfig | undefined => {
400
+ if (retry == null) return undefined;
401
+ if (typeof retry === 'number') return { count: retry };
402
+ return retry;
403
+ };
404
+
405
+ /** @internal */
406
+ const resolveRetryDelay = (delay: UseFetchRetryConfig['delay'], attempt: number): number => {
407
+ if (delay == null) return Math.min(1000 * 2 ** attempt, 30_000);
408
+ if (typeof delay === 'number') return delay;
409
+ return delay(attempt);
410
+ };
411
+
412
+ /** @internal */
413
+ const sleepWithSignal = (ms: number, abortSignal?: AbortSignal): Promise<void> =>
414
+ new Promise<void>((resolve, reject) => {
415
+ if (abortSignal?.aborted) {
416
+ reject(abortSignal.reason ?? new DOMException('The operation was aborted.', 'AbortError'));
417
+ return;
418
+ }
419
+ let cleanedUp = false;
420
+ let timer: ReturnType<typeof setTimeout>;
421
+
422
+ const onAbort = (): void => {
423
+ if (cleanedUp) return;
424
+ cleanedUp = true;
425
+ clearTimeout(timer);
426
+ abortSignal?.removeEventListener('abort', onAbort);
427
+ reject(abortSignal?.reason ?? new DOMException('The operation was aborted.', 'AbortError'));
428
+ };
429
+
430
+ timer = setTimeout(() => {
431
+ if (cleanedUp) return;
432
+ cleanedUp = true;
433
+ abortSignal?.removeEventListener('abort', onAbort);
434
+ resolve();
435
+ }, ms);
436
+
437
+ abortSignal?.addEventListener('abort', onAbort, { once: true });
438
+ });
439
+
348
440
  /**
349
441
  * Reactive fetch composable using the browser Fetch API.
350
442
  *
443
+ * Supports timeout, abort, retry, and custom status validation in addition
444
+ * to the core useFetch features (query params, JSON body, baseUrl, watch).
445
+ *
351
446
  * @template TResponse - Raw parsed response type
352
447
  * @template TData - Stored response type after optional transformation
353
448
  * @param input - Request URL, Request object, or lazy input factory
354
449
  * @param options - Request and reactive state options
355
- * @returns Reactive fetch state with execute(), refresh(), and clear()
450
+ * @returns Reactive fetch state with execute(), refresh(), abort(), clear(), and dispose()
356
451
  *
357
452
  * @example
358
453
  * ```ts
359
- * const users = useFetch<{ id: number; name: string }[]>('/api/users');
454
+ * const users = useFetch<{ id: number; name: string }[]>('/api/users', {
455
+ * timeout: 5000,
456
+ * retry: 3,
457
+ * });
360
458
  * ```
361
459
  */
362
460
  export const useFetch = <TResponse = unknown, TData = TResponse>(
@@ -366,8 +464,22 @@ export const useFetch = <TResponse = unknown, TData = TResponse>(
366
464
  const fetchConfig = getBqueryConfig().fetch;
367
465
  const parseAs = options.parseAs ?? fetchConfig?.parseAs ?? 'json';
368
466
  const fetcher = options.fetcher ?? fetch;
467
+ const validateStatus = options.validateStatus ?? DEFAULT_VALIDATE_STATUS;
468
+
469
+ let currentAbortController: AbortController | null = null;
470
+ const normalizeAbortLikeError = (reason: unknown, didTimeout: boolean): Error => {
471
+ const isTimeout =
472
+ didTimeout ||
473
+ isTimeoutDomException(reason) ||
474
+ isTimeoutDomException(currentAbortController?.signal.reason);
475
+
476
+ return Object.assign(
477
+ new Error(isTimeout ? `Request timeout of ${options.timeout}ms exceeded` : 'Request aborted'),
478
+ { code: isTimeout ? 'TIMEOUT' : 'ABORT' }
479
+ );
480
+ };
369
481
 
370
- return useAsyncData<TResponse, TData>(async () => {
482
+ const state = useAsyncData<TResponse, TData>(async () => {
371
483
  const requestInput = resolveInput(input);
372
484
  const requestUrl =
373
485
  typeof requestInput === 'string' || requestInput instanceof URL
@@ -380,7 +492,7 @@ export const useFetch = <TResponse = unknown, TData = TResponse>(
380
492
  appendQuery(requestUrl, options.query);
381
493
  }
382
494
 
383
- const headers = toHeaders(
495
+ const baseHeaders = toHeaders(
384
496
  fetchConfig?.headers,
385
497
  requestInput instanceof Request ? requestInput.headers : undefined,
386
498
  options.headers
@@ -393,23 +505,51 @@ export const useFetch = <TResponse = unknown, TData = TResponse>(
393
505
  throw new Error(`Cannot send a request body with ${bodylessMethod} requests`);
394
506
  }
395
507
  const requestInitMethod = resolveRequestInitMethod(explicitMethod, requestInput, method);
396
- const requestInit: RequestInit = {
508
+ const retryConfig = normalizeRetryConfig(options.retry);
509
+ const maxAttempts = (retryConfig?.count ?? 0) + 1;
510
+
511
+ // Abort controller: compose timeout + external signal + manual abort
512
+ const abortController = new AbortController();
513
+ currentAbortController = abortController;
514
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
515
+ let didTimeout = false;
516
+ let externalAbortHandler: (() => void) | undefined;
517
+
518
+ if (options.signal) {
519
+ if (options.signal.aborted) {
520
+ abortController.abort(options.signal.reason);
521
+ } else {
522
+ externalAbortHandler = () => abortController.abort(options.signal?.reason);
523
+ options.signal.addEventListener('abort', externalAbortHandler, { once: true });
524
+ }
525
+ }
526
+
527
+ if (options.timeout && options.timeout > 0) {
528
+ timeoutId = setTimeout(() => {
529
+ didTimeout = true;
530
+ abortController.abort(new DOMException('Request timeout', 'TimeoutError'));
531
+ }, options.timeout);
532
+ }
533
+
534
+ const baseRequestInit: Omit<RequestInit, 'body' | 'signal'> = {
397
535
  ...options,
398
536
  method: requestInitMethod,
399
- headers,
400
- body: serializeBody(options.body, headers),
537
+ headers: baseHeaders,
401
538
  };
402
539
 
403
- delete (requestInit as Partial<UseFetchOptions>).baseUrl;
404
- delete (requestInit as Partial<UseFetchOptions>).query;
405
- delete (requestInit as Partial<UseFetchOptions>).parseAs;
406
- delete (requestInit as Partial<UseFetchOptions>).fetcher;
407
- delete (requestInit as Partial<UseFetchOptions>).defaultValue;
408
- delete (requestInit as Partial<UseFetchOptions>).immediate;
409
- delete (requestInit as Partial<UseFetchOptions>).watch;
410
- delete (requestInit as Partial<UseFetchOptions>).transform;
411
- delete (requestInit as Partial<UseFetchOptions>).onSuccess;
412
- delete (requestInit as Partial<UseFetchOptions>).onError;
540
+ delete (baseRequestInit as Partial<UseFetchOptions>).baseUrl;
541
+ delete (baseRequestInit as Partial<UseFetchOptions>).query;
542
+ delete (baseRequestInit as Partial<UseFetchOptions>).parseAs;
543
+ delete (baseRequestInit as Partial<UseFetchOptions>).fetcher;
544
+ delete (baseRequestInit as Partial<UseFetchOptions>).defaultValue;
545
+ delete (baseRequestInit as Partial<UseFetchOptions>).immediate;
546
+ delete (baseRequestInit as Partial<UseFetchOptions>).watch;
547
+ delete (baseRequestInit as Partial<UseFetchOptions>).transform;
548
+ delete (baseRequestInit as Partial<UseFetchOptions>).onSuccess;
549
+ delete (baseRequestInit as Partial<UseFetchOptions>).onError;
550
+ delete (baseRequestInit as Partial<UseFetchOptions>).timeout;
551
+ delete (baseRequestInit as Partial<UseFetchOptions>).retry;
552
+ delete (baseRequestInit as Partial<UseFetchOptions>).validateStatus;
413
553
 
414
554
  let requestTarget: Request | string | URL = requestUrl ?? requestInput;
415
555
  if (
@@ -417,22 +557,103 @@ export const useFetch = <TResponse = unknown, TData = TResponse>(
417
557
  requestUrl &&
418
558
  requestUrl.toString() !== requestInput.url
419
559
  ) {
420
- // Rebuild Request inputs when query params changed so the updated URL is preserved.
421
- // String/URL inputs already use `requestUrl` directly, so only Request objects need rebuilding.
422
560
  requestTarget = new Request(requestUrl.toString(), toRequestInit(requestInput));
423
561
  }
424
- const response = await fetcher(requestTarget, requestInit);
425
-
426
- if (!response.ok) {
427
- throw Object.assign(new Error(`Request failed with status ${response.status}`), {
428
- response,
429
- status: response.status,
430
- statusText: response.statusText,
431
- });
562
+
563
+ const createAttemptRequestInit = (): RequestInit => {
564
+ const headers = new Headers(baseHeaders);
565
+ return {
566
+ ...baseRequestInit,
567
+ headers,
568
+ body: serializeBody(options.body, headers),
569
+ signal: abortController.signal,
570
+ };
571
+ };
572
+
573
+ if (
574
+ maxAttempts > 1 &&
575
+ typeof ReadableStream !== 'undefined' &&
576
+ options.body instanceof ReadableStream
577
+ ) {
578
+ throw new Error('Cannot retry requests with ReadableStream bodies');
432
579
  }
433
580
 
434
- return parseResponse<TResponse>(response, parseAs);
581
+ if (
582
+ maxAttempts > 1 &&
583
+ typeof Request !== 'undefined' &&
584
+ requestTarget instanceof Request &&
585
+ requestTarget.body !== null
586
+ ) {
587
+ throw new Error('Cannot retry requests with non-replayable Request bodies');
588
+ }
589
+ let lastError: Error | undefined;
590
+
591
+ try {
592
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
593
+ try {
594
+ const response = await fetcher(requestTarget, createAttemptRequestInit());
595
+
596
+ if (!validateStatus(response.status)) {
597
+ throw Object.assign(new Error(`Request failed with status ${response.status}`), {
598
+ response,
599
+ status: response.status,
600
+ statusText: response.statusText,
601
+ });
602
+ }
603
+
604
+ return await parseResponse<TResponse>(response, parseAs);
605
+ } catch (error) {
606
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
607
+
608
+ // Abort errors should not be retried
609
+ if (
610
+ abortController.signal.aborted ||
611
+ isAbortDomException(normalizedError) ||
612
+ isTimeoutDomException(normalizedError)
613
+ ) {
614
+ throw normalizeAbortLikeError(
615
+ abortController.signal.aborted ? abortController.signal.reason : normalizedError,
616
+ didTimeout
617
+ );
618
+ }
619
+
620
+ lastError = normalizedError;
621
+
622
+ const shouldRetry = retryConfig
623
+ ? (retryConfig.retryOn ?? DEFAULT_RETRY_ON)(normalizedError, attempt)
624
+ : false;
625
+
626
+ if (!shouldRetry || attempt >= maxAttempts - 1) {
627
+ throw normalizedError;
628
+ }
629
+
630
+ await sleepWithSignal(
631
+ resolveRetryDelay(retryConfig!.delay, attempt),
632
+ abortController.signal
633
+ );
634
+ }
635
+ }
636
+
637
+ throw lastError!;
638
+ } finally {
639
+ if (timeoutId !== undefined) clearTimeout(timeoutId);
640
+ if (options.signal && externalAbortHandler) {
641
+ options.signal.removeEventListener('abort', externalAbortHandler);
642
+ }
643
+ if (currentAbortController === abortController) {
644
+ currentAbortController = null;
645
+ }
646
+ }
435
647
  }, options);
648
+
649
+ // Override abort with real abort logic
650
+ state.abort = (): void => {
651
+ if (currentAbortController) {
652
+ currentAbortController.abort(new DOMException('Request aborted', 'AbortError'));
653
+ }
654
+ };
655
+
656
+ return state;
436
657
  };
437
658
 
438
659
  /**