-
Notifications
You must be signed in to change notification settings - Fork 71
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Update Onyx.clear to accept passed in keys to preserve, and use multiSet to clear storage #214
Conversation
@@ -73,15 +74,12 @@ describe('Set data while storage is clearing', () => { | |||
Onyx.clear(); | |||
return waitForPromisesToResolve() | |||
.then(() => { | |||
// Then the value in Onyx and the cache is the default key state | |||
// Then the value in Onyx, the cache, and Storage is the default key state |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A nice side effect of this change: the cache and Storage are actually in sync when clearing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Love that
e98f078
to
851da7d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changes look good to me. I haven't had a chance to test though.
@@ -73,15 +74,12 @@ describe('Set data while storage is clearing', () => { | |||
Onyx.clear(); | |||
return waitForPromisesToResolve() | |||
.then(() => { | |||
// Then the value in Onyx and the cache is the default key state | |||
// Then the value in Onyx, the cache, and Storage is the default key state |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Love that
Co-authored-by: Marc Glasser <marc.aaron.glasser@gmail.com>
lib/Onyx.js
Outdated
@@ -1025,17 +1028,22 @@ function initializeWithDefaultKeyStates() { | |||
* Storage.setItem() from Onyx.clear() will have already finished and the merged | |||
* value will be saved to storage after the default value. | |||
* | |||
* @param {Array} keysToPreserve ONYXKEYS which values you want to keep |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* @param {Array} keysToPreserve ONYXKEYS which values you want to keep | |
* @param {Array} keysToPreserve is a list of ONYXKEYS which values you don't want to be cleared with the rest of the data |
The original description read a little funny to me and I think this is more clear
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good point Tim, I find the wording odd too, especially "which". What about this?
* @param {Array} keysToPreserve is a list of ONYXKEYS that should not be cleared with the rest of the data
lib/Onyx.js
Outdated
const defaultKeys = _.keys(defaultKeyStates); | ||
const keysToClear = _.without(keys, ..._.union(keysToPreserve, defaultKeys)); | ||
const keyValuesToClear = _.map(keysToClear, key => [key, null]); | ||
const defaultKeyValues = _.pairs(_.omit(defaultKeyStates, ...keysToPreserve)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, why a spread operator?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const defaultKeyValues = _.pairs(_.omit(defaultKeyStates, ...keysToPreserve)); | |
const defaultKeyValuePairs = _.pairs(_.omit(defaultKeyStates, ...keysToPreserve)); |
lib/Onyx.js
Outdated
const keyValuesToClear = _.map(keysToClear, key => [key, null]); | ||
const defaultKeyValues = _.pairs(_.omit(defaultKeyStates, ...keysToPreserve)); | ||
const newStoreKeyValues = _.union(keyValuesToClear, defaultKeyValues); | ||
_.each(newStoreKeyValues, (keyValue) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we still sure that we want this to be done before it's actually cleared in Onyx? Could we instead move this to multiSet().then()
so that there is no possibility of a weird race condition where a subscriber has gotten a value, but the storage still hasn't cleared it yet?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah so switching this around breaks the test here:
react-native-onyx/tests/unit/onyxClearNativeStorageTest.js
Lines 46 to 66 in 335aca0
it('should persist the value of Onyx.merge when called between the cache and storage clearing', () => { | |
expect.assertions(3); | |
// Given that Onyx is completely clear | |
// When Onyx.clear() is called | |
Onyx.clear(); | |
// When merge is called between the cache and storage clearing, on a key with a default key state | |
Onyx.merge(ONYX_KEYS.DEFAULT_KEY, MERGED_VALUE); | |
return waitForPromisesToResolve() | |
.then(() => { | |
// Then the value in Onyx, the cache, and the storage is the merged value | |
expect(onyxValue).toBe(MERGED_VALUE); | |
const cachedValue = cache.getValue(ONYX_KEYS.DEFAULT_KEY); | |
expect(cachedValue).toBe(MERGED_VALUE); | |
// Use parseInt to convert the string value from the storage mock back to an integer | |
return Storage.getItem(ONYX_KEYS.DEFAULT_KEY) | |
.then(storedValue => expect(parseInt(storedValue, 10)).toBe(MERGED_VALUE)); | |
}); | |
}); |
Where the onyxValue
ends up being the DEFAULT_VALUE
instead. What I think is happening is that because we're calling notifySubscribersOnNextTick
only after clearing the storage that tick happens at the same time / after the merge call and this thus the DEFAULT_VALUE
is preserved.
@neil-marcellini thoughts on this? I know you dealt with this weird race condition and clear
before...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah so the problem we had with Onyx.clear()
for the transition flows is that after Onyx cleared and the previous user was signed out, we would sign in the new transitioning user (with an Onyx merge), and then the default Onyx value would get set for the session key, which would sign the user out again.
So yes we need to make sure, that clear()
followed by merge()
on a key with a default state results in the merged value getting saved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, that's a nasty race condition, and I think notifySubscribersOnNextTick
really bites us in that instance. It would be great to move a lot of the context from these comments into the code comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@neil-marcellini feel free to take a look at the updated comment I added above this and see if you agree 👍
lib/Onyx.js
Outdated
const keysToClear = _.without(keys, ..._.union(keysToPreserve, defaultKeys)); | ||
const keyValuesToClear = _.map(keysToClear, key => [key, null]); | ||
const defaultKeyValues = _.pairs(_.omit(defaultKeyStates, ...keysToPreserve)); | ||
const newStoreKeyValues = _.union(keyValuesToClear, defaultKeyValues); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a bit confusing at this point what exactly is being done with all this logic. Maybe adding some comments will help? Maybe explaining why we need to keep doing stuff with default keys, and why we are doing all the keys(), without(), map(), pairs(), union(). It's all just a tad confusing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the additional comments! I think what is still confusing is some of the variable names.
keyValuesToClear
sounds like it would be the thing that eventually is passed tomultiSet()
just by the name of itnewStoreKeyValues
is what actually gets passed tomultiSet()
but the name doesn't really imply that at all. "New" is pretty meaningless in the name of this variable
What I would suggest is to start with an empty variable:
const keyValuesToClear = [];
Then as you progress through the logic, things will get added to that array.
keyValuesToClear.push(defaultKeyValues);
Eventually, that's what gets passed to multiSet
Storage.multiSet(keyValuesToClear);
return waitForPromisesToResolve() | ||
.then(() => { | ||
// Then the value in Onyx and the cache for the default key is the default key state | ||
expect(onyxValue).toBe(DEFAULT_VALUE); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It appears like these assertions are already done in the test above. Why is it necessary to test them again? Just to ensure that passing a list of keys doesn't change the default behavior? If so, maybe we can DRY this up a little bit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to ensure that passing a list of keys doesn't change the default behavior?
Yeah, that was the thought. Although yeah I think it's kind of redundant just thinking about the code. I think it'll be clearer just to remove this in hindsight 👍
}); | ||
|
||
it('should preserve the value of any keysToPreserve passed in', () => { | ||
expect.assertions(6); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a purpose for this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it helps to make sure that assertions within async callbacks are actually called during the test. In my experience it's really easy to mess up and have an assertion that jest does not wait for. https://jestjs.io/docs/expect#expectassertionsnumber
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh, interesting. I've never used it, but the point about async assertions makes sense.
Onyx.set(ONYX_KEYS.REGULAR_KEY, SET_VALUE); | ||
|
||
// When clear is called with a key to preserve | ||
Onyx.clear([ONYX_KEYS.REGULAR_KEY]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you want to be sure that the clear()
doesn't happen until set()
is done, so I would chain those together.
@@ -141,7 +141,11 @@ describe('Onyx.mergeCollection() amd WebStorage', () => { | |||
}); | |||
|
|||
it('setItem() and multiMerge()', () => { | |||
expect(localforageMock.storageMap).toEqual({}); | |||
expect(localforageMock.storageMap).toEqual({ | |||
test_1: null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why this change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm pretty sure he made this change because now instead of removing all of the keys and values when clearing Storage, the keys remain but all the values are set to null.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ahhh, OK. Could you add a code comment about that please?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This approach looks great. I have two small comments.
85b0a43
to
7f7f06b
Compare
Updated! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for being such a stickler for all the comments! This code is really complex and it's very important that intention and logic are super clear.
lib/Onyx.js
Outdated
const defaultKeys = _.keys(defaultKeyStates); | ||
const keysToClear = _.without(keys, ..._.union(keysToPreserve, defaultKeys)); | ||
const keyValuesToClear = _.map(keysToClear, key => [key, null]); | ||
const defaultKeyValues = _.pairs(_.omit(defaultKeyStates, ...keysToPreserve)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const defaultKeyValues = _.pairs(_.omit(defaultKeyStates, ...keysToPreserve)); | |
const defaultKeyValuePairs = _.pairs(_.omit(defaultKeyStates, ...keysToPreserve)); |
lib/Onyx.js
Outdated
// If keysToPreserve contains a key that also has a default key state, we'll preserve it rather than reset | ||
// it to the default. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please update this comment to replace the two instances of "it" with what the reference is? It's a little ambiguous and makes it hard to understand. The comment could also do a little bit better job of explaining why we want this behavior and the intention behind it. Why do we handle the conflict between keysToPreserve and defaultKeyStates this way as opposed to some other way?
lib/Onyx.js
Outdated
const keysToClear = _.without(keys, ..._.union(keysToPreserve, defaultKeys)); | ||
const keyValuesToClear = _.map(keysToClear, key => [key, null]); | ||
const defaultKeyValues = _.pairs(_.omit(defaultKeyStates, ...keysToPreserve)); | ||
const newStoreKeyValues = _.union(keyValuesToClear, defaultKeyValues); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the additional comments! I think what is still confusing is some of the variable names.
keyValuesToClear
sounds like it would be the thing that eventually is passed tomultiSet()
just by the name of itnewStoreKeyValues
is what actually gets passed tomultiSet()
but the name doesn't really imply that at all. "New" is pretty meaningless in the name of this variable
What I would suggest is to start with an empty variable:
const keyValuesToClear = [];
Then as you progress through the logic, things will get added to that array.
keyValuesToClear.push(defaultKeyValues);
Eventually, that's what gets passed to multiSet
Storage.multiSet(keyValuesToClear);
lib/Onyx.js
Outdated
const keyValuesToClear = _.map(keysToClear, key => [key, null]); | ||
const defaultKeyValues = _.pairs(_.omit(defaultKeyStates, ...keysToPreserve)); | ||
const newStoreKeyValues = _.union(keyValuesToClear, defaultKeyValues); | ||
_.each(newStoreKeyValues, (keyValue) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, that's a nasty race condition, and I think notifySubscribersOnNextTick
really bites us in that instance. It would be great to move a lot of the context from these comments into the code comment.
Co-authored-by: Tim Golen <tgolen@expensify.com>
Co-authored-by: Tim Golen <tgolen@expensify.com>
Co-authored-by: Tim Golen <tgolen@expensify.com>
Updated! |
lib/Onyx.js
Outdated
// Combine the pairs of keys we're clearing and keys we're setting back to default for our multiSet call | ||
const newStoreKeyValues = _.union(keyValuesToClear, defaultKeyValues); | ||
// Add the default key value pairs to the keyValuesToClear so that they get set back to their default values when we clear Onyx | ||
keyValuesToClear.push(...defaultKeyValuePairs); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick: what about keyValuesToReset
? We aren't clearing the defaultKeys we are actually resetting them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like!
Co-authored-by: Neil Marcellini <neil@expensify.com>
// We do this before clearing Storage so that any call to clear() followed by merge() on a key with a default state results in the merged value getting saved, since the update | ||
// from the merge() call would happen on the tick after the update from this clear() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this 👍
Updated! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great now, thanks for all the attention on those comments!
@neil-marcellini please review
cc @marcaaron @tgolen
Details
Add a
keysToPreserve
param toclear
to allow it to accept passed-in keys to preserve when clearing Onyx. To better support this, clear Onyx by usingmultiSet
to set all keys to null (except the ones we want to preserve)Related Issues
Expensify/App#13062
Automated Tests
Added tests in onyxClearWebStorageTest.js and onyxClearNativeStorageTest.js
Additionally, ran some quick performance tests on Android to make sure there were no regressions:
Linked PRs
Expensify/App#13384