diff --git a/apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/components/deployment-info/deployment-info.tsx b/apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/components/deployment-info/deployment-info.tsx index 596bdcdeef..6c1ad43fe8 100644 --- a/apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/components/deployment-info/deployment-info.tsx +++ b/apps/sim/app/w/[id]/components/control-bar/components/deploy-modal/components/deployment-info/deployment-info.tsx @@ -1,7 +1,7 @@ 'use client' -import { useState } from 'react' -import { Loader2 } from 'lucide-react' +import { useState, useEffect } from 'react' +import { Info, Loader2 } from 'lucide-react' import { AlertDialog, AlertDialogAction, @@ -66,11 +66,10 @@ export function DeploymentInfo({ // If deployedState is already loaded, use it directly if (deployedState) { - logger.info(`Using cached deployed state for workflow: ${workflowId}`) setIsViewingDeployed(true) return } else if (!isLoadingDeployedState) { - logger.debug(`[${workflowId}] No deployed state found`) + logger.debug(`No deployed state found`) addNotification('error', 'Cannot view deployment: No deployed state available', workflowId) } } diff --git a/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/components/deployed-workflow-card.tsx b/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/components/deployed-workflow-card.tsx index a458fddfbd..fe26e855a6 100644 --- a/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/components/deployed-workflow-card.tsx +++ b/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/components/deployed-workflow-card.tsx @@ -8,6 +8,7 @@ import { Label } from '@/components/ui/label' import { cn } from '@/lib/utils' import { createLogger } from '@/lib/logs/console-logger' import { WorkflowPreview } from '@/app/w/components/workflow-preview/workflow-preview' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' const logger = createLogger('DeployedWorkflowCard') @@ -16,11 +17,23 @@ interface DeployedWorkflowCardProps { blocks: Record edges: Array loops: Record + _metadata?: { + workflowId?: string + fetchTimestamp?: number + requestId?: number + [key: string]: any + } } deployedWorkflowState: { blocks: Record edges: Array loops: Record + _metadata?: { + workflowId?: string + fetchTimestamp?: number + requestId?: number + [key: string]: any + } } className?: string } @@ -32,13 +45,24 @@ export function DeployedWorkflowCard({ }: DeployedWorkflowCardProps) { const [showingDeployed, setShowingDeployed] = useState(true) const workflowToShow = showingDeployed ? deployedWorkflowState : currentWorkflowState + const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId) // Create sanitized workflow state const sanitizedWorkflowState = useMemo(() => { if (!workflowToShow) return null; + // Verify the workflow ID matches if metadata exists + if (workflowToShow._metadata?.workflowId && + workflowToShow._metadata.workflowId !== activeWorkflowId) { + logger.warn('Workflow ID mismatch detected in card', { + stateWorkflowId: workflowToShow._metadata.workflowId, + activeWorkflowId, + isDeployed: showingDeployed + }); + } + // Filter out invalid blocks and make deep clone to avoid reference issues - return { + const result = { blocks: Object.fromEntries( Object.entries(workflowToShow.blocks || {}) .filter(([_, block]) => block && block.type) // Filter out invalid blocks @@ -49,14 +73,22 @@ export function DeployedWorkflowCard({ }) ), edges: workflowToShow.edges ? JSON.parse(JSON.stringify(workflowToShow.edges)) : [], - loops: workflowToShow.loops ? JSON.parse(JSON.stringify(workflowToShow.loops)) : {} + loops: workflowToShow.loops ? JSON.parse(JSON.stringify(workflowToShow.loops)) : {}, + _metadata: { + ...(workflowToShow._metadata || {}), + workflowId: activeWorkflowId, + viewType: showingDeployed ? 'deployed' : 'current', + sanitizedAt: Date.now() + } }; - }, [workflowToShow]); + + return result; + }, [workflowToShow, showingDeployed, activeWorkflowId]); // Generate a unique key for the workflow preview const previewKey = useMemo(() => { - return `${showingDeployed ? 'deployed' : 'current'}-preview`; - }, [showingDeployed]); + return `${showingDeployed ? 'deployed' : 'current'}-preview-${activeWorkflowId}-${Date.now()}`; + }, [showingDeployed, activeWorkflowId]); return ( @@ -94,6 +126,8 @@ export function DeployedWorkflowCard({ + +
{/* Workflow preview with fixed height */} diff --git a/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/components/deployed-workflow-modal.tsx b/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/components/deployed-workflow-modal.tsx index 11a2dca099..8ebd1a883a 100644 --- a/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/components/deployed-workflow-modal.tsx +++ b/apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/components/deployed-workflow-modal.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState, useMemo, useEffect } from 'react' +import { useState, useMemo, useEffect, useRef } from 'react' import { AlertDialog, AlertDialogAction, @@ -36,6 +36,12 @@ interface DeployedWorkflowModalProps { edges: Array loops: Record parallels: Record + _metadata?: { + workflowId?: string + fetchTimestamp?: number + requestId?: number + [key: string]: any + } } } @@ -48,6 +54,27 @@ export function DeployedWorkflowModal({ const [isLoading, setIsLoading] = useState(false) const { revertToDeployedState } = useWorkflowStore() const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId) + + // Add instance ID to track component lifecycle + const instanceId = useRef(Date.now()); + const modalOpenCount = useRef(0); + + // Keep track of the original deployed state when modal opens + const initialDeployedStateRef = useRef(null); + + useEffect(() => { + if (isOpen) { + modalOpenCount.current += 1; + + // Store the initial deployed state when modal first opens + if (!initialDeployedStateRef.current) { + initialDeployedStateRef.current = deployedWorkflowState; + } + } else if (initialDeployedStateRef.current) { + // Reset the initial state reference when modal closes + initialDeployedStateRef.current = null; + } + }, [isOpen, deployedWorkflowState, activeWorkflowId]); // Get current workflow state to compare with deployed state const currentWorkflowState = useWorkflowStore((state) => ({ @@ -61,11 +88,6 @@ export function DeployedWorkflowModal({ const sanitizedCurrentState = useMemo(() => { if (!currentWorkflowState) return undefined; - logger.info('Before current state sanitization', { - workflowId: activeWorkflowId, - blockCount: Object.keys(currentWorkflowState.blocks || {}).length - }); - const result = { blocks: Object.fromEntries( Object.entries(currentWorkflowState.blocks || {}) @@ -76,14 +98,14 @@ export function DeployedWorkflowModal({ }) ), edges: currentWorkflowState.edges ? [...currentWorkflowState.edges] : [], - loops: currentWorkflowState.loops ? {...currentWorkflowState.loops} : {} + loops: currentWorkflowState.loops ? {...currentWorkflowState.loops} : {}, + _metadata: { + workflowId: activeWorkflowId || undefined, + type: 'current', + timestamp: Date.now() + } }; - logger.info('After current state sanitization', { - workflowId: activeWorkflowId, - blockCount: Object.keys(result.blocks).length - }); - return result; }, [currentWorkflowState, activeWorkflowId]); @@ -91,10 +113,40 @@ export function DeployedWorkflowModal({ if (!deployedWorkflowState) return { blocks: {}, edges: [], - loops: {} + loops: {}, + _metadata: { + workflowId: activeWorkflowId || undefined, + type: 'deployed-empty', + timestamp: Date.now() + } }; - return { + const stateWorkflowId = deployedWorkflowState?._metadata?.workflowId; + const stateMatch = stateWorkflowId === activeWorkflowId; + + // Check if the deployed state belongs to the current workflow + // This is a critical safety check to prevent showing the wrong workflow state + if (stateWorkflowId && !stateMatch) { + logger.error('Attempted to use deployed state from wrong workflow', { + stateWorkflowId, + activeWorkflowId, + }); + + // Return empty state to prevent showing wrong workflow data + return { + blocks: {}, + edges: [], + loops: {}, + _metadata: { + workflowId: activeWorkflowId || undefined, + type: 'deployed-empty-mismatch', + originalWorkflowId: stateWorkflowId, + timestamp: Date.now() + } + }; + } + + const result = { blocks: Object.fromEntries( Object.entries(deployedWorkflowState.blocks || {}) .filter(([_, block]) => block && block.type) @@ -104,47 +156,25 @@ export function DeployedWorkflowModal({ }) ), edges: deployedWorkflowState.edges ? [...deployedWorkflowState.edges] : [], - loops: deployedWorkflowState.loops ? {...deployedWorkflowState.loops} : {} + loops: deployedWorkflowState.loops ? {...deployedWorkflowState.loops} : {}, + _metadata: { + ...(deployedWorkflowState._metadata || {}), + workflowId: deployedWorkflowState._metadata?.workflowId || activeWorkflowId || undefined, + type: 'deployed-sanitized', + sanitizedAt: Date.now() + } }; - }, [deployedWorkflowState]); + + return result; + }, [deployedWorkflowState, activeWorkflowId]); const handleRevert = () => { -<<<<<<< HEAD // Revert to the deployed state revertToDeployedState(deployedWorkflowState) setShowRevertDialog(false) onClose() -======= - if (activeWorkflowId) { - logger.info(`Reverting to deployed state for workflow: ${activeWorkflowId}`) - revertToDeployedState(sanitizedDeployedState) - setShowRevertDialog(false) - onClose() - } ->>>>>>> 9594f7db (fix: good except for subblocks) } - useEffect(() => { - if (isOpen && activeWorkflowId) { - logger.info('DeployedWorkflowModal opened', { - workflowId: activeWorkflowId, - deployedStateBlockCount: Object.keys(deployedWorkflowState?.blocks || {}).length, - currentStateBlockCount: Object.keys(currentWorkflowState?.blocks || {}).length, - deployedStateChecksum: JSON.stringify(deployedWorkflowState).length, - currentStateChecksum: JSON.stringify(currentWorkflowState).length - }); - - // Log a sample of block IDs to verify they match expected workflow - const deployedBlockIds = Object.keys(deployedWorkflowState?.blocks || {}).slice(0, 2); - const currentBlockIds = Object.keys(currentWorkflowState?.blocks || {}).slice(0, 2); - - logger.info('State block samples', { - deployedBlockIds, - currentBlockIds - }); - } - }, [isOpen, activeWorkflowId, deployedWorkflowState, currentWorkflowState]); - return ( >>>>>> 2d314bcc (fix: deployed state preview persists across workflows) import { Loader2, Rocket } from 'lucide-react' import { Button } from '@/components/ui/button' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' @@ -42,6 +46,68 @@ export function DeploymentControls({ const [isDeploying, _setIsDeploying] = useState(false) const [isModalOpen, setIsModalOpen] = useState(false) + + // Add a ref to track the last seen workflow ID and deployed state + const lastWorkflowIdRef = useRef(null) + const lastDeployedStateRef = useRef(null) + + // Log when workflow ID changes + useEffect(() => { + if (activeWorkflowId !== lastWorkflowIdRef.current) { + logger.info('Workflow ID changed in DeploymentControls', { + previousId: lastWorkflowIdRef.current, + currentId: activeWorkflowId, + timestamp: Date.now() + }); + lastWorkflowIdRef.current = activeWorkflowId; + } + }, [activeWorkflowId]); + + // Log when deployed state changes + useEffect(() => { + if (deployedState && deployedState !== lastDeployedStateRef.current) { + const blockIds = Object.keys(deployedState?.blocks || {}); + logger.info('Deployed state changed in DeploymentControls', { + workflowId: activeWorkflowId, + blockCount: blockIds.length, + blockIds: JSON.stringify(blockIds.slice(0, 3)), + isLoadingState: isLoadingDeployedState, + timestamp: Date.now(), + stateHash: JSON.stringify(deployedState).length + }); + lastDeployedStateRef.current = deployedState; + } + }, [deployedState, activeWorkflowId, isLoadingDeployedState]); + + // Add wrapper around refetchDeployedState to track timing + const refetchWithLogging = async () => { + if (!activeWorkflowId) return; + + const fetchId = Date.now(); + logger.info('Starting deployedState refetch', { + workflowId: activeWorkflowId, + fetchId, + timestamp: Date.now() + }); + + try { + await refetchDeployedState(); + + logger.info('Completed deployedState refetch', { + workflowId: activeWorkflowId, + fetchId, + timestamp: Date.now(), + duration: Date.now() - fetchId + }); + } catch (error) { + logger.error('Error in deployedState refetch', { + workflowId: activeWorkflowId, + fetchId, + error, + timestamp: Date.now() + }); + } + }; // Update parent component when workflow-specific status changes useEffect(() => { @@ -108,7 +174,7 @@ export function DeploymentControls({ setNeedsRedeployment={setNeedsRedeployment} deployedState={deployedState} isLoadingDeployedState={isLoadingDeployedState} - refetchDeployedState={refetchDeployedState} + refetchDeployedState={refetchWithLogging} /> ) diff --git a/apps/sim/app/w/[id]/components/control-bar/control-bar.tsx b/apps/sim/app/w/[id]/components/control-bar/control-bar.tsx index 7bf982dc7e..313c089e20 100644 --- a/apps/sim/app/w/[id]/components/control-bar/control-bar.tsx +++ b/apps/sim/app/w/[id]/components/control-bar/control-bar.tsx @@ -111,6 +111,11 @@ export function ControlBar() { // Add deployedState management const [deployedState, setDeployedState] = useState(null) const [isLoadingDeployedState, setIsLoadingDeployedState] = useState(false) + + // Add refs to manage fetch state and prevent race conditions + const abortControllerRef = useRef(null) + const lastFetchedWorkflowIdRef = useRef(null) + const lastDeployedStateRef = useRef(false) // Workflow name editing state const [isEditing, setIsEditing] = useState(false) @@ -301,50 +306,68 @@ export function ControlBar() { * @returns Promise that resolves when the deployed state is fetched */ const fetchDeployedState = async (options = { forceRefetch: false }) => { + // Cancel any in-flight requests + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + + // Create new abort controller for this request + abortControllerRef.current = new AbortController(); + const { signal } = abortControllerRef.current; + + const requestId = Date.now(); + const currentWorkflowId = activeWorkflowId; + // Skip fetching if we don't have an active workflow ID or it's not deployed // unless we're explicitly forcing a refetch - if ((!activeWorkflowId || !isDeployed) && !options.forceRefetch) { + if ((!currentWorkflowId || !isDeployed) && !options.forceRefetch) { setDeployedState(null); return; } try { setIsLoadingDeployedState(true); - const logMessage = options.forceRefetch - ? `[CENTRAL] Explicitly refetching deployed state for workflow: ${activeWorkflowId}` - : `[CENTRAL] Fetching deployed state for workflow: ${activeWorkflowId} (Control Bar - Single Source of Truth)`; - logger.info(logMessage); + // Pass the abort signal to the fetch call + const response = await fetch( + `/api/workflows/${currentWorkflowId}/deployed`, + { signal } + ); - const response = await fetch(`/api/workflows/${activeWorkflowId}/deployed`); if (!response.ok) { throw new Error(`Failed to fetch deployed state: ${response.status}`); } const data = await response.json(); + // Final workflow ID check before updating state + if (currentWorkflowId !== activeWorkflowId) { + return; + } + if (data.deployedState) { - const successMessage = options.forceRefetch - ? 'Successfully refetched deployed state from DB after redeployment' - : 'Successfully fetched deployed state from DB - This is the only place that should fetch deployed state'; + // Create a single deep clone with metadata + const deployedStateWithMetadata = { + ...JSON.parse(JSON.stringify(data.deployedState)), + _metadata: { + workflowId: currentWorkflowId, + fetchTimestamp: Date.now(), + requestId + } + }; - logger.info(successMessage); - - // Create a deep clone to ensure no reference sharing with current state - const deepClonedState = JSON.parse(JSON.stringify(data.deployedState)); - logger.info('deepClonedState', deepClonedState); - setDeployedState(deepClonedState); + setDeployedState(deployedStateWithMetadata); } else { - const warningMessage = options.forceRefetch - ? 'No deployed state found in the database after refetch' - : 'No deployed state found in the database'; - - logger.warn(warningMessage); setDeployedState(null); } - } catch (error) { - logger.error('Error fetching deployed state:', error); - setDeployedState(null); + } catch (error: unknown) { + // Don't log AbortError as it's expected when cancelling requests + if (error instanceof Error && error.name === 'AbortError') { + // Silently ignore abort errors + } else { + logger.error(`Error fetching deployed state:`, { error }); + setDeployedState(null); + } } finally { setIsLoadingDeployedState(false); } @@ -355,7 +378,16 @@ export function ControlBar() { // Fetch deployed state when the workflow ID changes or deployment status changes useEffect(() => { - fetchDeployedState(); + // Only fetch if the workflow ID or deployed status has actually changed + if (activeWorkflowId !== lastFetchedWorkflowIdRef.current || + isDeployed !== lastDeployedStateRef.current) { + + // Update refs to track what we're fetching for + lastFetchedWorkflowIdRef.current = activeWorkflowId; + lastDeployedStateRef.current = isDeployed; + + fetchDeployedState(); + } }, [activeWorkflowId, isDeployed]); // Listen for deployment status changes diff --git a/apps/sim/app/w/components/workflow-preview/workflow-preview.tsx b/apps/sim/app/w/components/workflow-preview/workflow-preview.tsx index 4b6f4e3463..ce7c6b1dec 100644 --- a/apps/sim/app/w/components/workflow-preview/workflow-preview.tsx +++ b/apps/sim/app/w/components/workflow-preview/workflow-preview.tsx @@ -27,11 +27,7 @@ import { WorkflowEdge } from '@/app/w/[id]/components/workflow-edge/workflow-edg // import { LoopLabel } from '@/app/w/[id]/components/workflow-loop/components/loop-label/loop-label' // import { createLoopNode } from '@/app/w/[id]/components/workflow-loop/workflow-loop' import { getBlock } from '@/blocks' -<<<<<<< HEAD import type { SubBlockConfig } from '@/blocks/types' -======= -import { cn } from '@/lib/utils' ->>>>>>> 6f129dfc (fix: subblock rerender fixed) const logger = createLogger('WorkflowPreview') @@ -72,10 +68,6 @@ const edgeTypes: EdgeTypes = { workflowEdge: WorkflowEdge, } -// The subblocks should be getting passed from the state and not the subBlockStore. -// Create optional parameter boolan isPreview to pass in the block state to know how to render -// the subblocks - export function WorkflowPreview({ workflowState, showSubBlocks = true, @@ -86,14 +78,6 @@ export function WorkflowPreview({ defaultPosition, defaultZoom, }: WorkflowPreviewProps) { - // Use effect to log the workflow state once outside of useMemo - useEffect(() => { - logger.info('WorkflowPreview received new state', { - blockCount: Object.keys(workflowState?.blocks || {}).length, - withSubBlocks: Object.values(workflowState?.blocks || {}).filter(b => b.subBlocks && Object.keys(b.subBlocks).length > 0).length, - }); - }, [workflowState]); - // Transform blocks and loops into ReactFlow nodes const nodes: Node[] = useMemo(() => { const nodeArray: Node[] = [] @@ -144,7 +128,6 @@ export function WorkflowPreview({ subBlockValues: subBlocksClone, // Use the deep clone to avoid reference issues }, }) -<<<<<<< HEAD // Add children of this block if it's a loop if (block.type === 'loop') { @@ -178,14 +161,12 @@ export function WorkflowPreview({ }) }) } -======= logger.info(`Preview node created: ${blockId}`, { blockType: block.type, hasSubBlocks: block.subBlocks && Object.keys(block.subBlocks).length > 0 }); ->>>>>>> 6f129dfc (fix: subblock rerender fixed) }) - + return nodeArray }, [JSON.stringify(workflowState.blocks), JSON.stringify(workflowState.loops), showSubBlocks]) @@ -201,13 +182,12 @@ export function WorkflowPreview({ })) }, [JSON.stringify(workflowState.edges)]) - useEffect(() => { - logger.info('Rendering workflow state', { workflowState }) - }, [workflowState]) - return ( -
+