Skip to content

Commit e5cb6e3

Browse files
improvement(store-hydration): refactor loading state tracking for workflows (#2065)
* improvement(store-hydration): refactor loading state tracking for workflows * fix type issue * fix * fix deploy modal reference * sidebar * new panel * reset stores
1 parent e4ccedc commit e5cb6e3

File tree

11 files changed

+320
-335
lines changed

11 files changed

+320
-335
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,11 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) {
7878

7979
// Store hooks
8080
const { lastSaved, setNeedsRedeploymentFlag, blocks } = useWorkflowStore()
81-
const {
82-
workflows,
83-
activeWorkflowId,
84-
duplicateWorkflow,
85-
isLoading: isRegistryLoading,
86-
} = useWorkflowRegistry()
81+
const { workflows, activeWorkflowId, duplicateWorkflow, hydration } = useWorkflowRegistry()
82+
const isRegistryLoading =
83+
hydration.phase === 'idle' ||
84+
hydration.phase === 'metadata-loading' ||
85+
hydration.phase === 'state-loading'
8786
const { isExecuting, handleRunWorkflow, handleCancelExecution } = useWorkflowExecution()
8887
const { setActiveTab, togglePanel, isOpen } = usePanelStore()
8988
const { getFolderTree, expandedFolders } = useFolderStore()

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/copilot/components/user-input/hooks/use-mention-data.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,11 @@ export function useMentionData(props: UseMentionDataProps) {
118118

119119
// Use workflow registry as source of truth for workflows
120120
const registryWorkflows = useWorkflowRegistry((state) => state.workflows)
121-
const isLoadingWorkflows = useWorkflowRegistry((state) => state.isLoading)
121+
const hydrationPhase = useWorkflowRegistry((state) => state.hydration.phase)
122+
const isLoadingWorkflows =
123+
hydrationPhase === 'idle' ||
124+
hydrationPhase === 'metadata-loading' ||
125+
hydrationPhase === 'state-loading'
122126

123127
// Convert registry workflows to mention format, filtered by workspace and sorted
124128
const workflows: WorkflowItem[] = Object.values(registryWorkflows)

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/deploy/deploy.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ interface DeployProps {
2121
*/
2222
export function Deploy({ activeWorkflowId, userPermissions, className }: DeployProps) {
2323
const [isModalOpen, setIsModalOpen] = useState(false)
24-
const { isLoading: isRegistryLoading } = useWorkflowRegistry()
24+
const hydrationPhase = useWorkflowRegistry((state) => state.hydration.phase)
25+
const isRegistryLoading =
26+
hydrationPhase === 'idle' ||
27+
hydrationPhase === 'metadata-loading' ||
28+
hydrationPhase === 'state-loading'
2529
const { hasBlocks } = useCurrentWorkflow()
2630

2731
// Get deployment status from registry

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/panel-new.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,11 @@ export function Panel() {
8181
// Hooks
8282
const userPermissions = useUserPermissionsContext()
8383
const { isImporting, handleFileChange } = useImportWorkflow({ workspaceId })
84-
const {
85-
workflows,
86-
activeWorkflowId,
87-
duplicateWorkflow,
88-
isLoading: isRegistryLoading,
89-
} = useWorkflowRegistry()
84+
const { workflows, activeWorkflowId, duplicateWorkflow, hydration } = useWorkflowRegistry()
85+
const isRegistryLoading =
86+
hydration.phase === 'idle' ||
87+
hydration.phase === 'metadata-loading' ||
88+
hydration.phase === 'state-loading'
9089
const { getJson } = useWorkflowJsonStore()
9190
const { blocks } = useWorkflowStore()
9291

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ import { useCopilotStore } from '@/stores/panel-new/copilot/store'
4747
import { usePanelEditorStore } from '@/stores/panel-new/editor/store'
4848
import { useGeneralStore } from '@/stores/settings/general/store'
4949
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
50-
import { hasWorkflowsInitiallyLoaded, useWorkflowRegistry } from '@/stores/workflows/registry/store'
50+
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
5151
import { getUniqueBlockName } from '@/stores/workflows/utils'
5252
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
5353

@@ -83,7 +83,6 @@ interface BlockData {
8383

8484
const WorkflowContent = React.memo(() => {
8585
// State
86-
const [isWorkflowReady, setIsWorkflowReady] = useState(false)
8786

8887
// State for tracking node dragging
8988
const [draggedNodeId, setDraggedNodeId] = useState<string | null>(null)
@@ -102,11 +101,12 @@ const WorkflowContent = React.memo(() => {
102101

103102
// Get workspace ID from the params
104103
const workspaceId = params.workspaceId as string
104+
const workflowIdParam = params.workflowId as string
105105

106106
// Notification store
107107
const addNotification = useNotificationStore((state) => state.addNotification)
108108

109-
const { workflows, activeWorkflowId, isLoading, setActiveWorkflow } = useWorkflowRegistry()
109+
const { workflows, activeWorkflowId, hydration, setActiveWorkflow } = useWorkflowRegistry()
110110

111111
// Use the clean abstraction for current workflow state
112112
const currentWorkflow = useCurrentWorkflow()
@@ -127,6 +127,13 @@ const WorkflowContent = React.memo(() => {
127127
// Extract workflow data from the abstraction
128128
const { blocks, edges, isDiffMode, lastSaved } = currentWorkflow
129129

130+
const isWorkflowReady =
131+
hydration.phase === 'ready' &&
132+
hydration.workflowId === workflowIdParam &&
133+
activeWorkflowId === workflowIdParam &&
134+
Boolean(workflows[workflowIdParam]) &&
135+
lastSaved !== undefined
136+
130137
// Node utilities hook for position/hierarchy calculations (requires blocks)
131138
const {
132139
getNodeDepth,
@@ -1209,10 +1216,19 @@ const WorkflowContent = React.memo(() => {
12091216
useEffect(() => {
12101217
let cancelled = false
12111218
const currentId = params.workflowId as string
1219+
const currentWorkspaceHydration = hydration.workspaceId
1220+
1221+
const isRegistryReady = hydration.phase !== 'metadata-loading' && hydration.phase !== 'idle'
12121222

12131223
// Wait for registry to be ready to prevent race conditions
1214-
// Don't proceed if: no workflowId, registry is loading, or workflow not in registry
1215-
if (!currentId || isLoading || !workflows[currentId]) return
1224+
if (
1225+
!currentId ||
1226+
!workflows[currentId] ||
1227+
!isRegistryReady ||
1228+
(currentWorkspaceHydration && currentWorkspaceHydration !== workspaceId)
1229+
) {
1230+
return
1231+
}
12161232

12171233
if (activeWorkflowId !== currentId) {
12181234
// Clear diff and set as active
@@ -1229,25 +1245,15 @@ const WorkflowContent = React.memo(() => {
12291245
return () => {
12301246
cancelled = true
12311247
}
1232-
}, [params.workflowId, workflows, activeWorkflowId, setActiveWorkflow, isLoading])
1233-
1234-
// Track when workflow is ready for rendering
1235-
useEffect(() => {
1236-
const currentId = params.workflowId as string
1237-
1238-
// Workflow is ready when:
1239-
// 1. We have an active workflow that matches the URL
1240-
// 2. The workflow exists in the registry
1241-
// 3. Workflows are not currently loading
1242-
// 4. The workflow store has been initialized (lastSaved exists means state was loaded)
1243-
const shouldBeReady =
1244-
activeWorkflowId === currentId &&
1245-
Boolean(workflows[currentId]) &&
1246-
!isLoading &&
1247-
lastSaved !== undefined
1248-
1249-
setIsWorkflowReady(shouldBeReady)
1250-
}, [activeWorkflowId, params.workflowId, workflows, isLoading, lastSaved])
1248+
}, [
1249+
params.workflowId,
1250+
workflows,
1251+
activeWorkflowId,
1252+
setActiveWorkflow,
1253+
hydration.phase,
1254+
hydration.workspaceId,
1255+
workspaceId,
1256+
])
12511257

12521258
// Preload workspace environment - React Query handles caching automatically
12531259
useWorkspaceEnvironment(workspaceId)
@@ -1258,8 +1264,8 @@ const WorkflowContent = React.memo(() => {
12581264
const workflowIds = Object.keys(workflows)
12591265
const currentId = params.workflowId as string
12601266

1261-
// Wait for initial load to complete before making navigation decisions
1262-
if (!hasWorkflowsInitiallyLoaded() || isLoading) {
1267+
// Wait for metadata to finish loading before making navigation decisions
1268+
if (hydration.phase === 'metadata-loading' || hydration.phase === 'idle') {
12631269
return
12641270
}
12651271

@@ -1302,7 +1308,7 @@ const WorkflowContent = React.memo(() => {
13021308
}
13031309

13041310
validateAndNavigate()
1305-
}, [params.workflowId, workflows, isLoading, workspaceId, router])
1311+
}, [params.workflowId, workflows, hydration.phase, workspaceId, router])
13061312

13071313
// Cache block configs to prevent unnecessary re-fetches
13081314
const blockConfigCache = useRef<Map<string, any>>(new Map())

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,12 @@ interface TemplateData {
8181
export function Sidebar() {
8282
// useGlobalShortcuts()
8383

84-
const { workflows, isLoading: workflowsLoading, switchToWorkspace } = useWorkflowRegistry()
84+
const { workflows, switchToWorkspace } = useWorkflowRegistry()
85+
const workflowsHydrationPhase = useWorkflowRegistry((state) => state.hydration.phase)
86+
const workflowsLoading =
87+
workflowsHydrationPhase === 'idle' ||
88+
workflowsHydrationPhase === 'metadata-loading' ||
89+
workflowsHydrationPhase === 'state-loading'
8590
const { data: sessionData, isPending: sessionLoading } = useSession()
8691
const userPermissions = useUserPermissionsContext()
8792
const isLoading = workflowsLoading || sessionLoading

apps/sim/hooks/queries/workflows.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ async function fetchWorkflows(workspaceId: string): Promise<WorkflowMetadata[]>
4343
}
4444

4545
export function useWorkflows(workspaceId?: string) {
46-
const setWorkflows = useWorkflowRegistry((state) => state.setWorkflows)
46+
const beginMetadataLoad = useWorkflowRegistry((state) => state.beginMetadataLoad)
47+
const completeMetadataLoad = useWorkflowRegistry((state) => state.completeMetadataLoad)
48+
const failMetadataLoad = useWorkflowRegistry((state) => state.failMetadataLoad)
4749

4850
const query = useQuery({
4951
queryKey: workflowKeys.list(workspaceId),
@@ -54,10 +56,24 @@ export function useWorkflows(workspaceId?: string) {
5456
})
5557

5658
useEffect(() => {
57-
if (query.data) {
58-
setWorkflows(query.data)
59+
if (workspaceId && query.status === 'pending') {
60+
beginMetadataLoad(workspaceId)
5961
}
60-
}, [query.data, setWorkflows])
62+
}, [workspaceId, query.status, beginMetadataLoad])
63+
64+
useEffect(() => {
65+
if (workspaceId && query.status === 'success' && query.data) {
66+
completeMetadataLoad(workspaceId, query.data)
67+
}
68+
}, [workspaceId, query.status, query.data, completeMetadataLoad])
69+
70+
useEffect(() => {
71+
if (workspaceId && query.status === 'error') {
72+
const message =
73+
query.error instanceof Error ? query.error.message : 'Failed to fetch workflows'
74+
failMetadataLoad(workspaceId, message)
75+
}
76+
}, [workspaceId, query.status, query.error, failMetadataLoad])
6177

6278
return query
6379
}

apps/sim/stores/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,15 @@ export const resetAllStores = () => {
207207
useWorkflowRegistry.setState({
208208
workflows: {},
209209
activeWorkflowId: null,
210-
isLoading: false,
211210
error: null,
211+
deploymentStatuses: {},
212+
hydration: {
213+
phase: 'idle',
214+
workspaceId: null,
215+
workflowId: null,
216+
requestId: null,
217+
error: null,
218+
},
212219
})
213220
useWorkflowStore.getState().clear()
214221
useSubBlockStore.getState().clear()

0 commit comments

Comments
 (0)