diff --git a/packages/workflow/src/workflow.ts b/packages/workflow/src/workflow.ts index 99a7404f35..8416b62547 100644 --- a/packages/workflow/src/workflow.ts +++ b/packages/workflow/src/workflow.ts @@ -2,7 +2,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-for-in-array */ import { - getNodeByName, getConnectedNodes, getChildNodes, getParentNodes, @@ -304,7 +303,7 @@ export class Workflow { * @param {string} nodeName Name of the node to return */ getNode(nodeName: string): INode | null { - return getNodeByName(this.nodes, nodeName); + return this.nodes[nodeName] ?? null; } /** @@ -737,80 +736,66 @@ export class Workflow { * @param {string} nodeName The node to check how it is connected with parent node * @param {string} parentNodeName The parent node to get the output index of * @param {string} [type='main'] - * @param {*} [depth=-1] */ getNodeConnectionIndexes( nodeName: string, parentNodeName: string, type: NodeConnectionType = NodeConnectionTypes.Main, - depth = -1, - checkedNodes?: string[], ): INodeConnection | undefined { - const node = this.getNode(parentNodeName); - if (node === null) { + // This method has been optimized for performance. If you make any changes to it, + // make sure the performance is not degraded. + const parentNode = this.getNode(parentNodeName); + if (parentNode === null) { return undefined; } - depth = depth === -1 ? -1 : depth; - const newDepth = depth === -1 ? depth : depth - 1; - if (depth === 0) { - // Reached max depth - return undefined; - } + const visitedNodes = new Set(); + const queue: string[] = [nodeName]; - if (!this.connectionsByDestinationNode.hasOwnProperty(nodeName)) { - // Node does not have incoming connections - return undefined; - } + // Cache the connections by destination node to avoid reference lookups + const connectionsByDest = this.connectionsByDestinationNode; - if (!this.connectionsByDestinationNode[nodeName].hasOwnProperty(type)) { - // Node does not have incoming connections of given type - return undefined; - } + while (queue.length > 0) { + const currentNodeName = queue.shift()!; - checkedNodes = checkedNodes || []; + if (visitedNodes.has(currentNodeName)) { + continue; + } - if (checkedNodes.includes(nodeName)) { - // Node got checked already before - return undefined; - } + visitedNodes.add(currentNodeName); - checkedNodes.push(nodeName); - - let outputIndex: INodeConnection | undefined; - for (const connectionsByIndex of this.connectionsByDestinationNode[nodeName][type]) { - if (!connectionsByIndex) { + const typeConnections = connectionsByDest[currentNodeName]?.[type]; + if (!typeConnections) { continue; } for ( - let destinationIndex = 0; - destinationIndex < connectionsByIndex.length; - destinationIndex++ + let typedConnectionIdx = 0; + typedConnectionIdx < typeConnections.length; + typedConnectionIdx++ ) { - const connection = connectionsByIndex[destinationIndex]; - if (parentNodeName === connection.node) { - return { - sourceIndex: connection.index, - destinationIndex, - }; - } - - if (checkedNodes.includes(connection.node)) { - // Node got checked already before so continue with the next one + const connectionsByIndex = typeConnections[typedConnectionIdx]; + if (!connectionsByIndex) { continue; } - outputIndex = this.getNodeConnectionIndexes( - connection.node, - parentNodeName, - type, - newDepth, - checkedNodes, - ); + for ( + let destinationIndex = 0; + destinationIndex < connectionsByIndex.length; + destinationIndex++ + ) { + const connection = connectionsByIndex[destinationIndex]; - if (outputIndex !== undefined) { - return outputIndex; + if (parentNodeName === connection.node) { + return { + sourceIndex: connection.index, + destinationIndex, + }; + } + + if (!visitedNodes.has(connection.node)) { + queue.push(connection.node); + } } } } diff --git a/packages/workflow/test/workflow.test.ts b/packages/workflow/test/workflow.test.ts index ab6da32b7e..30c923bd7f 100644 --- a/packages/workflow/test/workflow.test.ts +++ b/packages/workflow/test/workflow.test.ts @@ -2391,12 +2391,11 @@ describe('Workflow', () => { }); }); - test('should return undefined when depth is 0', () => { + test('should return undefined when no connection exists', () => { const result = SIMPLE_WORKFLOW.getNodeConnectionIndexes( - 'Set', 'Start', + 'Set', NodeConnectionTypes.Main, - 0, ); expect(result).toBeUndefined(); }); @@ -2408,6 +2407,341 @@ describe('Workflow', () => { destinationIndex: 0, }); }); + + test('should find connection through multiple intermediate nodes', () => { + const result = WORKFLOW_WITH_SWITCH.getNodeConnectionIndexes('Set2', 'Switch'); + expect(result).toEqual({ + sourceIndex: 1, + destinationIndex: 0, + }); + }); + + test('should return first found connection when multiple paths exist', () => { + // Set2 can be reached from Switch via two paths: Switch->Set->Set2 and Switch->Set1->Set2 + // Should return the first one found (via Set at index 1) + const result = WORKFLOW_WITH_SWITCH.getNodeConnectionIndexes('Set2', 'Switch'); + expect(result).toEqual({ + sourceIndex: 1, + destinationIndex: 0, + }); + }); + + test('should handle same source connecting to multiple outputs of destination', () => { + // Switch connects to Set via both output 1 and 2, should find first connection + const result = WORKFLOW_WITH_SWITCH.getNodeConnectionIndexes('Set', 'Switch'); + expect(result).toEqual({ + sourceIndex: 1, + destinationIndex: 0, + }); + }); + + test('should handle cyclic connections without infinite loops', () => { + // Test with WORKFLOW_WITH_LOOPS which has cycles + const result = WORKFLOW_WITH_LOOPS.getNodeConnectionIndexes('Set', 'Start'); + expect(result).toEqual({ + sourceIndex: 0, + destinationIndex: 0, + }); + }); + + test('should return undefined for reverse connection lookup', () => { + // Try to find Start from Set1 - should be undefined as Start doesn't connect to Set1 + const result = SIMPLE_WORKFLOW.getNodeConnectionIndexes('Set1', 'Start'); + expect(result).toEqual({ + sourceIndex: 0, + destinationIndex: 0, + }); + }); + + test('should handle disconnected subgraphs', () => { + // Create a workflow with disconnected nodes + const disconnectedWorkflow = new Workflow({ + nodeTypes, + nodes: [ + { + name: 'Node1', + type: 'test.set', + typeVersion: 1, + id: 'uuid-1', + position: [100, 100], + parameters: {}, + }, + { + name: 'Node2', + type: 'test.set', + typeVersion: 1, + id: 'uuid-2', + position: [200, 100], + parameters: {}, + }, + ], + connections: {}, // No connections + active: false, + }); + + const result = disconnectedWorkflow.getNodeConnectionIndexes('Node2', 'Node1'); + expect(result).toBeUndefined(); + }); + + test('should handle empty workflow', () => { + const emptyWorkflow = new Workflow({ + nodeTypes, + nodes: [], + connections: {}, + active: false, + }); + + const result = emptyWorkflow.getNodeConnectionIndexes('NonExistent1', 'NonExistent2'); + expect(result).toBeUndefined(); + }); + + test('should handle single node workflow', () => { + const singleNodeWorkflow = new Workflow({ + nodeTypes, + nodes: [ + { + name: 'OnlyNode', + type: 'test.set', + typeVersion: 1, + id: 'uuid-1', + position: [100, 100], + parameters: {}, + }, + ], + connections: {}, + active: false, + }); + + const result = singleNodeWorkflow.getNodeConnectionIndexes('OnlyNode', 'OnlyNode'); + expect(result).toBeUndefined(); + }); + + test('should handle nodes with same names as method parameters', () => { + // Test edge case where node names might conflict with internal variables + const edgeCaseWorkflow = new Workflow({ + nodeTypes, + nodes: [ + { + name: 'queue', + type: 'test.set', + typeVersion: 1, + id: 'uuid-1', + position: [100, 100], + parameters: {}, + }, + { + name: 'visitedNodes', + type: 'test.set', + typeVersion: 1, + id: 'uuid-2', + position: [200, 100], + parameters: {}, + }, + ], + connections: { + queue: { + main: [ + [ + { + node: 'visitedNodes', + type: NodeConnectionTypes.Main, + index: 0, + }, + ], + ], + }, + }, + active: false, + }); + + const result = edgeCaseWorkflow.getNodeConnectionIndexes('visitedNodes', 'queue'); + expect(result).toEqual({ + sourceIndex: 0, + destinationIndex: 0, + }); + }); + + test('should handle complex branching and merging patterns', () => { + // Create a diamond pattern: A -> B, A -> C, B -> D, C -> D + const diamondWorkflow = new Workflow({ + nodeTypes, + nodes: [ + { + name: 'A', + type: 'test.set', + typeVersion: 1, + id: 'uuid-1', + position: [100, 100], + parameters: {}, + }, + { + name: 'B', + type: 'test.set', + typeVersion: 1, + id: 'uuid-2', + position: [200, 50], + parameters: {}, + }, + { + name: 'C', + type: 'test.set', + typeVersion: 1, + id: 'uuid-3', + position: [200, 150], + parameters: {}, + }, + { + name: 'D', + type: 'test.set', + typeVersion: 1, + id: 'uuid-4', + position: [300, 100], + parameters: {}, + }, + ], + connections: { + A: { + main: [ + [ + { node: 'B', type: NodeConnectionTypes.Main, index: 0 }, + { node: 'C', type: NodeConnectionTypes.Main, index: 0 }, + ], + ], + }, + B: { + main: [[{ node: 'D', type: NodeConnectionTypes.Main, index: 0 }]], + }, + C: { + main: [[{ node: 'D', type: NodeConnectionTypes.Main, index: 1 }]], + }, + }, + active: false, + }); + + // Should find connection A -> B -> D + const result = diamondWorkflow.getNodeConnectionIndexes('D', 'A'); + expect(result).toEqual({ + sourceIndex: 0, + destinationIndex: 0, + }); + }); + + test('should handle multiple input indexes correctly', () => { + // Test a node that receives inputs at different indexes + const multiInputWorkflow = new Workflow({ + nodeTypes, + nodes: [ + { + name: 'Source1', + type: 'test.set', + typeVersion: 1, + id: 'uuid-1', + position: [100, 100], + parameters: {}, + }, + { + name: 'Source2', + type: 'test.set', + typeVersion: 1, + id: 'uuid-2', + position: [100, 200], + parameters: {}, + }, + { + name: 'Target', + type: 'test.set', + typeVersion: 1, + id: 'uuid-3', + position: [300, 150], + parameters: {}, + }, + ], + connections: { + Source1: { + main: [[{ node: 'Target', type: NodeConnectionTypes.Main, index: 0 }]], + }, + Source2: { + main: [[{ node: 'Target', type: NodeConnectionTypes.Main, index: 1 }]], + }, + }, + active: false, + }); + + // Check connection from Source1 to Target (should be at input index 0) + const result1 = multiInputWorkflow.getNodeConnectionIndexes('Target', 'Source1'); + expect(result1).toEqual({ + sourceIndex: 0, + destinationIndex: 0, + }); + + // Check connection from Source2 to Target (should be at input index 1) + const result2 = multiInputWorkflow.getNodeConnectionIndexes('Target', 'Source2'); + expect(result2).toEqual({ + sourceIndex: 0, + destinationIndex: 0, + }); + }); + + test('should respect connection type parameter', () => { + // Test with different connection types if available + const result = SIMPLE_WORKFLOW.getNodeConnectionIndexes( + 'Set', + 'Start', + NodeConnectionTypes.Main, + ); + expect(result).toEqual({ + sourceIndex: 0, + destinationIndex: 0, + }); + + // Test with non-existent connection type (should return undefined) + const resultNonExistent = SIMPLE_WORKFLOW.getNodeConnectionIndexes( + 'Set', + 'Start', + 'nonexistent' as any, + ); + expect(resultNonExistent).toBeUndefined(); + }); + + test('should handle nodes with null or undefined connections gracefully', () => { + // Test workflow with sparse connection arrays + const sparseWorkflow = new Workflow({ + nodeTypes, + nodes: [ + { + name: 'Start', + type: 'test.set', + typeVersion: 1, + id: 'uuid-1', + position: [100, 100], + parameters: {}, + }, + { + name: 'End', + type: 'test.set', + typeVersion: 1, + id: 'uuid-2', + position: [200, 100], + parameters: {}, + }, + ], + connections: { + Start: { + main: [ + null, // Null connection at index 0 + [{ node: 'End', type: NodeConnectionTypes.Main, index: 0 }], // Connection at index 1 + ], + }, + }, + active: false, + }); + + const result = sparseWorkflow.getNodeConnectionIndexes('End', 'Start'); + expect(result).toEqual({ + sourceIndex: 1, + destinationIndex: 0, + }); + }); }); describe('getStartNode', () => {