refactor: extract early bootstrap logic to bootstrapStore

- Add useBootstrapStore to centralize early initialization (api.init, fetchNodeDefs)
- Move settings loading and custom nodes i18n loading to store bootstrap phase
- Use VueUse's `until` to coordinate async dependencies in GraphCanvas
- Load settings, i18n, and newUserService initialization in parallel where possible
- Add unit tests for bootstrapStore

Amp-Thread-ID: https://ampcode.com/threads/T-019bf48d-af90-738f-99ce-46309e4be688
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Alexander Brown
2026-01-25 01:48:34 -08:00
parent 33c457aca0
commit 326154f2b2
4 changed files with 222 additions and 23 deletions

View File

@@ -0,0 +1,68 @@
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useBootstrapStore } from './bootstrapStore'
vi.mock('@/scripts/api', () => ({
api: {
init: vi.fn().mockResolvedValue(undefined),
getNodeDefs: vi.fn().mockResolvedValue({ TestNode: { name: 'TestNode' } }),
getCustomNodesI18n: vi.fn().mockResolvedValue({}),
getUserConfig: vi.fn().mockResolvedValue({})
}
}))
vi.mock('@/i18n', () => ({
mergeCustomNodesI18n: vi.fn()
}))
vi.mock('@/platform/settings/settingStore', () => ({
useSettingStore: vi.fn(() => ({
loadSettingValues: vi.fn().mockResolvedValue(undefined)
}))
}))
vi.mock('@/stores/workspaceStore', () => ({
useWorkspaceStore: vi.fn(() => ({
workflow: {
syncWorkflows: vi.fn().mockResolvedValue(undefined)
}
}))
}))
describe('bootstrapStore', () => {
let store: ReturnType<typeof useBootstrapStore>
beforeEach(() => {
setActivePinia(createPinia())
store = useBootstrapStore()
vi.clearAllMocks()
})
it('initializes with all flags false', () => {
expect(store.isNodeDefsReady).toBe(false)
expect(store.isSettingsReady).toBe(false)
expect(store.isI18nReady).toBe(false)
})
it('starts early bootstrap (node defs)', async () => {
const { api } = await import('@/scripts/api')
store.startEarlyBootstrap()
await vi.waitFor(() => {
expect(store.isNodeDefsReady).toBe(true)
})
expect(api.getNodeDefs).toHaveBeenCalled()
})
it('starts store bootstrap (settings, i18n)', async () => {
void store.startStoreBootstrap()
await vi.waitFor(() => {
expect(store.isSettingsReady).toBe(true)
expect(store.isI18nReady).toBe(true)
})
})
})

View File

@@ -0,0 +1,112 @@
import { useAsyncState } from '@vueuse/core'
import { defineStore } from 'pinia'
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
import { api } from '@/scripts/api'
import { useUserStore } from '@/stores/userStore'
export const useBootstrapStore = defineStore('bootstrap', () => {
const {
state: nodeDefs,
isReady: isNodeDefsReady,
error: nodeDefsError,
execute: fetchNodeDefs
} = useAsyncState<Record<string, ComfyNodeDef>>(
async () => {
const defs = await api.getNodeDefs()
return defs
},
{},
{ immediate: false }
)
const {
isReady: isSettingsReady,
isLoading: isSettingsLoading,
error: settingsError,
execute: executeLoadSettings
} = useAsyncState(
async () => {
const { useSettingStore } =
await import('@/platform/settings/settingStore')
await useSettingStore().loadSettingValues()
},
undefined,
{ immediate: false }
)
function loadSettings() {
// TODO: This check makes the store "sticky" across logouts. Add a reset
// method to clear isSettingsReady, then replace window.location.reload()
// with router.push() in SidebarLogoutIcon.vue
if (!isSettingsReady.value && !isSettingsLoading.value) {
void executeLoadSettings()
}
}
const {
isReady: isI18nReady,
error: i18nError,
execute: loadI18n
} = useAsyncState(
async () => {
const { mergeCustomNodesI18n } = await import('@/i18n')
const i18nData = await api.getCustomNodesI18n()
mergeCustomNodesI18n(i18nData)
},
undefined,
{ immediate: false }
)
const {
isReady: isWorkflowsReady,
isLoading: isWorkflowsLoading,
execute: executeSyncWorkflows
} = useAsyncState(
async () => {
const { useWorkspaceStore } = await import('@/stores/workspaceStore')
await useWorkspaceStore().workflow.syncWorkflows()
},
undefined,
{ immediate: false }
)
function syncWorkflows() {
if (!isWorkflowsReady.value && !isWorkflowsLoading.value) {
void executeSyncWorkflows()
}
}
function startEarlyBootstrap() {
void fetchNodeDefs()
}
async function startStoreBootstrap() {
// Defer settings and workflows if multi-user login is required
// (settings API requires authentication in multi-user mode)
const userStore = useUserStore()
await userStore.initialize()
// i18n can load without authentication
void loadI18n()
if (!userStore.needsLogin) {
loadSettings()
syncWorkflows()
}
}
return {
nodeDefs,
isNodeDefsReady,
nodeDefsError,
isSettingsReady,
settingsError,
isI18nReady,
i18nError,
startEarlyBootstrap,
startStoreBootstrap,
loadSettings,
syncWorkflows
}
})