mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
perf(core): Optimize workflow getNodeConnectionIndexes (#18542)
This commit is contained in:
@@ -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<string>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user