@ametie/vue-muza-use 0.2.0 → 0.3.1

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
@@ -200,6 +200,38 @@ useApiPost('/user/settings', {
200
200
  })
201
201
  ```
202
202
 
203
+ ### ⏰ Auto-Polling (Interval)
204
+ Keep your data fresh with built-in polling support. It works smartly: by default, it **pauses** when the tab is hidden and **resumes immediately** when the user returns.
205
+
206
+ #### Simple Polling
207
+ Fetch data every 5 seconds.
208
+ ```typescript
209
+ const { data } = useApi('/notifications', {
210
+ poll: 5000 // Simple number: interval in ms
211
+ })
212
+ ```
213
+
214
+ #### Smart Control
215
+ Configure pause behavior and interval explicitly.
216
+ ```typescript
217
+ useApi('/stock-prices', {
218
+ poll: {
219
+ interval: 1000,
220
+ whenHidden: true // ⚠️ Keep polling even if tab is backgrounded
221
+ }
222
+ })
223
+ ```
224
+
225
+ #### Reactive Polling
226
+ You can pass a `Ref` to control polling dynamically. Perfect for "Pause/Resume" buttons.
227
+ ```typescript
228
+ const interval = ref(3000)
229
+
230
+ // If you set interval.value = 0, polling stops.
231
+ // If you change it to 5000, it restarts with new interval immediately.
232
+ useApi('/live-feed', { poll: interval })
233
+ ```
234
+
203
235
  ### Global Abort (Race Condition Killer)
204
236
  Useful for complex filters where changing one filter should invalidate all pending requests on the page (or scope).
205
237
 
@@ -290,6 +322,7 @@ The main composable.
290
322
  | `retry` | `boolean \| number` | `false` | Number of retries on failure. |
291
323
  | `debounce` | `number` | `0` | Debounce time in ms. |
292
324
  | `watch` | `WatchSource \| WatchSource[]` | `undefined` | Ref(s) to watch for auto-execution. |
325
+ | `poll` | `number \| { interval: number, whenHidden?: boolean } \| Ref` | `0` | Polling interval. Default pauses when hidden. |
293
326
  | `authMode` | `'default' \| 'public'` | `'default'` | `'public'` skips token injection. |
294
327
  | `initialData` | `T` | `null` | Initial value for `data` ref. |
295
328
  | `onSuccess` | `(res) => void` | - | Callback on 2xx response. |
package/dist/index.cjs CHANGED
@@ -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();
@@ -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>;
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>;
package/dist/index.mjs CHANGED
@@ -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();
@@ -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.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Powerful Vue 3 API composable (Muza Kit) with Axios, Auto-Refresh & TypeScript",
5
5
  "author": "MortyQ",
6
6
  "license": "MIT",
@@ -44,6 +44,7 @@
44
44
  },
45
45
  "scripts": {
46
46
  "build": "tsup",
47
+ "test": "vitest",
47
48
  "dev": "tsup --watch"
48
49
  },
49
50
  "peerDependencies": {
@@ -55,10 +56,13 @@
55
56
  "@semantic-release/github": "^12.0.3",
56
57
  "@semantic-release/npm": "^13.1.3",
57
58
  "@types/node": "^24.10.10",
59
+ "@vue/test-utils": "^2.4.6",
58
60
  "axios": "^1.13.4",
61
+ "happy-dom": "^20.5.0",
59
62
  "semantic-release": "^25.0.3",
60
63
  "tsup": "^8.5.1",
61
64
  "typescript": "^5.9.3",
65
+ "vitest": "^4.0.18",
62
66
  "vue": "^3.5.27"
63
67
  }
64
68
  }