@ametie/vue-muza-use 0.5.0 → 0.6.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
@@ -19,6 +19,7 @@ A production-ready composable that eliminates boilerplate and solves the hard pr
19
19
  - ⏱️ **Built-in Debouncing** — Perfect for search inputs and auto-save forms
20
20
  - 🛡️ **Race Condition Protection** — Global abort controller cancels stale requests automatically
21
21
  - 📊 **Auto-Polling** — Built-in interval fetching with smart tab visibility detection
22
+ - 🚀 **Batch Requests** — Execute multiple requests in parallel with progress tracking
22
23
  - 🧹 **Zero Memory Leaks** — Automatic cleanup of pending requests on component unmount
23
24
 
24
25
  **Advanced Features** (When you need them):
@@ -45,6 +46,7 @@ A production-ready composable that eliminates boilerplate and solves the hard pr
45
46
  **Real-World Examples:**
46
47
  - [Data Table with Pagination](#data-table-with-pagination--sorting)
47
48
  - [Request Cancellation](#request-cancellation)
49
+ - [Batch Requests](#batch-requests)
48
50
 
49
51
  **Advanced:**
50
52
  - [Custom Axios Instance](#-advanced-configuration)
@@ -541,6 +543,219 @@ const resetFilters = () => {
541
543
 
542
544
  ---
543
545
 
546
+ ### Batch Requests
547
+
548
+ Execute multiple API requests in parallel with full reactive state, progress tracking, and error tolerance.
549
+
550
+ #### Basic Usage
551
+
552
+ ```typescript
553
+ import { useApiBatch } from '@ametie/vue-muza-use'
554
+
555
+ interface User {
556
+ id: number
557
+ name: string
558
+ }
559
+
560
+ const {
561
+ successfulData, // Ref<User[]> - only successful results
562
+ loading, // Ref<boolean>
563
+ progress, // Ref<{ completed, total, percentage, succeeded, failed }>
564
+ execute
565
+ } = useApiBatch<User>([
566
+ '/users/1',
567
+ '/users/2',
568
+ '/users/3'
569
+ ])
570
+
571
+ 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 }
574
+ ```
575
+
576
+ #### Error Tolerance (Default)
577
+
578
+ By default, `useApiBatch` uses `settled: true` — failed requests don't stop the batch:
579
+
580
+ ```typescript
581
+ const {
582
+ successfulData,
583
+ errors, // Ref<ApiError[]> - all errors
584
+ progress,
585
+ execute
586
+ } = useApiBatch<User>([
587
+ '/users/1',
588
+ '/users/999', // Will fail (404)
589
+ '/users/3'
590
+ ])
591
+
592
+ 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
+ ```
598
+
599
+ #### Strict Mode
600
+
601
+ Fail immediately on first error:
602
+
603
+ ```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)
612
+ }
613
+ ```
614
+
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>
627
+
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
+ ```
637
+
638
+ #### Concurrency Limit
639
+
640
+ Control how many requests run in parallel (useful for rate limiting):
641
+
642
+ ```typescript
643
+ // Only 3 requests at a time
644
+ const { execute } = useApiBatch<User>(hundredUrls, {
645
+ concurrency: 3
646
+ })
647
+ ```
648
+
649
+ #### Reactive URLs
650
+
651
+ URLs can be reactive — use refs or computed:
652
+
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
+ })
660
+
661
+ // When userIds changes, call execute() to refetch
662
+ watch(userIds, () => execute())
663
+ ```
664
+
665
+ #### Auto Re-Execute with Watch
666
+
667
+ ```typescript
668
+ const filters = ref({ status: 'active' })
669
+
670
+ const { data } = useApiBatch<User>(urls, {
671
+ watch: filters, // Re-execute when filters change
672
+ immediate: true
673
+ })
674
+ ```
675
+
676
+ #### Item-Level Callbacks
677
+
678
+ ```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
+ ```
691
+
692
+ #### Full Return Type
693
+
694
+ ```typescript
695
+ 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
709
+
710
+ Each item in `data` contains:
711
+
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
+ }
721
+ ```
722
+
723
+ #### Real-World Example: Dashboard Loader
724
+
725
+ ```vue
726
+ <script setup lang="ts">
727
+ const dashboardUrls = [
728
+ '/api/stats',
729
+ '/api/recent-orders',
730
+ '/api/notifications',
731
+ '/api/user-activity'
732
+ ]
733
+
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
+ })
743
+
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)
747
+ </script>
748
+
749
+ <template>
750
+ <div v-if="loading" class="loading">
751
+ Loading dashboard... {{ progress.percentage }}%
752
+ </div>
753
+ <Dashboard v-else :stats="stats" :orders="orders" />
754
+ </template>
755
+ ```
756
+
757
+ ---
758
+
544
759
  ## ⚙️ Advanced Configuration
545
760
 
546
761
  ### Custom Axios Instance
@@ -1051,6 +1266,89 @@ app.use(createApi({
1051
1266
 
1052
1267
  ---
1053
1268
 
1269
+ ### `useApiBatch<T>(urls, options)`
1270
+
1271
+ Execute multiple API requests in parallel with full reactive state.
1272
+
1273
+ **Type Parameters:**
1274
+ - `T` — Response data type for each request
1275
+
1276
+ **Arguments:**
1277
+
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). |
1282
+
1283
+ ---
1284
+
1285
+ #### Batch Options
1286
+
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. |
1294
+
1295
+ **Callbacks:**
1296
+
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. |
1303
+
1304
+ ---
1305
+
1306
+ #### Batch Return Values
1307
+
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
1322
+ }
1323
+ ```
1324
+
1325
+ #### `BatchProgress`
1326
+
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
1334
+ }
1335
+ ```
1336
+
1337
+ #### `BatchResultItem<T>`
1338
+
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
1347
+ }
1348
+ ```
1349
+
1350
+ ---
1351
+
1054
1352
  ### `useAbortController()`
1055
1353
 
1056
1354
  Access the global abort controller for cancelling multiple requests.
package/dist/index.cjs CHANGED
@@ -38,6 +38,7 @@ __export(index_exports, {
38
38
  tokenManager: () => tokenManager,
39
39
  useAbortController: () => useAbortController,
40
40
  useApi: () => useApi,
41
+ useApiBatch: () => useApiBatch,
41
42
  useApiConfig: () => useApiConfig,
42
43
  useApiDelete: () => useApiDelete,
43
44
  useApiGet: () => useApiGet,
@@ -380,6 +381,242 @@ function useApiDelete(url, options) {
380
381
  return useApi(url, { ...options, method: "DELETE" });
381
382
  }
382
383
 
384
+ // src/useApiBatch.ts
385
+ var import_vue5 = require("vue");
386
+ function useApiBatch(urls, options = {}) {
387
+ const {
388
+ settled = true,
389
+ concurrency,
390
+ immediate = false,
391
+ skipErrorNotification = true,
392
+ watch: watchSource,
393
+ onItemSuccess,
394
+ onItemError,
395
+ onFinish,
396
+ onProgress,
397
+ ...apiOptions
398
+ } = options;
399
+ const getUrls = () => (0, import_vue5.toValue)(urls);
400
+ const data = (0, import_vue5.ref)([]);
401
+ const loading = (0, import_vue5.ref)(false);
402
+ const error = (0, import_vue5.ref)(null);
403
+ const errors = (0, import_vue5.ref)([]);
404
+ const progress = (0, import_vue5.ref)({
405
+ completed: 0,
406
+ total: 0,
407
+ percentage: 0,
408
+ succeeded: 0,
409
+ failed: 0
410
+ });
411
+ const successfulData = (0, import_vue5.computed)(
412
+ () => data.value.filter((item) => item.success && item.data !== null).map((item) => item.data)
413
+ );
414
+ const abortControllers = (0, import_vue5.ref)([]);
415
+ let isAborted = false;
416
+ const updateProgress = (succeeded, failed) => {
417
+ const currentUrls = getUrls();
418
+ const completed = succeeded + failed;
419
+ const newProgress = {
420
+ completed,
421
+ total: currentUrls.length,
422
+ percentage: currentUrls.length > 0 ? Math.round(completed / currentUrls.length * 100) : 0,
423
+ succeeded,
424
+ failed
425
+ };
426
+ progress.value = newProgress;
427
+ onProgress?.(newProgress);
428
+ };
429
+ const executeRequest = async (url, index, signal) => {
430
+ const { execute: execute2, error: reqError, statusCode } = useApi(url, {
431
+ ...apiOptions,
432
+ useGlobalAbort: false,
433
+ skipErrorNotification
434
+ });
435
+ try {
436
+ const result = await execute2({ signal });
437
+ if (signal.aborted) {
438
+ return {
439
+ url,
440
+ index,
441
+ success: false,
442
+ data: null,
443
+ error: { message: "Request aborted", status: 0, code: "ABORTED" },
444
+ statusCode: null
445
+ };
446
+ }
447
+ const item = {
448
+ url,
449
+ index,
450
+ success: result !== null && result !== void 0,
451
+ data: result ?? null,
452
+ error: reqError.value,
453
+ statusCode: statusCode.value
454
+ };
455
+ if (item.success) {
456
+ onItemSuccess?.(item, index);
457
+ } else if (item.error) {
458
+ onItemError?.(item, index);
459
+ }
460
+ return item;
461
+ } catch (err) {
462
+ const apiError = {
463
+ message: err instanceof Error ? err.message : "Unknown error",
464
+ status: 0,
465
+ code: "BATCH_ERROR"
466
+ };
467
+ const item = {
468
+ url,
469
+ index,
470
+ success: false,
471
+ data: null,
472
+ error: apiError,
473
+ statusCode: null
474
+ };
475
+ onItemError?.(item, index);
476
+ return item;
477
+ }
478
+ };
479
+ const executeWithConcurrency = async (urls2, limit) => {
480
+ const results = new Array(urls2.length);
481
+ let succeededCount = 0;
482
+ let failedCount = 0;
483
+ if (!limit || limit >= urls2.length) {
484
+ const promises = urls2.map((url, index) => {
485
+ const controller = new AbortController();
486
+ abortControllers.value.push(controller);
487
+ return executeRequest(url, index, controller.signal).then((result) => {
488
+ results[index] = result;
489
+ if (result.success) {
490
+ succeededCount++;
491
+ } else {
492
+ failedCount++;
493
+ if (result.error) {
494
+ errors.value.push(result.error);
495
+ }
496
+ }
497
+ updateProgress(succeededCount, failedCount);
498
+ if (!settled && !result.success && result.error) {
499
+ throw result.error;
500
+ }
501
+ return result;
502
+ });
503
+ });
504
+ if (settled) {
505
+ await Promise.allSettled(promises);
506
+ } else {
507
+ await Promise.all(promises);
508
+ }
509
+ } else {
510
+ let currentIndex = 0;
511
+ const executeNext = async () => {
512
+ while (currentIndex < urls2.length && !isAborted) {
513
+ const index = currentIndex++;
514
+ const url = urls2[index];
515
+ const controller = new AbortController();
516
+ abortControllers.value.push(controller);
517
+ const result = await executeRequest(url, index, controller.signal);
518
+ results[index] = result;
519
+ if (result.success) {
520
+ succeededCount++;
521
+ } else {
522
+ failedCount++;
523
+ if (result.error) {
524
+ errors.value.push(result.error);
525
+ }
526
+ }
527
+ updateProgress(succeededCount, failedCount);
528
+ if (!settled && !result.success && result.error) {
529
+ abort("First request failed in non-settled mode");
530
+ throw result.error;
531
+ }
532
+ }
533
+ };
534
+ const workers = Array.from({ length: Math.min(limit, urls2.length) }, () => executeNext());
535
+ if (settled) {
536
+ await Promise.allSettled(workers);
537
+ } else {
538
+ await Promise.all(workers);
539
+ }
540
+ }
541
+ return results;
542
+ };
543
+ const execute = async () => {
544
+ const currentUrls = getUrls();
545
+ isAborted = false;
546
+ loading.value = true;
547
+ error.value = null;
548
+ errors.value = [];
549
+ data.value = [];
550
+ abortControllers.value = [];
551
+ updateProgress(0, 0);
552
+ try {
553
+ const results = await executeWithConcurrency(currentUrls, concurrency);
554
+ data.value = results;
555
+ const allFailed = results.every((r) => !r.success);
556
+ if (allFailed && results.length > 0) {
557
+ error.value = {
558
+ message: `All ${results.length} requests failed`,
559
+ status: 0,
560
+ code: "BATCH_ALL_FAILED"
561
+ };
562
+ }
563
+ onFinish?.(results);
564
+ return results;
565
+ } catch (err) {
566
+ if (!settled) {
567
+ error.value = err;
568
+ }
569
+ throw err;
570
+ } finally {
571
+ loading.value = false;
572
+ abortControllers.value = [];
573
+ }
574
+ };
575
+ const abort = (message = "Batch aborted") => {
576
+ isAborted = true;
577
+ for (const controller of abortControllers.value) {
578
+ controller.abort(message);
579
+ }
580
+ abortControllers.value = [];
581
+ };
582
+ const reset = () => {
583
+ abort();
584
+ loading.value = false;
585
+ error.value = null;
586
+ errors.value = [];
587
+ data.value = [];
588
+ progress.value = {
589
+ completed: 0,
590
+ total: getUrls().length,
591
+ percentage: 0,
592
+ succeeded: 0,
593
+ failed: 0
594
+ };
595
+ };
596
+ if ((0, import_vue5.getCurrentScope)()) {
597
+ (0, import_vue5.onScopeDispose)(() => abort("Scope disposed"));
598
+ }
599
+ if (watchSource) {
600
+ (0, import_vue5.watch)(watchSource, () => {
601
+ execute();
602
+ }, { deep: true });
603
+ }
604
+ if (immediate) {
605
+ execute();
606
+ }
607
+ return {
608
+ data,
609
+ successfulData,
610
+ loading,
611
+ error,
612
+ errors,
613
+ progress,
614
+ execute,
615
+ abort,
616
+ reset
617
+ };
618
+ }
619
+
383
620
  // src/features/createInstance.ts
384
621
  var import_axios3 = __toESM(require("axios"), 1);
385
622
 
@@ -684,6 +921,7 @@ function createApiClient(options = {}) {
684
921
  tokenManager,
685
922
  useAbortController,
686
923
  useApi,
924
+ useApiBatch,
687
925
  useApiConfig,
688
926
  useApiDelete,
689
927
  useApiGet,
package/dist/index.d.cts CHANGED
@@ -94,6 +94,87 @@ interface AuthTokens$1 {
94
94
  refreshToken?: string;
95
95
  expiresIn?: number;
96
96
  }
97
+ /**
98
+ * Result of a single request in a batch operation
99
+ */
100
+ interface BatchResultItem<T = unknown> {
101
+ /** The URL that was requested */
102
+ url: string;
103
+ /** Index in the original urls array */
104
+ index: number;
105
+ /** Whether the request succeeded */
106
+ success: boolean;
107
+ /** The response data (null if failed) */
108
+ data: T | null;
109
+ /** Error details (null if succeeded) */
110
+ error: ApiError | null;
111
+ /** HTTP status code */
112
+ statusCode: number | null;
113
+ }
114
+ /**
115
+ * Progress information for batch operations
116
+ */
117
+ interface BatchProgress {
118
+ /** Number of completed requests (success + failed) */
119
+ completed: number;
120
+ /** Total number of requests */
121
+ total: number;
122
+ /** Completion percentage (0-100) */
123
+ percentage: number;
124
+ /** Number of successful requests */
125
+ succeeded: number;
126
+ /** Number of failed requests */
127
+ failed: number;
128
+ }
129
+ /**
130
+ * Options for useApiBatch
131
+ */
132
+ interface UseApiBatchOptions<T = unknown, D = unknown> extends Omit<ApiRequestConfig<D>, "url"> {
133
+ /**
134
+ * If true (default), failed requests don't stop the batch.
135
+ * If false, first error will reject the entire batch.
136
+ */
137
+ settled?: boolean;
138
+ /** Maximum concurrent requests. Default: unlimited */
139
+ concurrency?: number;
140
+ /** Execute immediately on mount */
141
+ immediate?: boolean;
142
+ /** Skip individual error notifications */
143
+ skipErrorNotification?: boolean;
144
+ /** Watch sources to trigger re-execution */
145
+ watch?: WatchSource | WatchSource[];
146
+ /** Callback when a single request succeeds */
147
+ onItemSuccess?: (item: BatchResultItem<T>, index: number) => void;
148
+ /** Callback when a single request fails */
149
+ onItemError?: (item: BatchResultItem<T>, index: number) => void;
150
+ /** Callback when all requests complete */
151
+ onFinish?: (results: BatchResultItem<T>[]) => void;
152
+ /** Callback when progress updates */
153
+ onProgress?: (progress: BatchProgress) => void;
154
+ }
155
+ /**
156
+ * Return type for useApiBatch
157
+ */
158
+ interface UseApiBatchReturn<T = unknown> {
159
+ /** All results with their status */
160
+ data: Ref<BatchResultItem<T>[]>;
161
+ /** Only successful results' data */
162
+ successfulData: Ref<T[]>;
163
+ /** Whether any request is still loading */
164
+ loading: Ref<boolean>;
165
+ /** Aggregated error (set if all requests failed) */
166
+ error: Ref<ApiError | null>;
167
+ /** List of all errors from failed requests */
168
+ errors: Ref<ApiError[]>;
169
+ /** Progress tracking */
170
+ progress: Ref<BatchProgress>;
171
+ /** Execute the batch */
172
+ execute: () => Promise<BatchResultItem<T>[]>;
173
+ /** Abort all pending requests */
174
+ abort: (message?: string) => void;
175
+ /** Reset state to initial */
176
+ reset: () => void;
177
+ }
97
178
 
98
179
  declare function createApi(options: ApiPluginOptions): {
99
180
  install(app: App): void;
@@ -153,6 +234,59 @@ declare function useApiPatch<T = unknown, D = unknown>(url: MaybeRefOrGetter<str
153
234
  */
154
235
  declare function useApiDelete<T = unknown>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T>, "method">): UseApiReturn<T>;
155
236
 
237
+ /**
238
+ * Execute multiple API requests in parallel with full reactive state
239
+ *
240
+ * Features:
241
+ * - Reactive loading, data, error, progress states
242
+ * - Reactive urls support (MaybeRefOrGetter)
243
+ * - Error tolerance with `settled: true` (default)
244
+ * - Concurrency limiting
245
+ * - Abort support for all pending requests
246
+ * - Detailed per-request results with URL mapping
247
+ * - Progress tracking
248
+ * - Watch option for auto re-execution
249
+ *
250
+ * @example
251
+ * ```ts
252
+ * // Basic usage - fetch multiple users
253
+ * const { data, loading, progress, execute } = useApiBatch<User>([
254
+ * '/users/1',
255
+ * '/users/2',
256
+ * '/users/3'
257
+ * ])
258
+ *
259
+ * await execute()
260
+ * console.log(data.value) // BatchResultItem<User>[]
261
+ *
262
+ * // With reactive urls
263
+ * const userIds = ref([1, 2, 3])
264
+ * const urls = computed(() => userIds.value.map(id => `/users/${id}`))
265
+ * const { successfulData } = useApiBatch<User>(urls, { immediate: true })
266
+ *
267
+ * // With options
268
+ * const { successfulData, errors, progress } = useApiBatch<Post>(
269
+ * ['/posts/1', '/posts/2', '/posts/3'],
270
+ * {
271
+ * concurrency: 2, // Max 2 parallel requests
272
+ * immediate: true, // Execute on mount
273
+ * onProgress: (p) => console.log(`${p.percentage}%`)
274
+ * }
275
+ * )
276
+ *
277
+ * // Strict mode - fail on first error
278
+ * const { execute } = useApiBatch<User>(urls, { settled: false })
279
+ *
280
+ * // Auto re-execute when dependency changes
281
+ * const filters = ref({ status: 'active' })
282
+ * const { data } = useApiBatch<User>(urls, {
283
+ * watch: filters,
284
+ * immediate: true
285
+ * })
286
+ * ```
287
+ */
288
+ declare function useApiBatch<T = unknown>(urls: MaybeRefOrGetter<string[]>, options?: UseApiBatchOptions<T>): UseApiBatchReturn<T>;
289
+
156
290
  /**
157
291
  * API State Composable
158
292
  *
@@ -424,4 +558,4 @@ interface AuthEventPayload {
424
558
  type AuthMonitorFn = (type: AuthEventType, payload: AuthEventPayload) => void;
425
559
  declare function setAuthMonitor(fn: AuthMonitorFn): void;
426
560
 
427
- export { type ApiError, type ApiPluginOptions, type ApiRequestConfig, type ApiState, type AuthEventPayload, AuthEventType, type AuthMode, type AuthMonitorFn, type AuthTokens$1 as AuthTokens, type SetDataInput, type UseApiOptions, type UseApiReturn, createApi, createApiClient, setAuthMonitor, setupInterceptors, tokenManager, useAbortController, useApi, useApiConfig, useApiDelete, useApiGet, useApiPatch, useApiPost, useApiPut, useApiState };
561
+ export { type ApiError, type ApiPluginOptions, type ApiRequestConfig, type ApiState, type AuthEventPayload, AuthEventType, type AuthMode, type AuthMonitorFn, type AuthTokens$1 as AuthTokens, type BatchProgress, type BatchResultItem, type SetDataInput, type UseApiBatchOptions, type UseApiBatchReturn, type UseApiOptions, type UseApiReturn, createApi, createApiClient, setAuthMonitor, setupInterceptors, tokenManager, useAbortController, useApi, useApiBatch, useApiConfig, useApiDelete, useApiGet, useApiPatch, useApiPost, useApiPut, useApiState };
package/dist/index.d.ts CHANGED
@@ -94,6 +94,87 @@ interface AuthTokens$1 {
94
94
  refreshToken?: string;
95
95
  expiresIn?: number;
96
96
  }
97
+ /**
98
+ * Result of a single request in a batch operation
99
+ */
100
+ interface BatchResultItem<T = unknown> {
101
+ /** The URL that was requested */
102
+ url: string;
103
+ /** Index in the original urls array */
104
+ index: number;
105
+ /** Whether the request succeeded */
106
+ success: boolean;
107
+ /** The response data (null if failed) */
108
+ data: T | null;
109
+ /** Error details (null if succeeded) */
110
+ error: ApiError | null;
111
+ /** HTTP status code */
112
+ statusCode: number | null;
113
+ }
114
+ /**
115
+ * Progress information for batch operations
116
+ */
117
+ interface BatchProgress {
118
+ /** Number of completed requests (success + failed) */
119
+ completed: number;
120
+ /** Total number of requests */
121
+ total: number;
122
+ /** Completion percentage (0-100) */
123
+ percentage: number;
124
+ /** Number of successful requests */
125
+ succeeded: number;
126
+ /** Number of failed requests */
127
+ failed: number;
128
+ }
129
+ /**
130
+ * Options for useApiBatch
131
+ */
132
+ interface UseApiBatchOptions<T = unknown, D = unknown> extends Omit<ApiRequestConfig<D>, "url"> {
133
+ /**
134
+ * If true (default), failed requests don't stop the batch.
135
+ * If false, first error will reject the entire batch.
136
+ */
137
+ settled?: boolean;
138
+ /** Maximum concurrent requests. Default: unlimited */
139
+ concurrency?: number;
140
+ /** Execute immediately on mount */
141
+ immediate?: boolean;
142
+ /** Skip individual error notifications */
143
+ skipErrorNotification?: boolean;
144
+ /** Watch sources to trigger re-execution */
145
+ watch?: WatchSource | WatchSource[];
146
+ /** Callback when a single request succeeds */
147
+ onItemSuccess?: (item: BatchResultItem<T>, index: number) => void;
148
+ /** Callback when a single request fails */
149
+ onItemError?: (item: BatchResultItem<T>, index: number) => void;
150
+ /** Callback when all requests complete */
151
+ onFinish?: (results: BatchResultItem<T>[]) => void;
152
+ /** Callback when progress updates */
153
+ onProgress?: (progress: BatchProgress) => void;
154
+ }
155
+ /**
156
+ * Return type for useApiBatch
157
+ */
158
+ interface UseApiBatchReturn<T = unknown> {
159
+ /** All results with their status */
160
+ data: Ref<BatchResultItem<T>[]>;
161
+ /** Only successful results' data */
162
+ successfulData: Ref<T[]>;
163
+ /** Whether any request is still loading */
164
+ loading: Ref<boolean>;
165
+ /** Aggregated error (set if all requests failed) */
166
+ error: Ref<ApiError | null>;
167
+ /** List of all errors from failed requests */
168
+ errors: Ref<ApiError[]>;
169
+ /** Progress tracking */
170
+ progress: Ref<BatchProgress>;
171
+ /** Execute the batch */
172
+ execute: () => Promise<BatchResultItem<T>[]>;
173
+ /** Abort all pending requests */
174
+ abort: (message?: string) => void;
175
+ /** Reset state to initial */
176
+ reset: () => void;
177
+ }
97
178
 
98
179
  declare function createApi(options: ApiPluginOptions): {
99
180
  install(app: App): void;
@@ -153,6 +234,59 @@ declare function useApiPatch<T = unknown, D = unknown>(url: MaybeRefOrGetter<str
153
234
  */
154
235
  declare function useApiDelete<T = unknown>(url: MaybeRefOrGetter<string | undefined>, options?: Omit<UseApiOptions<T>, "method">): UseApiReturn<T>;
155
236
 
237
+ /**
238
+ * Execute multiple API requests in parallel with full reactive state
239
+ *
240
+ * Features:
241
+ * - Reactive loading, data, error, progress states
242
+ * - Reactive urls support (MaybeRefOrGetter)
243
+ * - Error tolerance with `settled: true` (default)
244
+ * - Concurrency limiting
245
+ * - Abort support for all pending requests
246
+ * - Detailed per-request results with URL mapping
247
+ * - Progress tracking
248
+ * - Watch option for auto re-execution
249
+ *
250
+ * @example
251
+ * ```ts
252
+ * // Basic usage - fetch multiple users
253
+ * const { data, loading, progress, execute } = useApiBatch<User>([
254
+ * '/users/1',
255
+ * '/users/2',
256
+ * '/users/3'
257
+ * ])
258
+ *
259
+ * await execute()
260
+ * console.log(data.value) // BatchResultItem<User>[]
261
+ *
262
+ * // With reactive urls
263
+ * const userIds = ref([1, 2, 3])
264
+ * const urls = computed(() => userIds.value.map(id => `/users/${id}`))
265
+ * const { successfulData } = useApiBatch<User>(urls, { immediate: true })
266
+ *
267
+ * // With options
268
+ * const { successfulData, errors, progress } = useApiBatch<Post>(
269
+ * ['/posts/1', '/posts/2', '/posts/3'],
270
+ * {
271
+ * concurrency: 2, // Max 2 parallel requests
272
+ * immediate: true, // Execute on mount
273
+ * onProgress: (p) => console.log(`${p.percentage}%`)
274
+ * }
275
+ * )
276
+ *
277
+ * // Strict mode - fail on first error
278
+ * const { execute } = useApiBatch<User>(urls, { settled: false })
279
+ *
280
+ * // Auto re-execute when dependency changes
281
+ * const filters = ref({ status: 'active' })
282
+ * const { data } = useApiBatch<User>(urls, {
283
+ * watch: filters,
284
+ * immediate: true
285
+ * })
286
+ * ```
287
+ */
288
+ declare function useApiBatch<T = unknown>(urls: MaybeRefOrGetter<string[]>, options?: UseApiBatchOptions<T>): UseApiBatchReturn<T>;
289
+
156
290
  /**
157
291
  * API State Composable
158
292
  *
@@ -424,4 +558,4 @@ interface AuthEventPayload {
424
558
  type AuthMonitorFn = (type: AuthEventType, payload: AuthEventPayload) => void;
425
559
  declare function setAuthMonitor(fn: AuthMonitorFn): void;
426
560
 
427
- export { type ApiError, type ApiPluginOptions, type ApiRequestConfig, type ApiState, type AuthEventPayload, AuthEventType, type AuthMode, type AuthMonitorFn, type AuthTokens$1 as AuthTokens, type SetDataInput, type UseApiOptions, type UseApiReturn, createApi, createApiClient, setAuthMonitor, setupInterceptors, tokenManager, useAbortController, useApi, useApiConfig, useApiDelete, useApiGet, useApiPatch, useApiPost, useApiPut, useApiState };
561
+ export { type ApiError, type ApiPluginOptions, type ApiRequestConfig, type ApiState, type AuthEventPayload, AuthEventType, type AuthMode, type AuthMonitorFn, type AuthTokens$1 as AuthTokens, type BatchProgress, type BatchResultItem, type SetDataInput, type UseApiBatchOptions, type UseApiBatchReturn, type UseApiOptions, type UseApiReturn, createApi, createApiClient, setAuthMonitor, setupInterceptors, tokenManager, useAbortController, useApi, useApiBatch, useApiConfig, useApiDelete, useApiGet, useApiPatch, useApiPost, useApiPut, useApiState };
package/dist/index.mjs CHANGED
@@ -330,6 +330,242 @@ function useApiDelete(url, options) {
330
330
  return useApi(url, { ...options, method: "DELETE" });
331
331
  }
332
332
 
333
+ // src/useApiBatch.ts
334
+ import { ref as ref4, computed, getCurrentScope as getCurrentScope2, onScopeDispose as onScopeDispose2, toValue as toValue2, watch as watch2 } from "vue";
335
+ function useApiBatch(urls, options = {}) {
336
+ const {
337
+ settled = true,
338
+ concurrency,
339
+ immediate = false,
340
+ skipErrorNotification = true,
341
+ watch: watchSource,
342
+ onItemSuccess,
343
+ onItemError,
344
+ onFinish,
345
+ onProgress,
346
+ ...apiOptions
347
+ } = options;
348
+ const getUrls = () => toValue2(urls);
349
+ const data = ref4([]);
350
+ const loading = ref4(false);
351
+ const error = ref4(null);
352
+ const errors = ref4([]);
353
+ const progress = ref4({
354
+ completed: 0,
355
+ total: 0,
356
+ percentage: 0,
357
+ succeeded: 0,
358
+ failed: 0
359
+ });
360
+ const successfulData = computed(
361
+ () => data.value.filter((item) => item.success && item.data !== null).map((item) => item.data)
362
+ );
363
+ const abortControllers = ref4([]);
364
+ let isAborted = false;
365
+ const updateProgress = (succeeded, failed) => {
366
+ const currentUrls = getUrls();
367
+ const completed = succeeded + failed;
368
+ const newProgress = {
369
+ completed,
370
+ total: currentUrls.length,
371
+ percentage: currentUrls.length > 0 ? Math.round(completed / currentUrls.length * 100) : 0,
372
+ succeeded,
373
+ failed
374
+ };
375
+ progress.value = newProgress;
376
+ onProgress?.(newProgress);
377
+ };
378
+ const executeRequest = async (url, index, signal) => {
379
+ const { execute: execute2, error: reqError, statusCode } = useApi(url, {
380
+ ...apiOptions,
381
+ useGlobalAbort: false,
382
+ skipErrorNotification
383
+ });
384
+ try {
385
+ const result = await execute2({ signal });
386
+ if (signal.aborted) {
387
+ return {
388
+ url,
389
+ index,
390
+ success: false,
391
+ data: null,
392
+ error: { message: "Request aborted", status: 0, code: "ABORTED" },
393
+ statusCode: null
394
+ };
395
+ }
396
+ const item = {
397
+ url,
398
+ index,
399
+ success: result !== null && result !== void 0,
400
+ data: result ?? null,
401
+ error: reqError.value,
402
+ statusCode: statusCode.value
403
+ };
404
+ if (item.success) {
405
+ onItemSuccess?.(item, index);
406
+ } else if (item.error) {
407
+ onItemError?.(item, index);
408
+ }
409
+ return item;
410
+ } catch (err) {
411
+ const apiError = {
412
+ message: err instanceof Error ? err.message : "Unknown error",
413
+ status: 0,
414
+ code: "BATCH_ERROR"
415
+ };
416
+ const item = {
417
+ url,
418
+ index,
419
+ success: false,
420
+ data: null,
421
+ error: apiError,
422
+ statusCode: null
423
+ };
424
+ onItemError?.(item, index);
425
+ return item;
426
+ }
427
+ };
428
+ const executeWithConcurrency = async (urls2, limit) => {
429
+ const results = new Array(urls2.length);
430
+ let succeededCount = 0;
431
+ let failedCount = 0;
432
+ if (!limit || limit >= urls2.length) {
433
+ const promises = urls2.map((url, index) => {
434
+ const controller = new AbortController();
435
+ abortControllers.value.push(controller);
436
+ return executeRequest(url, index, controller.signal).then((result) => {
437
+ results[index] = result;
438
+ if (result.success) {
439
+ succeededCount++;
440
+ } else {
441
+ failedCount++;
442
+ if (result.error) {
443
+ errors.value.push(result.error);
444
+ }
445
+ }
446
+ updateProgress(succeededCount, failedCount);
447
+ if (!settled && !result.success && result.error) {
448
+ throw result.error;
449
+ }
450
+ return result;
451
+ });
452
+ });
453
+ if (settled) {
454
+ await Promise.allSettled(promises);
455
+ } else {
456
+ await Promise.all(promises);
457
+ }
458
+ } else {
459
+ let currentIndex = 0;
460
+ const executeNext = async () => {
461
+ while (currentIndex < urls2.length && !isAborted) {
462
+ const index = currentIndex++;
463
+ const url = urls2[index];
464
+ const controller = new AbortController();
465
+ abortControllers.value.push(controller);
466
+ const result = await executeRequest(url, index, controller.signal);
467
+ results[index] = result;
468
+ if (result.success) {
469
+ succeededCount++;
470
+ } else {
471
+ failedCount++;
472
+ if (result.error) {
473
+ errors.value.push(result.error);
474
+ }
475
+ }
476
+ updateProgress(succeededCount, failedCount);
477
+ if (!settled && !result.success && result.error) {
478
+ abort("First request failed in non-settled mode");
479
+ throw result.error;
480
+ }
481
+ }
482
+ };
483
+ const workers = Array.from({ length: Math.min(limit, urls2.length) }, () => executeNext());
484
+ if (settled) {
485
+ await Promise.allSettled(workers);
486
+ } else {
487
+ await Promise.all(workers);
488
+ }
489
+ }
490
+ return results;
491
+ };
492
+ const execute = async () => {
493
+ const currentUrls = getUrls();
494
+ isAborted = false;
495
+ loading.value = true;
496
+ error.value = null;
497
+ errors.value = [];
498
+ data.value = [];
499
+ abortControllers.value = [];
500
+ updateProgress(0, 0);
501
+ try {
502
+ const results = await executeWithConcurrency(currentUrls, concurrency);
503
+ data.value = results;
504
+ const allFailed = results.every((r) => !r.success);
505
+ if (allFailed && results.length > 0) {
506
+ error.value = {
507
+ message: `All ${results.length} requests failed`,
508
+ status: 0,
509
+ code: "BATCH_ALL_FAILED"
510
+ };
511
+ }
512
+ onFinish?.(results);
513
+ return results;
514
+ } catch (err) {
515
+ if (!settled) {
516
+ error.value = err;
517
+ }
518
+ throw err;
519
+ } finally {
520
+ loading.value = false;
521
+ abortControllers.value = [];
522
+ }
523
+ };
524
+ const abort = (message = "Batch aborted") => {
525
+ isAborted = true;
526
+ for (const controller of abortControllers.value) {
527
+ controller.abort(message);
528
+ }
529
+ abortControllers.value = [];
530
+ };
531
+ const reset = () => {
532
+ abort();
533
+ loading.value = false;
534
+ error.value = null;
535
+ errors.value = [];
536
+ data.value = [];
537
+ progress.value = {
538
+ completed: 0,
539
+ total: getUrls().length,
540
+ percentage: 0,
541
+ succeeded: 0,
542
+ failed: 0
543
+ };
544
+ };
545
+ if (getCurrentScope2()) {
546
+ onScopeDispose2(() => abort("Scope disposed"));
547
+ }
548
+ if (watchSource) {
549
+ watch2(watchSource, () => {
550
+ execute();
551
+ }, { deep: true });
552
+ }
553
+ if (immediate) {
554
+ execute();
555
+ }
556
+ return {
557
+ data,
558
+ successfulData,
559
+ loading,
560
+ error,
561
+ errors,
562
+ progress,
563
+ execute,
564
+ abort,
565
+ reset
566
+ };
567
+ }
568
+
333
569
  // src/features/createInstance.ts
334
570
  import axios from "axios";
335
571
 
@@ -633,6 +869,7 @@ export {
633
869
  tokenManager,
634
870
  useAbortController,
635
871
  useApi,
872
+ useApiBatch,
636
873
  useApiConfig,
637
874
  useApiDelete,
638
875
  useApiGet,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ametie/vue-muza-use",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Powerful Vue 3 API composable (Muza Kit) with Axios, Auto-Refresh & TypeScript",
5
5
  "author": "MortyQ",
6
6
  "license": "MIT",