Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/ONYXKEYS.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ export default {
// Note: These are Persisted Requests - not all requests in the main queue as the key name might lead one to believe
PERSISTED_REQUESTS: 'networkRequestQueue',

// Onyx updates from a response, or success or failure data from a request.
QUEUED_ONYX_UPDATES: 'queuedOnyxUpdates',

// Stores current date
CURRENT_DATE: 'currentDate',

Expand Down
5 changes: 5 additions & 0 deletions src/libs/API.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as Middleware from './Middleware';
import * as SequentialQueue from './Network/SequentialQueue';
import pkg from '../../package.json';
import CONST from '../CONST';
import * as Pusher from './Pusher/pusher';

// Setup API middlewares. Each request made will pass through a series of middleware functions that will get called in sequence (each one passing the result of the previous to the next).
// Note: The ordering here is intentional as we want to Log, Recheck Connection, Reauthenticate, and Save the Response in Onyx. Errors thrown in one middleware will bubble to the next.
Expand Down Expand Up @@ -46,6 +47,10 @@ function write(command, apiCommandParameters = {}, onyxData = {}) {
...apiCommandParameters,
appversion: pkg.version,
apiRequestType: CONST.API_REQUEST_TYPE.WRITE,

// We send the pusherSocketID with all write requests so that the api can include it in push events to prevent Pusher from sending the events to the requesting client. The push event
// is sent back to the requesting client in the response data instead, which prevents a replay effect in the UI. See https://github.com/Expensify/App/issues/12775.
pusherSocketID: Pusher.getPusherSocketID(),
};

// Assemble all the request data we'll be storing in the queue
Expand Down
22 changes: 14 additions & 8 deletions src/libs/Middleware/SaveResponseInOnyx.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import Onyx from 'react-native-onyx';
import CONST from '../../CONST';
import * as QueuedOnyxUpdates from '../actions/QueuedOnyxUpdates';

/**
* @param {Promise} response
Expand All @@ -13,23 +15,27 @@ function SaveResponseInOnyx(response, request) {
return;
}

// 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fwiw it is easy for people to find linked issues by using git blame. usually there is no need to add a link to a github issue.

const updateHandler = request.data.apiRequestType === CONST.API_REQUEST_TYPE.WRITE ? QueuedOnyxUpdates.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 = responseData.onyxData
? Onyx.update(responseData.onyxData)
? updateHandler(responseData.onyxData)
: Promise.resolve();

onyxDataUpdatePromise.then(() => {
return onyxDataUpdatePromise.then(() => {
// Handle the request's success/failure data (client-side data)
if (responseData.jsonCode === 200 && request.successData) {
Onyx.update(request.successData);
} else if (responseData.jsonCode !== 200 && request.failureData) {
Onyx.update(request.failureData);
return updateHandler(request.successData);
}
});

return responseData;
if (responseData.jsonCode !== 200 && request.failureData) {
return updateHandler(request.failureData);
}
return Promise.resolve();
}).then(() => responseData);
});
}

Expand Down
4 changes: 3 additions & 1 deletion src/libs/Network/SequentialQueue.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as ActiveClientManager from '../ActiveClientManager';
import * as Request from '../Request';
import * as RequestThrottle from '../RequestThrottle';
import CONST from '../../CONST';
import * as QueuedOnyxUpdates from '../actions/QueuedOnyxUpdates';

let resolveIsReadyPromise;
let isReadyPromise = new Promise((resolve) => {
Expand Down Expand Up @@ -53,7 +54,7 @@ function process() {
}

function flush() {
if (isSequentialQueueRunning) {
if (isSequentialQueueRunning || _.isEmpty(PersistedRequests.getAll())) {
return;
}

Expand All @@ -80,6 +81,7 @@ function flush() {
isSequentialQueueRunning = false;
resolveIsReadyPromise();
currentRequest = null;
Onyx.update(QueuedOnyxUpdates.getQueuedUpdates()).then(QueuedOnyxUpdates.clear);
});
},
});
Expand Down
11 changes: 11 additions & 0 deletions src/libs/Pusher/pusher.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Onyx.connect({
});

let socket;
let pusherSocketID = '';
const socketEventCallbacks = [];
let customAuthorizer;

Expand Down Expand Up @@ -81,6 +82,7 @@ function init(args, params) {
});

socket.connection.bind('connected', () => {
pusherSocketID = socket.connection.socket_id;
callSocketEventCallbacks('connected');
resolve();
});
Expand Down Expand Up @@ -355,6 +357,7 @@ function disconnect() {

socket.disconnect();
socket = null;
pusherSocketID = '';
}

/**
Expand All @@ -371,6 +374,13 @@ function reconnect() {
socket.connect();
}

/**
* @returns {String}
*/
function getPusherSocketID() {
return pusherSocketID;
}

if (window) {
/**
* Pusher socket for debugging purposes
Expand All @@ -393,4 +403,5 @@ export {
registerSocketEventCallback,
registerCustomAuthorizer,
TYPE,
getPusherSocketID,
};
35 changes: 35 additions & 0 deletions src/libs/actions/QueuedOnyxUpdates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Onyx from 'react-native-onyx';
import ONYXKEYS from '../../ONYXKEYS';

// In this file we manage a queue of Onyx updates while the SequentialQueue is processing. There are functions to get the updates and clear the queue after saving the updates in Onyx.

let queuedOnyxUpdates = [];
Onyx.connect({
key: ONYXKEYS.QUEUED_ONYX_UPDATES,
callback: val => queuedOnyxUpdates = val || [],
});

/**
* @param {Array<Object>} updates Onyx updates to queue for later
* @returns {Promise}
*/
function queueOnyxUpdates(updates) {
return Onyx.merge(ONYXKEYS.QUEUED_ONYX_UPDATES, updates);
}

function clear() {
Onyx.set(ONYXKEYS.QUEUED_ONYX_UPDATES, null);
}

/**
* @returns {Array<Object>}
*/
function getQueuedUpdates() {
return queuedOnyxUpdates;
}

export {
queueOnyxUpdates,
clear,
getQueuedUpdates,
};