feat(ui): migrate from lodash.isEqual to objectEquals

This commit is contained in:
psychedelicious
2025-06-25 18:36:17 +10:00
parent 7aefa8f36b
commit b5acc204a8
19 changed files with 76 additions and 50 deletions

View File

@@ -33,6 +33,18 @@ module.exports = {
'The Clipboard API is not available by default in Firefox. Use the `useClipboard` hook instead, which wraps clipboard access to prevent errors.',
},
],
'no-restricted-imports': [
'error',
{
paths: [
{
name: 'lodash-es',
importNames: ['isEqual'],
message: 'Please use objectEquals from @observ33r/object-equals instead.',
},
],
},
],
},
overrides: [
/**

View File

@@ -60,7 +60,8 @@
"@fontsource-variable/inter": "^5.2.5",
"@invoke-ai/ui-library": "^0.0.46",
"@nanostores/react": "^1.0.0",
"@reduxjs/toolkit": "2.8.2",
"@observ33r/object-equals": "^1.1.4",
"@reduxjs/toolkit": "2.7.0",
"@roarr/browser-log-writer": "^1.3.0",
"@xyflow/react": "^12.6.0",
"async-mutex": "^0.5.0",

View File

@@ -29,9 +29,12 @@ dependencies:
'@nanostores/react':
specifier: ^1.0.0
version: 1.0.0(nanostores@1.0.1)(react@18.3.1)
'@observ33r/object-equals':
specifier: ^1.1.4
version: 1.1.4
'@reduxjs/toolkit':
specifier: 2.8.2
version: 2.8.2(react-redux@9.2.0)(react@18.3.1)
specifier: 2.7.0
version: 2.7.0(react-redux@9.2.0)(react@18.3.1)
'@roarr/browser-log-writer':
specifier: ^1.3.0
version: 1.3.0
@@ -1853,6 +1856,10 @@ packages:
fastq: 1.17.1
dev: true
/@observ33r/object-equals@1.1.4:
resolution: {integrity: sha512-a46ys2Zvyyu1NPo8C8mF6FLztVxxaBtXpZwxlQutaaRtQFcD71yTMwyPY4DOuHsz//YEZjLkCw+mJoKDiG/CgA==}
dev: false
/@pkgjs/parseargs@0.11.0:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
@@ -2164,8 +2171,8 @@ packages:
- supports-color
dev: true
/@reduxjs/toolkit@2.8.2(react-redux@9.2.0)(react@18.3.1):
resolution: {integrity: sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==}
/@reduxjs/toolkit@2.7.0(react-redux@9.2.0)(react@18.3.1):
resolution: {integrity: sha512-XVwolG6eTqwV0N8z/oDlN93ITCIGIop6leXlGJI/4EKy+0POYkR+ABHRSdGXY+0MQvJBP8yAzh+EYFxTuvmBiQ==}
peerDependencies:
react: ^16.9.0 || ^17.0.0 || ^18 || ^19
react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
@@ -3098,7 +3105,7 @@ packages:
/@types/lodash.mergewith@4.6.7:
resolution: {integrity: sha512-3m+lkO5CLRRYU0fhGRp7zbsGi6+BZj0uTVSwvcKU+nSlhjA9/QRNfuSGnD2mX6hQA7ZbmcCkzk5h4ZYGOtk14A==}
dependencies:
'@types/lodash': 4.17.16
'@types/lodash': 4.17.18
dev: false
/@types/lodash.mergewith@4.6.9:
@@ -3110,8 +3117,8 @@ packages:
/@types/lodash@4.17.10:
resolution: {integrity: sha512-YpS0zzoduEhuOWjAotS6A5AVCva7X4lVlYLF0FYHAY9sdraBfnatttHItlWeZdGhuEkf+OzMNg2ZYAx8t+52uQ==}
/@types/lodash@4.17.16:
resolution: {integrity: sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==}
/@types/lodash@4.17.18:
resolution: {integrity: sha512-KJ65INaxqxmU6EoCiJmRPZC9H9RVWCRd349tXM2M3O5NA7cY6YL7c0bHAHQ93NOfTObEQ004kd2QVHs/r0+m4g==}
dev: false
/@types/mdx@2.0.13:

View File

@@ -1,13 +1,13 @@
import { objectEquals } from '@observ33r/object-equals';
import { createDraftSafeSelectorCreator, createSelectorCreator, lruMemoize } from '@reduxjs/toolkit';
import { isEqual } from 'lodash-es';
/**
* A memoized selector creator that uses LRU cache and lodash's isEqual for equality check.
* A memoized selector creator that uses LRU cache and @observ33r/object-equals's objectEquals for equality check.
*/
export const createMemoizedSelector = createSelectorCreator({
memoize: lruMemoize,
memoizeOptions: {
resultEqualityCheck: isEqual,
resultEqualityCheck: objectEquals,
},
argsMemoize: lruMemoize,
});

View File

@@ -1,8 +1,8 @@
import { objectEquals } from '@observ33r/object-equals';
import { createAction } from '@reduxjs/toolkit';
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { $baseUrl } from 'app/store/nanostores/baseUrl';
import { isEqual } from 'lodash-es';
import { atom } from 'nanostores';
import { api } from 'services/api';
import { modelsApi } from 'services/api/endpoints/models';
@@ -64,7 +64,7 @@ export const addSocketConnectedEventListener = (startAppListening: AppStartListe
const nextQueueStatusData = await queueStatusRequest.unwrap();
// If the queue hasn't changed, we don't need to do anything.
if (isEqual(prevQueueStatusData?.queue, nextQueueStatusData.queue)) {
if (objectEquals(prevQueueStatusData?.queue, nextQueueStatusData.queue)) {
return;
}

View File

@@ -1,3 +1,4 @@
import { objectEquals } from '@observ33r/object-equals';
import { Mutex } from 'async-mutex';
import { deepClone } from 'common/util/deepClone';
import { withResultAsync } from 'common/util/result';
@@ -12,7 +13,6 @@ import { getKonvaNodeDebugAttrs, loadImage } from 'features/controlLayers/konva/
import type { CanvasImageState } from 'features/controlLayers/store/types';
import { t } from 'i18next';
import Konva from 'konva';
import { isEqual } from 'lodash-es';
import type { Logger } from 'roarr';
import { getImageDTOSafe } from 'services/api/endpoints/images';
@@ -198,7 +198,7 @@ export class CanvasObjectImage extends CanvasModuleBase {
const { image } = state;
const { width, height } = image;
if (force || (!isEqual(this.state, state) && !this.isLoading)) {
if (force || (!objectEquals(this.state, state) && !this.isLoading)) {
const release = await this.mutex.acquire();
try {

View File

@@ -1,3 +1,4 @@
import { objectEquals } from '@observ33r/object-equals';
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSelector, createSlice } from '@reduxjs/toolkit';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
@@ -6,7 +7,7 @@ import { getPrefixedId } from 'features/controlLayers/konva/util';
import { canvasMetadataRecalled } from 'features/controlLayers/store/canvasSlice';
import type { FLUXReduxImageInfluence, RefImagesState } from 'features/controlLayers/store/types';
import { zModelIdentifierField } from 'features/nodes/types/common';
import { clamp, isEqual } from 'lodash-es';
import { clamp } from 'lodash-es';
import type { ApiModelConfig, FLUXReduxModelConfig, ImageDTO, IPAdapterModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
import type { PartialDeep } from 'type-fest';
@@ -102,7 +103,7 @@ export const refImagesSlice = createSlice({
return;
}
if (isEqual(oldModel, entity.config.model)) {
if (objectEquals(oldModel, entity.config.model)) {
// Nothing changed, so we don't need to do anything
return;
}

View File

@@ -1,7 +1,8 @@
import { objectEquals } from '@observ33r/object-equals';
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { isEqual, uniq } from 'lodash-es';
import { uniq } from 'lodash-es';
import type { BoardRecordOrderBy } from 'services/api/types';
import type { BoardId, ComparisonMode, GalleryState, GalleryView, OrderDir } from './types';
@@ -53,7 +54,7 @@ export const gallerySlice = createSlice({
}
// If the selected image is different from the current selection, clear the selection and select the new image
if (!isEqual(state.selection[0], selectedImageName)) {
if (state.selection[0] !== selectedImageName) {
state.selection = [selectedImageName];
return;
}
@@ -74,7 +75,7 @@ export const gallerySlice = createSlice({
}
// If the new selection is different, update the selection
if (!isEqual(newSelection, state.selection)) {
if (!objectEquals(newSelection, state.selection)) {
state.selection = newSelection;
return;
}

View File

@@ -1,10 +1,10 @@
import { objectEquals } from '@observ33r/object-equals';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplateOrThrow';
import { fieldValueReset } from 'features/nodes/store/nodesSlice';
import { selectNodesSlice } from 'features/nodes/store/selectors';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { isEqual } from 'lodash-es';
import { useCallback, useMemo } from 'react';
export const useInputFieldDefaultValue = (nodeId: string, fieldName: string) => {
@@ -19,7 +19,7 @@ export const useInputFieldDefaultValue = (nodeId: string, fieldName: string) =>
return;
}
const value = node.data.inputs[fieldName]?.value;
return !isEqual(value, fieldTemplate.default);
return !objectEquals(value, fieldTemplate.default);
}),
[fieldName, fieldTemplate.default, nodeId]
);

View File

@@ -1,9 +1,9 @@
import { objectEquals } from '@observ33r/object-equals';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { fieldValueReset } from 'features/nodes/store/nodesSlice';
import { selectNodesSlice } from 'features/nodes/store/selectors';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { isEqual } from 'lodash-es';
import { useCallback, useMemo } from 'react';
const uniqueNonexistentValue = Symbol('uniqueNonexistentValue');
@@ -32,7 +32,7 @@ export const useInputFieldInitialFormValue = (elementId: string, nodeId: string,
return;
}
const value = node.data.inputs[fieldName]?.value;
return !isEqual(value, initialValue);
return !objectEquals(value, initialValue);
}),
[fieldName, initialValue, nodeId]
);

View File

@@ -1,3 +1,4 @@
import { objectEquals } from '@observ33r/object-equals';
import type { EdgeChange, NodeChange } from '@xyflow/react';
import { logger } from 'app/logging/logger';
import { getStore } from 'app/store/nanostores/store';
@@ -16,7 +17,7 @@ import { findUnoccupiedPosition } from 'features/nodes/store/util/findUnoccupied
import { validateConnection } from 'features/nodes/store/util/validateConnection';
import type { AnyEdge, AnyNode } from 'features/nodes/types/invocation';
import { t } from 'i18next';
import { isEqual, uniqWith } from 'lodash-es';
import { uniqWith } from 'lodash-es';
import { v4 as uuidv4 } from 'uuid';
const log = logger('workflows');
@@ -44,7 +45,7 @@ const _pasteSelection = (withEdgesToCopiedNodes?: boolean) => {
if (withEdgesToCopiedNodes) {
const edgesToCopiedNodes = deepClone($edgesToCopiedNodes.get());
copiedEdges = uniqWith([...copiedEdges, ...edgesToCopiedNodes], isEqual);
copiedEdges = uniqWith([...copiedEdges, ...edgesToCopiedNodes], objectEquals);
}
// Calculate an offset to reposition nodes to surround the cursor position, maintaining relative positioning

View File

@@ -1,5 +1,6 @@
import { objectEquals } from '@observ33r/object-equals';
import type { FieldType } from 'features/nodes/types/field';
import { isEqual, omit } from 'lodash-es';
import { omit } from 'lodash-es';
/**
* Checks if two types are equal. If the field types have original types, those are also compared. Any match is
@@ -13,16 +14,16 @@ export const areTypesEqual = (firstType: FieldType, secondType: FieldType) => {
const _secondType = 'originalType' in secondType ? omit(secondType, 'originalType') : secondType;
const _originalFirstType = 'originalType' in firstType ? firstType.originalType : null;
const _originalSecondType = 'originalType' in secondType ? secondType.originalType : null;
if (isEqual(_firstType, _secondType)) {
if (objectEquals(_firstType, _secondType)) {
return true;
}
if (_originalSecondType && isEqual(_firstType, _originalSecondType)) {
if (_originalSecondType && objectEquals(_firstType, _originalSecondType)) {
return true;
}
if (_originalFirstType && isEqual(_originalFirstType, _secondType)) {
if (_originalFirstType && objectEquals(_originalFirstType, _secondType)) {
return true;
}
if (_originalFirstType && _originalSecondType && isEqual(_originalFirstType, _originalSecondType)) {
if (_originalFirstType && _originalSecondType && objectEquals(_originalFirstType, _originalSecondType)) {
return true;
}
return false;

View File

@@ -1,6 +1,7 @@
import { objectEquals } from '@observ33r/object-equals';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import { type ModelIdentifierField, zModelIdentifierField } from 'features/nodes/types/common';
import { forEach, groupBy, isEqual, unset, values } from 'lodash-es';
import { forEach, groupBy, unset, values } from 'lodash-es';
import type {
AnyInvocation,
AnyInvocationIncMetadata,
@@ -169,7 +170,7 @@ export class Graph {
source: { node_id: fromNode.id, field: fromField },
destination: { node_id: toNode.id, field: toField },
};
const edgeAlreadyExists = this._graph.edges.some((e) => isEqual(e, edge));
const edgeAlreadyExists = this._graph.edges.some((e) => objectEquals(e, edge));
assert(!edgeAlreadyExists, `Edge ${Graph.edgeToString(edge)} already exists`);
this._graph.edges.push(edge);
return edge;
@@ -182,7 +183,7 @@ export class Graph {
* @raises `AssertionError` if an edge with the same source and destination already exists.
*/
addEdgeFromObj(edge: Edge): Edge {
const edgeAlreadyExists = this._graph.edges.some((e) => isEqual(e, edge));
const edgeAlreadyExists = this._graph.edges.some((e) => objectEquals(e, edge));
assert(!edgeAlreadyExists, `Edge ${Graph.edgeToString(edge)} already exists`);
this._graph.edges.push(edge);
return edge;
@@ -275,11 +276,11 @@ export class Graph {
}
/**
* INTERNAL: Delete _all_ matching edges from the graph. Uses _.isEqual for comparison.
* INTERNAL: Delete _all_ matching edges from the graph. Uses _.objectEquals for comparison.
* @param edge The edge to delete
*/
private _deleteEdge(edge: Edge): void {
this._graph.edges = this._graph.edges.filter((e) => !isEqual(e, edge));
this._graph.edges = this._graph.edges.filter((e) => !objectEquals(e, edge));
}
/**
@@ -317,7 +318,7 @@ export class Graph {
this.getNode(edge.source.node_id);
this.getNode(edge.destination.node_id);
assert(
!this._graph.edges.filter((e) => e !== edge).find((e) => isEqual(e, edge)),
!this._graph.edges.filter((e) => e !== edge).find((e) => objectEquals(e, edge)),
`Duplicate edge: ${Graph.edgeToString(edge)}`
);
}

View File

@@ -1,3 +1,4 @@
import { objectEquals } from '@observ33r/object-equals';
import type { RootState } from 'app/store/store';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getPrefixedId } from 'features/controlLayers/konva/util';
@@ -6,7 +7,6 @@ import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import type { Dimensions } from 'features/controlLayers/store/types';
import type { Graph } from 'features/nodes/util/graph/generation/Graph';
import { isEqual } from 'lodash-es';
import type { Invocation } from 'services/api/types';
type AddFLUXFillArg = {
@@ -52,7 +52,7 @@ export const addFLUXFill = async ({
const fluxFill = g.addNode({ type: 'flux_fill', id: getPrefixedId('flux_fill') });
const needsScaleBeforeProcessing = !isEqual(scaledSize, originalSize);
const needsScaleBeforeProcessing = !objectEquals(scaledSize, originalSize);
if (needsScaleBeforeProcessing) {
// Scale before processing requires some resizing

View File

@@ -1,3 +1,4 @@
import { objectEquals } from '@observ33r/object-equals';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import type { CanvasState, Dimensions } from 'features/controlLayers/store/types';
@@ -8,7 +9,6 @@ import type {
MainModelLoaderNodes,
VaeSourceNodes,
} from 'features/nodes/util/graph/types';
import { isEqual } from 'lodash-es';
import type { Invocation } from 'services/api/types';
type AddImageToImageArg = {
@@ -45,7 +45,7 @@ export const addImageToImage = async ({
silent: true,
});
if (!isEqual(scaledSize, originalSize)) {
if (!objectEquals(scaledSize, originalSize)) {
// Resize the initial image to the scaled size, denoise, then resize back to the original size
const resizeImageToScaledSize = g.addNode({
type: 'img_resize',

View File

@@ -1,3 +1,4 @@
import { objectEquals } from '@observ33r/object-equals';
import type { RootState } from 'app/store/store';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getPrefixedId } from 'features/controlLayers/konva/util';
@@ -13,7 +14,6 @@ import type {
MainModelLoaderNodes,
VaeSourceNodes,
} from 'features/nodes/util/graph/types';
import { isEqual } from 'lodash-es';
import type { ImageDTO, Invocation } from 'services/api/types';
type AddInpaintArg = {
@@ -93,7 +93,7 @@ export const addInpaint = async ({
}
);
const needsScaleBeforeProcessing = !isEqual(scaledSize, originalSize);
const needsScaleBeforeProcessing = !objectEquals(scaledSize, originalSize);
if (needsScaleBeforeProcessing) {
// Scale before processing requires some resizing

View File

@@ -1,3 +1,4 @@
import { objectEquals } from '@observ33r/object-equals';
import type { RootState } from 'app/store/store';
import type { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
import { getPrefixedId } from 'features/controlLayers/konva/util';
@@ -14,7 +15,6 @@ import type {
MainModelLoaderNodes,
VaeSourceNodes,
} from 'features/nodes/util/graph/types';
import { isEqual } from 'lodash-es';
import type { ImageDTO, Invocation } from 'services/api/types';
type AddOutpaintArg = {
@@ -98,7 +98,7 @@ export const addOutpaint = async ({
const infill = getInfill(g, params);
const needsScaleBeforeProcessing = !isEqual(scaledSize, originalSize);
const needsScaleBeforeProcessing = !objectEquals(scaledSize, originalSize);
if (needsScaleBeforeProcessing) {
// Scale before processing requires some resizing

View File

@@ -1,8 +1,8 @@
import { objectEquals } from '@observ33r/object-equals';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import type { Dimensions } from 'features/controlLayers/store/types';
import type { Graph } from 'features/nodes/util/graph/generation/Graph';
import type { LatentToImageNodes } from 'features/nodes/util/graph/types';
import { isEqual } from 'lodash-es';
import type { Invocation } from 'services/api/types';
type AddTextToImageArg = {
@@ -18,7 +18,7 @@ export const addTextToImage = ({
originalSize,
scaledSize,
}: AddTextToImageArg): Invocation<'img_resize' | 'l2i' | 'flux_vae_decode' | 'sd3_l2i' | 'cogview4_l2i'> => {
if (!isEqual(scaledSize, originalSize)) {
if (!objectEquals(scaledSize, originalSize)) {
// We need to resize the output image back to the original size
const resizeImageToOriginalSize = g.addNode({
id: getPrefixedId('resize_image_to_original_size'),

View File

@@ -1,3 +1,4 @@
import { objectEquals } from '@observ33r/object-equals';
import { logger } from 'app/logging/logger';
import { deepClone } from 'common/util/deepClone';
import { parseify } from 'common/util/serialize';
@@ -17,7 +18,7 @@ import {
isInvocationSchemaObject,
} from 'features/nodes/types/openapi';
import { t } from 'i18next';
import { isEqual, reduce } from 'lodash-es';
import { reduce } from 'lodash-es';
import type { OpenAPIV3_1 } from 'openapi-types';
import { serializeError } from 'serialize-error';
import type { JsonObject } from 'type-fest';
@@ -153,7 +154,7 @@ export const parseSchema = (
return inputsAccumulator;
}
if (isStatefulFieldType(fieldType) && originalFieldType && !isEqual(originalFieldType, fieldType)) {
if (isStatefulFieldType(fieldType) && originalFieldType && !objectEquals(originalFieldType, fieldType)) {
fieldType.originalType = deepClone(originalFieldType);
}
@@ -225,7 +226,7 @@ export const parseSchema = (
return outputsAccumulator;
}
if (isStatefulFieldType(fieldType) && originalFieldType && !isEqual(originalFieldType, fieldType)) {
if (isStatefulFieldType(fieldType) && originalFieldType && !objectEquals(originalFieldType, fieldType)) {
fieldType.originalType = deepClone(originalFieldType);
}