Skip to content

Commit 14a6a9a

Browse files
DrJKLampcode-com
andcommitted
feat: add bootstrapStore for optimized startup
- Create bootstrapStore using VueUse useAsyncState for parallel async operations - Move API init, nodeDefs fetch, settings, i18n, and workflow sync to early bootstrap - Fix deadlock: wait for ready OR error since useAsyncState doesn't set isReady on rejection - Extract ensureNodeDefsLoaded helper in app.ts to eliminate duplicate code - Add timing telemetry for bootstrap performance measurement perf: move non-DOM composables to script setup level Moved useGlobalLitegraph, useContextMenuTranslation, useVueFeatureFlags from onMounted to script setup level in GraphCanvas.vue. These composables don't use onUnmounted hooks, so they can run earlier. Amp-Thread-ID: https://ampcode.com/threads/T-019bf1e3-1b3a-713d-8eaf-5f3eee1493b8 Co-authored-by: Amp <amp@ampcode.com> perf: parallelize pre-canvas bootstrap operations Start i18n and settings waits in parallel, then wait for settings first (critical path) before running CORE_SETTINGS registration. Run i18n wait and newUserService in parallel since newUserService only needs settings. Amp-Thread-ID: https://ampcode.com/threads/T-019bf1e3-1b3a-713d-8eaf-5f3eee1493b8 Co-authored-by: Amp <amp@ampcode.com> refactor: simplify bootstrapStore and defer auth-dependent loading - Remove unused timing telemetry from bootstrapStore - Add multi-user login check before loading settings/workflows - Settings and workflows now defer if authentication is required - Extract loadSettings/syncWorkflows as guarded methods - Update related stores and services to handle auth dependencies Amp-Thread-ID: https://ampcode.com/threads/T-019bf274-0436-725d-999b-232e2198131b Co-authored-by: Amp <amp@ampcode.com> refactor: simplify bootstrapStore usage and remove type assertion - Add explicit generic type to useAsyncState instead of using 'as' assertion - Use store properties directly instead of storeToRefs in ensureNodeDefsLoaded - Move bootstrapStore import to top-level instead of dynamic import Amp-Thread-ID: https://ampcode.com/threads/T-019bf280-28ae-71bc-b33f-acd4484ffd96 Co-authored-by: Amp <amp@ampcode.com> fix: revert logout to use page reload to prevent stale store state Replace router.push() with window.location.reload() in SidebarLogoutIcon to ensure Pinia stores (bootstrapStore, settingStore) are fully reset. Using router navigation caused isSettingsReady/isNodeDefsReady flags to persist, making new users inherit the previous user's cached state. Added TODO comments documenting the sticky state issue for future refactoring to proper store reset methods. test: update tests for relaxed duplicate setting and cleanup behavior Amp-Thread-ID: https://ampcode.com/threads/T-019bf29e-0329-751d-80e2-771ea372a56e Co-authored-by: Amp <amp@ampcode.com> fix: remove duplicate api.init() from bootstrapStore The websocket was being initialized before status event handlers were registered, causing the initial status message to be missed. Queue size showed 'X' instead of 0. api.init() is already called in app.ts addApiUpdateHandlers() after registering handlers. Amp-Thread-ID: https://ampcode.com/threads/T-019bf2db-6aa1-7210-aeed-f161486f950a Co-authored-by: Amp <amp@ampcode.com>
1 parent 702c917 commit 14a6a9a

File tree

20 files changed

+362
-79
lines changed

20 files changed

+362
-79
lines changed

AGENTS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,12 @@ When referencing Comfy-Org repos:
300300
301301
Rules for agent-based coding tasks.
302302
303+
### Chrome DevTools MCP
304+
305+
When using `take_snapshot` to inspect dropdowns, listboxes, or other components with dynamic options:
306+
- Use `verbose: true` to see the full accessibility tree including list items
307+
- Non-verbose snapshots often omit nested options in comboboxes/listboxes
308+
303309
### Temporary Files
304310
305311
- Put planning documents under `/temp/plans/`

src/AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
- Clear public interfaces
1818
- Restrict extension access
1919
- Clean up subscriptions
20+
- Only expose state/actions that are used externally; keep internal state private
2021

2122
## General Guidelines
2223

src/components/graph/GraphCanvas.vue

Lines changed: 47 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,62 @@ useCanvasDrop(canvasRef)
404401
useLitegraphSettings()
405402
useNodeBadge()
406403
404+
// These composables only do global setup (no component refs, no onUnmounted)
405+
// Moving them outside onMounted allows earlier initialization
406+
useGlobalLitegraph()
407+
useContextMenuTranslation()
408+
useVueFeatureFlags()
409+
407410
onMounted(async () => {
408-
useGlobalLitegraph()
409-
useContextMenuTranslation()
411+
// These composables use document event listeners, safe to call after mount
410412
useCopy()
411413
usePaste()
412414
useWorkflowAutoSave()
413-
useVueFeatureFlags()
414415
415416
comfyApp.vueAppReady = true
416417
417418
workspaceStore.spinner = true
418419
// ChangeTracker needs to be initialized before setup, as it will overwrite
419420
// some listeners of litegraph canvas.
420421
ChangeTracker.init()
421-
await loadCustomNodesI18n()
422-
try {
423-
await settingStore.loadSettingValues()
424-
} catch (error) {
425-
if (error instanceof UnauthorizedError) {
422+
423+
// Wait for bootstrapStore to complete settings and i18n loading
424+
// (already started in main.ts via startStoreBootstrap)
425+
// Start both waits in parallel (useAsyncState doesn't set isReady on rejection)
426+
const i18nPromise = until(() => isI18nReady.value || !!i18nError.value).toBe(
427+
true
428+
)
429+
const settingsPromise = until(
430+
() => isSettingsReady.value || !!settingsError.value
431+
).toBe(true)
432+
433+
// Wait for settings first (critical path - needed for CORE_SETTINGS and newUserService)
434+
await settingsPromise
435+
if (settingsError.value) {
436+
if (settingsError.value instanceof UnauthorizedError) {
426437
localStorage.removeItem('Comfy.userId')
427438
localStorage.removeItem('Comfy.userName')
428439
window.location.reload()
429-
} else {
430-
throw error
440+
return
431441
}
442+
throw settingsError.value
432443
}
444+
445+
// Register core settings immediately after settings are ready
433446
CORE_SETTINGS.forEach(settingStore.addSetting)
434447
435-
await newUserService().initializeIfNewUser(settingStore)
448+
// Wait for both i18n and newUserService in parallel
449+
// (newUserService only needs settings, not i18n)
450+
await Promise.all([
451+
i18nPromise,
452+
newUserService().initializeIfNewUser(settingStore)
453+
])
454+
if (i18nError.value) {
455+
console.warn(
456+
'[GraphCanvas] Failed to load custom nodes i18n:',
457+
i18nError.value
458+
)
459+
}
436460
437461
// @ts-expect-error fixme ts strict error
438462
await comfyApp.setup(canvasRef.value)

src/components/sidebar/SidebarLogoutIcon.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,21 @@
1111
import { computed } from 'vue'
1212
import { useI18n } from 'vue-i18n'
1313
14+
import { useColorPaletteService } from '@/services/colorPaletteService'
1415
import { useUserStore } from '@/stores/userStore'
1516
1617
import SidebarIcon from './SidebarIcon.vue'
1718
1819
const { t } = useI18n()
1920
const userStore = useUserStore()
21+
const colorPaletteService = useColorPaletteService()
2022
2123
const tooltip = computed(
2224
() => `${t('sideToolbar.logout')} (${userStore.currentUser?.username})`
2325
)
2426
const logout = async () => {
2527
await userStore.logout()
28+
colorPaletteService.clearColorPalette()
2629
window.location.reload()
2730
}
2831
</script>

src/composables/node/useNodeBadge.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ export const useNodeBadge = () => {
7171
}
7272

7373
onMounted(() => {
74+
if (extensionStore.isExtensionInstalled('Comfy.NodeBadge')) return
75+
76+
// TODO: Fix the composables and watchers being setup in onMounted
7477
const nodePricing = useNodePricing()
7578

7679
watch(

src/composables/useCoreCommands.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,9 @@ import { useWorkflowTemplateSelectorDialog } from './useWorkflowTemplateSelector
6767
import { useMaskEditorStore } from '@/stores/maskEditorStore'
6868
import { useDialogStore } from '@/stores/dialogStore'
6969

70-
const { isActiveSubscription, showSubscriptionDialog } = useSubscription()
71-
7270
const moveSelectedNodesVersionAdded = '1.22.2'
7371
export function useCoreCommands(): ComfyCommand[] {
72+
const { isActiveSubscription, showSubscriptionDialog } = useSubscription()
7473
const workflowService = useWorkflowService()
7574
const workflowStore = useWorkflowStore()
7675
const dialogService = useDialogService()

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/platform/cloud/subscription/composables/useSubscription.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,10 @@ function useSubscriptionInternal() {
203203
if (loggedIn) {
204204
try {
205205
await fetchSubscriptionStatus()
206+
} catch (error) {
207+
// Network errors are expected during navigation/component unmount
208+
// and when offline - log for debugging but don't surface to user
209+
console.error('Failed to fetch subscription status:', error)
206210
} finally {
207211
isInitialized.value = true
208212
}

src/platform/distribution/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const DISTRIBUTION: Distribution = __DISTRIBUTION__
1818
/** Distribution type checks */
1919
export const isDesktop = DISTRIBUTION === 'desktop' || isElectron() // TODO: replace with build var
2020
export const isCloud = DISTRIBUTION === 'cloud'
21+
export const isDev = import.meta.env?.DEV ?? false
2122
// export const isLocalhost = DISTRIBUTION === 'localhost' || (!isDesktop && !isCloud)
2223

2324
/**

src/platform/settings/settingStore.test.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,18 +82,24 @@ describe('useSettingStore', () => {
8282
expect(store.settingsById['test.setting']).toEqual(setting)
8383
})
8484

85-
it('should throw error for duplicate setting ID', () => {
85+
it('should warn and skip for duplicate setting ID', () => {
8686
const setting: SettingParams = {
8787
id: 'test.setting',
8888
name: 'test.setting',
8989
type: 'text',
9090
defaultValue: 'default'
9191
}
92+
const consoleWarnSpy = vi
93+
.spyOn(console, 'warn')
94+
.mockImplementation(() => {})
9295

9396
store.addSetting(setting)
94-
expect(() => store.addSetting(setting)).toThrow(
95-
'Setting test.setting must have a unique ID.'
97+
store.addSetting(setting)
98+
99+
expect(consoleWarnSpy).toHaveBeenCalledWith(
100+
'Setting already registered: test.setting'
96101
)
102+
consoleWarnSpy.mockRestore()
97103
})
98104

99105
it('should migrate deprecated values', () => {

0 commit comments

Comments
 (0)