mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
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:
@@ -93,7 +93,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useEventListener, whenever } from '@vueuse/core'
|
||||
import { until, useEventListener, whenever } from '@vueuse/core'
|
||||
import {
|
||||
computed,
|
||||
nextTick,
|
||||
@@ -129,7 +129,7 @@ import { useCopy } from '@/composables/useCopy'
|
||||
import { useGlobalLitegraph } from '@/composables/useGlobalLitegraph'
|
||||
import { usePaste } from '@/composables/usePaste'
|
||||
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
||||
import { mergeCustomNodesI18n, t } from '@/i18n'
|
||||
import { t } from '@/i18n'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { useLitegraphSettings } from '@/platform/settings/composables/useLitegraphSettings'
|
||||
import { CORE_SETTINGS } from '@/platform/settings/constants/coreSettings'
|
||||
@@ -144,12 +144,15 @@ import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteracti
|
||||
import TransformPane from '@/renderer/core/layout/transform/TransformPane.vue'
|
||||
import MiniMap from '@/renderer/extensions/minimap/MiniMap.vue'
|
||||
import LGraphNode from '@/renderer/extensions/vueNodes/components/LGraphNode.vue'
|
||||
import { UnauthorizedError, api } from '@/scripts/api'
|
||||
import { UnauthorizedError } from '@/scripts/api'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
import { ChangeTracker } from '@/scripts/changeTracker'
|
||||
import { IS_CONTROL_WIDGET, updateControlWidgetLabel } from '@/scripts/widgets'
|
||||
import { useColorPaletteService } from '@/services/colorPaletteService'
|
||||
import { newUserService } from '@/services/newUserService'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
import { useBootstrapStore } from '@/stores/bootstrapStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
@@ -180,6 +183,9 @@ const toastStore = useToastStore()
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
const colorPaletteService = useColorPaletteService()
|
||||
const canvasInteractions = useCanvasInteractions()
|
||||
const bootstrapStore = useBootstrapStore()
|
||||
const { isI18nReady, i18nError, isSettingsReady, settingsError } =
|
||||
storeToRefs(bootstrapStore)
|
||||
|
||||
const betaMenuEnabled = computed(
|
||||
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
|
||||
@@ -386,15 +392,6 @@ useEventListener(
|
||||
{ passive: true }
|
||||
)
|
||||
|
||||
const loadCustomNodesI18n = async () => {
|
||||
try {
|
||||
const i18nData = await api.getCustomNodesI18n()
|
||||
mergeCustomNodesI18n(i18nData)
|
||||
} catch (error) {
|
||||
console.error('Failed to load custom nodes i18n', error)
|
||||
}
|
||||
}
|
||||
|
||||
const comfyAppReady = ref(false)
|
||||
const workflowPersistence = useWorkflowPersistence()
|
||||
const { flags } = useFeatureFlags()
|
||||
@@ -404,13 +401,14 @@ useCanvasDrop(canvasRef)
|
||||
useLitegraphSettings()
|
||||
useNodeBadge()
|
||||
|
||||
useGlobalLitegraph()
|
||||
useContextMenuTranslation()
|
||||
useVueFeatureFlags()
|
||||
|
||||
onMounted(async () => {
|
||||
useGlobalLitegraph()
|
||||
useContextMenuTranslation()
|
||||
useCopy()
|
||||
usePaste()
|
||||
useWorkflowAutoSave()
|
||||
useVueFeatureFlags()
|
||||
|
||||
comfyApp.vueAppReady = true
|
||||
|
||||
@@ -418,21 +416,34 @@ onMounted(async () => {
|
||||
// ChangeTracker needs to be initialized before setup, as it will overwrite
|
||||
// some listeners of litegraph canvas.
|
||||
ChangeTracker.init()
|
||||
await loadCustomNodesI18n()
|
||||
try {
|
||||
await settingStore.loadSettingValues()
|
||||
} catch (error) {
|
||||
if (error instanceof UnauthorizedError) {
|
||||
|
||||
await until(() => isSettingsReady.value || !!settingsError.value).toBe(true)
|
||||
|
||||
if (settingsError.value) {
|
||||
if (settingsError.value instanceof UnauthorizedError) {
|
||||
localStorage.removeItem('Comfy.userId')
|
||||
localStorage.removeItem('Comfy.userName')
|
||||
window.location.reload()
|
||||
} else {
|
||||
throw error
|
||||
return
|
||||
}
|
||||
throw settingsError.value
|
||||
}
|
||||
|
||||
// Register core settings immediately after settings are ready
|
||||
CORE_SETTINGS.forEach(settingStore.addSetting)
|
||||
|
||||
await newUserService().initializeIfNewUser(settingStore)
|
||||
// Wait for both i18n and newUserService in parallel
|
||||
// (newUserService only needs settings, not i18n)
|
||||
await Promise.all([
|
||||
until(() => isI18nReady.value || !!i18nError.value).toBe(true),
|
||||
newUserService().initializeIfNewUser(settingStore)
|
||||
])
|
||||
if (i18nError.value) {
|
||||
console.warn(
|
||||
'[GraphCanvas] Failed to load custom nodes i18n:',
|
||||
i18nError.value
|
||||
)
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
await comfyApp.setup(canvasRef.value)
|
||||
|
||||
@@ -14,6 +14,7 @@ import { VueFire, VueFireAuth } from 'vuefire'
|
||||
import { getFirebaseConfig } from '@/config/firebase'
|
||||
import '@/lib/litegraph/public/css/litegraph.css'
|
||||
import router from '@/router'
|
||||
import { useBootstrapStore } from '@/stores/bootstrapStore'
|
||||
|
||||
import App from './App.vue'
|
||||
// Intentionally relative import to ensure the CSS is loaded in the right order (after litegraph.css)
|
||||
@@ -43,6 +44,10 @@ const firebaseApp = initializeApp(getFirebaseConfig())
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
// Start early bootstrap (api.init, fetchNodeDefs) before Pinia is registered
|
||||
const bootstrapStore = useBootstrapStore(pinia)
|
||||
bootstrapStore.startEarlyBootstrap()
|
||||
Sentry.init({
|
||||
app,
|
||||
dsn: __SENTRY_DSN__,
|
||||
@@ -88,4 +93,7 @@ app
|
||||
modules: [VueFireAuth()]
|
||||
})
|
||||
|
||||
// Start store bootstrap (settings, i18n, workflows) after Pinia is registered
|
||||
void bootstrapStore.startStoreBootstrap()
|
||||
|
||||
app.mount('#vue-app')
|
||||
|
||||
68
src/stores/bootstrapStore.test.ts
Normal file
68
src/stores/bootstrapStore.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
112
src/stores/bootstrapStore.ts
Normal file
112
src/stores/bootstrapStore.ts
Normal 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
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user