@kimesh/query 0.0.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 +3 -0
- package/dist/index.d.mts +794 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1005 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +43 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1005 @@
|
|
|
1
|
+
import { QueryClient, QueryClient as QueryClient$1, VueQueryPlugin, VueQueryPlugin as VueQueryPlugin$1, queryOptions, useInfiniteQuery, useInfiniteQuery as useInfiniteQuery$1, useMutation, useMutation as useMutation$1, useQuery, useQuery as useQuery$1, useQueryClient, useQueryClient as useQueryClient$1 } from "@tanstack/vue-query";
|
|
2
|
+
import { ofetch } from "ofetch";
|
|
3
|
+
import { computed, ref, toValue, watch } from "vue";
|
|
4
|
+
import { hash } from "ohash";
|
|
5
|
+
|
|
6
|
+
//#region src/plugin.ts
|
|
7
|
+
/** Default timing values */
|
|
8
|
+
const DEFAULT_STALE_TIME = 5 * 1e3;
|
|
9
|
+
const DEFAULT_GC_TIME = 300 * 1e3;
|
|
10
|
+
const DEFAULT_RETRY = 1;
|
|
11
|
+
const DEFAULT_REFETCH_ON_WINDOW_FOCUS = false;
|
|
12
|
+
/**
|
|
13
|
+
* Create a QueryClient with Kimesh-optimized defaults
|
|
14
|
+
*
|
|
15
|
+
* Defaults are designed for typical SPA usage:
|
|
16
|
+
* - Short stale time (5s) for responsive UX
|
|
17
|
+
* - Longer GC time (5min) to preserve cache during navigation
|
|
18
|
+
* - Single retry to recover from transient errors
|
|
19
|
+
* - No refetch on focus to reduce unnecessary requests
|
|
20
|
+
*/
|
|
21
|
+
function createKimeshQueryClient(options = {}) {
|
|
22
|
+
return new QueryClient$1({ defaultOptions: { queries: {
|
|
23
|
+
staleTime: options.staleTime ?? DEFAULT_STALE_TIME,
|
|
24
|
+
gcTime: options.gcTime ?? DEFAULT_GC_TIME,
|
|
25
|
+
retry: options.retry ?? DEFAULT_RETRY,
|
|
26
|
+
refetchOnWindowFocus: options.refetchOnWindowFocus ?? DEFAULT_REFETCH_ON_WINDOW_FOCUS
|
|
27
|
+
} } });
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* KimeshQueryPlugin - Centralized query plugin with sensible defaults
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* import { KimeshQueryPlugin } from '@kimesh/query'
|
|
35
|
+
*
|
|
36
|
+
* // Basic usage with defaults
|
|
37
|
+
* app.use(KimeshQueryPlugin)
|
|
38
|
+
*
|
|
39
|
+
* // With custom options
|
|
40
|
+
* app.use(KimeshQueryPlugin, {
|
|
41
|
+
* staleTime: 10 * 1000,
|
|
42
|
+
* retry: 2,
|
|
43
|
+
* })
|
|
44
|
+
*
|
|
45
|
+
* // With custom QueryClient
|
|
46
|
+
* app.use(KimeshQueryPlugin, {
|
|
47
|
+
* queryClient: myCustomQueryClient,
|
|
48
|
+
* })
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
const KimeshQueryPlugin = { install(app, options = {}) {
|
|
52
|
+
const queryClient = options.queryClient ?? createKimeshQueryClient(options);
|
|
53
|
+
app.use(VueQueryPlugin$1, {
|
|
54
|
+
...options,
|
|
55
|
+
queryClient
|
|
56
|
+
});
|
|
57
|
+
} };
|
|
58
|
+
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/fetch/create-fetch.ts
|
|
61
|
+
/**
|
|
62
|
+
* Factory function for creating configured fetch instances.
|
|
63
|
+
*/
|
|
64
|
+
/**
|
|
65
|
+
* Create a configured fetch instance.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* const api = createKmFetch({
|
|
70
|
+
* baseURL: 'https://api.example.com',
|
|
71
|
+
* timeout: 5000,
|
|
72
|
+
* })
|
|
73
|
+
*
|
|
74
|
+
* const users = await api('/users')
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
function createKmFetch(config = {}) {
|
|
78
|
+
const fetchInstance = ofetch.create(config);
|
|
79
|
+
fetchInstance.create = (defaults) => createKmFetch({
|
|
80
|
+
...config,
|
|
81
|
+
...defaults,
|
|
82
|
+
headers: {
|
|
83
|
+
...config.headers,
|
|
84
|
+
...defaults.headers
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
return fetchInstance;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/fetch/layer-fetch.ts
|
|
92
|
+
/**
|
|
93
|
+
* Layer-aware fetch system.
|
|
94
|
+
*
|
|
95
|
+
* Provides fetch instances that automatically switch based on the current route's layer.
|
|
96
|
+
* This enables layer-specific API configurations (e.g., blog layer uses blog-api.com).
|
|
97
|
+
*/
|
|
98
|
+
const currentLayer = ref("app");
|
|
99
|
+
const layerInstances = /* @__PURE__ */ new Map();
|
|
100
|
+
const layerInterceptors = /* @__PURE__ */ new Map();
|
|
101
|
+
let defaultConfig = {};
|
|
102
|
+
let isInitialized = false;
|
|
103
|
+
let routerRef = null;
|
|
104
|
+
let pendingRoute = null;
|
|
105
|
+
function registerLayerInterceptors(layer, interceptors) {
|
|
106
|
+
layerInterceptors.set(layer, interceptors);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Set router reference for layer detection.
|
|
110
|
+
* Installs navigation hooks to track pending route during navigation.
|
|
111
|
+
*/
|
|
112
|
+
function setRouter(router) {
|
|
113
|
+
routerRef = router;
|
|
114
|
+
router.beforeEach((to) => {
|
|
115
|
+
pendingRoute = to;
|
|
116
|
+
});
|
|
117
|
+
router.afterEach(() => {
|
|
118
|
+
pendingRoute = null;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
function getLayerFromRoute(route) {
|
|
122
|
+
return [...route.matched].reverse().find((r) => r.meta.__kimeshLayer)?.meta.__kimeshLayer ?? "app";
|
|
123
|
+
}
|
|
124
|
+
function detectCurrentLayer() {
|
|
125
|
+
if (pendingRoute) return getLayerFromRoute(pendingRoute);
|
|
126
|
+
if (routerRef) return getLayerFromRoute(routerRef.currentRoute.value);
|
|
127
|
+
return currentLayer.value;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Initialize the layer-aware fetch system.
|
|
131
|
+
*/
|
|
132
|
+
function initLayerFetch(configs) {
|
|
133
|
+
defaultConfig = configs.default;
|
|
134
|
+
layerInstances.clear();
|
|
135
|
+
for (const [layer, config] of Object.entries(configs.layers)) {
|
|
136
|
+
const mergedConfig = {
|
|
137
|
+
...defaultConfig,
|
|
138
|
+
...config,
|
|
139
|
+
headers: {
|
|
140
|
+
...defaultConfig.headers,
|
|
141
|
+
...config.headers
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
layerInstances.set(layer, createKmFetch(mergedConfig));
|
|
145
|
+
}
|
|
146
|
+
if (!layerInstances.has("app")) layerInstances.set("app", createKmFetch(defaultConfig));
|
|
147
|
+
isInitialized = true;
|
|
148
|
+
}
|
|
149
|
+
function setCurrentLayer(layer) {
|
|
150
|
+
currentLayer.value = layer;
|
|
151
|
+
}
|
|
152
|
+
function getCurrentLayer() {
|
|
153
|
+
return currentLayer.value;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Get fetch instance for a specific layer.
|
|
157
|
+
* Falls back to 'app' layer if the requested layer is not found.
|
|
158
|
+
*/
|
|
159
|
+
function getLayerFetch(layer) {
|
|
160
|
+
const targetLayer = layer ?? detectCurrentLayer();
|
|
161
|
+
if (layerInstances.has(targetLayer)) return layerInstances.get(targetLayer);
|
|
162
|
+
if (layerInstances.has("app")) return layerInstances.get("app");
|
|
163
|
+
layerInstances.set("app", createKmFetch(defaultConfig));
|
|
164
|
+
return layerInstances.get("app");
|
|
165
|
+
}
|
|
166
|
+
function isLayerFetchInitialized() {
|
|
167
|
+
return isInitialized;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Layer-aware $fetch proxy.
|
|
171
|
+
*
|
|
172
|
+
* Automatically routes requests through the current layer's fetch instance.
|
|
173
|
+
* When navigating to /blog/*, the current layer becomes 'blog' and uses
|
|
174
|
+
* the blog layer's configuration.
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```ts
|
|
178
|
+
* // In a blog route component - uses blog layer's baseURL
|
|
179
|
+
* const posts = await $fetch('/posts')
|
|
180
|
+
*
|
|
181
|
+
* // Force specific layer
|
|
182
|
+
* const hostData = await getLayerFetch('app')('/data')
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
const $fetch = new Proxy((() => {}), {
|
|
186
|
+
get(_target, prop) {
|
|
187
|
+
const instance = getLayerFetch();
|
|
188
|
+
const value = instance[prop];
|
|
189
|
+
return typeof value === "function" ? value.bind(instance) : value;
|
|
190
|
+
},
|
|
191
|
+
apply(_target, _thisArg, args) {
|
|
192
|
+
const layer = detectCurrentLayer();
|
|
193
|
+
const instance = getLayerFetch(layer);
|
|
194
|
+
const interceptors = layerInterceptors.get(layer);
|
|
195
|
+
if (interceptors?.onRequest) interceptors.onRequest({
|
|
196
|
+
request: args[0],
|
|
197
|
+
options: args[1] ?? {}
|
|
198
|
+
});
|
|
199
|
+
const promise = instance(args[0], args[1]);
|
|
200
|
+
if (interceptors?.onResponse) return promise.then((response) => {
|
|
201
|
+
interceptors.onResponse({
|
|
202
|
+
request: args[0],
|
|
203
|
+
response: { status: 200 }
|
|
204
|
+
});
|
|
205
|
+
return response;
|
|
206
|
+
});
|
|
207
|
+
return promise;
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
//#endregion
|
|
212
|
+
//#region src/fetch/fetch-plugin.ts
|
|
213
|
+
function extractFetchConfig(config) {
|
|
214
|
+
if (!config) return {};
|
|
215
|
+
return {
|
|
216
|
+
baseURL: config.baseURL,
|
|
217
|
+
timeout: config.timeout,
|
|
218
|
+
retry: config.retry,
|
|
219
|
+
retryDelay: config.retryDelay,
|
|
220
|
+
credentials: config.credentials,
|
|
221
|
+
headers: config.headers
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function mergeHeaders(...headerSets) {
|
|
225
|
+
return Object.assign({}, ...headerSets.filter(Boolean));
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Create a Kimesh runtime plugin for $fetch.
|
|
229
|
+
*
|
|
230
|
+
* @example Basic usage
|
|
231
|
+
* ```typescript
|
|
232
|
+
* import { fetchPlugin } from '@kimesh/query'
|
|
233
|
+
* export default fetchPlugin()
|
|
234
|
+
* ```
|
|
235
|
+
*
|
|
236
|
+
* @example With custom config
|
|
237
|
+
* ```typescript
|
|
238
|
+
* import { fetchPlugin } from '@kimesh/query'
|
|
239
|
+
* export default fetchPlugin({
|
|
240
|
+
* config: {
|
|
241
|
+
* onRequest: ({ options }) => {
|
|
242
|
+
* options.headers = { ...options.headers, 'X-Custom': 'value' }
|
|
243
|
+
* },
|
|
244
|
+
* },
|
|
245
|
+
* })
|
|
246
|
+
* ```
|
|
247
|
+
*/
|
|
248
|
+
function fetchPlugin(options = {}) {
|
|
249
|
+
const { config: overrideConfig = {}, key = "fetch", disableLayerAware = false } = options;
|
|
250
|
+
const plugin = (app) => {
|
|
251
|
+
const defaultFetchConfig = extractFetchConfig(app.$config?.fetch);
|
|
252
|
+
const defaultConfig$1 = {
|
|
253
|
+
...defaultFetchConfig,
|
|
254
|
+
...overrideConfig,
|
|
255
|
+
headers: mergeHeaders(defaultFetchConfig.headers, overrideConfig.headers)
|
|
256
|
+
};
|
|
257
|
+
const layersConfig = app.$layersConfig ?? {};
|
|
258
|
+
const hasLayerConfigs = Object.keys(layersConfig).length > 0;
|
|
259
|
+
if (!disableLayerAware && hasLayerConfigs) {
|
|
260
|
+
const layerFetchConfigs = {};
|
|
261
|
+
for (const [layerName, layerConfig] of Object.entries(layersConfig)) {
|
|
262
|
+
const fetchConfig = extractFetchConfig(layerConfig.fetch);
|
|
263
|
+
if (Object.keys(fetchConfig).some((k) => fetchConfig[k] !== void 0)) layerFetchConfigs[layerName] = fetchConfig;
|
|
264
|
+
}
|
|
265
|
+
initLayerFetch({
|
|
266
|
+
layers: layerFetchConfigs,
|
|
267
|
+
default: defaultConfig$1
|
|
268
|
+
});
|
|
269
|
+
setRouter(app.router);
|
|
270
|
+
return { provide: { [key]: $fetch } };
|
|
271
|
+
}
|
|
272
|
+
return { provide: { [key]: createKmFetch(defaultConfig$1) } };
|
|
273
|
+
};
|
|
274
|
+
plugin.__kimesh_plugin = true;
|
|
275
|
+
plugin._name = "kimesh:fetch";
|
|
276
|
+
plugin.meta = {
|
|
277
|
+
name: "kimesh:fetch",
|
|
278
|
+
enforce: "pre"
|
|
279
|
+
};
|
|
280
|
+
return plugin;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Create a named fetch plugin for multi-instance scenarios.
|
|
284
|
+
*
|
|
285
|
+
* @example
|
|
286
|
+
* ```typescript
|
|
287
|
+
* export const blogFetch = createNamedFetchPlugin('blogFetch', {
|
|
288
|
+
* baseURL: 'https://blog-api.example.com',
|
|
289
|
+
* })
|
|
290
|
+
* ```
|
|
291
|
+
*/
|
|
292
|
+
function createNamedFetchPlugin(name, config = {}) {
|
|
293
|
+
return fetchPlugin({
|
|
294
|
+
key: name,
|
|
295
|
+
config
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Create a layer-specific fetch interceptor plugin.
|
|
300
|
+
* Use this in layer plugins to add logging or modify requests for that layer only.
|
|
301
|
+
*
|
|
302
|
+
* @example
|
|
303
|
+
* ```typescript
|
|
304
|
+
* // blog/src/plugins/01.fetch.ts
|
|
305
|
+
* import { layerFetchPlugin } from '@kimesh/query'
|
|
306
|
+
*
|
|
307
|
+
* export default layerFetchPlugin({
|
|
308
|
+
* layer: 'blog',
|
|
309
|
+
* onRequest: ({ request }) => console.log(`[Blog] Fetching: ${request}`),
|
|
310
|
+
* })
|
|
311
|
+
* ```
|
|
312
|
+
*/
|
|
313
|
+
function layerFetchPlugin(options) {
|
|
314
|
+
const { layer, onRequest, onResponse } = options;
|
|
315
|
+
const plugin = () => {
|
|
316
|
+
registerLayerInterceptors(layer, {
|
|
317
|
+
onRequest,
|
|
318
|
+
onResponse
|
|
319
|
+
});
|
|
320
|
+
};
|
|
321
|
+
plugin.__kimesh_plugin = true;
|
|
322
|
+
plugin._name = `kimesh:fetch:${layer}`;
|
|
323
|
+
return plugin;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
//#endregion
|
|
327
|
+
//#region src/fetch/global-fetch.ts
|
|
328
|
+
/**
|
|
329
|
+
* Global $fetch instance configured from runtime config.
|
|
330
|
+
*/
|
|
331
|
+
let globalInstance = null;
|
|
332
|
+
/**
|
|
333
|
+
* Initialize the global $fetch instance with runtime config.
|
|
334
|
+
* Called automatically by Kimesh during app initialization.
|
|
335
|
+
*/
|
|
336
|
+
function initGlobalFetch(config) {
|
|
337
|
+
globalInstance = createKmFetch({
|
|
338
|
+
baseURL: config.fetch?.baseURL ?? config.apiBase,
|
|
339
|
+
headers: config.fetch?.headers,
|
|
340
|
+
timeout: config.fetch?.timeout,
|
|
341
|
+
retry: config.fetch?.retry,
|
|
342
|
+
retryDelay: config.fetch?.retryDelay,
|
|
343
|
+
credentials: config.fetch?.credentials
|
|
344
|
+
});
|
|
345
|
+
return globalInstance;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Get the global fetch instance. Creates a default instance if not initialized.
|
|
349
|
+
*/
|
|
350
|
+
function useGlobalFetch() {
|
|
351
|
+
if (!globalInstance) globalInstance = createKmFetch({});
|
|
352
|
+
return globalInstance;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Global $fetch proxy that lazily accesses the global fetch instance.
|
|
356
|
+
*
|
|
357
|
+
* @example
|
|
358
|
+
* ```ts
|
|
359
|
+
* const users = await $fetch('/api/users')
|
|
360
|
+
* const user = await $fetch('/api/users', { method: 'POST', body: { name: 'John' } })
|
|
361
|
+
* ```
|
|
362
|
+
*/
|
|
363
|
+
const $fetch$1 = new Proxy((() => {}), {
|
|
364
|
+
get(_target, prop) {
|
|
365
|
+
const instance = useGlobalFetch();
|
|
366
|
+
const value = instance[prop];
|
|
367
|
+
return typeof value === "function" ? value.bind(instance) : value;
|
|
368
|
+
},
|
|
369
|
+
apply(_target, _thisArg, args) {
|
|
370
|
+
return useGlobalFetch()(args[0], args[1]);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
//#endregion
|
|
375
|
+
//#region src/loader.ts
|
|
376
|
+
/**
|
|
377
|
+
* Normalizes a loader result to an array
|
|
378
|
+
*/
|
|
379
|
+
function normalizeLoaders(loader) {
|
|
380
|
+
return Array.isArray(loader) ? loader : [loader];
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Execute a loader and prefetch data
|
|
384
|
+
*/
|
|
385
|
+
async function executeLoader(queryClient, loader) {
|
|
386
|
+
const loaders = normalizeLoaders(loader);
|
|
387
|
+
await Promise.all(loaders.map((def) => queryClient.prefetchQuery({
|
|
388
|
+
queryKey: def.queryKey,
|
|
389
|
+
queryFn: def.queryFn,
|
|
390
|
+
staleTime: def.staleTime,
|
|
391
|
+
gcTime: def.gcTime
|
|
392
|
+
})));
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Type guard to check if a component has a loader function
|
|
396
|
+
*/
|
|
397
|
+
function hasLoader(component) {
|
|
398
|
+
if (typeof component !== "object" || component === null) return false;
|
|
399
|
+
return "loader" in component && typeof component.loader === "function";
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Extract loaders from route matched components
|
|
403
|
+
*/
|
|
404
|
+
function extractLoaders(to) {
|
|
405
|
+
const loaders = [];
|
|
406
|
+
for (const matched of to.matched) {
|
|
407
|
+
const component = matched.components?.default;
|
|
408
|
+
if (!hasLoader(component)) continue;
|
|
409
|
+
const result = component.loader(to);
|
|
410
|
+
if (!result) continue;
|
|
411
|
+
loaders.push(...normalizeLoaders(result));
|
|
412
|
+
}
|
|
413
|
+
return loaders;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
//#endregion
|
|
417
|
+
//#region src/prefetch.ts
|
|
418
|
+
/**
|
|
419
|
+
* Check if a path matches a single pattern (string or regex)
|
|
420
|
+
*/
|
|
421
|
+
function matchesSinglePattern(path, pattern) {
|
|
422
|
+
if (typeof pattern === "string") return path === pattern || path.startsWith(pattern + "/");
|
|
423
|
+
return pattern.test(path);
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Check if a path matches any pattern in the array
|
|
427
|
+
*/
|
|
428
|
+
function matchesAnyPattern(path, patterns) {
|
|
429
|
+
return patterns.some((pattern) => matchesSinglePattern(path, pattern));
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Determines if a path should be prefetched based on include/exclude patterns
|
|
433
|
+
*/
|
|
434
|
+
function shouldPrefetch(path, exclude, include) {
|
|
435
|
+
if (exclude.length > 0 && matchesAnyPattern(path, exclude)) return false;
|
|
436
|
+
if (include && !matchesAnyPattern(path, include)) return false;
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Setup query prefetching on route navigation
|
|
441
|
+
*
|
|
442
|
+
* @example
|
|
443
|
+
* ```ts
|
|
444
|
+
* import { setupQueryPrefetching } from '@kimesh/query'
|
|
445
|
+
*
|
|
446
|
+
* const router = createRouter({ ... })
|
|
447
|
+
* const queryClient = new QueryClient()
|
|
448
|
+
*
|
|
449
|
+
* setupQueryPrefetching(router, queryClient, {
|
|
450
|
+
* onStart: (to) => console.log('Prefetching', to.path),
|
|
451
|
+
* onError: (error) => console.error('Prefetch failed', error),
|
|
452
|
+
* })
|
|
453
|
+
* ```
|
|
454
|
+
*
|
|
455
|
+
* @returns Function to remove the guard
|
|
456
|
+
*/
|
|
457
|
+
function setupQueryPrefetching(router, queryClient, options = {}) {
|
|
458
|
+
const { onStart, onComplete, onError, exclude = [], include } = options;
|
|
459
|
+
return router.beforeResolve(async (to) => {
|
|
460
|
+
if (!shouldPrefetch(to.path, exclude, include)) return;
|
|
461
|
+
const loaders = extractLoaders(to);
|
|
462
|
+
if (loaders.length === 0) return;
|
|
463
|
+
onStart?.(to, loaders);
|
|
464
|
+
try {
|
|
465
|
+
await Promise.all(loaders.map((loader) => executeLoader(queryClient, loader)));
|
|
466
|
+
onComplete?.(to, loaders);
|
|
467
|
+
} catch (error) {
|
|
468
|
+
onError?.(error, to);
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Create a prefetch function for manual prefetching (e.g., on hover)
|
|
474
|
+
*
|
|
475
|
+
* @example
|
|
476
|
+
* ```ts
|
|
477
|
+
* const prefetch = createPrefetcher(queryClient, router)
|
|
478
|
+
*
|
|
479
|
+
* // Prefetch on hover
|
|
480
|
+
* <RouterLink :to="{ name: 'user' }" @mouseenter="prefetch('/user/123')">
|
|
481
|
+
* ```
|
|
482
|
+
*/
|
|
483
|
+
function createPrefetcher(queryClient, router) {
|
|
484
|
+
return async function prefetch(path) {
|
|
485
|
+
const loaders = extractLoaders(router.resolve(path));
|
|
486
|
+
if (loaders.length === 0) return;
|
|
487
|
+
await Promise.all(loaders.map((loader) => executeLoader(queryClient, loader)));
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
//#endregion
|
|
492
|
+
//#region src/composables/useKmFetch.ts
|
|
493
|
+
/**
|
|
494
|
+
* Builds a Nuxt-style result from a TanStack Query result.
|
|
495
|
+
*/
|
|
496
|
+
function buildAsyncDataResult$1(query, queryKey, queryClient) {
|
|
497
|
+
const pending = computed(() => query.isLoading.value || query.isFetching.value);
|
|
498
|
+
const status = computed(() => {
|
|
499
|
+
if (query.isLoading.value) return "pending";
|
|
500
|
+
if (query.isError.value) return "error";
|
|
501
|
+
if (query.isSuccess.value) return "success";
|
|
502
|
+
return "idle";
|
|
503
|
+
});
|
|
504
|
+
return {
|
|
505
|
+
data: query.data,
|
|
506
|
+
pending,
|
|
507
|
+
error: query.error,
|
|
508
|
+
status,
|
|
509
|
+
refresh: async () => {
|
|
510
|
+
await query.refetch();
|
|
511
|
+
},
|
|
512
|
+
execute: async () => {
|
|
513
|
+
await query.refetch();
|
|
514
|
+
},
|
|
515
|
+
clear: () => {
|
|
516
|
+
queryClient.removeQueries({ queryKey: [...queryKey] });
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Generate a unique cache key from URL and options.
|
|
522
|
+
*/
|
|
523
|
+
function generateKey(url, method, query, body) {
|
|
524
|
+
return `kmFetch:${hash({
|
|
525
|
+
url,
|
|
526
|
+
method,
|
|
527
|
+
query,
|
|
528
|
+
body
|
|
529
|
+
})}`;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Nuxt-style fetch composable built on $fetch and TanStack Query.
|
|
533
|
+
*
|
|
534
|
+
* @example
|
|
535
|
+
* ```ts
|
|
536
|
+
* // Simple GET
|
|
537
|
+
* const { data, pending, error } = useKmFetch('/api/users')
|
|
538
|
+
*
|
|
539
|
+
* // With query params
|
|
540
|
+
* const { data } = useKmFetch('/api/users', {
|
|
541
|
+
* query: { page: 1, limit: 10 },
|
|
542
|
+
* })
|
|
543
|
+
*
|
|
544
|
+
* // POST with body
|
|
545
|
+
* const { data } = useKmFetch('/api/users', {
|
|
546
|
+
* method: 'POST',
|
|
547
|
+
* body: { name: 'John' },
|
|
548
|
+
* })
|
|
549
|
+
*
|
|
550
|
+
* // With interceptors
|
|
551
|
+
* const { data } = useKmFetch('/api/protected', {
|
|
552
|
+
* onRequest({ options }) {
|
|
553
|
+
* options.headers.set('Authorization', `Bearer ${token}`)
|
|
554
|
+
* },
|
|
555
|
+
* })
|
|
556
|
+
*
|
|
557
|
+
* // Reactive URL
|
|
558
|
+
* const userId = ref(1)
|
|
559
|
+
* const { data } = useKmFetch(() => `/api/users/${userId.value}`)
|
|
560
|
+
* ```
|
|
561
|
+
*/
|
|
562
|
+
function useKmFetch(url, options = {}) {
|
|
563
|
+
const queryClient = useQueryClient$1();
|
|
564
|
+
const fetchInstance = options.$fetch ?? useGlobalFetch();
|
|
565
|
+
const resolvedUrl = computed(() => toValue(url));
|
|
566
|
+
const resolvedMethod = computed(() => toValue(options.method) ?? "GET");
|
|
567
|
+
const resolvedQuery = computed(() => toValue(options.query ?? options.params));
|
|
568
|
+
const resolvedBody = computed(() => toValue(options.body));
|
|
569
|
+
const resolvedHeaders = computed(() => toValue(options.headers));
|
|
570
|
+
const resolvedBaseURL = computed(() => toValue(options.baseURL));
|
|
571
|
+
const resolvedTimeout = computed(() => toValue(options.timeout));
|
|
572
|
+
const queryKey = computed(() => {
|
|
573
|
+
return [options.key ?? generateKey(resolvedUrl.value, resolvedMethod.value, resolvedQuery.value, resolvedBody.value)];
|
|
574
|
+
});
|
|
575
|
+
const query = useQuery$1({
|
|
576
|
+
queryKey: queryKey.value,
|
|
577
|
+
queryFn: async () => {
|
|
578
|
+
const response = await fetchInstance(resolvedUrl.value, {
|
|
579
|
+
method: resolvedMethod.value,
|
|
580
|
+
query: resolvedQuery.value,
|
|
581
|
+
body: resolvedBody.value,
|
|
582
|
+
headers: resolvedHeaders.value,
|
|
583
|
+
baseURL: resolvedBaseURL.value,
|
|
584
|
+
timeout: resolvedTimeout.value,
|
|
585
|
+
onRequest: options.onRequest,
|
|
586
|
+
onRequestError: options.onRequestError,
|
|
587
|
+
onResponse: options.onResponse,
|
|
588
|
+
onResponseError: options.onResponseError
|
|
589
|
+
});
|
|
590
|
+
return options.transform ? options.transform(response) : response;
|
|
591
|
+
},
|
|
592
|
+
enabled: options.immediate !== false,
|
|
593
|
+
staleTime: options.staleTime,
|
|
594
|
+
gcTime: options.gcTime,
|
|
595
|
+
initialData: options.default
|
|
596
|
+
});
|
|
597
|
+
if (options.watch !== false) {
|
|
598
|
+
const watchSources = options.watch ?? [
|
|
599
|
+
resolvedUrl,
|
|
600
|
+
resolvedMethod,
|
|
601
|
+
resolvedQuery,
|
|
602
|
+
resolvedBody
|
|
603
|
+
];
|
|
604
|
+
if (watchSources.length > 0) watch(watchSources, () => {
|
|
605
|
+
query.refetch();
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
return buildAsyncDataResult$1(query, queryKey.value, queryClient);
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Lazy variant - starts fetching immediately (non-blocking pattern).
|
|
612
|
+
*
|
|
613
|
+
* Note: Despite the "lazy" name (Nuxt convention), this starts immediately.
|
|
614
|
+
* Use this when data loading shouldn't block rendering.
|
|
615
|
+
*/
|
|
616
|
+
function useLazyKmFetch(url, options = {}) {
|
|
617
|
+
return useKmFetch(url, {
|
|
618
|
+
...options,
|
|
619
|
+
immediate: true
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
//#endregion
|
|
624
|
+
//#region src/composables/useKmAsyncData.ts
|
|
625
|
+
/**
|
|
626
|
+
* Builds a Nuxt-style result from a TanStack Query result.
|
|
627
|
+
*/
|
|
628
|
+
function buildAsyncDataResult(query, queryKey, queryClient) {
|
|
629
|
+
const pending = computed(() => query.isLoading.value || query.isFetching.value);
|
|
630
|
+
const status = computed(() => {
|
|
631
|
+
if (query.isLoading.value) return "pending";
|
|
632
|
+
if (query.isError.value) return "error";
|
|
633
|
+
if (query.isSuccess.value) return "success";
|
|
634
|
+
return "idle";
|
|
635
|
+
});
|
|
636
|
+
return {
|
|
637
|
+
data: query.data,
|
|
638
|
+
pending,
|
|
639
|
+
error: query.error,
|
|
640
|
+
status,
|
|
641
|
+
refresh: async () => {
|
|
642
|
+
await query.refetch();
|
|
643
|
+
},
|
|
644
|
+
execute: async () => {
|
|
645
|
+
await query.refetch();
|
|
646
|
+
},
|
|
647
|
+
clear: () => {
|
|
648
|
+
queryClient.removeQueries({ queryKey: [...queryKey] });
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Generic async data composable.
|
|
654
|
+
*
|
|
655
|
+
* @example
|
|
656
|
+
* ```ts
|
|
657
|
+
* // Basic usage
|
|
658
|
+
* const { data, pending } = useKmAsyncData('currentUser', async () => {
|
|
659
|
+
* return await api.users.getCurrentUser()
|
|
660
|
+
* })
|
|
661
|
+
*
|
|
662
|
+
* // With $fetch from context
|
|
663
|
+
* const { data } = useKmAsyncData('users', async ({ $fetch }) => {
|
|
664
|
+
* return await $fetch('/api/users')
|
|
665
|
+
* })
|
|
666
|
+
*
|
|
667
|
+
* // With transform
|
|
668
|
+
* const { data } = useKmAsyncData('posts', fetchPosts, {
|
|
669
|
+
* transform: (data) => data.items,
|
|
670
|
+
* })
|
|
671
|
+
* ```
|
|
672
|
+
*/
|
|
673
|
+
function useKmAsyncData(key, handler, options = {}) {
|
|
674
|
+
const queryClient = useQueryClient$1();
|
|
675
|
+
const fetchInstance = options.$fetch ?? useGlobalFetch();
|
|
676
|
+
const queryKey = [key];
|
|
677
|
+
const query = useQuery$1({
|
|
678
|
+
queryKey,
|
|
679
|
+
queryFn: async () => {
|
|
680
|
+
const raw = await handler({ $fetch: fetchInstance });
|
|
681
|
+
return options.transform ? options.transform(raw) : raw;
|
|
682
|
+
},
|
|
683
|
+
enabled: options.immediate !== false,
|
|
684
|
+
staleTime: options.staleTime,
|
|
685
|
+
gcTime: options.gcTime,
|
|
686
|
+
initialData: options.default
|
|
687
|
+
});
|
|
688
|
+
if (options.watch?.length) watch(options.watch, () => {
|
|
689
|
+
query.refetch();
|
|
690
|
+
});
|
|
691
|
+
return buildAsyncDataResult(query, queryKey, queryClient);
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Lazy variant - starts fetching immediately (non-blocking pattern).
|
|
695
|
+
*
|
|
696
|
+
* Note: Despite the "lazy" name (Nuxt convention), this starts immediately.
|
|
697
|
+
* Use this when data loading shouldn't block rendering.
|
|
698
|
+
*/
|
|
699
|
+
function useLazyKmAsyncData(key, handler, options = {}) {
|
|
700
|
+
return useKmAsyncData(key, handler, {
|
|
701
|
+
...options,
|
|
702
|
+
immediate: true
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
//#endregion
|
|
707
|
+
//#region src/composables/useKmData.ts
|
|
708
|
+
/**
|
|
709
|
+
* Access cached data by key
|
|
710
|
+
*
|
|
711
|
+
* @example
|
|
712
|
+
* ```ts
|
|
713
|
+
* const user = useKmData<User>('currentUser')
|
|
714
|
+
* // Can also update
|
|
715
|
+
* user.value = { ...user.value, name: 'New Name' }
|
|
716
|
+
* ```
|
|
717
|
+
*/
|
|
718
|
+
function useKmData(key) {
|
|
719
|
+
const queryClient = useQueryClient$1();
|
|
720
|
+
return computed({
|
|
721
|
+
get: () => queryClient.getQueryData([key]),
|
|
722
|
+
set: (value) => {
|
|
723
|
+
queryClient.setQueryData([key], value);
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
//#endregion
|
|
729
|
+
//#region src/define-query.ts
|
|
730
|
+
/**
|
|
731
|
+
* Extracts the query function from options, supporting both naming styles
|
|
732
|
+
*/
|
|
733
|
+
function extractQueryFn(options) {
|
|
734
|
+
const queryFn = options.query ?? options.queryFn;
|
|
735
|
+
if (!queryFn) throw new Error("Query function required: provide either \"query\" or \"queryFn\"");
|
|
736
|
+
return queryFn;
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Define static query options (Pinia Colada style)
|
|
740
|
+
*
|
|
741
|
+
* @example
|
|
742
|
+
* ```ts
|
|
743
|
+
* // Pinia Colada naming (preferred)
|
|
744
|
+
* export const postsListQuery = defineQueryOptions({
|
|
745
|
+
* key: ['posts', 'list'],
|
|
746
|
+
* query: () => api.posts.getAll(),
|
|
747
|
+
* staleTime: 5 * 60 * 1000,
|
|
748
|
+
* })
|
|
749
|
+
*
|
|
750
|
+
* // TanStack naming (also supported)
|
|
751
|
+
* export const postsListQuery = defineQueryOptions({
|
|
752
|
+
* key: ['posts', 'list'],
|
|
753
|
+
* queryFn: () => api.posts.getAll(),
|
|
754
|
+
* })
|
|
755
|
+
*
|
|
756
|
+
* // Usage with useQuery
|
|
757
|
+
* const { data } = useQuery(postsListQuery)
|
|
758
|
+
* ```
|
|
759
|
+
*/
|
|
760
|
+
function defineQueryOptions(options) {
|
|
761
|
+
const queryFn = extractQueryFn(options);
|
|
762
|
+
return {
|
|
763
|
+
key: options.key,
|
|
764
|
+
query: queryFn,
|
|
765
|
+
queryKey: options.key,
|
|
766
|
+
queryFn,
|
|
767
|
+
staleTime: options.staleTime,
|
|
768
|
+
gcTime: options.gcTime,
|
|
769
|
+
enabled: options.enabled
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Create a reusable query composable (Pinia Colada style)
|
|
774
|
+
*
|
|
775
|
+
* @example
|
|
776
|
+
* ```ts
|
|
777
|
+
* export const useCurrentUser = defineQuery({
|
|
778
|
+
* key: ['user', 'current'],
|
|
779
|
+
* query: () => api.users.getCurrentUser(),
|
|
780
|
+
* })
|
|
781
|
+
*
|
|
782
|
+
* // State is shared across components!
|
|
783
|
+
* const { data: user } = useCurrentUser()
|
|
784
|
+
* ```
|
|
785
|
+
*/
|
|
786
|
+
function defineQuery(options) {
|
|
787
|
+
const queryFn = extractQueryFn(options);
|
|
788
|
+
return function useDefinedQuery() {
|
|
789
|
+
return useQuery$1({
|
|
790
|
+
queryKey: options.key,
|
|
791
|
+
queryFn,
|
|
792
|
+
staleTime: options.staleTime,
|
|
793
|
+
gcTime: options.gcTime
|
|
794
|
+
});
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
//#endregion
|
|
799
|
+
//#region src/define-mutation.ts
|
|
800
|
+
/**
|
|
801
|
+
* Define a reusable mutation composable (Pinia Colada style)
|
|
802
|
+
*
|
|
803
|
+
* Creates a composable that can be imported and used across multiple components.
|
|
804
|
+
* The mutation configuration is defined once and reused everywhere.
|
|
805
|
+
*
|
|
806
|
+
* @example
|
|
807
|
+
* ```ts
|
|
808
|
+
* // Define the mutation
|
|
809
|
+
* export const useCreatePost = defineMutation({
|
|
810
|
+
* mutation: (data: { title: string; body: string }) => api.posts.create(data),
|
|
811
|
+
* onSuccess: (data, variables) => {
|
|
812
|
+
* queryClient.invalidateQueries({ queryKey: ['posts'] })
|
|
813
|
+
* },
|
|
814
|
+
* })
|
|
815
|
+
*
|
|
816
|
+
* // Use in component
|
|
817
|
+
* const { mutate, mutateAsync, isPending } = useCreatePost()
|
|
818
|
+
* mutate({ title: 'New Post', body: 'Content...' })
|
|
819
|
+
* ```
|
|
820
|
+
*/
|
|
821
|
+
function defineMutation(options) {
|
|
822
|
+
return function useDefinedMutation() {
|
|
823
|
+
return useMutation$1({
|
|
824
|
+
mutationFn: options.mutation,
|
|
825
|
+
onSuccess: options.onSuccess,
|
|
826
|
+
onError: options.onError,
|
|
827
|
+
onSettled: options.onSettled
|
|
828
|
+
});
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
//#endregion
|
|
833
|
+
//#region src/query-keys.ts
|
|
834
|
+
/**
|
|
835
|
+
* Create a type-safe query key factory
|
|
836
|
+
*
|
|
837
|
+
* Creates a factory object with methods that generate consistent, hierarchical
|
|
838
|
+
* query keys. This helps maintain cache invalidation patterns and ensures
|
|
839
|
+
* type safety across your application.
|
|
840
|
+
*
|
|
841
|
+
* @example
|
|
842
|
+
* ```ts
|
|
843
|
+
* export const postKeys = createQueryKeyFactory('posts', {
|
|
844
|
+
* lists: null, // () => ['posts', 'lists']
|
|
845
|
+
* list: (filters: { page: number }) => filters, // (f) => ['posts', 'list', f]
|
|
846
|
+
* details: null, // () => ['posts', 'details']
|
|
847
|
+
* detail: (id: string) => id, // (id) => ['posts', 'detail', id]
|
|
848
|
+
* })
|
|
849
|
+
*
|
|
850
|
+
* // Usage
|
|
851
|
+
* postKeys.all() // ['posts']
|
|
852
|
+
* postKeys.lists() // ['posts', 'lists']
|
|
853
|
+
* postKeys.list({ page: 1 }) // ['posts', 'list', { page: 1 }]
|
|
854
|
+
* postKeys.detail('123') // ['posts', 'detail', '123']
|
|
855
|
+
*
|
|
856
|
+
* // Invalidation example
|
|
857
|
+
* queryClient.invalidateQueries({ queryKey: postKeys.all() })
|
|
858
|
+
* ```
|
|
859
|
+
*/
|
|
860
|
+
function createQueryKeyFactory(root, keys) {
|
|
861
|
+
const factory = { all: () => [root] };
|
|
862
|
+
for (const [key, handler] of Object.entries(keys)) if (handler === null) factory[key] = () => [root, key];
|
|
863
|
+
else factory[key] = (arg) => [
|
|
864
|
+
root,
|
|
865
|
+
key,
|
|
866
|
+
handler(arg)
|
|
867
|
+
];
|
|
868
|
+
return factory;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
//#endregion
|
|
872
|
+
//#region src/defer.ts
|
|
873
|
+
/**
|
|
874
|
+
* Prefetches a single query using the query client
|
|
875
|
+
*/
|
|
876
|
+
function prefetchSingleQuery(queryClient, query) {
|
|
877
|
+
return queryClient.prefetchQuery({
|
|
878
|
+
queryKey: query.queryKey,
|
|
879
|
+
queryFn: query.queryFn,
|
|
880
|
+
staleTime: query.staleTime,
|
|
881
|
+
gcTime: query.gcTime
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Start fetching queries without blocking navigation (fire-and-forget).
|
|
886
|
+
*
|
|
887
|
+
* Use this in route loaders for non-critical data. The queries will start
|
|
888
|
+
* fetching in the background, and components using useSuspenseQuery will
|
|
889
|
+
* suspend until the data is ready.
|
|
890
|
+
*
|
|
891
|
+
* @example
|
|
892
|
+
* ```ts
|
|
893
|
+
* export const Route = createFileRoute('/users/:userId')({
|
|
894
|
+
* loader: async ({ params, context }) => {
|
|
895
|
+
* const { queryClient } = context
|
|
896
|
+
*
|
|
897
|
+
* // Critical - blocks navigation until ready
|
|
898
|
+
* await queryClient.ensureQueryData(userQuery(params.userId))
|
|
899
|
+
*
|
|
900
|
+
* // Deferred - starts fetching without blocking navigation
|
|
901
|
+
* defer(queryClient,
|
|
902
|
+
* userActivityQuery(params.userId),
|
|
903
|
+
* userRecommendationsQuery(params.userId),
|
|
904
|
+
* )
|
|
905
|
+
* },
|
|
906
|
+
* })
|
|
907
|
+
* ```
|
|
908
|
+
*/
|
|
909
|
+
function defer(queryClient, ...queries) {
|
|
910
|
+
for (const query of queries) prefetchSingleQuery(queryClient, query);
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Start fetching queries and return promises for optional awaiting.
|
|
914
|
+
*
|
|
915
|
+
* Unlike defer(), this returns the prefetch promises, allowing you to
|
|
916
|
+
* optionally await them later (e.g., for analytics or debugging).
|
|
917
|
+
*
|
|
918
|
+
* @example
|
|
919
|
+
* ```ts
|
|
920
|
+
* // Start fetching
|
|
921
|
+
* const promises = deferWithPromises(queryClient, slowQuery1, slowQuery2)
|
|
922
|
+
*
|
|
923
|
+
* // Continue with other work...
|
|
924
|
+
*
|
|
925
|
+
* // Later, if you need to know when they complete:
|
|
926
|
+
* await Promise.all(promises)
|
|
927
|
+
* ```
|
|
928
|
+
*/
|
|
929
|
+
function deferWithPromises(queryClient, ...queries) {
|
|
930
|
+
return queries.map((query) => prefetchSingleQuery(queryClient, query));
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
//#endregion
|
|
934
|
+
//#region src/suspense.ts
|
|
935
|
+
/**
|
|
936
|
+
* @kimesh/query - Suspense Query Composables
|
|
937
|
+
*
|
|
938
|
+
* Provides a simple API for suspense-compatible data fetching.
|
|
939
|
+
* Uses top-level await in <script setup> to trigger Vue's Suspense.
|
|
940
|
+
*
|
|
941
|
+
* @example
|
|
942
|
+
* ```vue
|
|
943
|
+
* <script setup>
|
|
944
|
+
* // Top-level await triggers Suspense automatically
|
|
945
|
+
* const { data } = await useSuspenseQuery(postsQuery)
|
|
946
|
+
* <\/script>
|
|
947
|
+
* ```
|
|
948
|
+
*/
|
|
949
|
+
/**
|
|
950
|
+
* Suspense-compatible query hook for Vue.
|
|
951
|
+
*
|
|
952
|
+
* Returns a Promise that resolves to the query result when data is ready.
|
|
953
|
+
* Use with top-level await in <script setup> to trigger Suspense.
|
|
954
|
+
*
|
|
955
|
+
* @example
|
|
956
|
+
* ```vue
|
|
957
|
+
* <script setup>
|
|
958
|
+
* import { useSuspenseQuery } from '@kimesh/query'
|
|
959
|
+
* import { postsQuery } from './posts.data'
|
|
960
|
+
*
|
|
961
|
+
* // Awaiting triggers Suspense - component suspends until data ready
|
|
962
|
+
* // data is typed as Ref<Post[]> (not Ref<Post[] | undefined>)
|
|
963
|
+
* const { data: posts } = await useSuspenseQuery(postsQuery)
|
|
964
|
+
* <\/script>
|
|
965
|
+
*
|
|
966
|
+
* <template>
|
|
967
|
+
* <div v-for="post in posts" :key="post.id">{{ post.title }}</div>
|
|
968
|
+
* </template>
|
|
969
|
+
* ```
|
|
970
|
+
*/
|
|
971
|
+
async function useSuspenseQuery(options) {
|
|
972
|
+
const result = useQuery$1({
|
|
973
|
+
queryKey: options.queryKey,
|
|
974
|
+
queryFn: options.queryFn,
|
|
975
|
+
staleTime: options.staleTime,
|
|
976
|
+
gcTime: options.gcTime,
|
|
977
|
+
enabled: options.enabled
|
|
978
|
+
});
|
|
979
|
+
await result.suspense();
|
|
980
|
+
return result;
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Suspense-compatible infinite query hook for Vue.
|
|
984
|
+
*
|
|
985
|
+
* Note: This is a basic implementation. For full infinite query support,
|
|
986
|
+
* you may need to extend the options interface to include initialPageParam
|
|
987
|
+
* and getNextPageParam configuration.
|
|
988
|
+
*/
|
|
989
|
+
async function useSuspenseInfiniteQuery(options) {
|
|
990
|
+
const result = useInfiniteQuery$1({
|
|
991
|
+
queryKey: options.queryKey,
|
|
992
|
+
queryFn: options.queryFn,
|
|
993
|
+
staleTime: options.staleTime,
|
|
994
|
+
gcTime: options.gcTime,
|
|
995
|
+
enabled: options.enabled,
|
|
996
|
+
initialPageParam: 0,
|
|
997
|
+
getNextPageParam: () => void 0
|
|
998
|
+
});
|
|
999
|
+
await result.suspense();
|
|
1000
|
+
return result;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
//#endregion
|
|
1004
|
+
export { $fetch, KimeshQueryPlugin, QueryClient, VueQueryPlugin, createKimeshQueryClient, createKmFetch, createNamedFetchPlugin, createPrefetcher, createQueryKeyFactory, defer, deferWithPromises, defineMutation, defineQuery, defineQueryOptions, executeLoader, extractLoaders, fetchPlugin, getCurrentLayer, getLayerFetch, hasLoader, initGlobalFetch, initLayerFetch, isLayerFetchInitialized, layerFetchPlugin, queryOptions, setCurrentLayer, setupQueryPrefetching, useGlobalFetch, useInfiniteQuery, useKmAsyncData, useKmData, useKmFetch, useLazyKmAsyncData, useLazyKmFetch, useMutation, useQuery, useQueryClient, useSuspenseInfiniteQuery, useSuspenseQuery };
|
|
1005
|
+
//# sourceMappingURL=index.mjs.map
|