@d34dman/flowdrop 0.0.33 → 0.0.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/WorkflowEditor.svelte +22 -0
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.js +2 -0
- package/dist/helpers/workflowEditorHelper.d.ts +32 -3
- package/dist/helpers/workflowEditorHelper.js +57 -6
- package/dist/styles/base.css +9 -0
- package/dist/types/index.d.ts +3 -2
- package/dist/utils/connections.d.ts +46 -0
- package/dist/utils/connections.js +91 -0
- package/package.json +1 -1
|
@@ -620,4 +620,26 @@
|
|
|
620
620
|
stroke: var(--flowdrop-edge-data-color-selected);
|
|
621
621
|
stroke-width: 2;
|
|
622
622
|
}
|
|
623
|
+
|
|
624
|
+
/* Loopback Edge: Dashed gray line for loop iteration connections */
|
|
625
|
+
:global(.flowdrop--edge--loopback path.svelte-flow__edge-path) {
|
|
626
|
+
stroke: var(--flowdrop-edge-loopback-color);
|
|
627
|
+
stroke-width: var(--flowdrop-edge-loopback-width);
|
|
628
|
+
stroke-dasharray: var(--flowdrop-edge-loopback-dasharray);
|
|
629
|
+
opacity: var(--flowdrop-edge-loopback-opacity);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
:global(.flowdrop--edge--loopback:hover path.svelte-flow__edge-path) {
|
|
633
|
+
stroke: var(--flowdrop-edge-loopback-color-hover);
|
|
634
|
+
stroke-width: var(--flowdrop-edge-loopback-width-hover);
|
|
635
|
+
opacity: 1;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
:global(.flowdrop--edge--loopback.selected path.svelte-flow__edge-path) {
|
|
639
|
+
stroke: var(--flowdrop-edge-loopback-color-selected);
|
|
640
|
+
stroke-width: var(--flowdrop-edge-loopback-width-hover);
|
|
641
|
+
stroke-dasharray: var(--flowdrop-edge-loopback-dasharray);
|
|
642
|
+
filter: drop-shadow(0 0 3px rgba(139, 92, 246, 0.4));
|
|
643
|
+
opacity: 1;
|
|
644
|
+
}
|
|
623
645
|
</style>
|
package/dist/core/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* import { getStatusColor, createDefaultExecutionInfo } from "@d34dman/flowdrop/core";
|
|
15
15
|
* ```
|
|
16
16
|
*/
|
|
17
|
-
export type { NodeCategory, NodeDataType, NodePort, DynamicPort, Branch, NodeMetadata, NodeExtensions, NodeUIExtensions, ConfigValues, WorkflowNode, WorkflowEdge, Workflow, ApiResponse, NodesResponse, WorkflowResponse, WorkflowsResponse, ExecutionStatus, ExecutionResult, FlowDropConfig, WorkflowEvents, BuiltinNodeType, PortConfig, PortCompatibilityRule, ConfigSchema, ConfigProperty, HttpMethod, DynamicSchemaEndpoint, ExternalEditLink, ConfigEditOptions } from '../types/index.js';
|
|
17
|
+
export type { NodeCategory, NodeDataType, NodePort, DynamicPort, Branch, NodeMetadata, NodeExtensions, NodeUIExtensions, ConfigValues, WorkflowNode, WorkflowEdge, Workflow, ApiResponse, NodesResponse, WorkflowResponse, WorkflowsResponse, ExecutionStatus, ExecutionResult, FlowDropConfig, WorkflowEvents, BuiltinNodeType, PortConfig, PortCompatibilityRule, ConfigSchema, ConfigProperty, HttpMethod, DynamicSchemaEndpoint, ExternalEditLink, ConfigEditOptions, EdgeCategory } from '../types/index.js';
|
|
18
18
|
export type { WorkflowEditorConfig, EditorFeatures, UIConfig, APIConfig, ExecutionConfig, StorageConfig } from '../types/config.js';
|
|
19
19
|
export type { AuthProvider, StaticAuthConfig, CallbackAuthConfig } from '../types/auth.js';
|
|
20
20
|
export type { WorkflowChangeType, FlowDropEventHandlers, FlowDropFeatures } from '../types/events.js';
|
|
@@ -35,6 +35,7 @@ export * from '../utils/colors.js';
|
|
|
35
35
|
export * from '../utils/icons.js';
|
|
36
36
|
export * from '../utils/config.js';
|
|
37
37
|
export * from '../utils/nodeTypes.js';
|
|
38
|
+
export { isLoopbackEdge, isValidLoopbackCycle, hasCycles, hasInvalidCycles } from '../utils/connections.js';
|
|
38
39
|
export { isFieldOptionArray, normalizeOptions } from '../components/form/types.js';
|
|
39
40
|
export { DEFAULT_PORT_CONFIG } from '../config/defaultPortConfig.js';
|
|
40
41
|
export { defaultEndpointConfig, createEndpointConfig } from '../config/endpoints.js';
|
package/dist/core/index.js
CHANGED
|
@@ -38,6 +38,8 @@ export * from '../utils/icons.js';
|
|
|
38
38
|
export * from '../utils/config.js';
|
|
39
39
|
// Node type utilities
|
|
40
40
|
export * from '../utils/nodeTypes.js';
|
|
41
|
+
// Connection utilities (including loopback edge detection)
|
|
42
|
+
export { isLoopbackEdge, isValidLoopbackCycle, hasCycles, hasInvalidCycles } from '../utils/connections.js';
|
|
41
43
|
// Form type utilities
|
|
42
44
|
export { isFieldOptionArray, normalizeOptions } from '../components/form/types.js';
|
|
43
45
|
// ============================================================================
|
|
@@ -14,9 +14,10 @@ export declare function generateNodeId(nodeTypeId: string, existingNodes: Workfl
|
|
|
14
14
|
* Edge category type for styling purposes
|
|
15
15
|
* - trigger: For control flow connections (dataType: "trigger")
|
|
16
16
|
* - tool: Dashed amber line for tool connections (dataType: "tool")
|
|
17
|
+
* - loopback: Dashed gray line for loop iteration connections (targets loop_back port)
|
|
17
18
|
* - data: Normal gray line for all other data connections
|
|
18
19
|
*/
|
|
19
|
-
export type EdgeCategory = 'trigger' | 'tool' | 'data';
|
|
20
|
+
export type EdgeCategory = 'trigger' | 'tool' | 'loopback' | 'data';
|
|
20
21
|
/**
|
|
21
22
|
* Edge styling configuration based on source port data type
|
|
22
23
|
*/
|
|
@@ -49,12 +50,25 @@ export declare class EdgeStylingHelper {
|
|
|
49
50
|
static getPortDataType(node: WorkflowNodeType, portId: string, portType: 'input' | 'output'): string | null;
|
|
50
51
|
/**
|
|
51
52
|
* Determine the edge category based on source port data type
|
|
53
|
+
* Note: This method does not check for loopback edges.
|
|
54
|
+
* Use getEdgeCategoryWithLoopback() for full edge categorization.
|
|
55
|
+
*
|
|
52
56
|
* @param sourcePortDataType - The data type of the source output port
|
|
53
57
|
* @returns The edge category for styling
|
|
54
58
|
*/
|
|
55
59
|
static getEdgeCategory(sourcePortDataType: string | null): EdgeCategory;
|
|
56
60
|
/**
|
|
57
|
-
*
|
|
61
|
+
* Determine the full edge category including loopback detection
|
|
62
|
+
* Loopback edges take precedence over source port data type
|
|
63
|
+
*
|
|
64
|
+
* @param edge - The edge to categorize
|
|
65
|
+
* @param sourcePortDataType - The data type of the source output port
|
|
66
|
+
* @returns The edge category for styling
|
|
67
|
+
*/
|
|
68
|
+
static getEdgeCategoryWithLoopback(edge: WorkflowEdge, sourcePortDataType: string | null): EdgeCategory;
|
|
69
|
+
/**
|
|
70
|
+
* Apply custom styling to connection edges based on edge type:
|
|
71
|
+
* - Loopback: Dashed gray line for loop iteration (targets loop_back port)
|
|
58
72
|
* - Trigger ports: Solid black line with arrow
|
|
59
73
|
* - Tool ports: Dashed amber line with arrow
|
|
60
74
|
* - Data ports: Normal gray line with arrow
|
|
@@ -118,9 +132,24 @@ export declare class WorkflowOperationsHelper {
|
|
|
118
132
|
*/
|
|
119
133
|
static exportWorkflow(workflow: Workflow | null): void;
|
|
120
134
|
/**
|
|
121
|
-
* Check if workflow has cycles
|
|
135
|
+
* Check if workflow has invalid cycles (excludes valid loopback cycles)
|
|
136
|
+
* Valid loopback cycles are used for ForEach node iteration and should not
|
|
137
|
+
* trigger a warning.
|
|
138
|
+
*
|
|
139
|
+
* @param nodes - Array of workflow nodes
|
|
140
|
+
* @param edges - Array of workflow edges
|
|
141
|
+
* @returns True if there are invalid (non-loopback) cycles
|
|
122
142
|
*/
|
|
123
143
|
static checkWorkflowCycles(nodes: WorkflowNodeType[], edges: WorkflowEdge[]): boolean;
|
|
144
|
+
/**
|
|
145
|
+
* Check if workflow has any cycles (including valid loopback cycles)
|
|
146
|
+
* Use this when you need to detect ALL cycles regardless of type.
|
|
147
|
+
*
|
|
148
|
+
* @param nodes - Array of workflow nodes
|
|
149
|
+
* @param edges - Array of workflow edges
|
|
150
|
+
* @returns True if any cycle exists
|
|
151
|
+
*/
|
|
152
|
+
static checkWorkflowHasAnyCycles(nodes: WorkflowNodeType[], edges: WorkflowEdge[]): boolean;
|
|
124
153
|
}
|
|
125
154
|
/**
|
|
126
155
|
* Configuration helper
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Contains business logic for workflow operations
|
|
4
4
|
*/
|
|
5
5
|
import { MarkerType } from '@xyflow/svelte';
|
|
6
|
-
import { hasCycles } from '../utils/connections.js';
|
|
6
|
+
import { hasCycles, hasInvalidCycles, isLoopbackEdge } from '../utils/connections.js';
|
|
7
7
|
import { workflowApi, nodeApi, setEndpointConfig } from '../services/api.js';
|
|
8
8
|
import { v4 as uuidv4 } from 'uuid';
|
|
9
9
|
import { workflowActions } from '../stores/workflowStore.js';
|
|
@@ -104,6 +104,9 @@ export class EdgeStylingHelper {
|
|
|
104
104
|
}
|
|
105
105
|
/**
|
|
106
106
|
* Determine the edge category based on source port data type
|
|
107
|
+
* Note: This method does not check for loopback edges.
|
|
108
|
+
* Use getEdgeCategoryWithLoopback() for full edge categorization.
|
|
109
|
+
*
|
|
107
110
|
* @param sourcePortDataType - The data type of the source output port
|
|
108
111
|
* @returns The edge category for styling
|
|
109
112
|
*/
|
|
@@ -118,7 +121,25 @@ export class EdgeStylingHelper {
|
|
|
118
121
|
return 'data';
|
|
119
122
|
}
|
|
120
123
|
/**
|
|
121
|
-
*
|
|
124
|
+
* Determine the full edge category including loopback detection
|
|
125
|
+
* Loopback edges take precedence over source port data type
|
|
126
|
+
*
|
|
127
|
+
* @param edge - The edge to categorize
|
|
128
|
+
* @param sourcePortDataType - The data type of the source output port
|
|
129
|
+
* @returns The edge category for styling
|
|
130
|
+
*/
|
|
131
|
+
static getEdgeCategoryWithLoopback(edge, sourcePortDataType) {
|
|
132
|
+
// Loopback edges are identified by their target handle
|
|
133
|
+
// Check this first as it takes precedence
|
|
134
|
+
if (isLoopbackEdge(edge)) {
|
|
135
|
+
return 'loopback';
|
|
136
|
+
}
|
|
137
|
+
// Fall back to source port data type categorization
|
|
138
|
+
return this.getEdgeCategory(sourcePortDataType);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Apply custom styling to connection edges based on edge type:
|
|
142
|
+
* - Loopback: Dashed gray line for loop iteration (targets loop_back port)
|
|
122
143
|
* - Trigger ports: Solid black line with arrow
|
|
123
144
|
* - Tool ports: Dashed amber line with arrow
|
|
124
145
|
* - Data ports: Normal gray line with arrow
|
|
@@ -130,17 +151,30 @@ export class EdgeStylingHelper {
|
|
|
130
151
|
const sourcePortDataType = sourcePortId
|
|
131
152
|
? this.getPortDataType(sourceNode, sourcePortId, 'output')
|
|
132
153
|
: null;
|
|
133
|
-
// Determine edge category
|
|
134
|
-
const edgeCategory = this.
|
|
154
|
+
// Determine edge category (loopback takes precedence)
|
|
155
|
+
const edgeCategory = this.getEdgeCategoryWithLoopback(edge, sourcePortDataType);
|
|
135
156
|
// Edge color constants (matching CSS tokens in base.css)
|
|
136
157
|
const EDGE_COLORS = {
|
|
137
158
|
trigger: '#111827', // --color-ref-gray-900
|
|
138
159
|
tool: '#f59e0b', // --color-ref-amber-500
|
|
160
|
+
loopback: '#6b7280', // --color-ref-gray-500
|
|
139
161
|
data: '#9ca3af' // --color-ref-gray-400
|
|
140
162
|
};
|
|
141
163
|
// Apply styling based on edge category
|
|
142
164
|
// CSS classes handle styling via tokens; inline styles are fallback
|
|
143
165
|
switch (edgeCategory) {
|
|
166
|
+
case 'loopback':
|
|
167
|
+
// Loopback edges: dashed gray line for loop iteration
|
|
168
|
+
edge.style =
|
|
169
|
+
'stroke: var(--flowdrop-edge-loopback-color); stroke-dasharray: 5 5; stroke-width: var(--flowdrop-edge-loopback-width);';
|
|
170
|
+
edge.class = 'flowdrop--edge--loopback';
|
|
171
|
+
edge.markerEnd = {
|
|
172
|
+
type: MarkerType.ArrowClosed,
|
|
173
|
+
width: 14,
|
|
174
|
+
height: 14,
|
|
175
|
+
color: EDGE_COLORS.loopback
|
|
176
|
+
};
|
|
177
|
+
break;
|
|
144
178
|
case 'trigger':
|
|
145
179
|
// Trigger edges: solid dark line for control flow
|
|
146
180
|
edge.style =
|
|
@@ -183,7 +217,7 @@ export class EdgeStylingHelper {
|
|
|
183
217
|
metadata: {
|
|
184
218
|
...(edge.data?.metadata || {}),
|
|
185
219
|
edgeType: edgeCategory,
|
|
186
|
-
sourcePortDataType: sourcePortDataType
|
|
220
|
+
sourcePortDataType: sourcePortDataType ?? undefined
|
|
187
221
|
},
|
|
188
222
|
targetNodeType: targetNode.type,
|
|
189
223
|
targetCategory: targetNode.data.metadata.category
|
|
@@ -484,9 +518,26 @@ export class WorkflowOperationsHelper {
|
|
|
484
518
|
URL.revokeObjectURL(url);
|
|
485
519
|
}
|
|
486
520
|
/**
|
|
487
|
-
* Check if workflow has cycles
|
|
521
|
+
* Check if workflow has invalid cycles (excludes valid loopback cycles)
|
|
522
|
+
* Valid loopback cycles are used for ForEach node iteration and should not
|
|
523
|
+
* trigger a warning.
|
|
524
|
+
*
|
|
525
|
+
* @param nodes - Array of workflow nodes
|
|
526
|
+
* @param edges - Array of workflow edges
|
|
527
|
+
* @returns True if there are invalid (non-loopback) cycles
|
|
488
528
|
*/
|
|
489
529
|
static checkWorkflowCycles(nodes, edges) {
|
|
530
|
+
return hasInvalidCycles(nodes, edges);
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Check if workflow has any cycles (including valid loopback cycles)
|
|
534
|
+
* Use this when you need to detect ALL cycles regardless of type.
|
|
535
|
+
*
|
|
536
|
+
* @param nodes - Array of workflow nodes
|
|
537
|
+
* @param edges - Array of workflow edges
|
|
538
|
+
* @returns True if any cycle exists
|
|
539
|
+
*/
|
|
540
|
+
static checkWorkflowHasAnyCycles(nodes, edges) {
|
|
490
541
|
return hasCycles(nodes, edges);
|
|
491
542
|
}
|
|
492
543
|
}
|
package/dist/styles/base.css
CHANGED
|
@@ -1157,6 +1157,15 @@
|
|
|
1157
1157
|
--flowdrop-edge-data-color-hover: var(--color-ref-gray-500);
|
|
1158
1158
|
--flowdrop-edge-data-color-selected: var(--color-ref-violet-600);
|
|
1159
1159
|
|
|
1160
|
+
/* Loopback edge styling tokens */
|
|
1161
|
+
--flowdrop-edge-loopback-color: var(--color-ref-gray-500);
|
|
1162
|
+
--flowdrop-edge-loopback-color-hover: var(--color-ref-gray-600);
|
|
1163
|
+
--flowdrop-edge-loopback-color-selected: var(--color-ref-violet-600);
|
|
1164
|
+
--flowdrop-edge-loopback-width: 1.5px;
|
|
1165
|
+
--flowdrop-edge-loopback-width-hover: 2.5px;
|
|
1166
|
+
--flowdrop-edge-loopback-dasharray: 5 5;
|
|
1167
|
+
--flowdrop-edge-loopback-opacity: 0.85;
|
|
1168
|
+
|
|
1160
1169
|
/* Tool node theming tokens */
|
|
1161
1170
|
--flowdrop-tool-node-color: var(--color-ref-amber-500);
|
|
1162
1171
|
--flowdrop-tool-node-color-light: var(--color-ref-amber-50);
|
package/dist/types/index.d.ts
CHANGED
|
@@ -656,13 +656,14 @@ export interface WorkflowNode extends Node {
|
|
|
656
656
|
};
|
|
657
657
|
}
|
|
658
658
|
/**
|
|
659
|
-
* Edge category types based on source port data type
|
|
659
|
+
* Edge category types based on source port data type or target handle
|
|
660
660
|
* Used for visual styling of edges on the canvas
|
|
661
661
|
* - trigger: For control flow connections (dataType: "trigger")
|
|
662
662
|
* - tool: Dashed amber line for tool connections (dataType: "tool")
|
|
663
|
+
* - loopback: Dashed gray line for loop iteration (targets loop_back port)
|
|
663
664
|
* - data: Normal gray line for all other data connections
|
|
664
665
|
*/
|
|
665
|
-
export type EdgeCategory = 'trigger' | 'tool' | 'data';
|
|
666
|
+
export type EdgeCategory = 'trigger' | 'tool' | 'loopback' | 'data';
|
|
666
667
|
/**
|
|
667
668
|
* Extended edge type for workflows
|
|
668
669
|
*/
|
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
* Connection validation utilities for FlowDrop
|
|
3
3
|
*/
|
|
4
4
|
import type { NodeMetadata, NodePort, NodeDataType, WorkflowNode, WorkflowEdge, PortConfig, PortDataTypeConfig } from '../types/index.js';
|
|
5
|
+
/**
|
|
6
|
+
* Determines if an edge is a loopback edge.
|
|
7
|
+
* Loopback edges target the special `loop_back` input port on ForEach nodes.
|
|
8
|
+
* These edges are used to trigger the next iteration in a loop construct.
|
|
9
|
+
*
|
|
10
|
+
* @param edge - The edge to check
|
|
11
|
+
* @returns True if the edge is a loopback edge
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* const edge = { targetHandle: "foreach.1-input-loop_back", ... };
|
|
16
|
+
* const isLoop = isLoopbackEdge(edge); // true
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare function isLoopbackEdge(edge: WorkflowEdge): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Checks if a cycle consists entirely of loopback edges.
|
|
22
|
+
* A valid loopback cycle only contains edges that target loop_back ports.
|
|
23
|
+
*
|
|
24
|
+
* @param cycleEdges - Array of edges that form a cycle
|
|
25
|
+
* @returns True if all edges in the cycle are loopback edges
|
|
26
|
+
*/
|
|
27
|
+
export declare function isValidLoopbackCycle(cycleEdges: WorkflowEdge[]): boolean;
|
|
5
28
|
/**
|
|
6
29
|
* Configurable port compatibility checker
|
|
7
30
|
*/
|
|
@@ -71,8 +94,31 @@ export declare function getConnectionSuggestions(nodeId: string, nodes: Workflow
|
|
|
71
94
|
}>;
|
|
72
95
|
/**
|
|
73
96
|
* Check if a workflow has any cycles (prevent infinite loops)
|
|
97
|
+
* Note: This function detects ALL cycles, including valid loopback cycles.
|
|
98
|
+
* Use `hasInvalidCycles` to check only for cycles that could cause infinite execution.
|
|
99
|
+
*
|
|
100
|
+
* @param nodes - Array of workflow nodes
|
|
101
|
+
* @param edges - Array of workflow edges
|
|
102
|
+
* @returns True if any cycle exists in the workflow
|
|
74
103
|
*/
|
|
75
104
|
export declare function hasCycles(nodes: WorkflowNode[], edges: WorkflowEdge[]): boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Check if a workflow has any invalid cycles (non-loopback cycles).
|
|
107
|
+
* This excludes valid loopback cycles used for ForEach iteration.
|
|
108
|
+
* Only cycles that could cause infinite execution are detected.
|
|
109
|
+
*
|
|
110
|
+
* @param nodes - Array of workflow nodes
|
|
111
|
+
* @param edges - Array of workflow edges
|
|
112
|
+
* @returns True if any invalid (non-loopback) cycle exists
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```typescript
|
|
116
|
+
* // A cycle through a loopback edge is valid (returns false)
|
|
117
|
+
* // A cycle through regular data edges is invalid (returns true)
|
|
118
|
+
* const hasInvalid = hasInvalidCycles(nodes, edges);
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export declare function hasInvalidCycles(nodes: WorkflowNode[], edges: WorkflowEdge[]): boolean;
|
|
76
122
|
/**
|
|
77
123
|
* Get the execution order for a workflow (topological sort)
|
|
78
124
|
*/
|
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Connection validation utilities for FlowDrop
|
|
3
3
|
*/
|
|
4
|
+
/**
|
|
5
|
+
* Loopback port name constant
|
|
6
|
+
* This is the standard input port name used for loop iteration triggers
|
|
7
|
+
*/
|
|
8
|
+
const LOOPBACK_PORT_NAME = "loop_back";
|
|
9
|
+
/**
|
|
10
|
+
* Determines if an edge is a loopback edge.
|
|
11
|
+
* Loopback edges target the special `loop_back` input port on ForEach nodes.
|
|
12
|
+
* These edges are used to trigger the next iteration in a loop construct.
|
|
13
|
+
*
|
|
14
|
+
* @param edge - The edge to check
|
|
15
|
+
* @returns True if the edge is a loopback edge
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* const edge = { targetHandle: "foreach.1-input-loop_back", ... };
|
|
20
|
+
* const isLoop = isLoopbackEdge(edge); // true
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function isLoopbackEdge(edge) {
|
|
24
|
+
const targetHandle = edge.targetHandle ?? "";
|
|
25
|
+
return targetHandle.includes(`-input-${LOOPBACK_PORT_NAME}`);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Checks if a cycle consists entirely of loopback edges.
|
|
29
|
+
* A valid loopback cycle only contains edges that target loop_back ports.
|
|
30
|
+
*
|
|
31
|
+
* @param cycleEdges - Array of edges that form a cycle
|
|
32
|
+
* @returns True if all edges in the cycle are loopback edges
|
|
33
|
+
*/
|
|
34
|
+
export function isValidLoopbackCycle(cycleEdges) {
|
|
35
|
+
return cycleEdges.every((edge) => isLoopbackEdge(edge));
|
|
36
|
+
}
|
|
4
37
|
/**
|
|
5
38
|
* Configurable port compatibility checker
|
|
6
39
|
*/
|
|
@@ -225,6 +258,12 @@ export function getConnectionSuggestions(nodeId, nodes, nodeTypes) {
|
|
|
225
258
|
}
|
|
226
259
|
/**
|
|
227
260
|
* Check if a workflow has any cycles (prevent infinite loops)
|
|
261
|
+
* Note: This function detects ALL cycles, including valid loopback cycles.
|
|
262
|
+
* Use `hasInvalidCycles` to check only for cycles that could cause infinite execution.
|
|
263
|
+
*
|
|
264
|
+
* @param nodes - Array of workflow nodes
|
|
265
|
+
* @param edges - Array of workflow edges
|
|
266
|
+
* @returns True if any cycle exists in the workflow
|
|
228
267
|
*/
|
|
229
268
|
export function hasCycles(nodes, edges) {
|
|
230
269
|
const visited = new Set();
|
|
@@ -254,6 +293,58 @@ export function hasCycles(nodes, edges) {
|
|
|
254
293
|
}
|
|
255
294
|
return false;
|
|
256
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* Check if a workflow has any invalid cycles (non-loopback cycles).
|
|
298
|
+
* This excludes valid loopback cycles used for ForEach iteration.
|
|
299
|
+
* Only cycles that could cause infinite execution are detected.
|
|
300
|
+
*
|
|
301
|
+
* @param nodes - Array of workflow nodes
|
|
302
|
+
* @param edges - Array of workflow edges
|
|
303
|
+
* @returns True if any invalid (non-loopback) cycle exists
|
|
304
|
+
*
|
|
305
|
+
* @example
|
|
306
|
+
* ```typescript
|
|
307
|
+
* // A cycle through a loopback edge is valid (returns false)
|
|
308
|
+
* // A cycle through regular data edges is invalid (returns true)
|
|
309
|
+
* const hasInvalid = hasInvalidCycles(nodes, edges);
|
|
310
|
+
* ```
|
|
311
|
+
*/
|
|
312
|
+
export function hasInvalidCycles(nodes, edges) {
|
|
313
|
+
// Filter out loopback edges - these create valid cycles for loop iteration
|
|
314
|
+
const nonLoopbackEdges = edges.filter((edge) => !isLoopbackEdge(edge));
|
|
315
|
+
// Check for cycles using only non-loopback edges
|
|
316
|
+
const visited = new Set();
|
|
317
|
+
const recursionStack = new Set();
|
|
318
|
+
/**
|
|
319
|
+
* DFS utility to detect cycles in the graph
|
|
320
|
+
* @param nodeId - Current node being visited
|
|
321
|
+
* @returns True if a cycle is found from this node
|
|
322
|
+
*/
|
|
323
|
+
function hasCycleUtil(nodeId) {
|
|
324
|
+
if (recursionStack.has(nodeId))
|
|
325
|
+
return true;
|
|
326
|
+
if (visited.has(nodeId))
|
|
327
|
+
return false;
|
|
328
|
+
visited.add(nodeId);
|
|
329
|
+
recursionStack.add(nodeId);
|
|
330
|
+
// Get all outgoing non-loopback edges from this node
|
|
331
|
+
const outgoingEdges = nonLoopbackEdges.filter((e) => e.source === nodeId);
|
|
332
|
+
for (const edge of outgoingEdges) {
|
|
333
|
+
if (hasCycleUtil(edge.target))
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
recursionStack.delete(nodeId);
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
// Check each node for cycles
|
|
340
|
+
for (const node of nodes) {
|
|
341
|
+
if (!visited.has(node.id)) {
|
|
342
|
+
if (hasCycleUtil(node.id))
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
257
348
|
/**
|
|
258
349
|
* Get the execution order for a workflow (topological sort)
|
|
259
350
|
*/
|