fix: make useApiRequest loading state concurrency-safe

Replace the single boolean isLoading ref with a pending-request counter
exposed as a computed, so concurrent executeRequest calls on the same
composable instance no longer flip isLoading back to false while siblings
are still in flight. Update service mocks to the new ComputedRef type.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Matt Miller
2026-07-16 17:57:44 -07:00
parent 8126713177
commit e68ba626ba
5 changed files with 36 additions and 11 deletions

View File

@@ -38,6 +38,29 @@ describe('useApiRequest', () => {
expect(error.value).toBeNull()
})
it('stays loading until every concurrent request settles', async () => {
const { isLoading, executeRequest } = useApiRequest({
client,
mapError: vi.fn()
})
let resolveFirst!: (value: AxiosResponse<string>) => void
const first = executeRequest(
() => new Promise<AxiosResponse<string>>((res) => (resolveFirst = res)),
{ errorContext: 'ctx' }
)
const second = executeRequest(async () => response('second'), {
errorContext: 'ctx'
})
await second
expect(isLoading.value).toBe(true)
resolveFirst(response('first'))
await first
expect(isLoading.value).toBe(false)
})
it('passes the injected client to the api call', async () => {
const apiCall = vi.fn(async () => response(1))
const { executeRequest } = useApiRequest({ client, mapError: vi.fn() })

View File

@@ -1,5 +1,5 @@
import type { AxiosInstance, AxiosResponse } from 'axios'
import { ref } from 'vue'
import { computed, ref } from 'vue'
import { isAbortError } from '@/utils/typeGuardUtil'
@@ -33,7 +33,8 @@ export function useApiRequest({
client: AxiosInstance
mapError: ApiErrorMapper
}) {
const isLoading = ref(false)
const pendingCount = ref(0)
const isLoading = computed(() => pendingCount.value > 0)
const error = ref<string | null>(null)
async function executeRequest<T>(
@@ -42,7 +43,7 @@ export function useApiRequest({
): Promise<T | null> {
const { errorContext, routeSpecificErrors, onSuccess } = options
isLoading.value = true
pendingCount.value++
error.value = null
try {
@@ -55,7 +56,7 @@ export function useApiRequest({
error.value = mapError(err, errorContext, routeSpecificErrors)
return null
} finally {
isLoading.value = false
pendingCount.value--
}
}

View File

@@ -1,7 +1,8 @@
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
import type { ComputedRef } from 'vue'
import { computed, ref } from 'vue'
import { useComfyRegistryService } from '@/services/comfyRegistryService'
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
@@ -69,7 +70,7 @@ const mockListResult: operations['listAllNodes']['responses'][200]['content']['a
describe('useComfyRegistryStore', () => {
let mockRegistryService: {
isLoading: ReturnType<typeof ref<boolean>>
isLoading: ComputedRef<boolean>
error: ReturnType<typeof ref<string | null>>
listAllPacks: ReturnType<typeof vi.fn>
getPackById: ReturnType<typeof vi.fn>
@@ -87,7 +88,7 @@ describe('useComfyRegistryStore', () => {
setActivePinia(createTestingPinia({ stubActions: false }))
vi.clearAllMocks()
mockRegistryService = {
isLoading: ref(false),
isLoading: computed(() => false),
error: ref(null),
listAllPacks: vi.fn().mockImplementation((params) => {
// If node_id is provided, return specific nodes

View File

@@ -120,7 +120,7 @@ describe('useConflictDetection', () => {
const mockComfyManagerService = {
getImportFailInfoBulk: vi.fn(),
isLoading: ref(false),
isLoading: computed(() => false),
error: ref<string | null>(null)
} as Partial<ReturnType<typeof useComfyManagerService>> as ReturnType<
typeof useComfyManagerService
@@ -128,7 +128,7 @@ describe('useConflictDetection', () => {
const mockRegistryService = {
getBulkNodeVersions: vi.fn(),
isLoading: ref(false),
isLoading: computed(() => false),
error: ref<string | null>(null)
} as Partial<ReturnType<typeof useComfyRegistryService>> as ReturnType<
typeof useComfyRegistryService

View File

@@ -1,7 +1,7 @@
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick, ref } from 'vue'
import { computed, nextTick, ref } from 'vue'
import { useComfyManagerService } from '@/workbench/extensions/manager/services/comfyManagerService'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
@@ -80,7 +80,7 @@ describe('useComfyManagerStore', () => {
setActivePinia(createTestingPinia({ stubActions: false }))
vi.clearAllMocks()
mockManagerService = {
isLoading: ref(false),
isLoading: computed(() => false),
error: ref(null),
startQueue: vi.fn().mockResolvedValue(null),
getQueueStatus: vi.fn().mockResolvedValue(null),