From 4d9bd6fea172c06e03be33bccbbf7445fd6be576 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Feb 2026 15:34:49 +0000 Subject: [PATCH] feat(vue): use cancellablePromise in useAsyncState for promise cancellation Co-authored-by: robonen <26167508+robonen@users.noreply.github.com> --- .../state/useAsyncState/index.test.ts | 7 ++++ .../composables/state/useAsyncState/index.ts | 34 ++++++++++++------- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/web/vue/src/composables/state/useAsyncState/index.test.ts b/web/vue/src/composables/state/useAsyncState/index.test.ts index c0e55c6..b2c2fe1 100644 --- a/web/vue/src/composables/state/useAsyncState/index.test.ts +++ b/web/vue/src/composables/state/useAsyncState/index.test.ts @@ -22,6 +22,7 @@ describe(useAsyncState, () => { expect(isLoading.value).toBeTruthy(); expect(error.value).toBe(null); + await nextTick(); await nextTick(); expect(state.value).toBe('data'); @@ -41,6 +42,7 @@ describe(useAsyncState, () => { expect(isLoading.value).toBeTruthy(); expect(error.value).toBe(null); + await nextTick(); await nextTick(); expect(state.value).toBe('data'); @@ -60,6 +62,7 @@ describe(useAsyncState, () => { expect(isLoading.value).toBeTruthy(); expect(error.value).toBe(null); + await nextTick(); await nextTick(); expect(state.value).toBe('initial'); @@ -77,6 +80,7 @@ describe(useAsyncState, () => { { onSuccess }, ); + await nextTick(); await nextTick(); expect(onSuccess).toHaveBeenCalledWith('data'); @@ -92,6 +96,7 @@ describe(useAsyncState, () => { { onError }, ); + await nextTick(); await nextTick(); expect(onError).toHaveBeenCalledWith(error); @@ -164,6 +169,7 @@ describe(useAsyncState, () => { expect(isReady.value).toBeFalsy(); expect(error.value).toBe(null); + await nextTick(); await nextTick(); expect(state.value).toBe('data'); @@ -289,6 +295,7 @@ describe(useAsyncState, () => { executeImmediately(); resolvePromise!('new data'); await nextTick(); + await nextTick(); expect(state.value).toBe('new data'); expect(isReady.value).toBeTruthy(); diff --git a/web/vue/src/composables/state/useAsyncState/index.ts b/web/vue/src/composables/state/useAsyncState/index.ts index a488fd9..a92636d 100644 --- a/web/vue/src/composables/state/useAsyncState/index.ts +++ b/web/vue/src/composables/state/useAsyncState/index.ts @@ -1,6 +1,6 @@ import { ref, shallowRef, watch } from 'vue'; import type { Ref, ShallowRef, UnwrapRef } from 'vue'; -import { isFunction, sleep } from '@robonen/stdlib'; +import { isFunction, sleep, cancellablePromise as makeCancellable, CancelledError } from '@robonen/stdlib'; export interface UseAsyncStateOptions { delay?: number; @@ -51,10 +51,13 @@ export function useAsyncState void) | undefined; const execute = async (actualDelay = delay, ...params: any[]) => { - const currentVersion = ++version; + cancelPending?.(); + + let active = true; + cancelPending = () => { active = false; }; if (resetOnExecute) state.value = initialState; @@ -66,20 +69,25 @@ export function useAsyncState 0) await sleep(actualDelay); - const promise = isFunction(maybePromise) ? maybePromise(...params as Params) : maybePromise; + if (!active) + return state.value as Data; + + const rawPromise = isFunction(maybePromise) ? maybePromise(...params as Params) : maybePromise; + const { promise, cancel } = makeCancellable(rawPromise); + cancelPending = (reason?: string) => { + active = false; + cancel(reason); + }; try { const data = await promise; - if (currentVersion !== version) - return state.value as Data; - state.value = data; isReady.value = true; onSuccess?.(data); } catch (e: unknown) { - if (currentVersion !== version) + if (e instanceof CancelledError) return state.value as Data; error.value = e; @@ -89,7 +97,7 @@ export function useAsyncState { - version++; + cancelPending?.(); + cancelPending = undefined; isLoading.value = false; }; @@ -120,10 +129,12 @@ export function useAsyncState>((resolve, reject) => { - watch( + const unwatch = watch( isLoading, (loading) => { if (loading === false) { + unwatch(); + if (error.value) reject(error.value); else @@ -132,7 +143,6 @@ export function useAsyncState