refactor: move node definition loading from bootstrapStore to nodeDefStore

- Extract loadNodeDefs action to nodeDefStore for better separation of concerns
- Add initializeFromBackend action to nodeDefStore that handles full initialization
- Simplify bootstrapStore by delegating node def loading to nodeDefStore
- Update app.ts to use new nodeDefStore.initializeFromBackend method
- Expand test coverage for both stores

Amp-Thread-ID: https://ampcode.com/threads/T-019bfccc-e67e-739e-965f-d33c913371ef
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Alexander Brown
2026-01-26 17:04:07 -08:00
parent 16b285a79c
commit 8f2736095c
6 changed files with 268 additions and 104 deletions

View File

@@ -45,6 +45,9 @@ const firebaseApp = initializeApp(getFirebaseConfig())
const app = createApp(App) const app = createApp(App)
const pinia = createPinia() const pinia = createPinia()
const bootstrapStore = useBootstrapStore(pinia)
bootstrapStore.startEarlyBootstrap()
Sentry.init({ Sentry.init({
app, app,
dsn: __SENTRY_DSN__, dsn: __SENTRY_DSN__,
@@ -90,7 +93,6 @@ app
modules: [VueFireAuth()] modules: [VueFireAuth()]
}) })
const bootstrapStore = useBootstrapStore(pinia)
void bootstrapStore.startStoreBootstrap() void bootstrapStore.startStoreBootstrap()
app.mount('#vue-app') app.mount('#vue-app')

View File

@@ -6,7 +6,7 @@ import { shallowRef } from 'vue'
import { useCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion' import { useCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'
import { registerProxyWidgets } from '@/core/graph/subgraph/proxyWidget' import { registerProxyWidgets } from '@/core/graph/subgraph/proxyWidget'
import { st, t } from '@/i18n' import { t } from '@/i18n'
import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces' import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces'
import { import {
LGraph, LGraph,
@@ -61,7 +61,7 @@ import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { useNodeOutputStore } from '@/stores/imagePreviewStore' import { useNodeOutputStore } from '@/stores/imagePreviewStore'
import { KeyComboImpl, useKeybindingStore } from '@/stores/keybindingStore' import { KeyComboImpl, useKeybindingStore } from '@/stores/keybindingStore'
import { useModelStore } from '@/stores/modelStore' import { useModelStore } from '@/stores/modelStore'
import { SYSTEM_NODE_DEFS, useNodeDefStore } from '@/stores/nodeDefStore' import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useSubgraphStore } from '@/stores/subgraphStore' import { useSubgraphStore } from '@/stores/subgraphStore'
import { useWidgetStore } from '@/stores/widgetStore' import { useWidgetStore } from '@/stores/widgetStore'
import { useWorkspaceStore } from '@/stores/workspaceStore' import { useWorkspaceStore } from '@/stores/workspaceStore'
@@ -881,21 +881,18 @@ export class ComfyApp {
this.canvas?.draw(true, true) this.canvas?.draw(true, true)
} }
private updateVueAppNodeDefs(defs: Record<string, ComfyNodeDefV1>) { private buildFrontendOnlyDefs(
// Frontend only nodes registered by custom nodes. backendDefs: Record<string, ComfyNodeDefV1>
// Example: https://github.com/rgthree/rgthree-comfy/blob/dd534e5384be8cf0c0fa35865afe2126ba75ac55/src_web/comfyui/fast_groups_bypasser.ts#L10 ): ComfyNodeDefV1[] {
const frontendOnlyDefs: ComfyNodeDefV1[] = []
// Only create frontend_only definitions for nodes that don't have backend definitions
const frontendOnlyDefs: Record<string, ComfyNodeDefV1> = {}
for (const [name, node] of Object.entries( for (const [name, node] of Object.entries(
LiteGraph.registered_node_types LiteGraph.registered_node_types
)) { )) {
// Skip if we already have a backend definition or system definition if (name in backendDefs || node.skip_list) {
if (name in defs || name in SYSTEM_NODE_DEFS || node.skip_list) {
continue continue
} }
frontendOnlyDefs[name] = { frontendOnlyDefs.push({
name, name,
display_name: name, display_name: name,
category: node.category || '__frontend_only__', category: node.category || '__frontend_only__',
@@ -906,54 +903,33 @@ export class ComfyApp {
output_node: false, output_node: false,
python_module: 'custom_nodes.frontend_only', python_module: 'custom_nodes.frontend_only',
description: node.description ?? `Frontend only node for ${name}` description: node.description ?? `Frontend only node for ${name}`
} as ComfyNodeDefV1 })
} }
return frontendOnlyDefs
const allNodeDefs = {
...frontendOnlyDefs,
...defs,
...SYSTEM_NODE_DEFS
}
const nodeDefStore = useNodeDefStore()
const nodeDefArray: ComfyNodeDefV1[] = Object.values(allNodeDefs)
useExtensionService().invokeExtensions(
'beforeRegisterVueAppNodeDefs',
nodeDefArray,
this
)
nodeDefStore.updateNodeDefs(nodeDefArray)
}
async getNodeDefs(): Promise<Record<string, ComfyNodeDefV1>> {
const translateNodeDef = (def: ComfyNodeDefV1): ComfyNodeDefV1 => ({
...def,
display_name: st(
`nodeDefs.${def.name}.display_name`,
def.display_name ?? def.name
),
description: def.description
? st(`nodeDefs.${def.name}.description`, def.description)
: '',
category: def.category
.split('/')
.map((category: string) => st(`nodeCategories.${category}`, category))
.join('/')
})
return _.mapValues(await api.getNodeDefs(), (def) => translateNodeDef(def))
} }
/** /**
* Registers nodes with the graph * Registers nodes with the graph
*/ */
async registerNodes() { async registerNodes() {
// Load node definitions from the backend const nodeDefStore = useNodeDefStore()
const defs = await this.getNodeDefs()
while (!nodeDefStore.isReady) {
await new Promise((resolve) => setTimeout(resolve, 50))
}
if (nodeDefStore.error) {
throw nodeDefStore.error
}
const defs = nodeDefStore.rawNodeDefs
await this.registerNodesFromDefs(defs) await this.registerNodesFromDefs(defs)
await useExtensionService().invokeExtensionsAsync('registerCustomNodes') await useExtensionService().invokeExtensionsAsync('registerCustomNodes')
if (this.vueAppReady) { if (this.vueAppReady) {
this.updateVueAppNodeDefs(defs) const frontendOnlyDefs = this.buildFrontendOnlyDefs(defs)
const allDefs = [...frontendOnlyDefs, ...Object.values(defs)]
useNodeDefStore().updateNodeDefs(allDefs, this)
} }
} }
@@ -1653,7 +1629,7 @@ export class ComfyApp {
useToastStore().add(requestToastMessage) useToastStore().add(requestToastMessage)
} }
const defs = await this.getNodeDefs() const defs = await api.getNodeDefs()
for (const nodeId in defs) { for (const nodeId in defs) {
this.registerNodeDef(nodeId, defs[nodeId]) this.registerNodeDef(nodeId, defs[nodeId])
} }
@@ -1698,7 +1674,9 @@ export class ComfyApp {
) )
if (this.vueAppReady) { if (this.vueAppReady) {
this.updateVueAppNodeDefs(defs) const frontendOnlyDefs = this.buildFrontendOnlyDefs(defs)
const allDefs = [...frontendOnlyDefs, ...Object.values(defs)]
useNodeDefStore().updateNodeDefs(allDefs, this)
useToastStore().remove(requestToastMessage) useToastStore().remove(requestToastMessage)
useToastStore().add({ useToastStore().add({
severity: 'success', severity: 'success',

View File

@@ -3,8 +3,6 @@ import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue' import { ref } from 'vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useBootstrapStore } from './bootstrapStore' import { useBootstrapStore } from './bootstrapStore'
vi.mock('@/scripts/api', () => ({ vi.mock('@/scripts/api', () => ({
@@ -21,17 +19,30 @@ vi.mock('@/i18n', () => ({
})) }))
const mockIsSettingsReady = ref(false) const mockIsSettingsReady = ref(false)
const mockIsNodeDefsReady = ref(false)
const mockNodeDefStoreLoad = vi.fn(() => {
mockIsNodeDefsReady.value = true
})
const mockSettingStoreLoad = vi.fn(() => {
mockIsSettingsReady.value = true
})
vi.mock('@/platform/settings/settingStore', () => ({ vi.mock('@/platform/settings/settingStore', () => ({
useSettingStore: vi.fn(() => ({ useSettingStore: vi.fn(() => ({
load: vi.fn(() => { load: mockSettingStoreLoad,
mockIsSettingsReady.value = true isReady: mockIsSettingsReady,
}), isLoading: { value: false },
get isReady() { error: { value: undefined }
return mockIsSettingsReady.value }))
}, }))
isLoading: ref(false),
error: ref(undefined) vi.mock('@/stores/nodeDefStore', () => ({
useNodeDefStore: vi.fn(() => ({
load: mockNodeDefStoreLoad,
isReady: mockIsNodeDefsReady,
isLoading: { value: false },
error: { value: undefined },
rawNodeDefs: { value: {} }
})) }))
})) }))
@@ -54,23 +65,33 @@ describe('bootstrapStore', () => {
beforeEach(() => { beforeEach(() => {
mockIsSettingsReady.value = false mockIsSettingsReady.value = false
mockIsNodeDefsReady.value = false
setActivePinia(createTestingPinia({ stubActions: false })) setActivePinia(createTestingPinia({ stubActions: false }))
store = useBootstrapStore() store = useBootstrapStore()
vi.clearAllMocks() vi.clearAllMocks()
}) })
it('initializes with all flags false', () => { it('initializes with all flags false', () => {
const settingStore = useSettingStore() expect(mockIsNodeDefsReady.value).toBe(false)
expect(settingStore.isReady).toBe(false) expect(mockIsSettingsReady.value).toBe(false)
expect(store.isI18nReady).toBe(false) expect(store.isI18nReady).toBe(false)
}) })
it('starts early bootstrap (node defs)', async () => {
store.startEarlyBootstrap()
await vi.waitFor(() => {
expect(mockIsNodeDefsReady.value).toBe(true)
})
expect(mockNodeDefStoreLoad).toHaveBeenCalled()
})
it('starts store bootstrap (settings, i18n)', async () => { it('starts store bootstrap (settings, i18n)', async () => {
const settingStore = useSettingStore()
void store.startStoreBootstrap() void store.startStoreBootstrap()
await vi.waitFor(() => { await vi.waitFor(() => {
expect(settingStore.isReady).toBe(true) expect(mockIsSettingsReady.value).toBe(true)
expect(store.isI18nReady).toBe(true) expect(store.isI18nReady).toBe(true)
}) })
}) })

View File

@@ -4,10 +4,12 @@ import { defineStore } from 'pinia'
import { useSettingStore } from '@/platform/settings/settingStore' import { useSettingStore } from '@/platform/settings/settingStore'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { api } from '@/scripts/api' import { api } from '@/scripts/api'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useUserStore } from '@/stores/userStore' import { useUserStore } from '@/stores/userStore'
export const useBootstrapStore = defineStore('bootstrap', () => { export const useBootstrapStore = defineStore('bootstrap', () => {
const settingStore = useSettingStore() const settingStore = useSettingStore()
const nodeDefStore = useNodeDefStore()
const workflowStore = useWorkflowStore() const workflowStore = useWorkflowStore()
const { const {
@@ -24,13 +26,14 @@ export const useBootstrapStore = defineStore('bootstrap', () => {
{ immediate: false } { immediate: false }
) )
function startEarlyBootstrap() {
void nodeDefStore.load()
}
async function startStoreBootstrap() { async function startStoreBootstrap() {
// Defer settings and workflows if multi-user login is required
// (settings API requires authentication in multi-user mode)
const userStore = useUserStore() const userStore = useUserStore()
await userStore.initialize() await userStore.initialize()
// i18n can load without authentication
void loadI18n() void loadI18n()
if (!userStore.needsLogin) { if (!userStore.needsLogin) {
@@ -42,6 +45,7 @@ export const useBootstrapStore = defineStore('bootstrap', () => {
return { return {
isI18nReady, isI18nReady,
i18nError, i18nError,
startEarlyBootstrap,
startStoreBootstrap startStoreBootstrap
} }
}) })

View File

@@ -1,16 +1,28 @@
import { createPinia, setActivePinia } from 'pinia' import { createTestingPinia } from '@pinia/testing'
import { beforeEach, describe, expect, it } from 'vitest' import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema' import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
import { useNodeDefStore } from '@/stores/nodeDefStore' import { SYSTEM_NODE_DEFS, useNodeDefStore } from '@/stores/nodeDefStore'
import type { NodeDefFilter } from '@/stores/nodeDefStore' import type { NodeDefFilter } from '@/stores/nodeDefStore'
describe('useNodeDefStore', () => { vi.mock('@/scripts/api', () => ({
let store: ReturnType<typeof useNodeDefStore> api: {
getNodeDefs: vi.fn().mockResolvedValue({ TestNode: { name: 'TestNode' } }),
apiURL: vi.fn((path: string) => `/api${path}`),
addEventListener: vi.fn(),
getUserData: vi.fn(),
storeUserData: vi.fn(),
listUserDataFullInfo: vi.fn()
}
}))
const SYSTEM_NODE_COUNT = Object.keys(SYSTEM_NODE_DEFS).length
describe('useNodeDefStore', () => {
beforeEach(() => { beforeEach(() => {
setActivePinia(createPinia()) setActivePinia(createTestingPinia({ stubActions: false }))
store = useNodeDefStore() vi.clearAllMocks()
}) })
const createMockNodeDef = ( const createMockNodeDef = (
@@ -31,8 +43,42 @@ describe('useNodeDefStore', () => {
...overrides ...overrides
}) })
describe('load', () => {
it('initializes with isReady false', () => {
const store = useNodeDefStore()
expect(store.isReady).toBe(false)
})
it('loads node definitions from API', async () => {
const store = useNodeDefStore()
const { api } = await import('@/scripts/api')
await store.load()
await vi.waitFor(() => {
expect(store.isReady).toBe(true)
})
expect(api.getNodeDefs).toHaveBeenCalled()
})
it('does not reload if already ready', async () => {
const store = useNodeDefStore()
const { api } = await import('@/scripts/api')
await store.load()
await vi.waitFor(() => expect(store.isReady).toBe(true))
vi.clearAllMocks()
await store.load()
expect(api.getNodeDefs).not.toHaveBeenCalled()
})
})
describe('filter registry', () => { describe('filter registry', () => {
it('should register a new filter', () => { it('should register a new filter', () => {
const store = useNodeDefStore()
const filter: NodeDefFilter = { const filter: NodeDefFilter = {
id: 'test.filter', id: 'test.filter',
name: 'Test Filter', name: 'Test Filter',
@@ -44,6 +90,7 @@ describe('useNodeDefStore', () => {
}) })
it('should unregister a filter by id', () => { it('should unregister a filter by id', () => {
const store = useNodeDefStore()
const filter: NodeDefFilter = { const filter: NodeDefFilter = {
id: 'test.filter', id: 'test.filter',
name: 'Test Filter', name: 'Test Filter',
@@ -56,6 +103,7 @@ describe('useNodeDefStore', () => {
}) })
it('should register core filters on initialization', () => { it('should register core filters on initialization', () => {
const store = useNodeDefStore()
const deprecatedFilter = store.nodeDefFilters.find( const deprecatedFilter = store.nodeDefFilters.find(
(f) => f.id === 'core.deprecated' (f) => f.id === 'core.deprecated'
) )
@@ -69,12 +117,23 @@ describe('useNodeDefStore', () => {
}) })
describe('filter application', () => { describe('filter application', () => {
beforeEach(() => { const systemNodeFilter: NodeDefFilter = {
id: 'test.no-system',
name: 'Hide System Nodes',
predicate: (node) => !(node.name in SYSTEM_NODE_DEFS)
}
function createFilterTestStore() {
const store = useNodeDefStore()
// Clear existing filters for isolated tests // Clear existing filters for isolated tests
store.nodeDefFilters.splice(0) store.nodeDefFilters.splice(0)
}) // Exclude system nodes from filter tests for cleaner assertions
store.registerNodeDefFilter(systemNodeFilter)
return store
}
it('should apply single filter to visible nodes', () => { it('should apply single filter to visible nodes', () => {
const store = createFilterTestStore()
const normalNode = createMockNodeDef({ const normalNode = createMockNodeDef({
name: 'normal', name: 'normal',
deprecated: false deprecated: false
@@ -98,6 +157,7 @@ describe('useNodeDefStore', () => {
}) })
it('should apply multiple filters with AND logic', () => { it('should apply multiple filters with AND logic', () => {
const store = createFilterTestStore()
const node1 = createMockNodeDef({ const node1 = createMockNodeDef({
name: 'node1', name: 'node1',
deprecated: false, deprecated: false,
@@ -140,6 +200,10 @@ describe('useNodeDefStore', () => {
}) })
it('should show all nodes when no filters are registered', () => { it('should show all nodes when no filters are registered', () => {
const store = createFilterTestStore()
// Remove system node filter for this test
store.unregisterNodeDefFilter('test.no-system')
const nodes = [ const nodes = [
createMockNodeDef({ name: 'node1' }), createMockNodeDef({ name: 'node1' }),
createMockNodeDef({ name: 'node2' }), createMockNodeDef({ name: 'node2' }),
@@ -147,10 +211,12 @@ describe('useNodeDefStore', () => {
] ]
store.updateNodeDefs(nodes) store.updateNodeDefs(nodes)
expect(store.visibleNodeDefs).toHaveLength(3) // Includes 3 test nodes + 4 system nodes
expect(store.visibleNodeDefs).toHaveLength(3 + SYSTEM_NODE_COUNT)
}) })
it('should update visibility when filter is removed', () => { it('should update visibility when filter is removed', () => {
const store = createFilterTestStore()
const deprecatedNode = createMockNodeDef({ const deprecatedNode = createMockNodeDef({
name: 'deprecated', name: 'deprecated',
deprecated: true deprecated: true
@@ -163,7 +229,7 @@ describe('useNodeDefStore', () => {
predicate: (node) => !node.deprecated predicate: (node) => !node.deprecated
} }
// Add filter - node should be hidden // Add filter - node should be hidden (only system nodes remain, but they're filtered too)
store.registerNodeDefFilter(filter) store.registerNodeDefFilter(filter)
expect(store.visibleNodeDefs).toHaveLength(0) expect(store.visibleNodeDefs).toHaveLength(0)
@@ -175,6 +241,7 @@ describe('useNodeDefStore', () => {
describe('core filters behavior', () => { describe('core filters behavior', () => {
it('should hide deprecated nodes by default', () => { it('should hide deprecated nodes by default', () => {
const store = useNodeDefStore()
const normalNode = createMockNodeDef({ const normalNode = createMockNodeDef({
name: 'normal', name: 'normal',
deprecated: false deprecated: false
@@ -186,11 +253,18 @@ describe('useNodeDefStore', () => {
store.updateNodeDefs([normalNode, deprecatedNode]) store.updateNodeDefs([normalNode, deprecatedNode])
expect(store.visibleNodeDefs).toHaveLength(1) // 1 normal test node + 4 system nodes
expect(store.visibleNodeDefs[0].name).toBe('normal') expect(store.visibleNodeDefs).toHaveLength(1 + SYSTEM_NODE_COUNT)
expect(
store.visibleNodeDefs.find((n) => n.name === 'normal')
).toBeDefined()
expect(
store.visibleNodeDefs.find((n) => n.name === 'deprecated')
).toBeUndefined()
}) })
it('should show deprecated nodes when showDeprecated is true', () => { it('should show deprecated nodes when showDeprecated is true', () => {
const store = useNodeDefStore()
const normalNode = createMockNodeDef({ const normalNode = createMockNodeDef({
name: 'normal', name: 'normal',
deprecated: false deprecated: false
@@ -203,10 +277,12 @@ describe('useNodeDefStore', () => {
store.updateNodeDefs([normalNode, deprecatedNode]) store.updateNodeDefs([normalNode, deprecatedNode])
store.showDeprecated = true store.showDeprecated = true
expect(store.visibleNodeDefs).toHaveLength(2) // 2 test nodes + 4 system nodes
expect(store.visibleNodeDefs).toHaveLength(2 + SYSTEM_NODE_COUNT)
}) })
it('should hide experimental nodes by default', () => { it('should hide experimental nodes by default', () => {
const store = useNodeDefStore()
const normalNode = createMockNodeDef({ const normalNode = createMockNodeDef({
name: 'normal', name: 'normal',
experimental: false experimental: false
@@ -218,11 +294,18 @@ describe('useNodeDefStore', () => {
store.updateNodeDefs([normalNode, experimentalNode]) store.updateNodeDefs([normalNode, experimentalNode])
expect(store.visibleNodeDefs).toHaveLength(1) // 1 normal test node + 4 system nodes
expect(store.visibleNodeDefs[0].name).toBe('normal') expect(store.visibleNodeDefs).toHaveLength(1 + SYSTEM_NODE_COUNT)
expect(
store.visibleNodeDefs.find((n) => n.name === 'normal')
).toBeDefined()
expect(
store.visibleNodeDefs.find((n) => n.name === 'experimental')
).toBeUndefined()
}) })
it('should show experimental nodes when showExperimental is true', () => { it('should show experimental nodes when showExperimental is true', () => {
const store = useNodeDefStore()
const normalNode = createMockNodeDef({ const normalNode = createMockNodeDef({
name: 'normal', name: 'normal',
experimental: false experimental: false
@@ -235,10 +318,12 @@ describe('useNodeDefStore', () => {
store.updateNodeDefs([normalNode, experimentalNode]) store.updateNodeDefs([normalNode, experimentalNode])
store.showExperimental = true store.showExperimental = true
expect(store.visibleNodeDefs).toHaveLength(2) // 2 test nodes + 4 system nodes
expect(store.visibleNodeDefs).toHaveLength(2 + SYSTEM_NODE_COUNT)
}) })
it('should hide subgraph nodes by default', () => { it('should hide subgraph nodes by default', () => {
const store = useNodeDefStore()
const normalNode = createMockNodeDef({ const normalNode = createMockNodeDef({
name: 'normal', name: 'normal',
category: 'conditioning', category: 'conditioning',
@@ -252,11 +337,18 @@ describe('useNodeDefStore', () => {
store.updateNodeDefs([normalNode, subgraphNode]) store.updateNodeDefs([normalNode, subgraphNode])
expect(store.visibleNodeDefs).toHaveLength(1) // 1 normal test node + 4 system nodes
expect(store.visibleNodeDefs[0].name).toBe('normal') expect(store.visibleNodeDefs).toHaveLength(1 + SYSTEM_NODE_COUNT)
expect(
store.visibleNodeDefs.find((n) => n.name === 'normal')
).toBeDefined()
expect(
store.visibleNodeDefs.find((n) => n.name === 'MySubgraph')
).toBeUndefined()
}) })
it('should show non-subgraph nodes with subgraph category', () => { it('should show non-subgraph nodes with subgraph category', () => {
const store = useNodeDefStore()
const normalNode = createMockNodeDef({ const normalNode = createMockNodeDef({
name: 'normal', name: 'normal',
category: 'conditioning', category: 'conditioning',
@@ -270,20 +362,23 @@ describe('useNodeDefStore', () => {
store.updateNodeDefs([normalNode, fakeSubgraphNode]) store.updateNodeDefs([normalNode, fakeSubgraphNode])
expect(store.visibleNodeDefs).toHaveLength(2) // 2 test nodes + 4 system nodes
expect(store.visibleNodeDefs.map((n) => n.name)).toEqual([ expect(store.visibleNodeDefs).toHaveLength(2 + SYSTEM_NODE_COUNT)
'normal', const testNodes = store.visibleNodeDefs
'FakeSubgraph' .filter((n) => !(n.name in SYSTEM_NODE_DEFS))
]) .map((n) => n.name)
expect(testNodes).toEqual(['normal', 'FakeSubgraph'])
}) })
}) })
describe('performance', () => { describe('performance', () => {
it('should perform single traversal for multiple filters', () => { it('should perform single traversal for multiple filters', () => {
const store = useNodeDefStore()
let filterCallCount = 0 let filterCallCount = 0
const testFilterCount = 5
// Register multiple filters that count their calls // Register multiple filters that count their calls
for (let i = 0; i < 5; i++) { for (let i = 0; i < testFilterCount; i++) {
store.registerNodeDefFilter({ store.registerNodeDefFilter({
id: `test.counter-${i}`, id: `test.counter-${i}`,
name: `Counter ${i}`, name: `Counter ${i}`,
@@ -294,7 +389,8 @@ describe('useNodeDefStore', () => {
}) })
} }
const nodes = Array.from({ length: 10 }, (_, i) => const testNodeCount = 10
const nodes = Array.from({ length: testNodeCount }, (_, i) =>
createMockNodeDef({ name: `node${i}` }) createMockNodeDef({ name: `node${i}` })
) )
store.updateNodeDefs(nodes) store.updateNodeDefs(nodes)
@@ -302,8 +398,9 @@ describe('useNodeDefStore', () => {
// Force recomputation by accessing visibleNodeDefs // Force recomputation by accessing visibleNodeDefs
expect(store.visibleNodeDefs).toBeDefined() expect(store.visibleNodeDefs).toBeDefined()
// Each node (10) should be checked by each filter (5 test + 2 core = 7 total) // Each node (test nodes + system nodes) checked by each test filter
expect(filterCallCount).toBe(10 * 5) const totalNodes = testNodeCount + SYSTEM_NODE_COUNT
expect(filterCallCount).toBe(totalNodes * testFilterCount)
}) })
}) })
}) })

View File

@@ -1,9 +1,12 @@
import { useAsyncState } from '@vueuse/core'
import axios from 'axios' import axios from 'axios'
import { retry } from 'es-toolkit'
import _ from 'es-toolkit/compat' import _ from 'es-toolkit/compat'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { isProxyWidget } from '@/core/graph/subgraph/proxyWidget' import { isProxyWidget } from '@/core/graph/subgraph/proxyWidget'
import { st } from '@/i18n'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph' import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { transformNodeDefV1ToV2 } from '@/schemas/nodeDef/migration' import { transformNodeDefV1ToV2 } from '@/schemas/nodeDef/migration'
import type { import type {
@@ -17,7 +20,10 @@ import type {
ComfyOutputTypesSpec as ComfyOutputSpecV1, ComfyOutputTypesSpec as ComfyOutputSpecV1,
PriceBadge PriceBadge
} from '@/schemas/nodeDefSchema' } from '@/schemas/nodeDefSchema'
import { api } from '@/scripts/api'
import type { ComfyApp } from '@/scripts/app'
import { NodeSearchService } from '@/services/nodeSearchService' import { NodeSearchService } from '@/services/nodeSearchService'
import { useExtensionService } from '@/services/extensionService'
import { useSubgraphStore } from '@/stores/subgraphStore' import { useSubgraphStore } from '@/stores/subgraphStore'
import { NodeSourceType, getNodeSource } from '@/types/nodeSource' import { NodeSourceType, getNodeSource } from '@/types/nodeSource'
import type { NodeSource } from '@/types/nodeSource' import type { NodeSource } from '@/types/nodeSource'
@@ -305,6 +311,28 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
const showExperimental = ref(false) const showExperimental = ref(false)
const nodeDefFilters = ref<NodeDefFilter[]>([]) const nodeDefFilters = ref<NodeDefFilter[]>([])
const {
state: rawNodeDefs,
isReady,
isLoading,
error,
execute: fetchNodeDefs
} = useAsyncState<Record<string, ComfyNodeDefV1>>(
() =>
retry(() => api.getNodeDefs(), {
retries: 3,
delay: (attempt: number) => Math.min(1000 * Math.pow(2, attempt), 10000)
}),
{},
{ immediate: false }
)
async function load() {
if (!isReady.value && !isLoading.value) {
return fetchNodeDefs()
}
}
const nodeDefs = computed(() => { const nodeDefs = computed(() => {
const subgraphStore = useSubgraphStore() const subgraphStore = useSubgraphStore()
// Blueprints first for discoverability in the node library sidebar // Blueprints first for discoverability in the node library sidebar
@@ -335,18 +363,46 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
) )
const nodeTree = computed(() => buildNodeDefTree(visibleNodeDefs.value)) const nodeTree = computed(() => buildNodeDefTree(visibleNodeDefs.value))
function updateNodeDefs(nodeDefs: ComfyNodeDefV1[]) { function translateNodeDef(def: ComfyNodeDefV1): ComfyNodeDefV1 {
return {
...def,
display_name: st(
`nodeDefs.${def.name}.display_name`,
def.display_name ?? def.name
),
description: def.description
? st(`nodeDefs.${def.name}.description`, def.description)
: '',
category: def.category
.split('/')
.map((category: string) => st(`nodeCategories.${category}`, category))
.join('/')
}
}
function updateNodeDefs(defs: ComfyNodeDefV1[], app?: ComfyApp) {
const allDefs = [...Object.values(SYSTEM_NODE_DEFS), ...defs]
if (app) {
useExtensionService().invokeExtensions(
'beforeRegisterVueAppNodeDefs',
allDefs,
app
)
}
const newNodeDefsByName: Record<string, ComfyNodeDefImpl> = {} const newNodeDefsByName: Record<string, ComfyNodeDefImpl> = {}
const newNodeDefsByDisplayName: Record<string, ComfyNodeDefImpl> = {} const newNodeDefsByDisplayName: Record<string, ComfyNodeDefImpl> = {}
for (const nodeDef of nodeDefs) { for (const def of allDefs) {
const translatedDef = translateNodeDef(def)
const nodeDefImpl = const nodeDefImpl =
nodeDef instanceof ComfyNodeDefImpl translatedDef instanceof ComfyNodeDefImpl
? nodeDef ? translatedDef
: new ComfyNodeDefImpl(nodeDef) : new ComfyNodeDefImpl(translatedDef)
newNodeDefsByName[nodeDef.name] = nodeDefImpl newNodeDefsByName[translatedDef.name] = nodeDefImpl
newNodeDefsByDisplayName[nodeDef.display_name] = nodeDefImpl newNodeDefsByDisplayName[translatedDef.display_name] = nodeDefImpl
} }
nodeDefsByName.value = newNodeDefsByName nodeDefsByName.value = newNodeDefsByName
@@ -448,6 +504,12 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
showExperimental, showExperimental,
nodeDefFilters, nodeDefFilters,
rawNodeDefs,
isReady,
isLoading,
error,
load,
nodeDefs, nodeDefs,
nodeDataTypes, nodeDataTypes,
visibleNodeDefs, visibleNodeDefs,