|
| 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