import { ref, shallowRef, watch, type Ref, type ShallowRef, type UnwrapRef } from 'vue'; import { isFunction, sleep } from '@robonen/stdlib'; export interface UseAsyncStateOptions { delay?: number; shallow?: Shallow; immediate?: boolean; resetOnExecute?: boolean; throwError?: boolean; onError?: (error: unknown) => void; onSuccess?: (data: Data) => void; } export interface UseAsyncStateReturnBase { state: Shallow extends true ? ShallowRef : Ref>; isLoading: Ref; isReady: Ref; error: Ref; execute: (delay?: number, ...params: Params) => Promise; executeImmediately: (...params: Params) => Promise; } export type UseAsyncStateReturn = & UseAsyncStateReturnBase & PromiseLike>; /** * @name useAsyncState * @category State * @description A composable that provides a state for async operations without setup blocking */ export function useAsyncState( maybePromise: Promise | ((...args: Params) => Promise), initialState: Data, options?: UseAsyncStateOptions, ): UseAsyncStateReturn { const { delay = 0, shallow = true, immediate = true, resetOnExecute = false, throwError = false, onError, onSuccess, } = options ?? {}; const state = shallow ? shallowRef(initialState) : ref(initialState); const error = ref(null); const isLoading = ref(false); const isReady = ref(false); const execute = async (actualDelay = delay, ...params: any[]) => { if (resetOnExecute) state.value = initialState; isLoading.value = true; isReady.value = false; error.value = null; if (actualDelay > 0) await sleep(actualDelay); const promise = isFunction(maybePromise) ? maybePromise(...params as Params) : maybePromise; try { const data = await promise; state.value = data; isReady.value = true; onSuccess?.(data); } catch (e: unknown) { error.value = e; onError?.(e); if (throwError) throw e; } finally { isLoading.value = false; } return state.value as Data; }; const executeImmediately = (...params: Params) => { return execute(0, ...params); }; if (immediate) execute(); const shell = { state: state as Shallow extends true ? ShallowRef : Ref>, isLoading, isReady, error, execute, executeImmediately, }; function waitResolve() { return new Promise>((resolve, reject) => { watch( isLoading, (loading) => { if (loading === false) error.value ? reject(error.value) : resolve(shell); }, { immediate: true, once: true, flush: 'sync', }, ); }); } return { ...shell, then(onFulfilled, onRejected) { return waitResolve().then(onFulfilled, onRejected); }, } }