Skip to content

[$250] App update clears Onyx data not synced with server / PERSISTED_REQUESTS must be preserved during Onyx.clear() #86710

@MelvinBot

Description

@MelvinBot

Split from https://github.com/Expensify/Expensify/issues/618149

Problem

When Onyx.clear() is called (e.g., via the RESET_REQUIRED listener), the PERSISTED_REQUESTS queue — which holds all offline API requests waiting to sync — is wiped because it is not in the KEYS_TO_PRESERVE list. This causes permanent loss of offline-created expenses that were never synced to the server.

Customers are reporting missing expenses after app updates. Investigation confirms these expenses were created offline and existed only in the Onyx cache / SequentialQueue.

Root Cause

There are three data loss vectors:

1. RESET_REQUIRED listener wipes offline queue

The listener at App.ts:154-168 calls Onyx.clear(KEYS_TO_PRESERVE) then openApp() without saving/restoring the SequentialQueue.

Contrast with clearOnyxAndResetApp() at App.ts:827-876 which correctly snapshots the queue first:

function clearOnyxAndResetApp() {
    const sequentialQueue = getAll();          // ← Saves queue first
    rollbackOngoingRequest();
    Onyx.clear(KEYS_TO_PRESERVE).then(() => {
        openApp().then(() => {
            for (const request of sequentialQueue) { save(request); }  // ← Restores queue
        });
    });
}

2. Full reconnect replaces local state

When the server returns previousUpdateID=0 (triggered when clientUpdateID is stale or -1), a Pusher ReconnectApp event causes a full state replacement from the server — which knows nothing about expenses that were never synced.

3. Processing window vulnerability

In PersistedRequests.ts:255-284, when a request starts processing, it's removed from PERSISTED_REQUESTS and held only in memory. Expense creation commands do not set persistWhenOngoing, so if the app is killed during this window, the request is lost.

Recommended Fix

  1. Add ONYXKEYS.PERSISTED_REQUESTS and ONYXKEYS.PERSISTED_ONGOING_REQUESTS to the KEYS_TO_PRESERVE list in App.ts:122-146, OR modify the RESET_REQUIRED listener to snapshot/restore the queue the same way clearOnyxAndResetApp() already does.

  2. Set persistWhenOngoing: true for expense creation commands (REQUEST_MONEY, TRACK_EXPENSE, SPLIT_BILL_AND_OPEN_REPORT, etc.) so in-flight requests survive app kills.

Relevant Code

Implementation Notes

  • Adding PERSISTED_REQUESTS to KEYS_TO_PRESERVE is the simplest fix for the RESET_REQUIRED path, but care must be taken to ensure the queue is processed correctly after openApp() refreshes the Onyx state (since the optimistic data keys the queue references will have been cleared)
  • The persistWhenOngoing flag already exists in the infrastructure — expense creation commands just need to opt into it
Issue OwnerCurrent Issue Owner: @ShridharGoel
Upwork Automation - Do Not Edit
  • Upwork Job URL: https://www.upwork.com/jobs/~022039242963579817769
  • Upwork Job ID: 2039242963579817769
  • Last Price Increase: 2026-04-01

Metadata

Metadata

Labels

DailyKSv2ExternalAdded to denote the issue can be worked on by a contributorHelp WantedApply this label when an issue is open to proposals by contributorsImprovementItem broken or needs improvement.

Type

No type

Projects

Status

HIGH

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions