-
Notifications
You must be signed in to change notification settings - Fork 3.1k
/
Copy pathindex.js
226 lines (195 loc) · 9.11 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
import _ from 'underscore';
import lodashGet from 'lodash/get';
import HttpUtils from '../HttpUtils';
import * as ActiveClientManager from '../ActiveClientManager';
import CONST from '../../CONST';
import * as PersistedRequests from '../actions/PersistedRequests';
import RetryCounter from './RetryCounter';
import * as NetworkStore from './NetworkStore';
import * as NetworkEvents from './NetworkEvents';
import * as PersistedRequestsQueue from './PersistedRequestsQueue';
import processRequest from './processRequest';
// Queue for network requests so we don't lose actions done by the user while offline
let networkRequestQueue = [];
// Keep track of retries for any non-persisted requests
const mainQueueRetryCounter = new RetryCounter();
/**
* Checks to see if a request can be made.
*
* @param {Object} request
* @param {String} request.type
* @param {String} request.command
* @param {Object} [request.data]
* @param {Boolean} request.data.forceNetworkRequest
* @return {Boolean}
*/
function canMakeRequest(request) {
// We must attempt to read authToken and credentials from storage before allowing any requests to happen so that any requests that
// require authToken or trigger reauthentication will succeed.
if (!NetworkStore.hasReadRequiredDataFromStorage()) {
return false;
}
// Some requests are always made even when we are in the process of authenticating (typically because they require no authToken e.g. Log, GetAccountStatus)
// However, if we are in the process of authenticating we always want to queue requests until we are no longer authenticating.
return request.data.forceNetworkRequest === true || !NetworkStore.isAuthenticating();
}
/**
* @param {Object} queuedRequest
* @param {*} error
* @returns {Boolean} true if we were able to retry
*/
function retryFailedRequest(queuedRequest, error) {
// When the request did not reach its destination add it back the queue to be retried if we can
const shouldRetry = lodashGet(queuedRequest, 'data.shouldRetry');
if (!shouldRetry) {
return false;
}
const retryCount = mainQueueRetryCounter.incrementRetries(queuedRequest);
NetworkEvents.getLogger().info('A retryable request failed', false, {
retryCount,
command: queuedRequest.command,
error: error.message,
});
if (retryCount < CONST.NETWORK.MAX_REQUEST_RETRIES) {
networkRequestQueue.push(queuedRequest);
return true;
}
NetworkEvents.getLogger().info('Request was retried too many times with no success. No more retries left');
return false;
}
/**
* While we are offline any requests that can be persisted are removed from the main network request queue and moved to a separate map + saved to storage.
*/
function removeAllPersistableRequestsFromMainQueue() {
// We filter persisted requests from the normal queue so they can be processed separately
const [networkRequestQueueWithoutPersistedRequests, requestsToPersist] = _.partition(networkRequestQueue, (request) => {
const shouldRetry = lodashGet(request, 'data.shouldRetry');
const shouldPersist = lodashGet(request, 'data.persist');
return !shouldRetry || !shouldPersist;
});
networkRequestQueue = networkRequestQueueWithoutPersistedRequests;
if (!requestsToPersist.length) {
return;
}
// Remove any functions as they are not serializable and cannot be stored to disk
const requestsToPersistWithoutFunctions = _.map(requestsToPersist, request => _.omit(request, val => _.isFunction(val)));
PersistedRequests.save(requestsToPersistWithoutFunctions);
}
/**
* Process the networkRequestQueue by looping through the queue and attempting to make the requests
*/
function processNetworkRequestQueue() {
if (NetworkStore.getIsOffline()) {
if (!networkRequestQueue.length) {
return;
}
removeAllPersistableRequestsFromMainQueue();
return;
}
// When the queue length is empty an early return is performed since nothing needs to be processed
if (networkRequestQueue.length === 0) {
return;
}
// Some requests should be retried and will end up here if the following conditions are met:
// - we are in the process of authenticating and the request is retryable (most are)
// - the request does not have forceNetworkRequest === true (this will trigger it to process immediately)
// - the request does not have shouldRetry === false (specified when we do not want to retry, defaults to true)
const requestsToProcessOnNextRun = [];
_.each(networkRequestQueue, (queuedRequest) => {
// Check if we can make this request at all and if we can't see if we should save it for the next run or chuck it into the ether
if (!canMakeRequest(queuedRequest)) {
const shouldRetry = lodashGet(queuedRequest, 'data.shouldRetry');
if (shouldRetry) {
requestsToProcessOnNextRun.push(queuedRequest);
} else {
console.debug('Skipping request that should not be re-tried: ', {command: queuedRequest.command});
}
return;
}
processRequest(queuedRequest)
.then(response => NetworkEvents.onResponse(queuedRequest, response))
.catch((error) => {
// Cancelled requests are normal and can happen when a user logs out. No extra handling is needed here.
if (error.name === CONST.ERROR.REQUEST_CANCELLED) {
NetworkEvents.onError(queuedRequest, error);
return;
}
// Because we ran into an error we assume we might be offline and do a "connection" health test
NetworkEvents.triggerRecheckNeeded();
// Retry any request that returns a "Failed to fetch" error. 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.
if (error.message === CONST.ERROR.FAILED_TO_FETCH) {
if (retryFailedRequest(queuedRequest, error)) {
return;
}
// We were not able to retry so pass the error to the handler in API.js
NetworkEvents.onError(queuedRequest, error);
} else {
NetworkEvents.getLogger().alert(`${CONST.ERROR.ENSURE_BUGBOT} unknown error caught while processing request`, {
command: queuedRequest.command,
error: error.message,
});
}
});
});
// We clear the request queue at the end by setting the queue to requestsToProcessOnNextRun which will either have some
// requests we want to retry or an empty array
networkRequestQueue = requestsToProcessOnNextRun;
}
// We must wait until the ActiveClientManager is ready so that we ensure only the "leader" tab processes any persisted requests
ActiveClientManager.isReady().then(() => {
PersistedRequestsQueue.flush();
// Start main queue and process once every n ms delay
setInterval(processNetworkRequestQueue, CONST.NETWORK.PROCESS_REQUEST_DELAY_MS);
});
/**
* Perform a queued post request
*
* @param {String} command
* @param {*} [data]
* @param {String} [type]
* @param {Boolean} shouldUseSecure - Whether we should use the secure API
* @returns {Promise}
*/
function post(command, data = {}, type = CONST.NETWORK.METHOD.POST, shouldUseSecure = false) {
return new Promise((resolve, reject) => {
const request = {
command,
data,
type,
resolve,
reject,
shouldUseSecure,
};
// By default, request are retry-able and cancellable
// (e.g. any requests currently happening when the user logs out are cancelled)
request.data = {
...data,
shouldRetry: lodashGet(data, 'shouldRetry', true),
canCancel: lodashGet(data, 'canCancel', true),
};
// Add the request to a queue of actions to perform
networkRequestQueue.push(request);
// This check is mainly used to prevent API commands from triggering calls to processNetworkRequestQueue from inside the context of a previous
// call to processNetworkRequestQueue() e.g. calling a Log command without this would cause the requests in networkRequestQueue to double process
// since we call Log inside processNetworkRequestQueue().
const shouldProcessImmediately = lodashGet(request, 'data.shouldProcessImmediately', true);
if (!shouldProcessImmediately) {
return;
}
// Try to fire off the request as soon as it's queued so we don't add a delay to every queued command
processNetworkRequestQueue();
});
}
/**
* Clear the queue and cancels all pending requests
* Non-cancellable requests like Log would not be cleared
*/
function clearRequestQueue() {
networkRequestQueue = _.filter(networkRequestQueue, request => !request.data.canCancel);
HttpUtils.cancelPendingRequests();
}
export {
post,
clearRequestQueue,
};