22import { deepEqual } from 'fast-equals' ;
33import lodashClone from 'lodash/clone' ;
44import type { ValueOf } from 'type-fest' ;
5+ import lodashPick from 'lodash/pick' ;
56import DevTools from './DevTools' ;
67import * as Logger from './Logger' ;
78import type Onyx from './Onyx' ;
@@ -71,6 +72,8 @@ let lastConnectionCallbackData = new Map<number, OnyxValue<OnyxKey>>();
7172
7273let snapshotKey : OnyxKey | null = null ;
7374
75+ let fullyMergedSnapshotKeys : Set < string > | undefined ;
76+
7477// Keeps track of the last subscriptionID that was used so we can keep incrementing it
7578let lastSubscriptionID = 0 ;
7679
@@ -132,8 +135,9 @@ function setSkippableCollectionMemberIDs(ids: Set<string>): void {
132135 * @param keys - `ONYXKEYS` constants object from Onyx.init()
133136 * @param initialKeyStates - initial data to set when `init()` and `clear()` are called
134137 * @param evictableKeys - This is an array of keys (individual or collection patterns) that when provided to Onyx are flagged as "safe" for removal.
138+ * @param fullyMergedSnapshotKeys - Array of snapshot collection keys where full merge is supported and data structure can be changed after merge.
135139 */
136- function initStoreValues ( keys : DeepRecord < string , OnyxKey > , initialKeyStates : Partial < KeyValueMapping > , evictableKeys : OnyxKey [ ] ) : void {
140+ function initStoreValues ( keys : DeepRecord < string , OnyxKey > , initialKeyStates : Partial < KeyValueMapping > , evictableKeys : OnyxKey [ ] , fullyMergedSnapshotKeysParam ?: string [ ] ) : void {
137141 // We need the value of the collection keys later for checking if a
138142 // key is a collection. We store it in a map for faster lookup.
139143 const collectionValues = Object . values ( keys . COLLECTION ?? { } ) as string [ ] ;
@@ -152,6 +156,7 @@ function initStoreValues(keys: DeepRecord<string, OnyxKey>, initialKeyStates: Pa
152156
153157 if ( typeof keys . COLLECTION === 'object' && typeof keys . COLLECTION . SNAPSHOT === 'string' ) {
154158 snapshotKey = keys . COLLECTION . SNAPSHOT ;
159+ fullyMergedSnapshotKeys = new Set ( fullyMergedSnapshotKeysParam ?? [ ] ) ;
155160 }
156161}
157162
@@ -446,8 +451,8 @@ function isCollectionKey(key: OnyxKey): key is CollectionKeyBase {
446451 return onyxCollectionKeySet . has ( key ) ;
447452}
448453
449- function isCollectionMemberKey < TCollectionKey extends CollectionKeyBase > ( collectionKey : TCollectionKey , key : string , collectionKeyLength : number ) : key is `${TCollectionKey } ${string } ` {
450- return key . startsWith ( collectionKey ) && key . length > collectionKeyLength ;
454+ function isCollectionMemberKey < TCollectionKey extends CollectionKeyBase > ( collectionKey : TCollectionKey , key : string ) : key is `${TCollectionKey } ${string } ` {
455+ return key . startsWith ( collectionKey ) && key . length > collectionKey . length ;
451456}
452457
453458/**
@@ -461,7 +466,7 @@ function splitCollectionMemberKey<TKey extends CollectionKey, CollectionKeyType
461466 key : TKey ,
462467 collectionKey ?: string ,
463468) : [ CollectionKeyType , string ] {
464- if ( collectionKey && ! isCollectionMemberKey ( collectionKey , key , collectionKey . length ) ) {
469+ if ( collectionKey && ! isCollectionMemberKey ( collectionKey , key ) ) {
465470 throw new Error ( `Invalid '${ collectionKey } ' collection key provided, it isn't compatible with '${ key } ' key.` ) ;
466471 }
467472
@@ -556,14 +561,12 @@ function getCachedCollection<TKey extends CollectionKeyBase>(collectionKey: TKey
556561 const allKeys = collectionMemberKeys || cache . getAllKeys ( ) ;
557562 const collection : OnyxCollection < KeyValueMapping [ TKey ] > = { } ;
558563
559- const collectionKeyLength = collectionKey . length ;
560-
561564 // forEach exists on both Set and Array
562565 allKeys . forEach ( ( key ) => {
563566 // If we don't have collectionMemberKeys array then we have to check whether a key is a collection member key.
564567 // Because in that case the keys will be coming from `cache.getAllKeys()` and we need to filter out the keys that
565568 // are not part of the collection.
566- if ( ! collectionMemberKeys && ! isCollectionMemberKey ( collectionKey , key , collectionKeyLength ) ) {
569+ if ( ! collectionMemberKeys && ! isCollectionMemberKey ( collectionKey , key ) ) {
567570 return ;
568571 }
569572
@@ -599,7 +602,6 @@ function keysChanged<TKey extends CollectionKeyBase>(
599602 // individual collection key member for the collection that is being updated. It is important to note that the collection parameter cane be a PARTIAL collection
600603 // and does not represent all of the combined keys and values for a collection key. It is just the "new" data that was merged in via mergeCollection().
601604 const stateMappingKeys = Object . keys ( callbackToStateMapping ) ;
602- const collectionKeyLength = collectionKey . length ;
603605
604606 for ( const stateMappingKey of stateMappingKeys ) {
605607 const subscriber = callbackToStateMapping [ stateMappingKey ] ;
@@ -620,7 +622,7 @@ function keysChanged<TKey extends CollectionKeyBase>(
620622 /**
621623 * e.g. Onyx.connect({key: `${ONYXKEYS.COLLECTION.REPORT}{reportID}`, callback: ...});
622624 */
623- const isSubscribedToCollectionMemberKey = isCollectionMemberKey ( collectionKey , subscriber . key , collectionKeyLength ) ;
625+ const isSubscribedToCollectionMemberKey = isCollectionMemberKey ( collectionKey , subscriber . key ) ;
624626
625627 // Regular Onyx.connect() subscriber found.
626628 if ( typeof subscriber . callback === 'function' ) {
@@ -1293,12 +1295,21 @@ function subscribeToKey<TKey extends OnyxKey>(connectOptions: ConnectOptions<TKe
12931295 // can send data back to the subscriber. Note that multiple keys can match as a subscriber could either be
12941296 // subscribed to a "collection key" or a single key.
12951297 const matchingKeys : string [ ] = [ ] ;
1296- keys . forEach ( ( key ) => {
1297- if ( ! isKeyMatch ( mapping . key , key ) ) {
1298- return ;
1298+
1299+ // Performance optimization: For single key subscriptions, avoid O(n) iteration
1300+ if ( ! isCollectionKey ( mapping . key ) ) {
1301+ if ( keys . has ( mapping . key ) ) {
1302+ matchingKeys . push ( mapping . key ) ;
12991303 }
1300- matchingKeys . push ( key ) ;
1301- } ) ;
1304+ } else {
1305+ // Collection case - need to iterate through all keys to find matches (O(n))
1306+ keys . forEach ( ( key ) => {
1307+ if ( ! isKeyMatch ( mapping . key , key ) ) {
1308+ return ;
1309+ }
1310+ matchingKeys . push ( key ) ;
1311+ } ) ;
1312+ }
13021313 // If the key being connected to does not exist we initialize the value with null. For subscribers that connected
13031314 // directly via connect() they will simply get a null value sent to them without any information about which key matched
13041315 // since there are none matched. In withOnyx() we wait for all connected keys to return a value before rendering the child
@@ -1380,7 +1391,6 @@ function updateSnapshots(data: OnyxUpdate[], mergeFn: typeof Onyx.merge): Array<
13801391 const promises : Array < ( ) => Promise < void > > = [ ] ;
13811392
13821393 const snapshotCollection = OnyxUtils . getCachedCollection ( snapshotCollectionKey ) ;
1383- const snapshotCollectionKeyLength = snapshotCollectionKey . length ;
13841394
13851395 Object . entries ( snapshotCollection ) . forEach ( ( [ snapshotEntryKey , snapshotEntryValue ] ) => {
13861396 // Snapshots may not be present in cache. We don't know how to update them so we skip.
@@ -1392,7 +1402,7 @@ function updateSnapshots(data: OnyxUpdate[], mergeFn: typeof Onyx.merge): Array<
13921402
13931403 data . forEach ( ( { key, value} ) => {
13941404 // snapshots are normal keys so we want to skip update if they are written to Onyx
1395- if ( OnyxUtils . isCollectionMemberKey ( snapshotCollectionKey , key , snapshotCollectionKeyLength ) ) {
1405+ if ( OnyxUtils . isCollectionMemberKey ( snapshotCollectionKey , key ) ) {
13961406 return ;
13971407 }
13981408
@@ -1416,8 +1426,17 @@ function updateSnapshots(data: OnyxUpdate[], mergeFn: typeof Onyx.merge): Array<
14161426 }
14171427
14181428 const oldValue = updatedData [ key ] || { } ;
1429+ let collectionKey : string | undefined ;
1430+ try {
1431+ collectionKey = getCollectionKey ( key ) ;
1432+ } catch ( e ) {
1433+ // If getCollectionKey() throws an error it means the key is not a collection key.
1434+ collectionKey = undefined ;
1435+ }
1436+ const shouldFullyMerge = fullyMergedSnapshotKeys ?. has ( collectionKey || key ) ;
1437+ const newValue = shouldFullyMerge ? value : lodashPick ( value , Object . keys ( snapshotData [ key ] ) ) ;
14191438
1420- updatedData = { ...updatedData , [ key ] : Object . assign ( oldValue , value ) } ;
1439+ updatedData = { ...updatedData , [ key ] : Object . assign ( oldValue , newValue ) } ;
14211440 } ) ;
14221441
14231442 // Skip the update if there's no data to be merged
0 commit comments