fix knowledge retrieval node add freeze

Knowledge Retrieval panel effects depend on setInputs. In the collaboration branch the callback chain started changing identity on every render, so adding this node could repeatedly write node data and freeze the browser.

Stabilize the collaborative workflow API returned by useCollaborativeWorkflow and keep useNodeCrud.setInputs stable while forwarding to the latest updater. Add a regression test for the stable setInputs contract.
This commit is contained in:
hjlarry 2026-04-13 16:59:10 +08:00
parent d32bc1a364
commit 2f7336b5d9
3 changed files with 99 additions and 6 deletions

View File

@ -1,5 +1,5 @@
import type { Edge, Node } from '../types'
import { useCallback } from 'react'
import { useCallback, useMemo } from 'react'
import { useStoreApi } from 'reactflow'
import { collaborationManager } from '../collaboration/core/collaboration-manager'
@ -77,9 +77,9 @@ export const useCollaborativeWorkflow = () => {
}
}, [store, setNodes, setEdges])
return {
return useMemo(() => ({
getState: collaborativeStore,
setNodes,
setEdges,
}
}), [collaborativeStore, setEdges, setNodes])
}

View File

@ -0,0 +1,87 @@
import type { CommonNodeType } from '@/app/components/workflow/types'
import { renderHook } from '@testing-library/react'
import { BlockEnum } from '@/app/components/workflow/types'
import useNodeCrud from '../use-node-crud'
const mockHandleNodeDataUpdateWithSyncDraft = vi.hoisted(() => ({
current: vi.fn(),
}))
vi.mock('@/app/components/workflow/hooks', () => ({
useNodeDataUpdate: () => ({
handleNodeDataUpdateWithSyncDraft: mockHandleNodeDataUpdateWithSyncDraft.current,
}),
}))
type TestNodeData = CommonNodeType<{
value: string
}>
const createData = (value = 'initial'): TestNodeData => ({
type: BlockEnum.LLM,
title: 'Test Node',
desc: '',
value,
})
describe('useNodeCrud', () => {
beforeEach(() => {
vi.clearAllMocks()
mockHandleNodeDataUpdateWithSyncDraft.current = vi.fn()
})
it('keeps setInputs stable across rerenders when id does not change', () => {
const { result, rerender } = renderHook(
({ id, data }) => useNodeCrud(id, data),
{
initialProps: {
id: 'node-1',
data: createData(),
},
},
)
const firstSetInputs = result.current.setInputs
rerender({
id: 'node-1',
data: createData('updated'),
})
expect(result.current.setInputs).toBe(firstSetInputs)
})
it('forwards node data updates with the current node id and latest updater', () => {
const { result, rerender } = renderHook(
({ id, data }) => useNodeCrud(id, data),
{
initialProps: {
id: 'node-1',
data: createData(),
},
},
)
result.current.setInputs(createData('changed'))
expect(mockHandleNodeDataUpdateWithSyncDraft.current).toHaveBeenCalledWith({
id: 'node-1',
data: createData('changed'),
})
const nextUpdater = vi.fn()
mockHandleNodeDataUpdateWithSyncDraft.current = nextUpdater
rerender({
id: 'node-1',
data: createData('changed'),
})
result.current.setInputs(createData('latest'))
expect(nextUpdater).toHaveBeenCalledWith({
id: 'node-1',
data: createData('latest'),
})
})
})

View File

@ -1,15 +1,21 @@
import type { CommonNodeType } from '@/app/components/workflow/types'
import { useCallback, useEffect, useRef } from 'react'
import { useNodeDataUpdate } from '@/app/components/workflow/hooks'
const useNodeCrud = <T>(id: string, data: CommonNodeType<T>) => {
const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
const updateRef = useRef(handleNodeDataUpdateWithSyncDraft)
const setInputs = (newInputs: CommonNodeType<T>) => {
handleNodeDataUpdateWithSyncDraft({
useEffect(() => {
updateRef.current = handleNodeDataUpdateWithSyncDraft
}, [handleNodeDataUpdateWithSyncDraft])
const setInputs = useCallback((newInputs: CommonNodeType<T>) => {
updateRef.current({
id,
data: newInputs,
})
}
}, [id])
return {
inputs: data,