Some state trees may need to reference an object more than once (such as the tree for my 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 property keeps track of additional values that would normally be attached to parent_ so that functionality relying on parent_ (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.
During finalization, there may be a case where an unmodified object contains a reference to a modified object as a child and, thus, even unmodified children must be finalized. All objects encountered during finalization must be drafted and traversed so we can catch references to objects which were never referenced in the recipe.
Frozen data structures are not guaranteed to be immune to this and therefore should be warned against.
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 class 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 1-to-1 circular references (which Immer does a sanity check against normally) should not throw an error when multi-ref is enabled
When the properties of multiple references are modified, all references are modified
A number of tests to verify this functionality for each archetype of object are provided.
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)
Additionally, I've added allowMultiRefs into the base.js test matrix and adjusted tests as necessary to accommodate for allowMultiRefs' required functionality.
Tests are performed on all relevant object archetypes where applicable.
Outstanding Discussion TODOs
[ ] What to do regarding documentation
[ ] Add an error for when WeakMap isn't supported in the current environment? (supported in every noteworthy browser and server environment since late 2015)
[!NOTE]
I have yet to compare the performance of stock Immer to the version of Immer created by my pull request without allowMultiRefs enabled. There may be no significant difference but work might be needed to address significant performance regressions should they exist.
[!CAUTION]
Some tests are still failing when allowMultiRefs is set to true. That said, NO TESTS FAIL WHEN allowMultiRefs IS SET TO false.
What This Does
Some state trees may need to reference an object more than once (such as the tree for my 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
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.revoke_()
function that, once called the first time, calls the oldrevoke_()
function. The result is that the finalrevoke_()
must be called once for every requested draft before the Proxy is finally revoked. Since a proxy which has has itsrevoke_()
method called should be considered revoked by all code paths, duplicate calls should not be an issue.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.extraParents_
property to theImmerBaseState
interface. This property keeps track of additional values that would normally be attached toparent_
so that functionality relying on parent_ (such as marking the parent state as modified) is retained for objects with multiple parent objectsuseStrictShallowCopy
). That is, either specify it in the config object passed to the class's constructor OR call the relevant class method,setAllowMultiRefs()
Tests
The file
__tests__/multiref.ts
contains a number of tests related to this multi-reference support implementation. Such tests seek to verify that:===
] just as the references beforeproduce()
would have been)Additionally, I've added
allowMultiRefs
into the base.js test matrix and adjusted tests as necessary to accommodate forallowMultiRefs
' required functionality.Tests are performed on all relevant object archetypes where applicable.
Outstanding Discussion TODOs