1- /* eslint-disable no-continue */
21import _ from 'underscore' ;
32import lodashPick from 'lodash/pick' ;
43import * as Logger from './Logger' ;
@@ -27,21 +26,23 @@ import type {
2726 OnyxValue ,
2827 OnyxInput ,
2928 OnyxMethodMap ,
29+ MultiMergeReplaceNullPatches ,
3030} from './types' ;
3131import OnyxUtils from './OnyxUtils' ;
3232import logMessages from './logMessages' ;
3333import type { Connection } from './OnyxConnectionManager' ;
3434import connectionManager from './OnyxConnectionManager' ;
3535import * as GlobalSettings from './GlobalSettings' ;
3636import decorateWithMetrics from './metrics' ;
37+ import OnyxMerge from './OnyxMerge/index.native' ;
3738
3839/** Initialize the store with actions and listening for storage events */
3940function init ( {
4041 keys = { } ,
4142 initialKeyStates = { } ,
4243 safeEvictionKeys = [ ] ,
4344 maxCachedKeysCount = 1000 ,
44- shouldSyncMultipleInstances = Boolean ( global . localStorage ) ,
45+ shouldSyncMultipleInstances = ! ! global . localStorage ,
4546 debugSetState = false ,
4647 enablePerformanceMetrics = false ,
4748 skippableCollectionMemberIDs = [ ] ,
@@ -169,38 +170,31 @@ function set<TKey extends OnyxKey>(key: TKey, value: OnyxSetInput<TKey>): Promis
169170 return Promise . resolve ( ) ;
170171 }
171172
172- // If the value is null, we remove the key from storage
173- const { value : valueAfterRemoving , wasRemoved} = OnyxUtils . removeNullValues ( key , value ) ;
174-
175- const logSetCall = ( hasChanged = true ) => {
176- // Logging properties only since values could be sensitive things we don't want to log
177- Logger . logInfo ( `set called for key: ${ key } ${ _ . isObject ( value ) ? ` properties: ${ _ . keys ( value ) . join ( ',' ) } ` : '' } hasChanged: ${ hasChanged } ` ) ;
178- } ;
179-
180- // Calling "OnyxUtils.removeNullValues" removes the key from storage and cache and updates the subscriber.
173+ // If the change is null, we can just delete the key.
181174 // Therefore, we don't need to further broadcast and update the value so we can return early.
182- if ( wasRemoved ) {
183- logSetCall ( ) ;
175+ if ( value === null ) {
176+ OnyxUtils . remove ( key ) ;
177+ Logger . logInfo ( `set called for key: ${ key } => null passed, so key was removed` ) ;
184178 return Promise . resolve ( ) ;
185179 }
186180
187- const valueWithoutNullValues = valueAfterRemoving as OnyxValue < TKey > ;
188- const hasChanged = cache . hasValueChanged ( key , valueWithoutNullValues ) ;
181+ const valueWithoutNestedNullValues = utils . removeNestedNullValues ( value ) as OnyxValue < TKey > ;
182+ const hasChanged = cache . hasValueChanged ( key , valueWithoutNestedNullValues ) ;
189183
190- logSetCall ( hasChanged ) ;
184+ Logger . logInfo ( `set called for key: ${ key } ${ _ . isObject ( value ) ? ` properties: ${ _ . keys ( value ) . join ( ',' ) } ` : '' } hasChanged: ${ hasChanged } ` ) ;
191185
192186 // This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
193- const updatePromise = OnyxUtils . broadcastUpdate ( key , valueWithoutNullValues , hasChanged ) ;
187+ const updatePromise = OnyxUtils . broadcastUpdate ( key , valueWithoutNestedNullValues , hasChanged ) ;
194188
195189 // 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.
196190 if ( ! hasChanged ) {
197191 return updatePromise ;
198192 }
199193
200- return Storage . setItem ( key , valueWithoutNullValues )
201- . catch ( ( error ) => OnyxUtils . evictStorageAndRetry ( error , set , key , valueWithoutNullValues ) )
194+ return Storage . setItem ( key , valueWithoutNestedNullValues )
195+ . catch ( ( error ) => OnyxUtils . evictStorageAndRetry ( error , set , key , valueWithoutNestedNullValues ) )
202196 . then ( ( ) => {
203- OnyxUtils . sendActionToDevTools ( OnyxUtils . METHOD . SET , key , valueWithoutNullValues ) ;
197+ OnyxUtils . sendActionToDevTools ( OnyxUtils . METHOD . SET , key , valueWithoutNestedNullValues ) ;
204198 return updatePromise ;
205199 } ) ;
206200}
@@ -307,7 +301,6 @@ function merge<TKey extends OnyxKey>(key: TKey, changes: OnyxMergeInput<TKey>):
307301 }
308302
309303 try {
310- // We first only merge the changes, so we use OnyxUtils.batchMergeChanges() to combine all the changes into just one.
311304 const validChanges = mergeQueue [ key ] . filter ( ( change ) => {
312305 const { isCompatible, existingValueType, newValueType} = utils . checkCompatibilityWithExistingValue ( change , existingValue ) ;
313306 if ( ! isCompatible ) {
@@ -319,54 +312,21 @@ function merge<TKey extends OnyxKey>(key: TKey, changes: OnyxMergeInput<TKey>):
319312 if ( ! validChanges . length ) {
320313 return Promise . resolve ( ) ;
321314 }
322- const batchedDeltaChanges = OnyxUtils . batchMergeChanges ( validChanges ) . result ;
323-
324- // Case (1): When there is no existing value in storage, we want to set the value instead of merge it.
325- // Case (2): The presence of a top-level `null` in the merge queue instructs us to drop the whole existing value.
326- // 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.
327- const shouldSetValue = ! existingValue || mergeQueue [ key ] . includes ( null ) ;
328315
329316 // Clean up the write queue, so we don't apply these changes again.
330317 delete mergeQueue [ key ] ;
331318 delete mergeQueuePromise [ key ] ;
332319
333- const logMergeCall = ( hasChanged = true ) => {
334- // Logging properties only since values could be sensitive things we don't want to log.
335- Logger . logInfo ( `merge called for key: ${ key } ${ _ . isObject ( batchedDeltaChanges ) ? ` properties: ${ _ . keys ( batchedDeltaChanges ) . join ( ',' ) } ` : '' } hasChanged: ${ hasChanged } ` ) ;
336- } ;
337-
338- // If the batched changes equal null, we want to remove the key from storage, to reduce storage size.
339- const { wasRemoved} = OnyxUtils . removeNullValues ( key , batchedDeltaChanges ) ;
340-
341- // Calling "OnyxUtils.removeNullValues" removes the key from storage and cache and updates the subscriber.
320+ // If the last change is null, we can just delete the key.
342321 // Therefore, we don't need to further broadcast and update the value so we can return early.
343- if ( wasRemoved ) {
344- logMergeCall ( ) ;
322+ if ( validChanges . at ( - 1 ) === null ) {
323+ Logger . logInfo ( `merge called for key: ${ key } => null passed, so key was removed` ) ;
324+ OnyxUtils . remove ( key ) ;
345325 return Promise . resolve ( ) ;
346326 }
347327
348- // If "shouldSetValue" is true, it means that we want to completely replace the existing value with the batched changes,
349- // so we pass `undefined` to OnyxUtils.applyMerge() first parameter to make it use "batchedDeltaChanges" to
350- // create a new object for us.
351- // If "shouldSetValue" is false, it means that we want to merge the batched changes into the existing value,
352- // so we pass "existingValue" to the first parameter.
353- const resultValue = OnyxUtils . applyMerge ( shouldSetValue ? undefined : existingValue , [ batchedDeltaChanges ] ) ;
354-
355- // In cache, we don't want to remove the key if it's null to improve performance and speed up the next merge.
356- const hasChanged = cache . hasValueChanged ( key , resultValue ) ;
357-
358- logMergeCall ( hasChanged ) ;
359-
360- // This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
361- const updatePromise = OnyxUtils . broadcastUpdate ( key , resultValue as OnyxValue < TKey > , hasChanged ) ;
362-
363- // If the value has not changed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
364- if ( ! hasChanged ) {
365- return updatePromise ;
366- }
367-
368- return Storage . mergeItem ( key , resultValue as OnyxValue < TKey > ) . then ( ( ) => {
369- OnyxUtils . sendActionToDevTools ( OnyxUtils . METHOD . MERGE , key , changes , resultValue ) ;
328+ return OnyxMerge . applyMerge ( key , existingValue , validChanges ) . then ( ( { mergedValue, updatePromise} ) => {
329+ OnyxUtils . sendActionToDevTools ( OnyxUtils . METHOD . MERGE , key , changes , mergedValue ) ;
370330 return updatePromise ;
371331 } ) ;
372332 } catch ( error ) {
@@ -394,7 +354,7 @@ function merge<TKey extends OnyxKey>(key: TKey, changes: OnyxMergeInput<TKey>):
394354function mergeCollection < TKey extends CollectionKeyBase , TMap > (
395355 collectionKey : TKey ,
396356 collection : OnyxMergeCollectionInput < TKey , TMap > ,
397- mergeReplaceNullPatches ?: MixedOperationsQueue [ 'mergeReplaceNullPatches' ] ,
357+ mergeReplaceNullPatches ?: MultiMergeReplaceNullPatches ,
398358) : Promise < void > {
399359 if ( ! OnyxUtils . isValidNonEmptyCollectionForMerge ( collection ) ) {
400360 Logger . logInfo ( 'mergeCollection() called with invalid or empty value. Skipping this update.' ) ;
@@ -445,10 +405,12 @@ function mergeCollection<TKey extends CollectionKeyBase, TMap>(
445405
446406 const existingKeyCollection = existingKeys . reduce ( ( obj : OnyxInputKeyValueMapping , key ) => {
447407 const { isCompatible, existingValueType, newValueType} = utils . checkCompatibilityWithExistingValue ( resultCollection [ key ] , cachedCollectionForExistingKeys [ key ] ) ;
408+
448409 if ( ! isCompatible ) {
449410 Logger . logAlert ( logMessages . incompatibleUpdateAlert ( key , 'mergeCollection' , existingValueType , newValueType ) ) ;
450411 return obj ;
451412 }
413+
452414 // eslint-disable-next-line no-param-reassign
453415 obj [ key ] = resultCollection [ key ] ;
454416 return obj ;
@@ -465,7 +427,7 @@ function mergeCollection<TKey extends CollectionKeyBase, TMap>(
465427 // When (multi-)merging the values with the existing values in storage,
466428 // we don't want to remove nested null values from the data that we pass to the storage layer,
467429 // because the storage layer uses them to remove nested keys from storage natively.
468- const keyValuePairsForExistingCollection = OnyxUtils . prepareKeyValuePairsForStorage ( existingKeyCollection , false ) ;
430+ const keyValuePairsForExistingCollection = OnyxUtils . prepareKeyValuePairsForStorage ( existingKeyCollection , false , mergeReplaceNullPatches ) ;
469431
470432 // We can safely remove nested null values when using (multi-)set,
471433 // because we will simply overwrite the existing values in storage.
@@ -480,7 +442,7 @@ function mergeCollection<TKey extends CollectionKeyBase, TMap>(
480442 // New keys will be added via multiSet while existing keys will be updated using multiMerge
481443 // This is because setting a key that doesn't exist yet with multiMerge will throw errors
482444 if ( keyValuePairsForExistingCollection . length > 0 ) {
483- promises . push ( Storage . multiMerge ( keyValuePairsForExistingCollection , mergeReplaceNullPatches ) ) ;
445+ promises . push ( Storage . multiMerge ( keyValuePairsForExistingCollection ) ) ;
484446 }
485447
486448 if ( keyValuePairsForNewCollection . length > 0 ) {
@@ -774,7 +736,7 @@ function update(data: OnyxUpdate[]): Promise<void> {
774736 // Remove the collection-related key from the updateQueue so that it won't be processed individually.
775737 delete updateQueue [ key ] ;
776738
777- const batchedChanges = OnyxUtils . batchMergeChanges ( operations ) ;
739+ const batchedChanges = OnyxUtils . mergeAndMarkChanges ( operations ) ;
778740 if ( operations [ 0 ] === null ) {
779741 // eslint-disable-next-line no-param-reassign
780742 queue . set [ key ] = batchedChanges . result ;
@@ -806,13 +768,16 @@ function update(data: OnyxUpdate[]): Promise<void> {
806768 } ) ;
807769
808770 Object . entries ( updateQueue ) . forEach ( ( [ key , operations ] ) => {
809- const batchedChanges = OnyxUtils . batchMergeChanges ( operations ) . result ;
810-
811771 if ( operations [ 0 ] === null ) {
772+ const batchedChanges = OnyxUtils . mergeAndMarkChanges ( operations ) . result ;
812773 promises . push ( ( ) => set ( key , batchedChanges ) ) ;
813- } else {
814- promises . push ( ( ) => merge ( key , batchedChanges ) ) ;
774+ return ;
815775 }
776+
777+ const mergePromises = operations . map ( ( operation ) => {
778+ return merge ( key , operation ) ;
779+ } ) ;
780+ promises . push ( ( ) => mergePromises . at ( 0 ) ?? Promise . resolve ( ) ) ;
816781 } ) ;
817782
818783 const snapshotPromises = updateSnapshots ( data ) ;
0 commit comments