diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index 16405e45ff9d..089c1452ccb4 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -1,6 +1,8 @@ import type {OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {SetRequired} from 'type-fest'; +import {resolveDuplicationConflictAction, resolveEnableFeatureConflicts} from '@libs/actions/RequestConflictUtils'; +import type {EnablePolicyFeatureCommand, RequestMatcher} from '@libs/actions/RequestConflictUtils'; import Log from '@libs/Log'; import {handleDeletedAccount, HandleUnusedOptimisticID, Logging, Pagination, Reauthentication, RecheckConnection, SaveResponseInOnyx} from '@libs/Middleware'; import {isOffline} from '@libs/Network/NetworkStore'; @@ -134,16 +136,6 @@ function processRequest(request: OnyxRequest, type: ApiRequestType): Promise( @@ -157,6 +149,39 @@ function write( return processRequest(request, CONST.API_REQUEST_TYPE.WRITE); } +/** + * This function is used to write data to the API while ensuring that there are no duplicate requests in the queue. + * If a duplicate request is found, it resolves the conflict by replacing the duplicated request with the new one. + */ +function writeWithNoDuplicatesConflictAction( + command: TCommand, + apiCommandParameters: ApiRequestCommandParameters[TCommand], + onyxData: OnyxData = {}, + requestMatcher: RequestMatcher = (request) => request.command === command, +): Promise { + const conflictResolver = { + checkAndFixConflictingRequest: (persistedRequests: OnyxRequest[]) => resolveDuplicationConflictAction(persistedRequests, requestMatcher), + }; + + return write(command, apiCommandParameters, onyxData, conflictResolver); +} + +/** + * This function is used to write data to the API while ensuring that there are no conflicts with enabling policy features. + * If a conflict is found, it resolves the conflict by deleting the duplicated request. + */ +function writeWithNoDuplicatesEnableFeatureConflicts( + command: TCommand, + apiCommandParameters: ApiRequestCommandParameters[TCommand], + onyxData: OnyxData = {}, +): Promise { + const conflictResolver = { + checkAndFixConflictingRequest: (persistedRequests: OnyxRequest[]) => resolveEnableFeatureConflicts(command, persistedRequests, apiCommandParameters), + }; + + return write(command, apiCommandParameters, onyxData, conflictResolver); +} + /** * For commands where the network response must be accessed directly or when there is functionality that can only * happen once the request is finished (eg. calling third-party services like Onfido and Plaid, redirecting a user @@ -164,17 +189,6 @@ function write( * It works just like API.read(), except that it will return a promise. * Using this method is discouraged and will throw an ESLint error. Use it sparingly and only when all other alternatives have been exhausted. * It is best to discuss it in Slack anytime you are tempted to use this method. - * - * @param command - Name of API command to call. - * @param apiCommandParameters - Parameters to send to the API. - * @param onyxData - Object containing errors, loading states, and optimistic UI data that will be merged - * into Onyx before and after a request is made. Each nested object will be formatted in - * the same way as an API response. - * @param [onyxData.optimisticData] - Onyx instructions that will be passed to Onyx.update() before the request is made. - * @param [onyxData.successData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200. - * @param [onyxData.failureData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode !== 200. - * @param [onyxData.finallyData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200 or jsonCode !== 200. - * @returns */ function makeRequestWithSideEffects( command: TCommand, @@ -202,16 +216,6 @@ function waitForWrites(command: TCommand) { /** * Requests made with this method are not be persisted to disk. If there is no network connectivity, the request is ignored and discarded. - * - * @param command - Name of API command to call. - * @param apiCommandParameters - Parameters to send to the API. - * @param onyxData - Object containing errors, loading states, and optimistic UI data that will be merged - * into Onyx before and after a request is made. Each nested object will be formatted in - * the same way as an API response. - * @param [onyxData.optimisticData] - Onyx instructions that will be passed to Onyx.update() before the request is made. - * @param [onyxData.successData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200. - * @param [onyxData.failureData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode !== 200. - * @param [onyxData.finallyData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200 or jsonCode !== 200. */ function read(command: TCommand, apiCommandParameters: ApiRequestCommandParameters[TCommand], onyxData: OnyxData = {}): void { Log.info('[API] Called API.read', false, {command, ...apiCommandParameters}); @@ -281,4 +285,4 @@ function paginate { const params: OpenAppParams = {enablePriorityModeFilter: true, ...policyParams}; - return API.write(WRITE_COMMANDS.OPEN_APP, params, getOnyxDataForOpenOrReconnect(true, undefined, shouldKeepPublicRooms), { - checkAndFixConflictingRequest: (persistedRequests) => resolveDuplicationConflictAction(persistedRequests, (request) => request.command === WRITE_COMMANDS.OPEN_APP), - }); + return API.writeWithNoDuplicatesConflictAction(WRITE_COMMANDS.OPEN_APP, params, getOnyxDataForOpenOrReconnect(true, undefined, shouldKeepPublicRooms)); }); } @@ -359,9 +356,7 @@ function reconnectApp(updateIDFrom: OnyxEntry = 0) { } const isFullReconnect = !updateIDFrom; - API.write(WRITE_COMMANDS.RECONNECT_APP, params, getOnyxDataForOpenOrReconnect(false, isFullReconnect), { - checkAndFixConflictingRequest: (persistedRequests) => resolveDuplicationConflictAction(persistedRequests, (request) => request.command === WRITE_COMMANDS.RECONNECT_APP), - }); + API.writeWithNoDuplicatesConflictAction(WRITE_COMMANDS.RECONNECT_APP, params, getOnyxDataForOpenOrReconnect(false, isFullReconnect, isSidebarLoaded)); }); }); } diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index 62f1f1c5ddbb..ea506ab923c3 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -30,7 +30,6 @@ import enhanceParameters from '@libs/Network/enhanceParameters'; import {hasEnabledOptions} from '@libs/OptionsListUtils'; import {getPolicy, goBackWhenEnableFeature} from '@libs/PolicyUtils'; import {getAllPolicyReports, pushTransactionViolationsOnyxData} from '@libs/ReportUtils'; -import {resolveEnableFeatureConflicts} from '@userActions/RequestConflictUtils'; import {getFinishOnboardingTaskOnyxData} from '@userActions/Task'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -1104,9 +1103,7 @@ function enablePolicyCategories( const parameters: EnablePolicyCategoriesParams = {policyID, enabled}; - API.write(WRITE_COMMANDS.ENABLE_POLICY_CATEGORIES, parameters, onyxData, { - checkAndFixConflictingRequest: (persistedRequests) => resolveEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_CATEGORIES, persistedRequests, parameters), - }); + API.writeWithNoDuplicatesEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_CATEGORIES, parameters, onyxData); if (enabled && getIsNarrowLayout() && shouldGoBack) { goBackWhenEnableFeature(policyID); diff --git a/src/libs/actions/Policy/DistanceRate.ts b/src/libs/actions/Policy/DistanceRate.ts index 0ef355c243b0..03c197d02ab2 100644 --- a/src/libs/actions/Policy/DistanceRate.ts +++ b/src/libs/actions/Policy/DistanceRate.ts @@ -15,7 +15,6 @@ import * as ErrorUtils from '@libs/ErrorUtils'; import getIsNarrowLayout from '@libs/getIsNarrowLayout'; import {getDistanceRateCustomUnit, goBackWhenEnableFeature, removePendingFieldsFromCustomUnit} from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import {resolveEnableFeatureConflicts} from '@userActions/RequestConflictUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy, Report, Transaction, TransactionViolation} from '@src/types/onyx'; @@ -180,9 +179,7 @@ function enablePolicyDistanceRates(policyID: string, enabled: boolean) { const parameters: EnablePolicyDistanceRatesParams = {policyID, enabled}; - API.write(WRITE_COMMANDS.ENABLE_POLICY_DISTANCE_RATES, parameters, onyxData, { - checkAndFixConflictingRequest: (persistedRequests) => resolveEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_DISTANCE_RATES, persistedRequests, parameters), - }); + API.writeWithNoDuplicatesEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_DISTANCE_RATES, parameters, onyxData); if (enabled && getIsNarrowLayout()) { goBackWhenEnableFeature(policyID); diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 5fe09c4b6903..971f5684f76f 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -80,7 +80,6 @@ import * as ReportUtils from '@libs/ReportUtils'; import type {PolicySelector} from '@pages/home/sidebar/FloatingActionButtonAndPopover'; import * as PaymentMethods from '@userActions/PaymentMethods'; import * as PersistedRequests from '@userActions/PersistedRequests'; -import {resolveEnableFeatureConflicts} from '@userActions/RequestConflictUtils'; import {buildTaskData} from '@userActions/Task'; import {getOnboardingMessages} from '@userActions/Welcome/OnboardingFlow'; import type {OnboardingCompanySize, OnboardingPurpose} from '@userActions/Welcome/OnboardingFlow'; @@ -3101,9 +3100,7 @@ function enablePolicyConnections(policyID: string, enabled: boolean) { const parameters: EnablePolicyConnectionsParams = {policyID, enabled}; - API.write(WRITE_COMMANDS.ENABLE_POLICY_CONNECTIONS, parameters, onyxData, { - checkAndFixConflictingRequest: (persistedRequests) => resolveEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_CONNECTIONS, persistedRequests, parameters), - }); + API.writeWithNoDuplicatesEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_CONNECTIONS, parameters, onyxData); if (enabled && getIsNarrowLayout()) { goBackWhenEnableFeature(policyID); @@ -3160,9 +3157,7 @@ function enableExpensifyCard(policyID: string, enabled: boolean, shouldNavigateT const parameters: EnablePolicyExpensifyCardsParams = {authToken, policyID, enabled}; - API.write(WRITE_COMMANDS.ENABLE_POLICY_EXPENSIFY_CARDS, parameters, onyxData, { - checkAndFixConflictingRequest: (persistedRequests) => resolveEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_EXPENSIFY_CARDS, persistedRequests, parameters), - }); + API.writeWithNoDuplicatesEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_EXPENSIFY_CARDS, parameters, onyxData); if (enabled && shouldNavigateToExpensifyCardPage) { navigateToExpensifyCardPage(policyID); @@ -3217,9 +3212,7 @@ function enableCompanyCards(policyID: string, enabled: boolean, shouldGoBack = t const parameters: EnablePolicyCompanyCardsParams = {authToken, policyID, enabled}; - API.write(WRITE_COMMANDS.ENABLE_POLICY_COMPANY_CARDS, parameters, onyxData, { - checkAndFixConflictingRequest: (persistedRequests) => resolveEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_COMPANY_CARDS, persistedRequests, parameters), - }); + API.writeWithNoDuplicatesEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_COMPANY_CARDS, parameters, onyxData); if (enabled && getIsNarrowLayout() && shouldGoBack) { goBackWhenEnableFeature(policyID); @@ -3267,9 +3260,7 @@ function enablePolicyReportFields(policyID: string, enabled: boolean) { const parameters: EnablePolicyReportFieldsParams = {policyID, enabled}; - API.write(WRITE_COMMANDS.ENABLE_POLICY_REPORT_FIELDS, parameters, onyxData, { - checkAndFixConflictingRequest: (persistedRequests) => resolveEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_REPORT_FIELDS, persistedRequests, parameters), - }); + API.writeWithNoDuplicatesEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_REPORT_FIELDS, parameters, onyxData); } function enablePolicyTaxes(policyID: string, enabled: boolean) { @@ -3387,9 +3378,7 @@ function enablePolicyTaxes(policyID: string, enabled: boolean) { if (shouldAddDefaultTaxRatesData) { parameters.taxFields = JSON.stringify(defaultTaxRates); } - API.write(WRITE_COMMANDS.ENABLE_POLICY_TAXES, parameters, onyxData, { - checkAndFixConflictingRequest: (persistedRequests) => resolveEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_TAXES, persistedRequests, parameters), - }); + API.writeWithNoDuplicatesEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_TAXES, parameters, onyxData); if (enabled && getIsNarrowLayout()) { goBackWhenEnableFeature(policyID); @@ -3492,9 +3481,7 @@ function enablePolicyWorkflows(policyID: string, enabled: boolean) { setWorkspaceAutoReportingFrequency(policyID, CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT); } - API.write(WRITE_COMMANDS.ENABLE_POLICY_WORKFLOWS, parameters, onyxData, { - checkAndFixConflictingRequest: (persistedRequests) => resolveEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_WORKFLOWS, persistedRequests, parameters), - }); + API.writeWithNoDuplicatesEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_WORKFLOWS, parameters, onyxData); if (enabled && getIsNarrowLayout()) { goBackWhenEnableFeature(policyID); @@ -3560,9 +3547,7 @@ function enablePolicyRules(policyID: string, enabled: boolean, shouldGoBack = tr }; const parameters: SetPolicyRulesEnabledParams = {policyID, enabled}; - API.write(WRITE_COMMANDS.SET_POLICY_RULES_ENABLED, parameters, onyxData, { - checkAndFixConflictingRequest: (persistedRequests) => resolveEnableFeatureConflicts(WRITE_COMMANDS.SET_POLICY_RULES_ENABLED, persistedRequests, parameters), - }); + API.writeWithNoDuplicatesEnableFeatureConflicts(WRITE_COMMANDS.SET_POLICY_RULES_ENABLED, parameters, onyxData); if (enabled && getIsNarrowLayout() && shouldGoBack) { goBackWhenEnableFeature(policyID); @@ -3675,9 +3660,7 @@ function enablePolicyInvoicing(policyID: string, enabled: boolean) { const parameters: EnablePolicyInvoicingParams = {policyID, enabled}; - API.write(WRITE_COMMANDS.ENABLE_POLICY_INVOICING, parameters, onyxData, { - checkAndFixConflictingRequest: (persistedRequests) => resolveEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_INVOICING, persistedRequests, parameters), - }); + API.writeWithNoDuplicatesEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_INVOICING, parameters, onyxData); if (enabled && getIsNarrowLayout()) { goBackWhenEnableFeature(policyID); diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index afe9ef6365b8..2f98884b9cfd 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -28,7 +28,6 @@ import {goBackWhenEnableFeature} from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import {getTagArrayFromName} from '@libs/TransactionUtils'; import type {PolicyTagList} from '@pages/workspace/tags/types'; -import {resolveEnableFeatureConflicts} from '@userActions/RequestConflictUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ImportedSpreadsheet, Policy, PolicyTag, PolicyTagLists, PolicyTags, RecentlyUsedTags, Report} from '@src/types/onyx'; @@ -765,9 +764,7 @@ function enablePolicyTags(policyID: string, enabled: boolean) { const parameters: EnablePolicyTagsParams = {policyID, enabled}; - API.write(WRITE_COMMANDS.ENABLE_POLICY_TAGS, parameters, onyxData, { - checkAndFixConflictingRequest: (persistedRequests) => resolveEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_TAGS, persistedRequests, parameters), - }); + API.writeWithNoDuplicatesEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_TAGS, parameters, onyxData); if (enabled && getIsNarrowLayout()) { goBackWhenEnableFeature(policyID); diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 91f5655ef8f2..12e8dd2fb7d3 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1595,14 +1595,11 @@ function readNewestAction(reportID: string | undefined, shouldResetUnreadMarker lastReadTime, }; - API.write( + API.writeWithNoDuplicatesConflictAction( WRITE_COMMANDS.READ_NEWEST_ACTION, parameters, {optimisticData}, - { - checkAndFixConflictingRequest: (persistedRequests) => - resolveDuplicationConflictAction(persistedRequests, (request) => request.command === WRITE_COMMANDS.READ_NEWEST_ACTION && request.data?.reportID === parameters.reportID), - }, + (request) => request.command === WRITE_COMMANDS.READ_NEWEST_ACTION && request.data?.reportID === parameters.reportID, ); if (shouldResetUnreadMarker) { diff --git a/src/libs/actions/RequestConflictUtils.ts b/src/libs/actions/RequestConflictUtils.ts index 1cbeb2eb82de..359db4ada266 100644 --- a/src/libs/actions/RequestConflictUtils.ts +++ b/src/libs/actions/RequestConflictUtils.ts @@ -228,3 +228,5 @@ export { resolveEnableFeatureConflicts, enablePolicyFeatureCommand, }; + +export type {EnablePolicyFeatureCommand, RequestMatcher}; diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 65c2bb51c688..c406de940c55 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -56,7 +56,6 @@ import {reconnectApp} from './App'; import applyOnyxUpdatesReliably from './applyOnyxUpdatesReliably'; import {openOldDotLink} from './Link'; import {showReportActionNotification} from './Report'; -import {resolveDuplicationConflictAction} from './RequestConflictUtils'; import {resendValidateCode as sessionResendValidateCode} from './Session'; import Timing from './Timing'; @@ -814,14 +813,7 @@ function pingPusher() { lastPingSentTimestamp = pingTimestamp; const parameters: PusherPingParams = {pingID, pingTimestamp}; - API.write( - WRITE_COMMANDS.PUSHER_PING, - parameters, - {}, - { - checkAndFixConflictingRequest: (persistedRequests) => resolveDuplicationConflictAction(persistedRequests, (request) => request.command === WRITE_COMMANDS.PUSHER_PING), - }, - ); + API.writeWithNoDuplicatesConflictAction(WRITE_COMMANDS.PUSHER_PING, parameters); Log.info(`[Pusher PINGPONG] Sending a PING to the server: ${pingID} timestamp: ${pingTimestamp}`); Timing.start(CONST.TIMING.PUSHER_PING_PONG); } diff --git a/tests/unit/APITest.ts b/tests/unit/APITest.ts index 8a3d3b365516..4de2b79a862c 100644 --- a/tests/unit/APITest.ts +++ b/tests/unit/APITest.ts @@ -1,5 +1,6 @@ import MockedOnyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; +import type {EnablePolicyFeatureCommand} from '@libs/actions/RequestConflictUtils'; import type {ApiRequestCommandParameters, ReadCommand, WriteCommand} from '@libs/API/types'; import CONST from '@src/CONST'; import * as PersistedRequests from '@src/libs/actions/PersistedRequests'; @@ -626,4 +627,177 @@ describe('APITests', () => { const [thirdCommand] = xhr.mock.calls.at(2) ?? []; expect(thirdCommand).toBe('MockReadCommand'); }); + + test('duplicated write APIs with resolveDuplicationConflictAction conflict', () => { + const xhr = jest.spyOn(HttpUtils, 'xhr').mockResolvedValue({jsonCode: CONST.JSON_CODE.SUCCESS}); + return Onyx.multiSet({ + [ONYXKEYS.SESSION]: {authToken: 'anyToken'}, + [ONYXKEYS.NETWORK]: {isOffline: true}, + [ONYXKEYS.CREDENTIALS]: {autoGeneratedLogin: 'test_user', autoGeneratedPassword: 'password'}, + }) + .then(() => { + // When we queue 3 duplicate persistable commands + API.writeWithNoDuplicatesConflictAction('MockCommand' as WriteCommand, {content: 'value1'} as ApiRequestCommandParameters[WriteCommand]); + API.writeWithNoDuplicatesConflictAction('MockCommand' as WriteCommand, {content: 'value2'} as ApiRequestCommandParameters[WriteCommand]); + API.writeWithNoDuplicatesConflictAction('MockCommand' as WriteCommand, {content: 'value3'} as ApiRequestCommandParameters[WriteCommand]); + return waitForBatchedUpdates(); + }) + .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: false})) + .then(waitForBatchedUpdates) + .then(() => { + // Then expect only 1 call to have been made and for the Writes to be made at the last one that was made + expect(xhr.mock.calls.length).toBe(1); + expect(xhr.mock.calls.at(0)?.[1].content).toBe('value3'); + }); + }); + + test('different write APIs with resolveDuplicationConflictAction conflict', () => { + const xhr = jest.spyOn(HttpUtils, 'xhr').mockResolvedValue({jsonCode: CONST.JSON_CODE.SUCCESS}); + return Onyx.multiSet({ + [ONYXKEYS.SESSION]: {authToken: 'anyToken'}, + [ONYXKEYS.NETWORK]: {isOffline: true}, + [ONYXKEYS.CREDENTIALS]: {autoGeneratedLogin: 'test_user', autoGeneratedPassword: 'password'}, + }) + .then(() => { + // When we queue 3 different persistable commands + API.writeWithNoDuplicatesConflictAction('MockCommandOne' as WriteCommand, {content: 'value1'} as ApiRequestCommandParameters[WriteCommand]); + API.writeWithNoDuplicatesConflictAction('MockCommandTwo' as WriteCommand, {content: 'value2'} as ApiRequestCommandParameters[WriteCommand]); + API.writeWithNoDuplicatesConflictAction('MockCommandThree' as WriteCommand, {content: 'value3'} as ApiRequestCommandParameters[WriteCommand]); + return waitForBatchedUpdates(); + }) + .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: false})) + .then(waitForBatchedUpdates) + .then(() => { + // Then expect all 3 calls to have been made and for the Writes to be made in the order that we made them + expect(xhr.mock.calls.length).toBe(3); + expect(xhr.mock.calls.at(0)?.[1].content).toBe('value1'); + expect(xhr.mock.calls.at(1)?.[1].content).toBe('value2'); + expect(xhr.mock.calls.at(2)?.[1].content).toBe('value3'); + }); + }); + + test('duplicated write APIs with resolveEnableFeatureConflicts conflict and same policyID', () => { + const xhr = jest.spyOn(HttpUtils, 'xhr').mockResolvedValue({jsonCode: CONST.JSON_CODE.SUCCESS}); + return Onyx.multiSet({ + [ONYXKEYS.SESSION]: {authToken: 'anyToken'}, + [ONYXKEYS.NETWORK]: {isOffline: true}, + [ONYXKEYS.CREDENTIALS]: {autoGeneratedLogin: 'test_user', autoGeneratedPassword: 'password'}, + }) + .then(() => { + // When we queue 3 duplicate persistable commands with same policyID and different enabled values + API.writeWithNoDuplicatesEnableFeatureConflicts( + 'MockCommand' as EnablePolicyFeatureCommand, + {policyID: '1', enabled: true} as ApiRequestCommandParameters[EnablePolicyFeatureCommand], + ); + API.writeWithNoDuplicatesEnableFeatureConflicts( + 'MockCommand' as EnablePolicyFeatureCommand, + {policyID: '1', enabled: false} as ApiRequestCommandParameters[EnablePolicyFeatureCommand], + ); + API.writeWithNoDuplicatesEnableFeatureConflicts( + 'MockCommand' as EnablePolicyFeatureCommand, + {policyID: '1', enabled: true} as ApiRequestCommandParameters[EnablePolicyFeatureCommand], + ); + return waitForBatchedUpdates(); + }) + .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: false})) + .then(waitForBatchedUpdates) + .then(() => { + // Then expect only 1 call to have been made and for the Writes to be made at the first one that was made + expect(xhr.mock.calls.length).toBe(1); + expect(xhr.mock.calls.at(0)?.[1].enabled).toBe(true); + }); + }); + + test('consecutively enable and disable a feature with resolveEnableFeatureConflicts conflict', () => { + const xhr = jest.spyOn(HttpUtils, 'xhr').mockResolvedValue({jsonCode: CONST.JSON_CODE.SUCCESS}); + return Onyx.multiSet({ + [ONYXKEYS.SESSION]: {authToken: 'anyToken'}, + [ONYXKEYS.NETWORK]: {isOffline: true}, + [ONYXKEYS.CREDENTIALS]: {autoGeneratedLogin: 'test_user', autoGeneratedPassword: 'password'}, + }) + .then(() => { + // When we queue 2 duplicate persistable commands with same policyID and true and false enabled values + API.writeWithNoDuplicatesEnableFeatureConflicts( + 'MockCommand' as EnablePolicyFeatureCommand, + {policyID: '1', enabled: true} as ApiRequestCommandParameters[EnablePolicyFeatureCommand], + ); + API.writeWithNoDuplicatesEnableFeatureConflicts( + 'MockCommand' as EnablePolicyFeatureCommand, + {policyID: '1', enabled: false} as ApiRequestCommandParameters[EnablePolicyFeatureCommand], + ); + return waitForBatchedUpdates(); + }) + .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: false})) + .then(waitForBatchedUpdates) + .then(() => { + // Then expect no calls is made + expect(xhr.mock.calls.length).toBe(0); + }); + }); + + test('multiple write APIs with resolveEnableFeatureConflicts conflict with different policyIDs', () => { + const xhr = jest.spyOn(HttpUtils, 'xhr').mockResolvedValue({jsonCode: CONST.JSON_CODE.SUCCESS}); + return Onyx.multiSet({ + [ONYXKEYS.SESSION]: {authToken: 'anyToken'}, + [ONYXKEYS.NETWORK]: {isOffline: true}, + [ONYXKEYS.CREDENTIALS]: {autoGeneratedLogin: 'test_user', autoGeneratedPassword: 'password'}, + }) + .then(() => { + // When we queue 3 different persistable commands with different policyIDs + API.writeWithNoDuplicatesEnableFeatureConflicts( + 'MockCommand' as EnablePolicyFeatureCommand, + {policyID: '1', enabled: true} as ApiRequestCommandParameters[EnablePolicyFeatureCommand], + ); + API.writeWithNoDuplicatesEnableFeatureConflicts( + 'MockCommand' as EnablePolicyFeatureCommand, + {policyID: '2', enabled: false} as ApiRequestCommandParameters[EnablePolicyFeatureCommand], + ); + API.writeWithNoDuplicatesEnableFeatureConflicts( + 'MockCommand' as EnablePolicyFeatureCommand, + {policyID: '3', enabled: true} as ApiRequestCommandParameters[EnablePolicyFeatureCommand], + ); + return waitForBatchedUpdates(); + }) + .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: false})) + .then(waitForBatchedUpdates) + .then(() => { + // Then expect all 3 calls to have been made and for the Writes to be made in the order that we made them + expect(xhr.mock.calls.length).toBe(3); + expect(xhr.mock.calls.at(0)?.[1].policyID).toBe('1'); + expect(xhr.mock.calls.at(1)?.[1].policyID).toBe('2'); + expect(xhr.mock.calls.at(2)?.[1].policyID).toBe('3'); + }); + }); + + test('multiple write APIs with resolveEnableFeatureConflicts conflict with some different policyIDs', () => { + const xhr = jest.spyOn(HttpUtils, 'xhr').mockResolvedValue({jsonCode: CONST.JSON_CODE.SUCCESS}); + return Onyx.multiSet({ + [ONYXKEYS.SESSION]: {authToken: 'anyToken'}, + [ONYXKEYS.NETWORK]: {isOffline: true}, + [ONYXKEYS.CREDENTIALS]: {autoGeneratedLogin: 'test_user', autoGeneratedPassword: 'password'}, + }) + .then(() => { + // When we queue 3 different persistable commands with only 2 policyIDs + API.writeWithNoDuplicatesEnableFeatureConflicts( + 'MockCommand' as EnablePolicyFeatureCommand, + {policyID: '1', enabled: true} as ApiRequestCommandParameters[EnablePolicyFeatureCommand], + ); + API.writeWithNoDuplicatesEnableFeatureConflicts( + 'MockCommand' as EnablePolicyFeatureCommand, + {policyID: '2', enabled: false} as ApiRequestCommandParameters[EnablePolicyFeatureCommand], + ); + API.writeWithNoDuplicatesEnableFeatureConflicts( + 'MockCommand' as EnablePolicyFeatureCommand, + {policyID: '1', enabled: false} as ApiRequestCommandParameters[EnablePolicyFeatureCommand], + ); + return waitForBatchedUpdates(); + }) + .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: false})) + .then(waitForBatchedUpdates) + .then(() => { + // Then expect only 1 call to have been made and for the Writes that have unique policyID to be made + expect(xhr.mock.calls.length).toBe(1); + expect(xhr.mock.calls.at(0)?.[1].policyID).toBe('2'); + }); + }); });