@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 +49 -3
- package/dist/index.cjs +62 -2
- package/dist/index.d.cts +15 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.mjs +62 -2
- package/package.json +8 -2
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.
|
|
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
|
}
|