fix: Open executions with large number of execution items without crashing tab (#8423)

Co-authored-by: Tomi Turtiainen <10324676+tomi@users.noreply.github.com>
This commit is contained in:
Mutasem Aldmour 2024-01-29 09:33:00 +01:00 committed by GitHub
parent 16439dec02
commit 56da2e4352
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 434 additions and 3 deletions

View file

@ -0,0 +1,400 @@
import type { IExecutionResponse, TargetItem } from '@/Interface';
import {
getPairedItemId,
getSourceItems,
getPairedItemsMapping,
MAX_ITEM_COUNT_FOR_PAIRING,
} from '../pairedItemUtils';
const MOCK_EXECUTION: Partial<IExecutionResponse> = {
data: {
startData: {},
resultData: {
runData: {
'When clicking "Test workflow"': [
{
startTime: 1706027170005,
executionTime: 0,
source: [],
executionStatus: 'success',
data: { main: [[{ json: {}, pairedItem: { item: 0 } }]] },
},
],
DebugHelper: [
{
startTime: 1706027170005,
executionTime: 1,
source: [{ previousNode: 'When clicking "Test workflow"' }],
executionStatus: 'success',
data: {
main: [
[
{
json: {
uid: '54563b92-e4d9-425d-826d-9fe471196a28',
email: 'Jon_Ebert@yahoo.com',
firstname: 'April',
lastname: 'Aufderhar',
password: '+xziPGNy',
},
pairedItem: { item: 0 },
},
{
json: {
uid: '3d3ee69e-f013-478c-8f5b-7723f508c02b',
email: 'Jeffery_Wehner@yahoo.com',
firstname: 'Amelia',
lastname: 'Mante',
password: '2DJR~2owf',
},
pairedItem: { item: 0 },
},
],
],
},
},
],
If: [
{
startTime: 1706027170006,
executionTime: 1,
source: [{ previousNode: 'DebugHelper' }],
executionStatus: 'success',
data: {
main: [
[
{
json: {
uid: '54563b92-e4d9-425d-826d-9fe471196a28',
email: 'Jon_Ebert@yahoo.com',
firstname: 'April',
lastname: 'Aufderhar',
password: '+xziPGNy',
},
pairedItem: { item: 0 },
},
],
[
{
json: {
uid: '3d3ee69e-f013-478c-8f5b-7723f508c02b',
email: 'Jeffery_Wehner@yahoo.com',
firstname: 'Amelia',
lastname: 'Mante',
password: '2DJR~2owf',
},
pairedItem: { item: 1 },
},
],
],
},
},
],
'Edit Fields': [
{
startTime: 1706027170008,
executionTime: 0,
source: [{ previousNode: 'If', previousNodeOutput: 1 }],
executionStatus: 'success',
data: {
main: [
[
{
json: {
uid: '3d3ee69e-f013-478c-8f5b-7723f508c02b',
email: 'Jeffery_Wehner@yahoo.com',
firstname: 'Amelia',
lastname: 'Mante',
password: '2DJR~2owf',
},
pairedItem: { item: 0 },
},
],
],
},
},
{
startTime: 1706027170009,
executionTime: 0,
source: [{ previousNode: 'If' }],
executionStatus: 'success',
data: {
main: [
[
{
json: {
uid: '54563b92-e4d9-425d-826d-9fe471196a28',
email: 'Jon_Ebert@yahoo.com',
firstname: 'April',
lastname: 'Aufderhar',
password: '+xziPGNy',
},
pairedItem: { item: 0 },
},
],
],
},
},
],
'Edit Fields1': [
{
startTime: 1706027170008,
executionTime: 0,
source: [{ previousNode: 'Edit Fields' }],
executionStatus: 'success',
data: {
main: [
[
{
json: {
uid: '3d3ee69e-f013-478c-8f5b-7723f508c02b',
email: 'Jeffery_Wehner@yahoo.com',
firstname: 'Amelia',
lastname: 'Mante',
password: '2DJR~2owf',
},
pairedItem: { item: 0 },
},
],
],
},
},
{
startTime: 1706027170010,
executionTime: 0,
source: [{ previousNode: 'Edit Fields', previousNodeRun: 1 }],
executionStatus: 'success',
data: {
main: [
[
{
json: {
uid: '54563b92-e4d9-425d-826d-9fe471196a28',
email: 'Jon_Ebert@yahoo.com',
firstname: 'April',
lastname: 'Aufderhar',
password: '+xziPGNy',
},
pairedItem: { item: 0 },
},
],
],
},
},
],
},
pinData: {},
lastNodeExecuted: 'Edit Fields1',
},
executionData: {
contextData: {},
nodeExecutionStack: [],
metadata: {},
waitingExecution: {},
waitingExecutionSource: {},
},
},
mode: 'manual',
startedAt: new Date('2024-01-23T16:26:10.003Z'),
stoppedAt: new Date('2024-01-23T16:26:10.011Z'),
status: 'success',
finished: true,
};
describe('pairedItemUtils', () => {
describe('getPairedItemId', () => {
it('should return the correct paired item ID', () => {
const node = 'myNode';
const run = 1;
const output = 2;
const item = 3;
const expectedPairedItemId = 'myNode_r1_o2_i3';
const pairedItemId = getPairedItemId(node, run, output, item);
expect(pairedItemId).toEqual(expectedPairedItemId);
});
});
describe('getSourceItems', () => {
it('should return the source items for the given target item', () => {
const target: TargetItem = { nodeName: 'If', runIndex: 0, outputIndex: 0, itemIndex: 0 };
const expected: TargetItem[] = [
{ nodeName: 'DebugHelper', runIndex: 0, itemIndex: 0, outputIndex: 0 },
];
const actual = getSourceItems(MOCK_EXECUTION, target);
expect(actual).toEqual(expected);
});
it('should return the source items for the given target item across outputs', () => {
const target: TargetItem = { nodeName: 'If', runIndex: 0, outputIndex: 1, itemIndex: 0 };
const expected: TargetItem[] = [
{ nodeName: 'DebugHelper', runIndex: 0, itemIndex: 1, outputIndex: 0 },
];
const actual = getSourceItems(MOCK_EXECUTION, target);
expect(actual).toEqual(expected);
});
it('should return the source items for the given target item across runs', () => {
const target: TargetItem = {
nodeName: 'Edit Fields1',
runIndex: 1,
outputIndex: 0,
itemIndex: 0,
};
const expected: TargetItem[] = [
{ nodeName: 'Edit Fields', runIndex: 1, itemIndex: 0, outputIndex: 0 },
];
const actual = getSourceItems(MOCK_EXECUTION, target);
expect(actual).toEqual(expected);
});
});
describe('getPairedItemsMapping', () => {
it('should return the mapping of paired items', () => {
const actual = getPairedItemsMapping(MOCK_EXECUTION);
const expected = {
DebugHelper_r0_o0_i0: new Set([
'When clicking "Test workflow"_r0_o0_i0',
'If_r0_o0_i0',
'Edit Fields_r1_o0_i0',
'Edit Fields1_r1_o0_i0',
]),
DebugHelper_r0_o0_i1: new Set([
'When clicking "Test workflow"_r0_o0_i0',
'If_r0_o1_i0',
'Edit Fields_r0_o0_i0',
'Edit Fields1_r0_o0_i0',
]),
'Edit Fields1_r0_o0_i0': new Set([
'When clicking "Test workflow"_r0_o0_i0',
'DebugHelper_r0_o0_i1',
'If_r0_o1_i0',
'Edit Fields_r0_o0_i0',
]),
'Edit Fields1_r1_o0_i0': new Set([
'When clicking "Test workflow"_r0_o0_i0',
'DebugHelper_r0_o0_i0',
'If_r0_o0_i0',
'Edit Fields_r1_o0_i0',
]),
'Edit Fields_r0_o0_i0': new Set([
'When clicking "Test workflow"_r0_o0_i0',
'DebugHelper_r0_o0_i1',
'If_r0_o1_i0',
'Edit Fields1_r0_o0_i0',
]),
'Edit Fields_r1_o0_i0': new Set([
'When clicking "Test workflow"_r0_o0_i0',
'DebugHelper_r0_o0_i0',
'If_r0_o0_i0',
'Edit Fields1_r1_o0_i0',
]),
If_r0_o0_i0: new Set([
'When clicking "Test workflow"_r0_o0_i0',
'DebugHelper_r0_o0_i0',
'Edit Fields_r1_o0_i0',
'Edit Fields1_r1_o0_i0',
]),
If_r0_o1_i0: new Set([
'When clicking "Test workflow"_r0_o0_i0',
'DebugHelper_r0_o0_i1',
'Edit Fields_r0_o0_i0',
'Edit Fields1_r0_o0_i0',
]),
'When clicking "Test workflow"_r0_o0_i0': new Set([
'DebugHelper_r0_o0_i0',
'DebugHelper_r0_o0_i1',
'If_r0_o0_i0',
'If_r0_o1_i0',
'Edit Fields_r0_o0_i0',
'Edit Fields_r1_o0_i0',
'Edit Fields1_r0_o0_i0',
'Edit Fields1_r1_o0_i0',
]),
};
expect(actual).toEqual(expected);
});
it('should skip mapping if execution has more than max items overall', () => {
const mockExecution: Partial<IExecutionResponse> = {
data: {
startData: {},
resultData: {
runData: {
Start: [
{
startTime: 1706027170005,
executionTime: 0,
source: [],
executionStatus: 'success',
data: {
main: [[]],
},
},
],
DebugHelper: [
{
startTime: 1706027170005,
executionTime: 1,
source: [{ previousNode: 'Start' }],
executionStatus: 'success',
data: {
main: [[]],
},
},
],
},
pinData: {},
lastNodeExecuted: 'DebugHelper',
},
executionData: {
contextData: {},
nodeExecutionStack: [],
metadata: {},
waitingExecution: {},
waitingExecutionSource: {},
},
},
mode: 'manual',
startedAt: new Date('2024-01-23T16:26:10.003Z'),
stoppedAt: new Date('2024-01-23T16:26:10.011Z'),
status: 'success',
finished: true,
};
for (let i = 0; i < MAX_ITEM_COUNT_FOR_PAIRING / 2; i++) {
mockExecution.data?.resultData.runData.Start[0].data?.main[0]?.push({
json: {},
pairedItem: { item: 0 },
});
}
for (let i = 0; i < MAX_ITEM_COUNT_FOR_PAIRING / 2; i++) {
mockExecution.data?.resultData.runData.DebugHelper[0]?.data?.main[0]?.push({
json: {
uid: '3d3ee69e-f013-478c-8f5b-7723f508c02b',
email: 'Jeffery_Wehner@yahoo.com',
firstname: 'Amelia',
lastname: 'Mante',
password: '2DJR~2owf',
},
pairedItem: { item: 0 },
});
}
const actual = getPairedItemsMapping(mockExecution);
expect(Object.keys(actual).length).toEqual(MAX_ITEM_COUNT_FOR_PAIRING / 2 + 1);
mockExecution.data?.resultData.runData.Start[0].data?.main?.[0]?.push({
json: {},
pairedItem: { item: 0 },
});
expect(getPairedItemsMapping(mockExecution)).toEqual({});
});
});
});

View file

@ -2,6 +2,8 @@ import type { IPairedItemData, IRunData, ITaskData } from 'n8n-workflow';
import type { IExecutionResponse, TargetItem } from '@/Interface';
import { isNotNull } from '@/utils/typeGuards';
export const MAX_ITEM_COUNT_FOR_PAIRING = 1000;
/*
Utility functions that provide shared functionalities used to add paired item support to nodes
*/
@ -10,7 +12,10 @@ export function getPairedItemId(node: string, run: number, output: number, item:
return `${node}_r${run}_o${output}_i${item}`;
}
export function getSourceItems(data: IExecutionResponse, target: TargetItem): TargetItem[] {
export function getSourceItems(
data: Partial<IExecutionResponse>,
target: TargetItem,
): TargetItem[] {
if (!data?.data?.resultData?.runData) {
return [];
}
@ -165,16 +170,42 @@ function getMapping(paths: { [item: string]: string[][] }): { [item: string]: Se
return mapping;
}
export function getPairedItemsMapping(executionResponse: IExecutionResponse | null): {
function getItemsCount(runData: IRunData) {
let itemsCount = 0;
for (const node in runData) {
for (const taskData of runData[node]) {
const data = taskData.data;
if (!data) continue;
for (const connectionType in data) {
const runsCount = data[connectionType].reduce((sum: number, run) => {
return run ? sum + run.length : sum;
}, 0);
itemsCount += runsCount;
}
}
}
return itemsCount;
}
export function getPairedItemsMapping(executionResponse: Partial<IExecutionResponse> | null): {
[itemId: string]: Set<string>;
} {
if (!executionResponse?.data?.resultData?.runData) {
return {};
}
const seen = new Set<string>();
const runData = executionResponse.data.resultData.runData;
const itemsCount = getItemsCount(runData);
if (itemsCount > MAX_ITEM_COUNT_FOR_PAIRING) {
return {};
}
const seen = new Set<string>();
const pinned = new Set(Object.keys(executionResponse.data.resultData.pinData || {}));
const paths: { [item: string]: string[][] } = {};