feat: optimize empty search to use cached /nodes endpoint (#8159)

## Summary

Optimizes the Manager dialog to use the cached `GET /nodes` endpoint
instead of `GET /nodes/search` for empty search queries (when the dialog
first opens). This significantly reduces Algolia usage since empty
searches account for the majority of search requests.

## Changes

- **registrySearchProvider.ts**: Modified `searchPacks()` to detect
empty queries and route them to `listAllPacks()` instead of `search()`
- **registrySearchProvider.test.ts**: Added 5 new test cases covering
empty query behavior
- Cache clearing now clears both `search` and `listAllPacks` caches

## Technical Details

**Empty Query Flow (NEW):**
- Query: `""` or whitespace
- Endpoint: `GET /nodes?limit=X&page=Y`
- Cache: Server-side cached (via omitting `latest` parameter)
- Result: Fast, cached node pack list

**Non-Empty Query Flow (UNCHANGED):**
- Query: Any non-empty string
- Endpoint: `GET /nodes/search?search=X` or `comfy_node_search=X`
- Result: Search results as before

## Testing

```bash
pnpm test:unit -- src/services/providers/registrySearchProvider.test.ts
pnpm typecheck
```
This commit is contained in:
Johnpaul Chiwetelu
2026-01-20 00:03:17 +01:00
committed by GitHub
parent b0d7a7f0f4
commit 0d0576faab
2 changed files with 100 additions and 11 deletions

View File

@@ -11,6 +11,8 @@ vi.mock('@/stores/comfyRegistryStore', () => ({
describe('useComfyRegistrySearchProvider', () => {
const mockSearchCall = vi.fn()
const mockSearchClear = vi.fn()
const mockListAllPacksCall = vi.fn()
const mockListAllPacksClear = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
@@ -20,6 +22,10 @@ describe('useComfyRegistrySearchProvider', () => {
search: {
call: mockSearchCall,
clear: mockSearchClear
},
listAllPacks: {
call: mockListAllPacksCall,
clear: mockListAllPacksClear
}
} as any)
})
@@ -111,14 +117,85 @@ describe('useComfyRegistrySearchProvider', () => {
expect(result.nodePacks).toEqual([])
expect(result.querySuggestions).toEqual([])
})
it('should use listAllPacks for empty query', async () => {
const mockResults = {
nodes: [
{ id: '1', name: 'Pack 1' },
{ id: '2', name: 'Pack 2' }
]
}
mockListAllPacksCall.mockResolvedValue(mockResults)
const provider = useComfyRegistrySearchProvider()
const result = await provider.searchPacks('', {
pageSize: 20,
pageNumber: 0
})
expect(mockListAllPacksCall).toHaveBeenCalledWith({
limit: 20,
page: 1
})
expect(mockSearchCall).not.toHaveBeenCalled()
expect(result.nodePacks).toEqual(mockResults.nodes)
expect(result.querySuggestions).toEqual([])
})
it('should use listAllPacks for whitespace-only query', async () => {
const mockResults = {
nodes: [{ id: '1', name: 'Pack 1' }]
}
mockListAllPacksCall.mockResolvedValue(mockResults)
const provider = useComfyRegistrySearchProvider()
const result = await provider.searchPacks(' ', {
pageSize: 10,
pageNumber: 0
})
expect(mockListAllPacksCall).toHaveBeenCalledWith({
limit: 10,
page: 1
})
expect(mockSearchCall).not.toHaveBeenCalled()
expect(result.nodePacks).toEqual(mockResults.nodes)
})
it('should handle empty results from listAllPacks', async () => {
mockListAllPacksCall.mockResolvedValue({ nodes: [] })
const provider = useComfyRegistrySearchProvider()
const result = await provider.searchPacks('', {
pageSize: 10,
pageNumber: 0
})
expect(result.nodePacks).toEqual([])
expect(result.querySuggestions).toEqual([])
})
it('should handle null results from listAllPacks', async () => {
mockListAllPacksCall.mockResolvedValue(null)
const provider = useComfyRegistrySearchProvider()
const result = await provider.searchPacks('', {
pageSize: 10,
pageNumber: 0
})
expect(result.nodePacks).toEqual([])
expect(result.querySuggestions).toEqual([])
})
})
describe('clearSearchCache', () => {
it('should delegate to store search.clear', () => {
it('should clear both search and listAllPacks caches', () => {
const provider = useComfyRegistrySearchProvider()
provider.clearSearchCache()
expect(mockSearchClear).toHaveBeenCalled()
expect(mockListAllPacksClear).toHaveBeenCalled()
})
})

View File

@@ -25,33 +25,45 @@ export const useComfyRegistrySearchProvider = (): NodePackSearchProvider => {
): Promise<SearchPacksResult> => {
const { pageSize, pageNumber, restrictSearchableAttributes } = params
// Determine search mode based on searchable attributes
// For empty queries, use the cached listAllPacks endpoint instead of search
if (!query || query.trim() === '') {
const listParams = {
limit: pageSize,
page: pageNumber + 1 // Registry API uses 1-based pagination
// Note: omitting 'latest' parameter defaults to cached result
}
const listResult = await registryStore.listAllPacks.call(listParams)
const nodePacks = listResult?.nodes ?? []
return {
nodePacks,
querySuggestions: []
}
}
// For non-empty queries, use the search endpoint
const isNodeSearch = restrictSearchableAttributes?.includes('comfy_nodes')
const searchParams = {
search: isNodeSearch ? undefined : query,
comfy_node_search: isNodeSearch ? query : undefined,
limit: pageSize,
page: pageNumber + 1 // Registry API uses 1-based pagination
page: pageNumber + 1
}
const searchResult = await registryStore.search.call(searchParams)
if (!searchResult || !searchResult.nodes) {
return {
nodePacks: [],
querySuggestions: []
}
}
const nodePacks = searchResult?.nodes ?? []
return {
nodePacks: searchResult.nodes,
nodePacks,
querySuggestions: [] // Registry doesn't support query suggestions
}
}
const clearSearchCache = () => {
registryStore.search.clear()
registryStore.listAllPacks.clear()
}
const getSortValue = (