@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/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