Skip to content

Commit

Permalink
switch lodash.isEqual to fast-equals.deepEqual to improve performance
Browse files Browse the repository at this point in the history
  • Loading branch information
igorbrasileiro committed Jan 7, 2025
1 parent f5a24b2 commit b122692
Show file tree
Hide file tree
Showing 13 changed files with 747 additions and 370 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ should change the heading of the (upcoming) version to include a major version b
-->

# 5.25.0

## @rjsf/utils

- Switched uses of `lodash.isEqual()` to `fast-equals.deepEqual()` in many utility functions.

## @rjsf/validator-ajv8

- Switched uses of `lodash.isEqual()` to `fast-equals.deepEqual()` at precompiledValidator.

# 5.24.0

## @rjsf/core
Expand Down
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"react": "^16.14.0 || >=17"
},
"dependencies": {
"fast-equals": "^5.2.1",
"json-schema-merge-allof": "^0.8.1",
"jsonpointer": "^5.0.1",
"lodash": "^4.17.21",
Expand Down
20 changes: 11 additions & 9 deletions packages/utils/src/enumOptionsDeselectValue.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import isEqual from 'lodash/isEqual';
import { deepEqual } from "fast-equals";

Check failure on line 1 in packages/utils/src/enumOptionsDeselectValue.ts

View workflow job for this annotation

GitHub Actions / build (14.x)

Replace `"fast-equals"` with `'fast-equals'`

Check failure on line 1 in packages/utils/src/enumOptionsDeselectValue.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

Replace `"fast-equals"` with `'fast-equals'`

Check failure on line 1 in packages/utils/src/enumOptionsDeselectValue.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Replace `"fast-equals"` with `'fast-equals'`

import { EnumOptionsType, RJSFSchema, StrictRJSFSchema } from './types';
import enumOptionsValueForIndex from './enumOptionsValueForIndex';
import { EnumOptionsType, RJSFSchema, StrictRJSFSchema } from "./types";

Check failure on line 3 in packages/utils/src/enumOptionsDeselectValue.ts

View workflow job for this annotation

GitHub Actions / build (14.x)

Replace `"./types"` with `'./types'`

Check failure on line 3 in packages/utils/src/enumOptionsDeselectValue.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

Replace `"./types"` with `'./types'`

Check failure on line 3 in packages/utils/src/enumOptionsDeselectValue.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Replace `"./types"` with `'./types'`
import enumOptionsValueForIndex from "./enumOptionsValueForIndex";

Check failure on line 4 in packages/utils/src/enumOptionsDeselectValue.ts

View workflow job for this annotation

GitHub Actions / build (14.x)

Replace `"./enumOptionsValueForIndex"` with `'./enumOptionsValueForIndex'`

Check failure on line 4 in packages/utils/src/enumOptionsDeselectValue.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

Replace `"./enumOptionsValueForIndex"` with `'./enumOptionsValueForIndex'`

Check failure on line 4 in packages/utils/src/enumOptionsDeselectValue.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Replace `"./enumOptionsValueForIndex"` with `'./enumOptionsValueForIndex'`

/** Removes the enum option value at the `valueIndex` from the currently `selected` (list of) value(s). If `selected` is
* a list, then that list is updated to remove the enum option value with the `valueIndex` in `allEnumOptions`. If it is
Expand All @@ -15,14 +15,16 @@ import enumOptionsValueForIndex from './enumOptionsValueForIndex';
* unless `selected` is a single value. In that case, if the `valueIndex` value matches `selected`, returns
* undefined, otherwise `selected`.
*/
export default function enumOptionsDeselectValue<S extends StrictRJSFSchema = RJSFSchema>(
export default function enumOptionsDeselectValue<

Check failure on line 18 in packages/utils/src/enumOptionsDeselectValue.ts

View workflow job for this annotation

GitHub Actions / build (14.x)

Replace `⏎··S·extends·StrictRJSFSchema·=·RJSFSchema,⏎` with `S·extends·StrictRJSFSchema·=·RJSFSchema`

Check failure on line 18 in packages/utils/src/enumOptionsDeselectValue.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

Replace `⏎··S·extends·StrictRJSFSchema·=·RJSFSchema,⏎` with `S·extends·StrictRJSFSchema·=·RJSFSchema`

Check failure on line 18 in packages/utils/src/enumOptionsDeselectValue.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Replace `⏎··S·extends·StrictRJSFSchema·=·RJSFSchema,⏎` with `S·extends·StrictRJSFSchema·=·RJSFSchema`
S extends StrictRJSFSchema = RJSFSchema,
>(
valueIndex: string | number,
selected?: EnumOptionsType<S>['value'] | EnumOptionsType<S>['value'][],
allEnumOptions: EnumOptionsType<S>[] = []
): EnumOptionsType<S>['value'] | EnumOptionsType<S>['value'][] | undefined {
selected?: EnumOptionsType<S>["value"] | EnumOptionsType<S>["value"][],

Check failure on line 22 in packages/utils/src/enumOptionsDeselectValue.ts

View workflow job for this annotation

GitHub Actions / build (14.x)

Replace `"value"]·|·EnumOptionsType<S>["value"` with `'value']·|·EnumOptionsType<S>['value'`

Check failure on line 22 in packages/utils/src/enumOptionsDeselectValue.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

Replace `"value"]·|·EnumOptionsType<S>["value"` with `'value']·|·EnumOptionsType<S>['value'`

Check failure on line 22 in packages/utils/src/enumOptionsDeselectValue.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Replace `"value"]·|·EnumOptionsType<S>["value"` with `'value']·|·EnumOptionsType<S>['value'`
allEnumOptions: EnumOptionsType<S>[] = [],

Check failure on line 23 in packages/utils/src/enumOptionsDeselectValue.ts

View workflow job for this annotation

GitHub Actions / build (14.x)

Delete `,`

Check failure on line 23 in packages/utils/src/enumOptionsDeselectValue.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

Delete `,`

Check failure on line 23 in packages/utils/src/enumOptionsDeselectValue.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Delete `,`
): EnumOptionsType<S>["value"] | EnumOptionsType<S>["value"][] | undefined {

Check failure on line 24 in packages/utils/src/enumOptionsDeselectValue.ts

View workflow job for this annotation

GitHub Actions / build (14.x)

Replace `"value"]·|·EnumOptionsType<S>["value"` with `'value']·|·EnumOptionsType<S>['value'`

Check failure on line 24 in packages/utils/src/enumOptionsDeselectValue.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

Replace `"value"]·|·EnumOptionsType<S>["value"` with `'value']·|·EnumOptionsType<S>['value'`

Check failure on line 24 in packages/utils/src/enumOptionsDeselectValue.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Replace `"value"]·|·EnumOptionsType<S>["value"` with `'value']·|·EnumOptionsType<S>['value'`
const value = enumOptionsValueForIndex<S>(valueIndex, allEnumOptions);
if (Array.isArray(selected)) {
return selected.filter((v) => !isEqual(v, value));
return selected.filter((v) => !deepEqual(v, value));
}
return isEqual(value, selected) ? undefined : selected;
return deepEqual(value, selected) ? undefined : selected;
}
16 changes: 9 additions & 7 deletions packages/utils/src/enumOptionsIsSelected.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import isEqual from 'lodash/isEqual';
import { deepEqual } from "fast-equals";

Check failure on line 1 in packages/utils/src/enumOptionsIsSelected.ts

View workflow job for this annotation

GitHub Actions / build (14.x)

Replace `"fast-equals"` with `'fast-equals'`

Check failure on line 1 in packages/utils/src/enumOptionsIsSelected.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

Replace `"fast-equals"` with `'fast-equals'`

Check failure on line 1 in packages/utils/src/enumOptionsIsSelected.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Replace `"fast-equals"` with `'fast-equals'`

import { EnumOptionsType, RJSFSchema, StrictRJSFSchema } from './types';
import { EnumOptionsType, RJSFSchema, StrictRJSFSchema } from "./types";

Check failure on line 3 in packages/utils/src/enumOptionsIsSelected.ts

View workflow job for this annotation

GitHub Actions / build (14.x)

Replace `"./types"` with `'./types'`

Check failure on line 3 in packages/utils/src/enumOptionsIsSelected.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

Replace `"./types"` with `'./types'`

Check failure on line 3 in packages/utils/src/enumOptionsIsSelected.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Replace `"./types"` with `'./types'`

/** Determines whether the given `value` is (one of) the `selected` value(s).
*
* @param value - The value being checked to see if it is selected
* @param selected - The current selected value or list of values
* @returns - true if the `value` is one of the `selected` ones, false otherwise
*/
export default function enumOptionsIsSelected<S extends StrictRJSFSchema = RJSFSchema>(
value: EnumOptionsType<S>['value'],
selected: EnumOptionsType<S>['value'] | EnumOptionsType<S>['value'][]
export default function enumOptionsIsSelected<

Check failure on line 11 in packages/utils/src/enumOptionsIsSelected.ts

View workflow job for this annotation

GitHub Actions / build (14.x)

Replace `⏎··S·extends·StrictRJSFSchema·=·RJSFSchema,⏎` with `S·extends·StrictRJSFSchema·=·RJSFSchema`

Check failure on line 11 in packages/utils/src/enumOptionsIsSelected.ts

View workflow job for this annotation

GitHub Actions / build (16.x)

Replace `⏎··S·extends·StrictRJSFSchema·=·RJSFSchema,⏎` with `S·extends·StrictRJSFSchema·=·RJSFSchema`

Check failure on line 11 in packages/utils/src/enumOptionsIsSelected.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Replace `⏎··S·extends·StrictRJSFSchema·=·RJSFSchema,⏎` with `S·extends·StrictRJSFSchema·=·RJSFSchema`
S extends StrictRJSFSchema = RJSFSchema,
>(
value: EnumOptionsType<S>["value"],
selected: EnumOptionsType<S>["value"] | EnumOptionsType<S>["value"][],
) {
if (Array.isArray(selected)) {
return selected.some((sel) => isEqual(sel, value));
return selected.some((sel) => deepEqual(sel, value));
}
return isEqual(selected, value);
return deepEqual(selected, value);
}
56 changes: 36 additions & 20 deletions packages/utils/src/parser/ParserValidator.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import get from 'lodash/get';
import isEqual from 'lodash/isEqual';
import get from "lodash/get";
import { deepEqual } from "fast-equals";

import { ID_KEY } from '../constants';
import hashForSchema from '../hashForSchema';
import { ID_KEY } from "../constants";
import hashForSchema from "../hashForSchema";
import {
CustomValidator,
ErrorSchema,
Expand All @@ -14,7 +14,7 @@ import {
UiSchema,
ValidationData,
ValidatorType,
} from '../types';
} from "../types";

/** The type of the map of schema hash to schema
*/
Expand All @@ -29,9 +29,11 @@ export type SchemaMap<S extends StrictRJSFSchema = RJSFSchema> = {
* the hashed value of the schema. NOTE: After hashing the schema, an $id with the hash value is added to the
* schema IF that schema doesn't already have an $id, prior to putting the schema into the map.
*/
export default class ParserValidator<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>
implements ValidatorType<T, S, F>
{
export default class ParserValidator<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any,
> implements ValidatorType<T, S, F> {
/** The rootSchema provided during construction of the class */
readonly rootSchema: S;

Expand Down Expand Up @@ -67,11 +69,11 @@ export default class ParserValidator<T = any, S extends StrictRJSFSchema = RJSFS
const existing = this.schemaMap[key];
if (!existing) {
this.schemaMap[key] = identifiedSchema;
} else if (!isEqual(existing, identifiedSchema)) {
console.error('existing schema:', JSON.stringify(existing, null, 2));
console.error('new schema:', JSON.stringify(identifiedSchema, null, 2));
} else if (!deepEqual(existing, identifiedSchema)) {
console.error("existing schema:", JSON.stringify(existing, null, 2));
console.error("new schema:", JSON.stringify(identifiedSchema, null, 2));
throw new Error(
`Two different schemas exist with the same key ${key}! What a bad coincidence. If possible, try adding an $id to one of the schemas`
`Two different schemas exist with the same key ${key}! What a bad coincidence. If possible, try adding an $id to one of the schemas`,
);
}
}
Expand All @@ -91,8 +93,10 @@ export default class ParserValidator<T = any, S extends StrictRJSFSchema = RJSFS
* @throws - Error when the given `rootSchema` differs from the root schema provided during construction
*/
isValid(schema: S, _formData: T, rootSchema: S): boolean {
if (!isEqual(rootSchema, this.rootSchema)) {
throw new Error('Unexpectedly calling isValid() with a rootSchema that differs from the construction rootSchema');
if (!deepEqual(rootSchema, this.rootSchema)) {
throw new Error(
"Unexpectedly calling isValid() with a rootSchema that differs from the construction rootSchema",
);
}
this.addSchema(schema, hashForSchema<S>(schema));

Expand All @@ -104,17 +108,27 @@ export default class ParserValidator<T = any, S extends StrictRJSFSchema = RJSFS
* @param _schema - The schema parameter that is ignored
* @param _formData - The formData parameter that is ignored
*/
rawValidation<Result = any>(_schema: S, _formData?: T): { errors?: Result[]; validationError?: Error } {
throw new Error('Unexpectedly calling the `rawValidation()` method during schema parsing');
rawValidation<Result = any>(
_schema: S,
_formData?: T,
): { errors?: Result[]; validationError?: Error } {
throw new Error(
"Unexpectedly calling the `rawValidation()` method during schema parsing",
);
}

/** Implements the `ValidatorType` `toErrorList()` method to throw an error since it is never supposed to be called
*
* @param _errorSchema - The error schema parameter that is ignored
* @param _fieldPath - The field path parameter that is ignored
*/
toErrorList(_errorSchema?: ErrorSchema<T>, _fieldPath?: string[]): RJSFValidationError[] {
throw new Error('Unexpectedly calling the `toErrorList()` method during schema parsing');
toErrorList(
_errorSchema?: ErrorSchema<T>,
_fieldPath?: string[],
): RJSFValidationError[] {
throw new Error(
"Unexpectedly calling the `toErrorList()` method during schema parsing",
);
}

/** Implements the `ValidatorType` `validateFormData()` method to throw an error since it is never supposed to be
Expand All @@ -131,8 +145,10 @@ export default class ParserValidator<T = any, S extends StrictRJSFSchema = RJSFS
_schema: S,
_customValidate?: CustomValidator<T, S, F>,
_transformErrors?: ErrorTransformer<T, S, F>,
_uiSchema?: UiSchema<T, S, F>
_uiSchema?: UiSchema<T, S, F>,
): ValidationData<T> {
throw new Error('Unexpectedly calling the `validateFormData()` method during schema parsing');
throw new Error(
"Unexpectedly calling the `validateFormData()` method during schema parsing",
);
}
}
69 changes: 53 additions & 16 deletions packages/utils/src/parser/schemaParser.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import forEach from 'lodash/forEach';
import isEqual from 'lodash/isEqual';
import forEach from "lodash/forEach";

import { FormContextType, RJSFSchema, StrictRJSFSchema } from '../types';
import { PROPERTIES_KEY, ITEMS_KEY } from '../constants';
import ParserValidator, { SchemaMap } from './ParserValidator';
import { retrieveSchemaInternal, resolveAnyOrOneOfSchemas } from '../schema/retrieveSchema';
import { FormContextType, RJSFSchema, StrictRJSFSchema } from "../types";
import { ITEMS_KEY, PROPERTIES_KEY } from "../constants";
import ParserValidator, { SchemaMap } from "./ParserValidator";
import {
resolveAnyOrOneOfSchemas,
retrieveSchemaInternal,
} from "../schema/retrieveSchema";
import { deepEqual } from "fast-equals";

/** Recursive function used to parse the given `schema` belonging to the `rootSchema`. The `validator` is used to
* capture the sub-schemas that the `isValid()` function is called with. For each schema returned by the
Expand All @@ -16,27 +19,57 @@ import { retrieveSchemaInternal, resolveAnyOrOneOfSchemas } from '../schema/retr
* @param rootSchema - The root schema from which the schema parsing began
* @param schema - The current schema element being parsed
*/
function parseSchema<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
function parseSchema<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any,
>(
validator: ParserValidator<T, S, F>,
recurseList: S[],
rootSchema: S,
schema: S
schema: S,
) {
const schemas = retrieveSchemaInternal<T, S, F>(validator, schema, rootSchema, undefined, true);
const schemas = retrieveSchemaInternal<T, S, F>(
validator,
schema,
rootSchema,
undefined,
true,
);
schemas.forEach((schema) => {
const sameSchemaIndex = recurseList.findIndex((item) => isEqual(item, schema));
const sameSchemaIndex = recurseList.findIndex((item) =>
deepEqual(item, schema)
);
if (sameSchemaIndex === -1) {
recurseList.push(schema);
const allOptions = resolveAnyOrOneOfSchemas<T, S, F>(validator, schema, rootSchema, true);
const allOptions = resolveAnyOrOneOfSchemas<T, S, F>(
validator,
schema,
rootSchema,
true,
);
allOptions.forEach((s) => {
if (PROPERTIES_KEY in s && s[PROPERTIES_KEY]) {
forEach(schema[PROPERTIES_KEY], (value) => {
parseSchema<T, S, F>(validator, recurseList, rootSchema, value as S);
parseSchema<T, S, F>(
validator,
recurseList,
rootSchema,
value as S,
);
});
}
});
if (ITEMS_KEY in schema && !Array.isArray(schema.items) && typeof schema.items !== 'boolean') {
parseSchema<T, S, F>(validator, recurseList, rootSchema, schema.items as S);
if (
ITEMS_KEY in schema && !Array.isArray(schema.items) &&
typeof schema.items !== "boolean"
) {
parseSchema<T, S, F>(
validator,
recurseList,
rootSchema,
schema.items as S,
);
}
}
});
Expand All @@ -48,8 +81,12 @@ function parseSchema<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends
* @param rootSchema - The root schema to parse for sub-schemas used by `isValid()` calls
* @returns - The `SchemaMap` of all schemas that were parsed
*/
export default function schemaParser<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
rootSchema: S
export default function schemaParser<
T = any,
S extends StrictRJSFSchema = RJSFSchema,
F extends FormContextType = any,
>(
rootSchema: S,
): SchemaMap<S> {
const validator = new ParserValidator<T, S, F>(rootSchema);
const recurseList: S[] = [];
Expand Down
Loading

0 comments on commit b122692

Please sign in to comment.