@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 CHANGED
@@ -1,4 +1,4 @@
1
- # Vue Muza Use ๐ŸŽน
1
+ # @ametie/vue-muza-use ๐ŸŽน
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@ametie/vue-muza-use.svg?style=flat-square)](https://www.npmjs.com/package/@ametie/vue-muza-use)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](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 that respects component unmounting
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
- - [Polling](#polling-background-updates)
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
- - [Custom Axios Instance](#-advanced-configuration)
53
- - [Authentication & Tokens](#-authentication--token-management) *(Optional)*
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 // Auto-fetch on mount
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
- // ๐Ÿ’ก Use a getter function for dynamic URLs
140
- const { data, loading } = useApi(
152
+ const { data, loading } = useApi<Product[]>(
141
153
  () => `/products/search?q=${searchQuery.value}`,
142
154
  {
143
- watch: searchQuery, // Auto-refetch when query changes
144
- debounce: 500 // Wait 500ms after typing stops
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
- **๐ŸŽฏ What just happened?**
163
- - No race conditions โ€” previous searches auto-cancel
164
- - Debounce โ€” waits for user to stop typing
165
- - TypeScript โ€” full type safety
166
- - Clean code โ€” no manual cleanup needed
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 // Fetches automatically
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, // Automatically unwrapped
198
- watch: filters, // Re-fetch when filters change
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('/auth/login', {
219
- method: 'POST',
220
- data: form, // Ref is auto-unwrapped
221
- onSuccess: (response) => {
222
- console.log('Logged in!', response.data)
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(() => `/users/${userId.value}`, {
252
- watch: userId,
253
- immediate: true
254
- })
357
+ const { data } = useApi<User>(
358
+ () => `/users/${userId.value}`,
359
+ { watch: userId, immediate: true }
360
+ )
255
361
 
256
- // Change userId โ†’ automatic refetch
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, // Deep watch by default
285
- debounce: 1000, // Save 1s after changes stop
286
- onSuccess: () => toast.success('Saved!')
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. Automatically pauses when browser tab is hidden.
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 // Fetch every 5 seconds
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 = 0
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
- // Resume with different interval
317
- pollInterval.value = 5000
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
- toast.error('User not found')
574
+ console.error('User not found')
330
575
  } else {
331
- toast.error('Something went wrong')
576
+ console.error('Something went wrong')
332
577
  }
333
578
  },
334
- skipErrorNotification: true // Skip global handler
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
- #### Retry on Failure
654
+ Global default with per-request override:
655
+
339
656
  ```typescript
340
- useApi('/flaky-endpoint', {
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: 3, // Retry 3 times
343
- retryDelay: 1000 // Wait 1s between retries
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
- loadingBar.start()
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
- loadingBar.finish() // Always called
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
- Use `mutate` to manually update the data ref. Supports direct values or updater functions (like React's `setState`).
714
+ **TL;DR: Update local data optimistically or post-process fetched data without re-fetching.**
382
715
 
383
- > ๐ŸŽ“ **When to use `mutate`:**
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
- // Update item
408
- const updateTodo = (id: number, updates: Partial<Todo>) => {
409
- mutate(prev =>
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 // Optional field
753
+ fullName?: string
442
754
  }
443
755
 
444
- const { data, mutate } = useApi<User[]>('/users', {
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
- // If API returns different structure, use computed for transformation
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>${{ order.total }}</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
- // Multiple requests
534
- const { data: products } = useApi('/products', { params: filters })
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() // ๐Ÿ›‘ Cancel both requests
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, // Ref<User[]> - only successful results
562
- loading, // Ref<boolean>
563
- progress, // Ref<{ completed, total, percentage, succeeded, failed }>
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) // [User, User, User]
573
- console.log(progress.value) // { completed: 3, total: 3, percentage: 100, succeeded: 3, failed: 0 }
872
+ console.log(successfulData.value)
873
+ console.log(progress.value)
574
874
  ```
575
875
 
576
- #### Error Tolerance (Default)
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
- By default, `useApiBatch` uses `settled: true` โ€” failed requests don't stop the batch:
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
- const {
582
- successfulData,
583
- errors, // Ref<ApiError[]> - all errors
584
- progress,
585
- execute
586
- } = useApiBatch<User>([
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
- '/users/999', // Will fail (404)
589
- '/users/3'
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
- #### Strict Mode
600
-
601
- Fail immediately on first error:
911
+ `BatchRequestConfig` interface:
602
912
 
603
913
  ```typescript
604
- const { execute } = useApiBatch<User>(urls, {
605
- settled: false // First error will reject the entire batch
606
- })
607
-
608
- try {
609
- await execute()
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
- #### With Progress Tracking
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
- <template>
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
- #### Concurrency Limit
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
- Control how many requests run in parallel (useful for rate limiting):
938
+ Accessing response headers from a batch result:
641
939
 
642
940
  ```typescript
643
- // Only 3 requests at a time
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
- URLs can be reactive โ€” use refs or computed:
943
+ const { data, execute } = useApiBatch(['/users/1', '/users/2'])
652
944
 
653
- ```typescript
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
- // When userIds changes, call execute() to refetch
662
- watch(userIds, () => execute())
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
- #### Auto Re-Execute with Watch
666
-
667
- ```typescript
668
- const filters = ref({ status: 'active' })
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
- const { data } = useApiBatch<User>(urls, {
671
- watch: filters, // Re-execute when filters change
672
- immediate: true
673
- })
674
- ```
963
+ #### Error Tolerance (Default)
675
964
 
676
- #### Item-Level Callbacks
965
+ By default, `useApiBatch` uses `settled: true` โ€” failed requests don't stop the batch.
677
966
 
678
967
  ```typescript
679
- const { execute } = useApiBatch<User>(urls, {
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
- #### Full Return Type
970
+ interface User { id: number; name: string }
693
971
 
694
- ```typescript
695
972
  const {
696
- data, // Ref<BatchResultItem<T>[]> - all results with metadata
697
- successfulData, // Ref<T[]> - only successful data (computed)
698
- loading, // Ref<boolean>
699
- error, // Ref<ApiError | null> - set if ALL requests failed
700
- errors, // Ref<ApiError[]> - all individual errors
701
- progress, // Ref<BatchProgress>
702
- execute, // () => Promise<BatchResultItem<T>[]>
703
- abort, // (message?: string) => void
704
- reset // () => void
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
- Each item in `data` contains:
983
+ await execute()
711
984
 
712
- ```typescript
713
- interface BatchResultItem<T> {
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
- #### Real-World Example: Dashboard Loader
989
+ #### With Progress Tracking
724
990
 
725
991
  ```vue
726
992
  <script setup lang="ts">
727
- const dashboardUrls = [
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
- // Extract individual data
745
- const stats = computed(() => results.value.find(r => r.url.includes('stats'))?.data)
746
- const orders = computed(() => results.value.find(r => r.url.includes('orders'))?.data)
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" class="loading">
751
- Loading dashboard... {{ progress.percentage }}%
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
- Full control over Axios configuration:
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
- // Your logic
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
- app.use(createApi({
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
- toast.error(error.message)
1057
+ console.error(`[API Error] ${error.status}: ${error.message}`)
797
1058
  },
798
-
799
- // Error parser (normalize backend responses)
800
- errorParser: (error: any) => {
801
- const response = error.response?.data
802
-
803
- // Laravel validation errors
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: error.response.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 || error.message || 'Unknown error',
816
- status: error.response?.status || 500,
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 authentication to your API client:
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, // Enable automatic token injection
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, // ๐Ÿ”‘ Send cookies for refresh
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', // No auth for login endpoint
1191
+ authMode: 'public',
911
1192
  onSuccess(response) {
912
1193
  tokenManager.setTokens({
913
1194
  accessToken: response.data.accessToken,
914
- refreshToken: response.data.refreshToken, // Optional in cookie mode
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
- ### Public Endpoints
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
- Skip authentication for public endpoints:
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
- // Login (no auth needed)
931
- useApi('/auth/login', {
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 posts
938
- useApi('/blog/posts', {
939
- authMode: 'public',
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
- ### Advanced: Custom Refresh Payload
1241
+ ### tokenManager โ€” Manual Token Control
947
1242
 
948
- Send additional data with token refresh requests:
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
- const api = createApiClient({
952
- baseURL: 'https://api.example.com',
953
- authOptions: {
954
- refreshUrl: '/auth/refresh',
955
-
956
- // โš ๏ธ Use function for dynamic data
957
- refreshPayload: () => ({
958
- refreshToken: tokenManager.getRefreshToken(),
959
- deviceId: getDeviceId(),
960
- timestamp: Date.now()
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: Token Refresh Callback
1316
+ ### Advanced: extractTokens
1317
+
1318
+ **TL;DR: Use this when your API uses non-standard field names for tokens.**
969
1319
 
970
- Handle additional data from refresh response:
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
- // Called after successful token refresh
979
- onTokenRefreshed: (response) => {
980
- const { user, permissions } = response.data
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
- ## ๐Ÿ“š API Reference
1340
+ ### Advanced: AuthMonitor โ€” Observing Token Lifecycle Events
998
1341
 
999
- ### `useApi<T, D>(url, options)`
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
- The main composable for making HTTP requests.
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
- **Type Parameters:**
1004
- - `T` โ€” Response data type
1005
- - `D` โ€” Request body type (for POST/PUT/PATCH)
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. Can be a string, ref, or getter function. |
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
- ### Configuration Options
1494
+ #### UseApiOptions โ€” Complete Reference
1017
1495
 
1018
- #### Request Configuration
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 (auto-unwrapped if ref). |
1024
- | `params` | `MaybeRefOrGetter<any>` | `undefined` | URL query parameters (auto-unwrapped). |
1025
- | `headers` | `Record<string, string>` | `undefined` | Custom headers. |
1026
- | `authMode` | `'default' \| 'public'` | `'default'` | Set to `'public'` to skip token injection. |
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
- #### Reactivity & Auto-Execution
1506
+ **Reactivity & Auto-Execution:**
1029
1507
 
1030
1508
  | Option | Type | Default | Description |
1031
1509
  |--------|------|---------|-------------|
1032
- | `immediate` | `boolean` | `false` | Auto-execute on component mount. |
1033
- | `watch` | `WatchSource \| WatchSource[]` | `undefined` | Refs to watch for auto-refetch. |
1034
- | `debounce` | `number` | `0` | Debounce delay in ms (for watch). |
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
- #### Polling
1514
+ **Polling:**
1037
1515
 
1038
1516
  | Option | Type | Default | Description |
1039
1517
  |--------|------|---------|-------------|
1040
- | `poll` | `number \| { interval: number, whenHidden?: boolean } \| Ref<number>` | `0` | Polling interval in ms. Set to `0` to disable. |
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
- #### Retry & Error Handling
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` | Delay between retries in ms. |
1053
- | `skipErrorNotification` | `boolean` | `false` | Skip global error handler. |
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
- #### State Management
1528
+ **State Initialization:**
1056
1529
 
1057
1530
  | Option | Type | Default | Description |
1058
1531
  |--------|------|---------|-------------|
1059
- | `initialData` | `T` | `null` | Initial value for `data` ref. |
1060
- | `initialLoading` | `boolean` | `false` | Initial value for `loading` ref. |
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
- #### Lifecycle Hooks
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 on error. |
1069
- | `onFinish` | `() => void` | Called after request completes (success or error). |
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
- #### Advanced
1544
+ **Error Control:**
1072
1545
 
1073
1546
  | Option | Type | Default | Description |
1074
1547
  |--------|------|---------|-------------|
1075
- | `useGlobalAbort` | `boolean` | `true` | Subscribe to global abort controller. |
1548
+ | `skipErrorNotification` | `boolean` | `false` | When `true`, the global `onError` handler is NOT called for this request |
1076
1549
 
1077
- ---
1550
+ **Advanced:**
1078
1551
 
1079
- ### Return Values
1552
+ | Option | Type | Default | Description |
1553
+ |--------|------|---------|-------------|
1554
+ | `useGlobalAbort` | `boolean` | `true` | When `true`, this request participates in the global abort controller |
1080
1555
 
1081
- ```typescript
1082
- {
1083
- // State
1084
- data: Ref<T | null> // Response data
1085
- loading: Ref<boolean> // Loading state
1086
- error: Ref<ApiError | null> // Error object
1087
- statusCode: Ref<number | null> // HTTP status code
1088
- response: Ref<AxiosResponse<T>> // Full Axios response
1089
-
1090
- // Methods
1091
- execute: (config?: AxiosRequestConfig) => Promise<T | null>
1092
- mutate: (data: T | null | ((prev: T | null) => T | null)) => void
1093
- abort: (reason?: string) => void
1094
- reset: () => void
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
- Manually trigger the request. Optionally override configuration:
1574
+
1575
+ Manually trigger the request. Pass a config object to override options for this call only.
1100
1576
 
1101
1577
  ```typescript
1102
- const { execute } = useApi('/users')
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 config
1108
- await execute({ params: { page: 2 } })
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
- #### `mutate(newData)`
1112
- Manually update the `data` ref. Supports direct values or updater functions:
1591
+ // Override authMode for this call only
1592
+ await execute({ authMode: 'public' })
1593
+ ```
1113
1594
 
1114
- ```typescript
1115
- const { data, mutate } = useApi<User[]>('/users')
1595
+ ---
1116
1596
 
1117
- // Direct value
1118
- mutate([{ id: 1, name: 'John' }])
1597
+ ### `createApi(options)`
1119
1598
 
1120
- // Updater function (like React's setState)
1121
- mutate(prev => prev ? [...prev, newUser] : [newUser])
1599
+ Vue plugin factory. Call this once in `main.ts` to provide global configuration.
1122
1600
 
1123
- // Remove item
1124
- mutate(prev => prev?.filter(u => u.id !== userId) ?? null)
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
- > **Note:** `mutate` automatically clears any existing error.
1606
+ const api = createApiClient({ baseURL: 'https://api.example.com' })
1128
1607
 
1129
- #### `abort(reason?)`
1130
- Cancel the current request:
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
- ```typescript
1133
- const { execute, abort } = useApi('/long-task')
1622
+ **Options:**
1134
1623
 
1135
- execute()
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
- // Cancel after 5 seconds
1138
- setTimeout(() => abort('Timeout'), 5000)
1139
- ```
1631
+ ---
1140
1632
 
1141
- #### `reset()`
1142
- Reset all state to initial values:
1633
+ ### `createApiClient(options)`
1143
1634
 
1144
- ```typescript
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
- // Clear everything
1148
- reset()
1149
- // data.value = null, error.value = null, loading.value = false
1637
+ **Options:**
1150
1638
 
1151
- // Cancel after 5 seconds
1152
- setTimeout(() => abort('Timeout'), 5000)
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
- ### `createApiClient(options)`
1661
+ ### `useApiBatch<T>(urls, options)`
1158
1662
 
1159
- Factory function to create a configured Axios instance with built-in auth features.
1663
+ Execute multiple API requests in parallel with full reactive state.
1160
1664
 
1161
- **Options:**
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
- interface CreateApiClientOptions extends AxiosRequestConfig {
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
- **Default Configuration:**
1677
+ **UseApiBatchOptions:**
1185
1678
 
1186
- The library comes with sensible defaults:
1187
- - `timeout: 60000` (60 seconds)
1188
- - `headers: { "Content-Type": "application/json" }`
1189
- - `withCredentials: false`
1190
- - `refreshWithCredentials: false`
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
- **Example:**
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
- // Standard setup (tokens in localStorage)
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
- // With httpOnly cookies for refresh token only
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
- // With cookies for ALL requests (use with caution - CSRF risk)
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
- > ๐Ÿ”’ **Security Note:** Only enable `withCredentials` when necessary. Using it globally can expose you to CSRF attacks. Prefer `refreshWithCredentials: true` if you only need cookies for token refresh.
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
- ### `createApi(options)`
1740
+ ---
1235
1741
 
1236
- Vue plugin factory for global configuration.
1742
+ ### `tokenManager`
1237
1743
 
1238
- **Options:**
1744
+ See the full [tokenManager section](#tokenmanager--manual-token-control) above.
1239
1745
 
1746
+ Quick import:
1240
1747
  ```typescript
1241
- interface CreateApiOptions {
1242
- axios: AxiosInstance // Required: Axios instance
1243
- onError?: (error: ApiError) => void
1244
- errorParser?: (error: any) => ApiError
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
- **Example:**
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
- app.use(createApi({
1252
- axios: api,
1253
- onError: (error) => {
1254
- toast.error(error.message)
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
- ### `useApiBatch<T>(urls, options)`
1771
+ ## ๐Ÿงฉ Common Patterns
1270
1772
 
1271
- Execute multiple API requests in parallel with full reactive state.
1773
+ ### 1. Search with Debounce and Reset
1272
1774
 
1273
- **Type Parameters:**
1274
- - `T` โ€” Response data type for each request
1775
+ Full component: debounced search that resets cleanly without triggering an intermediate request.
1275
1776
 
1276
- **Arguments:**
1777
+ ```vue
1778
+ <script setup lang="ts">
1779
+ import { ref } from 'vue'
1780
+ import { useApi } from '@ametie/vue-muza-use'
1277
1781
 
1278
- | Argument | Type | Description |
1279
- |----------|------|-------------|
1280
- | `urls` | `MaybeRefOrGetter<string[]>` | Array of API endpoints. Can be static array, ref, or getter. |
1281
- | `options` | `UseApiBatchOptions<T>` | Configuration object (see below). |
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
- #### Batch Options
1826
+ ### 2. Paginated List with Filter Reset
1286
1827
 
1287
- | Option | Type | Default | Description |
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
- **Callbacks:**
1830
+ ```vue
1831
+ <script setup lang="ts">
1832
+ import { ref } from 'vue'
1833
+ import { useApi } from '@ametie/vue-muza-use'
1296
1834
 
1297
- | Option | Type | Description |
1298
- |--------|------|-------------|
1299
- | `onItemSuccess` | `(item: BatchResultItem<T>, index: number) => void` | Called when individual request succeeds. |
1300
- | `onItemError` | `(item: BatchResultItem<T>, index: number) => void` | Called when individual request fails. |
1301
- | `onProgress` | `(progress: BatchProgress) => void` | Called when progress updates. |
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
- #### Batch Return Values
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
- ```typescript
1309
- {
1310
- // State
1311
- data: Ref<BatchResultItem<T>[]> // All results with metadata
1312
- successfulData: Ref<T[]> // Only successful data (computed)
1313
- loading: Ref<boolean> // True while any request pending
1314
- error: Ref<ApiError | null> // Set if ALL requests failed
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
- #### `BatchProgress`
1879
+ ---
1880
+
1881
+ ### 3. Form Submit with Loading, Error Display, and Retry
1326
1882
 
1327
- ```typescript
1328
- interface BatchProgress {
1329
- completed: number // Requests finished (success + failed)
1330
- total: number // Total requests
1331
- percentage: number // 0-100
1332
- succeeded: number // Successful requests
1333
- failed: number // Failed requests
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
- #### `BatchResultItem<T>`
1893
+ interface Post {
1894
+ id: number
1895
+ title: string
1896
+ }
1338
1897
 
1339
- ```typescript
1340
- interface BatchResultItem<T> {
1341
- url: string // Requested URL
1342
- index: number // Position in original array
1343
- success: boolean // Whether succeeded
1344
- data: T | null // Response data
1345
- error: ApiError | null // Error if failed
1346
- statusCode: number | null
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
- ### `useAbortController()`
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
- Access the global abort controller for cancelling multiple requests.
1946
+ interface Stats { totalUsers: number; revenue: number }
1947
+ interface Order { id: number; total: number }
1948
+ interface Notification { id: number; text: string }
1355
1949
 
1356
- **Returns:**
1950
+ const requests: BatchRequestConfig[] = [
1951
+ { url: '/api/stats' },
1952
+ { url: '/api/recent-orders', params: { limit: 5 } },
1953
+ { url: '/api/notifications' }
1954
+ ]
1357
1955
 
1358
- ```typescript
1359
- {
1360
- abortAll: (reason?: string) => void // Cancel all subscribed requests
1361
- signal: Ref<AbortSignal> // Current abort signal
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
- **Example:**
1993
+ ---
1366
1994
 
1367
- ```typescript
1368
- import { useAbortController } from '@ametie/vue-muza-use'
1995
+ ### 5. Login + Token Save + Logout
1369
1996
 
1370
- const { abortAll } = useAbortController()
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
- const resetFilters = () => {
1373
- abortAll('Filter reset')
1374
- // ... reset logic
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
- ### Type Definitions
2053
+ ### 6. Polling Status Until Done
1381
2054
 
1382
- #### `ApiError`
2055
+ Start polling every 2 seconds and stop automatically when the job reaches a terminal status.
1383
2056
 
1384
- ```typescript
1385
- interface ApiError {
1386
- message: string // User-friendly error message
1387
- status?: number // HTTP status code
1388
- code?: string // Custom error code
1389
- errors?: Record<string, string[]> // Validation errors
1390
- details?: any // Original error object
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
- #### `MaybeRefOrGetter<T>`
2068
+ const jobId = ref<string | null>(null)
2069
+ const pollInterval = ref(0)
1395
2070
 
1396
- ```typescript
1397
- type MaybeRefOrGetter<T> = T | Ref<T> | (() => T)
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
- Accepts a value, a ref, or a getter function. Automatically unwrapped by the library.
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
-