From e147e93346725e19c21fe5cdc1e5fa9acf385dcc Mon Sep 17 00:00:00 2001 From: Szymon Tondowski Date: Fri, 26 Jun 2026 18:08:19 +0200 Subject: [PATCH] fix: Resolve race condition between palette fetch and node upload in the app (WB-340) --- .changeset/nodes-palette-race-condition.md | 5 ++++ .../src/store/slices/diagram-slice/actions.ts | 25 ++++++++++++++++++- .../src/store/slices/palette/palette-slice.ts | 4 +++ .../src/utils/validation/get-node-errors.ts | 4 +++ 4 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 .changeset/nodes-palette-race-condition.md diff --git a/.changeset/nodes-palette-race-condition.md b/.changeset/nodes-palette-race-condition.md new file mode 100644 index 000000000..50e6f2931 --- /dev/null +++ b/.changeset/nodes-palette-race-condition.md @@ -0,0 +1,5 @@ +--- +'@workflowbuilder/sdk': patch +--- + +Simultaneous loading of nodes and the palette can result in a race condition. If nodes are loaded before the palette, new errors (for example, for fields that were not evaluated earlier) are skipped, and the node only shows a “!” indicator when the user selects it. diff --git a/packages/sdk/src/store/slices/diagram-slice/actions.ts b/packages/sdk/src/store/slices/diagram-slice/actions.ts index b70ba0d4c..282e3b000 100644 --- a/packages/sdk/src/store/slices/diagram-slice/actions.ts +++ b/packages/sdk/src/store/slices/diagram-slice/actions.ts @@ -1,4 +1,6 @@ // About actions: apps/demo/src/app/store/README.md +import { isDeepEqual } from 'remeda'; + import { migrateLegacyHandleIdsOnEdges, migrateLegacyHandleIdsOnNodes, @@ -8,7 +10,7 @@ import type { VariableDefinition } from '../../../features/variables/types'; import type { LayoutDirection } from '../../../node/common'; import type { WorkflowBuilderEdge, WorkflowBuilderNode } from '../../../node/node-data'; import type { IntegrationDataFormat } from '../../../types/integration'; -import { getNodeWithErrors } from '../../../utils/validation/get-node-errors'; +import { getNodeWithErrors, getNodesWithErrors } from '../../../utils/validation/get-node-errors'; import { useStore } from '../../store'; import { skipDynamicValuesInEdges, skipDynamicValuesInNodes } from './utils/dynamic-values'; @@ -122,6 +124,27 @@ export function getStoreSingleSelected() { return selectSingleSelectedElement(state); } +/** + * Revalidates all nodes in the store and updates their JSON schema validation errors if needed. + * + * Compares current nodes with revalidated ones and only updates state when errors change, + * avoiding unnecessary re-renders. + * + * @category Store + */ +export function refreshNodesErrorsIfNeeded() { + const stateNodes = useStore.getState().nodes; + const stateNodesWithRefreshedErrors = getNodesWithErrors(stateNodes); + + if (isDeepEqual(stateNodes, stateNodesWithRefreshedErrors)) { + return; + } + + useStore.setState({ + nodes: stateNodesWithRefreshedErrors, + }); +} + export function saveVariableDefinition(definition: VariableDefinition) { useStore.setState((state) => ({ globalVariables: { diff --git a/packages/sdk/src/store/slices/palette/palette-slice.ts b/packages/sdk/src/store/slices/palette/palette-slice.ts index 3d9d81826..8714a463a 100644 --- a/packages/sdk/src/store/slices/palette/palette-slice.ts +++ b/packages/sdk/src/store/slices/palette/palette-slice.ts @@ -7,6 +7,7 @@ import { StatusType, } from '../../../node/common'; import type { GetDiagramState, SetDiagramState } from '../../store'; +import { refreshNodesErrorsIfNeeded } from '../diagram-slice/actions'; export type PaletteState = { isSidebarExpanded: boolean; @@ -40,6 +41,9 @@ export function usePaletteSlice(set: SetDiagramState, get: GetDiagramState): Pal data: getPaletteData(), fetchDataStatus: StatusType.Success, }); + + // Set a timeout to postpone to next event loop iteration + setTimeout(() => refreshNodesErrorsIfNeeded(), 1); }, getNodeDefinition: (nodeType) => { const { data } = get(); diff --git a/packages/sdk/src/utils/validation/get-node-errors.ts b/packages/sdk/src/utils/validation/get-node-errors.ts index 15f98c9d3..40ea02521 100644 --- a/packages/sdk/src/utils/validation/get-node-errors.ts +++ b/packages/sdk/src/utils/validation/get-node-errors.ts @@ -42,3 +42,7 @@ export function getNodeWithErrors(node: WorkflowBuilderNode) { }, }; } + +export function getNodesWithErrors(nodes: WorkflowBuilderNode[]): WorkflowBuilderNode[] { + return nodes.map(getNodeWithErrors); +}