import type { WatchSource, WatchOptions, MaybeRef } from "vue"; import { reactive, watch, ref, computed, unref } from "vue"; import { delay, exclusiveRequest } from "@milaboratories/helpers"; export type FetchResult = { loading: boolean; value: V | undefined; error: E; }; // TODO Should we keep the old value while fetching the new value? /** * Watch any reactive source and perform an asynchronous operation * * @example * ```ts * const v = useWatchFetch( * watchSource, * async (sourceValue) => { * return await fetchDataFromApi(sourceValue); * } * ); * * // Usage in a template * * ``` */ export function useWatchFetch( watchSource: WatchSource, doFetch: (s: S) => Promise, settings?: { watchOptions?: WatchOptions; debounce?: MaybeRef; // debounce time in ms filterWatchResult?: (s: S) => boolean; // prevents fetching if false }, ): FetchResult { const loadingRef = ref(0); const data = reactive({ loading: computed(() => loadingRef.value > 0), loadingRef, value: undefined as V, error: undefined, }) as FetchResult; const exclusive = exclusiveRequest(async (s: S) => { if (settings?.debounce) { await delay(unref(settings.debounce)); } return doFetch(s); }); watch( watchSource, async (s) => { if (settings?.filterWatchResult && !settings.filterWatchResult(s)) { return; } data.error = undefined; loadingRef.value++; exclusive(s) .then((res) => { if (res.ok) { data.value = res.value; } }) .catch((err) => { data.value = undefined; data.error = err; }) .finally(() => { loadingRef.value--; }); }, Object.assign({ immediate: true, deep: true }, settings?.watchOptions ?? {}), ); return data; }