@bquery/bquery 1.4.0 → 1.5.0
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 +139 -120
- package/dist/component/component.d.ts.map +1 -1
- package/dist/component/index.d.ts +2 -0
- package/dist/component/index.d.ts.map +1 -1
- package/dist/component/library.d.ts +34 -0
- package/dist/component/library.d.ts.map +1 -0
- package/dist/component/types.d.ts +10 -6
- package/dist/component/types.d.ts.map +1 -1
- package/dist/component-CY5MVoYN.js +531 -0
- package/dist/component-CY5MVoYN.js.map +1 -0
- package/dist/component.es.mjs +6 -184
- package/dist/config-DRmZZno3.js +40 -0
- package/dist/config-DRmZZno3.js.map +1 -0
- package/dist/core-CK2Mfpf4.js +648 -0
- package/dist/core-CK2Mfpf4.js.map +1 -0
- package/dist/core-DPdbItcq.js +112 -0
- package/dist/core-DPdbItcq.js.map +1 -0
- package/dist/core.es.mjs +45 -1261
- package/dist/full.d.ts +6 -6
- package/dist/full.d.ts.map +1 -1
- package/dist/full.es.mjs +98 -92
- package/dist/full.iife.js +173 -3
- package/dist/full.iife.js.map +1 -1
- package/dist/full.umd.js +173 -3
- package/dist/full.umd.js.map +1 -1
- package/dist/index.es.mjs +143 -139
- package/dist/motion/transition.d.ts +1 -1
- package/dist/motion/transition.d.ts.map +1 -1
- package/dist/motion/types.d.ts +11 -1
- package/dist/motion/types.d.ts.map +1 -1
- package/dist/motion-C5DRdPnO.js +415 -0
- package/dist/motion-C5DRdPnO.js.map +1 -0
- package/dist/motion.es.mjs +25 -361
- package/dist/object-qGpWr6-J.js +38 -0
- package/dist/object-qGpWr6-J.js.map +1 -0
- package/dist/platform/announcer.d.ts +59 -0
- package/dist/platform/announcer.d.ts.map +1 -0
- package/dist/platform/config.d.ts +92 -0
- package/dist/platform/config.d.ts.map +1 -0
- package/dist/platform/cookies.d.ts +45 -0
- package/dist/platform/cookies.d.ts.map +1 -0
- package/dist/platform/index.d.ts +8 -0
- package/dist/platform/index.d.ts.map +1 -1
- package/dist/platform/meta.d.ts +62 -0
- package/dist/platform/meta.d.ts.map +1 -0
- package/dist/platform-B7JhGBc7.js +361 -0
- package/dist/platform-B7JhGBc7.js.map +1 -0
- package/dist/platform.es.mjs +11 -248
- package/dist/reactive/async-data.d.ts +114 -0
- package/dist/reactive/async-data.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/signal.d.ts +2 -0
- package/dist/reactive/signal.d.ts.map +1 -1
- package/dist/reactive-BDya-ia8.js +253 -0
- package/dist/reactive-BDya-ia8.js.map +1 -0
- package/dist/reactive.es.mjs +18 -34
- package/dist/router-CijiICxt.js +188 -0
- package/dist/router-CijiICxt.js.map +1 -0
- package/dist/router.es.mjs +11 -200
- package/dist/sanitize-jyJ2ryE2.js +302 -0
- package/dist/sanitize-jyJ2ryE2.js.map +1 -0
- package/dist/security/constants.d.ts.map +1 -1
- package/dist/security.es.mjs +10 -56
- package/dist/store-CPK9E62U.js +262 -0
- package/dist/store-CPK9E62U.js.map +1 -0
- package/dist/store.es.mjs +12 -25
- package/dist/view-Cdi0g-qo.js +396 -0
- package/dist/view-Cdi0g-qo.js.map +1 -0
- package/dist/view.es.mjs +10 -430
- package/package.json +15 -11
- package/src/component/component.ts +319 -289
- package/src/component/index.ts +42 -40
- package/src/component/library.ts +504 -0
- package/src/component/types.ts +91 -85
- package/src/core/collection.ts +628 -628
- package/src/core/element.ts +774 -774
- package/src/core/index.ts +48 -48
- package/src/core/utils/function.ts +151 -151
- package/src/full.ts +223 -187
- package/src/motion/animate.ts +113 -113
- package/src/motion/flip.ts +176 -176
- package/src/motion/scroll.ts +57 -57
- package/src/motion/spring.ts +150 -150
- package/src/motion/timeline.ts +246 -246
- package/src/motion/transition.ts +53 -7
- package/src/motion/types.ts +208 -198
- package/src/platform/announcer.ts +208 -0
- package/src/platform/config.ts +163 -0
- package/src/platform/cookies.ts +165 -0
- package/src/platform/index.ts +39 -18
- package/src/platform/meta.ts +168 -0
- package/src/platform/storage.ts +215 -215
- package/src/reactive/async-data.ts +486 -0
- package/src/reactive/core.ts +114 -114
- package/src/reactive/effect.ts +54 -54
- package/src/reactive/index.ts +37 -23
- package/src/reactive/internals.ts +122 -122
- package/src/reactive/signal.ts +29 -20
- package/src/security/constants.ts +211 -209
- package/src/security/sanitize-core.ts +364 -364
- package/src/view/evaluate.ts +290 -290
- package/dist/batch-x7b2eZST.js +0 -13
- package/dist/batch-x7b2eZST.js.map +0 -1
- package/dist/component.es.mjs.map +0 -1
- package/dist/core-BhpuvPhy.js +0 -170
- package/dist/core-BhpuvPhy.js.map +0 -1
- package/dist/core.es.mjs.map +0 -1
- package/dist/full.es.mjs.map +0 -1
- package/dist/index.es.mjs.map +0 -1
- package/dist/motion.es.mjs.map +0 -1
- package/dist/persisted-DHoi3uEs.js +0 -278
- package/dist/persisted-DHoi3uEs.js.map +0 -1
- package/dist/platform.es.mjs.map +0 -1
- package/dist/reactive.es.mjs.map +0 -1
- package/dist/router.es.mjs.map +0 -1
- package/dist/sanitize-Cxvxa-DX.js +0 -283
- package/dist/sanitize-Cxvxa-DX.js.map +0 -1
- package/dist/security.es.mjs.map +0 -1
- package/dist/store.es.mjs.map +0 -1
- package/dist/type-guards-BdKlYYlS.js +0 -32
- package/dist/type-guards-BdKlYYlS.js.map +0 -1
- package/dist/untrack-DNnnqdlR.js +0 -6
- package/dist/untrack-DNnnqdlR.js.map +0 -1
- package/dist/view.es.mjs.map +0 -1
- package/dist/watch-DXXv3iAI.js +0 -58
- package/dist/watch-DXXv3iAI.js.map +0 -1
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async data and fetch composables built on bQuery signals.
|
|
3
|
+
*
|
|
4
|
+
* @module bquery/reactive
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { merge } from '../core/utils/object';
|
|
8
|
+
import { getBqueryConfig, type BqueryFetchParseAs } from '../platform/config';
|
|
9
|
+
import { computed } from './computed';
|
|
10
|
+
import { effect } from './effect';
|
|
11
|
+
import { Signal, signal } from './core';
|
|
12
|
+
import { untrack } from './untrack';
|
|
13
|
+
|
|
14
|
+
/** Allowed status values for async composables. */
|
|
15
|
+
export type AsyncDataStatus = 'idle' | 'pending' | 'success' | 'error';
|
|
16
|
+
|
|
17
|
+
/** Reactive source types that can trigger refreshes. */
|
|
18
|
+
export type AsyncWatchSource = (() => unknown) | { value: unknown };
|
|
19
|
+
|
|
20
|
+
/** Options shared by async composables. */
|
|
21
|
+
export interface UseAsyncDataOptions<TResult, TData = TResult> {
|
|
22
|
+
/** Run the handler immediately (default: true). */
|
|
23
|
+
immediate?: boolean;
|
|
24
|
+
/** Default data value before the first successful execution. */
|
|
25
|
+
defaultValue?: TData;
|
|
26
|
+
/** Optional reactive sources that trigger refreshes when they change. */
|
|
27
|
+
watch?: AsyncWatchSource[];
|
|
28
|
+
/** Transform the resolved value before storing it. */
|
|
29
|
+
transform?: (value: TResult) => TData;
|
|
30
|
+
/** Called after a successful execution. */
|
|
31
|
+
onSuccess?: (value: TData) => void;
|
|
32
|
+
/** Called after a failed execution. */
|
|
33
|
+
onError?: (error: Error) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Return value of useAsyncData() and useFetch(). */
|
|
37
|
+
export interface AsyncDataState<TData> {
|
|
38
|
+
/** Reactive data signal. */
|
|
39
|
+
data: Signal<TData | undefined>;
|
|
40
|
+
/** Last error encountered by the composable. */
|
|
41
|
+
error: Signal<Error | null>;
|
|
42
|
+
/** Current lifecycle status. */
|
|
43
|
+
status: Signal<AsyncDataStatus>;
|
|
44
|
+
/** Computed boolean that mirrors `status === 'pending'`. */
|
|
45
|
+
pending: { readonly value: boolean; peek(): boolean };
|
|
46
|
+
/** Execute the handler manually. Returns the cached data value when called after dispose(). */
|
|
47
|
+
execute: () => Promise<TData | undefined>;
|
|
48
|
+
/** Alias for execute(). */
|
|
49
|
+
refresh: () => Promise<TData | undefined>;
|
|
50
|
+
/** Clear data, error, and status back to the initial state. */
|
|
51
|
+
clear: () => void;
|
|
52
|
+
/** Dispose reactive watchers and prevent future executions. */
|
|
53
|
+
dispose: () => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Options for useFetch(). */
|
|
57
|
+
export interface UseFetchOptions<TResponse = unknown, TData = TResponse>
|
|
58
|
+
extends UseAsyncDataOptions<TResponse, TData>, Omit<RequestInit, 'body' | 'headers'> {
|
|
59
|
+
/** Base URL prepended to relative URLs. */
|
|
60
|
+
baseUrl?: string;
|
|
61
|
+
/** Query parameters appended to the request URL. */
|
|
62
|
+
query?: Record<string, unknown>;
|
|
63
|
+
/** Request headers. */
|
|
64
|
+
headers?: HeadersInit;
|
|
65
|
+
/** Request body, including plain objects for JSON requests. */
|
|
66
|
+
body?: BodyInit | Record<string, unknown> | unknown[] | null;
|
|
67
|
+
/** Override the parser used for the response body. */
|
|
68
|
+
parseAs?: BqueryFetchParseAs;
|
|
69
|
+
/** Custom fetch implementation for testing or adapters. */
|
|
70
|
+
fetcher?: typeof fetch;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Input accepted by useFetch(). */
|
|
74
|
+
export type FetchInput = string | URL | Request | (() => string | URL | Request);
|
|
75
|
+
|
|
76
|
+
const normalizeError = (error: unknown): Error => {
|
|
77
|
+
if (error instanceof Error) return error;
|
|
78
|
+
if (typeof error === 'string') {
|
|
79
|
+
return new Error(error);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
return new Error(JSON.stringify(error));
|
|
84
|
+
} catch {
|
|
85
|
+
return new Error(String(error));
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const readWatchSource = (source: AsyncWatchSource): unknown => {
|
|
90
|
+
if (typeof source === 'function') {
|
|
91
|
+
return source();
|
|
92
|
+
}
|
|
93
|
+
return source.value;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const toHeaders = (...sources: Array<HeadersInit | undefined>): Headers => {
|
|
97
|
+
const headers = new Headers();
|
|
98
|
+
for (const source of sources) {
|
|
99
|
+
if (!source) continue;
|
|
100
|
+
new Headers(source).forEach((value, key) => {
|
|
101
|
+
headers.set(key, value);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return headers;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const isBodyLike = (value: unknown): value is BodyInit => {
|
|
108
|
+
if (typeof value === 'string') return true;
|
|
109
|
+
if (value instanceof Blob || value instanceof FormData || value instanceof URLSearchParams) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
if (typeof ArrayBuffer !== 'undefined' && value instanceof ArrayBuffer) return true;
|
|
113
|
+
if (typeof ReadableStream !== 'undefined' && value instanceof ReadableStream) return true;
|
|
114
|
+
return typeof value === 'object' && value !== null && ArrayBuffer.isView(value);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const serializeBody = (
|
|
118
|
+
body: UseFetchOptions['body'],
|
|
119
|
+
headers: Headers
|
|
120
|
+
): BodyInit | null | undefined => {
|
|
121
|
+
if (body == null) return body;
|
|
122
|
+
if (isBodyLike(body)) return body;
|
|
123
|
+
|
|
124
|
+
if (!headers.has('content-type')) {
|
|
125
|
+
headers.set('content-type', 'application/json');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return JSON.stringify(body);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const resolveInput = (input: FetchInput): string | URL | Request => {
|
|
132
|
+
return typeof input === 'function' ? input() : input;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const appendQuery = (url: URL, query: Record<string, unknown>): void => {
|
|
136
|
+
for (const [key, value] of Object.entries(query)) {
|
|
137
|
+
if (value == null) continue;
|
|
138
|
+
|
|
139
|
+
if (Array.isArray(value)) {
|
|
140
|
+
for (const item of value) {
|
|
141
|
+
if (item != null) {
|
|
142
|
+
url.searchParams.append(key, String(item));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
url.searchParams.set(key, String(value));
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const toUrl = (input: string | URL, baseUrl?: string): URL => {
|
|
153
|
+
const runtimeBase =
|
|
154
|
+
typeof window !== 'undefined' && /^https?:/i.test(window.location.href)
|
|
155
|
+
? window.location.href
|
|
156
|
+
: 'http://localhost';
|
|
157
|
+
const base = baseUrl ? new URL(baseUrl, runtimeBase).toString() : runtimeBase;
|
|
158
|
+
return input instanceof URL ? new URL(input.toString(), base) : new URL(input, base);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const parseResponse = async <TResponse>(
|
|
162
|
+
response: Response,
|
|
163
|
+
parseAs: BqueryFetchParseAs
|
|
164
|
+
): Promise<TResponse> => {
|
|
165
|
+
if (parseAs === 'response') return response as TResponse;
|
|
166
|
+
if (parseAs === 'text') return (await response.text()) as TResponse;
|
|
167
|
+
if (parseAs === 'blob') return (await response.blob()) as TResponse;
|
|
168
|
+
if (parseAs === 'arrayBuffer') return (await response.arrayBuffer()) as TResponse;
|
|
169
|
+
if (parseAs === 'formData') return (await response.formData()) as TResponse;
|
|
170
|
+
|
|
171
|
+
const text = await response.text();
|
|
172
|
+
if (!text) {
|
|
173
|
+
return undefined as TResponse;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
return JSON.parse(text) as TResponse;
|
|
178
|
+
} catch (error) {
|
|
179
|
+
const detail = response.url ? ` for ${response.url}` : '';
|
|
180
|
+
throw new Error(
|
|
181
|
+
`Failed to parse JSON response${detail} (status ${response.status}): ${error instanceof Error ? error.message : String(error)}`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const normalizeMethod = (method?: string): string | undefined => {
|
|
187
|
+
const normalized = method?.trim();
|
|
188
|
+
return normalized ? normalized.toUpperCase() : undefined;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const resolveMethod = (
|
|
192
|
+
explicitMethod: string | undefined,
|
|
193
|
+
requestInput: string | URL | Request,
|
|
194
|
+
bodyProvided: boolean
|
|
195
|
+
): string | undefined => {
|
|
196
|
+
const requestMethod =
|
|
197
|
+
requestInput instanceof Request ? normalizeMethod(requestInput.method) : undefined;
|
|
198
|
+
return explicitMethod ?? requestMethod ?? (bodyProvided ? 'POST' : undefined);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const resolveRequestInitMethod = (
|
|
202
|
+
explicitMethod: string | undefined,
|
|
203
|
+
requestInput: string | URL | Request,
|
|
204
|
+
method: string | undefined
|
|
205
|
+
): string | undefined => {
|
|
206
|
+
if (explicitMethod) return explicitMethod;
|
|
207
|
+
return requestInput instanceof Request ? undefined : method;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const toRequestInit = (request: Request): RequestInit => {
|
|
211
|
+
const requestMethod = normalizeMethod(request.method);
|
|
212
|
+
let body: BodyInit | undefined;
|
|
213
|
+
if (requestMethod !== 'GET' && requestMethod !== 'HEAD' && !request.bodyUsed) {
|
|
214
|
+
try {
|
|
215
|
+
body = request.clone().body ?? undefined;
|
|
216
|
+
} catch {
|
|
217
|
+
body = undefined;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
method: requestMethod,
|
|
223
|
+
headers: request.headers,
|
|
224
|
+
body,
|
|
225
|
+
cache: request.cache,
|
|
226
|
+
credentials: request.credentials,
|
|
227
|
+
integrity: request.integrity,
|
|
228
|
+
keepalive: request.keepalive,
|
|
229
|
+
mode: request.mode,
|
|
230
|
+
redirect: request.redirect,
|
|
231
|
+
referrer: request.referrer,
|
|
232
|
+
referrerPolicy: request.referrerPolicy,
|
|
233
|
+
signal: request.signal,
|
|
234
|
+
};
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Create a reactive wrapper around an async resolver.
|
|
239
|
+
*
|
|
240
|
+
* @template TResult - Raw result type returned by the handler
|
|
241
|
+
* @template TData - Stored data type after optional transformation
|
|
242
|
+
* @param handler - Async function to execute
|
|
243
|
+
* @param options - Execution, transform, and refresh options
|
|
244
|
+
* @returns Reactive data state with execute(), refresh(), and clear()
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* ```ts
|
|
248
|
+
* const user = useAsyncData(() => fetch('/api/user').then((res) => res.json()));
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
export const useAsyncData = <TResult, TData = TResult>(
|
|
252
|
+
handler: () => Promise<TResult>,
|
|
253
|
+
options: UseAsyncDataOptions<TResult, TData> = {}
|
|
254
|
+
): AsyncDataState<TData> => {
|
|
255
|
+
const immediate = options.immediate ?? true;
|
|
256
|
+
const data = signal<TData | undefined>(options.defaultValue);
|
|
257
|
+
const error = signal<Error | null>(null);
|
|
258
|
+
const status = signal<AsyncDataStatus>('idle');
|
|
259
|
+
const pending = computed(() => status.value === 'pending');
|
|
260
|
+
let executionId = 0;
|
|
261
|
+
let disposed = false;
|
|
262
|
+
let stopWatching = (): void => {};
|
|
263
|
+
|
|
264
|
+
const clear = (): void => {
|
|
265
|
+
executionId += 1;
|
|
266
|
+
data.value = options.defaultValue;
|
|
267
|
+
error.value = null;
|
|
268
|
+
status.value = 'idle';
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const dispose = (): void => {
|
|
272
|
+
if (disposed) return;
|
|
273
|
+
disposed = true;
|
|
274
|
+
executionId += 1;
|
|
275
|
+
stopWatching();
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const execute = async (): Promise<TData | undefined> => {
|
|
279
|
+
if (disposed) {
|
|
280
|
+
return data.peek();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const currentExecution = ++executionId;
|
|
284
|
+
status.value = 'pending';
|
|
285
|
+
error.value = null;
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
const resolved = await handler();
|
|
289
|
+
const transformed = options.transform
|
|
290
|
+
? options.transform(resolved)
|
|
291
|
+
: (resolved as unknown as TData);
|
|
292
|
+
|
|
293
|
+
if (disposed || currentExecution !== executionId) {
|
|
294
|
+
return data.peek();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
data.value = transformed;
|
|
298
|
+
status.value = 'success';
|
|
299
|
+
options.onSuccess?.(transformed);
|
|
300
|
+
return transformed;
|
|
301
|
+
} catch (caught) {
|
|
302
|
+
const normalizedError = normalizeError(caught);
|
|
303
|
+
|
|
304
|
+
if (disposed || currentExecution !== executionId) {
|
|
305
|
+
return data.peek();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
error.value = normalizedError;
|
|
309
|
+
status.value = 'error';
|
|
310
|
+
options.onError?.(normalizedError);
|
|
311
|
+
return data.peek();
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
if (options.watch?.length) {
|
|
316
|
+
let initialized = false;
|
|
317
|
+
stopWatching = effect(() => {
|
|
318
|
+
for (const source of options.watch ?? []) {
|
|
319
|
+
readWatchSource(source);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (!initialized) {
|
|
323
|
+
initialized = true;
|
|
324
|
+
if (immediate) {
|
|
325
|
+
void untrack(() => execute());
|
|
326
|
+
}
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
void untrack(() => execute());
|
|
331
|
+
});
|
|
332
|
+
} else if (immediate) {
|
|
333
|
+
void execute();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
data,
|
|
338
|
+
error,
|
|
339
|
+
status,
|
|
340
|
+
pending,
|
|
341
|
+
execute,
|
|
342
|
+
refresh: execute,
|
|
343
|
+
clear,
|
|
344
|
+
dispose,
|
|
345
|
+
};
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Reactive fetch composable using the browser Fetch API.
|
|
350
|
+
*
|
|
351
|
+
* @template TResponse - Raw parsed response type
|
|
352
|
+
* @template TData - Stored response type after optional transformation
|
|
353
|
+
* @param input - Request URL, Request object, or lazy input factory
|
|
354
|
+
* @param options - Request and reactive state options
|
|
355
|
+
* @returns Reactive fetch state with execute(), refresh(), and clear()
|
|
356
|
+
*
|
|
357
|
+
* @example
|
|
358
|
+
* ```ts
|
|
359
|
+
* const users = useFetch<{ id: number; name: string }[]>('/api/users');
|
|
360
|
+
* ```
|
|
361
|
+
*/
|
|
362
|
+
export const useFetch = <TResponse = unknown, TData = TResponse>(
|
|
363
|
+
input: FetchInput,
|
|
364
|
+
options: UseFetchOptions<TResponse, TData> = {}
|
|
365
|
+
): AsyncDataState<TData> => {
|
|
366
|
+
const fetchConfig = getBqueryConfig().fetch;
|
|
367
|
+
const parseAs = options.parseAs ?? fetchConfig?.parseAs ?? 'json';
|
|
368
|
+
const fetcher = options.fetcher ?? fetch;
|
|
369
|
+
|
|
370
|
+
return useAsyncData<TResponse, TData>(async () => {
|
|
371
|
+
const requestInput = resolveInput(input);
|
|
372
|
+
const requestUrl =
|
|
373
|
+
typeof requestInput === 'string' || requestInput instanceof URL
|
|
374
|
+
? toUrl(requestInput, options.baseUrl ?? fetchConfig?.baseUrl)
|
|
375
|
+
: requestInput instanceof Request && options.query
|
|
376
|
+
? new URL(requestInput.url)
|
|
377
|
+
: null;
|
|
378
|
+
|
|
379
|
+
if (requestUrl && options.query) {
|
|
380
|
+
appendQuery(requestUrl, options.query);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const headers = toHeaders(
|
|
384
|
+
fetchConfig?.headers,
|
|
385
|
+
requestInput instanceof Request ? requestInput.headers : undefined,
|
|
386
|
+
options.headers
|
|
387
|
+
);
|
|
388
|
+
const bodyProvided = options.body != null;
|
|
389
|
+
const explicitMethod = normalizeMethod(options.method);
|
|
390
|
+
const method = resolveMethod(explicitMethod, requestInput, bodyProvided);
|
|
391
|
+
const bodylessMethod = method === 'GET' || method === 'HEAD' ? method : null;
|
|
392
|
+
if (bodyProvided && bodylessMethod) {
|
|
393
|
+
throw new Error(`Cannot send a request body with ${bodylessMethod} requests`);
|
|
394
|
+
}
|
|
395
|
+
const requestInitMethod = resolveRequestInitMethod(explicitMethod, requestInput, method);
|
|
396
|
+
const requestInit: RequestInit = {
|
|
397
|
+
...options,
|
|
398
|
+
method: requestInitMethod,
|
|
399
|
+
headers,
|
|
400
|
+
body: serializeBody(options.body, headers),
|
|
401
|
+
};
|
|
402
|
+
|
|
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;
|
|
413
|
+
|
|
414
|
+
let requestTarget: Request | string | URL = requestUrl ?? requestInput;
|
|
415
|
+
if (
|
|
416
|
+
requestInput instanceof Request &&
|
|
417
|
+
requestUrl &&
|
|
418
|
+
requestUrl.toString() !== requestInput.url
|
|
419
|
+
) {
|
|
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
|
+
requestTarget = new Request(requestUrl.toString(), toRequestInit(requestInput));
|
|
423
|
+
}
|
|
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
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return parseResponse<TResponse>(response, parseAs);
|
|
435
|
+
}, options);
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Create a preconfigured useFetch() helper.
|
|
440
|
+
*
|
|
441
|
+
* @param defaults - Default request options merged into every useFetch() call
|
|
442
|
+
* @returns A useFetch-compatible function with merged defaults
|
|
443
|
+
*
|
|
444
|
+
* @example
|
|
445
|
+
* ```ts
|
|
446
|
+
* const useApiFetch = createUseFetch({ baseUrl: 'https://api.example.com' });
|
|
447
|
+
* const profile = useApiFetch('/profile');
|
|
448
|
+
* ```
|
|
449
|
+
*/
|
|
450
|
+
/** Overload for factories without a configured transform, preserving per-call `TResponse -> TData` inference. */
|
|
451
|
+
export function createUseFetch<TDefaultResponse = unknown>(
|
|
452
|
+
defaults?: UseFetchOptions<TDefaultResponse, TDefaultResponse>
|
|
453
|
+
): <TResponse = TDefaultResponse, TData = TResponse>(
|
|
454
|
+
input: FetchInput,
|
|
455
|
+
options?: UseFetchOptions<TResponse, TData>
|
|
456
|
+
) => AsyncDataState<TData>;
|
|
457
|
+
|
|
458
|
+
/** Overload for factories with a configured transform, preserving the transformed factory data type by default. */
|
|
459
|
+
export function createUseFetch<TDefaultResponse = unknown, TDefaultData = TDefaultResponse>(
|
|
460
|
+
defaults: UseFetchOptions<TDefaultResponse, TDefaultData>
|
|
461
|
+
): <TResponse = TDefaultResponse, TData = TDefaultData>(
|
|
462
|
+
input: FetchInput,
|
|
463
|
+
options?: UseFetchOptions<TResponse, TData>
|
|
464
|
+
) => AsyncDataState<TData>;
|
|
465
|
+
|
|
466
|
+
export function createUseFetch<TDefaultResponse = unknown, TDefaultData = TDefaultResponse>(
|
|
467
|
+
defaults: UseFetchOptions<TDefaultResponse, TDefaultData> = {}
|
|
468
|
+
) {
|
|
469
|
+
return <TResponse = TDefaultResponse, TData = TDefaultData>(
|
|
470
|
+
input: FetchInput,
|
|
471
|
+
options: UseFetchOptions<TResponse, TData> = {}
|
|
472
|
+
): AsyncDataState<TData> => {
|
|
473
|
+
const resolvedDefaults = defaults as unknown as UseFetchOptions<TResponse, TData>;
|
|
474
|
+
const mergedQuery = merge({}, resolvedDefaults.query ?? {}, options.query ?? {}) as Record<
|
|
475
|
+
string,
|
|
476
|
+
unknown
|
|
477
|
+
>;
|
|
478
|
+
|
|
479
|
+
return useFetch<TResponse, TData>(input, {
|
|
480
|
+
...resolvedDefaults,
|
|
481
|
+
...options,
|
|
482
|
+
headers: toHeaders(resolvedDefaults.headers, options.headers),
|
|
483
|
+
query: Object.keys(mergedQuery).length > 0 ? mergedQuery : undefined,
|
|
484
|
+
});
|
|
485
|
+
};
|
|
486
|
+
}
|