@ametie/vue-muza-use 0.1.1 → 0.3.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
@@ -218,6 +218,55 @@ const { abortAll } = useAbortController()
218
218
 
219
219
  ---
220
220
 
221
+ ## 🚨 Advanced Error Handling
222
+
223
+ By default, the library attempts to normalize errors into a standard `ApiError` format. However, every backend is different. You can fully customize how errors are parsed globally.
224
+
225
+ ### Default Behavior
226
+ If you don't provide a parser, we extract the message from `error.response.data.message` or `error.message`.
227
+
228
+ ### Custom Error Parser
229
+ Inject your own logic to transform your backend's specific error format into our uniform structure.
230
+
231
+ ```typescript
232
+ // main.ts
233
+ app.use(createApi({
234
+ axios: api,
235
+ // 👇 Define how to parse errors from your specific API
236
+ errorParser: (error: any) => {
237
+ const data = error.response?.data
238
+
239
+ // Example: Laravel/Rails style validation errors
240
+ if (data?.errors) {
241
+ return {
242
+ message: 'Validation Failed',
243
+ status: error.response.status,
244
+ code: 'VALIDATION_ERROR',
245
+ errors: data.errors // { email: ['Invalid email'] }
246
+ }
247
+ }
248
+
249
+ // Example: Custom wrap format { success: false, error: { msg: "..." } }
250
+ if (data?.error?.msg) {
251
+ return {
252
+ message: data.error.msg,
253
+ status: error.response.status,
254
+ code: data.error.code
255
+ }
256
+ }
257
+
258
+ // Fallback to default behavior
259
+ return {
260
+ message: error.message || 'Unknown error',
261
+ status: error.response?.status || 500,
262
+ details: error
263
+ }
264
+ }
265
+ }))
266
+ ```
267
+
268
+ ---
269
+
221
270
  ## 📚 API Reference
222
271
 
223
272
  ### `useApi<T, D>(url, options)`
@@ -298,6 +347,3 @@ This happens transparently to your components. They just "wait" a bit longer for
298
347
 
299
348
  ---
300
349
 
301
- ## License
302
-
303
- MIT © [Ametie](https://github.com/ametie)
package/dist/index.cjs CHANGED
@@ -182,7 +182,7 @@ function useAbortController() {
182
182
 
183
183
  // src/useApi.ts
184
184
  function useApi(url, options = {}) {
185
- const { axios: axios2, onError: globalErrorHandler, globalOptions } = useApiConfig();
185
+ const { axios: axios2, onError: globalErrorHandler, globalOptions, errorParser } = useApiConfig();
186
186
  const {
187
187
  method = "GET",
188
188
  immediate = false,
@@ -198,13 +198,27 @@ function useApi(url, options = {}) {
198
198
  authMode = "default",
199
199
  useGlobalAbort = globalOptions?.useGlobalAbort ?? true,
200
200
  initialLoading = false,
201
+ poll = 0,
201
202
  ...axiosConfig
202
203
  } = options;
203
204
  const startLoading = initialLoading ?? immediate;
204
205
  const state = useApiState(initialData, { initialLoading: startLoading });
205
206
  const abortController2 = (0, import_vue4.ref)(null);
206
207
  const globalAbort = useGlobalAbort ? useAbortController() : null;
208
+ let pollTimer = null;
209
+ const getPollConfig = () => {
210
+ const val = (0, import_vue4.toValue)(poll);
211
+ if (typeof val === "number") return { interval: val, whenHidden: false };
212
+ if (val && typeof val === "object") {
213
+ return {
214
+ interval: (0, import_vue4.toValue)(val.interval),
215
+ whenHidden: (0, import_vue4.toValue)(val.whenHidden) ?? false
216
+ };
217
+ }
218
+ return { interval: 0, whenHidden: false };
219
+ };
207
220
  const executeRequest = async (config) => {
221
+ if (pollTimer) clearTimeout(pollTimer);
208
222
  const requestUrl = typeof url === "string" ? url : url.value;
209
223
  if (abortController2.value) abortController2.value.abort("Cancelled by new request");
210
224
  const controller = new AbortController();
@@ -247,7 +261,7 @@ function useApi(url, options = {}) {
247
261
  wasCancelled = true;
248
262
  return null;
249
263
  }
250
- const apiError = parseApiError(err);
264
+ const apiError = errorParser ? errorParser(err) : parseApiError(err);
251
265
  if (!skipErrorNotification && globalErrorHandler) {
252
266
  globalErrorHandler(apiError, err);
253
267
  }
@@ -260,11 +274,25 @@ function useApi(url, options = {}) {
260
274
  if (!wasCancelled) {
261
275
  state.setLoading(false);
262
276
  onFinish?.();
277
+ const { interval, whenHidden } = getPollConfig();
278
+ if (interval > 0) {
279
+ const shouldPoll = whenHidden || typeof document !== "undefined" && !document.hidden;
280
+ if (shouldPoll) {
281
+ pollTimer = setTimeout(() => {
282
+ pollTimer = null;
283
+ const { whenHidden: currentWhenHidden } = getPollConfig();
284
+ if (currentWhenHidden || (typeof document === "undefined" || !document.hidden)) {
285
+ execute();
286
+ }
287
+ }, interval);
288
+ }
289
+ }
263
290
  }
264
291
  }
265
292
  };
266
293
  const execute = debounce > 0 ? debounceFn(executeRequest, debounce) : executeRequest;
267
294
  const abort = (msg) => {
295
+ if (pollTimer) clearTimeout(pollTimer);
268
296
  abortController2.value?.abort(msg);
269
297
  abortController2.value = null;
270
298
  };
@@ -282,6 +310,38 @@ function useApi(url, options = {}) {
282
310
  (0, import_vue4.onScopeDispose)(() => abort("Scope disposed"));
283
311
  }
284
312
  if (immediate) execute();
313
+ if (typeof document !== "undefined") {
314
+ const handleVisibility = () => {
315
+ if (document.hidden) return;
316
+ const { interval } = getPollConfig();
317
+ if (interval > 0 && !pollTimer && !state.loading.value) {
318
+ execute();
319
+ }
320
+ };
321
+ document.addEventListener("visibilitychange", handleVisibility);
322
+ if ((0, import_vue4.getCurrentScope)()) {
323
+ (0, import_vue4.onScopeDispose)(() => document.removeEventListener("visibilitychange", handleVisibility));
324
+ }
325
+ }
326
+ if (poll) {
327
+ (0, import_vue4.watch)(() => (0, import_vue4.toValue)(poll), () => {
328
+ const { interval } = getPollConfig();
329
+ if (interval > 0) {
330
+ if (pollTimer) {
331
+ clearTimeout(pollTimer);
332
+ pollTimer = null;
333
+ }
334
+ if (!state.loading.value) {
335
+ execute();
336
+ }
337
+ } else {
338
+ if (pollTimer) {
339
+ clearTimeout(pollTimer);
340
+ pollTimer = null;
341
+ }
342
+ }
343
+ }, { deep: true });
344
+ }
285
345
  return { ...state, execute, abort, reset };
286
346
  }
287
347
  function useApiGet(url, options) {
package/dist/index.d.cts CHANGED
@@ -34,6 +34,16 @@ interface UseApiOptions<T = unknown, D = unknown> extends ApiRequestConfig<D> {
34
34
  useGlobalAbort?: boolean;
35
35
  initialLoading?: boolean;
36
36
  watch?: WatchSource | WatchSource[];
37
+ /**
38
+ * Polling configuration.
39
+ * - Pass a **number** (ms) for simple polling.
40
+ * - Pass an **object** `{ interval: number, whenHidden?: boolean }` for advanced control.
41
+ * Properties inside the object can also be Refs.
42
+ */
43
+ poll?: MaybeRefOrGetter<number | {
44
+ interval: MaybeRefOrGetter<number>;
45
+ whenHidden?: MaybeRefOrGetter<boolean>;
46
+ }>;
37
47
  }
38
48
  interface UseApiReturn<T = unknown, D = unknown> {
39
49
  data: Ref<T | null>;
@@ -48,6 +58,11 @@ interface UseApiReturn<T = unknown, D = unknown> {
48
58
  interface ApiPluginOptions {
49
59
  axios: AxiosInstance;
50
60
  onError?: (error: ApiError, originalError: any) => void;
61
+ /**
62
+ * Custom error parser to transform backend errors into ApiError format.
63
+ * Useful if your backend has a different error structure.
64
+ */
65
+ errorParser?: (error: unknown) => ApiError;
51
66
  globalOptions?: {
52
67
  retry?: number | boolean;
53
68
  retryDelay?: number;
package/dist/index.d.ts CHANGED
@@ -34,6 +34,16 @@ interface UseApiOptions<T = unknown, D = unknown> extends ApiRequestConfig<D> {
34
34
  useGlobalAbort?: boolean;
35
35
  initialLoading?: boolean;
36
36
  watch?: WatchSource | WatchSource[];
37
+ /**
38
+ * Polling configuration.
39
+ * - Pass a **number** (ms) for simple polling.
40
+ * - Pass an **object** `{ interval: number, whenHidden?: boolean }` for advanced control.
41
+ * Properties inside the object can also be Refs.
42
+ */
43
+ poll?: MaybeRefOrGetter<number | {
44
+ interval: MaybeRefOrGetter<number>;
45
+ whenHidden?: MaybeRefOrGetter<boolean>;
46
+ }>;
37
47
  }
38
48
  interface UseApiReturn<T = unknown, D = unknown> {
39
49
  data: Ref<T | null>;
@@ -48,6 +58,11 @@ interface UseApiReturn<T = unknown, D = unknown> {
48
58
  interface ApiPluginOptions {
49
59
  axios: AxiosInstance;
50
60
  onError?: (error: ApiError, originalError: any) => void;
61
+ /**
62
+ * Custom error parser to transform backend errors into ApiError format.
63
+ * Useful if your backend has a different error structure.
64
+ */
65
+ errorParser?: (error: unknown) => ApiError;
51
66
  globalOptions?: {
52
67
  retry?: number | boolean;
53
68
  retryDelay?: number;
package/dist/index.mjs CHANGED
@@ -132,7 +132,7 @@ function useAbortController() {
132
132
 
133
133
  // src/useApi.ts
134
134
  function useApi(url, options = {}) {
135
- const { axios: axios2, onError: globalErrorHandler, globalOptions } = useApiConfig();
135
+ const { axios: axios2, onError: globalErrorHandler, globalOptions, errorParser } = useApiConfig();
136
136
  const {
137
137
  method = "GET",
138
138
  immediate = false,
@@ -148,13 +148,27 @@ function useApi(url, options = {}) {
148
148
  authMode = "default",
149
149
  useGlobalAbort = globalOptions?.useGlobalAbort ?? true,
150
150
  initialLoading = false,
151
+ poll = 0,
151
152
  ...axiosConfig
152
153
  } = options;
153
154
  const startLoading = initialLoading ?? immediate;
154
155
  const state = useApiState(initialData, { initialLoading: startLoading });
155
156
  const abortController2 = ref3(null);
156
157
  const globalAbort = useGlobalAbort ? useAbortController() : null;
158
+ let pollTimer = null;
159
+ const getPollConfig = () => {
160
+ const val = toValue(poll);
161
+ if (typeof val === "number") return { interval: val, whenHidden: false };
162
+ if (val && typeof val === "object") {
163
+ return {
164
+ interval: toValue(val.interval),
165
+ whenHidden: toValue(val.whenHidden) ?? false
166
+ };
167
+ }
168
+ return { interval: 0, whenHidden: false };
169
+ };
157
170
  const executeRequest = async (config) => {
171
+ if (pollTimer) clearTimeout(pollTimer);
158
172
  const requestUrl = typeof url === "string" ? url : url.value;
159
173
  if (abortController2.value) abortController2.value.abort("Cancelled by new request");
160
174
  const controller = new AbortController();
@@ -197,7 +211,7 @@ function useApi(url, options = {}) {
197
211
  wasCancelled = true;
198
212
  return null;
199
213
  }
200
- const apiError = parseApiError(err);
214
+ const apiError = errorParser ? errorParser(err) : parseApiError(err);
201
215
  if (!skipErrorNotification && globalErrorHandler) {
202
216
  globalErrorHandler(apiError, err);
203
217
  }
@@ -210,11 +224,25 @@ function useApi(url, options = {}) {
210
224
  if (!wasCancelled) {
211
225
  state.setLoading(false);
212
226
  onFinish?.();
227
+ const { interval, whenHidden } = getPollConfig();
228
+ if (interval > 0) {
229
+ const shouldPoll = whenHidden || typeof document !== "undefined" && !document.hidden;
230
+ if (shouldPoll) {
231
+ pollTimer = setTimeout(() => {
232
+ pollTimer = null;
233
+ const { whenHidden: currentWhenHidden } = getPollConfig();
234
+ if (currentWhenHidden || (typeof document === "undefined" || !document.hidden)) {
235
+ execute();
236
+ }
237
+ }, interval);
238
+ }
239
+ }
213
240
  }
214
241
  }
215
242
  };
216
243
  const execute = debounce > 0 ? debounceFn(executeRequest, debounce) : executeRequest;
217
244
  const abort = (msg) => {
245
+ if (pollTimer) clearTimeout(pollTimer);
218
246
  abortController2.value?.abort(msg);
219
247
  abortController2.value = null;
220
248
  };
@@ -232,6 +260,38 @@ function useApi(url, options = {}) {
232
260
  onScopeDispose(() => abort("Scope disposed"));
233
261
  }
234
262
  if (immediate) execute();
263
+ if (typeof document !== "undefined") {
264
+ const handleVisibility = () => {
265
+ if (document.hidden) return;
266
+ const { interval } = getPollConfig();
267
+ if (interval > 0 && !pollTimer && !state.loading.value) {
268
+ execute();
269
+ }
270
+ };
271
+ document.addEventListener("visibilitychange", handleVisibility);
272
+ if (getCurrentScope()) {
273
+ onScopeDispose(() => document.removeEventListener("visibilitychange", handleVisibility));
274
+ }
275
+ }
276
+ if (poll) {
277
+ watch(() => toValue(poll), () => {
278
+ const { interval } = getPollConfig();
279
+ if (interval > 0) {
280
+ if (pollTimer) {
281
+ clearTimeout(pollTimer);
282
+ pollTimer = null;
283
+ }
284
+ if (!state.loading.value) {
285
+ execute();
286
+ }
287
+ } else {
288
+ if (pollTimer) {
289
+ clearTimeout(pollTimer);
290
+ pollTimer = null;
291
+ }
292
+ }
293
+ }, { deep: true });
294
+ }
235
295
  return { ...state, execute, abort, reset };
236
296
  }
237
297
  function useApiGet(url, options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ametie/vue-muza-use",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Powerful Vue 3 API composable (Muza Kit) with Axios, Auto-Refresh & TypeScript",
5
5
  "author": "MortyQ",
6
6
  "license": "MIT",
@@ -22,7 +22,9 @@
22
22
  "request",
23
23
  "auth",
24
24
  "refresh",
25
- "muza"
25
+ "muza",
26
+ "rest",
27
+ "crud"
26
28
  ],
27
29
  "type": "module",
28
30
  "types": "./dist/index.d.ts",
@@ -42,6 +44,7 @@
42
44
  },
43
45
  "scripts": {
44
46
  "build": "tsup",
47
+ "test": "vitest",
45
48
  "dev": "tsup --watch"
46
49
  },
47
50
  "peerDependencies": {
@@ -53,10 +56,13 @@
53
56
  "@semantic-release/github": "^12.0.3",
54
57
  "@semantic-release/npm": "^13.1.3",
55
58
  "@types/node": "^24.10.10",
59
+ "@vue/test-utils": "^2.4.6",
56
60
  "axios": "^1.13.4",
61
+ "happy-dom": "^20.5.0",
57
62
  "semantic-release": "^25.0.3",
58
63
  "tsup": "^8.5.1",
59
64
  "typescript": "^5.9.3",
65
+ "vitest": "^4.0.18",
60
66
  "vue": "^3.5.27"
61
67
  }
62
68
  }