Skip to content

Commit 0ab2dd9

Browse files
francoislOSBotify
authored andcommitted
Merge pull request #99814 from Abdukhamid000/revert-98012-split-defer
Revert "perf: defer the split-save API.write behind the destination layout" (cherry picked from commit b8aba2b) (cherry-picked to staging by francoisl)
1 parent 5d41468 commit 0ab2dd9

6 files changed

Lines changed: 12 additions & 267 deletions

File tree

src/libs/API/__mocks__/writeWhenReady.ts

Lines changed: 0 additions & 28 deletions
This file was deleted.

src/libs/actions/IOU/SplitTransactionUpdate.ts

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ import type {SearchActionsContextValue, SearchStateContextValue} from '@componen
33

44
import type {CurrencyListActionsContextType} from '@hooks/useCurrencyList';
55

6-
import {createTransitionBarrier, write as apiWrite, writeWhenReady} from '@libs/API';
6+
import {write as apiWrite} from '@libs/API';
77
import type {RevertSplitTransactionParams, SplitTransactionParams, SplitTransactionSplitsParam} from '@libs/API/parameters';
8-
import type {ApiRequestCommandParameters, WriteCommand} from '@libs/API/types';
98
import {WRITE_COMMANDS} from '@libs/API/types';
109
import DateUtils from '@libs/DateUtils';
1110
import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils';
@@ -1990,24 +1989,14 @@ function updateSplitTransactions({
19901989
}
19911990
}
19921991

1993-
// optimisticData is applied synchronously, re-rendering the destination screen mid-transition.
1994-
// Defer the write until the transition ends, unless there is none to wait on.
1995-
const writeSplit = <TCommand extends WriteCommand>(command: TCommand, parameters: ApiRequestCommandParameters[TCommand]) => {
1996-
if (!isFromSplitExpensesFlow) {
1997-
apiWrite(command, parameters, onyxData);
1998-
return;
1999-
}
2000-
writeWhenReady(command, parameters, onyxData, createTransitionBarrier('navigation'));
2001-
};
2002-
20031992
if (isReverseSplitOperation) {
20041993
const parameters = {
20051994
...splits.at(0),
20061995
comment: splits.at(0)?.comment?.comment,
20071996
waypoints: splits.at(0)?.waypoints ? JSON.stringify(splits.at(0)?.waypoints) : undefined,
20081997
copiedComments: splits.at(0)?.copiedComments ? JSON.stringify(splits.at(0)?.copiedComments) : undefined,
20091998
} as RevertSplitTransactionParams;
2010-
writeSplit(WRITE_COMMANDS.REVERT_SPLIT_TRANSACTION, parameters);
1999+
apiWrite(WRITE_COMMANDS.REVERT_SPLIT_TRANSACTION, parameters, onyxData);
20112000
} else {
20122001
// Prepare splitApiParams for the Transaction_Split API call which requires a specific format for the splits
20132002
// The format is: splits[0][amount], splits[0][category], splits[0][tag] etc.
@@ -2023,7 +2012,11 @@ function updateSplitTransactions({
20232012
transactionID: originalTransactionID,
20242013
};
20252014

2026-
writeSplit(isCreationOfSplits ? WRITE_COMMANDS.SPLIT_TRANSACTION : WRITE_COMMANDS.UPDATE_SPLIT_TRANSACTION, splitParameters);
2015+
if (isCreationOfSplits) {
2016+
apiWrite(WRITE_COMMANDS.SPLIT_TRANSACTION, splitParameters, onyxData);
2017+
} else {
2018+
apiWrite(WRITE_COMMANDS.UPDATE_SPLIT_TRANSACTION, splitParameters, onyxData);
2019+
}
20272020
}
20282021
TransitionTracker.runAfterTransitions({callback: () => removeDraftSplitTransaction(originalTransactionID), waitForUpcomingTransition: true});
20292022
}
@@ -2062,7 +2055,11 @@ function updateSplitTransactionsFromSplitExpensesFlow(params: UpdateSplitTransac
20622055
const reverseSplitKeepsOriginalInExpenseReport = isReverseSplitOperation && splitExpenses.at(0)?.reportID === expenseReportID;
20632056
const willExpenseReportBecomeEmpty =
20642057
!!expenseReportID && areAllExpenseReportTransactionsSplitChildren && !anyRemainingSplitStaysInExpenseReport && !reverseSplitKeepsOriginalInExpenseReport;
2065-
const isLastTransactionInReport = willExpenseReportBecomeEmpty || (isReverseSplitOperation && !reverseSplitKeepsOriginalInExpenseReport && expenseReportTransactions.length === 1);
2058+
const isLastTransactionInReport =
2059+
willExpenseReportBecomeEmpty ||
2060+
(isReverseSplitOperation &&
2061+
!reverseSplitKeepsOriginalInExpenseReport &&
2062+
Object.values(params.allTransactionsList ?? {}).filter((itemTransaction) => itemTransaction?.reportID === expenseReportID).length === 1);
20662063
const fallbackReportID = params.expenseReport?.chatReportID ?? params.expenseReport?.parentReportID;
20672064

20682065
if (isLastTransactionInReport && fallbackReportID) {
@@ -2192,4 +2189,3 @@ function updateSplitTransactionsFromSplitExpensesFlow(params: UpdateSplitTransac
21922189
}
21932190

21942191
export {updateSplitTransactions, updateSplitTransactionsFromSplitExpensesFlow};
2195-
export type {UpdateSplitTransactionsParams};

tests/actions/IOU/SplitReportTotalsTest.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ jest.mock('@libs/actions/IOU/PendingNewTransactions', () => ({
6969
deletePendingNewTransactionIDs: jest.fn(),
7070
isOneToTwoTransactionTransition: jest.fn(() => false),
7171
}));
72-
jest.mock('@libs/API/writeWhenReady');
7372
// In production, requestMoney defers its API.write() call until the target screen's
7473
// content lays out (or a safety timeout fires). In tests there is no target component
7574
// to flush the deferred write, so we bypass the deferral by executing the callback immediately.

tests/actions/IOUTest/SplitSelfDMTest.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ jest.mock('@src/libs/actions/Report', () => {
5656

5757
jest.mock('@libs/Navigation/helpers/isSearchTopmostFullScreenRoute', () => jest.fn());
5858

59-
jest.mock('@libs/API/writeWhenReady');
6059
jest.mock('@src/libs/SearchQueryUtils', () => {
6160
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
6261
const actual = jest.requireActual('@src/libs/SearchQueryUtils');

tests/actions/IOUTest/SplitTest.ts

Lines changed: 0 additions & 220 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,8 @@ import {createWorkspace, generatePolicyID, setWorkspaceApprovalMode} from '@libs
1010
import {addComment, notifyNewAction} from '@libs/actions/Report';
1111
import initSplitExpense from '@libs/actions/SplitExpenses';
1212
import type * as API from '@libs/API';
13-
import type {WriteCommand} from '@libs/API/types';
1413
import {WRITE_COMMANDS} from '@libs/API/types';
15-
import {createTransitionBarrier, writeWhenReady} from '@libs/API/writeWhenReady';
1614
import {getCurrencyDecimals, getCurrencySymbol} from '@libs/CurrencyUtils';
17-
import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute';
1815
import {rand64} from '@libs/NumberUtils';
1916
import {getIOUActionForReportID, getIOUActionForTransactionID, getOriginalMessage, isActionOfType, isAddCommentAction, isDeletedAction, isMoneyRequestAction} from '@libs/ReportActionsUtils';
2017
import {buildOptimisticIOUReportAction, getAncestors, getReportOrDraftReport} from '@libs/ReportUtils';
@@ -38,7 +35,6 @@ import {
3835
updateSplitExpenseAmountField,
3936
updateSplitExpenseField,
4037
} from '@userActions/IOU/SplitExpenseItems';
41-
import type {UpdateSplitTransactionsParams} from '@userActions/IOU/SplitTransactionUpdate';
4238
import {updateSplitTransactions, updateSplitTransactionsFromSplitExpensesFlow} from '@userActions/IOU/SplitTransactionUpdate';
4339

4440
import CONST from '@src/CONST';
@@ -125,7 +121,6 @@ jest.mock('@src/libs/actions/Report', () => {
125121

126122
jest.mock('@libs/Navigation/helpers/isSearchTopmostFullScreenRoute', () => jest.fn());
127123
jest.mock('@libs/Navigation/helpers/isReportTopmostSplitNavigator', () => jest.fn());
128-
jest.mock('@libs/API/writeWhenReady');
129124
jest.mock('@libs/deferredLayoutWrite', () => ({
130125
registerDeferredWrite: (_key: string, callback: () => void) => callback(),
131126
flushDeferredWrite: jest.fn(),
@@ -10305,221 +10300,6 @@ describe('startSplitBill delegateAccountID forwarding', () => {
1030510300
});
1030610301
});
1030710302

10308-
/**
10309-
* Fixture for the save tests below: an expense report with a single transaction, plus a draft that splits that
10310-
* transaction into two.
10311-
*
10312-
* `withExistingSplitChildren` also seeds those two split transactions in Onyx. That is what makes a one-item
10313-
* `splitExpenses` mean "collapse an existing split back into one expense" (a reverse split) rather than "split
10314-
* this expense into one".
10315-
*/
10316-
const buildSplitFlowParams = async ({withExistingSplitChildren = false, asSelfDMThread = false} = {}) => {
10317-
const expenseReport: Report = {
10318-
...createRandomReport(9001, undefined),
10319-
type: CONST.REPORT.TYPE.EXPENSE,
10320-
};
10321-
const transaction: Transaction = {
10322-
amount: 100,
10323-
currency: 'USD',
10324-
transactionID: '9001',
10325-
reportID: expenseReport.reportID,
10326-
created: DateUtils.getDBTime(),
10327-
merchant: 'test',
10328-
};
10329-
// `asSelfDMThread` makes the thread a selfDM child of the expense report, which is the only shape that
10330-
// reaches the navigate-first branch in `updateSplitTransactionsFromSplitExpensesFlow`.
10331-
const transactionThread: Report = {
10332-
...createRandomReport(9002, undefined),
10333-
type: CONST.REPORT.TYPE.CHAT,
10334-
...(asSelfDMThread ? {chatType: CONST.REPORT.CHAT_TYPE.SELF_DM, parentReportID: expenseReport.reportID} : {}),
10335-
};
10336-
const iouAction: ReportAction = {
10337-
...buildOptimisticIOUReportAction({
10338-
type: CONST.IOU.REPORT_ACTION_TYPE.CREATE,
10339-
amount: transaction.amount,
10340-
currency: transaction.currency,
10341-
comment: '',
10342-
participants: [],
10343-
transactionID: transaction.transactionID,
10344-
iouReportID: expenseReport.reportID,
10345-
getCurrencyDecimals: getCurrencyDecimalsLocal,
10346-
}),
10347-
childReportID: transactionThread.reportID,
10348-
};
10349-
10350-
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, expenseReport);
10351-
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${transactionThread.reportID}`, transactionThread);
10352-
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, {[iouAction.reportActionID]: iouAction});
10353-
await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transaction);
10354-
if (withExistingSplitChildren) {
10355-
for (const childTransactionID of ['9003', '9004']) {
10356-
await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${childTransactionID}`, {
10357-
...transaction,
10358-
transactionID: childTransactionID,
10359-
amount: 50,
10360-
comment: {originalTransactionID: transaction.transactionID, source: CONST.IOU.TYPE.SPLIT},
10361-
});
10362-
}
10363-
}
10364-
await waitForBatchedUpdates();
10365-
10366-
let allTransactions: OnyxCollection<Transaction>;
10367-
let allReports: OnyxCollection<Report>;
10368-
await getOnyxData({
10369-
key: ONYXKEYS.COLLECTION.TRANSACTION,
10370-
callback: (value) => {
10371-
allTransactions = value;
10372-
},
10373-
});
10374-
await getOnyxData({
10375-
key: ONYXKEYS.COLLECTION.REPORT,
10376-
callback: (value) => {
10377-
allReports = value;
10378-
},
10379-
});
10380-
10381-
const reports = getTransactionAndExpenseReports(asSelfDMThread ? transactionThread.reportID : expenseReport.reportID);
10382-
const params: UpdateSplitTransactionsParams = {
10383-
allTransactionsList: allTransactions,
10384-
allReportsList: allReports,
10385-
allReportActionsList: undefined,
10386-
allReportNameValuePairsList: undefined,
10387-
transactionData: {
10388-
reportID: expenseReport.reportID,
10389-
originalTransactionID: transaction.transactionID,
10390-
splitExpenses: [
10391-
{transactionID: '9003', amount: 50, description: '', category: '', tags: [''], created: transaction.created, reportID: expenseReport.reportID},
10392-
{transactionID: '9004', amount: 50, description: '', category: '', tags: [''], created: transaction.created, reportID: expenseReport.reportID},
10393-
],
10394-
splitExpensesTotal: 100,
10395-
},
10396-
searchContext: {currentSearchHash: -2},
10397-
policyCategories: undefined,
10398-
policy: undefined,
10399-
policyRecentlyUsedCategories: [],
10400-
iouReport: expenseReport,
10401-
firstIOU: iouAction,
10402-
isASAPSubmitBetaEnabled: false,
10403-
currentUserPersonalDetails,
10404-
transactionViolations: {},
10405-
policyRecentlyUsedCurrencies: [],
10406-
quickAction: undefined,
10407-
betas: [CONST.BETAS.ALL],
10408-
allPolicyTags: undefined,
10409-
personalDetails: {[RORY_ACCOUNT_ID]: {accountID: RORY_ACCOUNT_ID, login: RORY_EMAIL}},
10410-
transactionReport: reports.transactionReport,
10411-
expenseReport: reports.expenseReport,
10412-
isOffline: false,
10413-
delegateAccountID: undefined,
10414-
isTrackIntentUser: false,
10415-
getCurrencyDecimals: getCurrencyDecimalsLocal,
10416-
formatPhoneNumber,
10417-
getCurrencySymbol: getCurrencySymbolLocal,
10418-
};
10419-
10420-
return {expenseReport, iouAction, params};
10421-
};
10422-
10423-
/**
10424-
* `@libs/API` re-declares `writeWhenReady` rather than re-exporting it, and its wrapper always forwards the
10425-
* optional 5th argument. So the arity the mock records is the wrapper's, not the call site's, and asserting
10426-
* with `toHaveBeenCalledWith` breaks whenever that wrapper's signature grows. Assert the arguments this flow
10427-
* actually chooses - the command and the barrier - and ignore the rest.
10428-
*/
10429-
function expectDeferredWriteFor(command: WriteCommand) {
10430-
const deferredCall = jest.mocked(writeWhenReady).mock.calls.find(([calledCommand]) => calledCommand === command);
10431-
expect(deferredCall).toBeDefined();
10432-
expect(deferredCall?.at(3)).toEqual(expect.any(Function));
10433-
}
10434-
10435-
describe('split save deferred write', () => {
10436-
beforeEach(() => {
10437-
jest.mocked(isSearchTopmostFullScreenRoute).mockReturnValue(false);
10438-
});
10439-
10440-
it('defers the write behind a navigation barrier when saving from the split-expenses flow', async () => {
10441-
// Given a split saved through the split-expenses flow
10442-
const {params} = await buildSplitFlowParams();
10443-
10444-
// When the split is saved
10445-
updateSplitTransactionsFromSplitExpensesFlow(params);
10446-
await waitForBatchedUpdates();
10447-
10448-
// Then the write goes through writeWhenReady gated on a screen transition specifically, so its
10449-
// optimistic data does not land while the press is still trying to paint, and a stray modal or
10450-
// keyboard blip cannot release it early
10451-
expect(createTransitionBarrier).toHaveBeenCalledWith('navigation');
10452-
expectDeferredWriteFor(WRITE_COMMANDS.SPLIT_TRANSACTION);
10453-
});
10454-
10455-
it('navigates back to the selfDM before deferring the write when Search is not topmost', async () => {
10456-
const Navigation = jest.requireMock('@src/libs/Navigation/Navigation');
10457-
10458-
// Given a split whose transaction thread is a selfDM, the one shape that reaches the navigate-first branch
10459-
const {params} = await buildSplitFlowParams({asSelfDMThread: true});
10460-
10461-
// When it is saved while Search is not the topmost full screen route
10462-
updateSplitTransactionsFromSplitExpensesFlow(params);
10463-
await waitForBatchedUpdates();
10464-
10465-
// Then the flow navigates back to the selfDM first, so the transaction thread is off screen before the
10466-
// data changes under it. The write is still deferred, it just happens after that navigation.
10467-
expect(Navigation.dismissModal).toHaveBeenCalled();
10468-
expectDeferredWriteFor(WRITE_COMMANDS.SPLIT_TRANSACTION);
10469-
});
10470-
10471-
it('defers the write for the same selfDM split when the Search page is topmost', async () => {
10472-
const Navigation = jest.requireMock('@src/libs/Navigation/Navigation');
10473-
10474-
// Given the identical selfDM fixture, changed only by Search being the topmost full screen route
10475-
jest.mocked(isSearchTopmostFullScreenRoute).mockReturnValue(true);
10476-
const {params} = await buildSplitFlowParams({asSelfDMThread: true});
10477-
10478-
// When the split is saved
10479-
updateSplitTransactionsFromSplitExpensesFlow(params);
10480-
await waitForBatchedUpdates();
10481-
10482-
// Then the navigate-first branch is skipped and the write goes through the barrier instead
10483-
expect(Navigation.dismissModal).not.toHaveBeenCalled();
10484-
expect(createTransitionBarrier).toHaveBeenCalledWith('navigation');
10485-
expectDeferredWriteFor(WRITE_COMMANDS.SPLIT_TRANSACTION);
10486-
});
10487-
10488-
it('defers the reverse-split write behind the same barrier', async () => {
10489-
// Given an existing two-way split being collapsed back to a single expense
10490-
const {params} = await buildSplitFlowParams({withExistingSplitChildren: true});
10491-
const remainingSplit = params.transactionData.splitExpenses.at(0);
10492-
10493-
// When the reverse split is saved from the split-expenses flow
10494-
updateSplitTransactions({
10495-
...params,
10496-
transactionData: {...params.transactionData, splitExpenses: remainingSplit ? [remainingSplit] : []},
10497-
isFromSplitExpensesFlow: true,
10498-
});
10499-
await waitForBatchedUpdates();
10500-
10501-
// Then REVERT_SPLIT_TRANSACTION goes through the same fork as the creation path
10502-
expect(createTransitionBarrier).toHaveBeenCalledWith('navigation');
10503-
expectDeferredWriteFor(WRITE_COMMANDS.REVERT_SPLIT_TRANSACTION);
10504-
});
10505-
10506-
it('writes immediately when the caller is not the split-expenses flow', async () => {
10507-
// Given a direct updateSplitTransactions call, as useDeleteTransactions makes
10508-
const {params} = await buildSplitFlowParams();
10509-
const writeSpy = jest.spyOn(APIlib, 'write');
10510-
10511-
// When it runs outside the split-expenses flow
10512-
updateSplitTransactions({...params, isFromSplitExpensesFlow: false});
10513-
await waitForBatchedUpdates();
10514-
10515-
// Then the write still happens, it is just not deferred behind a barrier
10516-
expect(writeSpy).toHaveBeenCalledWith(WRITE_COMMANDS.SPLIT_TRANSACTION, expect.anything(), expect.anything());
10517-
expect(writeWhenReady).not.toHaveBeenCalled();
10518-
expect(createTransitionBarrier).not.toHaveBeenCalled();
10519-
writeSpy.mockRestore();
10520-
});
10521-
});
10522-
1052310303
describe('resolveOptimisticSplitChatReportID', () => {
1052410304
it('returns the existing chat and no optimistic ID when the participants already share one', async () => {
1052510305
const reportID = 'existing-split-chat';

tests/actions/TransactionTest.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,6 @@ jest.mock('@src/libs/actions/Report', () => {
101101
});
102102
jest.mock('@libs/Navigation/helpers/isSearchTopmostFullScreenRoute', () => jest.fn());
103103
jest.mock('@libs/Navigation/helpers/isReportTopmostSplitNavigator', () => jest.fn());
104-
jest.mock('@libs/API/writeWhenReady');
105104
// In production, requestMoney defers its API.write() call until the target screen's
106105
// content lays out (or a safety timeout fires). In tests there is no target component
107106
// to flush the deferred write, so we bypass the deferral by executing the callback immediately.

0 commit comments

Comments
 (0)