Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
fix(deploy): use client-side comparison for editor header, remove ser…
…ver polling

The lastSaved-based server polling was triggering API calls on every
local store mutation (before socket persistence), wasting requests and
checking stale DB state. Revert the editor header to pure client-side
hasWorkflowChanged comparison — zero network during editing, instant
badge updates. Child workflow badges still use server-side
useDeploymentInfo (they don't have Zustand state).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  • Loading branch information
waleedlatif1 and claude committed Mar 16, 2026
commit abd5b707f75fb5a2854f13cfc26675840b637efe
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
useDeployment,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks'
import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow'
import { useDeployedWorkflowState, useDeploymentInfo } from '@/hooks/queries/deployments'
import { useDeployedWorkflowState } from '@/hooks/queries/deployments'
import type { WorkspaceUserPermissions } from '@/hooks/use-user-permissions'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'

Expand All @@ -33,12 +33,6 @@ export function Deploy({ activeWorkflowId, userPermissions, className }: DeployP
)
const isDeployed = deploymentStatus?.isDeployed || false

// Server-side deployment info (authoritative source for needsRedeployment)
const { data: deploymentInfoData, isLoading: isLoadingDeploymentInfo } = useDeploymentInfo(
activeWorkflowId,
{ enabled: isDeployed && !isRegistryLoading }
)

const isDeployedStateEnabled = Boolean(activeWorkflowId) && isDeployed && !isRegistryLoading
const { data: deployedStateData, isLoading: isLoadingDeployedState } = useDeployedWorkflowState(
activeWorkflowId,
Expand All @@ -50,8 +44,6 @@ export function Deploy({ activeWorkflowId, userPermissions, className }: DeployP
workflowId: activeWorkflowId,
deployedState,
isLoadingDeployedState,
serverNeedsRedeployment: deploymentInfoData?.needsRedeployment,
isServerLoading: isLoadingDeploymentInfo,
})

const { isDeploying, handleDeployClick } = useDeployment({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { useEffect, useMemo, useRef } from 'react'
import { useQueryClient } from '@tanstack/react-query'
import { useMemo } from 'react'
import { hasWorkflowChanged } from '@/lib/workflows/comparison'
import { mergeSubblockStateWithValues } from '@/lib/workflows/subblocks'
import { deploymentKeys } from '@/hooks/queries/deployments'
import { useVariablesStore } from '@/stores/panel/variables/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
Expand All @@ -12,34 +10,23 @@ interface UseChangeDetectionProps {
workflowId: string | null
deployedState: WorkflowState | null
isLoadingDeployedState: boolean
serverNeedsRedeployment: boolean | undefined
isServerLoading: boolean
}

/**
* Detects meaningful changes between current workflow state and deployed state.
*
* Uses the server-side needsRedeployment (from useDeploymentInfo) as the
* authoritative signal. The server compares the persisted DB state to the
* deployed version state, which avoids false positives from client-side
* representation differences.
*
* When the workflow store is updated (e.g. after auto-save), the deployment
* info query is invalidated so the server can recheck for changes.
* Performs comparison entirely on the client using hasWorkflowChanged — no API
* calls needed. The deployed state snapshot is fetched once via React Query and
* refreshed after deploy/undeploy/version-activate mutations.
*/
export function useChangeDetection({
workflowId,
deployedState,
isLoadingDeployedState,
serverNeedsRedeployment,
isServerLoading,
}: UseChangeDetectionProps) {
const queryClient = useQueryClient()
const blocks = useWorkflowStore((state) => state.blocks)
const edges = useWorkflowStore((state) => state.edges)
const loops = useWorkflowStore((state) => state.loops)
const parallels = useWorkflowStore((state) => state.parallels)
const lastSaved = useWorkflowStore((state) => state.lastSaved)
const subBlockValues = useSubBlockStore((state) =>
workflowId ? state.workflowValues[workflowId] : null
)
Expand All @@ -55,50 +42,8 @@ export function useChangeDetection({
return vars
}, [workflowId, allVariables])

// Tracks the lastSaved timestamp at mount to distinguish real saves from initial hydration.
const initialLastSavedRef = useRef<number | undefined>(undefined)
const workflowIdRef = useRef(workflowId)

// Must run before the lastSaved effect to prevent stale-ref invalidation on workflow switch.
useEffect(() => {
workflowIdRef.current = workflowId
initialLastSavedRef.current = undefined
}, [workflowId])

useEffect(() => {
if (lastSaved !== undefined && initialLastSavedRef.current === undefined) {
initialLastSavedRef.current = lastSaved
return
}

if (
lastSaved === undefined ||
initialLastSavedRef.current === undefined ||
lastSaved === initialLastSavedRef.current ||
!workflowId
) {
return
}

initialLastSavedRef.current = lastSaved

const capturedWorkflowId = workflowId
const timer = setTimeout(() => {
if (workflowIdRef.current !== capturedWorkflowId) return
queryClient.invalidateQueries({
queryKey: deploymentKeys.info(capturedWorkflowId),
})
}, 500)

return () => clearTimeout(timer)
}, [lastSaved, workflowId, queryClient])

// Skip expensive state merge when server result is available (the common path).
// Only build currentState for the client-side fallback comparison.
const needsClientFallback = serverNeedsRedeployment === undefined && !isServerLoading

const currentState = useMemo((): WorkflowState | null => {
if (!needsClientFallback || !workflowId || !deployedState) return null
if (!workflowId || !deployedState) return null

const mergedBlocks = mergeSubblockStateWithValues(blocks, subBlockValues ?? {})

Expand All @@ -110,7 +55,6 @@ export function useChangeDetection({
variables: workflowVariables,
} as WorkflowState & { variables: Record<string, any> }
}, [
needsClientFallback,
workflowId,
deployedState,
blocks,
Expand All @@ -122,21 +66,9 @@ export function useChangeDetection({
])

const changeDetected = useMemo(() => {
if (isServerLoading) return false

if (serverNeedsRedeployment !== undefined) {
return serverNeedsRedeployment
}

if (!currentState || !deployedState || isLoadingDeployedState) return false
return hasWorkflowChanged(currentState, deployedState)
}, [
currentState,
deployedState,
isLoadingDeployedState,
serverNeedsRedeployment,
isServerLoading,
])
}, [currentState, deployedState, isLoadingDeployedState])

return { changeDetected }
}
Loading