From 44bbfa9f39afdd636d1f3af77682c46af5411c68 Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Thu, 3 Jul 2025 17:59:21 -0700 Subject: [PATCH] [feat] Implement getNodeByComfyNodeName API integration (#4343) --- src/composables/nodePack/useWorkflowPacks.ts | 17 ++--- src/services/comfyRegistryService.ts | 44 +++++++++++- src/stores/comfyRegistryStore.ts | 14 ++++ .../tests/store/comfyRegistryStore.test.ts | 72 ++++++++++++++++++- 4 files changed, 137 insertions(+), 10 deletions(-) diff --git a/src/composables/nodePack/useWorkflowPacks.ts b/src/composables/nodePack/useWorkflowPacks.ts index 564fb9438..52c69858f 100644 --- a/src/composables/nodePack/useWorkflowPacks.ts +++ b/src/composables/nodePack/useWorkflowPacks.ts @@ -26,7 +26,7 @@ const CORE_NODES_PACK_NAME = 'comfy-core' export const useWorkflowPacks = (options: UseNodePacksOptions = {}) => { const nodeDefStore = useNodeDefStore() const systemStatsStore = useSystemStatsStore() - const { search } = useComfyRegistryStore() + const { inferPackFromNodeName } = useComfyRegistryStore() const workflowPacks = ref([]) @@ -70,18 +70,19 @@ export const useWorkflowPacks = (options: UseNodePacksOptions = {}) => { } } - // Search the registry for non-core nodes - const searchResult = await search.call({ - comfy_node_search: nodeName, - limit: 1 - }) - if (searchResult?.nodes?.length) { - const pack = searchResult.nodes[0] + // Query the registry to find which pack provides this node + const pack = await inferPackFromNodeName.call(nodeName) + + if (pack) { return { id: pack.id, version: pack.latest_version?.version ?? SelectedVersion.NIGHTLY } } + + // No pack found - this node doesn't exist in the registry or couldn't be + // extracted from the parent node pack successfully + return undefined } /** diff --git a/src/services/comfyRegistryService.ts b/src/services/comfyRegistryService.ts index 74edfd44a..20b418cfa 100644 --- a/src/services/comfyRegistryService.ts +++ b/src/services/comfyRegistryService.ts @@ -318,6 +318,47 @@ export const useComfyRegistryService = () => { ) } + /** + * Get the node pack that contains a specific ComfyUI node by its name. + * This method queries the registry to find which pack provides the given node. + * + * When multiple packs contain a node with the same name, the API returns the best match based on: + * 1. Preemption match - If the node name matches any in the pack's preempted_comfy_node_names array + * 2. Search ranking - Lower search_ranking values are preferred + * 3. Total installs - Higher installation counts are preferred as a tiebreaker + * + * @param nodeName - The name of the ComfyUI node (e.g., 'KSampler', 'CLIPTextEncode') + * @param signal - Optional AbortSignal for request cancellation + * @returns The node pack containing the specified node, or null if not found or on error + * + * @example + * ```typescript + * const pack = await inferPackFromNodeName('KSampler') + * if (pack) { + * console.log(`Node found in pack: ${pack.name}`) + * } + * ``` + */ + const inferPackFromNodeName = async ( + nodeName: operations['getNodeByComfyNodeName']['parameters']['path']['comfyNodeName'], + signal?: AbortSignal + ) => { + const endpoint = `/comfy-nodes/${nodeName}/node` + const errorContext = 'Failed to infer pack from comfy node name' + const routeSpecificErrors = { + 404: `Comfy node not found: The node with name ${nodeName} does not exist in the registry` + } + + return executeApiRequest( + () => + registryApiClient.get(endpoint, { + signal + }), + errorContext, + routeSpecificErrors + ) + } + return { isLoading, error, @@ -330,6 +371,7 @@ export const useComfyRegistryService = () => { getPublisherById, listPacksForPublisher, getNodeDefs, - postPackReview + postPackReview, + inferPackFromNodeName } } diff --git a/src/stores/comfyRegistryStore.ts b/src/stores/comfyRegistryStore.ts index 7aa9a5cf2..5183e0a2c 100644 --- a/src/stores/comfyRegistryStore.ts +++ b/src/stores/comfyRegistryStore.ts @@ -104,6 +104,17 @@ export const useComfyRegistryStore = defineStore('comfyRegistry', () => { ListPacksResult >(registryService.search, { maxSize: PACK_LIST_CACHE_SIZE }) + /** + * Get the node pack that contains a specific ComfyUI node by its name. + * Results are cached to avoid redundant API calls. + * + * @see {@link useComfyRegistryService.inferPackFromNodeName} for details on the ranking algorithm + */ + const inferPackFromNodeName = useCachedRequest< + operations['getNodeByComfyNodeName']['parameters']['path']['comfyNodeName'], + NodePack + >(registryService.inferPackFromNodeName, { maxSize: PACK_BY_ID_CACHE_SIZE }) + /** * Clear all cached data */ @@ -111,6 +122,7 @@ export const useComfyRegistryStore = defineStore('comfyRegistry', () => { getNodeDefs.clear() listAllPacks.clear() getPackById.clear() + inferPackFromNodeName.clear() } /** @@ -120,6 +132,7 @@ export const useComfyRegistryStore = defineStore('comfyRegistry', () => { getNodeDefs.cancel() listAllPacks.cancel() getPackById.cancel() + inferPackFromNodeName.cancel() getPacksByIdController?.abort() } @@ -132,6 +145,7 @@ export const useComfyRegistryStore = defineStore('comfyRegistry', () => { }, getNodeDefs, search, + inferPackFromNodeName, clearCache, cancelRequests, diff --git a/tests-ui/tests/store/comfyRegistryStore.test.ts b/tests-ui/tests/store/comfyRegistryStore.test.ts index 44db6dfeb..fa2c86ab7 100644 --- a/tests-ui/tests/store/comfyRegistryStore.test.ts +++ b/tests-ui/tests/store/comfyRegistryStore.test.ts @@ -72,6 +72,14 @@ describe('useComfyRegistryStore', () => { error: ReturnType> listAllPacks: ReturnType getPackById: ReturnType + inferPackFromNodeName: ReturnType + search: ReturnType + getPackVersions: ReturnType + getPackByVersion: ReturnType + getPublisherById: ReturnType + listPacksForPublisher: ReturnType + getNodeDefs: ReturnType + postPackReview: ReturnType } beforeEach(() => { @@ -106,7 +114,15 @@ describe('useComfyRegistryStore', () => { // Otherwise return paginated results return Promise.resolve(mockListResult) }), - getPackById: vi.fn().mockResolvedValue(mockNodePack) + getPackById: vi.fn().mockResolvedValue(mockNodePack), + inferPackFromNodeName: vi.fn().mockResolvedValue(mockNodePack), + search: vi.fn().mockResolvedValue(mockListResult), + getPackVersions: vi.fn().mockResolvedValue([]), + getPackByVersion: vi.fn().mockResolvedValue({}), + getPublisherById: vi.fn().mockResolvedValue({}), + listPacksForPublisher: vi.fn().mockResolvedValue([]), + getNodeDefs: vi.fn().mockResolvedValue({}), + postPackReview: vi.fn().mockResolvedValue({}) } vi.mocked(useComfyRegistryService).mockReturnValue( @@ -186,4 +202,58 @@ describe('useComfyRegistryStore', () => { expect.any(Object) // abort signal ) }) + + describe('inferPackFromNodeName', () => { + it('should fetch a pack by comfy node name', async () => { + const store = useComfyRegistryStore() + const nodeName = 'KSampler' + + const result = await store.inferPackFromNodeName.call(nodeName) + + expect(result).toEqual(mockNodePack) + expect(mockRegistryService.inferPackFromNodeName).toHaveBeenCalledWith( + nodeName, + expect.any(Object) // abort signal + ) + }) + + it('should cache results', async () => { + const store = useComfyRegistryStore() + const nodeName = 'KSampler' + + // First call + const result1 = await store.inferPackFromNodeName.call(nodeName) + expect(mockRegistryService.inferPackFromNodeName).toHaveBeenCalledTimes(1) + + // Second call - should use cache + const result2 = await store.inferPackFromNodeName.call(nodeName) + expect(mockRegistryService.inferPackFromNodeName).toHaveBeenCalledTimes(1) + expect(result2).toEqual(result1) + }) + + it('should handle null results when node is not found', async () => { + mockRegistryService.inferPackFromNodeName.mockResolvedValueOnce(null) + + const store = useComfyRegistryStore() + const result = await store.inferPackFromNodeName.call('NonExistentNode') + + expect(result).toBeNull() + }) + + it('should clear cache when clearCache is called', async () => { + const store = useComfyRegistryStore() + const nodeName = 'KSampler' + + // First call to populate cache + await store.inferPackFromNodeName.call(nodeName) + expect(mockRegistryService.inferPackFromNodeName).toHaveBeenCalledTimes(1) + + // Clear cache + store.clearCache() + + // Call again - should hit the service again + await store.inferPackFromNodeName.call(nodeName) + expect(mockRegistryService.inferPackFromNodeName).toHaveBeenCalledTimes(2) + }) + }) })