Skip to content

Conversation

@tgolen
Copy link
Contributor

@tgolen tgolen commented Aug 20, 2020

Fixes #253

Tests

  1. Load the app
  2. Go offline (either turn off wifi or set the browser to offline mode)
  3. Post a comment
  4. In another browser, go to expensify.com and go to the same report that is open for the chat just commented on
  5. Post a comment on the report page
  6. Now switch back to the chat app and turn it back to online mode
  7. Verify that you see the comment from 5 show up

@tgolen tgolen requested a review from cead22 August 20, 2020 21:56
@tgolen tgolen self-assigned this Aug 20, 2020
@tgolen
Copy link
Contributor Author

tgolen commented Aug 20, 2020

Oh, I also renamed the action files. Something @iwiznia requested. Sorry to pollute the PR :D

// Two things will bring the app online again...
// 1. Pusher reconnecting (see registerSocketEventCallback at the top of this file)
// 2. Getting a 200 response back from the API (happens right below)
Ion.get(IONKEYS.NETWORK, 'isOffline')
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change actually removes the need to rely on a local variable for knowing if the app is offline or not

return;
}
// Make a simple request every second to see if the API is online again
fetch(`${CONFIG.EXPENSIFY.API_ROOT}command=Get`, {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where I changed request() to fetch()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use request or xhr instead? They have logic like catch that this doesn'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 can try it again, yes. That catch() logic doesn't actually do anything to help what we're trying to do here (ie. we KNOW this is going to fail until the API is back online).

@tgolen tgolen requested a review from marcaaron August 20, 2020 22:00
@tgolen
Copy link
Contributor Author

tgolen commented Aug 20, 2020

@marcaaron I'm adding you here to take a look at this as well

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.

I just thought of something and I'd like to get your thoughts before we commit to this solution. We don't really need to add a "connect" listener to Network or an extra callback property to the Ion mapping because:

  1. There is only one situation where we must refetch keys (isOffline going from false > true)
  2. Ion already knows when we are going online or offline because it is responsible for watching these changes.
  3. We should just tell Ion which actions it needs to fetch and have Network only worry about updating isOffline.


// If the request failed, we need to put the request object back into the queue as long as there is no
// doNotRetry option set in the data
if (data.doNotRetry !== true) {
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 still keep this right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, my logic was that there was only one thing using it (the processWriteQueue method), and if that method was no longer going to call request() then it didn't need to stay here. However, if we really want to avoid using fetch() in processWriteQueue then this will probably come back.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense, but does that mean that requests that go through the network request queue won't be re-tried?

|| nextProps.displayAsGroup !== this.props.displayAsGroup
|| nextProps.authToken !== this.props.authToken;
|| nextProps.authToken !== this.props.authToken
|| !_.isEqual(nextProps.historyItem, this.props.historyItem);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this line makes nextProps.historyItem.sequenceNumber !== this.props.historyItem.sequenceNumber redundant?

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, yes it does 👍

src/lib/Ion.js Outdated
* @param {string} mapping.statePropertyName the name of the property in the state to connect the data to
* @param {boolean} [mapping.addAsCollection] rather than setting a single state value, this will add things to an array
* @param {string} [mapping.collectionID] the name of the ID property to use for the collection
* @param {object} [mapping.callback] An alternative to using mapping.reactComponent so that a method can be bound to
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An alternative to using mapping.reactComponent , what does this mean? I don't see it anymore

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe reactComponent is withIonInstance now?

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 yep, it was renamed.

src/lib/Ion.js Outdated
[mappedComponent.statePropertyName]: newValue,
});
// If there is a callback attached to the mapping, then trigger the callback and pass it the new value
if (mappedComponent.callback) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (_.isFunction(mappedComponent.callback)) {

return;
}
// Make a simple request every second to see if the API is online again
fetch(`${CONFIG.EXPENSIFY.API_ROOT}command=Get`, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use request or xhr instead? They have logic like catch that this doesn't

@tgolen
Copy link
Contributor Author

tgolen commented Aug 21, 2020

All right, updated!

marcaaron
marcaaron previously approved these changes Aug 21, 2020
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.

LGTM but I can't actually get these test steps to work because when I go offline in Chrome Dev Tools web sockets still work 🤣

src/lib/Ion.js Outdated
[mappedComponent.statePropertyName]: newValue,
});
// If there is a withIonInstance, then the state needs to be set on that instance with the new value
if (mappedComponent.withIonInstance) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we leaving this in case we want to call this without a withIonInstance?

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 can remove this and just keep it as a required param for now.

_.each(reconnectionCallbacks, cb => cb());
}
isAppOffline = isCurrentlyOffline;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check the store value instead? If I land on this file for the first time it's not clear whether I should use the local variable or Ion to get the offline status. I think we can remove the local variable entirely now.

My recommendation would be to change this to...

async function setNewOfflineStatus(isCurrentlyOffline) {
    const previousOfflineStatus = await Ion.get(IONKEYS.NETWORK, 'isOffline');
    Ion.merge(IONKEYS.NETWORK, {isOffline: isCurrentlyOffline});
    if (previousOfflineStatus && !isCurrentlyOffline) {
        _.each(reconnectionCallbacks, cb => cb());
    }
}

:trollface:

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, OK. That makes sense, yeah. That will help clean this up a little.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... and I see what you did there ... :trollface:

@marcaaron
Copy link
Contributor

Hmm tried with ngrok and it still doesn't seem to be working

@marcaaron
Copy link
Contributor

marcaaron commented Aug 21, 2020

  1. Load the app
  2. Go offline (either turn off wifi or set the browser to offline mode)
  3. Post a comment
  4. In another browser, go to expensify.com and go to the same report that is open for the chat just commented on
  5. Post a comment on the report page
  6. Now switch back to the chat app and turn it back to online mode
  7. Verify that you see the comment from 5 show up

No comment shows up until a page refresh and the comment that was queued to send on the report that went offline never shows up again.

@tgolen
Copy link
Contributor Author

tgolen commented Aug 21, 2020

You should still be able to test this with just the offline browser setting. In step 3, when you try to post the comment in offline mode, the failed API call will trigger the isOffline: true state in the app (so you don't have to worry about pusher).

Here is a video of me testing it:

2020-08-21_09-15-37.mp4.zip

@tgolen
Copy link
Contributor Author

tgolen commented Aug 21, 2020

Updated

@marcaaron
Copy link
Contributor

Alright lemme give it another try.

@marcaaron
Copy link
Contributor

I watched your video and noticed that the comment you are leaving on the online tab appears on the offline tab before you set it to online. It is being handled by Pusher somehow and replaces the optimistic comment. Once you come back online everything does seem to work.

I tested it again and got the same result so happy to move forward on this. Especially since I don't think it's possible for an offline comment to get overwritten by Pusher in production.

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.

Looks great to me!

@tgolen
Copy link
Contributor Author

tgolen commented Aug 21, 2020

Ah, cool. Glad you got it to work!

@tgolen
Copy link
Contributor Author

tgolen commented Aug 21, 2020

@cead22 All you!

*/
function setNewOfflineStatus(isCurrentlyOffline) {
Ion.get(IONKEYS.NETWORK, 'isOffline')
.then((prevWasOffline) => {
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 suggestions for better var names: wasPreviouslyOffline, wasOffline or previouslyWasOffline

@cead22 cead22 merged commit fb03397 into master Aug 24, 2020
@cead22 cead22 deleted the tgolen-resync-data branch August 24, 2020 15:59
}
for (let i = 0; i < networkRequestQueue.length; i++) {
// Take the request object out of the queue and make the request
const queuedRequest = networkRequestQueue.shift();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fyi and @cead22 looking at this again, I think this is a bug since we are modifying the size of the queue on each iteration?

e.g.

iteration 1: i = 0; i < size 3
iteration 2: i = 1; i < size 2
iteration 3: i = 2; i < size 1

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do this instead

networkRequestQueue.forEach(queuedRequest => {
    request(queuedRequest.command, queuedRequest.data)
        .then(queuedRequest.callback);
});
networkRequestQueue = [];

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. We need to make sure that if one request fails that we pause, and then resume from the same spot

Copy link
Contributor

@marcaaron marcaaron Aug 24, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we make them synchronous then?

Copy link
Contributor

@cead22 cead22 Aug 24, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, no I don't think so, but to rephrase, we need to make sure we don't lose any of these requests in the process of queueing them / firing them off / ignoring them when offline or reauthenticating

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it must be. Otherwise I am confused about how this can happen...

Screenshot 1267

Would it be a problem to fetch all at once and then set to an empty array? If we don't care about the order then when any fail then we can re-queue them?

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'm a little confused about what that console code is showing or what the possible bug here is. Can you restate what the bug is that you think is happening?

I don't think we should assume that they can happen in any order, and we should try to preserve the order that they were queued in when possible.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Problem: Given an array of queued network requests with a size greater than 1 we will never process the entire queue in one call of this method with this existing logic.

I don't know if this has any super obvious bug-like effects just yet. But it seems to force the requests that did not get processed on one call to be processed in the next call of this function which happens once per second? The queue will eventually empty. But is that what we want?

Mostly, I'm pointing out that this logic is tricky, doesn't do what it looks like it would do, and doesn't do what we initially wanted... which is to empty the entire queue.

Let me know if that makes sense!

I don't think we should assume that they can happen in any order, and we should try to preserve the order that they were queued in when possible.

To clarify, are you suggesting that the requests should not complete in any order? Or that they need only be requested in a certain order?

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, OK. That makes sense, and you're right. Though I don't think it's causing a bug, it is delaying requests unnecessarily and the code isn't very clear. I think using a do/while is probably better. Like this:

const array  = [1,2,3];
let i = 0;
do {
    const item = array.shift();
    console.log(i);
    i++;
} while (array.length > 0)

To clarify, are you suggesting that the requests should not complete in any order? Or that they need only be requested in a certain order?

Only that they be requested in a certain order.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool! In that case, yes this will work. Tbh not too sure I see any clear advantage to using a do/while vs. a for loop or forEach, but maybe you are seeing something I'm not.

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.

Resynchronize rooms upon reconnection to catch missing content

4 participants