Skip to content

Commit 326154f

Browse files
DrJKLampcode-com
andcommitted
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>
1 parent 33c457a commit 326154f

File tree

4 files changed

+222
-23
lines changed

4 files changed

+222
-23
lines changed

src/components/graph/GraphCanvas.vue

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@
9393
</template>
9494

9595
<script setup lang="ts">
96-
import { useEventListener, whenever } from '@vueuse/core'
96+
import { until, useEventListener, whenever } from '@vueuse/core'
9797
import {
9898
computed,
9999
nextTick,
@@ -129,7 +129,7 @@ import { useCopy } from '@/composables/useCopy'
129129
import { useGlobalLitegraph } from '@/composables/useGlobalLitegraph'
130130
import { usePaste } from '@/composables/usePaste'
131131
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
132-
import { mergeCustomNodesI18n, t } from '@/i18n'
132+
import { t } from '@/i18n'
133133
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
134134
import { useLitegraphSettings } from '@/platform/settings/composables/useLitegraphSettings'
135135
import { CORE_SETTINGS } from '@/platform/settings/constants/coreSettings'
@@ -144,12 +144,15 @@ import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteracti
144144
import TransformPane from '@/renderer/core/layout/transform/TransformPane.vue'
145145
import MiniMap from '@/renderer/extensions/minimap/MiniMap.vue'
146146
import LGraphNode from '@/renderer/extensions/vueNodes/components/LGraphNode.vue'
147-
import { UnauthorizedError, api } from '@/scripts/api'
147+
import { UnauthorizedError } from '@/scripts/api'
148148
import { app as comfyApp } from '@/scripts/app'
149149
import { ChangeTracker } from '@/scripts/changeTracker'
150150
import { IS_CONTROL_WIDGET, updateControlWidgetLabel } from '@/scripts/widgets'
151151
import { useColorPaletteService } from '@/services/colorPaletteService'
152152
import { newUserService } from '@/services/newUserService'
153+
import { storeToRefs } from 'pinia'
154+
155+
import { useBootstrapStore } from '@/stores/bootstrapStore'
153156
import { useCommandStore } from '@/stores/commandStore'
154157
import { useExecutionStore } from '@/stores/executionStore'
155158
import { useNodeDefStore } from '@/stores/nodeDefStore'
@@ -180,6 +183,9 @@ const toastStore = useToastStore()
180183
const colorPaletteStore = useColorPaletteStore()
181184
const colorPaletteService = useColorPaletteService()
182185
const canvasInteractions = useCanvasInteractions()
186+
const bootstrapStore = useBootstrapStore()
187+
const { isI18nReady, i18nError, isSettingsReady, settingsError } =
188+
storeToRefs(bootstrapStore)
183189
184190
const betaMenuEnabled = computed(
185191
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
@@ -386,15 +392,6 @@ useEventListener(
386392
{ passive: true }
387393
)
388394
389-
const loadCustomNodesI18n = async () => {
390-
try {
391-
const i18nData = await api.getCustomNodesI18n()
392-
mergeCustomNodesI18n(i18nData)
393-
} catch (error) {
394-
console.error('Failed to load custom nodes i18n', error)
395-
}
396-
}
397-
398395
const comfyAppReady = ref(false)
399396
const workflowPersistence = useWorkflowPersistence()
400397
const { flags } = useFeatureFlags()
@@ -404,35 +401,49 @@ useCanvasDrop(canvasRef)
404401
useLitegraphSettings()
405402
useNodeBadge()
406403
404+
useGlobalLitegraph()
405+
useContextMenuTranslation()
406+
useVueFeatureFlags()
407+
407408
onMounted(async () => {
408-
useGlobalLitegraph()
409-
useContextMenuTranslation()
410409
useCopy()
411410
usePaste()
412411
useWorkflowAutoSave()
413-
useVueFeatureFlags()
414412
415413
comfyApp.vueAppReady = true
416414
417415
workspaceStore.spinner = true
418416
// ChangeTracker needs to be initialized before setup, as it will overwrite
419417
// some listeners of litegraph canvas.
420418
ChangeTracker.init()
421-
await loadCustomNodesI18n()
422-
try {
423-
await settingStore.loadSettingValues()
424-
} catch (error) {
425-
if (error instanceof UnauthorizedError) {
419+
420+
await until(() => isSettingsReady.value || !!settingsError.value).toBe(true)
421+
422+
if (settingsError.value) {
423+
if (settingsError.value instanceof UnauthorizedError) {
426424
localStorage.removeItem('Comfy.userId')
427425
localStorage.removeItem('Comfy.userName')
428426
window.location.reload()
429-
} else {
430-
throw error
427+
return
431428
}
429+
throw settingsError.value
432430
}
431+
432+
// Register core settings immediately after settings are ready
433433
CORE_SETTINGS.forEach(settingStore.addSetting)
434434
435-
await newUserService().initializeIfNewUser(settingStore)
435+
// Wait for both i18n and newUserService in parallel
436+
// (newUserService only needs settings, not i18n)
437+
await Promise.all([
438+
until(() => isI18nReady.value || !!i18nError.value).toBe(true),
439+
newUserService().initializeIfNewUser(settingStore)
440+
])
441+
if (i18nError.value) {
442+
console.warn(
443+
'[GraphCanvas] Failed to load custom nodes i18n:',
444+
i18nError.value
445+
)
446+
}
436447
437448
// @ts-expect-error fixme ts strict error
438449
await comfyApp.setup(canvasRef.value)

src/main.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { VueFire, VueFireAuth } from 'vuefire'
1414
import { getFirebaseConfig } from '@/config/firebase'
1515
import '@/lib/litegraph/public/css/litegraph.css'
1616
import router from '@/router'
17+
import { useBootstrapStore } from '@/stores/bootstrapStore'
1718

1819
import App from './App.vue'
1920
// Intentionally relative import to ensure the CSS is loaded in the right order (after litegraph.css)
@@ -43,6 +44,10 @@ const firebaseApp = initializeApp(getFirebaseConfig())
4344

4445
const app = createApp(App)
4546
const pinia = createPinia()
47+
48+
// Start early bootstrap (api.init, fetchNodeDefs) before Pinia is registered
49+
const bootstrapStore = useBootstrapStore(pinia)
50+
bootstrapStore.startEarlyBootstrap()
4651
Sentry.init({
4752
app,
4853
dsn: __SENTRY_DSN__,
@@ -88,4 +93,7 @@ app
8893
modules: [VueFireAuth()]
8994
})
9095

96+
// Start store bootstrap (settings, i18n, workflows) after Pinia is registered
97+
void bootstrapStore.startStoreBootstrap()
98+
9199
app.mount('#vue-app')

src/stores/bootstrapStore.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { createPinia, setActivePinia } from 'pinia'
2+
import { beforeEach, describe, expect, it, vi } from 'vitest'
3+
4+
import { useBootstrapStore } from './bootstrapStore'
5+
6+
vi.mock('@/scripts/api', () => ({
7+
api: {
8+
init: vi.fn().mockResolvedValue(undefined),
9+
getNodeDefs: vi.fn().mockResolvedValue({ TestNode: { name: 'TestNode' } }),
10+
getCustomNodesI18n: vi.fn().mockResolvedValue({}),
11+
getUserConfig: vi.fn().mockResolvedValue({})
12+
}
13+
}))
14+
15+
vi.mock('@/i18n', () => ({
16+
mergeCustomNodesI18n: vi.fn()
17+
}))
18+
19+
vi.mock('@/platform/settings/settingStore', () => ({
20+
useSettingStore: vi.fn(() => ({
21+
loadSettingValues: vi.fn().mockResolvedValue(undefined)
22+
}))
23+
}))
24+
25+
vi.mock('@/stores/workspaceStore', () => ({
26+
useWorkspaceStore: vi.fn(() => ({
27+
workflow: {
28+
syncWorkflows: vi.fn().mockResolvedValue(undefined)
29+
}
30+
}))
31+
}))
32+
33+
describe('bootstrapStore', () => {
34+
let store: ReturnType<typeof useBootstrapStore>
35+
36+
beforeEach(() => {
37+
setActivePinia(createPinia())
38+
store = useBootstrapStore()
39+
vi.clearAllMocks()
40+
})
41+
42+
it('initializes with all flags false', () => {
43+
expect(store.isNodeDefsReady).toBe(false)
44+
expect(store.isSettingsReady).toBe(false)
45+
expect(store.isI18nReady).toBe(false)
46+
})
47+
48+
it('starts early bootstrap (node defs)', async () => {
49+
const { api } = await import('@/scripts/api')
50+
51+
store.startEarlyBootstrap()
52+
53+
await vi.waitFor(() => {
54+
expect(store.isNodeDefsReady).toBe(true)
55+
})
56+
57+
expect(api.getNodeDefs).toHaveBeenCalled()
58+
})
59+
60+
it('starts store bootstrap (settings, i18n)', async () => {
61+
void store.startStoreBootstrap()
62+
63+
await vi.waitFor(() => {
64+
expect(store.isSettingsReady).toBe(true)
65+
expect(store.isI18nReady).toBe(true)
66+
})
67+
})
68+
})

src/stores/bootstrapStore.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { useAsyncState } from '@vueuse/core'
2+
import { defineStore } from 'pinia'
3+
4+
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
5+
import { api } from '@/scripts/api'
6+
import { useUserStore } from '@/stores/userStore'
7+
8+
export const useBootstrapStore = defineStore('bootstrap', () => {
9+
const {
10+
state: nodeDefs,
11+
isReady: isNodeDefsReady,
12+
error: nodeDefsError,
13+
execute: fetchNodeDefs
14+
} = useAsyncState<Record<string, ComfyNodeDef>>(
15+
async () => {
16+
const defs = await api.getNodeDefs()
17+
return defs
18+
},
19+
{},
20+
{ immediate: false }
21+
)
22+
23+
const {
24+
isReady: isSettingsReady,
25+
isLoading: isSettingsLoading,
26+
error: settingsError,
27+
execute: executeLoadSettings
28+
} = useAsyncState(
29+
async () => {
30+
const { useSettingStore } =
31+
await import('@/platform/settings/settingStore')
32+
await useSettingStore().loadSettingValues()
33+
},
34+
undefined,
35+
{ immediate: false }
36+
)
37+
38+
function loadSettings() {
39+
// TODO: This check makes the store "sticky" across logouts. Add a reset
40+
// method to clear isSettingsReady, then replace window.location.reload()
41+
// with router.push() in SidebarLogoutIcon.vue
42+
if (!isSettingsReady.value && !isSettingsLoading.value) {
43+
void executeLoadSettings()
44+
}
45+
}
46+
47+
const {
48+
isReady: isI18nReady,
49+
error: i18nError,
50+
execute: loadI18n
51+
} = useAsyncState(
52+
async () => {
53+
const { mergeCustomNodesI18n } = await import('@/i18n')
54+
const i18nData = await api.getCustomNodesI18n()
55+
mergeCustomNodesI18n(i18nData)
56+
},
57+
undefined,
58+
{ immediate: false }
59+
)
60+
61+
const {
62+
isReady: isWorkflowsReady,
63+
isLoading: isWorkflowsLoading,
64+
execute: executeSyncWorkflows
65+
} = useAsyncState(
66+
async () => {
67+
const { useWorkspaceStore } = await import('@/stores/workspaceStore')
68+
await useWorkspaceStore().workflow.syncWorkflows()
69+
},
70+
undefined,
71+
{ immediate: false }
72+
)
73+
74+
function syncWorkflows() {
75+
if (!isWorkflowsReady.value && !isWorkflowsLoading.value) {
76+
void executeSyncWorkflows()
77+
}
78+
}
79+
80+
function startEarlyBootstrap() {
81+
void fetchNodeDefs()
82+
}
83+
84+
async function startStoreBootstrap() {
85+
// Defer settings and workflows if multi-user login is required
86+
// (settings API requires authentication in multi-user mode)
87+
const userStore = useUserStore()
88+
await userStore.initialize()
89+
90+
// i18n can load without authentication
91+
void loadI18n()
92+
93+
if (!userStore.needsLogin) {
94+
loadSettings()
95+
syncWorkflows()
96+
}
97+
}
98+
99+
return {
100+
nodeDefs,
101+
isNodeDefsReady,
102+
nodeDefsError,
103+
isSettingsReady,
104+
settingsError,
105+
isI18nReady,
106+
i18nError,
107+
startEarlyBootstrap,
108+
startStoreBootstrap,
109+
loadSettings,
110+
syncWorkflows
111+
}
112+
})

0 commit comments

Comments
 (0)