-
Notifications
You must be signed in to change notification settings - Fork 2.9k
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
Enable active client manager, multiple tabs, and previous PR cleanup #469
Conversation
src/lib/Ion.js
Outdated
@@ -1,39 +1,13 @@ | |||
import _ from 'underscore'; | |||
import AsyncStorage from '@react-native-community/async-storage'; | |||
import StorageEvent from './StorageEvent'; |
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.
NAB but importing this as addStorageEventListener
would make init read like this and imo be clearer
function init() {
addStorageEventListener((key, newValue) => keyChanged(key, newValue));
}
src/lib/Ion.js
Outdated
// Values that are objects can be merged into storage | ||
if (_.isObject(val)) { | ||
// Values that are objects or arrays are merged into storage | ||
if (_.isObject(val) || _.isArray(val)) { |
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.
Second condition is redundant, but the comment is good
> _.isObject([])
> true
Returns true if value is an Object. Note that JavaScript arrays and functions are objects, while (normal) strings and numbers are not.
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.
Ah, you know, I had read that... but I ran into issues with that and I needed to add this code. Let me try again.
Updated with both suggestions 👍 |
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.
Nice! Looks great to me. Just have a few NAB/questions.
src/Expensify.js
Outdated
</Router> | ||
|
||
// </Beforeunload> | ||
<Beforeunload onBeforeunload={ActiveClientManager.removeClient}> |
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.
weird that this is not onBeforeUnload
is it something we control?
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 straight from the lib. Now that I think about it though, we could definitely just write this ourselves and not need a third-party lib to do it.
src/Expensify.js
Outdated
<Route path={['/home', '/']} component={HomePage} /> | ||
</Switch> | ||
</Router> | ||
</Beforeunload> |
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 curious why we are using this as a wrapper and not render with a self-closing tag? Is there a reason?
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.
No reason. In fact, if we're writing our own version, I'd rather just have it be a utility method as opposed to some component that's rendered.
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.
Yep you are reading my mind 😅
src/lib/ActiveClientManager.js
Outdated
const init = () => Ion.set(IONKEYS.ACTIVE_CLIENTS, {[clientID]: clientID}); | ||
function init() { | ||
Ion.merge(IONKEYS.ACTIVE_CLIENTS, {[clientID]: clientID}); | ||
} | ||
|
||
/** | ||
* Remove this client ID from the array of active client IDs when this client is exited |
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.
NAB, maybe use map
instead of array
here just to avoid confusion as the activeClients
is an Object
and not Array
src/lib/ActiveClientManager.js
Outdated
// At the moment activeClients only has 1 value i.e., the latest clientID so let's compare if | ||
// the latest matches the current browsers clientID. | ||
return activeClients[clientID] === clientID; | ||
return _.first(_.keys(activeClients)) === clientID; |
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 any reason we are using an object and not an array? I know if we are adding a new string key to an object that the order will be preserved. But is the same true for Ion.merge()
?
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 tried using an array, and I ran into a lot of problems with it being converted into a strange object instead like {0: '1234', 1: '3456'}
. I can try digging into this more.
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.
Oh that's interesting. Is this just a limitation of AsyncStorage
when we try to save an array and get it again? Or does this happen when we use AsyncStorage.mergeItem()
?
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 reverts commit aa4811f.
Updated with simplifications! One thing that I have seen happen is that it's fairly easy for an active client GUID to get stuck at the front of the array. I've seen this happen multiple times in local development, but I don't know if it was just because of errors in the code, or actual behavior. @marcaaron I've changed the name of the storage key to help with this (and not conflict with what was previously stored), but please keep an eye out for this. If it happens in production, it's bad because it would mean that web would never have notifications happening. I don't know if there is some sort of "fail-safe" that we can add to this to ensure that this situation would be minimized. Maybe like the GUID expiring after a certain amount of time? I don't know... |
*/ | ||
function addStorageEventHandler(callback) { | ||
window.addEventListener('storage', (e) => { | ||
callback(e.key, JSON.parse(e.newValue)); |
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 wondering if we should add any sort of check to see if the localStorage
changes are actually related to our app as other things (chrome extensions etc) might possibly be sharing localStorage
? Maybe it doesn't matter since we'll call keyChanged()
and won't have any subscribers? But it would be better to not call that at all.
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.
Let's not worry about that unless it becomes an issue 👍
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 works for me. It's easy enough to add in later if it becomes a problem since we should theoretically list all keys in IONKEYS
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.
We can also wait to see if this is a problem, but I was thinking of wrapping this in try/catch in case newValue isn't valid JSON, but I don't know how that would happen
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.
Ah, adding a try/catch for the json.parse()
is a good idea. Let me add that.
Would it be possible to get this merged soon? |
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.
LGTM just left one more comment about Ion.merge with arrays and there are conflicts.
@@ -2,7 +2,7 @@ | |||
* This is a file containing constants for all the top level keys in our store | |||
*/ | |||
export default { | |||
ACTIVE_CLIENTS: 'activeClients', | |||
ACTIVE_CLIENTS: 'activeClients2', |
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 this intentional?
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, it was intentional... and I'm open to a better solution. The problem is that these were stored as an object before, and sense we are only calling Ion.merge()
for this, moving from an object to an array is not possible and will result in really bad values being stored.
One possible solution is to rename it to maybe activeClientIDs
, then add some code which clears out activeClients
, and then after a week, we can go back and set it back to activeClients
. Whatever solution we think of here, we should try to find some kind of method that is fairly reproducible because I think we will need to do this occassionally.
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 changing the name works for now!
I think learn term we need a way to perform localStorage
migrations for when keys change by adding some kind of instructions before the app updates or inits. That might mean we have some migration code running that based on the version will alter the localStorage
schema or simply blow it away and start over from scratch minus session information.
Seems like a similar problem people run into when implementing a ServiceWorker
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 agree with this. The way I imagine this would work is something like
- If the new version of the app renames a key or wants to clear old keys
- When the app inits
- Clear local storage (like signout does)
- Re-authenticate
- Load everything from scratch
That should all be transparent to the user
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, I think the catch there is that process wouldn't be exactly "transparent" to the user, particularly the "load everything from scratch" part. I'm going to spin up an issue so we can discuss this further and plan on leaving this code the way it is for now.
src/lib/Ion.js
Outdated
console.warn(`It looks like a React component subscribed to multiple Ion keys without | ||
providing an 'indexBy' option. This will result in undefined behavior. The best thing to do is | ||
provide an 'indexBy' value, or use a more specific regex that will only match a single Ion 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.
Can maybe be removed after merging master
?
*/ | ||
function addStorageEventHandler(callback) { | ||
window.addEventListener('storage', (e) => { | ||
callback(e.key, JSON.parse(e.newValue)); |
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 works for me. It's easy enough to add in later if it becomes a problem since we should theoretically list all keys in IONKEYS
Updated, thanks! |
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.
Oh that's interesting so... |
src/lib/ActiveClientManager/index.js
Outdated
* @returns {boolean} | ||
*/ | ||
function isClientTheLeader() { | ||
return _.first(activeClients) === clientID; |
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 double checking something here. Wouldn't it be safer to give priority to the last client that was added instead of the first?
e.g. if a beforeunload
event doesn't happen then I think we'll might get a zombie "leader" that disappeared a long time ago. Which would leave us in a bad state since we are pushing new clients onto the end of the array. But this wouldn't happen if new clients were pushed onto the front of the array or we treated the last item as the "leader".
Yeah, so I think you're seeing what I mentioned earlier about GUIDs getting stuck in that array.
I am 99% sure that clearing local storage directly does clear AsyncStorage (which is just using local storage underneath). I think something else is happening here. First thing I always check is that there are no other windows open (because those will for sure be adding GUIDs into the array). I think I like the suggestion about using the last GUID instead of the first GUID! That makes sense and should solve that problem hopefully. |
Yeah thinking about it some more... we don't really even need a |
Hm, so when we reach the max, then we'd just start removing the oldest GUID? That could work |
Yep, that works! Updated. |
Bump for review please :) |
@AndrewGable or @cead22 want to take a look? |
Conflicts |
Sorry, I didn't get a chance to review today, but I'll give this a look tomorrow |
Conflicts resolved. |
*/ | ||
function addStorageEventHandler(callback) { | ||
window.addEventListener('storage', (e) => { | ||
callback(e.key, JSON.parse(e.newValue)); |
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.
We can also wait to see if this is a problem, but I was thinking of wrapping this in try/catch in case newValue isn't valid JSON, but I don't know how that would happen
@@ -2,7 +2,7 @@ | |||
* This is a file containing constants for all the top level keys in our store | |||
*/ | |||
export default { | |||
ACTIVE_CLIENTS: 'activeClients', | |||
ACTIVE_CLIENTS: 'activeClients2', |
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 agree with this. The way I imagine this would work is something like
- If the new version of the app renames a key or wants to clear old keys
- When the app inits
- Clear local storage (like signout does)
- Re-authenticate
- Load everything from scratch
That should all be transparent to the user
|
||
callback: (val) => { | ||
activeClients = val; | ||
if (activeClients.length >= maxClients) { |
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.
Don't we need to update active clients in Ion in the else
case?
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 don't see why we would, no. Can you explain why you're thinking it would be necessary? If you added a set()
in the else
, then the value of active clients in Ion wouldn't be changing at all.
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 don't think I fully understood how this works yesterday, but I think I get it now. When init
is called, we just add clients. If somebody calls init
and we reach the max of clients, we remove the first one that was added 👍
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.
Yep! Exactly.
Updated with a try/catch for the JSON parsing |
} catch (err) { | ||
console.error('Could not parse the newValue of the storage event', err, e); | ||
} | ||
callback(e.key, JSON.parse(newValue)); |
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 should be callback(e.key, newValue);
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.
ah, haha 🤦
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.
Fixed
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.
(and tested)
Ion.connect()
and provides instructions on proper useTests for multi-tab functionality
activeClients
has multiple GUIDs[NOTIFICATION] Skipping notification because this client is not the leader