1- /* eslint-disable no-continue */
21import _ from 'underscore' ;
32import * as Logger from './Logger' ;
43import cache , { TASK } from './OnyxCache' ;
@@ -26,13 +25,15 @@ import type {
2625 OnyxValue ,
2726 OnyxInput ,
2827 OnyxMethodMap ,
28+ MultiMergeReplaceNullPatches ,
2929} from './types' ;
3030import OnyxUtils from './OnyxUtils' ;
3131import logMessages from './logMessages' ;
3232import type { Connection } from './OnyxConnectionManager' ;
3333import connectionManager from './OnyxConnectionManager' ;
3434import * as GlobalSettings from './GlobalSettings' ;
3535import decorateWithMetrics from './metrics' ;
36+ import OnyxMerge from './OnyxMerge' ;
3637
3738/** Initialize the store with actions and listening for storage events */
3839function init ( {
@@ -170,38 +171,31 @@ function set<TKey extends OnyxKey>(key: TKey, value: OnyxSetInput<TKey>): Promis
170171 return Promise . resolve ( ) ;
171172 }
172173
173- // If the value is null, we remove the key from storage
174- const { value : valueAfterRemoving , wasRemoved} = OnyxUtils . removeNullValues ( key , value ) ;
175-
176- const logSetCall = ( hasChanged = true ) => {
177- // Logging properties only since values could be sensitive things we don't want to log
178- Logger . logInfo ( `set called for key: ${ key } ${ _ . isObject ( value ) ? ` properties: ${ _ . keys ( value ) . join ( ',' ) } ` : '' } hasChanged: ${ hasChanged } ` ) ;
179- } ;
180-
181- // Calling "OnyxUtils.removeNullValues" removes the key from storage and cache and updates the subscriber.
174+ // If the change is null, we can just delete the key.
182175 // Therefore, we don't need to further broadcast and update the value so we can return early.
183- if ( wasRemoved ) {
184- logSetCall ( ) ;
176+ if ( value === null ) {
177+ OnyxUtils . remove ( key ) ;
178+ Logger . logInfo ( `set called for key: ${ key } => null passed, so key was removed` ) ;
185179 return Promise . resolve ( ) ;
186180 }
187181
188- const valueWithoutNullValues = valueAfterRemoving as OnyxValue < TKey > ;
189- const hasChanged = cache . hasValueChanged ( key , valueWithoutNullValues ) ;
182+ const valueWithoutNestedNullValues = utils . removeNestedNullValues ( value ) as OnyxValue < TKey > ;
183+ const hasChanged = cache . hasValueChanged ( key , valueWithoutNestedNullValues ) ;
190184
191- logSetCall ( hasChanged ) ;
185+ Logger . logInfo ( `set called for key: ${ key } ${ _ . isObject ( value ) ? ` properties: ${ _ . keys ( value ) . join ( ',' ) } ` : '' } hasChanged: ${ hasChanged } ` ) ;
192186
193187 // This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
194- const updatePromise = OnyxUtils . broadcastUpdate ( key , valueWithoutNullValues , hasChanged ) ;
188+ const updatePromise = OnyxUtils . broadcastUpdate ( key , valueWithoutNestedNullValues , hasChanged ) ;
195189
196190 // If the value has not changed or the key got removed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
197191 if ( ! hasChanged ) {
198192 return updatePromise ;
199193 }
200194
201- return Storage . setItem ( key , valueWithoutNullValues )
202- . catch ( ( error ) => OnyxUtils . evictStorageAndRetry ( error , set , key , valueWithoutNullValues ) )
195+ return Storage . setItem ( key , valueWithoutNestedNullValues )
196+ . catch ( ( error ) => OnyxUtils . evictStorageAndRetry ( error , set , key , valueWithoutNestedNullValues ) )
203197 . then ( ( ) => {
204- OnyxUtils . sendActionToDevTools ( OnyxUtils . METHOD . SET , key , valueWithoutNullValues ) ;
198+ OnyxUtils . sendActionToDevTools ( OnyxUtils . METHOD . SET , key , valueWithoutNestedNullValues ) ;
205199 return updatePromise ;
206200 } ) ;
207201}
@@ -313,7 +307,6 @@ function merge<TKey extends OnyxKey>(key: TKey, changes: OnyxMergeInput<TKey>):
313307 }
314308
315309 try {
316- // We first only merge the changes, so we use OnyxUtils.batchMergeChanges() to combine all the changes into just one.
317310 const validChanges = mergeQueue [ key ] . filter ( ( change ) => {
318311 const { isCompatible, existingValueType, newValueType} = utils . checkCompatibilityWithExistingValue ( change , existingValue ) ;
319312 if ( ! isCompatible ) {
@@ -325,54 +318,21 @@ function merge<TKey extends OnyxKey>(key: TKey, changes: OnyxMergeInput<TKey>):
325318 if ( ! validChanges . length ) {
326319 return Promise . resolve ( ) ;
327320 }
328- const batchedDeltaChanges = OnyxUtils . batchMergeChanges ( validChanges ) ;
329-
330- // Case (1): When there is no existing value in storage, we want to set the value instead of merge it.
331- // Case (2): The presence of a top-level `null` in the merge queue instructs us to drop the whole existing value.
332- // In this case, we can't simply merge the batched changes with the existing value, because then the null in the merge queue would have no effect.
333- const shouldSetValue = ! existingValue || mergeQueue [ key ] . includes ( null ) ;
334321
335322 // Clean up the write queue, so we don't apply these changes again.
336323 delete mergeQueue [ key ] ;
337324 delete mergeQueuePromise [ key ] ;
338325
339- const logMergeCall = ( hasChanged = true ) => {
340- // Logging properties only since values could be sensitive things we don't want to log.
341- Logger . logInfo ( `merge called for key: ${ key } ${ _ . isObject ( batchedDeltaChanges ) ? ` properties: ${ _ . keys ( batchedDeltaChanges ) . join ( ',' ) } ` : '' } hasChanged: ${ hasChanged } ` ) ;
342- } ;
343-
344- // If the batched changes equal null, we want to remove the key from storage, to reduce storage size.
345- const { wasRemoved} = OnyxUtils . removeNullValues ( key , batchedDeltaChanges ) ;
346-
347- // Calling "OnyxUtils.removeNullValues" removes the key from storage and cache and updates the subscriber.
326+ // If the last change is null, we can just delete the key.
348327 // Therefore, we don't need to further broadcast and update the value so we can return early.
349- if ( wasRemoved ) {
350- logMergeCall ( ) ;
328+ if ( validChanges . at ( - 1 ) === null ) {
329+ Logger . logInfo ( `merge called for key: ${ key } => null passed, so key was removed` ) ;
330+ OnyxUtils . remove ( key ) ;
351331 return Promise . resolve ( ) ;
352332 }
353333
354- // If "shouldSetValue" is true, it means that we want to completely replace the existing value with the batched changes,
355- // so we pass `undefined` to OnyxUtils.applyMerge() first parameter to make it use "batchedDeltaChanges" to
356- // create a new object for us.
357- // If "shouldSetValue" is false, it means that we want to merge the batched changes into the existing value,
358- // so we pass "existingValue" to the first parameter.
359- const resultValue = OnyxUtils . applyMerge ( shouldSetValue ? undefined : existingValue , [ batchedDeltaChanges ] ) ;
360-
361- // In cache, we don't want to remove the key if it's null to improve performance and speed up the next merge.
362- const hasChanged = cache . hasValueChanged ( key , resultValue ) ;
363-
364- logMergeCall ( hasChanged ) ;
365-
366- // This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
367- const updatePromise = OnyxUtils . broadcastUpdate ( key , resultValue as OnyxValue < TKey > , hasChanged ) ;
368-
369- // If the value has not changed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
370- if ( ! hasChanged ) {
371- return updatePromise ;
372- }
373-
374- return Storage . mergeItem ( key , resultValue as OnyxValue < TKey > ) . then ( ( ) => {
375- OnyxUtils . sendActionToDevTools ( OnyxUtils . METHOD . MERGE , key , changes , resultValue ) ;
334+ return OnyxMerge . applyMerge ( key , existingValue , validChanges ) . then ( ( { mergedValue, updatePromise} ) => {
335+ OnyxUtils . sendActionToDevTools ( OnyxUtils . METHOD . MERGE , key , changes , mergedValue ) ;
376336 return updatePromise ;
377337 } ) ;
378338 } catch ( error ) {
@@ -397,7 +357,11 @@ function merge<TKey extends OnyxKey>(key: TKey, changes: OnyxMergeInput<TKey>):
397357 * @param collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT`
398358 * @param collection Object collection keyed by individual collection member keys and values
399359 */
400- function mergeCollection < TKey extends CollectionKeyBase , TMap > ( collectionKey : TKey , collection : OnyxMergeCollectionInput < TKey , TMap > ) : Promise < void > {
360+ function mergeCollection < TKey extends CollectionKeyBase , TMap > (
361+ collectionKey : TKey ,
362+ collection : OnyxMergeCollectionInput < TKey , TMap > ,
363+ mergeReplaceNullPatches ?: MultiMergeReplaceNullPatches ,
364+ ) : Promise < void > {
401365 if ( ! OnyxUtils . isValidNonEmptyCollectionForMerge ( collection ) ) {
402366 Logger . logInfo ( 'mergeCollection() called with invalid or empty value. Skipping this update.' ) ;
403367 return Promise . resolve ( ) ;
@@ -447,10 +411,12 @@ function mergeCollection<TKey extends CollectionKeyBase, TMap>(collectionKey: TK
447411
448412 const existingKeyCollection = existingKeys . reduce ( ( obj : OnyxInputKeyValueMapping , key ) => {
449413 const { isCompatible, existingValueType, newValueType} = utils . checkCompatibilityWithExistingValue ( resultCollection [ key ] , cachedCollectionForExistingKeys [ key ] ) ;
414+
450415 if ( ! isCompatible ) {
451416 Logger . logAlert ( logMessages . incompatibleUpdateAlert ( key , 'mergeCollection' , existingValueType , newValueType ) ) ;
452417 return obj ;
453418 }
419+
454420 // eslint-disable-next-line no-param-reassign
455421 obj [ key ] = resultCollection [ key ] ;
456422 return obj ;
@@ -467,7 +433,7 @@ function mergeCollection<TKey extends CollectionKeyBase, TMap>(collectionKey: TK
467433 // When (multi-)merging the values with the existing values in storage,
468434 // we don't want to remove nested null values from the data that we pass to the storage layer,
469435 // because the storage layer uses them to remove nested keys from storage natively.
470- const keyValuePairsForExistingCollection = OnyxUtils . prepareKeyValuePairsForStorage ( existingKeyCollection , false ) ;
436+ const keyValuePairsForExistingCollection = OnyxUtils . prepareKeyValuePairsForStorage ( existingKeyCollection , false , mergeReplaceNullPatches ) ;
471437
472438 // We can safely remove nested null values when using (multi-)set,
473439 // because we will simply overwrite the existing values in storage.
@@ -717,38 +683,48 @@ function update(data: OnyxUpdate[]): Promise<void> {
717683 // Remove the collection-related key from the updateQueue so that it won't be processed individually.
718684 delete updateQueue [ key ] ;
719685
720- const batchedChanges = OnyxUtils . batchMergeChanges ( operations ) ;
686+ const batchedChanges = OnyxUtils . mergeAndMarkChanges ( operations ) ;
721687 if ( operations [ 0 ] === null ) {
722688 // eslint-disable-next-line no-param-reassign
723- queue . set [ key ] = batchedChanges ;
689+ queue . set [ key ] = batchedChanges . result ;
724690 } else {
725691 // eslint-disable-next-line no-param-reassign
726- queue . merge [ key ] = batchedChanges ;
692+ queue . merge [ key ] = batchedChanges . result ;
693+ if ( batchedChanges . replaceNullPatches . length > 0 ) {
694+ // eslint-disable-next-line no-param-reassign
695+ queue . mergeReplaceNullPatches [ key ] = batchedChanges . replaceNullPatches ;
696+ }
727697 }
728698 return queue ;
729699 } ,
730700 {
731701 merge : { } ,
702+ mergeReplaceNullPatches : { } ,
732703 set : { } ,
733704 } ,
734705 ) ;
735706
736707 if ( ! utils . isEmptyObject ( batchedCollectionUpdates . merge ) ) {
737- promises . push ( ( ) => mergeCollection ( collectionKey , batchedCollectionUpdates . merge as Collection < CollectionKey , unknown , unknown > ) ) ;
708+ promises . push ( ( ) =>
709+ mergeCollection ( collectionKey , batchedCollectionUpdates . merge as Collection < CollectionKey , unknown , unknown > , batchedCollectionUpdates . mergeReplaceNullPatches ) ,
710+ ) ;
738711 }
739712 if ( ! utils . isEmptyObject ( batchedCollectionUpdates . set ) ) {
740713 promises . push ( ( ) => multiSet ( batchedCollectionUpdates . set ) ) ;
741714 }
742715 } ) ;
743716
744717 Object . entries ( updateQueue ) . forEach ( ( [ key , operations ] ) => {
745- const batchedChanges = OnyxUtils . batchMergeChanges ( operations ) ;
746-
747718 if ( operations [ 0 ] === null ) {
719+ const batchedChanges = OnyxUtils . mergeAndMarkChanges ( operations ) . result ;
748720 promises . push ( ( ) => set ( key , batchedChanges ) ) ;
749- } else {
750- promises . push ( ( ) => merge ( key , batchedChanges ) ) ;
721+ return ;
751722 }
723+
724+ const mergePromises = operations . map ( ( operation ) => {
725+ return merge ( key , operation ) ;
726+ } ) ;
727+ promises . push ( ( ) => mergePromises . at ( 0 ) ?? Promise . resolve ( ) ) ;
752728 } ) ;
753729
754730 const snapshotPromises = OnyxUtils . updateSnapshots ( data , merge ) ;
0 commit comments