@@ -842,8 +842,20 @@ function retryOperation<TMethod extends RetriableOnyxOperation>(error: Error, on
842842 Logger . logInfo ( `Out of storage. Evicting least recently accessed key (${ keyForRemoval } ) and retrying. Error: ${ error } ` ) ;
843843 reportStorageQuota ( error ) ;
844844
845- // @ts -expect-error No overload matches this call.
846- return remove ( keyForRemoval ) . then ( ( ) => onyxMethod ( defaultParams , nextRetryAttempt ) ) ;
845+ return remove ( keyForRemoval ) . then ( ( ) => {
846+ // remove() drops `keyForRemoval` from cache and fires keyChanged(undefined). If that key is
847+ // part of this in-flight write, the upcoming retry would land the value in storage but
848+ // cache + subscribers would stay in the "removed" state. Each orchestrator passes a
849+ // restoreEvictedKey closure via params that re-applies the orchestrator's cache write +
850+ // subscriber notification for the evicted key only — preserving the cache-first invariant
851+ // across eviction-driven retries.
852+ const restore = ( defaultParams as { restoreEvictedKey ?: ( key : OnyxKey ) => void } ) . restoreEvictedKey ;
853+ if ( typeof restore === 'function' ) {
854+ restore ( keyForRemoval ) ;
855+ }
856+ // @ts -expect-error No overload matches this call.
857+ return onyxMethod ( defaultParams , nextRetryAttempt ) ;
858+ } ) ;
847859}
848860
849861/**
@@ -1338,7 +1350,10 @@ function setWithRetry<TKey extends OnyxKey>({key, value, options}: SetParams<TKe
13381350 * storage step. Cache and subscriber notifications already happened in the orchestrator, so
13391351 * retries no longer re-fire `waitForCollectionCallback` subscribers with the same payload.
13401352 */
1341- function persistMultiSetWrite ( params : { keyValuePairsToStore : StorageKeyValuePair [ ] ; newData : OnyxMultiSetInput } , retryAttempt ?: number ) : Promise < void > {
1353+ function persistMultiSetWrite (
1354+ params : { keyValuePairsToStore : StorageKeyValuePair [ ] ; newData : OnyxMultiSetInput ; restoreEvictedKey ?: ( evictedKey : OnyxKey ) => void } ,
1355+ retryAttempt ?: number ,
1356+ ) : Promise < void > {
13421357 const { keyValuePairsToStore, newData} = params ;
13431358
13441359 return Storage . multiSet ( keyValuePairsToStore )
@@ -1426,7 +1441,31 @@ function multiSetWithRetry(data: OnyxMultiSetInput, retryAttempt?: number): Prom
14261441 return ! OnyxKeys . isRamOnlyKey ( key ) ;
14271442 } ) ;
14281443
1429- return persistMultiSetWrite ( { keyValuePairsToStore, newData} , retryAttempt ) ;
1444+ // Capture the in-flight key/value map so retryOperation can re-apply cache state for any
1445+ // evicted-then-retried key (see retryOperation's eviction branch for context).
1446+ const inFlightValueByKey = new Map < OnyxKey , OnyxValue < OnyxKey > > ( ) ;
1447+ for ( const [ inFlightKey , inFlightValue ] of keyValuePairsToSet ) {
1448+ inFlightValueByKey . set ( inFlightKey , inFlightValue ) ;
1449+ }
1450+ const restoreEvictedKey = ( evictedKey : OnyxKey ) : void => {
1451+ if ( ! inFlightValueByKey . has ( evictedKey ) ) {
1452+ return ;
1453+ }
1454+ const value = inFlightValueByKey . get ( evictedKey ) as OnyxValue < OnyxKey > ;
1455+ const evictedCollectionKey = OnyxKeys . getCollectionKey ( evictedKey ) ;
1456+ cache . set ( evictedKey , value ) ;
1457+ if ( evictedCollectionKey && OnyxKeys . isCollectionMemberKey ( evictedCollectionKey , evictedKey ) ) {
1458+ keysChanged (
1459+ evictedCollectionKey as CollectionKeyBase ,
1460+ { [ evictedKey ] : value } as Record < string , OnyxValue < OnyxKey > > ,
1461+ { [ evictedKey ] : undefined } as Record < string , OnyxValue < OnyxKey > > ,
1462+ ) ;
1463+ } else {
1464+ keyChanged ( evictedKey , value ) ;
1465+ }
1466+ } ;
1467+
1468+ return persistMultiSetWrite ( { keyValuePairsToStore, newData, restoreEvictedKey} , retryAttempt ) ;
14301469}
14311470
14321471/**
@@ -1439,6 +1478,7 @@ function persistCollectionWrite<TKey extends CollectionKeyBase>(
14391478 collectionKey : TKey ;
14401479 keyValuePairs : StorageKeyValuePair [ ] ;
14411480 mutableCollection : OnyxInputKeyValueMapping ;
1481+ restoreEvictedKey ?: ( evictedKey : OnyxKey ) => void ;
14421482 } ,
14431483 retryAttempt ?: number ,
14441484) : Promise < void > {
@@ -1517,7 +1557,22 @@ function setCollectionWithRetry<TKey extends CollectionKeyBase>({collectionKey,
15171557
15181558 keysChanged ( collectionKey , mutableCollection , previousCollection ) ;
15191559
1520- return persistCollectionWrite ( { collectionKey, keyValuePairs, mutableCollection} , retryAttempt ) ;
1560+ // Capture the in-flight key/value map so retryOperation can re-apply cache state for any
1561+ // evicted-then-retried key (see retryOperation's eviction branch for context).
1562+ const inFlightValueByKey = new Map < OnyxKey , OnyxValue < OnyxKey > > ( ) ;
1563+ for ( const [ inFlightKey , inFlightValue ] of keyValuePairs ) {
1564+ inFlightValueByKey . set ( inFlightKey , inFlightValue ) ;
1565+ }
1566+ const restoreEvictedKey = ( evictedKey : OnyxKey ) : void => {
1567+ if ( ! inFlightValueByKey . has ( evictedKey ) ) {
1568+ return ;
1569+ }
1570+ const value = inFlightValueByKey . get ( evictedKey ) as OnyxValue < OnyxKey > ;
1571+ cache . set ( evictedKey , value ) ;
1572+ keysChanged ( collectionKey , { [ evictedKey ] : value } as Record < string , OnyxValue < OnyxKey > > , { [ evictedKey ] : undefined } as Record < string , OnyxValue < OnyxKey > > ) ;
1573+ } ;
1574+
1575+ return persistCollectionWrite ( { collectionKey, keyValuePairs, mutableCollection, restoreEvictedKey} , retryAttempt ) ;
15211576 } ) ;
15221577}
15231578
@@ -1533,6 +1588,7 @@ function persistMergedCollectionWrite<TKey extends CollectionKeyBase>(
15331588 keyValuePairsForExistingCollection : StorageKeyValuePair [ ] ;
15341589 keyValuePairsForNewCollection : StorageKeyValuePair [ ] ;
15351590 resultCollection : OnyxInputKeyValueMapping ;
1591+ restoreEvictedKey ?: ( evictedKey : OnyxKey ) => void ;
15361592 } ,
15371593 retryAttempt ?: number ,
15381594) : Promise < void > {
@@ -1678,7 +1734,29 @@ function mergeCollectionWithPatches<TKey extends CollectionKeyBase>(
16781734 cache . merge ( finalMergedCollection ) ;
16791735 keysChanged ( collectionKey , finalMergedCollection , previousCollection ) ;
16801736
1681- return persistMergedCollectionWrite ( { collectionKey, keyValuePairsForExistingCollection, keyValuePairsForNewCollection, resultCollection} , retryAttempt ) ;
1737+ // Snapshot the post-merge cache values for the in-flight keys. The orchestrator's
1738+ // cache.merge already merged the deltas into the previous storage values, so the cache
1739+ // now holds the *full* merged value for each key. Using the snapshot (rather than the
1740+ // delta in `finalMergedCollection`) preserves previously-merged-in fields when the
1741+ // eviction's cache.drop forces us to re-populate cache from scratch on retry.
1742+ const inFlightMergedSnapshot = new Map < OnyxKey , OnyxValue < OnyxKey > > ( ) ;
1743+ for ( const inFlightKey of Object . keys ( finalMergedCollection ) ) {
1744+ inFlightMergedSnapshot . set ( inFlightKey , cache . get ( inFlightKey ) ) ;
1745+ }
1746+
1747+ // Closure invoked by retryOperation after an eviction picks a key that's part of this
1748+ // merge. We re-apply the full merged value + a keysChanged notification for that one
1749+ // key so subscribers don't stay in the "removed" state across the imminent storage retry.
1750+ const restoreEvictedKey = ( evictedKey : OnyxKey ) : void => {
1751+ if ( ! inFlightMergedSnapshot . has ( evictedKey ) ) {
1752+ return ;
1753+ }
1754+ const value = inFlightMergedSnapshot . get ( evictedKey ) as OnyxValue < OnyxKey > ;
1755+ cache . set ( evictedKey , value ) ;
1756+ keysChanged ( collectionKey , { [ evictedKey ] : value } as Record < string , OnyxValue < OnyxKey > > , { [ evictedKey ] : undefined } as Record < string , OnyxValue < OnyxKey > > ) ;
1757+ } ;
1758+
1759+ return persistMergedCollectionWrite ( { collectionKey, keyValuePairsForExistingCollection, keyValuePairsForNewCollection, resultCollection, restoreEvictedKey} , retryAttempt ) ;
16821760 } ) ;
16831761 } )
16841762 . then ( ( ) => undefined ) ;
@@ -1732,7 +1810,22 @@ function partialSetCollection<TKey extends CollectionKeyBase>({collectionKey, co
17321810
17331811 keysChanged ( collectionKey , mutableCollection , previousCollection ) ;
17341812
1735- return persistCollectionWrite ( { collectionKey, keyValuePairs, mutableCollection} , retryAttempt ) ;
1813+ // Capture the in-flight key/value map so retryOperation can re-apply cache state for any
1814+ // evicted-then-retried key (see retryOperation's eviction branch for context).
1815+ const inFlightValueByKey = new Map < OnyxKey , OnyxValue < OnyxKey > > ( ) ;
1816+ for ( const [ inFlightKey , inFlightValue ] of keyValuePairs ) {
1817+ inFlightValueByKey . set ( inFlightKey , inFlightValue ) ;
1818+ }
1819+ const restoreEvictedKey = ( evictedKey : OnyxKey ) : void => {
1820+ if ( ! inFlightValueByKey . has ( evictedKey ) ) {
1821+ return ;
1822+ }
1823+ const value = inFlightValueByKey . get ( evictedKey ) as OnyxValue < OnyxKey > ;
1824+ cache . set ( evictedKey , value ) ;
1825+ keysChanged ( collectionKey , { [ evictedKey ] : value } as Record < string , OnyxValue < OnyxKey > > , { [ evictedKey ] : undefined } as Record < string , OnyxValue < OnyxKey > > ) ;
1826+ } ;
1827+
1828+ return persistCollectionWrite ( { collectionKey, keyValuePairs, mutableCollection, restoreEvictedKey} , retryAttempt ) ;
17361829 } ) ;
17371830}
17381831
0 commit comments