From 3fc74f107aab163777dbd9992e540a1fb7b57714 Mon Sep 17 00:00:00 2001 From: madmax330 Date: Thu, 2 Jun 2022 18:10:27 +0400 Subject: [PATCH 001/171] Add retry logic --- src/libs/Middleware/Retry.js | 9 +++---- src/libs/Network/SequentialQueue.js | 38 +++++++++++++++++++++++++-- src/libs/actions/PersistedRequests.js | 11 -------- 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/libs/Middleware/Retry.js b/src/libs/Middleware/Retry.js index dd100efa681a..2cf71ee64f34 100644 --- a/src/libs/Middleware/Retry.js +++ b/src/libs/Middleware/Retry.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import * as PersistedRequests from '../actions/PersistedRequests'; import Log from '../Log'; import CONST from '../../CONST'; @@ -18,12 +19,8 @@ function Retry(response, request, isFromSequentialQueue) { if (isFromSequentialQueue) { const retryCount = PersistedRequests.incrementRetries(request); - Log.info('Persisted request failed', false, {retryCount, command: request.command, error: error.message}); - if (retryCount >= CONST.NETWORK.MAX_REQUEST_RETRIES) { - Log.info('Request failed too many times, removing from storage', false, {retryCount, command: request.command, error: error.message}); - PersistedRequests.remove(request); - } - return; + Log.info('Persisted request failed', false, {command: request.command, error: error.message}); + throw new Error('Retry request'); } if (request.command !== 'Log') { diff --git a/src/libs/Network/SequentialQueue.js b/src/libs/Network/SequentialQueue.js index 9de4cbf5b587..cbb140a1e520 100644 --- a/src/libs/Network/SequentialQueue.js +++ b/src/libs/Network/SequentialQueue.js @@ -24,17 +24,51 @@ let isSequentialQueueRunning = false; function process() { const persistedRequests = PersistedRequests.getAll(); - // This sanity check is also a recursion exit point - if (NetworkStore.isOffline() || _.isEmpty(persistedRequests)) { + if (_.isEmpty(persistedRequests)) { return Promise.resolve(); } + isSequentialQueueRunning = true; + while (!_.isEmpty(persistedRequests)) { + // If we're not online, stop processing + if (NetworkStore.isOffline()) { + isSequentialQueueRunning = false; + return Promise.resolve(); + } + + const currentRequest = persistedRequests.shift(); + + try { + return Request.processWithMiddleware(currentRequest, true); + } catch(error) { + // If we get an error here it means the retry middleware threw and error and we want to retry the request + currentWaitTime = getRequestWaitTime(currentWaitTime); + sleep() + + } + } + + + + const task = _.reduce(persistedRequests, (previousRequest, request) => previousRequest.then(() => Request.processWithMiddleware(request, true)), Promise.resolve()); // Do a recursive call in case the queue is not empty after processing the current batch return task.then(process); } +function getRequestWaitTime(currentWaitTime) { + if (currentWaitTime) { + return Math.max(currentWaitTime * 2, 10000); + } + + return 10 + _.random(90); +} + +function sleep (time) { + return new Promise((resolve) => setTimeout(resolve, time)); + } + function flush() { if (isSequentialQueueRunning) { return; diff --git a/src/libs/actions/PersistedRequests.js b/src/libs/actions/PersistedRequests.js index 260526c13be2..76b8a4952d80 100644 --- a/src/libs/actions/PersistedRequests.js +++ b/src/libs/actions/PersistedRequests.js @@ -2,9 +2,7 @@ import Onyx from 'react-native-onyx'; import _ from 'underscore'; import lodashUnionWith from 'lodash/unionWith'; import ONYXKEYS from '../../ONYXKEYS'; -import RetryCounter from '../RetryCounter'; -const persistedRequestsRetryCounter = new RetryCounter(); let persistedRequests = []; Onyx.connect({ @@ -41,18 +39,9 @@ function getAll() { return persistedRequests; } -/** - * @param {Object} request - * @returns {Number} - */ -function incrementRetries(request) { - return persistedRequestsRetryCounter.incrementRetries(request); -} - export { clear, save, getAll, remove, - incrementRetries, }; From 84dda04339dff5a49a0654a721d3213696a413b2 Mon Sep 17 00:00:00 2001 From: madmax330 Date: Mon, 6 Jun 2022 13:19:27 +0400 Subject: [PATCH 002/171] remove retry middleware --- src/libs/Middleware/Retry.js | 38 --------------------------- src/libs/actions/PersistedRequests.js | 2 -- 2 files changed, 40 deletions(-) delete mode 100644 src/libs/Middleware/Retry.js diff --git a/src/libs/Middleware/Retry.js b/src/libs/Middleware/Retry.js deleted file mode 100644 index 915fadcb26a6..000000000000 --- a/src/libs/Middleware/Retry.js +++ /dev/null @@ -1,38 +0,0 @@ -import _ from 'underscore'; -import * as PersistedRequests from '../actions/PersistedRequests'; -import Log from '../Log'; -import CONST from '../../CONST'; - -/** - * @param {Promise} response - * @param {Object} request - * @param {Boolean} isFromSequentialQueue - * @returns {Promise} - */ -function Retry(response, request, isFromSequentialQueue) { - return response - .catch((error) => { - // Do not retry any requests that are cancelled - if (error.name === CONST.ERROR.REQUEST_CANCELLED) { - return; - } - - if (isFromSequentialQueue) { - const retryCount = PersistedRequests.incrementRetries(request); - Log.info('Persisted request failed', false, {command: request.command, error: error.message}); - throw new Error('Retry request'); - } - - if (request.command !== 'Log') { - Log.hmmm('[Network] Handled error when making request', error); - } else { - console.debug('[Network] There was an error in the Log API command, unable to log to server!', error); - } - - if (request.resolve) { - request.resolve({jsonCode: CONST.JSON_CODE.UNABLE_TO_RETRY}); - } - }); -} - -export default Retry; diff --git a/src/libs/actions/PersistedRequests.js b/src/libs/actions/PersistedRequests.js index 76b8a4952d80..d35775af88d9 100644 --- a/src/libs/actions/PersistedRequests.js +++ b/src/libs/actions/PersistedRequests.js @@ -12,7 +12,6 @@ Onyx.connect({ function clear() { Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, []); - persistedRequestsRetryCounter.clear(); } /** @@ -27,7 +26,6 @@ function save(requestsToPersist) { * @param {Object} requestToRemove */ function remove(requestToRemove) { - persistedRequestsRetryCounter.remove(requestToRemove); persistedRequests = _.reject(persistedRequests, persistedRequest => _.isEqual(persistedRequest, requestToRemove)); Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, persistedRequests); } From 0375837714ebcdbe45bc8dc83678cf44ad283f8d Mon Sep 17 00:00:00 2001 From: madmax330 Date: Mon, 6 Jun 2022 15:12:09 +0400 Subject: [PATCH 003/171] Finish theoretical implementation --- src/libs/Middleware/Logging.js | 6 --- src/libs/Network/SequentialQueue.js | 77 +++++++++++++++-------------- src/libs/RequestThrottle.js | 29 +++++++++++ src/libs/RetryCounter.js | 27 ---------- 4 files changed, 70 insertions(+), 69 deletions(-) create mode 100644 src/libs/RequestThrottle.js delete mode 100644 src/libs/RetryCounter.js diff --git a/src/libs/Middleware/Logging.js b/src/libs/Middleware/Logging.js index af58c6d9e0e4..174b1b4fa9af 100644 --- a/src/libs/Middleware/Logging.js +++ b/src/libs/Middleware/Logging.js @@ -2,7 +2,6 @@ import _ from 'underscore'; import lodashGet from 'lodash/get'; import Log from '../Log'; import CONST from '../../CONST'; -import * as PersistedRequests from '../actions/PersistedRequests'; /** * @param {String} message @@ -53,15 +52,10 @@ function Logging(response, request) { return data; }) .catch((error) => { - const persisted = lodashGet(request, 'data.persist'); - // Cancelled requests are normal and can happen when a user logs out. No extra handling is needed here besides // remove the request from the PersistedRequests if the request exists. if (error.name === CONST.ERROR.REQUEST_CANCELLED) { Log.info('[Network] Error: Request canceled', false, request); - if (persisted) { - PersistedRequests.remove(request); - } // Re-throw this error so the next handler can manage it throw error; diff --git a/src/libs/Network/SequentialQueue.js b/src/libs/Network/SequentialQueue.js index cbb140a1e520..3e3cef692752 100644 --- a/src/libs/Network/SequentialQueue.js +++ b/src/libs/Network/SequentialQueue.js @@ -5,12 +5,28 @@ import * as NetworkStore from './NetworkStore'; import ONYXKEYS from '../../ONYXKEYS'; import * as ActiveClientManager from '../ActiveClientManager'; import * as Request from '../Request'; +import RequestThrottle from '../RequestThrottle'; +import CONST from '../../CONST'; let resolveIsReadyPromise; let isReadyPromise = new Promise((resolve) => { resolveIsReadyPromise = resolve; }); +const requestThrottle = new RequestThrottle(); +const errorsToRetry = [ + CONST.ERROR.FAILED_TO_FETCH, + CONST.ERROR.IOS_NETWORK_CONNECTION_LOST, + CONST.ERROR.NETWORK_REQUEST_FAILED, + CONST.ERROR.IOS_NETWORK_CONNECTION_LOST_RUSSIAN, + CONST.ERROR.IOS_NETWORK_CONNECTION_LOST_SWEDISH, + CONST.ERROR.FIREFOX_DOCUMENT_LOAD_ABORTED, + CONST.ERROR.SAFARI_DOCUMENT_LOAD_ABORTED, + CONST.ERROR.IOS_LOAD_FAILED, + CONST.ERROR.GATEWAY_TIMEOUT, + CONST.ERROR.EXPENSIFY_SERVICE_INTERRUPTED, +]; + // Resolve the isReadyPromise immediately so that the queue starts working as soon as the page loads resolveIsReadyPromise(); @@ -24,51 +40,40 @@ let isSequentialQueueRunning = false; function process() { const persistedRequests = PersistedRequests.getAll(); - if (_.isEmpty(persistedRequests)) { + // If we have no persisted requests or we are offline we don't want to make any requests so we return early + if (_.isEmpty(persistedRequests) || NetworkStore.isOffline()) { + requestThrottle.clear(); return Promise.resolve(); } - isSequentialQueueRunning = true; - while (!_.isEmpty(persistedRequests)) { - // If we're not online, stop processing - if (NetworkStore.isOffline()) { - isSequentialQueueRunning = false; + // Get the first request in the queue and process it + const request = persistedRequests.shift(); + return Request.processWithMiddleware(request, true).then(() => { + // If the request is successful we want to: + // - Remove it from the queue + // - Clear any wait time we may have added if it failed before + // - Call process again to process the other requests in the queue + PersistedRequests.remove(request); + requestThrottle.clear(); + return process(); + }).catch((error) => { + // If a request fails with a non-retryable error we just remove it from the queue and return + if (!_.contains(errorsToRetry, error.message)) { + PersistedRequests.remove(request); return Promise.resolve(); } - const currentRequest = persistedRequests.shift(); - - try { - return Request.processWithMiddleware(currentRequest, true); - } catch(error) { - // If we get an error here it means the retry middleware threw and error and we want to retry the request - currentWaitTime = getRequestWaitTime(currentWaitTime); - sleep() - + // If the request failed and we want to retry it: + // - Check that we are not offline + // - Sleep for a period of time + // - Call process again. This will retry the same request since we have not removed it from the queue + if (NetworkStore.isOffline()) { + return Promise.resolve(); } - } - - - - - const task = _.reduce(persistedRequests, (previousRequest, request) => previousRequest.then(() => Request.processWithMiddleware(request, true)), Promise.resolve()); - - // Do a recursive call in case the queue is not empty after processing the current batch - return task.then(process); -} - -function getRequestWaitTime(currentWaitTime) { - if (currentWaitTime) { - return Math.max(currentWaitTime * 2, 10000); - } - - return 10 + _.random(90); + return requestThrottle.sleep(requestThrottle.getRequestWaitTime()).then(() => process()); + }); } -function sleep (time) { - return new Promise((resolve) => setTimeout(resolve, time)); - } - function flush() { if (isSequentialQueueRunning) { return; diff --git a/src/libs/RequestThrottle.js b/src/libs/RequestThrottle.js new file mode 100644 index 000000000000..c8a4c09c215d --- /dev/null +++ b/src/libs/RequestThrottle.js @@ -0,0 +1,29 @@ +import _ from 'underscore'; + +export default class RequestThrottle { + constructor() { + this.waitTime = 0; + } + + clear() { + this.waitTime = 0; + } + + /** + * @returns {Number} time to wait in ms + */ + getRequestWaitTime() { + if (this.waitTime) { + return Math.min(this.waitTime * 2, 10000); + } + return 10 + _.random(90); + } + + /** + * @param {Number} time + * @returns {Promise} + */ + sleep(time) { + return new Promise(resolve => setTimeout(resolve, time)); + } +} diff --git a/src/libs/RetryCounter.js b/src/libs/RetryCounter.js deleted file mode 100644 index 6885046ab8a3..000000000000 --- a/src/libs/RetryCounter.js +++ /dev/null @@ -1,27 +0,0 @@ -export default class RetryCounter { - constructor() { - this.retryMap = new Map(); - } - - clear() { - this.retryMap.clear(); - } - - /** - * @param {Object} request - * @returns {Number} retry count - */ - incrementRetries(request) { - const current = this.retryMap.get(request) || 0; - const next = current + 1; - this.retryMap.set(request, next); - return next; - } - - /** - * @param {Object} request - */ - remove(request) { - this.retryMap.delete(request); - } -} From 4463550f0dab7594b208de575ccb8a43c2ab66d1 Mon Sep 17 00:00:00 2001 From: madmax330 Date: Mon, 6 Jun 2022 15:15:27 +0400 Subject: [PATCH 004/171] fix throttle --- src/libs/RequestThrottle.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libs/RequestThrottle.js b/src/libs/RequestThrottle.js index c8a4c09c215d..95cd000e85b6 100644 --- a/src/libs/RequestThrottle.js +++ b/src/libs/RequestThrottle.js @@ -14,9 +14,11 @@ export default class RequestThrottle { */ getRequestWaitTime() { if (this.waitTime) { - return Math.min(this.waitTime * 2, 10000); + this.waitTime = Math.min(this.waitTime * 2, 10000); + } else { + this.waitTime = 10 + _.random(90); } - return 10 + _.random(90); + return this.waitTime; } /** From ab110b74aa7e665c7a0f0b19ba7b82e9fd22cb46 Mon Sep 17 00:00:00 2001 From: madmax330 Date: Mon, 6 Jun 2022 15:30:50 +0400 Subject: [PATCH 005/171] remove in queue --- src/libs/Middleware/Reauthentication.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libs/Middleware/Reauthentication.js b/src/libs/Middleware/Reauthentication.js index 7dc971191b87..ef743fd4ff24 100644 --- a/src/libs/Middleware/Reauthentication.js +++ b/src/libs/Middleware/Reauthentication.js @@ -3,7 +3,6 @@ import CONST from '../../CONST'; import * as NetworkStore from '../Network/NetworkStore'; import * as MainQueue from '../Network/MainQueue'; import * as Authentication from '../Authentication'; -import * as PersistedRequests from '../actions/PersistedRequests'; import * as Request from '../Request'; import Log from '../Log'; @@ -79,7 +78,6 @@ function Reauthentication(response, request, isFromSequentialQueue) { } if (isFromSequentialQueue) { - PersistedRequests.remove(request); return data; } From 4594ce81de4982c91a78114e18606a5f1f405fe4 Mon Sep 17 00:00:00 2001 From: madmax330 Date: Tue, 7 Jun 2022 17:25:31 +0400 Subject: [PATCH 006/171] more cleanup --- src/libs/Network/SequentialQueue.js | 55 +++++++++-------------------- src/libs/RequestThrottle.js | 4 +-- 2 files changed, 18 insertions(+), 41 deletions(-) diff --git a/src/libs/Network/SequentialQueue.js b/src/libs/Network/SequentialQueue.js index 17f1077dd62d..bb75ed9811be 100644 --- a/src/libs/Network/SequentialQueue.js +++ b/src/libs/Network/SequentialQueue.js @@ -8,11 +8,6 @@ import * as Request from '../Request'; import RequestThrottle from '../RequestThrottle'; import CONST from '../../CONST'; -let resolveIsReadyPromise; -let isReadyPromise = new Promise((resolve) => { - resolveIsReadyPromise = resolve; -}); - const requestThrottle = new RequestThrottle(); const errorsToRetry = [ CONST.ERROR.FAILED_TO_FETCH, @@ -27,11 +22,7 @@ const errorsToRetry = [ CONST.ERROR.EXPENSIFY_SERVICE_INTERRUPTED, ]; -// Resolve the isReadyPromise immediately so that the queue starts working as soon as the page loads -resolveIsReadyPromise(); - let isSequentialQueueRunning = false; - let currentRequest = null; /** @@ -44,35 +35,34 @@ function process() { // If we have no persisted requests or we are offline we don't want to make any requests so we return early if (_.isEmpty(persistedRequests) || NetworkStore.isOffline()) { + isSequentialQueueRunning = false; requestThrottle.clear(); return Promise.resolve(); } + isSequentialQueueRunning = true; + // Get the first request in the queue and process it - const request = persistedRequests.shift(); - return Request.processWithMiddleware(request, true).then(() => { + currentRequest = persistedRequests.shift(); + return Request.processWithMiddleware(currentRequest, true).then(() => { // If the request is successful we want to: // - Remove it from the queue // - Clear any wait time we may have added if it failed before // - Call process again to process the other requests in the queue - PersistedRequests.remove(request); + PersistedRequests.remove(currentRequest); requestThrottle.clear(); return process(); }).catch((error) => { - // If a request fails with a non-retryable error we just remove it from the queue and return + // If a request fails with a non-retryable error we just remove it from the queue and move on to the next request if (!_.contains(errorsToRetry, error.message)) { - PersistedRequests.remove(request); - return Promise.resolve(); + PersistedRequests.remove(currentRequest); + return process(); } // If the request failed and we want to retry it: - // - Check that we are not offline // - Sleep for a period of time // - Call process again. This will retry the same request since we have not removed it from the queue - if (NetworkStore.isOffline()) { - return Promise.resolve(); - } - return requestThrottle.sleep(requestThrottle.getRequestWaitTime()).then(() => process()); + requestThrottle.sleep().then(() => process()); }); } @@ -87,24 +77,12 @@ function flush() { return; } - isSequentialQueueRunning = true; - - // Reset the isReadyPromise so that the queue will be flushed as soon as the request is finished - isReadyPromise = new Promise((resolve) => { - resolveIsReadyPromise = resolve; - }); - // Ensure persistedRequests are read from storage before proceeding with the queue const connectionID = Onyx.connect({ key: ONYXKEYS.PERSISTED_REQUESTS, callback: () => { Onyx.disconnect(connectionID); - process() - .finally(() => { - isSequentialQueueRunning = false; - resolveIsReadyPromise(); - currentRequest = null; - }); + process(); }, }); } @@ -119,6 +97,9 @@ function isRunning() { // Flush the queue when the connection resumes NetworkStore.onReconnection(flush); +// Call flush immediately so that the queue starts running as soon as the page loads +flush(); + /** * @param {Object} request */ @@ -131,13 +112,9 @@ function push(request) { return; } - // If the queue is running this request will run once it has finished processing the current batch - if (isSequentialQueueRunning) { - isReadyPromise.then(flush); - return; + if (!isSequentialQueueRunning) { + flush(); } - - flush(); } /** diff --git a/src/libs/RequestThrottle.js b/src/libs/RequestThrottle.js index 95cd000e85b6..6364e915165f 100644 --- a/src/libs/RequestThrottle.js +++ b/src/libs/RequestThrottle.js @@ -25,7 +25,7 @@ export default class RequestThrottle { * @param {Number} time * @returns {Promise} */ - sleep(time) { - return new Promise(resolve => setTimeout(resolve, time)); + sleep() { + return new Promise(resolve => setTimeout(resolve, this.getRequestWaitTime())); } } From 055c76a70ce578884740392727a0db9f0aa36492 Mon Sep 17 00:00:00 2001 From: madmax330 Date: Wed, 8 Jun 2022 10:39:53 +0400 Subject: [PATCH 007/171] more cleanup --- src/libs/Middleware/index.js | 2 -- src/libs/deprecatedAPI.js | 3 --- 2 files changed, 5 deletions(-) diff --git a/src/libs/Middleware/index.js b/src/libs/Middleware/index.js index 4e270b009c1d..3fd68c833590 100644 --- a/src/libs/Middleware/index.js +++ b/src/libs/Middleware/index.js @@ -1,13 +1,11 @@ import Logging from './Logging'; import Reauthentication from './Reauthentication'; import RecheckConnection from './RecheckConnection'; -import Retry from './Retry'; import SaveResponseInOnyx from './SaveResponseInOnyx'; export { Logging, Reauthentication, RecheckConnection, - Retry, SaveResponseInOnyx, }; diff --git a/src/libs/deprecatedAPI.js b/src/libs/deprecatedAPI.js index 67333741f0a7..ab39018c0dce 100644 --- a/src/libs/deprecatedAPI.js +++ b/src/libs/deprecatedAPI.js @@ -20,9 +20,6 @@ Request.use(Middleware.RecheckConnection); // Reauthentication - Handles jsonCode 407 which indicates an expired authToken. We need to reauthenticate and get a new authToken with our stored credentials. Request.use(Middleware.Reauthentication); -// Retry - Handles retrying any failed requests. -Request.use(Middleware.Retry); - // SaveResponseInOnyx - Merges either the successData or failureData into Onyx depending on if the call was successful or not Request.use(Middleware.SaveResponseInOnyx); From 95d3cdae49e7093e6170aa1f6cd3d14c62533bc2 Mon Sep 17 00:00:00 2001 From: madmax330 Date: Fri, 10 Jun 2022 16:27:28 +0400 Subject: [PATCH 008/171] test diff + changes --- src/libs/Middleware/Logging.js | 6 ++---- src/libs/Network/SequentialQueue.js | 9 ++++++++- src/libs/RequestThrottle.js | 4 +++- src/libs/actions/PersistedRequests.js | 3 +++ 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/libs/Middleware/Logging.js b/src/libs/Middleware/Logging.js index 174b1b4fa9af..0baa9bc3f1b4 100644 --- a/src/libs/Middleware/Logging.js +++ b/src/libs/Middleware/Logging.js @@ -56,9 +56,6 @@ function Logging(response, request) { // remove the request from the PersistedRequests if the request exists. if (error.name === CONST.ERROR.REQUEST_CANCELLED) { Log.info('[Network] Error: Request canceled', false, request); - - // Re-throw this error so the next handler can manage it - throw error; } if (error.message === CONST.ERROR.FAILED_TO_FETCH) { @@ -102,7 +99,8 @@ function Logging(response, request) { }, false); } - throw new Error(CONST.ERROR.XHR_FAILED); + // Re-throw this error so the next handler can manage it + throw error; }); } diff --git a/src/libs/Network/SequentialQueue.js b/src/libs/Network/SequentialQueue.js index bb75ed9811be..6a4ac0a922d4 100644 --- a/src/libs/Network/SequentialQueue.js +++ b/src/libs/Network/SequentialQueue.js @@ -31,10 +31,12 @@ let currentRequest = null; * @returns {Promise} */ function process() { - const persistedRequests = PersistedRequests.getAll(); + const persistedRequests = [...PersistedRequests.getAll()]; + console.log('running queue', persistedRequests); // If we have no persisted requests or we are offline we don't want to make any requests so we return early if (_.isEmpty(persistedRequests) || NetworkStore.isOffline()) { + console.log('Offline or no requests'); isSequentialQueueRunning = false; requestThrottle.clear(); return Promise.resolve(); @@ -44,6 +46,7 @@ function process() { // Get the first request in the queue and process it currentRequest = persistedRequests.shift(); + console.log('processing', currentRequest); return Request.processWithMiddleware(currentRequest, true).then(() => { // If the request is successful we want to: // - Remove it from the queue @@ -51,10 +54,12 @@ function process() { // - Call process again to process the other requests in the queue PersistedRequests.remove(currentRequest); requestThrottle.clear(); + console.log('done, moving on to next request'); return process(); }).catch((error) => { // If a request fails with a non-retryable error we just remove it from the queue and move on to the next request if (!_.contains(errorsToRetry, error.message)) { + console.log('not retrying, moving on', error); PersistedRequests.remove(currentRequest); return process(); } @@ -67,6 +72,7 @@ function process() { } function flush() { + console.log('Called flush'); if (isSequentialQueueRunning) { return; } @@ -82,6 +88,7 @@ function flush() { key: ONYXKEYS.PERSISTED_REQUESTS, callback: () => { Onyx.disconnect(connectionID); + console.log('Call process'); process(); }, }); diff --git a/src/libs/RequestThrottle.js b/src/libs/RequestThrottle.js index 6364e915165f..24a17dc8155e 100644 --- a/src/libs/RequestThrottle.js +++ b/src/libs/RequestThrottle.js @@ -26,6 +26,8 @@ export default class RequestThrottle { * @returns {Promise} */ sleep() { - return new Promise(resolve => setTimeout(resolve, this.getRequestWaitTime())); + const wait = this.getRequestWaitTime(); + console.log('sleeping', wait); + return new Promise(resolve => setTimeout(resolve, wait)); } } diff --git a/src/libs/actions/PersistedRequests.js b/src/libs/actions/PersistedRequests.js index d35775af88d9..396596a8347f 100644 --- a/src/libs/actions/PersistedRequests.js +++ b/src/libs/actions/PersistedRequests.js @@ -11,6 +11,7 @@ Onyx.connect({ }); function clear() { + console.log('clearing'); Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, []); } @@ -18,6 +19,7 @@ function clear() { * @param {Array} requestsToPersist */ function save(requestsToPersist) { + console.log('adding', requestsToPersist); persistedRequests = lodashUnionWith(persistedRequests, requestsToPersist, _.isEqual); Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, persistedRequests); } @@ -26,6 +28,7 @@ function save(requestsToPersist) { * @param {Object} requestToRemove */ function remove(requestToRemove) { + console.log('removing', requestToRemove); persistedRequests = _.reject(persistedRequests, persistedRequest => _.isEqual(persistedRequest, requestToRemove)); Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, persistedRequests); } From 157b00a9fbe8a33bc639afb843c18f8657ef7426 Mon Sep 17 00:00:00 2001 From: madmax330 Date: Fri, 10 Jun 2022 16:30:13 +0400 Subject: [PATCH 009/171] cleanup --- src/libs/Network/SequentialQueue.js | 7 ------- src/libs/RequestThrottle.js | 4 +--- src/libs/actions/PersistedRequests.js | 3 --- 3 files changed, 1 insertion(+), 13 deletions(-) diff --git a/src/libs/Network/SequentialQueue.js b/src/libs/Network/SequentialQueue.js index 6a4ac0a922d4..f11504adb86d 100644 --- a/src/libs/Network/SequentialQueue.js +++ b/src/libs/Network/SequentialQueue.js @@ -32,11 +32,9 @@ let currentRequest = null; */ function process() { const persistedRequests = [...PersistedRequests.getAll()]; - console.log('running queue', persistedRequests); // If we have no persisted requests or we are offline we don't want to make any requests so we return early if (_.isEmpty(persistedRequests) || NetworkStore.isOffline()) { - console.log('Offline or no requests'); isSequentialQueueRunning = false; requestThrottle.clear(); return Promise.resolve(); @@ -46,7 +44,6 @@ function process() { // Get the first request in the queue and process it currentRequest = persistedRequests.shift(); - console.log('processing', currentRequest); return Request.processWithMiddleware(currentRequest, true).then(() => { // If the request is successful we want to: // - Remove it from the queue @@ -54,12 +51,10 @@ function process() { // - Call process again to process the other requests in the queue PersistedRequests.remove(currentRequest); requestThrottle.clear(); - console.log('done, moving on to next request'); return process(); }).catch((error) => { // If a request fails with a non-retryable error we just remove it from the queue and move on to the next request if (!_.contains(errorsToRetry, error.message)) { - console.log('not retrying, moving on', error); PersistedRequests.remove(currentRequest); return process(); } @@ -72,7 +67,6 @@ function process() { } function flush() { - console.log('Called flush'); if (isSequentialQueueRunning) { return; } @@ -88,7 +82,6 @@ function flush() { key: ONYXKEYS.PERSISTED_REQUESTS, callback: () => { Onyx.disconnect(connectionID); - console.log('Call process'); process(); }, }); diff --git a/src/libs/RequestThrottle.js b/src/libs/RequestThrottle.js index 24a17dc8155e..6364e915165f 100644 --- a/src/libs/RequestThrottle.js +++ b/src/libs/RequestThrottle.js @@ -26,8 +26,6 @@ export default class RequestThrottle { * @returns {Promise} */ sleep() { - const wait = this.getRequestWaitTime(); - console.log('sleeping', wait); - return new Promise(resolve => setTimeout(resolve, wait)); + return new Promise(resolve => setTimeout(resolve, this.getRequestWaitTime())); } } diff --git a/src/libs/actions/PersistedRequests.js b/src/libs/actions/PersistedRequests.js index 396596a8347f..d35775af88d9 100644 --- a/src/libs/actions/PersistedRequests.js +++ b/src/libs/actions/PersistedRequests.js @@ -11,7 +11,6 @@ Onyx.connect({ }); function clear() { - console.log('clearing'); Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, []); } @@ -19,7 +18,6 @@ function clear() { * @param {Array} requestsToPersist */ function save(requestsToPersist) { - console.log('adding', requestsToPersist); persistedRequests = lodashUnionWith(persistedRequests, requestsToPersist, _.isEqual); Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, persistedRequests); } @@ -28,7 +26,6 @@ function save(requestsToPersist) { * @param {Object} requestToRemove */ function remove(requestToRemove) { - console.log('removing', requestToRemove); persistedRequests = _.reject(persistedRequests, persistedRequest => _.isEqual(persistedRequest, requestToRemove)); Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, persistedRequests); } From b61d1ca222c673bd87c100b8d3bd75a9b325ae2f Mon Sep 17 00:00:00 2001 From: madmax330 Date: Fri, 10 Jun 2022 18:44:59 +0400 Subject: [PATCH 010/171] smal fix + remove irrelevant tests --- src/libs/Middleware/Logging.js | 4 +--- tests/unit/NetworkTest.js | 27 --------------------------- 2 files changed, 1 insertion(+), 30 deletions(-) diff --git a/src/libs/Middleware/Logging.js b/src/libs/Middleware/Logging.js index 0baa9bc3f1b4..2176987a7fe0 100644 --- a/src/libs/Middleware/Logging.js +++ b/src/libs/Middleware/Logging.js @@ -56,9 +56,7 @@ function Logging(response, request) { // remove the request from the PersistedRequests if the request exists. if (error.name === CONST.ERROR.REQUEST_CANCELLED) { Log.info('[Network] Error: Request canceled', false, request); - } - - if (error.message === CONST.ERROR.FAILED_TO_FETCH) { + } else if (error.message === CONST.ERROR.FAILED_TO_FETCH) { // Throw when we get a "Failed to fetch" error so we can retry. Very common if a user is offline or experiencing an unlikely scenario like // incorrect url, bad cors headers returned by the server, DNS lookup failure etc. Log.hmmm('[Network] Error: Failed to fetch', {message: error.message, status: error.status}); diff --git a/tests/unit/NetworkTest.js b/tests/unit/NetworkTest.js index 1e4208f5e770..c3a450666da3 100644 --- a/tests/unit/NetworkTest.js +++ b/tests/unit/NetworkTest.js @@ -434,33 +434,6 @@ test('persisted request should not be cleared until a backend response occurs', }); }); -test(`persisted request should be retried up to ${CONST.NETWORK.MAX_REQUEST_RETRIES} times`, () => { - // We're setting up xhr handler that always rejects with a fetch error - const xhr = jest.spyOn(HttpUtils, 'xhr') - .mockRejectedValue(new Error(CONST.ERROR.FAILED_TO_FETCH)); - - // Given we have a request made while we're offline - return Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}) - .then(() => { - // When network calls with `persist` are made - Network.post('mock command', {param1: 'value1', persist: true}); - return waitForPromisesToResolve(); - }) - - // When we resume connectivity - .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: false})) - .then(waitForPromisesToResolve) - .then(() => { - // The request should be retried a number of times - expect(xhr).toHaveBeenCalledTimes(CONST.NETWORK.MAX_REQUEST_RETRIES); - _.each(xhr.mock.calls, (args) => { - expect(args).toEqual( - expect.arrayContaining(['mock command', expect.objectContaining({param1: 'value1', persist: true})]), - ); - }); - }); -}); - test('test bad response will log alert', () => { global.fetch = jest.fn() .mockResolvedValueOnce({ok: false, status: 502, statusText: 'Bad Gateway'}); From da83bba852de6fa37444718788b1ee0adfd97130 Mon Sep 17 00:00:00 2001 From: madmax330 Date: Fri, 10 Jun 2022 18:46:07 +0400 Subject: [PATCH 011/171] remove more irrelevant tests --- tests/unit/NetworkTest.js | 53 --------------------------------------- 1 file changed, 53 deletions(-) diff --git a/tests/unit/NetworkTest.js b/tests/unit/NetworkTest.js index c3a450666da3..b67cb4434b59 100644 --- a/tests/unit/NetworkTest.js +++ b/tests/unit/NetworkTest.js @@ -274,37 +274,6 @@ test('Request will not run until credentials are read from Onyx', () => { }); }); -test('Non-retryable request will not be retried if connection is lost in flight', () => { - // Given a xhr mock that will fail as if network connection dropped - const xhr = jest.spyOn(HttpUtils, 'xhr') - .mockImplementationOnce(() => { - Onyx.merge(ONYXKEYS.NETWORK, {isOffline: true}); - return Promise.reject(new Error(CONST.ERROR.FAILED_TO_FETCH)); - }); - - // Given a non-retryable request (that is bound to fail) - const promise = Network.post('Get'); - - return waitForPromisesToResolve() - .then(() => { - // When network connection is recovered - Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false}); - return waitForPromisesToResolve(); - }) - .then(() => { - // Advance the network request queue by 1 second so that it can realize it's back online - jest.advanceTimersByTime(CONST.NETWORK.PROCESS_REQUEST_DELAY_MS); - return waitForPromisesToResolve(); - }) - .then(() => { - // Then the request should only have been attempted once and we should get an unable to retry - expect(xhr).toHaveBeenCalledTimes(1); - - // And the promise should be resolved with the special offline jsonCode - return expect(promise).resolves.toEqual({jsonCode: CONST.JSON_CODE.UNABLE_TO_RETRY}); - }); -}); - test('Retryable requests should be persisted while offline', () => { // We don't expect calls `xhr` so we make the test fail if such call is made const xhr = jest.spyOn(HttpUtils, 'xhr').mockRejectedValue(new Error('Unexpected xhr call')); @@ -451,28 +420,6 @@ test('test bad response will log alert', () => { }); }); -test('test Failed to fetch error for non-retryable requests resolve with unable to retry jsonCode', () => { - // Setup xhr handler that rejects once with a Failed to Fetch - global.fetch = jest.fn().mockRejectedValue(new Error(CONST.ERROR.FAILED_TO_FETCH)); - const onResolved = jest.fn(); - - // Given we have a request made while online - return Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}) - .then(() => { - expect(NetworkStore.isOffline()).toBe(false); - - // When network calls with are made - Network.post('mock command', {param1: 'value1'}) - .then(onResolved); - return waitForPromisesToResolve(); - }) - .then(() => { - const response = onResolved.mock.calls[0][0]; - expect(onResolved).toHaveBeenCalled(); - expect(response.jsonCode).toBe(CONST.JSON_CODE.UNABLE_TO_RETRY); - }); -}); - test('persisted request can trigger reauthentication for anything retryable', () => { // We're setting up xhr handler that rejects once with a 407 code and again with success const xhr = jest.spyOn(HttpUtils, 'xhr') From 0d164a3ab70a065540afc8d6706acc44d5bd7a9a Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Thu, 19 Jan 2023 15:46:13 -0800 Subject: [PATCH 012/171] Fix waitForIdle by restoring the isReadyPromise --- src/libs/Network/SequentialQueue.js | 33 +++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/src/libs/Network/SequentialQueue.js b/src/libs/Network/SequentialQueue.js index 10e26821ee09..22fec95e6210 100644 --- a/src/libs/Network/SequentialQueue.js +++ b/src/libs/Network/SequentialQueue.js @@ -22,6 +22,14 @@ const errorsToRetry = [ CONST.ERROR.EXPENSIFY_SERVICE_INTERRUPTED, ]; +let resolveIsReadyPromise; +let isReadyPromise = new Promise((resolve) => { + resolveIsReadyPromise = resolve; +}); + +// Resolve the isReadyPromise immediately so that the queue starts working as soon as the page loads +resolveIsReadyPromise(); + let isSequentialQueueRunning = false; let currentRequest = null; @@ -77,12 +85,24 @@ function flush() { return; } + isSequentialQueueRunning = true; + + // Reset the isReadyPromise so that the queue will be flushed as soon as the request is finished + isReadyPromise = new Promise((resolve) => { + resolveIsReadyPromise = resolve; + }); + // Ensure persistedRequests are read from storage before proceeding with the queue const connectionID = Onyx.connect({ key: ONYXKEYS.PERSISTED_REQUESTS, callback: () => { Onyx.disconnect(connectionID); - process(); + process() + .finally(() => { + isSequentialQueueRunning = false; + resolveIsReadyPromise(); + currentRequest = null; + }); }, }); } @@ -97,9 +117,6 @@ function isRunning() { // Flush the queue when the connection resumes NetworkStore.onReconnection(flush); -// Call flush immediately so that the queue starts running as soon as the page loads -flush(); - /** * @param {Object} request */ @@ -112,9 +129,13 @@ function push(request) { return; } - if (!isSequentialQueueRunning) { - flush(); + // If the queue is running this request will run once it has finished processing the current batch + if (isSequentialQueueRunning) { + isReadyPromise.then(flush); + return; } + + flush(); } /** From d8c71d08982aeedf5c334b1d0ddc71c0b25a6b35 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Mon, 23 Jan 2023 15:11:24 -0800 Subject: [PATCH 013/171] Remove duplicate tests from merge --- tests/unit/NetworkTest.js | 458 -------------------------------------- 1 file changed, 458 deletions(-) diff --git a/tests/unit/NetworkTest.js b/tests/unit/NetworkTest.js index 3514b2df83c9..0a02132af77e 100644 --- a/tests/unit/NetworkTest.js +++ b/tests/unit/NetworkTest.js @@ -808,461 +808,3 @@ describe('NetworkTests', () => { }); }); }); - -test('Retryable requests should be persisted while offline', () => { - // We don't expect calls `xhr` so we make the test fail if such call is made - const xhr = jest.spyOn(HttpUtils, 'xhr').mockRejectedValue(new Error('Unexpected xhr call')); - - // Given we're offline - return Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}) - .then(() => { - // When network calls with `persist` are made - Network.post('mock command', {param1: 'value1', persist: true}); - Network.post('mock command', {param2: 'value2'}); - Network.post('mock command', {param3: 'value3', persist: true}); - return waitForPromisesToResolve(); - }) - .then(() => { - // Then `xhr` should not be used and requests should be persisted to storage - expect(xhr).not.toHaveBeenCalled(); - - const persisted = PersistedRequests.getAll(); - expect(persisted).toEqual([ - expect.objectContaining({command: 'mock command', data: expect.objectContaining({param1: 'value1'})}), - expect.objectContaining({command: 'mock command', data: expect.objectContaining({param3: 'value3'})}), - ]); - - PersistedRequests.clear(); - return waitForPromisesToResolve(); - }) - .then(() => { - expect(PersistedRequests.getAll()).toEqual([]); - }); -}); - -test('Retryable requests should resume when we are online', () => { - // We're setting up a basic case where all requests succeed when we resume connectivity - const xhr = jest.spyOn(HttpUtils, 'xhr').mockResolvedValue({jsonCode: CONST.JSON_CODE.SUCCESS}); - - // Given we have some requests made while we're offline - return Onyx.multiSet({ - [ONYXKEYS.NETWORK]: {isOffline: true}, - [ONYXKEYS.CREDENTIALS]: {autoGeneratedLogin: 'test', autoGeneratedPassword: 'passwd'}, - [ONYXKEYS.SESSION]: {authToken: 'testToken'}, - }) - .then(() => { - // When network calls with `persist` are made - Network.post('mock command', {param1: 'value1', persist: true}); - Network.post('mock command', {param2: 'value2', persist: true}); - return waitForPromisesToResolve(); - }) - .then(() => { - const persisted = PersistedRequests.getAll(); - expect(persisted).toHaveLength(2); - }) - - // When we resume connectivity - .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: false})) - .then(waitForPromisesToResolve) - .then(() => { - expect(NetworkStore.isOffline()).toBe(false); - expect(SequentialQueue.isRunning()).toBe(false); - - // Then `xhr` should be called with expected data, and the persisted queue should be empty - expect(xhr).toHaveBeenCalledTimes(2); - expect(xhr.mock.calls).toEqual([ - expect.arrayContaining(['mock command', expect.objectContaining({param1: 'value1'})]), - expect.arrayContaining(['mock command', expect.objectContaining({param2: 'value2'})]), - ]); - - const persisted = PersistedRequests.getAll(); - expect(persisted).toEqual([]); - }); -}); - -test('persisted request should not be cleared until a backend response occurs', () => { - // We're setting up xhr handler that will resolve calls programmatically - const xhrCalls = []; - const promises = []; - - jest.spyOn(HttpUtils, 'xhr') - .mockImplementation(() => { - promises.push(new Promise((resolve, reject) => { - xhrCalls.push({resolve, reject}); - })); - - return _.last(promises); - }); - - // Given we have some requests made while we're offline - return Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}) - .then(() => { - // When network calls with `persist` are made - Network.post('mock command', {param1: 'value1', persist: true}); - Network.post('mock command', {param2: 'value2', persist: true}); - return waitForPromisesToResolve(); - }) - - // When we resume connectivity - .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: false})) - .then(() => { - // Then requests should remain persisted until the xhr call is resolved - expect(_.size(PersistedRequests.getAll())).toEqual(2); - - xhrCalls[0].resolve({jsonCode: CONST.JSON_CODE.SUCCESS}); - return waitForPromisesToResolve(); - }) - .then(waitForPromisesToResolve) - .then(() => { - expect(_.size(PersistedRequests.getAll())).toEqual(1); - expect(PersistedRequests.getAll()).toEqual([ - expect.objectContaining({command: 'mock command', data: expect.objectContaining({param2: 'value2'})}), - ]); - - // When a request fails it should be retried - xhrCalls[1].reject(new Error(CONST.ERROR.FAILED_TO_FETCH)); - return waitForPromisesToResolve(); - }) - .then(() => { - expect(_.size(PersistedRequests.getAll())).toEqual(1); - expect(PersistedRequests.getAll()).toEqual([ - expect.objectContaining({command: 'mock command', data: expect.objectContaining({param2: 'value2'})}), - ]); - - // Finally, after it succeeds the queue should be empty - xhrCalls[2].resolve({jsonCode: CONST.JSON_CODE.SUCCESS}); - return waitForPromisesToResolve(); - }) - .then(() => { - expect(_.size(PersistedRequests.getAll())).toEqual(0); - }); -}); - -test('test Bad Gateway status will log hmmm', () => { - global.fetch = jest.fn() - .mockResolvedValueOnce({ok: false, status: 502, statusText: 'Bad Gateway'}); - - const logHmmmSpy = jest.spyOn(Log, 'hmmm'); - - // Given we have a request made while online - return Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}) - .then(() => { - Network.post('MockBadNetworkResponse', {param1: 'value1'}); - return waitForPromisesToResolve(); - }) - .then(() => { - expect(logHmmmSpy).toHaveBeenCalled(); - }); -}); - -test('test unknown status will log alert', () => { - global.fetch = jest.fn() - .mockResolvedValueOnce({ok: false, status: 418, statusText: 'I\'m a teapot'}); - - const logAlertSpy = jest.spyOn(Log, 'alert'); - - // Given we have a request made while online - return Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}) - .then(() => { - Network.post('MockBadNetworkResponse', {param1: 'value1'}); - return waitForPromisesToResolve(); - }) - .then(() => { - expect(logAlertSpy).toHaveBeenCalled(); - }); -}); - -test('persisted request can trigger reauthentication for anything retryable', () => { - // We're setting up xhr handler that rejects once with a 407 code and again with success - const xhr = jest.spyOn(HttpUtils, 'xhr') - .mockResolvedValue({jsonCode: CONST.JSON_CODE.SUCCESS}) // Default - .mockResolvedValueOnce({jsonCode: CONST.JSON_CODE.NOT_AUTHENTICATED}) // Initial call to test command return 407 - .mockResolvedValueOnce({jsonCode: CONST.JSON_CODE.SUCCESS}) // Call to Authenticate return 200 - .mockResolvedValueOnce({jsonCode: CONST.JSON_CODE.SUCCESS}); // Original command return 200 - - // Given we have a request made while we're offline and we have credentials available to reauthenticate - Onyx.merge(ONYXKEYS.CREDENTIALS, {autoGeneratedLogin: 'test', autoGeneratedPassword: 'passwd'}); - return waitForPromisesToResolve() - .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: true})) - .then(() => { - Network.post('Mock', {param1: 'value1', persist: true, shouldRetry: true}); - return waitForPromisesToResolve(); - }) - - // When we resume connectivity - .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: false})) - .then(waitForPromisesToResolve) - .then(() => { - const nonLogCalls = _.filter(xhr.mock.calls, ([commandName]) => commandName !== 'Log'); - - // The request should be retried once and reauthenticate should be called the second time - // expect(xhr).toHaveBeenCalledTimes(3); - const [call1, call2, call3] = nonLogCalls; - const [commandName1] = call1; - const [commandName2] = call2; - const [commandName3] = call3; - expect(commandName1).toBe('Mock'); - expect(commandName2).toBe('Authenticate'); - expect(commandName3).toBe('Mock'); - }); -}); - -test('several actions made while offline will get added in the order they are created', () => { - // Given offline state where all requests will eventualy succeed without issue - const xhr = jest.spyOn(HttpUtils, 'xhr') - .mockResolvedValue({jsonCode: CONST.JSON_CODE.SUCCESS}); - return Onyx.multiSet({ - [ONYXKEYS.SESSION]: {authToken: 'anyToken'}, - [ONYXKEYS.NETWORK]: {isOffline: true}, - [ONYXKEYS.CREDENTIALS]: {autoGeneratedLogin: 'test_user', autoGeneratedPassword: 'psswd'}, - }) - .then(() => { - // When we queue 6 persistable commands and one not persistable - Network.post('MockCommand', {content: 'value1', persist: true}); - Network.post('MockCommand', {content: 'value2', persist: true}); - Network.post('MockCommand', {content: 'value3', persist: true}); - Network.post('MockCommand', {content: 'not-persisted'}); - Network.post('MockCommand', {content: 'value4', persist: true}); - Network.post('MockCommand', {content: 'value5', persist: true}); - Network.post('MockCommand', {content: 'value6', persist: true}); - - return waitForPromisesToResolve(); - }) - .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: false})) - .then(waitForPromisesToResolve) - .then(() => { - // Then expect only 6 calls to have been made and for them to be made in the order that we made them - // and the non-persistable request isn't one of them - expect(xhr.mock.calls.length).toBe(6); - expect(xhr.mock.calls[0][1].content).toBe('value1'); - expect(xhr.mock.calls[1][1].content).toBe('value2'); - expect(xhr.mock.calls[2][1].content).toBe('value3'); - expect(xhr.mock.calls[3][1].content).toBe('value4'); - expect(xhr.mock.calls[4][1].content).toBe('value5'); - expect(xhr.mock.calls[5][1].content).toBe('value6'); - }); -}); - -test('several actions made while offline will get added in the order they are created when we need to reauthenticate', () => { - // Given offline state where all requests will eventualy succeed without issue and assumed to be valid credentials - const xhr = jest.spyOn(HttpUtils, 'xhr') - .mockResolvedValueOnce({jsonCode: CONST.JSON_CODE.NOT_AUTHENTICATED}) - .mockResolvedValue({jsonCode: CONST.JSON_CODE.SUCCESS}); - - return Onyx.multiSet({ - [ONYXKEYS.NETWORK]: {isOffline: true}, - [ONYXKEYS.SESSION]: {authToken: 'test'}, - [ONYXKEYS.CREDENTIALS]: {autoGeneratedLogin: 'test', autoGeneratedPassword: 'passwd'}, - }) - .then(() => { - // When we queue 6 persistable commands - Network.post('MockCommand', {content: 'value1', persist: true}); - Network.post('MockCommand', {content: 'value2', persist: true}); - Network.post('MockCommand', {content: 'value3', persist: true}); - Network.post('MockCommand', {content: 'value4', persist: true}); - Network.post('MockCommand', {content: 'value5', persist: true}); - Network.post('MockCommand', {content: 'value6', persist: true}); - return waitForPromisesToResolve(); - }) - .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: false})) - .then(waitForPromisesToResolve) - .then(() => { - // Then expect only 8 calls to have been made total and for them to be made in the order that we made them despite requiring reauthentication - expect(xhr.mock.calls.length).toBe(8); - expect(xhr.mock.calls[0][1].content).toBe('value1'); - - // Our call to Authenticate will not have a "content" field - expect(xhr.mock.calls[1][1].content).not.toBeDefined(); - - // Rest of the calls have the expected params and are called in sequence - expect(xhr.mock.calls[2][1].content).toBe('value1'); - expect(xhr.mock.calls[3][1].content).toBe('value2'); - expect(xhr.mock.calls[4][1].content).toBe('value3'); - expect(xhr.mock.calls[5][1].content).toBe('value4'); - expect(xhr.mock.calls[6][1].content).toBe('value5'); - expect(xhr.mock.calls[7][1].content).toBe('value6'); - }); -}); - -test('Sequential queue will succeed if triggered while reauthentication via main queue is in progress', () => { - // Given offline state where all requests will eventualy succeed without issue and assumed to be valid credentials - const xhr = jest.spyOn(HttpUtils, 'xhr') - .mockResolvedValueOnce({jsonCode: CONST.JSON_CODE.NOT_AUTHENTICATED}) - .mockResolvedValueOnce({jsonCode: CONST.JSON_CODE.NOT_AUTHENTICATED}) - .mockResolvedValue({jsonCode: CONST.JSON_CODE.SUCCESS, authToken: 'newToken'}); - - return Onyx.multiSet({ - [ONYXKEYS.SESSION]: {authToken: 'oldToken'}, - [ONYXKEYS.NETWORK]: {isOffline: false}, - [ONYXKEYS.CREDENTIALS]: {autoGeneratedLogin: 'test_user', autoGeneratedPassword: 'psswd'}, - }) - .then(() => { - // When we queue both non-persistable and persistable commands that will trigger reauthentication and go offline at the same time - Network.post('Push_Authenticate', {content: 'value1'}); - Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); - expect(NetworkStore.isOffline()).toBe(false); - expect(NetworkStore.isAuthenticating()).toBe(false); - return waitForPromisesToResolve(); - }) - .then(() => { - Network.post('MockCommand', {persist: true}); - expect(PersistedRequests.getAll().length).toBe(1); - expect(NetworkStore.isOffline()).toBe(true); - expect(SequentialQueue.isRunning()).toBe(false); - expect(NetworkStore.isAuthenticating()).toBe(false); - - // We should only have a single call at this point as the main queue is stopped since we've gone offline - expect(xhr.mock.calls.length).toBe(1); - - // Come back from offline to trigger the sequential queue flush - return Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); - }) - .then(() => { - // When we wait for the sequential queue to finish - expect(SequentialQueue.isRunning()).toBe(true); - return waitForPromisesToResolve(); - }) - .then(() => { - // Then we should expect to see that... - // The sequential queue has stopped - expect(SequentialQueue.isRunning()).toBe(false); - - // All persisted requests have run - expect(PersistedRequests.getAll().length).toBe(0); - - // We are not offline anymore - expect(NetworkStore.isOffline()).toBe(false); - - // First call to xhr is the Push_Authenticate request that could not call Authenticate because we went offline - const [firstCommand] = xhr.mock.calls[0]; - expect(firstCommand).toBe('Push_Authenticate'); - - // Second call to xhr is the MockCommand that also failed with a 407 - const [secondCommand] = xhr.mock.calls[1]; - expect(secondCommand).toBe('MockCommand'); - - // Third command should be the call to Authenticate - const [thirdCommand] = xhr.mock.calls[2]; - expect(thirdCommand).toBe('Authenticate'); - - const [fourthCommand] = xhr.mock.calls[3]; - expect(fourthCommand).toBe('MockCommand'); - - // We are using the new authToken - expect(NetworkStore.getAuthToken()).toBe('newToken'); - - // We are no longer authenticating - expect(NetworkStore.isAuthenticating()).toBe(false); - }); -}); - -test('Sequential queue will not run until credentials are read', () => { - const xhr = jest.spyOn(HttpUtils, 'xhr'); - const processWithMiddleware = jest.spyOn(Request, 'processWithMiddleware'); - - // Given a simulated a condition where the credentials have not yet been read from storage and we are offline - return Onyx.multiSet({ - [ONYXKEYS.NETWORK]: {isOffline: true}, - [ONYXKEYS.CREDENTIALS]: null, - [ONYXKEYS.SESSION]: null, - }) - .then(() => { - expect(NetworkStore.isOffline()).toBe(true); - - NetworkStore.resetHasReadRequiredDataFromStorage(); - - // And queue a request while offline - Network.post('MockCommand', {content: 'value1', persist: true}); - - // Then we should expect the request to get persisted - expect(PersistedRequests.getAll().length).toBe(1); - - // When we go online and wait for promises to resolve - return Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}); - }) - .then(() => { - expect(processWithMiddleware).toHaveBeenCalled(); - - // Then we should not expect XHR to run - expect(xhr).not.toHaveBeenCalled(); - - // When we set our credentials and authToken - return Onyx.multiSet({ - [ONYXKEYS.CREDENTIALS]: {autoGeneratedLogin: 'test_user', autoGeneratedPassword: 'psswd'}, - [ONYXKEYS.SESSION]: {authToken: 'oldToken'}, - }); - }) - .then(waitForPromisesToResolve) - .then(() => { - // Then we should expect XHR to run - expect(xhr).toHaveBeenCalled(); - }); -}); - -test('persistable request will move directly to the SequentialQueue when we are online and block non-persistable requests', () => { - const xhr = jest.spyOn(HttpUtils, 'xhr'); - return Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}) - .then(() => { - // GIVEN that we are online - expect(NetworkStore.isOffline()).toBe(false); - - // WHEN we make a request that should be retried, one that should not, and another that should - Network.post('MockCommandOne', {persist: true}); - Network.post('MockCommandTwo'); - Network.post('MockCommandThree', {persist: true}); - - // THEN the retryable requests should immediately be added to the persisted requests - expect(PersistedRequests.getAll().length).toBe(2); - - // WHEN we wait for the queue to run and finish processing - return waitForPromisesToResolve(); - }) - .then(() => { - // THEN the queue should be stopped and there should be no more requests to run - expect(SequentialQueue.isRunning()).toBe(false); - expect(PersistedRequests.getAll().length).toBe(0); - - // And our persistable request should run before our non persistable one in a blocking way - const firstRequest = xhr.mock.calls[0]; - const [firstRequestCommandName] = firstRequest; - expect(firstRequestCommandName).toBe('MockCommandOne'); - - const secondRequest = xhr.mock.calls[1]; - const [secondRequestCommandName] = secondRequest; - expect(secondRequestCommandName).toBe('MockCommandThree'); - - // WHEN we advance the main queue timer and wait for promises - jest.advanceTimersByTime(CONST.NETWORK.PROCESS_REQUEST_DELAY_MS); - return waitForPromisesToResolve(); - }) - .then(() => { - // THEN we should see that our third (non-persistable) request has run last - const thirdRequest = xhr.mock.calls[2]; - const [thirdRequestCommandName] = thirdRequest; - expect(thirdRequestCommandName).toBe('MockCommandTwo'); - }); -}); - -test('cancelled requests should not be retried', () => { - const xhr = jest.spyOn(HttpUtils, 'xhr'); - - // GIVEN a mock that will return a "cancelled" request error - global.fetch = jest.fn() - .mockRejectedValue(new DOMException('Aborted', CONST.ERROR.REQUEST_CANCELLED)); - - return Onyx.set(ONYXKEYS.NETWORK, {isOffline: false}) - .then(() => { - // WHEN we make a few requests and then cancel them - Network.post('MockCommandOne'); - Network.post('MockCommandTwo'); - Network.post('MockCommandThree'); - - // WHEN we wait for the requests to all cancel - return waitForPromisesToResolve(); - }) - .then(() => { - // THEN expect our queue to be empty and for no requests to have been retried - expect(MainQueue.getAll().length).toBe(0); - expect(xhr.mock.calls.length).toBe(3); - }); -}); From f7bc3d2d742f5e64bd068e5c7fec2232af398392 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Tue, 24 Jan 2023 15:21:59 -0800 Subject: [PATCH 014/171] Fix re-auth test by resolving failed requests --- src/libs/Middleware/Reauthentication.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/libs/Middleware/Reauthentication.js b/src/libs/Middleware/Reauthentication.js index 822598ec05d8..f93d6e411be0 100644 --- a/src/libs/Middleware/Reauthentication.js +++ b/src/libs/Middleware/Reauthentication.js @@ -111,6 +111,15 @@ function Reauthentication(response, request, isFromSequentialQueue) { // Return response data so we can chain the response with the following middlewares. return data; + }) + .catch(() => { + // Only deprecated api requests have a resolve function, so do nothing if this is not a deprecated api request + if (!request.resolve) { + return; + } + + // If we have caught a networking error from a deprecated api request, resolve it as unable to retry, otherwise the request will never resolve or reject. + request.resolve({jsonCode: CONST.JSON_CODE.UNABLE_TO_RETRY}); }); } From e26ee5244813690c83f7f463a1a47e693a1b7cb3 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Wed, 25 Jan 2023 17:44:09 -0800 Subject: [PATCH 015/171] Rethrow errors to the sequential queue for retry --- src/libs/Middleware/Reauthentication.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/Middleware/Reauthentication.js b/src/libs/Middleware/Reauthentication.js index f93d6e411be0..dceb7b2a885f 100644 --- a/src/libs/Middleware/Reauthentication.js +++ b/src/libs/Middleware/Reauthentication.js @@ -112,10 +112,10 @@ function Reauthentication(response, request, isFromSequentialQueue) { // Return response data so we can chain the response with the following middlewares. return data; }) - .catch(() => { - // Only deprecated api requests have a resolve function, so do nothing if this is not a deprecated api request - if (!request.resolve) { - return; + .catch((error) => { + // If the request is on the sequential queue, re-throw the error so we can decide to retry or not + if (isFromSequentialQueue) { + throw error; } // If we have caught a networking error from a deprecated api request, resolve it as unable to retry, otherwise the request will never resolve or reject. From c0cea4f58dbd0c693409dfbd0c461e20574d9180 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Thu, 26 Jan 2023 11:28:54 -0800 Subject: [PATCH 016/171] Return promise from retrying with a back off --- src/libs/Network/SequentialQueue.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Network/SequentialQueue.js b/src/libs/Network/SequentialQueue.js index 22fec95e6210..846fe7297bd7 100644 --- a/src/libs/Network/SequentialQueue.js +++ b/src/libs/Network/SequentialQueue.js @@ -70,7 +70,7 @@ function process() { // If the request failed and we want to retry it: // - Sleep for a period of time // - Call process again. This will retry the same request since we have not removed it from the queue - requestThrottle.sleep().then(() => process()); + return requestThrottle.sleep().then(process); }); } From 81a82774ed0026155da2f6eb1253e3a8cefdb72f Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Thu, 26 Jan 2023 11:34:18 -0800 Subject: [PATCH 017/171] Wait for the back off timer to resolve retry --- tests/unit/NetworkTest.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/unit/NetworkTest.js b/tests/unit/NetworkTest.js index 0a02132af77e..617646365dc2 100644 --- a/tests/unit/NetworkTest.js +++ b/tests/unit/NetworkTest.js @@ -420,6 +420,11 @@ describe('NetworkTests', () => { expect.objectContaining({command: 'mock command', data: expect.objectContaining({param2: 'value2'})}), ]); + // We need to wait for the request throttle back off timer because the request won't we retried until then + jest.runOnlyPendingTimers(); + return waitForPromisesToResolve(); + }) + .then(() => { // Finally, after it succeeds the queue should be empty xhrCalls[2].resolve({jsonCode: CONST.JSON_CODE.SUCCESS}); return waitForPromisesToResolve(); From 90313987c4fb1a99b2e639ef89504e0961a807ef Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Thu, 26 Jan 2023 12:04:41 -0800 Subject: [PATCH 018/171] Advance timers past request throttle --- tests/unit/NetworkTest.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/NetworkTest.js b/tests/unit/NetworkTest.js index 617646365dc2..f90e16c13ce6 100644 --- a/tests/unit/NetworkTest.js +++ b/tests/unit/NetworkTest.js @@ -420,8 +420,8 @@ describe('NetworkTests', () => { expect.objectContaining({command: 'mock command', data: expect.objectContaining({param2: 'value2'})}), ]); - // We need to wait for the request throttle back off timer because the request won't we retried until then - jest.runOnlyPendingTimers(); + // We need to advance past the request throttle back off timer because the request won't be retried until then + jest.advanceTimersByTime(100); return waitForPromisesToResolve(); }) .then(() => { From 68eeba07ee4d162a9f7b305a4d36a961b41774df Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Thu, 26 Jan 2023 12:12:16 -0800 Subject: [PATCH 019/171] Use constants for retry wait times --- src/CONST.js | 3 +++ src/libs/RequestThrottle.js | 5 +++-- tests/unit/NetworkTest.js | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/CONST.js b/src/CONST.js index fe10ef4496f6..33c9dcbc9a05 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -419,6 +419,9 @@ const CONST = { POST: 'post', }, MAX_REQUEST_RETRIES: 10, + MIN_RETRY_WAIT_TIME: 10, + MAX_RANDOM_RETRY_WAIT_TIME: 100, + MAX_RETRY_WAIT_TIME: 10 * 1000, PROCESS_REQUEST_DELAY_MS: 1000, MAX_PENDING_TIME_MS: 10 * 1000, }, diff --git a/src/libs/RequestThrottle.js b/src/libs/RequestThrottle.js index 6364e915165f..c9ec23938032 100644 --- a/src/libs/RequestThrottle.js +++ b/src/libs/RequestThrottle.js @@ -1,4 +1,5 @@ import _ from 'underscore'; +import CONST from '../CONST'; export default class RequestThrottle { constructor() { @@ -14,9 +15,9 @@ export default class RequestThrottle { */ getRequestWaitTime() { if (this.waitTime) { - this.waitTime = Math.min(this.waitTime * 2, 10000); + this.waitTime = Math.min(this.waitTime * 2, CONST.NETWORK.MAX_RETRY_WAIT_TIME); } else { - this.waitTime = 10 + _.random(90); + this.waitTime = CONST.NETWORK.MIN_RETRY_WAIT_TIME + _.random(CONST.NETWORK.MAX_RANDOM_RETRY_WAIT_TIME - CONST.NETWORK.MIN_RETRY_WAIT_TIME); } return this.waitTime; } diff --git a/tests/unit/NetworkTest.js b/tests/unit/NetworkTest.js index f90e16c13ce6..b86f2aaf4664 100644 --- a/tests/unit/NetworkTest.js +++ b/tests/unit/NetworkTest.js @@ -421,7 +421,7 @@ describe('NetworkTests', () => { ]); // We need to advance past the request throttle back off timer because the request won't be retried until then - jest.advanceTimersByTime(100); + jest.advanceTimersByTime(CONST.NETWORK.MAX_RANDOM_RETRY_WAIT_TIME); return waitForPromisesToResolve(); }) .then(() => { From c2103c10d591e89001d6795bca7ae652884b3dc5 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Fri, 27 Jan 2023 09:17:34 -0800 Subject: [PATCH 020/171] WIP exponential back off test --- tests/unit/NetworkTest.js | 48 ++++++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/tests/unit/NetworkTest.js b/tests/unit/NetworkTest.js index b86f2aaf4664..8b8bb55bf49a 100644 --- a/tests/unit/NetworkTest.js +++ b/tests/unit/NetworkTest.js @@ -2,7 +2,7 @@ import _ from 'underscore'; import Onyx from 'react-native-onyx'; import { - beforeEach, jest, describe, test, expect, afterEach, + beforeEach, describe, test, expect, afterEach, } from '@jest/globals'; import * as DeprecatedAPI from '../../src/libs/deprecatedAPI'; import * as TestHelper from '../utils/TestHelper'; @@ -434,10 +434,27 @@ describe('NetworkTests', () => { }); }); - test(`persisted request should be retried up to ${CONST.NETWORK.MAX_REQUEST_RETRIES} times`, () => { - // We're setting up xhr handler that always rejects with a fetch error + test('persisted requests should be retried using exponential backoff', () => { + // We're setting up xhr handler that rejects with a fetch error 3 times and then succeeds const xhr = jest.spyOn(HttpUtils, 'xhr') - .mockRejectedValue(new Error(CONST.ERROR.FAILED_TO_FETCH)); + .mockRejectedValueOnce(new Error(CONST.ERROR.FAILED_TO_FETCH)) + .mockRejectedValueOnce(new Error(CONST.ERROR.FAILED_TO_FETCH)) + .mockRejectedValueOnce(new Error(CONST.ERROR.FAILED_TO_FETCH)) + .mockResolvedValueOnce({jsonCode: CONST.JSON_CODE.SUCCESS}); + + const initialRequestWaitTime = 50; + jest.mock('../../src/libs/RequestThrottle', () => { + jest.fn().mockImplementation(() => { + const RequestThrottle = jest.requireActual('../../src/libs/RequestThrottle'); + class MockedRequestThrottle extends RequestThrottle { + constructor() { + super(); + this.waitTime = initialRequestWaitTime; + } + } + return new MockedRequestThrottle(); + }); + }); // Given we have a request made while we're offline return Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}) @@ -451,13 +468,22 @@ describe('NetworkTests', () => { .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: false})) .then(waitForPromisesToResolve) .then(() => { - // The request should be retried a number of times - expect(xhr).toHaveBeenCalledTimes(CONST.NETWORK.MAX_REQUEST_RETRIES); - _.each(xhr.mock.calls, (args) => { - expect(args).toEqual( - expect.arrayContaining(['mock command', expect.objectContaining({param1: 'value1', persist: true})]), - ); - }); + // Then there has only been one request so far + expect(xhr).toHaveBeenCalledTimes(1); + + // And we still have 1 persisted request + expect(_.size(PersistedRequests.getAll())).toEqual(1); + expect(PersistedRequests.getAll()).toEqual([ + expect.objectContaining({command: 'mock command', data: expect.objectContaining({param1: 'value1'})}), + ]); + + // After the initial wait time + jest.advanceTimersByTime(initialRequestWaitTime); + return waitForPromisesToResolve(); + }) + .then(() => { + // Then we have made another request + expect(xhr).toHaveBeenCalledTimes(2); }); }); From 805c1c15e29871d2e8e4054383d05f8fbfa15eb9 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Fri, 27 Jan 2023 14:09:44 -0800 Subject: [PATCH 021/171] Clear the request throttle after vs when offline --- src/libs/Network/SequentialQueue.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Network/SequentialQueue.js b/src/libs/Network/SequentialQueue.js index 846fe7297bd7..ba26425d8b6c 100644 --- a/src/libs/Network/SequentialQueue.js +++ b/src/libs/Network/SequentialQueue.js @@ -44,7 +44,6 @@ function process() { // If we have no persisted requests or we are offline we don't want to make any requests so we return early if (_.isEmpty(persistedRequests) || NetworkStore.isOffline()) { isSequentialQueueRunning = false; - requestThrottle.clear(); return Promise.resolve(); } @@ -64,6 +63,7 @@ function process() { // If a request fails with a non-retryable error we just remove it from the queue and move on to the next request if (!_.contains(errorsToRetry, error.message)) { PersistedRequests.remove(currentRequest); + requestThrottle.clear(); return process(); } From 3876ed27ddf6957795352dc9a39158050251f935 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Fri, 27 Jan 2023 14:53:49 -0800 Subject: [PATCH 022/171] Mock the request throttle to set a wait time --- src/libs/__mocks__/RequestThrottle.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/libs/__mocks__/RequestThrottle.js diff --git a/src/libs/__mocks__/RequestThrottle.js b/src/libs/__mocks__/RequestThrottle.js new file mode 100644 index 000000000000..cd4f64613306 --- /dev/null +++ b/src/libs/__mocks__/RequestThrottle.js @@ -0,0 +1,22 @@ +const initialRequestWaitTime = 50; +const OriginalRequestThrottle = jest.requireActual('../RequestThrottle'); +class RequestThrottle extends OriginalRequestThrottle.default { + constructor() { + super(); + this.waitTime = initialRequestWaitTime; + this.isFirstRetry = true; + } + + getRequestWaitTime() { + // Use the mocked initialRequestWaitTime for the first retry instead of doubling + if (this.isFirstRetry) { + this.isFirstRetry = false; + return this.waitTime; + } + super.getRequestWaitTime(); + } +} +export default RequestThrottle; +export { + initialRequestWaitTime, +}; From cfb1335fbed4a9879870670888b9bae6f08fff51 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Fri, 27 Jan 2023 14:54:38 -0800 Subject: [PATCH 023/171] Use the request throttle mock --- tests/unit/NetworkTest.js | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/tests/unit/NetworkTest.js b/tests/unit/NetworkTest.js index 8b8bb55bf49a..c17bd4df045d 100644 --- a/tests/unit/NetworkTest.js +++ b/tests/unit/NetworkTest.js @@ -18,6 +18,9 @@ import Log from '../../src/libs/Log'; import * as SequentialQueue from '../../src/libs/Network/SequentialQueue'; import * as MainQueue from '../../src/libs/Network/MainQueue'; import * as Request from '../../src/libs/Request'; +import * as RequestThrottleMock from '../../src/libs/__mocks__/RequestThrottle'; + +jest.mock('../../src/libs/RequestThrottle'); jest.useFakeTimers(); @@ -442,20 +445,6 @@ describe('NetworkTests', () => { .mockRejectedValueOnce(new Error(CONST.ERROR.FAILED_TO_FETCH)) .mockResolvedValueOnce({jsonCode: CONST.JSON_CODE.SUCCESS}); - const initialRequestWaitTime = 50; - jest.mock('../../src/libs/RequestThrottle', () => { - jest.fn().mockImplementation(() => { - const RequestThrottle = jest.requireActual('../../src/libs/RequestThrottle'); - class MockedRequestThrottle extends RequestThrottle { - constructor() { - super(); - this.waitTime = initialRequestWaitTime; - } - } - return new MockedRequestThrottle(); - }); - }); - // Given we have a request made while we're offline return Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}) .then(() => { @@ -478,7 +467,7 @@ describe('NetworkTests', () => { ]); // After the initial wait time - jest.advanceTimersByTime(initialRequestWaitTime); + jest.advanceTimersByTime(RequestThrottleMock.initialRequestWaitTime); return waitForPromisesToResolve(); }) .then(() => { From 5a24733306c0fbde840ad8223b7f0b7154b2471f Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Fri, 27 Jan 2023 16:22:57 -0800 Subject: [PATCH 024/171] Complete the exponential back off test --- tests/unit/NetworkTest.js | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/tests/unit/NetworkTest.js b/tests/unit/NetworkTest.js index c17bd4df045d..5c84868ca20d 100644 --- a/tests/unit/NetworkTest.js +++ b/tests/unit/NetworkTest.js @@ -437,7 +437,7 @@ describe('NetworkTests', () => { }); }); - test('persisted requests should be retried using exponential backoff', () => { + test('persisted requests should be retried using exponential back off', () => { // We're setting up xhr handler that rejects with a fetch error 3 times and then succeeds const xhr = jest.spyOn(HttpUtils, 'xhr') .mockRejectedValueOnce(new Error(CONST.ERROR.FAILED_TO_FETCH)) @@ -471,8 +471,27 @@ describe('NetworkTests', () => { return waitForPromisesToResolve(); }) .then(() => { - // Then we have made another request + // Then we have retried the failing request expect(xhr).toHaveBeenCalledTimes(2); + + // Now we will double the wait time before the next retry + jest.advanceTimersByTime(RequestThrottleMock.initialRequestWaitTime * 2); + return waitForPromisesToResolve(); + }) + .then(() => { + // Then we have retried again + expect(xhr).toHaveBeenCalledTimes(3); + + // Now we double the wait time again + jest.advanceTimersByTime(RequestThrottleMock.initialRequestWaitTime * 2 * 2); + return waitForPromisesToResolve(); + }) + .then(() => { + // Then the request is retried again + expect(xhr).toHaveBeenCalledTimes(4); + + // The request succeeds so the queue is empty + expect(_.size(PersistedRequests.getAll())).toEqual(0); }); }); From 00927cd914bee59cb8426f852bdc68e142d957b8 Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Mon, 30 Jan 2023 10:14:39 -0800 Subject: [PATCH 025/171] Add skeleton for component --- src/pages/signin/SignInPageLayout/Footer.js | 33 +++++++++++++++++++++ src/pages/signin/SignInPageLayout/index.js | 6 +++- 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 src/pages/signin/SignInPageLayout/Footer.js diff --git a/src/pages/signin/SignInPageLayout/Footer.js b/src/pages/signin/SignInPageLayout/Footer.js new file mode 100644 index 000000000000..1ba307e6c056 --- /dev/null +++ b/src/pages/signin/SignInPageLayout/Footer.js @@ -0,0 +1,33 @@ +import {Pressable} from 'react-native'; +import React from 'react'; +import _ from 'underscore'; +import styles from '../../../styles/styles'; +import * as StyleUtils from '../../../styles/StyleUtils'; +import * as Link from '../../../libs/actions/Link'; +import SVGImage from '../../../components/SVGImage'; + +const backgroundStyle = StyleUtils.getLoginPagePromoStyle(); + +const Footer = () => ( + { + Link.openExternalLink(backgroundStyle.redirectUri); + }} + disabled={_.isEmpty(backgroundStyle.redirectUri)} + > + + +); + +Footer.displayName = 'Footer'; + +export default Footer; diff --git a/src/pages/signin/SignInPageLayout/index.js b/src/pages/signin/SignInPageLayout/index.js index c165b336bb3d..49fde2ee3229 100644 --- a/src/pages/signin/SignInPageLayout/index.js +++ b/src/pages/signin/SignInPageLayout/index.js @@ -2,6 +2,7 @@ import React from 'react'; import {View} from 'react-native'; import PropTypes from 'prop-types'; import SignInPageContent from './SignInPageContent'; +import Footer from './Footer'; import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions'; import styles from '../../../styles/styles'; import SignInPageGraphics from './SignInPageGraphics'; @@ -39,7 +40,10 @@ const SignInPageLayout = (props) => { {props.children} {!props.isSmallScreenWidth && ( - + <> + +