Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Fix max stack errors on cycles. #108

Merged
merged 1 commit into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
40 changes: 40 additions & 0 deletions src/fields/deepEquals.ts
Original file line number Diff line number Diff line change
@@ -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<any> = 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;
}
20 changes: 20 additions & 0 deletions src/formState.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<AuthorInput>(
{
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, {
Expand Down
8 changes: 4 additions & 4 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -124,16 +124,16 @@ export function isEmpty(value: any): boolean {
*/
export function areEqual<T>(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)));
Expand Down
1 change: 0 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading