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:
psychedelicious 2024-04-02 22:59:07 +11:00 committed by Kent Keirsey
parent a6c91979af
commit 69ec14c7bb
14 changed files with 100 additions and 43 deletions

View File

@ -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",

View File

@ -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

View File

@ -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>';
} }

View File

@ -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;
}; };

View 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);

View File

@ -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);
} }

View File

@ -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;
}; };

View File

@ -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}`);
} }

View File

@ -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;
}; };

View File

@ -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));

View File

@ -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

View File

@ -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!

View File

@ -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 },
}); });
} }

View File

@ -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);