@ametie/vue-muza-use 0.6.1 → 0.7.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/dist/index.cjs CHANGED
@@ -75,17 +75,34 @@ function useApiConfig() {
75
75
  }
76
76
 
77
77
  // src/utils/debounce.ts
78
+ var DebounceCancelledError = class extends Error {
79
+ isDebounceCancelled = true;
80
+ constructor() {
81
+ super("Debounced call was superseded by a newer call");
82
+ this.name = "DebounceCancelledError";
83
+ }
84
+ };
78
85
  function debounceFn(fn, delay) {
79
86
  let timeoutId = null;
87
+ let pendingReject = null;
80
88
  return function(...args) {
81
- return new Promise((resolve) => {
82
- if (timeoutId) {
83
- clearTimeout(timeoutId);
84
- }
89
+ if (pendingReject) {
90
+ pendingReject(new DebounceCancelledError());
91
+ pendingReject = null;
92
+ }
93
+ if (timeoutId) {
94
+ clearTimeout(timeoutId);
95
+ }
96
+ return new Promise((resolve, reject) => {
97
+ pendingReject = reject;
85
98
  timeoutId = setTimeout(async () => {
86
- const result = await fn(...args);
87
- resolve(result);
99
+ pendingReject = null;
88
100
  timeoutId = null;
101
+ try {
102
+ resolve(await fn(...args));
103
+ } catch (err) {
104
+ reject(err);
105
+ }
89
106
  }, delay);
90
107
  });
91
108
  };
@@ -196,6 +213,26 @@ function useAbortController() {
196
213
  }
197
214
 
198
215
  // src/useApi.ts
216
+ var DEFAULT_RETRY_STATUS_CODES = [408, 429, 500, 502, 503, 504];
217
+ function cancellableSleep(ms, signal) {
218
+ return new Promise((resolve) => {
219
+ if (signal.aborted) {
220
+ resolve(true);
221
+ return;
222
+ }
223
+ const timer = setTimeout(() => {
224
+ cleanup();
225
+ resolve(false);
226
+ }, ms);
227
+ const onAbort = () => {
228
+ clearTimeout(timer);
229
+ cleanup();
230
+ resolve(true);
231
+ };
232
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
233
+ signal.addEventListener("abort", onAbort, { once: true });
234
+ });
235
+ }
199
236
  function useApi(url, options = {}) {
200
237
  const { axios: axios2, onError: globalErrorHandler, globalOptions, errorParser } = useApiConfig();
201
238
  const {
@@ -210,12 +247,14 @@ function useApi(url, options = {}) {
210
247
  skipErrorNotification = false,
211
248
  retry = globalOptions?.retry ?? false,
212
249
  retryDelay = globalOptions?.retryDelay ?? 1e3,
250
+ retryStatusCodes = globalOptions?.retryStatusCodes ?? DEFAULT_RETRY_STATUS_CODES,
213
251
  authMode = "default",
214
252
  useGlobalAbort = globalOptions?.useGlobalAbort ?? true,
215
253
  initialLoading = false,
216
254
  poll = 0,
217
255
  ...axiosConfig
218
256
  } = options;
257
+ const maxRetries = retry === false ? 0 : retry === true ? 3 : retry;
219
258
  const startLoading = initialLoading ?? immediate;
220
259
  const state = useApiState(initialData, { initialLoading: startLoading });
221
260
  const abortController2 = (0, import_vue4.ref)(null);
@@ -255,28 +294,57 @@ function useApi(url, options = {}) {
255
294
  state.setLoading(true);
256
295
  state.setError(null);
257
296
  let wasCancelled = false;
297
+ let retryCount = 0;
258
298
  try {
299
+ if (!requestUrl) {
300
+ throw new Error("Request URL is missing");
301
+ }
259
302
  const rawData = config?.data !== void 0 ? config.data : axiosConfig.data;
260
303
  const resolvedData = (0, import_vue4.toValue)(rawData);
261
304
  const rawParams = config?.params !== void 0 ? config.params : axiosConfig.params;
262
305
  const resolvedParams = (0, import_vue4.toValue)(rawParams);
263
- if (!requestUrl) {
264
- throw new Error("Request URL is missing");
306
+ while (true) {
307
+ try {
308
+ const response = await axios2.request({
309
+ url: requestUrl,
310
+ method,
311
+ ...axiosConfig,
312
+ ...config,
313
+ data: resolvedData,
314
+ params: resolvedParams,
315
+ signal: controller.signal,
316
+ ...{ authMode: config?.authMode || authMode }
317
+ });
318
+ state.mutate(response.data, response);
319
+ state.setStatusCode(response.status);
320
+ onSuccess?.(response);
321
+ return response.data;
322
+ } catch (err) {
323
+ if (controller.signal.aborted || (0, import_axios2.isAxiosError)(err) && err.code === "ERR_CANCELED") {
324
+ wasCancelled = true;
325
+ return null;
326
+ }
327
+ const apiError = errorParser ? errorParser(err) : parseApiError(err);
328
+ const canRetry = retryCount < maxRetries && (retryStatusCodes.length === 0 || retryStatusCodes.includes(apiError.status));
329
+ if (canRetry) {
330
+ retryCount++;
331
+ const aborted = await cancellableSleep(retryDelay, controller.signal);
332
+ if (aborted) {
333
+ wasCancelled = true;
334
+ state.setLoading(false);
335
+ return null;
336
+ }
337
+ continue;
338
+ }
339
+ if (!skipErrorNotification && globalErrorHandler) {
340
+ globalErrorHandler(apiError, err);
341
+ }
342
+ state.setError(apiError);
343
+ state.setStatusCode(apiError.status);
344
+ onError?.(apiError);
345
+ return null;
346
+ }
265
347
  }
266
- const response = await axios2.request({
267
- url: requestUrl,
268
- method,
269
- ...axiosConfig,
270
- ...config,
271
- data: resolvedData,
272
- params: resolvedParams,
273
- signal: controller.signal,
274
- ...{ authMode: config?.authMode || authMode }
275
- });
276
- state.mutate(response.data, response);
277
- state.setStatusCode(response.status);
278
- onSuccess?.(response);
279
- return response.data;
280
348
  } catch (err) {
281
349
  if (controller.signal.aborted || (0, import_axios2.isAxiosError)(err) && err.code === "ERR_CANCELED") {
282
350
  wasCancelled = true;
@@ -311,7 +379,11 @@ function useApi(url, options = {}) {
311
379
  }
312
380
  }
313
381
  };
314
- const execute = debounce > 0 ? debounceFn(executeRequest, debounce) : executeRequest;
382
+ const _debounced = debounce > 0 ? debounceFn(executeRequest, debounce) : null;
383
+ const execute = _debounced ? (config) => _debounced(config).catch((err) => {
384
+ if (err instanceof DebounceCancelledError) return null;
385
+ throw err;
386
+ }) : executeRequest;
315
387
  const abort = (msg) => {
316
388
  if (pollTimer) clearTimeout(pollTimer);
317
389
  abortController2.value?.abort(msg);
@@ -383,7 +455,11 @@ function useApiDelete(url, options) {
383
455
 
384
456
  // src/useApiBatch.ts
385
457
  var import_vue5 = require("vue");
386
- function useApiBatch(urls, options = {}) {
458
+ function normalizeRequest(item) {
459
+ if (typeof item === "string") return { url: item, method: "GET" };
460
+ return { method: "GET", ...item };
461
+ }
462
+ function useApiBatch(requests, options = {}) {
387
463
  const {
388
464
  settled = true,
389
465
  concurrency,
@@ -396,7 +472,7 @@ function useApiBatch(urls, options = {}) {
396
472
  onProgress,
397
473
  ...apiOptions
398
474
  } = options;
399
- const getUrls = () => (0, import_vue5.toValue)(urls);
475
+ const getRequests = () => (0, import_vue5.toValue)(requests).map(normalizeRequest);
400
476
  const data = (0, import_vue5.ref)([]);
401
477
  const loading = (0, import_vue5.ref)(false);
402
478
  const error = (0, import_vue5.ref)(null);
@@ -414,43 +490,54 @@ function useApiBatch(urls, options = {}) {
414
490
  const abortControllers = (0, import_vue5.ref)([]);
415
491
  let isAborted = false;
416
492
  const updateProgress = (succeeded, failed) => {
417
- const currentUrls = getUrls();
493
+ const currentRequests = getRequests();
418
494
  const completed = succeeded + failed;
419
495
  const newProgress = {
420
496
  completed,
421
- total: currentUrls.length,
422
- percentage: currentUrls.length > 0 ? Math.round(completed / currentUrls.length * 100) : 0,
497
+ total: currentRequests.length,
498
+ percentage: currentRequests.length > 0 ? Math.round(completed / currentRequests.length * 100) : 0,
423
499
  succeeded,
424
500
  failed
425
501
  };
426
502
  progress.value = newProgress;
427
503
  onProgress?.(newProgress);
428
504
  };
429
- const executeRequest = async (url, index, signal) => {
430
- const { execute: execute2, error: reqError, statusCode } = useApi(url, {
505
+ const executeRequest = async (config, index, signal) => {
506
+ const scope = (0, import_vue5.effectScope)();
507
+ const api = scope.run(() => useApi(config.url, {
431
508
  ...apiOptions,
509
+ method: config.method,
510
+ data: config.data,
511
+ params: config.params,
512
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
513
+ ...config.headers && { headers: config.headers },
432
514
  useGlobalAbort: false,
433
515
  skipErrorNotification
434
- });
516
+ }));
517
+ const { execute: execute2, error: reqError, statusCode, response } = api;
435
518
  try {
436
519
  const result = await execute2({ signal });
437
520
  if (signal.aborted) {
438
521
  return {
439
- url,
522
+ url: config.url,
440
523
  index,
441
524
  success: false,
442
525
  data: null,
443
526
  error: { message: "Request aborted", status: 0, code: "ABORTED" },
444
- statusCode: null
527
+ statusCode: null,
528
+ response: null,
529
+ request: config
445
530
  };
446
531
  }
447
532
  const item = {
448
- url,
533
+ url: config.url,
449
534
  index,
450
535
  success: result !== null && result !== void 0,
451
536
  data: result ?? null,
452
537
  error: reqError.value,
453
- statusCode: statusCode.value
538
+ statusCode: statusCode.value,
539
+ response: response.value,
540
+ request: config
454
541
  };
455
542
  if (item.success) {
456
543
  onItemSuccess?.(item, index);
@@ -465,26 +552,30 @@ function useApiBatch(urls, options = {}) {
465
552
  code: "BATCH_ERROR"
466
553
  };
467
554
  const item = {
468
- url,
555
+ url: config.url,
469
556
  index,
470
557
  success: false,
471
558
  data: null,
472
559
  error: apiError,
473
- statusCode: null
560
+ statusCode: null,
561
+ response: null,
562
+ request: config
474
563
  };
475
564
  onItemError?.(item, index);
476
565
  return item;
566
+ } finally {
567
+ scope.stop();
477
568
  }
478
569
  };
479
- const executeWithConcurrency = async (urls2, limit) => {
480
- const results = new Array(urls2.length);
570
+ const executeWithConcurrency = async (requests2, limit) => {
571
+ const results = new Array(requests2.length);
481
572
  let succeededCount = 0;
482
573
  let failedCount = 0;
483
- if (!limit || limit >= urls2.length) {
484
- const promises = urls2.map((url, index) => {
574
+ if (!limit || limit >= requests2.length) {
575
+ const promises = requests2.map((config, index) => {
485
576
  const controller = new AbortController();
486
577
  abortControllers.value.push(controller);
487
- return executeRequest(url, index, controller.signal).then((result) => {
578
+ return executeRequest(config, index, controller.signal).then((result) => {
488
579
  results[index] = result;
489
580
  if (result.success) {
490
581
  succeededCount++;
@@ -509,12 +600,12 @@ function useApiBatch(urls, options = {}) {
509
600
  } else {
510
601
  let currentIndex = 0;
511
602
  const executeNext = async () => {
512
- while (currentIndex < urls2.length && !isAborted) {
603
+ while (currentIndex < requests2.length && !isAborted) {
513
604
  const index = currentIndex++;
514
- const url = urls2[index];
605
+ const config = requests2[index];
515
606
  const controller = new AbortController();
516
607
  abortControllers.value.push(controller);
517
- const result = await executeRequest(url, index, controller.signal);
608
+ const result = await executeRequest(config, index, controller.signal);
518
609
  results[index] = result;
519
610
  if (result.success) {
520
611
  succeededCount++;
@@ -531,7 +622,7 @@ function useApiBatch(urls, options = {}) {
531
622
  }
532
623
  }
533
624
  };
534
- const workers = Array.from({ length: Math.min(limit, urls2.length) }, () => executeNext());
625
+ const workers = Array.from({ length: Math.min(limit, requests2.length) }, () => executeNext());
535
626
  if (settled) {
536
627
  await Promise.allSettled(workers);
537
628
  } else {
@@ -541,7 +632,7 @@ function useApiBatch(urls, options = {}) {
541
632
  return results;
542
633
  };
543
634
  const execute = async () => {
544
- const currentUrls = getUrls();
635
+ const currentRequests = getRequests();
545
636
  isAborted = false;
546
637
  loading.value = true;
547
638
  error.value = null;
@@ -550,7 +641,7 @@ function useApiBatch(urls, options = {}) {
550
641
  abortControllers.value = [];
551
642
  updateProgress(0, 0);
552
643
  try {
553
- const results = await executeWithConcurrency(currentUrls, concurrency);
644
+ const results = await executeWithConcurrency(currentRequests, concurrency);
554
645
  data.value = results;
555
646
  const allFailed = results.every((r) => !r.success);
556
647
  if (allFailed && results.length > 0) {
@@ -587,7 +678,7 @@ function useApiBatch(urls, options = {}) {
587
678
  data.value = [];
588
679
  progress.value = {
589
680
  completed: 0,
590
- total: getUrls().length,
681
+ total: getRequests().length,
591
682
  percentage: 0,
592
683
  succeeded: 0,
593
684
  failed: 0
@@ -779,16 +870,16 @@ var tokenManager = new TokenManager();
779
870
  // src/features/interceptors.ts
780
871
  var AUTH_HEADER = "Authorization";
781
872
  var TOKEN_TYPE2 = "Bearer";
782
- var failedQueue = [];
783
- var isRefreshing = false;
784
- function processQueue(error, token = null) {
785
- failedQueue.forEach((promise) => {
786
- if (error) promise.reject(error);
787
- else if (token) promise.resolve(token);
788
- });
789
- failedQueue = [];
790
- }
791
873
  function setupInterceptors(axiosInstance, options = {}) {
874
+ let failedQueue = [];
875
+ let isRefreshing = false;
876
+ function processQueue(error, token = null) {
877
+ failedQueue.forEach((promise) => {
878
+ if (error) promise.reject(error);
879
+ else if (token) promise.resolve(token);
880
+ });
881
+ failedQueue = [];
882
+ }
792
883
  const {
793
884
  refreshUrl = "/auth/refresh",
794
885
  refreshWithCredentials = false,
package/dist/index.d.cts CHANGED
@@ -23,6 +23,12 @@ interface ApiRequestConfig<D = unknown> extends Omit<AxiosRequestConfig<D>, "dat
23
23
  authMode?: AuthMode;
24
24
  retry?: boolean | number;
25
25
  retryDelay?: number;
26
+ /**
27
+ * Retry only when the response status code is in this list.
28
+ * Default: [408, 429, 500, 502, 503, 504]
29
+ * Empty array = retry on any error (network errors included).
30
+ */
31
+ retryStatusCodes?: number[];
26
32
  }
27
33
  interface UseApiOptions<T = unknown, D = unknown> extends ApiRequestConfig<D> {
28
34
  immediate?: boolean;
@@ -86,6 +92,7 @@ interface ApiPluginOptions {
86
92
  globalOptions?: {
87
93
  retry?: number | boolean;
88
94
  retryDelay?: number;
95
+ retryStatusCodes?: number[];
89
96
  useGlobalAbort?: boolean;
90
97
  };
91
98
  }
@@ -94,6 +101,23 @@ interface AuthTokens$1 {
94
101
  refreshToken?: string;
95
102
  expiresIn?: number;
96
103
  }
104
+ /**
105
+ * Per-request configuration for a single item in a batch operation.
106
+ * String items in the batch array are automatically normalized to this shape
107
+ * with method: 'GET' and no data/params/headers.
108
+ */
109
+ interface BatchRequestConfig<D = unknown> {
110
+ /** The URL to request */
111
+ url: string;
112
+ /** HTTP method. Default: 'GET' */
113
+ method?: string;
114
+ /** Request body (for POST, PUT, PATCH) */
115
+ data?: D;
116
+ /** Query parameters */
117
+ params?: D;
118
+ /** Per-request headers that override global defaults for this request only */
119
+ headers?: Record<string, string>;
120
+ }
97
121
  /**
98
122
  * Result of a single request in a batch operation
99
123
  */
@@ -110,6 +134,10 @@ interface BatchResultItem<T = unknown> {
110
134
  error: ApiError | null;
111
135
  /** HTTP status code */
112
136
  statusCode: number | null;
137
+ /** Full AxiosResponse (null if failed — headers, status, etc. accessible here) */
138
+ response: AxiosResponse<T> | null;
139
+ /** The original normalized request config that produced this result */
140
+ request: BatchRequestConfig;
113
141
  }
114
142
  /**
115
143
  * Progress information for batch operations
@@ -239,7 +267,9 @@ declare function useApiDelete<T = unknown>(url: MaybeRefOrGetter<string | undefi
239
267
  *
240
268
  * Features:
241
269
  * - Reactive loading, data, error, progress states
242
- * - Reactive urls support (MaybeRefOrGetter)
270
+ * - Reactive request list support (MaybeRefOrGetter)
271
+ * - Per-request method, data, params, headers configuration
272
+ * - Full backward compatibility — plain string arrays still work
243
273
  * - Error tolerance with `settled: true` (default)
244
274
  * - Concurrency limiting
245
275
  * - Abort support for all pending requests
@@ -249,43 +279,29 @@ declare function useApiDelete<T = unknown>(url: MaybeRefOrGetter<string | undefi
249
279
  *
250
280
  * @example
251
281
  * ```ts
252
- * // Basic usage - fetch multiple users
253
- * const { data, loading, progress, execute } = useApiBatch<User>([
254
- * '/users/1',
255
- * '/users/2',
256
- * '/users/3'
257
- * ])
282
+ * // Basic usage plain strings (backward compatible)
283
+ * const { data, execute } = useApiBatch(['/users/1', '/users/2'])
258
284
  *
259
- * await execute()
260
- * console.log(data.value) // BatchResultItem<User>[]
285
+ * // Per-request config — method, data, params, headers
286
+ * const { data } = useApiBatch([
287
+ * { url: '/users', params: { page: 1 } },
288
+ * { url: '/posts', method: 'POST', data: { title: 'New' } },
289
+ * '/health', // string and object can be mixed
290
+ * ])
261
291
  *
262
- * // With reactive urls
263
- * const userIds = ref([1, 2, 3])
264
- * const urls = computed(() => userIds.value.map(id => `/users/${id}`))
265
- * const { successfulData } = useApiBatch<User>(urls, { immediate: true })
292
+ * // Batch DELETE by IDs
293
+ * const ids = [1, 2, 3]
294
+ * useApiBatch(ids.map(id => ({ url: `/users/${id}`, method: 'DELETE' })))
266
295
  *
267
- * // With options
268
- * const { successfulData, errors, progress } = useApiBatch<Post>(
269
- * ['/posts/1', '/posts/2', '/posts/3'],
270
- * {
271
- * concurrency: 2, // Max 2 parallel requests
272
- * immediate: true, // Execute on mount
273
- * onProgress: (p) => console.log(`${p.percentage}%`)
274
- * }
296
+ * // Reactive getter with object configs
297
+ * const pages = ref([1, 2, 3])
298
+ * const { successfulData } = useApiBatch(
299
+ * () => pages.value.map(page => ({ url: '/users', params: { page } })),
300
+ * { watch: pages, immediate: true }
275
301
  * )
276
- *
277
- * // Strict mode - fail on first error
278
- * const { execute } = useApiBatch<User>(urls, { settled: false })
279
- *
280
- * // Auto re-execute when dependency changes
281
- * const filters = ref({ status: 'active' })
282
- * const { data } = useApiBatch<User>(urls, {
283
- * watch: filters,
284
- * immediate: true
285
- * })
286
302
  * ```
287
303
  */
288
- declare function useApiBatch<T = unknown>(urls: MaybeRefOrGetter<string[]>, options?: UseApiBatchOptions<T>): UseApiBatchReturn<T>;
304
+ declare function useApiBatch<T = unknown>(requests: MaybeRefOrGetter<Array<string | BatchRequestConfig>>, options?: UseApiBatchOptions<T>): UseApiBatchReturn<T>;
289
305
 
290
306
  /**
291
307
  * API State Composable
@@ -558,4 +574,4 @@ interface AuthEventPayload {
558
574
  type AuthMonitorFn = (type: AuthEventType, payload: AuthEventPayload) => void;
559
575
  declare function setAuthMonitor(fn: AuthMonitorFn): void;
560
576
 
561
- export { type ApiError, type ApiPluginOptions, type ApiRequestConfig, type ApiState, type AuthEventPayload, AuthEventType, type AuthMode, type AuthMonitorFn, type AuthTokens$1 as AuthTokens, type BatchProgress, type BatchResultItem, type SetDataInput, type UseApiBatchOptions, type UseApiBatchReturn, type UseApiOptions, type UseApiReturn, createApi, createApiClient, setAuthMonitor, setupInterceptors, tokenManager, useAbortController, useApi, useApiBatch, useApiConfig, useApiDelete, useApiGet, useApiPatch, useApiPost, useApiPut, useApiState };
577
+ 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 SetDataInput, type UseApiBatchOptions, type UseApiBatchReturn, type UseApiOptions, type UseApiReturn, createApi, createApiClient, setAuthMonitor, setupInterceptors, tokenManager, useAbortController, useApi, useApiBatch, useApiConfig, useApiDelete, useApiGet, useApiPatch, useApiPost, useApiPut, useApiState };
package/dist/index.d.ts CHANGED
@@ -23,6 +23,12 @@ interface ApiRequestConfig<D = unknown> extends Omit<AxiosRequestConfig<D>, "dat
23
23
  authMode?: AuthMode;
24
24
  retry?: boolean | number;
25
25
  retryDelay?: number;
26
+ /**
27
+ * Retry only when the response status code is in this list.
28
+ * Default: [408, 429, 500, 502, 503, 504]
29
+ * Empty array = retry on any error (network errors included).
30
+ */
31
+ retryStatusCodes?: number[];
26
32
  }
27
33
  interface UseApiOptions<T = unknown, D = unknown> extends ApiRequestConfig<D> {
28
34
  immediate?: boolean;
@@ -86,6 +92,7 @@ interface ApiPluginOptions {
86
92
  globalOptions?: {
87
93
  retry?: number | boolean;
88
94
  retryDelay?: number;
95
+ retryStatusCodes?: number[];
89
96
  useGlobalAbort?: boolean;
90
97
  };
91
98
  }
@@ -94,6 +101,23 @@ interface AuthTokens$1 {
94
101
  refreshToken?: string;
95
102
  expiresIn?: number;
96
103
  }
104
+ /**
105
+ * Per-request configuration for a single item in a batch operation.
106
+ * String items in the batch array are automatically normalized to this shape
107
+ * with method: 'GET' and no data/params/headers.
108
+ */
109
+ interface BatchRequestConfig<D = unknown> {
110
+ /** The URL to request */
111
+ url: string;
112
+ /** HTTP method. Default: 'GET' */
113
+ method?: string;
114
+ /** Request body (for POST, PUT, PATCH) */
115
+ data?: D;
116
+ /** Query parameters */
117
+ params?: D;
118
+ /** Per-request headers that override global defaults for this request only */
119
+ headers?: Record<string, string>;
120
+ }
97
121
  /**
98
122
  * Result of a single request in a batch operation
99
123
  */
@@ -110,6 +134,10 @@ interface BatchResultItem<T = unknown> {
110
134
  error: ApiError | null;
111
135
  /** HTTP status code */
112
136
  statusCode: number | null;
137
+ /** Full AxiosResponse (null if failed — headers, status, etc. accessible here) */
138
+ response: AxiosResponse<T> | null;
139
+ /** The original normalized request config that produced this result */
140
+ request: BatchRequestConfig;
113
141
  }
114
142
  /**
115
143
  * Progress information for batch operations
@@ -239,7 +267,9 @@ declare function useApiDelete<T = unknown>(url: MaybeRefOrGetter<string | undefi
239
267
  *
240
268
  * Features:
241
269
  * - Reactive loading, data, error, progress states
242
- * - Reactive urls support (MaybeRefOrGetter)
270
+ * - Reactive request list support (MaybeRefOrGetter)
271
+ * - Per-request method, data, params, headers configuration
272
+ * - Full backward compatibility — plain string arrays still work
243
273
  * - Error tolerance with `settled: true` (default)
244
274
  * - Concurrency limiting
245
275
  * - Abort support for all pending requests
@@ -249,43 +279,29 @@ declare function useApiDelete<T = unknown>(url: MaybeRefOrGetter<string | undefi
249
279
  *
250
280
  * @example
251
281
  * ```ts
252
- * // Basic usage - fetch multiple users
253
- * const { data, loading, progress, execute } = useApiBatch<User>([
254
- * '/users/1',
255
- * '/users/2',
256
- * '/users/3'
257
- * ])
282
+ * // Basic usage plain strings (backward compatible)
283
+ * const { data, execute } = useApiBatch(['/users/1', '/users/2'])
258
284
  *
259
- * await execute()
260
- * console.log(data.value) // BatchResultItem<User>[]
285
+ * // Per-request config — method, data, params, headers
286
+ * const { data } = useApiBatch([
287
+ * { url: '/users', params: { page: 1 } },
288
+ * { url: '/posts', method: 'POST', data: { title: 'New' } },
289
+ * '/health', // string and object can be mixed
290
+ * ])
261
291
  *
262
- * // With reactive urls
263
- * const userIds = ref([1, 2, 3])
264
- * const urls = computed(() => userIds.value.map(id => `/users/${id}`))
265
- * const { successfulData } = useApiBatch<User>(urls, { immediate: true })
292
+ * // Batch DELETE by IDs
293
+ * const ids = [1, 2, 3]
294
+ * useApiBatch(ids.map(id => ({ url: `/users/${id}`, method: 'DELETE' })))
266
295
  *
267
- * // With options
268
- * const { successfulData, errors, progress } = useApiBatch<Post>(
269
- * ['/posts/1', '/posts/2', '/posts/3'],
270
- * {
271
- * concurrency: 2, // Max 2 parallel requests
272
- * immediate: true, // Execute on mount
273
- * onProgress: (p) => console.log(`${p.percentage}%`)
274
- * }
296
+ * // Reactive getter with object configs
297
+ * const pages = ref([1, 2, 3])
298
+ * const { successfulData } = useApiBatch(
299
+ * () => pages.value.map(page => ({ url: '/users', params: { page } })),
300
+ * { watch: pages, immediate: true }
275
301
  * )
276
- *
277
- * // Strict mode - fail on first error
278
- * const { execute } = useApiBatch<User>(urls, { settled: false })
279
- *
280
- * // Auto re-execute when dependency changes
281
- * const filters = ref({ status: 'active' })
282
- * const { data } = useApiBatch<User>(urls, {
283
- * watch: filters,
284
- * immediate: true
285
- * })
286
302
  * ```
287
303
  */
288
- declare function useApiBatch<T = unknown>(urls: MaybeRefOrGetter<string[]>, options?: UseApiBatchOptions<T>): UseApiBatchReturn<T>;
304
+ declare function useApiBatch<T = unknown>(requests: MaybeRefOrGetter<Array<string | BatchRequestConfig>>, options?: UseApiBatchOptions<T>): UseApiBatchReturn<T>;
289
305
 
290
306
  /**
291
307
  * API State Composable
@@ -558,4 +574,4 @@ interface AuthEventPayload {
558
574
  type AuthMonitorFn = (type: AuthEventType, payload: AuthEventPayload) => void;
559
575
  declare function setAuthMonitor(fn: AuthMonitorFn): void;
560
576
 
561
- export { type ApiError, type ApiPluginOptions, type ApiRequestConfig, type ApiState, type AuthEventPayload, AuthEventType, type AuthMode, type AuthMonitorFn, type AuthTokens$1 as AuthTokens, type BatchProgress, type BatchResultItem, type SetDataInput, type UseApiBatchOptions, type UseApiBatchReturn, type UseApiOptions, type UseApiReturn, createApi, createApiClient, setAuthMonitor, setupInterceptors, tokenManager, useAbortController, useApi, useApiBatch, useApiConfig, useApiDelete, useApiGet, useApiPatch, useApiPost, useApiPut, useApiState };
577
+ 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 SetDataInput, type UseApiBatchOptions, type UseApiBatchReturn, type UseApiOptions, type UseApiReturn, createApi, createApiClient, setAuthMonitor, setupInterceptors, tokenManager, useAbortController, useApi, useApiBatch, useApiConfig, useApiDelete, useApiGet, useApiPatch, useApiPost, useApiPut, useApiState };
package/dist/index.mjs CHANGED
@@ -24,17 +24,34 @@ function useApiConfig() {
24
24
  }
25
25
 
26
26
  // src/utils/debounce.ts
27
+ var DebounceCancelledError = class extends Error {
28
+ isDebounceCancelled = true;
29
+ constructor() {
30
+ super("Debounced call was superseded by a newer call");
31
+ this.name = "DebounceCancelledError";
32
+ }
33
+ };
27
34
  function debounceFn(fn, delay) {
28
35
  let timeoutId = null;
36
+ let pendingReject = null;
29
37
  return function(...args) {
30
- return new Promise((resolve) => {
31
- if (timeoutId) {
32
- clearTimeout(timeoutId);
33
- }
38
+ if (pendingReject) {
39
+ pendingReject(new DebounceCancelledError());
40
+ pendingReject = null;
41
+ }
42
+ if (timeoutId) {
43
+ clearTimeout(timeoutId);
44
+ }
45
+ return new Promise((resolve, reject) => {
46
+ pendingReject = reject;
34
47
  timeoutId = setTimeout(async () => {
35
- const result = await fn(...args);
36
- resolve(result);
48
+ pendingReject = null;
37
49
  timeoutId = null;
50
+ try {
51
+ resolve(await fn(...args));
52
+ } catch (err) {
53
+ reject(err);
54
+ }
38
55
  }, delay);
39
56
  });
40
57
  };
@@ -145,6 +162,26 @@ function useAbortController() {
145
162
  }
146
163
 
147
164
  // src/useApi.ts
165
+ var DEFAULT_RETRY_STATUS_CODES = [408, 429, 500, 502, 503, 504];
166
+ function cancellableSleep(ms, signal) {
167
+ return new Promise((resolve) => {
168
+ if (signal.aborted) {
169
+ resolve(true);
170
+ return;
171
+ }
172
+ const timer = setTimeout(() => {
173
+ cleanup();
174
+ resolve(false);
175
+ }, ms);
176
+ const onAbort = () => {
177
+ clearTimeout(timer);
178
+ cleanup();
179
+ resolve(true);
180
+ };
181
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
182
+ signal.addEventListener("abort", onAbort, { once: true });
183
+ });
184
+ }
148
185
  function useApi(url, options = {}) {
149
186
  const { axios: axios2, onError: globalErrorHandler, globalOptions, errorParser } = useApiConfig();
150
187
  const {
@@ -159,12 +196,14 @@ function useApi(url, options = {}) {
159
196
  skipErrorNotification = false,
160
197
  retry = globalOptions?.retry ?? false,
161
198
  retryDelay = globalOptions?.retryDelay ?? 1e3,
199
+ retryStatusCodes = globalOptions?.retryStatusCodes ?? DEFAULT_RETRY_STATUS_CODES,
162
200
  authMode = "default",
163
201
  useGlobalAbort = globalOptions?.useGlobalAbort ?? true,
164
202
  initialLoading = false,
165
203
  poll = 0,
166
204
  ...axiosConfig
167
205
  } = options;
206
+ const maxRetries = retry === false ? 0 : retry === true ? 3 : retry;
168
207
  const startLoading = initialLoading ?? immediate;
169
208
  const state = useApiState(initialData, { initialLoading: startLoading });
170
209
  const abortController2 = ref3(null);
@@ -204,28 +243,57 @@ function useApi(url, options = {}) {
204
243
  state.setLoading(true);
205
244
  state.setError(null);
206
245
  let wasCancelled = false;
246
+ let retryCount = 0;
207
247
  try {
248
+ if (!requestUrl) {
249
+ throw new Error("Request URL is missing");
250
+ }
208
251
  const rawData = config?.data !== void 0 ? config.data : axiosConfig.data;
209
252
  const resolvedData = toValue(rawData);
210
253
  const rawParams = config?.params !== void 0 ? config.params : axiosConfig.params;
211
254
  const resolvedParams = toValue(rawParams);
212
- if (!requestUrl) {
213
- throw new Error("Request URL is missing");
255
+ while (true) {
256
+ try {
257
+ const response = await axios2.request({
258
+ url: requestUrl,
259
+ method,
260
+ ...axiosConfig,
261
+ ...config,
262
+ data: resolvedData,
263
+ params: resolvedParams,
264
+ signal: controller.signal,
265
+ ...{ authMode: config?.authMode || authMode }
266
+ });
267
+ state.mutate(response.data, response);
268
+ state.setStatusCode(response.status);
269
+ onSuccess?.(response);
270
+ return response.data;
271
+ } catch (err) {
272
+ if (controller.signal.aborted || isAxiosError2(err) && err.code === "ERR_CANCELED") {
273
+ wasCancelled = true;
274
+ return null;
275
+ }
276
+ const apiError = errorParser ? errorParser(err) : parseApiError(err);
277
+ const canRetry = retryCount < maxRetries && (retryStatusCodes.length === 0 || retryStatusCodes.includes(apiError.status));
278
+ if (canRetry) {
279
+ retryCount++;
280
+ const aborted = await cancellableSleep(retryDelay, controller.signal);
281
+ if (aborted) {
282
+ wasCancelled = true;
283
+ state.setLoading(false);
284
+ return null;
285
+ }
286
+ continue;
287
+ }
288
+ if (!skipErrorNotification && globalErrorHandler) {
289
+ globalErrorHandler(apiError, err);
290
+ }
291
+ state.setError(apiError);
292
+ state.setStatusCode(apiError.status);
293
+ onError?.(apiError);
294
+ return null;
295
+ }
214
296
  }
215
- const response = await axios2.request({
216
- url: requestUrl,
217
- method,
218
- ...axiosConfig,
219
- ...config,
220
- data: resolvedData,
221
- params: resolvedParams,
222
- signal: controller.signal,
223
- ...{ authMode: config?.authMode || authMode }
224
- });
225
- state.mutate(response.data, response);
226
- state.setStatusCode(response.status);
227
- onSuccess?.(response);
228
- return response.data;
229
297
  } catch (err) {
230
298
  if (controller.signal.aborted || isAxiosError2(err) && err.code === "ERR_CANCELED") {
231
299
  wasCancelled = true;
@@ -260,7 +328,11 @@ function useApi(url, options = {}) {
260
328
  }
261
329
  }
262
330
  };
263
- const execute = debounce > 0 ? debounceFn(executeRequest, debounce) : executeRequest;
331
+ const _debounced = debounce > 0 ? debounceFn(executeRequest, debounce) : null;
332
+ const execute = _debounced ? (config) => _debounced(config).catch((err) => {
333
+ if (err instanceof DebounceCancelledError) return null;
334
+ throw err;
335
+ }) : executeRequest;
264
336
  const abort = (msg) => {
265
337
  if (pollTimer) clearTimeout(pollTimer);
266
338
  abortController2.value?.abort(msg);
@@ -331,8 +403,12 @@ function useApiDelete(url, options) {
331
403
  }
332
404
 
333
405
  // src/useApiBatch.ts
334
- import { ref as ref4, computed, getCurrentScope as getCurrentScope2, onScopeDispose as onScopeDispose2, toValue as toValue2, watch as watch2 } from "vue";
335
- function useApiBatch(urls, options = {}) {
406
+ import { ref as ref4, computed, effectScope, getCurrentScope as getCurrentScope2, onScopeDispose as onScopeDispose2, toValue as toValue2, watch as watch2 } from "vue";
407
+ function normalizeRequest(item) {
408
+ if (typeof item === "string") return { url: item, method: "GET" };
409
+ return { method: "GET", ...item };
410
+ }
411
+ function useApiBatch(requests, options = {}) {
336
412
  const {
337
413
  settled = true,
338
414
  concurrency,
@@ -345,7 +421,7 @@ function useApiBatch(urls, options = {}) {
345
421
  onProgress,
346
422
  ...apiOptions
347
423
  } = options;
348
- const getUrls = () => toValue2(urls);
424
+ const getRequests = () => toValue2(requests).map(normalizeRequest);
349
425
  const data = ref4([]);
350
426
  const loading = ref4(false);
351
427
  const error = ref4(null);
@@ -363,43 +439,54 @@ function useApiBatch(urls, options = {}) {
363
439
  const abortControllers = ref4([]);
364
440
  let isAborted = false;
365
441
  const updateProgress = (succeeded, failed) => {
366
- const currentUrls = getUrls();
442
+ const currentRequests = getRequests();
367
443
  const completed = succeeded + failed;
368
444
  const newProgress = {
369
445
  completed,
370
- total: currentUrls.length,
371
- percentage: currentUrls.length > 0 ? Math.round(completed / currentUrls.length * 100) : 0,
446
+ total: currentRequests.length,
447
+ percentage: currentRequests.length > 0 ? Math.round(completed / currentRequests.length * 100) : 0,
372
448
  succeeded,
373
449
  failed
374
450
  };
375
451
  progress.value = newProgress;
376
452
  onProgress?.(newProgress);
377
453
  };
378
- const executeRequest = async (url, index, signal) => {
379
- const { execute: execute2, error: reqError, statusCode } = useApi(url, {
454
+ const executeRequest = async (config, index, signal) => {
455
+ const scope = effectScope();
456
+ const api = scope.run(() => useApi(config.url, {
380
457
  ...apiOptions,
458
+ method: config.method,
459
+ data: config.data,
460
+ params: config.params,
461
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
462
+ ...config.headers && { headers: config.headers },
381
463
  useGlobalAbort: false,
382
464
  skipErrorNotification
383
- });
465
+ }));
466
+ const { execute: execute2, error: reqError, statusCode, response } = api;
384
467
  try {
385
468
  const result = await execute2({ signal });
386
469
  if (signal.aborted) {
387
470
  return {
388
- url,
471
+ url: config.url,
389
472
  index,
390
473
  success: false,
391
474
  data: null,
392
475
  error: { message: "Request aborted", status: 0, code: "ABORTED" },
393
- statusCode: null
476
+ statusCode: null,
477
+ response: null,
478
+ request: config
394
479
  };
395
480
  }
396
481
  const item = {
397
- url,
482
+ url: config.url,
398
483
  index,
399
484
  success: result !== null && result !== void 0,
400
485
  data: result ?? null,
401
486
  error: reqError.value,
402
- statusCode: statusCode.value
487
+ statusCode: statusCode.value,
488
+ response: response.value,
489
+ request: config
403
490
  };
404
491
  if (item.success) {
405
492
  onItemSuccess?.(item, index);
@@ -414,26 +501,30 @@ function useApiBatch(urls, options = {}) {
414
501
  code: "BATCH_ERROR"
415
502
  };
416
503
  const item = {
417
- url,
504
+ url: config.url,
418
505
  index,
419
506
  success: false,
420
507
  data: null,
421
508
  error: apiError,
422
- statusCode: null
509
+ statusCode: null,
510
+ response: null,
511
+ request: config
423
512
  };
424
513
  onItemError?.(item, index);
425
514
  return item;
515
+ } finally {
516
+ scope.stop();
426
517
  }
427
518
  };
428
- const executeWithConcurrency = async (urls2, limit) => {
429
- const results = new Array(urls2.length);
519
+ const executeWithConcurrency = async (requests2, limit) => {
520
+ const results = new Array(requests2.length);
430
521
  let succeededCount = 0;
431
522
  let failedCount = 0;
432
- if (!limit || limit >= urls2.length) {
433
- const promises = urls2.map((url, index) => {
523
+ if (!limit || limit >= requests2.length) {
524
+ const promises = requests2.map((config, index) => {
434
525
  const controller = new AbortController();
435
526
  abortControllers.value.push(controller);
436
- return executeRequest(url, index, controller.signal).then((result) => {
527
+ return executeRequest(config, index, controller.signal).then((result) => {
437
528
  results[index] = result;
438
529
  if (result.success) {
439
530
  succeededCount++;
@@ -458,12 +549,12 @@ function useApiBatch(urls, options = {}) {
458
549
  } else {
459
550
  let currentIndex = 0;
460
551
  const executeNext = async () => {
461
- while (currentIndex < urls2.length && !isAborted) {
552
+ while (currentIndex < requests2.length && !isAborted) {
462
553
  const index = currentIndex++;
463
- const url = urls2[index];
554
+ const config = requests2[index];
464
555
  const controller = new AbortController();
465
556
  abortControllers.value.push(controller);
466
- const result = await executeRequest(url, index, controller.signal);
557
+ const result = await executeRequest(config, index, controller.signal);
467
558
  results[index] = result;
468
559
  if (result.success) {
469
560
  succeededCount++;
@@ -480,7 +571,7 @@ function useApiBatch(urls, options = {}) {
480
571
  }
481
572
  }
482
573
  };
483
- const workers = Array.from({ length: Math.min(limit, urls2.length) }, () => executeNext());
574
+ const workers = Array.from({ length: Math.min(limit, requests2.length) }, () => executeNext());
484
575
  if (settled) {
485
576
  await Promise.allSettled(workers);
486
577
  } else {
@@ -490,7 +581,7 @@ function useApiBatch(urls, options = {}) {
490
581
  return results;
491
582
  };
492
583
  const execute = async () => {
493
- const currentUrls = getUrls();
584
+ const currentRequests = getRequests();
494
585
  isAborted = false;
495
586
  loading.value = true;
496
587
  error.value = null;
@@ -499,7 +590,7 @@ function useApiBatch(urls, options = {}) {
499
590
  abortControllers.value = [];
500
591
  updateProgress(0, 0);
501
592
  try {
502
- const results = await executeWithConcurrency(currentUrls, concurrency);
593
+ const results = await executeWithConcurrency(currentRequests, concurrency);
503
594
  data.value = results;
504
595
  const allFailed = results.every((r) => !r.success);
505
596
  if (allFailed && results.length > 0) {
@@ -536,7 +627,7 @@ function useApiBatch(urls, options = {}) {
536
627
  data.value = [];
537
628
  progress.value = {
538
629
  completed: 0,
539
- total: getUrls().length,
630
+ total: getRequests().length,
540
631
  percentage: 0,
541
632
  succeeded: 0,
542
633
  failed: 0
@@ -728,16 +819,16 @@ var tokenManager = new TokenManager();
728
819
  // src/features/interceptors.ts
729
820
  var AUTH_HEADER = "Authorization";
730
821
  var TOKEN_TYPE2 = "Bearer";
731
- var failedQueue = [];
732
- var isRefreshing = false;
733
- function processQueue(error, token = null) {
734
- failedQueue.forEach((promise) => {
735
- if (error) promise.reject(error);
736
- else if (token) promise.resolve(token);
737
- });
738
- failedQueue = [];
739
- }
740
822
  function setupInterceptors(axiosInstance, options = {}) {
823
+ let failedQueue = [];
824
+ let isRefreshing = false;
825
+ function processQueue(error, token = null) {
826
+ failedQueue.forEach((promise) => {
827
+ if (error) promise.reject(error);
828
+ else if (token) promise.resolve(token);
829
+ });
830
+ failedQueue = [];
831
+ }
741
832
  const {
742
833
  refreshUrl = "/auth/refresh",
743
834
  refreshWithCredentials = false,
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@ametie/vue-muza-use",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Powerful Vue 3 API composable (Muza Kit) with Axios, Auto-Refresh & TypeScript",
5
5
  "author": "MortyQ",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "git+https://github.com/MortyQ/vue-useApi.git"
9
+ "url": "https://github.com/MortyQ/vue-useApi",
10
+ "directory": "packages/use-api"
10
11
  },
11
12
  "files": [
12
13
  "dist",