[feat] Implement getNodeByComfyNodeName API integration (#4343)

This commit is contained in:
Christian Byrne
2025-07-03 17:59:21 -07:00
committed by GitHub
parent 1b4ad61e7f
commit 44bbfa9f39
4 changed files with 137 additions and 10 deletions

View File

@@ -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<WorkflowPack[]>([])
@@ -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
}
/**

View File

@@ -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<components['schemas']['Node']>(endpoint, {
signal
}),
errorContext,
routeSpecificErrors
)
}
return {
isLoading,
error,
@@ -330,6 +371,7 @@ export const useComfyRegistryService = () => {
getPublisherById,
listPacksForPublisher,
getNodeDefs,
postPackReview
postPackReview,
inferPackFromNodeName
}
}

View File

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

View File

@@ -72,6 +72,14 @@ describe('useComfyRegistryStore', () => {
error: ReturnType<typeof ref<string | null>>
listAllPacks: ReturnType<typeof vi.fn>
getPackById: ReturnType<typeof vi.fn>
inferPackFromNodeName: ReturnType<typeof vi.fn>
search: ReturnType<typeof vi.fn>
getPackVersions: ReturnType<typeof vi.fn>
getPackByVersion: ReturnType<typeof vi.fn>
getPublisherById: ReturnType<typeof vi.fn>
listPacksForPublisher: ReturnType<typeof vi.fn>
getNodeDefs: ReturnType<typeof vi.fn>
postPackReview: ReturnType<typeof vi.fn>
}
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)
})
})
})