From 1e9859da83249cc069cf0f235e2a1e3d2f1777c9 Mon Sep 17 00:00:00 2001 From: robonen Date: Thu, 10 Jul 2025 05:34:12 +0700 Subject: [PATCH] feat(web/vue): enhance async state management for useAsyncState with improved error handling and loading states --- .../src/composables/useAsyncState/index.ts | 87 +++++++++++++++---- 1 file changed, 71 insertions(+), 16 deletions(-) diff --git a/web/vue/src/composables/useAsyncState/index.ts b/web/vue/src/composables/useAsyncState/index.ts index 0091a61..0c80ef7 100644 --- a/web/vue/src/composables/useAsyncState/index.ts +++ b/web/vue/src/composables/useAsyncState/index.ts @@ -1,13 +1,8 @@ -import { ref, shallowRef } from 'vue'; -import { isFunction } from '@robonen/stdlib'; - -export enum AsyncStateStatus { - PENDING, - FULFILLED, - REJECTED, -} +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; @@ -16,6 +11,19 @@ export interface UseAsyncStateOptions { 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 @@ -25,35 +33,82 @@ export function useAsyncState | ((...args: Params) => Promise), initialState: Data, options?: UseAsyncStateOptions, -) { +): UseAsyncStateReturn { const state = options?.shallow ? shallowRef(initialState) : ref(initialState); - const status = ref(null); + const error = ref(null); + const isLoading = ref(false); + const isReady = ref(false); - const execute = async (...params: any[]) => { + const execute = async (delay = options?.delay ?? 0, ...params: any[]) => { if (options?.resetOnExecute) state.value = initialState; - status.value = AsyncStateStatus.PENDING; + isLoading.value = true; + isReady.value = false; + error.value = null; + + if (delay > 0) + await sleep(delay); const promise = isFunction(maybePromise) ? maybePromise(...params as Params) : maybePromise; try { const data = await promise; state.value = data; - status.value = AsyncStateStatus.FULFILLED; + isReady.value = true; options?.onSuccess?.(data); } - catch (error) { - status.value = AsyncStateStatus.REJECTED; - options?.onError?.(error); + catch (e: unknown) { + error.value = e; + options?.onError?.(e); if (options?.throwError) throw error; } + finally { + isLoading.value = false; + } return state.value as Data; }; + const executeImmediately = (...params: Params) => { + return execute(0, ...params); + }; + if (options?.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); + }, + } }