From d40564e243bf3fd1ecd1b478aa32e2188799dc4a Mon Sep 17 00:00:00 2001 From: Stephen Haberman Date: Thu, 17 Oct 2024 09:00:42 -0500 Subject: [PATCH] fix: Fix max stack errors on cycles. (#108) Replaces fast-deep-equal with a copy/pasted/inlined version of a cycle-aware deepEquals. --- package.json | 1 - src/fields/deepEquals.ts | 40 ++++++++++++++++++++++++++++++++++++++++ src/formState.test.tsx | 20 ++++++++++++++++++++ src/utils.ts | 8 ++++---- yarn.lock | 1 - 5 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 src/fields/deepEquals.ts diff --git a/package.json b/package.json index b13b047..2798f49 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,6 @@ "src/**/*.{ts,tsx,css,md}": "prettier --write" }, "dependencies": { - "fast-deep-equal": "^3.1.3", "is-plain-object": "^5.0.0" }, "peerDependencies": { diff --git a/src/fields/deepEquals.ts b/src/fields/deepEquals.ts new file mode 100644 index 0000000..a4746de --- /dev/null +++ b/src/fields/deepEquals.ts @@ -0,0 +1,40 @@ +/** + * Our own `deepEquals` because either `fast-deep-equals` or `dequal` or etc actually + * handle cyclic data structures, despite ChatGTP's assertions/hallucinations. + * + * Ported from https://github.com/KoryNunn/cyclic-deep-equal which is ISC. + */ +export function deepEquals(a: any, b: any, visited: Set = new Set()): boolean { + const aType = typeof a; + if (aType !== typeof b) return false; + + if (a == null || b == null || !(aType === "object" || aType === "function")) { + if (aType === "number" && isNaN(a) && isNaN(b)) return true; + return a === b; + } + + if (Array.isArray(a) !== Array.isArray(b)) return false; + + const aKeys = Object.keys(a), + bKeys = Object.keys(b); + if (aKeys.length !== bKeys.length) return false; + + let equal = true; + + for (const key of aKeys) { + if (!(key in b)) { + equal = false; + break; + } + if (a[key] && a[key] instanceof Object) { + if (visited.has(a[key])) break; + visited.add(a[key]); + } + if (!deepEquals(a[key], b[key], visited)) { + equal = false; + break; + } + } + + return equal; +} diff --git a/src/formState.test.tsx b/src/formState.test.tsx index 402ed92..6f6cd5d 100644 --- a/src/formState.test.tsx +++ b/src/formState.test.tsx @@ -1688,6 +1688,26 @@ describe("formState", () => { expect(a.value).toEqual({ address: { street: "123", city: "nyc" } }); }); + it("can have child object states with cycles", () => { + // Given an author with an address child + // And two addresses that are basically identical and both have cycles + const address1: AuthorAddress = { city: "city2" }; + (address1 as any).someCycle = address1; + const address2: AuthorAddress = { city: "city2" }; + (address2 as any).someCycle = address2; + const a = createObjectState( + { + id: { type: "value" }, + address: { type: "value" }, + }, + { address: address1 }, + ); + // When we change the address + a.address.set(address2); + // Then it doesn't error on dirty checks + expect(a.address.dirty).toBe(false); + }); + it("provides isNewEntity", () => { // Given an author without an id const formState = createObjectState(authorWithAddressConfig, { diff --git a/src/utils.ts b/src/utils.ts index 08176de..de1a031 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,7 +1,7 @@ -import equal from "fast-deep-equal"; import { isPlainObject } from "is-plain-object"; import { isObservable, toJS } from "mobx"; import { ListFieldConfig, ObjectConfig, ObjectFieldConfig, ValueFieldConfig } from "src/config"; +import { deepEquals } from "src/fields/deepEquals"; import { InputAndMap, QueryAndMap, UseFormStateOpts } from "src/useFormState"; export type Builtin = Date | Function | Uint8Array | string | number | boolean; @@ -124,16 +124,16 @@ export function isEmpty(value: any): boolean { */ export function areEqual(a?: T, b?: T, strictOrder?: boolean): boolean { if (isPlainObject(a)) { - return equal(toJS(a), toJS(b)); + return deepEquals(toJS(a), toJS(b)); } if (hasToJSON(a) || hasToJSON(b)) { const a1 = hasToJSON(a) ? a.toJSON() : a; const b1 = hasToJSON(b) ? b.toJSON() : b; - return equal(a1, b1); + return deepEquals(a1, b1); } if (a && b && a instanceof Array && b instanceof Array) { if (strictOrder !== false) { - return equal(a, b); + return deepEquals(a, b); } if (a.length !== b.length) return false; return a.every((a1) => b.some((b1) => areEqual(a1, b1))); diff --git a/yarn.lock b/yarn.lock index c8f43d2..0834dfb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2843,7 +2843,6 @@ __metadata: eslint-plugin-jsx-a11y: ^6.4.1 eslint-plugin-react: ^7.22.0 eslint-plugin-react-hooks: ^4.2.0 - fast-deep-equal: ^3.1.3 husky: ^3.1.0 is-plain-object: ^5.0.0 jest: ^29.7.0