-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Refactor the rest of the code to remove Ion.get() #426
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
Conversation
Fixes #402
|
OK, comments were addressed, and branch was tested. I'd appreciate someone double-checking that everything still works (like signing out, signing in, making new comments, having brand new users send comments, unread indicators, etc.). |
marcaaron
left a 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.
Ok here are the tests I performed. Overall working good. But I couldn't repro either of these on the master branch.
Unable to sign in without first refreshing sign in page
- Clear local storage
- Sign in with Expensify login
- Unable to sign in
- Refresh page
- Now able to sign in
Once we are logged in leaving comments works well
- Sign out
- Sign in
- Everything seems ok
But one weird thing that I noticed is that signing out does not unsubscribe you from Pusher. I think maybe it should? But was unsure if there would be any bad effects from that. I tested on master and nothing bad happens (even though yes we are still subscribed there as well) then tested on this branch and ran into problems:
- Sign out as person A
- Leave a comment from another user on a report as person B
- Sign in as person A
- Observed the following errors (see below) and unresponsive app
- Refresh page and everything works fine again
The Pusher issue seems pretty edge case + we just need to unsubscribe, but I think we should fix the sign in bug.
|
OK, thanks for verifying the sign-in problem. I wasn't sure if it was only me experiencing that or not, so let me take a deeper look into that. I agree that we should disconnect from pusher when signing out too. |
|
OK, I updated this with pusher disconnect. I had to refactor a little about how we are connecting and subscribing. I also fixed the active client manager so notifications will work. |
|
Ok everything is working perfect for me now 👍 |
|
Pushed a change to fix the strange routing issue discovered so removing myself as reviewer. |
Dismissing my own review since I pushed a change
|
@cead22 all you! |
| 1. **UI Binds to data on disk** | ||
| - Ion is a Pub/Sub library to connect the application to the data stored on disk. | ||
| - UI components subscribe to Ion (using `withIon()`) and any change to the Ion data is published to the component by calling `setState()` with the changed data. | ||
| - Libraries subscribe to Ion (with `Ion.connect()`) and any change to the Ion data is published to the callback callback with the changed data. |
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.
callback callback
| - When data needs to be written to or read from the server, this is done through Actions only. | ||
| - Public action methods should never return anything (not data or a promise). This is done to ensure that action methods can be called in parallel with no dependency on other methods (see discussion above). | ||
| - Actions should favor using `Ion.merge()` over `Ion.set()` so that other values in an object aren't completely overwritten. | ||
| - In general, the operations that happen inside an action should be done in parallel and not in sequence ((eg. don't use the promise of one Ion method to trigger a second Ion method). Ion is built so that every operation is done in parallel and it doesn't matter what order they finish in. XHRs on the other hand need to be handled in sequence with promise chains in order to access and act upon the response. |
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.
((eg
| } | ||
|
|
||
| // Anything else (strings and numbers) need to be set into storage | ||
| AsyncStorage.setItem(key, JSON.stringify(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.
Do strings and number need to be JSON.stringified?
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, no they don't 👍
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 guess they do actually. That's because get() does a JSON.parse() on everything (since it doesn't know what kind of a value is stored).
| .then(() => detailsFormatted); | ||
| queueRequest('PersonalDetails_GetForEmails', {emailList}) | ||
| .then((data) => { | ||
| const details = _.omit(data, ['jsonCode', 'requestID']); |
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 I think we should prefer _.pick over _.omit to be explicit about what we're storing and so we don't store anything we don't want to accidentally (whitelisting vs blacklisting)
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 good idea. I think we could use _.pick() with emailList and that should work.
| queueRequest('PersonalDetails_GetForEmails', {emailList}) | ||
| .then((data) => { | ||
| const details = _.omit(data, ['jsonCode', 'requestID']); | ||
| const formattedDetails = formatPersonalDetails(details); |
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: I'm a fan of temp variables even when they're unnecessary if their name makes the code more obvious, but I don't think it does in this case and Ion.merge(IONKEYS.PERSONAL_DETAILS, formatPersonalDetails(details)) is just as 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.
Yeah, no worries, I can change this. I think I did this for debugging, and then never put it back.
| // We need to return the promise from setSuccessfulSignInData to ensure the authToken is updated before | ||
| // we try to create a login below | ||
| return setSuccessfulSignInData(response, parameters.exitTo); | ||
| setSuccessfulSignInData(response, parameters.exitTo); | ||
| authToken = response.authToken; |
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 should revert this change (or remove the comment, and make sure we know exactly why this works but the previous version didn't)
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, I can do that, however, considering that the previous version wasn't what we wanted, I don't think there is a whole lot of value in that.
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, I just quickly looked at it and I can tell right away why the return wasn't working. It's because setSuccessfulSignInData() was not returning the promise from Ion.merge() at the end of it, so the race condition still existed where the authToken was being accessed before it was set in Ion.
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.
Good catch, the previous version wasn't what we wanted because it was relying on the return value of a promise right? If that's the case I agree. However, I'd also argue this isn't what we want, because we're now updating the local auth token in two places, here and the Ion connection, do you agree?
If so, what's the best way to solve this? maybe put something in ion that dictates we're ready to continue that flow, and use an Ion connection to that something to make the calls that we're making here when the value is 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.
because we're now updating the local auth token in two places
Yeah, I understand what you're saying. I don't think it's really the most ideal, no. If all code started updating these local variables whenever they want, then you could end up with bugs where the local variable is not the same as what is (or will be) in Ion.
I think I keep going back to the best way to solve it is what I had posted in Slack:
start Ion off with:
SESSION: {
authToken: null,
login: null,
password: null,
}
Then connect to Ion:
Ion.connect({key: SESSION, callback: (val) => {
if (!val.authToken) {
// Need to authenticate()
Ion.set('authenticating', true);
} else if (!val.login) {
// Need to create()
Ion.set('authenticating', false);
}
}});
let authenticating;
Ion.connect({key: 'authenticating', callback: (isCurrentlyAuthenticating) => {
// When the app is no longer authenticating
if (authenticating && !isCurrentlyAuthenticating) {
// Restart the network queue
}
}});
Then:
- 407
- Add request to network queue
- Ion.set(SESSION.AUTHTOKEN, null) (and at this point all the steps to fill out a complete session are automatically handled)
This is a more ideal solution because essentially EVERYTHING that needs to happen is triggered off of Ion events once the data in Ion has been 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.
Perfect, I can do that here #454
|
|
||
| const clientID = Guid(); | ||
|
|
||
| // @TODO make all this work by uncommenting code. This will work once |
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 meant we should have no @todos in the comment ideally, or commented out code for that matter. That's what GH issues are for (otherwise we'll go years with these TODOs polluting the code), and git, so we should remove the commented out code
| const allPersonalDetails = formatPersonalDetails(data.personalDetailsList); | ||
| Ion.merge(IONKEYS.PERSONAL_DETAILS, allPersonalDetails); | ||
|
|
||
| // Get my personal details so they can be easily accessed and subscribed to on their own 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.
s/Get/Set
| }) | ||
| .catch(error => console.error('Error fetching personal details', error)); | ||
|
|
||
| // Refresh the personal details every 30 minutes |
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 do we need to poll for this, that seems contrary to our whole design philosophy for this app, is it because we're not pushing events when somebody updates their personal details? If so, can you include that in the comment?
| .catch(err => Ion.merge(IONKEYS.SESSION, {error: err.message})); | ||
| redirectToSignIn(); | ||
| API.deleteLogin({ | ||
| partnerUserID: credentials && credentials.login |
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 shouldn't call deleteLogin with
partnerID: null - Should we add
doNotRetry: trueto this call?
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.
Should we add doNotRetry: true to this call?
I don't know, what is the criteria for adding that?
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 saw DeleteLogin fail before due to a bug, because we didn't have the right auth token, or because create login itself failed, but I think in either of those cases, since we're signing out, it doesn't make sense to retry the request if it fails
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 makes sense. Thanks! I'll add it.
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, actually... I just looked, and API.deleteLogin() adds it in there already 🙈
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.
Hahaha Go API! I wonder who wrote that... :ohnothing:
|
Merging since this is a big PR and while there are some comments that I'd consider blockers, nothing will happen if we merge this and address those in separate PRs (cc @tgolen) |




This final refactoring does quite a few things:
Ion.get()andIon.multiGet()loader,loaderParams,path,defaultValue, andaddAsCollectionconnect()processTests
Need to regression test everything. Make sure to sign out and sign in!