@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.
- package/README.md +760 -716
- package/dist/{a11y-C5QOVvRn.js → a11y-DVBCy09c.js} +3 -3
- package/dist/a11y-DVBCy09c.js.map +1 -0
- package/dist/a11y.es.mjs +1 -1
- package/dist/component/library.d.ts.map +1 -1
- package/dist/{component-CuuTijA6.js → component-L3-JfOFz.js} +5 -5
- package/dist/component-L3-JfOFz.js.map +1 -0
- package/dist/component.es.mjs +1 -1
- package/dist/{config-BW35FKuA.js → config-DhT9auRm.js} +1 -1
- package/dist/{config-BW35FKuA.js.map → config-DhT9auRm.js.map} +1 -1
- package/dist/{constraints-3lV9yyBw.js → constraints-D5RHQLmP.js} +1 -1
- package/dist/constraints-D5RHQLmP.js.map +1 -0
- package/dist/core/collection.d.ts +86 -0
- package/dist/core/collection.d.ts.map +1 -1
- package/dist/core/element.d.ts +28 -0
- package/dist/core/element.d.ts.map +1 -1
- package/dist/core/shared.d.ts +6 -0
- package/dist/core/shared.d.ts.map +1 -1
- package/dist/core-DdtZHzsS.js +168 -0
- package/dist/core-DdtZHzsS.js.map +1 -0
- package/dist/{core-Cjl7GUu8.js → core-EMYSLzaT.js} +289 -259
- package/dist/core-EMYSLzaT.js.map +1 -0
- package/dist/core.es.mjs +48 -47
- package/dist/{custom-directives-7wAShnnd.js → custom-directives-Dr4C5lVV.js} +1 -1
- package/dist/custom-directives-Dr4C5lVV.js.map +1 -0
- package/dist/{devtools-D2fQLhDN.js → devtools-BhB2iDPT.js} +2 -2
- package/dist/devtools-BhB2iDPT.js.map +1 -0
- package/dist/devtools.es.mjs +1 -1
- package/dist/{dnd-B8EgyzaI.js → dnd-NwZBYh4l.js} +1 -1
- package/dist/dnd-NwZBYh4l.js.map +1 -0
- package/dist/dnd.es.mjs +1 -1
- package/dist/{env-NeVmr4Gf.js → env-CTdvLaH2.js} +1 -1
- package/dist/env-CTdvLaH2.js.map +1 -0
- package/dist/forms/create-form.d.ts.map +1 -1
- package/dist/forms/index.d.ts +3 -2
- package/dist/forms/index.d.ts.map +1 -1
- package/dist/forms/types.d.ts +46 -0
- package/dist/forms/types.d.ts.map +1 -1
- package/dist/forms/use-field.d.ts +34 -0
- package/dist/forms/use-field.d.ts.map +1 -0
- package/dist/forms/validators.d.ts +25 -0
- package/dist/forms/validators.d.ts.map +1 -1
- package/dist/forms-UcRHsYxC.js +227 -0
- package/dist/forms-UcRHsYxC.js.map +1 -0
- package/dist/forms.es.mjs +14 -12
- package/dist/full.d.ts +17 -26
- package/dist/full.d.ts.map +1 -1
- package/dist/full.es.mjs +206 -181
- package/dist/full.iife.js +33 -33
- package/dist/full.iife.js.map +1 -1
- package/dist/full.umd.js +33 -33
- package/dist/full.umd.js.map +1 -1
- package/dist/function-Cybd57JV.js +33 -0
- package/dist/function-Cybd57JV.js.map +1 -0
- package/dist/{i18n-BnnhTFOS.js → i18n-kuF6Ekj6.js} +3 -3
- package/dist/i18n-kuF6Ekj6.js.map +1 -0
- package/dist/i18n.es.mjs +1 -1
- package/dist/index.es.mjs +251 -228
- package/dist/media/breakpoints.d.ts.map +1 -1
- package/dist/media/types.d.ts +2 -2
- package/dist/media/types.d.ts.map +1 -1
- package/dist/{media-Di2Ta22s.js → media-i-fB5WxI.js} +3 -3
- package/dist/media-i-fB5WxI.js.map +1 -0
- package/dist/media.es.mjs +1 -1
- package/dist/{motion-qPj_TYGv.js → motion-BJsAuULb.js} +2 -2
- package/dist/motion-BJsAuULb.js.map +1 -0
- package/dist/motion.es.mjs +1 -1
- package/dist/{mount-SM07RUa6.js → mount-B4Y8bk8Z.js} +5 -5
- package/dist/mount-B4Y8bk8Z.js.map +1 -0
- package/dist/{platform-CPbCprb6.js → platform-Dw2gE3zI.js} +3 -3
- package/dist/{platform-CPbCprb6.js.map → platform-Dw2gE3zI.js.map} +1 -1
- package/dist/platform.es.mjs +2 -2
- package/dist/plugin/registry.d.ts.map +1 -1
- package/dist/{plugin-cPoOHFLY.js → plugin-C2WuC8SF.js} +20 -18
- package/dist/plugin-C2WuC8SF.js.map +1 -0
- package/dist/plugin.es.mjs +1 -1
- package/dist/reactive/async-data.d.ts +28 -3
- package/dist/reactive/async-data.d.ts.map +1 -1
- package/dist/reactive/computed.d.ts +3 -0
- package/dist/reactive/computed.d.ts.map +1 -1
- package/dist/reactive/effect.d.ts +3 -0
- package/dist/reactive/effect.d.ts.map +1 -1
- package/dist/reactive/http.d.ts +194 -0
- package/dist/reactive/http.d.ts.map +1 -0
- package/dist/reactive/index.d.ts +2 -2
- package/dist/reactive/index.d.ts.map +1 -1
- package/dist/reactive/pagination.d.ts +126 -0
- package/dist/reactive/pagination.d.ts.map +1 -0
- package/dist/reactive/polling.d.ts +55 -0
- package/dist/reactive/polling.d.ts.map +1 -0
- package/dist/reactive/readonly.d.ts +20 -1
- package/dist/reactive/readonly.d.ts.map +1 -1
- package/dist/reactive/rest.d.ts +293 -0
- package/dist/reactive/rest.d.ts.map +1 -0
- package/dist/reactive/scope.d.ts +140 -0
- package/dist/reactive/scope.d.ts.map +1 -0
- package/dist/reactive/signal.d.ts +16 -2
- package/dist/reactive/signal.d.ts.map +1 -1
- package/dist/reactive/to-value.d.ts +57 -0
- package/dist/reactive/to-value.d.ts.map +1 -0
- package/dist/reactive/websocket.d.ts +285 -0
- package/dist/reactive/websocket.d.ts.map +1 -0
- package/dist/reactive-DwkhUJfP.js +1148 -0
- package/dist/reactive-DwkhUJfP.js.map +1 -0
- package/dist/reactive.es.mjs +38 -19
- package/dist/{registry-CWf368tT.js → registry-B08iilIh.js} +1 -1
- package/dist/{registry-CWf368tT.js.map → registry-B08iilIh.js.map} +1 -1
- package/dist/router/constraints.d.ts.map +1 -1
- package/dist/router/index.d.ts +1 -1
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/state.d.ts +25 -2
- package/dist/router/state.d.ts.map +1 -1
- package/dist/router-CQikC9Ed.js +492 -0
- package/dist/router-CQikC9Ed.js.map +1 -0
- package/dist/router.es.mjs +9 -8
- package/dist/ssr/hydrate.d.ts.map +1 -1
- package/dist/{ssr-B2qd_WBB.js → ssr-_dAcGdzu.js} +4 -4
- package/dist/ssr-_dAcGdzu.js.map +1 -0
- package/dist/ssr.es.mjs +1 -1
- package/dist/store/persisted.d.ts.map +1 -1
- package/dist/{store-DWpyH6p5.js → store-Cb3gPRve.js} +7 -7
- package/dist/store-Cb3gPRve.js.map +1 -0
- package/dist/store.es.mjs +2 -2
- package/dist/storybook.es.mjs.map +1 -1
- package/dist/{testing-CsqjNUyy.js → testing-C5Sjfsna.js} +8 -8
- package/dist/testing-C5Sjfsna.js.map +1 -0
- package/dist/testing.es.mjs +1 -1
- package/dist/{type-guards-Do9DWgNp.js → type-guards-BMX2c0LP.js} +1 -1
- package/dist/{type-guards-Do9DWgNp.js.map → type-guards-BMX2c0LP.js.map} +1 -1
- package/dist/untrack-D0fnO5k2.js +36 -0
- package/dist/untrack-D0fnO5k2.js.map +1 -0
- package/dist/view/custom-directives.d.ts.map +1 -1
- package/dist/view.es.mjs +4 -4
- package/package.json +177 -177
- package/src/a11y/announce.ts +131 -131
- package/src/a11y/audit.ts +314 -314
- package/src/a11y/index.ts +68 -68
- package/src/a11y/media-preferences.ts +255 -255
- package/src/a11y/roving-tab-index.ts +164 -164
- package/src/a11y/skip-link.ts +255 -255
- package/src/a11y/trap-focus.ts +184 -184
- package/src/a11y/types.ts +183 -183
- package/src/component/component.ts +599 -599
- package/src/component/html.ts +153 -153
- package/src/component/index.ts +52 -52
- package/src/component/library.ts +540 -542
- package/src/component/scope.ts +212 -212
- package/src/component/types.ts +310 -310
- package/src/core/collection.ts +876 -707
- package/src/core/element.ts +1015 -981
- package/src/core/env.ts +60 -60
- package/src/core/index.ts +49 -49
- package/src/core/shared.ts +77 -62
- package/src/core/utils/index.ts +148 -148
- package/src/devtools/devtools.ts +410 -410
- package/src/devtools/index.ts +48 -48
- package/src/devtools/types.ts +104 -104
- package/src/dnd/draggable.ts +296 -296
- package/src/dnd/droppable.ts +228 -228
- package/src/dnd/index.ts +62 -62
- package/src/dnd/sortable.ts +307 -307
- package/src/dnd/types.ts +293 -293
- package/src/forms/create-form.ts +320 -278
- package/src/forms/index.ts +70 -65
- package/src/forms/types.ts +203 -154
- package/src/forms/use-field.ts +231 -0
- package/src/forms/validators.ts +294 -265
- package/src/full.ts +554 -480
- package/src/i18n/formatting.ts +67 -67
- package/src/i18n/i18n.ts +200 -200
- package/src/i18n/index.ts +67 -67
- package/src/i18n/translate.ts +182 -182
- package/src/i18n/types.ts +171 -171
- package/src/index.ts +108 -108
- package/src/media/battery.ts +116 -116
- package/src/media/breakpoints.ts +129 -131
- package/src/media/clipboard.ts +80 -80
- package/src/media/device-sensors.ts +158 -158
- package/src/media/geolocation.ts +119 -119
- package/src/media/index.ts +76 -76
- package/src/media/media-query.ts +92 -92
- package/src/media/network.ts +115 -115
- package/src/media/types.ts +177 -177
- package/src/media/viewport.ts +84 -84
- package/src/motion/index.ts +57 -57
- package/src/motion/morph.ts +151 -151
- package/src/motion/parallax.ts +120 -120
- package/src/motion/reduced-motion.ts +66 -66
- package/src/motion/types.ts +271 -271
- package/src/motion/typewriter.ts +164 -164
- package/src/plugin/index.ts +37 -37
- package/src/plugin/registry.ts +284 -269
- package/src/plugin/types.ts +137 -137
- package/src/reactive/async-data.ts +250 -29
- package/src/reactive/computed.ts +144 -130
- package/src/reactive/effect.ts +29 -6
- package/src/reactive/http.ts +790 -0
- package/src/reactive/index.ts +60 -0
- package/src/reactive/pagination.ts +317 -0
- package/src/reactive/polling.ts +179 -0
- package/src/reactive/readonly.ts +52 -8
- package/src/reactive/rest.ts +859 -0
- package/src/reactive/scope.ts +276 -0
- package/src/reactive/signal.ts +61 -1
- package/src/reactive/to-value.ts +71 -0
- package/src/reactive/websocket.ts +849 -0
- package/src/router/bq-link.ts +279 -279
- package/src/router/constraints.ts +204 -201
- package/src/router/index.ts +49 -49
- package/src/router/match.ts +312 -312
- package/src/router/path-pattern.ts +52 -52
- package/src/router/query.ts +38 -38
- package/src/router/router.ts +421 -402
- package/src/router/state.ts +51 -3
- package/src/router/types.ts +139 -139
- package/src/router/use-route.ts +68 -68
- package/src/router/utils.ts +157 -157
- package/src/security/index.ts +12 -12
- package/src/ssr/hydrate.ts +84 -82
- package/src/ssr/index.ts +70 -70
- package/src/ssr/render.ts +508 -508
- package/src/ssr/serialize.ts +296 -296
- package/src/ssr/types.ts +81 -81
- package/src/store/create-store.ts +467 -467
- package/src/store/index.ts +27 -27
- package/src/store/persisted.ts +245 -249
- package/src/store/types.ts +247 -247
- package/src/store/utils.ts +135 -135
- package/src/storybook/index.ts +480 -480
- package/src/testing/index.ts +42 -42
- package/src/testing/testing.ts +593 -593
- package/src/testing/types.ts +170 -170
- package/src/view/custom-directives.ts +28 -30
- package/src/view/evaluate.ts +292 -292
- package/src/view/process.ts +108 -108
- package/dist/a11y-C5QOVvRn.js.map +0 -1
- package/dist/component-CuuTijA6.js.map +0 -1
- package/dist/constraints-3lV9yyBw.js.map +0 -1
- package/dist/core-Cjl7GUu8.js.map +0 -1
- package/dist/core-DnlyjbF2.js +0 -112
- package/dist/core-DnlyjbF2.js.map +0 -1
- package/dist/custom-directives-7wAShnnd.js.map +0 -1
- package/dist/devtools-D2fQLhDN.js.map +0 -1
- package/dist/dnd-B8EgyzaI.js.map +0 -1
- package/dist/env-NeVmr4Gf.js.map +0 -1
- package/dist/forms-C3yovgH9.js +0 -141
- package/dist/forms-C3yovgH9.js.map +0 -1
- package/dist/i18n-BnnhTFOS.js.map +0 -1
- package/dist/media-Di2Ta22s.js.map +0 -1
- package/dist/motion-qPj_TYGv.js.map +0 -1
- package/dist/mount-SM07RUa6.js.map +0 -1
- package/dist/plugin-cPoOHFLY.js.map +0 -1
- package/dist/reactive-Cfv0RK6x.js +0 -233
- package/dist/reactive-Cfv0RK6x.js.map +0 -1
- package/dist/router-BrthaP_z.js +0 -473
- package/dist/router-BrthaP_z.js.map +0 -1
- package/dist/ssr-B2qd_WBB.js.map +0 -1
- package/dist/store-DWpyH6p5.js.map +0 -1
- package/dist/testing-CsqjNUyy.js.map +0 -1
- package/dist/untrack-DJVQQ2WM.js +0 -33
- package/dist/untrack-DJVQQ2WM.js.map +0 -1
package/src/reactive/index.ts
CHANGED
|
@@ -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
|
+
};
|
package/src/reactive/readonly.ts
CHANGED
|
@@ -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>):
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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>>;
|