Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useOperationQueueStore } from '@/stores/operation-queue/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
import { createDeploymentSignature } from '@/lib/workflows/deployment-signature'

const logger = createLogger('useChangeDetection')

Expand All @@ -17,16 +18,14 @@ interface UseChangeDetectionProps {
/**
* Hook to detect changes between current workflow state and deployed state
* Uses API-based change detection for accuracy
* Only triggers checks when deployment-relevant changes occur (ignores UI-only changes like position, layout, etc.)
*/
export function useChangeDetection({
workflowId,
deployedState,
isLoadingDeployedState,
}: UseChangeDetectionProps) {
const [changeDetected, setChangeDetected] = useState(false)
const [blockStructureVersion, setBlockStructureVersion] = useState(0)
const [edgeStructureVersion, setEdgeStructureVersion] = useState(0)
const [subBlockStructureVersion, setSubBlockStructureVersion] = useState(0)

// Get current store state for change detection
const currentBlocks = useWorkflowStore((state) => state.blocks)
Expand All @@ -36,35 +35,19 @@ export function useChangeDetection({
workflowId ? state.workflowValues[workflowId] : null
)

// Track structure changes
useEffect(() => {
setBlockStructureVersion((version) => version + 1)
}, [currentBlocks])

useEffect(() => {
setEdgeStructureVersion((version) => version + 1)
}, [currentEdges])

useEffect(() => {
setSubBlockStructureVersion((version) => version + 1)
}, [subBlockValues])

// Reset version counters when workflow changes
useEffect(() => {
setBlockStructureVersion(0)
setEdgeStructureVersion(0)
setSubBlockStructureVersion(0)
}, [workflowId])
// Create a deployment signature that only includes deployment-relevant properties
// This excludes UI-only changes like position, layout, expanded states, etc.
const deploymentSignature = useMemo(() => {
return createDeploymentSignature(currentBlocks, currentEdges, subBlockValues)
}, [currentBlocks, currentEdges, subBlockValues])

// Create trigger for status check
// Include lastSaved to trigger check after save operations
const statusCheckTrigger = useMemo(() => {
return JSON.stringify({
lastSaved: lastSaved ?? 0,
blockVersion: blockStructureVersion,
edgeVersion: edgeStructureVersion,
subBlockVersion: subBlockStructureVersion,
signature: deploymentSignature,
})
}, [lastSaved, blockStructureVersion, edgeStructureVersion, subBlockStructureVersion])
}, [lastSaved, deploymentSignature])

const debouncedStatusCheckTrigger = useDebounce(statusCheckTrigger, 500)

Expand Down
123 changes: 123 additions & 0 deletions apps/sim/lib/workflows/deployment-signature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import type { Edge } from 'reactflow'
import type { BlockState } from '@/stores/workflows/workflow/types'

/**
* Deployment Signature Module
*
* This module provides utilities to create a "deployment signature" from workflow state.
* The signature only includes properties that are relevant for deployment decisions,
* excluding UI-only changes such as:
* - Block positions
* - Layout measurements (width, height)
* - UI state (expanded/collapsed states)
* - Test values
*
* This ensures that the workflow redeployment check (via /api/workflows/[id]/status)
* is only triggered by meaningful changes that would actually require redeployment,
* not by UI interactions like moving blocks, opening/closing tools, etc.
*
* The normalization logic mirrors the hasWorkflowChanged function in @/lib/workflows/utils
* to ensure consistency between change detection and actual deployment checks.
*/

/**
* Extracts deployment-relevant properties from a block, excluding UI-only changes
* This mirrors the logic in hasWorkflowChanged to ensure consistency
*/
function normalizeBlockForSignature(block: BlockState): Record<string, any> {
const {
position: _pos,
layout: _layout,
height: _height,
subBlocks = {},
...rest
} = block

// Exclude width/height from data object (container dimensions from autolayout)
const { width: _width, height: _dataHeight, ...dataRest } = rest.data || {}

// For subBlocks, we need to extract just the values
const normalizedSubBlocks: Record<string, any> = {}
for (const [subBlockId, subBlock] of Object.entries(subBlocks)) {
// Special handling for tools subBlock - exclude UI-only 'isExpanded' field
if (subBlockId === 'tools' && Array.isArray(subBlock.value)) {
normalizedSubBlocks[subBlockId] = subBlock.value.map((tool: any) => {
if (tool && typeof tool === 'object') {
const { isExpanded: _isExpanded, ...toolRest } = tool
return toolRest
}
return tool
})
} else if (subBlockId === 'inputFormat' && Array.isArray(subBlock.value)) {
// Handle inputFormat - exclude collapsed state and test values
normalizedSubBlocks[subBlockId] = subBlock.value.map((field: any) => {
if (field && typeof field === 'object') {
const { value: _value, collapsed: _collapsed, ...fieldRest } = field
return fieldRest
}
return field
})
} else {
normalizedSubBlocks[subBlockId] = subBlock.value
}
}

return {
...rest,
data: dataRest,
subBlocks: normalizedSubBlocks,
}
}

/**
* Extracts deployment-relevant properties from an edge
*/
function normalizeEdgeForSignature(edge: Edge): Record<string, any> {
return {
source: edge.source,
sourceHandle: edge.sourceHandle,
target: edge.target,
targetHandle: edge.targetHandle,
}
}

/**
* Creates a deployment signature from workflow state that only includes
* properties that would trigger a redeployment. UI-only changes like
* position, layout, expanded states, etc. are excluded.
*
* @param blocks - Current blocks from workflow store
* @param edges - Current edges from workflow store
* @param subBlockValues - Current subblock values from subblock store
* @returns A stringified signature that changes only when deployment-relevant changes occur
*/
export function createDeploymentSignature(
blocks: Record<string, BlockState>,
edges: Edge[],
subBlockValues: Record<string, any> | null
): string {
// Normalize blocks (excluding UI-only properties)
const normalizedBlocks: Record<string, any> = {}
for (const [blockId, block] of Object.entries(blocks)) {
normalizedBlocks[blockId] = normalizeBlockForSignature(block)
}

// Normalize edges (only connection information)
const normalizedEdges = edges
.map(normalizeEdgeForSignature)
.sort((a, b) =>
`${a.source}-${a.sourceHandle}-${a.target}-${a.targetHandle}`.localeCompare(
`${b.source}-${b.sourceHandle}-${b.target}-${b.targetHandle}`
)
)

// Create signature object
const signature = {
blockIds: Object.keys(blocks).sort(),
blocks: normalizedBlocks,
edges: normalizedEdges,
subBlockValues: subBlockValues || {},
}

return JSON.stringify(signature)
}
Loading