From 2f7336b5d94f6e2a7c001d1e789c5f073f9010f5 Mon Sep 17 00:00:00 2001 From: hjlarry Date: Mon, 13 Apr 2026 16:59:10 +0800 Subject: [PATCH] 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. --- .../hooks/use-collaborative-workflow.ts | 6 +- .../hooks/__tests__/use-node-crud.spec.ts | 87 +++++++++++++++++++ .../nodes/_base/hooks/use-node-crud.ts | 12 ++- 3 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 web/app/components/workflow/nodes/_base/hooks/__tests__/use-node-crud.spec.ts diff --git a/web/app/components/workflow/hooks/use-collaborative-workflow.ts b/web/app/components/workflow/hooks/use-collaborative-workflow.ts index 267bb2ecb3..c3d4ddc294 100644 --- a/web/app/components/workflow/hooks/use-collaborative-workflow.ts +++ b/web/app/components/workflow/hooks/use-collaborative-workflow.ts @@ -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]) } diff --git a/web/app/components/workflow/nodes/_base/hooks/__tests__/use-node-crud.spec.ts b/web/app/components/workflow/nodes/_base/hooks/__tests__/use-node-crud.spec.ts new file mode 100644 index 0000000000..1ad0c38e8a --- /dev/null +++ b/web/app/components/workflow/nodes/_base/hooks/__tests__/use-node-crud.spec.ts @@ -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'), + }) + }) +}) diff --git a/web/app/components/workflow/nodes/_base/hooks/use-node-crud.ts b/web/app/components/workflow/nodes/_base/hooks/use-node-crud.ts index d1741f0bbb..fad9aa9c5e 100644 --- a/web/app/components/workflow/nodes/_base/hooks/use-node-crud.ts +++ b/web/app/components/workflow/nodes/_base/hooks/use-node-crud.ts @@ -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 = (id: string, data: CommonNodeType) => { const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate() + const updateRef = useRef(handleNodeDataUpdateWithSyncDraft) - const setInputs = (newInputs: CommonNodeType) => { - handleNodeDataUpdateWithSyncDraft({ + useEffect(() => { + updateRef.current = handleNodeDataUpdateWithSyncDraft + }, [handleNodeDataUpdateWithSyncDraft]) + + const setInputs = useCallback((newInputs: CommonNodeType) => { + updateRef.current({ id, data: newInputs, }) - } + }, [id]) return { inputs: data,