mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 19:21:54 +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 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')
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user