@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 +431 -7
- package/dist/index.cjs +29 -10
- package/dist/index.d.cts +100 -9
- package/dist/index.d.ts +100 -9
- package/dist/index.mjs +29 -10
- package/package.json +1 -1
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<
|
|
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<
|
|
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` | `
|
|
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<
|
|
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<
|
|
1567
|
-
| `
|
|
1568
|
-
| `
|
|
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
|
-
|
|
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
|
-
|
|
342
|
+
controller.abort("Cancelled by global abort");
|
|
330
343
|
};
|
|
331
344
|
gs.addEventListener("abort", globalAbortHandler);
|
|
332
345
|
}
|
|
333
346
|
}
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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<
|
|
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<
|
|
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<
|
|
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<
|
|
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<
|
|
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<
|
|
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<
|
|
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
|
-
|
|
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<
|
|
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<
|
|
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<
|
|
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<
|
|
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<
|
|
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<
|
|
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<
|
|
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
|
-
|
|
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
|
-
|
|
289
|
+
controller.abort("Cancelled by global abort");
|
|
277
290
|
};
|
|
278
291
|
gs.addEventListener("abort", globalAbortHandler);
|
|
279
292
|
}
|
|
280
293
|
}
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|