@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 +298 -0
- package/dist/index.cjs +238 -0
- package/dist/index.d.cts +135 -1
- package/dist/index.d.ts +135 -1
- package/dist/index.mjs +237 -0
- package/package.json +1 -1
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,
|