diff --git a/API-INTERNAL.md b/API-INTERNAL.md index 33d5f4de..0dc8b5d8 100644 --- a/API-INTERNAL.md +++ b/API-INTERNAL.md @@ -20,6 +20,12 @@
getEvictionBlocklist()

Getter - returns the eviction block list.

+
getSkippableCollectionMemberIDs()
+

Getter - returns the skippable collection member IDs.

+
+
setSkippableCollectionMemberIDs()
+

Setter - sets the skippable collection member IDs.

+
initStoreValues(keys, initialKeyStates, safeEvictionKeys)

Sets the initial values for the Onyx store

@@ -53,7 +59,7 @@ The resulting collection will only contain items that are returned by the select

Checks to see if the subscriber's supplied key is associated with a collection of keys.

-
splitCollectionMemberKey(key)
+
splitCollectionMemberKey(key, collectionKey)

Splits a collection member key into the collection key part and the ID part.

isKeyMatch()
@@ -187,6 +193,18 @@ Getter - returns the deffered init task. ## getEvictionBlocklist() Getter - returns the eviction block list. +**Kind**: global function + + +## getSkippableCollectionMemberIDs() +Getter - returns the skippable collection member IDs. + +**Kind**: global function + + +## setSkippableCollectionMemberIDs() +Setter - sets the skippable collection member IDs. + **Kind**: global function @@ -268,7 +286,7 @@ is associated with a collection of keys. **Kind**: global function -## splitCollectionMemberKey(key) ⇒ +## splitCollectionMemberKey(key, collectionKey) ⇒ Splits a collection member key into the collection key part and the ID part. **Kind**: global function @@ -278,6 +296,7 @@ or throws an Error if the key is not a collection one. | Param | Description | | --- | --- | | key | The collection member key to split. | +| collectionKey | The collection key of the `key` param that can be passed in advance to optimize the function. | diff --git a/lib/Onyx.ts b/lib/Onyx.ts index a2fcdde3..baf7c068 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -44,6 +44,7 @@ function init({ shouldSyncMultipleInstances = Boolean(global.localStorage), debugSetState = false, enablePerformanceMetrics = false, + skippableCollectionMemberIDs = [], }: InitOptions): void { if (enablePerformanceMetrics) { GlobalSettings.setPerformanceMetricsEnabled(true); @@ -52,6 +53,8 @@ function init({ Storage.init(); + OnyxUtils.setSkippableCollectionMemberIDs(new Set(skippableCollectionMemberIDs)); + if (shouldSyncMultipleInstances) { Storage.keepInstancesSync?.((key, value) => { const prevValue = cache.get(key, false) as OnyxValue; @@ -134,6 +137,20 @@ function set(key: TKey, value: OnyxSetInput): Promis delete OnyxUtils.getMergeQueue()[key]; } + const skippableCollectionMemberIDs = OnyxUtils.getSkippableCollectionMemberIDs(); + if (skippableCollectionMemberIDs.size) { + try { + const [, collectionMemberID] = OnyxUtils.splitCollectionMemberKey(key); + if (skippableCollectionMemberIDs.has(collectionMemberID)) { + // The key is a skippable one, so we set the new value to null. + // eslint-disable-next-line no-param-reassign + value = null; + } + } catch (e) { + // The key is not a collection one or something went wrong during split, so we proceed with the function's logic. + } + } + // Onyx.set will ignore `undefined` values as inputs, therefore we can return early. if (value === undefined) { return Promise.resolve(); @@ -196,7 +213,27 @@ function set(key: TKey, value: OnyxSetInput): Promis * @param data object keyed by ONYXKEYS and the values to set */ function multiSet(data: OnyxMultiSetInput): Promise { - const keyValuePairsToSet = OnyxUtils.prepareKeyValuePairsForStorage(data, true); + let newData = data; + + const skippableCollectionMemberIDs = OnyxUtils.getSkippableCollectionMemberIDs(); + if (skippableCollectionMemberIDs.size) { + newData = Object.keys(newData).reduce((result: OnyxMultiSetInput, key) => { + try { + const [, collectionMemberID] = OnyxUtils.splitCollectionMemberKey(key); + // If the collection member key is a skippable one we set its value to null. + // eslint-disable-next-line no-param-reassign + result[key] = !skippableCollectionMemberIDs.has(collectionMemberID) ? newData[key] : null; + } catch { + // The key is not a collection one or something went wrong during split, so we assign the data to result anyway. + // eslint-disable-next-line no-param-reassign + result[key] = newData[key]; + } + + return result; + }, {}); + } + + const keyValuePairsToSet = OnyxUtils.prepareKeyValuePairsForStorage(newData, true); const updatePromises = keyValuePairsToSet.map(([key, value]) => { const prevValue = cache.get(key, false); @@ -207,9 +244,9 @@ function multiSet(data: OnyxMultiSetInput): Promise { }); return Storage.multiSet(keyValuePairsToSet) - .catch((error) => OnyxUtils.evictStorageAndRetry(error, multiSet, data)) + .catch((error) => OnyxUtils.evictStorageAndRetry(error, multiSet, newData)) .then(() => { - OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.MULTI_SET, undefined, data); + OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.MULTI_SET, undefined, newData); return Promise.all(updatePromises); }) .then(() => undefined); @@ -232,6 +269,20 @@ function multiSet(data: OnyxMultiSetInput): Promise { * Onyx.merge(ONYXKEYS.POLICY, {name: 'My Workspace'}); // -> {id: 1, name: 'My Workspace'} */ function merge(key: TKey, changes: OnyxMergeInput): Promise { + const skippableCollectionMemberIDs = OnyxUtils.getSkippableCollectionMemberIDs(); + if (skippableCollectionMemberIDs.size) { + try { + const [, collectionMemberID] = OnyxUtils.splitCollectionMemberKey(key); + if (skippableCollectionMemberIDs.has(collectionMemberID)) { + // The key is a skippable one, so we set the new changes to undefined. + // eslint-disable-next-line no-param-reassign + changes = undefined; + } + } catch (e) { + // The key is not a collection one or something went wrong during split, so we proceed with the function's logic. + } + } + const mergeQueue = OnyxUtils.getMergeQueue(); const mergeQueuePromise = OnyxUtils.getMergeQueuePromise(); @@ -346,19 +397,38 @@ function mergeCollection(collectionKey: TK return Promise.resolve(); } - const mergedCollection: OnyxInputKeyValueMapping = collection; + let resultCollection: OnyxInputKeyValueMapping = collection; + let resultCollectionKeys = Object.keys(resultCollection); // Confirm all the collection keys belong to the same parent - const mergedCollectionKeys = Object.keys(mergedCollection); - if (!OnyxUtils.doAllCollectionItemsBelongToSameParent(collectionKey, mergedCollectionKeys)) { + if (!OnyxUtils.doAllCollectionItemsBelongToSameParent(collectionKey, resultCollectionKeys)) { return Promise.resolve(); } + const skippableCollectionMemberIDs = OnyxUtils.getSkippableCollectionMemberIDs(); + if (skippableCollectionMemberIDs.size) { + resultCollection = resultCollectionKeys.reduce((result: OnyxInputKeyValueMapping, key) => { + try { + const [, collectionMemberID] = OnyxUtils.splitCollectionMemberKey(key, collectionKey); + // If the collection member key is a skippable one we set its value to null. + // eslint-disable-next-line no-param-reassign + result[key] = !skippableCollectionMemberIDs.has(collectionMemberID) ? resultCollection[key] : null; + } catch { + // Something went wrong during split, so we assign the data to result anyway. + // eslint-disable-next-line no-param-reassign + result[key] = resultCollection[key]; + } + + return result; + }, {}); + } + resultCollectionKeys = Object.keys(resultCollection); + return OnyxUtils.getAllKeys() .then((persistedKeys) => { // Split to keys that exist in storage and keys that don't - const keys = mergedCollectionKeys.filter((key) => { - if (mergedCollection[key] === null) { + const keys = resultCollectionKeys.filter((key) => { + if (resultCollection[key] === null) { OnyxUtils.remove(key); return false; } @@ -370,13 +440,13 @@ function mergeCollection(collectionKey: TK const cachedCollectionForExistingKeys = OnyxUtils.getCachedCollection(collectionKey, existingKeys); const existingKeyCollection = existingKeys.reduce((obj: OnyxInputKeyValueMapping, key) => { - const {isCompatible, existingValueType, newValueType} = utils.checkCompatibilityWithExistingValue(mergedCollection[key], cachedCollectionForExistingKeys[key]); + const {isCompatible, existingValueType, newValueType} = utils.checkCompatibilityWithExistingValue(resultCollection[key], cachedCollectionForExistingKeys[key]); if (!isCompatible) { Logger.logAlert(logMessages.incompatibleUpdateAlert(key, 'mergeCollection', existingValueType, newValueType)); return obj; } // eslint-disable-next-line no-param-reassign - obj[key] = mergedCollection[key]; + obj[key] = resultCollection[key]; return obj; }, {}) as Record>; @@ -385,7 +455,7 @@ function mergeCollection(collectionKey: TK if (persistedKeys.has(key)) { return; } - newCollection[key] = mergedCollection[key]; + newCollection[key] = resultCollection[key]; }); // When (multi-)merging the values with the existing values in storage, @@ -424,9 +494,9 @@ function mergeCollection(collectionKey: TK }); return Promise.all(promises) - .catch((error) => OnyxUtils.evictStorageAndRetry(error, mergeCollection, collectionKey, mergedCollection)) + .catch((error) => OnyxUtils.evictStorageAndRetry(error, mergeCollection, collectionKey, resultCollection)) .then(() => { - OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.MERGE_COLLECTION, undefined, mergedCollection); + OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.MERGE_COLLECTION, undefined, resultCollection); return promiseUpdate; }); }) @@ -735,8 +805,6 @@ function update(data: OnyxUpdate[]): Promise { .then(() => undefined); } -type BaseCollection = Record; - /** * Sets a collection by replacing all existing collection members with new values. * Any existing collection members not included in the new data will be removed. @@ -750,22 +818,43 @@ type BaseCollection = Record; * @param collectionKey e.g. `ONYXKEYS.COLLECTION.REPORT` * @param collection Object collection keyed by individual collection member keys and values */ -function setCollection(collectionKey: TKey, collection: OnyxMergeCollectionInput): Promise { - const newCollectionKeys = Object.keys(collection); +function setCollection(collectionKey: TKey, collection: OnyxMergeCollectionInput): Promise { + let resultCollection: OnyxInputKeyValueMapping = collection; + let resultCollectionKeys = Object.keys(resultCollection); - if (!OnyxUtils.doAllCollectionItemsBelongToSameParent(collectionKey, newCollectionKeys)) { + // Confirm all the collection keys belong to the same parent + if (!OnyxUtils.doAllCollectionItemsBelongToSameParent(collectionKey, resultCollectionKeys)) { Logger.logAlert(`setCollection called with keys that do not belong to the same parent ${collectionKey}. Skipping this update.`); return Promise.resolve(); } + const skippableCollectionMemberIDs = OnyxUtils.getSkippableCollectionMemberIDs(); + if (skippableCollectionMemberIDs.size) { + resultCollection = resultCollectionKeys.reduce((result: OnyxInputKeyValueMapping, key) => { + try { + const [, collectionMemberID] = OnyxUtils.splitCollectionMemberKey(key, collectionKey); + // If the collection member key is a skippable one we set its value to null. + // eslint-disable-next-line no-param-reassign + result[key] = !skippableCollectionMemberIDs.has(collectionMemberID) ? resultCollection[key] : null; + } catch { + // Something went wrong during split, so we assign the data to result anyway. + // eslint-disable-next-line no-param-reassign + result[key] = resultCollection[key]; + } + + return result; + }, {}); + } + resultCollectionKeys = Object.keys(resultCollection); + return OnyxUtils.getAllKeys().then((persistedKeys) => { - const mutableCollection: BaseCollection = {...collection}; + const mutableCollection: OnyxInputKeyValueMapping = {...resultCollection}; persistedKeys.forEach((key) => { if (!key.startsWith(collectionKey)) { return; } - if (newCollectionKeys.includes(key)) { + if (resultCollectionKeys.includes(key)) { return; } diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 15863624..a3ef656a 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -88,6 +88,9 @@ let lastSubscriptionID = 0; // Connections can be made before `Onyx.init`. They would wait for this task before resolving const deferredInitTask = createDeferredTask(); +// Holds a set of collection member IDs which updates will be ignored when using Onyx methods. +let skippableCollectionMemberIDs = new Set(); + function getSnapshotKey(): OnyxKey | null { return snapshotKey; } @@ -127,6 +130,20 @@ function getEvictionBlocklist(): Record { return evictionBlocklist; } +/** + * Getter - returns the skippable collection member IDs. + */ +function getSkippableCollectionMemberIDs(): Set { + return skippableCollectionMemberIDs; +} + +/** + * Setter - sets the skippable collection member IDs. + */ +function setSkippableCollectionMemberIDs(ids: Set): void { + skippableCollectionMemberIDs = ids; +} + /** * Sets the initial values for the Onyx store * @@ -257,6 +274,19 @@ function get>(key: TKey): P // Otherwise retrieve the value from storage and capture a promise to aid concurrent usages const promise = Storage.getItem(key) .then((val) => { + if (skippableCollectionMemberIDs.size) { + try { + const [, collectionMemberID] = splitCollectionMemberKey(key); + if (skippableCollectionMemberIDs.has(collectionMemberID)) { + // The key is a skippable one, so we set the value to undefined. + // eslint-disable-next-line no-param-reassign + val = undefined as OnyxValue; + } + } catch (e) { + // The key is not a collection one or something went wrong during split, so we proceed with the function's logic. + } + } + if (val === undefined) { cache.addNullishStorageKey(key); return undefined; @@ -335,6 +365,18 @@ function multiGet(keys: CollectionKeyBase[]): Promise = {}; values.forEach(([key, value]) => { + if (skippableCollectionMemberIDs.size) { + try { + const [, collectionMemberID] = OnyxUtils.splitCollectionMemberKey(key); + if (skippableCollectionMemberIDs.has(collectionMemberID)) { + // The key is a skippable one, so we skip this iteration. + return; + } + } catch (e) { + // The key is not a collection one or something went wrong during split, so we proceed with the function's logic. + } + } + dataMap.set(key, value as OnyxValue); temp[key] = value as OnyxValue; }); @@ -419,11 +461,23 @@ function isCollectionMemberKey(collect /** * Splits a collection member key into the collection key part and the ID part. * @param key - The collection member key to split. + * @param collectionKey - The collection key of the `key` param that can be passed in advance to optimize the function. * @returns A tuple where the first element is the collection part and the second element is the ID part, * or throws an Error if the key is not a collection one. */ -function splitCollectionMemberKey(key: TKey): [CollectionKeyType, string] { - const collectionKey = getCollectionKey(key); +function splitCollectionMemberKey( + key: TKey, + collectionKey?: string, +): [CollectionKeyType, string] { + if (collectionKey && !isCollectionMemberKey(collectionKey, key, collectionKey.length)) { + throw new Error(`Invalid '${collectionKey}' collection key provided, it isn't compatible with '${key}' key.`); + } + + if (!collectionKey) { + // eslint-disable-next-line no-param-reassign + collectionKey = getCollectionKey(key); + } + return [collectionKey as CollectionKeyType, key.slice(collectionKey.length)]; } @@ -1418,6 +1472,8 @@ const OnyxUtils = { subscribeToKey, unsubscribeFromKey, getEvictionBlocklist, + getSkippableCollectionMemberIDs, + setSkippableCollectionMemberIDs, }; GlobalSettings.addGlobalSettingsChangeListener(({enablePerformanceMetrics}) => { diff --git a/lib/types.ts b/lib/types.ts index 4f619e80..b722fa41 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -474,6 +474,12 @@ type InitOptions = { * @default false */ enablePerformanceMetrics?: boolean; + + /** + * Array of collection member IDs which updates will be ignored when using Onyx methods. + * Additionally, any subscribers from these keys to won't receive any data from Onyx. + */ + skippableCollectionMemberIDs?: string[]; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/tests/components/ViewWithCollections.tsx b/tests/components/ViewWithCollections.tsx index 799168c6..fd15f5e9 100644 --- a/tests/components/ViewWithCollections.tsx +++ b/tests/components/ViewWithCollections.tsx @@ -26,7 +26,7 @@ function ViewWithCollections( {Object.values(collections).map((collection, i) => ( // eslint-disable-next-line react/no-array-index-key - {collection.ID} + {collection?.ID} ))} ); diff --git a/tests/unit/onyxTest.ts b/tests/unit/onyxTest.ts index ed75c46c..44400e71 100644 --- a/tests/unit/onyxTest.ts +++ b/tests/unit/onyxTest.ts @@ -30,6 +30,7 @@ Onyx.init({ [ONYX_KEYS.OTHER_TEST]: 42, [ONYX_KEYS.KEY_WITH_UNDERSCORE]: 'default', }, + skippableCollectionMemberIDs: ['skippable-id'], }); describe('Onyx', () => { @@ -1906,4 +1907,113 @@ describe('Onyx', () => { }); }); }); + + describe('skippable collection member ids', () => { + it('should skip the collection member id value when using Onyx.set()', async () => { + let testKeyValue: unknown; + connection = Onyx.connect({ + key: ONYX_KEYS.COLLECTION.TEST_KEY, + initWithStoredValues: false, + waitForCollectionCallback: true, + callback: (value) => { + testKeyValue = value; + }, + }); + + await Onyx.set(`${ONYX_KEYS.COLLECTION.TEST_KEY}entry1`, {id: 'entry1_id', name: 'entry2_name'}); + await Onyx.set(`${ONYX_KEYS.COLLECTION.TEST_KEY}skippable-id`, {id: 'skippable-id_id', name: 'skippable-id_name'}); + + expect(testKeyValue).toEqual({ + [`${ONYX_KEYS.COLLECTION.TEST_KEY}entry1`]: {id: 'entry1_id', name: 'entry2_name'}, + }); + }); + + it('should skip the collection member id value when using Onyx.merge()', async () => { + let testKeyValue: unknown; + connection = Onyx.connect({ + key: ONYX_KEYS.COLLECTION.TEST_KEY, + initWithStoredValues: false, + waitForCollectionCallback: true, + callback: (value) => { + testKeyValue = value; + }, + }); + + await Onyx.merge(`${ONYX_KEYS.COLLECTION.TEST_KEY}entry1`, {id: 'entry1_id', name: 'entry2_name'}); + await Onyx.merge(`${ONYX_KEYS.COLLECTION.TEST_KEY}skippable-id`, {id: 'skippable-id_id', name: 'skippable-id_name'}); + + expect(testKeyValue).toEqual({ + [`${ONYX_KEYS.COLLECTION.TEST_KEY}entry1`]: {id: 'entry1_id', name: 'entry2_name'}, + }); + }); + + it('should skip the collection member id value when using Onyx.mergeCollection()', async () => { + let testKeyValue: unknown; + connection = Onyx.connect({ + key: ONYX_KEYS.COLLECTION.TEST_KEY, + initWithStoredValues: false, + waitForCollectionCallback: true, + callback: (value) => { + testKeyValue = value; + }, + }); + + await Onyx.mergeCollection(ONYX_KEYS.COLLECTION.TEST_KEY, { + [`${ONYX_KEYS.COLLECTION.TEST_KEY}entry1`]: {id: 'entry1_id', name: 'entry1_name'}, + [`${ONYX_KEYS.COLLECTION.TEST_KEY}entry2`]: {id: 'entry2_id', name: 'entry2_name'}, + [`${ONYX_KEYS.COLLECTION.TEST_KEY}skippable-id`]: {id: 'skippable-id_id', name: 'skippable-id_name'}, + } as GenericCollection); + + expect(testKeyValue).toEqual({ + [`${ONYX_KEYS.COLLECTION.TEST_KEY}entry1`]: {id: 'entry1_id', name: 'entry1_name'}, + [`${ONYX_KEYS.COLLECTION.TEST_KEY}entry2`]: {id: 'entry2_id', name: 'entry2_name'}, + }); + }); + + it('should skip the collection member id value when using Onyx.setCollection()', async () => { + let testKeyValue: unknown; + connection = Onyx.connect({ + key: ONYX_KEYS.COLLECTION.TEST_KEY, + initWithStoredValues: false, + waitForCollectionCallback: true, + callback: (value) => { + testKeyValue = value; + }, + }); + + await Onyx.setCollection(ONYX_KEYS.COLLECTION.TEST_KEY, { + [`${ONYX_KEYS.COLLECTION.TEST_KEY}entry1`]: {id: 'entry1_id', name: 'entry1_name'}, + [`${ONYX_KEYS.COLLECTION.TEST_KEY}entry2`]: {id: 'entry2_id', name: 'entry2_name'}, + [`${ONYX_KEYS.COLLECTION.TEST_KEY}skippable-id`]: {id: 'skippable-id_id', name: 'skippable-id_name'}, + } as GenericCollection); + + expect(testKeyValue).toEqual({ + [`${ONYX_KEYS.COLLECTION.TEST_KEY}entry1`]: {id: 'entry1_id', name: 'entry1_name'}, + [`${ONYX_KEYS.COLLECTION.TEST_KEY}entry2`]: {id: 'entry2_id', name: 'entry2_name'}, + }); + }); + + it('should skip the collection member id value when using Onyx.multiSet()', async () => { + let testKeyValue: unknown; + connection = Onyx.connect({ + key: ONYX_KEYS.COLLECTION.TEST_KEY, + initWithStoredValues: false, + waitForCollectionCallback: true, + callback: (value) => { + testKeyValue = value; + }, + }); + + await Onyx.multiSet({ + [`${ONYX_KEYS.COLLECTION.TEST_KEY}entry1`]: {id: 'entry1_id', name: 'entry1_name'}, + [`${ONYX_KEYS.COLLECTION.TEST_KEY}entry2`]: {id: 'entry2_id', name: 'entry2_name'}, + [`${ONYX_KEYS.COLLECTION.TEST_KEY}skippable-id`]: {id: 'skippable-id_id', name: 'skippable-id_name'}, + } as GenericCollection); + + expect(testKeyValue).toEqual({ + [`${ONYX_KEYS.COLLECTION.TEST_KEY}entry1`]: {id: 'entry1_id', name: 'entry1_name'}, + [`${ONYX_KEYS.COLLECTION.TEST_KEY}entry2`]: {id: 'entry2_id', name: 'entry2_name'}, + }); + }); + }); }); diff --git a/tests/unit/onyxUtilsTest.ts b/tests/unit/onyxUtilsTest.ts index 305c7cbc..06e6be1d 100644 --- a/tests/unit/onyxUtilsTest.ts +++ b/tests/unit/onyxUtilsTest.ts @@ -45,6 +45,18 @@ describe('OnyxUtils', () => { OnyxUtils.splitCollectionMemberKey(''); }).toThrowError("Invalid '' key provided, only collection keys are allowed."); }); + + it('should allow passing the collection key beforehand for performance gains', () => { + const [collectionKey, id] = OnyxUtils.splitCollectionMemberKey(`${ONYXKEYS.COLLECTION.TEST_KEY}id1`, ONYXKEYS.COLLECTION.TEST_KEY); + expect(collectionKey).toEqual(ONYXKEYS.COLLECTION.TEST_KEY); + expect(id).toEqual('id1'); + }); + + it("should throw error if the passed collection key isn't compatible with the key", () => { + expect(() => { + OnyxUtils.splitCollectionMemberKey(`${ONYXKEYS.COLLECTION.TEST_KEY}id1`, ONYXKEYS.COLLECTION.TEST_LEVEL_KEY); + }).toThrowError("Invalid 'test_level_' collection key provided, it isn't compatible with 'test_id1' key."); + }); }); describe('getCollectionKey', () => { diff --git a/tests/unit/useOnyxTest.ts b/tests/unit/useOnyxTest.ts index 2c874469..92a7ffb5 100644 --- a/tests/unit/useOnyxTest.ts +++ b/tests/unit/useOnyxTest.ts @@ -19,6 +19,7 @@ const ONYXKEYS = { Onyx.init({ keys: ONYXKEYS, safeEvictionKeys: [ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY], + skippableCollectionMemberIDs: ['skippable-id'], }); beforeEach(() => Onyx.clear()); @@ -659,6 +660,57 @@ describe('useOnyx', () => { }); }); + describe('skippable collection member ids', () => { + it('should always return undefined entry when subscribing to a collection with skippable member ids', async () => { + Onyx.mergeCollection(ONYXKEYS.COLLECTION.TEST_KEY, { + [`${ONYXKEYS.COLLECTION.TEST_KEY}entry1`]: {id: 'entry1_id', name: 'entry1_name'}, + [`${ONYXKEYS.COLLECTION.TEST_KEY}entry2`]: {id: 'entry2_id', name: 'entry2_name'}, + [`${ONYXKEYS.COLLECTION.TEST_KEY}skippable-id`]: {id: 'skippable-id_id', name: 'skippable-id_name'}, + } as GenericCollection); + + const {result} = renderHook(() => useOnyx(ONYXKEYS.COLLECTION.TEST_KEY)); + + await act(async () => waitForPromisesToResolve()); + + expect(result.current[0]).toEqual({ + [`${ONYXKEYS.COLLECTION.TEST_KEY}entry1`]: {id: 'entry1_id', name: 'entry1_name'}, + [`${ONYXKEYS.COLLECTION.TEST_KEY}entry2`]: {id: 'entry2_id', name: 'entry2_name'}, + }); + expect(result.current[1].status).toEqual('loaded'); + + await act(async () => + Onyx.mergeCollection(ONYXKEYS.COLLECTION.TEST_KEY, { + [`${ONYXKEYS.COLLECTION.TEST_KEY}entry1`]: {id: 'entry1_id', name: 'entry1_name_changed'}, + [`${ONYXKEYS.COLLECTION.TEST_KEY}entry2`]: {id: 'entry2_id', name: 'entry2_name_changed'}, + [`${ONYXKEYS.COLLECTION.TEST_KEY}skippable-id`]: {id: 'skippable-id_id', name: 'skippable-id_name_changed'}, + } as GenericCollection), + ); + + expect(result.current[0]).toEqual({ + [`${ONYXKEYS.COLLECTION.TEST_KEY}entry1`]: {id: 'entry1_id', name: 'entry1_name_changed'}, + [`${ONYXKEYS.COLLECTION.TEST_KEY}entry2`]: {id: 'entry2_id', name: 'entry2_name_changed'}, + }); + expect(result.current[1].status).toEqual('loaded'); + }); + + it('should always return undefined when subscribing to a skippable collection member id', async () => { + // @ts-expect-error bypass + await StorageMock.setItem(`${ONYXKEYS.COLLECTION.TEST_KEY}skippable-id`, 'skippable-id_value'); + + const {result} = renderHook(() => useOnyx(`${ONYXKEYS.COLLECTION.TEST_KEY}skippable-id`)); + + await act(async () => waitForPromisesToResolve()); + + expect(result.current[0]).toBeUndefined(); + expect(result.current[1].status).toEqual('loaded'); + + await act(async () => Onyx.merge(`${ONYXKEYS.COLLECTION.TEST_KEY}skippable-id`, 'skippable-id_value_changed')); + + expect(result.current[0]).toBeUndefined(); + expect(result.current[1].status).toEqual('loaded'); + }); + }); + // This test suite must be the last one to avoid problems when running the other tests here. describe('canEvict', () => { const error = (key: string) => `canEvict can't be used on key '${key}'. This key must explicitly be flagged as safe for removal by adding it to Onyx.init({safeEvictionKeys: []}).`; diff --git a/tests/unit/withOnyxTest.tsx b/tests/unit/withOnyxTest.tsx index b29e8f40..a36eb30d 100644 --- a/tests/unit/withOnyxTest.tsx +++ b/tests/unit/withOnyxTest.tsx @@ -1,6 +1,6 @@ /* eslint-disable rulesdir/onyx-props-must-have-default */ import React from 'react'; -import {render} from '@testing-library/react-native'; +import {act, render} from '@testing-library/react-native'; import Onyx, {withOnyx} from '../../lib'; import type {ViewWithTextOnyxProps, ViewWithTextProps} from '../components/ViewWithText'; import ViewWithText from '../components/ViewWithText'; @@ -28,6 +28,7 @@ const ONYX_KEYS = { Onyx.init({ keys: ONYX_KEYS, + skippableCollectionMemberIDs: ['skippable-id'], }); beforeEach(() => Onyx.clear()); @@ -817,4 +818,61 @@ describe('withOnyxTest', () => { textComponent = renderResult.getByText('null'); expect(textComponent).not.toBeNull(); }); + + describe('skippable collection member ids', () => { + it('should always return undefined entry when subscribing to a collection with skippable member ids', async () => { + await Onyx.mergeCollection(ONYX_KEYS.COLLECTION.TEST_KEY, { + [`${ONYX_KEYS.COLLECTION.TEST_KEY}entry1`]: {ID: 'entry1_id'}, + [`${ONYX_KEYS.COLLECTION.TEST_KEY}entry2`]: {ID: 'entry2_id'}, + [`${ONYX_KEYS.COLLECTION.TEST_KEY}skippable-id`]: {ID: 'skippable-id_id'}, + } as GenericCollection); + + const TestComponentWithOnyx = withOnyx({ + collections: { + key: ONYX_KEYS.COLLECTION.TEST_KEY, + }, + })(ViewWithCollections); + const renderResult = render(); + + await waitForPromisesToResolve(); + + expect(renderResult.queryByText('entry1_id')).not.toBeNull(); + expect(renderResult.queryByText('entry2_id')).not.toBeNull(); + expect(renderResult.queryByText('entry3_id')).toBeNull(); + + await act(async () => + Onyx.mergeCollection(ONYX_KEYS.COLLECTION.TEST_KEY, { + [`${ONYX_KEYS.COLLECTION.TEST_KEY}entry1`]: {ID: 'entry1_id_changed'}, + [`${ONYX_KEYS.COLLECTION.TEST_KEY}entry2`]: {ID: 'entry2_id_changed'}, + [`${ONYX_KEYS.COLLECTION.TEST_KEY}skippable-id`]: {ID: 'skippable-id_id_changed'}, + } as GenericCollection), + ); + + expect(renderResult.queryByText('entry1_id_changed')).not.toBeNull(); + expect(renderResult.queryByText('entry2_id_changed')).not.toBeNull(); + expect(renderResult.queryByText('skippable-id_id_changed')).toBeNull(); + }); + + it('should always return undefined when subscribing to a skippable collection member id', async () => { + const TestComponentWithOnyx = withOnyx({ + text: { + key: `${ONYX_KEYS.COLLECTION.TEST_KEY}skippable-id`, + }, + })(ViewWithText); + + // @ts-expect-error bypass + await StorageMock.setItem(`${ONYX_KEYS.COLLECTION.TEST_KEY}skippable-id`, 'skippable-id_value'); + + const renderResult = render(); + + await waitForPromisesToResolve(); + await waitForPromisesToResolve(); + + expect(renderResult.queryByText('skippable-id_value')).toBeNull(); + + await act(async () => Onyx.merge(`${ONYX_KEYS.COLLECTION.TEST_KEY}skippable-id`, 'skippable-id_value_changed')); + + expect(renderResult.queryByText('skippable-id_value_changed')).toBeNull(); + }); + }); });