fix(editor): Fix browser crash with large execution result (#13580)

This commit is contained in:
autologie 2025-03-03 08:57:57 +01:00 committed by GitHub
parent 2cb9d9e29f
commit 1c8c7e34f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 76 additions and 20 deletions

View file

@ -5,6 +5,7 @@ import {
getPairedItemsMapping, getPairedItemsMapping,
MAX_ITEM_COUNT_FOR_PAIRING, MAX_ITEM_COUNT_FOR_PAIRING,
} from './pairedItemUtils'; } from './pairedItemUtils';
import { type ITaskData } from 'n8n-workflow';
const MOCK_EXECUTION: Partial<IExecutionResponse> = { const MOCK_EXECUTION: Partial<IExecutionResponse> = {
data: { data: {
@ -396,5 +397,41 @@ describe('pairedItemUtils', () => {
expect(getPairedItemsMapping(mockExecution)).toEqual({}); expect(getPairedItemsMapping(mockExecution)).toEqual({});
}); });
it('should abort mapping and return empty object if execution has too many pairs', () => {
const nodeCount = 10;
const runCount = 3;
const itemCountPerRun = 3;
const pairedItemCount = 3;
const mockExecution: Partial<IExecutionResponse> = {
data: {
resultData: {
runData: Object.fromEntries(
Array.from({ length: nodeCount }).map<[string, ITaskData[]]>((_, j) => [
`node_${j}`,
Array.from({ length: runCount }).map(() => ({
startTime: 1706027170005,
executionTime: 0,
source: j === 0 ? [] : [{ previousNode: `node_${j - 1}` }],
executionStatus: 'success',
data: {
main: [
Array.from({ length: itemCountPerRun }).map(() => ({
json: {},
pairedItem: Array.from({ length: pairedItemCount }).map((__, i) => ({
item: i,
})),
})),
],
},
})),
]),
),
},
},
};
expect(getPairedItemsMapping(mockExecution)).toEqual({});
});
}); });
}); });

View file

@ -1,9 +1,16 @@
import type { IPairedItemData, IRunData, ITaskData } from 'n8n-workflow'; import { type IPairedItemData, type IRunData, type ITaskData } from 'n8n-workflow';
import type { IExecutionResponse, TargetItem } from '@/Interface'; import type { IExecutionResponse, TargetItem } from '@/Interface';
import { isNotNull } from '@/utils/typeGuards'; import { isNotNull } from '@/utils/typeGuards';
export const MAX_ITEM_COUNT_FOR_PAIRING = 1000; export const MAX_ITEM_COUNT_FOR_PAIRING = 1000;
const MAX_PAIR_COUNT = 100000;
interface Paths {
data: { [item: string]: string[][] };
size: number;
}
/* /*
Utility functions that provide shared functionalities used to add paired item support to nodes Utility functions that provide shared functionalities used to add paired item support to nodes
*/ */
@ -51,29 +58,36 @@ export function getSourceItems(
} }
function addPairing( function addPairing(
paths: { [item: string]: string[][] }, paths: Paths,
pairedItemId: string, pairedItemId: string,
pairedItem: IPairedItemData, pairedItem: IPairedItemData,
sources: ITaskData['source'], sources: ITaskData['source'],
) { ) {
paths[pairedItemId] = paths[pairedItemId] || []; if (paths.size >= MAX_PAIR_COUNT) {
throw Error();
}
paths.data[pairedItemId] = paths.data[pairedItemId] || [];
const input = pairedItem.input || 0; const input = pairedItem.input || 0;
const sourceNode = sources[input]?.previousNode; const sourceNode = sources[input]?.previousNode;
if (!sourceNode) { if (!sourceNode) {
// trigger nodes for example // trigger nodes for example
paths[pairedItemId].push([pairedItemId]); paths.data[pairedItemId].push([pairedItemId]);
paths.size++;
return; return;
} }
const sourceNodeOutput = sources[input]?.previousNodeOutput || 0; const sourceNodeOutput = sources[input]?.previousNodeOutput || 0;
const sourceNodeRun = sources[input]?.previousNodeRun || 0; const sourceNodeRun = sources[input]?.previousNodeRun || 0;
const sourceItem = getPairedItemId(sourceNode, sourceNodeRun, sourceNodeOutput, pairedItem.item); const sourceItem = getPairedItemId(sourceNode, sourceNodeRun, sourceNodeOutput, pairedItem.item);
if (!paths[sourceItem]) { if (!paths.data[sourceItem]) {
paths[sourceItem] = [[sourceItem]]; // pinned data case paths.data[sourceItem] = [[sourceItem]]; // pinned data case
paths.size++;
} }
paths[sourceItem]?.forEach((path) => { paths.data[sourceItem]?.forEach((path) => {
paths?.[pairedItemId]?.push([...path, pairedItemId]); paths.data[pairedItemId]?.push([...path, pairedItemId]);
paths.size++;
}); });
} }
@ -82,7 +96,7 @@ function addPairedItemIdsRec(
runIndex: number, runIndex: number,
runData: IRunData, runData: IRunData,
seen: Set<string>, seen: Set<string>,
paths: { [item: string]: string[][] }, paths: Paths,
pinned: Set<string>, pinned: Set<string>,
) { ) {
const key = `${node}_r${runIndex}`; const key = `${node}_r${runIndex}`;
@ -128,7 +142,7 @@ function addPairedItemIdsRec(
outputData.forEach((executionData, item: number) => { outputData.forEach((executionData, item: number) => {
const pairedItemId = getPairedItemId(node, runIndex, output, item); const pairedItemId = getPairedItemId(node, runIndex, output, item);
if (!executionData.pairedItem) { if (!executionData.pairedItem) {
paths[pairedItemId] = []; paths.data[pairedItemId] = [];
return; return;
} }
@ -150,11 +164,11 @@ function addPairedItemIdsRec(
}); });
} }
function getMapping(paths: { [item: string]: string[][] }): { [item: string]: Set<string> } { function getMapping(paths: Paths): { [item: string]: Set<string> } {
const mapping: { [itemId: string]: Set<string> } = {}; const mapping: { [itemId: string]: Set<string> } = {};
Object.keys(paths).forEach((item) => { Object.keys(paths.data).forEach((item) => {
paths?.[item]?.forEach((path) => { paths.data[item]?.forEach((path) => {
path.forEach((otherItem) => { path.forEach((otherItem) => {
if (otherItem !== item) { if (otherItem !== item) {
mapping[otherItem] = mapping[otherItem] || new Set(); mapping[otherItem] = mapping[otherItem] || new Set();
@ -205,15 +219,20 @@ export function getPairedItemsMapping(executionResponse: Partial<IExecutionRespo
return {}; return {};
} }
const seen = new Set<string>(); const paths: Paths = { size: 0, data: {} };
const pinned = new Set(Object.keys(executionResponse.data.resultData.pinData || {}));
const paths: { [item: string]: string[][] } = {}; try {
Object.keys(runData).forEach((node) => { const seen = new Set<string>();
runData[node].forEach((_, runIndex: number) => { const pinned = new Set(Object.keys(executionResponse.data.resultData.pinData ?? {}));
addPairedItemIdsRec(node, runIndex, runData, seen, paths, pinned);
Object.keys(runData).forEach((node) => {
runData[node].forEach((_, runIndex: number) => {
addPairedItemIdsRec(node, runIndex, runData, seen, paths, pinned);
});
}); });
}); } catch {
return {};
}
return getMapping(paths); return getMapping(paths);
} }