fix(deploy-check): race condition fixes (#2710)

This commit is contained in:
Vikhyath Mondreti
2026-01-07 10:48:54 -08:00
committed by GitHub
parent 261becd129
commit 833825f04a
3 changed files with 24 additions and 46 deletions

View File

@@ -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}
/>
</>
)

View File

@@ -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<string, any> = {}
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<string, any> }
}, [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 }
}

View File

@@ -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) {