mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +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-unsafe-return */
|
||||||
/* eslint-disable @typescript-eslint/no-for-in-array */
|
/* eslint-disable @typescript-eslint/no-for-in-array */
|
||||||
import {
|
import {
|
||||||
getNodeByName,
|
|
||||||
getConnectedNodes,
|
getConnectedNodes,
|
||||||
getChildNodes,
|
getChildNodes,
|
||||||
getParentNodes,
|
getParentNodes,
|
||||||
@@ -304,7 +303,7 @@ export class Workflow {
|
|||||||
* @param {string} nodeName Name of the node to return
|
* @param {string} nodeName Name of the node to return
|
||||||
*/
|
*/
|
||||||
getNode(nodeName: string): INode | null {
|
getNode(nodeName: string): INode | null {
|
||||||
return getNodeByName(this.nodes, nodeName);
|
return this.nodes[nodeName] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -737,48 +736,45 @@ export class Workflow {
|
|||||||
* @param {string} nodeName The node to check how it is connected with parent node
|
* @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} parentNodeName The parent node to get the output index of
|
||||||
* @param {string} [type='main']
|
* @param {string} [type='main']
|
||||||
* @param {*} [depth=-1]
|
|
||||||
*/
|
*/
|
||||||
getNodeConnectionIndexes(
|
getNodeConnectionIndexes(
|
||||||
nodeName: string,
|
nodeName: string,
|
||||||
parentNodeName: string,
|
parentNodeName: string,
|
||||||
type: NodeConnectionType = NodeConnectionTypes.Main,
|
type: NodeConnectionType = NodeConnectionTypes.Main,
|
||||||
depth = -1,
|
|
||||||
checkedNodes?: string[],
|
|
||||||
): INodeConnection | undefined {
|
): INodeConnection | undefined {
|
||||||
const node = this.getNode(parentNodeName);
|
// This method has been optimized for performance. If you make any changes to it,
|
||||||
if (node === null) {
|
// make sure the performance is not degraded.
|
||||||
|
const parentNode = this.getNode(parentNodeName);
|
||||||
|
if (parentNode === null) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
depth = depth === -1 ? -1 : depth;
|
const visitedNodes = new Set<string>();
|
||||||
const newDepth = depth === -1 ? depth : depth - 1;
|
const queue: string[] = [nodeName];
|
||||||
if (depth === 0) {
|
|
||||||
// Reached max depth
|
// Cache the connections by destination node to avoid reference lookups
|
||||||
return undefined;
|
const connectionsByDest = this.connectionsByDestinationNode;
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const currentNodeName = queue.shift()!;
|
||||||
|
|
||||||
|
if (visitedNodes.has(currentNodeName)) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.connectionsByDestinationNode.hasOwnProperty(nodeName)) {
|
visitedNodes.add(currentNodeName);
|
||||||
// Node does not have incoming connections
|
|
||||||
return undefined;
|
const typeConnections = connectionsByDest[currentNodeName]?.[type];
|
||||||
|
if (!typeConnections) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.connectionsByDestinationNode[nodeName].hasOwnProperty(type)) {
|
for (
|
||||||
// Node does not have incoming connections of given type
|
let typedConnectionIdx = 0;
|
||||||
return undefined;
|
typedConnectionIdx < typeConnections.length;
|
||||||
}
|
typedConnectionIdx++
|
||||||
|
) {
|
||||||
checkedNodes = checkedNodes || [];
|
const connectionsByIndex = typeConnections[typedConnectionIdx];
|
||||||
|
|
||||||
if (checkedNodes.includes(nodeName)) {
|
|
||||||
// Node got checked already before
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
checkedNodes.push(nodeName);
|
|
||||||
|
|
||||||
let outputIndex: INodeConnection | undefined;
|
|
||||||
for (const connectionsByIndex of this.connectionsByDestinationNode[nodeName][type]) {
|
|
||||||
if (!connectionsByIndex) {
|
if (!connectionsByIndex) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -789,6 +785,7 @@ export class Workflow {
|
|||||||
destinationIndex++
|
destinationIndex++
|
||||||
) {
|
) {
|
||||||
const connection = connectionsByIndex[destinationIndex];
|
const connection = connectionsByIndex[destinationIndex];
|
||||||
|
|
||||||
if (parentNodeName === connection.node) {
|
if (parentNodeName === connection.node) {
|
||||||
return {
|
return {
|
||||||
sourceIndex: connection.index,
|
sourceIndex: connection.index,
|
||||||
@@ -796,21 +793,9 @@ export class Workflow {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (checkedNodes.includes(connection.node)) {
|
if (!visitedNodes.has(connection.node)) {
|
||||||
// Node got checked already before so continue with the next one
|
queue.push(connection.node);
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
outputIndex = this.getNodeConnectionIndexes(
|
|
||||||
connection.node,
|
|
||||||
parentNodeName,
|
|
||||||
type,
|
|
||||||
newDepth,
|
|
||||||
checkedNodes,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (outputIndex !== undefined) {
|
|
||||||
return outputIndex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
const result = SIMPLE_WORKFLOW.getNodeConnectionIndexes(
|
||||||
'Set',
|
|
||||||
'Start',
|
'Start',
|
||||||
|
'Set',
|
||||||
NodeConnectionTypes.Main,
|
NodeConnectionTypes.Main,
|
||||||
0,
|
|
||||||
);
|
);
|
||||||
expect(result).toBeUndefined();
|
expect(result).toBeUndefined();
|
||||||
});
|
});
|
||||||
@@ -2408,6 +2407,341 @@ describe('Workflow', () => {
|
|||||||
destinationIndex: 0,
|
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', () => {
|
describe('getStartNode', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user