Skip to content
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

Implement the API changes for getting incremental reconnect updates #23516

Merged
merged 23 commits into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/ONYXKEYS.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',

Expand Down
17 changes: 13 additions & 4 deletions src/libs/Navigation/AppNavigator/AuthScreens.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand All @@ -105,6 +108,7 @@ const defaultProps = {
email: null,
},
lastOpenedPublicRoomID: null,
onyxUpdatesLastUpdateID: 0,
};

class AuthScreens extends React.Component {
Expand All @@ -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,
Expand All @@ -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();
johnmlee101 marked this conversation as resolved.
Show resolved Hide resolved
if (shouldGetAllData) {
App.openApp();
} else {
App.reconnectApp();
App.reconnectApp(this.props.onyxUpdatesLastUpdateID);
}

App.setUpPoliciesAndNavigate(this.props.session, !this.props.isSmallScreenWidth);
Expand Down Expand Up @@ -323,5 +329,8 @@ export default compose(
isUsingMemoryOnlyKeys: {
key: ONYXKEYS.IS_USING_MEMORY_ONLY_KEYS,
},
onyxUpdatesLastUpdateID: {
key: ONYXKEYS.ONYX_UPDATES.LAST_UPDATE_ID,
},
}),
)(AuthScreens);
131 changes: 81 additions & 50 deletions src/libs/actions/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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());
});
}

/**
Expand Down
17 changes: 15 additions & 2 deletions src/libs/actions/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down