@ametie/vue-muza-use 0.9.0 โ†’ 0.10.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 CHANGED
@@ -9,6 +9,14 @@
9
9
 
10
10
  A production-ready composable that eliminates boilerplate and solves the hard problems: race conditions, token refresh queues, automatic retries, and reactive request management. Write less code, ship faster, sleep better.
11
11
 
12
+ > [!IMPORTANT]
13
+ > ### ๐Ÿค– Claude Code โ€” Built-in AI Skill
14
+ >
15
+ > This library ships with a skill that teaches Claude the feature wrapper pattern, naming conventions, and all `UseApiOptions`.
16
+ > Claude will generate correct, architecture-consistent API layer code out of the box โ€” no extra prompting needed.
17
+ >
18
+ > ๐Ÿ“„ **[View skill file โ†’](https://github.com/MortyQ/vue-useApi/blob/main/.claude/skills/use-api/SKILL.md)**
19
+
12
20
  ---
13
21
 
14
22
  ## โœจ Features
@@ -22,11 +30,15 @@ A production-ready composable that eliminates boilerplate and solves the hard pr
22
30
  - ๐Ÿš€ **Batch Requests** โ€” Execute multiple requests in parallel with progress tracking
23
31
  - ๐Ÿงน **Zero Memory Leaks** โ€” Automatic cleanup of pending requests on component unmount
24
32
  - ๐Ÿ”• **ignoreUpdates** โ€” Atomic updates without triggering intermediate requests
33
+ - ๐Ÿ—„๏ธ **Response Caching** โ€” In-memory cache with configurable TTL and manual invalidation
34
+ - โšก **Stale-While-Revalidate** โ€” Serve cached data instantly while refreshing silently in the background
35
+ - ๐Ÿ”ฌ **select** โ€” Transform or filter response data declaratively; re-applied on every fetch automatically
25
36
 
26
37
  **Advanced Features** (When you need them):
27
38
  - โ™ป๏ธ **Intelligent Retries** โ€” Lifecycle-aware retry logic with configurable status codes
28
39
  - ๐Ÿ” **JWT Token Management** โ€” Automatic token refresh with request queueing on 401 responses
29
40
  - ๐ŸŽ›๏ธ **Flexible Architecture** โ€” Bring your own Axios instance with full configuration control
41
+ - ๐Ÿช **withCredentials** โ€” Per-request cookie and cross-origin credential control
30
42
 
31
43
  ---
32
44
 
@@ -40,11 +52,14 @@ A production-ready composable that eliminates boilerplate and solves the hard pr
40
52
  **Core Features:**
41
53
  - [Watch & Auto-Refetch](#watch--auto-refetch)
42
54
  - [ignoreUpdates โ€” Atomic Updates Without Refetch](#ignoreupdates--atomic-updates-without-refetch)
55
+ - [Response Caching](#response-caching)
56
+ - [Stale-While-Revalidate (SWR)](#stale-while-revalidate-swr)
43
57
  - [Polling (Background Updates)](#polling-background-updates)
44
58
  - [Error Handling](#error-handling)
45
59
  - [retry โ€” Automatic Request Retry](#retry--automatic-request-retry)
46
60
  - [Loading States](#loading-states)
47
61
  - [Manual Data Updates (mutate)](#manual-data-updates-mutate)
62
+ - [select โ€” Declarative Data Transformation](#select--declarative-data-transformation)
48
63
 
49
64
  **Real-World Examples:**
50
65
  - [Data Table with Pagination](#data-table-with-pagination--sorting)
@@ -499,6 +514,248 @@ ignoreUpdates(() => {
499
514
 
500
515
  ---
501
516
 
517
+ ### Response Caching
518
+
519
+ **TL;DR: Pass `cache: 'key'` to serve repeated requests from memory instead of the network. Entries expire after 5 minutes by default.**
520
+
521
+ The cache is an in-memory `Map` shared across all `useApi` instances in the app.
522
+ It is intentionally simple: no reactive subscriptions, no persistence, no background timers.
523
+ Entries expire **lazily** โ€” stale entries are removed the next time they are read.
524
+
525
+ #### Basic Usage โ€” String Shorthand
526
+
527
+ ```vue
528
+ <script setup lang="ts">
529
+ import { useApi } from '@ametie/vue-muza-use'
530
+
531
+ const { data, loading } = useApi<Category[]>('/categories', {
532
+ cache: 'categories', // uses DEFAULT_STALE_TIME (5 minutes)
533
+ immediate: true,
534
+ })
535
+ </script>
536
+ ```
537
+
538
+ The first call hits the network and caches the result under the key `'categories'`.
539
+ Every subsequent `execute()` within 5 minutes is served from cache instantly โ€” `loading` never becomes `true` and no axios request is made.
540
+
541
+ #### Custom TTL โ€” CacheOptions Object
542
+
543
+ ```vue
544
+ <script setup lang="ts">
545
+ import { useApi } from '@ametie/vue-muza-use'
546
+
547
+ const { data, execute } = useApi<Product[]>('/products', {
548
+ cache: {
549
+ id: 'products',
550
+ staleTime: 60_000, // 1 minute
551
+ },
552
+ immediate: true,
553
+ })
554
+ </script>
555
+ ```
556
+
557
+ #### Cache Hit Behavior
558
+
559
+ When a valid cache entry is found:
560
+
561
+ | Property / Hook | Cache Hit |
562
+ |---|---|
563
+ | `loading` | stays `false` โ€” never set to `true` |
564
+ | `data` | updated immediately via `mutate()` |
565
+ | `onBefore` | **not called** |
566
+ | `onSuccess` | **not called** |
567
+ | `onFinish` | **not called** |
568
+ | axios request | **not made** |
569
+
570
+ This is intentional โ€” a cache hit is silent. If you need to know when data comes from cache vs the network, track it with `onSuccess` (only fires on network hits).
571
+
572
+ #### invalidateCache โ€” Bust Related Caches on Mutation
573
+
574
+ Use `invalidateCache` on a POST/PUT/DELETE to automatically clear caches when the mutation succeeds.
575
+
576
+ ```vue
577
+ <script setup lang="ts">
578
+ import { useApi } from '@ametie/vue-muza-use'
579
+
580
+ // GET โ€” caches the list
581
+ const { data: products, execute: reload } = useApi<Product[]>('/products', {
582
+ cache: 'products',
583
+ immediate: true,
584
+ })
585
+
586
+ // POST โ€” busts the list cache on success so the next GET hits the network
587
+ const { execute: createProduct, loading } = useApi('/products', {
588
+ method: 'POST',
589
+ invalidateCache: 'products',
590
+ })
591
+
592
+ async function submit(form: NewProduct) {
593
+ await createProduct({ data: form })
594
+ await reload() // cache is gone โ€” fetches fresh data
595
+ }
596
+ </script>
597
+ ```
598
+
599
+ `invalidateCache` fires **only on HTTP 2xx success**. It never runs in `catch` or `finally`.
600
+ Pass an array to bust multiple keys at once:
601
+
602
+ ```typescript
603
+ const { execute } = useApi('/orders', {
604
+ method: 'POST',
605
+ invalidateCache: ['orders', 'products', 'inventory'],
606
+ })
607
+ ```
608
+
609
+ #### Imperative Cache Control
610
+
611
+ Import `invalidateCache` or `clearAllCache` anywhere in your app โ€” outside components, in Pinia stores, in route guards:
612
+
613
+ ```typescript
614
+ import { invalidateCache, clearAllCache } from '@ametie/vue-muza-use'
615
+
616
+ // Bust a single key (e.g. after a WebSocket push)
617
+ invalidateCache('products')
618
+
619
+ // Bust multiple keys at once
620
+ invalidateCache(['products', 'categories'])
621
+
622
+ // Wipe everything โ€” call on logout to prevent data leaks between users
623
+ clearAllCache()
624
+ ```
625
+
626
+ #### cache + watch
627
+
628
+ When `watch` is configured, each watch-triggered `execute()` still checks the cache first:
629
+
630
+ ```vue
631
+ <script setup lang="ts">
632
+ import { useApi } from '@ametie/vue-muza-use'
633
+ import { ref } from 'vue'
634
+
635
+ const categoryId = ref<number>(1)
636
+
637
+ const { data } = useApi<Product[]>(() => `/categories/${categoryId.value}/products`, {
638
+ cache: { id: `products-cat-${categoryId.value}`, staleTime: 30_000 },
639
+ watch: categoryId,
640
+ immediate: true,
641
+ })
642
+ </script>
643
+ ```
644
+
645
+ > [!NOTE]
646
+ > The cache `id` is evaluated once when `useApi` is called. To cache per category,
647
+ > use a computed or a dynamic key string derived from your reactive state.
648
+
649
+ #### cache + retry
650
+
651
+ Cache is written **after the final successful attempt**, not after the first.
652
+ If the first attempt fails and a retry succeeds, the retry's response is cached:
653
+
654
+ ```typescript
655
+ const { data } = useApi('/reports', {
656
+ cache: 'reports',
657
+ retry: 2,
658
+ retryStatusCodes: [500, 503],
659
+ immediate: true,
660
+ })
661
+ ```
662
+
663
+ #### Complete Options Reference
664
+
665
+ | Option | Type | Default | Description |
666
+ |--------|------|---------|-------------|
667
+ | `cache` | `string \| CacheOptions` | `undefined` | Enable caching. String = `{ id, staleTime: 300_000 }` shorthand |
668
+ | `invalidateCache` | `string \| string[]` | `undefined` | Cache key(s) to delete on 2xx success |
669
+
670
+ **`CacheOptions`**
671
+
672
+ | Field | Type | Default | Description |
673
+ |-------|------|---------|-------------|
674
+ | `id` | `string` | โ€” | Unique cache key |
675
+ | `staleTime` | `number` | `300_000` | TTL in milliseconds. Entry is deleted on next read after this time |
676
+
677
+ #### Out of Scope (by design)
678
+
679
+ The following are intentionally **not** supported in v1:
680
+
681
+ - ๐Ÿšซ No reactive cache entries โ€” the cache is a plain `Map`, not Vue refs
682
+ - ๐Ÿšซ No `localStorage` / `sessionStorage` persistence
683
+ - ๐Ÿšซ No background TTL timers โ€” expiry is checked lazily on read
684
+ - ๐Ÿšซ No cache for `useApiBatch` โ€” batch requests manage their own state
685
+ - ๐Ÿšซ No automatic refetch on cache invalidation โ€” call `execute()` manually after invalidating
686
+ - ๐Ÿšซ No request deduplication โ€” concurrent calls for the same key each fire their own request
687
+
688
+ > [!WARNING]
689
+ > The cache store is **module-level** (a singleton). In SSR / Node.js environments it is
690
+ > shared between all incoming requests. Call `clearAllCache()` between requests or avoid
691
+ > using caching in SSR contexts.
692
+
693
+ ---
694
+
695
+ ### Stale-While-Revalidate (SWR)
696
+
697
+ **TL;DR: Return cached data instantly while fetching fresh data in the background. No loading spinner, no blank screen.**
698
+
699
+ Requires the `cache` option to be set. On a cache hit, the stale data is returned immediately (no `loading: true`, no spinner) while a silent background request runs. Use the `revalidating` ref to show a subtle refresh indicator if needed.
700
+
701
+ On a **cache miss** (first load), the request behaves exactly like a normal request โ€” `loading: true`, no stale data.
702
+
703
+ #### Basic Usage
704
+
705
+ ```vue
706
+ <script setup lang="ts">
707
+ import { useApi } from '@ametie/vue-muza-use'
708
+
709
+ interface User { id: number; name: string }
710
+
711
+ const { data, revalidating } = useApi<User[]>('/users', {
712
+ cache: 'users',
713
+ staleWhileRevalidate: true,
714
+ immediate: true,
715
+ })
716
+ </script>
717
+
718
+ <template>
719
+ <!-- data renders immediately from cache โ€” no blank screen -->
720
+ <ul>
721
+ <li v-for="user in data" :key="user.id">
722
+ {{ user.name }}
723
+ <span v-if="revalidating">โ†ป</span>
724
+ </li>
725
+ </ul>
726
+ </template>
727
+ ```
728
+
729
+ #### SWR vs Normal Cache Hit
730
+
731
+ | | Normal cache hit | SWR cache hit |
732
+ |---|---|---|
733
+ | `loading` | `false` | `false` |
734
+ | `data` | Stale data, no new request | Stale data โ†’ then fresh data |
735
+ | `revalidating` | `false` | `true` while fetching, then `false` |
736
+ | Axios request | **Not made** | **Made** (silent background fetch) |
737
+ | `onBefore` | Not called | **Not called** (silent) |
738
+ | `onSuccess` | Not called | **Called** with fresh response |
739
+ | `onFinish` | Not called | **Called** after background fetch |
740
+
741
+ #### Error Handling
742
+
743
+ If the background revalidation request fails:
744
+ - `revalidating` resets to `false`
745
+ - `error` is set
746
+ - The **stale data is preserved** โ€” your UI doesn't go blank
747
+
748
+ ```typescript
749
+ const { data, revalidating, error } = useApi('/dashboard', {
750
+ cache: 'dashboard',
751
+ staleWhileRevalidate: true,
752
+ immediate: true,
753
+ })
754
+ // data.value stays the cached value even after a failed revalidation
755
+ ```
756
+
757
+ ---
758
+
502
759
  ### Polling (Background Updates)
503
760
 
504
761
  **TL;DR: Keep data fresh with smart polling that automatically pauses when the browser tab is hidden.**
@@ -768,6 +1025,73 @@ const { mutate } = useApi<User[]>('/users', { immediate: true })
768
1025
 
769
1026
  ---
770
1027
 
1028
+ ### select โ€” Declarative Data Transformation
1029
+
1030
+ **TL;DR: Transform, filter, or reshape response data once โ€” it's re-applied automatically on every fetch, polling tick, and SWR revalidation.**
1031
+
1032
+ Use `select` when you want the same transformation applied every time the request fires. Unlike `mutate` (which you call manually), `select` is declared once and runs silently on each response.
1033
+
1034
+ The second generic parameter of `useApi` controls the output type of `select`.
1035
+
1036
+ #### Extract a Nested Field
1037
+
1038
+ APIs that wrap responses in `{ data: [...], meta: {...} }`:
1039
+
1040
+ ```typescript
1041
+ interface ApiResponse { data: User[]; meta: { total: number } }
1042
+ interface User { id: number; name: string }
1043
+
1044
+ const { data } = useApi<ApiResponse, User[]>('/users', {
1045
+ immediate: true,
1046
+ select: (res) => res.data,
1047
+ // data.value is User[], not ApiResponse
1048
+ })
1049
+ ```
1050
+
1051
+ #### Transform Items
1052
+
1053
+ ```typescript
1054
+ interface RawUser { id: number; firstName: string; lastName: string }
1055
+ interface User { id: number; fullName: string }
1056
+
1057
+ const { data } = useApi<RawUser[], User[]>('/users', {
1058
+ immediate: true,
1059
+ select: (users) => users.map(u => ({
1060
+ id: u.id,
1061
+ fullName: `${u.firstName} ${u.lastName}`,
1062
+ })),
1063
+ })
1064
+ ```
1065
+
1066
+ #### Filter Results
1067
+
1068
+ ```typescript
1069
+ const { data } = useApi<Task[]>('/tasks', {
1070
+ immediate: true,
1071
+ select: (tasks) => tasks.filter(t => t.status === 'active'),
1072
+ })
1073
+ ```
1074
+
1075
+ #### select vs mutate
1076
+
1077
+ | | `select` | `mutate` |
1078
+ |---|---|---|
1079
+ | When it runs | On every successful response (auto) | When you call it manually |
1080
+ | With polling | Re-applied on every tick | Need to call in `onSuccess` each time |
1081
+ | With SWR | Re-applied on revalidation | Need to call in `onSuccess` |
1082
+ | `onSuccess` receives | Raw `AxiosResponse<TRaw>` | โ€” |
1083
+
1084
+ > [!NOTE]
1085
+ > `onSuccess` always receives the **raw** `AxiosResponse` from the server, not the selected value.
1086
+ > This lets you access headers, status, and the original shape if needed.
1087
+
1088
+ > [!NOTE]
1089
+ > The cache always stores the **raw** server response, not the selected value.
1090
+ > `select` is re-applied each time data is read from cache โ€” including SWR cache hits.
1091
+ > If you change your `select` function, the next cache hit will re-apply the new transformation.
1092
+
1093
+ ---
1094
+
771
1095
  ## ๐Ÿ“Š Real-World Examples
772
1096
 
773
1097
  ### Data Table with Pagination & Sorting
@@ -1172,6 +1496,38 @@ const api = createApiClient({
1172
1496
 
1173
1497
  ---
1174
1498
 
1499
+ ### withCredentials โ€” Per-Request Cookie Control
1500
+
1501
+ **TL;DR: Override the Axios instance default for a single request without changing global settings.**
1502
+
1503
+ `withCredentials` controls whether cookies and other credentials are included in cross-origin requests (CORS). Set it globally in `createApiClient` and override it per request when needed.
1504
+
1505
+ ```typescript
1506
+ // Global: withCredentials: false (Bearer token auth, no cookies)
1507
+ const api = createApiClient({ baseURL: '/api' })
1508
+
1509
+ // Override: this specific endpoint needs cookies
1510
+ const { data } = useApi('/user/session', {
1511
+ withCredentials: true,
1512
+ immediate: true,
1513
+ })
1514
+ ```
1515
+
1516
+ ```typescript
1517
+ // Global: withCredentials: true (full cookie-based auth)
1518
+ const api = createApiClient({ baseURL: '/api', withCredentials: true })
1519
+
1520
+ // Override: skip cookies for a public CDN request
1521
+ const { data } = useApi('https://cdn.example.com/config.json', {
1522
+ withCredentials: false,
1523
+ immediate: true,
1524
+ })
1525
+ ```
1526
+
1527
+ Omitting `withCredentials` in `useApi` options means the Axios instance default is used โ€” no override applied.
1528
+
1529
+ ---
1530
+
1175
1531
  ### Saving Tokens After Login
1176
1532
 
1177
1533
  ```typescript
@@ -1480,14 +1836,22 @@ function useMyCustomComposable<T>(fetchFn: () => Promise<T>) {
1480
1836
 
1481
1837
  ## ๐Ÿ“š API Reference
1482
1838
 
1483
- ### `useApi<T, D>(url, options)`
1839
+ ### `useApi<TRaw, D, TSelected>(url, options)`
1840
+
1841
+ Three type parameters โ€” all optional with defaults:
1842
+
1843
+ | Parameter | Default | Description |
1844
+ |-----------|---------|-------------|
1845
+ | `TRaw` | `unknown` | Shape of the raw response data from the server |
1846
+ | `D` | `unknown` | Shape of the request body / params |
1847
+ | `TSelected` | `TRaw` | Shape of `data.value` after `select` is applied. Equals `TRaw` when `select` is not used |
1484
1848
 
1485
1849
  **Arguments:**
1486
1850
 
1487
1851
  | Argument | Type | Description |
1488
1852
  |----------|------|-------------|
1489
1853
  | `url` | `MaybeRefOrGetter<string \| undefined>` | API endpoint. String, ref, or getter function. Returning `undefined` prevents the request. |
1490
- | `options` | `UseApiOptions<T, D>` | Configuration object (see below). |
1854
+ | `options` | `UseApiOptions<TRaw, D, TSelected>` | Configuration object (see below). |
1491
1855
 
1492
1856
  ---
1493
1857
 
@@ -1511,6 +1875,14 @@ function useMyCustomComposable<T>(fetchFn: () => Promise<T>) {
1511
1875
  | `watch` | `WatchSource \| WatchSource[]` | `undefined` | One or more refs to watch โ€” request re-fires when any of them change |
1512
1876
  | `debounce` | `number` | `0` | Milliseconds to wait after the last watch change before firing the request |
1513
1877
 
1878
+ **Caching:**
1879
+
1880
+ | Option | Type | Default | Description |
1881
+ |--------|------|---------|-------------|
1882
+ | `cache` | `string \| CacheOptions` | `undefined` | Cache the response in memory. String shorthand uses default 5-min TTL. See [Response Caching](#response-caching) |
1883
+ | `invalidateCache` | `string \| string[]` | `undefined` | Cache key(s) to delete on 2xx success. Never fires on error |
1884
+ | `staleWhileRevalidate` | `boolean` | `false` | When `true` and a cache hit occurs, return stale data immediately and revalidate in the background. `revalidating` is `true` during the background fetch. See [SWR](#stale-while-revalidate-swr) |
1885
+
1514
1886
  **Polling:**
1515
1887
 
1516
1888
  | Option | Type | Default | Description |
@@ -1525,11 +1897,17 @@ function useMyCustomComposable<T>(fetchFn: () => Promise<T>) {
1525
1897
  | `retryDelay` | `number` | `1000` | How many milliseconds to wait between retry attempts |
1526
1898
  | `retryStatusCodes` | `number[]` | `[408,429,500,502,503,504]` | HTTP status codes that trigger a retry. `[]` means retry on any error |
1527
1899
 
1900
+ **Data Transformation:**
1901
+
1902
+ | Option | Type | Default | Description |
1903
+ |--------|------|---------|-------------|
1904
+ | `select` | `(data: TRaw) => TSelected` | `undefined` | Transform response data before it is stored in `data`. Re-applied on every fetch, polling tick, and SWR revalidation. Cache always stores raw data. See [select](#select--declarative-data-transformation) |
1905
+
1528
1906
  **State Initialization:**
1529
1907
 
1530
1908
  | Option | Type | Default | Description |
1531
1909
  |--------|------|---------|-------------|
1532
- | `initialData` | `T` | `null` | Initial value for `data` before the first request completes |
1910
+ | `initialData` | `TSelected` | `null` | Initial value for `data` before the first request completes |
1533
1911
  | `initialLoading` | `boolean` | `false` | Initial value for `loading` โ€” set `true` to show a spinner before the first request fires |
1534
1912
 
1535
1913
  **Lifecycle Hooks:**
@@ -1547,6 +1925,12 @@ function useMyCustomComposable<T>(fetchFn: () => Promise<T>) {
1547
1925
  |--------|------|---------|-------------|
1548
1926
  | `skipErrorNotification` | `boolean` | `false` | When `true`, the global `onError` handler is NOT called for this request |
1549
1927
 
1928
+ **Credentials:**
1929
+
1930
+ | Option | Type | Default | Description |
1931
+ |--------|------|---------|-------------|
1932
+ | `withCredentials` | `boolean` | `undefined` | Override the Axios instance default for this request only. `true` = send cookies/credentials, `false` = omit them. Omitting uses the instance default set in `createApiClient` |
1933
+
1550
1934
  **Advanced:**
1551
1935
 
1552
1936
  | Option | Type | Default | Description |
@@ -1559,13 +1943,14 @@ function useMyCustomComposable<T>(fetchFn: () => Promise<T>) {
1559
1943
 
1560
1944
  | Name | Type | Description |
1561
1945
  |------|------|-------------|
1562
- | `data` | `Ref<T \| null>` | Response data from the last successful request |
1946
+ | `data` | `Ref<TSelected \| null>` | Response data from the last successful request (transformed by `select` if provided) |
1563
1947
  | `loading` | `Ref<boolean>` | `true` while a request is in flight (including retry delays) |
1564
1948
  | `error` | `Ref<ApiError \| null>` | Error from the last failed request; `null` on success |
1565
1949
  | `statusCode` | `Ref<number \| null>` | HTTP status code from the last completed request |
1566
- | `response` | `Ref<AxiosResponse<T> \| null>` | Full Axios response object including headers |
1567
- | `execute(config?)` | `(config?: ApiRequestConfig<D>) => Promise<T \| null>` | Manually trigger the request, optionally overriding options |
1568
- | `mutate(newData)` | `(newData: T \| null \| ((prev: T \| null) => T \| null)) => void` | Update `data` locally without a network request; clears `error` |
1950
+ | `response` | `Ref<AxiosResponse<unknown> \| null>` | Full Axios response object including headers (raw, before `select`) |
1951
+ | `revalidating` | `Ref<boolean>` | `true` while a background SWR revalidation is in flight. Always `false` when `staleWhileRevalidate` is not set |
1952
+ | `execute(config?)` | `(config?: ApiRequestConfig<D>) => Promise<TSelected \| null>` | Manually trigger the request, optionally overriding options |
1953
+ | `mutate(newData)` | `(newData: TSelected \| null \| ((prev: TSelected \| null) => TSelected \| null)) => void` | Update `data` locally without a network request; clears `error` |
1569
1954
  | `abort(msg?)` | `(message?: string) => void` | Cancel the current in-flight request |
1570
1955
  | `reset()` | `() => void` | Cancel the request and reset all state to initial values |
1571
1956
  | `ignoreUpdates(fn)` | `(updater: () => void) => void` | Run `updater` without triggering watch-based re-execution |
@@ -1768,6 +2153,45 @@ const { data, loading, error, mutate, setLoading, setError, reset } =
1768
2153
 
1769
2154
  ---
1770
2155
 
2156
+ ### `invalidateCache(id)` / `clearAllCache()`
2157
+
2158
+ **TL;DR: Imperatively delete one, many, or all cache entries from anywhere in your app.**
2159
+
2160
+ ```typescript
2161
+ import { invalidateCache, clearAllCache } from '@ametie/vue-muza-use'
2162
+ ```
2163
+
2164
+ | Function | Signature | Description |
2165
+ |----------|-----------|-------------|
2166
+ | `invalidateCache` | `(id: string \| string[]) => void` | Delete one or more cache entries by key |
2167
+ | `clearAllCache` | `() => void` | Wipe the entire cache โ€” use on logout |
2168
+
2169
+ **Example โ€” bust cache after a WebSocket push:**
2170
+
2171
+ ```typescript
2172
+ // pinia store or composable outside a component
2173
+ import { invalidateCache } from '@ametie/vue-muza-use'
2174
+
2175
+ socket.on('products:updated', () => {
2176
+ invalidateCache('products')
2177
+ })
2178
+ ```
2179
+
2180
+ **Example โ€” clear all on logout:**
2181
+
2182
+ ```typescript
2183
+ import { clearAllCache } from '@ametie/vue-muza-use'
2184
+ import { tokenManager } from '@ametie/vue-muza-use'
2185
+
2186
+ function logout() {
2187
+ tokenManager.clearTokens()
2188
+ clearAllCache() // prevent stale data from leaking to the next user session
2189
+ router.push('/login')
2190
+ }
2191
+ ```
2192
+
2193
+ ---
2194
+
1771
2195
  ## ๐Ÿงฉ Common Patterns
1772
2196
 
1773
2197
  ### 1. Search with Debounce and Reset
package/dist/index.cjs CHANGED
@@ -285,11 +285,20 @@ function useApi(url, options = {}) {
285
285
  useGlobalAbort = globalOptions?.useGlobalAbort ?? true,
286
286
  initialLoading = false,
287
287
  poll = 0,
288
+ // Explicitly excluded from axiosConfig โ€” these are useApi-only options
289
+ // and must not be forwarded to axios.request()
290
+ cache: _cache,
291
+ invalidateCache: _invalidateCache,
292
+ watch: _watch,
293
+ staleWhileRevalidate = false,
294
+ select,
288
295
  ...axiosConfig
289
296
  } = options;
290
297
  const maxRetries = retry === false ? 0 : retry === true ? 3 : retry;
298
+ const applySelect = (raw) => select ? select(raw) : raw;
291
299
  const startLoading = initialLoading ?? immediate;
292
300
  const state = useApiState(initialData, { initialLoading: startLoading });
301
+ const revalidating = (0, import_vue4.ref)(false);
293
302
  const abortController2 = (0, import_vue4.ref)(null);
294
303
  const globalAbort = useGlobalAbort ? useAbortController() : null;
295
304
  let pollTimer = null;
@@ -306,11 +315,16 @@ function useApi(url, options = {}) {
306
315
  };
307
316
  const executeRequest = async (config) => {
308
317
  const cacheOpts = normalizeCacheOptions(options.cache);
318
+ let isRevalidating = false;
309
319
  if (cacheOpts) {
310
320
  const cached = readCache(cacheOpts.id);
311
321
  if (cached !== null) {
312
- state.mutate(cached);
313
- return cached;
322
+ state.mutate(applySelect(cached));
323
+ if (!staleWhileRevalidate) {
324
+ return applySelect(cached);
325
+ }
326
+ isRevalidating = true;
327
+ revalidating.value = true;
314
328
  }
315
329
  }
316
330
  if (pollTimer) clearTimeout(pollTimer);
@@ -324,15 +338,16 @@ function useApi(url, options = {}) {
324
338
  const gs = globalAbort.getSignal();
325
339
  if (!gs.aborted) {
326
340
  subscribedSignal = gs;
327
- const currentCount = globalAbort.abortCount.value;
328
341
  globalAbortHandler = () => {
329
- if (globalAbort.abortCount.value === currentCount) controller.abort("Global filter change");
342
+ controller.abort("Cancelled by global abort");
330
343
  };
331
344
  gs.addEventListener("abort", globalAbortHandler);
332
345
  }
333
346
  }
334
- onBefore?.();
335
- state.setLoading(true);
347
+ if (!isRevalidating) {
348
+ onBefore?.();
349
+ state.setLoading(true);
350
+ }
336
351
  state.setError(null);
337
352
  let wasCancelled = false;
338
353
  let retryCount = 0;
@@ -356,7 +371,8 @@ function useApi(url, options = {}) {
356
371
  signal: controller.signal,
357
372
  ...{ authMode: config?.authMode || authMode }
358
373
  });
359
- state.mutate(response.data, response);
374
+ const selected = applySelect(response.data);
375
+ state.mutate(selected, response);
360
376
  state.setStatusCode(response.status);
361
377
  if (cacheOpts) {
362
378
  writeCache(cacheOpts.id, response.data, cacheOpts.staleTime);
@@ -365,7 +381,7 @@ function useApi(url, options = {}) {
365
381
  invalidateCache(options.invalidateCache);
366
382
  }
367
383
  onSuccess?.(response);
368
- return response.data;
384
+ return selected;
369
385
  } catch (err) {
370
386
  if (controller.signal.aborted || (0, import_axios2.isAxiosError)(err) && err.code === "ERR_CANCELED") {
371
387
  wasCancelled = true;
@@ -407,8 +423,9 @@ function useApi(url, options = {}) {
407
423
  return null;
408
424
  } finally {
409
425
  if (globalAbortHandler && subscribedSignal) subscribedSignal.removeEventListener("abort", globalAbortHandler);
426
+ revalidating.value = false;
410
427
  if (!wasCancelled) {
411
- state.setLoading(false);
428
+ if (!isRevalidating) state.setLoading(false);
412
429
  onFinish?.();
413
430
  const { interval, whenHidden } = getPollConfig();
414
431
  if (interval > 0) {
@@ -492,8 +509,10 @@ function useApi(url, options = {}) {
492
509
  }
493
510
  }, { deep: true });
494
511
  }
495
- return { ...state, execute, abort, reset, ignoreUpdates };
512
+ return { ...state, revalidating, execute, abort, reset, ignoreUpdates };
496
513
  }
514
+
515
+ // src/useApi.helpers.ts
497
516
  function useApiGet(url, options) {
498
517
  return useApi(url, { ...options, method: "GET" });
499
518
  }
package/dist/index.d.cts CHANGED
@@ -37,18 +37,97 @@ interface ApiRequestConfig<D = unknown> extends Omit<AxiosRequestConfig<D>, "dat
37
37
  * Empty array = retry on any error (network errors included).
38
38
  */
39
39
  retryStatusCodes?: number[];
40
+ /**
41
+ * Include credentials (cookies, Authorization headers) in cross-origin requests.
42
+ *
43
+ * Supports three auth strategies:
44
+ *
45
+ * **1. Bearer token (default)** โ€” tokens in localStorage, no cookies needed:
46
+ * ```ts
47
+ * createApiClient({ baseURL: '/api' }) // withCredentials: false by default
48
+ * ```
49
+ *
50
+ * **2. Full cookie-based auth** โ€” server sets httpOnly cookies for both tokens:
51
+ * ```ts
52
+ * createApiClient({ baseURL: '/api', withCredentials: true })
53
+ * ```
54
+ *
55
+ * **3. Hybrid** โ€” Bearer access token + httpOnly refresh cookie:
56
+ * ```ts
57
+ * createApiClient({
58
+ * baseURL: '/api',
59
+ * authOptions: { refreshWithCredentials: true } // cookies only on /auth/refresh
60
+ * })
61
+ * ```
62
+ *
63
+ * **Per-request override** โ€” override the global setting for a specific request:
64
+ * ```ts
65
+ * // Global: withCredentials: false, but this endpoint needs cookies
66
+ * const { data } = useApi('/user/profile', { withCredentials: true })
67
+ *
68
+ * // Global: withCredentials: true, but skip cookies for public CDN
69
+ * const { data } = useApi('https://cdn.example.com/config.json', {
70
+ * withCredentials: false,
71
+ * immediate: true
72
+ * })
73
+ * ```
74
+ */
75
+ withCredentials?: boolean;
40
76
  }
41
- interface UseApiOptions<T = unknown, D = unknown> extends ApiRequestConfig<D> {
77
+ interface UseApiOptions<T = unknown, D = unknown, TSelected = T> extends ApiRequestConfig<D> {
42
78
  immediate?: boolean;
43
79
  onSuccess?: (response: AxiosResponse<T>) => void;
44
80
  onError?: (error: ApiError) => void;
45
81
  onBefore?: () => void;
46
82
  onFinish?: () => void;
47
- initialData?: T;
83
+ /**
84
+ * Transform the raw response data before it is stored in `data`.
85
+ * Applied on every successful response โ€” including polling, SWR revalidation,
86
+ * and watch-triggered re-fetches. The cache always stores the raw server data;
87
+ * `select` is re-applied each time data is read from cache.
88
+ *
89
+ * The second generic parameter of `useApi` becomes the output type of `select`.
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * // Extract nested field
94
+ * const { data } = useApi<ApiResponse, User[]>('/users', {
95
+ * select: (res) => res.data,
96
+ * })
97
+ *
98
+ * // Transform items
99
+ * const { data } = useApi<RawUser[], User[]>('/users', {
100
+ * select: (users) => users.map(u => ({ ...u, fullName: `${u.first} ${u.last}` })),
101
+ * })
102
+ * ```
103
+ */
104
+ select?: (data: T) => TSelected;
105
+ initialData?: TSelected;
48
106
  debounce?: number;
49
107
  useGlobalAbort?: boolean;
50
108
  initialLoading?: boolean;
51
109
  watch?: WatchSource | WatchSource[];
110
+ /**
111
+ * Return cached data immediately and revalidate in the background.
112
+ *
113
+ * Requires the `cache` option to be set. On a cache hit the cached value
114
+ * is returned right away (no loading state, no spinner) while a fresh
115
+ * request runs silently. The `revalidating` ref is `true` during the
116
+ * background fetch so you can show a subtle indicator if needed.
117
+ *
118
+ * On a cache miss the request behaves normally (loading: true).
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * const { data, revalidating } = useApi('/users', {
123
+ * cache: 'users',
124
+ * staleWhileRevalidate: true,
125
+ * immediate: true,
126
+ * })
127
+ * // Template: <span v-if="revalidating">โ†ป</span>
128
+ * ```
129
+ */
130
+ staleWhileRevalidate?: boolean;
52
131
  /**
53
132
  * Polling configuration.
54
133
  * - Pass a **number** (ms) for simple polling.
@@ -81,7 +160,13 @@ interface UseApiReturn<T = unknown, D = unknown> {
81
160
  loading: Ref<boolean>;
82
161
  error: Ref<ApiError | null>;
83
162
  statusCode: Ref<number | null>;
84
- response: Ref<AxiosResponse<T> | null>;
163
+ response: Ref<AxiosResponse<unknown> | null>;
164
+ /**
165
+ * `true` while a background revalidation request is in-flight.
166
+ * Only active when `staleWhileRevalidate: true` and a cache hit occurred.
167
+ * Use it to show a subtle refresh indicator without blocking the UI.
168
+ */
169
+ revalidating: Ref<boolean>;
85
170
  execute: (config?: ApiRequestConfig<D>) => Promise<T | null | undefined>;
86
171
  abort: (message?: string) => void;
87
172
  reset: () => void;
@@ -254,7 +339,8 @@ declare function createApi(options: ApiPluginOptions): {
254
339
  };
255
340
  declare function useApiConfig(): ApiPluginOptions;
256
341
 
257
- declare function useApi<T = unknown, D = unknown>(url: MaybeRefOrGetter<string | undefined>, options?: UseApiOptions<T, D>): UseApiReturn<T, D>;
342
+ declare function useApi<T = unknown, D = unknown, TSelected = T>(url: MaybeRefOrGetter<string | undefined>, options?: UseApiOptions<T, D, TSelected>): UseApiReturn<TSelected, D>;
343
+
258
344
  /**
259
345
  * Helper for GET requests
260
346
  *
@@ -263,9 +349,14 @@ declare function useApi<T = unknown, D = unknown>(url: MaybeRefOrGetter<string |
263
349
  * const { data, loading, error } = useApiGet<User[]>('/users', {
264
350
  * immediate: true
265
351
  * })
352
+ *
353
+ * // With select:
354
+ * const { data } = useApiGet<ApiResponse, User[]>('/users', {
355
+ * select: (res) => res.data,
356
+ * })
266
357
  * ```
267
358
  */
268
- declare function useApiGet<T = unknown>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T>, "method">): UseApiReturn<T>;
359
+ declare function useApiGet<T = unknown, TSelected = T>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T, unknown, TSelected>, "method">): UseApiReturn<TSelected>;
269
360
  /**
270
361
  * Helper for POST requests
271
362
  *
@@ -275,7 +366,7 @@ declare function useApiGet<T = unknown>(url: MaybeRefOrGetter<string | undefined
275
366
  * await execute({ data: { name: 'John' } })
276
367
  * ```
277
368
  */
278
- declare function useApiPost<T = unknown, D = unknown>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T, D>, "method">): UseApiReturn<T, D>;
369
+ declare function useApiPost<T = unknown, D = unknown, TSelected = T>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T, D, TSelected>, "method">): UseApiReturn<TSelected, D>;
279
370
  /**
280
371
  * Helper for PUT requests
281
372
  *
@@ -285,7 +376,7 @@ declare function useApiPost<T = unknown, D = unknown>(url: MaybeRefOrGetter<stri
285
376
  * await execute({ data: { name: 'John Doe' } })
286
377
  * ```
287
378
  */
288
- declare function useApiPut<T = unknown, D = unknown>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T, D>, "method">): UseApiReturn<T, D>;
379
+ declare function useApiPut<T = unknown, D = unknown, TSelected = T>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T, D, TSelected>, "method">): UseApiReturn<TSelected, D>;
289
380
  /**
290
381
  * Helper for PATCH requests
291
382
  *
@@ -295,7 +386,7 @@ declare function useApiPut<T = unknown, D = unknown>(url: MaybeRefOrGetter<strin
295
386
  * await execute({ data: { name: 'John' } })
296
387
  * ```
297
388
  */
298
- declare function useApiPatch<T = unknown, D = unknown>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T, D>, "method">): UseApiReturn<T, D>;
389
+ declare function useApiPatch<T = unknown, D = unknown, TSelected = T>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T, D, TSelected>, "method">): UseApiReturn<TSelected, D>;
299
390
  /**
300
391
  * Helper for DELETE requests
301
392
  *
@@ -305,7 +396,7 @@ declare function useApiPatch<T = unknown, D = unknown>(url: MaybeRefOrGetter<str
305
396
  * await execute()
306
397
  * ```
307
398
  */
308
- declare function useApiDelete<T = unknown>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T>, "method">): UseApiReturn<T>;
399
+ declare function useApiDelete<T = unknown, TSelected = T>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T, unknown, TSelected>, "method">): UseApiReturn<TSelected>;
309
400
 
310
401
  /**
311
402
  * Execute multiple API requests in parallel with full reactive state
package/dist/index.d.ts CHANGED
@@ -37,18 +37,97 @@ interface ApiRequestConfig<D = unknown> extends Omit<AxiosRequestConfig<D>, "dat
37
37
  * Empty array = retry on any error (network errors included).
38
38
  */
39
39
  retryStatusCodes?: number[];
40
+ /**
41
+ * Include credentials (cookies, Authorization headers) in cross-origin requests.
42
+ *
43
+ * Supports three auth strategies:
44
+ *
45
+ * **1. Bearer token (default)** โ€” tokens in localStorage, no cookies needed:
46
+ * ```ts
47
+ * createApiClient({ baseURL: '/api' }) // withCredentials: false by default
48
+ * ```
49
+ *
50
+ * **2. Full cookie-based auth** โ€” server sets httpOnly cookies for both tokens:
51
+ * ```ts
52
+ * createApiClient({ baseURL: '/api', withCredentials: true })
53
+ * ```
54
+ *
55
+ * **3. Hybrid** โ€” Bearer access token + httpOnly refresh cookie:
56
+ * ```ts
57
+ * createApiClient({
58
+ * baseURL: '/api',
59
+ * authOptions: { refreshWithCredentials: true } // cookies only on /auth/refresh
60
+ * })
61
+ * ```
62
+ *
63
+ * **Per-request override** โ€” override the global setting for a specific request:
64
+ * ```ts
65
+ * // Global: withCredentials: false, but this endpoint needs cookies
66
+ * const { data } = useApi('/user/profile', { withCredentials: true })
67
+ *
68
+ * // Global: withCredentials: true, but skip cookies for public CDN
69
+ * const { data } = useApi('https://cdn.example.com/config.json', {
70
+ * withCredentials: false,
71
+ * immediate: true
72
+ * })
73
+ * ```
74
+ */
75
+ withCredentials?: boolean;
40
76
  }
41
- interface UseApiOptions<T = unknown, D = unknown> extends ApiRequestConfig<D> {
77
+ interface UseApiOptions<T = unknown, D = unknown, TSelected = T> extends ApiRequestConfig<D> {
42
78
  immediate?: boolean;
43
79
  onSuccess?: (response: AxiosResponse<T>) => void;
44
80
  onError?: (error: ApiError) => void;
45
81
  onBefore?: () => void;
46
82
  onFinish?: () => void;
47
- initialData?: T;
83
+ /**
84
+ * Transform the raw response data before it is stored in `data`.
85
+ * Applied on every successful response โ€” including polling, SWR revalidation,
86
+ * and watch-triggered re-fetches. The cache always stores the raw server data;
87
+ * `select` is re-applied each time data is read from cache.
88
+ *
89
+ * The second generic parameter of `useApi` becomes the output type of `select`.
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * // Extract nested field
94
+ * const { data } = useApi<ApiResponse, User[]>('/users', {
95
+ * select: (res) => res.data,
96
+ * })
97
+ *
98
+ * // Transform items
99
+ * const { data } = useApi<RawUser[], User[]>('/users', {
100
+ * select: (users) => users.map(u => ({ ...u, fullName: `${u.first} ${u.last}` })),
101
+ * })
102
+ * ```
103
+ */
104
+ select?: (data: T) => TSelected;
105
+ initialData?: TSelected;
48
106
  debounce?: number;
49
107
  useGlobalAbort?: boolean;
50
108
  initialLoading?: boolean;
51
109
  watch?: WatchSource | WatchSource[];
110
+ /**
111
+ * Return cached data immediately and revalidate in the background.
112
+ *
113
+ * Requires the `cache` option to be set. On a cache hit the cached value
114
+ * is returned right away (no loading state, no spinner) while a fresh
115
+ * request runs silently. The `revalidating` ref is `true` during the
116
+ * background fetch so you can show a subtle indicator if needed.
117
+ *
118
+ * On a cache miss the request behaves normally (loading: true).
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * const { data, revalidating } = useApi('/users', {
123
+ * cache: 'users',
124
+ * staleWhileRevalidate: true,
125
+ * immediate: true,
126
+ * })
127
+ * // Template: <span v-if="revalidating">โ†ป</span>
128
+ * ```
129
+ */
130
+ staleWhileRevalidate?: boolean;
52
131
  /**
53
132
  * Polling configuration.
54
133
  * - Pass a **number** (ms) for simple polling.
@@ -81,7 +160,13 @@ interface UseApiReturn<T = unknown, D = unknown> {
81
160
  loading: Ref<boolean>;
82
161
  error: Ref<ApiError | null>;
83
162
  statusCode: Ref<number | null>;
84
- response: Ref<AxiosResponse<T> | null>;
163
+ response: Ref<AxiosResponse<unknown> | null>;
164
+ /**
165
+ * `true` while a background revalidation request is in-flight.
166
+ * Only active when `staleWhileRevalidate: true` and a cache hit occurred.
167
+ * Use it to show a subtle refresh indicator without blocking the UI.
168
+ */
169
+ revalidating: Ref<boolean>;
85
170
  execute: (config?: ApiRequestConfig<D>) => Promise<T | null | undefined>;
86
171
  abort: (message?: string) => void;
87
172
  reset: () => void;
@@ -254,7 +339,8 @@ declare function createApi(options: ApiPluginOptions): {
254
339
  };
255
340
  declare function useApiConfig(): ApiPluginOptions;
256
341
 
257
- declare function useApi<T = unknown, D = unknown>(url: MaybeRefOrGetter<string | undefined>, options?: UseApiOptions<T, D>): UseApiReturn<T, D>;
342
+ declare function useApi<T = unknown, D = unknown, TSelected = T>(url: MaybeRefOrGetter<string | undefined>, options?: UseApiOptions<T, D, TSelected>): UseApiReturn<TSelected, D>;
343
+
258
344
  /**
259
345
  * Helper for GET requests
260
346
  *
@@ -263,9 +349,14 @@ declare function useApi<T = unknown, D = unknown>(url: MaybeRefOrGetter<string |
263
349
  * const { data, loading, error } = useApiGet<User[]>('/users', {
264
350
  * immediate: true
265
351
  * })
352
+ *
353
+ * // With select:
354
+ * const { data } = useApiGet<ApiResponse, User[]>('/users', {
355
+ * select: (res) => res.data,
356
+ * })
266
357
  * ```
267
358
  */
268
- declare function useApiGet<T = unknown>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T>, "method">): UseApiReturn<T>;
359
+ declare function useApiGet<T = unknown, TSelected = T>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T, unknown, TSelected>, "method">): UseApiReturn<TSelected>;
269
360
  /**
270
361
  * Helper for POST requests
271
362
  *
@@ -275,7 +366,7 @@ declare function useApiGet<T = unknown>(url: MaybeRefOrGetter<string | undefined
275
366
  * await execute({ data: { name: 'John' } })
276
367
  * ```
277
368
  */
278
- declare function useApiPost<T = unknown, D = unknown>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T, D>, "method">): UseApiReturn<T, D>;
369
+ declare function useApiPost<T = unknown, D = unknown, TSelected = T>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T, D, TSelected>, "method">): UseApiReturn<TSelected, D>;
279
370
  /**
280
371
  * Helper for PUT requests
281
372
  *
@@ -285,7 +376,7 @@ declare function useApiPost<T = unknown, D = unknown>(url: MaybeRefOrGetter<stri
285
376
  * await execute({ data: { name: 'John Doe' } })
286
377
  * ```
287
378
  */
288
- declare function useApiPut<T = unknown, D = unknown>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T, D>, "method">): UseApiReturn<T, D>;
379
+ declare function useApiPut<T = unknown, D = unknown, TSelected = T>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T, D, TSelected>, "method">): UseApiReturn<TSelected, D>;
289
380
  /**
290
381
  * Helper for PATCH requests
291
382
  *
@@ -295,7 +386,7 @@ declare function useApiPut<T = unknown, D = unknown>(url: MaybeRefOrGetter<strin
295
386
  * await execute({ data: { name: 'John' } })
296
387
  * ```
297
388
  */
298
- declare function useApiPatch<T = unknown, D = unknown>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T, D>, "method">): UseApiReturn<T, D>;
389
+ declare function useApiPatch<T = unknown, D = unknown, TSelected = T>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T, D, TSelected>, "method">): UseApiReturn<TSelected, D>;
299
390
  /**
300
391
  * Helper for DELETE requests
301
392
  *
@@ -305,7 +396,7 @@ declare function useApiPatch<T = unknown, D = unknown>(url: MaybeRefOrGetter<str
305
396
  * await execute()
306
397
  * ```
307
398
  */
308
- declare function useApiDelete<T = unknown>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T>, "method">): UseApiReturn<T>;
399
+ declare function useApiDelete<T = unknown, TSelected = T>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T, unknown, TSelected>, "method">): UseApiReturn<TSelected>;
309
400
 
310
401
  /**
311
402
  * Execute multiple API requests in parallel with full reactive state
package/dist/index.mjs CHANGED
@@ -232,11 +232,20 @@ function useApi(url, options = {}) {
232
232
  useGlobalAbort = globalOptions?.useGlobalAbort ?? true,
233
233
  initialLoading = false,
234
234
  poll = 0,
235
+ // Explicitly excluded from axiosConfig โ€” these are useApi-only options
236
+ // and must not be forwarded to axios.request()
237
+ cache: _cache,
238
+ invalidateCache: _invalidateCache,
239
+ watch: _watch,
240
+ staleWhileRevalidate = false,
241
+ select,
235
242
  ...axiosConfig
236
243
  } = options;
237
244
  const maxRetries = retry === false ? 0 : retry === true ? 3 : retry;
245
+ const applySelect = (raw) => select ? select(raw) : raw;
238
246
  const startLoading = initialLoading ?? immediate;
239
247
  const state = useApiState(initialData, { initialLoading: startLoading });
248
+ const revalidating = ref3(false);
240
249
  const abortController2 = ref3(null);
241
250
  const globalAbort = useGlobalAbort ? useAbortController() : null;
242
251
  let pollTimer = null;
@@ -253,11 +262,16 @@ function useApi(url, options = {}) {
253
262
  };
254
263
  const executeRequest = async (config) => {
255
264
  const cacheOpts = normalizeCacheOptions(options.cache);
265
+ let isRevalidating = false;
256
266
  if (cacheOpts) {
257
267
  const cached = readCache(cacheOpts.id);
258
268
  if (cached !== null) {
259
- state.mutate(cached);
260
- return cached;
269
+ state.mutate(applySelect(cached));
270
+ if (!staleWhileRevalidate) {
271
+ return applySelect(cached);
272
+ }
273
+ isRevalidating = true;
274
+ revalidating.value = true;
261
275
  }
262
276
  }
263
277
  if (pollTimer) clearTimeout(pollTimer);
@@ -271,15 +285,16 @@ function useApi(url, options = {}) {
271
285
  const gs = globalAbort.getSignal();
272
286
  if (!gs.aborted) {
273
287
  subscribedSignal = gs;
274
- const currentCount = globalAbort.abortCount.value;
275
288
  globalAbortHandler = () => {
276
- if (globalAbort.abortCount.value === currentCount) controller.abort("Global filter change");
289
+ controller.abort("Cancelled by global abort");
277
290
  };
278
291
  gs.addEventListener("abort", globalAbortHandler);
279
292
  }
280
293
  }
281
- onBefore?.();
282
- state.setLoading(true);
294
+ if (!isRevalidating) {
295
+ onBefore?.();
296
+ state.setLoading(true);
297
+ }
283
298
  state.setError(null);
284
299
  let wasCancelled = false;
285
300
  let retryCount = 0;
@@ -303,7 +318,8 @@ function useApi(url, options = {}) {
303
318
  signal: controller.signal,
304
319
  ...{ authMode: config?.authMode || authMode }
305
320
  });
306
- state.mutate(response.data, response);
321
+ const selected = applySelect(response.data);
322
+ state.mutate(selected, response);
307
323
  state.setStatusCode(response.status);
308
324
  if (cacheOpts) {
309
325
  writeCache(cacheOpts.id, response.data, cacheOpts.staleTime);
@@ -312,7 +328,7 @@ function useApi(url, options = {}) {
312
328
  invalidateCache(options.invalidateCache);
313
329
  }
314
330
  onSuccess?.(response);
315
- return response.data;
331
+ return selected;
316
332
  } catch (err) {
317
333
  if (controller.signal.aborted || isAxiosError2(err) && err.code === "ERR_CANCELED") {
318
334
  wasCancelled = true;
@@ -354,8 +370,9 @@ function useApi(url, options = {}) {
354
370
  return null;
355
371
  } finally {
356
372
  if (globalAbortHandler && subscribedSignal) subscribedSignal.removeEventListener("abort", globalAbortHandler);
373
+ revalidating.value = false;
357
374
  if (!wasCancelled) {
358
- state.setLoading(false);
375
+ if (!isRevalidating) state.setLoading(false);
359
376
  onFinish?.();
360
377
  const { interval, whenHidden } = getPollConfig();
361
378
  if (interval > 0) {
@@ -439,8 +456,10 @@ function useApi(url, options = {}) {
439
456
  }
440
457
  }, { deep: true });
441
458
  }
442
- return { ...state, execute, abort, reset, ignoreUpdates };
459
+ return { ...state, revalidating, execute, abort, reset, ignoreUpdates };
443
460
  }
461
+
462
+ // src/useApi.helpers.ts
444
463
  function useApiGet(url, options) {
445
464
  return useApi(url, { ...options, method: "GET" });
446
465
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ametie/vue-muza-use",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Powerful Vue 3 API composable (Muza Kit) with Axios, Auto-Refresh & TypeScript",
5
5
  "author": "MortyQ",
6
6
  "license": "MIT",