diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index 7848c52b92..81fc9c4dd3 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -94,6 +94,7 @@ "reactflow": "^11.10.4", "redux-dynamic-middlewares": "^2.2.0", "redux-remember": "^5.1.0", + "rfdc": "^1.3.1", "roarr": "^7.21.1", "serialize-error": "^11.0.3", "socket.io-client": "^4.7.5", diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index 377aae46ad..4be16619ec 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -137,6 +137,9 @@ dependencies: redux-remember: specifier: ^5.1.0 version: 5.1.0(redux@5.0.1) + rfdc: + specifier: ^1.3.1 + version: 1.3.1 roarr: specifier: ^7.21.1 version: 7.21.1 @@ -12128,6 +12131,10 @@ packages: resolution: {integrity: sha512-/x8uIPdTafBqakK0TmPNJzgkLP+3H+yxpUJhCQHsLBg1rYEVNR2D8BRYNWQhVBjyOd7oo1dZRVzIkwMY2oqfYQ==} dev: true + /rfdc@1.3.1: + resolution: {integrity: sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==} + dev: false + /rimraf@2.6.3: resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} hasBin: true diff --git a/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts b/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts index ed8c82d91c..508109caf5 100644 --- a/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts +++ b/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts @@ -1,7 +1,7 @@ import type { UnknownAction } from '@reduxjs/toolkit'; +import { deepClone } from 'common/util/deepClone'; import { isAnyGraphBuilt } from 'features/nodes/store/actions'; import { nodeTemplatesBuilt } from 'features/nodes/store/nodesSlice'; -import { cloneDeep } from 'lodash-es'; import { appInfoApi } from 'services/api/endpoints/appInfo'; import type { Graph } from 'services/api/types'; import { socketGeneratorProgress } from 'services/events/actions'; @@ -33,7 +33,7 @@ export const actionSanitizer = (action: A): A => { } if (socketGeneratorProgress.match(action)) { - const sanitized = cloneDeep(action); + const sanitized = deepClone(action); if (sanitized.payload.data.progress_image) { sanitized.payload.data.progress_image.dataURL = ''; } diff --git a/invokeai/frontend/web/src/common/components/OverlayScrollbars/constants.ts b/invokeai/frontend/web/src/common/components/OverlayScrollbars/constants.ts index 4ec4b5620d..d72d20e846 100644 --- a/invokeai/frontend/web/src/common/components/OverlayScrollbars/constants.ts +++ b/invokeai/frontend/web/src/common/components/OverlayScrollbars/constants.ts @@ -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 type { UseOverlayScrollbarsParams } from 'overlayscrollbars-react'; @@ -22,7 +23,7 @@ export const getOverlayScrollbarsParams = ( overflowX: 'hidden' | 'scroll' = 'hidden', overflowY: 'hidden' | 'scroll' = 'scroll' ) => { - const params = cloneDeep(overlayScrollbarsParams); + const params = deepClone(overlayScrollbarsParams); merge(params, { options: { overflow: { y: overflowY, x: overflowX } } }); return params; }; diff --git a/invokeai/frontend/web/src/common/util/deepClone.ts b/invokeai/frontend/web/src/common/util/deepClone.ts new file mode 100644 index 0000000000..211fc6c5b4 --- /dev/null +++ b/invokeai/frontend/web/src/common/util/deepClone.ts @@ -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 = (obj: T): T => _rfdc(obj); diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts index e3e24962f3..441d377b69 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts @@ -1,6 +1,7 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; +import { deepClone } from 'common/util/deepClone'; import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple'; import calculateCoordinates from 'features/canvas/util/calculateCoordinates'; 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 { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension'; 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 { queueApi } from 'services/api/endpoints/queue'; import type { ImageDTO } from 'services/api/types'; @@ -166,7 +167,7 @@ export const canvasSlice = createSlice({ pushToPrevLayerStates(state); state.layerState = { - ...cloneDeep(initialLayerState), + ...deepClone(initialLayerState), objects: [ { kind: 'image', @@ -277,7 +278,7 @@ export const canvasSlice = createSlice({ discardStagedImages: (state) => { pushToPrevLayerStates(state); - state.layerState.stagingArea = cloneDeep(cloneDeep(initialLayerState)).stagingArea; + state.layerState.stagingArea = deepClone(initialLayerState.stagingArea); state.futureLayerStates = []; state.shouldShowStagingOutline = true; @@ -414,7 +415,7 @@ export const canvasSlice = createSlice({ }, resetCanvas: (state) => { pushToPrevLayerStates(state); - state.layerState = cloneDeep(initialLayerState); + state.layerState = deepClone(initialLayerState); state.futureLayerStates = []; state.batchIds = []; state.boundingBoxCoordinates = { @@ -517,7 +518,7 @@ export const canvasSlice = createSlice({ ...imageToCommit, }); } - state.layerState.stagingArea = cloneDeep(initialLayerState).stagingArea; + state.layerState.stagingArea = deepClone(initialLayerState.stagingArea); state.futureLayerStates = []; state.shouldShowStagingOutline = true; @@ -709,14 +710,14 @@ export const canvasPersistConfig: PersistConfig = { }; const pushToPrevLayerStates = (state: CanvasState) => { - state.pastLayerStates.push(cloneDeep(state.layerState)); + state.pastLayerStates.push(deepClone(state.layerState)); if (state.pastLayerStates.length > MAX_HISTORY) { state.pastLayerStates = state.pastLayerStates.slice(-MAX_HISTORY); } }; const pushToFutureLayerStates = (state: CanvasState) => { - state.futureLayerStates.unshift(cloneDeep(state.layerState)); + state.futureLayerStates.unshift(deepClone(state.layerState)); if (state.futureLayerStates.length > MAX_HISTORY) { state.futureLayerStates = state.futureLayerStates.slice(0, MAX_HISTORY); } diff --git a/invokeai/frontend/web/src/features/controlAdapters/store/controlAdaptersSlice.ts b/invokeai/frontend/web/src/features/controlAdapters/store/controlAdaptersSlice.ts index 39dc0dce3d..f4edca41bb 100644 --- a/invokeai/frontend/web/src/features/controlAdapters/store/controlAdaptersSlice.ts +++ b/invokeai/frontend/web/src/features/controlAdapters/store/controlAdaptersSlice.ts @@ -2,10 +2,11 @@ import type { PayloadAction, Update } from '@reduxjs/toolkit'; import { createEntityAdapter, createSlice, isAnyOf } from '@reduxjs/toolkit'; import { getSelectorsOptions } from 'app/store/createMemoizedSelector'; import type { PersistConfig, RootState } from 'app/store/store'; +import { deepClone } from 'common/util/deepClone'; import { buildControlAdapter } from 'features/controlAdapters/util/buildControlAdapter'; import { buildControlAdapterProcessor } from 'features/controlAdapters/util/buildControlAdapterProcessor'; 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 { socketInvocationError } from 'services/events/actions'; import { v4 as uuidv4 } from 'uuid'; @@ -114,7 +115,7 @@ export const controlAdaptersSlice = createSlice({ if (!controlAdapter) { return; } - const newControlAdapter = merge(cloneDeep(controlAdapter), { + const newControlAdapter = merge(deepClone(controlAdapter), { id: newId, isEnabled: true, }); @@ -270,7 +271,7 @@ export const controlAdaptersSlice = createSlice({ return; } - const processorNode = merge(cloneDeep(cn.processorNode), params); + const processorNode = merge(deepClone(cn.processorNode), params); caAdapter.updateOne(state, { id, @@ -293,7 +294,7 @@ export const controlAdaptersSlice = createSlice({ return; } - const processorNode = cloneDeep( + const processorNode = deepClone( CONTROLNET_PROCESSORS[processorType].buildDefaults(cn.model?.base) ) as RequiredControlAdapterProcessorNode; @@ -333,7 +334,7 @@ export const controlAdaptersSlice = createSlice({ caAdapter.updateOne(state, update); }, controlAdaptersReset: () => { - return cloneDeep(initialControlAdaptersState); + return deepClone(initialControlAdaptersState); }, pendingControlImagesCleared: (state) => { state.pendingControlImages = []; @@ -406,7 +407,7 @@ const migrateControlAdaptersState = (state: any): any => { state._version = 1; } if (state._version === 1) { - state = cloneDeep(initialControlAdaptersState); + state = deepClone(initialControlAdaptersState); } return state; }; diff --git a/invokeai/frontend/web/src/features/controlAdapters/util/buildControlAdapter.ts b/invokeai/frontend/web/src/features/controlAdapters/util/buildControlAdapter.ts index 94a867cf88..d4796572d4 100644 --- a/invokeai/frontend/web/src/features/controlAdapters/util/buildControlAdapter.ts +++ b/invokeai/frontend/web/src/features/controlAdapters/util/buildControlAdapter.ts @@ -1,3 +1,4 @@ +import { deepClone } from 'common/util/deepClone'; import { CONTROLNET_PROCESSORS } from 'features/controlAdapters/store/constants'; import type { ControlAdapterConfig, @@ -7,7 +8,7 @@ import type { RequiredCannyImageProcessorInvocation, T2IAdapterConfig, } from 'features/controlAdapters/store/types'; -import { cloneDeep, merge } from 'lodash-es'; +import { merge } from 'lodash-es'; export const initialControlNet: Omit = { type: 'controlnet', @@ -57,11 +58,11 @@ export const buildControlAdapter = ( ): ControlAdapterConfig => { switch (type) { case 'controlnet': - return merge(cloneDeep(initialControlNet), { id, ...overrides }); + return merge(deepClone(initialControlNet), { id, ...overrides }); case 't2i_adapter': - return merge(cloneDeep(initialT2IAdapter), { id, ...overrides }); + return merge(deepClone(initialT2IAdapter), { id, ...overrides }); case 'ip_adapter': - return merge(cloneDeep(initialIPAdapter), { id, ...overrides }); + return merge(deepClone(initialIPAdapter), { id, ...overrides }); default: throw new Error(`Unknown control adapter type: ${type}`); } diff --git a/invokeai/frontend/web/src/features/lora/store/loraSlice.ts b/invokeai/frontend/web/src/features/lora/store/loraSlice.ts index 250142ec4a..2382e9ffe4 100644 --- a/invokeai/frontend/web/src/features/lora/store/loraSlice.ts +++ b/invokeai/frontend/web/src/features/lora/store/loraSlice.ts @@ -1,9 +1,9 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; +import { deepClone } from 'common/util/deepClone'; import { zModelIdentifierField } from 'features/nodes/types/common'; import type { ParameterLoRAModel } from 'features/parameters/types/parameterSchemas'; -import { cloneDeep } from 'lodash-es'; import type { LoRAModelConfig } from 'services/api/types'; export type LoRA = { @@ -58,7 +58,7 @@ export const loraSlice = createSlice({ } lora.isEnabled = isEnabled; }, - lorasReset: () => cloneDeep(initialLoraState), + lorasReset: () => deepClone(initialLoraState), }, }); @@ -74,7 +74,7 @@ const migrateLoRAState = (state: any): any => { } if (state._version === 1) { // Model type has changed, so we need to reset the state - too risky to migrate - state = cloneDeep(initialLoraState); + state = deepClone(initialLoraState); } return state; }; diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 8c5f894548..4a1b438271 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -1,6 +1,7 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice, isAnyOf } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; +import { deepClone } from 'common/util/deepClone'; import { workflowLoaded } from 'features/nodes/store/actions'; import { SHARED_NODE_PROPERTIES } from 'features/nodes/types/constants'; import type { @@ -44,7 +45,7 @@ import { } from 'features/nodes/types/field'; import type { AnyNode, InvocationTemplate, NodeExecutionState } 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 { Connection, Edge, @@ -571,8 +572,23 @@ export const nodesSlice = createSlice({ ); }, selectionCopied: (state) => { - state.nodesToCopy = state.nodes.filter((n) => n.selected).map(cloneDeep); - state.edgesToCopy = state.edges.filter((e) => e.selected).map(cloneDeep); + const nodesToCopy: AnyNode[] = []; + 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) { const averagePosition = { x: 0, y: 0 }; @@ -594,11 +610,21 @@ export const nodesSlice = createSlice({ }, selectionPasted: (state, action: PayloadAction<{ cursorPosition?: XYPosition }>) => { 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 newEdges = state.edgesToCopy - .filter((e) => oldNodeIds.includes(e.source) && oldNodeIds.includes(e.target)) - .map(cloneDeep); + + const newEdges: Edge[] = []; + + 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)); diff --git a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts index fcd4b5e1ac..6293d3cce5 100644 --- a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts @@ -1,6 +1,7 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; +import { deepClone } from 'common/util/deepClone'; import { workflowLoaded } from 'features/nodes/store/actions'; import { isAnyNodeOrEdgeMutation, nodeEditorReset, nodesChanged, nodesDeleted } from 'features/nodes/store/nodesSlice'; import type { @@ -11,7 +12,7 @@ import type { import type { FieldIdentifier } from 'features/nodes/types/field'; import { isInvocationNode } from 'features/nodes/types/invocation'; 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 = { name: '', @@ -131,8 +132,8 @@ export const workflowSlice = createSlice({ }); return { - ...cloneDeep(initialWorkflowState), - ...cloneDeep(workflowExtra), + ...deepClone(initialWorkflowState), + ...deepClone(workflowExtra), originalExposedFieldValues, 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) => { // Not all changes to nodes should result in the workflow being marked touched diff --git a/invokeai/frontend/web/src/features/nodes/util/node/nodeUpdate.ts b/invokeai/frontend/web/src/features/nodes/util/node/nodeUpdate.ts index f720bdd188..7fa5e1552d 100644 --- a/invokeai/frontend/web/src/features/nodes/util/node/nodeUpdate.ts +++ b/invokeai/frontend/web/src/features/nodes/util/node/nodeUpdate.ts @@ -1,8 +1,9 @@ +import { deepClone } from 'common/util/deepClone'; import { satisfies } from 'compare-versions'; import { NodeUpdateError } from 'features/nodes/types/error'; import type { InvocationNode, InvocationTemplate } from 'features/nodes/types/invocation'; 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'; @@ -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 // 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. - const clone = cloneDeep(node); + const clone = deepClone(node); clone.data.version = template.version; defaultsDeep(clone, defaults); // mutates! diff --git a/invokeai/frontend/web/src/features/nodes/util/workflow/buildWorkflow.ts b/invokeai/frontend/web/src/features/nodes/util/workflow/buildWorkflow.ts index c242eba1f4..b164dde90e 100644 --- a/invokeai/frontend/web/src/features/nodes/util/workflow/buildWorkflow.ts +++ b/invokeai/frontend/web/src/features/nodes/util/workflow/buildWorkflow.ts @@ -1,11 +1,12 @@ import { logger } from 'app/logging/logger'; +import { deepClone } from 'common/util/deepClone'; import { parseify } from 'common/util/serialize'; import type { NodesState, WorkflowsState } from 'features/nodes/store/types'; import { isInvocationNode, isNotesNode } from 'features/nodes/types/invocation'; import type { WorkflowV3 } from 'features/nodes/types/workflow'; import { zWorkflowV3 } from 'features/nodes/types/workflow'; import i18n from 'i18n'; -import { cloneDeep, pick } from 'lodash-es'; +import { pick } from 'lodash-es'; import { fromZodError } from 'zod-validation-error'; export type BuildWorkflowArg = { @@ -30,7 +31,7 @@ const workflowKeys = [ type BuildWorkflowFunction = (arg: 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 = { ...clonedWorkflow, @@ -43,14 +44,14 @@ export const buildWorkflowFast: BuildWorkflowFunction = ({ nodes, edges, workflo newWorkflow.nodes.push({ id: node.id, type: node.type, - data: cloneDeep(node.data), + data: deepClone(node.data), position: { ...node.position }, }); } else if (isNotesNode(node) && node.type) { newWorkflow.nodes.push({ id: node.id, type: node.type, - data: cloneDeep(node.data), + data: deepClone(node.data), position: { ...node.position }, }); } diff --git a/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts b/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts index 559c524808..56fb04d61d 100644 --- a/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts +++ b/invokeai/frontend/web/src/features/nodes/util/workflow/migrations.ts @@ -1,4 +1,5 @@ import { $store } from 'app/store/nanostores/store'; +import { deepClone } from 'common/util/deepClone'; import { WorkflowMigrationError, WorkflowVersionError } from 'features/nodes/types/error'; import type { FieldType } from 'features/nodes/types/field'; 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 { zWorkflowV3 } from 'features/nodes/types/workflow'; import { t } from 'i18next'; -import { cloneDeep, forEach } from 'lodash-es'; +import { forEach } from 'lodash-es'; import { z } from 'zod'; /** @@ -89,7 +90,7 @@ export const parseAndMigrateWorkflow = (data: unknown): WorkflowV3 => { 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') { const v1 = zWorkflowV1.parse(workflow);