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 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')

View File

@@ -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',

View File

@@ -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)
})
})

View File

@@ -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
}
})

View File

@@ -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)
})
})
})

View File

@@ -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,