Set, Map, WeakMap, etc?I had a quick try at adding Set and Map for es6 environment. I don't completely get everything happening in immer, so I well may have missed some complexities. At first glance it seems actually pretty easy to proxy these since all mutations happen through method calls:
const nonMutatingMethods = [
"forEach",
"get",
"has",
"keys",
"set",
"values",
"entries",
"@@iterator"
]
const mutatingMethods = ["set", "delete", "clear", "add"]
const properties = ["size"]
function source(state) {
return state.modified ? state.copy : state.base
}
export function proxyMapOrSet(state) {
var proxyState = state
var proxy = {}
nonMutatingMethods.forEach(function(method) {
proxy[method] = function(...args) {
return source(proxyState)[method](...args)
}
})
properties.forEach(function(prop) {
proxy[prop] = source(proxyState)[prop]
})
mutatingMethods.forEach(function(method) {
proxy[method] = function(...args) {
markChanged(proxyState)
return proxyState.copy[method](...args)
}
})
createHiddenProperty(proxy, PROXY_STATE, proxyState)
return {proxy}
}
I guess you don't need to recurse into the Map/Set values since they are all set through method calls?
but it seems you can't freeze these, which means you have to do something kind of hacky there I guess:
function freezeMapOrSet(value) {
const insertMethod = isMap(value) ? "set" : "add"
if (autoFreeze) {
;[insertMethod, "delete", "clear"].forEach(function(method) {
Object.defineProperty(value, method, {
value: function() {
throw "In a frozen state, cannot be mutated"
},
enumerable: false,
writable: false
})
})
Object.freeze(value)
}
return value
}
Tried it out today here https://github.com/RikuVan/immer/tree/support_map_and_set with only a few basic tests
So I wanted to get full support for ES6 Map/Set. I also needed to get change logs of every update -- since the proxy makes this pretty efficient I took my own stab at re-building the library a bit.
It is still a little messy since I was just getting it into the fold but the example does a good example of showing it in action:
import immuta from "immuta";
import printDifference from "immuta/utils/print-difference";
const state = {
deep: {
foo: {
bar: {
baz: true
}
},
set: new Set([{ one: "two" }, { two: "three" }]),
map: new Map([["one", { foo: "bar" }]]),
array: [{ i: 1 }, { i: 2 }, { i: 3 }, { i: 4 }, { i: 5 }]
}
};
const next = immuta(
// provide the state to start with
state,
// draft is a proxy that will copy-on-write
draft => {
const one = draft.deep.map.get("one");
if (one) {
one.foo = 1;
}
draft.deep.set.clear();
draft.deep.set.add({ some: "obj" });
draft.deep.array[2].foo = "bar!";
},
// optional callback for change events
(changedState, changedMap, rollback) => {
// rollback() will cancel the changes and return original object
// changedMap is Map { 'path.to.change' => changedValue }
// changedState is the new state being returned to caller (nextState)
changedMap.forEach((v, changedKey) => {
console.log("Change: ", changedKey);
});
}
);
printDifference(state, next);
Change: deep
Change: deep.map(=> [MAP])
Change: deep.map(=> [MAP]).get.(one)
Change: deep.map(=> [MAP]).get.(one).foo
Change: deep.set(=> [SET])
Change: deep.array
Change: deep.array.2
Change: deep.array.2.foo

As for the Object.freeze - you can't freeze it directly using Object.freeze() but you could certainly use a proxy to do it. Not quite there yet.
Probably the most annoying part is having to handle stepping multiple times when receiving functions (to determine if the function is supposed to belong to our Map/Set). Question also becomes whether or not the support should instead be "Set-like and Map-like instances" rather than requiring it be a true instanceof
For me I also needed to add the extra syntax and RE which will be a perf hit - plan to rethink how that is implemented shortly.
It should go as deep as needed. It will obviously not, and never should, support Weak anything.
Although I've done some thinking about what it would look like to take this to the next level and support function calls with given arguments (since Set/Map required the creation of such a concept). This would be kind of insane/interesting but probably a bad idea/for another lib :-P
Hoping my efforts here might be of some use in some way to y'all! Thanks for the concept, it's solid!
Update - Just to update - got one last blocker in place but believe im able to rid most of the hurdles here using this format and it should translate well into immer
Cool, I am curious to look at your version more carefully as soon as I have more time.
Yeah it is working well now and performs very well. As for mentioning not needing to recurse, I disagree. Take this example:
const state = {
deep: {
foo: {
map: new Map([['one', { foo: 'bar' }]]),
},
};
const next = immuta(
// provide the state to start with
state,
// draft is a proxy that will copy-on-write
draft => {
const val = draft.deep.map.get('one');
val.baz = 'qux';
},
// optional callback for change events
(changedState, changedMap, rollback) => {
// rollback() will cancel the changes and return original object
// changedMap is Map { 'path.to.change' => changedValue }
// changedState is the new state being returned to caller (nextState)
changedMap.forEach((v, changedKey) => {
console.log('Change: ', changedKey);
});
},
);
Which I need to improve still but it works for all cases I've tested against thus far. Just would be issue with non-confirming prototype methods so need to do an instance check
I provided some more insight into some of the challenges with Set (and a little bit into Map) with the release of some extra helpers to handle deep merging. They may also be helpful to anyone who might want to take on such a task.
https://github.com/odo-network/immuta/issues/1
Essentially Set introduces a fairly difficult problem in that it when you want to change the reference to the item in the Set you actually need to delete the original value which ends up changing the insertion order of the Set.
Merge is essentially impossible because of this, when insertion order is no longer maintained there is really no way to "map" the desired values to their originals that we want to merge with.
Overall Map is pretty perfect (aside from keys not being immutable), but Set is fair to say is almost guaranteed to cause problems when merging - and possible to cause issues when doing standard mutations in a draft (using iteration) -- which would be fairly slow anyway since it cant use mergeWithDraft and must iterate the proxy.
For now added some docs on how to work with Map's and Set's in combination with Immer:
https://github.com/mweststrate/immer/commit/d9b4d566f4e6043384ab10a758c5ff943b33f57a
Closing this issue for now, but if PR shows up that supports both Maps and Sets completely, with deep updates and also works in ES5 mode :)
Someone might want to add this PR? :)
Support for maps and set is the last thing that is missing in this cool library. Then I could get rid of the dependence on immutable.js
@mweststrate what if a PR for ES6 only is going to show up? Moreover, what if it is only for Maps?
Let me be honest, I use Maps a lot and I really really would like immer to have a first-class support for them. Yet, I, fortunately, do not have to support ES5 envs in my projects. So I'm wondering if it makes sense for me to work on a PR for immer which includes at least (but not at most) ES6 support for Maps only?
My priorities would be:
Would it be beneficial for the community to have at least ES6 Maps support? Would you be willing to merge such a PR?
@mweststrate I also have a question on the design. Would you expect a mutation of a Map item to cause a creation of a new Map?
In other words, should this work?
const state = {
users: new Map([["michel", {name: "miche"}]])
}
const nextState = produce(state, draft => {
const user = draft.users.get("michel")
// Should this one work, effectively creating a new Map?
user.name = 'Aladdin'
})
Yes :)
On Sun, Apr 14, 2019 at 9:38 PM Andrey Goncharov notifications@github.com
wrote:
@mweststrate https://github.com/mweststrate I also have a question on
the design. Would you expect a mutation of a Map item to cause a creation
of a new Map?In other words, should this work?
const state = {
users: new Map([["michel", {name: "miche"}]])
}
const nextState = produce(state, draft => {
const user = draft.users.get("michel")
// Should this one work?
user.name = 'Aladdin'
})—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/mweststrate/immer/issues/146#issuecomment-483048217,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ABvGhL9GjxMi-EVHIFrSHTm7ICQVeJImks5vg4OkgaJpZM4Tp_Wl
.
So far I managed to make an early preview of nested updates for ES6 Maps. The ongoing work can be tracked here. If anyone is interested in providing any kind of help with this feature please email me at andrey.[email protected]
@mweststrate PR for ES6 maps https://github.com/immerjs/immer/pull/350
Reopened this issue, as the PR is still pending,
@mweststrate @aleclarson Guys look at this https://github.com/immerjs/immer/pull/437
any updates on this?
If the PR is solid, it's coming, but didn't have any time to review yet.
It would really help us because we have built our state with Set and
appears that it became mutable and mutates primary object...
On Fri, Oct 11, 2019, 1:35 AM Michel Weststrate notifications@github.com
wrote:
If the PR is solid, it's coming, but didn't have any time to review yet.
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/immerjs/immer/issues/146?email_source=notifications&email_token=AA54ZF6DLJ7T6JRYETSSHY3QN632RA5CNFSM4E5H6WS2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEA6JEPI#issuecomment-540840509,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AA54ZF752ENTHQAE2SUNCCTQN632RANCNFSM4E5H6WSQ
.
This has been released as Immer 5.0!
Most helpful comment
This has been released as Immer 5.0!