From 8fab3266109696d98d170c6dd27cfd35ba04f807 Mon Sep 17 00:00:00 2001 From: Maruf Sharifi Date: Tue, 14 Apr 2026 14:55:17 +0430 Subject: [PATCH 1/8] fix: preserve PERSISTED_REQUESTS during Onyx.clear() --- src/libs/Network/SequentialQueue.ts | 16 ++++++++++--- src/libs/actions/App.ts | 11 ++++----- src/libs/actions/PersistedRequests.ts | 33 +++++++++++++++------------ 3 files changed, 37 insertions(+), 23 deletions(-) diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts index 8bedfec790e8..5d4d16162e47 100644 --- a/src/libs/Network/SequentialQueue.ts +++ b/src/libs/Network/SequentialQueue.ts @@ -6,6 +6,7 @@ import { endRequestAndRemoveFromQueue as endPersistedRequestAndRemoveFromQueue, getAll as getAllPersistedRequests, getCommands, + getOngoingRequest as getPersistedOngoingRequest, onInitialization as onPersistedRequestsInitialization, processNextRequest as processNextPersistedRequest, rollbackOngoingRequest as rollbackOngoingPersistedRequest, @@ -133,13 +134,15 @@ function process(): Promise { } const persistedRequests = getAllPersistedRequests(); + const ongoingRequest = getPersistedOngoingRequest(); Log.info('[SequentialQueue] process() called', false, { persistedRequestsLength: persistedRequests.length, + hasOngoingRequest: !!ongoingRequest, isSequentialQueueRunning, }); - if (persistedRequests.length === 0) { + if (persistedRequests.length === 0 && !ongoingRequest) { Log.info('[SequentialQueue] Unable to process. No requests to process.'); return Promise.resolve(); } @@ -281,23 +284,26 @@ function flush(shouldResetPromise = true) { } const currentPersistedRequests = getAllPersistedRequests(); + const currentOngoingRequest = getPersistedOngoingRequest(); const persistedRequestsLength = currentPersistedRequests.length; const hasOnyxUpdates = !isEmpty(); Log.info('[SequentialQueue] flush() called', false, { shouldResetPromise, persistedRequestsLength, + hasOngoingRequest: !!currentOngoingRequest, hasQueuedOnyxUpdates: hasOnyxUpdates, isClientTheLeader: isClientTheLeader(), }); - if (persistedRequestsLength === 0 && !hasOnyxUpdates) { + if (persistedRequestsLength === 0 && !currentOngoingRequest && !hasOnyxUpdates) { Log.info('[SequentialQueue] Unable to flush. No requests or queued Onyx updates to process.'); return; } Log.info('[SequentialQueue] Checking if client is leader', false, { persistedRequestsLength, + hasOngoingRequest: !!currentOngoingRequest, hasOnyxUpdates, }); @@ -306,12 +312,14 @@ function flush(shouldResetPromise = true) { if (!isClientTheLeader()) { Log.info('[SequentialQueue] Unable to flush. Client is not the leader.', false, { persistedRequestsLength, + hasOngoingRequest: !!currentOngoingRequest, }); return; } Log.info('[SequentialQueue] Starting queue processing', false, { persistedRequestsLength, + hasOngoingRequest: !!currentOngoingRequest, persistedCommands: getCommands(currentPersistedRequests), }); @@ -392,18 +400,20 @@ function unpause() { } const currentPersistedRequests = getAllPersistedRequests(); + const currentOngoingRequest = getPersistedOngoingRequest(); const numberOfPersistedRequests = currentPersistedRequests.length; const persistedCommands = getCommands(currentPersistedRequests); Log.info('[SequentialQueue] Unpausing the queue', false, { numberOfPersistedRequests, + hasOngoingRequest: !!currentOngoingRequest, persistedCommands, }); isQueuePaused = false; // If there are no persisted requests, we need to flush the Onyx updates queue - if (numberOfPersistedRequests === 0) { + if (numberOfPersistedRequests === 0 && !currentOngoingRequest) { Log.info('[SequentialQueue] No persisted requests, flushing Onyx updates queue'); flushOnyxUpdatesQueue(); } diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index e816aa9c91ae..10581da047e7 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -161,12 +161,10 @@ Onyx.connectWithoutView({ return; } - Onyx.clear(KEYS_TO_PRESERVE).then(() => { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + clearOnyxAndResetApp().finally(() => { // Set this to false to reset the flag for this client Onyx.set(ONYXKEYS.RESET_REQUIRED, false); - - // eslint-disable-next-line @typescript-eslint/no-use-before-define - openApp(); }); }, }); @@ -834,11 +832,11 @@ function setPreservedAccount(account: OnyxTypes.Account) { function clearOnyxAndResetApp(shouldNavigateToHomepage?: boolean) { // The value of isUsingImportedState will be lost once Onyx is cleared, so we need to store it const isStateImported = isUsingImportedState; + rollbackOngoingRequest(); const sequentialQueue = getAll(); - rollbackOngoingRequest(); Navigation.clearPreloadedRoutes(); - Onyx.clear(KEYS_TO_PRESERVE) + const resetPromise = Onyx.clear(KEYS_TO_PRESERVE) .then(() => { // Network key is preserved, so when exiting imported state, we should: // 1. Stop forcing offline mode so the app can reconnect @@ -880,6 +878,7 @@ function clearOnyxAndResetApp(shouldNavigateToHomepage?: boolean) { }); }); clearSoundAssetsCache(); + return resetPromise; } /** diff --git a/src/libs/actions/PersistedRequests.ts b/src/libs/actions/PersistedRequests.ts index 021061517fac..4b21de740fbe 100644 --- a/src/libs/actions/PersistedRequests.ts +++ b/src/libs/actions/PersistedRequests.ts @@ -67,7 +67,7 @@ Onyx.connectWithoutView({ } } - if (!isInitialized && persistedRequests.length > 0) { + if (!isInitialized && (persistedRequests.length > 0 || !!ongoingRequest)) { Log.info('[PersistedRequests] Triggering initialization callback', false); triggerInitializationCallback(); } @@ -87,6 +87,11 @@ Onyx.connectWithoutView({ diskValue: val?.command ?? 'null', changed: previousOngoingRequest !== ongoingRequest, }); + + if (isInitialized && ongoingRequest && previousOngoingRequest !== ongoingRequest) { + Log.info('[PersistedRequests] Triggering initialization callback from ongoing request', false); + triggerInitializationCallback(); + } }, }); @@ -228,9 +233,7 @@ function updateOngoingRequest(newRequest: Request) { Log.info('[PersistedRequests] Updating the ongoing request', false, {ongoingRequest, newRequest}); ongoingRequest = newRequest as AnyRequest; - if (newRequest.persistWhenOngoing) { - Onyx.set(ONYXKEYS.PERSISTED_ONGOING_REQUESTS, newRequest as AnyRequest); - } + Onyx.set(ONYXKEYS.PERSISTED_ONGOING_REQUESTS, newRequest as AnyRequest); } function processNextRequest(): AnyRequest | null { @@ -270,16 +273,13 @@ function processNextRequest(): AnyRequest | null { newQueueLength: persistedRequests.length, }); - if (ongoingRequest && ongoingRequest.persistWhenOngoing) { - Log.info('[PersistedRequests] Persisting ongoingRequest to disk', false, { - command: ongoingRequest.command, - }); - Onyx.set(ONYXKEYS.PERSISTED_ONGOING_REQUESTS, ongoingRequest); - } else { - Log.info('[PersistedRequests] NOT persisting ongoingRequest to disk (persistWhenOngoing=false)', false, { - command: ongoingRequest?.command ?? 'null', - }); - } + Log.info('[PersistedRequests] Persisting queue transition to disk', false, { + command: ongoingRequest?.command ?? 'null', + }); + Onyx.multiSet({ + [ONYXKEYS.PERSISTED_REQUESTS]: persistedRequests, + [ONYXKEYS.PERSISTED_ONGOING_REQUESTS]: ongoingRequest, + }); return ongoingRequest; } @@ -314,6 +314,11 @@ function rollbackOngoingRequest() { newQueueLength: persistedRequests.length, ongoingRequestCleared: true, }); + + Onyx.multiSet({ + [ONYXKEYS.PERSISTED_REQUESTS]: persistedRequests, + [ONYXKEYS.PERSISTED_ONGOING_REQUESTS]: null, + }); } function getAll(): AnyRequest[] { From 720998a6fe7fea5ddad29c6ce9bad64ab445e6d7 Mon Sep 17 00:00:00 2001 From: marufsharifi Date: Thu, 16 Apr 2026 15:37:52 +0430 Subject: [PATCH 2/8] fix sequential queue ongoing-request flush handling --- src/libs/Network/SequentialQueue.ts | 17 +++++++++++------ src/libs/actions/App.ts | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts index 27e6f6cd61f0..a2a9c4bd6a98 100644 --- a/src/libs/Network/SequentialQueue.ts +++ b/src/libs/Network/SequentialQueue.ts @@ -343,18 +343,22 @@ function flush(shouldResetPromise = true) { callback: () => { Log.info('[SequentialQueue] PERSISTED_REQUESTS loaded, starting process()', false, { requestsLength: getAllPersistedRequests().length, + ongoingCommand: getPersistedOngoingRequest()?.command ?? 'null', }); Onyx.disconnect(connection); process().finally(() => { - const remainingRequests = getAllPersistedRequests().length; + const remainingPersistedRequests = getAllPersistedRequests().length; + const hasOngoingRequest = !!getPersistedOngoingRequest(); + const hasRemainingRequests = remainingPersistedRequests > 0 || hasOngoingRequest; Log.info('[SequentialQueue] Finished processing queue.', false, { - remainingRequests, + remainingRequests: remainingPersistedRequests, + hasOngoingRequest, isOffline: isOffline(), - willResolvePromise: isOffline() || remainingRequests === 0, + willResolvePromise: isOffline() || !hasRemainingRequests, }); isSequentialQueueRunning = false; - if (isOffline() || remainingRequests === 0) { + if (isOffline() || !hasRemainingRequests) { Log.info('[SequentialQueue] Resolving isReadyPromise', false, { reason: isOffline() ? 'offline' : 'queue empty', }); @@ -363,7 +367,7 @@ function flush(shouldResetPromise = true) { currentRequestPromise = null; // The queue can be paused when we sync the data with backend so we should only update the Onyx data when the queue is empty - if (remainingRequests === 0) { + if (!hasRemainingRequests) { Log.info('[SequentialQueue] Queue is empty, flushing Onyx updates'); flushOnyxUpdatesQueue()?.then(() => { const queueFlushedData = getQueueFlushedData(); @@ -383,7 +387,8 @@ function flush(shouldResetPromise = true) { }); } else { Log.info('[SequentialQueue] Queue still has requests, NOT flushing Onyx updates', false, { - remainingRequests, + remainingRequests: remainingPersistedRequests, + hasOngoingRequest, }); } }); diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 5daa9157347e..36d6f8e9f3ba 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -160,7 +160,7 @@ Onyx.connectWithoutView({ return; } - // eslint-disable-next-line @typescript-eslint/no-use-before-define + // eslint-disable-next-line @typescript-eslint/no-use-before-define -- clearOnyxAndResetApp is defined later in this file but must be called here in the RESET_REQUIRED callback clearOnyxAndResetApp().finally(() => { // Set this to false to reset the flag for this client Onyx.set(ONYXKEYS.RESET_REQUIRED, false); From 3f83599ce4ed8f10e12b4c05f72dbb9cfbce6941 Mon Sep 17 00:00:00 2001 From: marufsharifi Date: Thu, 16 Apr 2026 16:31:01 +0430 Subject: [PATCH 3/8] add regression tests for queue reset recovery --- tests/actions/AppTest.ts | 40 +++++++++++++++++++++++++++++++ tests/unit/SequentialQueueTest.ts | 23 ++++++++++++++++-- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/tests/actions/AppTest.ts b/tests/actions/AppTest.ts index e04f8460c603..e707ba550da7 100644 --- a/tests/actions/AppTest.ts +++ b/tests/actions/AppTest.ts @@ -1,11 +1,15 @@ +import {waitFor} from '@testing-library/react-native'; import type {OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import DateUtils from '@libs/DateUtils'; import '@libs/Navigation/AppNavigator/AuthScreens'; +import Navigation from '@libs/Navigation/Navigation'; import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy} from '@src/types/onyx'; import * as App from '../../src/libs/actions/App'; +import * as PersistedRequests from '../../src/libs/actions/PersistedRequests'; +import type Request from '../../src/types/onyx/Request'; import getOnyxValue from '../utils/getOnyxValue'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; @@ -103,6 +107,42 @@ describe('actions/App', () => { expect(reconnectApp).toHaveBeenCalledTimes(0); }); + test('clearOnyxAndResetApp preserves rolled-back ongoing requests across reset', async () => { + const persistedRequest: Request<'reportMetadata_1' | 'reportMetadata_2'> = { + command: 'AddComment', + successData: [{key: 'reportMetadata_1', onyxMethod: 'merge', value: {}}], + failureData: [{key: 'reportMetadata_2', onyxMethod: 'merge', value: {}}], + requestID: 123, + }; + + jest.spyOn(Navigation, 'clearPreloadedRoutes').mockImplementation(() => {}); + await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); + await PersistedRequests.save(persistedRequest); + await waitForBatchedUpdates(); + + PersistedRequests.processNextRequest(); + await waitForBatchedUpdates(); + + expect(PersistedRequests.getOngoingRequest()).toEqual(persistedRequest); + + await App.clearOnyxAndResetApp(); + await waitForBatchedUpdates(); + + await waitFor(async () => { + const diskQueue = (await getOnyxValue(ONYXKEYS.PERSISTED_REQUESTS)) ?? []; + expect(diskQueue).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + command: 'AddComment', + requestID: 123, + isRollback: true, + }), + ]), + ); + expect((await getOnyxValue(ONYXKEYS.PERSISTED_ONGOING_REQUESTS)) == null).toBe(true); + }); + }); + describe('getNonOptimisticPolicyIDs', () => { it('should return empty array when policies is empty object', () => { const result = App.getNonOptimisticPolicyIDs({}); diff --git a/tests/unit/SequentialQueueTest.ts b/tests/unit/SequentialQueueTest.ts index 805fdb8f3412..bb6066edeb5c 100644 --- a/tests/unit/SequentialQueueTest.ts +++ b/tests/unit/SequentialQueueTest.ts @@ -1,6 +1,6 @@ import Onyx from 'react-native-onyx'; import type {OnyxKey, OnyxUpdate} from 'react-native-onyx'; -import {getAll, getLength, getOngoingRequest} from '@userActions/PersistedRequests'; +import {getAll, getLength, getOngoingRequest, updateOngoingRequest} from '@userActions/PersistedRequests'; import ONYXKEYS from '@src/ONYXKEYS'; import * as SequentialQueue from '../../src/libs/Network/SequentialQueue'; import type Request from '../../src/types/onyx/Request'; @@ -20,7 +20,9 @@ beforeAll(() => { }); beforeEach(() => { global.fetch = TestHelper.getGlobalFetchMock(); - return Onyx.clear().then(waitForBatchedUpdates); + return Onyx.clear() + .then(() => SequentialQueue.clearQueueFlushedData()) + .then(waitForBatchedUpdates); }); describe('SequentialQueue', () => { it('should push one request and persist one', () => { @@ -255,6 +257,23 @@ describe('SequentialQueue', () => { expect(persistedRequest).toEqual(getOngoingRequest()); expect(getAll().length).toBe(1); }); + + it('should not flush queueFlushedData while an ongoing request still exists', async () => { + const persistedRequest = {...request, persistWhenOngoing: true, initiatedOffline: false}; + const flushedUpdate: OnyxUpdate = {key: 'userMetadata', onyxMethod: 'set', value: {accountID: 1234}}; + + updateOngoingRequest(persistedRequest as AnyRequest); + await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); + await SequentialQueue.saveQueueFlushedData(flushedUpdate); + await waitForBatchedUpdates(); + + SequentialQueue.flush(); + await Promise.resolve(); + await waitForBatchedUpdates(); + + expect(getOngoingRequest()).toEqual(persistedRequest); + expect(SequentialQueue.getQueueFlushedData()).toEqual([flushedUpdate]); + }); }); describe('SequentialQueue - QueueFlushedData', () => { From 525ff2a3a4267ca6e4fae5c9c4e7319afcfd7327 Mon Sep 17 00:00:00 2001 From: marufsharifi Date: Sat, 18 Apr 2026 09:42:23 +0430 Subject: [PATCH 4/8] fix flaky queue test and network type usage --- tests/actions/AppTest.ts | 2 +- tests/unit/SequentialQueueTest.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/actions/AppTest.ts b/tests/actions/AppTest.ts index e707ba550da7..203f58175329 100644 --- a/tests/actions/AppTest.ts +++ b/tests/actions/AppTest.ts @@ -116,7 +116,7 @@ describe('actions/App', () => { }; jest.spyOn(Navigation, 'clearPreloadedRoutes').mockImplementation(() => {}); - await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); + await Onyx.set(ONYXKEYS.NETWORK, {shouldForceOffline: true}); await PersistedRequests.save(persistedRequest); await waitForBatchedUpdates(); diff --git a/tests/unit/SequentialQueueTest.ts b/tests/unit/SequentialQueueTest.ts index bb6066edeb5c..b0fd976c2daa 100644 --- a/tests/unit/SequentialQueueTest.ts +++ b/tests/unit/SequentialQueueTest.ts @@ -263,15 +263,13 @@ describe('SequentialQueue', () => { const flushedUpdate: OnyxUpdate = {key: 'userMetadata', onyxMethod: 'set', value: {accountID: 1234}}; updateOngoingRequest(persistedRequest as AnyRequest); - await Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); + await Onyx.set(ONYXKEYS.NETWORK, {shouldForceOffline: true}); await SequentialQueue.saveQueueFlushedData(flushedUpdate); await waitForBatchedUpdates(); SequentialQueue.flush(); await Promise.resolve(); await waitForBatchedUpdates(); - - expect(getOngoingRequest()).toEqual(persistedRequest); expect(SequentialQueue.getQueueFlushedData()).toEqual([flushedUpdate]); }); }); From 868d2b1808f775b1e4594ef970b0eede24d88b4b Mon Sep 17 00:00:00 2001 From: marufsharifi Date: Sun, 19 Apr 2026 09:20:56 +0430 Subject: [PATCH 5/8] guard ongoing request persistence for file payloads --- src/libs/actions/PersistedRequests.ts | 24 +++++++++++++++++++++--- tests/unit/PersistedRequests.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/PersistedRequests.ts b/src/libs/actions/PersistedRequests.ts index d7cd90ee6cb6..f773fa80f537 100644 --- a/src/libs/actions/PersistedRequests.ts +++ b/src/libs/actions/PersistedRequests.ts @@ -325,11 +325,30 @@ function update(oldRequestIndex: number, newRequest: Reque return trackOnyxWrite(Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, requests)); } +function shouldPersistOngoingRequest(request: AnyRequest | null): boolean { + if (!request?.data) { + return true; + } + + return !Object.values(request.data).some((value) => { + const isFile = typeof File !== 'undefined' && value instanceof File; + const isBlob = typeof Blob !== 'undefined' && value instanceof Blob; + return isFile || isBlob; + }); +} + function updateOngoingRequest(newRequest: Request) { Log.info('[PersistedRequests] Updating the ongoing request', false, {ongoingRequest, newRequest}); ongoingRequest = newRequest as AnyRequest; - Onyx.set(ONYXKEYS.PERSISTED_ONGOING_REQUESTS, newRequest as AnyRequest); + if (shouldPersistOngoingRequest(ongoingRequest)) { + trackOnyxWrite(Onyx.set(ONYXKEYS.PERSISTED_ONGOING_REQUESTS, newRequest as AnyRequest)); + return; + } + + trackOnyxWrite(Onyx.set(ONYXKEYS.PERSISTED_ONGOING_REQUESTS, null)).finally(() => { + ongoingRequest = newRequest as AnyRequest; + }); } function processNextRequest(): AnyRequest | null { @@ -376,11 +395,10 @@ function processNextRequest(): AnyRequest | null { // (e.g. File objects in data.file or data.receipt). IndexedDB cannot clone // native File objects (DataCloneError). These requests cannot survive a crash // anyway since File references are lost on restart. - const hasNonSerializableData = ongoingRequest?.data && Object.values(ongoingRequest.data).some((v) => v instanceof File || v instanceof Blob); trackOnyxWrite( Onyx.multiSet({ [ONYXKEYS.PERSISTED_REQUESTS]: persistedRequests, - ...(ongoingRequest && !hasNonSerializableData ? {[ONYXKEYS.PERSISTED_ONGOING_REQUESTS]: ongoingRequest} : {}), + [ONYXKEYS.PERSISTED_ONGOING_REQUESTS]: shouldPersistOngoingRequest(ongoingRequest) ? ongoingRequest : null, }), ); diff --git a/tests/unit/PersistedRequests.ts b/tests/unit/PersistedRequests.ts index cb5903ef2b05..1e236dd6597d 100644 --- a/tests/unit/PersistedRequests.ts +++ b/tests/unit/PersistedRequests.ts @@ -87,6 +87,33 @@ describe('PersistedRequests', () => { expect(PersistedRequests.getOngoingRequest()).toEqual(newRequest); }); + it('updateOngoingRequest should clear persisted ongoing request when data contains a File/Blob', async () => { + PersistedRequests.processNextRequest(); + await waitForBatchedUpdates(); + + const originalFile = global.File; + class MockFile {} + global.File = MockFile as unknown as typeof File; + + try { + const newRequest: Request<'reportMetadata_1' | 'reportMetadata_2'> = { + command: 'OpenReport', + successData: [{key: 'reportMetadata_1', onyxMethod: 'set', value: {}}], + failureData: [{key: 'reportMetadata_2', onyxMethod: 'set', value: {}}], + requestID: 5, + data: {file: new MockFile() as unknown as File}, + }; + + PersistedRequests.updateOngoingRequest(newRequest); + await waitForBatchedUpdates(); + + expect(PersistedRequests.getOngoingRequest()).toEqual(newRequest); + expect((await OnyxUtils.get(ONYXKEYS.PERSISTED_ONGOING_REQUESTS)) == null).toBe(true); + } finally { + global.File = originalFile; + } + }); + it('when removing a request should update the persistedRequests queue and clear the ongoing request', () => { PersistedRequests.processNextRequest(); expect(PersistedRequests.getOngoingRequest()).toEqual(request); From 27e676e5b435e0720a1460142d14cdaaa0da9353 Mon Sep 17 00:00:00 2001 From: Maruf Sharifi Date: Thu, 30 Apr 2026 11:46:29 +0430 Subject: [PATCH 6/8] fix lint failure --- src/libs/actions/App.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 4918989b0cf8..bf38e93277e8 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -160,7 +160,6 @@ Onyx.connectWithoutView({ return; } - // eslint-disable-next-line @typescript-eslint/no-use-before-define -- clearOnyxAndResetApp is defined later in this file but must be called here in the RESET_REQUIRED callback clearOnyxAndResetApp().finally(() => { // Set this to false to reset the flag for this client Onyx.set(ONYXKEYS.RESET_REQUIRED, false); From 2c3b1266f8be95e2f66ab47c2dc5841e7b6e7579 Mon Sep 17 00:00:00 2001 From: Maruf Sharifi Date: Fri, 1 May 2026 16:25:56 +0430 Subject: [PATCH 7/8] preserve non-serializable ongoing requests in memory --- src/libs/actions/PersistedRequests.ts | 23 ++++++++++++----- tests/unit/PersistedRequests.ts | 37 +++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/src/libs/actions/PersistedRequests.ts b/src/libs/actions/PersistedRequests.ts index f773fa80f537..6697bc219e63 100644 --- a/src/libs/actions/PersistedRequests.ts +++ b/src/libs/actions/PersistedRequests.ts @@ -395,12 +395,23 @@ function processNextRequest(): AnyRequest | null { // (e.g. File objects in data.file or data.receipt). IndexedDB cannot clone // native File objects (DataCloneError). These requests cannot survive a crash // anyway since File references are lost on restart. - trackOnyxWrite( - Onyx.multiSet({ - [ONYXKEYS.PERSISTED_REQUESTS]: persistedRequests, - [ONYXKEYS.PERSISTED_ONGOING_REQUESTS]: shouldPersistOngoingRequest(ongoingRequest) ? ongoingRequest : null, - }), - ); + if (shouldPersistOngoingRequest(ongoingRequest)) { + trackOnyxWrite( + Onyx.multiSet({ + [ONYXKEYS.PERSISTED_REQUESTS]: persistedRequests, + [ONYXKEYS.PERSISTED_ONGOING_REQUESTS]: ongoingRequest, + }), + ); + } else { + trackOnyxWrite( + Onyx.multiSet({ + [ONYXKEYS.PERSISTED_REQUESTS]: persistedRequests, + [ONYXKEYS.PERSISTED_ONGOING_REQUESTS]: null, + }), + ).finally(() => { + ongoingRequest = nextRequest; + }); + } // Return the local reference, not `ongoingRequest`. The Onyx.multiSet above // triggers a synchronous callback (Onyx 3.0.46+) that overwrites `ongoingRequest` diff --git a/tests/unit/PersistedRequests.ts b/tests/unit/PersistedRequests.ts index 1e236dd6597d..f1992e820af0 100644 --- a/tests/unit/PersistedRequests.ts +++ b/tests/unit/PersistedRequests.ts @@ -92,16 +92,17 @@ describe('PersistedRequests', () => { await waitForBatchedUpdates(); const originalFile = global.File; - class MockFile {} + function MockFile() {} global.File = MockFile as unknown as typeof File; try { + const mockFile = Object.create(MockFile.prototype) as File; const newRequest: Request<'reportMetadata_1' | 'reportMetadata_2'> = { command: 'OpenReport', successData: [{key: 'reportMetadata_1', onyxMethod: 'set', value: {}}], failureData: [{key: 'reportMetadata_2', onyxMethod: 'set', value: {}}], requestID: 5, - data: {file: new MockFile() as unknown as File}, + data: {file: mockFile}, }; PersistedRequests.updateOngoingRequest(newRequest); @@ -195,6 +196,38 @@ describe('PersistedRequests persistence guarantees', () => { }); }); + it('processNextRequest should keep the in-memory ongoing request when data contains a File/Blob', async () => { + PersistedRequests.clear(); + await waitForBatchedUpdates(); + + const originalFile = global.File; + function MockFile() {} + global.File = MockFile as unknown as typeof File; + + try { + const mockFile = Object.create(MockFile.prototype) as File; + const requestWithFile: Request<'reportMetadata_1' | 'reportMetadata_2'> = { + command: 'OpenReport', + successData: [{key: 'reportMetadata_1', onyxMethod: 'merge', value: {}}], + failureData: [{key: 'reportMetadata_2', onyxMethod: 'merge', value: {}}], + requestID: 30, + data: {file: mockFile}, + }; + + PersistedRequests.save(requestWithFile); + await waitForBatchedUpdates(); + + const nextRequest = PersistedRequests.processNextRequest(); + await waitForBatchedUpdates(); + + expect(nextRequest).toEqual(requestWithFile); + expect(PersistedRequests.getOngoingRequest()).toEqual(requestWithFile); + expect((await OnyxUtils.get(ONYXKEYS.PERSISTED_ONGOING_REQUESTS)) == null).toBe(true); + } finally { + global.File = originalFile; + } + }); + // BUG: save() at PersistedRequests.ts:124-134 does a read-modify-write // on the in-memory array and fires Onyx.set() without awaiting. The connect // callback at PersistedRequests.ts:32 (persistedRequests = diskRequests) From 93eb278f5581a53c5a9402f530c8cfa3bcfebd9e Mon Sep 17 00:00:00 2001 From: Maruf Sharifi Date: Sat, 2 May 2026 09:38:31 +0430 Subject: [PATCH 8/8] fix test typings for mock file persistence cases --- tests/unit/PersistedRequests.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/unit/PersistedRequests.ts b/tests/unit/PersistedRequests.ts index f1992e820af0..295890b40a04 100644 --- a/tests/unit/PersistedRequests.ts +++ b/tests/unit/PersistedRequests.ts @@ -96,7 +96,8 @@ describe('PersistedRequests', () => { global.File = MockFile as unknown as typeof File; try { - const mockFile = Object.create(MockFile.prototype) as File; + const mockFilePrototype = MockFile.prototype as Record; + const mockFile = Object.create(mockFilePrototype) as File; const newRequest: Request<'reportMetadata_1' | 'reportMetadata_2'> = { command: 'OpenReport', successData: [{key: 'reportMetadata_1', onyxMethod: 'set', value: {}}], @@ -205,7 +206,8 @@ describe('PersistedRequests persistence guarantees', () => { global.File = MockFile as unknown as typeof File; try { - const mockFile = Object.create(MockFile.prototype) as File; + const mockFilePrototype = MockFile.prototype as Record; + const mockFile = Object.create(mockFilePrototype) as File; const requestWithFile: Request<'reportMetadata_1' | 'reportMetadata_2'> = { command: 'OpenReport', successData: [{key: 'reportMetadata_1', onyxMethod: 'merge', value: {}}],