Skip to content

Conversation

@tgolen
Copy link
Contributor

@tgolen tgolen commented Sep 8, 2020

This final refactoring does quite a few things:

  1. It removes all traces of Ion.get() and Ion.multiGet()
  2. It simplifies the Ion connection config to remove loader, loaderParams, path, defaultValue, and addAsCollection
  3. It adds a lot to the readme to help clarify the code philosophy
  4. It initializes all Ion connections with the data already in Ion as part of the connect() process
  5. It adds local timezone support!
  6. Simplifies a lot of the promise chains

Tests

Need to regression test everything. Make sure to sign out and sign in!

@tgolen
Copy link
Contributor Author

tgolen commented Sep 10, 2020

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 marcaaron self-requested a review September 10, 2020 20:21
Copy link
Contributor

@marcaaron marcaaron left a 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

  1. Clear local storage
  2. Sign in with Expensify login
  3. Unable to sign in
  4. Refresh page
  5. Now able to sign in

Once we are logged in leaving comments works well

  1. Sign out
  2. Sign in
  3. 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:

  1. Sign out as person A
  2. Leave a comment from another user on a report as person B
  3. Sign in as person A
  4. Observed the following errors (see below) and unresponsive app
  5. Refresh page and everything works fine again

Screenshot 1329

Screenshot 1330

Screenshot 1332

The Pusher issue seems pretty edge case + we just need to unsubscribe, but I think we should fix the sign in bug.

@tgolen
Copy link
Contributor Author

tgolen commented Sep 10, 2020

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.

@marcaaron
Copy link
Contributor

It seems like browser notifications don't work anymore as well, but I'm not sure if that's expected?

I am seeing this log and only have one tab open

Screenshot 1334

@tgolen
Copy link
Contributor Author

tgolen commented Sep 10, 2020

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.

@marcaaron
Copy link
Contributor

Ok everything is working perfect for me now 👍

@marcaaron
Copy link
Contributor

Pushed a change to fix the strange routing issue discovered so removing myself as reviewer.

@marcaaron marcaaron requested review from cead22 and marcaaron and removed request for marcaaron September 10, 2020 22:48
@marcaaron marcaaron dismissed their stale review September 10, 2020 22:48

Dismissing my own review since I pushed a change

@tgolen
Copy link
Contributor Author

tgolen commented Sep 10, 2020

@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.
Copy link
Contributor

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.
Copy link
Contributor

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))
Copy link
Contributor

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?

Copy link
Contributor Author

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 👍

Copy link
Contributor Author

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']);
Copy link
Contributor

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)

Copy link
Contributor Author

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);
Copy link
Contributor

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

Copy link
Contributor Author

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.

Comment on lines 154 to +157
// 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;
Copy link
Contributor

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)

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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

Copy link
Contributor Author

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.

Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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: true to this call?

Copy link
Contributor Author

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?

Copy link
Contributor

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

Copy link
Contributor Author

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.

Copy link
Contributor Author

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 🙈

Copy link
Contributor

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:

@cead22
Copy link
Contributor

cead22 commented Sep 10, 2020

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants