Skip to content

Commit f824da8

Browse files
authored
Merge pull request Expensify#89262 from callstack-internal/perf/preserve-last-full-reconnect-time-on-delegate-switch
Don't fire duplicate ReconnectApp on delegate account switch
2 parents 290ed93 + 1d4e420 commit f824da8

2 files changed

Lines changed: 58 additions & 6 deletions

File tree

src/libs/actions/Delegate.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {OnyxEntry, OnyxKey, OnyxUpdate} from 'react-native-onyx';
44
import * as API from '@libs/API';
55
import type {AddDelegateParams as APIAddDelegateParams, RemoveDelegateParams as APIRemoveDelegateParams, UpdateDelegateRoleParams as APIUpdateDelegateRoleParams} from '@libs/API/parameters';
66
import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
7+
import DateUtils from '@libs/DateUtils';
78
import * as ErrorUtils from '@libs/ErrorUtils';
89
import Log from '@libs/Log';
910
import {clearPreservedSearchNavigatorStates} from '@libs/Navigation/AppNavigator/createSplitNavigator/usePreserveNavigatorState';
@@ -42,14 +43,25 @@ const KEYS_TO_PRESERVE_DELEGATE_ACCESS = [
4243
];
4344

4445
/**
45-
* Atomically reset Onyx for a delegate-access transition while keeping IS_LOADING_APP=true
46-
* so consumers never observe the post-clear state with HAS_LOADED_APP=true and
47-
* IS_LOADING_APP=undefined. That combination falsely looks like a stuck app and triggers
48-
* DelegateAccessHandler's recovery effect, producing a duplicate openApp queued behind the
49-
* explicit openApp the caller is about to make.
46+
* Atomically reset Onyx for a delegate-access transition while seeding two values that
47+
* subscribers would otherwise misinterpret on the post-clear state and double up calls
48+
* the caller is about to make explicitly:
49+
*
50+
* - IS_LOADING_APP=true: without it, consumers observe HAS_LOADED_APP=true and
51+
* IS_LOADING_APP=undefined together, which looks like a stuck app and triggers
52+
* DelegateAccessHandler's recovery effect, queueing a duplicate openApp.
53+
* - LAST_FULL_RECONNECT_TIME=now: subscribeToFullReconnect compares this against the
54+
* server-supplied NVP_RECONNECT_APP_IF_FULL_RECONNECT_BEFORE that lands in OpenApp's
55+
* response.onyxData. Because applyHTTPSOnyxUpdates applies response.onyxData before
56+
* successData, the timestamp would still be empty when the comparison runs, falsely
57+
* triggering a duplicate ReconnectApp. Seeding to `now` short-circuits the subscriber
58+
* until OpenApp's successData refreshes it.
5059
*/
5160
function clearOnyxForDelegateTransition(): Promise<void> {
52-
return Onyx.merge(ONYXKEYS.IS_LOADING_APP, true).then(() => Onyx.clear([...KEYS_TO_PRESERVE_DELEGATE_ACCESS, ONYXKEYS.IS_LOADING_APP]));
61+
return Onyx.multiSet({
62+
[ONYXKEYS.IS_LOADING_APP]: true,
63+
[ONYXKEYS.LAST_FULL_RECONNECT_TIME]: DateUtils.getDBTime(),
64+
}).then(() => Onyx.clear([...KEYS_TO_PRESERVE_DELEGATE_ACCESS, ONYXKEYS.IS_LOADING_APP, ONYXKEYS.LAST_FULL_RECONNECT_TIME]));
5365
}
5466

5567
type WithDelegatedAccess = {

tests/actions/DelegateTest.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
removeDelegate,
99
updateDelegateRole,
1010
} from '@libs/actions/Delegate';
11+
import DateUtils from '@libs/DateUtils';
1112
import {pause, resetQueue} from '@libs/Network/SequentialQueue';
1213
import CONST from '@src/CONST';
1314
import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager';
@@ -293,5 +294,44 @@ describe('actions/Delegate', () => {
293294
});
294295
});
295296
});
297+
298+
it('seeds LAST_FULL_RECONNECT_TIME so subscribeToFullReconnect does not fire a duplicate ReconnectApp before OpenApp successData arrives', async () => {
299+
// Simulate the pre-switch state: timestamp is empty, mimicking the post-clear baseline.
300+
await Onyx.multiSet({
301+
[ONYXKEYS.LAST_FULL_RECONNECT_TIME]: null,
302+
[ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT,
303+
});
304+
await waitForBatchedUpdates();
305+
306+
const before = DateUtils.getDBTime();
307+
await clearOnyxForDelegateTransition();
308+
await waitForBatchedUpdates();
309+
310+
// The timestamp must be a non-empty DB time string seeded at-or-after `before`.
311+
await new Promise<void>((resolve) => {
312+
const conn = Onyx.connect({
313+
key: ONYXKEYS.LAST_FULL_RECONNECT_TIME,
314+
callback: (value) => {
315+
expect(typeof value).toBe('string');
316+
expect(value && value.length > 0).toBe(true);
317+
expect((value ?? '') >= before).toBe(true);
318+
Onyx.disconnect(conn);
319+
resolve();
320+
},
321+
});
322+
});
323+
324+
// A non-preserved key should still have been cleared alongside the seed.
325+
await new Promise<void>((resolve) => {
326+
const conn = Onyx.connect({
327+
key: ONYXKEYS.NVP_PRIORITY_MODE,
328+
callback: (value) => {
329+
expect(value).toBeUndefined();
330+
Onyx.disconnect(conn);
331+
resolve();
332+
},
333+
});
334+
});
335+
});
296336
});
297337
});

0 commit comments

Comments
 (0)