Skip to content

Commit

Permalink
Merge pull request #42044 from margelo/@chrispader/add-deferred-updat…
Browse files Browse the repository at this point in the history
…es--queue-functions

Add deferred updates queue functions to `OnyxUpdateManager` to manually apply updates (e.g. from push notifications)
  • Loading branch information
arosiclair authored May 23, 2024
2 parents cb5c597 + 342873e commit 791058a
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 40 deletions.
30 changes: 13 additions & 17 deletions src/libs/actions/OnyxUpdateManager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ 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 * as DeferredOnyxUpdates from './utils/DeferredOnyxUpdates';

// 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 +39,6 @@ Onyx.connect({
},
});

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

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

const resetDeferralLogicVariables = () => {
queryPromise = undefined;
deferredUpdatesProxy.deferredUpdates = {};
DeferredOnyxUpdates.clear({shouldUnpauseSequentialQueue: false});
};

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

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

/**
Expand Down Expand Up @@ -111,25 +106,24 @@ 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 (DeferredOnyxUpdates.getMissingOnyxUpdatesQueryPromise()) {
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();
DeferredOnyxUpdates.setMissingOnyxUpdatesQueryPromise(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 areDeferredUpdatesQueued = !DeferredOnyxUpdates.isEmpty();

// Add the new update to the deferred updates
deferredUpdatesProxy.deferredUpdates[Number(updateParams.lastUpdateID)] = updateParams;
DeferredOnyxUpdates.enqueue(updateParams, {shouldPauseSequentialQueue: false});

// If there are deferred updates already, we don't need to fetch the missing updates again.
if (existingDeferredUpdatesCount > 0) {
if (areDeferredUpdatesQueued) {
return;
}

Expand All @@ -142,10 +136,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));
DeferredOnyxUpdates.setMissingOnyxUpdatesQueryPromise(
App.getMissingOnyxUpdates(lastUpdateIDFromClient, previousUpdateIDFromServer).then(() => OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates(clientLastUpdateID)),
);
}

queryPromise.finally(finalizeUpdatesAndResumeQueue);
DeferredOnyxUpdates.getMissingOnyxUpdatesQueryPromise()?.finally(finalizeUpdatesAndResumeQueue);
}

export default () => {
Expand Down
133 changes: 133 additions & 0 deletions src/libs/actions/OnyxUpdateManager/utils/DeferredOnyxUpdates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
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, Response} from '@src/types/onyx';
import {isValidOnyxUpdateFromServer} from '@src/types/onyx/OnyxUpdatesFromServer';
// eslint-disable-next-line import/no-cycle
import * as OnyxUpdateManagerUtils from '.';

let missingOnyxUpdatesQueryPromise: Promise<Response | Response[] | void> | undefined;
let deferredUpdates: DeferredUpdatesDictionary = {};

/**
* Returns the promise that fetches the missing onyx updates
* @returns the promise
*/
function getMissingOnyxUpdatesQueryPromise() {
return missingOnyxUpdatesQueryPromise;
}

/**
* Sets the promise that fetches the missing onyx updates
*/
function setMissingOnyxUpdatesQueryPromise(promise: Promise<Response | Response[] | void>) {
missingOnyxUpdatesQueryPromise = promise;
}

type GetDeferredOnyxUpdatesOptiosn = {
minUpdateID?: number;
};

/**
* Returns the deferred updates that are currently in the queue
* @param minUpdateID An optional minimum update ID to filter the deferred updates by
* @returns
*/
function getUpdates(options?: GetDeferredOnyxUpdatesOptiosn) {
if (options?.minUpdateID == null) {
return deferredUpdates;
}

return Object.entries(deferredUpdates).reduce<DeferredUpdatesDictionary>(
(accUpdates, [lastUpdateID, update]) => ({
...accUpdates,
...(Number(lastUpdateID) > (options.minUpdateID ?? 0) ? {[Number(lastUpdateID)]: update} : {}),
}),
{},
);
}

/**
* Returns a boolean indicating whether the deferred updates queue is empty
* @returns a boolean indicating whether the deferred updates queue is empty
*/
function isEmpty() {
return Object.keys(deferredUpdates).length === 0;
}

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

missingOnyxUpdatesQueryPromise = OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates();
}

type EnqueueDeferredOnyxUpdatesOptions = {
shouldPauseSequentialQueue?: boolean;
};

/**
* Allows adding onyx updates to the deferred updates queue manually.
* @param updates The updates that should be applied (e.g. updates from push notifications)
* @param options additional flags to change the behaviour of this function
*/
function enqueue(updates: OnyxUpdatesFromServer | DeferredUpdatesDictionary, options?: EnqueueDeferredOnyxUpdatesOptions) {
if (options?.shouldPauseSequentialQueue ?? true) {
SequentialQueue.pause();
}

// We check here if the "updates" param is a single update.
// If so, we only need to insert one update into the deferred updates queue.
if (isValidOnyxUpdateFromServer(updates)) {
const lastUpdateID = Number(updates.lastUpdateID);
deferredUpdates[lastUpdateID] = updates;
} else {
// If the "updates" param is an object, we need to insert multiple updates into the deferred updates queue.
Object.entries(updates).forEach(([lastUpdateIDString, update]) => {
const lastUpdateID = Number(lastUpdateIDString);
if (deferredUpdates[lastUpdateID]) {
return;
}

deferredUpdates[lastUpdateID] = update;
});
}
}

/**
* Adds updates to the deferred updates queue and processes them immediately
* @param updates The updates that should be applied (e.g. updates from push notifications)
*/
function enqueueAndProcess(updates: OnyxUpdatesFromServer | DeferredUpdatesDictionary, options?: EnqueueDeferredOnyxUpdatesOptions) {
enqueue(updates, options);
process();
}

type ClearDeferredOnyxUpdatesOptions = {
shouldResetGetMissingOnyxUpdatesPromise?: boolean;
shouldUnpauseSequentialQueue?: boolean;
};

/**
* Clears the deferred updates queue and unpauses the SequentialQueue
* @param options additional flags to change the behaviour of this function
*/
function clear(options?: ClearDeferredOnyxUpdatesOptions) {
deferredUpdates = {};

if (options?.shouldResetGetMissingOnyxUpdatesPromise ?? true) {
missingOnyxUpdatesQueryPromise = undefined;
}

if (options?.shouldUnpauseSequentialQueue ?? true) {
Onyx.set(ONYXKEYS.ONYX_UPDATES_FROM_SERVER, null);
SequentialQueue.unpause();
}
}

export {getMissingOnyxUpdatesQueryPromise, setMissingOnyxUpdatesQueryPromise, getUpdates, isEmpty, process, enqueue, enqueueAndProcess, clear};
8 changes: 0 additions & 8 deletions src/libs/actions/OnyxUpdateManager/utils/deferredUpdates.ts

This file was deleted.

31 changes: 16 additions & 15 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 * as DeferredOnyxUpdates from './DeferredOnyxUpdates';

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,22 +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>(
(accUpdates, [lastUpdateID, update]) => ({
...accUpdates,
...(Number(lastUpdateID) > lastUpdateIDFromClient ? {[Number(lastUpdateID)]: update} : {}),
}),
{},
);
const pendingDeferredUpdates = DeferredOnyxUpdates.getUpdates({minUpdateID: lastUpdateIDFromClient});

// If there are no remaining deferred updates after filtering out outdated ones,
// we can just unpause the queue and return
Expand All @@ -106,7 +107,7 @@ function validateAndApplyDeferredUpdates(clientLastUpdateID?: number, previousPa
Log.info('[DeferredUpdates] Gap detected in deferred updates', false, {lastUpdateIDFromClient, latestMissingUpdateID});

return new Promise((resolve, reject) => {
deferredUpdatesProxy.deferredUpdates = {};
DeferredOnyxUpdates.clear({shouldUnpauseSequentialQueue: false, shouldResetGetMissingOnyxUpdatesPromise: false});

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

const newLastUpdateIDFromClient = clientLastUpdateID ?? lastUpdateIDAppliedToClient ?? 0;

deferredUpdatesProxy.deferredUpdates = {...deferredUpdatesProxy.deferredUpdates, ...updatesAfterGaps};
DeferredOnyxUpdates.enqueue(updatesAfterGaps, {shouldPauseSequentialQueue: false});

// 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

0 comments on commit 791058a

Please sign in to comment.