mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-01 11:10:00 +00:00
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:
@@ -45,6 +45,9 @@ const firebaseApp = initializeApp(getFirebaseConfig())
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
const bootstrapStore = useBootstrapStore(pinia)
|
||||
bootstrapStore.startEarlyBootstrap()
|
||||
|
||||
Sentry.init({
|
||||
app,
|
||||
dsn: __SENTRY_DSN__,
|
||||
@@ -90,7 +93,6 @@ app
|
||||
modules: [VueFireAuth()]
|
||||
})
|
||||
|
||||
const bootstrapStore = useBootstrapStore(pinia)
|
||||
void bootstrapStore.startStoreBootstrap()
|
||||
|
||||
app.mount('#vue-app')
|
||||
|
||||
@@ -6,7 +6,7 @@ import { shallowRef } from 'vue'
|
||||
|
||||
import { useCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'
|
||||
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 {
|
||||
LGraph,
|
||||
@@ -61,7 +61,7 @@ import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { KeyComboImpl, useKeybindingStore } from '@/stores/keybindingStore'
|
||||
import { useModelStore } from '@/stores/modelStore'
|
||||
import { SYSTEM_NODE_DEFS, useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useSubgraphStore } from '@/stores/subgraphStore'
|
||||
import { useWidgetStore } from '@/stores/widgetStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
@@ -881,21 +881,18 @@ export class ComfyApp {
|
||||
this.canvas?.draw(true, true)
|
||||
}
|
||||
|
||||
private updateVueAppNodeDefs(defs: Record<string, ComfyNodeDefV1>) {
|
||||
// Frontend only nodes registered by custom nodes.
|
||||
// Example: https://github.com/rgthree/rgthree-comfy/blob/dd534e5384be8cf0c0fa35865afe2126ba75ac55/src_web/comfyui/fast_groups_bypasser.ts#L10
|
||||
|
||||
// Only create frontend_only definitions for nodes that don't have backend definitions
|
||||
const frontendOnlyDefs: Record<string, ComfyNodeDefV1> = {}
|
||||
private buildFrontendOnlyDefs(
|
||||
backendDefs: Record<string, ComfyNodeDefV1>
|
||||
): ComfyNodeDefV1[] {
|
||||
const frontendOnlyDefs: ComfyNodeDefV1[] = []
|
||||
for (const [name, node] of Object.entries(
|
||||
LiteGraph.registered_node_types
|
||||
)) {
|
||||
// Skip if we already have a backend definition or system definition
|
||||
if (name in defs || name in SYSTEM_NODE_DEFS || node.skip_list) {
|
||||
if (name in backendDefs || node.skip_list) {
|
||||
continue
|
||||
}
|
||||
|
||||
frontendOnlyDefs[name] = {
|
||||
frontendOnlyDefs.push({
|
||||
name,
|
||||
display_name: name,
|
||||
category: node.category || '__frontend_only__',
|
||||
@@ -906,54 +903,33 @@ export class ComfyApp {
|
||||
output_node: false,
|
||||
python_module: 'custom_nodes.frontend_only',
|
||||
description: node.description ?? `Frontend only node for ${name}`
|
||||
} as ComfyNodeDefV1
|
||||
})
|
||||
}
|
||||
|
||||
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))
|
||||
return frontendOnlyDefs
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers nodes with the graph
|
||||
*/
|
||||
async registerNodes() {
|
||||
// Load node definitions from the backend
|
||||
const defs = await this.getNodeDefs()
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
|
||||
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 useExtensionService().invokeExtensionsAsync('registerCustomNodes')
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
const defs = await this.getNodeDefs()
|
||||
const defs = await api.getNodeDefs()
|
||||
for (const nodeId in defs) {
|
||||
this.registerNodeDef(nodeId, defs[nodeId])
|
||||
}
|
||||
@@ -1698,7 +1674,9 @@ export class ComfyApp {
|
||||
)
|
||||
|
||||
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().add({
|
||||
severity: 'success',
|
||||
|
||||
@@ -3,8 +3,6 @@ import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
|
||||
import { useBootstrapStore } from './bootstrapStore'
|
||||
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
@@ -21,17 +19,30 @@ vi.mock('@/i18n', () => ({
|
||||
}))
|
||||
|
||||
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', () => ({
|
||||
useSettingStore: vi.fn(() => ({
|
||||
load: vi.fn(() => {
|
||||
mockIsSettingsReady.value = true
|
||||
}),
|
||||
get isReady() {
|
||||
return mockIsSettingsReady.value
|
||||
},
|
||||
isLoading: ref(false),
|
||||
error: ref(undefined)
|
||||
load: mockSettingStoreLoad,
|
||||
isReady: mockIsSettingsReady,
|
||||
isLoading: { value: false },
|
||||
error: { value: 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(() => {
|
||||
mockIsSettingsReady.value = false
|
||||
mockIsNodeDefsReady.value = false
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
store = useBootstrapStore()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('initializes with all flags false', () => {
|
||||
const settingStore = useSettingStore()
|
||||
expect(settingStore.isReady).toBe(false)
|
||||
expect(mockIsNodeDefsReady.value).toBe(false)
|
||||
expect(mockIsSettingsReady.value).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 () => {
|
||||
const settingStore = useSettingStore()
|
||||
void store.startStoreBootstrap()
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(settingStore.isReady).toBe(true)
|
||||
expect(mockIsSettingsReady.value).toBe(true)
|
||||
expect(store.isI18nReady).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,10 +4,12 @@ import { defineStore } from 'pinia'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useUserStore } from '@/stores/userStore'
|
||||
|
||||
export const useBootstrapStore = defineStore('bootstrap', () => {
|
||||
const settingStore = useSettingStore()
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const {
|
||||
@@ -24,13 +26,14 @@ export const useBootstrapStore = defineStore('bootstrap', () => {
|
||||
{ immediate: false }
|
||||
)
|
||||
|
||||
function startEarlyBootstrap() {
|
||||
void nodeDefStore.load()
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -42,6 +45,7 @@ export const useBootstrapStore = defineStore('bootstrap', () => {
|
||||
return {
|
||||
isI18nReady,
|
||||
i18nError,
|
||||
startEarlyBootstrap,
|
||||
startStoreBootstrap
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
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'
|
||||
|
||||
describe('useNodeDefStore', () => {
|
||||
let store: ReturnType<typeof useNodeDefStore>
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
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(() => {
|
||||
setActivePinia(createPinia())
|
||||
store = useNodeDefStore()
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
const createMockNodeDef = (
|
||||
@@ -31,8 +43,42 @@ describe('useNodeDefStore', () => {
|
||||
...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', () => {
|
||||
it('should register a new filter', () => {
|
||||
const store = useNodeDefStore()
|
||||
const filter: NodeDefFilter = {
|
||||
id: 'test.filter',
|
||||
name: 'Test Filter',
|
||||
@@ -44,6 +90,7 @@ describe('useNodeDefStore', () => {
|
||||
})
|
||||
|
||||
it('should unregister a filter by id', () => {
|
||||
const store = useNodeDefStore()
|
||||
const filter: NodeDefFilter = {
|
||||
id: 'test.filter',
|
||||
name: 'Test Filter',
|
||||
@@ -56,6 +103,7 @@ describe('useNodeDefStore', () => {
|
||||
})
|
||||
|
||||
it('should register core filters on initialization', () => {
|
||||
const store = useNodeDefStore()
|
||||
const deprecatedFilter = store.nodeDefFilters.find(
|
||||
(f) => f.id === 'core.deprecated'
|
||||
)
|
||||
@@ -69,12 +117,23 @@ describe('useNodeDefStore', () => {
|
||||
})
|
||||
|
||||
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
|
||||
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', () => {
|
||||
const store = createFilterTestStore()
|
||||
const normalNode = createMockNodeDef({
|
||||
name: 'normal',
|
||||
deprecated: false
|
||||
@@ -98,6 +157,7 @@ describe('useNodeDefStore', () => {
|
||||
})
|
||||
|
||||
it('should apply multiple filters with AND logic', () => {
|
||||
const store = createFilterTestStore()
|
||||
const node1 = createMockNodeDef({
|
||||
name: 'node1',
|
||||
deprecated: false,
|
||||
@@ -140,6 +200,10 @@ describe('useNodeDefStore', () => {
|
||||
})
|
||||
|
||||
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 = [
|
||||
createMockNodeDef({ name: 'node1' }),
|
||||
createMockNodeDef({ name: 'node2' }),
|
||||
@@ -147,10 +211,12 @@ describe('useNodeDefStore', () => {
|
||||
]
|
||||
|
||||
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', () => {
|
||||
const store = createFilterTestStore()
|
||||
const deprecatedNode = createMockNodeDef({
|
||||
name: 'deprecated',
|
||||
deprecated: true
|
||||
@@ -163,7 +229,7 @@ describe('useNodeDefStore', () => {
|
||||
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)
|
||||
expect(store.visibleNodeDefs).toHaveLength(0)
|
||||
|
||||
@@ -175,6 +241,7 @@ describe('useNodeDefStore', () => {
|
||||
|
||||
describe('core filters behavior', () => {
|
||||
it('should hide deprecated nodes by default', () => {
|
||||
const store = useNodeDefStore()
|
||||
const normalNode = createMockNodeDef({
|
||||
name: 'normal',
|
||||
deprecated: false
|
||||
@@ -186,11 +253,18 @@ describe('useNodeDefStore', () => {
|
||||
|
||||
store.updateNodeDefs([normalNode, deprecatedNode])
|
||||
|
||||
expect(store.visibleNodeDefs).toHaveLength(1)
|
||||
expect(store.visibleNodeDefs[0].name).toBe('normal')
|
||||
// 1 normal test node + 4 system nodes
|
||||
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', () => {
|
||||
const store = useNodeDefStore()
|
||||
const normalNode = createMockNodeDef({
|
||||
name: 'normal',
|
||||
deprecated: false
|
||||
@@ -203,10 +277,12 @@ describe('useNodeDefStore', () => {
|
||||
store.updateNodeDefs([normalNode, deprecatedNode])
|
||||
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', () => {
|
||||
const store = useNodeDefStore()
|
||||
const normalNode = createMockNodeDef({
|
||||
name: 'normal',
|
||||
experimental: false
|
||||
@@ -218,11 +294,18 @@ describe('useNodeDefStore', () => {
|
||||
|
||||
store.updateNodeDefs([normalNode, experimentalNode])
|
||||
|
||||
expect(store.visibleNodeDefs).toHaveLength(1)
|
||||
expect(store.visibleNodeDefs[0].name).toBe('normal')
|
||||
// 1 normal test node + 4 system nodes
|
||||
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', () => {
|
||||
const store = useNodeDefStore()
|
||||
const normalNode = createMockNodeDef({
|
||||
name: 'normal',
|
||||
experimental: false
|
||||
@@ -235,10 +318,12 @@ describe('useNodeDefStore', () => {
|
||||
store.updateNodeDefs([normalNode, experimentalNode])
|
||||
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', () => {
|
||||
const store = useNodeDefStore()
|
||||
const normalNode = createMockNodeDef({
|
||||
name: 'normal',
|
||||
category: 'conditioning',
|
||||
@@ -252,11 +337,18 @@ describe('useNodeDefStore', () => {
|
||||
|
||||
store.updateNodeDefs([normalNode, subgraphNode])
|
||||
|
||||
expect(store.visibleNodeDefs).toHaveLength(1)
|
||||
expect(store.visibleNodeDefs[0].name).toBe('normal')
|
||||
// 1 normal test node + 4 system nodes
|
||||
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', () => {
|
||||
const store = useNodeDefStore()
|
||||
const normalNode = createMockNodeDef({
|
||||
name: 'normal',
|
||||
category: 'conditioning',
|
||||
@@ -270,20 +362,23 @@ describe('useNodeDefStore', () => {
|
||||
|
||||
store.updateNodeDefs([normalNode, fakeSubgraphNode])
|
||||
|
||||
expect(store.visibleNodeDefs).toHaveLength(2)
|
||||
expect(store.visibleNodeDefs.map((n) => n.name)).toEqual([
|
||||
'normal',
|
||||
'FakeSubgraph'
|
||||
])
|
||||
// 2 test nodes + 4 system nodes
|
||||
expect(store.visibleNodeDefs).toHaveLength(2 + SYSTEM_NODE_COUNT)
|
||||
const testNodes = store.visibleNodeDefs
|
||||
.filter((n) => !(n.name in SYSTEM_NODE_DEFS))
|
||||
.map((n) => n.name)
|
||||
expect(testNodes).toEqual(['normal', 'FakeSubgraph'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('performance', () => {
|
||||
it('should perform single traversal for multiple filters', () => {
|
||||
const store = useNodeDefStore()
|
||||
let filterCallCount = 0
|
||||
const testFilterCount = 5
|
||||
|
||||
// Register multiple filters that count their calls
|
||||
for (let i = 0; i < 5; i++) {
|
||||
for (let i = 0; i < testFilterCount; i++) {
|
||||
store.registerNodeDefFilter({
|
||||
id: `test.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}` })
|
||||
)
|
||||
store.updateNodeDefs(nodes)
|
||||
@@ -302,8 +398,9 @@ describe('useNodeDefStore', () => {
|
||||
// Force recomputation by accessing visibleNodeDefs
|
||||
expect(store.visibleNodeDefs).toBeDefined()
|
||||
|
||||
// Each node (10) should be checked by each filter (5 test + 2 core = 7 total)
|
||||
expect(filterCallCount).toBe(10 * 5)
|
||||
// Each node (test nodes + system nodes) checked by each test filter
|
||||
const totalNodes = testNodeCount + SYSTEM_NODE_COUNT
|
||||
expect(filterCallCount).toBe(totalNodes * testFilterCount)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { useAsyncState } from '@vueuse/core'
|
||||
import axios from 'axios'
|
||||
import { retry } from 'es-toolkit'
|
||||
import _ from 'es-toolkit/compat'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { isProxyWidget } from '@/core/graph/subgraph/proxyWidget'
|
||||
import { st } from '@/i18n'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { transformNodeDefV1ToV2 } from '@/schemas/nodeDef/migration'
|
||||
import type {
|
||||
@@ -17,7 +20,10 @@ import type {
|
||||
ComfyOutputTypesSpec as ComfyOutputSpecV1,
|
||||
PriceBadge
|
||||
} from '@/schemas/nodeDefSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import type { ComfyApp } from '@/scripts/app'
|
||||
import { NodeSearchService } from '@/services/nodeSearchService'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
import { useSubgraphStore } from '@/stores/subgraphStore'
|
||||
import { NodeSourceType, getNodeSource } from '@/types/nodeSource'
|
||||
import type { NodeSource } from '@/types/nodeSource'
|
||||
@@ -305,6 +311,28 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
|
||||
const showExperimental = ref(false)
|
||||
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 subgraphStore = useSubgraphStore()
|
||||
// Blueprints first for discoverability in the node library sidebar
|
||||
@@ -335,18 +363,46 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
|
||||
)
|
||||
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 newNodeDefsByDisplayName: Record<string, ComfyNodeDefImpl> = {}
|
||||
|
||||
for (const nodeDef of nodeDefs) {
|
||||
for (const def of allDefs) {
|
||||
const translatedDef = translateNodeDef(def)
|
||||
const nodeDefImpl =
|
||||
nodeDef instanceof ComfyNodeDefImpl
|
||||
? nodeDef
|
||||
: new ComfyNodeDefImpl(nodeDef)
|
||||
translatedDef instanceof ComfyNodeDefImpl
|
||||
? translatedDef
|
||||
: new ComfyNodeDefImpl(translatedDef)
|
||||
|
||||
newNodeDefsByName[nodeDef.name] = nodeDefImpl
|
||||
newNodeDefsByDisplayName[nodeDef.display_name] = nodeDefImpl
|
||||
newNodeDefsByName[translatedDef.name] = nodeDefImpl
|
||||
newNodeDefsByDisplayName[translatedDef.display_name] = nodeDefImpl
|
||||
}
|
||||
|
||||
nodeDefsByName.value = newNodeDefsByName
|
||||
@@ -448,6 +504,12 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
|
||||
showExperimental,
|
||||
nodeDefFilters,
|
||||
|
||||
rawNodeDefs,
|
||||
isReady,
|
||||
isLoading,
|
||||
error,
|
||||
load,
|
||||
|
||||
nodeDefs,
|
||||
nodeDataTypes,
|
||||
visibleNodeDefs,
|
||||
|
||||
Reference in New Issue
Block a user