diff --git a/src/libs/ActiveClientManager/index.js b/src/libs/ActiveClientManager/index.js index 0d2fcd48101..3b23f22c83c 100644 --- a/src/libs/ActiveClientManager/index.js +++ b/src/libs/ActiveClientManager/index.js @@ -7,6 +7,14 @@ const clientID = Str.guid(); const maxClients = 20; let activeClients; +let isInitialized; + +// Keeps track of the ActiveClientManager's readiness in one place +// so that multiple calls of isReady resolve the same promise +const isInitializedPromise = new Promise((resolve) => { + isInitialized = resolve; +}); + Onyx.connect({ key: ONYXKEYS.ACTIVE_CLIENTS, callback: (val) => { @@ -22,7 +30,11 @@ Onyx.connect({ * Add our client ID to the list of active IDs */ function init() { - Onyx.merge(ONYXKEYS.ACTIVE_CLIENTS, [clientID]); + Onyx.merge(ONYXKEYS.ACTIVE_CLIENTS, [clientID]).then(isInitialized); +} + +function isReady() { + return isInitializedPromise; } /** @@ -37,4 +49,5 @@ function isClientTheLeader() { export { init, isClientTheLeader, + isReady, }; diff --git a/src/libs/ActiveClientManager/index.native.js b/src/libs/ActiveClientManager/index.native.js index 9bda7c6d3c1..f75bbd7ccc6 100644 --- a/src/libs/ActiveClientManager/index.native.js +++ b/src/libs/ActiveClientManager/index.native.js @@ -7,8 +7,12 @@ function init() {} function isClientTheLeader() { return true; } +function isReady() { + return Promise.resolve(); +} export { init, + isReady, isClientTheLeader, }; diff --git a/src/libs/Network.js b/src/libs/Network.js index 5116cc2d002..a781595089c 100644 --- a/src/libs/Network.js +++ b/src/libs/Network.js @@ -3,6 +3,7 @@ import lodashGet from 'lodash/get'; import Onyx from 'react-native-onyx'; import HttpUtils from './HttpUtils'; import ONYXKEYS from '../ONYXKEYS'; +import * as ActiveClientManager from './ActiveClientManager'; import CONST from '../CONST'; let isQueuePaused = false; @@ -20,30 +21,73 @@ let enhanceParameters; let onResponse = () => {}; let onError = () => {}; +let didLoadPersistedRequests; +let isOffline; + +/** + * Process the offline NETWORK_REQUEST_QUEUE + * @param {Array | null} persistedRequests - Requests + */ +function processOfflineQueue(persistedRequests) { + // NETWORK_REQUEST_QUEUE is shared across clients, thus every client will have similiar copy of + // NETWORK_REQUEST_QUEUE. It is very important to only process the queue from leader client + // otherwise requests will be duplicated. + // We only process the persisted requests when + // a) Client is leader. + // b) User is online. + // c) requests are not already loaded, + // d) When there is at least one request + if (!ActiveClientManager.isClientTheLeader() + || isOffline + || didLoadPersistedRequests + || !persistedRequests + || !persistedRequests.length) { + return; + } + + // Queue processing expects handlers but due to we are loading the requests from Storage + // we just noop them to ignore the errors. + _.each(persistedRequests, (request) => { + request.resolve = () => {}; + request.reject = () => {}; + }); + + // Merge the persisted requests with the requests in memory then clear out the queue as we only need to load + // this once when the app initializes + networkRequestQueue = [...networkRequestQueue, ...persistedRequests]; + Onyx.set(ONYXKEYS.NETWORK_REQUEST_QUEUE, []); + didLoadPersistedRequests = true; +} + // We subscribe to changes to the online/offline status of the network to determine when we should fire off API calls // vs queueing them for later. -let isOffline; Onyx.connect({ key: ONYXKEYS.NETWORK, - callback: val => isOffline = val && val.isOffline, -}); - -let didLoadPersistedRequests; -Onyx.connect({ - key: ONYXKEYS.NETWORK_REQUEST_QUEUE, - callback: (persistedRequests) => { - if (didLoadPersistedRequests || !persistedRequests) { + callback: (val) => { + if (!val) { return; } - // Merge the persisted requests with the requests in memory then clear out the queue as we only need to load - // this once when the app initializes - networkRequestQueue = [...networkRequestQueue, ...persistedRequests]; - didLoadPersistedRequests = true; - Onyx.set(ONYXKEYS.NETWORK_REQUEST_QUEUE, []); + // Client becomes online, process the queue. + if (isOffline && !val.isOffline) { + const connection = Onyx.connect({ + key: ONYXKEYS.NETWORK_REQUEST_QUEUE, + callback: processOfflineQueue, + }); + Onyx.disconnect(connection); + } + isOffline = val.isOffline; }, }); +// Subscribe to NETWORK_REQUEST_QUEUE queue as soon as Client is ready +ActiveClientManager.isReady().then(() => { + Onyx.connect({ + key: ONYXKEYS.NETWORK_REQUEST_QUEUE, + callback: processOfflineQueue, + }); +}); + // Subscribe to the user's session so we can include their email in every request and include it in the server logs let email; Onyx.connect({ @@ -107,12 +151,19 @@ function processNetworkRequestQueue() { if (!networkRequestQueue.length) { return; } - - // If we have a request then we need to check if it can be persisted in case we close the tab while offline - const retryableRequests = _.filter(networkRequestQueue, request => ( - !request.data.doNotRetry && request.data.persist - )); - Onyx.set(ONYXKEYS.NETWORK_REQUEST_QUEUE, retryableRequests); + const retryableRequests = []; + + // If we have a request then we need to check if it can be persisted in case we close the tab while offline. + // We filter persisted requests from the normal Queue to remove duplicates + networkRequestQueue = _.reject(networkRequestQueue, (request) => { + if (!request.data.doNotRetry && request.data.persist) { + retryableRequests.push(request); + return true; + } + }); + if (retryableRequests.length) { + Onyx.merge(ONYXKEYS.NETWORK_REQUEST_QUEUE, retryableRequests); + } return; } @@ -161,6 +212,18 @@ function processNetworkRequestQueue() { .catch(error => onError(queuedRequest, error)); }); + // We should clear the NETWORK_REQUEST_QUEUE when we have loaded the persisted requests & they are processed. + // As multiple client will be sharing the same Queue and NETWORK_REQUEST_QUEUE is synchronized among clients, + // we only ask Leader client to clear the queue + if (ActiveClientManager.isClientTheLeader() && didLoadPersistedRequests) { + Onyx.set(ONYXKEYS.NETWORK_REQUEST_QUEUE, []); + } + + // User could have bad connectivity and he can go offline multiple times + // thus we allow NETWORK_REQUEST_QUEUE to be processed multiple times but only after we have processed + // old requests in the NETWORK_REQUEST_QUEUE + didLoadPersistedRequests = false; + // We clear the request queue at the end by setting the queue to retryableRequests which will either have some // requests we want to retry or an empty array networkRequestQueue = requestsToProcessOnNextRun; diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 76b77847272..045c772d61e 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -587,8 +587,8 @@ function updateReportWithNewAction(reportID, reportAction, notificationPreferenc const reportActionsToMerge = {}; if (reportAction.clientID) { - // Remove the optimistic action from the report since we are about to replace it with the real one (which has - // the true sequenceNumber) + // Remove the optimistic action from the report since we are about to replace it + // with the real one (which has the true sequenceNumber) reportActionsToMerge[reportAction.clientID] = null; }