@ametie/vue-muza-use 1.1.1 → 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 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, DevTools, or advanced server-state normalization.
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)
@@ -1405,6 +1409,77 @@ const { loading, progress, execute } = useApiBatch(urls, {
1405
1409
  </template>
1406
1410
  ```
1407
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
+
1408
1483
  ---
1409
1484
 
1410
1485
  ## ⚙️ Advanced Configuration
@@ -1496,6 +1571,44 @@ createApp(App).use(createApi({
1496
1571
 
1497
1572
  ---
1498
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
+
1499
1612
  ## 🔐 Authentication & Token Management
1500
1613
 
1501
1614
  > **Note:** Authentication setup is optional. Only add this if your API requires JWT tokens.
@@ -2141,15 +2254,17 @@ type BatchInput = string | BatchRequestConfig
2141
2254
 
2142
2255
  | Option | Type | Default | Description |
2143
2256
  |--------|------|---------|-------------|
2144
- | `settled` | `boolean` | `true` | When `true`, all requests run even if some fail. When `false`, the first error stops the batch |
2257
+ | `settled` | `boolean` | `true` | When `true`, all requests run even if some fail. When `false`, the first error aborts remaining requests |
2145
2258
  | `concurrency` | `number` | unlimited | Maximum number of requests that run in parallel at once |
2146
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 |
2147
2262
  | `skipErrorNotification` | `boolean` | `true` | Suppress global error handler for individual item failures |
2148
- | `watch` | `WatchSource \| WatchSource[]` | `undefined` | Re-execute the batch when these sources change |
2263
+ | `watch` | `WatchSource \| WatchSource[]` | `undefined` | **Deprecated** use a reactive getter with `lazy: false` instead. Will be removed in v2.0 |
2149
2264
  | `onItemSuccess` | `(item: BatchResultItem<T>, index: number) => void` | `undefined` | Called each time a single request in the batch succeeds |
2150
2265
  | `onItemError` | `(item: BatchResultItem<T>, index: number) => void` | `undefined` | Called each time a single request in the batch fails |
2151
2266
  | `onProgress` | `(progress: BatchProgress) => void` | `undefined` | Called after each request completes with updated progress |
2152
- | `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) |
2153
2268
 
2154
2269
  **UseApiBatchReturn:**
2155
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;
@@ -403,6 +484,9 @@ function useApi(url, options = {}) {
403
484
  state.setError(null);
404
485
  let wasCancelled = false;
405
486
  let retryCount = 0;
487
+ let devtoolsRequestId = null;
488
+ let devtoolsRequestStartedAt = 0;
489
+ let devtoolsRequestEndResult = null;
406
490
  try {
407
491
  if (!requestUrl) {
408
492
  throw new Error("Request URL is missing");
@@ -411,6 +495,21 @@ function useApi(url, options = {}) {
411
495
  const resolvedData = (0, import_vue5.toValue)(rawData);
412
496
  const rawParams = config?.params !== void 0 ? config.params : axiosConfig.params;
413
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
+ });
414
513
  while (true) {
415
514
  try {
416
515
  const response = await axios2.request({
@@ -434,6 +533,12 @@ function useApi(url, options = {}) {
434
533
  }
435
534
  onSuccess?.(response);
436
535
  notifyFetched();
536
+ devtoolsRequestEndResult = {
537
+ status: "success",
538
+ statusCode: response.status,
539
+ response: response.data,
540
+ duration: Date.now() - devtoolsRequestStartedAt
541
+ };
437
542
  return selected;
438
543
  } catch (err) {
439
544
  if (controller.signal.aborted || (0, import_axios2.isAxiosError)(err) && err.code === "ERR_CANCELED") {
@@ -452,6 +557,12 @@ function useApi(url, options = {}) {
452
557
  }
453
558
  continue;
454
559
  }
560
+ devtoolsRequestEndResult = {
561
+ status: "error",
562
+ error: apiError,
563
+ statusCode: apiError.status ?? null,
564
+ duration: Date.now() - devtoolsRequestStartedAt
565
+ };
455
566
  if (!skipErrorNotification && globalErrorHandler) {
456
567
  globalErrorHandler(apiError, err);
457
568
  }
@@ -467,6 +578,12 @@ function useApi(url, options = {}) {
467
578
  return null;
468
579
  }
469
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
+ };
470
587
  if (!skipErrorNotification && globalErrorHandler) {
471
588
  globalErrorHandler(apiError, err);
472
589
  }
@@ -475,6 +592,12 @@ function useApi(url, options = {}) {
475
592
  onError?.(apiError);
476
593
  return null;
477
594
  } finally {
595
+ if (devtoolsRequestId !== null) {
596
+ devtoolsBridge.onRequestEnd(
597
+ devtoolsRequestId,
598
+ devtoolsRequestEndResult ?? { status: "aborted", duration: Date.now() - devtoolsRequestStartedAt }
599
+ );
600
+ }
478
601
  if (globalAbortHandler && subscribedSignal) subscribedSignal.removeEventListener("abort", globalAbortHandler);
479
602
  revalidating.value = false;
480
603
  if (!wasCancelled) {
@@ -551,7 +674,10 @@ function useApi(url, options = {}) {
551
674
  }
552
675
  };
553
676
  if ((0, import_vue5.getCurrentScope)()) {
554
- (0, import_vue5.onScopeDispose)(() => abort("Scope disposed"));
677
+ (0, import_vue5.onScopeDispose)(() => {
678
+ abort("Scope disposed");
679
+ devtoolsBridge.onInstanceDestroyed(instanceId);
680
+ });
555
681
  }
556
682
  if (immediate) execute();
557
683
  if (typeof document !== "undefined") {
package/dist/index.d.cts CHANGED
@@ -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;
@@ -397,6 +399,92 @@ interface UseApiBatchReturn<T = unknown> {
397
399
  /** Reset state to initial */
398
400
  reset: () => void;
399
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
+ }
400
488
 
401
489
  declare function createApi(options: ApiPluginOptions): {
402
490
  install(app: App): void;
@@ -782,4 +870,4 @@ declare function invalidateCache(id: string | string[]): void;
782
870
  */
783
871
  declare function clearAllCache(): void;
784
872
 
785
- 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
@@ -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;
@@ -397,6 +399,92 @@ interface UseApiBatchReturn<T = unknown> {
397
399
  /** Reset state to initial */
398
400
  reset: () => void;
399
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
+ }
400
488
 
401
489
  declare function createApi(options: ApiPluginOptions): {
402
490
  install(app: App): void;
@@ -782,4 +870,4 @@ declare function invalidateCache(id: string | string[]): void;
782
870
  */
783
871
  declare function clearAllCache(): void;
784
872
 
785
- 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;
@@ -350,6 +431,9 @@ function useApi(url, options = {}) {
350
431
  state.setError(null);
351
432
  let wasCancelled = false;
352
433
  let retryCount = 0;
434
+ let devtoolsRequestId = null;
435
+ let devtoolsRequestStartedAt = 0;
436
+ let devtoolsRequestEndResult = null;
353
437
  try {
354
438
  if (!requestUrl) {
355
439
  throw new Error("Request URL is missing");
@@ -358,6 +442,21 @@ function useApi(url, options = {}) {
358
442
  const resolvedData = toValue(rawData);
359
443
  const rawParams = config?.params !== void 0 ? config.params : axiosConfig.params;
360
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
+ });
361
460
  while (true) {
362
461
  try {
363
462
  const response = await axios2.request({
@@ -381,6 +480,12 @@ function useApi(url, options = {}) {
381
480
  }
382
481
  onSuccess?.(response);
383
482
  notifyFetched();
483
+ devtoolsRequestEndResult = {
484
+ status: "success",
485
+ statusCode: response.status,
486
+ response: response.data,
487
+ duration: Date.now() - devtoolsRequestStartedAt
488
+ };
384
489
  return selected;
385
490
  } catch (err) {
386
491
  if (controller.signal.aborted || isAxiosError2(err) && err.code === "ERR_CANCELED") {
@@ -399,6 +504,12 @@ function useApi(url, options = {}) {
399
504
  }
400
505
  continue;
401
506
  }
507
+ devtoolsRequestEndResult = {
508
+ status: "error",
509
+ error: apiError,
510
+ statusCode: apiError.status ?? null,
511
+ duration: Date.now() - devtoolsRequestStartedAt
512
+ };
402
513
  if (!skipErrorNotification && globalErrorHandler) {
403
514
  globalErrorHandler(apiError, err);
404
515
  }
@@ -414,6 +525,12 @@ function useApi(url, options = {}) {
414
525
  return null;
415
526
  }
416
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
+ };
417
534
  if (!skipErrorNotification && globalErrorHandler) {
418
535
  globalErrorHandler(apiError, err);
419
536
  }
@@ -422,6 +539,12 @@ function useApi(url, options = {}) {
422
539
  onError?.(apiError);
423
540
  return null;
424
541
  } finally {
542
+ if (devtoolsRequestId !== null) {
543
+ devtoolsBridge.onRequestEnd(
544
+ devtoolsRequestId,
545
+ devtoolsRequestEndResult ?? { status: "aborted", duration: Date.now() - devtoolsRequestStartedAt }
546
+ );
547
+ }
425
548
  if (globalAbortHandler && subscribedSignal) subscribedSignal.removeEventListener("abort", globalAbortHandler);
426
549
  revalidating.value = false;
427
550
  if (!wasCancelled) {
@@ -498,7 +621,10 @@ function useApi(url, options = {}) {
498
621
  }
499
622
  };
500
623
  if (getCurrentScope2()) {
501
- onScopeDispose2(() => abort("Scope disposed"));
624
+ onScopeDispose2(() => {
625
+ abort("Scope disposed");
626
+ devtoolsBridge.onInstanceDestroyed(instanceId);
627
+ });
502
628
  }
503
629
  if (immediate) execute();
504
630
  if (typeof document !== "undefined") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ametie/vue-muza-use",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Powerful Vue 3 API composable (Muza) with Axios, Auto-Refresh & TypeScript",
5
5
  "author": "MortyQ",
6
6
  "license": "MIT",