From a2caade6acd03571af82fa0e8e1ddd05a39aeb7e Mon Sep 17 00:00:00 2001 From: BellCube Dev <33764825+BellCubeDev@users.noreply.github.com> Date: Mon, 18 Mar 2024 20:46:26 -0400 Subject: [PATCH 1/7] Add Optional Support For Multiple References to an Object MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # What This Does Some state trees may need to reference an object more than once (such as the tree for my [fomod](https://www.npmjs.com/package/fomod) library). In essence, we store existing drafts when an off-by-default Immer class configuration option is enabled. This should be a painless solution. Specifics are described below. ## Implementation Details * Two `WeakMap` are used to keep track of draft states and related data at different parts of the immerification process: * `existingStateMap_` maps a given base object to the first draft state created for it. This state includes a reference to the revokable draft. * If a state is referenced multiple times, it will be given a new `revoke_()` function that, once called the first time, calls the old `revoke_()` function. The result is that the final `revoke_()` must be called once for every requested draft before the Proxy is finally revoked. Since a proxy which has has its `revoke_()` method called should be considered revoked by all code paths, duplicate calls should not be an issue. * During finalization, `encounteredObjects` keeps track of objects we've finalized and doesn't traverse an object if it's already seen it. It prevents infinite recursion when circular references are present. * Introduced the `extraParents_` property to the `ImmerBaseState` interface. This keeps track of additional values that would normally be attached to `parent_` so that functionality such as marking the parent state as modified is retained for objects with multiple parent objects * For Maps and Sets, a proxy is established between the state and DraftMap/DraftSet classes to handle multiple references to these native classes while preserving the idea of having one DraftSet per reference. * For Sets, each child draft has a single symbol value set so that a copy is prepared. (discussion needed; see TODOs below) * During finalization, objects may have drafted children and, thus, even unmodified children are finalized in multi-ref mode * To enable the feature, it is the same as other Immer class configuration options (such as `useStrictShallowCopy`). That is, either specify it in the config object passed to the class's constructor OR call the relevant method, `setAllowMultiRefs()` > [!NOTE] > Because of the extra computation involved with checking every proxied object against a map and traversing every object in a tree, enabling multi-ref will have a significant performance impact—even on trees which contain no repeated references. # Tests The file `__tests__/multiref.ts` contains a number of tests related to this multi-reference support implementation. Such tests seek to verify that: * Direct circular references (which Immer tests for normally) do not throw an error when multi-ref is enabled * When the properties of multiple references are modified, all references are modified * Unmodified references to the same object are kept * The same copy is provided for every reference (new references are strictly equivalent [`===`] just as the references before `produce()` would have been) Tests are performed on all relevant object archetypes where applicable. # Outstanding Discussion TODOs * [ ] What to do regarding documentation * [ ] Possible alternate solution for preparing copies for multi-reference DraftSet children * [ ] Add an error for when WeakMap isn't supported in the current environment? (supported in every noteworthy browser and server environment since late 2015) --- __tests__/__prod_snapshots__/base.js.snap | 128 +++++++++ __tests__/__prod_snapshots__/manual.js.snap | 2 + __tests__/multiref.ts | 283 ++++++++++++++++++++ src/core/finalize.ts | 87 ++++-- src/core/immerClass.ts | 51 +++- src/core/proxy.ts | 67 +++-- src/plugins/mapset.ts | 182 +++++++++---- src/types/types-internal.ts | 2 + src/utils/plugins.ts | 15 +- website/docs/pitfalls.md | 2 + 10 files changed, 722 insertions(+), 97 deletions(-) create mode 100644 __tests__/multiref.ts diff --git a/__tests__/__prod_snapshots__/base.js.snap b/__tests__/__prod_snapshots__/base.js.snap index 84c5e6b2..146f5669 100644 --- a/__tests__/__prod_snapshots__/base.js.snap +++ b/__tests__/__prod_snapshots__/base.js.snap @@ -8,6 +8,22 @@ exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener= exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false recipe functions cannot return an object that references itself 1`] = `"Maximum call stack size exceeded"`; +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false revokes the draft once produce returns 1`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false revokes the draft once produce returns 2`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false revokes the draft once produce returns 3`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false revokes the draft once produce returns 4`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false revokes the draft once produce returns 5`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false revokes the draft once produce returns 6`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false revokes the draft once produce returns 7`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false revokes the draft once produce returns 8`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false set drafts revokes sets 1`] = `"[Immer] minified error nr: 3. Full error at: https://bit.ly/3cXEKWf"`; exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false set drafts revokes sets 2`] = `"[Immer] minified error nr: 3. Full error at: https://bit.ly/3cXEKWf"`; @@ -26,6 +42,22 @@ exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener= exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true recipe functions cannot return an object that references itself 1`] = `"Maximum call stack size exceeded"`; +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true revokes the draft once produce returns 1`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true revokes the draft once produce returns 2`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true revokes the draft once produce returns 3`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true revokes the draft once produce returns 4`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true revokes the draft once produce returns 5`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true revokes the draft once produce returns 6`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true revokes the draft once produce returns 7`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true revokes the draft once produce returns 8`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true set drafts revokes sets 1`] = `"[Immer] minified error nr: 3. Full error at: https://bit.ly/3cXEKWf"`; exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true set drafts revokes sets 2`] = `"[Immer] minified error nr: 3. Full error at: https://bit.ly/3cXEKWf"`; @@ -44,6 +76,22 @@ exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=f exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false recipe functions cannot return an object that references itself 1`] = `"Maximum call stack size exceeded"`; +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false revokes the draft once produce returns 1`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false revokes the draft once produce returns 2`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false revokes the draft once produce returns 3`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false revokes the draft once produce returns 4`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false revokes the draft once produce returns 5`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false revokes the draft once produce returns 6`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false revokes the draft once produce returns 7`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false revokes the draft once produce returns 8`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false set drafts revokes sets 1`] = `"[Immer] minified error nr: 3. Full error at: https://bit.ly/3cXEKWf"`; exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false set drafts revokes sets 2`] = `"[Immer] minified error nr: 3. Full error at: https://bit.ly/3cXEKWf"`; @@ -62,6 +110,22 @@ exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=t exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true recipe functions cannot return an object that references itself 1`] = `"Maximum call stack size exceeded"`; +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true revokes the draft once produce returns 1`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true revokes the draft once produce returns 2`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true revokes the draft once produce returns 3`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true revokes the draft once produce returns 4`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true revokes the draft once produce returns 5`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true revokes the draft once produce returns 6`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true revokes the draft once produce returns 7`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true revokes the draft once produce returns 8`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true set drafts revokes sets 1`] = `"[Immer] minified error nr: 3. Full error at: https://bit.ly/3cXEKWf"`; exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true set drafts revokes sets 2`] = `"[Immer] minified error nr: 3. Full error at: https://bit.ly/3cXEKWf"`; @@ -80,6 +144,22 @@ exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=f exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false recipe functions cannot return an object that references itself 1`] = `"Maximum call stack size exceeded"`; +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false revokes the draft once produce returns 1`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false revokes the draft once produce returns 2`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false revokes the draft once produce returns 3`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false revokes the draft once produce returns 4`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false revokes the draft once produce returns 5`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false revokes the draft once produce returns 6`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false revokes the draft once produce returns 7`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false revokes the draft once produce returns 8`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false set drafts revokes sets 1`] = `"[Immer] minified error nr: 3. Full error at: https://bit.ly/3cXEKWf"`; exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false set drafts revokes sets 2`] = `"[Immer] minified error nr: 3. Full error at: https://bit.ly/3cXEKWf"`; @@ -98,6 +178,22 @@ exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=t exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true recipe functions cannot return an object that references itself 1`] = `"Maximum call stack size exceeded"`; +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true revokes the draft once produce returns 1`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true revokes the draft once produce returns 2`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true revokes the draft once produce returns 3`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true revokes the draft once produce returns 4`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true revokes the draft once produce returns 5`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true revokes the draft once produce returns 6`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true revokes the draft once produce returns 7`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true revokes the draft once produce returns 8`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true set drafts revokes sets 1`] = `"[Immer] minified error nr: 3. Full error at: https://bit.ly/3cXEKWf"`; exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true set drafts revokes sets 2`] = `"[Immer] minified error nr: 3. Full error at: https://bit.ly/3cXEKWf"`; @@ -116,6 +212,22 @@ exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=fa exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false recipe functions cannot return an object that references itself 1`] = `"Maximum call stack size exceeded"`; +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false revokes the draft once produce returns 1`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false revokes the draft once produce returns 2`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false revokes the draft once produce returns 3`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false revokes the draft once produce returns 4`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false revokes the draft once produce returns 5`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false revokes the draft once produce returns 6`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false revokes the draft once produce returns 7`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false revokes the draft once produce returns 8`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false set drafts revokes sets 1`] = `"[Immer] minified error nr: 3. Full error at: https://bit.ly/3cXEKWf"`; exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false set drafts revokes sets 2`] = `"[Immer] minified error nr: 3. Full error at: https://bit.ly/3cXEKWf"`; @@ -134,6 +246,22 @@ exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=tr exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true recipe functions cannot return an object that references itself 1`] = `"Maximum call stack size exceeded"`; +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true revokes the draft once produce returns 1`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true revokes the draft once produce returns 2`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true revokes the draft once produce returns 3`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true revokes the draft once produce returns 4`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true revokes the draft once produce returns 5`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true revokes the draft once produce returns 6`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true revokes the draft once produce returns 7`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true revokes the draft once produce returns 8`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true set drafts revokes sets 1`] = `"[Immer] minified error nr: 3. Full error at: https://bit.ly/3cXEKWf"`; exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true set drafts revokes sets 2`] = `"[Immer] minified error nr: 3. Full error at: https://bit.ly/3cXEKWf"`; diff --git a/__tests__/__prod_snapshots__/manual.js.snap b/__tests__/__prod_snapshots__/manual.js.snap index 40315388..4b055da7 100644 --- a/__tests__/__prod_snapshots__/manual.js.snap +++ b/__tests__/__prod_snapshots__/manual.js.snap @@ -2,6 +2,8 @@ exports[`manual - proxy cannot finishDraft twice 1`] = `"Cannot perform 'get' on a proxy that has been revoked"`; +exports[`manual - proxy cannot modify after finish 1`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + exports[`manual - proxy should check arguments 1`] = `"[Immer] minified error nr: 8. Full error at: https://bit.ly/3cXEKWf"`; exports[`manual - proxy should check arguments 2`] = `"[Immer] minified error nr: 8. Full error at: https://bit.ly/3cXEKWf"`; diff --git a/__tests__/multiref.ts b/__tests__/multiref.ts new file mode 100644 index 00000000..312ed63f --- /dev/null +++ b/__tests__/multiref.ts @@ -0,0 +1,283 @@ +import {Immer, enableMapSet} from "../src/immer" +import {inspect} from "util" + +// Implementation note: TypeScript says ES5 doesn't support iterating directly over a Set so I've used Array.from(). +// If the project is moved to a later JS feature set, we can drop the Array.from() and do `for (const value of ref)` instead. + +test("modified circular object", () => { + const immer = new Immer({allowMultiRefs: true}) + const base = {a: 1, b: null} as any + base.b = base + + const envs = ["production", "development", "testing"] + for (const env of envs) { + process.env.NODE_ENV = env + expect(() => { + const next = immer.produce(base, (draft: any) => { + draft.a = 2 + }) + expect(next).toEqual({a: 2, b: next}) + }).not.toThrow() + } +}) + +test("unmodified circular object", () => { + const immer = new Immer({allowMultiRefs: true}) + const base = {a: 1, b: null} as any + base.b = base + + const envs = ["production", "development", "testing"] + for (const env of envs) { + process.env.NODE_ENV = env + expect(() => { + const next = immer.produce({state: null}, (draft: any) => { + draft.state = base + }) + expect(next.state).toBe(base) + }).not.toThrow() + } +}) + +describe("access value & change child's child value", () => { + describe("with object", () => { + const immer = new Immer({allowMultiRefs: true}) + const sameRef = {someNumber: 1, someString: "one"} + const objectOfRefs = {a: sameRef, b: sameRef, c: sameRef, d: sameRef} + + const base = { + objectRef1: objectOfRefs, + objectRef2: objectOfRefs, + objectRef3: objectOfRefs, + objectRef4: objectOfRefs + } + const next = immer.produce(base, draft => { + draft.objectRef2.b.someNumber = 2 + draft.objectRef3.c.someString = "two" + }) + + it("should have kept the Object refs the same", () => { + expect(next.objectRef1).toBe(next.objectRef2), + expect(next.objectRef2).toBe(next.objectRef3), + expect(next.objectRef3).toBe(next.objectRef4) + }) + + it("should have updated the values across everything", () => { + function verifyObjectReference( + ref: {[key: string]: {someNumber: number; someString: string}}, + objectNum: number + ) { + verifySingleReference(ref.a, objectNum, "a") + verifySingleReference(ref.b, objectNum, "b") + verifySingleReference(ref.c, objectNum, "c") + verifySingleReference(ref.d, objectNum, "d") + } + + function verifySingleReference( + ref: {someNumber: number; someString: string}, + objectNum: number, + refKey: string + ) { + //it(`should have updated the values across everything (ref ${refKey.toUpperCase()} in object #${objectNum})`, () => { + expect(ref.someNumber).toBe(2) + expect(ref.someString).toBe("two") + //}) + } + + verifyObjectReference(next.objectRef1, 1) + verifyObjectReference(next.objectRef2, 2) + verifyObjectReference(next.objectRef3, 3) + verifyObjectReference(next.objectRef4, 4) + }); + }) + + describe("with map", () => { + const immer = new Immer({allowMultiRefs: true}) + enableMapSet() + const sameRef = {someNumber: 1, someString: "one"} + const mapOfRefs = new Map([ + ["a", sameRef], + ["b", sameRef], + ["c", sameRef], + ["d", sameRef] + ]) + + const base = { + mapRef1: mapOfRefs, + mapRef2: mapOfRefs, + mapRef3: mapOfRefs, + mapRef4: mapOfRefs + } + const next = immer.produce(base, draft => { + draft.mapRef2.get("b")!.someNumber = 2 + draft.mapRef3.get("c")!.someString = "two" + }) + + it("should have kept the Map refs the same", () => { + expect(next.mapRef1).toBe(next.mapRef2), + expect(next.mapRef2).toBe(next.mapRef3), + expect(next.mapRef3).toBe(next.mapRef4) + }) + + it("should have updated the values across everything", () => { + function verifyMapReference( + ref: Map, + mapNum: number + ) { + verifySingleReference(ref.get("a")!, mapNum, "a") + verifySingleReference(ref.get("b")!, mapNum, "b") + verifySingleReference(ref.get("c")!, mapNum, "c") + verifySingleReference(ref.get("d")!, mapNum, "d") + + //it(`should have the same child refs (map #${mapNum})`, () => { + expect(ref.get("a")).toBe(ref.get("b")), + expect(ref.get("b")).toBe(ref.get("c")), + expect(ref.get("c")).toBe(ref.get("d")) + //}) + } + + function verifySingleReference( + ref: {someNumber: number; someString: string}, + mapNum: number, + refKey: string + ) { + //it(`should have updated the values across everything (ref ${refKey.toUpperCase()} in map #${mapNum})`, () => { + expect(ref.someNumber).toBe(2) + expect(ref.someString).toBe("two") + //}) + } + + verifyMapReference(next.mapRef1, 1) + verifyMapReference(next.mapRef2, 2) + verifyMapReference(next.mapRef3, 3) + verifyMapReference(next.mapRef4, 4) + + }); + }) + + describe("with array", () => { + const immer = new Immer({allowMultiRefs: true}) + const sameRef = {someNumber: 1, someString: "one"} + const arrayOfRefs = [sameRef, sameRef, sameRef, sameRef] + + const base = { + arrayRef1: arrayOfRefs, + arrayRef2: arrayOfRefs, + arrayRef3: arrayOfRefs, + arrayRef4: arrayOfRefs + } + const next = immer.produce(base, draft => { + draft.arrayRef2[1].someNumber = 2 + draft.arrayRef3[2].someString = "two" + }) + + it("should have kept the Array refs the same", () => { + expect(next.arrayRef1).toBe(next.arrayRef2), + expect(next.arrayRef2).toBe(next.arrayRef3), + expect(next.arrayRef3).toBe(next.arrayRef4) + }) + + it("should have updated the values across everything", () => { + function verifyArrayReference( + ref: {someNumber: number; someString: string}[], + arrayNum: number + ) { + let i = 0 + for (const value of ref) { + //it(`should have updated the values across everything (ref #${i++} in array #${arrayNum})`, () => { + verifySingleReference(value) + //}) + } + } + + function verifySingleReference(ref: { + someNumber: number + someString: string + }) { + expect(ref.someNumber).toBe(2) + expect(ref.someString).toBe("two") + } + + verifyArrayReference(next.arrayRef1, 1) + verifyArrayReference(next.arrayRef2, 2) + verifyArrayReference(next.arrayRef3, 3) + verifyArrayReference(next.arrayRef4, 4) + }); + }) + + describe("with set", () => { + const immer = new Immer({allowMultiRefs: true}) + enableMapSet() + const sameRef = {someNumber: 1, someString: "one"} + const setOfRefs = new Set([{sameRef}, {sameRef}, {sameRef}, {sameRef}]) + + const base = { + setRef1: setOfRefs, + setRef2: setOfRefs, + setRef3: setOfRefs, + setRef4: setOfRefs + } + //console.log("base", inspect(base, {depth: 6, colors: true, compact: true})) + + const next = immer.produce(base, draft => { + const set2Values = draft.setRef2.values() + set2Values.next() + set2Values.next().value.sameRef.someNumber = 2 + + const set3Values = draft.setRef3.values() + set3Values.next() + set3Values.next() + set3Values.next().value.sameRef.someString = "two" + }) + + //console.log( + // "next", + // inspect(next, { + // depth: 20, + // colors: true, + // compact: true, + // breakLength: Infinity + // }) + //) + + it("should have kept the Set refs the same", () => { + expect(next.setRef1).toBe(next.setRef2), + expect(next.setRef2).toBe(next.setRef3), + expect(next.setRef3).toBe(next.setRef4) + }) + + it("should have updated the values across everything", () => { + function verifySetReference( + ref: Set<{sameRef: {someNumber: number; someString: string}}>, + setLetter: string + ) { + //it(`should have the same child refs (set ${setLetter.toUpperCase()})`, () => { + let first = ref.values().next().value.sameRef + for (const value of Array.from(ref)) { + expect(value.sameRef).toBe(first) + } + //}) + + let i = 0 + for (const value of Array.from(ref)) { + //it(`should have updated the values across everything (ref #${i++} in set ${setLetter.toUpperCase()})`, () => { + verifySingleReference(value.sameRef) + //}) + } + } + + function verifySingleReference(ref: { + someNumber: number + someString: string + }) { + expect(ref.someNumber).toBe(2) + expect(ref.someString).toBe("two") + } + + verifySetReference(next.setRef1, "a") + verifySetReference(next.setRef2, "b") + verifySetReference(next.setRef3, "c") + verifySetReference(next.setRef4, "d") + + }); + }) +}) diff --git a/src/core/finalize.ts b/src/core/finalize.ts index 6ee69ce6..660667e7 100644 --- a/src/core/finalize.ts +++ b/src/core/finalize.ts @@ -15,10 +15,15 @@ import { getPlugin, die, revokeScope, - isFrozen + isFrozen, + Objectish } from "../internal" -export function processResult(result: any, scope: ImmerScope) { +export function processResult( + result: any, + scope: ImmerScope, + existingStateMap?: WeakMap +) { scope.unfinalizedDrafts_ = scope.drafts_.length const baseDraft = scope.drafts_![0] const isReplaced = result !== undefined && result !== baseDraft @@ -29,8 +34,8 @@ export function processResult(result: any, scope: ImmerScope) { } if (isDraftable(result)) { // Finalize the result in case it contains (or is) a subset of the draft. - result = finalize(scope, result) - if (!scope.parent_) maybeFreeze(scope, result) + result = finalize(scope, result, undefined, existingStateMap) + if (!scope.parent_) maybeFreeze(scope, result, false) } if (scope.patches_) { getPlugin("Patches").generateReplacementPatches_( @@ -42,7 +47,7 @@ export function processResult(result: any, scope: ImmerScope) { } } else { // Finalize the base draft. - result = finalize(scope, baseDraft, []) + result = finalize(scope, baseDraft, [], existingStateMap) } revokeScope(scope) if (scope.patches_) { @@ -51,23 +56,43 @@ export function processResult(result: any, scope: ImmerScope) { return result !== NOTHING ? result : undefined } -function finalize(rootScope: ImmerScope, value: any, path?: PatchPath) { +function finalize( + rootScope: ImmerScope, + value: any, + path?: PatchPath, + existingStateMap?: WeakMap, + encounteredObjects = new WeakSet() +): any { // Don't recurse in tho recursive data structures - if (isFrozen(value)) return value + if (isFrozen(value) || encounteredObjects.has(value)) return value + encounteredObjects.add(value) + + let state: ImmerState = value[DRAFT_STATE] - const state: ImmerState = value[DRAFT_STATE] // A plain object, might need freezing, might contain drafts - if (!state) { - each(value, (key, childValue) => - finalizeProperty(rootScope, state, value, key, childValue, path) + if (!state || (!state.modified_ && state.existingStateMap_)) { + each( + value, + (key, childValue) => + finalizeProperty( + rootScope, + state, + value, + key, + childValue, + path, + undefined, + existingStateMap, + encounteredObjects + ) ) - return value + return state ? state.base_ : value } // Never finalize drafts owned by another scope. if (state.scope_ !== rootScope) return value // Unmodified draft, return the (frozen) original if (!state.modified_) { - maybeFreeze(rootScope, state.base_, true) + maybeFreeze(rootScope, state.copy_ ?? state.base_, true) return state.base_ } // Not finalized yet, let's do that now @@ -87,7 +112,17 @@ function finalize(rootScope: ImmerScope, value: any, path?: PatchPath) { isSet = true } each(resultEach, (key, childValue) => - finalizeProperty(rootScope, state, result, key, childValue, path, isSet) + finalizeProperty( + rootScope, + state, + result, + key, + childValue, + path, + isSet, + existingStateMap, + encounteredObjects + ) ) // everything inside is frozen, we can freeze here maybeFreeze(rootScope, result, false) @@ -101,6 +136,7 @@ function finalize(rootScope: ImmerScope, value: any, path?: PatchPath) { ) } } + return state.copy_ } @@ -111,10 +147,20 @@ function finalizeProperty( prop: string | number, childValue: any, rootPath?: PatchPath, - targetIsSet?: boolean + targetIsSet?: boolean, + existingStateMap?: WeakMap, + encounteredObjects = new WeakSet() ) { if (process.env.NODE_ENV !== "production" && childValue === targetObject) die(5) + + if (!isDraft(childValue) && isDraftable(childValue)) { + const existingState = existingStateMap?.get(childValue) + if (existingState) { + childValue = existingState.draft_ + } + } + if (isDraft(childValue)) { const path = rootPath && @@ -124,7 +170,7 @@ function finalizeProperty( ? rootPath!.concat(prop) : undefined // Drafts owned by `scope` are finalized here. - const res = finalize(rootScope, childValue, path) + const res = finalize(rootScope, childValue, path, existingStateMap) set(targetObject, prop, res) // Drafts from another scope must prevented to be frozen // if we got a draft back from finalize, we're in a nested produce and shouldn't freeze @@ -134,6 +180,7 @@ function finalizeProperty( } else if (targetIsSet) { targetObject.add(childValue) } + // Search new objects for unfinalized drafts. Frozen objects should never contain drafts. if (isDraftable(childValue) && !isFrozen(childValue)) { if (!rootScope.immer_.autoFreeze_ && rootScope.unfinalizedDrafts_ < 1) { @@ -144,7 +191,13 @@ function finalizeProperty( // See add-data.js perf test return } - finalize(rootScope, childValue) + finalize( + rootScope, + childValue, + undefined, + existingStateMap, + encounteredObjects + ) // Immer deep freezes plain objects, so if there is no parent state, we freeze as well // Per #590, we never freeze symbolic properties. Just to make sure don't accidentally interfere // with other frameworks. diff --git a/src/core/immerClass.ts b/src/core/immerClass.ts index 6c673e0a..88eb07a9 100644 --- a/src/core/immerClass.ts +++ b/src/core/immerClass.ts @@ -34,12 +34,19 @@ interface ProducersFns { export class Immer implements ProducersFns { autoFreeze_: boolean = true useStrictShallowCopy_: boolean = false + allowMultiRefs_: boolean = false - constructor(config?: {autoFreeze?: boolean; useStrictShallowCopy?: boolean}) { + constructor(config?: { + autoFreeze?: boolean + useStrictShallowCopy?: boolean + allowMultiRefs: boolean + }) { if (typeof config?.autoFreeze === "boolean") this.setAutoFreeze(config!.autoFreeze) if (typeof config?.useStrictShallowCopy === "boolean") this.setUseStrictShallowCopy(config!.useStrictShallowCopy) + if (typeof config?.allowMultiRefs === "boolean") + this.setAllowMultiRefs(config!.allowMultiRefs) } /** @@ -86,7 +93,8 @@ export class Immer implements ProducersFns { // Only plain objects, arrays, and "immerable classes" are drafted. if (isDraftable(base)) { const scope = enterScope(this) - const proxy = createProxy(base, undefined) + const stateMap = this.allowMultiRefs_ ? new Map() : undefined + const proxy = createProxy(base, undefined, stateMap) let hasError = true try { result = recipe(proxy) @@ -97,7 +105,7 @@ export class Immer implements ProducersFns { else leaveScope(scope) } usePatchesInScope(scope, patchListener) - return processResult(result, scope) + return processResult(result, scope, stateMap) } else if (!base || typeof base !== "object") { result = recipe(base) if (result === undefined) result = base @@ -132,7 +140,11 @@ export class Immer implements ProducersFns { if (!isDraftable(base)) die(8) if (isDraft(base)) base = current(base) const scope = enterScope(this) - const proxy = createProxy(base, undefined) + const proxy = createProxy( + base, + undefined, + this.allowMultiRefs_ ? new WeakMap() : undefined + ) proxy[DRAFT_STATE].isManual_ = true leaveScope(scope) return proxy as any @@ -144,9 +156,10 @@ export class Immer implements ProducersFns { ): D extends Draft ? T : never { const state: ImmerState = draft && (draft as any)[DRAFT_STATE] if (!state || !state.isManual_) die(9) - const {scope_: scope} = state + + const {scope_: scope, existingStateMap_} = state usePatchesInScope(scope, patchListener) - return processResult(undefined, scope) + return processResult(undefined, scope, existingStateMap_) as any } /** @@ -167,6 +180,11 @@ export class Immer implements ProducersFns { this.useStrictShallowCopy_ = value } + /** Pass true to allow multiple references to the same object in the same state tree. */ + setAllowMultiRefs(value: boolean) { + this.allowMultiRefs_ = value + } + applyPatches(base: T, patches: Patch[]): T { // If a patch replaces the entire state, take that replacement as base // before applying patches @@ -198,16 +216,29 @@ export class Immer implements ProducersFns { export function createProxy( value: T, - parent?: ImmerState + parent?: ImmerState, + stateMap: + | WeakMap + | undefined = parent?.existingStateMap_ ): Drafted { // precondition: createProxy should be guarded by isDraftable, so we know we can safely draft const draft: Drafted = isMap(value) - ? getPlugin("MapSet").proxyMap_(value, parent) + ? getPlugin("MapSet").proxyMap_( + value, + parent, + stateMap ?? parent?.existingStateMap_ + ) : isSet(value) - ? getPlugin("MapSet").proxySet_(value, parent) - : createProxyProxy(value, parent) + ? getPlugin("MapSet").proxySet_( + value, + parent, + stateMap ?? parent?.existingStateMap_ + ) + : createProxyProxy(value, parent, stateMap ?? parent?.existingStateMap_) const scope = parent ? parent.scope_ : getCurrentScope() + scope.drafts_.push(draft) + return draft } diff --git a/src/core/proxy.ts b/src/core/proxy.ts index 3ce06aa8..80447738 100644 --- a/src/core/proxy.ts +++ b/src/core/proxy.ts @@ -51,10 +51,11 @@ type ProxyState = ProxyObjectState | ProxyArrayState */ export function createProxyProxy( base: T, - parent?: ImmerState + parent?: ImmerState, + stateMap?: WeakMap ): Drafted { const isArray = Array.isArray(base) - const state: ProxyState = { + const state: ProxyState = (stateMap?.get(base) as ProxyState) || { type_: isArray ? ArchType.Array : (ArchType.Object as any), // Track which produce call this is associated with. scope_: parent ? parent.scope_ : getCurrentScope()!, @@ -74,7 +75,13 @@ export function createProxyProxy( copy_: null, // Called by the `produce` function. revoke_: null as any, - isManual_: false + isManual_: false, + existingStateMap_: stateMap + } + + if (parent && state.parent_ !== parent) { + if (state.extraParents_) state.extraParents_.push(parent) + else state.extraParents_ = [parent] } // the traps must target something, a bit like the 'real' base. @@ -90,10 +97,21 @@ export function createProxyProxy( traps = arrayTraps } - const {revoke, proxy} = Proxy.revocable(target, traps) - state.draft_ = proxy as any - state.revoke_ = revoke - return proxy as any + if (state.revoke_) { + let thisHasBeenRevoked = false + const oldRevoke = state.revoke_ + state.revoke_ = () => { + if (thisHasBeenRevoked) return oldRevoke() + thisHasBeenRevoked = true + } + } else { + const {revoke, proxy} = Proxy.revocable(target, traps) + + if (!state.draft_) state.draft_ = proxy as any + state.revoke_ = revoke + } + + return state.draft_ as any } /** @@ -116,7 +134,11 @@ export const objectTraps: ProxyHandler = { // Assigned values are never drafted. This catches any drafts we created, too. if (value === peek(state.base_, prop)) { prepareCopy(state) - return (state.copy_![prop as any] = createProxy(value, state)) + return (state.copy_![prop as any] = createProxy( + value, + state, + state.existingStateMap_ + )) } return value }, @@ -275,18 +297,27 @@ export function markChanged(state: ImmerState) { if (state.parent_) { markChanged(state.parent_) } + if (state.extraParents_) { + for (let i = 0; i < state.extraParents_.length; i++) { + markChanged(state.extraParents_[i]) + } + } } } -export function prepareCopy(state: { - base_: any - copy_: any - scope_: ImmerScope -}) { - if (!state.copy_) { - state.copy_ = shallowCopy( - state.base_, - state.scope_.immer_.useStrictShallowCopy_ - ) +export function prepareCopy(state: ImmerState) { + if (state.copy_) return + + const existing = state.existingStateMap_?.get(state.base_) + if (existing) { + Object.assign(state, existing) + return } + + state.copy_ = shallowCopy( + state.base_, + state.scope_.immer_.useStrictShallowCopy_ + ) + + state.existingStateMap_?.set(state.base_, state) } diff --git a/src/plugins/mapset.ts b/src/plugins/mapset.ts index edc628a7..6244dc3c 100644 --- a/src/plugins/mapset.ts +++ b/src/plugins/mapset.ts @@ -14,27 +14,60 @@ import { markChanged, die, ArchType, - each + each, + Objectish } from "../internal" export function enableMapSet() { class DraftMap extends Map { [DRAFT_STATE]: MapState - constructor(target: AnyMap, parent?: ImmerState) { + constructor( + target: AnyMap, + parent?: ImmerState, + stateMap: + | WeakMap + | undefined = parent?.existingStateMap_ + ) { super() - this[DRAFT_STATE] = { - type_: ArchType.Map, - parent_: parent, - scope_: parent ? parent.scope_ : getCurrentScope()!, - modified_: false, - finalized_: false, - copy_: undefined, - assigned_: undefined, - base_: target, - draft_: this as any, - isManual_: false, - revoked_: false + let revoked = false + const this_ = this + this[DRAFT_STATE] = new Proxy( + (stateMap?.get(target) as MapState) || { + type_: ArchType.Map, + parent_: parent, + scope_: parent ? parent.scope_ : getCurrentScope()!, + modified_: false, + finalized_: false, + copy_: undefined, + assigned_: undefined, + base_: target, + draft_: this as any, + isManual_: false, + revoked_: false, + existingStateMap_: parent?.existingStateMap_ as any + }, + { + get(target, p, receiver) { + if (p === "revoked_") return revoked + if (p === "draft_") return this_ + return Reflect.get(target, p, receiver) + }, + set(target, p, newValue, receiver) { + if (p === "revoked_") { + revoked = newValue + return true + } + if (p === "draft_") return false + return Reflect.set(target, p, newValue, receiver) + } + } + ) + + if (parent && this[DRAFT_STATE].parent_ !== parent) { + if (this[DRAFT_STATE].extraParents_) + this[DRAFT_STATE].extraParents_.push(parent) + else this[DRAFT_STATE].extraParents_ = [parent] } } @@ -109,7 +142,7 @@ export function enableMapSet() { return value // either already drafted or reassigned } // despite what it looks, this creates a draft only once, see above condition - const draft = createProxy(value, state) + const draft = createProxy(value, state, state.existingStateMap_) prepareMapCopy(state) state.copy_!.set(key, draft) return draft @@ -158,34 +191,72 @@ export function enableMapSet() { } } - function proxyMap_(target: T, parent?: ImmerState): T { + function proxyMap_( + target: T, + parent?: ImmerState, + stateMap: + | WeakMap + | undefined = parent?.existingStateMap_ + ): T { // @ts-ignore - return new DraftMap(target, parent) + return new DraftMap(target, parent, stateMap) } function prepareMapCopy(state: MapState) { - if (!state.copy_) { - state.assigned_ = new Map() - state.copy_ = new Map(state.base_) - } + if (state.copy_) return + state.assigned_ = new Map() + state.copy_ = new Map(state.base_) + state.existingStateMap_?.set(state.base_, state) } class DraftSet extends Set { [DRAFT_STATE]: SetState - constructor(target: AnySet, parent?: ImmerState) { + constructor( + target: AnySet, + parent?: ImmerState, + stateMap: + | WeakMap + | undefined = parent?.existingStateMap_ + ) { super() - this[DRAFT_STATE] = { - type_: ArchType.Set, - parent_: parent, - scope_: parent ? parent.scope_ : getCurrentScope()!, - modified_: false, - finalized_: false, - copy_: undefined, - base_: target, - draft_: this, - drafts_: new Map(), - revoked_: false, - isManual_: false + let revoked = false + const this_ = this + this[DRAFT_STATE] = new Proxy( + (stateMap?.get(target) as SetState) || { + type_: ArchType.Set, + parent_: parent, + scope_: parent ? parent.scope_ : getCurrentScope()!, + modified_: false, + finalized_: false, + copy_: undefined, + base_: target, + draft_: this, + drafts_: new Map(), + revoked_: false, + isManual_: false, + existingStateMap_: parent?.existingStateMap_ as any + }, + { + get(target, p, receiver) { + if (p === "revoked_") return revoked + if (p === "draft_") return this_ + return Reflect.get(target, p, receiver) + }, + set(target, p, newValue, receiver) { + if (p === "revoked_") { + revoked = newValue + return true + } + if (p === "draft_") return false + return Reflect.set(target, p, newValue, receiver) + } + } + ) + + if (parent && this[DRAFT_STATE].parent_ !== parent) { + if (this[DRAFT_STATE].extraParents_) + this[DRAFT_STATE].extraParents_.push(parent) + else this[DRAFT_STATE].extraParents_ = [parent] } } @@ -267,6 +338,7 @@ export function enableMapSet() { } forEach(cb: any, thisArg?: any) { + console.log("Set forEach", this) const iterator = this.values() let result = iterator.next() while (!result.done) { @@ -275,25 +347,37 @@ export function enableMapSet() { } } } - function proxySet_(target: T, parent?: ImmerState): T { + + function proxySet_( + target: T, + parent?: ImmerState, + stateMap: + | WeakMap + | undefined = parent?.existingStateMap_ + ): T { // @ts-ignore - return new DraftSet(target, parent) + return new DraftSet(target, parent, stateMap) } + const unusedValueSymbol = Symbol("unused") + function prepareSetCopy(state: SetState) { - if (!state.copy_) { - // create drafts for all entries to preserve insertion order - state.copy_ = new Set() - state.base_.forEach(value => { - if (isDraftable(value)) { - const draft = createProxy(value, state) - state.drafts_.set(value, draft) - state.copy_!.add(draft) - } else { - state.copy_!.add(value) - } - }) - } + if (state.copy_) return + // create drafts for all entries to preserve insertion order + state.copy_ = new Set() + // @ts-ignore + state.existingStateMap_?.set(state.base_, state) + state.base_.forEach(value => { + if (isDraftable(value)) { + const draft = createProxy(value, state, state.existingStateMap_) + if (state.existingStateMap_) + draft[unusedValueSymbol] = unusedValueSymbol + state.drafts_.set(value, draft) + state.copy_!.add(draft) + } else { + state.copy_!.add(value) + } + }) } function assertUnrevoked(state: any /*ES5State | MapState | SetState*/) { diff --git a/src/types/types-internal.ts b/src/types/types-internal.ts index 5c506252..47cf6b83 100644 --- a/src/types/types-internal.ts +++ b/src/types/types-internal.ts @@ -24,10 +24,12 @@ export const enum ArchType { export interface ImmerBaseState { parent_?: ImmerState + extraParents_?: ImmerState[] scope_: ImmerScope modified_: boolean finalized_: boolean isManual_: boolean + existingStateMap_?: WeakMap | undefined } export type ImmerState = diff --git a/src/utils/plugins.ts b/src/utils/plugins.ts index 36cc1d70..4db4f97f 100644 --- a/src/utils/plugins.ts +++ b/src/utils/plugins.ts @@ -6,7 +6,8 @@ import { AnyMap, AnySet, ArchType, - die + die, + Objectish } from "../internal" /** Plugin utilities */ @@ -27,8 +28,16 @@ const plugins: { applyPatches_(draft: T, patches: Patch[]): T } MapSet?: { - proxyMap_(target: T, parent?: ImmerState): T - proxySet_(target: T, parent?: ImmerState): T + proxyMap_( + target: T, + parent?: ImmerState, + stateMap?: WeakMap + ): T + proxySet_( + target: T, + parent?: ImmerState, + stateMap?: WeakMap + ): T } } = {} diff --git a/website/docs/pitfalls.md b/website/docs/pitfalls.md index afa58c2b..01de6fed 100644 --- a/website/docs/pitfalls.md +++ b/website/docs/pitfalls.md @@ -17,6 +17,8 @@ Never reassign the `draft` argument (example: `draft = myCoolNewState`). Instead ### Immer only supports unidirectional trees + + Immer assumes your state to be a unidirectional tree. That is, no object should appear twice in the tree, there should be no circular references. There should be exactly one path from the root to any node of the tree. ### Never explicitly return `undefined` from a producer From 438700bf529291ed4632a011ac8ce68c21d5b9bd Mon Sep 17 00:00:00 2001 From: BellCube Dev <33764825+BellCubeDev@users.noreply.github.com> Date: Wed, 20 Mar 2024 22:08:07 -0400 Subject: [PATCH 2/7] Remove erroneous console.log --- src/plugins/mapset.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plugins/mapset.ts b/src/plugins/mapset.ts index 6244dc3c..02c06175 100644 --- a/src/plugins/mapset.ts +++ b/src/plugins/mapset.ts @@ -338,7 +338,6 @@ export function enableMapSet() { } forEach(cb: any, thisArg?: any) { - console.log("Set forEach", this) const iterator = this.values() let result = iterator.next() while (!result.done) { From e346500cc7d5bcc78fcc886973a1fef01b0a4df4 Mon Sep 17 00:00:00 2001 From: BellCube Dev <33764825+BellCubeDev@users.noreply.github.com> Date: Wed, 20 Mar 2024 22:42:51 -0400 Subject: [PATCH 3/7] fix: encounteredObjects not being passed or properly handling drafts --- src/core/finalize.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/finalize.ts b/src/core/finalize.ts index 660667e7..ba900c2d 100644 --- a/src/core/finalize.ts +++ b/src/core/finalize.ts @@ -64,10 +64,10 @@ function finalize( encounteredObjects = new WeakSet() ): any { // Don't recurse in tho recursive data structures - if (isFrozen(value) || encounteredObjects.has(value)) return value - encounteredObjects.add(value) + let state: ImmerState | undefined = value[DRAFT_STATE] + if (isFrozen(value) || encounteredObjects.has(state ? state.base_ : value)) return state ? state.copy_ : value + encounteredObjects.add(state ? state.base_ : value) - let state: ImmerState = value[DRAFT_STATE] // A plain object, might need freezing, might contain drafts if (!state || (!state.modified_ && state.existingStateMap_)) { @@ -170,7 +170,7 @@ function finalizeProperty( ? rootPath!.concat(prop) : undefined // Drafts owned by `scope` are finalized here. - const res = finalize(rootScope, childValue, path, existingStateMap) + const res = finalize(rootScope, childValue, path, existingStateMap, encounteredObjects) set(targetObject, prop, res) // Drafts from another scope must prevented to be frozen // if we got a draft back from finalize, we're in a nested produce and shouldn't freeze From fcf20c4a1026fe7dd01c193efb28e98ae6988449 Mon Sep 17 00:00:00 2001 From: BellCube Dev <33764825+BellCubeDev@users.noreply.github.com> Date: Mon, 25 Mar 2024 11:12:14 -0400 Subject: [PATCH 4/7] Add `allowMultiRefs` to base.js test matrix & remove collateral effects --- __tests__/__snapshots__/base.js.snap | 1022 +++++++++++++++++++++++++- __tests__/base.js | 48 +- src/core/finalize.ts | 12 +- 3 files changed, 1042 insertions(+), 40 deletions(-) diff --git a/__tests__/__snapshots__/base.js.snap b/__tests__/__snapshots__/base.js.snap index c545cde6..892615f4 100644 --- a/__tests__/__snapshots__/base.js.snap +++ b/__tests__/__snapshots__/base.js.snap @@ -38,6 +38,82 @@ exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener= exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false throws when the draft is modified and another object is returned 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=false array drafts throws when a non-numeric property is added 1`] = `"[Immer] Immer only supports setting array indices and the 'length' property"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=false array drafts throws when a non-numeric property is deleted 1`] = `"[Immer] Immer only supports deleting array indices"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=false map drafts revokes map proxies 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=false map drafts revokes map proxies 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=false recipe functions cannot return a modified child draft 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=false recipe functions cannot return an object that references itself 1`] = `"[Immer] Immer forbids circular references"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=false revokes the draft once produce returns 1`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=false revokes the draft once produce returns 2`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=false revokes the draft once produce returns 3`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=false revokes the draft once produce returns 4`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=false revokes the draft once produce returns 5`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=false revokes the draft once produce returns 6`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=false revokes the draft once produce returns 7`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=false revokes the draft once produce returns 8`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=false set drafts revokes sets 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=false set drafts revokes sets 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=false throws when Object.defineProperty() is used on drafts 1`] = `"[Immer] Object.defineProperty() cannot be used on an Immer draft"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=false throws when Object.setPrototypeOf() is used on a draft 1`] = `"[Immer] Object.setPrototypeOf() cannot be used on an Immer draft"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=false throws when the draft is modified and another object is returned 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=true array drafts throws when a non-numeric property is added 1`] = `"[Immer] Immer only supports setting array indices and the 'length' property"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=true array drafts throws when a non-numeric property is deleted 1`] = `"[Immer] Immer only supports deleting array indices"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=true map drafts revokes map proxies 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=true map drafts revokes map proxies 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=true recipe functions cannot return a modified child draft 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=true recipe functions cannot return an object that references itself 1`] = `"[Immer] Immer forbids circular references"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=true revokes the draft once produce returns 1`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=true revokes the draft once produce returns 2`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=true revokes the draft once produce returns 3`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=true revokes the draft once produce returns 4`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=true revokes the draft once produce returns 5`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=true revokes the draft once produce returns 6`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=true revokes the draft once produce returns 7`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=true revokes the draft once produce returns 8`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=true set drafts revokes sets 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=true set drafts revokes sets 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=true throws when Object.defineProperty() is used on drafts 1`] = `"[Immer] Object.defineProperty() cannot be used on an Immer draft"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=true throws when Object.setPrototypeOf() is used on a draft 1`] = `"[Immer] Object.setPrototypeOf() cannot be used on an Immer draft"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=true throws when the draft is modified and another object is returned 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true array drafts throws when a non-numeric property is added 1`] = `"[Immer] Immer only supports setting array indices and the 'length' property"`; exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true array drafts throws when a non-numeric property is deleted 1`] = `"[Immer] Immer only supports deleting array indices"`; @@ -76,6 +152,82 @@ exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener= exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true throws when the draft is modified and another object is returned 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=false array drafts throws when a non-numeric property is added 1`] = `"[Immer] Immer only supports setting array indices and the 'length' property"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=false array drafts throws when a non-numeric property is deleted 1`] = `"[Immer] Immer only supports deleting array indices"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=false map drafts revokes map proxies 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=false map drafts revokes map proxies 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=false recipe functions cannot return a modified child draft 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=false recipe functions cannot return an object that references itself 1`] = `"[Immer] Immer forbids circular references"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=false revokes the draft once produce returns 1`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=false revokes the draft once produce returns 2`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=false revokes the draft once produce returns 3`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=false revokes the draft once produce returns 4`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=false revokes the draft once produce returns 5`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=false revokes the draft once produce returns 6`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=false revokes the draft once produce returns 7`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=false revokes the draft once produce returns 8`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=false set drafts revokes sets 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=false set drafts revokes sets 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=false throws when Object.defineProperty() is used on drafts 1`] = `"[Immer] Object.defineProperty() cannot be used on an Immer draft"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=false throws when Object.setPrototypeOf() is used on a draft 1`] = `"[Immer] Object.setPrototypeOf() cannot be used on an Immer draft"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=false throws when the draft is modified and another object is returned 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=true array drafts throws when a non-numeric property is added 1`] = `"[Immer] Immer only supports setting array indices and the 'length' property"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=true array drafts throws when a non-numeric property is deleted 1`] = `"[Immer] Immer only supports deleting array indices"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=true map drafts revokes map proxies 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=true map drafts revokes map proxies 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=true recipe functions cannot return a modified child draft 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=true recipe functions cannot return an object that references itself 1`] = `"[Immer] Immer forbids circular references"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=true revokes the draft once produce returns 1`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=true revokes the draft once produce returns 2`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=true revokes the draft once produce returns 3`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=true revokes the draft once produce returns 4`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=true revokes the draft once produce returns 5`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=true revokes the draft once produce returns 6`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=true revokes the draft once produce returns 7`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=true revokes the draft once produce returns 8`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=true set drafts revokes sets 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=true set drafts revokes sets 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=true throws when Object.defineProperty() is used on drafts 1`] = `"[Immer] Object.defineProperty() cannot be used on an Immer draft"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=true throws when Object.setPrototypeOf() is used on a draft 1`] = `"[Immer] Object.setPrototypeOf() cannot be used on an Immer draft"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=true throws when the draft is modified and another object is returned 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false array drafts throws when a non-numeric property is added 1`] = `"[Immer] Immer only supports setting array indices and the 'length' property"`; exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false array drafts throws when a non-numeric property is deleted 1`] = `"[Immer] Immer only supports deleting array indices"`; @@ -114,6 +266,82 @@ exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=f exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false throws when the draft is modified and another object is returned 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=false array drafts throws when a non-numeric property is added 1`] = `"[Immer] Immer only supports setting array indices and the 'length' property"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=false array drafts throws when a non-numeric property is deleted 1`] = `"[Immer] Immer only supports deleting array indices"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=false map drafts revokes map proxies 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=false map drafts revokes map proxies 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=false recipe functions cannot return a modified child draft 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=false recipe functions cannot return an object that references itself 1`] = `"[Immer] Immer forbids circular references"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=false revokes the draft once produce returns 1`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=false revokes the draft once produce returns 2`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=false revokes the draft once produce returns 3`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=false revokes the draft once produce returns 4`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=false revokes the draft once produce returns 5`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=false revokes the draft once produce returns 6`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=false revokes the draft once produce returns 7`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=false revokes the draft once produce returns 8`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=false set drafts revokes sets 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=false set drafts revokes sets 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=false throws when Object.defineProperty() is used on drafts 1`] = `"[Immer] Object.defineProperty() cannot be used on an Immer draft"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=false throws when Object.setPrototypeOf() is used on a draft 1`] = `"[Immer] Object.setPrototypeOf() cannot be used on an Immer draft"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=false throws when the draft is modified and another object is returned 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=true array drafts throws when a non-numeric property is added 1`] = `"[Immer] Immer only supports setting array indices and the 'length' property"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=true array drafts throws when a non-numeric property is deleted 1`] = `"[Immer] Immer only supports deleting array indices"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=true map drafts revokes map proxies 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=true map drafts revokes map proxies 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=true recipe functions cannot return a modified child draft 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=true recipe functions cannot return an object that references itself 1`] = `"[Immer] Immer forbids circular references"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=true revokes the draft once produce returns 1`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=true revokes the draft once produce returns 2`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=true revokes the draft once produce returns 3`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=true revokes the draft once produce returns 4`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=true revokes the draft once produce returns 5`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=true revokes the draft once produce returns 6`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=true revokes the draft once produce returns 7`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=true revokes the draft once produce returns 8`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=true set drafts revokes sets 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=true set drafts revokes sets 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=true throws when Object.defineProperty() is used on drafts 1`] = `"[Immer] Object.defineProperty() cannot be used on an Immer draft"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=true throws when Object.setPrototypeOf() is used on a draft 1`] = `"[Immer] Object.setPrototypeOf() cannot be used on an Immer draft"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=true throws when the draft is modified and another object is returned 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true array drafts throws when a non-numeric property is added 1`] = `"[Immer] Immer only supports setting array indices and the 'length' property"`; exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true array drafts throws when a non-numeric property is deleted 1`] = `"[Immer] Immer only supports deleting array indices"`; @@ -152,6 +380,82 @@ exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=t exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true throws when the draft is modified and another object is returned 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=false array drafts throws when a non-numeric property is added 1`] = `"[Immer] Immer only supports setting array indices and the 'length' property"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=false array drafts throws when a non-numeric property is deleted 1`] = `"[Immer] Immer only supports deleting array indices"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=false map drafts revokes map proxies 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=false map drafts revokes map proxies 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=false recipe functions cannot return a modified child draft 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=false recipe functions cannot return an object that references itself 1`] = `"[Immer] Immer forbids circular references"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=false revokes the draft once produce returns 1`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=false revokes the draft once produce returns 2`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=false revokes the draft once produce returns 3`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=false revokes the draft once produce returns 4`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=false revokes the draft once produce returns 5`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=false revokes the draft once produce returns 6`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=false revokes the draft once produce returns 7`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=false revokes the draft once produce returns 8`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=false set drafts revokes sets 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=false set drafts revokes sets 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=false throws when Object.defineProperty() is used on drafts 1`] = `"[Immer] Object.defineProperty() cannot be used on an Immer draft"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=false throws when Object.setPrototypeOf() is used on a draft 1`] = `"[Immer] Object.setPrototypeOf() cannot be used on an Immer draft"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=false throws when the draft is modified and another object is returned 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=true array drafts throws when a non-numeric property is added 1`] = `"[Immer] Immer only supports setting array indices and the 'length' property"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=true array drafts throws when a non-numeric property is deleted 1`] = `"[Immer] Immer only supports deleting array indices"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=true map drafts revokes map proxies 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=true map drafts revokes map proxies 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=true recipe functions cannot return a modified child draft 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=true recipe functions cannot return an object that references itself 1`] = `"[Immer] Immer forbids circular references"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=true revokes the draft once produce returns 1`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=true revokes the draft once produce returns 2`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=true revokes the draft once produce returns 3`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=true revokes the draft once produce returns 4`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=true revokes the draft once produce returns 5`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=true revokes the draft once produce returns 6`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=true revokes the draft once produce returns 7`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=true revokes the draft once produce returns 8`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=true set drafts revokes sets 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=true set drafts revokes sets 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=true throws when Object.defineProperty() is used on drafts 1`] = `"[Immer] Object.defineProperty() cannot be used on an Immer draft"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=true throws when Object.setPrototypeOf() is used on a draft 1`] = `"[Immer] Object.setPrototypeOf() cannot be used on an Immer draft"`; + +exports[`base functionality - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=true throws when the draft is modified and another object is returned 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false array drafts throws when a non-numeric property is added 1`] = `"[Immer] Immer only supports setting array indices and the 'length' property"`; exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false array drafts throws when a non-numeric property is deleted 1`] = `"[Immer] Immer only supports deleting array indices"`; @@ -190,6 +494,82 @@ exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=f exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false throws when the draft is modified and another object is returned 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=false array drafts throws when a non-numeric property is added 1`] = `"[Immer] Immer only supports setting array indices and the 'length' property"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=false array drafts throws when a non-numeric property is deleted 1`] = `"[Immer] Immer only supports deleting array indices"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=false map drafts revokes map proxies 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=false map drafts revokes map proxies 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=false recipe functions cannot return a modified child draft 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=false recipe functions cannot return an object that references itself 1`] = `"[Immer] Immer forbids circular references"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=false revokes the draft once produce returns 1`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=false revokes the draft once produce returns 2`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=false revokes the draft once produce returns 3`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=false revokes the draft once produce returns 4`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=false revokes the draft once produce returns 5`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=false revokes the draft once produce returns 6`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=false revokes the draft once produce returns 7`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=false revokes the draft once produce returns 8`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=false set drafts revokes sets 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=false set drafts revokes sets 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=false throws when Object.defineProperty() is used on drafts 1`] = `"[Immer] Object.defineProperty() cannot be used on an Immer draft"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=false throws when Object.setPrototypeOf() is used on a draft 1`] = `"[Immer] Object.setPrototypeOf() cannot be used on an Immer draft"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=false throws when the draft is modified and another object is returned 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=true array drafts throws when a non-numeric property is added 1`] = `"[Immer] Immer only supports setting array indices and the 'length' property"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=true array drafts throws when a non-numeric property is deleted 1`] = `"[Immer] Immer only supports deleting array indices"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=true map drafts revokes map proxies 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=true map drafts revokes map proxies 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=true recipe functions cannot return a modified child draft 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=true recipe functions cannot return an object that references itself 1`] = `"[Immer] Immer forbids circular references"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=true revokes the draft once produce returns 1`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=true revokes the draft once produce returns 2`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=true revokes the draft once produce returns 3`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=true revokes the draft once produce returns 4`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=true revokes the draft once produce returns 5`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=true revokes the draft once produce returns 6`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=true revokes the draft once produce returns 7`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=true revokes the draft once produce returns 8`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=true set drafts revokes sets 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=true set drafts revokes sets 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=true throws when Object.defineProperty() is used on drafts 1`] = `"[Immer] Object.defineProperty() cannot be used on an Immer draft"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=true throws when Object.setPrototypeOf() is used on a draft 1`] = `"[Immer] Object.setPrototypeOf() cannot be used on an Immer draft"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=true throws when the draft is modified and another object is returned 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true array drafts throws when a non-numeric property is added 1`] = `"[Immer] Immer only supports setting array indices and the 'length' property"`; exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true array drafts throws when a non-numeric property is deleted 1`] = `"[Immer] Immer only supports deleting array indices"`; @@ -228,6 +608,82 @@ exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=t exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true throws when the draft is modified and another object is returned 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=false array drafts throws when a non-numeric property is added 1`] = `"[Immer] Immer only supports setting array indices and the 'length' property"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=false array drafts throws when a non-numeric property is deleted 1`] = `"[Immer] Immer only supports deleting array indices"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=false map drafts revokes map proxies 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=false map drafts revokes map proxies 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=false recipe functions cannot return a modified child draft 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=false recipe functions cannot return an object that references itself 1`] = `"[Immer] Immer forbids circular references"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=false revokes the draft once produce returns 1`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=false revokes the draft once produce returns 2`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=false revokes the draft once produce returns 3`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=false revokes the draft once produce returns 4`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=false revokes the draft once produce returns 5`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=false revokes the draft once produce returns 6`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=false revokes the draft once produce returns 7`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=false revokes the draft once produce returns 8`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=false set drafts revokes sets 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=false set drafts revokes sets 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=false throws when Object.defineProperty() is used on drafts 1`] = `"[Immer] Object.defineProperty() cannot be used on an Immer draft"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=false throws when Object.setPrototypeOf() is used on a draft 1`] = `"[Immer] Object.setPrototypeOf() cannot be used on an Immer draft"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=false throws when the draft is modified and another object is returned 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=true array drafts throws when a non-numeric property is added 1`] = `"[Immer] Immer only supports setting array indices and the 'length' property"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=true array drafts throws when a non-numeric property is deleted 1`] = `"[Immer] Immer only supports deleting array indices"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=true map drafts revokes map proxies 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=true map drafts revokes map proxies 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=true recipe functions cannot return a modified child draft 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=true recipe functions cannot return an object that references itself 1`] = `"[Immer] Immer forbids circular references"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=true revokes the draft once produce returns 1`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=true revokes the draft once produce returns 2`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=true revokes the draft once produce returns 3`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=true revokes the draft once produce returns 4`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=true revokes the draft once produce returns 5`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=true revokes the draft once produce returns 6`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=true revokes the draft once produce returns 7`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=true revokes the draft once produce returns 8`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=true set drafts revokes sets 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=true set drafts revokes sets 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=true throws when Object.defineProperty() is used on drafts 1`] = `"[Immer] Object.defineProperty() cannot be used on an Immer draft"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=true throws when Object.setPrototypeOf() is used on a draft 1`] = `"[Immer] Object.setPrototypeOf() cannot be used on an Immer draft"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=true throws when the draft is modified and another object is returned 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false array drafts throws when a non-numeric property is added 1`] = `"[Immer] Immer only supports setting array indices and the 'length' property"`; exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false array drafts throws when a non-numeric property is deleted 1`] = `"[Immer] Immer only supports deleting array indices"`; @@ -266,43 +722,571 @@ exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=fa exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false throws when the draft is modified and another object is returned 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; -exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true array drafts throws when a non-numeric property is added 1`] = `"[Immer] Immer only supports setting array indices and the 'length' property"`; +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=false array drafts throws when a non-numeric property is added 1`] = `"[Immer] Immer only supports setting array indices and the 'length' property"`; -exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true array drafts throws when a non-numeric property is deleted 1`] = `"[Immer] Immer only supports deleting array indices"`; +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=false array drafts throws when a non-numeric property is deleted 1`] = `"[Immer] Immer only supports deleting array indices"`; -exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true map drafts revokes map proxies 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=false map drafts revokes map proxies 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; -exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true map drafts revokes map proxies 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=false map drafts revokes map proxies 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; -exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true recipe functions cannot return a modified child draft 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=false recipe functions cannot return a modified child draft 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; -exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true recipe functions cannot return an object that references itself 1`] = `"[Immer] Immer forbids circular references"`; +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=false recipe functions cannot return an object that references itself 1`] = `"[Immer] Immer forbids circular references"`; -exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true revokes the draft once produce returns 1`] = `"Cannot perform 'get' on a proxy that has been revoked"`; +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=false revokes the draft once produce returns 1`] = `"Cannot perform 'get' on a proxy that has been revoked"`; -exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true revokes the draft once produce returns 2`] = `"Cannot perform 'set' on a proxy that has been revoked"`; +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=false revokes the draft once produce returns 2`] = `"Cannot perform 'set' on a proxy that has been revoked"`; -exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true revokes the draft once produce returns 3`] = `"Cannot perform 'get' on a proxy that has been revoked"`; +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=false revokes the draft once produce returns 3`] = `"Cannot perform 'get' on a proxy that has been revoked"`; -exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true revokes the draft once produce returns 4`] = `"Cannot perform 'set' on a proxy that has been revoked"`; +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=false revokes the draft once produce returns 4`] = `"Cannot perform 'set' on a proxy that has been revoked"`; -exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true revokes the draft once produce returns 5`] = `"Cannot perform 'get' on a proxy that has been revoked"`; +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=false revokes the draft once produce returns 5`] = `"Cannot perform 'get' on a proxy that has been revoked"`; -exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true revokes the draft once produce returns 6`] = `"Cannot perform 'set' on a proxy that has been revoked"`; +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=false revokes the draft once produce returns 6`] = `"Cannot perform 'set' on a proxy that has been revoked"`; -exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true revokes the draft once produce returns 7`] = `"Cannot perform 'get' on a proxy that has been revoked"`; +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=false revokes the draft once produce returns 7`] = `"Cannot perform 'get' on a proxy that has been revoked"`; -exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true revokes the draft once produce returns 8`] = `"Cannot perform 'set' on a proxy that has been revoked"`; +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=false revokes the draft once produce returns 8`] = `"Cannot perform 'set' on a proxy that has been revoked"`; -exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true set drafts revokes sets 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=false set drafts revokes sets 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; -exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true set drafts revokes sets 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=false set drafts revokes sets 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; -exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true throws when Object.defineProperty() is used on drafts 1`] = `"[Immer] Object.defineProperty() cannot be used on an Immer draft"`; +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=false throws when Object.defineProperty() is used on drafts 1`] = `"[Immer] Object.defineProperty() cannot be used on an Immer draft"`; -exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true throws when Object.setPrototypeOf() is used on a draft 1`] = `"[Immer] Object.setPrototypeOf() cannot be used on an Immer draft"`; +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=false throws when Object.setPrototypeOf() is used on a draft 1`] = `"[Immer] Object.setPrototypeOf() cannot be used on an Immer draft"`; -exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true throws when the draft is modified and another object is returned 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=false throws when the draft is modified and another object is returned 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=true array drafts throws when a non-numeric property is added 1`] = `"[Immer] Immer only supports setting array indices and the 'length' property"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=true array drafts throws when a non-numeric property is deleted 1`] = `"[Immer] Immer only supports deleting array indices"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=true map drafts revokes map proxies 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=true map drafts revokes map proxies 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=true recipe functions cannot return a modified child draft 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=true recipe functions cannot return an object that references itself 1`] = `"[Immer] Immer forbids circular references"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=true revokes the draft once produce returns 1`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=true revokes the draft once produce returns 2`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=true revokes the draft once produce returns 3`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=true revokes the draft once produce returns 4`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=true revokes the draft once produce returns 5`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=true revokes the draft once produce returns 6`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=true revokes the draft once produce returns 7`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=true revokes the draft once produce returns 8`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=true set drafts revokes sets 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=true set drafts revokes sets 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=true throws when Object.defineProperty() is used on drafts 1`] = `"[Immer] Object.defineProperty() cannot be used on an Immer draft"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=true throws when Object.setPrototypeOf() is used on a draft 1`] = `"[Immer] Object.setPrototypeOf() cannot be used on an Immer draft"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=true throws when the draft is modified and another object is returned 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true array drafts throws when a non-numeric property is added 1`] = `"[Immer] Immer only supports setting array indices and the 'length' property"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true array drafts throws when a non-numeric property is deleted 1`] = `"[Immer] Immer only supports deleting array indices"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true map drafts revokes map proxies 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true map drafts revokes map proxies 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true recipe functions cannot return a modified child draft 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true recipe functions cannot return an object that references itself 1`] = `"[Immer] Immer forbids circular references"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true revokes the draft once produce returns 1`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true revokes the draft once produce returns 2`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true revokes the draft once produce returns 3`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true revokes the draft once produce returns 4`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true revokes the draft once produce returns 5`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true revokes the draft once produce returns 6`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true revokes the draft once produce returns 7`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true revokes the draft once produce returns 8`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true set drafts revokes sets 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true set drafts revokes sets 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true throws when Object.defineProperty() is used on drafts 1`] = `"[Immer] Object.defineProperty() cannot be used on an Immer draft"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true throws when Object.setPrototypeOf() is used on a draft 1`] = `"[Immer] Object.setPrototypeOf() cannot be used on an Immer draft"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true throws when the draft is modified and another object is returned 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=false array drafts throws when a non-numeric property is added 1`] = `"[Immer] Immer only supports setting array indices and the 'length' property"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=false array drafts throws when a non-numeric property is deleted 1`] = `"[Immer] Immer only supports deleting array indices"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=false map drafts revokes map proxies 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=false map drafts revokes map proxies 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=false recipe functions cannot return a modified child draft 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=false recipe functions cannot return an object that references itself 1`] = `"[Immer] Immer forbids circular references"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=false revokes the draft once produce returns 1`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=false revokes the draft once produce returns 2`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=false revokes the draft once produce returns 3`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=false revokes the draft once produce returns 4`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=false revokes the draft once produce returns 5`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=false revokes the draft once produce returns 6`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=false revokes the draft once produce returns 7`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=false revokes the draft once produce returns 8`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=false set drafts revokes sets 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=false set drafts revokes sets 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=false throws when Object.defineProperty() is used on drafts 1`] = `"[Immer] Object.defineProperty() cannot be used on an Immer draft"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=false throws when Object.setPrototypeOf() is used on a draft 1`] = `"[Immer] Object.setPrototypeOf() cannot be used on an Immer draft"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=false throws when the draft is modified and another object is returned 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=true array drafts throws when a non-numeric property is added 1`] = `"[Immer] Immer only supports setting array indices and the 'length' property"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=true array drafts throws when a non-numeric property is deleted 1`] = `"[Immer] Immer only supports deleting array indices"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=true map drafts revokes map proxies 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=true map drafts revokes map proxies 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=true recipe functions cannot return a modified child draft 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=true recipe functions cannot return an object that references itself 1`] = `"[Immer] Immer forbids circular references"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=true revokes the draft once produce returns 1`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=true revokes the draft once produce returns 2`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=true revokes the draft once produce returns 3`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=true revokes the draft once produce returns 4`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=true revokes the draft once produce returns 5`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=true revokes the draft once produce returns 6`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=true revokes the draft once produce returns 7`] = `"Cannot perform 'get' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=true revokes the draft once produce returns 8`] = `"Cannot perform 'set' on a proxy that has been revoked"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=true set drafts revokes sets 1`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=true set drafts revokes sets 2`] = `"[Immer] Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? {}"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=true throws when Object.defineProperty() is used on drafts 1`] = `"[Immer] Object.defineProperty() cannot be used on an Immer draft"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=true throws when Object.setPrototypeOf() is used on a draft 1`] = `"[Immer] Object.setPrototypeOf() cannot be used on an Immer draft"`; + +exports[`base functionality - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=true throws when the draft is modified and another object is returned 1`] = `"[Immer] An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft."`; + +exports[`complex nesting map / set / object - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=false modify deep object 1`] = ` +{ + "map": Map { + "set1" => Set { + { + "a": 2, + }, + { + "b": 2, + }, + }, + "set2" => Set { + { + "c": 3, + }, + }, + }, +} +`; + +exports[`complex nesting map / set / object - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=false modify deep object 2`] = ` +[ + { + "op": "remove", + "path": [ + "map", + "set1", + 0, + ], + "value": { + "a": 1, + }, + }, + { + "op": "add", + "path": [ + "map", + "set1", + 0, + ], + "value": { + "a": 2, + }, + }, +] +`; + +exports[`complex nesting map / set / object - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=false modify deep object 1`] = ` +{ + "map": Map { + "set1" => Set { + { + "a": 2, + }, + { + "b": 2, + }, + }, + "set2" => Set { + { + "c": 3, + }, + }, + }, +} +`; + +exports[`complex nesting map / set / object - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=false modify deep object 2`] = ` +[ + { + "op": "remove", + "path": [ + "map", + "set1", + 0, + ], + "value": { + "a": 1, + }, + }, + { + "op": "add", + "path": [ + "map", + "set1", + 0, + ], + "value": { + "a": 2, + }, + }, +] +`; + +exports[`complex nesting map / set / object - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=false modify deep object 1`] = ` +{ + "map": Map { + "set1" => Set { + { + "a": 2, + }, + { + "b": 2, + }, + }, + "set2" => Set { + { + "c": 3, + }, + }, + }, +} +`; + +exports[`complex nesting map / set / object - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=false modify deep object 2`] = ` +[ + { + "op": "remove", + "path": [ + "map", + "set1", + 0, + ], + "value": { + "a": 1, + }, + }, + { + "op": "add", + "path": [ + "map", + "set1", + 0, + ], + "value": { + "a": 2, + }, + }, +] +`; + +exports[`complex nesting map / set / object - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=false modify deep object 1`] = ` +{ + "map": Map { + "set1" => Set { + { + "a": 2, + }, + { + "b": 2, + }, + }, + "set2" => Set { + { + "c": 3, + }, + }, + }, +} +`; + +exports[`complex nesting map / set / object - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=false modify deep object 2`] = ` +[ + { + "op": "remove", + "path": [ + "map", + "set1", + 0, + ], + "value": { + "a": 1, + }, + }, + { + "op": "add", + "path": [ + "map", + "set1", + 0, + ], + "value": { + "a": 2, + }, + }, +] +`; + +exports[`complex nesting map / set / object - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=false modify deep object 1`] = ` +{ + "map": Map { + "set1" => Set { + { + "a": 2, + }, + { + "b": 2, + }, + }, + "set2" => Set { + { + "c": 3, + }, + }, + }, +} +`; + +exports[`complex nesting map / set / object - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=false modify deep object 2`] = ` +[ + { + "op": "remove", + "path": [ + "map", + "set1", + 0, + ], + "value": { + "a": 1, + }, + }, + { + "op": "add", + "path": [ + "map", + "set1", + 0, + ], + "value": { + "a": 2, + }, + }, +] +`; + +exports[`complex nesting map / set / object - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=false modify deep object 1`] = ` +{ + "map": Map { + "set1" => Set { + { + "a": 2, + }, + { + "b": 2, + }, + }, + "set2" => Set { + { + "c": 3, + }, + }, + }, +} +`; + +exports[`complex nesting map / set / object - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=false modify deep object 2`] = ` +[ + { + "op": "remove", + "path": [ + "map", + "set1", + 0, + ], + "value": { + "a": 1, + }, + }, + { + "op": "add", + "path": [ + "map", + "set1", + 0, + ], + "value": { + "a": 2, + }, + }, +] +`; + +exports[`complex nesting map / set / object - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=false modify deep object 1`] = ` +{ + "map": Map { + "set1" => Set { + { + "a": 2, + }, + { + "b": 2, + }, + }, + "set2" => Set { + { + "c": 3, + }, + }, + }, +} +`; + +exports[`complex nesting map / set / object - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=false modify deep object 2`] = ` +[ + { + "op": "remove", + "path": [ + "map", + "set1", + 0, + ], + "value": { + "a": 1, + }, + }, + { + "op": "add", + "path": [ + "map", + "set1", + 0, + ], + "value": { + "a": 2, + }, + }, +] +`; + +exports[`complex nesting map / set / object - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=false modify deep object 1`] = ` +{ + "map": Map { + "set1" => Set { + { + "a": 2, + }, + { + "b": 2, + }, + }, + "set2" => Set { + { + "c": 3, + }, + }, + }, +} +`; + +exports[`complex nesting map / set / object - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=false modify deep object 2`] = ` +[ + { + "op": "remove", + "path": [ + "map", + "set1", + 0, + ], + "value": { + "a": 1, + }, + }, + { + "op": "add", + "path": [ + "map", + "set1", + 0, + ], + "value": { + "a": 2, + }, + }, +] +`; exports[`complex nesting map / set / object modify deep object 1`] = ` { diff --git a/__tests__/base.js b/__tests__/base.js index 6cfc05c2..bf3e97dc 100644 --- a/__tests__/base.js +++ b/__tests__/base.js @@ -26,21 +26,26 @@ test("immer should have no dependencies", () => { for (const autoFreeze of [true, false]) { for (const useStrictShallowCopy of [true, false]) { for (const useListener of [true, false]) { - const name = `${autoFreeze ? "auto-freeze=true" : "auto-freeze=false"}:${ - useStrictShallowCopy ? "shallow-copy=true" : "shallow-copy=false" - }:${useListener ? "use-listener=true" : "use-listener=false"}` - runBaseTest(name, autoFreeze, useStrictShallowCopy, useListener) + for (const allowMultiRefs of [true, false]) { + const name = `${autoFreeze ? "auto-freeze=true" : "auto-freeze=false"}:${ + useStrictShallowCopy ? "shallow-copy=true" : "shallow-copy=false" + }:${useListener ? "use-listener=true" : "use-listener=false"}:${ + allowMultiRefs ? "allow-multi-refs=true" : "allow-multi-refs=false" + }` + runBaseTest(name, autoFreeze, useStrictShallowCopy, useListener, allowMultiRefs) + } } } } class Foo {} -function runBaseTest(name, autoFreeze, useStrictShallowCopy, useListener) { +function runBaseTest(name, autoFreeze, useStrictShallowCopy, useListener, allowMultiRefs) { const listener = useListener ? function() {} : undefined const {produce, produceWithPatches} = createPatchedImmer({ autoFreeze, - useStrictShallowCopy + useStrictShallowCopy, + allowMultiRefs, }) // When `useListener` is true, append a function to the arguments of every @@ -1426,19 +1431,29 @@ function runBaseTest(name, autoFreeze, useStrictShallowCopy, useListener) { // "Upvalues" are variables from a parent scope. it("does not finalize upvalue drafts", () => { produce({a: {}, b: {}}, parent => { - expect(produce({}, () => parent)).toBe(parent) - parent.x // Ensure proxy not revoked. + const producedParent1 = produce({}, () => parent); + expect(()=>parent.x).not.toThrow() // Ensure proxy not revoked. + expect(()=>producedParent1.x).not.toThrow() // Ensure proxy not revoked. + expect(Object.is(parent, producedParent1)).toBe(true) + - expect(produce({}, () => [parent])[0]).toBe(parent) - parent.x // Ensure proxy not revoked. + const producedParent2 = produce({}, () => [parent][0]); + expect(()=>parent.x).not.toThrow() // Ensure proxy not revoked. + expect(()=>producedParent2.x).not.toThrow() // Ensure proxy not revoked. + expect(Object.is(parent, producedParent2)).toBe(true) - expect(produce({}, () => parent.a)).toBe(parent.a) - parent.a.x // Ensure proxy not revoked. + const producedChildA = produce({}, () => parent.a) + expect(()=>parent.a.x).not.toThrow() // Ensure proxy not revoked. + expect(()=>producedChildA.x).not.toThrow() // Ensure proxy not revoked. + expect(Object.is(parent.a, producedChildA)).toBe(true) // Modified parent test parent.c = 1 - expect(produce({}, () => [parent.b])[0]).toBe(parent.b) - parent.b.x // Ensure proxy not revoked. + + const producedChildB = produce({}, () => [parent.b])[0] + expect(()=>parent.b.x).not.toThrow() // Ensure proxy not revoked. + expect(()=>producedChildB.x).not.toThrow() // Ensure proxy not revoked. + expect(Object.is(parent.b, producedChildB)).toBe(true) }) }) @@ -1702,11 +1717,12 @@ function runBaseTest(name, autoFreeze, useStrictShallowCopy, useListener) { const next = produce(base, d => { return [d.a, d.a] }) + console.log(base, next); expect(next[0]).toBe(base.a) expect(next[0]).toBe(next[1]) }) - it("cannot return an object that references itself", () => { + if (!allowMultiRefs) it("cannot return an object that references itself", () => { const res = {} res.self = res expect(() => { @@ -2030,7 +2046,7 @@ function runBaseTest(name, autoFreeze, useStrictShallowCopy, useListener) { }) }) - describe(`complex nesting map / set / object`, () => { + describe(`complex nesting map / set / object - ${name}`, () => { const a = {a: 1} const b = {b: 2} const c = {c: 3} diff --git a/src/core/finalize.ts b/src/core/finalize.ts index ba900c2d..fe6b5a2d 100644 --- a/src/core/finalize.ts +++ b/src/core/finalize.ts @@ -63,9 +63,13 @@ function finalize( existingStateMap?: WeakMap, encounteredObjects = new WeakSet() ): any { - // Don't recurse in tho recursive data structures let state: ImmerState | undefined = value[DRAFT_STATE] - if (isFrozen(value) || encounteredObjects.has(state ? state.base_ : value)) return state ? state.copy_ : value + + // Never finalize drafts owned by another scope. + if (state && state.scope_ !== rootScope) return value + + // Don't recurse into recursive data structures + if (isFrozen(value) || encounteredObjects.has(state ? state.base_ : value)) return state ? (state.modified_? state.copy_ : state.base_) : value encounteredObjects.add(state ? state.base_ : value) @@ -86,10 +90,8 @@ function finalize( encounteredObjects ) ) - return state ? state.base_ : value + return state ? (state.copy_ ? state.copy_ : state.base_) : value } - // Never finalize drafts owned by another scope. - if (state.scope_ !== rootScope) return value // Unmodified draft, return the (frozen) original if (!state.modified_) { maybeFreeze(rootScope, state.copy_ ?? state.base_, true) From 68b4ba9b424956a9e108e68777a712708958c786 Mon Sep 17 00:00:00 2001 From: BellCube Dev <33764825+BellCubeDev@users.noreply.github.com> Date: Mon, 25 Mar 2024 11:46:39 -0400 Subject: [PATCH 5/7] Switch to scope-based implementation & Inherit State Map From Parent Cuts down on value passing and works better with nested producers. --- src/core/finalize.ts | 19 +++++--------- src/core/immerClass.ts | 25 +++++++------------ src/core/proxy.ts | 21 +++++++--------- src/core/scope.ts | 6 +++-- src/plugins/mapset.ts | 49 +++++++++++++------------------------ src/types/types-internal.ts | 1 - src/utils/plugins.ts | 6 ++--- 7 files changed, 47 insertions(+), 80 deletions(-) diff --git a/src/core/finalize.ts b/src/core/finalize.ts index fe6b5a2d..3b21cee4 100644 --- a/src/core/finalize.ts +++ b/src/core/finalize.ts @@ -16,13 +16,11 @@ import { die, revokeScope, isFrozen, - Objectish } from "../internal" export function processResult( result: any, - scope: ImmerScope, - existingStateMap?: WeakMap + scope: ImmerScope ) { scope.unfinalizedDrafts_ = scope.drafts_.length const baseDraft = scope.drafts_![0] @@ -34,7 +32,7 @@ export function processResult( } if (isDraftable(result)) { // Finalize the result in case it contains (or is) a subset of the draft. - result = finalize(scope, result, undefined, existingStateMap) + result = finalize(scope, result) if (!scope.parent_) maybeFreeze(scope, result, false) } if (scope.patches_) { @@ -47,7 +45,7 @@ export function processResult( } } else { // Finalize the base draft. - result = finalize(scope, baseDraft, [], existingStateMap) + result = finalize(scope, baseDraft, []) } revokeScope(scope) if (scope.patches_) { @@ -60,7 +58,6 @@ function finalize( rootScope: ImmerScope, value: any, path?: PatchPath, - existingStateMap?: WeakMap, encounteredObjects = new WeakSet() ): any { let state: ImmerState | undefined = value[DRAFT_STATE] @@ -74,7 +71,7 @@ function finalize( // A plain object, might need freezing, might contain drafts - if (!state || (!state.modified_ && state.existingStateMap_)) { + if (!state || (!state.modified_ && state.scope_.existingStateMap_)) { each( value, (key, childValue) => @@ -86,7 +83,6 @@ function finalize( childValue, path, undefined, - existingStateMap, encounteredObjects ) ) @@ -122,7 +118,6 @@ function finalize( childValue, path, isSet, - existingStateMap, encounteredObjects ) ) @@ -150,14 +145,13 @@ function finalizeProperty( childValue: any, rootPath?: PatchPath, targetIsSet?: boolean, - existingStateMap?: WeakMap, encounteredObjects = new WeakSet() ) { if (process.env.NODE_ENV !== "production" && childValue === targetObject) die(5) if (!isDraft(childValue) && isDraftable(childValue)) { - const existingState = existingStateMap?.get(childValue) + const existingState = rootScope.existingStateMap_?.get(childValue) if (existingState) { childValue = existingState.draft_ } @@ -172,7 +166,7 @@ function finalizeProperty( ? rootPath!.concat(prop) : undefined // Drafts owned by `scope` are finalized here. - const res = finalize(rootScope, childValue, path, existingStateMap, encounteredObjects) + const res = finalize(rootScope, childValue, path, encounteredObjects) set(targetObject, prop, res) // Drafts from another scope must prevented to be frozen // if we got a draft back from finalize, we're in a nested produce and shouldn't freeze @@ -197,7 +191,6 @@ function finalizeProperty( rootScope, childValue, undefined, - existingStateMap, encounteredObjects ) // Immer deep freezes plain objects, so if there is no parent state, we freeze as well diff --git a/src/core/immerClass.ts b/src/core/immerClass.ts index 88eb07a9..f1924fb0 100644 --- a/src/core/immerClass.ts +++ b/src/core/immerClass.ts @@ -93,8 +93,7 @@ export class Immer implements ProducersFns { // Only plain objects, arrays, and "immerable classes" are drafted. if (isDraftable(base)) { const scope = enterScope(this) - const stateMap = this.allowMultiRefs_ ? new Map() : undefined - const proxy = createProxy(base, undefined, stateMap) + const proxy = createProxy(base, undefined) let hasError = true try { result = recipe(proxy) @@ -105,7 +104,7 @@ export class Immer implements ProducersFns { else leaveScope(scope) } usePatchesInScope(scope, patchListener) - return processResult(result, scope, stateMap) + return processResult(result, scope) } else if (!base || typeof base !== "object") { result = recipe(base) if (result === undefined) result = base @@ -142,8 +141,7 @@ export class Immer implements ProducersFns { const scope = enterScope(this) const proxy = createProxy( base, - undefined, - this.allowMultiRefs_ ? new WeakMap() : undefined + undefined ) proxy[DRAFT_STATE].isManual_ = true leaveScope(scope) @@ -157,9 +155,9 @@ export class Immer implements ProducersFns { const state: ImmerState = draft && (draft as any)[DRAFT_STATE] if (!state || !state.isManual_) die(9) - const {scope_: scope, existingStateMap_} = state + const {scope_: scope} = state usePatchesInScope(scope, patchListener) - return processResult(undefined, scope, existingStateMap_) as any + return processResult(undefined, scope) as any } /** @@ -216,25 +214,20 @@ export class Immer implements ProducersFns { export function createProxy( value: T, - parent?: ImmerState, - stateMap: - | WeakMap - | undefined = parent?.existingStateMap_ + parent?: ImmerState ): Drafted { // precondition: createProxy should be guarded by isDraftable, so we know we can safely draft const draft: Drafted = isMap(value) ? getPlugin("MapSet").proxyMap_( value, - parent, - stateMap ?? parent?.existingStateMap_ + parent ) : isSet(value) ? getPlugin("MapSet").proxySet_( value, - parent, - stateMap ?? parent?.existingStateMap_ + parent ) - : createProxyProxy(value, parent, stateMap ?? parent?.existingStateMap_) + : createProxyProxy(value, parent) const scope = parent ? parent.scope_ : getCurrentScope() diff --git a/src/core/proxy.ts b/src/core/proxy.ts index 80447738..760a5659 100644 --- a/src/core/proxy.ts +++ b/src/core/proxy.ts @@ -51,14 +51,15 @@ type ProxyState = ProxyObjectState | ProxyArrayState */ export function createProxyProxy( base: T, - parent?: ImmerState, - stateMap?: WeakMap + parent?: ImmerState ): Drafted { + const scope_ = parent ? parent.scope_ : getCurrentScope()! + const isArray = Array.isArray(base) - const state: ProxyState = (stateMap?.get(base) as ProxyState) || { + const state: ProxyState = (scope_.existingStateMap_?.get(base) as ProxyState) || { type_: isArray ? ArchType.Array : (ArchType.Object as any), // Track which produce call this is associated with. - scope_: parent ? parent.scope_ : getCurrentScope()!, + scope_: scope_, // True for both shallow and deep changes. modified_: false, // Used during finalization. @@ -76,7 +77,6 @@ export function createProxyProxy( // Called by the `produce` function. revoke_: null as any, isManual_: false, - existingStateMap_: stateMap } if (parent && state.parent_ !== parent) { @@ -136,8 +136,7 @@ export const objectTraps: ProxyHandler = { prepareCopy(state) return (state.copy_![prop as any] = createProxy( value, - state, - state.existingStateMap_ + state )) } return value @@ -186,7 +185,6 @@ export const objectTraps: ProxyHandler = { ) return true - // @ts-ignore state.copy_![prop] = value state.assigned_[prop] = true return true @@ -245,8 +243,7 @@ each(objectTraps, (key, fn) => { arrayTraps.deleteProperty = function(state, prop) { if (process.env.NODE_ENV !== "production" && isNaN(parseInt(prop as any))) die(13) - // @ts-ignore - return arrayTraps.set!.call(this, state, prop, undefined) + return arrayTraps.set!.call(this, state, prop, undefined, undefined) } arrayTraps.set = function(state, prop, value) { if ( @@ -308,7 +305,7 @@ export function markChanged(state: ImmerState) { export function prepareCopy(state: ImmerState) { if (state.copy_) return - const existing = state.existingStateMap_?.get(state.base_) + const existing = state.scope_.existingStateMap_?.get(state.base_) if (existing) { Object.assign(state, existing) return @@ -319,5 +316,5 @@ export function prepareCopy(state: ImmerState) { state.scope_.immer_.useStrictShallowCopy_ ) - state.existingStateMap_?.set(state.base_, state) + state.scope_.existingStateMap_?.set(state.base_, state) } diff --git a/src/core/scope.ts b/src/core/scope.ts index 65eb5eed..36a91299 100644 --- a/src/core/scope.ts +++ b/src/core/scope.ts @@ -19,7 +19,8 @@ export interface ImmerScope { parent_?: ImmerScope patchListener_?: PatchListener immer_: Immer - unfinalizedDrafts_: number + unfinalizedDrafts_: number, + existingStateMap_?: WeakMap } let currentScope: ImmerScope | undefined @@ -39,7 +40,8 @@ function createScope( // Whenever the modified draft contains a draft from another scope, we // need to prevent auto-freezing so the unowned draft can be finalized. canAutoFreeze_: true, - unfinalizedDrafts_: 0 + unfinalizedDrafts_: 0, + existingStateMap_: parent_ ? parent_.existingStateMap_ : (immer_.allowMultiRefs_ ? new WeakMap() : undefined) } } diff --git a/src/plugins/mapset.ts b/src/plugins/mapset.ts index 02c06175..8c2dc04c 100644 --- a/src/plugins/mapset.ts +++ b/src/plugins/mapset.ts @@ -24,16 +24,14 @@ export function enableMapSet() { constructor( target: AnyMap, - parent?: ImmerState, - stateMap: - | WeakMap - | undefined = parent?.existingStateMap_ + parent?: ImmerState ) { super() let revoked = false const this_ = this + const scope_ = parent ? parent.scope_ : getCurrentScope()! this[DRAFT_STATE] = new Proxy( - (stateMap?.get(target) as MapState) || { + (scope_.existingStateMap_?.get(target) as MapState) || { type_: ArchType.Map, parent_: parent, scope_: parent ? parent.scope_ : getCurrentScope()!, @@ -45,7 +43,6 @@ export function enableMapSet() { draft_: this as any, isManual_: false, revoked_: false, - existingStateMap_: parent?.existingStateMap_ as any }, { get(target, p, receiver) { @@ -142,7 +139,7 @@ export function enableMapSet() { return value // either already drafted or reassigned } // despite what it looks, this creates a draft only once, see above condition - const draft = createProxy(value, state, state.existingStateMap_) + const draft = createProxy(value, state) prepareMapCopy(state) state.copy_!.set(key, draft) return draft @@ -193,39 +190,33 @@ export function enableMapSet() { function proxyMap_( target: T, - parent?: ImmerState, - stateMap: - | WeakMap - | undefined = parent?.existingStateMap_ + parent?: ImmerState ): T { - // @ts-ignore - return new DraftMap(target, parent, stateMap) + return new DraftMap(target, parent) as unknown as T } function prepareMapCopy(state: MapState) { if (state.copy_) return state.assigned_ = new Map() state.copy_ = new Map(state.base_) - state.existingStateMap_?.set(state.base_, state) + state.scope_.existingStateMap_?.set(state.base_, state) } class DraftSet extends Set { [DRAFT_STATE]: SetState constructor( target: AnySet, - parent?: ImmerState, - stateMap: - | WeakMap - | undefined = parent?.existingStateMap_ + parent?: ImmerState ) { super() let revoked = false const this_ = this + const scope_ = parent ? parent.scope_ : getCurrentScope()! this[DRAFT_STATE] = new Proxy( - (stateMap?.get(target) as SetState) || { + (scope_.existingStateMap_?.get(target) as SetState) || { type_: ArchType.Set, parent_: parent, - scope_: parent ? parent.scope_ : getCurrentScope()!, + scope_, modified_: false, finalized_: false, copy_: undefined, @@ -233,8 +224,7 @@ export function enableMapSet() { draft_: this, drafts_: new Map(), revoked_: false, - isManual_: false, - existingStateMap_: parent?.existingStateMap_ as any + isManual_: false }, { get(target, p, receiver) { @@ -349,13 +339,9 @@ export function enableMapSet() { function proxySet_( target: T, - parent?: ImmerState, - stateMap: - | WeakMap - | undefined = parent?.existingStateMap_ + parent?: ImmerState ): T { - // @ts-ignore - return new DraftSet(target, parent, stateMap) + return new DraftSet(target, parent) as unknown as T } const unusedValueSymbol = Symbol("unused") @@ -364,12 +350,11 @@ export function enableMapSet() { if (state.copy_) return // create drafts for all entries to preserve insertion order state.copy_ = new Set() - // @ts-ignore - state.existingStateMap_?.set(state.base_, state) + state.scope_.existingStateMap_?.set(state.base_, state) state.base_.forEach(value => { if (isDraftable(value)) { - const draft = createProxy(value, state, state.existingStateMap_) - if (state.existingStateMap_) + const draft = createProxy(value, state) + if (state.scope_.existingStateMap_) draft[unusedValueSymbol] = unusedValueSymbol state.drafts_.set(value, draft) state.copy_!.add(draft) diff --git a/src/types/types-internal.ts b/src/types/types-internal.ts index 47cf6b83..f21c8554 100644 --- a/src/types/types-internal.ts +++ b/src/types/types-internal.ts @@ -29,7 +29,6 @@ export interface ImmerBaseState { modified_: boolean finalized_: boolean isManual_: boolean - existingStateMap_?: WeakMap | undefined } export type ImmerState = diff --git a/src/utils/plugins.ts b/src/utils/plugins.ts index 4db4f97f..d447d9de 100644 --- a/src/utils/plugins.ts +++ b/src/utils/plugins.ts @@ -30,13 +30,11 @@ const plugins: { MapSet?: { proxyMap_( target: T, - parent?: ImmerState, - stateMap?: WeakMap + parent?: ImmerState ): T proxySet_( target: T, - parent?: ImmerState, - stateMap?: WeakMap + parent?: ImmerState ): T } } = {} From c748d5009540e4297bccc751ed050d2f64c0c4be Mon Sep 17 00:00:00 2001 From: BellCube Dev <33764825+BellCubeDev@users.noreply.github.com> Date: Sat, 20 Apr 2024 21:11:44 -0400 Subject: [PATCH 6/7] Support Deeply-Nested Undrafted Multiref Objects + Fix More Tests See new tests and comments in multiref.ts for elaboration regarding the extended multiref support included in this commit. --- __tests__/__snapshots__/base.js.snap | 376 +++++++++++++++++++++++++++ __tests__/base.js | 13 +- __tests__/multiref.ts | 83 +++++- src/core/finalize.ts | 105 ++++++-- src/core/proxy.ts | 11 +- src/plugins/mapset.ts | 23 +- 6 files changed, 576 insertions(+), 35 deletions(-) diff --git a/__tests__/__snapshots__/base.js.snap b/__tests__/__snapshots__/base.js.snap index 892615f4..496056c0 100644 --- a/__tests__/__snapshots__/base.js.snap +++ b/__tests__/__snapshots__/base.js.snap @@ -959,6 +959,53 @@ exports[`complex nesting map / set / object - auto-freeze=false:shallow-copy=fal ] `; +exports[`complex nesting map / set / object - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=true modify deep object 1`] = ` +{ + "map": Map { + "set1" => Set { + { + "a": 2, + }, + { + "b": 2, + }, + }, + "set2" => Set { + { + "c": 3, + }, + }, + }, +} +`; + +exports[`complex nesting map / set / object - auto-freeze=false:shallow-copy=false:use-listener=false:allow-multi-refs=true modify deep object 2`] = ` +[ + { + "op": "remove", + "path": [ + "map", + "set1", + 0, + ], + "value": { + "a": 1, + }, + }, + { + "op": "add", + "path": [ + "map", + "set1", + 0, + ], + "value": { + "a": 2, + }, + }, +] +`; + exports[`complex nesting map / set / object - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=false modify deep object 1`] = ` { "map": Map { @@ -1006,6 +1053,53 @@ exports[`complex nesting map / set / object - auto-freeze=false:shallow-copy=fal ] `; +exports[`complex nesting map / set / object - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=true modify deep object 1`] = ` +{ + "map": Map { + "set1" => Set { + { + "a": 2, + }, + { + "b": 2, + }, + }, + "set2" => Set { + { + "c": 3, + }, + }, + }, +} +`; + +exports[`complex nesting map / set / object - auto-freeze=false:shallow-copy=false:use-listener=true:allow-multi-refs=true modify deep object 2`] = ` +[ + { + "op": "remove", + "path": [ + "map", + "set1", + 0, + ], + "value": { + "a": 1, + }, + }, + { + "op": "add", + "path": [ + "map", + "set1", + 0, + ], + "value": { + "a": 2, + }, + }, +] +`; + exports[`complex nesting map / set / object - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=false modify deep object 1`] = ` { "map": Map { @@ -1053,6 +1147,53 @@ exports[`complex nesting map / set / object - auto-freeze=false:shallow-copy=tru ] `; +exports[`complex nesting map / set / object - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=true modify deep object 1`] = ` +{ + "map": Map { + "set1" => Set { + { + "a": 2, + }, + { + "b": 2, + }, + }, + "set2" => Set { + { + "c": 3, + }, + }, + }, +} +`; + +exports[`complex nesting map / set / object - auto-freeze=false:shallow-copy=true:use-listener=false:allow-multi-refs=true modify deep object 2`] = ` +[ + { + "op": "remove", + "path": [ + "map", + "set1", + 0, + ], + "value": { + "a": 1, + }, + }, + { + "op": "add", + "path": [ + "map", + "set1", + 0, + ], + "value": { + "a": 2, + }, + }, +] +`; + exports[`complex nesting map / set / object - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=false modify deep object 1`] = ` { "map": Map { @@ -1100,6 +1241,53 @@ exports[`complex nesting map / set / object - auto-freeze=false:shallow-copy=tru ] `; +exports[`complex nesting map / set / object - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=true modify deep object 1`] = ` +{ + "map": Map { + "set1" => Set { + { + "a": 2, + }, + { + "b": 2, + }, + }, + "set2" => Set { + { + "c": 3, + }, + }, + }, +} +`; + +exports[`complex nesting map / set / object - auto-freeze=false:shallow-copy=true:use-listener=true:allow-multi-refs=true modify deep object 2`] = ` +[ + { + "op": "remove", + "path": [ + "map", + "set1", + 0, + ], + "value": { + "a": 1, + }, + }, + { + "op": "add", + "path": [ + "map", + "set1", + 0, + ], + "value": { + "a": 2, + }, + }, +] +`; + exports[`complex nesting map / set / object - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=false modify deep object 1`] = ` { "map": Map { @@ -1147,6 +1335,53 @@ exports[`complex nesting map / set / object - auto-freeze=true:shallow-copy=fals ] `; +exports[`complex nesting map / set / object - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=true modify deep object 1`] = ` +{ + "map": Map { + "set1" => Set { + { + "a": 2, + }, + { + "b": 2, + }, + }, + "set2" => Set { + { + "c": 3, + }, + }, + }, +} +`; + +exports[`complex nesting map / set / object - auto-freeze=true:shallow-copy=false:use-listener=false:allow-multi-refs=true modify deep object 2`] = ` +[ + { + "op": "remove", + "path": [ + "map", + "set1", + 0, + ], + "value": { + "a": 1, + }, + }, + { + "op": "add", + "path": [ + "map", + "set1", + 0, + ], + "value": { + "a": 2, + }, + }, +] +`; + exports[`complex nesting map / set / object - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=false modify deep object 1`] = ` { "map": Map { @@ -1194,6 +1429,53 @@ exports[`complex nesting map / set / object - auto-freeze=true:shallow-copy=fals ] `; +exports[`complex nesting map / set / object - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=true modify deep object 1`] = ` +{ + "map": Map { + "set1" => Set { + { + "a": 2, + }, + { + "b": 2, + }, + }, + "set2" => Set { + { + "c": 3, + }, + }, + }, +} +`; + +exports[`complex nesting map / set / object - auto-freeze=true:shallow-copy=false:use-listener=true:allow-multi-refs=true modify deep object 2`] = ` +[ + { + "op": "remove", + "path": [ + "map", + "set1", + 0, + ], + "value": { + "a": 1, + }, + }, + { + "op": "add", + "path": [ + "map", + "set1", + 0, + ], + "value": { + "a": 2, + }, + }, +] +`; + exports[`complex nesting map / set / object - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=false modify deep object 1`] = ` { "map": Map { @@ -1241,6 +1523,53 @@ exports[`complex nesting map / set / object - auto-freeze=true:shallow-copy=true ] `; +exports[`complex nesting map / set / object - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=true modify deep object 1`] = ` +{ + "map": Map { + "set1" => Set { + { + "a": 2, + }, + { + "b": 2, + }, + }, + "set2" => Set { + { + "c": 3, + }, + }, + }, +} +`; + +exports[`complex nesting map / set / object - auto-freeze=true:shallow-copy=true:use-listener=false:allow-multi-refs=true modify deep object 2`] = ` +[ + { + "op": "remove", + "path": [ + "map", + "set1", + 0, + ], + "value": { + "a": 1, + }, + }, + { + "op": "add", + "path": [ + "map", + "set1", + 0, + ], + "value": { + "a": 2, + }, + }, +] +`; + exports[`complex nesting map / set / object - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=false modify deep object 1`] = ` { "map": Map { @@ -1288,6 +1617,53 @@ exports[`complex nesting map / set / object - auto-freeze=true:shallow-copy=true ] `; +exports[`complex nesting map / set / object - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=true modify deep object 1`] = ` +{ + "map": Map { + "set1" => Set { + { + "a": 2, + }, + { + "b": 2, + }, + }, + "set2" => Set { + { + "c": 3, + }, + }, + }, +} +`; + +exports[`complex nesting map / set / object - auto-freeze=true:shallow-copy=true:use-listener=true:allow-multi-refs=true modify deep object 2`] = ` +[ + { + "op": "remove", + "path": [ + "map", + "set1", + 0, + ], + "value": { + "a": 1, + }, + }, + { + "op": "add", + "path": [ + "map", + "set1", + 0, + ], + "value": { + "a": 2, + }, + }, +] +`; + exports[`complex nesting map / set / object modify deep object 1`] = ` { "map": Map { diff --git a/__tests__/base.js b/__tests__/base.js index bf3e97dc..2bd1b6dc 100644 --- a/__tests__/base.js +++ b/__tests__/base.js @@ -1038,7 +1038,7 @@ function runBaseTest(name, autoFreeze, useStrictShallowCopy, useListener, allowM }) }) - it("optimization: does not visit properties of new data structures if autofreeze is disabled and no drafts are unfinalized", () => { + it("optimization: does not visit properties of new data structures if autofreeze and multiref are disabled and no drafts are unfinalized", () => { const newData = {} Object.defineProperty(newData, "x", { enumerable: true, @@ -1051,7 +1051,7 @@ function runBaseTest(name, autoFreeze, useStrictShallowCopy, useListener, allowM produce({}, d => { d.data = newData }) - if (autoFreeze) { + if (autoFreeze || allowMultiRefs) { expect(run).toThrow("visited!") } else { expect(run).not.toThrow("visited!") @@ -1537,7 +1537,7 @@ function runBaseTest(name, autoFreeze, useStrictShallowCopy, useListener, allowM expect(world.inc(world).counter.count).toBe(2) }) - it("doesnt recurse into frozen structures if external data is frozen", () => { + it("doesnt recurse into frozen structures if external data is frozen and is not multiref", () => { const frozen = {} Object.defineProperty(frozen, "x", { get() { @@ -1548,11 +1548,14 @@ function runBaseTest(name, autoFreeze, useStrictShallowCopy, useListener, allowM }) Object.freeze(frozen) - expect(() => { + const expectTest = expect(() => { produce({}, d => { d.x = frozen }) - }).not.toThrow() + }) + + if (allowMultiRefs) expectTest.toThrow("oops") + else expectTest.not.toThrow("oops") }) // See here: https://github.com/mweststrate/immer/issues/89 diff --git a/__tests__/multiref.ts b/__tests__/multiref.ts index 312ed63f..b5e25ead 100644 --- a/__tests__/multiref.ts +++ b/__tests__/multiref.ts @@ -1,5 +1,6 @@ import {Immer, enableMapSet} from "../src/immer" import {inspect} from "util" +import * as v8 from "v8" // Implementation note: TypeScript says ES5 doesn't support iterating directly over a Set so I've used Array.from(). // If the project is moved to a later JS feature set, we can drop the Array.from() and do `for (const value of ref)` instead. @@ -9,8 +10,7 @@ test("modified circular object", () => { const base = {a: 1, b: null} as any base.b = base - const envs = ["production", "development", "testing"] - for (const env of envs) { + for (const env of ["production", "development", "testing"]) { process.env.NODE_ENV = env expect(() => { const next = immer.produce(base, (draft: any) => { @@ -26,8 +26,7 @@ test("unmodified circular object", () => { const base = {a: 1, b: null} as any base.b = base - const envs = ["production", "development", "testing"] - for (const env of envs) { + for (const env of ["production", "development", "testing"]) { process.env.NODE_ENV = env expect(() => { const next = immer.produce({state: null}, (draft: any) => { @@ -38,6 +37,82 @@ test("unmodified circular object", () => { } }) +test("circular object using a draft and a non-draft", () => { + const immer = new Immer({allowMultiRefs: true}) + const baseState: Record = { + someValue: 'abcd', + state1: null, + state2: {state: null}, + state3: {state_: {state: null}}, + state4: {state__: {state_: {state: null}}}, + } + baseState.state1 = baseState + baseState.state2.state = baseState + baseState.state3.state_.state = baseState + baseState.state4.state__.state_.state = baseState + + for (const env of ["production", "development", "testing"]) { + process.env.NODE_ENV = env + v8.setFlagsFromString("--stack-size=2000") + expect(() => { + const next = immer.produce(baseState, (draft: any) => { + draft.state3.state_.state.someValue = 'efgh' + draft.state2.state.someValue = 'ijkl' + }) + + expect(next.someValue).toBe('ijkl') + }).not.toThrow('Maximum call stack size exceeded') + } +}); + +test("replacing a deeply-nested value modified elsewhere does not modify the original object", () => { + + // When failing, produces the following output: + // originalBase: { + // someState: { something: { b: 'b' } }, + // someOtherState: { someState: { something: { b: 'b' } } } + // }, + // base: { + // someState: { something: { b: 'b' } }, + // someOtherState: { someState: { something: { b: 'a modified value' } } } // <----- ⚠ This should be 'b'; we just modified the original state! ⚠ + // }, + // next: { + // someState: { something: { b: 'a modified value' } }, + // someOtherState: { someState: { something: { b: 'a modified value' } } } + // } + + + const immer = new Immer({allowMultiRefs: true}) + + const someState = {something: {b: 'b'}} + const base = {someState, someOtherState: { someState }} + + const originalBaseString = JSON.stringify(base) + + const next = immer.produce(base, draft => { + draft.someState.something.b = 'a modified value' + }) + + let hasError = true + + try { + expect(next.someOtherState.someState).toBe(next.someState) // Make sure multi-ref is working on the surface first + + expect(next.someState.something.b).toBe('a modified value') + expect(base.someState.something.b).not.toBe('a modified value') + expect(next.someOtherState).not.toBe(base.someOtherState) + + expect(next.someOtherState.someState.something.b).toBe('a modified value') + expect(base.someOtherState.someState.something.b).not.toBe('a modified value') + expect(next.someOtherState).not.toBe(base.someOtherState) + hasError = false + } finally { + if (hasError) console.log('Objects for test "replacing a deeply-nested value modified elsewhere does not modify the original object":', + inspect({originalBase: JSON.parse(originalBaseString), base, next}, {depth: 99, colors: true}) + ) + } +}); + describe("access value & change child's child value", () => { describe("with object", () => { const immer = new Immer({allowMultiRefs: true}) diff --git a/src/core/finalize.ts b/src/core/finalize.ts index 3b21cee4..46d84099 100644 --- a/src/core/finalize.ts +++ b/src/core/finalize.ts @@ -16,6 +16,10 @@ import { die, revokeScope, isFrozen, + createProxy, + enterScope, + markChanged, + latest, } from "../internal" export function processResult( @@ -32,8 +36,7 @@ export function processResult( } if (isDraftable(result)) { // Finalize the result in case it contains (or is) a subset of the draft. - result = finalize(scope, result) - if (!scope.parent_) maybeFreeze(scope, result, false) + result = enterFinalize(scope, result) } if (scope.patches_) { getPlugin("Patches").generateReplacementPatches_( @@ -45,7 +48,7 @@ export function processResult( } } else { // Finalize the base draft. - result = finalize(scope, baseDraft, []) + result = enterFinalize(scope, baseDraft) } revokeScope(scope) if (scope.patches_) { @@ -54,20 +57,66 @@ export function processResult( return result !== NOTHING ? result : undefined } +// If we have an existingStateMap, enter a second scope while finalize the draft to catch any proxies we create during the process. +// That way, we can catch any deeply-nested objects which weren't drafted in the recipe WITHOUT modifying the original state. +// see the `multiref.ts` test "replacing a deeply-nested value modified elsewhere does not modify the original object" +function enterFinalize( + scope: ImmerScope, + value: any +) { + let secondScope = null; + try { + if (scope.existingStateMap_) { + secondScope = enterScope(scope.immer_) + secondScope.parent_ = scope + secondScope.existingStateMap_ = scope.existingStateMap_ + if (!isDraft(value)) { + value = createProxy(value, undefined) + } + } + const oldStateDEBUGGINGONLY = value[DRAFT_STATE] + + value = finalize(scope, secondScope, value, []) + + if (oldStateDEBUGGINGONLY) console.log("DEBUGGING!"); + } finally { + if (secondScope) { + revokeScope(secondScope) + } + } + if (!scope.parent_) maybeFreeze(scope, value, false) + return value +} + +function finalizeIfIsDraft( + rootScope: ImmerScope, + secondScope: ImmerScope | null, + value: any, + path?: PatchPath, + encounteredObjects = new WeakSet(), +): any { + if (isDraft(value)) { + return finalize(rootScope, secondScope, value, path, encounteredObjects) + } + return value +} + function finalize( rootScope: ImmerScope, + secondScope: ImmerScope | null, value: any, path?: PatchPath, - encounteredObjects = new WeakSet() + encounteredObjects = new WeakSet(), ): any { let state: ImmerState | undefined = value[DRAFT_STATE] // Never finalize drafts owned by another scope. - if (state && state.scope_ !== rootScope) return value + if (state && state.scope_ !== rootScope && state.scope_ !== secondScope) return value // Don't recurse into recursive data structures if (isFrozen(value) || encounteredObjects.has(state ? state.base_ : value)) return state ? (state.modified_? state.copy_ : state.base_) : value encounteredObjects.add(state ? state.base_ : value) + if (state?.copy_) encounteredObjects.add(state.copy_) // A plain object, might need freezing, might contain drafts @@ -77,6 +126,7 @@ function finalize( (key, childValue) => finalizeProperty( rootScope, + secondScope, state, value, key, @@ -86,12 +136,13 @@ function finalize( encounteredObjects ) ) - return state ? (state.copy_ ? state.copy_ : state.base_) : value + + if (!state) return finalizeIfIsDraft(rootScope, secondScope, value, path, encounteredObjects) } // Unmodified draft, return the (frozen) original if (!state.modified_) { - maybeFreeze(rootScope, state.copy_ ?? state.base_, true) - return state.base_ + maybeFreeze(rootScope, state.base_, true) + return finalizeIfIsDraft(rootScope, secondScope, state.base_, path, encounteredObjects) } // Not finalized yet, let's do that now if (!state.finalized_) { @@ -112,6 +163,7 @@ function finalize( each(resultEach, (key, childValue) => finalizeProperty( rootScope, + secondScope, state, result, key, @@ -134,11 +186,12 @@ function finalize( } } - return state.copy_ + return finalizeIfIsDraft(rootScope, secondScope, state.copy_, path, encounteredObjects) } function finalizeProperty( rootScope: ImmerScope, + secondScope: ImmerScope | null, parentState: undefined | ImmerState, targetObject: any, prop: string | number, @@ -147,16 +200,21 @@ function finalizeProperty( targetIsSet?: boolean, encounteredObjects = new WeakSet() ) { - if (process.env.NODE_ENV !== "production" && childValue === targetObject) - die(5) - - if (!isDraft(childValue) && isDraftable(childValue)) { - const existingState = rootScope.existingStateMap_?.get(childValue) - if (existingState) { - childValue = existingState.draft_ + if (!rootScope.existingStateMap_) { + if (process.env.NODE_ENV !== "production" && childValue === targetObject) + die(5) + } else { + if (!isDraft(childValue) && isDraftable(childValue)) { + const existingState = rootScope.existingStateMap_.get(childValue) + if (existingState) { + childValue = existingState.draft_ + } else { + childValue = createProxy(childValue, parentState) + } } } + if (isDraft(childValue)) { const path = rootPath && @@ -165,11 +223,19 @@ function finalizeProperty( !has((parentState as Exclude).assigned_!, prop) // Skip deep patches for assigned keys. ? rootPath!.concat(prop) : undefined + // Drafts owned by `scope` are finalized here. - const res = finalize(rootScope, childValue, path, encounteredObjects) + const state = childValue[DRAFT_STATE] + const res = finalize(rootScope, secondScope, childValue, path, encounteredObjects) set(targetObject, prop, res) + // Drafts from another scope must prevented to be frozen // if we got a draft back from finalize, we're in a nested produce and shouldn't freeze + + if (parentState && rootScope.existingStateMap_ && state.modified_) { + markChanged(parentState) + } + if (isDraft(res)) { rootScope.canAutoFreeze_ = false } else return @@ -189,6 +255,7 @@ function finalizeProperty( } finalize( rootScope, + secondScope, childValue, undefined, encounteredObjects @@ -205,9 +272,9 @@ function finalizeProperty( } } -function maybeFreeze(scope: ImmerScope, value: any, deep = false) { +function maybeFreeze(rootScope: ImmerScope, value: any, deep = false) { // we never freeze for a non-root scope; as it would prevent pruning for drafts inside wrapping objects - if (!scope.parent_ && scope.immer_.autoFreeze_ && scope.canAutoFreeze_) { + if (!rootScope.parent_ && rootScope.immer_.autoFreeze_ && rootScope.canAutoFreeze_) { freeze(value, deep) } } diff --git a/src/core/proxy.ts b/src/core/proxy.ts index 760a5659..a963ba06 100644 --- a/src/core/proxy.ts +++ b/src/core/proxy.ts @@ -17,7 +17,8 @@ import { die, createProxy, ArchType, - ImmerScope + ImmerScope, + isDraft } from "../internal" interface ProxyBaseState extends ImmerBaseState { @@ -114,6 +115,14 @@ export function createProxyProxy( return state.draft_ as any } +function isScopeDescendedFrom(parent: ImmerScope, child: ImmerScope | undefined) { + while (child) { + if (child === parent) return true + child = child.parent_ + } + return false +} + /** * Object drafts */ diff --git a/src/plugins/mapset.ts b/src/plugins/mapset.ts index 8c2dc04c..96961820 100644 --- a/src/plugins/mapset.ts +++ b/src/plugins/mapset.ts @@ -15,7 +15,8 @@ import { die, ArchType, each, - Objectish + Objectish, + isDraft } from "../internal" export function enableMapSet() { @@ -76,10 +77,24 @@ export function enableMapSet() { return latest(this[DRAFT_STATE]).has(key) } + isValueChanging(key: any, value: any): boolean { + const state: MapState = this[DRAFT_STATE] + if (!latest(state).has(key)) return true + + const baseValue = latest(state).get(key) + + if (baseValue === value) return false + + if (isDraft(baseValue) && latest(baseValue[DRAFT_STATE]) === value) return false + + return true + } + set(key: any, value: any) { const state: MapState = this[DRAFT_STATE] + const valueState: ImmerState | false = isDraft(value) && value[DRAFT_STATE] assertUnrevoked(state) - if (!latest(state).has(key) || latest(state).get(key) !== value) { + if (this.isValueChanging(key, value)) { prepareMapCopy(state) markChanged(state) state.assigned_!.set(key, true) @@ -344,8 +359,6 @@ export function enableMapSet() { return new DraftSet(target, parent) as unknown as T } - const unusedValueSymbol = Symbol("unused") - function prepareSetCopy(state: SetState) { if (state.copy_) return // create drafts for all entries to preserve insertion order @@ -354,8 +367,6 @@ export function enableMapSet() { state.base_.forEach(value => { if (isDraftable(value)) { const draft = createProxy(value, state) - if (state.scope_.existingStateMap_) - draft[unusedValueSymbol] = unusedValueSymbol state.drafts_.set(value, draft) state.copy_!.add(draft) } else { From 4b22cb51b7810d26e52d03e3a4420b00fb985993 Mon Sep 17 00:00:00 2001 From: BellCube Dev <33764825+BellCubeDev@users.noreply.github.com> Date: Sun, 21 Apr 2024 15:08:10 -0400 Subject: [PATCH 7/7] fix: remove console.log calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 👀 left those in by mistake --- src/core/finalize.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/core/finalize.ts b/src/core/finalize.ts index 46d84099..36fff7bd 100644 --- a/src/core/finalize.ts +++ b/src/core/finalize.ts @@ -65,6 +65,7 @@ function enterFinalize( value: any ) { let secondScope = null; + try { if (scope.existingStateMap_) { secondScope = enterScope(scope.immer_) @@ -74,16 +75,14 @@ function enterFinalize( value = createProxy(value, undefined) } } - const oldStateDEBUGGINGONLY = value[DRAFT_STATE] value = finalize(scope, secondScope, value, []) - - if (oldStateDEBUGGINGONLY) console.log("DEBUGGING!"); } finally { if (secondScope) { revokeScope(secondScope) } } + if (!scope.parent_) maybeFreeze(scope, value, false) return value }