Skip to content

Commit d18d042

Browse files
committed
Merge branch 'main' into bugfix/615-solution2
2 parents 38a5f13 + ba6be93 commit d18d042

9 files changed

Lines changed: 63 additions & 34 deletions

File tree

lib/Onyx.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ function init({
4545
debugSetState = false,
4646
enablePerformanceMetrics = false,
4747
skippableCollectionMemberIDs = [],
48+
fullyMergedSnapshotKeys = [],
4849
}: InitOptions): void {
4950
if (enablePerformanceMetrics) {
5051
GlobalSettings.setPerformanceMetricsEnabled(true);
@@ -71,7 +72,7 @@ function init({
7172
cache.setRecentKeysLimit(maxCachedKeysCount);
7273
}
7374

74-
OnyxUtils.initStoreValues(keys, initialKeyStates, evictableKeys);
75+
OnyxUtils.initStoreValues(keys, initialKeyStates, evictableKeys, fullyMergedSnapshotKeys);
7576

7677
// Initialize all of our keys with data provided then give green light to any pending connections
7778
Promise.all([cache.addEvictableKeysToRecentlyAccessedList(OnyxUtils.isCollectionKey, OnyxUtils.getAllKeys), OnyxUtils.initializeWithDefaultKeyStates()]).then(

lib/OnyxCache.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class OnyxCache {
4747
private evictionBlocklist: Record<OnyxKey, string[] | undefined> = {};
4848

4949
/** List of keys that have been directly subscribed to or recently modified from least to most recent */
50-
private recentlyAccessedKeys: OnyxKey[] = [];
50+
private recentlyAccessedKeys = new Set<OnyxKey>();
5151

5252
constructor() {
5353
this.storageKeys = new Set();
@@ -317,7 +317,7 @@ class OnyxCache {
317317
* Remove a key from the recently accessed key list.
318318
*/
319319
removeLastAccessedKey(key: OnyxKey): void {
320-
this.recentlyAccessedKeys = this.recentlyAccessedKeys.filter((recentlyAccessedKey) => recentlyAccessedKey !== key);
320+
this.recentlyAccessedKeys.delete(key);
321321
}
322322

323323
/**
@@ -332,7 +332,7 @@ class OnyxCache {
332332
}
333333

334334
this.removeLastAccessedKey(key);
335-
this.recentlyAccessedKeys.push(key);
335+
this.recentlyAccessedKeys.add(key);
336336
}
337337

338338
/**
@@ -361,7 +361,12 @@ class OnyxCache {
361361
* Finds a key that can be safely evicted
362362
*/
363363
getKeyForEviction(): OnyxKey | undefined {
364-
return this.recentlyAccessedKeys.find((key) => !this.evictionBlocklist[key]);
364+
for (const key of this.recentlyAccessedKeys) {
365+
if (!this.evictionBlocklist[key]) {
366+
return key;
367+
}
368+
}
369+
return undefined;
365370
}
366371
}
367372

lib/OnyxUtils.ts

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import {deepEqual} from 'fast-equals';
33
import lodashClone from 'lodash/clone';
44
import type {ValueOf} from 'type-fest';
5+
import lodashPick from 'lodash/pick';
56
import DevTools from './DevTools';
67
import * as Logger from './Logger';
78
import type Onyx from './Onyx';
@@ -74,6 +75,8 @@ const lastConnectionCallbackData = new Map<number, OnyxValue<OnyxKey>>();
7475

7576
let snapshotKey: OnyxKey | null = null;
7677

78+
let fullyMergedSnapshotKeys: Set<string> | undefined;
79+
7780
// Keeps track of the last subscriptionID that was used so we can keep incrementing it
7881
let lastSubscriptionID = 0;
7982

@@ -135,8 +138,9 @@ function setSkippableCollectionMemberIDs(ids: Set<string>): void {
135138
* @param keys - `ONYXKEYS` constants object from Onyx.init()
136139
* @param initialKeyStates - initial data to set when `init()` and `clear()` are called
137140
* @param evictableKeys - This is an array of keys (individual or collection patterns) that when provided to Onyx are flagged as "safe" for removal.
141+
* @param fullyMergedSnapshotKeys - Array of snapshot collection keys where full merge is supported and data structure can be changed after merge.
138142
*/
139-
function initStoreValues(keys: DeepRecord<string, OnyxKey>, initialKeyStates: Partial<KeyValueMapping>, evictableKeys: OnyxKey[]): void {
143+
function initStoreValues(keys: DeepRecord<string, OnyxKey>, initialKeyStates: Partial<KeyValueMapping>, evictableKeys: OnyxKey[], fullyMergedSnapshotKeysParam?: string[]): void {
140144
// We need the value of the collection keys later for checking if a
141145
// key is a collection. We store it in a map for faster lookup.
142146
const collectionValues = Object.values(keys.COLLECTION ?? {}) as string[];
@@ -155,6 +159,7 @@ function initStoreValues(keys: DeepRecord<string, OnyxKey>, initialKeyStates: Pa
155159

156160
if (typeof keys.COLLECTION === 'object' && typeof keys.COLLECTION.SNAPSHOT === 'string') {
157161
snapshotKey = keys.COLLECTION.SNAPSHOT;
162+
fullyMergedSnapshotKeys = new Set(fullyMergedSnapshotKeysParam ?? []);
158163
}
159164
}
160165

@@ -449,8 +454,8 @@ function isCollectionKey(key: OnyxKey): key is CollectionKeyBase {
449454
return onyxCollectionKeySet.has(key);
450455
}
451456

452-
function isCollectionMemberKey<TCollectionKey extends CollectionKeyBase>(collectionKey: TCollectionKey, key: string, collectionKeyLength: number): key is `${TCollectionKey}${string}` {
453-
return key.startsWith(collectionKey) && key.length > collectionKeyLength;
457+
function isCollectionMemberKey<TCollectionKey extends CollectionKeyBase>(collectionKey: TCollectionKey, key: string): key is `${TCollectionKey}${string}` {
458+
return key.startsWith(collectionKey) && key.length > collectionKey.length;
454459
}
455460

456461
/**
@@ -464,7 +469,7 @@ function splitCollectionMemberKey<TKey extends CollectionKey, CollectionKeyType
464469
key: TKey,
465470
collectionKey?: string,
466471
): [CollectionKeyType, string] {
467-
if (collectionKey && !isCollectionMemberKey(collectionKey, key, collectionKey.length)) {
472+
if (collectionKey && !isCollectionMemberKey(collectionKey, key)) {
468473
throw new Error(`Invalid '${collectionKey}' collection key provided, it isn't compatible with '${key}' key.`);
469474
}
470475

@@ -559,14 +564,12 @@ function getCachedCollection<TKey extends CollectionKeyBase>(collectionKey: TKey
559564
const allKeys = collectionMemberKeys || cache.getAllKeys();
560565
const collection: OnyxCollection<KeyValueMapping[TKey]> = {};
561566

562-
const collectionKeyLength = collectionKey.length;
563-
564567
// forEach exists on both Set and Array
565568
allKeys.forEach((key) => {
566569
// If we don't have collectionMemberKeys array then we have to check whether a key is a collection member key.
567570
// Because in that case the keys will be coming from `cache.getAllKeys()` and we need to filter out the keys that
568571
// are not part of the collection.
569-
if (!collectionMemberKeys && !isCollectionMemberKey(collectionKey, key, collectionKeyLength)) {
572+
if (!collectionMemberKeys && !isCollectionMemberKey(collectionKey, key)) {
570573
return;
571574
}
572575

@@ -602,7 +605,6 @@ function keysChanged<TKey extends CollectionKeyBase>(
602605
// 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
603606
// 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().
604607
const stateMappingKeys = Object.keys(callbackToStateMapping);
605-
const collectionKeyLength = collectionKey.length;
606608

607609
for (const stateMappingKey of stateMappingKeys) {
608610
const subscriber = callbackToStateMapping[stateMappingKey];
@@ -623,7 +625,7 @@ function keysChanged<TKey extends CollectionKeyBase>(
623625
/**
624626
* e.g. Onyx.connect({key: `${ONYXKEYS.COLLECTION.REPORT}{reportID}`, callback: ...});
625627
*/
626-
const isSubscribedToCollectionMemberKey = isCollectionMemberKey(collectionKey, subscriber.key, collectionKeyLength);
628+
const isSubscribedToCollectionMemberKey = isCollectionMemberKey(collectionKey, subscriber.key);
627629

628630
// Regular Onyx.connect() subscriber found.
629631
if (typeof subscriber.callback === 'function') {
@@ -1409,7 +1411,6 @@ function updateSnapshots(data: OnyxUpdate[], mergeFn: typeof Onyx.merge): Array<
14091411
const promises: Array<() => Promise<void>> = [];
14101412

14111413
const snapshotCollection = OnyxUtils.getCachedCollection(snapshotCollectionKey);
1412-
const snapshotCollectionKeyLength = snapshotCollectionKey.length;
14131414

14141415
Object.entries(snapshotCollection).forEach(([snapshotEntryKey, snapshotEntryValue]) => {
14151416
// Snapshots may not be present in cache. We don't know how to update them so we skip.
@@ -1421,7 +1422,7 @@ function updateSnapshots(data: OnyxUpdate[], mergeFn: typeof Onyx.merge): Array<
14211422

14221423
data.forEach(({key, value}) => {
14231424
// snapshots are normal keys so we want to skip update if they are written to Onyx
1424-
if (OnyxUtils.isCollectionMemberKey(snapshotCollectionKey, key, snapshotCollectionKeyLength)) {
1425+
if (OnyxUtils.isCollectionMemberKey(snapshotCollectionKey, key)) {
14251426
return;
14261427
}
14271428

@@ -1445,8 +1446,17 @@ function updateSnapshots(data: OnyxUpdate[], mergeFn: typeof Onyx.merge): Array<
14451446
}
14461447

14471448
const oldValue = updatedData[key] || {};
1449+
let collectionKey: string | undefined;
1450+
try {
1451+
collectionKey = getCollectionKey(key);
1452+
} catch (e) {
1453+
// If getCollectionKey() throws an error it means the key is not a collection key.
1454+
collectionKey = undefined;
1455+
}
1456+
const shouldFullyMerge = fullyMergedSnapshotKeys?.has(collectionKey || key);
1457+
const newValue = shouldFullyMerge ? value : lodashPick(value, Object.keys(snapshotData[key]));
14481458

1449-
updatedData = {...updatedData, [key]: Object.assign(oldValue, value)};
1459+
updatedData = {...updatedData, [key]: Object.assign(oldValue, newValue)};
14501460
});
14511461

14521462
// Skip the update if there's no data to be merged

lib/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,14 @@ type InitOptions = {
481481
* Additionally, any subscribers from these keys to won't receive any data from Onyx.
482482
*/
483483
skippableCollectionMemberIDs?: string[];
484+
485+
/**
486+
* Array of snapshot collection keys where full merge is supported and data structure can be changed after merge.
487+
* For e.g. if oldSnapshotData is {report_1: {name 'Fitsum'}} and BE update is {report_1: {name:'Fitsum2', nickName:'Fitse'}}
488+
* if it is fullyMergedSnapshotkey the `nickName` prop that didn't exist in the previous data will be merged
489+
* otherwise only existing prop will be picked from the BE update and merged (in this case only name).
490+
*/
491+
fullyMergedSnapshotKeys?: string[];
484492
};
485493

486494
// eslint-disable-next-line @typescript-eslint/no-explicit-any

lib/useOnyx.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,7 @@ function useOnyx<TKey extends OnyxKey, TReturnValue = OnyxValue<TKey>>(
143143
const previousCollectionKey = OnyxUtils.splitCollectionMemberKey(previousKey)[0];
144144
const collectionKey = OnyxUtils.splitCollectionMemberKey(key)[0];
145145

146-
if (
147-
OnyxUtils.isCollectionMemberKey(previousCollectionKey, previousKey, previousCollectionKey.length) &&
148-
OnyxUtils.isCollectionMemberKey(collectionKey, key, collectionKey.length) &&
149-
previousCollectionKey === collectionKey
150-
) {
146+
if (OnyxUtils.isCollectionMemberKey(previousCollectionKey, previousKey) && OnyxUtils.isCollectionMemberKey(collectionKey, key) && previousCollectionKey === collectionKey) {
151147
return;
152148
}
153149
} catch (e) {

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-native-onyx",
3-
"version": "2.0.110",
3+
"version": "2.0.112",
44
"author": "Expensify, Inc.",
55
"homepage": "https://expensify.com",
66
"description": "State management for React Native",

tests/perf-test/OnyxUtils.perf-test.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,14 +152,12 @@ describe('OnyxUtils', () => {
152152
});
153153

154154
describe('isCollectionMemberKey', () => {
155-
const collectionKeyLength = collectionKey.length;
156-
157155
test('one call with correct key', async () => {
158-
await measureFunction(() => OnyxUtils.isCollectionMemberKey(collectionKey, `${collectionKey}entry1`, collectionKeyLength));
156+
await measureFunction(() => OnyxUtils.isCollectionMemberKey(collectionKey, `${collectionKey}entry1`));
159157
});
160158

161159
test('one call with wrong key', async () => {
162-
await measureFunction(() => OnyxUtils.isCollectionMemberKey(collectionKey, `${ONYXKEYS.COLLECTION.TEST_KEY_2}entry1`, collectionKeyLength));
160+
await measureFunction(() => OnyxUtils.isCollectionMemberKey(collectionKey, `${ONYXKEYS.COLLECTION.TEST_KEY_2}entry1`));
163161
});
164162
});
165163

tests/unit/onyxTest.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Onyx.init({
3333
[ONYX_KEYS.KEY_WITH_UNDERSCORE]: 'default',
3434
},
3535
skippableCollectionMemberIDs: ['skippable-id'],
36+
fullyMergedSnapshotKeys: [ONYX_KEYS.COLLECTION.ANIMALS, ONYX_KEYS.OTHER_TEST],
3637
});
3738

3839
describe('Onyx', () => {
@@ -1445,13 +1446,19 @@ describe('Onyx', () => {
14451446

14461447
it('should update Snapshot when its data changed', async () => {
14471448
const cat = `${ONYX_KEYS.COLLECTION.ANIMALS}cat`;
1449+
const people = `${ONYX_KEYS.COLLECTION.PEOPLE}1`;
14481450
const snapshot1 = `${ONYX_KEYS.COLLECTION.SNAPSHOT}1`;
14491451

14501452
const initialValue = {name: 'Fluffy'};
1451-
const finalValue = {name: 'Kitty', nickName: 'Fitse'};
1453+
const initialValueOtherTest = {1: {name: 'Kitty'}};
1454+
const finalValuePeople = {name: 'Kitty'};
1455+
const finalValueOtherTest = {1: {name: 'First person'}, 2: {name: 'Second person'}};
1456+
const finalValueCat = {name: 'Kitty', nickName: 'Fitse'};
1457+
const onyxUpdate = {name: 'Kitty', nickName: 'Fitse'};
14521458

14531459
await Onyx.set(cat, initialValue);
1454-
await Onyx.set(snapshot1, {data: {[cat]: initialValue}});
1460+
await Onyx.set(people, initialValue);
1461+
await Onyx.set(snapshot1, {data: {[ONYX_KEYS.OTHER_TEST]: initialValueOtherTest, [cat]: initialValue, [people]: initialValue}});
14551462

14561463
const callback = jest.fn();
14571464

@@ -1462,11 +1469,15 @@ describe('Onyx', () => {
14621469

14631470
await waitForPromisesToResolve();
14641471

1465-
await Onyx.update([{key: cat, value: finalValue, onyxMethod: Onyx.METHOD.MERGE}]);
1472+
await Onyx.update([
1473+
{key: cat, value: onyxUpdate, onyxMethod: Onyx.METHOD.MERGE},
1474+
{key: people, value: onyxUpdate, onyxMethod: Onyx.METHOD.MERGE},
1475+
{key: ONYX_KEYS.OTHER_TEST, value: finalValueOtherTest, onyxMethod: Onyx.METHOD.MERGE},
1476+
]);
14661477

14671478
expect(callback).toBeCalledTimes(2);
1468-
expect(callback).toHaveBeenNthCalledWith(1, {data: {[cat]: initialValue}}, snapshot1);
1469-
expect(callback).toHaveBeenNthCalledWith(2, {data: {[cat]: finalValue}}, snapshot1);
1479+
expect(callback).toHaveBeenNthCalledWith(1, {data: {[cat]: initialValue, [ONYX_KEYS.OTHER_TEST]: initialValueOtherTest, [people]: initialValue}}, snapshot1);
1480+
expect(callback).toHaveBeenNthCalledWith(2, {data: {[cat]: finalValueCat, [ONYX_KEYS.OTHER_TEST]: finalValueOtherTest, [people]: finalValuePeople}}, snapshot1);
14701481
});
14711482

14721483
describe('update', () => {

0 commit comments

Comments
 (0)