From 440262a51b28272fbba6c16a8a9b4cf5bbb9b920 Mon Sep 17 00:00:00 2001 From: lyzno1 <92089059+lyzno1@users.noreply.github.com> Date: Mon, 27 Oct 2025 13:29:40 +0800 Subject: [PATCH] fix: serialize workflow draft sync operations (#27487) --- .../hooks/use-nodes-sync-draft.ts | 5 ++++- .../hooks/use-nodes-sync-draft.ts | 5 ++++- web/app/components/workflow/hooks/index.ts | 1 + .../hooks/use-serial-async-callback.ts | 22 +++++++++++++++++++ .../components/workflow/hooks/use-workflow.ts | 4 ++-- 5 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 web/app/components/workflow/hooks/use-serial-async-callback.ts diff --git a/web/app/components/rag-pipeline/hooks/use-nodes-sync-draft.ts b/web/app/components/rag-pipeline/hooks/use-nodes-sync-draft.ts index d9de69716e..b70a2e6a34 100644 --- a/web/app/components/rag-pipeline/hooks/use-nodes-sync-draft.ts +++ b/web/app/components/rag-pipeline/hooks/use-nodes-sync-draft.ts @@ -7,6 +7,7 @@ import { import { useNodesReadOnly, } from '@/app/components/workflow/hooks/use-workflow' +import { useSerialAsyncCallback } from '@/app/components/workflow/hooks/use-serial-async-callback' import { API_PREFIX } from '@/config' import { syncWorkflowDraft } from '@/service/workflow' import { usePipelineRefreshDraft } from '.' @@ -83,7 +84,7 @@ export const useNodesSyncDraft = () => { } }, [getPostParams, getNodesReadOnly]) - const doSyncWorkflowDraft = useCallback(async ( + const performSync = useCallback(async ( notRefreshWhenSyncError?: boolean, callback?: { onSuccess?: () => void @@ -121,6 +122,8 @@ export const useNodesSyncDraft = () => { } }, [getPostParams, getNodesReadOnly, workflowStore, handleRefreshWorkflowDraft]) + const doSyncWorkflowDraft = useSerialAsyncCallback(performSync, getNodesReadOnly) + return { doSyncWorkflowDraft, syncWorkflowDraftWhenPageClose, diff --git a/web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts b/web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts index 41b43f5041..56d9021feb 100644 --- a/web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts +++ b/web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts @@ -3,6 +3,7 @@ import { produce } from 'immer' import { useStoreApi } from 'reactflow' import { useWorkflowStore } from '@/app/components/workflow/store' import { useNodesReadOnly } from '@/app/components/workflow/hooks/use-workflow' +import { useSerialAsyncCallback } from '@/app/components/workflow/hooks/use-serial-async-callback' import { syncWorkflowDraft } from '@/service/workflow' import { useFeaturesStore } from '@/app/components/base/features/hooks' import { API_PREFIX } from '@/config' @@ -87,7 +88,7 @@ export const useNodesSyncDraft = () => { navigator.sendBeacon(`${API_PREFIX}${postParams.url}`, JSON.stringify(postParams.params)) }, [getPostParams, getNodesReadOnly]) - const doSyncWorkflowDraft = useCallback(async ( + const performSync = useCallback(async ( notRefreshWhenSyncError?: boolean, callback?: { onSuccess?: () => void @@ -125,6 +126,8 @@ export const useNodesSyncDraft = () => { } }, [workflowStore, getPostParams, getNodesReadOnly, handleRefreshWorkflowDraft]) + const doSyncWorkflowDraft = useSerialAsyncCallback(performSync, getNodesReadOnly) + return { doSyncWorkflowDraft, syncWorkflowDraftWhenPageClose, diff --git a/web/app/components/workflow/hooks/index.ts b/web/app/components/workflow/hooks/index.ts index 933b328227..1131836b35 100644 --- a/web/app/components/workflow/hooks/index.ts +++ b/web/app/components/workflow/hooks/index.ts @@ -23,3 +23,4 @@ export * from './use-inspect-vars-crud' export * from './use-set-workflow-vars-with-value' export * from './use-workflow-search' export * from './use-auto-generate-webhook-url' +export * from './use-serial-async-callback' diff --git a/web/app/components/workflow/hooks/use-serial-async-callback.ts b/web/app/components/workflow/hooks/use-serial-async-callback.ts new file mode 100644 index 0000000000..c36409a776 --- /dev/null +++ b/web/app/components/workflow/hooks/use-serial-async-callback.ts @@ -0,0 +1,22 @@ +import { + useCallback, + useRef, +} from 'react' + +export const useSerialAsyncCallback = ( + fn: (...args: Args) => Promise | Result, + shouldSkip?: () => boolean, +) => { + const queueRef = useRef>(Promise.resolve()) + + return useCallback((...args: Args) => { + if (shouldSkip?.()) + return Promise.resolve(undefined as Result) + + const lastPromise = queueRef.current.catch(() => undefined) + const nextPromise = lastPromise.then(() => fn(...args)) + queueRef.current = nextPromise + + return nextPromise + }, [fn, shouldSkip]) +} diff --git a/web/app/components/workflow/hooks/use-workflow.ts b/web/app/components/workflow/hooks/use-workflow.ts index 9ab35e3133..07ea488d2f 100644 --- a/web/app/components/workflow/hooks/use-workflow.ts +++ b/web/app/components/workflow/hooks/use-workflow.ts @@ -523,14 +523,14 @@ export const useNodesReadOnly = () => { const historyWorkflowData = useStore(s => s.historyWorkflowData) const isRestoring = useStore(s => s.isRestoring) - const getNodesReadOnly = useCallback(() => { + const getNodesReadOnly = useCallback((): boolean => { const { workflowRunningData, historyWorkflowData, isRestoring, } = workflowStore.getState() - return workflowRunningData?.result.status === WorkflowRunningStatus.Running || historyWorkflowData || isRestoring + return !!(workflowRunningData?.result.status === WorkflowRunningStatus.Running || historyWorkflowData || isRestoring) }, [workflowStore]) return {