-
Notifications
You must be signed in to change notification settings - Fork 3.1k
/
Copy pathOnyxUpdates.ts
203 lines (178 loc) · 10.3 KB
/
OnyxUpdates.ts
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
import type {OnyxUpdate} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import type {Merge} from 'type-fest';
import Log from '@libs/Log';
import Performance from '@libs/Performance';
import PusherUtils from '@libs/PusherUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {OnyxUpdateEvent, OnyxUpdatesFromServer, Request} from '@src/types/onyx';
import type Response from '@src/types/onyx/Response';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import {queueOnyxUpdates} from './QueuedOnyxUpdates';
// This key needs to be separate from ONYXKEYS.ONYX_UPDATES_FROM_SERVER so that it can be updated without triggering the callback when the server IDs are updated. If that
// callback were triggered it would lead to duplicate processing of server updates.
let lastUpdateIDAppliedToClient: number | undefined = 0;
Onyx.connect({
key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT,
callback: (val) => (lastUpdateIDAppliedToClient = val),
});
// This promise is used to ensure pusher events are always processed in the order they are received,
// even when such events are received over multiple separate pusher updates.
let pusherEventsPromise = Promise.resolve();
let airshipEventsPromise = Promise.resolve();
function applyHTTPSOnyxUpdates(request: Request, response: Response) {
Performance.markStart(CONST.TIMING.APPLY_HTTPS_UPDATES);
console.debug('[OnyxUpdateManager] Applying https update');
// For most requests we can immediately update Onyx. For write requests we queue the updates and apply them after the sequential queue has flushed to prevent a replay effect in
// the UI. See https://github.com/Expensify/App/issues/12775 for more info.
const updateHandler: (updates: OnyxUpdate[]) => Promise<unknown> = request?.data?.apiRequestType === CONST.API_REQUEST_TYPE.WRITE ? queueOnyxUpdates : Onyx.update;
// First apply any onyx data updates that are being sent back from the API. We wait for this to complete and then
// apply successData or failureData. This ensures that we do not update any pending, loading, or other UI states contained
// in successData/failureData until after the component has received and API data.
const onyxDataUpdatePromise = response.onyxData ? updateHandler(response.onyxData) : Promise.resolve();
return onyxDataUpdatePromise
.then(() => {
// Handle the request's success/failure data (client-side data)
if (response.jsonCode === 200 && request.successData) {
return updateHandler(request.successData);
}
if (response.jsonCode !== 200 && request.failureData) {
// 460 jsonCode in Expensify world means "admin required".
// Typically, this would only happen if a user attempts an API command that requires policy admin access when they aren't an admin.
// In this case, we don't want to apply failureData because it will likely result in a RedBrickRoad error on a policy field which is not accessible.
// Meaning that there's a red dot you can't dismiss.
if (response.jsonCode === 460) {
Log.info('[OnyxUpdateManager] Received 460 status code, not applying failure data');
return Promise.resolve();
}
return updateHandler(request.failureData);
}
return Promise.resolve();
})
.then(() => {
if (request.finallyData) {
return updateHandler(request.finallyData);
}
return Promise.resolve();
})
.then(() => {
Performance.markEnd(CONST.TIMING.APPLY_HTTPS_UPDATES);
console.debug('[OnyxUpdateManager] Done applying HTTPS update');
return Promise.resolve(response);
});
}
function applyPusherOnyxUpdates(updates: OnyxUpdateEvent[]) {
Performance.markStart(CONST.TIMING.APPLY_PUSHER_UPDATES);
pusherEventsPromise = pusherEventsPromise.then(() => {
console.debug('[OnyxUpdateManager] Applying pusher update');
});
pusherEventsPromise = updates
.reduce((promise, update) => promise.then(() => PusherUtils.triggerMultiEventHandler(update.eventType, update.data)), pusherEventsPromise)
.then(() => {
Performance.markEnd(CONST.TIMING.APPLY_PUSHER_UPDATES);
console.debug('[OnyxUpdateManager] Done applying Pusher update');
});
return pusherEventsPromise;
}
function applyAirshipOnyxUpdates(updates: OnyxUpdateEvent[]) {
Performance.markStart(CONST.TIMING.APPLY_AIRSHIP_UPDATES);
airshipEventsPromise = airshipEventsPromise.then(() => {
console.debug('[OnyxUpdateManager] Applying Airship updates');
});
airshipEventsPromise = updates
.reduce((promise, update) => promise.then(() => Onyx.update(update.data)), airshipEventsPromise)
.then(() => {
Performance.markEnd(CONST.TIMING.APPLY_AIRSHIP_UPDATES);
console.debug('[OnyxUpdateManager] Done applying Airship updates');
});
return airshipEventsPromise;
}
/**
* @param [updateParams.request] Exists if updateParams.type === 'https'
* @param [updateParams.response] Exists if updateParams.type === 'https'
* @param [updateParams.updates] Exists if updateParams.type === 'pusher'
*/
function apply({lastUpdateID, type, request, response, updates}: Merge<OnyxUpdatesFromServer, {updates: OnyxUpdateEvent[]; type: 'pusher'}>): Promise<void>;
function apply({lastUpdateID, type, request, response, updates}: Merge<OnyxUpdatesFromServer, {request: Request; response: Response; type: 'https'}>): Promise<Response>;
function apply({lastUpdateID, type, request, response, updates}: OnyxUpdatesFromServer): Promise<Response>;
function apply({lastUpdateID, type, request, response, updates}: OnyxUpdatesFromServer): Promise<void | Response> | undefined {
Log.info(`[OnyxUpdateManager] Applying update type: ${type} with lastUpdateID: ${lastUpdateID}`, false, {command: request?.command});
if (lastUpdateID && lastUpdateIDAppliedToClient && Number(lastUpdateID) <= lastUpdateIDAppliedToClient) {
Log.info('[OnyxUpdateManager] Update received was older than or the same as current state, returning without applying the updates other than successData and failureData', false, {
lastUpdateID,
lastUpdateIDAppliedToClient,
});
// In this case, we're already received the OnyxUpdate included in the response, so we don't need to apply it again.
// However, we do need to apply the successData and failureData from the request
if (
type === CONST.ONYX_UPDATE_TYPES.HTTPS &&
request &&
response &&
(!isEmptyObject(request.successData) || !isEmptyObject(request.failureData) || !isEmptyObject(request.finallyData))
) {
Log.info('[OnyxUpdateManager] Applying success or failure data from request without onyxData from response');
// We use a spread here instead of delete because we don't want to change the response for other middlewares
const {onyxData, ...responseWithoutOnyxData} = response;
return applyHTTPSOnyxUpdates(request, responseWithoutOnyxData);
}
return Promise.resolve();
}
if (lastUpdateID && (lastUpdateIDAppliedToClient === undefined || Number(lastUpdateID) > lastUpdateIDAppliedToClient)) {
Onyx.merge(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, Number(lastUpdateID));
}
if (type === CONST.ONYX_UPDATE_TYPES.HTTPS && request && response) {
return applyHTTPSOnyxUpdates(request, response);
}
if (type === CONST.ONYX_UPDATE_TYPES.PUSHER && updates) {
return applyPusherOnyxUpdates(updates);
}
if (type === CONST.ONYX_UPDATE_TYPES.AIRSHIP && updates) {
return applyAirshipOnyxUpdates(updates);
}
}
/**
* @param [updateParams.request] Exists if updateParams.type === 'https'
* @param [updateParams.response] Exists if updateParams.type === 'https'
* @param [updateParams.updates] Exists if updateParams.type === 'pusher'
*/
function saveUpdateInformation(updateParams: OnyxUpdatesFromServer) {
let modifiedUpdateParams = updateParams;
// We don't want to store the data in the updateParams if it's a HTTPS update since it is useless anyways
// and it causes serialization issues when storing in Onyx
if (updateParams.type === CONST.ONYX_UPDATE_TYPES.HTTPS && updateParams.request) {
modifiedUpdateParams = {...modifiedUpdateParams, request: {...updateParams.request, data: undefined}};
}
// Always use set() here so that the updateParams are never merged and always unique to the request that came in
Onyx.set(ONYXKEYS.ONYX_UPDATES_FROM_SERVER, modifiedUpdateParams);
}
type DoesClientNeedToBeUpdatedParams = {
clientLastUpdateID?: number;
previousUpdateID?: number;
};
/**
* This function will receive the previousUpdateID from any request/pusher update that has it, compare to our current app state
* and return if an update is needed
* @param previousUpdateID The previousUpdateID contained in the response object
* @param clientLastUpdateID an optional override for the lastUpdateIDAppliedToClient
*/
function doesClientNeedToBeUpdated({previousUpdateID, clientLastUpdateID}: DoesClientNeedToBeUpdatedParams): boolean {
// If no previousUpdateID is sent, this is not a WRITE request so we don't need to update our current state
if (!previousUpdateID) {
return false;
}
const lastUpdateIDFromClient = clientLastUpdateID ?? lastUpdateIDAppliedToClient;
// If we don't have any value in lastUpdateIDFromClient, this is the first time we're receiving anything, so we need to do a last reconnectApp
if (!lastUpdateIDFromClient) {
Log.info('We do not have lastUpdateIDFromClient, client needs updating');
return true;
}
if (lastUpdateIDFromClient < previousUpdateID) {
Log.info('lastUpdateIDFromClient is less than the previousUpdateID received, client needs updating', false, {lastUpdateIDFromClient, previousUpdateID});
return true;
}
return false;
}
// eslint-disable-next-line import/prefer-default-export
export {apply, doesClientNeedToBeUpdated, saveUpdateInformation, applyHTTPSOnyxUpdates as INTERNAL_DO_NOT_USE_applyHTTPSOnyxUpdates};
export type {DoesClientNeedToBeUpdatedParams as ManualOnyxUpdateCheckIds};