Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apply Onyx updates in an ordered fashion #25455

Merged
merged 85 commits into from
Sep 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
89c2ff6
Add skeleton application methods
tgolen Aug 18, 2023
424f9e9
Build update application methods
tgolen Aug 18, 2023
59fd99f
Use a promise to know when updates are done
tgolen Aug 18, 2023
fd0ab65
Merge branch 'main' into tgolen-ordered-updates
tgolen Aug 18, 2023
ea1ebe3
Remove debug
tgolen Aug 18, 2023
5f19b31
Merge branch 'main' into tgolen-ordered-updates
tgolen Aug 19, 2023
b9ecfc7
Refactor the location of code
tgolen Aug 19, 2023
842ec64
Start the update manager from authscreens
tgolen Aug 19, 2023
ba59617
Improve logging
tgolen Aug 19, 2023
427043e
Rename method
tgolen Aug 19, 2023
ebe24bf
Rename file and add promises to pusher events
tgolen Aug 19, 2023
ff9a773
Improve logs
tgolen Aug 19, 2023
528062b
Fix the updating and improve comments
tgolen Aug 19, 2023
3cfa38f
Merge branch 'main' into tgolen-ordered-updates
danieldoglas Aug 23, 2023
7b42169
added ANY to format on updates since that's still undefined
danieldoglas Aug 23, 2023
3002c64
adding early return in case of responses without onyxData
danieldoglas Aug 23, 2023
c4d4094
updating save function to not return early if missing lastUpdateID
danieldoglas Aug 23, 2023
fe50fd0
adding check on reconnect app after switching to updates beta to not …
danieldoglas Aug 23, 2023
a5e8127
adding reconnect app to the list of methods we shouldn't check
danieldoglas Aug 23, 2023
ba47492
adding check to prevent us from running both ReconnectApp and GetOnyx…
danieldoglas Aug 23, 2023
dde0814
prettier
danieldoglas Aug 23, 2023
321041a
adding fields that were missing in the request object
danieldoglas Aug 24, 2023
85bd8ba
adding response object
danieldoglas Aug 24, 2023
659de58
adding OnyxUpdatesFromServer object
danieldoglas Aug 24, 2023
85b4ef0
renaming and changing object in onyxKeys
danieldoglas Aug 24, 2023
42555c1
renaming property to updates
danieldoglas Aug 24, 2023
4d02eb5
renaming multipleEvents to updates
danieldoglas Aug 24, 2023
c5d7264
prettier
danieldoglas Aug 29, 2023
473d5df
renaming property
danieldoglas Aug 29, 2023
001eee9
Merge branch 'main' into tgolen-ordered-updates
danieldoglas Aug 29, 2023
6a51999
lint errors
danieldoglas Aug 29, 2023
72629eb
reverting file to main
danieldoglas Aug 29, 2023
c2f3097
add onyxupdatemanager on tests
danieldoglas Aug 30, 2023
57874c7
adding additional checks for early return
danieldoglas Aug 30, 2023
4cf1934
adding onyx update manager to Network tests
danieldoglas Aug 30, 2023
64e006c
adding space for style
danieldoglas Aug 30, 2023
3ed8dce
adding new method to return promise on reconnectApp
danieldoglas Sep 1, 2023
d5cc741
adding usage to new method to reconnect app
danieldoglas Sep 1, 2023
c613170
refactor the code a little bit
danieldoglas Sep 1, 2023
377cb93
adding a few more checks and refactoring
danieldoglas Sep 1, 2023
df82f2e
adding check if queue is paused before moving on with applying queued…
danieldoglas Sep 1, 2023
181c54a
prettier
danieldoglas Sep 1, 2023
97ef06e
prettier, some refactoring and adding the unpause in the right place
danieldoglas Sep 1, 2023
03da972
using the promise instead of passing variable down
danieldoglas Sep 1, 2023
d5fd7ed
linting files and adjusting checks
danieldoglas Sep 1, 2023
b8abf43
adding the right check in lastUpdateAppliedToClient
danieldoglas Sep 1, 2023
92bd9cb
Merge branch 'main' into tgolen-ordered-updates
danieldoglas Sep 1, 2023
ee3cddf
refactoring in progress
danieldoglas Sep 5, 2023
db642eb
adding clarifying comment
danieldoglas Sep 5, 2023
bd72f2a
adding syncronous processing in case there's no need to fetch updates…
danieldoglas Sep 5, 2023
b758fd4
fixing parameters
danieldoglas Sep 5, 2023
fb9e483
removing unecessary check for APIs
danieldoglas Sep 5, 2023
06d7250
renaming redundant name
danieldoglas Sep 5, 2023
a516500
adding check on network if we should pause after a request is done
danieldoglas Sep 5, 2023
440eaf9
adding new property on middleware response so we can block the queue …
danieldoglas Sep 5, 2023
204cfd1
working on onyx updates queue so we don't lose updates when flushing
danieldoglas Sep 5, 2023
798dde2
a little bit of refactor
danieldoglas Sep 5, 2023
4de4fb7
more refactor
danieldoglas Sep 5, 2023
b3da51b
fixing User.js calls
danieldoglas Sep 5, 2023
812253b
Merge branch 'main' into tgolen-ordered-updates
danieldoglas Sep 5, 2023
a07073b
adding property
danieldoglas Sep 5, 2023
48934f8
returning responseData on applyHTTPSOnyxUpdates
danieldoglas Sep 5, 2023
d30a4eb
adding comment for clarity
danieldoglas Sep 5, 2023
ec719bf
clearing unused method
danieldoglas Sep 5, 2023
7aab96e
prettier, fixing const reference
danieldoglas Sep 5, 2023
9ede8ad
fixing import to the right path
danieldoglas Sep 5, 2023
8a73c0a
adding back a method that is used
danieldoglas Sep 5, 2023
a8697ee
adding back the flush call when restarting the queue
danieldoglas Sep 5, 2023
2cda55b
adding sequentialqueue.unpause on promise cal
danieldoglas Sep 5, 2023
b333592
prettier and adding a check to ignore updates if they are older than …
danieldoglas Sep 5, 2023
45d3a6d
addressing comments, fixing lint issues and prettier
danieldoglas Sep 5, 2023
7e0e60a
lint
danieldoglas Sep 5, 2023
1d91a4d
fixing order hopefully for the last time for linter
danieldoglas Sep 5, 2023
7ab2d52
fixing resolve response
danieldoglas Sep 6, 2023
30d8eaf
fixing check to apply updates in case lastUpdateID is 0
danieldoglas Sep 6, 2023
05938e2
removing comment about the circular reference
danieldoglas Sep 8, 2023
ead5186
updating signature for ts mapping
danieldoglas Sep 8, 2023
0b8a8cc
renaming responseData to response
danieldoglas Sep 8, 2023
ec61746
changing from reduce to map
danieldoglas Sep 8, 2023
c5a8eb5
linter and fixing signature again
danieldoglas Sep 8, 2023
20fead9
fixing lint
danieldoglas Sep 8, 2023
250aa17
changing object signature to simplify it
danieldoglas Sep 8, 2023
e07c47c
removing data reference
danieldoglas Sep 8, 2023
9b70014
prettier
danieldoglas Sep 9, 2023
d012c54
adding previousUpdateID
danieldoglas Sep 10, 2023
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
2 changes: 2 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {CurrentReportIDContextProvider} from './components/withCurrentReportID';
import {EnvironmentProvider} from './components/withEnvironment';
import * as Session from './libs/actions/Session';
import useDefaultDragAndDrop from './hooks/useDefaultDragAndDrop';
import OnyxUpdateManager from './libs/actions/OnyxUpdateManager';

// For easier debugging and development, when we are in web we expose Onyx to the window, so you can more easily set data into Onyx
if (window && Environment.isDevelopment()) {
Expand All @@ -42,6 +43,7 @@ const fill = {flex: 1};

function App() {
useDefaultDragAndDrop();
OnyxUpdateManager();
return (
<GestureHandlerRootView style={fill}>
<ComposeProviders
Expand Down
4 changes: 4 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2629,6 +2629,10 @@ const CONST = {
DEFAULT_COORDINATE: [-122.4021, 37.7911],
STYLE_URL: 'mapbox://styles/expensify/cllcoiqds00cs01r80kp34tmq',
},
ONYX_UPDATE_TYPES: {
HTTPS: 'https',
PUSHER: 'pusher',
},
} as const;

export default CONST;
2 changes: 1 addition & 1 deletion src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ type OnyxValues = {
[ONYXKEYS.SELECTED_TAB]: string;
[ONYXKEYS.RECEIPT_MODAL]: OnyxTypes.ReceiptModal;
[ONYXKEYS.MAPBOX_ACCESS_TOKEN]: OnyxTypes.MapboxAccessToken;
[ONYXKEYS.ONYX_UPDATES_FROM_SERVER]: number;
[ONYXKEYS.ONYX_UPDATES_FROM_SERVER]: OnyxTypes.OnyxUpdatesFromServer;
[ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT]: number;
[ONYXKEYS.MAX_CANVAS_AREA]: number;
[ONYXKEYS.MAX_CANVAS_HEIGHT]: number;
Expand Down
3 changes: 2 additions & 1 deletion src/libs/API.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ 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);

// SaveResponseInOnyx - Merges either the successData or failureData into Onyx depending on if the call was successful or not
// SaveResponseInOnyx - Merges either the successData or failureData into Onyx depending on if the call was successful or not. This needs to be the LAST middleware we use, don't add any
// middlewares after this, because the SequentialQueue depends on the result of this middleware to pause the queue (if needed) to bring the app to an up-to-date state.
Request.use(Middleware.SaveResponseInOnyx);

/**
Expand Down
70 changes: 32 additions & 38 deletions src/libs/Middleware/SaveResponseInOnyx.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,32 @@
import Onyx from 'react-native-onyx';
import _ from 'underscore';
import CONST from '../../CONST';
import ONYXKEYS from '../../ONYXKEYS';
import * as QueuedOnyxUpdates from '../actions/QueuedOnyxUpdates';
import * as MemoryOnlyKeys from '../actions/MemoryOnlyKeys/MemoryOnlyKeys';
import * as OnyxUpdates from '../actions/OnyxUpdates';

// If we're executing any of these requests, we don't need to trigger our OnyxUpdates flow to update the current data even if our current value is out of
// date because all these requests are updating the app to the most current state.
const requestsToIgnoreLastUpdateID = ['OpenApp', 'ReconnectApp', 'GetMissingOnyxMessages'];
Comment on lines +7 to +8
Copy link
Contributor

Choose a reason for hiding this comment

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

even if our current value is out of date

Just curious how can this happen if the requests are fetching the current state. Doesn't happen at all?

Copy link
Contributor

Choose a reason for hiding this comment

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

We talked personally and I explained this!


/**
* @param {Promise} response
* @param {Promise} requestResponse
* @param {Object} request
* @returns {Promise}
*/
function SaveResponseInOnyx(response, request) {
return response.then((responseData) => {
// Make sure we have response data (i.e. response isn't a promise being passed down to us by a failed retry request and responseData undefined)
if (!responseData) {
function SaveResponseInOnyx(requestResponse, request) {
return requestResponse.then((response) => {
// Make sure we have response data (i.e. response isn't a promise being passed down to us by a failed retry request and response undefined)
if (!response) {
return;
}
const onyxUpdates = response.onyxData;

// The data for this response comes in two different formats:
// 1. Original format - this is what was sent before the RELIABLE_UPDATES project and will go away once RELIABLE_UPDATES is fully complete
// - The data is an array of objects, where each object is an onyx update
// Example: [{onyxMethod: 'whatever', key: 'foo', value: 'bar'}]
// 1. Reliable updates format - this is what was sent with the RELIABLE_UPDATES project and will be the format from now on
// - The data is an object, containing updateIDs from the server and an array of onyx updates (this array is the same format as the original format above)
// Example: {lastUpdateID: 1, previousUpdateID: 0, onyxData: [{onyxMethod: 'whatever', key: 'foo', value: 'bar'}]}
// NOTE: This is slightly different than the format of the pusher event data, where pusher has "updates" and HTTPS responses have "onyxData" (long story)
// Sometimes we call requests that are successfull but they don't have any response or any success/failure data to set. Let's return early since
// we don't need to store anything here.
if (!onyxUpdates && !request.successData && !request.failureData) {
return Promise.resolve(response);
}

// Supports both the old format and the new format
const onyxUpdates = _.isArray(responseData) ? responseData : responseData.onyxData;
// If there is an OnyxUpdate for using memory only keys, enable them
_.find(onyxUpdates, ({key, value}) => {
if (key !== ONYXKEYS.IS_USING_MEMORY_ONLY_KEYS || !value) {
Expand All @@ -39,30 +37,26 @@ function SaveResponseInOnyx(response, request) {
return true;
});

// Save the update IDs to Onyx so they can be used to fetch incremental updates if the client gets out of sync from the server
OnyxUpdates.saveUpdateIDs(Number(responseData.lastUpdateID || 0), Number(responseData.previousUpdateID || 0));
const responseToApply = {
type: CONST.ONYX_UPDATE_TYPES.HTTPS,
lastUpdateID: Number(response.lastUpdateID || 0),
previousUpdateID: Number(response.previousUpdateID || 0),
request,
response,
};

// 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 = request.data.apiRequestType === CONST.API_REQUEST_TYPE.WRITE ? QueuedOnyxUpdates.queueOnyxUpdates : Onyx.update;
if (_.includes(requestsToIgnoreLastUpdateID, request.command) || !OnyxUpdates.doesClientNeedToBeUpdated(Number(response.previousUpdateID || 0))) {
return OnyxUpdates.apply(responseToApply);
}

// 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 ? updateHandler(responseData.onyxData) : Promise.resolve();
// Save the update IDs to Onyx so they can be used to fetch incremental updates if the client gets out of sync from the server
OnyxUpdates.saveUpdateInformation(responseToApply);

return onyxDataUpdatePromise
.then(() => {
// Handle the request's success/failure data (client-side data)
if (responseData.jsonCode === 200 && request.successData) {
return updateHandler(request.successData);
}
if (responseData.jsonCode !== 200 && request.failureData) {
return updateHandler(request.failureData);
}
return Promise.resolve();
})
.then(() => responseData);
// Ensure the queue is paused while the client resolves the gap in onyx updates so that updates are guaranteed to happen in a specific order.
return Promise.resolve({
...response,
shouldPauseQueue: true,
});
});
}

Expand Down
74 changes: 46 additions & 28 deletions src/libs/Network/SequentialQueue.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,30 @@ let isSequentialQueueRunning = false;
let currentRequest = null;
let isQueuePaused = false;

/**
* Puts the queue into a paused state so that no requests will be processed
*/
function pause() {
if (isQueuePaused) {
return;
}

console.debug('[SequentialQueue] Pausing the queue');
isQueuePaused = true;
}

/**
* Gets the current Onyx queued updates, apply them and clear the queue if the queue is not paused.
*/
function flushOnyxUpdatesQueue() {
// The only situation where the queue is paused is if we found a gap between the app current data state and our server's. If that happens,
// we'll trigger async calls to make the client updated again. While we do that, we don't want to insert anything in Onyx.
if (isQueuePaused) {
return;
}
QueuedOnyxUpdates.flushQueue();
}

/**
* Process any persisted requests, when online, one at a time until the queue is empty.
*
Expand All @@ -44,7 +68,12 @@ function process() {

// Set the current request to a promise awaiting its processing so that getCurrentRequest can be used to take some action after the current request has processed.
currentRequest = Request.processWithMiddleware(requestToProcess, true)
.then(() => {
.then((response) => {
// A response might indicate that the queue should be paused. This happens when a gap in onyx updates is detected between the client and the server and
// that gap needs resolved before the queue can continue.
if (response.shouldPauseQueue) {
pause();
}
PersistedRequests.remove(requestToProcess);
RequestThrottle.clear();
return process();
Expand Down Expand Up @@ -94,12 +123,27 @@ function flush() {
isSequentialQueueRunning = false;
resolveIsReadyPromise();
currentRequest = null;
Onyx.update(QueuedOnyxUpdates.getQueuedUpdates()).then(QueuedOnyxUpdates.clear);
flushOnyxUpdatesQueue();
});
},
});
}

/**
* Unpauses the queue and flushes all the requests that were in it or were added to it while paused
*/
function unpause() {
if (!isQueuePaused) {
return;
}

const numberOfPersistedRequests = PersistedRequests.getAll().length || 0;
console.debug(`[SequentialQueue] Unpausing the queue and flushing ${numberOfPersistedRequests} requests`);
isQueuePaused = false;
flushOnyxUpdatesQueue();
flush();
}

/**
* @returns {Boolean}
*/
Expand Down Expand Up @@ -149,30 +193,4 @@ function waitForIdle() {
return isReadyPromise;
}

/**
* Puts the queue into a paused state so that no requests will be processed
*/
function pause() {
if (isQueuePaused) {
return;
}

console.debug('[SequentialQueue] Pausing the queue');
isQueuePaused = true;
}

/**
* Unpauses the queue and flushes all the requests that were in it or were added to it while paused
*/
function unpause() {
if (!isQueuePaused) {
return;
}

const numberOfPersistedRequests = PersistedRequests.getAll().length || 0;
console.debug(`[SequentialQueue] Unpausing the queue and flushing ${numberOfPersistedRequests} requests`);
isQueuePaused = false;
flush();
}

export {flush, getCurrentRequest, isRunning, push, waitForIdle, pause, unpause};
5 changes: 3 additions & 2 deletions src/libs/PusherUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ function subscribeToMultiEvent(eventType, callback) {
/**
* @param {String} eventType
* @param {Mixed} data
* @returns {Promise}
*/
function triggerMultiEventHandler(eventType, data) {
danieldoglas marked this conversation as resolved.
Show resolved Hide resolved
if (!multiEventCallbackMapping[eventType]) {
return;
return Promise.resolve();
}
multiEventCallbackMapping[eventType](data);
return multiEventCallbackMapping[eventType](data);
}

/**
Expand Down
74 changes: 31 additions & 43 deletions src/libs/actions/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import * as Session from './Session';
import * as ReportActionsUtils from '../ReportActionsUtils';
import Timing from './Timing';
import * as Browser from '../Browser';
import * as SequentialQueue from '../Network/SequentialQueue';

let currentUserAccountID;
let currentUserEmail;
Expand Down Expand Up @@ -208,6 +207,35 @@ function reconnectApp(updateIDFrom = 0) {
});
}

/**
* Fetches data when the app will call reconnectApp without params for the last time. This is a separate function
* because it will follow patterns that are not recommended so we can be sure we're not putting the app in a unusable
* state because of race conditions between reconnectApp and other pusher updates being applied at the same time.
* @return {Promise}
*/
function finalReconnectAppAfterActivatingReliableUpdates() {
console.debug(`[OnyxUpdates] Executing last reconnect app with promise`);
return getPolicyParamsForOpenOrReconnect().then((policyParams) => {
const params = {...policyParams};

// When the app reconnects we do a fast "sync" of the LHN and only return chats that have new messages. We achieve this by sending the most recent reportActionID.
// we have locally. And then only update the user about chats with messages that have occurred after that reportActionID.
//
// - Look through the local report actions and reports to find the most recently modified report action or report.
// - We send this to the server so that it can compute which new chats the user needs to see and return only those as an optimization.
Timing.start(CONST.TIMING.CALCULATE_MOST_RECENT_LAST_MODIFIED_ACTION);
params.mostRecentReportActionLastModified = ReportActionsUtils.getMostRecentReportActionLastModified();
Timing.end(CONST.TIMING.CALCULATE_MOST_RECENT_LAST_MODIFIED_ACTION, '', 500);

// It is SUPER BAD FORM to return promises from action methods.
// DO NOT FOLLOW THIS PATTERN!!!!!
// It was absolutely necessary in order to not break the app while migrating to the new reliable updates pattern. This method will be removed
// as soon as we have everyone migrated to the reliableUpdate beta.
// eslint-disable-next-line rulesdir/no-api-side-effects-method
return API.makeRequestWithSideEffects('ReconnectApp', params, getOnyxDataForOpenOrReconnect());
});
}

/**
* Fetches data when the client has discovered it missed some Onyx updates from the server
* @param {Number} [updateIDFrom] the ID of the Onyx update that we want to start fetching from
Expand All @@ -231,48 +259,6 @@ function getMissingOnyxUpdates(updateIDFrom = 0, updateIDTo = 0) {
);
}

// The next 40ish lines of code are used for detecting when there is a gap of OnyxUpdates between what was last applied to the client and the updates the server has.
// When a gap is detected, the missing updates are fetched from the API.

// These 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
let lastUpdateIDAppliedToClient = 0;
Onyx.connect({
key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT,
callback: (val) => (lastUpdateIDAppliedToClient = val),
});

Onyx.connect({
key: ONYXKEYS.ONYX_UPDATES_FROM_SERVER,
callback: (val) => {
if (!val) {
return;
}

const {lastUpdateIDFromServer, previousUpdateIDFromServer} = val;
console.debug('[OnyxUpdates] Received lastUpdateID from server', lastUpdateIDFromServer);
console.debug('[OnyxUpdates] Received previousUpdateID from server', previousUpdateIDFromServer);
console.debug('[OnyxUpdates] Last update ID applied to the client', lastUpdateIDAppliedToClient);

// If the previous update from the server does not match the last update the client got, then the client is missing some updates.
// getMissingOnyxUpdates will fetch updates starting from the last update this client got and going to the last update the server sent.
if (lastUpdateIDAppliedToClient && previousUpdateIDFromServer && lastUpdateIDAppliedToClient < previousUpdateIDFromServer) {
console.debug('[OnyxUpdates] Gap detected in update IDs so fetching incremental updates');
Log.info('Gap detected in update IDs from server so fetching incremental updates', true, {
lastUpdateIDFromServer,
previousUpdateIDFromServer,
lastUpdateIDAppliedToClient,
});
SequentialQueue.pause();
getMissingOnyxUpdates(lastUpdateIDAppliedToClient, lastUpdateIDFromServer).finally(SequentialQueue.unpause);
}

if (lastUpdateIDFromServer > lastUpdateIDAppliedToClient) {
// Update this value so that it matches what was just received from the server
Onyx.merge(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, lastUpdateIDFromServer || 0);
}
},
});

/**
* This promise is used so that deeplink component know when a transition is end.
* This is necessary because we want to begin deeplink redirection after the transition is end.
Expand Down Expand Up @@ -484,4 +470,6 @@ export {
beginDeepLinkRedirect,
beginDeepLinkRedirectAfterTransition,
createWorkspaceAndNavigateToIt,
getMissingOnyxUpdates,
finalReconnectAppAfterActivatingReliableUpdates,
};
Loading
Loading