Skip to content
3 changes: 2 additions & 1 deletion lib/Onyx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ function init({
debugSetState = false,
enablePerformanceMetrics = false,
skippableCollectionMemberIDs = [],
fullyMergedSnapshotKeys = [],
}: InitOptions): void {
if (enablePerformanceMetrics) {
GlobalSettings.setPerformanceMetricsEnabled(true);
Expand All @@ -70,7 +71,7 @@ function init({
cache.setRecentKeysLimit(maxCachedKeysCount);
}

OnyxUtils.initStoreValues(keys, initialKeyStates, evictableKeys);
OnyxUtils.initStoreValues(keys, initialKeyStates, evictableKeys, fullyMergedSnapshotKeys);

// Initialize all of our keys with data provided then give green light to any pending connections
Promise.all([cache.addEvictableKeysToRecentlyAccessedList(OnyxUtils.isCollectionKey, OnyxUtils.getAllKeys), OnyxUtils.initializeWithDefaultKeyStates()]).then(
Expand Down
18 changes: 16 additions & 2 deletions lib/OnyxUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import {deepEqual} from 'fast-equals';
import lodashClone from 'lodash/clone';
import type {ValueOf} from 'type-fest';
import lodashPick from 'lodash/pick';
import DevTools from './DevTools';
import * as Logger from './Logger';
import type Onyx from './Onyx';
Expand Down Expand Up @@ -71,6 +72,8 @@ const lastConnectionCallbackData = new Map<number, OnyxValue<OnyxKey>>();

let snapshotKey: OnyxKey | null = null;

let fullyMergedSnapshotKeys: string[] | undefined = [];
Comment thread
FitseTLT marked this conversation as resolved.
Outdated

// Keeps track of the last subscriptionID that was used so we can keep incrementing it
let lastSubscriptionID = 0;

Expand All @@ -84,6 +87,10 @@ function getSnapshotKey(): OnyxKey | null {
return snapshotKey;
}

function getFullyMergedSnapshotKeys(): string[] | undefined {
Comment thread
FitseTLT marked this conversation as resolved.
Outdated
return fullyMergedSnapshotKeys;
}

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

if (typeof keys.COLLECTION === 'object' && typeof keys.COLLECTION.SNAPSHOT === 'string') {
snapshotKey = keys.COLLECTION.SNAPSHOT;
fullyMergedSnapshotKeys = fullyMergedSnapshotKeysParam;
}
}

Expand Down Expand Up @@ -1416,8 +1425,13 @@ function updateSnapshots(data: OnyxUpdate[], mergeFn: typeof Onyx.merge): Array<
}

const oldValue = updatedData[key] || {};
const fullyMergedKeys = getFullyMergedSnapshotKeys();
const newValue =
fullyMergedKeys && fullyMergedKeys.some((collectionKey) => isCollectionMemberKey(collectionKey, key, collectionKey.length))
Comment thread
FitseTLT marked this conversation as resolved.
Outdated
? value
: lodashPick(value, Object.keys(snapshotData[key]));

updatedData = {...updatedData, [key]: Object.assign(oldValue, value)};
updatedData = {...updatedData, [key]: Object.assign(oldValue, newValue)};
});

// Skip the update if there's no data to be merged
Expand Down
8 changes: 8 additions & 0 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,14 @@ type InitOptions = {
* Additionally, any subscribers from these keys to won't receive any data from Onyx.
*/
skippableCollectionMemberIDs?: string[];

/**
* Array of snapshot collection keys where full merge is supported and data structure can be changed after merge.
* For e.g. if oldSnapshotData is {report_1: {name 'Fitsum'}} and BE update is {report_1: {name:'Fitsum2', nickName:'Fitse'}}
* if it is fullyMergedSnapshotkey the `nickName` prop that didn't exist in the previous data will be merged
* otherwise only existing prop will be picked from the BE update and merged (in this case only name).
*/
fullyMergedSnapshotKeys?: string[];
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
18 changes: 13 additions & 5 deletions tests/unit/onyxTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Onyx.init({
[ONYX_KEYS.KEY_WITH_UNDERSCORE]: 'default',
},
skippableCollectionMemberIDs: ['skippable-id'],
fullyMergedSnapshotKeys: [ONYX_KEYS.COLLECTION.ANIMALS],
});

describe('Onyx', () => {
Expand Down Expand Up @@ -1445,13 +1446,17 @@ describe('Onyx', () => {

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

const initialValue = {name: 'Fluffy'};
const finalValue = {name: 'Kitty', nickName: 'Fitse'};
const finalValuePeople = {name: 'Kitty'};
const finalValueCat = {name: 'Kitty', nickName: 'Fitse'};
const onyxUpdate = {name: 'Kitty', nickName: 'Fitse'};

await Onyx.set(cat, initialValue);
await Onyx.set(snapshot1, {data: {[cat]: initialValue}});
await Onyx.set(people, initialValue);
await Onyx.set(snapshot1, {data: {[cat]: initialValue, [people]: initialValue}});

const callback = jest.fn();

Expand All @@ -1462,11 +1467,14 @@ describe('Onyx', () => {

await waitForPromisesToResolve();

await Onyx.update([{key: cat, value: finalValue, onyxMethod: Onyx.METHOD.MERGE}]);
await Onyx.update([
{key: cat, value: onyxUpdate, onyxMethod: Onyx.METHOD.MERGE},
{key: people, value: onyxUpdate, onyxMethod: Onyx.METHOD.MERGE},
]);

expect(callback).toBeCalledTimes(2);
expect(callback).toHaveBeenNthCalledWith(1, {data: {[cat]: initialValue}}, snapshot1);
expect(callback).toHaveBeenNthCalledWith(2, {data: {[cat]: finalValue}}, snapshot1);
expect(callback).toHaveBeenNthCalledWith(1, {data: {[cat]: initialValue, [people]: initialValue}}, snapshot1);
expect(callback).toHaveBeenNthCalledWith(2, {data: {[cat]: finalValueCat, [people]: finalValuePeople}}, snapshot1);
});

describe('update', () => {
Expand Down
Loading