@ametie/vue-muza-use 1.0.0 → 1.2.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 +125 -8
- package/dist/index.cjs +201 -20
- package/dist/index.d.cts +122 -8
- package/dist/index.d.ts +122 -8
- package/dist/index.mjs +202 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,6 +41,7 @@ A production-ready composable that eliminates boilerplate and solves the hard pr
|
|
|
41
41
|
- 🔐 **JWT Token Management** — Automatic token refresh with request queueing on 401 responses
|
|
42
42
|
- 🎛️ **Flexible Architecture** — Bring your own Axios instance with full configuration control
|
|
43
43
|
- 🍪 **withCredentials** — Per-request cookie and cross-origin credential control
|
|
44
|
+
- 🔭 **DevTools Panel** — Inspect live requests, payloads, and instance state via `@ametie/vue-muza-devtools`
|
|
44
45
|
|
|
45
46
|
---
|
|
46
47
|
|
|
@@ -63,11 +64,11 @@ A production-ready composable that eliminates boilerplate and solves the hard pr
|
|
|
63
64
|
| **Response caching** | ✅ | ❌ | ✅ | ✅ |
|
|
64
65
|
| **TypeScript** | ✅ | ✅ | ✅ | ✅ |
|
|
65
66
|
| **SSR / Nuxt** | ❌ | ✅ | ✅ | ✅ |
|
|
66
|
-
| **DevTools** |
|
|
67
|
+
| **DevTools** | ✅ | ❌ | ✅ | ❌ |
|
|
67
68
|
|
|
68
69
|
**Choose vue-muza-use if:** you build Vue 3 SPAs with Axios, need JWT token refresh out of the box, and want reactive request management without a heavyweight server-state solution.
|
|
69
70
|
|
|
70
|
-
**Choose TanStack Query if:** you need SSR
|
|
71
|
+
**Choose TanStack Query if:** you need SSR or advanced server-state normalization.
|
|
71
72
|
|
|
72
73
|
**Choose @vueuse/useFetch if:** you want a minimal fetch wrapper with no opinions.
|
|
73
74
|
|
|
@@ -97,9 +98,12 @@ A production-ready composable that eliminates boilerplate and solves the hard pr
|
|
|
97
98
|
- [Data Table with Pagination](#data-table-with-pagination--sorting)
|
|
98
99
|
- [Request Cancellation](#request-cancellation)
|
|
99
100
|
- [Batch Requests](#batch-requests)
|
|
101
|
+
- [Reactive Requests (Auto-tracking)](#reactive-requests--auto-tracking)
|
|
102
|
+
- [Batch Polling](#batch-polling)
|
|
100
103
|
|
|
101
104
|
**Advanced:**
|
|
102
105
|
- [Advanced Configuration](#️-advanced-configuration)
|
|
106
|
+
- [DevTools Panel](#-devtools-panel)
|
|
103
107
|
- [Authentication & Token Management](#-authentication--token-management)
|
|
104
108
|
- [Error Handling Reference](#-error-handling-reference)
|
|
105
109
|
- [Utilities & Standalone Composables](#-utilities--standalone-composables)
|
|
@@ -798,13 +802,15 @@ const { data } = useApi('/messages', {
|
|
|
798
802
|
Apply to all `useApi` instances at once:
|
|
799
803
|
|
|
800
804
|
```typescript
|
|
801
|
-
createApiClient({
|
|
802
|
-
|
|
805
|
+
const api = createApiClient({ baseURL: 'https://api.example.com' })
|
|
806
|
+
|
|
807
|
+
app.use(createApi({
|
|
808
|
+
axios: api,
|
|
803
809
|
globalOptions: {
|
|
804
810
|
refetchOnFocus: true,
|
|
805
811
|
refetchOnReconnect: true,
|
|
806
812
|
},
|
|
807
|
-
})
|
|
813
|
+
}))
|
|
808
814
|
```
|
|
809
815
|
|
|
810
816
|
Opt individual requests out with `refetchOnFocus: false`:
|
|
@@ -1403,6 +1409,77 @@ const { loading, progress, execute } = useApiBatch(urls, {
|
|
|
1403
1409
|
</template>
|
|
1404
1410
|
```
|
|
1405
1411
|
|
|
1412
|
+
#### Reactive Requests — Auto-tracking
|
|
1413
|
+
|
|
1414
|
+
**TL;DR: Pass a getter function as `requests` — the batch re-executes automatically when the getter's reactive dependencies change.**
|
|
1415
|
+
|
|
1416
|
+
This mirrors `useApi`'s auto-tracking behavior. No explicit `watch` option needed.
|
|
1417
|
+
|
|
1418
|
+
```typescript
|
|
1419
|
+
import { ref } from 'vue'
|
|
1420
|
+
import { useApiBatch } from '@ametie/vue-muza-use'
|
|
1421
|
+
|
|
1422
|
+
interface User { id: number; name: string }
|
|
1423
|
+
|
|
1424
|
+
const pages = ref([1, 2, 3])
|
|
1425
|
+
|
|
1426
|
+
// Getter is auto-tracked — re-executes when pages.value changes
|
|
1427
|
+
const { successfulData, loading } = useApiBatch<User>(
|
|
1428
|
+
() => pages.value.map(page => ({ url: '/users', params: { page } }))
|
|
1429
|
+
)
|
|
1430
|
+
|
|
1431
|
+
pages.value = [4, 5, 6] // → new batch fires automatically
|
|
1432
|
+
```
|
|
1433
|
+
|
|
1434
|
+
Set `lazy: true` to disable auto-tracking and keep full manual control:
|
|
1435
|
+
|
|
1436
|
+
```typescript
|
|
1437
|
+
const { execute } = useApiBatch(
|
|
1438
|
+
() => ids.value.map(id => `/items/${id}`),
|
|
1439
|
+
{ lazy: true } // reactive changes to ids do NOT trigger re-execution
|
|
1440
|
+
)
|
|
1441
|
+
|
|
1442
|
+
// You control when it runs
|
|
1443
|
+
await execute()
|
|
1444
|
+
```
|
|
1445
|
+
|
|
1446
|
+
#### Batch Polling
|
|
1447
|
+
|
|
1448
|
+
**TL;DR: Re-execute the batch on a fixed interval — useful for dashboards and status monitoring.**
|
|
1449
|
+
|
|
1450
|
+
Same semantics as `useApi`'s `poll` option.
|
|
1451
|
+
|
|
1452
|
+
```typescript
|
|
1453
|
+
import { useApiBatch } from '@ametie/vue-muza-use'
|
|
1454
|
+
|
|
1455
|
+
const { data, loading } = useApiBatch(
|
|
1456
|
+
['/stats/cpu', '/stats/memory', '/stats/disk'],
|
|
1457
|
+
{
|
|
1458
|
+
immediate: true,
|
|
1459
|
+
poll: 5000 // re-execute every 5 seconds
|
|
1460
|
+
}
|
|
1461
|
+
)
|
|
1462
|
+
```
|
|
1463
|
+
|
|
1464
|
+
With `whenHidden` control:
|
|
1465
|
+
|
|
1466
|
+
```typescript
|
|
1467
|
+
import { ref } from 'vue'
|
|
1468
|
+
import { useApiBatch } from '@ametie/vue-muza-use'
|
|
1469
|
+
|
|
1470
|
+
const interval = ref(10000)
|
|
1471
|
+
|
|
1472
|
+
const { data, abort } = useApiBatch(
|
|
1473
|
+
['/queue/jobs', '/queue/workers'],
|
|
1474
|
+
{
|
|
1475
|
+
immediate: true,
|
|
1476
|
+
poll: { interval, whenHidden: false } // pauses when tab is hidden
|
|
1477
|
+
}
|
|
1478
|
+
)
|
|
1479
|
+
|
|
1480
|
+
interval.value = 0 // stop polling
|
|
1481
|
+
```
|
|
1482
|
+
|
|
1406
1483
|
---
|
|
1407
1484
|
|
|
1408
1485
|
## ⚙️ Advanced Configuration
|
|
@@ -1494,6 +1571,44 @@ createApp(App).use(createApi({
|
|
|
1494
1571
|
|
|
1495
1572
|
---
|
|
1496
1573
|
|
|
1574
|
+
## 🔭 DevTools Panel
|
|
1575
|
+
|
|
1576
|
+
**TL;DR: Enable it in the plugin and get a live network inspector in your browser. No extra packages to install.**
|
|
1577
|
+
|
|
1578
|
+
The devtools panel is included with `@ametie/vue-muza-use`. It loads on demand and has zero impact on production bundles when disabled.
|
|
1579
|
+
|
|
1580
|
+
### Setup
|
|
1581
|
+
|
|
1582
|
+
Pass `devtools` to `createApi`. Gate it on `NODE_ENV` to keep production builds clean:
|
|
1583
|
+
|
|
1584
|
+
```typescript
|
|
1585
|
+
import { createApp } from 'vue'
|
|
1586
|
+
import { createApi, createApiClient } from '@ametie/vue-muza-use'
|
|
1587
|
+
import App from './App.vue'
|
|
1588
|
+
|
|
1589
|
+
const api = createApiClient({ baseURL: 'https://api.example.com' })
|
|
1590
|
+
|
|
1591
|
+
createApp(App).use(createApi({
|
|
1592
|
+
axios: api,
|
|
1593
|
+
devtools: {
|
|
1594
|
+
enabled: process.env.NODE_ENV !== 'production'
|
|
1595
|
+
}
|
|
1596
|
+
}))
|
|
1597
|
+
```
|
|
1598
|
+
|
|
1599
|
+
The panel loads asynchronously — it has zero impact on startup time.
|
|
1600
|
+
|
|
1601
|
+
### DevTools Options
|
|
1602
|
+
|
|
1603
|
+
| Option | Type | Default | Description |
|
|
1604
|
+
|--------|------|---------|-------------|
|
|
1605
|
+
| `enabled` | `boolean` | — | Required. Set `true` to mount the panel |
|
|
1606
|
+
| `maxHistory` | `number` | `300` | Maximum number of requests kept in the Network tab history |
|
|
1607
|
+
| `maxPayloadSize` | `number` | `200_000` | Maximum bytes per payload/response before truncation in the viewer |
|
|
1608
|
+
| `tabs` | `DevtoolsTab[]` | `[]` | Additional custom tabs to register in the panel |
|
|
1609
|
+
|
|
1610
|
+
---
|
|
1611
|
+
|
|
1497
1612
|
## 🔐 Authentication & Token Management
|
|
1498
1613
|
|
|
1499
1614
|
> **Note:** Authentication setup is optional. Only add this if your API requires JWT tokens.
|
|
@@ -2139,15 +2254,17 @@ type BatchInput = string | BatchRequestConfig
|
|
|
2139
2254
|
|
|
2140
2255
|
| Option | Type | Default | Description |
|
|
2141
2256
|
|--------|------|---------|-------------|
|
|
2142
|
-
| `settled` | `boolean` | `true` | When `true`, all requests run even if some fail. When `false`, the first error
|
|
2257
|
+
| `settled` | `boolean` | `true` | When `true`, all requests run even if some fail. When `false`, the first error aborts remaining requests |
|
|
2143
2258
|
| `concurrency` | `number` | unlimited | Maximum number of requests that run in parallel at once |
|
|
2144
2259
|
| `immediate` | `boolean` | `false` | Execute the batch automatically when the composable is created |
|
|
2260
|
+
| `lazy` | `boolean` | `false` | Disable auto-tracking. When `false`, a getter passed as `requests` re-executes the batch automatically when its reactive deps change. Set `true` for full manual control via `execute()` |
|
|
2261
|
+
| `poll` | `number \| { interval: MaybeRefOrGetter<number>, whenHidden?: MaybeRefOrGetter<boolean> }` | `0` | Polling interval in ms. After each execution, schedules the next one after `interval` ms. `0` disables polling. `whenHidden: false` (default) pauses when the tab is hidden |
|
|
2145
2262
|
| `skipErrorNotification` | `boolean` | `true` | Suppress global error handler for individual item failures |
|
|
2146
|
-
| `watch` | `WatchSource \| WatchSource[]` | `undefined` |
|
|
2263
|
+
| `watch` | `WatchSource \| WatchSource[]` | `undefined` | **Deprecated** — use a reactive getter with `lazy: false` instead. Will be removed in v2.0 |
|
|
2147
2264
|
| `onItemSuccess` | `(item: BatchResultItem<T>, index: number) => void` | `undefined` | Called each time a single request in the batch succeeds |
|
|
2148
2265
|
| `onItemError` | `(item: BatchResultItem<T>, index: number) => void` | `undefined` | Called each time a single request in the batch fails |
|
|
2149
2266
|
| `onProgress` | `(progress: BatchProgress) => void` | `undefined` | Called after each request completes with updated progress |
|
|
2150
|
-
| `onFinish` | `(results: BatchResultItem<T>[]) => void` | `undefined` | Called once when all requests have completed |
|
|
2267
|
+
| `onFinish` | `(results: BatchResultItem<T>[]) => void` | `undefined` | Called once when all requests have completed (even on `settled: false` rejection) |
|
|
2151
2268
|
|
|
2152
2269
|
**UseApiBatchReturn:**
|
|
2153
2270
|
|
package/dist/index.cjs
CHANGED
|
@@ -53,6 +53,48 @@ module.exports = __toCommonJS(index_exports);
|
|
|
53
53
|
|
|
54
54
|
// src/plugin.ts
|
|
55
55
|
var import_vue = require("vue");
|
|
56
|
+
|
|
57
|
+
// src/devtools.ts
|
|
58
|
+
var bridge = null;
|
|
59
|
+
var requestCounter = 0;
|
|
60
|
+
var pendingCalls = [];
|
|
61
|
+
function nextRequestId() {
|
|
62
|
+
return `req_${++requestCounter}`;
|
|
63
|
+
}
|
|
64
|
+
async function initDevtools(options, app) {
|
|
65
|
+
if (!options.enabled) return;
|
|
66
|
+
try {
|
|
67
|
+
const { createBridge } = await import("@ametie/vue-muza-devtools");
|
|
68
|
+
bridge = createBridge(options, app);
|
|
69
|
+
for (const fn of pendingCalls) fn();
|
|
70
|
+
pendingCalls.length = 0;
|
|
71
|
+
} catch {
|
|
72
|
+
console.warn("[vue-muza-use] devtools enabled but @ametie/vue-muza-devtools is not installed");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
var devtoolsBridge = {
|
|
76
|
+
onInstanceCreated(id, url, options) {
|
|
77
|
+
if (bridge) {
|
|
78
|
+
bridge.onInstanceCreated(id, url, options);
|
|
79
|
+
} else {
|
|
80
|
+
pendingCalls.push(() => bridge?.onInstanceCreated(id, url, options));
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
onInstanceDestroyed(id) {
|
|
84
|
+
bridge?.onInstanceDestroyed(id);
|
|
85
|
+
},
|
|
86
|
+
onStateUpdate(id, state) {
|
|
87
|
+
bridge?.onStateUpdate(id, state);
|
|
88
|
+
},
|
|
89
|
+
onRequestStart(record) {
|
|
90
|
+
bridge?.onRequestStart(record);
|
|
91
|
+
},
|
|
92
|
+
onRequestEnd(id, result) {
|
|
93
|
+
bridge?.onRequestEnd(id, result);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// src/plugin.ts
|
|
56
98
|
var API_INJECTION_KEY = /* @__PURE__ */ Symbol("use-api-config");
|
|
57
99
|
var globalConfig = null;
|
|
58
100
|
function createApi(options) {
|
|
@@ -60,6 +102,9 @@ function createApi(options) {
|
|
|
60
102
|
return {
|
|
61
103
|
install(app) {
|
|
62
104
|
app.provide(API_INJECTION_KEY, options);
|
|
105
|
+
if (options.devtools) {
|
|
106
|
+
void initDevtools(options.devtools, app);
|
|
107
|
+
}
|
|
63
108
|
}
|
|
64
109
|
};
|
|
65
110
|
}
|
|
@@ -110,6 +155,18 @@ function debounceFn(fn, delay) {
|
|
|
110
155
|
};
|
|
111
156
|
}
|
|
112
157
|
|
|
158
|
+
// src/utils/urlUtils.ts
|
|
159
|
+
function parseUrlQueryParams(url) {
|
|
160
|
+
if (!url) return null;
|
|
161
|
+
const qIndex = url.indexOf("?");
|
|
162
|
+
if (qIndex === -1) return null;
|
|
163
|
+
const params = {};
|
|
164
|
+
for (const [k, v] of new URLSearchParams(url.slice(qIndex + 1))) {
|
|
165
|
+
params[k] = v;
|
|
166
|
+
}
|
|
167
|
+
return Object.keys(params).length > 0 ? params : null;
|
|
168
|
+
}
|
|
169
|
+
|
|
113
170
|
// src/useApi.ts
|
|
114
171
|
var import_axios2 = require("axios");
|
|
115
172
|
var import_vue5 = require("vue");
|
|
@@ -339,6 +396,30 @@ function useApi(url, options = {}) {
|
|
|
339
396
|
const startLoading = initialLoading ?? immediate;
|
|
340
397
|
const state = useApiState(initialData, { initialLoading: startLoading });
|
|
341
398
|
const revalidating = (0, import_vue5.ref)(false);
|
|
399
|
+
const instanceId = (0, import_vue5.getCurrentInstance)() != null ? (0, import_vue5.useId)() : nextRequestId();
|
|
400
|
+
devtoolsBridge.onInstanceCreated(instanceId, (0, import_vue5.toValue)(url), {
|
|
401
|
+
authMode: options.authMode ?? "default",
|
|
402
|
+
cache: options.cache,
|
|
403
|
+
retry: options.retry ?? false,
|
|
404
|
+
poll: (() => {
|
|
405
|
+
const v = (0, import_vue5.toValue)(options.poll);
|
|
406
|
+
return typeof v === "number" ? v : 0;
|
|
407
|
+
})(),
|
|
408
|
+
immediate: options.immediate ?? false,
|
|
409
|
+
lazy: options.lazy ?? false
|
|
410
|
+
});
|
|
411
|
+
if ((0, import_vue5.getCurrentScope)()) {
|
|
412
|
+
(0, import_vue5.watch)(
|
|
413
|
+
() => ({
|
|
414
|
+
loading: state.loading.value,
|
|
415
|
+
error: state.error.value,
|
|
416
|
+
statusCode: state.statusCode.value,
|
|
417
|
+
data: state.data.value
|
|
418
|
+
}),
|
|
419
|
+
(s) => devtoolsBridge.onStateUpdate(instanceId, s),
|
|
420
|
+
{ deep: true }
|
|
421
|
+
);
|
|
422
|
+
}
|
|
342
423
|
const abortController2 = (0, import_vue5.ref)(null);
|
|
343
424
|
const globalAbort = useGlobalAbort ? useAbortController() : null;
|
|
344
425
|
let pollTimer = null;
|
|
@@ -374,6 +455,16 @@ function useApi(url, options = {}) {
|
|
|
374
455
|
if (abortController2.value) abortController2.value.abort("Cancelled by new request");
|
|
375
456
|
const controller = new AbortController();
|
|
376
457
|
abortController2.value = controller;
|
|
458
|
+
if (config?.signal) {
|
|
459
|
+
const signal = config.signal;
|
|
460
|
+
if (signal.aborted) {
|
|
461
|
+
controller.abort(signal.reason);
|
|
462
|
+
} else {
|
|
463
|
+
signal.addEventListener("abort", () => {
|
|
464
|
+
controller.abort(signal.reason);
|
|
465
|
+
}, { once: true });
|
|
466
|
+
}
|
|
467
|
+
}
|
|
377
468
|
let globalAbortHandler = null;
|
|
378
469
|
let subscribedSignal = null;
|
|
379
470
|
if (globalAbort) {
|
|
@@ -393,6 +484,9 @@ function useApi(url, options = {}) {
|
|
|
393
484
|
state.setError(null);
|
|
394
485
|
let wasCancelled = false;
|
|
395
486
|
let retryCount = 0;
|
|
487
|
+
let devtoolsRequestId = null;
|
|
488
|
+
let devtoolsRequestStartedAt = 0;
|
|
489
|
+
let devtoolsRequestEndResult = null;
|
|
396
490
|
try {
|
|
397
491
|
if (!requestUrl) {
|
|
398
492
|
throw new Error("Request URL is missing");
|
|
@@ -401,6 +495,21 @@ function useApi(url, options = {}) {
|
|
|
401
495
|
const resolvedData = (0, import_vue5.toValue)(rawData);
|
|
402
496
|
const rawParams = config?.params !== void 0 ? config.params : axiosConfig.params;
|
|
403
497
|
const resolvedParams = (0, import_vue5.toValue)(rawParams);
|
|
498
|
+
const devtoolsQueryParams = resolvedParams ?? parseUrlQueryParams(requestUrl);
|
|
499
|
+
devtoolsRequestId = nextRequestId();
|
|
500
|
+
devtoolsRequestStartedAt = Date.now();
|
|
501
|
+
devtoolsBridge.onRequestStart({
|
|
502
|
+
id: devtoolsRequestId,
|
|
503
|
+
instanceId,
|
|
504
|
+
url: requestUrl,
|
|
505
|
+
method,
|
|
506
|
+
startedAt: devtoolsRequestStartedAt,
|
|
507
|
+
status: "pending",
|
|
508
|
+
statusCode: null,
|
|
509
|
+
requestHeaders: {},
|
|
510
|
+
payload: resolvedData ?? null,
|
|
511
|
+
queryParams: devtoolsQueryParams
|
|
512
|
+
});
|
|
404
513
|
while (true) {
|
|
405
514
|
try {
|
|
406
515
|
const response = await axios2.request({
|
|
@@ -424,6 +533,12 @@ function useApi(url, options = {}) {
|
|
|
424
533
|
}
|
|
425
534
|
onSuccess?.(response);
|
|
426
535
|
notifyFetched();
|
|
536
|
+
devtoolsRequestEndResult = {
|
|
537
|
+
status: "success",
|
|
538
|
+
statusCode: response.status,
|
|
539
|
+
response: response.data,
|
|
540
|
+
duration: Date.now() - devtoolsRequestStartedAt
|
|
541
|
+
};
|
|
427
542
|
return selected;
|
|
428
543
|
} catch (err) {
|
|
429
544
|
if (controller.signal.aborted || (0, import_axios2.isAxiosError)(err) && err.code === "ERR_CANCELED") {
|
|
@@ -442,6 +557,12 @@ function useApi(url, options = {}) {
|
|
|
442
557
|
}
|
|
443
558
|
continue;
|
|
444
559
|
}
|
|
560
|
+
devtoolsRequestEndResult = {
|
|
561
|
+
status: "error",
|
|
562
|
+
error: apiError,
|
|
563
|
+
statusCode: apiError.status ?? null,
|
|
564
|
+
duration: Date.now() - devtoolsRequestStartedAt
|
|
565
|
+
};
|
|
445
566
|
if (!skipErrorNotification && globalErrorHandler) {
|
|
446
567
|
globalErrorHandler(apiError, err);
|
|
447
568
|
}
|
|
@@ -457,6 +578,12 @@ function useApi(url, options = {}) {
|
|
|
457
578
|
return null;
|
|
458
579
|
}
|
|
459
580
|
const apiError = errorParser ? errorParser(err) : parseApiError(err);
|
|
581
|
+
devtoolsRequestEndResult = {
|
|
582
|
+
status: "error",
|
|
583
|
+
error: apiError,
|
|
584
|
+
statusCode: null,
|
|
585
|
+
duration: Date.now() - devtoolsRequestStartedAt
|
|
586
|
+
};
|
|
460
587
|
if (!skipErrorNotification && globalErrorHandler) {
|
|
461
588
|
globalErrorHandler(apiError, err);
|
|
462
589
|
}
|
|
@@ -465,6 +592,12 @@ function useApi(url, options = {}) {
|
|
|
465
592
|
onError?.(apiError);
|
|
466
593
|
return null;
|
|
467
594
|
} finally {
|
|
595
|
+
if (devtoolsRequestId !== null) {
|
|
596
|
+
devtoolsBridge.onRequestEnd(
|
|
597
|
+
devtoolsRequestId,
|
|
598
|
+
devtoolsRequestEndResult ?? { status: "aborted", duration: Date.now() - devtoolsRequestStartedAt }
|
|
599
|
+
);
|
|
600
|
+
}
|
|
468
601
|
if (globalAbortHandler && subscribedSignal) subscribedSignal.removeEventListener("abort", globalAbortHandler);
|
|
469
602
|
revalidating.value = false;
|
|
470
603
|
if (!wasCancelled) {
|
|
@@ -541,7 +674,10 @@ function useApi(url, options = {}) {
|
|
|
541
674
|
}
|
|
542
675
|
};
|
|
543
676
|
if ((0, import_vue5.getCurrentScope)()) {
|
|
544
|
-
(0, import_vue5.onScopeDispose)(() =>
|
|
677
|
+
(0, import_vue5.onScopeDispose)(() => {
|
|
678
|
+
abort("Scope disposed");
|
|
679
|
+
devtoolsBridge.onInstanceDestroyed(instanceId);
|
|
680
|
+
});
|
|
545
681
|
}
|
|
546
682
|
if (immediate) execute();
|
|
547
683
|
if (typeof document !== "undefined") {
|
|
@@ -608,6 +744,8 @@ function useApiBatch(requests, options = {}) {
|
|
|
608
744
|
concurrency,
|
|
609
745
|
immediate = false,
|
|
610
746
|
skipErrorNotification = true,
|
|
747
|
+
lazy = false,
|
|
748
|
+
poll = 0,
|
|
611
749
|
watch: watchSource,
|
|
612
750
|
onItemSuccess,
|
|
613
751
|
onItemError,
|
|
@@ -632,13 +770,21 @@ function useApiBatch(requests, options = {}) {
|
|
|
632
770
|
);
|
|
633
771
|
const abortControllers = (0, import_vue6.ref)([]);
|
|
634
772
|
let isAborted = false;
|
|
635
|
-
|
|
636
|
-
|
|
773
|
+
let pollTimer = null;
|
|
774
|
+
const getPollConfig = () => {
|
|
775
|
+
const val = (0, import_vue6.toValue)(poll);
|
|
776
|
+
if (typeof val === "number") return { interval: val, whenHidden: false };
|
|
777
|
+
return {
|
|
778
|
+
interval: (0, import_vue6.toValue)(val.interval),
|
|
779
|
+
whenHidden: (0, import_vue6.toValue)(val.whenHidden) ?? false
|
|
780
|
+
};
|
|
781
|
+
};
|
|
782
|
+
const updateProgress = (succeeded, failed, total) => {
|
|
637
783
|
const completed = succeeded + failed;
|
|
638
784
|
const newProgress = {
|
|
639
785
|
completed,
|
|
640
|
-
total
|
|
641
|
-
percentage:
|
|
786
|
+
total,
|
|
787
|
+
percentage: total > 0 ? Math.round(completed / total * 100) : 0,
|
|
642
788
|
succeeded,
|
|
643
789
|
failed
|
|
644
790
|
};
|
|
@@ -655,7 +801,8 @@ function useApiBatch(requests, options = {}) {
|
|
|
655
801
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
656
802
|
...config.headers && { headers: config.headers },
|
|
657
803
|
useGlobalAbort: false,
|
|
658
|
-
skipErrorNotification
|
|
804
|
+
skipErrorNotification,
|
|
805
|
+
lazy: true
|
|
659
806
|
}));
|
|
660
807
|
const { execute: execute2, error: reqError, statusCode, response } = api;
|
|
661
808
|
try {
|
|
@@ -710,7 +857,7 @@ function useApiBatch(requests, options = {}) {
|
|
|
710
857
|
scope.stop();
|
|
711
858
|
}
|
|
712
859
|
};
|
|
713
|
-
const executeWithConcurrency = async (requests2, limit) => {
|
|
860
|
+
const executeWithConcurrency = async (requests2, limit, total) => {
|
|
714
861
|
const results = new Array(requests2.length);
|
|
715
862
|
let succeededCount = 0;
|
|
716
863
|
let failedCount = 0;
|
|
@@ -728,8 +875,9 @@ function useApiBatch(requests, options = {}) {
|
|
|
728
875
|
errors.value.push(result.error);
|
|
729
876
|
}
|
|
730
877
|
}
|
|
731
|
-
updateProgress(succeededCount, failedCount);
|
|
878
|
+
updateProgress(succeededCount, failedCount, total);
|
|
732
879
|
if (!settled && !result.success && result.error) {
|
|
880
|
+
abort("First request failed in non-settled mode");
|
|
733
881
|
throw result.error;
|
|
734
882
|
}
|
|
735
883
|
return result;
|
|
@@ -758,7 +906,7 @@ function useApiBatch(requests, options = {}) {
|
|
|
758
906
|
errors.value.push(result.error);
|
|
759
907
|
}
|
|
760
908
|
}
|
|
761
|
-
updateProgress(succeededCount, failedCount);
|
|
909
|
+
updateProgress(succeededCount, failedCount, total);
|
|
762
910
|
if (!settled && !result.success && result.error) {
|
|
763
911
|
abort("First request failed in non-settled mode");
|
|
764
912
|
throw result.error;
|
|
@@ -775,6 +923,9 @@ function useApiBatch(requests, options = {}) {
|
|
|
775
923
|
return results;
|
|
776
924
|
};
|
|
777
925
|
const execute = async () => {
|
|
926
|
+
if (loading.value) {
|
|
927
|
+
abort("Replaced by new execution");
|
|
928
|
+
}
|
|
778
929
|
const currentRequests = getRequests();
|
|
779
930
|
isAborted = false;
|
|
780
931
|
loading.value = true;
|
|
@@ -782,20 +933,21 @@ function useApiBatch(requests, options = {}) {
|
|
|
782
933
|
errors.value = [];
|
|
783
934
|
data.value = [];
|
|
784
935
|
abortControllers.value = [];
|
|
785
|
-
|
|
936
|
+
const total = currentRequests.length;
|
|
937
|
+
updateProgress(0, 0, total);
|
|
938
|
+
let finalResults = [];
|
|
786
939
|
try {
|
|
787
|
-
|
|
788
|
-
data.value =
|
|
789
|
-
const allFailed =
|
|
790
|
-
if (allFailed &&
|
|
940
|
+
finalResults = await executeWithConcurrency(currentRequests, concurrency, total);
|
|
941
|
+
data.value = finalResults;
|
|
942
|
+
const allFailed = finalResults.every((r) => !r.success);
|
|
943
|
+
if (allFailed && finalResults.length > 0) {
|
|
791
944
|
error.value = {
|
|
792
|
-
message: `All ${
|
|
945
|
+
message: `All ${finalResults.length} requests failed`,
|
|
793
946
|
status: 0,
|
|
794
947
|
code: "BATCH_ALL_FAILED"
|
|
795
948
|
};
|
|
796
949
|
}
|
|
797
|
-
|
|
798
|
-
return results;
|
|
950
|
+
return finalResults;
|
|
799
951
|
} catch (err) {
|
|
800
952
|
if (!settled) {
|
|
801
953
|
error.value = err;
|
|
@@ -804,10 +956,27 @@ function useApiBatch(requests, options = {}) {
|
|
|
804
956
|
} finally {
|
|
805
957
|
loading.value = false;
|
|
806
958
|
abortControllers.value = [];
|
|
959
|
+
onFinish?.(finalResults);
|
|
960
|
+
if (!isAborted) {
|
|
961
|
+
const { interval, whenHidden } = getPollConfig();
|
|
962
|
+
if (interval > 0) {
|
|
963
|
+
const hidden = typeof document !== "undefined" && document.hidden;
|
|
964
|
+
if (whenHidden || !hidden) {
|
|
965
|
+
pollTimer = setTimeout(() => {
|
|
966
|
+
pollTimer = null;
|
|
967
|
+
execute();
|
|
968
|
+
}, interval);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
807
972
|
}
|
|
808
973
|
};
|
|
809
974
|
const abort = (message = "Batch aborted") => {
|
|
810
975
|
isAborted = true;
|
|
976
|
+
if (pollTimer) {
|
|
977
|
+
clearTimeout(pollTimer);
|
|
978
|
+
pollTimer = null;
|
|
979
|
+
}
|
|
811
980
|
for (const controller of abortControllers.value) {
|
|
812
981
|
controller.abort(message);
|
|
813
982
|
}
|
|
@@ -830,14 +999,26 @@ function useApiBatch(requests, options = {}) {
|
|
|
830
999
|
if ((0, import_vue6.getCurrentScope)()) {
|
|
831
1000
|
(0, import_vue6.onScopeDispose)(() => abort("Scope disposed"));
|
|
832
1001
|
}
|
|
1002
|
+
if (!lazy && typeof requests === "function") {
|
|
1003
|
+
const trackingScope = (0, import_vue6.effectScope)();
|
|
1004
|
+
trackingScope.run(() => {
|
|
1005
|
+
const requestsComputed = (0, import_vue6.computed)(
|
|
1006
|
+
() => requests().map(normalizeRequest)
|
|
1007
|
+
);
|
|
1008
|
+
(0, import_vue6.watch)(requestsComputed, () => {
|
|
1009
|
+
execute();
|
|
1010
|
+
}, { deep: true });
|
|
1011
|
+
});
|
|
1012
|
+
if ((0, import_vue6.getCurrentScope)()) (0, import_vue6.onScopeDispose)(() => trackingScope.stop());
|
|
1013
|
+
execute();
|
|
1014
|
+
} else if (immediate) {
|
|
1015
|
+
execute();
|
|
1016
|
+
}
|
|
833
1017
|
if (watchSource) {
|
|
834
1018
|
(0, import_vue6.watch)(watchSource, () => {
|
|
835
1019
|
execute();
|
|
836
1020
|
}, { deep: true });
|
|
837
1021
|
}
|
|
838
|
-
if (immediate) {
|
|
839
|
-
execute();
|
|
840
|
-
}
|
|
841
1022
|
return {
|
|
842
1023
|
data,
|
|
843
1024
|
successfulData,
|
package/dist/index.d.cts
CHANGED
|
@@ -139,7 +139,7 @@ interface UseApiOptions<T = unknown, D = unknown, TSelected = T> extends ApiRequ
|
|
|
139
139
|
* Compatible with `lazy: true` — focus is a browser trigger, not a reactive dep.
|
|
140
140
|
* Compatible with `poll` — both register separate listeners; `!loading` guard prevents duplicates.
|
|
141
141
|
*
|
|
142
|
-
* Can be set globally via `
|
|
142
|
+
* Can be set globally via `createApi({ axios, globalOptions: { refetchOnFocus: true } })`.
|
|
143
143
|
* Per-request value takes precedence over global (including `false` to opt-out).
|
|
144
144
|
*/
|
|
145
145
|
refetchOnFocus?: boolean | {
|
|
@@ -151,7 +151,7 @@ interface UseApiOptions<T = unknown, D = unknown, TSelected = T> extends ApiRequ
|
|
|
151
151
|
* No throttle is applied — reconnect is already a rare event.
|
|
152
152
|
* No refetch fires if a request is already in-flight (`loading: true`).
|
|
153
153
|
*
|
|
154
|
-
* Can be set globally via `
|
|
154
|
+
* Can be set globally via `createApi({ axios, globalOptions: { refetchOnReconnect: true } })`.
|
|
155
155
|
* Per-request value takes precedence over global (including `false` to opt-out).
|
|
156
156
|
*/
|
|
157
157
|
refetchOnReconnect?: boolean;
|
|
@@ -244,6 +244,8 @@ interface ApiPluginOptions {
|
|
|
244
244
|
* Useful if your backend has a different error structure.
|
|
245
245
|
*/
|
|
246
246
|
errorParser?: (error: unknown) => ApiError;
|
|
247
|
+
/** Devtools panel configuration. Panel is disabled by default. */
|
|
248
|
+
devtools?: DevtoolsOptions;
|
|
247
249
|
globalOptions?: {
|
|
248
250
|
retry?: number | boolean;
|
|
249
251
|
retryDelay?: number;
|
|
@@ -336,7 +338,34 @@ interface UseApiBatchOptions<T = unknown, D = unknown> extends Omit<ApiRequestCo
|
|
|
336
338
|
immediate?: boolean;
|
|
337
339
|
/** Skip individual error notifications */
|
|
338
340
|
skipErrorNotification?: boolean;
|
|
339
|
-
/**
|
|
341
|
+
/**
|
|
342
|
+
* Disable auto-tracking. When true, reactive changes to the `requests` getter
|
|
343
|
+
* will NOT trigger re-execution. Use when you want full manual control via execute().
|
|
344
|
+
* Default: false — auto-tracks when `requests` is a function.
|
|
345
|
+
*/
|
|
346
|
+
lazy?: boolean;
|
|
347
|
+
/**
|
|
348
|
+
* Polling interval in ms, or advanced config object.
|
|
349
|
+
* - Pass a number: `poll: 5000` — re-execute every 5 seconds.
|
|
350
|
+
* - Pass an object: `poll: { interval: 5000, whenHidden: false }` — skip polling when tab is hidden.
|
|
351
|
+
* Properties inside the object can also be Refs.
|
|
352
|
+
*/
|
|
353
|
+
poll?: MaybeRefOrGetter<number | {
|
|
354
|
+
interval: MaybeRefOrGetter<number>;
|
|
355
|
+
whenHidden?: MaybeRefOrGetter<boolean>;
|
|
356
|
+
}>;
|
|
357
|
+
/**
|
|
358
|
+
* @deprecated Use a reactive getter for `requests` with `lazy: false` (default).
|
|
359
|
+
* Auto-tracking will re-execute when the getter's dependencies change.
|
|
360
|
+
* Will be removed in v2.0.
|
|
361
|
+
*
|
|
362
|
+
* @example
|
|
363
|
+
* // Before (deprecated):
|
|
364
|
+
* useApiBatch(() => ids.value.map(id => `/items/${id}`), { watch: ids })
|
|
365
|
+
*
|
|
366
|
+
* // After (preferred):
|
|
367
|
+
* useApiBatch(() => ids.value.map(id => `/items/${id}`))
|
|
368
|
+
*/
|
|
340
369
|
watch?: WatchSource | WatchSource[];
|
|
341
370
|
/** Callback when a single request succeeds */
|
|
342
371
|
onItemSuccess?: (item: BatchResultItem<T>, index: number) => void;
|
|
@@ -370,6 +399,92 @@ interface UseApiBatchReturn<T = unknown> {
|
|
|
370
399
|
/** Reset state to initial */
|
|
371
400
|
reset: () => void;
|
|
372
401
|
}
|
|
402
|
+
/** Lifecycle status of an HTTP request tracked by devtools. */
|
|
403
|
+
type RequestStatus = "pending" | "success" | "error" | "aborted";
|
|
404
|
+
/** Current reactive state of a useApi instance as seen by devtools. */
|
|
405
|
+
interface DevtoolsInstanceState {
|
|
406
|
+
loading: boolean;
|
|
407
|
+
error: ApiError | null;
|
|
408
|
+
statusCode: number | null;
|
|
409
|
+
data: unknown;
|
|
410
|
+
}
|
|
411
|
+
/** Configuration options of a useApi instance as seen by devtools. */
|
|
412
|
+
interface DevtoolsInstanceOptions {
|
|
413
|
+
authMode: AuthMode;
|
|
414
|
+
cache: CacheOptions | string | undefined;
|
|
415
|
+
retry: boolean | number;
|
|
416
|
+
poll: number;
|
|
417
|
+
immediate: boolean;
|
|
418
|
+
lazy: boolean;
|
|
419
|
+
}
|
|
420
|
+
/** An outgoing HTTP request record sent to devtools on request start. */
|
|
421
|
+
interface DevtoolsRequestRecord {
|
|
422
|
+
id: string;
|
|
423
|
+
instanceId: string | null;
|
|
424
|
+
url: string;
|
|
425
|
+
method: string;
|
|
426
|
+
startedAt: number;
|
|
427
|
+
status: RequestStatus;
|
|
428
|
+
statusCode: null;
|
|
429
|
+
requestHeaders: Record<string, string>;
|
|
430
|
+
payload: unknown;
|
|
431
|
+
queryParams: unknown;
|
|
432
|
+
}
|
|
433
|
+
/** Result of a completed HTTP request, sent to devtools on request end. */
|
|
434
|
+
type RequestEndResult = {
|
|
435
|
+
status: "success";
|
|
436
|
+
statusCode: number;
|
|
437
|
+
response: unknown;
|
|
438
|
+
duration: number;
|
|
439
|
+
} | {
|
|
440
|
+
status: "error";
|
|
441
|
+
error: ApiError;
|
|
442
|
+
statusCode: number | null;
|
|
443
|
+
duration: number;
|
|
444
|
+
} | {
|
|
445
|
+
status: "aborted";
|
|
446
|
+
duration: number;
|
|
447
|
+
};
|
|
448
|
+
/** Event callbacks implemented by the devtools panel, called by useApi instrumentation. */
|
|
449
|
+
interface DevtoolsBridge {
|
|
450
|
+
/** Fired when a useApi instance is created. */
|
|
451
|
+
onInstanceCreated: (id: string, url: string | undefined, options: DevtoolsInstanceOptions) => void;
|
|
452
|
+
/** Fired when a useApi instance is destroyed (scope disposed). */
|
|
453
|
+
onInstanceDestroyed: (id: string) => void;
|
|
454
|
+
/** Fired when instance state (loading, error, statusCode, data) changes. */
|
|
455
|
+
onStateUpdate: (id: string, state: Partial<DevtoolsInstanceState>) => void;
|
|
456
|
+
/** Fired when an HTTP request starts. */
|
|
457
|
+
onRequestStart: (record: DevtoolsRequestRecord) => void;
|
|
458
|
+
/** Fired when an HTTP request completes (success, error, or abort). */
|
|
459
|
+
onRequestEnd: (id: string, result: RequestEndResult) => void;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Options for the `@ametie/vue-muza-devtools` panel.
|
|
463
|
+
*
|
|
464
|
+
* @example
|
|
465
|
+
* ```ts
|
|
466
|
+
* app.use(createApi({
|
|
467
|
+
* axios: apiClient,
|
|
468
|
+
* devtools: { enabled: process.env.NODE_ENV !== 'production' },
|
|
469
|
+
* }))
|
|
470
|
+
* ```
|
|
471
|
+
*/
|
|
472
|
+
interface DevtoolsOptions {
|
|
473
|
+
/** Enable the devtools panel. Default: false. */
|
|
474
|
+
enabled: boolean;
|
|
475
|
+
/** Maximum number of network requests kept in history. Default: 300. */
|
|
476
|
+
maxHistory?: number;
|
|
477
|
+
/** Maximum payload/response size in bytes before truncation. Default: 200_000. */
|
|
478
|
+
maxPayloadSize?: number;
|
|
479
|
+
/** Custom tabs appended after built-in tabs. */
|
|
480
|
+
tabs?: Array<{
|
|
481
|
+
id: string;
|
|
482
|
+
label: string;
|
|
483
|
+
component: unknown;
|
|
484
|
+
icon?: unknown;
|
|
485
|
+
order?: number;
|
|
486
|
+
}>;
|
|
487
|
+
}
|
|
373
488
|
|
|
374
489
|
declare function createApi(options: ApiPluginOptions): {
|
|
375
490
|
install(app: App): void;
|
|
@@ -448,7 +563,7 @@ declare function useApiDelete<T = unknown, TSelected = T>(url: MaybeRefOrGetter<
|
|
|
448
563
|
* - Abort support for all pending requests
|
|
449
564
|
* - Detailed per-request results with URL mapping
|
|
450
565
|
* - Progress tracking
|
|
451
|
-
* -
|
|
566
|
+
* - Auto-tracking for reactive getter requests (lazy: false default)
|
|
452
567
|
*
|
|
453
568
|
* @example
|
|
454
569
|
* ```ts
|
|
@@ -466,11 +581,10 @@ declare function useApiDelete<T = unknown, TSelected = T>(url: MaybeRefOrGetter<
|
|
|
466
581
|
* const ids = [1, 2, 3]
|
|
467
582
|
* useApiBatch(ids.map(id => ({ url: `/users/${id}`, method: 'DELETE' })))
|
|
468
583
|
*
|
|
469
|
-
* // Reactive getter
|
|
584
|
+
* // Reactive getter — auto-tracks deps, re-executes when pages changes
|
|
470
585
|
* const pages = ref([1, 2, 3])
|
|
471
586
|
* const { successfulData } = useApiBatch(
|
|
472
|
-
* () => pages.value.map(page => ({ url: '/users', params: { page } }))
|
|
473
|
-
* { watch: pages, immediate: true }
|
|
587
|
+
* () => pages.value.map(page => ({ url: '/users', params: { page } }))
|
|
474
588
|
* )
|
|
475
589
|
* ```
|
|
476
590
|
*/
|
|
@@ -756,4 +870,4 @@ declare function invalidateCache(id: string | string[]): void;
|
|
|
756
870
|
*/
|
|
757
871
|
declare function clearAllCache(): void;
|
|
758
872
|
|
|
759
|
-
export { type ApiError, type ApiPluginOptions, type ApiRequestConfig, type ApiState, type AuthEventPayload, AuthEventType, type AuthMode, type AuthMonitorFn, type AuthTokens$1 as AuthTokens, type BatchProgress, type BatchRequestConfig, type BatchResultItem, type CacheOptions, type SetDataInput, type UseApiBatchOptions, type UseApiBatchReturn, type UseApiOptions, type UseApiReturn, clearAllCache, createApi, createApiClient, invalidateCache, setAuthMonitor, setupInterceptors, tokenManager, useAbortController, useApi, useApiBatch, useApiConfig, useApiDelete, useApiGet, useApiPatch, useApiPost, useApiPut, useApiState };
|
|
873
|
+
export { type ApiError, type ApiPluginOptions, type ApiRequestConfig, type ApiState, type AuthEventPayload, AuthEventType, type AuthMode, type AuthMonitorFn, type AuthTokens$1 as AuthTokens, type BatchProgress, type BatchRequestConfig, type BatchResultItem, type CacheOptions, type DevtoolsBridge, type DevtoolsInstanceOptions, type DevtoolsInstanceState, type DevtoolsOptions, type DevtoolsRequestRecord, type RequestEndResult, type RequestStatus, type SetDataInput, type UseApiBatchOptions, type UseApiBatchReturn, type UseApiOptions, type UseApiReturn, clearAllCache, createApi, createApiClient, invalidateCache, setAuthMonitor, setupInterceptors, tokenManager, useAbortController, useApi, useApiBatch, useApiConfig, useApiDelete, useApiGet, useApiPatch, useApiPost, useApiPut, useApiState };
|
package/dist/index.d.ts
CHANGED
|
@@ -139,7 +139,7 @@ interface UseApiOptions<T = unknown, D = unknown, TSelected = T> extends ApiRequ
|
|
|
139
139
|
* Compatible with `lazy: true` — focus is a browser trigger, not a reactive dep.
|
|
140
140
|
* Compatible with `poll` — both register separate listeners; `!loading` guard prevents duplicates.
|
|
141
141
|
*
|
|
142
|
-
* Can be set globally via `
|
|
142
|
+
* Can be set globally via `createApi({ axios, globalOptions: { refetchOnFocus: true } })`.
|
|
143
143
|
* Per-request value takes precedence over global (including `false` to opt-out).
|
|
144
144
|
*/
|
|
145
145
|
refetchOnFocus?: boolean | {
|
|
@@ -151,7 +151,7 @@ interface UseApiOptions<T = unknown, D = unknown, TSelected = T> extends ApiRequ
|
|
|
151
151
|
* No throttle is applied — reconnect is already a rare event.
|
|
152
152
|
* No refetch fires if a request is already in-flight (`loading: true`).
|
|
153
153
|
*
|
|
154
|
-
* Can be set globally via `
|
|
154
|
+
* Can be set globally via `createApi({ axios, globalOptions: { refetchOnReconnect: true } })`.
|
|
155
155
|
* Per-request value takes precedence over global (including `false` to opt-out).
|
|
156
156
|
*/
|
|
157
157
|
refetchOnReconnect?: boolean;
|
|
@@ -244,6 +244,8 @@ interface ApiPluginOptions {
|
|
|
244
244
|
* Useful if your backend has a different error structure.
|
|
245
245
|
*/
|
|
246
246
|
errorParser?: (error: unknown) => ApiError;
|
|
247
|
+
/** Devtools panel configuration. Panel is disabled by default. */
|
|
248
|
+
devtools?: DevtoolsOptions;
|
|
247
249
|
globalOptions?: {
|
|
248
250
|
retry?: number | boolean;
|
|
249
251
|
retryDelay?: number;
|
|
@@ -336,7 +338,34 @@ interface UseApiBatchOptions<T = unknown, D = unknown> extends Omit<ApiRequestCo
|
|
|
336
338
|
immediate?: boolean;
|
|
337
339
|
/** Skip individual error notifications */
|
|
338
340
|
skipErrorNotification?: boolean;
|
|
339
|
-
/**
|
|
341
|
+
/**
|
|
342
|
+
* Disable auto-tracking. When true, reactive changes to the `requests` getter
|
|
343
|
+
* will NOT trigger re-execution. Use when you want full manual control via execute().
|
|
344
|
+
* Default: false — auto-tracks when `requests` is a function.
|
|
345
|
+
*/
|
|
346
|
+
lazy?: boolean;
|
|
347
|
+
/**
|
|
348
|
+
* Polling interval in ms, or advanced config object.
|
|
349
|
+
* - Pass a number: `poll: 5000` — re-execute every 5 seconds.
|
|
350
|
+
* - Pass an object: `poll: { interval: 5000, whenHidden: false }` — skip polling when tab is hidden.
|
|
351
|
+
* Properties inside the object can also be Refs.
|
|
352
|
+
*/
|
|
353
|
+
poll?: MaybeRefOrGetter<number | {
|
|
354
|
+
interval: MaybeRefOrGetter<number>;
|
|
355
|
+
whenHidden?: MaybeRefOrGetter<boolean>;
|
|
356
|
+
}>;
|
|
357
|
+
/**
|
|
358
|
+
* @deprecated Use a reactive getter for `requests` with `lazy: false` (default).
|
|
359
|
+
* Auto-tracking will re-execute when the getter's dependencies change.
|
|
360
|
+
* Will be removed in v2.0.
|
|
361
|
+
*
|
|
362
|
+
* @example
|
|
363
|
+
* // Before (deprecated):
|
|
364
|
+
* useApiBatch(() => ids.value.map(id => `/items/${id}`), { watch: ids })
|
|
365
|
+
*
|
|
366
|
+
* // After (preferred):
|
|
367
|
+
* useApiBatch(() => ids.value.map(id => `/items/${id}`))
|
|
368
|
+
*/
|
|
340
369
|
watch?: WatchSource | WatchSource[];
|
|
341
370
|
/** Callback when a single request succeeds */
|
|
342
371
|
onItemSuccess?: (item: BatchResultItem<T>, index: number) => void;
|
|
@@ -370,6 +399,92 @@ interface UseApiBatchReturn<T = unknown> {
|
|
|
370
399
|
/** Reset state to initial */
|
|
371
400
|
reset: () => void;
|
|
372
401
|
}
|
|
402
|
+
/** Lifecycle status of an HTTP request tracked by devtools. */
|
|
403
|
+
type RequestStatus = "pending" | "success" | "error" | "aborted";
|
|
404
|
+
/** Current reactive state of a useApi instance as seen by devtools. */
|
|
405
|
+
interface DevtoolsInstanceState {
|
|
406
|
+
loading: boolean;
|
|
407
|
+
error: ApiError | null;
|
|
408
|
+
statusCode: number | null;
|
|
409
|
+
data: unknown;
|
|
410
|
+
}
|
|
411
|
+
/** Configuration options of a useApi instance as seen by devtools. */
|
|
412
|
+
interface DevtoolsInstanceOptions {
|
|
413
|
+
authMode: AuthMode;
|
|
414
|
+
cache: CacheOptions | string | undefined;
|
|
415
|
+
retry: boolean | number;
|
|
416
|
+
poll: number;
|
|
417
|
+
immediate: boolean;
|
|
418
|
+
lazy: boolean;
|
|
419
|
+
}
|
|
420
|
+
/** An outgoing HTTP request record sent to devtools on request start. */
|
|
421
|
+
interface DevtoolsRequestRecord {
|
|
422
|
+
id: string;
|
|
423
|
+
instanceId: string | null;
|
|
424
|
+
url: string;
|
|
425
|
+
method: string;
|
|
426
|
+
startedAt: number;
|
|
427
|
+
status: RequestStatus;
|
|
428
|
+
statusCode: null;
|
|
429
|
+
requestHeaders: Record<string, string>;
|
|
430
|
+
payload: unknown;
|
|
431
|
+
queryParams: unknown;
|
|
432
|
+
}
|
|
433
|
+
/** Result of a completed HTTP request, sent to devtools on request end. */
|
|
434
|
+
type RequestEndResult = {
|
|
435
|
+
status: "success";
|
|
436
|
+
statusCode: number;
|
|
437
|
+
response: unknown;
|
|
438
|
+
duration: number;
|
|
439
|
+
} | {
|
|
440
|
+
status: "error";
|
|
441
|
+
error: ApiError;
|
|
442
|
+
statusCode: number | null;
|
|
443
|
+
duration: number;
|
|
444
|
+
} | {
|
|
445
|
+
status: "aborted";
|
|
446
|
+
duration: number;
|
|
447
|
+
};
|
|
448
|
+
/** Event callbacks implemented by the devtools panel, called by useApi instrumentation. */
|
|
449
|
+
interface DevtoolsBridge {
|
|
450
|
+
/** Fired when a useApi instance is created. */
|
|
451
|
+
onInstanceCreated: (id: string, url: string | undefined, options: DevtoolsInstanceOptions) => void;
|
|
452
|
+
/** Fired when a useApi instance is destroyed (scope disposed). */
|
|
453
|
+
onInstanceDestroyed: (id: string) => void;
|
|
454
|
+
/** Fired when instance state (loading, error, statusCode, data) changes. */
|
|
455
|
+
onStateUpdate: (id: string, state: Partial<DevtoolsInstanceState>) => void;
|
|
456
|
+
/** Fired when an HTTP request starts. */
|
|
457
|
+
onRequestStart: (record: DevtoolsRequestRecord) => void;
|
|
458
|
+
/** Fired when an HTTP request completes (success, error, or abort). */
|
|
459
|
+
onRequestEnd: (id: string, result: RequestEndResult) => void;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Options for the `@ametie/vue-muza-devtools` panel.
|
|
463
|
+
*
|
|
464
|
+
* @example
|
|
465
|
+
* ```ts
|
|
466
|
+
* app.use(createApi({
|
|
467
|
+
* axios: apiClient,
|
|
468
|
+
* devtools: { enabled: process.env.NODE_ENV !== 'production' },
|
|
469
|
+
* }))
|
|
470
|
+
* ```
|
|
471
|
+
*/
|
|
472
|
+
interface DevtoolsOptions {
|
|
473
|
+
/** Enable the devtools panel. Default: false. */
|
|
474
|
+
enabled: boolean;
|
|
475
|
+
/** Maximum number of network requests kept in history. Default: 300. */
|
|
476
|
+
maxHistory?: number;
|
|
477
|
+
/** Maximum payload/response size in bytes before truncation. Default: 200_000. */
|
|
478
|
+
maxPayloadSize?: number;
|
|
479
|
+
/** Custom tabs appended after built-in tabs. */
|
|
480
|
+
tabs?: Array<{
|
|
481
|
+
id: string;
|
|
482
|
+
label: string;
|
|
483
|
+
component: unknown;
|
|
484
|
+
icon?: unknown;
|
|
485
|
+
order?: number;
|
|
486
|
+
}>;
|
|
487
|
+
}
|
|
373
488
|
|
|
374
489
|
declare function createApi(options: ApiPluginOptions): {
|
|
375
490
|
install(app: App): void;
|
|
@@ -448,7 +563,7 @@ declare function useApiDelete<T = unknown, TSelected = T>(url: MaybeRefOrGetter<
|
|
|
448
563
|
* - Abort support for all pending requests
|
|
449
564
|
* - Detailed per-request results with URL mapping
|
|
450
565
|
* - Progress tracking
|
|
451
|
-
* -
|
|
566
|
+
* - Auto-tracking for reactive getter requests (lazy: false default)
|
|
452
567
|
*
|
|
453
568
|
* @example
|
|
454
569
|
* ```ts
|
|
@@ -466,11 +581,10 @@ declare function useApiDelete<T = unknown, TSelected = T>(url: MaybeRefOrGetter<
|
|
|
466
581
|
* const ids = [1, 2, 3]
|
|
467
582
|
* useApiBatch(ids.map(id => ({ url: `/users/${id}`, method: 'DELETE' })))
|
|
468
583
|
*
|
|
469
|
-
* // Reactive getter
|
|
584
|
+
* // Reactive getter — auto-tracks deps, re-executes when pages changes
|
|
470
585
|
* const pages = ref([1, 2, 3])
|
|
471
586
|
* const { successfulData } = useApiBatch(
|
|
472
|
-
* () => pages.value.map(page => ({ url: '/users', params: { page } }))
|
|
473
|
-
* { watch: pages, immediate: true }
|
|
587
|
+
* () => pages.value.map(page => ({ url: '/users', params: { page } }))
|
|
474
588
|
* )
|
|
475
589
|
* ```
|
|
476
590
|
*/
|
|
@@ -756,4 +870,4 @@ declare function invalidateCache(id: string | string[]): void;
|
|
|
756
870
|
*/
|
|
757
871
|
declare function clearAllCache(): void;
|
|
758
872
|
|
|
759
|
-
export { type ApiError, type ApiPluginOptions, type ApiRequestConfig, type ApiState, type AuthEventPayload, AuthEventType, type AuthMode, type AuthMonitorFn, type AuthTokens$1 as AuthTokens, type BatchProgress, type BatchRequestConfig, type BatchResultItem, type CacheOptions, type SetDataInput, type UseApiBatchOptions, type UseApiBatchReturn, type UseApiOptions, type UseApiReturn, clearAllCache, createApi, createApiClient, invalidateCache, setAuthMonitor, setupInterceptors, tokenManager, useAbortController, useApi, useApiBatch, useApiConfig, useApiDelete, useApiGet, useApiPatch, useApiPost, useApiPut, useApiState };
|
|
873
|
+
export { type ApiError, type ApiPluginOptions, type ApiRequestConfig, type ApiState, type AuthEventPayload, AuthEventType, type AuthMode, type AuthMonitorFn, type AuthTokens$1 as AuthTokens, type BatchProgress, type BatchRequestConfig, type BatchResultItem, type CacheOptions, type DevtoolsBridge, type DevtoolsInstanceOptions, type DevtoolsInstanceState, type DevtoolsOptions, type DevtoolsRequestRecord, type RequestEndResult, type RequestStatus, type SetDataInput, type UseApiBatchOptions, type UseApiBatchReturn, type UseApiOptions, type UseApiReturn, clearAllCache, createApi, createApiClient, invalidateCache, setAuthMonitor, setupInterceptors, tokenManager, useAbortController, useApi, useApiBatch, useApiConfig, useApiDelete, useApiGet, useApiPatch, useApiPost, useApiPut, useApiState };
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
// src/plugin.ts
|
|
2
2
|
import { inject } from "vue";
|
|
3
|
+
|
|
4
|
+
// src/devtools.ts
|
|
5
|
+
var bridge = null;
|
|
6
|
+
var requestCounter = 0;
|
|
7
|
+
var pendingCalls = [];
|
|
8
|
+
function nextRequestId() {
|
|
9
|
+
return `req_${++requestCounter}`;
|
|
10
|
+
}
|
|
11
|
+
async function initDevtools(options, app) {
|
|
12
|
+
if (!options.enabled) return;
|
|
13
|
+
try {
|
|
14
|
+
const { createBridge } = await import("@ametie/vue-muza-devtools");
|
|
15
|
+
bridge = createBridge(options, app);
|
|
16
|
+
for (const fn of pendingCalls) fn();
|
|
17
|
+
pendingCalls.length = 0;
|
|
18
|
+
} catch {
|
|
19
|
+
console.warn("[vue-muza-use] devtools enabled but @ametie/vue-muza-devtools is not installed");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
var devtoolsBridge = {
|
|
23
|
+
onInstanceCreated(id, url, options) {
|
|
24
|
+
if (bridge) {
|
|
25
|
+
bridge.onInstanceCreated(id, url, options);
|
|
26
|
+
} else {
|
|
27
|
+
pendingCalls.push(() => bridge?.onInstanceCreated(id, url, options));
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
onInstanceDestroyed(id) {
|
|
31
|
+
bridge?.onInstanceDestroyed(id);
|
|
32
|
+
},
|
|
33
|
+
onStateUpdate(id, state) {
|
|
34
|
+
bridge?.onStateUpdate(id, state);
|
|
35
|
+
},
|
|
36
|
+
onRequestStart(record) {
|
|
37
|
+
bridge?.onRequestStart(record);
|
|
38
|
+
},
|
|
39
|
+
onRequestEnd(id, result) {
|
|
40
|
+
bridge?.onRequestEnd(id, result);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// src/plugin.ts
|
|
3
45
|
var API_INJECTION_KEY = /* @__PURE__ */ Symbol("use-api-config");
|
|
4
46
|
var globalConfig = null;
|
|
5
47
|
function createApi(options) {
|
|
@@ -7,6 +49,9 @@ function createApi(options) {
|
|
|
7
49
|
return {
|
|
8
50
|
install(app) {
|
|
9
51
|
app.provide(API_INJECTION_KEY, options);
|
|
52
|
+
if (options.devtools) {
|
|
53
|
+
void initDevtools(options.devtools, app);
|
|
54
|
+
}
|
|
10
55
|
}
|
|
11
56
|
};
|
|
12
57
|
}
|
|
@@ -57,9 +102,21 @@ function debounceFn(fn, delay) {
|
|
|
57
102
|
};
|
|
58
103
|
}
|
|
59
104
|
|
|
105
|
+
// src/utils/urlUtils.ts
|
|
106
|
+
function parseUrlQueryParams(url) {
|
|
107
|
+
if (!url) return null;
|
|
108
|
+
const qIndex = url.indexOf("?");
|
|
109
|
+
if (qIndex === -1) return null;
|
|
110
|
+
const params = {};
|
|
111
|
+
for (const [k, v] of new URLSearchParams(url.slice(qIndex + 1))) {
|
|
112
|
+
params[k] = v;
|
|
113
|
+
}
|
|
114
|
+
return Object.keys(params).length > 0 ? params : null;
|
|
115
|
+
}
|
|
116
|
+
|
|
60
117
|
// src/useApi.ts
|
|
61
118
|
import { isAxiosError as isAxiosError2 } from "axios";
|
|
62
|
-
import { ref as ref3, computed, effectScope, getCurrentScope as getCurrentScope2, onScopeDispose as onScopeDispose2, toValue, watch } from "vue";
|
|
119
|
+
import { ref as ref3, computed, effectScope, getCurrentInstance, getCurrentScope as getCurrentScope2, onScopeDispose as onScopeDispose2, toValue, watch, useId } from "vue";
|
|
63
120
|
|
|
64
121
|
// src/utils/errorParser.ts
|
|
65
122
|
import { isAxiosError } from "axios";
|
|
@@ -286,6 +343,30 @@ function useApi(url, options = {}) {
|
|
|
286
343
|
const startLoading = initialLoading ?? immediate;
|
|
287
344
|
const state = useApiState(initialData, { initialLoading: startLoading });
|
|
288
345
|
const revalidating = ref3(false);
|
|
346
|
+
const instanceId = getCurrentInstance() != null ? useId() : nextRequestId();
|
|
347
|
+
devtoolsBridge.onInstanceCreated(instanceId, toValue(url), {
|
|
348
|
+
authMode: options.authMode ?? "default",
|
|
349
|
+
cache: options.cache,
|
|
350
|
+
retry: options.retry ?? false,
|
|
351
|
+
poll: (() => {
|
|
352
|
+
const v = toValue(options.poll);
|
|
353
|
+
return typeof v === "number" ? v : 0;
|
|
354
|
+
})(),
|
|
355
|
+
immediate: options.immediate ?? false,
|
|
356
|
+
lazy: options.lazy ?? false
|
|
357
|
+
});
|
|
358
|
+
if (getCurrentScope2()) {
|
|
359
|
+
watch(
|
|
360
|
+
() => ({
|
|
361
|
+
loading: state.loading.value,
|
|
362
|
+
error: state.error.value,
|
|
363
|
+
statusCode: state.statusCode.value,
|
|
364
|
+
data: state.data.value
|
|
365
|
+
}),
|
|
366
|
+
(s) => devtoolsBridge.onStateUpdate(instanceId, s),
|
|
367
|
+
{ deep: true }
|
|
368
|
+
);
|
|
369
|
+
}
|
|
289
370
|
const abortController2 = ref3(null);
|
|
290
371
|
const globalAbort = useGlobalAbort ? useAbortController() : null;
|
|
291
372
|
let pollTimer = null;
|
|
@@ -321,6 +402,16 @@ function useApi(url, options = {}) {
|
|
|
321
402
|
if (abortController2.value) abortController2.value.abort("Cancelled by new request");
|
|
322
403
|
const controller = new AbortController();
|
|
323
404
|
abortController2.value = controller;
|
|
405
|
+
if (config?.signal) {
|
|
406
|
+
const signal = config.signal;
|
|
407
|
+
if (signal.aborted) {
|
|
408
|
+
controller.abort(signal.reason);
|
|
409
|
+
} else {
|
|
410
|
+
signal.addEventListener("abort", () => {
|
|
411
|
+
controller.abort(signal.reason);
|
|
412
|
+
}, { once: true });
|
|
413
|
+
}
|
|
414
|
+
}
|
|
324
415
|
let globalAbortHandler = null;
|
|
325
416
|
let subscribedSignal = null;
|
|
326
417
|
if (globalAbort) {
|
|
@@ -340,6 +431,9 @@ function useApi(url, options = {}) {
|
|
|
340
431
|
state.setError(null);
|
|
341
432
|
let wasCancelled = false;
|
|
342
433
|
let retryCount = 0;
|
|
434
|
+
let devtoolsRequestId = null;
|
|
435
|
+
let devtoolsRequestStartedAt = 0;
|
|
436
|
+
let devtoolsRequestEndResult = null;
|
|
343
437
|
try {
|
|
344
438
|
if (!requestUrl) {
|
|
345
439
|
throw new Error("Request URL is missing");
|
|
@@ -348,6 +442,21 @@ function useApi(url, options = {}) {
|
|
|
348
442
|
const resolvedData = toValue(rawData);
|
|
349
443
|
const rawParams = config?.params !== void 0 ? config.params : axiosConfig.params;
|
|
350
444
|
const resolvedParams = toValue(rawParams);
|
|
445
|
+
const devtoolsQueryParams = resolvedParams ?? parseUrlQueryParams(requestUrl);
|
|
446
|
+
devtoolsRequestId = nextRequestId();
|
|
447
|
+
devtoolsRequestStartedAt = Date.now();
|
|
448
|
+
devtoolsBridge.onRequestStart({
|
|
449
|
+
id: devtoolsRequestId,
|
|
450
|
+
instanceId,
|
|
451
|
+
url: requestUrl,
|
|
452
|
+
method,
|
|
453
|
+
startedAt: devtoolsRequestStartedAt,
|
|
454
|
+
status: "pending",
|
|
455
|
+
statusCode: null,
|
|
456
|
+
requestHeaders: {},
|
|
457
|
+
payload: resolvedData ?? null,
|
|
458
|
+
queryParams: devtoolsQueryParams
|
|
459
|
+
});
|
|
351
460
|
while (true) {
|
|
352
461
|
try {
|
|
353
462
|
const response = await axios2.request({
|
|
@@ -371,6 +480,12 @@ function useApi(url, options = {}) {
|
|
|
371
480
|
}
|
|
372
481
|
onSuccess?.(response);
|
|
373
482
|
notifyFetched();
|
|
483
|
+
devtoolsRequestEndResult = {
|
|
484
|
+
status: "success",
|
|
485
|
+
statusCode: response.status,
|
|
486
|
+
response: response.data,
|
|
487
|
+
duration: Date.now() - devtoolsRequestStartedAt
|
|
488
|
+
};
|
|
374
489
|
return selected;
|
|
375
490
|
} catch (err) {
|
|
376
491
|
if (controller.signal.aborted || isAxiosError2(err) && err.code === "ERR_CANCELED") {
|
|
@@ -389,6 +504,12 @@ function useApi(url, options = {}) {
|
|
|
389
504
|
}
|
|
390
505
|
continue;
|
|
391
506
|
}
|
|
507
|
+
devtoolsRequestEndResult = {
|
|
508
|
+
status: "error",
|
|
509
|
+
error: apiError,
|
|
510
|
+
statusCode: apiError.status ?? null,
|
|
511
|
+
duration: Date.now() - devtoolsRequestStartedAt
|
|
512
|
+
};
|
|
392
513
|
if (!skipErrorNotification && globalErrorHandler) {
|
|
393
514
|
globalErrorHandler(apiError, err);
|
|
394
515
|
}
|
|
@@ -404,6 +525,12 @@ function useApi(url, options = {}) {
|
|
|
404
525
|
return null;
|
|
405
526
|
}
|
|
406
527
|
const apiError = errorParser ? errorParser(err) : parseApiError(err);
|
|
528
|
+
devtoolsRequestEndResult = {
|
|
529
|
+
status: "error",
|
|
530
|
+
error: apiError,
|
|
531
|
+
statusCode: null,
|
|
532
|
+
duration: Date.now() - devtoolsRequestStartedAt
|
|
533
|
+
};
|
|
407
534
|
if (!skipErrorNotification && globalErrorHandler) {
|
|
408
535
|
globalErrorHandler(apiError, err);
|
|
409
536
|
}
|
|
@@ -412,6 +539,12 @@ function useApi(url, options = {}) {
|
|
|
412
539
|
onError?.(apiError);
|
|
413
540
|
return null;
|
|
414
541
|
} finally {
|
|
542
|
+
if (devtoolsRequestId !== null) {
|
|
543
|
+
devtoolsBridge.onRequestEnd(
|
|
544
|
+
devtoolsRequestId,
|
|
545
|
+
devtoolsRequestEndResult ?? { status: "aborted", duration: Date.now() - devtoolsRequestStartedAt }
|
|
546
|
+
);
|
|
547
|
+
}
|
|
415
548
|
if (globalAbortHandler && subscribedSignal) subscribedSignal.removeEventListener("abort", globalAbortHandler);
|
|
416
549
|
revalidating.value = false;
|
|
417
550
|
if (!wasCancelled) {
|
|
@@ -488,7 +621,10 @@ function useApi(url, options = {}) {
|
|
|
488
621
|
}
|
|
489
622
|
};
|
|
490
623
|
if (getCurrentScope2()) {
|
|
491
|
-
onScopeDispose2(() =>
|
|
624
|
+
onScopeDispose2(() => {
|
|
625
|
+
abort("Scope disposed");
|
|
626
|
+
devtoolsBridge.onInstanceDestroyed(instanceId);
|
|
627
|
+
});
|
|
492
628
|
}
|
|
493
629
|
if (immediate) execute();
|
|
494
630
|
if (typeof document !== "undefined") {
|
|
@@ -555,6 +691,8 @@ function useApiBatch(requests, options = {}) {
|
|
|
555
691
|
concurrency,
|
|
556
692
|
immediate = false,
|
|
557
693
|
skipErrorNotification = true,
|
|
694
|
+
lazy = false,
|
|
695
|
+
poll = 0,
|
|
558
696
|
watch: watchSource,
|
|
559
697
|
onItemSuccess,
|
|
560
698
|
onItemError,
|
|
@@ -579,13 +717,21 @@ function useApiBatch(requests, options = {}) {
|
|
|
579
717
|
);
|
|
580
718
|
const abortControllers = ref4([]);
|
|
581
719
|
let isAborted = false;
|
|
582
|
-
|
|
583
|
-
|
|
720
|
+
let pollTimer = null;
|
|
721
|
+
const getPollConfig = () => {
|
|
722
|
+
const val = toValue2(poll);
|
|
723
|
+
if (typeof val === "number") return { interval: val, whenHidden: false };
|
|
724
|
+
return {
|
|
725
|
+
interval: toValue2(val.interval),
|
|
726
|
+
whenHidden: toValue2(val.whenHidden) ?? false
|
|
727
|
+
};
|
|
728
|
+
};
|
|
729
|
+
const updateProgress = (succeeded, failed, total) => {
|
|
584
730
|
const completed = succeeded + failed;
|
|
585
731
|
const newProgress = {
|
|
586
732
|
completed,
|
|
587
|
-
total
|
|
588
|
-
percentage:
|
|
733
|
+
total,
|
|
734
|
+
percentage: total > 0 ? Math.round(completed / total * 100) : 0,
|
|
589
735
|
succeeded,
|
|
590
736
|
failed
|
|
591
737
|
};
|
|
@@ -602,7 +748,8 @@ function useApiBatch(requests, options = {}) {
|
|
|
602
748
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
603
749
|
...config.headers && { headers: config.headers },
|
|
604
750
|
useGlobalAbort: false,
|
|
605
|
-
skipErrorNotification
|
|
751
|
+
skipErrorNotification,
|
|
752
|
+
lazy: true
|
|
606
753
|
}));
|
|
607
754
|
const { execute: execute2, error: reqError, statusCode, response } = api;
|
|
608
755
|
try {
|
|
@@ -657,7 +804,7 @@ function useApiBatch(requests, options = {}) {
|
|
|
657
804
|
scope.stop();
|
|
658
805
|
}
|
|
659
806
|
};
|
|
660
|
-
const executeWithConcurrency = async (requests2, limit) => {
|
|
807
|
+
const executeWithConcurrency = async (requests2, limit, total) => {
|
|
661
808
|
const results = new Array(requests2.length);
|
|
662
809
|
let succeededCount = 0;
|
|
663
810
|
let failedCount = 0;
|
|
@@ -675,8 +822,9 @@ function useApiBatch(requests, options = {}) {
|
|
|
675
822
|
errors.value.push(result.error);
|
|
676
823
|
}
|
|
677
824
|
}
|
|
678
|
-
updateProgress(succeededCount, failedCount);
|
|
825
|
+
updateProgress(succeededCount, failedCount, total);
|
|
679
826
|
if (!settled && !result.success && result.error) {
|
|
827
|
+
abort("First request failed in non-settled mode");
|
|
680
828
|
throw result.error;
|
|
681
829
|
}
|
|
682
830
|
return result;
|
|
@@ -705,7 +853,7 @@ function useApiBatch(requests, options = {}) {
|
|
|
705
853
|
errors.value.push(result.error);
|
|
706
854
|
}
|
|
707
855
|
}
|
|
708
|
-
updateProgress(succeededCount, failedCount);
|
|
856
|
+
updateProgress(succeededCount, failedCount, total);
|
|
709
857
|
if (!settled && !result.success && result.error) {
|
|
710
858
|
abort("First request failed in non-settled mode");
|
|
711
859
|
throw result.error;
|
|
@@ -722,6 +870,9 @@ function useApiBatch(requests, options = {}) {
|
|
|
722
870
|
return results;
|
|
723
871
|
};
|
|
724
872
|
const execute = async () => {
|
|
873
|
+
if (loading.value) {
|
|
874
|
+
abort("Replaced by new execution");
|
|
875
|
+
}
|
|
725
876
|
const currentRequests = getRequests();
|
|
726
877
|
isAborted = false;
|
|
727
878
|
loading.value = true;
|
|
@@ -729,20 +880,21 @@ function useApiBatch(requests, options = {}) {
|
|
|
729
880
|
errors.value = [];
|
|
730
881
|
data.value = [];
|
|
731
882
|
abortControllers.value = [];
|
|
732
|
-
|
|
883
|
+
const total = currentRequests.length;
|
|
884
|
+
updateProgress(0, 0, total);
|
|
885
|
+
let finalResults = [];
|
|
733
886
|
try {
|
|
734
|
-
|
|
735
|
-
data.value =
|
|
736
|
-
const allFailed =
|
|
737
|
-
if (allFailed &&
|
|
887
|
+
finalResults = await executeWithConcurrency(currentRequests, concurrency, total);
|
|
888
|
+
data.value = finalResults;
|
|
889
|
+
const allFailed = finalResults.every((r) => !r.success);
|
|
890
|
+
if (allFailed && finalResults.length > 0) {
|
|
738
891
|
error.value = {
|
|
739
|
-
message: `All ${
|
|
892
|
+
message: `All ${finalResults.length} requests failed`,
|
|
740
893
|
status: 0,
|
|
741
894
|
code: "BATCH_ALL_FAILED"
|
|
742
895
|
};
|
|
743
896
|
}
|
|
744
|
-
|
|
745
|
-
return results;
|
|
897
|
+
return finalResults;
|
|
746
898
|
} catch (err) {
|
|
747
899
|
if (!settled) {
|
|
748
900
|
error.value = err;
|
|
@@ -751,10 +903,27 @@ function useApiBatch(requests, options = {}) {
|
|
|
751
903
|
} finally {
|
|
752
904
|
loading.value = false;
|
|
753
905
|
abortControllers.value = [];
|
|
906
|
+
onFinish?.(finalResults);
|
|
907
|
+
if (!isAborted) {
|
|
908
|
+
const { interval, whenHidden } = getPollConfig();
|
|
909
|
+
if (interval > 0) {
|
|
910
|
+
const hidden = typeof document !== "undefined" && document.hidden;
|
|
911
|
+
if (whenHidden || !hidden) {
|
|
912
|
+
pollTimer = setTimeout(() => {
|
|
913
|
+
pollTimer = null;
|
|
914
|
+
execute();
|
|
915
|
+
}, interval);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
754
919
|
}
|
|
755
920
|
};
|
|
756
921
|
const abort = (message = "Batch aborted") => {
|
|
757
922
|
isAborted = true;
|
|
923
|
+
if (pollTimer) {
|
|
924
|
+
clearTimeout(pollTimer);
|
|
925
|
+
pollTimer = null;
|
|
926
|
+
}
|
|
758
927
|
for (const controller of abortControllers.value) {
|
|
759
928
|
controller.abort(message);
|
|
760
929
|
}
|
|
@@ -777,14 +946,26 @@ function useApiBatch(requests, options = {}) {
|
|
|
777
946
|
if (getCurrentScope3()) {
|
|
778
947
|
onScopeDispose3(() => abort("Scope disposed"));
|
|
779
948
|
}
|
|
949
|
+
if (!lazy && typeof requests === "function") {
|
|
950
|
+
const trackingScope = effectScope2();
|
|
951
|
+
trackingScope.run(() => {
|
|
952
|
+
const requestsComputed = computed2(
|
|
953
|
+
() => requests().map(normalizeRequest)
|
|
954
|
+
);
|
|
955
|
+
watch2(requestsComputed, () => {
|
|
956
|
+
execute();
|
|
957
|
+
}, { deep: true });
|
|
958
|
+
});
|
|
959
|
+
if (getCurrentScope3()) onScopeDispose3(() => trackingScope.stop());
|
|
960
|
+
execute();
|
|
961
|
+
} else if (immediate) {
|
|
962
|
+
execute();
|
|
963
|
+
}
|
|
780
964
|
if (watchSource) {
|
|
781
965
|
watch2(watchSource, () => {
|
|
782
966
|
execute();
|
|
783
967
|
}, { deep: true });
|
|
784
968
|
}
|
|
785
|
-
if (immediate) {
|
|
786
|
-
execute();
|
|
787
|
-
}
|
|
788
969
|
return {
|
|
789
970
|
data,
|
|
790
971
|
successfulData,
|