feat(error-handling): created error path and upgraded ports

This commit is contained in:
Emir Karabeg
2025-03-26 00:04:18 -07:00
parent 891c1f5cf1
commit 3e3d370c6d
5 changed files with 200 additions and 29 deletions
@@ -84,7 +84,7 @@ export function ConnectionBlocks({ blockId, setIsConnecting }: ConnectionBlocksP
)
return (
<div className="absolute -left-[240px] top-0 space-y-2 flex flex-col items-end w-[220px] max-h-[400px] overflow-y-auto">
<div className="absolute -left-[162px] top-0 space-y-2 flex flex-col items-end max-h-[400px] overflow-y-auto">
{sortedConnections.map((connection) => (
<div key={connection.id} className="space-y-2">
{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"
>
<div className="text-sm">
<span className="font-medium leading-none">
@@ -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"
>
<div className="text-sm">
<span className="font-medium leading-none">
@@ -224,13 +224,20 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
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<WorkflowBlockProps>) {
{/* Output Handle */}
{type !== 'condition' && (
<Handle
type="source"
position={horizontalHandles ? Position.Right : Position.Bottom}
id="source"
className={cn(
'!w-3.5 !h-3.5',
'!bg-white !rounded-full !border !border-gray-200',
'group-hover:!border-blue-500',
'!cursor-crosshair',
'transition-[border-color] duration-150',
horizontalHandles ? '!right-[-7px]' : '!bottom-[-7px]'
<>
<Handle
type="source"
position={horizontalHandles ? Position.Right : Position.Bottom}
id="source"
className={cn(
'!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:!right-[-10px] hover:!rounded-r-full hover:!rounded-l-none'
: 'hover:!h-[10px] hover:!translate-y-[3px] hover:!rounded-b-full hover:!rounded-t-none',
'!cursor-crosshair',
'transition-all duration-150',
horizontalHandles ? '!right-[-7px]' : '!bottom-[-7px]'
)}
style={{
...(horizontalHandles ? { top: '50%', transform: 'translateY(-50%)' } : {}),
}}
data-nodeid={id}
data-handleid="source"
isConnectableStart={true}
isConnectableEnd={false}
isValidConnection={(connection) => connection.target !== id}
/>
{/* Error Handle - Don't show for starter blocks */}
{type !== 'starter' && (
<Handle
type="source"
position={horizontalHandles ? Position.Right : Position.Bottom}
id="error"
className={cn(
'!w-[7px] !h-5',
'!bg-red-400 dark:!bg-red-500 !rounded-[2px] !border-none',
'!z-[-1]',
'group-hover:!shadow-[0_0_0_3px_rgba(248,113,113,0.15)]',
horizontalHandles
? 'hover:!w-[10px] hover:!right-[-10px] hover:!rounded-r-full hover:!rounded-l-none'
: 'hover:!h-[10px] hover:!translate-y-[3px] hover:!rounded-b-full hover:!rounded-t-none',
'!cursor-crosshair',
'transition-all duration-150'
)}
style={{
position: 'absolute',
...(horizontalHandles
? {
right: '-8px',
top: 'auto',
bottom: '30px',
transform: 'translateY(0)',
}
: {
bottom: '-7px',
left: 'auto',
right: '30px',
transform: 'translateX(0)',
}),
}}
data-nodeid={id}
data-handleid="error"
isConnectableStart={true}
isConnectableEnd={false}
isValidConnection={(connection) => connection.target !== id}
/>
)}
data-nodeid={id}
data-handleid="source"
isConnectableStart={true}
isConnectableEnd={false}
isValidConnection={(connection) => connection.target !== id}
/>
</>
)}
</Card>
</div>
+96 -3
View File
@@ -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}` }
}
}
}
+21 -2
View File
@@ -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)
}
}
}
}
+2
View File
@@ -32,7 +32,9 @@ export interface NormalizedBlockOutput {
data?: any // Response data from API calls
status?: number // HTTP status code
headers?: Record<string, string> // 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
}