mirror of
https://github.com/langgenius/dify.git
synced 2026-04-05 20:22:39 +08:00
perf(skill): cache normalized asset tree index
This commit is contained in:
@@ -3,6 +3,7 @@ import type { AppAssetTreeResponse, AppAssetTreeView } from '@/types/app-asset'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import {
|
||||
getSkillAssetIndex,
|
||||
useExistingSkillNames,
|
||||
useSkillAssetNodeMap,
|
||||
useSkillAssetTreeData,
|
||||
@@ -102,6 +103,30 @@ describe('useSkillAssetTree', () => {
|
||||
expect(map.size).toBe(2)
|
||||
})
|
||||
|
||||
it('should reuse the same selector reference and cached map for the same tree response', () => {
|
||||
renderHook(() => useSkillAssetNodeMap())
|
||||
renderHook(() => useSkillAssetNodeMap())
|
||||
|
||||
const firstOptions = mockUseQuery.mock.calls[0][0] as {
|
||||
select: (data: AppAssetTreeResponse) => Map<string, AppAssetTreeView>
|
||||
}
|
||||
const secondOptions = mockUseQuery.mock.calls[1][0] as {
|
||||
select: (data: AppAssetTreeResponse) => Map<string, AppAssetTreeView>
|
||||
}
|
||||
const treeData = {
|
||||
children: [
|
||||
createTreeNode({
|
||||
id: 'folder-1',
|
||||
node_type: 'folder',
|
||||
name: 'skill-a',
|
||||
}),
|
||||
],
|
||||
} satisfies AppAssetTreeResponse
|
||||
|
||||
expect(firstOptions.select).toBe(secondOptions.select)
|
||||
expect(firstOptions.select(treeData)).toBe(firstOptions.select(treeData))
|
||||
})
|
||||
|
||||
it('should return an empty map when tree response has no children', () => {
|
||||
renderHook(() => useSkillAssetNodeMap())
|
||||
|
||||
@@ -157,6 +182,30 @@ describe('useSkillAssetTree', () => {
|
||||
expect(names.size).toBe(2)
|
||||
})
|
||||
|
||||
it('should reuse the same selector reference and cached names for the same tree response', () => {
|
||||
renderHook(() => useExistingSkillNames())
|
||||
renderHook(() => useExistingSkillNames())
|
||||
|
||||
const firstOptions = mockUseQuery.mock.calls[0][0] as {
|
||||
select: (data: AppAssetTreeResponse) => Set<string>
|
||||
}
|
||||
const secondOptions = mockUseQuery.mock.calls[1][0] as {
|
||||
select: (data: AppAssetTreeResponse) => Set<string>
|
||||
}
|
||||
const treeData = {
|
||||
children: [
|
||||
createTreeNode({
|
||||
id: 'folder-1',
|
||||
node_type: 'folder',
|
||||
name: 'skill-a',
|
||||
}),
|
||||
],
|
||||
} satisfies AppAssetTreeResponse
|
||||
|
||||
expect(firstOptions.select).toBe(secondOptions.select)
|
||||
expect(firstOptions.select(treeData)).toBe(firstOptions.select(treeData))
|
||||
})
|
||||
|
||||
it('should return an empty set when tree response has no children', () => {
|
||||
renderHook(() => useExistingSkillNames())
|
||||
|
||||
@@ -169,4 +218,41 @@ describe('useSkillAssetTree', () => {
|
||||
expect(names.size).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getSkillAssetIndex', () => {
|
||||
it('should share the same normalized index for the same tree response', () => {
|
||||
const treeData = {
|
||||
children: [
|
||||
createTreeNode({
|
||||
id: 'folder-1',
|
||||
node_type: 'folder',
|
||||
name: 'skill-a',
|
||||
children: [
|
||||
createTreeNode({
|
||||
id: 'file-1',
|
||||
node_type: 'file',
|
||||
name: 'README.md',
|
||||
extension: 'md',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
} satisfies AppAssetTreeResponse
|
||||
|
||||
const firstIndex = getSkillAssetIndex(treeData)
|
||||
const secondIndex = getSkillAssetIndex(treeData)
|
||||
|
||||
expect(firstIndex).toBe(secondIndex)
|
||||
expect(firstIndex.nodeMap).toBe(secondIndex.nodeMap)
|
||||
expect(firstIndex.existingSkillNames).toBe(secondIndex.existingSkillNames)
|
||||
expect(firstIndex.nodeMap.get('file-1')?.name).toBe('README.md')
|
||||
expect(firstIndex.existingSkillNames.has('skill-a')).toBe(true)
|
||||
})
|
||||
|
||||
it('should reuse the shared empty index when tree data is missing', () => {
|
||||
expect(getSkillAssetIndex()).toBe(getSkillAssetIndex(undefined))
|
||||
expect(getSkillAssetIndex(null).nodeMap.size).toBe(0)
|
||||
expect(getSkillAssetIndex(null).existingSkillNames.size).toBe(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,11 +4,49 @@ import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { appAssetTreeOptions } from '@/service/use-app-asset'
|
||||
import { buildNodeMap } from '../../../utils/tree-utils'
|
||||
|
||||
type SkillAssetIndex = {
|
||||
nodeMap: Map<string, AppAssetTreeView>
|
||||
existingSkillNames: Set<string>
|
||||
}
|
||||
|
||||
const EMPTY_NODE_MAP = new Map<string, AppAssetTreeView>()
|
||||
const EMPTY_SKILL_NAMES = new Set<string>()
|
||||
const EMPTY_SKILL_ASSET_INDEX: SkillAssetIndex = {
|
||||
nodeMap: EMPTY_NODE_MAP,
|
||||
existingSkillNames: EMPTY_SKILL_NAMES,
|
||||
}
|
||||
const skillAssetIndexCache = new WeakMap<AppAssetTreeResponse, SkillAssetIndex>()
|
||||
|
||||
function useSkillAppId(): string {
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
return appDetail?.id || ''
|
||||
}
|
||||
|
||||
export function getSkillAssetIndex(data?: AppAssetTreeResponse | null): SkillAssetIndex {
|
||||
if (!data?.children?.length)
|
||||
return EMPTY_SKILL_ASSET_INDEX
|
||||
|
||||
const cachedIndex = skillAssetIndexCache.get(data)
|
||||
if (cachedIndex)
|
||||
return cachedIndex
|
||||
|
||||
const existingSkillNames = new Set<string>()
|
||||
for (const node of data.children) {
|
||||
if (node.node_type === 'folder')
|
||||
existingSkillNames.add(node.name)
|
||||
}
|
||||
|
||||
const index = {
|
||||
nodeMap: buildNodeMap(data.children),
|
||||
existingSkillNames,
|
||||
}
|
||||
skillAssetIndexCache.set(data, index)
|
||||
return index
|
||||
}
|
||||
|
||||
const selectSkillAssetNodeMap = (data: AppAssetTreeResponse) => getSkillAssetIndex(data).nodeMap
|
||||
const selectExistingSkillNames = (data: AppAssetTreeResponse) => getSkillAssetIndex(data).existingSkillNames
|
||||
|
||||
export function useSkillAssetTreeData() {
|
||||
const appId = useSkillAppId()
|
||||
return useQuery(appAssetTreeOptions(appId))
|
||||
@@ -18,11 +56,7 @@ export function useSkillAssetNodeMap() {
|
||||
const appId = useSkillAppId()
|
||||
return useQuery({
|
||||
...appAssetTreeOptions(appId),
|
||||
select: (data: AppAssetTreeResponse): Map<string, AppAssetTreeView> => {
|
||||
if (!data?.children)
|
||||
return new Map()
|
||||
return buildNodeMap(data.children)
|
||||
},
|
||||
select: selectSkillAssetNodeMap,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -30,15 +64,6 @@ export function useExistingSkillNames() {
|
||||
const appId = useSkillAppId()
|
||||
return useQuery({
|
||||
...appAssetTreeOptions(appId),
|
||||
select: (data: AppAssetTreeResponse): Set<string> => {
|
||||
if (!data?.children)
|
||||
return new Set()
|
||||
const names = new Set<string>()
|
||||
for (const node of data.children) {
|
||||
if (node.node_type === 'folder')
|
||||
names.add(node.name)
|
||||
}
|
||||
return names
|
||||
},
|
||||
select: selectExistingSkillNames,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user