@ametie/vue-muza-use 0.6.1 โ 0.8.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 +1338 -615
- package/dist/index.cjs +161 -60
- package/dist/index.d.cts +70 -33
- package/dist/index.d.ts +70 -33
- package/dist/index.mjs +162 -61
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ametie/vue-muza-use ๐น
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@ametie/vue-muza-use)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -21,9 +21,10 @@ A production-ready composable that eliminates boilerplate and solves the hard pr
|
|
|
21
21
|
- ๐ **Auto-Polling** โ Built-in interval fetching with smart tab visibility detection
|
|
22
22
|
- ๐ **Batch Requests** โ Execute multiple requests in parallel with progress tracking
|
|
23
23
|
- ๐งน **Zero Memory Leaks** โ Automatic cleanup of pending requests on component unmount
|
|
24
|
+
- ๐ **ignoreUpdates** โ Atomic updates without triggering intermediate requests
|
|
24
25
|
|
|
25
26
|
**Advanced Features** (When you need them):
|
|
26
|
-
- โป๏ธ **Intelligent Retries** โ Lifecycle-aware retry logic
|
|
27
|
+
- โป๏ธ **Intelligent Retries** โ Lifecycle-aware retry logic with configurable status codes
|
|
27
28
|
- ๐ **JWT Token Management** โ Automatic token refresh with request queueing on 401 responses
|
|
28
29
|
- ๐๏ธ **Flexible Architecture** โ Bring your own Axios instance with full configuration control
|
|
29
30
|
|
|
@@ -38,10 +39,12 @@ A production-ready composable that eliminates boilerplate and solves the hard pr
|
|
|
38
39
|
|
|
39
40
|
**Core Features:**
|
|
40
41
|
- [Watch & Auto-Refetch](#watch--auto-refetch)
|
|
41
|
-
- [
|
|
42
|
+
- [ignoreUpdates โ Atomic Updates Without Refetch](#ignoreupdates--atomic-updates-without-refetch)
|
|
43
|
+
- [Polling (Background Updates)](#polling-background-updates)
|
|
42
44
|
- [Error Handling](#error-handling)
|
|
45
|
+
- [retry โ Automatic Request Retry](#retry--automatic-request-retry)
|
|
43
46
|
- [Loading States](#loading-states)
|
|
44
|
-
- [Manual Data Updates](#manual-data-updates)
|
|
47
|
+
- [Manual Data Updates (mutate)](#manual-data-updates-mutate)
|
|
45
48
|
|
|
46
49
|
**Real-World Examples:**
|
|
47
50
|
- [Data Table with Pagination](#data-table-with-pagination--sorting)
|
|
@@ -49,9 +52,13 @@ A production-ready composable that eliminates boilerplate and solves the hard pr
|
|
|
49
52
|
- [Batch Requests](#batch-requests)
|
|
50
53
|
|
|
51
54
|
**Advanced:**
|
|
52
|
-
- [
|
|
53
|
-
- [Authentication &
|
|
55
|
+
- [Advanced Configuration](#๏ธ-advanced-configuration)
|
|
56
|
+
- [Authentication & Token Management](#-authentication--token-management)
|
|
57
|
+
- [Error Handling Reference](#-error-handling-reference)
|
|
58
|
+
- [Utilities & Standalone Composables](#-utilities--standalone-composables)
|
|
54
59
|
- [API Reference](#-api-reference)
|
|
60
|
+
- [Common Patterns](#-common-patterns)
|
|
61
|
+
- [Troubleshooting](#-troubleshooting)
|
|
55
62
|
|
|
56
63
|
> ๐ก **New to the library?** Start with [Quick Start](#-quick-start), then explore [Basic Usage](#-basic-usage). Skip authentication until you need it!
|
|
57
64
|
|
|
@@ -70,6 +77,8 @@ pnpm add @ametie/vue-muza-use axios
|
|
|
70
77
|
yarn add @ametie/vue-muza-use axios
|
|
71
78
|
```
|
|
72
79
|
|
|
80
|
+
Peer dependencies are packages you need to install separately โ the library uses them but doesn't bundle them. You need `vue` (โฅ 3.x) and `axios` (โฅ 1.x) in your project.
|
|
81
|
+
|
|
73
82
|
---
|
|
74
83
|
|
|
75
84
|
## ๐ Quick Start
|
|
@@ -85,12 +94,10 @@ import App from './App.vue'
|
|
|
85
94
|
|
|
86
95
|
const app = createApp(App)
|
|
87
96
|
|
|
88
|
-
// Create API client with minimal config
|
|
89
97
|
const api = createApiClient({
|
|
90
98
|
baseURL: 'https://api.example.com'
|
|
91
99
|
})
|
|
92
100
|
|
|
93
|
-
// Install plugin
|
|
94
101
|
app.use(createApi({ axios: api }))
|
|
95
102
|
|
|
96
103
|
app.mount('#app')
|
|
@@ -111,7 +118,7 @@ interface User {
|
|
|
111
118
|
}
|
|
112
119
|
|
|
113
120
|
const { data, loading, error } = useApi<User>('/users/1', {
|
|
114
|
-
immediate: true
|
|
121
|
+
immediate: true
|
|
115
122
|
})
|
|
116
123
|
</script>
|
|
117
124
|
|
|
@@ -134,21 +141,26 @@ This example shows the library's power: **automatic race condition handling** an
|
|
|
134
141
|
import { ref } from 'vue'
|
|
135
142
|
import { useApi } from '@ametie/vue-muza-use'
|
|
136
143
|
|
|
144
|
+
interface Product {
|
|
145
|
+
id: number
|
|
146
|
+
name: string
|
|
147
|
+
price: number
|
|
148
|
+
}
|
|
149
|
+
|
|
137
150
|
const searchQuery = ref('')
|
|
138
151
|
|
|
139
|
-
|
|
140
|
-
const { data, loading } = useApi(
|
|
152
|
+
const { data, loading } = useApi<Product[]>(
|
|
141
153
|
() => `/products/search?q=${searchQuery.value}`,
|
|
142
154
|
{
|
|
143
|
-
watch: searchQuery,
|
|
144
|
-
debounce: 500
|
|
155
|
+
watch: searchQuery,
|
|
156
|
+
debounce: 500
|
|
145
157
|
}
|
|
146
158
|
)
|
|
147
159
|
</script>
|
|
148
160
|
|
|
149
161
|
<template>
|
|
150
162
|
<input v-model="searchQuery" placeholder="Search products..." />
|
|
151
|
-
|
|
163
|
+
|
|
152
164
|
<div v-if="loading">Searching...</div>
|
|
153
165
|
<ul v-else-if="data?.length">
|
|
154
166
|
<li v-for="product in data" :key="product.id">
|
|
@@ -159,11 +171,43 @@ const { data, loading } = useApi(
|
|
|
159
171
|
</template>
|
|
160
172
|
```
|
|
161
173
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
-
|
|
165
|
-
|
|
166
|
-
|
|
174
|
+
### 4. POST with Retry
|
|
175
|
+
|
|
176
|
+
Use `retry` to automatically re-attempt failed form submissions before showing an error.
|
|
177
|
+
|
|
178
|
+
```vue
|
|
179
|
+
<script setup lang="ts">
|
|
180
|
+
import { ref } from 'vue'
|
|
181
|
+
import { useApi } from '@ametie/vue-muza-use'
|
|
182
|
+
|
|
183
|
+
interface CreateOrderResponse {
|
|
184
|
+
id: number
|
|
185
|
+
status: string
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const form = ref({ productId: 1, quantity: 2 })
|
|
189
|
+
|
|
190
|
+
const { execute, loading, error } = useApi<CreateOrderResponse>(
|
|
191
|
+
'/orders',
|
|
192
|
+
{
|
|
193
|
+
method: 'POST',
|
|
194
|
+
data: form,
|
|
195
|
+
retry: 3,
|
|
196
|
+
retryDelay: 1000,
|
|
197
|
+
onSuccess: (response) => {
|
|
198
|
+
console.log('Order created:', response.data.id)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
)
|
|
202
|
+
</script>
|
|
203
|
+
|
|
204
|
+
<template>
|
|
205
|
+
<button :disabled="loading" @click="execute()">
|
|
206
|
+
{{ loading ? 'Placing order...' : 'Place Order' }}
|
|
207
|
+
</button>
|
|
208
|
+
<p v-if="error">{{ error.message }}</p>
|
|
209
|
+
</template>
|
|
210
|
+
```
|
|
167
211
|
|
|
168
212
|
---
|
|
169
213
|
|
|
@@ -173,33 +217,78 @@ const { data, loading } = useApi(
|
|
|
173
217
|
|
|
174
218
|
#### Manual Execution
|
|
175
219
|
```typescript
|
|
220
|
+
import { useApi } from '@ametie/vue-muza-use'
|
|
221
|
+
|
|
222
|
+
interface User {
|
|
223
|
+
id: number
|
|
224
|
+
name: string
|
|
225
|
+
}
|
|
226
|
+
|
|
176
227
|
const { data, loading, error, execute } = useApi<User>('/users/1')
|
|
177
228
|
|
|
178
|
-
// Trigger manually (e.g., on button click)
|
|
179
229
|
await execute()
|
|
180
230
|
```
|
|
181
231
|
|
|
182
232
|
#### Auto-Fetch on Mount
|
|
183
233
|
```typescript
|
|
234
|
+
import { useApi } from '@ametie/vue-muza-use'
|
|
235
|
+
|
|
236
|
+
interface User {
|
|
237
|
+
id: number
|
|
238
|
+
name: string
|
|
239
|
+
}
|
|
240
|
+
|
|
184
241
|
const { data } = useApi<User>('/users/1', {
|
|
185
|
-
immediate: true
|
|
242
|
+
immediate: true
|
|
186
243
|
})
|
|
187
244
|
```
|
|
188
245
|
|
|
189
246
|
#### With Query Parameters
|
|
190
247
|
```typescript
|
|
248
|
+
import { ref } from 'vue'
|
|
249
|
+
import { useApi } from '@ametie/vue-muza-use'
|
|
250
|
+
|
|
191
251
|
const filters = ref({
|
|
192
252
|
status: 'active',
|
|
193
253
|
limit: 20
|
|
194
254
|
})
|
|
195
255
|
|
|
196
256
|
const { data } = useApi('/users', {
|
|
197
|
-
params: filters,
|
|
198
|
-
watch: filters,
|
|
257
|
+
params: filters,
|
|
258
|
+
watch: filters,
|
|
199
259
|
immediate: true
|
|
200
260
|
})
|
|
201
261
|
```
|
|
202
262
|
|
|
263
|
+
### Conditional Fetching
|
|
264
|
+
|
|
265
|
+
Pass a getter function that returns `undefined` to prevent a request from firing until a required value is available.
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
import { ref } from 'vue'
|
|
269
|
+
import { useApi } from '@ametie/vue-muza-use'
|
|
270
|
+
|
|
271
|
+
interface User {
|
|
272
|
+
id: number
|
|
273
|
+
name: string
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const id = ref<number | null>(null)
|
|
277
|
+
|
|
278
|
+
const { data } = useApi<User>(
|
|
279
|
+
() => id.value ? `/users/${id.value}` : undefined,
|
|
280
|
+
{ watch: id }
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
// No request fires until id.value is set
|
|
284
|
+
id.value = 42 // โ triggers request to /users/42
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
> [!NOTE]
|
|
288
|
+
> When the URL getter returns `undefined`, the request throws internally with
|
|
289
|
+
> "Request URL is missing". This error is surfaced in `error.value` like any
|
|
290
|
+
> other request failure, so your error handling works as expected.
|
|
291
|
+
|
|
203
292
|
---
|
|
204
293
|
|
|
205
294
|
### POST/PUT/PATCH Requests
|
|
@@ -210,22 +299,31 @@ const { data } = useApi('/users', {
|
|
|
210
299
|
import { ref } from 'vue'
|
|
211
300
|
import { useApi } from '@ametie/vue-muza-use'
|
|
212
301
|
|
|
302
|
+
interface LoginResponse {
|
|
303
|
+
accessToken: string
|
|
304
|
+
refreshToken: string
|
|
305
|
+
}
|
|
306
|
+
|
|
213
307
|
const form = ref({
|
|
214
308
|
email: '',
|
|
215
309
|
password: ''
|
|
216
310
|
})
|
|
217
311
|
|
|
218
|
-
const { execute, loading, error } = useApi(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
312
|
+
const { execute, loading, error } = useApi<LoginResponse>(
|
|
313
|
+
'/auth/login',
|
|
314
|
+
{
|
|
315
|
+
method: 'POST',
|
|
316
|
+
data: form,
|
|
317
|
+
authMode: 'public',
|
|
318
|
+
onSuccess: (response) => {
|
|
319
|
+
console.log('Logged in!', response.data.accessToken)
|
|
320
|
+
}
|
|
223
321
|
}
|
|
224
|
-
|
|
322
|
+
)
|
|
225
323
|
</script>
|
|
226
324
|
|
|
227
325
|
<template>
|
|
228
|
-
<form @submit.prevent="execute">
|
|
326
|
+
<form @submit.prevent="execute()">
|
|
229
327
|
<input v-model="form.email" type="email" />
|
|
230
328
|
<input v-model="form.password" type="password" />
|
|
231
329
|
<button :disabled="loading">
|
|
@@ -246,19 +344,29 @@ Watch refs and automatically refetch when they change. Perfect for filters, sear
|
|
|
246
344
|
|
|
247
345
|
#### Single Dependency
|
|
248
346
|
```typescript
|
|
347
|
+
import { ref } from 'vue'
|
|
348
|
+
import { useApi } from '@ametie/vue-muza-use'
|
|
349
|
+
|
|
350
|
+
interface User {
|
|
351
|
+
id: number
|
|
352
|
+
name: string
|
|
353
|
+
}
|
|
354
|
+
|
|
249
355
|
const userId = ref(1)
|
|
250
356
|
|
|
251
|
-
const { data } = useApi(
|
|
252
|
-
|
|
253
|
-
immediate: true
|
|
254
|
-
|
|
357
|
+
const { data } = useApi<User>(
|
|
358
|
+
() => `/users/${userId.value}`,
|
|
359
|
+
{ watch: userId, immediate: true }
|
|
360
|
+
)
|
|
255
361
|
|
|
256
|
-
|
|
257
|
-
userId.value = 2
|
|
362
|
+
userId.value = 2 // โ automatic refetch
|
|
258
363
|
```
|
|
259
364
|
|
|
260
365
|
#### Multiple Dependencies
|
|
261
366
|
```typescript
|
|
367
|
+
import { ref } from 'vue'
|
|
368
|
+
import { useApi } from '@ametie/vue-muza-use'
|
|
369
|
+
|
|
262
370
|
const searchQuery = ref('')
|
|
263
371
|
const category = ref('all')
|
|
264
372
|
|
|
@@ -273,6 +381,9 @@ const { data } = useApi(
|
|
|
273
381
|
|
|
274
382
|
#### Auto-Save Form
|
|
275
383
|
```typescript
|
|
384
|
+
import { ref } from 'vue'
|
|
385
|
+
import { useApi } from '@ametie/vue-muza-use'
|
|
386
|
+
|
|
276
387
|
const settings = ref({
|
|
277
388
|
theme: 'dark',
|
|
278
389
|
notifications: true
|
|
@@ -281,28 +392,132 @@ const settings = ref({
|
|
|
281
392
|
useApi('/user/settings', {
|
|
282
393
|
method: 'PUT',
|
|
283
394
|
data: settings,
|
|
284
|
-
watch: settings,
|
|
285
|
-
debounce: 1000,
|
|
286
|
-
onSuccess: () =>
|
|
395
|
+
watch: settings,
|
|
396
|
+
debounce: 1000,
|
|
397
|
+
onSuccess: () => console.log('Saved!')
|
|
398
|
+
})
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
### ignoreUpdates โ Atomic Updates Without Refetch
|
|
404
|
+
|
|
405
|
+
**TL;DR: Change multiple reactive values at once without triggering a request between each change.**
|
|
406
|
+
|
|
407
|
+
When `watch` is active, every change to a watched ref triggers a new request. If you need to update three filter fields at once, you'd get three requests instead of one. `ignoreUpdates` wraps your changes so the watcher stays silent while you update all fields, then you call `execute()` once.
|
|
408
|
+
|
|
409
|
+
#### โ Without ignoreUpdates โ 2 requests fire
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
import { ref } from 'vue'
|
|
413
|
+
import { useApi } from '@ametie/vue-muza-use'
|
|
414
|
+
|
|
415
|
+
const page = ref(1)
|
|
416
|
+
const search = ref('')
|
|
417
|
+
|
|
418
|
+
const { execute } = useApi('/users', {
|
|
419
|
+
params: { page, search },
|
|
420
|
+
watch: [page, search]
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
// BAD: each assignment triggers a separate request
|
|
424
|
+
page.value = 1 // โ request 1: page=1, search=''
|
|
425
|
+
search.value = 'john' // โ request 2: page=1, search=john
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
#### โ
With ignoreUpdates โ 1 request fires
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
import { ref } from 'vue'
|
|
432
|
+
import { useApi } from '@ametie/vue-muza-use'
|
|
433
|
+
|
|
434
|
+
const page = ref(1)
|
|
435
|
+
const search = ref('')
|
|
436
|
+
|
|
437
|
+
const { execute, ignoreUpdates } = useApi('/users', {
|
|
438
|
+
params: { page, search },
|
|
439
|
+
watch: [page, search]
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
// GOOD: all changes are batched, only one request fires
|
|
443
|
+
ignoreUpdates(() => {
|
|
444
|
+
page.value = 1
|
|
445
|
+
search.value = 'john'
|
|
446
|
+
})
|
|
447
|
+
await execute() // โ single request: page=1, search=john
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
#### Reset filters without auto-fetching
|
|
451
|
+
|
|
452
|
+
Use `ignoreUpdates` to reset all filters to their defaults, then manually trigger a single request.
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
import { ref } from 'vue'
|
|
456
|
+
import { useApi } from '@ametie/vue-muza-use'
|
|
457
|
+
|
|
458
|
+
const page = ref(1)
|
|
459
|
+
const search = ref('')
|
|
460
|
+
const status = ref('all')
|
|
461
|
+
|
|
462
|
+
const { execute, ignoreUpdates } = useApi('/users', {
|
|
463
|
+
params: { page, search, status },
|
|
464
|
+
watch: [page, search, status]
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
function resetFilters() {
|
|
468
|
+
ignoreUpdates(() => {
|
|
469
|
+
page.value = 1
|
|
470
|
+
search.value = ''
|
|
471
|
+
status.value = 'all'
|
|
472
|
+
})
|
|
473
|
+
execute() // single request with reset values
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
#### Safe to call without a watch option
|
|
478
|
+
|
|
479
|
+
If no `watch` option is configured, `ignoreUpdates` still runs the updater โ it just has nothing to suppress.
|
|
480
|
+
|
|
481
|
+
```typescript
|
|
482
|
+
import { ref } from 'vue'
|
|
483
|
+
import { useApi } from '@ametie/vue-muza-use'
|
|
484
|
+
|
|
485
|
+
const counter = ref(0)
|
|
486
|
+
const { ignoreUpdates } = useApi('/data')
|
|
487
|
+
|
|
488
|
+
// Safe โ no error thrown, updater still runs
|
|
489
|
+
ignoreUpdates(() => {
|
|
490
|
+
counter.value = 42
|
|
287
491
|
})
|
|
288
492
|
```
|
|
289
493
|
|
|
494
|
+
> [!NOTE]
|
|
495
|
+
> `ignoreUpdates` is synchronous only. Changes made after an `await` inside the
|
|
496
|
+
> updater function will NOT be suppressed โ the flag resets after the synchronous
|
|
497
|
+
> portion completes. If you need to update async values, update them outside
|
|
498
|
+
> `ignoreUpdates` and call `execute()` manually.
|
|
499
|
+
|
|
290
500
|
---
|
|
291
501
|
|
|
292
502
|
### Polling (Background Updates)
|
|
293
503
|
|
|
294
|
-
Keep data fresh with smart polling
|
|
504
|
+
**TL;DR: Keep data fresh with smart polling that automatically pauses when the browser tab is hidden.**
|
|
295
505
|
|
|
296
506
|
#### Simple Polling
|
|
297
507
|
```typescript
|
|
508
|
+
import { useApi } from '@ametie/vue-muza-use'
|
|
509
|
+
|
|
298
510
|
const { data } = useApi('/notifications', {
|
|
299
511
|
immediate: true,
|
|
300
|
-
poll: 5000
|
|
512
|
+
poll: 5000
|
|
301
513
|
})
|
|
302
514
|
```
|
|
303
515
|
|
|
304
516
|
#### Dynamic Polling Control
|
|
305
517
|
```typescript
|
|
518
|
+
import { ref } from 'vue'
|
|
519
|
+
import { useApi } from '@ametie/vue-muza-use'
|
|
520
|
+
|
|
306
521
|
const pollInterval = ref(3000)
|
|
307
522
|
|
|
308
523
|
const { data } = useApi('/live-feed', {
|
|
@@ -310,37 +525,153 @@ const { data } = useApi('/live-feed', {
|
|
|
310
525
|
immediate: true
|
|
311
526
|
})
|
|
312
527
|
|
|
313
|
-
// Stop polling
|
|
314
|
-
pollInterval.value =
|
|
528
|
+
pollInterval.value = 0 // Stop polling
|
|
529
|
+
pollInterval.value = 5000 // Resume with new interval
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
#### Polling Object Syntax with Reactive Fields
|
|
533
|
+
|
|
534
|
+
Both `interval` and `whenHidden` can be reactive refs โ change them at runtime without re-creating the composable.
|
|
535
|
+
|
|
536
|
+
```typescript
|
|
537
|
+
import { ref } from 'vue'
|
|
538
|
+
import { useApi } from '@ametie/vue-muza-use'
|
|
539
|
+
|
|
540
|
+
const interval = ref(5000)
|
|
541
|
+
const whenHidden = ref(false)
|
|
542
|
+
|
|
543
|
+
const { data } = useApi('/status', {
|
|
544
|
+
immediate: true,
|
|
545
|
+
poll: { interval, whenHidden }
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
// Slow down polling
|
|
549
|
+
interval.value = 30000
|
|
550
|
+
|
|
551
|
+
// Allow polling even when tab is not visible
|
|
552
|
+
whenHidden.value = true
|
|
315
553
|
|
|
316
|
-
//
|
|
317
|
-
|
|
554
|
+
// Stop polling completely
|
|
555
|
+
interval.value = 0
|
|
318
556
|
```
|
|
319
557
|
|
|
558
|
+
> [!NOTE]
|
|
559
|
+
> By default `whenHidden: false` โ polling pauses when the browser tab is hidden
|
|
560
|
+
> and resumes automatically when the user switches back. Set `whenHidden: true`
|
|
561
|
+
> for background jobs that must continue regardless of tab visibility.
|
|
562
|
+
|
|
320
563
|
---
|
|
321
564
|
|
|
322
565
|
### Error Handling
|
|
323
566
|
|
|
324
567
|
#### Per-Request Error Handling
|
|
325
568
|
```typescript
|
|
569
|
+
import { useApi } from '@ametie/vue-muza-use'
|
|
570
|
+
|
|
326
571
|
const { error, execute } = useApi('/users', {
|
|
327
572
|
onError: (error) => {
|
|
328
573
|
if (error.status === 404) {
|
|
329
|
-
|
|
574
|
+
console.error('User not found')
|
|
330
575
|
} else {
|
|
331
|
-
|
|
576
|
+
console.error('Something went wrong')
|
|
332
577
|
}
|
|
333
578
|
},
|
|
334
|
-
skipErrorNotification: true
|
|
579
|
+
skipErrorNotification: true
|
|
580
|
+
})
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
---
|
|
584
|
+
|
|
585
|
+
### retry โ Automatic Request Retry
|
|
586
|
+
|
|
587
|
+
**TL;DR: Automatically retry failed requests before showing an error.**
|
|
588
|
+
|
|
589
|
+
Retries fire only after the request fails. The `loading` state stays `true` during all attempts. `onError` is only called after the final failure.
|
|
590
|
+
|
|
591
|
+
#### retry option
|
|
592
|
+
|
|
593
|
+
| Value | Meaning |
|
|
594
|
+
|-------|---------|
|
|
595
|
+
| `false` | Never retry (default) |
|
|
596
|
+
| `true` | Retry up to 3 times |
|
|
597
|
+
| `3` | Retry exactly 3 times |
|
|
598
|
+
|
|
599
|
+
#### retryDelay
|
|
600
|
+
|
|
601
|
+
How many milliseconds to wait between retry attempts. Default: `1000` (1 second).
|
|
602
|
+
|
|
603
|
+
#### retryStatusCodes โ The Priority Chain
|
|
604
|
+
|
|
605
|
+
`retryStatusCodes` controls which HTTP status codes should trigger a retry. The library uses a three-level priority chain:
|
|
606
|
+
|
|
607
|
+
```
|
|
608
|
+
Per-request retryStatusCodes
|
|
609
|
+
โ (if not set)
|
|
610
|
+
globalOptions.retryStatusCodes
|
|
611
|
+
โ (if not set)
|
|
612
|
+
Library default: [408, 429, 500, 502, 503, 504]
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
- **Per-request**: `retryStatusCodes` in `useApi()` options โ highest priority, overrides everything
|
|
616
|
+
- **globalOptions**: `retryStatusCodes` in `createApi()` โ applies to all requests that don't set their own
|
|
617
|
+
- **Library default**: `[408, 429, 500, 502, 503, 504]` โ used when neither level is configured
|
|
618
|
+
|
|
619
|
+
> [!NOTE]
|
|
620
|
+
> `retryStatusCodes: []` means retry on ANY error โ network errors, timeouts,
|
|
621
|
+
> and any non-2xx response. This is an explicit opt-in, not the default.
|
|
622
|
+
|
|
623
|
+
> [!WARNING]
|
|
624
|
+
> Retry does NOT fire on `AbortError` (cancelled requests) or when the component
|
|
625
|
+
> unmounts during a retry delay โ the library cleans up safely in both cases.
|
|
626
|
+
|
|
627
|
+
#### Examples
|
|
628
|
+
|
|
629
|
+
Retry only on server errors (500, 503):
|
|
630
|
+
|
|
631
|
+
```typescript
|
|
632
|
+
import { useApi } from '@ametie/vue-muza-use'
|
|
633
|
+
|
|
634
|
+
const { data } = useApi('/reports', {
|
|
635
|
+
immediate: true,
|
|
636
|
+
retry: 3,
|
|
637
|
+
retryDelay: 2000,
|
|
638
|
+
retryStatusCodes: [500, 503]
|
|
639
|
+
})
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
Retry on any error including network failures:
|
|
643
|
+
|
|
644
|
+
```typescript
|
|
645
|
+
import { useApi } from '@ametie/vue-muza-use'
|
|
646
|
+
|
|
647
|
+
const { data } = useApi('/critical-data', {
|
|
648
|
+
immediate: true,
|
|
649
|
+
retry: 5,
|
|
650
|
+
retryStatusCodes: [] // retry on any error
|
|
335
651
|
})
|
|
336
652
|
```
|
|
337
653
|
|
|
338
|
-
|
|
654
|
+
Global default with per-request override:
|
|
655
|
+
|
|
339
656
|
```typescript
|
|
340
|
-
|
|
657
|
+
import { createApp } from 'vue'
|
|
658
|
+
import { createApi, createApiClient, useApi } from '@ametie/vue-muza-use'
|
|
659
|
+
|
|
660
|
+
// main.ts โ global: retry 2 times on server errors
|
|
661
|
+
const api = createApiClient({ baseURL: 'https://api.example.com' })
|
|
662
|
+
createApp(App).use(createApi({
|
|
663
|
+
axios: api,
|
|
664
|
+
globalOptions: {
|
|
665
|
+
retry: 2,
|
|
666
|
+
retryStatusCodes: [500, 502, 503, 504]
|
|
667
|
+
}
|
|
668
|
+
}))
|
|
669
|
+
|
|
670
|
+
// In a component โ override: retry only once for this request
|
|
671
|
+
const { data } = useApi('/payments', {
|
|
341
672
|
immediate: true,
|
|
342
|
-
retry:
|
|
343
|
-
|
|
673
|
+
retry: 1,
|
|
674
|
+
retryStatusCodes: [500]
|
|
344
675
|
})
|
|
345
676
|
```
|
|
346
677
|
|
|
@@ -350,17 +681,19 @@ useApi('/flaky-endpoint', {
|
|
|
350
681
|
|
|
351
682
|
#### Per-Request Loading
|
|
352
683
|
```typescript
|
|
684
|
+
import { useApi } from '@ametie/vue-muza-use'
|
|
685
|
+
|
|
353
686
|
const { data: user, loading: userLoading } = useApi('/user')
|
|
354
687
|
const { data: posts, loading: postsLoading } = useApi('/posts')
|
|
355
|
-
|
|
356
|
-
// Each request tracks its own loading state
|
|
357
688
|
```
|
|
358
689
|
|
|
359
690
|
#### Lifecycle Hooks
|
|
360
691
|
```typescript
|
|
692
|
+
import { useApi } from '@ametie/vue-muza-use'
|
|
693
|
+
|
|
361
694
|
const { execute } = useApi('/analytics', {
|
|
362
695
|
onBefore: () => {
|
|
363
|
-
|
|
696
|
+
console.log('Request starting...')
|
|
364
697
|
},
|
|
365
698
|
onSuccess: (response) => {
|
|
366
699
|
console.log('Success!', response.data)
|
|
@@ -369,113 +702,70 @@ const { execute } = useApi('/analytics', {
|
|
|
369
702
|
console.error('Failed:', error.message)
|
|
370
703
|
},
|
|
371
704
|
onFinish: () => {
|
|
372
|
-
|
|
705
|
+
console.log('Request finished (success or error)')
|
|
373
706
|
}
|
|
374
707
|
})
|
|
375
708
|
```
|
|
376
709
|
|
|
377
710
|
---
|
|
378
711
|
|
|
379
|
-
### Manual Data Updates
|
|
712
|
+
### Manual Data Updates (mutate)
|
|
380
713
|
|
|
381
|
-
|
|
714
|
+
**TL;DR: Update local data optimistically or post-process fetched data without re-fetching.**
|
|
382
715
|
|
|
383
|
-
|
|
384
|
-
> โ
Adding/removing/updating items in arrays
|
|
385
|
-
> โ
Local sorting/filtering (without refetching)
|
|
386
|
-
> โ
Transform data in `onSuccess` (adding computed fields)
|
|
387
|
-
>
|
|
388
|
-
> **When to use `computed` instead:**
|
|
389
|
-
> โ
Completely changing data structure (e.g., API format โ App format)
|
|
390
|
-
> โ
Extracting nested data that changes the return type
|
|
391
|
-
> โ
Complex transformations that depend on other refs
|
|
716
|
+
Use `mutate` to manually update the `data` ref. Supports direct values or updater functions (like React's `setState`). Calling `mutate` automatically clears any existing error.
|
|
392
717
|
|
|
393
718
|
#### Add/Remove/Update Items
|
|
394
719
|
```typescript
|
|
720
|
+
import { useApi } from '@ametie/vue-muza-use'
|
|
721
|
+
|
|
722
|
+
interface Todo {
|
|
723
|
+
id: number
|
|
724
|
+
title: string
|
|
725
|
+
done: boolean
|
|
726
|
+
}
|
|
727
|
+
|
|
395
728
|
const { data, mutate } = useApi<Todo[]>('/todos', { immediate: true })
|
|
396
729
|
|
|
397
|
-
// Add item
|
|
398
730
|
const addTodo = (newTodo: Todo) => {
|
|
399
731
|
mutate(prev => prev ? [...prev, newTodo] : [newTodo])
|
|
400
732
|
}
|
|
401
733
|
|
|
402
|
-
// Remove item
|
|
403
734
|
const removeTodo = (id: number) => {
|
|
404
735
|
mutate(prev => prev?.filter(t => t.id !== id) ?? null)
|
|
405
736
|
}
|
|
406
737
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
prev?.map(t => t.id === id ? { ...t, ...updates } : t) ?? null
|
|
738
|
+
const toggleTodo = (id: number) => {
|
|
739
|
+
mutate(prev =>
|
|
740
|
+
prev?.map(t => t.id === id ? { ...t, done: !t.done } : t) ?? null
|
|
411
741
|
)
|
|
412
742
|
}
|
|
413
743
|
```
|
|
414
744
|
|
|
415
|
-
#### Sort/Filter Locally
|
|
416
|
-
```typescript
|
|
417
|
-
const { data, mutate } = useApi<Product[]>('/products', { immediate: true })
|
|
418
|
-
|
|
419
|
-
const sortByPrice = () => {
|
|
420
|
-
mutate(prev => prev ? [...prev].sort((a, b) => a.price - b.price) : null)
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
const filterActive = () => {
|
|
424
|
-
mutate(prev => prev?.filter(p => p.active) ?? null)
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// Reset to original
|
|
428
|
-
const resetFilters = () => execute()
|
|
429
|
-
```
|
|
430
|
-
|
|
431
745
|
#### Transform in `onSuccess`
|
|
432
|
-
|
|
433
|
-
Use `mutate` in `onSuccess` to transform data right after fetching. Two approaches:
|
|
434
|
-
|
|
435
|
-
**Approach 1: Same type (recommended)**
|
|
436
746
|
```typescript
|
|
747
|
+
import { useApi } from '@ametie/vue-muza-use'
|
|
748
|
+
|
|
437
749
|
interface User {
|
|
438
750
|
id: number
|
|
439
751
|
firstName: string
|
|
440
752
|
lastName: string
|
|
441
|
-
fullName?: string
|
|
753
|
+
fullName?: string
|
|
442
754
|
}
|
|
443
755
|
|
|
444
|
-
const { data
|
|
756
|
+
const { data } = useApi<User[]>('/users', {
|
|
445
757
|
immediate: true,
|
|
446
758
|
onSuccess: ({ data: users }) => {
|
|
447
|
-
// Add computed field - still User[] type
|
|
448
759
|
mutate(users.map(u => ({
|
|
449
760
|
...u,
|
|
450
761
|
fullName: `${u.firstName} ${u.lastName}`
|
|
451
762
|
})))
|
|
452
763
|
}
|
|
453
764
|
})
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
**Approach 2: Different structure (use separate computed)**
|
|
457
|
-
```typescript
|
|
458
|
-
interface ApiUser {
|
|
459
|
-
first_name: string
|
|
460
|
-
last_name: string
|
|
461
|
-
}
|
|
462
765
|
|
|
463
|
-
|
|
464
|
-
const { data: rawData } = useApi<ApiUser[]>('/users', { immediate: true })
|
|
465
|
-
|
|
466
|
-
const users = computed(() =>
|
|
467
|
-
rawData.value?.map(u => ({
|
|
468
|
-
firstName: u.first_name,
|
|
469
|
-
lastName: u.last_name,
|
|
470
|
-
fullName: `${u.first_name} ${u.last_name}`
|
|
471
|
-
})) ?? []
|
|
472
|
-
)
|
|
766
|
+
const { mutate } = useApi<User[]>('/users', { immediate: true })
|
|
473
767
|
```
|
|
474
768
|
|
|
475
|
-
> ๐ก **Rule of thumb:**
|
|
476
|
-
> - โ
**Use `mutate` in `onSuccess`** if you're adding/modifying fields but keeping the same base type
|
|
477
|
-
> - โ
**Use `computed`** if you're completely changing the data structure (e.g., snake_case โ camelCase)
|
|
478
|
-
|
|
479
769
|
---
|
|
480
770
|
|
|
481
771
|
## ๐ Real-World Examples
|
|
@@ -483,6 +773,20 @@ const users = computed(() =>
|
|
|
483
773
|
### Data Table with Pagination & Sorting
|
|
484
774
|
```vue
|
|
485
775
|
<script setup lang="ts">
|
|
776
|
+
import { ref, computed } from 'vue'
|
|
777
|
+
import { useApi } from '@ametie/vue-muza-use'
|
|
778
|
+
|
|
779
|
+
interface Order {
|
|
780
|
+
id: number
|
|
781
|
+
created_at: string
|
|
782
|
+
total: number
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
interface OrdersResponse {
|
|
786
|
+
data: Order[]
|
|
787
|
+
total: number
|
|
788
|
+
}
|
|
789
|
+
|
|
486
790
|
const page = ref(1)
|
|
487
791
|
const sortBy = ref('created_at')
|
|
488
792
|
const sortOrder = ref<'asc' | 'desc'>('desc')
|
|
@@ -494,7 +798,7 @@ const params = computed(() => ({
|
|
|
494
798
|
per_page: 20
|
|
495
799
|
}))
|
|
496
800
|
|
|
497
|
-
const { data, loading } = useApi('/orders', {
|
|
801
|
+
const { data, loading } = useApi<OrdersResponse>('/orders', {
|
|
498
802
|
params,
|
|
499
803
|
watch: params,
|
|
500
804
|
immediate: true
|
|
@@ -514,30 +818,26 @@ const { data, loading } = useApi('/orders', {
|
|
|
514
818
|
<tr v-for="order in data?.data" :key="order.id">
|
|
515
819
|
<td>{{ order.id }}</td>
|
|
516
820
|
<td>{{ order.created_at }}</td>
|
|
517
|
-
<td
|
|
821
|
+
<td>\${{ order.total }}</td>
|
|
518
822
|
</tr>
|
|
519
823
|
</tbody>
|
|
520
824
|
</table>
|
|
521
|
-
|
|
522
|
-
<Pagination v-model="page" :total="data?.total" />
|
|
523
825
|
</template>
|
|
524
826
|
```
|
|
525
827
|
|
|
828
|
+
---
|
|
526
829
|
|
|
527
830
|
### Request Cancellation
|
|
528
831
|
```typescript
|
|
529
|
-
import { useAbortController } from '@ametie/vue-muza-use'
|
|
832
|
+
import { useAbortController, useApi } from '@ametie/vue-muza-use'
|
|
530
833
|
|
|
531
834
|
const { abortAll } = useAbortController()
|
|
532
835
|
|
|
533
|
-
|
|
534
|
-
const { data:
|
|
535
|
-
const { data: stats } = useApi('/stats', { params: filters })
|
|
836
|
+
const { data: products } = useApi('/products')
|
|
837
|
+
const { data: stats } = useApi('/stats')
|
|
536
838
|
|
|
537
|
-
// Cancel all when filters reset
|
|
538
839
|
const resetFilters = () => {
|
|
539
|
-
abortAll()
|
|
540
|
-
filters.value = { /* defaults */ }
|
|
840
|
+
abortAll()
|
|
541
841
|
}
|
|
542
842
|
```
|
|
543
843
|
|
|
@@ -545,7 +845,7 @@ const resetFilters = () => {
|
|
|
545
845
|
|
|
546
846
|
### Batch Requests
|
|
547
847
|
|
|
548
|
-
Execute multiple API requests in parallel with full reactive state, progress tracking, and error tolerance
|
|
848
|
+
**TL;DR: Execute multiple API requests in parallel with full reactive state, progress tracking, and error tolerance.**
|
|
549
849
|
|
|
550
850
|
#### Basic Usage
|
|
551
851
|
|
|
@@ -557,11 +857,11 @@ interface User {
|
|
|
557
857
|
name: string
|
|
558
858
|
}
|
|
559
859
|
|
|
560
|
-
const {
|
|
561
|
-
successfulData,
|
|
562
|
-
loading,
|
|
563
|
-
progress,
|
|
564
|
-
execute
|
|
860
|
+
const {
|
|
861
|
+
successfulData,
|
|
862
|
+
loading,
|
|
863
|
+
progress,
|
|
864
|
+
execute
|
|
565
865
|
} = useApiBatch<User>([
|
|
566
866
|
'/users/1',
|
|
567
867
|
'/users/2',
|
|
@@ -569,188 +869,145 @@ const {
|
|
|
569
869
|
])
|
|
570
870
|
|
|
571
871
|
await execute()
|
|
572
|
-
console.log(successfulData.value)
|
|
573
|
-
console.log(progress.value)
|
|
872
|
+
console.log(successfulData.value)
|
|
873
|
+
console.log(progress.value)
|
|
574
874
|
```
|
|
575
875
|
|
|
576
|
-
####
|
|
876
|
+
#### Per-Request Config (BatchRequestConfig)
|
|
877
|
+
|
|
878
|
+
**TL;DR: Each request in the batch can have its own method, body, and headers.**
|
|
577
879
|
|
|
578
|
-
|
|
880
|
+
Pass objects instead of strings to specify per-request configuration. You can mix strings (simple GET) and config objects in the same array.
|
|
579
881
|
|
|
580
882
|
```typescript
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
} = useApiBatch
|
|
883
|
+
import { useApiBatch } from '@ametie/vue-muza-use'
|
|
884
|
+
|
|
885
|
+
interface User { id: number; name: string }
|
|
886
|
+
interface Post { id: number; title: string }
|
|
887
|
+
|
|
888
|
+
const { data, execute } = useApiBatch([
|
|
587
889
|
'/users/1',
|
|
588
|
-
|
|
589
|
-
|
|
890
|
+
{
|
|
891
|
+
url: '/users',
|
|
892
|
+
method: 'POST',
|
|
893
|
+
data: { name: 'Alice', email: 'alice@example.com' }
|
|
894
|
+
},
|
|
895
|
+
{
|
|
896
|
+
url: '/posts',
|
|
897
|
+
method: 'GET',
|
|
898
|
+
params: { userId: 1 }
|
|
899
|
+
},
|
|
900
|
+
{
|
|
901
|
+
url: '/analytics/track',
|
|
902
|
+
method: 'POST',
|
|
903
|
+
headers: { 'X-Source': 'dashboard' },
|
|
904
|
+
data: { event: 'page_view' }
|
|
905
|
+
}
|
|
590
906
|
])
|
|
591
907
|
|
|
592
908
|
await execute()
|
|
593
|
-
|
|
594
|
-
console.log(successfulData.value.length) // 2 (successful)
|
|
595
|
-
console.log(errors.value.length) // 1 (failed)
|
|
596
|
-
console.log(progress.value) // { succeeded: 2, failed: 1, ... }
|
|
597
909
|
```
|
|
598
910
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
Fail immediately on first error:
|
|
911
|
+
`BatchRequestConfig` interface:
|
|
602
912
|
|
|
603
913
|
```typescript
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
} catch (error) {
|
|
611
|
-
console.log('Batch failed:', error.message)
|
|
914
|
+
interface BatchRequestConfig {
|
|
915
|
+
url: string
|
|
916
|
+
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' // default: 'GET'
|
|
917
|
+
data?: unknown
|
|
918
|
+
params?: unknown
|
|
919
|
+
headers?: Record<string, string>
|
|
612
920
|
}
|
|
613
921
|
```
|
|
614
922
|
|
|
615
|
-
####
|
|
616
|
-
|
|
617
|
-
Perfect for loading indicators and progress bars:
|
|
618
|
-
|
|
619
|
-
```vue
|
|
620
|
-
<script setup lang="ts">
|
|
621
|
-
const { loading, progress, execute } = useApiBatch<User>(urls, {
|
|
622
|
-
onProgress: (p) => {
|
|
623
|
-
console.log(`${p.percentage}% complete (${p.succeeded} ok, ${p.failed} failed)`)
|
|
624
|
-
}
|
|
625
|
-
})
|
|
626
|
-
</script>
|
|
923
|
+
#### BatchResultItem โ What Each Result Contains
|
|
627
924
|
|
|
628
|
-
|
|
629
|
-
<div v-if="loading">
|
|
630
|
-
<div class="progress-bar">
|
|
631
|
-
<div :style="{ width: progress.percentage + '%' }"></div>
|
|
632
|
-
</div>
|
|
633
|
-
<span>{{ progress.completed }} / {{ progress.total }}</span>
|
|
634
|
-
</div>
|
|
635
|
-
</template>
|
|
636
|
-
```
|
|
925
|
+
Every item returned in `data` has this shape:
|
|
637
926
|
|
|
638
|
-
|
|
927
|
+
| Field | Type | Description |
|
|
928
|
+
|-------|------|-------------|
|
|
929
|
+
| `url` | `string` | The URL that was requested |
|
|
930
|
+
| `index` | `number` | Position in the original array |
|
|
931
|
+
| `success` | `boolean` | `true` if the request succeeded |
|
|
932
|
+
| `data` | `T \| null` | Response data (`null` if failed) |
|
|
933
|
+
| `error` | `ApiError \| null` | Error details (`null` if succeeded) |
|
|
934
|
+
| `statusCode` | `number \| null` | HTTP status code |
|
|
935
|
+
| `response` | `AxiosResponse<T> \| null` | Full Axios response โ access headers here (`null` if failed) |
|
|
936
|
+
| `request` | `BatchRequestConfig` | The original normalized request config |
|
|
639
937
|
|
|
640
|
-
|
|
938
|
+
Accessing response headers from a batch result:
|
|
641
939
|
|
|
642
940
|
```typescript
|
|
643
|
-
|
|
644
|
-
const { execute } = useApiBatch<User>(hundredUrls, {
|
|
645
|
-
concurrency: 3
|
|
646
|
-
})
|
|
647
|
-
```
|
|
648
|
-
|
|
649
|
-
#### Reactive URLs
|
|
941
|
+
import { useApiBatch } from '@ametie/vue-muza-use'
|
|
650
942
|
|
|
651
|
-
|
|
943
|
+
const { data, execute } = useApiBatch(['/users/1', '/users/2'])
|
|
652
944
|
|
|
653
|
-
|
|
654
|
-
const userIds = ref([1, 2, 3])
|
|
655
|
-
const urls = computed(() => userIds.value.map(id => `/users/${id}`))
|
|
656
|
-
|
|
657
|
-
const { successfulData, execute } = useApiBatch<User>(urls, {
|
|
658
|
-
immediate: true // Execute on mount
|
|
659
|
-
})
|
|
945
|
+
await execute()
|
|
660
946
|
|
|
661
|
-
|
|
662
|
-
|
|
947
|
+
for (const item of data.value) {
|
|
948
|
+
if (item.response) {
|
|
949
|
+
const rateLimit = item.response.headers['x-ratelimit-remaining']
|
|
950
|
+
console.log(`${item.url} โ rate limit remaining: ${rateLimit}`)
|
|
951
|
+
}
|
|
952
|
+
if (item.error) {
|
|
953
|
+
console.error(`${item.url} โ failed: ${item.error.message}`)
|
|
954
|
+
}
|
|
955
|
+
}
|
|
663
956
|
```
|
|
664
957
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
958
|
+
> [!WARNING]
|
|
959
|
+
> `response` and `request` are new fields. If you were serializing
|
|
960
|
+
> `BatchResultItem` to JSON or storing it in a database, update your
|
|
961
|
+
> serialization logic to handle these new fields.
|
|
669
962
|
|
|
670
|
-
|
|
671
|
-
watch: filters, // Re-execute when filters change
|
|
672
|
-
immediate: true
|
|
673
|
-
})
|
|
674
|
-
```
|
|
963
|
+
#### Error Tolerance (Default)
|
|
675
964
|
|
|
676
|
-
|
|
965
|
+
By default, `useApiBatch` uses `settled: true` โ failed requests don't stop the batch.
|
|
677
966
|
|
|
678
967
|
```typescript
|
|
679
|
-
|
|
680
|
-
onItemSuccess: (item, index) => {
|
|
681
|
-
console.log(`โ
[${index}] Loaded: ${item.url}`)
|
|
682
|
-
},
|
|
683
|
-
onItemError: (item, index) => {
|
|
684
|
-
console.log(`โ [${index}] Failed: ${item.url}`, item.error?.message)
|
|
685
|
-
},
|
|
686
|
-
onFinish: (results) => {
|
|
687
|
-
console.log(`Batch complete: ${results.length} items processed`)
|
|
688
|
-
}
|
|
689
|
-
})
|
|
690
|
-
```
|
|
968
|
+
import { useApiBatch } from '@ametie/vue-muza-use'
|
|
691
969
|
|
|
692
|
-
|
|
970
|
+
interface User { id: number; name: string }
|
|
693
971
|
|
|
694
|
-
```typescript
|
|
695
972
|
const {
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
} = useApiBatch<User>(urls)
|
|
706
|
-
```
|
|
707
|
-
|
|
708
|
-
#### BatchResultItem Structure
|
|
973
|
+
successfulData,
|
|
974
|
+
errors,
|
|
975
|
+
progress,
|
|
976
|
+
execute
|
|
977
|
+
} = useApiBatch<User>([
|
|
978
|
+
'/users/1',
|
|
979
|
+
'/users/999',
|
|
980
|
+
'/users/3'
|
|
981
|
+
])
|
|
709
982
|
|
|
710
|
-
|
|
983
|
+
await execute()
|
|
711
984
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
url: string // The requested URL
|
|
715
|
-
index: number // Position in original array
|
|
716
|
-
success: boolean // Whether request succeeded
|
|
717
|
-
data: T | null // Response data (null if failed)
|
|
718
|
-
error: ApiError | null // Error details (null if succeeded)
|
|
719
|
-
statusCode: number | null
|
|
720
|
-
}
|
|
985
|
+
console.log(successfulData.value.length) // 2
|
|
986
|
+
console.log(errors.value.length) // 1
|
|
721
987
|
```
|
|
722
988
|
|
|
723
|
-
####
|
|
989
|
+
#### With Progress Tracking
|
|
724
990
|
|
|
725
991
|
```vue
|
|
726
992
|
<script setup lang="ts">
|
|
727
|
-
|
|
728
|
-
'/api/stats',
|
|
729
|
-
'/api/recent-orders',
|
|
730
|
-
'/api/notifications',
|
|
731
|
-
'/api/user-activity'
|
|
732
|
-
]
|
|
993
|
+
import { useApiBatch } from '@ametie/vue-muza-use'
|
|
733
994
|
|
|
734
|
-
const
|
|
735
|
-
data: results,
|
|
736
|
-
loading,
|
|
737
|
-
progress,
|
|
738
|
-
execute
|
|
739
|
-
} = useApiBatch(dashboardUrls, {
|
|
740
|
-
immediate: true,
|
|
741
|
-
onProgress: (p) => console.log(`Dashboard loading: ${p.percentage}%`)
|
|
742
|
-
})
|
|
995
|
+
const urls = ['/users/1', '/users/2', '/users/3', '/users/4']
|
|
743
996
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
997
|
+
const { loading, progress, execute } = useApiBatch(urls, {
|
|
998
|
+
onProgress: (p) => {
|
|
999
|
+
console.log(`${p.percentage}% (${p.succeeded} ok, ${p.failed} failed)`)
|
|
1000
|
+
}
|
|
1001
|
+
})
|
|
747
1002
|
</script>
|
|
748
1003
|
|
|
749
1004
|
<template>
|
|
750
|
-
<div v-if="loading"
|
|
751
|
-
|
|
1005
|
+
<div v-if="loading">
|
|
1006
|
+
<div class="progress-bar">
|
|
1007
|
+
<div :style="{ width: progress.percentage + '%' }"></div>
|
|
1008
|
+
</div>
|
|
1009
|
+
<span>{{ progress.completed }} / {{ progress.total }}</span>
|
|
752
1010
|
</div>
|
|
753
|
-
<Dashboard v-else :stats="stats" :orders="orders" />
|
|
754
1011
|
</template>
|
|
755
1012
|
```
|
|
756
1013
|
|
|
@@ -760,7 +1017,7 @@ const orders = computed(() => results.value.find(r => r.url.includes('orders'))?
|
|
|
760
1017
|
|
|
761
1018
|
### Custom Axios Instance
|
|
762
1019
|
|
|
763
|
-
|
|
1020
|
+
**TL;DR: Pass any pre-configured Axios instance โ interceptors, timeouts, headers all work.**
|
|
764
1021
|
|
|
765
1022
|
```typescript
|
|
766
1023
|
import axios from 'axios'
|
|
@@ -772,9 +1029,8 @@ const customAxios = axios.create({
|
|
|
772
1029
|
headers: { 'X-Custom-Header': 'value' }
|
|
773
1030
|
})
|
|
774
1031
|
|
|
775
|
-
// Add custom interceptors
|
|
776
1032
|
customAxios.interceptors.request.use((config) => {
|
|
777
|
-
|
|
1033
|
+
config.headers['X-Request-ID'] = crypto.randomUUID()
|
|
778
1034
|
return config
|
|
779
1035
|
})
|
|
780
1036
|
|
|
@@ -785,41 +1041,63 @@ app.use(createApi({ axios: customAxios }))
|
|
|
785
1041
|
|
|
786
1042
|
### Global Error Handler
|
|
787
1043
|
|
|
788
|
-
Normalize errors from different backend formats
|
|
1044
|
+
**TL;DR: Normalize errors from different backend formats in one place.**
|
|
789
1045
|
|
|
790
1046
|
```typescript
|
|
791
|
-
|
|
1047
|
+
import { createApp } from 'vue'
|
|
1048
|
+
import { createApi, createApiClient } from '@ametie/vue-muza-use'
|
|
1049
|
+
import App from './App.vue'
|
|
1050
|
+
|
|
1051
|
+
const api = createApiClient({ baseURL: 'https://api.example.com' })
|
|
1052
|
+
|
|
1053
|
+
createApp(App).use(createApi({
|
|
792
1054
|
axios: api,
|
|
793
|
-
|
|
794
|
-
// Global error handler
|
|
1055
|
+
|
|
795
1056
|
onError: (error) => {
|
|
796
|
-
|
|
1057
|
+
console.error(`[API Error] ${error.status}: ${error.message}`)
|
|
797
1058
|
},
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
1059
|
+
|
|
1060
|
+
errorParser: (error: unknown) => {
|
|
1061
|
+
const axiosError = error as {
|
|
1062
|
+
response?: { data?: { message?: string; errors?: Record<string, string[]> }; status?: number }
|
|
1063
|
+
message?: string
|
|
1064
|
+
}
|
|
1065
|
+
const response = axiosError.response?.data
|
|
1066
|
+
|
|
804
1067
|
if (response?.errors) {
|
|
805
1068
|
return {
|
|
806
1069
|
message: 'Validation Failed',
|
|
807
|
-
status:
|
|
1070
|
+
status: axiosError.response?.status ?? 422,
|
|
808
1071
|
code: 'VALIDATION_ERROR',
|
|
809
1072
|
errors: response.errors
|
|
810
1073
|
}
|
|
811
1074
|
}
|
|
812
|
-
|
|
813
|
-
// Default format
|
|
1075
|
+
|
|
814
1076
|
return {
|
|
815
|
-
message: response?.message
|
|
816
|
-
status:
|
|
1077
|
+
message: response?.message ?? axiosError.message ?? 'Unknown error',
|
|
1078
|
+
status: axiosError.response?.status ?? 500,
|
|
817
1079
|
details: error
|
|
818
1080
|
}
|
|
1081
|
+
},
|
|
1082
|
+
|
|
1083
|
+
globalOptions: {
|
|
1084
|
+
retry: 2,
|
|
1085
|
+
retryDelay: 1000,
|
|
1086
|
+
retryStatusCodes: [408, 429, 500, 502, 503, 504],
|
|
1087
|
+
useGlobalAbort: true
|
|
819
1088
|
}
|
|
820
1089
|
}))
|
|
821
1090
|
```
|
|
822
1091
|
|
|
1092
|
+
`globalOptions` reference:
|
|
1093
|
+
|
|
1094
|
+
| Option | Type | Default | Description |
|
|
1095
|
+
|--------|------|---------|-------------|
|
|
1096
|
+
| `retry` | `false \| boolean \| number` | `false` | Default retry setting applied to all requests that don't specify their own |
|
|
1097
|
+
| `retryDelay` | `number` | `1000` | How many milliseconds to wait between retry attempts for all requests |
|
|
1098
|
+
| `retryStatusCodes` | `number[]` | `[408,429,500,502,503,504]` | Default HTTP status codes that trigger a retry across all requests |
|
|
1099
|
+
| `useGlobalAbort` | `boolean` | `true` | When `true`, all requests subscribe to the global abort controller |
|
|
1100
|
+
|
|
823
1101
|
---
|
|
824
1102
|
|
|
825
1103
|
## ๐ Authentication & Token Management
|
|
@@ -828,16 +1106,17 @@ app.use(createApi({
|
|
|
828
1106
|
|
|
829
1107
|
### Basic Auth Setup
|
|
830
1108
|
|
|
831
|
-
Add
|
|
1109
|
+
**TL;DR: Add `withAuth: true` and a `refreshUrl` to get automatic token injection and refresh.**
|
|
832
1110
|
|
|
833
1111
|
```typescript
|
|
1112
|
+
import { createApiClient } from '@ametie/vue-muza-use'
|
|
1113
|
+
|
|
834
1114
|
const api = createApiClient({
|
|
835
1115
|
baseURL: 'https://api.example.com',
|
|
836
|
-
withAuth: true,
|
|
1116
|
+
withAuth: true,
|
|
837
1117
|
authOptions: {
|
|
838
1118
|
refreshUrl: '/auth/refresh',
|
|
839
1119
|
onTokenRefreshFailed: () => {
|
|
840
|
-
// Redirect to login when refresh fails
|
|
841
1120
|
window.location.href = '/login'
|
|
842
1121
|
}
|
|
843
1122
|
}
|
|
@@ -856,9 +1135,9 @@ The library automatically:
|
|
|
856
1135
|
|
|
857
1136
|
#### Mode 1: localStorage (Default)
|
|
858
1137
|
|
|
859
|
-
Simple setup for development or internal tools:
|
|
860
|
-
|
|
861
1138
|
```typescript
|
|
1139
|
+
import { createApiClient } from '@ametie/vue-muza-use'
|
|
1140
|
+
|
|
862
1141
|
const api = createApiClient({
|
|
863
1142
|
baseURL: 'https://api.example.com',
|
|
864
1143
|
authOptions: {
|
|
@@ -868,53 +1147,55 @@ const api = createApiClient({
|
|
|
868
1147
|
})
|
|
869
1148
|
```
|
|
870
1149
|
|
|
871
|
-
**Storage:** Both `accessToken` and `refreshToken` in localStorage
|
|
872
|
-
**Security:** โ ๏ธ Vulnerable to XSS attacks
|
|
1150
|
+
**Storage:** Both `accessToken` and `refreshToken` in localStorage
|
|
1151
|
+
**Security:** โ ๏ธ Vulnerable to XSS attacks
|
|
873
1152
|
**Use case:** Development, internal tools
|
|
874
1153
|
|
|
875
|
-
---
|
|
876
|
-
|
|
877
1154
|
#### Mode 2: httpOnly Cookies (Production)
|
|
878
1155
|
|
|
879
|
-
Recommended for production apps with sensitive data:
|
|
880
|
-
|
|
881
1156
|
```typescript
|
|
1157
|
+
import { createApiClient } from '@ametie/vue-muza-use'
|
|
1158
|
+
|
|
882
1159
|
const api = createApiClient({
|
|
883
1160
|
baseURL: 'https://api.example.com',
|
|
884
1161
|
authOptions: {
|
|
885
1162
|
refreshUrl: '/auth/refresh',
|
|
886
|
-
refreshWithCredentials: true,
|
|
1163
|
+
refreshWithCredentials: true,
|
|
887
1164
|
onTokenRefreshFailed: () => router.push('/login')
|
|
888
1165
|
}
|
|
889
1166
|
})
|
|
890
1167
|
```
|
|
891
1168
|
|
|
892
|
-
**Storage:** Only `accessToken` in localStorage, `refreshToken` in httpOnly cookie
|
|
893
|
-
**Security:** ๐ Protected from XSS attacks
|
|
1169
|
+
**Storage:** Only `accessToken` in localStorage, `refreshToken` in httpOnly cookie
|
|
1170
|
+
**Security:** ๐ Protected from XSS attacks
|
|
894
1171
|
**Backend requirement:** Must set `Set-Cookie` with `HttpOnly; Secure; SameSite`
|
|
895
1172
|
|
|
896
|
-
**Common Issues:**
|
|
897
|
-
- **Cookie not sent?** Check cookie domain and `SameSite` attribute
|
|
898
|
-
- **CORS error?** Backend must set `Access-Control-Allow-Credentials: true`
|
|
899
|
-
- **401 on refresh?** Verify cookie is included in request headers
|
|
900
|
-
|
|
901
1173
|
---
|
|
902
1174
|
|
|
903
1175
|
### Saving Tokens After Login
|
|
904
1176
|
|
|
905
1177
|
```typescript
|
|
906
|
-
import { tokenManager } from '@ametie/vue-muza-use'
|
|
1178
|
+
import { useApi, tokenManager } from '@ametie/vue-muza-use'
|
|
1179
|
+
import { useRouter } from 'vue-router'
|
|
1180
|
+
|
|
1181
|
+
interface LoginResponse {
|
|
1182
|
+
accessToken: string
|
|
1183
|
+
refreshToken: string
|
|
1184
|
+
expiresIn: number
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
const router = useRouter()
|
|
907
1188
|
|
|
908
|
-
const { execute } = useApi('/auth/login', {
|
|
1189
|
+
const { execute } = useApi<LoginResponse>('/auth/login', {
|
|
909
1190
|
method: 'POST',
|
|
910
|
-
authMode: 'public',
|
|
1191
|
+
authMode: 'public',
|
|
911
1192
|
onSuccess(response) {
|
|
912
1193
|
tokenManager.setTokens({
|
|
913
1194
|
accessToken: response.data.accessToken,
|
|
914
|
-
refreshToken: response.data.refreshToken,
|
|
1195
|
+
refreshToken: response.data.refreshToken,
|
|
915
1196
|
expiresIn: response.data.expiresIn
|
|
916
1197
|
})
|
|
917
|
-
|
|
1198
|
+
|
|
918
1199
|
router.push('/dashboard')
|
|
919
1200
|
}
|
|
920
1201
|
})
|
|
@@ -922,482 +1203,926 @@ const { execute } = useApi('/auth/login', {
|
|
|
922
1203
|
|
|
923
1204
|
---
|
|
924
1205
|
|
|
925
|
-
###
|
|
1206
|
+
### authMode โ Controlling Auth Per Request
|
|
1207
|
+
|
|
1208
|
+
**TL;DR: Control whether a request includes auth tokens and whether it retries on 401.**
|
|
926
1209
|
|
|
927
|
-
|
|
1210
|
+
| Value | Token sent? | Retries on 401? | Use case |
|
|
1211
|
+
|-------|-------------|-----------------|----------|
|
|
1212
|
+
| `'default'` | โ
Always | โ
Yes | Protected endpoints (most requests) |
|
|
1213
|
+
| `'public'` | โ Never | โ No | Login, registration, public content |
|
|
1214
|
+
| `'optional'` | โ
If available | โ No | Content that works for guests and logged-in users |
|
|
928
1215
|
|
|
929
1216
|
```typescript
|
|
930
|
-
|
|
931
|
-
useApi
|
|
1217
|
+
import { ref } from 'vue'
|
|
1218
|
+
import { useApi } from '@ametie/vue-muza-use'
|
|
1219
|
+
|
|
1220
|
+
interface Credentials { email: string; password: string }
|
|
1221
|
+
interface Post { id: number; title: string }
|
|
1222
|
+
|
|
1223
|
+
const credentials = ref<Credentials>({ email: '', password: '' })
|
|
1224
|
+
|
|
1225
|
+
// Login โ never send a token here
|
|
1226
|
+
const { execute: login } = useApi('/auth/login', {
|
|
932
1227
|
method: 'POST',
|
|
933
1228
|
authMode: 'public',
|
|
934
1229
|
data: credentials
|
|
935
1230
|
})
|
|
936
1231
|
|
|
937
|
-
// Public blog
|
|
938
|
-
useApi('/
|
|
939
|
-
authMode: '
|
|
1232
|
+
// Public blog that shows extra content when logged in
|
|
1233
|
+
const { data: posts } = useApi<Post[]>('/posts', {
|
|
1234
|
+
authMode: 'optional',
|
|
940
1235
|
immediate: true
|
|
941
1236
|
})
|
|
942
1237
|
```
|
|
943
1238
|
|
|
944
1239
|
---
|
|
945
1240
|
|
|
946
|
-
###
|
|
1241
|
+
### tokenManager โ Manual Token Control
|
|
947
1242
|
|
|
948
|
-
|
|
1243
|
+
**TL;DR: Use this when you need to read, set, or clear tokens from outside a request.**
|
|
1244
|
+
|
|
1245
|
+
The library manages tokens automatically. You only need `tokenManager` directly for:
|
|
1246
|
+
1. Saving tokens after login
|
|
1247
|
+
2. Clearing tokens on logout
|
|
1248
|
+
3. Checking if the user is currently logged in
|
|
1249
|
+
|
|
1250
|
+
Full API reference:
|
|
1251
|
+
|
|
1252
|
+
| Method | Returns | What it does |
|
|
1253
|
+
|--------|---------|--------------|
|
|
1254
|
+
| `getAccessToken()` | `string \| null` | The current access token, or `null` if not set |
|
|
1255
|
+
| `getRefreshToken()` | `string \| null` | The refresh token, or `null` if using httpOnly cookies |
|
|
1256
|
+
| `setTokens({ accessToken, refreshToken?, expiresIn? })` | `void` | Save new tokens after a successful login |
|
|
1257
|
+
| `clearTokens()` | `void` | Remove all tokens (call on logout) |
|
|
1258
|
+
| `hasTokens()` | `boolean` | `true` if an access token exists |
|
|
1259
|
+
| `isTokenExpired()` | `boolean` | `true` if the token has expired (5-second safety buffer applied) |
|
|
1260
|
+
| `getTokenExpiresAt()` | `number \| null` | Unix timestamp (ms) when the current token expires |
|
|
1261
|
+
| `getAuthHeader()` | `string \| null` | `"Bearer <token>"` string ready for use in headers, or `null` |
|
|
1262
|
+
|
|
1263
|
+
After login โ save tokens:
|
|
949
1264
|
|
|
950
1265
|
```typescript
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1266
|
+
import { tokenManager } from '@ametie/vue-muza-use'
|
|
1267
|
+
import { useRouter } from 'vue-router'
|
|
1268
|
+
|
|
1269
|
+
const router = useRouter()
|
|
1270
|
+
|
|
1271
|
+
function onLoginSuccess(response: {
|
|
1272
|
+
accessToken: string
|
|
1273
|
+
refreshToken: string
|
|
1274
|
+
expiresIn: number
|
|
1275
|
+
}) {
|
|
1276
|
+
tokenManager.setTokens({
|
|
1277
|
+
accessToken: response.accessToken,
|
|
1278
|
+
refreshToken: response.refreshToken,
|
|
1279
|
+
expiresIn: response.expiresIn
|
|
1280
|
+
})
|
|
1281
|
+
router.push('/dashboard')
|
|
1282
|
+
}
|
|
1283
|
+
```
|
|
1284
|
+
|
|
1285
|
+
On logout โ clear tokens:
|
|
1286
|
+
|
|
1287
|
+
```typescript
|
|
1288
|
+
import { tokenManager } from '@ametie/vue-muza-use'
|
|
1289
|
+
import { useRouter } from 'vue-router'
|
|
1290
|
+
|
|
1291
|
+
const router = useRouter()
|
|
1292
|
+
|
|
1293
|
+
function logout() {
|
|
1294
|
+
tokenManager.clearTokens()
|
|
1295
|
+
router.push('/login')
|
|
1296
|
+
}
|
|
1297
|
+
```
|
|
1298
|
+
|
|
1299
|
+
Router guard โ check before navigating:
|
|
1300
|
+
|
|
1301
|
+
```typescript
|
|
1302
|
+
import { tokenManager } from '@ametie/vue-muza-use'
|
|
1303
|
+
import { createRouter } from 'vue-router'
|
|
1304
|
+
|
|
1305
|
+
const router = createRouter({ /* routes */ } as never)
|
|
1306
|
+
|
|
1307
|
+
router.beforeEach((to) => {
|
|
1308
|
+
if (to.meta.requiresAuth && !tokenManager.hasTokens()) {
|
|
1309
|
+
return '/login'
|
|
962
1310
|
}
|
|
963
1311
|
})
|
|
964
1312
|
```
|
|
965
1313
|
|
|
966
1314
|
---
|
|
967
1315
|
|
|
968
|
-
### Advanced:
|
|
1316
|
+
### Advanced: extractTokens
|
|
1317
|
+
|
|
1318
|
+
**TL;DR: Use this when your API uses non-standard field names for tokens.**
|
|
969
1319
|
|
|
970
|
-
|
|
1320
|
+
By default the library looks for `accessToken`/`access_token` and `refreshToken`/`refresh_token` in the refresh response. If your API uses different names, provide this function.
|
|
971
1321
|
|
|
972
1322
|
```typescript
|
|
1323
|
+
import { createApiClient } from '@ametie/vue-muza-use'
|
|
1324
|
+
|
|
973
1325
|
const api = createApiClient({
|
|
974
1326
|
baseURL: 'https://api.example.com',
|
|
975
1327
|
authOptions: {
|
|
976
1328
|
refreshUrl: '/auth/refresh',
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
// Update app state
|
|
983
|
-
store.commit('SET_USER', user)
|
|
984
|
-
store.commit('SET_PERMISSIONS', permissions)
|
|
985
|
-
},
|
|
986
|
-
|
|
987
|
-
onTokenRefreshFailed: () => {
|
|
988
|
-
localStorage.clear()
|
|
989
|
-
window.location.href = '/login'
|
|
990
|
-
}
|
|
1329
|
+
extractTokens: (response) => ({
|
|
1330
|
+
accessToken: response.data.jwt,
|
|
1331
|
+
refreshToken: response.data.refresh_jwt
|
|
1332
|
+
}),
|
|
1333
|
+
onTokenRefreshFailed: () => router.push('/login')
|
|
991
1334
|
}
|
|
992
1335
|
})
|
|
993
1336
|
```
|
|
994
1337
|
|
|
995
1338
|
---
|
|
996
1339
|
|
|
997
|
-
|
|
1340
|
+
### Advanced: AuthMonitor โ Observing Token Lifecycle Events
|
|
998
1341
|
|
|
999
|
-
|
|
1342
|
+
**TL;DR: Hook into token refresh events for logging, analytics, or error tracking.**
|
|
1343
|
+
|
|
1344
|
+
Use `setAuthMonitor` to observe every stage of the token refresh lifecycle. This is useful for Sentry integration, analytics, or debugging auth issues in production.
|
|
1345
|
+
|
|
1346
|
+
```typescript
|
|
1347
|
+
import {
|
|
1348
|
+
setAuthMonitor,
|
|
1349
|
+
AuthEventType
|
|
1350
|
+
} from '@ametie/vue-muza-use'
|
|
1351
|
+
|
|
1352
|
+
setAuthMonitor((type, payload) => {
|
|
1353
|
+
switch (type) {
|
|
1354
|
+
case AuthEventType.REFRESH_START:
|
|
1355
|
+
console.log('Token refresh started')
|
|
1356
|
+
break
|
|
1357
|
+
case AuthEventType.REFRESH_SUCCESS:
|
|
1358
|
+
console.log('Token refreshed successfully')
|
|
1359
|
+
break
|
|
1360
|
+
case AuthEventType.REFRESH_ERROR:
|
|
1361
|
+
// payload.error contains the failure reason
|
|
1362
|
+
console.error('Token refresh failed', payload.error)
|
|
1363
|
+
break
|
|
1364
|
+
case AuthEventType.REQUEST_QUEUED:
|
|
1365
|
+
console.log(
|
|
1366
|
+
`${payload.queueSize} request(s) waiting for refresh`
|
|
1367
|
+
)
|
|
1368
|
+
break
|
|
1369
|
+
}
|
|
1370
|
+
})
|
|
1371
|
+
```
|
|
1372
|
+
|
|
1373
|
+
`AuthEventType` reference:
|
|
1374
|
+
|
|
1375
|
+
| Event | When it fires |
|
|
1376
|
+
|-------|---------------|
|
|
1377
|
+
| `REFRESH_START` | A token refresh request has been sent to the server |
|
|
1378
|
+
| `REQUEST_QUEUED` | An API request was queued because a refresh is already in progress |
|
|
1379
|
+
| `REFRESH_SUCCESS` | The token was refreshed successfully |
|
|
1380
|
+
| `REFRESH_ERROR` | The token refresh failed (triggers `onTokenRefreshFailed`) |
|
|
1381
|
+
|
|
1382
|
+
> [!TIP]
|
|
1383
|
+
> In development mode, the default monitor already logs all auth events to
|
|
1384
|
+
> the browser console via `console.debug`. You only need to call `setAuthMonitor`
|
|
1385
|
+
> if you want custom behavior (e.g., sending events to Sentry).
|
|
1386
|
+
|
|
1387
|
+
---
|
|
1388
|
+
|
|
1389
|
+
## ๐ Error Handling Reference
|
|
1390
|
+
|
|
1391
|
+
### ApiError Shape
|
|
1392
|
+
|
|
1393
|
+
Every error surfaces as an `ApiError` object โ in `error.value`, `onError`, and the global error handler.
|
|
1394
|
+
|
|
1395
|
+
| Field | Type | Always present? | What it contains |
|
|
1396
|
+
|-------|------|-----------------|------------------|
|
|
1397
|
+
| `message` | `string` | โ
Yes | Human-readable error description |
|
|
1398
|
+
| `status` | `number` | โ
Yes | HTTP status code (`0` for network errors) |
|
|
1399
|
+
| `code` | `string \| undefined` | When backend sends it | Machine-readable error code from the backend |
|
|
1400
|
+
| `errors` | `Record<string, string[]> \| undefined` | For validation errors | Field-level validation messages (Laravel, Rails, etc.) |
|
|
1401
|
+
| `details` | `unknown` | When available | Raw response data from the backend |
|
|
1402
|
+
|
|
1403
|
+
---
|
|
1404
|
+
|
|
1405
|
+
### DebounceCancelledError
|
|
1406
|
+
|
|
1407
|
+
**TL;DR: This error is thrown when a debounced call is cancelled โ catch it to avoid console noise.**
|
|
1408
|
+
|
|
1409
|
+
When `debounce` is active and a new call arrives before the delay expires, the previous call is cancelled. If you `await`ed that call, it will throw `DebounceCancelledError`. This is not a real error โ it just means the call was replaced by a newer one.
|
|
1410
|
+
|
|
1411
|
+
```typescript
|
|
1412
|
+
import { useApi, DebounceCancelledError } from '@ametie/vue-muza-use'
|
|
1413
|
+
|
|
1414
|
+
const { execute } = useApi('/search', { debounce: 300 })
|
|
1415
|
+
|
|
1416
|
+
async function search() {
|
|
1417
|
+
try {
|
|
1418
|
+
await execute()
|
|
1419
|
+
} catch (err) {
|
|
1420
|
+
if (err instanceof DebounceCancelledError) {
|
|
1421
|
+
return // Expected โ a newer call replaced this one
|
|
1422
|
+
}
|
|
1423
|
+
throw err // Re-throw unexpected errors
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
```
|
|
1427
|
+
|
|
1428
|
+
> [!TIP]
|
|
1429
|
+
> If you use `onError` instead of awaiting `execute()`, `DebounceCancelledError`
|
|
1430
|
+
> is NOT passed to `onError` โ it is filtered out automatically.
|
|
1431
|
+
> You only need to handle it if you `await execute()` directly.
|
|
1432
|
+
|
|
1433
|
+
---
|
|
1434
|
+
|
|
1435
|
+
## ๐ง Utilities & Standalone Composables
|
|
1436
|
+
|
|
1437
|
+
### useApiState โ Standalone Reactive State
|
|
1438
|
+
|
|
1439
|
+
**TL;DR: Use this to build custom composables with the same state shape as useApi.**
|
|
1440
|
+
|
|
1441
|
+
If you're writing your own composable that wraps `useApi` โ or something similar โ you can use `useApiState` to get the same `data / loading / error / mutate` pattern without any HTTP logic attached.
|
|
1000
1442
|
|
|
1001
|
-
|
|
1443
|
+
```typescript
|
|
1444
|
+
import { useApiState } from '@ametie/vue-muza-use'
|
|
1445
|
+
import type { ApiError } from '@ametie/vue-muza-use'
|
|
1446
|
+
|
|
1447
|
+
function useMyCustomComposable<T>(fetchFn: () => Promise<T>) {
|
|
1448
|
+
const {
|
|
1449
|
+
data,
|
|
1450
|
+
loading,
|
|
1451
|
+
error,
|
|
1452
|
+
mutate,
|
|
1453
|
+
setLoading,
|
|
1454
|
+
setError,
|
|
1455
|
+
reset
|
|
1456
|
+
} = useApiState<T>()
|
|
1457
|
+
|
|
1458
|
+
async function load() {
|
|
1459
|
+
setLoading(true)
|
|
1460
|
+
setError(null)
|
|
1461
|
+
try {
|
|
1462
|
+
const result = await fetchFn()
|
|
1463
|
+
mutate(result)
|
|
1464
|
+
} catch (err) {
|
|
1465
|
+
const apiError: ApiError = {
|
|
1466
|
+
message: String(err),
|
|
1467
|
+
status: 0
|
|
1468
|
+
}
|
|
1469
|
+
setError(apiError)
|
|
1470
|
+
} finally {
|
|
1471
|
+
setLoading(false)
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
return { data, loading, error, load, reset }
|
|
1476
|
+
}
|
|
1477
|
+
```
|
|
1002
1478
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1479
|
+
---
|
|
1480
|
+
|
|
1481
|
+
## ๐ API Reference
|
|
1482
|
+
|
|
1483
|
+
### `useApi<T, D>(url, options)`
|
|
1006
1484
|
|
|
1007
1485
|
**Arguments:**
|
|
1008
1486
|
|
|
1009
1487
|
| Argument | Type | Description |
|
|
1010
1488
|
|----------|------|-------------|
|
|
1011
|
-
| `url` | `MaybeRefOrGetter<string>` | API endpoint.
|
|
1489
|
+
| `url` | `MaybeRefOrGetter<string \| undefined>` | API endpoint. String, ref, or getter function. Returning `undefined` prevents the request. |
|
|
1012
1490
|
| `options` | `UseApiOptions<T, D>` | Configuration object (see below). |
|
|
1013
1491
|
|
|
1014
1492
|
---
|
|
1015
1493
|
|
|
1016
|
-
|
|
1494
|
+
#### UseApiOptions โ Complete Reference
|
|
1017
1495
|
|
|
1018
|
-
|
|
1496
|
+
**Request Configuration:**
|
|
1019
1497
|
|
|
1020
1498
|
| Option | Type | Default | Description |
|
|
1021
1499
|
|--------|------|---------|-------------|
|
|
1022
|
-
| `method` | `'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'` | `'GET'` | HTTP method
|
|
1023
|
-
| `data` | `MaybeRefOrGetter<D>` | `undefined` | Request body
|
|
1024
|
-
| `params` | `MaybeRefOrGetter<any>` | `undefined` | URL query parameters
|
|
1025
|
-
| `headers` | `Record<string, string>` | `undefined` | Custom headers
|
|
1026
|
-
| `authMode` | `'default' \| 'public'
|
|
1500
|
+
| `method` | `'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'` | `'GET'` | HTTP method to use for the request |
|
|
1501
|
+
| `data` | `MaybeRefOrGetter<D>` | `undefined` | Request body โ automatically unwrapped if a ref |
|
|
1502
|
+
| `params` | `MaybeRefOrGetter<any>` | `undefined` | URL query parameters โ automatically unwrapped if a ref |
|
|
1503
|
+
| `headers` | `Record<string, string>` | `undefined` | Custom request headers added on top of defaults |
|
|
1504
|
+
| `authMode` | `'default' \| 'public' \| 'optional'` | `'default'` | Controls token injection and 401 retry behaviour |
|
|
1027
1505
|
|
|
1028
|
-
|
|
1506
|
+
**Reactivity & Auto-Execution:**
|
|
1029
1507
|
|
|
1030
1508
|
| Option | Type | Default | Description |
|
|
1031
1509
|
|--------|------|---------|-------------|
|
|
1032
|
-
| `immediate` | `boolean` | `false` |
|
|
1033
|
-
| `watch` | `WatchSource \| WatchSource[]` | `undefined` |
|
|
1034
|
-
| `debounce` | `number` | `0` |
|
|
1510
|
+
| `immediate` | `boolean` | `false` | When `true`, executes the request automatically when the composable is created |
|
|
1511
|
+
| `watch` | `WatchSource \| WatchSource[]` | `undefined` | One or more refs to watch โ request re-fires when any of them change |
|
|
1512
|
+
| `debounce` | `number` | `0` | Milliseconds to wait after the last watch change before firing the request |
|
|
1035
1513
|
|
|
1036
|
-
|
|
1514
|
+
**Polling:**
|
|
1037
1515
|
|
|
1038
1516
|
| Option | Type | Default | Description |
|
|
1039
1517
|
|--------|------|---------|-------------|
|
|
1040
|
-
| `poll` | `number \| { interval: number
|
|
1041
|
-
|
|
1042
|
-
**Polling Behavior:**
|
|
1043
|
-
- **Number**: Simple interval (pauses when tab hidden)
|
|
1044
|
-
- **Object**: `{ interval, whenHidden }` โ control pause behavior
|
|
1045
|
-
- **Ref**: Dynamic control โ change ref to update interval
|
|
1518
|
+
| `poll` | `number \| { interval: MaybeRefOrGetter<number>, whenHidden?: MaybeRefOrGetter<boolean> } \| Ref<number>` | `0` | Polling interval in ms. `0` disables polling. Object form allows reactive fields. |
|
|
1046
1519
|
|
|
1047
|
-
|
|
1520
|
+
**Retry:**
|
|
1048
1521
|
|
|
1049
1522
|
| Option | Type | Default | Description |
|
|
1050
1523
|
|--------|------|---------|-------------|
|
|
1051
|
-
| `retry` | `boolean \| number` | `false` | Number of retry attempts on failure. |
|
|
1052
|
-
| `retryDelay` | `number` | `1000` |
|
|
1053
|
-
| `
|
|
1524
|
+
| `retry` | `false \| boolean \| number` | `false` | Number of retry attempts on failure. `true` = 3 retries |
|
|
1525
|
+
| `retryDelay` | `number` | `1000` | How many milliseconds to wait between retry attempts |
|
|
1526
|
+
| `retryStatusCodes` | `number[]` | `[408,429,500,502,503,504]` | HTTP status codes that trigger a retry. `[]` means retry on any error |
|
|
1054
1527
|
|
|
1055
|
-
|
|
1528
|
+
**State Initialization:**
|
|
1056
1529
|
|
|
1057
1530
|
| Option | Type | Default | Description |
|
|
1058
1531
|
|--------|------|---------|-------------|
|
|
1059
|
-
| `initialData` | `T` | `null` | Initial value for `data`
|
|
1060
|
-
| `initialLoading` | `boolean` | `false` | Initial value for `loading`
|
|
1532
|
+
| `initialData` | `T` | `null` | Initial value for `data` before the first request completes |
|
|
1533
|
+
| `initialLoading` | `boolean` | `false` | Initial value for `loading` โ set `true` to show a spinner before the first request fires |
|
|
1061
1534
|
|
|
1062
|
-
|
|
1535
|
+
**Lifecycle Hooks:**
|
|
1063
1536
|
|
|
1064
1537
|
| Option | Type | Description |
|
|
1065
1538
|
|--------|------|-------------|
|
|
1066
|
-
| `onBefore` | `() => void` | Called before request starts
|
|
1067
|
-
| `onSuccess` | `(response: AxiosResponse<T>) => void` | Called on 2xx response
|
|
1068
|
-
| `onError` | `(error: ApiError) => void` | Called
|
|
1069
|
-
| `onFinish` | `() => void` | Called after request completes
|
|
1539
|
+
| `onBefore` | `() => void` | Called immediately before the request starts |
|
|
1540
|
+
| `onSuccess` | `(response: AxiosResponse<T>) => void` | Called on a successful 2xx response |
|
|
1541
|
+
| `onError` | `(error: ApiError) => void` | Called after the final failure (after all retries are exhausted) |
|
|
1542
|
+
| `onFinish` | `() => void` | Called after the request completes, whether success or error |
|
|
1070
1543
|
|
|
1071
|
-
|
|
1544
|
+
**Error Control:**
|
|
1072
1545
|
|
|
1073
1546
|
| Option | Type | Default | Description |
|
|
1074
1547
|
|--------|------|---------|-------------|
|
|
1075
|
-
| `
|
|
1548
|
+
| `skipErrorNotification` | `boolean` | `false` | When `true`, the global `onError` handler is NOT called for this request |
|
|
1076
1549
|
|
|
1077
|
-
|
|
1550
|
+
**Advanced:**
|
|
1078
1551
|
|
|
1079
|
-
|
|
1552
|
+
| Option | Type | Default | Description |
|
|
1553
|
+
|--------|------|---------|-------------|
|
|
1554
|
+
| `useGlobalAbort` | `boolean` | `true` | When `true`, this request participates in the global abort controller |
|
|
1080
1555
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1556
|
+
---
|
|
1557
|
+
|
|
1558
|
+
#### UseApiReturn โ Complete Reference
|
|
1559
|
+
|
|
1560
|
+
| Name | Type | Description |
|
|
1561
|
+
|------|------|-------------|
|
|
1562
|
+
| `data` | `Ref<T \| null>` | Response data from the last successful request |
|
|
1563
|
+
| `loading` | `Ref<boolean>` | `true` while a request is in flight (including retry delays) |
|
|
1564
|
+
| `error` | `Ref<ApiError \| null>` | Error from the last failed request; `null` on success |
|
|
1565
|
+
| `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` |
|
|
1569
|
+
| `abort(msg?)` | `(message?: string) => void` | Cancel the current in-flight request |
|
|
1570
|
+
| `reset()` | `() => void` | Cancel the request and reset all state to initial values |
|
|
1571
|
+
| `ignoreUpdates(fn)` | `(updater: () => void) => void` | Run `updater` without triggering watch-based re-execution |
|
|
1097
1572
|
|
|
1098
1573
|
#### `execute(config?)`
|
|
1099
|
-
|
|
1574
|
+
|
|
1575
|
+
Manually trigger the request. Pass a config object to override options for this call only.
|
|
1100
1576
|
|
|
1101
1577
|
```typescript
|
|
1102
|
-
|
|
1578
|
+
import { useApi } from '@ametie/vue-muza-use'
|
|
1579
|
+
|
|
1580
|
+
const { execute } = useApi<{ id: number }>('/users')
|
|
1103
1581
|
|
|
1104
1582
|
// Default execution
|
|
1105
1583
|
await execute()
|
|
1106
1584
|
|
|
1107
|
-
// Override
|
|
1108
|
-
await execute({
|
|
1109
|
-
|
|
1585
|
+
// Override data and params for this call only
|
|
1586
|
+
await execute({
|
|
1587
|
+
data: { name: 'John' },
|
|
1588
|
+
params: { notify: true }
|
|
1589
|
+
})
|
|
1110
1590
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1591
|
+
// Override authMode for this call only
|
|
1592
|
+
await execute({ authMode: 'public' })
|
|
1593
|
+
```
|
|
1113
1594
|
|
|
1114
|
-
|
|
1115
|
-
const { data, mutate } = useApi<User[]>('/users')
|
|
1595
|
+
---
|
|
1116
1596
|
|
|
1117
|
-
|
|
1118
|
-
mutate([{ id: 1, name: 'John' }])
|
|
1597
|
+
### `createApi(options)`
|
|
1119
1598
|
|
|
1120
|
-
|
|
1121
|
-
mutate(prev => prev ? [...prev, newUser] : [newUser])
|
|
1599
|
+
Vue plugin factory. Call this once in `main.ts` to provide global configuration.
|
|
1122
1600
|
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1601
|
+
```typescript
|
|
1602
|
+
import { createApp } from 'vue'
|
|
1603
|
+
import { createApi, createApiClient } from '@ametie/vue-muza-use'
|
|
1604
|
+
import App from './App.vue'
|
|
1126
1605
|
|
|
1127
|
-
|
|
1606
|
+
const api = createApiClient({ baseURL: 'https://api.example.com' })
|
|
1128
1607
|
|
|
1129
|
-
|
|
1130
|
-
|
|
1608
|
+
createApp(App).use(createApi({
|
|
1609
|
+
axios: api,
|
|
1610
|
+
onError: (error) => {
|
|
1611
|
+
console.error(error.message)
|
|
1612
|
+
},
|
|
1613
|
+
globalOptions: {
|
|
1614
|
+
retry: 2,
|
|
1615
|
+
retryDelay: 1000,
|
|
1616
|
+
retryStatusCodes: [500, 502, 503, 504],
|
|
1617
|
+
useGlobalAbort: true
|
|
1618
|
+
}
|
|
1619
|
+
}))
|
|
1620
|
+
```
|
|
1131
1621
|
|
|
1132
|
-
|
|
1133
|
-
const { execute, abort } = useApi('/long-task')
|
|
1622
|
+
**Options:**
|
|
1134
1623
|
|
|
1135
|
-
|
|
1624
|
+
| Option | Type | Required | Description |
|
|
1625
|
+
|--------|------|----------|-------------|
|
|
1626
|
+
| `axios` | `AxiosInstance` | โ
Yes | The Axios instance to use for all requests |
|
|
1627
|
+
| `onError` | `(error: ApiError, original: unknown) => void` | No | Global error handler called for every failed request (unless `skipErrorNotification: true`) |
|
|
1628
|
+
| `errorParser` | `(error: unknown) => ApiError` | No | Custom function to convert raw Axios errors into `ApiError` format |
|
|
1629
|
+
| `globalOptions` | `object` | No | Default options applied to every `useApi()` call (see globalOptions table above) |
|
|
1136
1630
|
|
|
1137
|
-
|
|
1138
|
-
setTimeout(() => abort('Timeout'), 5000)
|
|
1139
|
-
```
|
|
1631
|
+
---
|
|
1140
1632
|
|
|
1141
|
-
|
|
1142
|
-
Reset all state to initial values:
|
|
1633
|
+
### `createApiClient(options)`
|
|
1143
1634
|
|
|
1144
|
-
|
|
1145
|
-
const { data, error, loading, reset } = useApi('/users')
|
|
1635
|
+
Factory function that creates a configured Axios instance with built-in JWT auth features.
|
|
1146
1636
|
|
|
1147
|
-
|
|
1148
|
-
reset()
|
|
1149
|
-
// data.value = null, error.value = null, loading.value = false
|
|
1637
|
+
**Options:**
|
|
1150
1638
|
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1639
|
+
| Option | Type | Default | Description |
|
|
1640
|
+
|--------|------|---------|-------------|
|
|
1641
|
+
| `baseURL` | `string` | `undefined` | Base URL prepended to all request paths |
|
|
1642
|
+
| `timeout` | `number` | `60000` | Request timeout in milliseconds |
|
|
1643
|
+
| `withCredentials` | `boolean` | `false` | When `true`, all requests include cookies (needed for CORS with cookies) |
|
|
1644
|
+
| `withAuth` | `boolean` | `true` | Enable automatic token injection and refresh |
|
|
1645
|
+
| `authOptions.refreshUrl` | `string` | `'/auth/refresh'` | Endpoint used to refresh an expired access token |
|
|
1646
|
+
| `authOptions.refreshWithCredentials` | `boolean` | `false` | Send cookies on the refresh request only (use with httpOnly refresh tokens) |
|
|
1647
|
+
| `authOptions.onTokenRefreshFailed` | `() => void` | `undefined` | Called when token refresh fails โ typically redirect to login |
|
|
1648
|
+
| `authOptions.onTokenRefreshed` | `(response: AxiosResponse) => void \| Promise<void>` | `undefined` | Called after a successful refresh โ use to sync user state |
|
|
1649
|
+
| `authOptions.extractTokens` | `(response: AxiosResponse) => { accessToken: string, refreshToken?: string }` | `undefined` | Override token field names from the refresh response |
|
|
1650
|
+
| `authOptions.refreshPayload` | `Record<string, unknown> \| (() => Record<string, unknown>)` | `undefined` | Extra data to send with the refresh request (device ID, etc.) |
|
|
1651
|
+
|
|
1652
|
+
> [!WARNING]
|
|
1653
|
+
> If you create two `createApiClient()` instances in the same app, they share the
|
|
1654
|
+
> module-level `isRefreshing` flag and `failedQueue`. This can cause unexpected
|
|
1655
|
+
> behaviour when both instances handle 401 refresh at the same time. Use a single
|
|
1656
|
+
> `createApiClient` per app, and route different API domains through it using
|
|
1657
|
+
> interceptors or `baseURL` overrides on individual requests.
|
|
1154
1658
|
|
|
1155
1659
|
---
|
|
1156
1660
|
|
|
1157
|
-
### `
|
|
1661
|
+
### `useApiBatch<T>(urls, options)`
|
|
1158
1662
|
|
|
1159
|
-
|
|
1663
|
+
Execute multiple API requests in parallel with full reactive state.
|
|
1160
1664
|
|
|
1161
|
-
**
|
|
1665
|
+
**Arguments:**
|
|
1162
1666
|
|
|
1667
|
+
| Argument | Type | Description |
|
|
1668
|
+
|----------|------|-------------|
|
|
1669
|
+
| `urls` | `MaybeRefOrGetter<BatchInput[]>` | Array of URLs (strings) or `BatchRequestConfig` objects, or a ref/getter of that array |
|
|
1670
|
+
| `options` | `UseApiBatchOptions<T>` | Configuration object |
|
|
1671
|
+
|
|
1672
|
+
`BatchInput` type:
|
|
1163
1673
|
```typescript
|
|
1164
|
-
|
|
1165
|
-
// Standard Axios config
|
|
1166
|
-
baseURL?: string
|
|
1167
|
-
timeout?: number
|
|
1168
|
-
headers?: Record<string, string>
|
|
1169
|
-
withCredentials?: boolean // Default: false
|
|
1170
|
-
|
|
1171
|
-
// Auth features
|
|
1172
|
-
withAuth?: boolean // Default: true
|
|
1173
|
-
authOptions?: {
|
|
1174
|
-
refreshUrl?: string // Default: '/auth/refresh'
|
|
1175
|
-
refreshWithCredentials?: boolean // Default: false (set true for httpOnly cookies)
|
|
1176
|
-
onTokenRefreshFailed?: () => void
|
|
1177
|
-
onTokenRefreshed?: (response: AxiosResponse) => void | Promise<void> // โจ NEW: Handle refresh response
|
|
1178
|
-
extractTokens?: (response: AxiosResponse) => { accessToken: string, refreshToken?: string }
|
|
1179
|
-
refreshPayload?: Record<string, unknown> | (() => Record<string, unknown> | Promise<Record<string, unknown>>)
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1674
|
+
type BatchInput = string | BatchRequestConfig
|
|
1182
1675
|
```
|
|
1183
1676
|
|
|
1184
|
-
**
|
|
1677
|
+
**UseApiBatchOptions:**
|
|
1185
1678
|
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1679
|
+
| Option | Type | Default | Description |
|
|
1680
|
+
|--------|------|---------|-------------|
|
|
1681
|
+
| `settled` | `boolean` | `true` | When `true`, all requests run even if some fail. When `false`, the first error stops the batch |
|
|
1682
|
+
| `concurrency` | `number` | unlimited | Maximum number of requests that run in parallel at once |
|
|
1683
|
+
| `immediate` | `boolean` | `false` | Execute the batch automatically when the composable is created |
|
|
1684
|
+
| `skipErrorNotification` | `boolean` | `true` | Suppress global error handler for individual item failures |
|
|
1685
|
+
| `watch` | `WatchSource \| WatchSource[]` | `undefined` | Re-execute the batch when these sources change |
|
|
1686
|
+
| `onItemSuccess` | `(item: BatchResultItem<T>, index: number) => void` | `undefined` | Called each time a single request in the batch succeeds |
|
|
1687
|
+
| `onItemError` | `(item: BatchResultItem<T>, index: number) => void` | `undefined` | Called each time a single request in the batch fails |
|
|
1688
|
+
| `onProgress` | `(progress: BatchProgress) => void` | `undefined` | Called after each request completes with updated progress |
|
|
1689
|
+
| `onFinish` | `(results: BatchResultItem<T>[]) => void` | `undefined` | Called once when all requests have completed |
|
|
1690
|
+
|
|
1691
|
+
**UseApiBatchReturn:**
|
|
1692
|
+
|
|
1693
|
+
| Name | Type | Description |
|
|
1694
|
+
|------|------|-------------|
|
|
1695
|
+
| `data` | `Ref<BatchResultItem<T>[]>` | All results with full metadata |
|
|
1696
|
+
| `successfulData` | `Ref<T[]>` | Only the data from successful requests |
|
|
1697
|
+
| `loading` | `Ref<boolean>` | `true` while any request is still in flight |
|
|
1698
|
+
| `error` | `Ref<ApiError \| null>` | Set only if ALL requests in the batch failed |
|
|
1699
|
+
| `errors` | `Ref<ApiError[]>` | All individual errors from failed requests |
|
|
1700
|
+
| `progress` | `Ref<BatchProgress>` | Current progress tracking object |
|
|
1701
|
+
| `execute` | `() => Promise<BatchResultItem<T>[]>` | Start the batch |
|
|
1702
|
+
| `abort` | `(message?: string) => void` | Cancel all pending requests |
|
|
1703
|
+
| `reset` | `() => void` | Reset all state to initial values |
|
|
1704
|
+
|
|
1705
|
+
**BatchResultItem<T>:**
|
|
1706
|
+
|
|
1707
|
+
| Field | Type | Description |
|
|
1708
|
+
|-------|------|-------------|
|
|
1709
|
+
| `url` | `string` | The URL that was requested |
|
|
1710
|
+
| `index` | `number` | Position in the original array |
|
|
1711
|
+
| `success` | `boolean` | `true` if the request succeeded |
|
|
1712
|
+
| `data` | `T \| null` | Response data (`null` if failed) |
|
|
1713
|
+
| `error` | `ApiError \| null` | Error details (`null` if succeeded) |
|
|
1714
|
+
| `statusCode` | `number \| null` | HTTP status code |
|
|
1715
|
+
| `response` | `AxiosResponse<T> \| null` | Full Axios response (`null` if failed) |
|
|
1716
|
+
| `request` | `BatchRequestConfig` | The original normalized request config |
|
|
1191
1717
|
|
|
1192
|
-
|
|
1718
|
+
---
|
|
1719
|
+
|
|
1720
|
+
### `useAbortController()`
|
|
1721
|
+
|
|
1722
|
+
**TL;DR: Manually cancel all active requests at once โ useful when navigating away or resetting filters.**
|
|
1193
1723
|
|
|
1194
1724
|
```typescript
|
|
1195
|
-
|
|
1196
|
-
const api = createApiClient({
|
|
1197
|
-
baseURL: 'https://api.example.com',
|
|
1198
|
-
timeout: 30000,
|
|
1199
|
-
withAuth: true,
|
|
1200
|
-
authOptions: {
|
|
1201
|
-
refreshUrl: '/auth/refresh',
|
|
1202
|
-
onTokenRefreshFailed: () => {
|
|
1203
|
-
router.push('/login')
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
})
|
|
1725
|
+
import { useAbortController } from '@ametie/vue-muza-use'
|
|
1207
1726
|
|
|
1208
|
-
|
|
1209
|
-
const apiWithCookies = createApiClient({
|
|
1210
|
-
baseURL: 'https://api.example.com',
|
|
1211
|
-
authOptions: {
|
|
1212
|
-
refreshUrl: '/auth/refresh',
|
|
1213
|
-
refreshWithCredentials: true, // ๐ช Send cookies only for refresh request
|
|
1214
|
-
onTokenRefreshFailed: () => router.push('/login')
|
|
1215
|
-
}
|
|
1216
|
-
})
|
|
1727
|
+
const { abortAll, getSignal, abortCount } = useAbortController()
|
|
1217
1728
|
|
|
1218
|
-
|
|
1219
|
-
const apiWithAllCookies = createApiClient({
|
|
1220
|
-
baseURL: 'https://api.example.com',
|
|
1221
|
-
withCredentials: true, // โ ๏ธ All requests will send cookies
|
|
1222
|
-
authOptions: {
|
|
1223
|
-
refreshUrl: '/auth/refresh',
|
|
1224
|
-
refreshWithCredentials: true
|
|
1225
|
-
}
|
|
1226
|
-
})
|
|
1729
|
+
abortAll('Filter reset')
|
|
1227
1730
|
```
|
|
1228
1731
|
|
|
1229
|
-
|
|
1230
|
-
```
|
|
1732
|
+
**Returns:**
|
|
1231
1733
|
|
|
1232
|
-
|
|
1734
|
+
| Name | Type | Description |
|
|
1735
|
+
|------|------|-------------|
|
|
1736
|
+
| `abortAll` | `(reason?: string) => void` | Cancel all requests currently subscribed to this controller |
|
|
1737
|
+
| `getSignal` | `() => AbortSignal` | Get the current AbortSignal to attach to manual fetch calls |
|
|
1738
|
+
| `abortCount` | `Ref<number>` | Increments each time `abortAll` is called |
|
|
1233
1739
|
|
|
1234
|
-
|
|
1740
|
+
---
|
|
1235
1741
|
|
|
1236
|
-
|
|
1742
|
+
### `tokenManager`
|
|
1237
1743
|
|
|
1238
|
-
|
|
1744
|
+
See the full [tokenManager section](#tokenmanager--manual-token-control) above.
|
|
1239
1745
|
|
|
1746
|
+
Quick import:
|
|
1240
1747
|
```typescript
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1748
|
+
import { tokenManager } from '@ametie/vue-muza-use'
|
|
1749
|
+
|
|
1750
|
+
tokenManager.setTokens({ accessToken: '...', expiresIn: 3600 })
|
|
1751
|
+
tokenManager.clearTokens()
|
|
1752
|
+
const isLoggedIn = tokenManager.hasTokens()
|
|
1246
1753
|
```
|
|
1247
1754
|
|
|
1248
|
-
|
|
1755
|
+
---
|
|
1756
|
+
|
|
1757
|
+
### `useApiState<T>()`
|
|
1249
1758
|
|
|
1759
|
+
See the full [useApiState section](#useapistate--standalone-reactive-state) above.
|
|
1760
|
+
|
|
1761
|
+
Quick import:
|
|
1250
1762
|
```typescript
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
},
|
|
1256
|
-
errorParser: (error) => {
|
|
1257
|
-
// Custom error transformation
|
|
1258
|
-
return {
|
|
1259
|
-
message: error.response?.data?.message || error.message,
|
|
1260
|
-
status: error.response?.status,
|
|
1261
|
-
code: error.response?.data?.code
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
}))
|
|
1763
|
+
import { useApiState } from '@ametie/vue-muza-use'
|
|
1764
|
+
|
|
1765
|
+
const { data, loading, error, mutate, setLoading, setError, reset } =
|
|
1766
|
+
useApiState<MyType>()
|
|
1265
1767
|
```
|
|
1266
1768
|
|
|
1267
1769
|
---
|
|
1268
1770
|
|
|
1269
|
-
|
|
1771
|
+
## ๐งฉ Common Patterns
|
|
1270
1772
|
|
|
1271
|
-
|
|
1773
|
+
### 1. Search with Debounce and Reset
|
|
1272
1774
|
|
|
1273
|
-
|
|
1274
|
-
- `T` โ Response data type for each request
|
|
1775
|
+
Full component: debounced search that resets cleanly without triggering an intermediate request.
|
|
1275
1776
|
|
|
1276
|
-
|
|
1777
|
+
```vue
|
|
1778
|
+
<script setup lang="ts">
|
|
1779
|
+
import { ref } from 'vue'
|
|
1780
|
+
import { useApi } from '@ametie/vue-muza-use'
|
|
1277
1781
|
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1782
|
+
interface User {
|
|
1783
|
+
id: number
|
|
1784
|
+
name: string
|
|
1785
|
+
email: string
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
const search = ref('')
|
|
1789
|
+
const page = ref(1)
|
|
1790
|
+
|
|
1791
|
+
const { data, loading, execute, ignoreUpdates } = useApi<User[]>(
|
|
1792
|
+
() => `/users?search=${search.value}&page=${page.value}`,
|
|
1793
|
+
{
|
|
1794
|
+
watch: [search, page],
|
|
1795
|
+
debounce: 400,
|
|
1796
|
+
immediate: true
|
|
1797
|
+
}
|
|
1798
|
+
)
|
|
1799
|
+
|
|
1800
|
+
function resetSearch() {
|
|
1801
|
+
ignoreUpdates(() => {
|
|
1802
|
+
search.value = ''
|
|
1803
|
+
page.value = 1
|
|
1804
|
+
})
|
|
1805
|
+
execute() // single request with reset values
|
|
1806
|
+
}
|
|
1807
|
+
</script>
|
|
1808
|
+
|
|
1809
|
+
<template>
|
|
1810
|
+
<div>
|
|
1811
|
+
<input v-model="search" placeholder="Search users..." />
|
|
1812
|
+
<button @click="resetSearch">Clear</button>
|
|
1813
|
+
|
|
1814
|
+
<div v-if="loading">Searching...</div>
|
|
1815
|
+
<ul v-else>
|
|
1816
|
+
<li v-for="user in data" :key="user.id">
|
|
1817
|
+
{{ user.name }} โ {{ user.email }}
|
|
1818
|
+
</li>
|
|
1819
|
+
</ul>
|
|
1820
|
+
</div>
|
|
1821
|
+
</template>
|
|
1822
|
+
```
|
|
1282
1823
|
|
|
1283
1824
|
---
|
|
1284
1825
|
|
|
1285
|
-
|
|
1826
|
+
### 2. Paginated List with Filter Reset
|
|
1286
1827
|
|
|
1287
|
-
|
|
1288
|
-
|--------|------|---------|-------------|
|
|
1289
|
-
| `settled` | `boolean` | `true` | If `true`, failed requests don't stop the batch. If `false`, first error rejects entire batch. |
|
|
1290
|
-
| `concurrency` | `number` | `undefined` | Max parallel requests. Default: unlimited. |
|
|
1291
|
-
| `immediate` | `boolean` | `false` | Auto-execute on mount. |
|
|
1292
|
-
| `skipErrorNotification` | `boolean` | `true` | Skip global error handler for individual failures. |
|
|
1293
|
-
| `watch` | `WatchSource \| WatchSource[]` | `undefined` | Re-execute when sources change. |
|
|
1828
|
+
When the user changes a filter, reset the page to 1 using `ignoreUpdates` so only one request fires.
|
|
1294
1829
|
|
|
1295
|
-
|
|
1830
|
+
```vue
|
|
1831
|
+
<script setup lang="ts">
|
|
1832
|
+
import { ref } from 'vue'
|
|
1833
|
+
import { useApi } from '@ametie/vue-muza-use'
|
|
1296
1834
|
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
| `onFinish` | `(results: BatchResultItem<T>[]) => void` | Called when all requests complete. |
|
|
1835
|
+
interface Post {
|
|
1836
|
+
id: number
|
|
1837
|
+
title: string
|
|
1838
|
+
status: string
|
|
1839
|
+
}
|
|
1303
1840
|
|
|
1304
|
-
|
|
1841
|
+
const page = ref(1)
|
|
1842
|
+
const status = ref('all')
|
|
1305
1843
|
|
|
1306
|
-
|
|
1844
|
+
const { data, loading, execute, ignoreUpdates } = useApi<Post[]>(
|
|
1845
|
+
() => `/posts?status=${status.value}&page=${page.value}`,
|
|
1846
|
+
{ watch: [status, page], immediate: true }
|
|
1847
|
+
)
|
|
1307
1848
|
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
errors: Ref<ApiError[]> // All individual errors
|
|
1316
|
-
progress: Ref<BatchProgress> // Progress tracking
|
|
1317
|
-
|
|
1318
|
-
// Methods
|
|
1319
|
-
execute: () => Promise<BatchResultItem<T>[]>
|
|
1320
|
-
abort: (message?: string) => void
|
|
1321
|
-
reset: () => void
|
|
1849
|
+
function changeStatus(newStatus: string) {
|
|
1850
|
+
// Reset page to 1 when filter changes โ one request, not two
|
|
1851
|
+
ignoreUpdates(() => {
|
|
1852
|
+
status.value = newStatus
|
|
1853
|
+
page.value = 1
|
|
1854
|
+
})
|
|
1855
|
+
execute()
|
|
1322
1856
|
}
|
|
1857
|
+
</script>
|
|
1858
|
+
|
|
1859
|
+
<template>
|
|
1860
|
+
<div>
|
|
1861
|
+
<button @click="changeStatus('all')">All</button>
|
|
1862
|
+
<button @click="changeStatus('published')">Published</button>
|
|
1863
|
+
<button @click="changeStatus('draft')">Drafts</button>
|
|
1864
|
+
|
|
1865
|
+
<div v-if="loading">Loading...</div>
|
|
1866
|
+
<ul v-else>
|
|
1867
|
+
<li v-for="post in data" :key="post.id">
|
|
1868
|
+
[{{ post.status }}] {{ post.title }}
|
|
1869
|
+
</li>
|
|
1870
|
+
</ul>
|
|
1871
|
+
|
|
1872
|
+
<button :disabled="page <= 1" @click="page--">Prev</button>
|
|
1873
|
+
<span>Page {{ page }}</span>
|
|
1874
|
+
<button @click="page++">Next</button>
|
|
1875
|
+
</div>
|
|
1876
|
+
</template>
|
|
1323
1877
|
```
|
|
1324
1878
|
|
|
1325
|
-
|
|
1879
|
+
---
|
|
1880
|
+
|
|
1881
|
+
### 3. Form Submit with Loading, Error Display, and Retry
|
|
1326
1882
|
|
|
1327
|
-
```
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1883
|
+
```vue
|
|
1884
|
+
<script setup lang="ts">
|
|
1885
|
+
import { ref } from 'vue'
|
|
1886
|
+
import { useApi, DebounceCancelledError } from '@ametie/vue-muza-use'
|
|
1887
|
+
|
|
1888
|
+
interface CreatePostDto {
|
|
1889
|
+
title: string
|
|
1890
|
+
body: string
|
|
1334
1891
|
}
|
|
1335
|
-
```
|
|
1336
1892
|
|
|
1337
|
-
|
|
1893
|
+
interface Post {
|
|
1894
|
+
id: number
|
|
1895
|
+
title: string
|
|
1896
|
+
}
|
|
1338
1897
|
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1898
|
+
const form = ref<CreatePostDto>({ title: '', body: '' })
|
|
1899
|
+
|
|
1900
|
+
const { execute, loading, error } = useApi<Post, CreatePostDto>(
|
|
1901
|
+
'/posts',
|
|
1902
|
+
{
|
|
1903
|
+
method: 'POST',
|
|
1904
|
+
data: form,
|
|
1905
|
+
retry: 2,
|
|
1906
|
+
retryDelay: 1500,
|
|
1907
|
+
retryStatusCodes: [500, 502, 503]
|
|
1908
|
+
}
|
|
1909
|
+
)
|
|
1910
|
+
|
|
1911
|
+
async function submit() {
|
|
1912
|
+
try {
|
|
1913
|
+
const result = await execute()
|
|
1914
|
+
if (result) {
|
|
1915
|
+
console.log('Post created with id:', result.id)
|
|
1916
|
+
}
|
|
1917
|
+
} catch (err) {
|
|
1918
|
+
if (err instanceof DebounceCancelledError) return
|
|
1919
|
+
throw err
|
|
1920
|
+
}
|
|
1347
1921
|
}
|
|
1922
|
+
</script>
|
|
1923
|
+
|
|
1924
|
+
<template>
|
|
1925
|
+
<form @submit.prevent="submit">
|
|
1926
|
+
<input v-model="form.title" placeholder="Title" required />
|
|
1927
|
+
<textarea v-model="form.body" placeholder="Body" required />
|
|
1928
|
+
<p v-if="error" class="error">{{ error.message }}</p>
|
|
1929
|
+
<button type="submit" :disabled="loading">
|
|
1930
|
+
{{ loading ? 'Saving...' : 'Create Post' }}
|
|
1931
|
+
</button>
|
|
1932
|
+
</form>
|
|
1933
|
+
</template>
|
|
1348
1934
|
```
|
|
1349
1935
|
|
|
1350
1936
|
---
|
|
1351
1937
|
|
|
1352
|
-
###
|
|
1938
|
+
### 4. Dashboard with Parallel Requests (useApiBatch)
|
|
1939
|
+
|
|
1940
|
+
```vue
|
|
1941
|
+
<script setup lang="ts">
|
|
1942
|
+
import { computed } from 'vue'
|
|
1943
|
+
import { useApiBatch } from '@ametie/vue-muza-use'
|
|
1944
|
+
import type { BatchRequestConfig } from '@ametie/vue-muza-use'
|
|
1353
1945
|
|
|
1354
|
-
|
|
1946
|
+
interface Stats { totalUsers: number; revenue: number }
|
|
1947
|
+
interface Order { id: number; total: number }
|
|
1948
|
+
interface Notification { id: number; text: string }
|
|
1355
1949
|
|
|
1356
|
-
|
|
1950
|
+
const requests: BatchRequestConfig[] = [
|
|
1951
|
+
{ url: '/api/stats' },
|
|
1952
|
+
{ url: '/api/recent-orders', params: { limit: 5 } },
|
|
1953
|
+
{ url: '/api/notifications' }
|
|
1954
|
+
]
|
|
1357
1955
|
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1956
|
+
const {
|
|
1957
|
+
data: results,
|
|
1958
|
+
loading,
|
|
1959
|
+
progress,
|
|
1960
|
+
execute
|
|
1961
|
+
} = useApiBatch(requests, { immediate: true })
|
|
1962
|
+
|
|
1963
|
+
const stats = computed(
|
|
1964
|
+
() => results.value.find(r => r.url.includes('stats'))?.data as Stats | undefined
|
|
1965
|
+
)
|
|
1966
|
+
const orders = computed(
|
|
1967
|
+
() => results.value.find(r => r.url.includes('orders'))?.data as Order[] | undefined
|
|
1968
|
+
)
|
|
1969
|
+
const notifications = computed(
|
|
1970
|
+
() => results.value.find(r => r.url.includes('notifications'))?.data as Notification[] | undefined
|
|
1971
|
+
)
|
|
1972
|
+
</script>
|
|
1973
|
+
|
|
1974
|
+
<template>
|
|
1975
|
+
<div v-if="loading">
|
|
1976
|
+
Loading dashboard... {{ progress.percentage }}%
|
|
1977
|
+
</div>
|
|
1978
|
+
<div v-else>
|
|
1979
|
+
<div v-if="stats">
|
|
1980
|
+
<p>Total users: {{ stats.totalUsers }}</p>
|
|
1981
|
+
<p>Revenue: \${{ stats.revenue }}</p>
|
|
1982
|
+
</div>
|
|
1983
|
+
<ul v-if="orders">
|
|
1984
|
+
<li v-for="order in orders" :key="order.id">\${{ order.total }}</li>
|
|
1985
|
+
</ul>
|
|
1986
|
+
<ul v-if="notifications">
|
|
1987
|
+
<li v-for="n in notifications" :key="n.id">{{ n.text }}</li>
|
|
1988
|
+
</ul>
|
|
1989
|
+
</div>
|
|
1990
|
+
</template>
|
|
1363
1991
|
```
|
|
1364
1992
|
|
|
1365
|
-
|
|
1993
|
+
---
|
|
1366
1994
|
|
|
1367
|
-
|
|
1368
|
-
import { useAbortController } from '@ametie/vue-muza-use'
|
|
1995
|
+
### 5. Login + Token Save + Logout
|
|
1369
1996
|
|
|
1370
|
-
|
|
1997
|
+
```vue
|
|
1998
|
+
<script setup lang="ts">
|
|
1999
|
+
import { ref } from 'vue'
|
|
2000
|
+
import { useApi, tokenManager } from '@ametie/vue-muza-use'
|
|
2001
|
+
import { useRouter } from 'vue-router'
|
|
1371
2002
|
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
2003
|
+
interface LoginResponse {
|
|
2004
|
+
accessToken: string
|
|
2005
|
+
refreshToken: string
|
|
2006
|
+
expiresIn: number
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
const router = useRouter()
|
|
2010
|
+
const credentials = ref({ email: '', password: '' })
|
|
2011
|
+
|
|
2012
|
+
const { execute: login, loading, error } = useApi<LoginResponse>(
|
|
2013
|
+
'/auth/login',
|
|
2014
|
+
{
|
|
2015
|
+
method: 'POST',
|
|
2016
|
+
authMode: 'public',
|
|
2017
|
+
data: credentials,
|
|
2018
|
+
onSuccess(response) {
|
|
2019
|
+
tokenManager.setTokens({
|
|
2020
|
+
accessToken: response.data.accessToken,
|
|
2021
|
+
refreshToken: response.data.refreshToken,
|
|
2022
|
+
expiresIn: response.data.expiresIn
|
|
2023
|
+
})
|
|
2024
|
+
router.push('/dashboard')
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
)
|
|
2028
|
+
|
|
2029
|
+
function logout() {
|
|
2030
|
+
tokenManager.clearTokens()
|
|
2031
|
+
router.push('/login')
|
|
1375
2032
|
}
|
|
2033
|
+
</script>
|
|
2034
|
+
|
|
2035
|
+
<template>
|
|
2036
|
+
<form @submit.prevent="login()">
|
|
2037
|
+
<input v-model="credentials.email" type="email" placeholder="Email" />
|
|
2038
|
+
<input
|
|
2039
|
+
v-model="credentials.password"
|
|
2040
|
+
type="password"
|
|
2041
|
+
placeholder="Password"
|
|
2042
|
+
/>
|
|
2043
|
+
<p v-if="error">{{ error.message }}</p>
|
|
2044
|
+
<button :disabled="loading">
|
|
2045
|
+
{{ loading ? 'Signing in...' : 'Login' }}
|
|
2046
|
+
</button>
|
|
2047
|
+
</form>
|
|
2048
|
+
</template>
|
|
1376
2049
|
```
|
|
1377
2050
|
|
|
1378
2051
|
---
|
|
1379
2052
|
|
|
1380
|
-
###
|
|
2053
|
+
### 6. Polling Status Until Done
|
|
1381
2054
|
|
|
1382
|
-
|
|
2055
|
+
Start polling every 2 seconds and stop automatically when the job reaches a terminal status.
|
|
1383
2056
|
|
|
1384
|
-
```
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
2057
|
+
```vue
|
|
2058
|
+
<script setup lang="ts">
|
|
2059
|
+
import { ref } from 'vue'
|
|
2060
|
+
import { useApi } from '@ametie/vue-muza-use'
|
|
2061
|
+
|
|
2062
|
+
interface JobStatus {
|
|
2063
|
+
id: string
|
|
2064
|
+
status: 'pending' | 'processing' | 'complete' | 'failed'
|
|
2065
|
+
progress: number
|
|
1391
2066
|
}
|
|
1392
|
-
```
|
|
1393
2067
|
|
|
1394
|
-
|
|
2068
|
+
const jobId = ref<string | null>(null)
|
|
2069
|
+
const pollInterval = ref(0)
|
|
1395
2070
|
|
|
1396
|
-
|
|
1397
|
-
|
|
2071
|
+
const { data: job, error } = useApi<JobStatus>(
|
|
2072
|
+
() => jobId.value ? `/jobs/${jobId.value}` : undefined,
|
|
2073
|
+
{
|
|
2074
|
+
watch: jobId,
|
|
2075
|
+
poll: { interval: pollInterval },
|
|
2076
|
+
onSuccess(response) {
|
|
2077
|
+
const { status } = response.data
|
|
2078
|
+
if (status === 'complete' || status === 'failed') {
|
|
2079
|
+
pollInterval.value = 0 // Stop polling
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
)
|
|
2084
|
+
|
|
2085
|
+
function startJob(id: string) {
|
|
2086
|
+
jobId.value = id
|
|
2087
|
+
pollInterval.value = 2000 // Start polling every 2s
|
|
2088
|
+
}
|
|
2089
|
+
</script>
|
|
2090
|
+
|
|
2091
|
+
<template>
|
|
2092
|
+
<div>
|
|
2093
|
+
<button @click="startJob('job-123')">Start Job</button>
|
|
2094
|
+
<div v-if="job">
|
|
2095
|
+
Status: {{ job.status }} โ {{ job.progress }}%
|
|
2096
|
+
</div>
|
|
2097
|
+
<p v-if="error">{{ error.message }}</p>
|
|
2098
|
+
</div>
|
|
2099
|
+
</template>
|
|
1398
2100
|
```
|
|
1399
2101
|
|
|
1400
|
-
|
|
2102
|
+
---
|
|
2103
|
+
|
|
2104
|
+
## ๐ Troubleshooting
|
|
2105
|
+
|
|
2106
|
+
| Problem | Likely Cause | Fix |
|
|
2107
|
+
|---------|--------------|-----|
|
|
2108
|
+
| `"createApi config not found"` | `createApi()` not called | Call `app.use(createApi(...))` in `main.ts` before mounting |
|
|
2109
|
+
| Request fires twice on mount | `immediate: true` AND `watch` on a ref both trigger on setup | Use only `immediate` OR `watch` for the first load โ not both |
|
|
2110
|
+
| `retry` option does nothing | Default is `retry: false` | Set `retry: true` or `retry: 3` |
|
|
2111
|
+
| ALL errors trigger retry, not just some | `retryStatusCodes` not set โ uses library default | Specify exact codes or use `retryStatusCodes: []` to retry on any error |
|
|
2112
|
+
| `ignoreUpdates` still triggers a request | Updater function contains an `await` | `ignoreUpdates` is sync-only โ move async logic outside the updater |
|
|
2113
|
+
| `DebounceCancelledError` in console | Not handling cancelled debounce calls | Catch `DebounceCancelledError` when you `await execute()` directly |
|
|
2114
|
+
| 401 not refreshing token | `authMode: 'public'` or `'optional'` set on the request | Change to `authMode: 'default'` for endpoints that require auth |
|
|
2115
|
+
| Token not sent on requests | `withAuth: false` in `createApiClient` | Remove `withAuth: false` โ it defaults to `true` |
|
|
2116
|
+
| Cookie not sent on refresh request | `refreshWithCredentials` not set | Set `refreshWithCredentials: true` in `authOptions` |
|
|
2117
|
+
| Batch request not sending body | URL passed as a plain string | Use `BatchRequestConfig` object: `{ url, method: 'POST', data }` |
|
|
2118
|
+
| `useApi` outside a component throws | Missing Vue provide context | `createApi` uses global config โ should work anywhere after `app.use()` |
|
|
2119
|
+
| Two Axios instances break token refresh | `isRefreshing` is module-level state | Use one `createApiClient` per app; multiple instances share internal state |
|
|
2120
|
+
|
|
2121
|
+
---
|
|
2122
|
+
|
|
2123
|
+
## ๐ Changelog / Migration
|
|
2124
|
+
|
|
2125
|
+
See [GitHub Releases](https://github.com/ametie/vue-muza-use/releases) for version history.
|
|
1401
2126
|
|
|
1402
2127
|
---
|
|
1403
2128
|
|
|
@@ -1416,5 +2141,3 @@ MIT ยฉ [Ametie](https://github.com/ametie)
|
|
|
1416
2141
|
## ๐ Acknowledgments
|
|
1417
2142
|
|
|
1418
2143
|
Built with โค๏ธ for the Vue.js community. Inspired by real-world challenges in modern web applications.
|
|
1419
|
-
|
|
1420
|
-
|