@bquery/bquery 1.7.0 → 1.8.2

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 +178 -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
@@ -9,16 +9,35 @@ export {
9
9
  Signal,
10
10
  batch,
11
11
  computed,
12
+ createHttp,
13
+ createRequestQueue,
14
+ createRestClient,
12
15
  createUseFetch,
16
+ deduplicateRequest,
13
17
  effect,
18
+ effectScope,
19
+ getCurrentScope,
20
+ http,
21
+ HttpError,
14
22
  isComputed,
15
23
  isSignal,
16
24
  linkedSignal,
25
+ onScopeDispose,
17
26
  persistedSignal,
18
27
  readonly,
19
28
  signal,
29
+ toValue,
20
30
  useAsyncData,
31
+ useEventSource,
21
32
  useFetch,
33
+ useInfiniteFetch,
34
+ usePaginatedFetch,
35
+ usePolling,
36
+ useResource,
37
+ useResourceList,
38
+ useSubmit,
39
+ useWebSocket,
40
+ useWebSocketChannel,
22
41
  untrack,
23
42
  watch,
24
43
  } from './signal';
@@ -27,11 +46,52 @@ export type {
27
46
  AsyncDataState,
28
47
  AsyncDataStatus,
29
48
  AsyncWatchSource,
49
+ ChannelMessage,
50
+ ChannelSubscription,
30
51
  CleanupFn,
52
+ EffectScope,
31
53
  FetchInput,
54
+ HttpClient,
55
+ HttpProgressEvent,
56
+ HttpRequestConfig,
57
+ HttpResponse,
58
+ IdExtractor,
59
+ InfiniteState,
60
+ Interceptor,
61
+ InterceptorManager,
32
62
  LinkedSignal,
63
+ MaybeSignal,
33
64
  Observer,
65
+ EventSourceStatus,
66
+ PaginatedState,
67
+ PollingState,
34
68
  ReadonlySignal,
69
+ ReadonlySignalHandle,
70
+ RequestQueue,
71
+ RequestQueueOptions,
72
+ ResourceListActions,
73
+ RestClient,
74
+ RetryConfig,
35
75
  UseAsyncDataOptions,
76
+ UseEventSourceOptions,
77
+ UseEventSourceReturn,
36
78
  UseFetchOptions,
79
+ UseFetchRetryConfig,
80
+ UseInfiniteFetchOptions,
81
+ UsePaginatedFetchOptions,
82
+ UsePollingOptions,
83
+ UseResourceListOptions,
84
+ UseResourceListReturn,
85
+ UseResourceOptions,
86
+ UseResourceReturn,
87
+ UseSubmitOptions,
88
+ UseSubmitReturn,
89
+ UseWebSocketChannelOptions,
90
+ UseWebSocketChannelReturn,
91
+ UseWebSocketOptions,
92
+ UseWebSocketReturn,
93
+ WebSocketHeartbeatConfig,
94
+ WebSocketReconnectConfig,
95
+ WebSocketSerializer,
96
+ WebSocketStatus,
37
97
  } from './signal';
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Pagination and infinite-scroll composables for reactive data fetching.
3
+ *
4
+ * @module bquery/reactive
5
+ */
6
+
7
+ import { computed } from './computed';
8
+ import { Signal, signal } from './core';
9
+ import {
10
+ useFetch,
11
+ type AsyncDataState,
12
+ type AsyncDataStatus,
13
+ type UseFetchOptions,
14
+ } from './async-data';
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // usePaginatedFetch
18
+ // ---------------------------------------------------------------------------
19
+
20
+ /** Options for usePaginatedFetch(). */
21
+ export interface UsePaginatedFetchOptions<
22
+ TResponse = unknown,
23
+ TData = TResponse,
24
+ > extends UseFetchOptions<TResponse, TData> {
25
+ /** Initial page number (default: 1). */
26
+ initialPage?: number;
27
+ }
28
+
29
+ /** Return value of usePaginatedFetch(). */
30
+ export interface PaginatedState<TData> extends AsyncDataState<TData> {
31
+ /** Current page number signal (writable). */
32
+ page: Signal<number>;
33
+ /** Go to the next page. */
34
+ next: () => Promise<TData | undefined>;
35
+ /** Go to the previous page (minimum 1). */
36
+ prev: () => Promise<TData | undefined>;
37
+ /** Jump to a specific page. */
38
+ goTo: (page: number) => Promise<TData | undefined>;
39
+ }
40
+
41
+ /**
42
+ * Reactive paginated fetch composable.
43
+ *
44
+ * Takes a URL factory receiving the current page number, and exposes
45
+ * `page`, `next()`, `prev()`, and `goTo()` helpers alongside the
46
+ * standard `AsyncDataState`.
47
+ *
48
+ * @template TResponse - Raw parsed response type
49
+ * @template TData - Stored response type after optional transformation
50
+ * @param inputFactory - Function that receives the page number and returns a URL string, URL, or Request
51
+ * @param options - Fetch and pagination options
52
+ * @returns Paginated data state
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * import { usePaginatedFetch } from '@bquery/bquery/reactive';
57
+ *
58
+ * const users = usePaginatedFetch<User[]>(
59
+ * (page) => `/api/users?page=${page}`,
60
+ * { baseUrl: 'https://api.example.com' }
61
+ * );
62
+ *
63
+ * // Navigate pages
64
+ * await users.next();
65
+ * await users.prev();
66
+ * await users.goTo(5);
67
+ * console.log(users.page.value); // 5
68
+ * ```
69
+ */
70
+ export const usePaginatedFetch = <TResponse = unknown, TData = TResponse>(
71
+ inputFactory: (page: number) => string | URL | Request,
72
+ options: UsePaginatedFetchOptions<TResponse, TData> = {}
73
+ ): PaginatedState<TData> => {
74
+ const { initialPage = 1, ...fetchOptions } = options;
75
+ const page = signal(initialPage);
76
+
77
+ const state = useFetch<TResponse, TData>(() => inputFactory(page.value), {
78
+ ...fetchOptions,
79
+ watch: fetchOptions.watch,
80
+ });
81
+
82
+ const next = async (): Promise<TData | undefined> => {
83
+ page.value = page.peek() + 1;
84
+ return state.execute();
85
+ };
86
+
87
+ const prev = async (): Promise<TData | undefined> => {
88
+ const current = page.peek();
89
+ if (current > 1) {
90
+ page.value = current - 1;
91
+ }
92
+ return state.execute();
93
+ };
94
+
95
+ const goTo = async (target: number): Promise<TData | undefined> => {
96
+ page.value = Math.max(1, target);
97
+ return state.execute();
98
+ };
99
+
100
+ return {
101
+ ...state,
102
+ page,
103
+ next,
104
+ prev,
105
+ goTo,
106
+ };
107
+ };
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // useInfiniteFetch
111
+ // ---------------------------------------------------------------------------
112
+
113
+ /** Options for useInfiniteFetch(). */
114
+ export interface UseInfiniteFetchOptions<
115
+ TResponse = unknown,
116
+ TData = TResponse,
117
+ TCursor = number,
118
+ > extends Omit<UseFetchOptions<TResponse, TData>, 'transform'> {
119
+ /** Extract the cursor for the next page from a response. */
120
+ getNextCursor: (lastResponse: TResponse, allPages: TResponse[]) => TCursor | undefined;
121
+ /** Transform all accumulated pages into the final data shape. */
122
+ transform?: (pages: TResponse[]) => TData;
123
+ /** Initial cursor value (default: undefined, meaning first page). */
124
+ initialCursor?: TCursor;
125
+ }
126
+
127
+ /** Return value of useInfiniteFetch(). */
128
+ export interface InfiniteState<TData, TResponse = unknown> {
129
+ /** All accumulated page data, transformed. */
130
+ data: Signal<TData | undefined>;
131
+ /** Raw accumulated pages. */
132
+ pages: Signal<TResponse[]>;
133
+ /** Last error encountered. */
134
+ error: Signal<Error | null>;
135
+ /** Current lifecycle status. */
136
+ status: Signal<AsyncDataStatus>;
137
+ /** Computed boolean that mirrors `status === 'pending'`. */
138
+ pending: { readonly value: boolean; peek(): boolean };
139
+ /** Whether there are more pages to load. */
140
+ hasMore: { readonly value: boolean; peek(): boolean };
141
+ /** Fetch the next page and append it to the accumulated data. */
142
+ fetchNextPage: () => Promise<TData | undefined>;
143
+ /** Reset all pages and re-fetch from the initial cursor. */
144
+ refresh: () => Promise<TData | undefined>;
145
+ /** Clear all accumulated data. */
146
+ clear: () => void;
147
+ /** Dispose reactive watchers and prevent future executions. */
148
+ dispose: () => void;
149
+ }
150
+
151
+ /**
152
+ * Reactive infinite-scroll / load-more composable.
153
+ *
154
+ * Accumulates pages of data and exposes `fetchNextPage()` to load
155
+ * additional results. Uses a cursor-based approach with `getNextCursor()`
156
+ * to determine pagination.
157
+ *
158
+ * @template TResponse - Raw parsed response type for a single page
159
+ * @template TData - Transformed accumulated data type
160
+ * @template TCursor - Cursor type used for pagination
161
+ * @param inputFactory - Function receiving the cursor and returning a FetchInput
162
+ * @param options - Fetch and infinite-scroll options
163
+ * @returns Infinite data state with fetchNextPage(), hasMore, and accumulated pages
164
+ *
165
+ * @example
166
+ * ```ts
167
+ * import { useInfiniteFetch } from '@bquery/bquery/reactive';
168
+ *
169
+ * const feed = useInfiniteFetch<Post[], Post[]>(
170
+ * (cursor) => `/api/posts?cursor=${cursor ?? ''}`,
171
+ * {
172
+ * getNextCursor: (page) => page.length > 0 ? page[page.length - 1].id : undefined,
173
+ * transform: (pages) => pages.flat(),
174
+ * baseUrl: 'https://api.example.com',
175
+ * }
176
+ * );
177
+ *
178
+ * // Load more pages
179
+ * await feed.fetchNextPage();
180
+ * console.log(feed.data.value); // All accumulated posts
181
+ * console.log(feed.hasMore.value); // true if more pages available
182
+ * ```
183
+ */
184
+ export const useInfiniteFetch = <TResponse = unknown, TData = TResponse[], TCursor = number>(
185
+ inputFactory: (cursor: TCursor | undefined) => string | URL | Request,
186
+ options: UseInfiniteFetchOptions<TResponse, TData, TCursor>
187
+ ): InfiniteState<TData, TResponse> => {
188
+ const {
189
+ getNextCursor,
190
+ transform: transformPages,
191
+ initialCursor,
192
+ immediate = true,
193
+ // Keep these callbacks on the infinite-fetch layer instead of forwarding
194
+ // them into the inner per-page useFetch() instance.
195
+ onSuccess: infiniteOnSuccess,
196
+ onError: infiniteOnError,
197
+ ...fetchOptions
198
+ } = options;
199
+
200
+ const pages = signal<TResponse[]>([]);
201
+ const data = signal<TData | undefined>(options.defaultValue);
202
+ const error = signal<Error | null>(null);
203
+ const status = signal<AsyncDataStatus>('idle');
204
+ const pending = computed(() => status.value === 'pending');
205
+ const nextCursor = signal<TCursor | undefined>(initialCursor);
206
+ const hasMore = computed(() => pages.value.length === 0 || nextCursor.value !== undefined);
207
+
208
+ let disposed = false;
209
+ let executionId = 0;
210
+
211
+ const applyTransform = (allPages: TResponse[]): TData => {
212
+ if (transformPages) {
213
+ return transformPages(allPages);
214
+ }
215
+ return allPages as unknown as TData;
216
+ };
217
+
218
+ const fetchNextPage = async (): Promise<TData | undefined> => {
219
+ if (disposed) return data.peek();
220
+
221
+ const currentExecution = ++executionId;
222
+ status.value = 'pending';
223
+ error.value = null;
224
+
225
+ try {
226
+ const cursor = nextCursor.peek();
227
+ const input = inputFactory(cursor);
228
+ const pageState = useFetch<TResponse>(input, {
229
+ ...(fetchOptions as UseFetchOptions<TResponse>),
230
+ immediate: false,
231
+ watch: undefined,
232
+ });
233
+
234
+ const pageData = await pageState.execute();
235
+ const pageError = pageState.error.peek();
236
+ pageState.dispose();
237
+
238
+ if (disposed || currentExecution !== executionId) return data.peek();
239
+
240
+ // Check if the inner fetch encountered an error
241
+ if (pageError) {
242
+ error.value = pageError;
243
+ status.value = 'error';
244
+ infiniteOnError?.(pageError);
245
+ return data.peek();
246
+ }
247
+
248
+ if (pageData !== undefined) {
249
+ const typedPageData = pageData as TResponse;
250
+ const newPages: TResponse[] = [...pages.peek(), typedPageData];
251
+ pages.value = newPages;
252
+
253
+ const newCursor = getNextCursor(typedPageData, newPages);
254
+ nextCursor.value = newCursor;
255
+
256
+ const transformed = applyTransform(newPages);
257
+ data.value = transformed;
258
+ status.value = 'success';
259
+ infiniteOnSuccess?.(transformed);
260
+ return transformed;
261
+ }
262
+
263
+ status.value = 'success';
264
+ return data.peek();
265
+ } catch (caught) {
266
+ if (disposed || currentExecution !== executionId) return data.peek();
267
+
268
+ const normalizedError = caught instanceof Error ? caught : new Error(String(caught));
269
+ error.value = normalizedError;
270
+ status.value = 'error';
271
+ infiniteOnError?.(normalizedError);
272
+ return data.peek();
273
+ }
274
+ };
275
+
276
+ const refresh = async (): Promise<TData | undefined> => {
277
+ pages.value = [];
278
+ nextCursor.value = initialCursor;
279
+ data.value = options.defaultValue;
280
+ error.value = null;
281
+ status.value = 'idle';
282
+ executionId += 1;
283
+ return fetchNextPage();
284
+ };
285
+
286
+ const clear = (): void => {
287
+ executionId += 1;
288
+ pages.value = [];
289
+ nextCursor.value = initialCursor;
290
+ data.value = options.defaultValue;
291
+ error.value = null;
292
+ status.value = 'idle';
293
+ };
294
+
295
+ const dispose = (): void => {
296
+ if (disposed) return;
297
+ disposed = true;
298
+ executionId += 1;
299
+ };
300
+
301
+ if (immediate) {
302
+ void fetchNextPage();
303
+ }
304
+
305
+ return {
306
+ data,
307
+ pages,
308
+ error,
309
+ status,
310
+ pending,
311
+ hasMore,
312
+ fetchNextPage,
313
+ refresh,
314
+ clear,
315
+ dispose,
316
+ };
317
+ };
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Reactive polling composable for periodic data fetching.
3
+ *
4
+ * @module bquery/reactive
5
+ */
6
+
7
+ import { computed } from './computed';
8
+ import { effect } from './effect';
9
+ import { signal } from './core';
10
+ import { untrack } from './untrack';
11
+ import { useFetch, type AsyncDataState, type FetchInput, type UseFetchOptions } from './async-data';
12
+
13
+ /** Options for usePolling(). */
14
+ export interface UsePollingOptions<TResponse = unknown, TData = TResponse> extends UseFetchOptions<
15
+ TResponse,
16
+ TData
17
+ > {
18
+ /** Polling interval in milliseconds. */
19
+ interval: number;
20
+ /** Whether polling is initially enabled (default: true). Can be a reactive getter. */
21
+ enabled?: boolean | (() => boolean);
22
+ /** Pause polling when the document is hidden (default: true). */
23
+ pauseOnHidden?: boolean;
24
+ /** Pause polling when the browser is offline (default: true). */
25
+ pauseOnOffline?: boolean;
26
+ }
27
+
28
+ /** Extended return value from usePolling(). */
29
+ export interface PollingState<TData> extends AsyncDataState<TData> {
30
+ /** Pause polling. */
31
+ pause: () => void;
32
+ /** Resume polling. */
33
+ resume: () => void;
34
+ /** Reactive boolean indicating whether polling is currently active. */
35
+ isActive: { readonly value: boolean; peek(): boolean };
36
+ }
37
+
38
+ /**
39
+ * Reactive polling composable that periodically fetches data.
40
+ *
41
+ * @template TResponse - Raw parsed response type
42
+ * @template TData - Stored response type after optional transformation
43
+ * @param input - Request URL, Request object, or lazy input factory
44
+ * @param options - Polling and fetch options
45
+ * @returns Extended fetch state with pause(), resume(), and isActive
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * import { usePolling } from '@bquery/bquery/reactive';
50
+ *
51
+ * const notifications = usePolling<Notification[]>('/api/notifications', {
52
+ * interval: 30_000,
53
+ * pauseOnHidden: true,
54
+ * pauseOnOffline: true,
55
+ * });
56
+ *
57
+ * // Manually pause/resume
58
+ * notifications.pause();
59
+ * notifications.resume();
60
+ * ```
61
+ */
62
+ export const usePolling = <TResponse = unknown, TData = TResponse>(
63
+ input: FetchInput,
64
+ options: UsePollingOptions<TResponse, TData>
65
+ ): PollingState<TData> => {
66
+ const {
67
+ interval,
68
+ enabled: enabledOption = true,
69
+ pauseOnHidden = true,
70
+ pauseOnOffline = true,
71
+ immediate = true,
72
+ ...fetchOptions
73
+ } = options;
74
+
75
+ if (!Number.isFinite(interval) || interval < 1) {
76
+ throw new Error('Polling interval must be a finite number of at least 1');
77
+ }
78
+
79
+ const manuallyPaused = signal(false);
80
+ const documentHidden = signal(false);
81
+ const browserOffline = signal(false);
82
+
83
+ const enabledGetter = typeof enabledOption === 'function' ? enabledOption : () => enabledOption;
84
+
85
+ const isActive = computed(
86
+ () =>
87
+ enabledGetter() &&
88
+ !manuallyPaused.value &&
89
+ !(pauseOnHidden && documentHidden.value) &&
90
+ !(pauseOnOffline && browserOffline.value)
91
+ );
92
+
93
+ // Create the underlying useFetch with immediate control
94
+ const fetchState = useFetch<TResponse, TData>(input, {
95
+ ...fetchOptions,
96
+ immediate: immediate && enabledGetter(),
97
+ });
98
+
99
+ let intervalId: ReturnType<typeof setInterval> | undefined;
100
+ let cleanups: Array<() => void> = [];
101
+
102
+ const startPolling = (): void => {
103
+ stopPolling();
104
+ intervalId = setInterval(() => {
105
+ void fetchState.execute();
106
+ }, interval);
107
+ };
108
+
109
+ const stopPolling = (): void => {
110
+ if (intervalId !== undefined) {
111
+ clearInterval(intervalId);
112
+ intervalId = undefined;
113
+ }
114
+ };
115
+
116
+ // Watch isActive and start/stop polling accordingly
117
+ const stopWatcher = effect(() => {
118
+ const active = isActive.value;
119
+ untrack(() => {
120
+ if (active) {
121
+ startPolling();
122
+ } else {
123
+ stopPolling();
124
+ }
125
+ });
126
+ });
127
+
128
+ // Listen for visibility changes
129
+ if (pauseOnHidden && typeof document !== 'undefined') {
130
+ documentHidden.value = document.hidden;
131
+ const onVisibilityChange = (): void => {
132
+ documentHidden.value = document.hidden;
133
+ };
134
+ document.addEventListener('visibilitychange', onVisibilityChange);
135
+ cleanups.push(() => document.removeEventListener('visibilitychange', onVisibilityChange));
136
+ }
137
+
138
+ // Listen for online/offline changes
139
+ if (pauseOnOffline && typeof window !== 'undefined') {
140
+ const onOnline = (): void => {
141
+ browserOffline.value = false;
142
+ };
143
+ const onOffline = (): void => {
144
+ browserOffline.value = true;
145
+ };
146
+ window.addEventListener('online', onOnline);
147
+ window.addEventListener('offline', onOffline);
148
+ cleanups.push(() => {
149
+ window.removeEventListener('online', onOnline);
150
+ window.removeEventListener('offline', onOffline);
151
+ });
152
+ browserOffline.value =
153
+ typeof navigator !== 'undefined' && navigator.onLine !== undefined
154
+ ? !navigator.onLine
155
+ : false;
156
+ }
157
+
158
+ const originalDispose = fetchState.dispose;
159
+
160
+ const dispose = (): void => {
161
+ stopPolling();
162
+ stopWatcher();
163
+ for (const cleanup of cleanups) cleanup();
164
+ cleanups = [];
165
+ originalDispose();
166
+ };
167
+
168
+ return {
169
+ ...fetchState,
170
+ pause: () => {
171
+ manuallyPaused.value = true;
172
+ },
173
+ resume: () => {
174
+ manuallyPaused.value = false;
175
+ },
176
+ isActive,
177
+ dispose,
178
+ };
179
+ };
@@ -4,6 +4,13 @@
4
4
 
5
5
  import type { Signal } from './core';
6
6
 
7
+ const READONLY_SIGNAL_BRAND: unique symbol = Symbol('bquery.readonlySignal');
8
+
9
+ /** @internal */
10
+ type ReadonlySignalWrapper<T> = ReadonlySignal<T> & {
11
+ readonly [READONLY_SIGNAL_BRAND]: true;
12
+ };
13
+
7
14
  /**
8
15
  * A readonly wrapper around a signal that prevents writes.
9
16
  * Provides read-only access to a signal's value while maintaining reactivity.
@@ -17,6 +24,19 @@ export interface ReadonlySignal<T> {
17
24
  peek(): T;
18
25
  }
19
26
 
27
+ /**
28
+ * Determines whether a value is a bQuery readonly signal wrapper.
29
+ *
30
+ * @internal
31
+ */
32
+ export const isReadonlySignal = <T>(value: unknown): value is ReturnType<typeof readonly<T>> => {
33
+ return (
34
+ typeof value === 'object' &&
35
+ value !== null &&
36
+ Object.prototype.hasOwnProperty.call(value, READONLY_SIGNAL_BRAND)
37
+ );
38
+ };
39
+
20
40
  /**
21
41
  * Creates a read-only view of a signal.
22
42
  * Useful for exposing reactive state without allowing modifications.
@@ -25,11 +45,35 @@ export interface ReadonlySignal<T> {
25
45
  * @param sig - The signal to wrap
26
46
  * @returns A readonly signal wrapper
27
47
  */
28
- export const readonly = <T>(sig: Signal<T>): ReadonlySignal<T> => ({
29
- get value(): T {
30
- return sig.value;
31
- },
32
- peek(): T {
33
- return sig.peek();
34
- },
35
- });
48
+ export const readonly = <T>(sig: Signal<T>): ReadonlySignalWrapper<T> =>
49
+ Object.defineProperties(
50
+ {},
51
+ {
52
+ value: {
53
+ get(): T {
54
+ return sig.value;
55
+ },
56
+ enumerable: true,
57
+ },
58
+ peek: {
59
+ value(): T {
60
+ return sig.peek();
61
+ },
62
+ enumerable: true,
63
+ },
64
+ [READONLY_SIGNAL_BRAND]: {
65
+ value: true,
66
+ enumerable: false,
67
+ configurable: false,
68
+ writable: false,
69
+ },
70
+ }
71
+ ) as ReadonlySignalWrapper<T>;
72
+
73
+ /**
74
+ * Branded readonly wrapper type produced by {@link readonly}.
75
+ *
76
+ * Useful for APIs that compose additional behavior on top of a readonly signal
77
+ * without widening to arbitrary structural `{ value, peek }` objects.
78
+ */
79
+ export type ReadonlySignalHandle<T> = ReturnType<typeof readonly<T>>;