From 93918c4d7876fbc1ebf6f0f7bb7ab759ebed6bb4 Mon Sep 17 00:00:00 2001 From: Rafe Colton Date: Sun, 22 Mar 2026 13:52:15 -0700 Subject: [PATCH 1/5] Allow adding collections to keysToPreserve --- lib/Onyx.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Onyx.ts b/lib/Onyx.ts index 3da6a4de..04b8d361 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -344,7 +344,7 @@ function clear(keysToPreserve: OnyxKey[] = []): Promise { // to null would cause unknown behavior) // 2.1 However, if a default key was explicitly set to null, we need to reset it to the default value for (const key of allKeys) { - const isKeyToPreserve = keysToPreserve.includes(key); + const isKeyToPreserve = keysToPreserve.some((preserveKey) => OnyxUtils.isKeyMatch(preserveKey, key)); const isDefaultKey = key in defaultKeyStates; // If the key is being removed or reset to default: @@ -382,7 +382,7 @@ function clear(keysToPreserve: OnyxKey[] = []): Promise { // Exclude RAM-only keys to prevent them from being saved to storage const defaultKeyValuePairs = Object.entries( Object.keys(defaultKeyStates) - .filter((key) => !keysToPreserve.includes(key) && !OnyxUtils.isRamOnlyKey(key)) + .filter((key) => !keysToPreserve.some((preserveKey) => OnyxUtils.isKeyMatch(preserveKey, key)) && !OnyxUtils.isRamOnlyKey(key)) .reduce((obj: KeyValueMapping, key) => { // eslint-disable-next-line no-param-reassign obj[key] = defaultKeyStates[key]; From ab3e7b1f88614862197e9ee8b0beda627f2545d8 Mon Sep 17 00:00:00 2001 From: Rafe Colton Date: Sun, 22 Mar 2026 14:00:56 -0700 Subject: [PATCH 2/5] Add unit tests --- tests/unit/onyxClearWebStorageTest.ts | 81 +++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/tests/unit/onyxClearWebStorageTest.ts b/tests/unit/onyxClearWebStorageTest.ts index f98ff510..8f56f904 100644 --- a/tests/unit/onyxClearWebStorageTest.ts +++ b/tests/unit/onyxClearWebStorageTest.ts @@ -127,6 +127,87 @@ describe('Set data while storage is clearing', () => { }); }); + it('should preserve all collection members when a collection key is passed to keysToPreserve', () => { + expect.assertions(6); + + const collectionItemKey1 = `${ONYX_KEYS.COLLECTION.TEST}1`; + const collectionItemKey2 = `${ONYX_KEYS.COLLECTION.TEST}2`; + + // Given that Onyx has a collection with two items + return Onyx.mergeCollection(ONYX_KEYS.COLLECTION.TEST, { + [collectionItemKey1]: {id: 1, name: 'first'}, + [collectionItemKey2]: {id: 2, name: 'second'}, + } as GenericCollection) + .then(() => { + // When clear is called with the collection prefix as a key to preserve + return Onyx.clear([ONYX_KEYS.COLLECTION.TEST]); + }) + .then(() => waitForPromisesToResolve()) + .then(() => { + // Then both collection members are preserved in the cache and storage + expect(cache.get(collectionItemKey1)).toEqual({id: 1, name: 'first'}); + expect(cache.get(collectionItemKey2)).toEqual({id: 2, name: 'second'}); + + return Promise.all([StorageMock.getItem(collectionItemKey1), StorageMock.getItem(collectionItemKey2)]); + }) + .then(([storedValue1, storedValue2]) => { + expect(storedValue1).toEqual({id: 1, name: 'first'}); + expect(storedValue2).toEqual({id: 2, name: 'second'}); + + // And non-collection keys are still cleared (default key reset to default) + expect(cache.get(ONYX_KEYS.DEFAULT_KEY)).toBe(DEFAULT_VALUE); + return expect(StorageMock.getItem(ONYX_KEYS.DEFAULT_KEY)).resolves.toBe(DEFAULT_VALUE); + }); + }); + + it('should preserve collection members and still clear regular keys not in keysToPreserve', () => { + expect.assertions(4); + + const collectionItemKey1 = `${ONYX_KEYS.COLLECTION.TEST}1`; + + // Given that Onyx has both a collection item and a regular key set + return Promise.all([Onyx.set(ONYX_KEYS.REGULAR_KEY, SET_VALUE), Onyx.mergeCollection(ONYX_KEYS.COLLECTION.TEST, {[collectionItemKey1]: 'value'} as GenericCollection)]) + .then(() => { + // When clear is called preserving only the collection + return Onyx.clear([ONYX_KEYS.COLLECTION.TEST]); + }) + .then(() => waitForPromisesToResolve()) + .then(() => { + // Then the collection member is preserved + expect(cache.get(collectionItemKey1)).toBe('value'); + return expect(StorageMock.getItem(collectionItemKey1)).resolves.toBe('value'); + }) + .then(() => { + // And the regular key is cleared + expect(cache.get(ONYX_KEYS.REGULAR_KEY)).toBeUndefined(); + return expect(StorageMock.getItem(ONYX_KEYS.REGULAR_KEY)).resolves.toBeNull(); + }); + }); + + it('should preserve both collection keys and individual keys when both are passed to keysToPreserve', () => { + expect.assertions(4); + + const collectionItemKey1 = `${ONYX_KEYS.COLLECTION.TEST}1`; + + // Given that Onyx has a collection item and a regular key set + return Promise.all([Onyx.set(ONYX_KEYS.REGULAR_KEY, SET_VALUE), Onyx.mergeCollection(ONYX_KEYS.COLLECTION.TEST, {[collectionItemKey1]: 'value'} as GenericCollection)]) + .then(() => { + // When clear is called preserving both the collection and the regular key + return Onyx.clear([ONYX_KEYS.COLLECTION.TEST, ONYX_KEYS.REGULAR_KEY]); + }) + .then(() => waitForPromisesToResolve()) + .then(() => { + // Then both the collection member and the regular key are preserved + expect(cache.get(collectionItemKey1)).toBe('value'); + expect(cache.get(ONYX_KEYS.REGULAR_KEY)).toBe(SET_VALUE); + return Promise.all([StorageMock.getItem(collectionItemKey1), StorageMock.getItem(ONYX_KEYS.REGULAR_KEY)]); + }) + .then(([storedCollectionValue, storedRegularValue]) => { + expect(storedCollectionValue).toBe('value'); + expect(storedRegularValue).toBe(SET_VALUE); + }); + }); + it('should only trigger the connection callback once when using wait for collection callback', () => { expect.assertions(4); From 0739d881fe6567dce189bde95a8a3f909ccb2956 Mon Sep 17 00:00:00 2001 From: Rafe Colton Date: Sun, 22 Mar 2026 14:09:27 -0700 Subject: [PATCH 3/5] Style --- tests/unit/onyxClearWebStorageTest.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/tests/unit/onyxClearWebStorageTest.ts b/tests/unit/onyxClearWebStorageTest.ts index 8f56f904..a5161b39 100644 --- a/tests/unit/onyxClearWebStorageTest.ts +++ b/tests/unit/onyxClearWebStorageTest.ts @@ -138,10 +138,8 @@ describe('Set data while storage is clearing', () => { [collectionItemKey1]: {id: 1, name: 'first'}, [collectionItemKey2]: {id: 2, name: 'second'}, } as GenericCollection) - .then(() => { - // When clear is called with the collection prefix as a key to preserve - return Onyx.clear([ONYX_KEYS.COLLECTION.TEST]); - }) + // When clear is called with the collection prefix as a key to preserve + .then(() => Onyx.clear([ONYX_KEYS.COLLECTION.TEST])) .then(() => waitForPromisesToResolve()) .then(() => { // Then both collection members are preserved in the cache and storage @@ -167,10 +165,8 @@ describe('Set data while storage is clearing', () => { // Given that Onyx has both a collection item and a regular key set return Promise.all([Onyx.set(ONYX_KEYS.REGULAR_KEY, SET_VALUE), Onyx.mergeCollection(ONYX_KEYS.COLLECTION.TEST, {[collectionItemKey1]: 'value'} as GenericCollection)]) - .then(() => { - // When clear is called preserving only the collection - return Onyx.clear([ONYX_KEYS.COLLECTION.TEST]); - }) + // When clear is called preserving only the collection + .then(() => Onyx.clear([ONYX_KEYS.COLLECTION.TEST])) .then(() => waitForPromisesToResolve()) .then(() => { // Then the collection member is preserved @@ -191,10 +187,8 @@ describe('Set data while storage is clearing', () => { // Given that Onyx has a collection item and a regular key set return Promise.all([Onyx.set(ONYX_KEYS.REGULAR_KEY, SET_VALUE), Onyx.mergeCollection(ONYX_KEYS.COLLECTION.TEST, {[collectionItemKey1]: 'value'} as GenericCollection)]) - .then(() => { - // When clear is called preserving both the collection and the regular key - return Onyx.clear([ONYX_KEYS.COLLECTION.TEST, ONYX_KEYS.REGULAR_KEY]); - }) + // When clear is called preserving both the collection and the regular key + .then(() => Onyx.clear([ONYX_KEYS.COLLECTION.TEST, ONYX_KEYS.REGULAR_KEY])) .then(() => waitForPromisesToResolve()) .then(() => { // Then both the collection member and the regular key are preserved From c676a20aa8db4c796d7ea5e832a1abe4b60f469d Mon Sep 17 00:00:00 2001 From: Rafe Colton Date: Sun, 22 Mar 2026 14:17:01 -0700 Subject: [PATCH 4/5] Prettier --- tests/unit/onyxClearWebStorageTest.ts | 102 ++++++++++++++------------ 1 file changed, 54 insertions(+), 48 deletions(-) diff --git a/tests/unit/onyxClearWebStorageTest.ts b/tests/unit/onyxClearWebStorageTest.ts index a5161b39..5089cc36 100644 --- a/tests/unit/onyxClearWebStorageTest.ts +++ b/tests/unit/onyxClearWebStorageTest.ts @@ -134,28 +134,30 @@ describe('Set data while storage is clearing', () => { const collectionItemKey2 = `${ONYX_KEYS.COLLECTION.TEST}2`; // Given that Onyx has a collection with two items - return Onyx.mergeCollection(ONYX_KEYS.COLLECTION.TEST, { - [collectionItemKey1]: {id: 1, name: 'first'}, - [collectionItemKey2]: {id: 2, name: 'second'}, - } as GenericCollection) - // When clear is called with the collection prefix as a key to preserve - .then(() => Onyx.clear([ONYX_KEYS.COLLECTION.TEST])) - .then(() => waitForPromisesToResolve()) - .then(() => { - // Then both collection members are preserved in the cache and storage - expect(cache.get(collectionItemKey1)).toEqual({id: 1, name: 'first'}); - expect(cache.get(collectionItemKey2)).toEqual({id: 2, name: 'second'}); + return ( + Onyx.mergeCollection(ONYX_KEYS.COLLECTION.TEST, { + [collectionItemKey1]: {id: 1, name: 'first'}, + [collectionItemKey2]: {id: 2, name: 'second'}, + } as GenericCollection) + // When clear is called with the collection prefix as a key to preserve + .then(() => Onyx.clear([ONYX_KEYS.COLLECTION.TEST])) + .then(() => waitForPromisesToResolve()) + .then(() => { + // Then both collection members are preserved in the cache and storage + expect(cache.get(collectionItemKey1)).toEqual({id: 1, name: 'first'}); + expect(cache.get(collectionItemKey2)).toEqual({id: 2, name: 'second'}); - return Promise.all([StorageMock.getItem(collectionItemKey1), StorageMock.getItem(collectionItemKey2)]); - }) - .then(([storedValue1, storedValue2]) => { - expect(storedValue1).toEqual({id: 1, name: 'first'}); - expect(storedValue2).toEqual({id: 2, name: 'second'}); + return Promise.all([StorageMock.getItem(collectionItemKey1), StorageMock.getItem(collectionItemKey2)]); + }) + .then(([storedValue1, storedValue2]) => { + expect(storedValue1).toEqual({id: 1, name: 'first'}); + expect(storedValue2).toEqual({id: 2, name: 'second'}); - // And non-collection keys are still cleared (default key reset to default) - expect(cache.get(ONYX_KEYS.DEFAULT_KEY)).toBe(DEFAULT_VALUE); - return expect(StorageMock.getItem(ONYX_KEYS.DEFAULT_KEY)).resolves.toBe(DEFAULT_VALUE); - }); + // And non-collection keys are still cleared (default key reset to default) + expect(cache.get(ONYX_KEYS.DEFAULT_KEY)).toBe(DEFAULT_VALUE); + return expect(StorageMock.getItem(ONYX_KEYS.DEFAULT_KEY)).resolves.toBe(DEFAULT_VALUE); + }) + ); }); it('should preserve collection members and still clear regular keys not in keysToPreserve', () => { @@ -164,20 +166,22 @@ describe('Set data while storage is clearing', () => { const collectionItemKey1 = `${ONYX_KEYS.COLLECTION.TEST}1`; // Given that Onyx has both a collection item and a regular key set - return Promise.all([Onyx.set(ONYX_KEYS.REGULAR_KEY, SET_VALUE), Onyx.mergeCollection(ONYX_KEYS.COLLECTION.TEST, {[collectionItemKey1]: 'value'} as GenericCollection)]) - // When clear is called preserving only the collection - .then(() => Onyx.clear([ONYX_KEYS.COLLECTION.TEST])) - .then(() => waitForPromisesToResolve()) - .then(() => { - // Then the collection member is preserved - expect(cache.get(collectionItemKey1)).toBe('value'); - return expect(StorageMock.getItem(collectionItemKey1)).resolves.toBe('value'); - }) - .then(() => { - // And the regular key is cleared - expect(cache.get(ONYX_KEYS.REGULAR_KEY)).toBeUndefined(); - return expect(StorageMock.getItem(ONYX_KEYS.REGULAR_KEY)).resolves.toBeNull(); - }); + return ( + Promise.all([Onyx.set(ONYX_KEYS.REGULAR_KEY, SET_VALUE), Onyx.mergeCollection(ONYX_KEYS.COLLECTION.TEST, {[collectionItemKey1]: 'value'} as GenericCollection)]) + // When clear is called preserving only the collection + .then(() => Onyx.clear([ONYX_KEYS.COLLECTION.TEST])) + .then(() => waitForPromisesToResolve()) + .then(() => { + // Then the collection member is preserved + expect(cache.get(collectionItemKey1)).toBe('value'); + return expect(StorageMock.getItem(collectionItemKey1)).resolves.toBe('value'); + }) + .then(() => { + // And the regular key is cleared + expect(cache.get(ONYX_KEYS.REGULAR_KEY)).toBeUndefined(); + return expect(StorageMock.getItem(ONYX_KEYS.REGULAR_KEY)).resolves.toBeNull(); + }) + ); }); it('should preserve both collection keys and individual keys when both are passed to keysToPreserve', () => { @@ -186,20 +190,22 @@ describe('Set data while storage is clearing', () => { const collectionItemKey1 = `${ONYX_KEYS.COLLECTION.TEST}1`; // Given that Onyx has a collection item and a regular key set - return Promise.all([Onyx.set(ONYX_KEYS.REGULAR_KEY, SET_VALUE), Onyx.mergeCollection(ONYX_KEYS.COLLECTION.TEST, {[collectionItemKey1]: 'value'} as GenericCollection)]) - // When clear is called preserving both the collection and the regular key - .then(() => Onyx.clear([ONYX_KEYS.COLLECTION.TEST, ONYX_KEYS.REGULAR_KEY])) - .then(() => waitForPromisesToResolve()) - .then(() => { - // Then both the collection member and the regular key are preserved - expect(cache.get(collectionItemKey1)).toBe('value'); - expect(cache.get(ONYX_KEYS.REGULAR_KEY)).toBe(SET_VALUE); - return Promise.all([StorageMock.getItem(collectionItemKey1), StorageMock.getItem(ONYX_KEYS.REGULAR_KEY)]); - }) - .then(([storedCollectionValue, storedRegularValue]) => { - expect(storedCollectionValue).toBe('value'); - expect(storedRegularValue).toBe(SET_VALUE); - }); + return ( + Promise.all([Onyx.set(ONYX_KEYS.REGULAR_KEY, SET_VALUE), Onyx.mergeCollection(ONYX_KEYS.COLLECTION.TEST, {[collectionItemKey1]: 'value'} as GenericCollection)]) + // When clear is called preserving both the collection and the regular key + .then(() => Onyx.clear([ONYX_KEYS.COLLECTION.TEST, ONYX_KEYS.REGULAR_KEY])) + .then(() => waitForPromisesToResolve()) + .then(() => { + // Then both the collection member and the regular key are preserved + expect(cache.get(collectionItemKey1)).toBe('value'); + expect(cache.get(ONYX_KEYS.REGULAR_KEY)).toBe(SET_VALUE); + return Promise.all([StorageMock.getItem(collectionItemKey1), StorageMock.getItem(ONYX_KEYS.REGULAR_KEY)]); + }) + .then(([storedCollectionValue, storedRegularValue]) => { + expect(storedCollectionValue).toBe('value'); + expect(storedRegularValue).toBe(SET_VALUE); + }) + ); }); it('should only trigger the connection callback once when using wait for collection callback', () => { From 8c59af32d2aa5f323a83d81d71b2ec4f84723f7a Mon Sep 17 00:00:00 2001 From: Rafe Colton Date: Mon, 23 Mar 2026 10:37:40 -0700 Subject: [PATCH 5/5] Apply PR suggestion --- lib/DevTools/RealDevTools.ts | 3 ++- tests/unit/DevToolsTest.ts | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/DevTools/RealDevTools.ts b/lib/DevTools/RealDevTools.ts index 4df7eb66..87c2e47f 100644 --- a/lib/DevTools/RealDevTools.ts +++ b/lib/DevTools/RealDevTools.ts @@ -1,4 +1,5 @@ import type {IDevTools, DevtoolsOptions, DevtoolsConnection, ReduxDevtools} from './types'; +import OnyxUtils from '../OnyxUtils'; const ERROR_LABEL = 'Onyx DevTools - Error: '; @@ -76,7 +77,7 @@ class RealDevTools implements IDevTools { clearState(keysToPreserve: string[] = []): void { const newState = Object.entries(this.state).reduce((obj: Record, [key, value]) => { // eslint-disable-next-line no-param-reassign - obj[key] = keysToPreserve.includes(key) ? value : this.defaultState[key]; + obj[key] = keysToPreserve.some((preserveKey) => OnyxUtils.isKeyMatch(preserveKey, key)) ? value : this.defaultState[key]; return obj; }, {}); diff --git a/tests/unit/DevToolsTest.ts b/tests/unit/DevToolsTest.ts index ee5f7dc8..60664c5b 100644 --- a/tests/unit/DevToolsTest.ts +++ b/tests/unit/DevToolsTest.ts @@ -137,5 +137,12 @@ describe('DevTools', () => { const devToolsInstance = getDevToolsInstance() as RealDevToolsType; expect(devToolsInstance['state']).toEqual({...initialKeyStates, [ONYX_KEYS.NUM_KEY]: 2}); }); + + it('Preserves collection member keys when a collection key is passed to keysToPreserve', async () => { + await Onyx.mergeCollection(ONYX_KEYS.COLLECTION.NUM_KEY, exampleCollection); + await Onyx.clear([ONYX_KEYS.COLLECTION.NUM_KEY]); + const devToolsInstance = getDevToolsInstance() as RealDevToolsType; + expect(devToolsInstance['state']).toEqual({...initialKeyStates, ...exampleCollection}); + }); }); });