From 3e3d370c6d5e9044232ecb1dc40a07fb4a061b37 Mon Sep 17 00:00:00 2001 From: Emir Karabeg Date: Tue, 25 Mar 2025 23:56:22 -0700 Subject: [PATCH] feat(error-handling): created error path and upgraded ports --- .../connection-blocks/connection-blocks.tsx | 6 +- .../workflow-block/workflow-block.tsx | 99 +++++++++++++++---- sim/executor/index.ts | 99 ++++++++++++++++++- sim/executor/path.ts | 23 ++++- sim/executor/types.ts | 2 + 5 files changed, 200 insertions(+), 29 deletions(-) diff --git a/sim/app/w/[id]/components/workflow-block/components/connection-blocks/connection-blocks.tsx b/sim/app/w/[id]/components/workflow-block/components/connection-blocks/connection-blocks.tsx index 19505e20c0..21fdbe8691 100644 --- a/sim/app/w/[id]/components/workflow-block/components/connection-blocks/connection-blocks.tsx +++ b/sim/app/w/[id]/components/workflow-block/components/connection-blocks/connection-blocks.tsx @@ -84,7 +84,7 @@ export function ConnectionBlocks({ blockId, setIsConnecting }: ConnectionBlocksP ) return ( -
+
{sortedConnections.map((connection) => (
{Array.isArray(connection.outputType) ? ( @@ -103,7 +103,7 @@ export function ConnectionBlocks({ blockId, setIsConnecting }: ConnectionBlocksP draggable onDragStart={(e) => handleDragStart(e, connection, field)} onDragEnd={handleDragEnd} - className="group flex items-center rounded-lg border bg-card p-2 shadow-sm transition-colors hover:bg-accent/50 cursor-grab active:cursor-grabbing w-fit" + className="group flex items-center rounded-lg border bg-card p-2 shadow-sm transition-colors hover:bg-accent/50 cursor-grab active:cursor-grabbing w-max" >
@@ -119,7 +119,7 @@ export function ConnectionBlocks({ blockId, setIsConnecting }: ConnectionBlocksP draggable onDragStart={(e) => handleDragStart(e, connection)} onDragEnd={handleDragEnd} - className="group flex items-center rounded-lg border bg-card p-2 shadow-sm transition-colors hover:bg-accent/50 cursor-grab active:cursor-grabbing w-fit" + className="group flex items-center rounded-lg border bg-card p-2 shadow-sm transition-colors hover:bg-accent/50 cursor-grab active:cursor-grabbing w-max" >
diff --git a/sim/app/w/[id]/components/workflow-block/workflow-block.tsx b/sim/app/w/[id]/components/workflow-block/workflow-block.tsx index 884913d0e8..95801922a0 100644 --- a/sim/app/w/[id]/components/workflow-block/workflow-block.tsx +++ b/sim/app/w/[id]/components/workflow-block/workflow-block.tsx @@ -224,13 +224,20 @@ export function WorkflowBlock({ id, data }: NodeProps) { position={horizontalHandles ? Position.Left : Position.Top} id="target" className={cn( - '!w-3.5 !h-3.5', - '!bg-white !rounded-full !border !border-gray-200', - 'group-hover:!border-blue-500', + '!w-[7px] !h-5', + '!bg-slate-300 dark:!bg-slate-500 !rounded-[2px] !border-none', + '!z-[-1]', + 'group-hover:!shadow-[0_0_0_3px_rgba(156,163,175,0.15)]', + horizontalHandles + ? 'hover:!w-[10px] hover:!left-[-10px] hover:!rounded-l-full hover:!rounded-r-none' + : 'hover:!h-[10px] hover:!translate-y-[-3px] hover:!rounded-t-full hover:!rounded-b-none', '!cursor-crosshair', - 'transition-[border-color] duration-150', + 'transition-all duration-150', horizontalHandles ? '!left-[-7px]' : '!top-[-7px]' )} + style={{ + ...(horizontalHandles ? { top: '50%', transform: 'translateY(-50%)' } : {}), + }} data-nodeid={id} data-handleid="target" isConnectableStart={false} @@ -362,24 +369,74 @@ export function WorkflowBlock({ id, data }: NodeProps) { {/* Output Handle */} {type !== 'condition' && ( - + connection.target !== id} + /> + + {/* Error Handle - Don't show for starter blocks */} + {type !== 'starter' && ( + connection.target !== id} + /> )} - data-nodeid={id} - data-handleid="source" - isConnectableStart={true} - isConnectableEnd={false} - isValidConnection={(connection) => connection.target !== id} - /> + )}
diff --git a/sim/executor/index.ts b/sim/executor/index.ts index ce8184aeb8..9538396c28 100644 --- a/sim/executor/index.ts +++ b/sim/executor/index.ts @@ -392,6 +392,10 @@ export class Executor { } else { const allDependenciesMet = incomingConnections.every((conn) => { const sourceExecuted = executedBlocks.has(conn.source) + const sourceBlock = this.workflow.blocks.find((b) => b.id === conn.source) + const sourceBlockState = context.blockStates.get(conn.source) + const hasSourceError = sourceBlockState?.output?.error !== undefined || + sourceBlockState?.output?.response?.error !== undefined // For condition blocks, check if this is the selected path if (conn.sourceHandle?.startsWith('condition-')) { @@ -411,7 +415,6 @@ export class Executor { } // For router blocks, check if this is the selected target - const sourceBlock = this.workflow.blocks.find((b) => b.id === conn.source) if (sourceBlock?.metadata?.id === 'router') { const selectedTarget = context.decisions.router.get(conn.source) @@ -424,6 +427,16 @@ export class Executor { return sourceExecuted && conn.target === selectedTarget } + // For error connections, check if the source had an error + if (conn.sourceHandle === 'error') { + return sourceExecuted && hasSourceError + } + + // For regular connections, check if the source was executed without error + if (conn.sourceHandle === 'source' || !conn.sourceHandle) { + return sourceExecuted && !hasSourceError + } + // If source is not in active path, consider this dependency met // This allows blocks with multiple inputs to execute even if some inputs are from inactive paths if (!context.activeExecutionPath.has(conn.source)) { @@ -556,6 +569,7 @@ export class Executor { blockLog.durationMs = new Date(blockLog.endedAt).getTime() - new Date(blockLog.startedAt).getTime() + // Log the error even if we'll continue execution through error path context.blockLogs.push(blockLog) addConsole({ output: {}, @@ -571,6 +585,34 @@ export class Executor { blockType: block.metadata?.id || 'unknown', }) + // Create error output with appropriate structure + const errorOutput: NormalizedBlockOutput = { + response: { + error: this.extractErrorMessage(error), + status: error.status || 500, + }, + error: this.extractErrorMessage(error), + } + + // Set block state with error output + context.blockStates.set(blockId, { + output: errorOutput, + executed: true, + executionTime: blockLog.durationMs, + }) + + // Check for error connections and follow them if they exist + const hasErrorPath = this.activateErrorPath(blockId, context) + + // Console.error the error for visibility + logger.error(`Error executing block ${block.metadata?.name || blockId}:`, this.sanitizeError(error)) + + // If there are error paths to follow, return error output instead of throwing + if (hasErrorPath) { + // Return the error output to allow execution to continue along error path + return errorOutput + } + // Create a proper error message that is never undefined let errorMessage = error.message @@ -590,6 +632,39 @@ export class Executor { } } + /** + * Activates error paths from a block that had an error. + * Checks for connections from the block's "error" handle and adds them to the active execution path. + * + * @param blockId - ID of the block that had an error + * @param context - Current execution context + * @returns Whether there was an error path to follow + */ + private activateErrorPath(blockId: string, context: ExecutionContext): boolean { + // Skip for starter blocks which don't have error handles + const block = this.workflow.blocks.find((b) => b.id === blockId) + if (block?.metadata?.id === 'starter' || block?.metadata?.id === 'condition') { + return false + } + + // Look for connections from this block's error handle + const errorConnections = this.workflow.connections.filter( + (conn) => conn.source === blockId && conn.sourceHandle === 'error' + ) + + if (errorConnections.length === 0) { + return false + } + + // Add all error connection targets to the active execution path + for (const conn of errorConnections) { + context.activeExecutionPath.add(conn.target) + logger.info(`Activated error path from ${blockId} to ${conn.target}`) + } + + return true + } + /** * Normalizes a block output to ensure it has the expected structure. * Handles different block types with appropriate response formats. @@ -599,8 +674,26 @@ export class Executor { * @returns Normalized output with consistent structure */ private normalizeBlockOutput(output: any, block: SerializedBlock): NormalizedBlockOutput { + // Handle error outputs + if (output && typeof output === 'object' && output.error) { + return { + response: { + error: output.error, + status: output.status || 500, + }, + error: output.error, + }; + } + if (output && typeof output === 'object' && 'response' in output) { - return output as NormalizedBlockOutput + // If response already contains an error, maintain it + if (output.response && output.response.error) { + return { + ...output, + error: output.response.error + }; + } + return output as NormalizedBlockOutput; } const blockType = block.metadata?.id @@ -830,4 +923,4 @@ export class Executor { return { message: `Unexpected error type: ${typeof error}` } } -} +} \ No newline at end of file diff --git a/sim/executor/path.ts b/sim/executor/path.ts index e77c1b2fa4..61a9a4da63 100644 --- a/sim/executor/path.ts +++ b/sim/executor/path.ts @@ -96,13 +96,32 @@ export class PathTracker { } } } else { - // For regular blocks, activate all outgoing connections + // For regular blocks, activate all outgoing connections based on success or error status + const blockState = context.blockStates.get(blockId) + const hasError = blockState?.output?.error !== undefined || blockState?.output?.response?.error !== undefined + + // Get all outgoing connections const outgoingConnections = this.workflow.connections.filter( (conn) => conn.source === blockId ) for (const conn of outgoingConnections) { - context.activeExecutionPath.add(conn.target) + // For error connections, only activate them on error + if (conn.sourceHandle === 'error') { + if (hasError) { + context.activeExecutionPath.add(conn.target) + } + } + // For regular (source) connections, only activate them if there's no error + else if (conn.sourceHandle === 'source' || !conn.sourceHandle) { + if (!hasError) { + context.activeExecutionPath.add(conn.target) + } + } + // All other types of connections (e.g., from condition blocks) follow their own rules + else { + context.activeExecutionPath.add(conn.target) + } } } } diff --git a/sim/executor/types.ts b/sim/executor/types.ts index 12d95219fe..091db740c9 100644 --- a/sim/executor/types.ts +++ b/sim/executor/types.ts @@ -32,7 +32,9 @@ export interface NormalizedBlockOutput { data?: any // Response data from API calls status?: number // HTTP status code headers?: Record // HTTP headers + error?: string // Error message if block execution failed } + error?: string // Top-level error field for easy error checking [key: string]: any // Additional properties }