diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js index 0d27ba64dedf..468178fb15ec 100755 --- a/src/ONYXKEYS.js +++ b/src/ONYXKEYS.js @@ -239,6 +239,14 @@ export default { // Experimental memory only Onyx mode flag IS_USING_MEMORY_ONLY_KEYS: 'isUsingMemoryOnlyKeys', + ONYX_UPDATES: { + // The ID of the last Onyx update that was applied to this client + LAST_UPDATE_ID: 'onyxUpdatesLastUpdateID', + + // The ID of the previous Onyx update that was applied to this client + PREVIOUS_UPDATE_ID: 'onyxUpdatesPreviousUpdateID', + }, + // Manual request tab selector SELECTED_TAB: 'selectedTab', diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js index 8fca9aea495c..ddd61da272e6 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.js +++ b/src/libs/Navigation/AppNavigator/AuthScreens.js @@ -96,6 +96,9 @@ const propTypes = { /** Opt-in experimental mode that prevents certain Onyx keys from persisting to disk */ isUsingMemoryOnlyKeys: PropTypes.bool, + /** The last Onyx update ID that is stored in Onyx (used for getting incremental updates when reconnecting) */ + onyxUpdatesLastUpdateID: PropTypes.number, + ...windowDimensionsPropTypes, }; @@ -105,6 +108,7 @@ const defaultProps = { email: null, }, lastOpenedPublicRoomID: null, + onyxUpdatesLastUpdateID: 0, }; class AuthScreens extends React.Component { @@ -116,7 +120,7 @@ class AuthScreens extends React.Component { componentDidMount() { NetworkConnection.listenForReconnect(); - NetworkConnection.onReconnect(() => App.reconnectApp()); + NetworkConnection.onReconnect(() => App.reconnectApp(this.props.onyxUpdatesLastUpdateID)); PusherConnectionManager.init(); Pusher.init({ appKey: CONFIG.PUSHER.APP_KEY, @@ -127,14 +131,16 @@ class AuthScreens extends React.Component { }); // If we are on this screen then we are "logged in", but the user might not have "just logged in". They could be reopening the app - // or returning from background. If so, we'll assume they have some app data already and we can call reconnectApp() instead of openApp(). + // or returning from background. If so, we'll assume they have some app data already and we can call + // reconnectApp(onyxUpdatesLastUpdateID) instead of openApp(). // Note: If a Guide has enabled the memory only key mode then we do want to run OpenApp as their app will not be rehydrated with // the correct state on refresh. They are explicitly opting out of storing data they would need (i.e. reports_) to take advantage of // the optimizations performed during ReconnectApp. - if (this.props.isUsingMemoryOnlyKeys || SessionUtils.didUserLogInDuringSession()) { + const shouldGetAllData = this.props.isUsingMemoryOnlyKeys || SessionUtils.didUserLogInDuringSession(); + if (shouldGetAllData) { App.openApp(); } else { - App.reconnectApp(); + App.reconnectApp(this.props.onyxUpdatesLastUpdateID); } App.setUpPoliciesAndNavigate(this.props.session, !this.props.isSmallScreenWidth); @@ -323,5 +329,8 @@ export default compose( isUsingMemoryOnlyKeys: { key: ONYXKEYS.IS_USING_MEMORY_ONLY_KEYS, }, + onyxUpdatesLastUpdateID: { + key: ONYXKEYS.ONYX_UPDATES.LAST_UPDATE_ID, + }, }), )(AuthScreens); diff --git a/src/libs/actions/App.js b/src/libs/actions/App.js index 925f64bb85e1..ca03380368c2 100644 --- a/src/libs/actions/App.js +++ b/src/libs/actions/App.js @@ -122,63 +122,94 @@ AppState.addEventListener('change', (nextAppState) => { }); /** - * Fetches data needed for app initialization - * @param {boolean} [isReconnecting] + * Gets the policy params that are passed to the server in the OpenApp and ReconnectApp API commands. This includes a full list of policy IDs the client knows about as well as when they were last modified. + * @returns {Promise} */ -function openApp(isReconnecting = false) { - isReadyToOpenApp.then(() => { - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.POLICY, - waitForCollectionCallback: true, - callback: (policies) => { - // When the app reconnects we do a fast "sync" of the LHN and only return chats that have new messages. We achieve this by sending the most recent reportActionID. - // we have locally. And then only update the user about chats with messages that have occurred after that reportActionID. - // - // - Look through the local report actions and reports to find the most recently modified report action or report. - // - We send this to the server so that it can compute which new chats the user needs to see and return only those as an optimization. - const params = {policyIDToLastModified: JSON.stringify(getNonOptimisticPolicyIDToLastModifiedMap(policies))}; - if (isReconnecting) { - Timing.start(CONST.TIMING.CALCULATE_MOST_RECENT_LAST_MODIFIED_ACTION); - params.mostRecentReportActionLastModified = ReportActionsUtils.getMostRecentReportActionLastModified(); - Timing.end(CONST.TIMING.CALCULATE_MOST_RECENT_LAST_MODIFIED_ACTION, '', 500); - } - Onyx.disconnect(connectionID); - - // eslint-disable-next-line rulesdir/no-multiple-api-calls - const apiMethod = isReconnecting ? API.write : API.read; - apiMethod(isReconnecting ? 'ReconnectApp' : 'OpenApp', params, { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.IS_LOADING_REPORT_DATA, - value: true, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.IS_LOADING_REPORT_DATA, - value: false, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.IS_LOADING_REPORT_DATA, - value: false, - }, - ], - }); - }, +function getPolicyParamsForOpenOrReconnect() { + return new Promise((resolve) => { + isReadyToOpenApp.then(() => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY, + waitForCollectionCallback: true, + callback: (policies) => { + Onyx.disconnect(connectionID); + resolve({policyIDToLastModified: JSON.stringify(getNonOptimisticPolicyIDToLastModifiedMap(policies))}); + }, + }); }); }); } /** - * Refreshes data when the app reconnects + * Returns the Onyx data that is used for both the OpenApp and ReconnectApp API commands. + * @returns {Object} + */ +function getOnyxDataForOpenOrReconnect() { + return { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.IS_LOADING_REPORT_DATA, + value: true, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.IS_LOADING_REPORT_DATA, + value: false, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.IS_LOADING_REPORT_DATA, + value: false, + }, + ], + }; +} + +/** + * Fetches data needed for app initialization + */ +function openApp() { + getPolicyParamsForOpenOrReconnect().then((policyParams) => { + API.read('OpenApp', policyParams, getOnyxDataForOpenOrReconnect()); + }); +} + +/** + * Fetches data when the app reconnects to the network + * @param {Number} [updateIDFrom] the ID of the Onyx update that we want to start fetching from + * @param {Number} [updateIDTo] the ID of the Onyx update that we want to fetch up to */ -function reconnectApp() { - openApp(true); +function reconnectApp(updateIDFrom = 0, updateIDTo = 0) { + console.debug(`[OnyxUpdates] App reconnecting with updateIDFrom: ${updateIDFrom} and updateIDTo: ${updateIDTo}`); + getPolicyParamsForOpenOrReconnect().then((policyParams) => { + const params = {...policyParams}; + + // When the app reconnects we do a fast "sync" of the LHN and only return chats that have new messages. We achieve this by sending the most recent reportActionID. + // we have locally. And then only update the user about chats with messages that have occurred after that reportActionID. + // + // - Look through the local report actions and reports to find the most recently modified report action or report. + // - We send this to the server so that it can compute which new chats the user needs to see and return only those as an optimization. + Timing.start(CONST.TIMING.CALCULATE_MOST_RECENT_LAST_MODIFIED_ACTION); + params.mostRecentReportActionLastModified = ReportActionsUtils.getMostRecentReportActionLastModified(); + Timing.end(CONST.TIMING.CALCULATE_MOST_RECENT_LAST_MODIFIED_ACTION, '', 500); + + // Include the update IDs when reconnecting so that the server can send incremental updates if they are available. + // Otherwise, a full set of app data will be returned. + if (updateIDFrom) { + params.updateIDFrom = updateIDFrom; + } + + if (updateIDTo) { + params.updateIDTo = updateIDTo; + } + + API.write('ReconnectApp', params, getOnyxDataForOpenOrReconnect()); + }); } /** diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index f6652bdf8fa2..d07cb23c039c 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -552,9 +552,22 @@ function subscribeToUserEvents() { if (_.isArray(pushJSON)) { updates = pushJSON; } else { - // const lastUpdateID = pushJSON.lastUpdateID; - // const previousUpdateID = pushJSON.previousUpdateID; updates = pushJSON.updates; + + // Not always we'll have the lastUpdateID and previousUpdateID properties in the pusher update + // until we finish the migration to reliable updates. So let's check it before actually updating + // the properties in Onyx + if (pushJSON.lastUpdateID && pushJSON.previousUpdateID) { + console.debug('[OnyxUpdates] Received lastUpdateID from pusher', pushJSON.lastUpdateID); + console.debug('[OnyxUpdates] Received previousUpdateID from pusher', pushJSON.previousUpdateID); + // Store these values in Onyx to allow App.reconnectApp() to fetch incremental updates from the server when a previous session is being reconnected to. + Onyx.multiSet({ + [ONYXKEYS.ONYX_UPDATES.LAST_UPDATE_ID]: Number(pushJSON.lastUpdateID || 0), + [ONYXKEYS.ONYX_UPDATES.PREVIOUS_UPDATE_ID]: Number(pushJSON.previousUpdateID || 0), + }); + } else { + console.debug('[OnyxUpdates] No lastUpdateID and previousUpdateID provided'); + } } _.each(updates, (multipleEvent) => { PusherUtils.triggerMultiEventHandler(multipleEvent.eventType, multipleEvent.data);