vue-condition-watcher
Version:
Vue composition API for automatic data fetching. With conditions as the core. Easily control and sync to URL query string by conditions
262 lines (261 loc) • 11.2 kB
JavaScript
import { computed, isRef, onUnmounted, reactive, readonly, ref, shallowRef, unref, watch, watchEffect, } from 'vue-demi';
import { containsProp, isNoData as isDataEmpty, isObject, isServer, rAF } from './utils/helper';
import { createParams, deepClone, filterNoneValueObject, isEquivalent, syncQuery2Conditions } from './utils/common';
import { createEvents } from './utils/createEvents';
import { useCache } from './composable/useCache';
import { useHistory } from './composable/useHistory';
import { usePromiseQueue } from './composable/usePromiseQueue';
export default function useConditionWatcher(config) {
function isFetchConfig(obj) {
return containsProp(obj, 'fetcher', 'conditions', 'defaultParams', 'initialData', 'manual', 'immediate', 'history', 'pollingInterval', 'pollingWhenHidden', 'pollingWhenOffline', 'revalidateOnFocus', 'cacheProvider', 'beforeFetch', 'afterFetch', 'onFetchError');
}
function isHistoryOption() {
if (!config.history || !config.history.sync)
return false;
return containsProp(config.history, 'navigation', 'ignore', 'sync');
}
// default config
let watcherConfig = {
fetcher: config.fetcher,
conditions: config.conditions,
immediate: true,
manual: false,
initialData: undefined,
pollingInterval: isRef(config.pollingInterval) ? config.pollingInterval : ref(config.pollingInterval || 0),
pollingWhenHidden: false,
pollingWhenOffline: false,
revalidateOnFocus: false,
cacheProvider: () => new Map(),
};
// update config
if (isFetchConfig(config)) {
watcherConfig = { ...watcherConfig, ...config };
}
const cache = useCache(watcherConfig.fetcher, watcherConfig.cacheProvider());
const backupIntiConditions = deepClone(watcherConfig.conditions);
const _conditions = reactive(watcherConfig.conditions);
const isFetching = ref(false);
const isOnline = ref(true);
const isActive = ref(true);
const data = shallowRef(cache.cached(backupIntiConditions) ? cache.get(backupIntiConditions) : watcherConfig.initialData || undefined);
const error = ref(undefined);
const query = ref({});
const pollingTimer = ref();
const { enqueue } = usePromiseQueue();
// - create fetch event & condition event & web event
const { conditionEvent, responseEvent, errorEvent, finallyEvent, reconnectEvent, focusEvent, visibilityEvent, stopFocusEvent, stopReconnectEvent, stopVisibilityEvent, } = createEvents();
const resetConditions = (cond) => {
Object.assign(_conditions, isObject(cond) && !cond.type ? cond : backupIntiConditions);
};
const loading = computed(() => !error.value && !data.value);
const conditionsChangeHandler = async (conditions, throwOnFailed = false) => {
const checkThrowOnFailed = typeof throwOnFailed === 'boolean' ? throwOnFailed : false;
if (isFetching.value)
return;
isFetching.value = true;
error.value = undefined;
const conditions2Object = conditions;
let customConditions = {};
const deepCopyCondition = deepClone(conditions2Object);
if (typeof watcherConfig.beforeFetch === 'function') {
let isCanceled = false;
customConditions = await watcherConfig.beforeFetch(deepCopyCondition, () => {
isCanceled = true;
});
if (isCanceled) {
// eslint-disable-next-line require-atomic-updates
isFetching.value = false;
return Promise.resolve(undefined);
}
if (!customConditions || typeof customConditions !== 'object' || customConditions.constructor !== Object) {
// eslint-disable-next-line require-atomic-updates
isFetching.value = false;
throw new Error(`[vue-condition-watcher]: beforeFetch should return an object`);
}
}
const validateCustomConditions = Object.keys(customConditions).length !== 0;
/*
* if custom conditions has value, just use custom conditions
* filterNoneValueObject will filter no value like [] , '', null, undefined
* example. {name: '', items: [], age: 0, tags: null}
* return result will be {age: 0}
*/
query.value = filterNoneValueObject(validateCustomConditions ? customConditions : conditions2Object);
const finalConditions = createParams(query.value, watcherConfig.defaultParams);
let responseData = undefined;
data.value = cache.cached(query.value) ? cache.get(query.value) : watcherConfig.initialData || undefined;
return new Promise((resolve, reject) => {
config
.fetcher(finalConditions)
.then(async (fetchResponse) => {
responseData = fetchResponse;
if (typeof watcherConfig.afterFetch === 'function') {
responseData = await watcherConfig.afterFetch(fetchResponse);
}
if (responseData === undefined) {
console.warn(`[vue-condition-watcher]: "afterFetch" return value is ${responseData}. Please check it.`);
}
if (!isEquivalent(data.value, responseData)) {
data.value = responseData;
}
if (!isEquivalent(cache.get(query.value), responseData)) {
cache.set(query.value, responseData);
}
responseEvent.trigger(responseData);
return resolve(fetchResponse);
})
.catch(async (fetchError) => {
if (typeof watcherConfig.onFetchError === 'function') {
// eslint-disable-next-line @typescript-eslint/no-extra-semi
;
({ data: responseData, error: fetchError } = await watcherConfig.onFetchError({
data: undefined,
error: fetchError,
}));
data.value = responseData || watcherConfig.initialData;
error.value = fetchError;
}
errorEvent.trigger(fetchError);
if (checkThrowOnFailed) {
return reject(fetchError);
}
return resolve(undefined);
})
.finally(() => {
isFetching.value = false;
finallyEvent.trigger();
});
});
};
const revalidate = (throwOnFailed = false) => enqueue(() => conditionsChangeHandler({ ..._conditions }, throwOnFailed));
function execute(throwOnFailed = false) {
if (isDataEmpty(data.value) || isServer) {
revalidate(throwOnFailed);
}
else {
rAF(() => revalidate(throwOnFailed));
}
}
// - Start polling with out setting to manual
if (!watcherConfig.manual) {
watchEffect((onCleanup) => {
if (unref(watcherConfig.pollingInterval)) {
pollingTimer.value = (() => {
let timerId = null;
function next() {
const interval = unref(watcherConfig.pollingInterval);
if (interval && timerId !== -1) {
timerId = setTimeout(nun, interval);
}
}
function nun() {
// Only run when the page is visible, online and not errored.
if (!error.value &&
(watcherConfig.pollingWhenHidden || isActive.value) &&
(watcherConfig.pollingWhenOffline || isOnline.value)) {
revalidate().then(next);
}
else {
next();
}
}
next();
return () => timerId && clearTimeout(timerId);
})();
}
onCleanup(() => {
pollingTimer.value && pollingTimer.value();
});
});
}
// - mutate: Modify `data` directly
// - `data` is read only by default, recommend modify `data` at `afterFetch`
// - When you need to modify `data`, you can use mutate() to directly modify data
/*
* Two way to use mutate
* - 1.
* mutate(newData)
* - 2.
* mutate((currentData) => {
* currentData[0].name = 'runkids'
* return currentData
* })
*/
const mutate = (...args) => {
const arg = args[0];
if (arg === undefined) {
return data.value;
}
if (typeof arg === 'function') {
data.value = arg(deepClone(data.value));
}
else {
data.value = arg;
}
cache.set({ ..._conditions }, data.value);
return data.value;
};
// - History mode base on vue-router
if (isHistoryOption()) {
const historyOption = {
sync: config.history.sync,
ignore: config.history.ignore || [],
navigation: config.history.navigation || 'push',
listener(parsedQuery) {
const queryObject = Object.keys(parsedQuery).length ? parsedQuery : backupIntiConditions;
syncQuery2Conditions(_conditions, queryObject, backupIntiConditions);
},
};
useHistory(query, historyOption);
}
// - Automatic data fetching by default
if (!watcherConfig.manual && watcherConfig.immediate) {
execute();
}
watch(() => ({ ..._conditions }), (nc, oc) => {
// - Deep check object if be true do nothing
if (isEquivalent(nc, oc))
return;
conditionEvent.trigger(deepClone(nc), deepClone(oc));
// - Automatic data fetching until manual to be false
!watcherConfig.manual && enqueue(() => conditionsChangeHandler(nc));
});
reconnectEvent.on((status) => {
isOnline.value = status;
});
visibilityEvent.on((status) => {
isActive.value = status;
});
const stopSubscribeFocus = focusEvent.on(() => {
if (!isActive.value)
return;
execute();
// if (isHistoryOption() && cache.cached({ ..._conditions })) {
//todo sync to query
// }
});
if (!watcherConfig.revalidateOnFocus) {
stopFocusEvent();
stopSubscribeFocus.off();
}
onUnmounted(() => {
pollingTimer.value && pollingTimer.value();
stopFocusEvent();
stopReconnectEvent();
stopVisibilityEvent();
});
return {
conditions: _conditions,
data: readonly(data),
error: readonly(error),
isFetching: readonly(isFetching),
loading,
execute,
mutate,
resetConditions,
onConditionsChange: conditionEvent.on,
onFetchSuccess: responseEvent.on,
onFetchError: errorEvent.on,
onFetchFinally: finallyEvent.on,
};
}