From 23530a3ada5f57e79cc9d188cbf54c610b675744 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 7 Jan 2026 10:38:24 -0800 Subject: [PATCH] fix(deploy-check): race condition fixes --- .../panel/components/deploy/deploy.tsx | 19 ++--------- .../deploy/hooks/use-change-detection.ts | 17 +++------- .../deploy/hooks/use-deployed-state.ts | 34 +++++++++---------- 3 files changed, 24 insertions(+), 46 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/deploy.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/deploy.tsx index ca3e049c9b..ad8d3b098d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/deploy.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/deploy.tsx @@ -1,6 +1,6 @@ 'use client' -import { useCallback, useState } from 'react' +import { useState } from 'react' import { Loader2 } from 'lucide-react' import { Button, Tooltip } from '@/components/emcn' import { DeployModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal' @@ -62,26 +62,13 @@ export function Deploy({ activeWorkflowId, userPermissions, className }: DeployP const canDeploy = userPermissions.canAdmin const isDisabled = isDeploying || !canDeploy || isEmpty - /** - * Handle deploy button click - */ - const onDeployClick = useCallback(async () => { + const onDeployClick = async () => { if (!canDeploy || !activeWorkflowId) return const result = await handleDeployClick() if (result.shouldOpenModal) { setIsModalOpen(true) } - }, [canDeploy, activeWorkflowId, handleDeployClick]) - - const refetchWithErrorHandling = async () => { - if (!activeWorkflowId) return - - try { - await refetchDeployedState() - } catch (error) { - // Error already logged in hook - } } /** @@ -135,7 +122,7 @@ export function Deploy({ activeWorkflowId, userPermissions, className }: DeployP needsRedeployment={changeDetected} deployedState={deployedState!} isLoadingDeployedState={isLoadingDeployedState} - refetchDeployedState={refetchWithErrorHandling} + refetchDeployedState={refetchDeployedState} /> ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-change-detection.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-change-detection.ts index 869c096ce7..ed0bb66f64 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-change-detection.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-change-detection.ts @@ -1,6 +1,5 @@ import { useMemo } from 'react' import { hasWorkflowChanged } from '@/lib/workflows/comparison' -import { useDebounce } from '@/hooks/use-debounce' import { useVariablesStore } from '@/stores/panel/variables/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' @@ -48,16 +47,12 @@ export function useChangeDetection({ const blockSubValues = subBlockValues?.[blockId] || {} const subBlocks: Record = {} - for (const [subId, value] of Object.entries(blockSubValues)) { - subBlocks[subId] = { value } - } - if (block.subBlocks) { for (const [subId, subBlock] of Object.entries(block.subBlocks)) { - if (!subBlocks[subId]) { - subBlocks[subId] = subBlock - } else { - subBlocks[subId] = { ...subBlock, value: subBlocks[subId].value } + const storedValue = blockSubValues[subId] + subBlocks[subId] = { + ...subBlock, + value: storedValue !== undefined ? storedValue : subBlock.value, } } } @@ -77,14 +72,12 @@ export function useChangeDetection({ } as WorkflowState & { variables: Record } }, [workflowId, blocks, edges, loops, parallels, subBlockValues, workflowVariables]) - const rawChangeDetected = useMemo(() => { + const changeDetected = useMemo(() => { if (!currentState || !deployedState || isLoadingDeployedState) { return false } return hasWorkflowChanged(currentState, deployedState) }, [currentState, deployedState, isLoadingDeployedState]) - const changeDetected = useDebounce(rawChangeDetected, 300) - return { changeDetected } } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-deployed-state.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-deployed-state.ts index 530250b0d4..0bcdad7006 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-deployed-state.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-deployed-state.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import { createLogger } from '@sim/logger' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import type { WorkflowState } from '@/stores/workflows/workflow/types' @@ -27,29 +27,27 @@ export function useDeployedState({ (state) => state.setWorkflowNeedsRedeployment ) - /** - * Fetches the deployed state of the workflow from the server - * This is the single source of truth for deployed workflow state - */ - const fetchDeployedState = async () => { - if (!workflowId || !isDeployed) { + const fetchDeployedState = useCallback(async () => { + const registry = useWorkflowRegistry.getState() + const currentWorkflowId = registry.activeWorkflowId + const deploymentStatus = currentWorkflowId + ? registry.getWorkflowDeploymentStatus(currentWorkflowId) + : null + const currentIsDeployed = deploymentStatus?.isDeployed ?? false + + if (!currentWorkflowId || !currentIsDeployed) { setDeployedState(null) return } - // Store the workflow ID at the start of the request to prevent race conditions - const requestWorkflowId = workflowId - - // Helper to get current active workflow ID for race condition checks - const getCurrentActiveWorkflowId = () => useWorkflowRegistry.getState().activeWorkflowId + const requestWorkflowId = currentWorkflowId try { setIsLoadingDeployedState(true) const response = await fetch(`/api/workflows/${requestWorkflowId}/deployed`) - // Check if the workflow ID changed during the request (user navigated away) - if (requestWorkflowId !== getCurrentActiveWorkflowId()) { + if (requestWorkflowId !== useWorkflowRegistry.getState().activeWorkflowId) { logger.debug('Workflow changed during deployed state fetch, ignoring response') return } @@ -64,22 +62,22 @@ export function useDeployedState({ const data = await response.json() - if (requestWorkflowId === getCurrentActiveWorkflowId()) { + if (requestWorkflowId === useWorkflowRegistry.getState().activeWorkflowId) { setDeployedState(data.deployedState || null) } else { logger.debug('Workflow changed after deployed state response, ignoring result') } } catch (error) { logger.error('Error fetching deployed state:', { error }) - if (requestWorkflowId === getCurrentActiveWorkflowId()) { + if (requestWorkflowId === useWorkflowRegistry.getState().activeWorkflowId) { setDeployedState(null) } } finally { - if (requestWorkflowId === getCurrentActiveWorkflowId()) { + if (requestWorkflowId === useWorkflowRegistry.getState().activeWorkflowId) { setIsLoadingDeployedState(false) } } - } + }, []) useEffect(() => { if (!workflowId) {