Skip to content

Commit dc0784d

Browse files
Refactor: Improve deployment change detection logic
Co-authored-by: emir <emir@simstudio.ai>
1 parent 4d1a9a3 commit dc0784d

File tree

2 files changed

+133
-27
lines changed

2 files changed

+133
-27
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-change-detection.ts

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useOperationQueueStore } from '@/stores/operation-queue/store'
55
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
66
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
77
import type { WorkflowState } from '@/stores/workflows/workflow/types'
8+
import { createDeploymentSignature } from '@/lib/workflows/deployment-signature'
89

910
const logger = createLogger('useChangeDetection')
1011

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

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

39-
// Track structure changes
40-
useEffect(() => {
41-
setBlockStructureVersion((version) => version + 1)
42-
}, [currentBlocks])
43-
44-
useEffect(() => {
45-
setEdgeStructureVersion((version) => version + 1)
46-
}, [currentEdges])
47-
48-
useEffect(() => {
49-
setSubBlockStructureVersion((version) => version + 1)
50-
}, [subBlockValues])
51-
52-
// Reset version counters when workflow changes
53-
useEffect(() => {
54-
setBlockStructureVersion(0)
55-
setEdgeStructureVersion(0)
56-
setSubBlockStructureVersion(0)
57-
}, [workflowId])
38+
// Create a deployment signature that only includes deployment-relevant properties
39+
// This excludes UI-only changes like position, layout, expanded states, etc.
40+
const deploymentSignature = useMemo(() => {
41+
return createDeploymentSignature(currentBlocks, currentEdges, subBlockValues)
42+
}, [currentBlocks, currentEdges, subBlockValues])
5843

59-
// Create trigger for status check
44+
// Include lastSaved to trigger check after save operations
6045
const statusCheckTrigger = useMemo(() => {
6146
return JSON.stringify({
6247
lastSaved: lastSaved ?? 0,
63-
blockVersion: blockStructureVersion,
64-
edgeVersion: edgeStructureVersion,
65-
subBlockVersion: subBlockStructureVersion,
48+
signature: deploymentSignature,
6649
})
67-
}, [lastSaved, blockStructureVersion, edgeStructureVersion, subBlockStructureVersion])
50+
}, [lastSaved, deploymentSignature])
6851

6952
const debouncedStatusCheckTrigger = useDebounce(statusCheckTrigger, 500)
7053

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import type { Edge } from 'reactflow'
2+
import type { BlockState } from '@/stores/workflows/workflow/types'
3+
4+
/**
5+
* Deployment Signature Module
6+
*
7+
* This module provides utilities to create a "deployment signature" from workflow state.
8+
* The signature only includes properties that are relevant for deployment decisions,
9+
* excluding UI-only changes such as:
10+
* - Block positions
11+
* - Layout measurements (width, height)
12+
* - UI state (expanded/collapsed states)
13+
* - Test values
14+
*
15+
* This ensures that the workflow redeployment check (via /api/workflows/[id]/status)
16+
* is only triggered by meaningful changes that would actually require redeployment,
17+
* not by UI interactions like moving blocks, opening/closing tools, etc.
18+
*
19+
* The normalization logic mirrors the hasWorkflowChanged function in @/lib/workflows/utils
20+
* to ensure consistency between change detection and actual deployment checks.
21+
*/
22+
23+
/**
24+
* Extracts deployment-relevant properties from a block, excluding UI-only changes
25+
* This mirrors the logic in hasWorkflowChanged to ensure consistency
26+
*/
27+
function normalizeBlockForSignature(block: BlockState): Record<string, any> {
28+
const {
29+
position: _pos,
30+
layout: _layout,
31+
height: _height,
32+
subBlocks = {},
33+
...rest
34+
} = block
35+
36+
// Exclude width/height from data object (container dimensions from autolayout)
37+
const { width: _width, height: _dataHeight, ...dataRest } = rest.data || {}
38+
39+
// For subBlocks, we need to extract just the values
40+
const normalizedSubBlocks: Record<string, any> = {}
41+
for (const [subBlockId, subBlock] of Object.entries(subBlocks)) {
42+
// Special handling for tools subBlock - exclude UI-only 'isExpanded' field
43+
if (subBlockId === 'tools' && Array.isArray(subBlock.value)) {
44+
normalizedSubBlocks[subBlockId] = subBlock.value.map((tool: any) => {
45+
if (tool && typeof tool === 'object') {
46+
const { isExpanded: _isExpanded, ...toolRest } = tool
47+
return toolRest
48+
}
49+
return tool
50+
})
51+
} else if (subBlockId === 'inputFormat' && Array.isArray(subBlock.value)) {
52+
// Handle inputFormat - exclude collapsed state and test values
53+
normalizedSubBlocks[subBlockId] = subBlock.value.map((field: any) => {
54+
if (field && typeof field === 'object') {
55+
const { value: _value, collapsed: _collapsed, ...fieldRest } = field
56+
return fieldRest
57+
}
58+
return field
59+
})
60+
} else {
61+
normalizedSubBlocks[subBlockId] = subBlock.value
62+
}
63+
}
64+
65+
return {
66+
...rest,
67+
data: dataRest,
68+
subBlocks: normalizedSubBlocks,
69+
}
70+
}
71+
72+
/**
73+
* Extracts deployment-relevant properties from an edge
74+
*/
75+
function normalizeEdgeForSignature(edge: Edge): Record<string, any> {
76+
return {
77+
source: edge.source,
78+
sourceHandle: edge.sourceHandle,
79+
target: edge.target,
80+
targetHandle: edge.targetHandle,
81+
}
82+
}
83+
84+
/**
85+
* Creates a deployment signature from workflow state that only includes
86+
* properties that would trigger a redeployment. UI-only changes like
87+
* position, layout, expanded states, etc. are excluded.
88+
*
89+
* @param blocks - Current blocks from workflow store
90+
* @param edges - Current edges from workflow store
91+
* @param subBlockValues - Current subblock values from subblock store
92+
* @returns A stringified signature that changes only when deployment-relevant changes occur
93+
*/
94+
export function createDeploymentSignature(
95+
blocks: Record<string, BlockState>,
96+
edges: Edge[],
97+
subBlockValues: Record<string, any> | null
98+
): string {
99+
// Normalize blocks (excluding UI-only properties)
100+
const normalizedBlocks: Record<string, any> = {}
101+
for (const [blockId, block] of Object.entries(blocks)) {
102+
normalizedBlocks[blockId] = normalizeBlockForSignature(block)
103+
}
104+
105+
// Normalize edges (only connection information)
106+
const normalizedEdges = edges
107+
.map(normalizeEdgeForSignature)
108+
.sort((a, b) =>
109+
`${a.source}-${a.sourceHandle}-${a.target}-${a.targetHandle}`.localeCompare(
110+
`${b.source}-${b.sourceHandle}-${b.target}-${b.targetHandle}`
111+
)
112+
)
113+
114+
// Create signature object
115+
const signature = {
116+
blockIds: Object.keys(blocks).sort(),
117+
blocks: normalizedBlocks,
118+
edges: normalizedEdges,
119+
subBlockValues: subBlockValues || {},
120+
}
121+
122+
return JSON.stringify(signature)
123+
}

0 commit comments

Comments
 (0)