refactor: parallelize bootstrap and simplify lifecycle with VueUse (#8307)

## Summary

Refactors bootstrap and lifecycle management to parallelize
initialization, use Vue best practices, and fix a logout state bug.

## Changes

### Bootstrap Store (`bootstrapStore.ts`)
- Extract early bootstrap logic into a dedicated store using
`useAsyncState`
- Parallelize settings, i18n, and workflow sync loading (previously
sequential)
- Handle multi-user login scenarios by deferring settings/workflows
until authenticated

### GraphCanvas Refactoring
- Move non-DOM composables (`useGlobalLitegraph`, `useCopy`, `usePaste`,
etc.) to script setup level for earlier initialization
- Move `watch` and `whenever` declarations outside `onMounted` (Vue best
practice)
- Use `until()` from VueUse to await bootstrap store readiness instead
of direct async calls

### GraphView Simplification
- Replace manual `addEventListener`/`removeEventListener` with
`useEventListener`
- Replace `setInterval` with `useIntervalFn` for automatic cleanup
- Move core command registration to script setup level

### Bug Fix
Using `router.push()` for logout caused `isSettingsReady` to persist as
`true`, making new users inherit the previous user's cached settings.
Reverted to `window.location.reload()` with TODOs for future store reset
implementation.

## Testing

- Verified login/logout cycle clears settings correctly
- Verified bootstrap sequence completes without errors

---------

Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Alexander Brown
2026-01-27 12:50:13 -08:00
committed by GitHub
parent 788f50834c
commit 14369c08a3
19 changed files with 422 additions and 187 deletions

View File

@@ -1,4 +1,5 @@
import _ from 'es-toolkit/compat'
import { until, useAsyncState } from '@vueuse/core'
import { defineStore } from 'pinia'
import { computed, markRaw, ref, shallowRef, watch } from 'vue'
import type { Raw } from 'vue'
@@ -531,48 +532,72 @@ export const useWorkflowStore = defineStore('workflow', () => {
workflow.isPersisted && !workflow.path.startsWith('subgraphs/')
)
)
const syncWorkflows = async (dir: string = '') => {
await syncEntities(
dir ? 'workflows/' + dir : 'workflows',
workflowLookup.value,
(file) =>
new ComfyWorkflow({
path: file.path,
modified: file.modified,
size: file.size
}),
(existingWorkflow, file) => {
const isActiveWorkflow =
activeWorkflow.value?.path === existingWorkflow.path
const nextLastModified = Math.max(
existingWorkflow.lastModified,
file.modified
)
const {
isReady: isSyncReady,
isLoading: isSyncLoading,
execute: executeSyncWorkflows
} = useAsyncState(
async (dir: string = '') => {
await syncEntities(
dir ? 'workflows/' + dir : 'workflows',
workflowLookup.value,
(file) =>
new ComfyWorkflow({
path: file.path,
modified: file.modified,
size: file.size
}),
(existingWorkflow, file) => {
const isActiveWorkflow =
activeWorkflow.value?.path === existingWorkflow.path
const isMetadataUnchanged =
nextLastModified === existingWorkflow.lastModified &&
file.size === existingWorkflow.size
const nextLastModified = Math.max(
existingWorkflow.lastModified,
file.modified
)
if (!isMetadataUnchanged) {
existingWorkflow.lastModified = nextLastModified
existingWorkflow.size = file.size
}
const isMetadataUnchanged =
nextLastModified === existingWorkflow.lastModified &&
file.size === existingWorkflow.size
// Never unload the active workflow - it may contain unsaved in-memory edits.
if (isActiveWorkflow) {
return
}
if (!isMetadataUnchanged) {
existingWorkflow.lastModified = nextLastModified
existingWorkflow.size = file.size
}
// If nothing changed, keep any loaded content cached.
if (isMetadataUnchanged) {
return
}
// Never unload the active workflow - it may contain unsaved in-memory edits.
if (isActiveWorkflow) {
return
}
existingWorkflow.unload()
},
/* exclude */ (workflow) => workflow.isTemporary
)
// If nothing changed, keep any loaded content cached.
if (isMetadataUnchanged) {
return
}
existingWorkflow.unload()
},
/* exclude */ (workflow) => workflow.isTemporary
)
},
undefined,
{ immediate: false }
)
async function syncWorkflows(dir: string = '') {
return executeSyncWorkflows(0, dir)
}
async function loadWorkflows(): Promise<void> {
if (isSyncReady.value) return
if (isSyncLoading.value) {
await until(isSyncLoading).toBe(false)
return
}
await syncWorkflows()
}
const bookmarkStore = useWorkflowBookmarkStore()
@@ -884,6 +909,7 @@ export const useWorkflowStore = defineStore('workflow', () => {
modifiedWorkflows,
getWorkflowByPath,
syncWorkflows,
loadWorkflows,
isSubgraphActive,
activeSubgraph,