mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
perf(ui): use rfdc
for deep copying of objects
- Add and use more performant `deepClone` method for deep copying throughout the UI. Benchmarks indicate the Really Fast Deep Clone library (`rfdc`) is the best all-around way to deep-clone large objects. This is particularly relevant in canvas. When drawing or otherwise manipulating canvas objects, we need to do a lot of deep cloning of the canvas layer state objects. Previously, we were using lodash's `cloneDeep`. I did some fairly realistic benchmarks with a handful of deep-cloning algorithms/libraries (including the native `structuredClone`). I used a snapshot of the canvas state as the data to be copied: On Chromium, `rfdc` is by far the fastest, over an order of magnitude faster than `cloneDeep`. On FF, `fastest-json-copy` and `recursiveDeepCopy` are even faster, but are rather limited in data types. `rfdc`, while only half as fast as the former 2, is still nearly an order of magnitude faster than `cloneDeep`. On Safari, `structuredClone` is the fastest, about 2x as fast as `cloneDeep`. `rfdc` is only 30% faster than `cloneDeep`. `rfdc`'s peak memory usage is about 10% more than `cloneDeep` on Chrome. I couldn't get memory measurements from FF and Safari, but let's just assume the memory usage is similar relative to the other algos. Overall, `rfdc` is the best choice for a single algo for all browsers. It's definitely the best for Chromium, by far the most popular desktop browser and thus our primary target. A future enhancement might be to detect the browser and use that to determine which algorithm to use.
This commit is contained in:
parent
a6c91979af
commit
69ec14c7bb
@ -94,6 +94,7 @@
|
|||||||
"reactflow": "^11.10.4",
|
"reactflow": "^11.10.4",
|
||||||
"redux-dynamic-middlewares": "^2.2.0",
|
"redux-dynamic-middlewares": "^2.2.0",
|
||||||
"redux-remember": "^5.1.0",
|
"redux-remember": "^5.1.0",
|
||||||
|
"rfdc": "^1.3.1",
|
||||||
"roarr": "^7.21.1",
|
"roarr": "^7.21.1",
|
||||||
"serialize-error": "^11.0.3",
|
"serialize-error": "^11.0.3",
|
||||||
"socket.io-client": "^4.7.5",
|
"socket.io-client": "^4.7.5",
|
||||||
|
@ -137,6 +137,9 @@ dependencies:
|
|||||||
redux-remember:
|
redux-remember:
|
||||||
specifier: ^5.1.0
|
specifier: ^5.1.0
|
||||||
version: 5.1.0(redux@5.0.1)
|
version: 5.1.0(redux@5.0.1)
|
||||||
|
rfdc:
|
||||||
|
specifier: ^1.3.1
|
||||||
|
version: 1.3.1
|
||||||
roarr:
|
roarr:
|
||||||
specifier: ^7.21.1
|
specifier: ^7.21.1
|
||||||
version: 7.21.1
|
version: 7.21.1
|
||||||
@ -12128,6 +12131,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-/x8uIPdTafBqakK0TmPNJzgkLP+3H+yxpUJhCQHsLBg1rYEVNR2D8BRYNWQhVBjyOd7oo1dZRVzIkwMY2oqfYQ==}
|
resolution: {integrity: sha512-/x8uIPdTafBqakK0TmPNJzgkLP+3H+yxpUJhCQHsLBg1rYEVNR2D8BRYNWQhVBjyOd7oo1dZRVzIkwMY2oqfYQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/rfdc@1.3.1:
|
||||||
|
resolution: {integrity: sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/rimraf@2.6.3:
|
/rimraf@2.6.3:
|
||||||
resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==}
|
resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import type { UnknownAction } from '@reduxjs/toolkit';
|
import type { UnknownAction } from '@reduxjs/toolkit';
|
||||||
|
import { deepClone } from 'common/util/deepClone';
|
||||||
import { isAnyGraphBuilt } from 'features/nodes/store/actions';
|
import { isAnyGraphBuilt } from 'features/nodes/store/actions';
|
||||||
import { nodeTemplatesBuilt } from 'features/nodes/store/nodesSlice';
|
import { nodeTemplatesBuilt } from 'features/nodes/store/nodesSlice';
|
||||||
import { cloneDeep } from 'lodash-es';
|
|
||||||
import { appInfoApi } from 'services/api/endpoints/appInfo';
|
import { appInfoApi } from 'services/api/endpoints/appInfo';
|
||||||
import type { Graph } from 'services/api/types';
|
import type { Graph } from 'services/api/types';
|
||||||
import { socketGeneratorProgress } from 'services/events/actions';
|
import { socketGeneratorProgress } from 'services/events/actions';
|
||||||
@ -33,7 +33,7 @@ export const actionSanitizer = <A extends UnknownAction>(action: A): A => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (socketGeneratorProgress.match(action)) {
|
if (socketGeneratorProgress.match(action)) {
|
||||||
const sanitized = cloneDeep(action);
|
const sanitized = deepClone(action);
|
||||||
if (sanitized.payload.data.progress_image) {
|
if (sanitized.payload.data.progress_image) {
|
||||||
sanitized.payload.data.progress_image.dataURL = '<Progress image omitted>';
|
sanitized.payload.data.progress_image.dataURL = '<Progress image omitted>';
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { cloneDeep, merge } from 'lodash-es';
|
import { deepClone } from 'common/util/deepClone';
|
||||||
|
import { merge } from 'lodash-es';
|
||||||
import { ClickScrollPlugin, OverlayScrollbars } from 'overlayscrollbars';
|
import { ClickScrollPlugin, OverlayScrollbars } from 'overlayscrollbars';
|
||||||
import type { UseOverlayScrollbarsParams } from 'overlayscrollbars-react';
|
import type { UseOverlayScrollbarsParams } from 'overlayscrollbars-react';
|
||||||
|
|
||||||
@ -22,7 +23,7 @@ export const getOverlayScrollbarsParams = (
|
|||||||
overflowX: 'hidden' | 'scroll' = 'hidden',
|
overflowX: 'hidden' | 'scroll' = 'hidden',
|
||||||
overflowY: 'hidden' | 'scroll' = 'scroll'
|
overflowY: 'hidden' | 'scroll' = 'scroll'
|
||||||
) => {
|
) => {
|
||||||
const params = cloneDeep(overlayScrollbarsParams);
|
const params = deepClone(overlayScrollbarsParams);
|
||||||
merge(params, { options: { overflow: { y: overflowY, x: overflowX } } });
|
merge(params, { options: { overflow: { y: overflowY, x: overflowX } } });
|
||||||
return params;
|
return params;
|
||||||
};
|
};
|
||||||
|
15
invokeai/frontend/web/src/common/util/deepClone.ts
Normal file
15
invokeai/frontend/web/src/common/util/deepClone.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import rfdc from 'rfdc';
|
||||||
|
const _rfdc = rfdc();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deep-clones an object using Really Fast Deep Clone.
|
||||||
|
* This is the fastest deep clone library on Chrome, but not the fastest on FF. Still, it's much faster than lodash
|
||||||
|
* and structuredClone, so it's the best all-around choice.
|
||||||
|
*
|
||||||
|
* Simple Benchmark: https://www.measurethat.net/Benchmarks/Show/30358/0/lodash-clonedeep-vs-jsonparsejsonstringify-vs-recursive
|
||||||
|
* Repo: https://github.com/davidmarkclements/rfdc
|
||||||
|
*
|
||||||
|
* @param obj The object to deep-clone
|
||||||
|
* @returns The cloned object
|
||||||
|
*/
|
||||||
|
export const deepClone = <T>(obj: T): T => _rfdc(obj);
|
@ -1,6 +1,7 @@
|
|||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import type { PersistConfig, RootState } from 'app/store/store';
|
import type { PersistConfig, RootState } from 'app/store/store';
|
||||||
|
import { deepClone } from 'common/util/deepClone';
|
||||||
import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple';
|
import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple';
|
||||||
import calculateCoordinates from 'features/canvas/util/calculateCoordinates';
|
import calculateCoordinates from 'features/canvas/util/calculateCoordinates';
|
||||||
import calculateScale from 'features/canvas/util/calculateScale';
|
import calculateScale from 'features/canvas/util/calculateScale';
|
||||||
@ -13,7 +14,7 @@ import { modelChanged } from 'features/parameters/store/generationSlice';
|
|||||||
import type { PayloadActionWithOptimalDimension } from 'features/parameters/store/types';
|
import type { PayloadActionWithOptimalDimension } from 'features/parameters/store/types';
|
||||||
import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension';
|
import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension';
|
||||||
import type { IRect, Vector2d } from 'konva/lib/types';
|
import type { IRect, Vector2d } from 'konva/lib/types';
|
||||||
import { clamp, cloneDeep } from 'lodash-es';
|
import { clamp } from 'lodash-es';
|
||||||
import type { RgbaColor } from 'react-colorful';
|
import type { RgbaColor } from 'react-colorful';
|
||||||
import { queueApi } from 'services/api/endpoints/queue';
|
import { queueApi } from 'services/api/endpoints/queue';
|
||||||
import type { ImageDTO } from 'services/api/types';
|
import type { ImageDTO } from 'services/api/types';
|
||||||
@ -166,7 +167,7 @@ export const canvasSlice = createSlice({
|
|||||||
pushToPrevLayerStates(state);
|
pushToPrevLayerStates(state);
|
||||||
|
|
||||||
state.layerState = {
|
state.layerState = {
|
||||||
...cloneDeep(initialLayerState),
|
...deepClone(initialLayerState),
|
||||||
objects: [
|
objects: [
|
||||||
{
|
{
|
||||||
kind: 'image',
|
kind: 'image',
|
||||||
@ -277,7 +278,7 @@ export const canvasSlice = createSlice({
|
|||||||
discardStagedImages: (state) => {
|
discardStagedImages: (state) => {
|
||||||
pushToPrevLayerStates(state);
|
pushToPrevLayerStates(state);
|
||||||
|
|
||||||
state.layerState.stagingArea = cloneDeep(cloneDeep(initialLayerState)).stagingArea;
|
state.layerState.stagingArea = deepClone(initialLayerState.stagingArea);
|
||||||
|
|
||||||
state.futureLayerStates = [];
|
state.futureLayerStates = [];
|
||||||
state.shouldShowStagingOutline = true;
|
state.shouldShowStagingOutline = true;
|
||||||
@ -414,7 +415,7 @@ export const canvasSlice = createSlice({
|
|||||||
},
|
},
|
||||||
resetCanvas: (state) => {
|
resetCanvas: (state) => {
|
||||||
pushToPrevLayerStates(state);
|
pushToPrevLayerStates(state);
|
||||||
state.layerState = cloneDeep(initialLayerState);
|
state.layerState = deepClone(initialLayerState);
|
||||||
state.futureLayerStates = [];
|
state.futureLayerStates = [];
|
||||||
state.batchIds = [];
|
state.batchIds = [];
|
||||||
state.boundingBoxCoordinates = {
|
state.boundingBoxCoordinates = {
|
||||||
@ -517,7 +518,7 @@ export const canvasSlice = createSlice({
|
|||||||
...imageToCommit,
|
...imageToCommit,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
state.layerState.stagingArea = cloneDeep(initialLayerState).stagingArea;
|
state.layerState.stagingArea = deepClone(initialLayerState.stagingArea);
|
||||||
|
|
||||||
state.futureLayerStates = [];
|
state.futureLayerStates = [];
|
||||||
state.shouldShowStagingOutline = true;
|
state.shouldShowStagingOutline = true;
|
||||||
@ -709,14 +710,14 @@ export const canvasPersistConfig: PersistConfig<CanvasState> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const pushToPrevLayerStates = (state: CanvasState) => {
|
const pushToPrevLayerStates = (state: CanvasState) => {
|
||||||
state.pastLayerStates.push(cloneDeep(state.layerState));
|
state.pastLayerStates.push(deepClone(state.layerState));
|
||||||
if (state.pastLayerStates.length > MAX_HISTORY) {
|
if (state.pastLayerStates.length > MAX_HISTORY) {
|
||||||
state.pastLayerStates = state.pastLayerStates.slice(-MAX_HISTORY);
|
state.pastLayerStates = state.pastLayerStates.slice(-MAX_HISTORY);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const pushToFutureLayerStates = (state: CanvasState) => {
|
const pushToFutureLayerStates = (state: CanvasState) => {
|
||||||
state.futureLayerStates.unshift(cloneDeep(state.layerState));
|
state.futureLayerStates.unshift(deepClone(state.layerState));
|
||||||
if (state.futureLayerStates.length > MAX_HISTORY) {
|
if (state.futureLayerStates.length > MAX_HISTORY) {
|
||||||
state.futureLayerStates = state.futureLayerStates.slice(0, MAX_HISTORY);
|
state.futureLayerStates = state.futureLayerStates.slice(0, MAX_HISTORY);
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,11 @@ import type { PayloadAction, Update } from '@reduxjs/toolkit';
|
|||||||
import { createEntityAdapter, createSlice, isAnyOf } from '@reduxjs/toolkit';
|
import { createEntityAdapter, createSlice, isAnyOf } from '@reduxjs/toolkit';
|
||||||
import { getSelectorsOptions } from 'app/store/createMemoizedSelector';
|
import { getSelectorsOptions } from 'app/store/createMemoizedSelector';
|
||||||
import type { PersistConfig, RootState } from 'app/store/store';
|
import type { PersistConfig, RootState } from 'app/store/store';
|
||||||
|
import { deepClone } from 'common/util/deepClone';
|
||||||
import { buildControlAdapter } from 'features/controlAdapters/util/buildControlAdapter';
|
import { buildControlAdapter } from 'features/controlAdapters/util/buildControlAdapter';
|
||||||
import { buildControlAdapterProcessor } from 'features/controlAdapters/util/buildControlAdapterProcessor';
|
import { buildControlAdapterProcessor } from 'features/controlAdapters/util/buildControlAdapterProcessor';
|
||||||
import { zModelIdentifierField } from 'features/nodes/types/common';
|
import { zModelIdentifierField } from 'features/nodes/types/common';
|
||||||
import { cloneDeep, merge, uniq } from 'lodash-es';
|
import { merge, uniq } from 'lodash-es';
|
||||||
import type { ControlNetModelConfig, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types';
|
import type { ControlNetModelConfig, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types';
|
||||||
import { socketInvocationError } from 'services/events/actions';
|
import { socketInvocationError } from 'services/events/actions';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
@ -114,7 +115,7 @@ export const controlAdaptersSlice = createSlice({
|
|||||||
if (!controlAdapter) {
|
if (!controlAdapter) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newControlAdapter = merge(cloneDeep(controlAdapter), {
|
const newControlAdapter = merge(deepClone(controlAdapter), {
|
||||||
id: newId,
|
id: newId,
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
});
|
});
|
||||||
@ -270,7 +271,7 @@ export const controlAdaptersSlice = createSlice({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const processorNode = merge(cloneDeep(cn.processorNode), params);
|
const processorNode = merge(deepClone(cn.processorNode), params);
|
||||||
|
|
||||||
caAdapter.updateOne(state, {
|
caAdapter.updateOne(state, {
|
||||||
id,
|
id,
|
||||||
@ -293,7 +294,7 @@ export const controlAdaptersSlice = createSlice({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const processorNode = cloneDeep(
|
const processorNode = deepClone(
|
||||||
CONTROLNET_PROCESSORS[processorType].buildDefaults(cn.model?.base)
|
CONTROLNET_PROCESSORS[processorType].buildDefaults(cn.model?.base)
|
||||||
) as RequiredControlAdapterProcessorNode;
|
) as RequiredControlAdapterProcessorNode;
|
||||||
|
|
||||||
@ -333,7 +334,7 @@ export const controlAdaptersSlice = createSlice({
|
|||||||
caAdapter.updateOne(state, update);
|
caAdapter.updateOne(state, update);
|
||||||
},
|
},
|
||||||
controlAdaptersReset: () => {
|
controlAdaptersReset: () => {
|
||||||
return cloneDeep(initialControlAdaptersState);
|
return deepClone(initialControlAdaptersState);
|
||||||
},
|
},
|
||||||
pendingControlImagesCleared: (state) => {
|
pendingControlImagesCleared: (state) => {
|
||||||
state.pendingControlImages = [];
|
state.pendingControlImages = [];
|
||||||
@ -406,7 +407,7 @@ const migrateControlAdaptersState = (state: any): any => {
|
|||||||
state._version = 1;
|
state._version = 1;
|
||||||
}
|
}
|
||||||
if (state._version === 1) {
|
if (state._version === 1) {
|
||||||
state = cloneDeep(initialControlAdaptersState);
|
state = deepClone(initialControlAdaptersState);
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
};
|
};
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { deepClone } from 'common/util/deepClone';
|
||||||
import { CONTROLNET_PROCESSORS } from 'features/controlAdapters/store/constants';
|
import { CONTROLNET_PROCESSORS } from 'features/controlAdapters/store/constants';
|
||||||
import type {
|
import type {
|
||||||
ControlAdapterConfig,
|
ControlAdapterConfig,
|
||||||
@ -7,7 +8,7 @@ import type {
|
|||||||
RequiredCannyImageProcessorInvocation,
|
RequiredCannyImageProcessorInvocation,
|
||||||
T2IAdapterConfig,
|
T2IAdapterConfig,
|
||||||
} from 'features/controlAdapters/store/types';
|
} from 'features/controlAdapters/store/types';
|
||||||
import { cloneDeep, merge } from 'lodash-es';
|
import { merge } from 'lodash-es';
|
||||||
|
|
||||||
export const initialControlNet: Omit<ControlNetConfig, 'id'> = {
|
export const initialControlNet: Omit<ControlNetConfig, 'id'> = {
|
||||||
type: 'controlnet',
|
type: 'controlnet',
|
||||||
@ -57,11 +58,11 @@ export const buildControlAdapter = (
|
|||||||
): ControlAdapterConfig => {
|
): ControlAdapterConfig => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'controlnet':
|
case 'controlnet':
|
||||||
return merge(cloneDeep(initialControlNet), { id, ...overrides });
|
return merge(deepClone(initialControlNet), { id, ...overrides });
|
||||||
case 't2i_adapter':
|
case 't2i_adapter':
|
||||||
return merge(cloneDeep(initialT2IAdapter), { id, ...overrides });
|
return merge(deepClone(initialT2IAdapter), { id, ...overrides });
|
||||||
case 'ip_adapter':
|
case 'ip_adapter':
|
||||||
return merge(cloneDeep(initialIPAdapter), { id, ...overrides });
|
return merge(deepClone(initialIPAdapter), { id, ...overrides });
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown control adapter type: ${type}`);
|
throw new Error(`Unknown control adapter type: ${type}`);
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import type { PersistConfig, RootState } from 'app/store/store';
|
import type { PersistConfig, RootState } from 'app/store/store';
|
||||||
|
import { deepClone } from 'common/util/deepClone';
|
||||||
import { zModelIdentifierField } from 'features/nodes/types/common';
|
import { zModelIdentifierField } from 'features/nodes/types/common';
|
||||||
import type { ParameterLoRAModel } from 'features/parameters/types/parameterSchemas';
|
import type { ParameterLoRAModel } from 'features/parameters/types/parameterSchemas';
|
||||||
import { cloneDeep } from 'lodash-es';
|
|
||||||
import type { LoRAModelConfig } from 'services/api/types';
|
import type { LoRAModelConfig } from 'services/api/types';
|
||||||
|
|
||||||
export type LoRA = {
|
export type LoRA = {
|
||||||
@ -58,7 +58,7 @@ export const loraSlice = createSlice({
|
|||||||
}
|
}
|
||||||
lora.isEnabled = isEnabled;
|
lora.isEnabled = isEnabled;
|
||||||
},
|
},
|
||||||
lorasReset: () => cloneDeep(initialLoraState),
|
lorasReset: () => deepClone(initialLoraState),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -74,7 +74,7 @@ const migrateLoRAState = (state: any): any => {
|
|||||||
}
|
}
|
||||||
if (state._version === 1) {
|
if (state._version === 1) {
|
||||||
// Model type has changed, so we need to reset the state - too risky to migrate
|
// Model type has changed, so we need to reset the state - too risky to migrate
|
||||||
state = cloneDeep(initialLoraState);
|
state = deepClone(initialLoraState);
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice, isAnyOf } from '@reduxjs/toolkit';
|
import { createSlice, isAnyOf } from '@reduxjs/toolkit';
|
||||||
import type { PersistConfig, RootState } from 'app/store/store';
|
import type { PersistConfig, RootState } from 'app/store/store';
|
||||||
|
import { deepClone } from 'common/util/deepClone';
|
||||||
import { workflowLoaded } from 'features/nodes/store/actions';
|
import { workflowLoaded } from 'features/nodes/store/actions';
|
||||||
import { SHARED_NODE_PROPERTIES } from 'features/nodes/types/constants';
|
import { SHARED_NODE_PROPERTIES } from 'features/nodes/types/constants';
|
||||||
import type {
|
import type {
|
||||||
@ -44,7 +45,7 @@ import {
|
|||||||
} from 'features/nodes/types/field';
|
} from 'features/nodes/types/field';
|
||||||
import type { AnyNode, InvocationTemplate, NodeExecutionState } from 'features/nodes/types/invocation';
|
import type { AnyNode, InvocationTemplate, NodeExecutionState } from 'features/nodes/types/invocation';
|
||||||
import { isInvocationNode, isNotesNode, zNodeStatus } from 'features/nodes/types/invocation';
|
import { isInvocationNode, isNotesNode, zNodeStatus } from 'features/nodes/types/invocation';
|
||||||
import { cloneDeep, forEach } from 'lodash-es';
|
import { forEach } from 'lodash-es';
|
||||||
import type {
|
import type {
|
||||||
Connection,
|
Connection,
|
||||||
Edge,
|
Edge,
|
||||||
@ -571,8 +572,23 @@ export const nodesSlice = createSlice({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
selectionCopied: (state) => {
|
selectionCopied: (state) => {
|
||||||
state.nodesToCopy = state.nodes.filter((n) => n.selected).map(cloneDeep);
|
const nodesToCopy: AnyNode[] = [];
|
||||||
state.edgesToCopy = state.edges.filter((e) => e.selected).map(cloneDeep);
|
const edgesToCopy: Edge[] = [];
|
||||||
|
|
||||||
|
for (const node of state.nodes) {
|
||||||
|
if (node.selected) {
|
||||||
|
nodesToCopy.push(deepClone(node));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const edge of state.edges) {
|
||||||
|
if (edge.selected) {
|
||||||
|
edgesToCopy.push(deepClone(edge));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.nodesToCopy = nodesToCopy;
|
||||||
|
state.edgesToCopy = edgesToCopy;
|
||||||
|
|
||||||
if (state.nodesToCopy.length > 0) {
|
if (state.nodesToCopy.length > 0) {
|
||||||
const averagePosition = { x: 0, y: 0 };
|
const averagePosition = { x: 0, y: 0 };
|
||||||
@ -594,11 +610,21 @@ export const nodesSlice = createSlice({
|
|||||||
},
|
},
|
||||||
selectionPasted: (state, action: PayloadAction<{ cursorPosition?: XYPosition }>) => {
|
selectionPasted: (state, action: PayloadAction<{ cursorPosition?: XYPosition }>) => {
|
||||||
const { cursorPosition } = action.payload;
|
const { cursorPosition } = action.payload;
|
||||||
const newNodes = state.nodesToCopy.map(cloneDeep);
|
const newNodes: AnyNode[] = [];
|
||||||
|
|
||||||
|
for (const node of state.nodesToCopy) {
|
||||||
|
newNodes.push(deepClone(node));
|
||||||
|
}
|
||||||
|
|
||||||
const oldNodeIds = newNodes.map((n) => n.data.id);
|
const oldNodeIds = newNodes.map((n) => n.data.id);
|
||||||
const newEdges = state.edgesToCopy
|
|
||||||
.filter((e) => oldNodeIds.includes(e.source) && oldNodeIds.includes(e.target))
|
const newEdges: Edge[] = [];
|
||||||
.map(cloneDeep);
|
|
||||||
|
for (const edge of state.edgesToCopy) {
|
||||||
|
if (oldNodeIds.includes(edge.source) && oldNodeIds.includes(edge.target)) {
|
||||||
|
newEdges.push(deepClone(edge));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
newEdges.forEach((e) => (e.selected = true));
|
newEdges.forEach((e) => (e.selected = true));
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import type { PersistConfig, RootState } from 'app/store/store';
|
import type { PersistConfig, RootState } from 'app/store/store';
|
||||||
|
import { deepClone } from 'common/util/deepClone';
|
||||||
import { workflowLoaded } from 'features/nodes/store/actions';
|
import { workflowLoaded } from 'features/nodes/store/actions';
|
||||||
import { isAnyNodeOrEdgeMutation, nodeEditorReset, nodesChanged, nodesDeleted } from 'features/nodes/store/nodesSlice';
|
import { isAnyNodeOrEdgeMutation, nodeEditorReset, nodesChanged, nodesDeleted } from 'features/nodes/store/nodesSlice';
|
||||||
import type {
|
import type {
|
||||||
@ -11,7 +12,7 @@ import type {
|
|||||||
import type { FieldIdentifier } from 'features/nodes/types/field';
|
import type { FieldIdentifier } from 'features/nodes/types/field';
|
||||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||||
import type { WorkflowCategory, WorkflowV3 } from 'features/nodes/types/workflow';
|
import type { WorkflowCategory, WorkflowV3 } from 'features/nodes/types/workflow';
|
||||||
import { cloneDeep, isEqual, omit, uniqBy } from 'lodash-es';
|
import { isEqual, omit, uniqBy } from 'lodash-es';
|
||||||
|
|
||||||
const blankWorkflow: Omit<WorkflowV3, 'nodes' | 'edges'> = {
|
const blankWorkflow: Omit<WorkflowV3, 'nodes' | 'edges'> = {
|
||||||
name: '',
|
name: '',
|
||||||
@ -131,8 +132,8 @@ export const workflowSlice = createSlice({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...cloneDeep(initialWorkflowState),
|
...deepClone(initialWorkflowState),
|
||||||
...cloneDeep(workflowExtra),
|
...deepClone(workflowExtra),
|
||||||
originalExposedFieldValues,
|
originalExposedFieldValues,
|
||||||
mode: state.mode,
|
mode: state.mode,
|
||||||
};
|
};
|
||||||
@ -144,7 +145,7 @@ export const workflowSlice = createSlice({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.addCase(nodeEditorReset, () => cloneDeep(initialWorkflowState));
|
builder.addCase(nodeEditorReset, () => deepClone(initialWorkflowState));
|
||||||
|
|
||||||
builder.addCase(nodesChanged, (state, action) => {
|
builder.addCase(nodesChanged, (state, action) => {
|
||||||
// Not all changes to nodes should result in the workflow being marked touched
|
// Not all changes to nodes should result in the workflow being marked touched
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
|
import { deepClone } from 'common/util/deepClone';
|
||||||
import { satisfies } from 'compare-versions';
|
import { satisfies } from 'compare-versions';
|
||||||
import { NodeUpdateError } from 'features/nodes/types/error';
|
import { NodeUpdateError } from 'features/nodes/types/error';
|
||||||
import type { InvocationNode, InvocationTemplate } from 'features/nodes/types/invocation';
|
import type { InvocationNode, InvocationTemplate } from 'features/nodes/types/invocation';
|
||||||
import { zParsedSemver } from 'features/nodes/types/semver';
|
import { zParsedSemver } from 'features/nodes/types/semver';
|
||||||
import { cloneDeep, defaultsDeep, keys, pick } from 'lodash-es';
|
import { defaultsDeep, keys, pick } from 'lodash-es';
|
||||||
|
|
||||||
import { buildInvocationNode } from './buildInvocationNode';
|
import { buildInvocationNode } from './buildInvocationNode';
|
||||||
|
|
||||||
@ -50,7 +51,7 @@ export const updateNode = (node: InvocationNode, template: InvocationTemplate):
|
|||||||
// The updateability of a node, via semver comparison, relies on the this kind of recursive merge
|
// The updateability of a node, via semver comparison, relies on the this kind of recursive merge
|
||||||
// being valid. We rely on the template's major version to be majorly incremented if this kind of
|
// being valid. We rely on the template's major version to be majorly incremented if this kind of
|
||||||
// merge would result in an invalid node.
|
// merge would result in an invalid node.
|
||||||
const clone = cloneDeep(node);
|
const clone = deepClone(node);
|
||||||
clone.data.version = template.version;
|
clone.data.version = template.version;
|
||||||
defaultsDeep(clone, defaults); // mutates!
|
defaultsDeep(clone, defaults); // mutates!
|
||||||
|
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { logger } from 'app/logging/logger';
|
import { logger } from 'app/logging/logger';
|
||||||
|
import { deepClone } from 'common/util/deepClone';
|
||||||
import { parseify } from 'common/util/serialize';
|
import { parseify } from 'common/util/serialize';
|
||||||
import type { NodesState, WorkflowsState } from 'features/nodes/store/types';
|
import type { NodesState, WorkflowsState } from 'features/nodes/store/types';
|
||||||
import { isInvocationNode, isNotesNode } from 'features/nodes/types/invocation';
|
import { isInvocationNode, isNotesNode } from 'features/nodes/types/invocation';
|
||||||
import type { WorkflowV3 } from 'features/nodes/types/workflow';
|
import type { WorkflowV3 } from 'features/nodes/types/workflow';
|
||||||
import { zWorkflowV3 } from 'features/nodes/types/workflow';
|
import { zWorkflowV3 } from 'features/nodes/types/workflow';
|
||||||
import i18n from 'i18n';
|
import i18n from 'i18n';
|
||||||
import { cloneDeep, pick } from 'lodash-es';
|
import { pick } from 'lodash-es';
|
||||||
import { fromZodError } from 'zod-validation-error';
|
import { fromZodError } from 'zod-validation-error';
|
||||||
|
|
||||||
export type BuildWorkflowArg = {
|
export type BuildWorkflowArg = {
|
||||||
@ -30,7 +31,7 @@ const workflowKeys = [
|
|||||||
type BuildWorkflowFunction = (arg: BuildWorkflowArg) => WorkflowV3;
|
type BuildWorkflowFunction = (arg: BuildWorkflowArg) => WorkflowV3;
|
||||||
|
|
||||||
export const buildWorkflowFast: BuildWorkflowFunction = ({ nodes, edges, workflow }: BuildWorkflowArg): WorkflowV3 => {
|
export const buildWorkflowFast: BuildWorkflowFunction = ({ nodes, edges, workflow }: BuildWorkflowArg): WorkflowV3 => {
|
||||||
const clonedWorkflow = pick(cloneDeep(workflow), workflowKeys);
|
const clonedWorkflow = pick(deepClone(workflow), workflowKeys);
|
||||||
|
|
||||||
const newWorkflow: WorkflowV3 = {
|
const newWorkflow: WorkflowV3 = {
|
||||||
...clonedWorkflow,
|
...clonedWorkflow,
|
||||||
@ -43,14 +44,14 @@ export const buildWorkflowFast: BuildWorkflowFunction = ({ nodes, edges, workflo
|
|||||||
newWorkflow.nodes.push({
|
newWorkflow.nodes.push({
|
||||||
id: node.id,
|
id: node.id,
|
||||||
type: node.type,
|
type: node.type,
|
||||||
data: cloneDeep(node.data),
|
data: deepClone(node.data),
|
||||||
position: { ...node.position },
|
position: { ...node.position },
|
||||||
});
|
});
|
||||||
} else if (isNotesNode(node) && node.type) {
|
} else if (isNotesNode(node) && node.type) {
|
||||||
newWorkflow.nodes.push({
|
newWorkflow.nodes.push({
|
||||||
id: node.id,
|
id: node.id,
|
||||||
type: node.type,
|
type: node.type,
|
||||||
data: cloneDeep(node.data),
|
data: deepClone(node.data),
|
||||||
position: { ...node.position },
|
position: { ...node.position },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { $store } from 'app/store/nanostores/store';
|
import { $store } from 'app/store/nanostores/store';
|
||||||
|
import { deepClone } from 'common/util/deepClone';
|
||||||
import { WorkflowMigrationError, WorkflowVersionError } from 'features/nodes/types/error';
|
import { WorkflowMigrationError, WorkflowVersionError } from 'features/nodes/types/error';
|
||||||
import type { FieldType } from 'features/nodes/types/field';
|
import type { FieldType } from 'features/nodes/types/field';
|
||||||
import type { InvocationNodeData } from 'features/nodes/types/invocation';
|
import type { InvocationNodeData } from 'features/nodes/types/invocation';
|
||||||
@ -11,7 +12,7 @@ import { zWorkflowV2 } from 'features/nodes/types/v2/workflow';
|
|||||||
import type { WorkflowV3 } from 'features/nodes/types/workflow';
|
import type { WorkflowV3 } from 'features/nodes/types/workflow';
|
||||||
import { zWorkflowV3 } from 'features/nodes/types/workflow';
|
import { zWorkflowV3 } from 'features/nodes/types/workflow';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import { cloneDeep, forEach } from 'lodash-es';
|
import { forEach } from 'lodash-es';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -89,7 +90,7 @@ export const parseAndMigrateWorkflow = (data: unknown): WorkflowV3 => {
|
|||||||
throw new WorkflowVersionError(t('nodes.unableToGetWorkflowVersion'));
|
throw new WorkflowVersionError(t('nodes.unableToGetWorkflowVersion'));
|
||||||
}
|
}
|
||||||
|
|
||||||
let workflow = cloneDeep(data) as WorkflowV1 | WorkflowV2 | WorkflowV3;
|
let workflow = deepClone(data) as WorkflowV1 | WorkflowV2 | WorkflowV3;
|
||||||
|
|
||||||
if (workflow.meta.version === '1.0.0') {
|
if (workflow.meta.version === '1.0.0') {
|
||||||
const v1 = zWorkflowV1.parse(workflow);
|
const v1 = zWorkflowV1.parse(workflow);
|
||||||
|
Loading…
Reference in New Issue
Block a user