Store state.messages as an Immutable.Map.#4390
Conversation
gnprice
left a comment
There was a problem hiding this comment.
Thanks @chrisbobbe ! This looks great -- two comments below, both on followups.
When I rebase I get a few Flow errors, so I'll hold off merging; please merge at will after resolving those.
| if (action.messageIds.every(messageId => !state.get(messageId))) { | ||
| return state; | ||
| } | ||
| return omit(state, action.messageIds); | ||
| return state.deleteAll(action.messageIds); |
There was a problem hiding this comment.
This can be simplified to just the deleteAll call. (I'm maybe 95% confident that Immutable will indeed return the identical value if none of the keys are present -- but it'd be nice to make sure there's a messagesReducer test confirming that.) Simpler and a bit more efficient, too.
Best as a separate followup commit, as it's nice that this commit is such a straightforward translation.
| const eventReactionAdd = (state, action) => { | ||
| const oldMessage = state[action.message_id]; | ||
| const oldMessage = state.get(action.message_id); | ||
| if (!oldMessage) { | ||
| return state; | ||
| } | ||
| return { | ||
| ...state, | ||
| [action.message_id]: { | ||
| ...oldMessage, | ||
| reactions: oldMessage.reactions.concat({ | ||
| emoji_name: action.emoji_name, | ||
| user_id: action.user_id, | ||
| reaction_type: action.reaction_type, | ||
| emoji_code: action.emoji_code, | ||
| }), | ||
| }, | ||
| }; | ||
| return state.set(action.message_id, { | ||
| ...oldMessage, | ||
| reactions: oldMessage.reactions.concat({ | ||
| emoji_name: action.emoji_name, | ||
| user_id: action.user_id, | ||
| reaction_type: action.reaction_type, | ||
| emoji_code: action.emoji_code, | ||
| }), | ||
| }); |
There was a problem hiding this comment.
This pattern can be condensed like so (in a followup), using Immutable.map#update:
const eventReactionAdd = (state, action) =>
state.update(
action.message_id,
oldMessage =>
oldMessage && {
...oldMessage,
reactions: oldMessage.reactions.concat({
emoji_name: action.emoji_name,
user_id: action.user_id,
reaction_type: action.reaction_type,
emoji_code: action.emoji_code,
}),
},
);A crucial feature here of update is that
if the
updaterfunction returns the same value it was called with, then no change will occur.
So if the message isn't present, the updater will be passed undefined, and just has to return undefined and the map will be returned unchanged.
I'm kind of ambivalent on this clarity-wise. It's a bit shorter, but does rely on a somewhat subtle fact about update's interface. But two things push me toward thinking that overall it's an improvement:
- It should be more efficient: the old code requires the key to be looked up in the data structure twice, and with this version Immutable can (and I expect it does) look it up just once.
- Because it no longer needs a local variable, it can be inlined conveniently into the
switchin the reducer. With similar simplifications, all of these helpers can be inlined conveniently into the reducer. That's the style of most of our reducers, and I generally prefer it because it means one fewer hop to look up what the effect of an action is (or vice versa, to find in what situation some code found in one of these is called.)
Those and:
- If we start making use of this feature routinely, as we well might, then it becomes a familiar one 🙂 and not a puzzling subtlety.
Greg points out [1], """ A crucial feature here of `update` is that > if the `updater` function returns the same value it was called > with, then no change will occur. So if the message isn't present, the updater will be passed `undefined`, and just has to return `undefined` and the map will be returned unchanged. """ See also, in that same comment, how Greg considers the effect on clarity of relying on that subtle fact, and how that weighs with other concerns, like efficiency. [1] zulip#4390 (comment)
As Greg points out [1], this means "one fewer hop to look up what the effect of an action is (or vice versa, to find in what situation some code found in one of these is called.)". [1] zulip#4390 (comment)
101477a to
04a6858
Compare
|
Thanks for the review! Revision pushed. The Flow errors, it turns out, were because I hadn't added |
|
You can see a draft of the followup I mention in the tip commit here. |
This test tests against input that wouldn't make it through type-checking.
In an upcoming commit, we'll start using the recently introduced `eg.makeMessagesState`, which will simplify the code in this file. It'll also have the nice bonus of reducing the diff for the commit where we switch over to using `Immutable.Map` for the messages state instead of an object-as-map.
We don't have any specific reason to believe this case exists -- but our type-checking should have alerted us to the fact that `messages[id]` might be undefined, as it would start to do when we store `state.messages` as an `Immutable.Map` in an upcoming commit.
`state.messages` is one thing we'd like to start using `Immutable` for, and it has numeric keys. As a practical solution, add another value for `[SERIALIZED_TYPE_FIELD_NAME]` alongside 'ImmutableMap', called 'ImmutableMapNumericKeys', and have the reviver call `parseInt` on all the keys in `data`. This choice should work well, with two assumptions: - We'll never have an `Immutable.Map` with mixed numeric and string keys. - We won't have a long list of different types we want as keys. They both seem solid now, but Greg has sketched out some alternatives in case they break down later [1]. [1] https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/persisting.20.60Immutable.2EMap.60s.20with.20numeric.20keys/near/1082872
An instance of zulip#3949 and zulip#3950, like 17bd752. One important difference from 17bd752 is that the `state.messages` map has numeric keys. We've handled the replace/revive logic for that, in the previous commit. But care must be taken whenever we use an `Immutable.Map(...)` call when we want a map with numeric keys, If you pass an object-as-map to `Immutable.Map`, it's impossible for the resulting `Immutable.Map`'s keys to be numbers, because the object-as-map's keys are necessarily strings, and that's up to JavaScript. The solution is to pass an array of key-value pairs, but unfortunately, Flow won't catch it if you forget to do this.
We just removed the last use of this in the previous commit. It's one of our several helper functions that hasn't yet grown a jsdoc. This function is quite short and simple, though, so I don't think it needs to have its own definition, with or without a jsdoc. Inlining the logic means one less thing to click through to find out what a piece of code is doing.
04a6858 to
64b4081
Compare
|
I've just rebased across the changes in #4393, fixing a few minor conflicts. |
This comment has been minimized.
This comment has been minimized.
With the new line added to messagesReducer-test.js, we can confirm that Immutable returns the identical value if none of the keys passed to `deleteAll` are present.
Greg points out [1], """ A crucial feature here of `update` is that > if the `updater` function returns the same value it was called > with, then no change will occur. So if the message isn't present, the updater will be passed `undefined`, and just has to return `undefined` and the map will be returned unchanged. """ See also, in that same comment, how Greg considers the effect on clarity of relying on that subtle fact, and how that weighs with other concerns, like efficiency. [1] zulip#4390 (comment)
As Greg points out [1], this means "one fewer hop to look up what the effect of an action is (or vice versa, to find in what situation some code found in one of these is called.)". [1] zulip#4390 (comment)
Like we did for `eventReactionAdd` (before its contents were inlined) in a recent commit.
Like we did in a recent commit, for `eventReactionAdd`. This just leaves `eventNewMessage`, but I'd like to try addressing its optimization TODO, and I'm not sure if the resulting code will be simple enough to inline.
|
Thanks for the revision! Looks good -- merged. |
After #4201, we're now using
Immutable.Mapforstate.narrows. This continues that work by makingstate.messagesuseImmutable.Map, too; it's one of the priority areas Greg has described.I haven't yet done any performance measurements; I'd be happy to take that up soon, unless we can already be confident enough that this won't cause any significant performance regressions.
One thing I'm concerned about (as mentioned in the main commit) is that, with the current types, Flow won't catch it if you accidentally pass
Immutable.Map({ 1: 'foo' })where a numeric-keyedImmutable.Mapis expected. (That value will have string keys because{ 1: 'foo' }is really{ '1': 'foo' }.) See where we handle MESSAGE_FETCH_COMPLETE in messagesReducer for one example of getting this right, but where we wouldn't be alerted if we got it wrong.