diff --git a/apps/sim/app/api/workflows/route.test.ts b/apps/sim/app/api/workflows/route.test.ts index b1d92ca64ed..af669d691c5 100644 --- a/apps/sim/app/api/workflows/route.test.ts +++ b/apps/sim/app/api/workflows/route.test.ts @@ -28,6 +28,13 @@ vi.mock('@sim/db', () => ({ db: { select: (...args: unknown[]) => mockDbSelect(...args), insert: (...args: unknown[]) => mockDbInsert(...args), + transaction: vi.fn(async (fn: (tx: Record) => Promise) => { + const tx = { + select: (...args: unknown[]) => mockDbSelect(...args), + insert: (...args: unknown[]) => mockDbInsert(...args), + } + await fn(tx) + }), }, })) @@ -87,6 +94,18 @@ vi.mock('@/lib/core/telemetry', () => ({ }, })) +vi.mock('@/lib/workflows/defaults', () => ({ + buildDefaultWorkflowArtifacts: vi.fn().mockReturnValue({ + workflowState: { blocks: {}, edges: [], loops: {}, parallels: {} }, + subBlockValues: {}, + startBlockId: 'start-block-id', + }), +})) + +vi.mock('@/lib/workflows/persistence/utils', () => ({ + saveWorkflowToNormalizedTables: vi.fn().mockResolvedValue({ success: true }), +})) + import { POST } from '@/app/api/workflows/route' describe('Workflows API Route - POST ordering', () => { diff --git a/apps/sim/app/api/workflows/route.ts b/apps/sim/app/api/workflows/route.ts index 4dc1d85a9c0..a9aba1bcc44 100644 --- a/apps/sim/app/api/workflows/route.ts +++ b/apps/sim/app/api/workflows/route.ts @@ -8,6 +8,8 @@ import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { getNextWorkflowColor } from '@/lib/workflows/colors' +import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults' +import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils' import { deduplicateWorkflowName, listWorkflows, type WorkflowScope } from '@/lib/workflows/utils' import { getUserEntityPermissions, workspaceExists } from '@/lib/workspaces/permissions/utils' import { verifyWorkspaceMembership } from '@/app/api/workflows/utils' @@ -247,24 +249,30 @@ export async function POST(req: NextRequest) { // Silently fail }) - await db.insert(workflow).values({ - id: workflowId, - userId, - workspaceId, - folderId: folderId || null, - sortOrder, - name, - description, - color, - lastSynced: now, - createdAt: now, - updatedAt: now, - isDeployed: false, - runCount: 0, - variables: {}, + const { workflowState, subBlockValues, startBlockId } = buildDefaultWorkflowArtifacts() + + await db.transaction(async (tx) => { + await tx.insert(workflow).values({ + id: workflowId, + userId, + workspaceId, + folderId: folderId || null, + sortOrder, + name, + description, + color, + lastSynced: now, + createdAt: now, + updatedAt: now, + isDeployed: false, + runCount: 0, + variables: {}, + }) + + await saveWorkflowToNormalizedTables(workflowId, workflowState, tx) }) - logger.info(`[${requestId}] Successfully created empty workflow ${workflowId}`) + logger.info(`[${requestId}] Successfully created workflow ${workflowId} with default blocks`) recordAudit({ workspaceId, @@ -290,6 +298,8 @@ export async function POST(req: NextRequest) { sortOrder, createdAt: now, updatedAt: now, + startBlockId, + subBlockValues, }) } catch (error) { if (error instanceof z.ZodError) { diff --git a/apps/sim/app/api/workspaces/route.ts b/apps/sim/app/api/workspaces/route.ts index 66493a5eb02..f6f51fee7b7 100644 --- a/apps/sim/app/api/workspaces/route.ts +++ b/apps/sim/app/api/workspaces/route.ts @@ -173,6 +173,9 @@ async function createWorkspace( runCount: 0, variables: {}, }) + + const { workflowState } = buildDefaultWorkflowArtifacts() + await saveWorkflowToNormalizedTables(workflowId, workflowState, tx) } logger.info( @@ -181,15 +184,6 @@ async function createWorkspace( : `Created workspace ${workspaceId} with initial workflow ${workflowId} for user ${userId}` ) }) - - if (!skipDefaultWorkflow) { - const { workflowState } = buildDefaultWorkflowArtifacts() - const seedResult = await saveWorkflowToNormalizedTables(workflowId, workflowState) - - if (!seedResult.success) { - throw new Error(seedResult.error || 'Failed to seed default workflow state') - } - } } catch (error) { logger.error(`Failed to create workspace ${workspaceId}:`, error) throw error diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 5e9bf92463b..57fd7a41706 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -2232,6 +2232,10 @@ const WorkflowContent = React.memo( return } + if (hydration.phase === 'creating') { + return + } + // If already loading (state-loading phase), skip if (hydration.phase === 'state-loading' && hydration.workflowId === currentId) { return @@ -2299,6 +2303,10 @@ const WorkflowContent = React.memo( return } + if (hydration.phase === 'creating') { + return + } + // If no workflows exist after loading, redirect to workspace root if (workflowCount === 0) { logger.info('No workflows found, redirecting to workspace root') diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx index 295f8d88033..6c9b0b21953 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx @@ -32,6 +32,7 @@ import { getWorkflows } from '@/hooks/queries/utils/workflow-cache' import { useCreateWorkflow } from '@/hooks/queries/workflows' import { useFolderStore } from '@/stores/folders/store' import type { FolderTreeNode } from '@/stores/folders/types' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { generateCreativeWorkflowName } from '@/stores/workflows/registry/utils' const logger = createLogger('FolderItem') @@ -135,29 +136,23 @@ export function FolderItem({ const isEditingRef = useRef(false) - const handleCreateWorkflowInFolder = useCallback(async () => { - try { - const name = generateCreativeWorkflowName() - const color = getNextWorkflowColor() + const handleCreateWorkflowInFolder = useCallback(() => { + const name = generateCreativeWorkflowName() + const color = getNextWorkflowColor() + const id = crypto.randomUUID() - const result = await createWorkflowMutation.mutateAsync({ - workspaceId, - folderId: folder.id, - name, - color, - id: crypto.randomUUID(), - }) + createWorkflowMutation.mutate({ + workspaceId, + folderId: folder.id, + name, + color, + id, + }) - if (result.id) { - router.push(`/workspace/${workspaceId}/w/${result.id}`) - expandFolder() - window.dispatchEvent( - new CustomEvent(SIDEBAR_SCROLL_EVENT, { detail: { itemId: result.id } }) - ) - } - } catch (error) { - logger.error('Failed to create workflow in folder:', error) - } + useWorkflowRegistry.getState().markWorkflowCreating(id) + expandFolder() + router.push(`/workspace/${workspaceId}/w/${id}`) + window.dispatchEvent(new CustomEvent(SIDEBAR_SCROLL_EVENT, { detail: { itemId: id } })) }, [createWorkflowMutation, workspaceId, folder.id, router, expandFolder]) const handleCreateFolderInFolder = useCallback(async () => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workflow-operations.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workflow-operations.ts index 054151a5a5b..3ed24ec83ed 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workflow-operations.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workflow-operations.ts @@ -1,13 +1,11 @@ import { useCallback, useMemo } from 'react' -import { createLogger } from '@sim/logger' import { useRouter } from 'next/navigation' import { getNextWorkflowColor } from '@/lib/workflows/colors' import { useCreateWorkflow, useWorkflowMap } from '@/hooks/queries/workflows' import { useWorkflowDiffStore } from '@/stores/workflow-diff/store' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { generateCreativeWorkflowName } from '@/stores/workflows/registry/utils' -const logger = createLogger('useWorkflowOperations') - interface UseWorkflowOperationsProps { workspaceId: string } @@ -25,30 +23,24 @@ export function useWorkflowOperations({ workspaceId }: UseWorkflowOperationsProp [workflows, workspaceId] ) - const handleCreateWorkflow = useCallback(async (): Promise => { - try { - const { clearDiff } = useWorkflowDiffStore.getState() - clearDiff() - - const name = generateCreativeWorkflowName() - const color = getNextWorkflowColor() - - const result = await createWorkflowMutation.mutateAsync({ - workspaceId, - name, - color, - id: crypto.randomUUID(), - }) - - if (result.id) { - router.push(`/workspace/${workspaceId}/w/${result.id}`) - return result.id - } - return null - } catch (error) { - logger.error('Error creating workflow:', error) - return null - } + const handleCreateWorkflow = useCallback((): Promise => { + const { clearDiff } = useWorkflowDiffStore.getState() + clearDiff() + + const name = generateCreativeWorkflowName() + const color = getNextWorkflowColor() + const id = crypto.randomUUID() + + createWorkflowMutation.mutate({ + workspaceId, + name, + color, + id, + }) + + useWorkflowRegistry.getState().markWorkflowCreating(id) + router.push(`/workspace/${workspaceId}/w/${id}`) + return Promise.resolve(id) }, [createWorkflowMutation, workspaceId, router]) return { diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-management.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-management.ts index 47da0573a4f..a3d0bd3c18b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-management.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-management.ts @@ -39,6 +39,7 @@ export function useWorkspaceManagement({ const { data: workspaces = [], isLoading: isWorkspacesLoading, + isFetching: isWorkspacesFetching, refetch: refetchWorkspaces, } = useWorkspacesQuery(Boolean(sessionUserId)) @@ -71,6 +72,9 @@ export function useWorkspaceManagement({ const matchingWorkspace = workspaces.find((w) => w.id === currentWorkspaceId) if (!matchingWorkspace) { + if (isWorkspacesFetching) { + return + } logger.warn(`Workspace ${currentWorkspaceId} not found in user's workspaces`) const fallbackWorkspace = workspaces[0] logger.info(`Redirecting to fallback workspace: ${fallbackWorkspace.id}`) @@ -78,7 +82,7 @@ export function useWorkspaceManagement({ } hasValidatedRef.current = true - }, [workspaces, isWorkspacesLoading]) + }, [workspaces, isWorkspacesLoading, isWorkspacesFetching]) const refreshWorkspaceList = useCallback(async () => { await queryClient.invalidateQueries({ queryKey: workspaceKeys.lists() }) diff --git a/apps/sim/app/workspace/providers/socket-provider.tsx b/apps/sim/app/workspace/providers/socket-provider.tsx index 04b06e7214b..6a27bd3a664 100644 --- a/apps/sim/app/workspace/providers/socket-provider.tsx +++ b/apps/sim/app/workspace/providers/socket-provider.tsx @@ -15,6 +15,7 @@ import { useParams } from 'next/navigation' import type { Socket } from 'socket.io-client' import { getEnv } from '@/lib/core/config/env' import { useOperationQueueStore } from '@/stores/operation-queue/store' +import { useWorkflowRegistry as useWorkflowRegistryStore } from '@/stores/workflows/registry/store' const logger = createLogger('SocketContext') @@ -387,13 +388,11 @@ export function SocketProvider({ children, user }: SocketProviderProps) { { useWorkflowRegistry }, { useWorkflowStore }, { useSubBlockStore }, - { useWorkflowDiffStore }, ] = await Promise.all([ import('@/stores/operation-queue/store'), import('@/stores/workflows/registry/store'), import('@/stores/workflows/workflow/store'), import('@/stores/workflows/subblock/store'), - import('@/stores/workflow-diff/store'), ]) const { activeWorkflowId } = useWorkflowRegistry.getState() @@ -542,9 +541,13 @@ export function SocketProvider({ children, user }: SocketProviderProps) { } }, [user?.id, authFailed]) + const hydrationPhase = useWorkflowRegistryStore((s) => s.hydration.phase) + useEffect(() => { if (!socket || !isConnected || !urlWorkflowId) return + if (hydrationPhase === 'creating') return + // Skip if already in the correct room if (currentWorkflowId === urlWorkflowId) return @@ -562,7 +565,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) { workflowId: urlWorkflowId, tabSessionId: getTabSessionId(), }) - }, [socket, isConnected, urlWorkflowId, currentWorkflowId]) + }, [socket, isConnected, urlWorkflowId, currentWorkflowId, hydrationPhase]) const joinWorkflow = useCallback( (workflowId: string) => { diff --git a/apps/sim/hooks/queries/workflows.ts b/apps/sim/hooks/queries/workflows.ts index f850eae3f40..729d3adf288 100644 --- a/apps/sim/hooks/queries/workflows.ts +++ b/apps/sim/hooks/queries/workflows.ts @@ -11,7 +11,6 @@ import { useQueryClient, } from '@tanstack/react-query' import { getNextWorkflowColor } from '@/lib/workflows/colors' -import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults' import { deploymentKeys } from '@/hooks/queries/deployments' import { fetchDeploymentVersionState } from '@/hooks/queries/utils/fetch-deployment-version-state' import { getFolderMap } from '@/hooks/queries/utils/folder-cache' @@ -105,6 +104,7 @@ interface CreateWorkflowResult { workspaceId: string folderId?: string | null sortOrder: number + subBlockValues?: Record> } export function useCreateWorkflow() { @@ -144,19 +144,6 @@ export function useCreateWorkflow() { logger.info(`Successfully created workflow ${workflowId}`) - const { workflowState } = buildDefaultWorkflowArtifacts() - - const stateResponse = await fetch(`/api/workflows/${workflowId}/state`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(workflowState), - }) - - if (!stateResponse.ok) { - const text = await stateResponse.text() - logger.error('Failed to persist default workflow state:', text) - } - return { id: workflowId, name: createdWorkflow.name, @@ -165,6 +152,7 @@ export function useCreateWorkflow() { workspaceId, folderId: createdWorkflow.folderId, sortOrder: createdWorkflow.sortOrder ?? 0, + subBlockValues: createdWorkflow.subBlockValues, } }, onMutate: async (variables) => { @@ -247,15 +235,18 @@ export function useCreateWorkflow() { }) } - const { subBlockValues } = buildDefaultWorkflowArtifacts() - useSubBlockStore.setState((state) => ({ - workflowValues: { - ...state.workflowValues, - [data.id]: subBlockValues, - }, - })) + if (data.subBlockValues) { + useSubBlockStore.setState((state) => ({ + workflowValues: { + ...state.workflowValues, + [data.id]: data.subBlockValues!, + }, + })) + } logger.info(`[CreateWorkflow] Success, replaced temp entry ${tempId}`) + + useWorkflowRegistry.getState().markWorkflowCreated(data.id) }, onError: (_error, variables, context) => { if (context?.snapshot) { @@ -265,6 +256,8 @@ export function useCreateWorkflow() { ) logger.info('[CreateWorkflow] Rolled back to previous state') } + + useWorkflowRegistry.getState().markWorkflowCreated(null) }, onSettled: (_data, _error, variables) => { return invalidateWorkflowLists(queryClient, variables.workspaceId, ['active', 'archived']) diff --git a/apps/sim/hooks/queries/workspace.ts b/apps/sim/hooks/queries/workspace.ts index b223ca18678..e77547b660e 100644 --- a/apps/sim/hooks/queries/workspace.ts +++ b/apps/sim/hooks/queries/workspace.ts @@ -65,7 +65,8 @@ interface CreateWorkspaceParams { /** * Creates a new workspace. - * Automatically invalidates the workspace list cache on success. + * Merges the created row into the active list cache before invalidation so navigation + * cannot race a stale list (see workspace validation fallback in use-workspace-management). */ export function useCreateWorkspace() { const queryClient = useQueryClient() @@ -86,7 +87,16 @@ export function useCreateWorkspace() { const data = await response.json() return data.workspace as Workspace }, - onSuccess: () => { + onSuccess: (newWorkspace) => { + queryClient.setQueryData(workspaceKeys.list('active'), (previous) => { + if (!previous?.length) { + return [newWorkspace] + } + if (previous.some((w) => w.id === newWorkspace.id)) { + return previous + } + return [newWorkspace, ...previous] + }) queryClient.invalidateQueries({ queryKey: workspaceKeys.lists() }) queryClient.invalidateQueries({ queryKey: workspaceKeys.adminLists() }) }, diff --git a/apps/sim/lib/workflows/persistence/utils.ts b/apps/sim/lib/workflows/persistence/utils.ts index 9044db5638b..e33c531cf6a 100644 --- a/apps/sim/lib/workflows/persistence/utils.ts +++ b/apps/sim/lib/workflows/persistence/utils.ts @@ -531,88 +531,89 @@ export async function loadWorkflowFromNormalizedTables( */ export async function saveWorkflowToNormalizedTables( workflowId: string, - state: WorkflowState + state: WorkflowState, + externalTx?: DbOrTx ): Promise<{ success: boolean; error?: string }> { - try { - const blockRecords = state.blocks as Record - const canonicalLoops = generateLoopBlocks(blockRecords) - const canonicalParallels = generateParallelBlocks(blockRecords) + const blockRecords = state.blocks as Record + const canonicalLoops = generateLoopBlocks(blockRecords) + const canonicalParallels = generateParallelBlocks(blockRecords) + + const execute = async (tx: DbOrTx) => { + await Promise.all([ + tx.delete(workflowBlocks).where(eq(workflowBlocks.workflowId, workflowId)), + tx.delete(workflowEdges).where(eq(workflowEdges.workflowId, workflowId)), + tx.delete(workflowSubflows).where(eq(workflowSubflows.workflowId, workflowId)), + ]) - // Start a transaction - await db.transaction(async (tx) => { - await Promise.all([ - tx.delete(workflowBlocks).where(eq(workflowBlocks.workflowId, workflowId)), - tx.delete(workflowEdges).where(eq(workflowEdges.workflowId, workflowId)), - tx.delete(workflowSubflows).where(eq(workflowSubflows.workflowId, workflowId)), - ]) - - // Insert blocks - if (Object.keys(state.blocks).length > 0) { - const blockInserts = Object.values(state.blocks).map((block) => ({ - id: block.id, - workflowId: workflowId, - type: block.type, - name: block.name || '', - positionX: String(block.position?.x || 0), - positionY: String(block.position?.y || 0), - enabled: block.enabled ?? true, - horizontalHandles: block.horizontalHandles ?? true, - advancedMode: block.advancedMode ?? false, - triggerMode: block.triggerMode ?? false, - height: String(block.height || 0), - subBlocks: block.subBlocks || {}, - outputs: block.outputs || {}, - data: block.data || {}, - parentId: block.data?.parentId || null, - extent: block.data?.extent || null, - locked: block.locked ?? false, - })) - - await tx.insert(workflowBlocks).values(blockInserts) - } + if (Object.keys(state.blocks).length > 0) { + const blockInserts = Object.values(state.blocks).map((block) => ({ + id: block.id, + workflowId: workflowId, + type: block.type, + name: block.name || '', + positionX: String(block.position?.x || 0), + positionY: String(block.position?.y || 0), + enabled: block.enabled ?? true, + horizontalHandles: block.horizontalHandles ?? true, + advancedMode: block.advancedMode ?? false, + triggerMode: block.triggerMode ?? false, + height: String(block.height || 0), + subBlocks: block.subBlocks || {}, + outputs: block.outputs || {}, + data: block.data || {}, + parentId: block.data?.parentId || null, + extent: block.data?.extent || null, + locked: block.locked ?? false, + })) + + await tx.insert(workflowBlocks).values(blockInserts) + } - // Insert edges - if (state.edges.length > 0) { - const edgeInserts = state.edges.map((edge) => ({ - id: edge.id, - workflowId: workflowId, - sourceBlockId: edge.source, - targetBlockId: edge.target, - sourceHandle: edge.sourceHandle || null, - targetHandle: edge.targetHandle || null, - })) - - await tx.insert(workflowEdges).values(edgeInserts) - } + if (state.edges.length > 0) { + const edgeInserts = state.edges.map((edge) => ({ + id: edge.id, + workflowId: workflowId, + sourceBlockId: edge.source, + targetBlockId: edge.target, + sourceHandle: edge.sourceHandle || null, + targetHandle: edge.targetHandle || null, + })) + + await tx.insert(workflowEdges).values(edgeInserts) + } - // Insert subflows (loops and parallels) - const subflowInserts: SubflowInsert[] = [] + const subflowInserts: SubflowInsert[] = [] - // Add loops - Object.values(canonicalLoops).forEach((loop) => { - subflowInserts.push({ - id: loop.id, - workflowId: workflowId, - type: SUBFLOW_TYPES.LOOP, - config: loop, - }) + Object.values(canonicalLoops).forEach((loop) => { + subflowInserts.push({ + id: loop.id, + workflowId: workflowId, + type: SUBFLOW_TYPES.LOOP, + config: loop, }) + }) - // Add parallels - Object.values(canonicalParallels).forEach((parallel) => { - subflowInserts.push({ - id: parallel.id, - workflowId: workflowId, - type: SUBFLOW_TYPES.PARALLEL, - config: parallel, - }) + Object.values(canonicalParallels).forEach((parallel) => { + subflowInserts.push({ + id: parallel.id, + workflowId: workflowId, + type: SUBFLOW_TYPES.PARALLEL, + config: parallel, }) - - if (subflowInserts.length > 0) { - await tx.insert(workflowSubflows).values(subflowInserts) - } }) + if (subflowInserts.length > 0) { + await tx.insert(workflowSubflows).values(subflowInserts) + } + } + + if (externalTx) { + await execute(externalTx) + return { success: true } + } + + try { + await db.transaction(execute) return { success: true } } catch (error) { logger.error(`Error saving workflow ${workflowId} to normalized tables:`, error) diff --git a/apps/sim/stores/workflows/registry/store.ts b/apps/sim/stores/workflows/registry/store.ts index 4d3fece524a..9ece2c4c18e 100644 --- a/apps/sim/stores/workflows/registry/store.ts +++ b/apps/sim/stores/workflows/registry/store.ts @@ -267,6 +267,16 @@ export const useWorkflowRegistry = create()( ? error.message : `Failed to load workflow ${workflowId}: Unknown error` logger.error(message) + + const currentHydration = get().hydration + if ( + currentHydration.requestId !== requestId || + currentHydration.workflowId !== workflowId + ) { + logger.info('Discarding stale workflow error', { workflowId, requestId }) + return + } + set((state) => ({ error: message, hydration: { @@ -301,6 +311,52 @@ export const useWorkflowRegistry = create()( await get().loadWorkflowState(id) }, + markWorkflowCreating: (workflowId: string) => { + set((state) => ({ + error: null, + hydration: { + phase: 'creating' as const, + workspaceId: state.hydration.workspaceId, + workflowId, + requestId: null, + error: null, + }, + })) + logger.info(`Marked workflow ${workflowId} as creating`) + }, + + markWorkflowCreated: (workflowId: string | null) => { + const { hydration } = get() + + if (!workflowId) { + if (hydration.phase === 'creating') { + set((state) => ({ + hydration: { + ...state.hydration, + phase: 'idle' as const, + workflowId: null, + error: null, + }, + })) + } + return + } + + if (hydration.phase !== 'creating' || hydration.workflowId !== workflowId) { + logger.info( + `Ignoring markWorkflowCreated for ${workflowId} — hydration is ${hydration.phase}/${hydration.workflowId}` + ) + return + } + + logger.info(`Workflow ${workflowId} created, loading state`) + get() + .loadWorkflowState(workflowId) + .catch((error) => { + logger.error(`Failed to load newly created workflow ${workflowId}:`, error) + }) + }, + logout: () => { logger.info('Logging out - clearing all workflow data') diff --git a/apps/sim/stores/workflows/registry/types.ts b/apps/sim/stores/workflows/registry/types.ts index 375ee0df239..f434967e413 100644 --- a/apps/sim/stores/workflows/registry/types.ts +++ b/apps/sim/stores/workflows/registry/types.ts @@ -32,7 +32,7 @@ export interface WorkflowMetadata { isSandbox?: boolean } -export type HydrationPhase = 'idle' | 'state-loading' | 'ready' | 'error' +export type HydrationPhase = 'idle' | 'creating' | 'state-loading' | 'ready' | 'error' export interface HydrationState { phase: HydrationPhase @@ -55,6 +55,8 @@ export interface WorkflowRegistryActions { setActiveWorkflow: (id: string) => Promise loadWorkflowState: (workflowId: string) => Promise switchToWorkspace: (id: string) => void + markWorkflowCreating: (workflowId: string) => void + markWorkflowCreated: (workflowId: string | null) => void getWorkflowDeploymentStatus: (workflowId: string | null) => DeploymentStatus | null setDeploymentStatus: ( workflowId: string | null,