vfetcher
Version:
Vue composables for fetching data, based on unjs/ofetch
449 lines (435 loc) • 11.7 kB
JavaScript
import { getCurrentScope, onScopeDispose, toValue, isRef, ref, readonly, shallowRef, watch, reactive, computed } from 'vue';
import { $fetch } from '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 (getCurrentScope()) {
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 = toValue(ms);
const maxDuration = 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 (!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 = 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 = 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);
}, toValue(interval));
}
if (immediate) {
isPending.value = true;
start();
}
tryOnScopeDispose(stop);
return {
isPending: readonly(isPending),
start,
stop
};
}
function useTimeoutPoll(fn, interval, timeoutPollOptions) {
const { start } = useTimeoutFn(loop, interval, { immediate: false });
const isActive = 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 = ref("idle");
const pending = ref(false);
const data = shallowRef(ctx.optionsComposable.default());
const error = 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 = () => 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();
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 = reactive(ctx.optionsWatch);
const req = computed(() => toValue(_req));
return useAsyncData(
() => $fetch(toValue(req), {
...ctx.options$fetch,
...Object.fromEntries(
Object.entries(toValue(watchOptions)).map(([k, v]) => [k, 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 = ref(defaultPageSize);
const pageCurrent = ref(1);
const assignPaginationKey = (p = {}) => ({
...p,
[pageCurrentKey]: p[pageCurrentKey] || toValue(pageCurrent),
[pageSizeKey]: p[pageSizeKey] || 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 = computed(() => get(val.data.value, totalKey, 0));
const pageTotal = 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();
export { useAsyncData, useFetch, usePagination };