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,
MAX_ITEM_COUNT_FOR_PAIRING,
} from './pairedItemUtils';
import { type ITaskData } from 'n8n-workflow';
const MOCK_EXECUTION: Partial<IExecutionResponse> = {
data: {
@ -396,5 +397,41 @@ describe('pairedItemUtils', () => {
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 { isNotNull } from '@/utils/typeGuards';
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
*/
@ -51,29 +58,36 @@ export function getSourceItems(
}
function addPairing(
paths: { [item: string]: string[][] },
paths: Paths,
pairedItemId: string,
pairedItem: IPairedItemData,
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 sourceNode = sources[input]?.previousNode;
if (!sourceNode) {
// trigger nodes for example
paths[pairedItemId].push([pairedItemId]);
paths.data[pairedItemId].push([pairedItemId]);
paths.size++;
return;
}
const sourceNodeOutput = sources[input]?.previousNodeOutput || 0;
const sourceNodeRun = sources[input]?.previousNodeRun || 0;
const sourceItem = getPairedItemId(sourceNode, sourceNodeRun, sourceNodeOutput, pairedItem.item);
if (!paths[sourceItem]) {
paths[sourceItem] = [[sourceItem]]; // pinned data case
if (!paths.data[sourceItem]) {
paths.data[sourceItem] = [[sourceItem]]; // pinned data case
paths.size++;
}
paths[sourceItem]?.forEach((path) => {
paths?.[pairedItemId]?.push([...path, pairedItemId]);
paths.data[sourceItem]?.forEach((path) => {
paths.data[pairedItemId]?.push([...path, pairedItemId]);
paths.size++;
});
}
@ -82,7 +96,7 @@ function addPairedItemIdsRec(
runIndex: number,
runData: IRunData,
seen: Set<string>,
paths: { [item: string]: string[][] },
paths: Paths,
pinned: Set<string>,
) {
const key = `${node}_r${runIndex}`;
@ -128,7 +142,7 @@ function addPairedItemIdsRec(
outputData.forEach((executionData, item: number) => {
const pairedItemId = getPairedItemId(node, runIndex, output, item);
if (!executionData.pairedItem) {
paths[pairedItemId] = [];
paths.data[pairedItemId] = [];
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> } = {};
Object.keys(paths).forEach((item) => {
paths?.[item]?.forEach((path) => {
Object.keys(paths.data).forEach((item) => {
paths.data[item]?.forEach((path) => {
path.forEach((otherItem) => {
if (otherItem !== item) {
mapping[otherItem] = mapping[otherItem] || new Set();
@ -205,15 +219,20 @@ export function getPairedItemsMapping(executionResponse: Partial<IExecutionRespo
return {};
}
const seen = new Set<string>();
const pinned = new Set(Object.keys(executionResponse.data.resultData.pinData || {}));
const paths: Paths = { size: 0, data: {} };
const paths: { [item: string]: string[][] } = {};
Object.keys(runData).forEach((node) => {
runData[node].forEach((_, runIndex: number) => {
addPairedItemIdsRec(node, runIndex, runData, seen, paths, pinned);
try {
const seen = new Set<string>();
const pinned = new Set(Object.keys(executionResponse.data.resultData.pinData ?? {}));
Object.keys(runData).forEach((node) => {
runData[node].forEach((_, runIndex: number) => {
addPairedItemIdsRec(node, runIndex, runData, seen, paths, pinned);
});
});
});
} catch {
return {};
}
return getMapping(paths);
}