Skip to content

Add deferred updates queue functions to OnyxUpdateManager to manually apply updates (e.g. from push notifications) #42044

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

Merged
Show file tree
Hide file tree
Changes from 10 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
29 changes: 14 additions & 15 deletions src/libs/actions/OnyxUpdateManager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import Log from '@libs/Log';
import * as SequentialQueue from '@libs/Network/SequentialQueue';
import * as App from '@userActions/App';
import ONYXKEYS from '@src/ONYXKEYS';
import type {OnyxUpdatesFromServer, Response} from '@src/types/onyx';
import type {OnyxUpdatesFromServer} from '@src/types/onyx';
import {isValidOnyxUpdateFromServer} from '@src/types/onyx/OnyxUpdatesFromServer';
import * as OnyxUpdateManagerUtils from './utils';
import deferredUpdatesProxy from './utils/deferredUpdates';
import DeferredUpdates from './utils/DeferredUpdates';
import GetMissingOnyxUpdatesPromiseProxy from './utils/GetMissingOnyxUpdatesPromise';

// This file is in charge of looking at the updateIDs coming from the server and comparing them to the last updateID that the client has.
// If the client is behind the server, then we need to
Expand Down Expand Up @@ -39,8 +40,6 @@ Onyx.connect({
},
});

let queryPromise: Promise<Response | Response[] | void> | undefined;

let resolveQueryPromiseWrapper: () => void;
const createQueryPromiseWrapper = () =>
new Promise<void>((resolve) => {
Expand All @@ -50,8 +49,8 @@ const createQueryPromiseWrapper = () =>
let queryPromiseWrapper = createQueryPromiseWrapper();

const resetDeferralLogicVariables = () => {
queryPromise = undefined;
deferredUpdatesProxy.deferredUpdates = {};
GetMissingOnyxUpdatesPromiseProxy.GetMissingOnyxUpdatesPromise = undefined;
DeferredUpdates.deferredUpdates = {};
};

// This function will reset the query variables, unpause the SequentialQueue and log an info to the user.
Expand All @@ -61,9 +60,7 @@ function finalizeUpdatesAndResumeQueue() {
resolveQueryPromiseWrapper();
queryPromiseWrapper = createQueryPromiseWrapper();

resetDeferralLogicVariables();
Onyx.set(ONYXKEYS.ONYX_UPDATES_FROM_SERVER, null);
SequentialQueue.unpause();
DeferredUpdates.clearDeferredUpdates();
}

/**
Expand Down Expand Up @@ -111,22 +108,22 @@ function handleOnyxUpdateGap(onyxUpdatesFromServer: OnyxEntry<OnyxUpdatesFromSer
// The flow below is setting the promise to a reconnect app to address flow (1) explained above.
if (!lastUpdateIDFromClient) {
// If there is a ReconnectApp query in progress, we should not start another one.
if (queryPromise) {
if (GetMissingOnyxUpdatesPromiseProxy.GetMissingOnyxUpdatesPromise) {
return;
}

Log.info('Client has not gotten reliable updates before so reconnecting the app to start the process');

// Since this is a full reconnectApp, we'll not apply the updates we received - those will come in the reconnect app request.
queryPromise = App.finalReconnectAppAfterActivatingReliableUpdates();
GetMissingOnyxUpdatesPromiseProxy.GetMissingOnyxUpdatesPromise = App.finalReconnectAppAfterActivatingReliableUpdates();
} else {
// The flow below is setting the promise to a getMissingOnyxUpdates to address flow (2) explained above.

// Get the number of deferred updates before adding the new one
const existingDeferredUpdatesCount = Object.keys(deferredUpdatesProxy.deferredUpdates).length;
const existingDeferredUpdatesCount = Object.keys(DeferredUpdates.deferredUpdates).length;

// Add the new update to the deferred updates
deferredUpdatesProxy.deferredUpdates[Number(updateParams.lastUpdateID)] = updateParams;
DeferredUpdates.deferredUpdates[Number(updateParams.lastUpdateID)] = updateParams;

// If there are deferred updates already, we don't need to fetch the missing updates again.
if (existingDeferredUpdatesCount > 0) {
Expand All @@ -142,10 +139,12 @@ function handleOnyxUpdateGap(onyxUpdatesFromServer: OnyxEntry<OnyxUpdatesFromSer

// Get the missing Onyx updates from the server and afterwards validate and apply the deferred updates.
// This will trigger recursive calls to "validateAndApplyDeferredUpdates" if there are gaps in the deferred updates.
queryPromise = App.getMissingOnyxUpdates(lastUpdateIDFromClient, previousUpdateIDFromServer).then(() => OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates(clientLastUpdateID));
GetMissingOnyxUpdatesPromiseProxy.GetMissingOnyxUpdatesPromise = App.getMissingOnyxUpdates(lastUpdateIDFromClient, previousUpdateIDFromServer).then(() =>
OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates(clientLastUpdateID),
);
}

queryPromise.finally(finalizeUpdatesAndResumeQueue);
GetMissingOnyxUpdatesPromiseProxy.GetMissingOnyxUpdatesPromise.finally(finalizeUpdatesAndResumeQueue);
}

export default () => {
Expand Down
68 changes: 68 additions & 0 deletions src/libs/actions/OnyxUpdateManager/utils/DeferredUpdates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import Onyx from 'react-native-onyx';
import type {DeferredUpdatesDictionary} from '@libs/actions/OnyxUpdateManager/types';
import * as SequentialQueue from '@libs/Network/SequentialQueue';
import ONYXKEYS from '@src/ONYXKEYS';
import type {OnyxUpdatesFromServer} from '@src/types/onyx';
import createProxyForObject from '@src/utils/createProxyForObject';
// eslint-disable-next-line import/no-cycle
import * as OnyxUpdateManagerUtils from '.';
import GetMissingOnyxUpdatesPromiseProxy from './GetMissingOnyxUpdatesPromise';

let deferredUpdates: DeferredUpdatesDictionary = {};

/**
* Manually processes and applies the updates from the deferred updates queue. (used e.g. for push notifications)
*/
function processDeferredUpdates() {
if (GetMissingOnyxUpdatesPromiseProxy.GetMissingOnyxUpdatesPromise) {
GetMissingOnyxUpdatesPromiseProxy.GetMissingOnyxUpdatesPromise.finally(() => OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates);
}

GetMissingOnyxUpdatesPromiseProxy.GetMissingOnyxUpdatesPromise = OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates();
}

/**
* Allows adding onyx updates to the deferred updates queue manually.
* By default, this will automatically process the updates. Setting "shouldProcessUpdates" to false will prevent this.
* @param updates The updates that should be applied (e.g. updates from push notifications)
* @param shouldProcessUpdates Whether the updates should be processed immediately
* @returns
*/
function enqueueDeferredUpdates(updates: OnyxUpdatesFromServer[], shouldProcessUpdates = true) {
SequentialQueue.pause();

updates.forEach((update) => {
const lastUpdateID = Number(update.lastUpdateID);
if (deferredUpdates[lastUpdateID]) {
return;
}

deferredUpdates[lastUpdateID] = update;
});

if (!shouldProcessUpdates) {
return;
}

processDeferredUpdates();
}

/**
* Clears the deferred updates queue and unpauses the SequentialQueue
* @param shouldUnpauseSequentialQueue Whether the SequentialQueue should be unpaused after clearing the deferred updates
*/
function clearDeferredUpdates(shouldUnpauseSequentialQueue = true) {
GetMissingOnyxUpdatesPromiseProxy.GetMissingOnyxUpdatesPromise = undefined;
deferredUpdates = {};

if (!shouldUnpauseSequentialQueue) {
return;
}

Onyx.set(ONYXKEYS.ONYX_UPDATES_FROM_SERVER, null);
SequentialQueue.unpause();
}

const DeferredUpdates = {deferredUpdates, enqueueDeferredUpdates, clearDeferredUpdates, processDeferredUpdates};

export default createProxyForObject(DeferredUpdates);
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type {Response} from '@src/types/onyx';
import createProxyForObject from '@src/utils/createProxyForObject';

const GetMissingOnyxUpdatesPromiseValue = {GetMissingOnyxUpdatesPromise: undefined as Promise<Response | Response[] | void> | undefined};

export default createProxyForObject(GetMissingOnyxUpdatesPromiseValue);
Copy link
Contributor

Choose a reason for hiding this comment

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

As you might guess I'm not a big fan of this proxy object pattern 😄. These feel more or less like global variables and are prone to error in the same way since their values can be changed anywhere, at anytime and in any way.

It feels like we're pretty much creating a lock with this object so it would be nice if we could abstract it like one. However, I'm not sure exactly how it would look.

Cleaning this up is NAB but if you know an easy way to write it I'm curious.

8 changes: 0 additions & 8 deletions src/libs/actions/OnyxUpdateManager/utils/deferredUpdates.ts

This file was deleted.

25 changes: 16 additions & 9 deletions src/libs/actions/OnyxUpdateManager/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,22 @@ import * as App from '@userActions/App';
import type {DeferredUpdatesDictionary, DetectGapAndSplitResult} from '@userActions/OnyxUpdateManager/types';
import ONYXKEYS from '@src/ONYXKEYS';
import {applyUpdates} from './applyUpdates';
import deferredUpdatesProxy from './deferredUpdates';
// eslint-disable-next-line import/no-cycle
import DeferredUpdates from './DeferredUpdates';

let lastUpdateIDAppliedToClient = 0;
Onyx.connect({
key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT,
callback: (value) => (lastUpdateIDAppliedToClient = value ?? 0),
});

// In order for the deferred updates to be applied correctly in order,
// we need to check if there are any gaps between deferred updates.

/**
* In order for the deferred updates to be applied correctly in order,
* we need to check if there are any gaps between deferred updates.
* @param updates The deferred updates to be checked for gaps
* @param clientLastUpdateID An optional lastUpdateID passed to use instead of the lastUpdateIDAppliedToClient
* @returns
*/
function detectGapsAndSplit(updates: DeferredUpdatesDictionary, clientLastUpdateID?: number): DetectGapAndSplitResult {
const lastUpdateIDFromClient = clientLastUpdateID ?? lastUpdateIDAppliedToClient ?? 0;

Expand Down Expand Up @@ -75,16 +80,18 @@ function detectGapsAndSplit(updates: DeferredUpdatesDictionary, clientLastUpdate
return {applicableUpdates, updatesAfterGaps, latestMissingUpdateID};
}

// This function will check for gaps in the deferred updates and
// apply the updates in order after the missing updates are fetched and applied
/**
* This function will check for gaps in the deferred updates and
* apply the updates in order after the missing updates are fetched and applied
*/
function validateAndApplyDeferredUpdates(clientLastUpdateID?: number, previousParams?: {newLastUpdateIDFromClient: number; latestMissingUpdateID: number}): Promise<void> {
const lastUpdateIDFromClient = clientLastUpdateID ?? lastUpdateIDAppliedToClient ?? 0;

Log.info('[DeferredUpdates] Processing deferred updates', false, {lastUpdateIDFromClient, previousParams});

// We only want to apply deferred updates that are newer than the last update that was applied to the client.
// At this point, the missing updates from "GetMissingOnyxUpdates" have been applied already, so we can safely filter out.
const pendingDeferredUpdates = Object.entries(deferredUpdatesProxy.deferredUpdates).reduce<DeferredUpdatesDictionary>(
const pendingDeferredUpdates = Object.entries(DeferredUpdates.deferredUpdates).reduce<DeferredUpdatesDictionary>(
(accUpdates, [lastUpdateID, update]) => ({
...accUpdates,
...(Number(lastUpdateID) > lastUpdateIDFromClient ? {[Number(lastUpdateID)]: update} : {}),
Expand All @@ -106,7 +113,7 @@ function validateAndApplyDeferredUpdates(clientLastUpdateID?: number, previousPa
Log.info('[DeferredUpdates] Gap detected in deferred updates', false, {lastUpdateIDFromClient, latestMissingUpdateID});

return new Promise((resolve, reject) => {
deferredUpdatesProxy.deferredUpdates = {};
DeferredUpdates.deferredUpdates = {};

applyUpdates(applicableUpdates).then(() => {
// After we have applied the applicable updates, there might have been new deferred updates added.
Expand All @@ -116,7 +123,7 @@ function validateAndApplyDeferredUpdates(clientLastUpdateID?: number, previousPa

const newLastUpdateIDFromClient = clientLastUpdateID ?? lastUpdateIDAppliedToClient ?? 0;

deferredUpdatesProxy.deferredUpdates = {...deferredUpdatesProxy.deferredUpdates, ...updatesAfterGaps};
DeferredUpdates.deferredUpdates = {...DeferredUpdates.deferredUpdates, ...updatesAfterGaps};

// If lastUpdateIDAppliedToClient got updated in the meantime, we will just retrigger the validation and application of the current deferred updates.
if (latestMissingUpdateID <= newLastUpdateIDFromClient) {
Expand Down
Loading