UNPKG

vfetcher

Version:

Vue composables for fetching data, based on unjs/ofetch

453 lines (438 loc) 11.8 kB
'use strict'; const vue = require('vue'); const ofetch = require('ofetch'); function noop() { } function toArray(array) { array = array ?? []; return Array.isArray(array) ? array : [array]; } function get(value, path, defaultValue) { const segments = path.split(/[.[\]]/g); let current = value; for (const key of segments) { if (current === null) return defaultValue; if (current === void 0) return defaultValue; const deQuoted = key.replace(/['"]/g, ""); if (deQuoted.trim() === "") continue; current = current[deQuoted]; } if (current === void 0) return defaultValue; return current; } function clearUndefined(obj) { Object.keys(obj).forEach((key) => obj[key] === void 0 ? delete obj[key] : {}); return obj; } function createContext(options) { const { /** composables options */ immediate = true, watch = [], pollingInterval, debounceInterval, throttleInterval, ready = true, transform = (r) => r, /** watch options */ agent, baseURL, body, dispatcher, headers, method, params, query, /** ofetch options */ cache, credentials, duplex, ignoreResponseError, integrity, keepalive, mode, onRequest, onRequestError, onResponse, onResponseError, parseResponse, priority, redirect, referrer, referrerPolicy, responseType, retry, retryDelay, retryStatusCodes, signal, timeout, window } = options; return { optionsComposable: { immediate, watch, pollingInterval, debounceInterval, throttleInterval, ready, transform, default: options.default ?? (() => null) }, optionsWatch: clearUndefined({ agent, baseURL, body, dispatcher, headers, method, params, query }), options$fetch: { cache, credentials, duplex, ignoreResponseError, integrity, keepalive, mode, onRequest, onRequestError, onResponse, onResponseError, parseResponse, priority, redirect, referrer, referrerPolicy, responseType, retry, retryDelay, retryStatusCodes, signal, timeout, window } }; } function pipe(...fns) { return fns.length === 1 ? fns[0] : (input) => fns.reduce(pipeReducer, input); } function pipeReducer(prev, fn) { return fn(prev); } function tryOnScopeDispose(fn) { if (vue.getCurrentScope()) { vue.onScopeDispose(fn); return true; } return false; } function createFilterWrapper(filter, fn) { function wrapper(...args) { return new Promise((resolve, reject) => { Promise.resolve(filter(() => fn.apply(this, args), { fn, thisArg: this, args })).then(resolve).catch(reject); }); } return wrapper; } function useDebounceFn(fn, ms = 200, options = {}) { return createFilterWrapper( debounceFilter(ms, options), fn ); } function debounceFilter(ms, options = {}) { let timer; let maxTimer; let lastRejector = noop; const _clearTimeout = (timer2) => { clearTimeout(timer2); lastRejector(); lastRejector = noop; }; const filter = (invoke) => { const duration = vue.toValue(ms); const maxDuration = vue.toValue(options.maxWait); if (timer) _clearTimeout(timer); if (duration <= 0 || maxDuration !== void 0 && maxDuration <= 0) { if (maxTimer) { _clearTimeout(maxTimer); maxTimer = null; } return Promise.resolve(invoke()); } return new Promise((resolve, reject) => { lastRejector = options.rejectOnCancel ? reject : resolve; if (maxDuration && !maxTimer) { maxTimer = setTimeout(() => { if (timer) _clearTimeout(timer); maxTimer = null; resolve(invoke()); }, maxDuration); } timer = setTimeout(() => { if (maxTimer) _clearTimeout(maxTimer); maxTimer = null; resolve(invoke()); }, duration); }); }; return filter; } function useThrottleFn(fn, ms = 200, trailing = false, leading = true, rejectOnCancel = false) { return createFilterWrapper( throttleFilter(ms, trailing, leading, rejectOnCancel), fn ); } function throttleFilter(...args) { let lastExec = 0; let timer; let isLeading = true; let lastRejector = noop; let lastValue; let ms; let trailing; let leading; let rejectOnCancel; if (!vue.isRef(args[0]) && typeof args[0] === "object") ({ delay: ms, trailing = true, leading = true, rejectOnCancel = false } = args[0]); else [ms, trailing = true, leading = true, rejectOnCancel = false] = args; const clear = () => { if (timer) { clearTimeout(timer); timer = void 0; lastRejector(); lastRejector = noop; } }; const filter = (_invoke) => { const duration = vue.toValue(ms); const elapsed = Date.now() - lastExec; const invoke = () => { return lastValue = _invoke(); }; clear(); if (duration <= 0) { lastExec = Date.now(); return invoke(); } if (elapsed > duration && (leading || !isLeading)) { lastExec = Date.now(); invoke(); } else if (trailing) { lastValue = new Promise((resolve, reject) => { lastRejector = rejectOnCancel ? reject : resolve; timer = setTimeout(() => { lastExec = Date.now(); isLeading = true; resolve(invoke()); clear(); }, Math.max(0, duration - elapsed)); }); } if (!leading && !timer) timer = setTimeout(() => isLeading = true, duration); isLeading = false; return lastValue; }; return filter; } function useTimeoutFn(cb, interval, options = {}) { const { immediate = true } = options; const isPending = vue.ref(false); let timer = null; function clear() { if (timer) { clearTimeout(timer); timer = null; } } function stop() { isPending.value = false; clear(); } function start(...args) { clear(); isPending.value = true; timer = setTimeout(() => { isPending.value = false; timer = null; cb(...args); }, vue.toValue(interval)); } if (immediate) { isPending.value = true; start(); } tryOnScopeDispose(stop); return { isPending: vue.readonly(isPending), start, stop }; } function useTimeoutPoll(fn, interval, timeoutPollOptions) { const { start } = useTimeoutFn(loop, interval, { immediate: false }); const isActive = vue.ref(false); async function loop() { if (!isActive.value) return; await fn(); start(); } async function resume() { if (!isActive.value) { isActive.value = true; await loop(); } } function pause() { isActive.value = false; } if (timeoutPollOptions?.immediate) resume(); tryOnScopeDispose(pause); return { isActive, pause, resume }; } function createUseAsyncData(defaultOptions = {}) { const useAsyncData = function(request, options = {}) { const ctx = createContext({ ...options, ...defaultOptions }); const status = vue.ref("idle"); const pending = vue.ref(false); const data = vue.shallowRef(ctx.optionsComposable.default()); const error = vue.ref(null); const executeRequest = async () => { try { status.value = "pending"; pending.value = true; data.value = await ctx.optionsComposable.transform(await request()); status.value = "success"; } catch (e) { error.value = e instanceof Error ? e : new Error(e); status.value = "error"; throw error.value; } finally { pending.value = false; } }; const executePipe = pipe( (r) => ctx.optionsComposable.debounceInterval !== void 0 ? useDebounceFn(r, ctx.optionsComposable.debounceInterval) : r, (r) => ctx.optionsComposable.throttleInterval !== void 0 ? useThrottleFn(r, ctx.optionsComposable.throttleInterval) : r )(executeRequest); const _execute = () => vue.toValue(ctx.optionsComposable.ready) ? executePipe() : Promise.resolve(); let resume; let _isFirstRun = true; if (ctx.optionsComposable.pollingInterval !== void 0) { const { resume: _ } = useTimeoutPoll(_execute, ctx.optionsComposable.pollingInterval); resume = () => { _isFirstRun = false; return _(); }; } const execute = resume ? () => _isFirstRun ? resume() : _execute() : _execute; if (ctx.optionsComposable.immediate) execute(); vue.watch( ctx.optionsComposable.watch === false ? [] : [...toArray(ctx.optionsComposable.watch || [])], () => execute() ); return { data, pending, status, error, execute, refresh: execute }; }; useAsyncData.create = (newDefaultOptions) => createUseAsyncData({ ...defaultOptions, ...newDefaultOptions }); return useAsyncData; } const defaultOptionsKey = Symbol("defaultOptionsKey"); function createUseFetch(defaultOptions = {}) { const useFetch = function(_req, options = {}) { const ctx = createContext({ ...options, ...defaultOptions }); const watchOptions = vue.reactive(ctx.optionsWatch); const req = vue.computed(() => vue.toValue(_req)); return useAsyncData( () => ofetch.$fetch(vue.toValue(req), { ...ctx.options$fetch, ...Object.fromEntries( Object.entries(vue.toValue(watchOptions)).map(([k, v]) => [k, vue.toValue(v)]) ) }), { ...ctx.optionsComposable, watch: ctx.optionsComposable.watch === false ? [] : [watchOptions, req, ...toArray(ctx.optionsComposable.watch || [])] } ); }; useFetch.create = (newDefaultOptions) => createUseFetch({ ...defaultOptions, ...newDefaultOptions }); useFetch[defaultOptionsKey] = { ...defaultOptions }; return useFetch; } function createUsePagination(defaultOptions = {}) { const usePagination = function(_req, options = {}) { const { pageCurrentKey = "current", pageSizeKey = "pageSize", defaultPageSize = 10, totalKey = "total", pageTotalKey = "totalPage", ...useFetchOptions } = { ...defaultOptions, ...options }; const pageSize = vue.ref(defaultPageSize); const pageCurrent = vue.ref(1); const assignPaginationKey = (p = {}) => ({ ...p, [pageCurrentKey]: p[pageCurrentKey] || vue.toValue(pageCurrent), [pageSizeKey]: p[pageSizeKey] || vue.toValue(pageSize) }); const val = useFetch(_req, { ...useFetchOptions, watch: useFetchOptions.watch === false ? [] : [pageSize, pageCurrent, ...toArray(useFetchOptions.watch || [])], onRequest: [ (context) => { context.options.query = assignPaginationKey(context.options.query); context.options.params = assignPaginationKey(context.options.params); }, ...toArray(useFetchOptions?.onRequest).filter(Boolean) ] }); const total = vue.computed(() => get(val.data.value, totalKey, 0)); const pageTotal = vue.computed(() => get(val.data.value, pageTotalKey, Math.ceil(total.value / pageSize.value))); return { ...val, pageCurrent, pageSize, total, pageTotal }; }; usePagination.create = (newDefaultOptions) => createUsePagination({ ...defaultOptions, ...newDefaultOptions }); usePagination[defaultOptionsKey] = { ...defaultOptions }; return usePagination; } const useAsyncData = createUseAsyncData(); const useFetch = createUseFetch(); const usePagination = createUsePagination(); exports.useAsyncData = useAsyncData; exports.useFetch = useFetch; exports.usePagination = usePagination;