diff --git a/API-INTERNAL.md b/API-INTERNAL.md index 4e29291a4..a31fa6135 100644 --- a/API-INTERNAL.md +++ b/API-INTERNAL.md @@ -58,6 +58,18 @@ is associated with a collection of keys.

isCollectionMember(key)

Checks if a given key is a collection member key (not just a collection key).

+
isRamOnlyKey(key)
+

Checks if a given key is a RAM-only key, RAM-only collection key, or a RAM-only collection member

+

For example:

+

For the following Onyx setup

+

ramOnlyKeys: ["ramOnlyKey", "ramOnlyCollection_"]

+ +
splitCollectionMemberKey(key, collectionKey)

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

@@ -307,6 +319,29 @@ Checks if a given key is a collection member key (not just a collection key). | --- | --- | | key | The key to check | + + +## isRamOnlyKey(key) ⇒ +Checks if a given key is a RAM-only key, RAM-only collection key, or a RAM-only collection member + +For example: + +For the following Onyx setup + +ramOnlyKeys: ["ramOnlyKey", "ramOnlyCollection_"] + +- `isRamOnlyKey("ramOnlyKey")` would return true +- `isRamOnlyKey("ramOnlyCollection_")` would return true +- `isRamOnlyKey("ramOnlyCollection_1")` would return true +- `isRamOnlyKey("someOtherKey")` would return false + +**Kind**: global function +**Returns**: true if key is a RAM-only key, RAM-only collection key, or a RAM-only collection member + +| Param | Description | +| --- | --- | +| key | The key to check | + ## splitCollectionMemberKey(key, collectionKey) ⇒ diff --git a/README.md b/README.md index de71666f7..e5384e8dc 100644 --- a/README.md +++ b/README.md @@ -460,6 +460,25 @@ Onyx.init({ }); ``` +### Using RAM-only keys + +You can choose not to save certain keys on disk and keep them RAM-only, that way their values will reset with each session. You just have to pass an array of `ramOnlyKeys` to the `Onyx.init` method. You can mark entire collections as RAM-only by including the collection key (e.g., `ONYXKEYS.COLLECTION.TEMP_DATA`). This will make all members of that collection RAM-only. Individual collection member keys cannot be selectively marked as RAM-only. + +```javascript +import Onyx from 'react-native-onyx'; + +Onyx.init({ + keys: ONYXKEYS, + ramOnlyKeys: [ + ONYXKEYS.RAM_ONLY_KEY_1, + ONYXKEYS.RAM_ONLY_KEY_2, + ONYXKEYS.COLLECTION.TEMP_DATA, + ], +}); +``` + +> Note: RAM-only keys still consume memory and will remain in cache until explicitly cleared or until Onyx.clear() is called. Use them judiciously for truly ephemeral data. + ### Usage The extension interface is pretty simple, on the left sidebar you can see all the updates made to the local storage, in ascending order, and on the right pane you can see the whole the current state, payload of an action and the diff between the previous state and the current state after the action was triggered. diff --git a/lib/Onyx.ts b/lib/Onyx.ts index 4bf93ca0e..bdac8c896 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -40,6 +40,7 @@ function init({ enablePerformanceMetrics = false, enableDevTools = true, skippableCollectionMemberIDs = [], + ramOnlyKeys = [], snapshotMergeKeys = [], }: InitOptions): void { if (enablePerformanceMetrics) { @@ -54,6 +55,8 @@ function init({ OnyxUtils.setSkippableCollectionMemberIDs(new Set(skippableCollectionMemberIDs)); OnyxUtils.setSnapshotMergeKeys(new Set(snapshotMergeKeys)); + cache.setRamOnlyKeys(new Set(ramOnlyKeys)); + if (shouldSyncMultipleInstances) { Storage.keepInstancesSync?.((key, value) => { cache.set(key, value); @@ -378,9 +381,10 @@ function clear(keysToPreserve: OnyxKey[] = []): Promise { updatePromises.push(OnyxUtils.scheduleNotifyCollectionSubscribers(key, value.newValues, value.oldValues)); } + // Exclude RAM-only keys to prevent them from being saved to storage const defaultKeyValuePairs = Object.entries( Object.keys(defaultKeyStates) - .filter((key) => !keysToPreserve.includes(key)) + .filter((key) => !keysToPreserve.includes(key) && !OnyxUtils.isRamOnlyKey(key)) .reduce((obj: KeyValueMapping, key) => { // eslint-disable-next-line no-param-reassign obj[key] = defaultKeyStates[key]; diff --git a/lib/OnyxCache.ts b/lib/OnyxCache.ts index bceada3b0..aa6d670d7 100644 --- a/lib/OnyxCache.ts +++ b/lib/OnyxCache.ts @@ -55,6 +55,9 @@ class OnyxCache { /** Set of collection keys for fast lookup */ private collectionKeys = new Set(); + /** Set of RAM-only keys for fast lookup */ + private ramOnlyKeys = new Set(); + constructor() { this.storageKeys = new Set(); this.nullishStorageKeys = new Set(); @@ -94,6 +97,8 @@ class OnyxCache { 'isCollectionKey', 'getCollectionKey', 'getCollectionData', + 'setRamOnlyKeys', + 'isRamOnlyKey', ); } @@ -474,6 +479,20 @@ class OnyxCache { // Return a shallow copy to ensure React detects changes when items are added/removed return {...cachedCollection}; } + + /** + * Set the RAM-only keys for optimized storage + */ + setRamOnlyKeys(ramOnlyKeys: Set): void { + this.ramOnlyKeys = ramOnlyKeys; + } + + /** + * Check if a key is a RAM-only key + */ + isRamOnlyKey(key: OnyxKey): boolean { + return this.ramOnlyKeys.has(key); + } } const instance = new OnyxCache(); diff --git a/lib/OnyxMerge/index.native.ts b/lib/OnyxMerge/index.native.ts index b796dfde4..ec8c242e3 100644 --- a/lib/OnyxMerge/index.native.ts +++ b/lib/OnyxMerge/index.native.ts @@ -28,8 +28,11 @@ const applyMerge: ApplyMerge = , hasChanged); + const shouldSkipStorageOperations = !hasChanged || OnyxUtils.isRamOnlyKey(key); + // If the value has not changed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead. - if (!hasChanged) { + // If the key is marked as RAM-only, it should not be saved nor updated in the storage. + if (shouldSkipStorageOperations) { return Promise.resolve({mergedValue, updatePromise}); } diff --git a/lib/OnyxMerge/index.ts b/lib/OnyxMerge/index.ts index 2a648499b..7eac789cb 100644 --- a/lib/OnyxMerge/index.ts +++ b/lib/OnyxMerge/index.ts @@ -20,8 +20,11 @@ const applyMerge: ApplyMerge = , hasChanged); + const shouldSkipStorageOperations = !hasChanged || OnyxUtils.isRamOnlyKey(key); + // If the value has not changed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead. - if (!hasChanged) { + // If the key is marked as RAM-only, it should not be saved nor updated in the storage. + if (shouldSkipStorageOperations) { return Promise.resolve({mergedValue, updatePromise}); } diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 2f73463cc..446e7bd2d 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -470,6 +470,35 @@ function isCollectionMember(key: OnyxKey): boolean { } } +/** + * Checks if a given key is a RAM-only key, RAM-only collection key, or a RAM-only collection member + * + * For example: + * + * For the following Onyx setup + * + * ramOnlyKeys: ["ramOnlyKey", "ramOnlyCollection_"] + * + * - `isRamOnlyKey("ramOnlyKey")` would return true + * - `isRamOnlyKey("ramOnlyCollection_")` would return true + * - `isRamOnlyKey("ramOnlyCollection_1")` would return true + * - `isRamOnlyKey("someOtherKey")` would return false + * + * @param key - The key to check + * @returns true if key is a RAM-only key, RAM-only collection key, or a RAM-only collection member + */ +function isRamOnlyKey(key: OnyxKey): boolean { + try { + const collectionKey = getCollectionKey(key); + // If collectionKey exists for a given key, check if it's a RAM-only key + return cache.isRamOnlyKey(collectionKey); + } catch { + // If getCollectionKey throws, the key is not a collection member + } + + return cache.isRamOnlyKey(key); +} + /** * Splits a collection member key into the collection key part and the ID part. * @param key - The collection member key to split. @@ -869,6 +898,11 @@ function scheduleNotifyCollectionSubscribers( function remove(key: TKey, isProcessingCollectionUpdate?: boolean): Promise { cache.drop(key); scheduleSubscriberUpdate(key, undefined as OnyxValue, undefined, isProcessingCollectionUpdate); + + if (isRamOnlyKey(key)) { + return Promise.resolve(); + } + return Storage.removeItem(key).then(() => undefined); } @@ -1344,6 +1378,12 @@ function setWithRetry({key, value, options}: SetParams OnyxUtils.retryOperation(error, setWithRetry, {key, value: valueWithoutNestedNullValues, options}, retryAttempt)) .then(() => { @@ -1394,7 +1434,13 @@ function multiSetWithRetry(data: OnyxMultiSetInput, retryAttempt?: number): Prom return OnyxUtils.scheduleSubscriberUpdate(key, value); }); - return Storage.multiSet(keyValuePairsToSet) + const keyValuePairsToStore = keyValuePairsToSet.filter((keyValuePair) => { + const [key] = keyValuePair; + // Filter out the RAM-only key value pairs, as they should not be saved to storage + return !isRamOnlyKey(key); + }); + + return Storage.multiSet(keyValuePairsToStore) .catch((error) => OnyxUtils.retryOperation(error, multiSetWithRetry, newData, retryAttempt)) .then(() => { OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.MULTI_SET, undefined, newData); @@ -1463,6 +1509,12 @@ function setCollectionWithRetry({collectionKey, const updatePromise = OnyxUtils.scheduleNotifyCollectionSubscribers(collectionKey, mutableCollection, previousCollection); + // RAM-only keys are not supposed to be saved to storage + if (isRamOnlyKey(collectionKey)) { + OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.SET_COLLECTION, undefined, mutableCollection); + return updatePromise; + } + return Storage.multiSet(keyValuePairs) .catch((error) => OnyxUtils.retryOperation(error, setCollectionWithRetry, {collectionKey, collection}, retryAttempt)) .then(() => { @@ -1573,11 +1625,13 @@ function mergeCollectionWithPatches( // New keys will be added via multiSet while existing keys will be updated using multiMerge // This is because setting a key that doesn't exist yet with multiMerge will throw errors - if (keyValuePairsForExistingCollection.length > 0) { + // We can skip this step for RAM-only keys as they should never be saved to storage + if (!isRamOnlyKey(collectionKey) && keyValuePairsForExistingCollection.length > 0) { promises.push(Storage.multiMerge(keyValuePairsForExistingCollection)); } - if (keyValuePairsForNewCollection.length > 0) { + // We can skip this step for RAM-only keys as they should never be saved to storage + if (!isRamOnlyKey(collectionKey) && keyValuePairsForNewCollection.length > 0) { promises.push(Storage.multiSet(keyValuePairsForNewCollection)); } @@ -1656,6 +1710,11 @@ function partialSetCollection({collectionKey, co const updatePromise = scheduleNotifyCollectionSubscribers(collectionKey, mutableCollection, previousCollection); + if (isRamOnlyKey(collectionKey)) { + sendActionToDevTools(METHOD.SET_COLLECTION, undefined, mutableCollection); + return updatePromise; + } + return Storage.multiSet(keyValuePairs) .catch((error) => retryOperation(error, partialSetCollection, {collectionKey, collection}, retryAttempt)) .then(() => { @@ -1741,6 +1800,7 @@ const OnyxUtils = { setWithRetry, multiSetWithRetry, setCollectionWithRetry, + isRamOnlyKey, }; GlobalSettings.addGlobalSettingsChangeListener(({enablePerformanceMetrics}) => { diff --git a/lib/types.ts b/lib/types.ts index 94b6622b4..a975d0480 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -426,6 +426,11 @@ type InitOptions = { */ skippableCollectionMemberIDs?: string[]; + /** + * Array of keys that when provided to Onyx are flagged as RAM-only keys, and thus are not saved to disk. + */ + ramOnlyKeys?: OnyxKey[]; + /** * A list of field names that should always be merged into snapshot entries even if those fields are * missing in the snapshot. Snapshots are saved "views" of a key's data used to populate read-only diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index b1670e3ce..0b28a450b 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -23,6 +23,7 @@ type UseOnyxOptions = { /** * If set to `false`, then no data will be prefilled into the component. + * @deprecated This param is going to be removed soon. Use RAM-only keys instead. */ initWithStoredValues?: boolean; diff --git a/tests/perf-test/OnyxConnectionManager.perf-test.ts b/tests/perf-test/OnyxConnectionManager.perf-test.ts index 19ac53202..284b4d0cb 100644 --- a/tests/perf-test/OnyxConnectionManager.perf-test.ts +++ b/tests/perf-test/OnyxConnectionManager.perf-test.ts @@ -8,6 +8,7 @@ import {getRandomReportActions} from '../utils/collections/reportActions'; const ONYXKEYS = { TEST_KEY: 'test', TEST_KEY_2: 'test2', + RAM_ONLY_TEST_KEY: 'ramOnlyTestKey', COLLECTION: { TEST_KEY: 'test_', TEST_NESTED_KEY: 'test_nested_', @@ -18,6 +19,7 @@ const ONYXKEYS = { TEST_KEY_5: 'test5_', EVICTABLE_TEST_KEY: 'evictable_test_', SNAPSHOT: 'snapshot_', + RAM_ONLY_TEST_COLLECTION: 'ramOnlyTestCollection_', }, }; @@ -47,6 +49,7 @@ describe('OnyxConnectionManager', () => { maxCachedKeysCount: 100000, evictableKeys: [ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY], skippableCollectionMemberIDs: ['skippable-id'], + ramOnlyKeys: [ONYXKEYS.RAM_ONLY_TEST_KEY, ONYXKEYS.COLLECTION.RAM_ONLY_TEST_COLLECTION], }); }); diff --git a/tests/perf-test/OnyxUtils.perf-test.ts b/tests/perf-test/OnyxUtils.perf-test.ts index 87e332b86..5a00d910e 100644 --- a/tests/perf-test/OnyxUtils.perf-test.ts +++ b/tests/perf-test/OnyxUtils.perf-test.ts @@ -14,6 +14,7 @@ import type {OnyxEntry, OnyxInputKeyValueMapping, OnyxKey, RetriableOnyxOperatio const ONYXKEYS = { TEST_KEY: 'test', TEST_KEY_2: 'test2', + RAM_ONLY_TEST_KEY: 'ramOnlyTestKey', COLLECTION: { TEST_KEY: 'test_', TEST_NESTED_KEY: 'test_nested_', @@ -24,6 +25,7 @@ const ONYXKEYS = { TEST_KEY_5: 'test5_', EVICTABLE_TEST_KEY: 'evictable_test_', SNAPSHOT: 'snapshot_', + RAM_ONLY_TEST_COLLECTION: 'ramOnlyTestCollection_', }, }; @@ -54,6 +56,7 @@ describe('OnyxUtils', () => { evictableKeys, initialKeyStates, skippableCollectionMemberIDs: ['skippable-id'], + ramOnlyKeys: [ONYXKEYS.RAM_ONLY_TEST_KEY, ONYXKEYS.COLLECTION.RAM_ONLY_TEST_COLLECTION], }); }); @@ -144,6 +147,20 @@ describe('OnyxUtils', () => { }); }); + describe('isRamOnlyKey', () => { + test('one call for RAM-only key', async () => { + await measureFunction(() => OnyxUtils.isRamOnlyKey(ONYXKEYS.RAM_ONLY_TEST_KEY)); + }); + + test('one call for RAM-only collection key', async () => { + await measureFunction(() => OnyxUtils.isRamOnlyKey(ONYXKEYS.COLLECTION.RAM_ONLY_TEST_COLLECTION)); + }); + + test('one call for RAM-only collection member key', async () => { + await measureFunction(() => OnyxUtils.isRamOnlyKey(`${ONYXKEYS.COLLECTION.RAM_ONLY_TEST_COLLECTION}1`)); + }); + }); + describe('isCollectionMemberKey', () => { test('one call with correct key', async () => { await measureFunction(() => OnyxUtils.isCollectionMemberKey(collectionKey, `${collectionKey}entry1`)); diff --git a/tests/perf-test/useOnyx.perf-test.tsx b/tests/perf-test/useOnyx.perf-test.tsx index 78672203b..963d5832d 100644 --- a/tests/perf-test/useOnyx.perf-test.tsx +++ b/tests/perf-test/useOnyx.perf-test.tsx @@ -11,6 +11,7 @@ const ONYXKEYS = { TEST_KEY: 'test', TEST_KEY_2: 'test2', TEST_KEY_3: 'test3', + RAM_ONLY_TEST_KEY: 'ramOnlyTestKey', }; const dataMatcher = (onyxKey: OnyxKey, expected: unknown) => `data: ${onyxKey}_${JSON.stringify(expected)}`; @@ -57,6 +58,7 @@ describe('useOnyx', () => { Onyx.init({ keys: ONYXKEYS, maxCachedKeysCount: 100000, + ramOnlyKeys: [ONYXKEYS.RAM_ONLY_TEST_KEY], }); }); diff --git a/tests/unit/onyxCacheTest.tsx b/tests/unit/onyxCacheTest.tsx index ae871f9bd..5f2154287 100644 --- a/tests/unit/onyxCacheTest.tsx +++ b/tests/unit/onyxCacheTest.tsx @@ -661,5 +661,25 @@ describe('Onyx', () => { expect(cache.hasCacheForKey(triggerKey)).toBe(true); }); }); + + it('should save RAM-only keys', () => { + const testKeys = { + ...ONYX_KEYS, + COLLECTION: { + ...ONYX_KEYS.COLLECTION, + RAM_ONLY_COLLECTION: 'ramOnlyCollection', + }, + RAM_ONLY_KEY: 'ramOnlyKey', + }; + + return initOnyx({ + keys: testKeys, + ramOnlyKeys: [testKeys.COLLECTION.RAM_ONLY_COLLECTION, testKeys.RAM_ONLY_KEY], + }).then(() => { + expect(cache.isRamOnlyKey(testKeys.RAM_ONLY_KEY)).toBeTruthy(); + expect(cache.isRamOnlyKey(testKeys.COLLECTION.RAM_ONLY_COLLECTION)).toBeTruthy(); + expect(cache.isRamOnlyKey(testKeys.TEST_KEY)).toBeFalsy(); + }); + }); }); }); diff --git a/tests/unit/onyxTest.ts b/tests/unit/onyxTest.ts index f0525109b..77842a9cc 100644 --- a/tests/unit/onyxTest.ts +++ b/tests/unit/onyxTest.ts @@ -24,7 +24,10 @@ const ONYX_KEYS = { ANIMALS: 'animals_', SNAPSHOT: 'snapshot_', ROUTES: 'routes_', + RAM_ONLY_COLLECTION: 'ramOnlyCollection_', }, + RAM_ONLY_TEST_KEY: 'ramOnlyKey', + RAM_ONLY_WITH_INITIAL_VALUE: 'ramOnlyWithInitialValue', }; Onyx.init({ @@ -32,7 +35,9 @@ Onyx.init({ initialKeyStates: { [ONYX_KEYS.OTHER_TEST]: 42, [ONYX_KEYS.KEY_WITH_UNDERSCORE]: 'default', + [ONYX_KEYS.RAM_ONLY_WITH_INITIAL_VALUE]: 'default', }, + ramOnlyKeys: [ONYX_KEYS.RAM_ONLY_TEST_KEY, ONYX_KEYS.COLLECTION.RAM_ONLY_COLLECTION, ONYX_KEYS.RAM_ONLY_WITH_INITIAL_VALUE], skippableCollectionMemberIDs: ['skippable-id'], snapshotMergeKeys: ['pendingAction', 'pendingFields'], }); @@ -2213,6 +2218,26 @@ describe('Onyx', () => { [`${ONYX_KEYS.COLLECTION.TEST_UPDATE}entry2`, entry2ExpectedResult], ]); }); + + it('should not save a RAM-only collection to storage', async () => { + const key1 = `${ONYX_KEYS.COLLECTION.RAM_ONLY_COLLECTION}1`; + const key2 = `${ONYX_KEYS.COLLECTION.RAM_ONLY_COLLECTION}2`; + + await Onyx.mergeCollection(ONYX_KEYS.COLLECTION.RAM_ONLY_COLLECTION, { + [key1]: 'value 1', + [key2]: 'value 2', + }); + + await Onyx.mergeCollection(ONYX_KEYS.COLLECTION.RAM_ONLY_COLLECTION, { + [key1]: 'updated value 1', + [key2]: 'updated value 2', + }); + + expect(await cache.get(key1)).toEqual('updated value 1'); + expect(await cache.get(key2)).toEqual('updated value 2'); + expect(await StorageMock.getItem(key1)).toBeNull(); + expect(await StorageMock.getItem(key2)).toBeNull(); + }); }); }); @@ -2364,6 +2389,43 @@ describe('Onyx', () => { Onyx.disconnect(connection1); Onyx.disconnect(connection2); }); + + it('should not save a RAM-only collection to storage', async () => { + const queuedUpdates: Array> = []; + + queuedUpdates.push( + { + key: `${ONYX_KEYS.COLLECTION.RAM_ONLY_COLLECTION}1`, + onyxMethod: 'merge', + value: null, + }, + { + key: `${ONYX_KEYS.COLLECTION.RAM_ONLY_COLLECTION}2`, + onyxMethod: 'merge', + value: null, + }, + ); + + queuedUpdates.push( + { + key: `${ONYX_KEYS.COLLECTION.RAM_ONLY_COLLECTION}1`, + onyxMethod: 'merge', + value: 'updated test 1', + }, + { + key: `${ONYX_KEYS.COLLECTION.RAM_ONLY_COLLECTION}2`, + onyxMethod: 'merge', + value: 'updated test 2', + }, + ); + + await Onyx.update(queuedUpdates); + + expect(cache.get(`${ONYX_KEYS.COLLECTION.RAM_ONLY_COLLECTION}1`)).toEqual('updated test 1'); + expect(cache.get(`${ONYX_KEYS.COLLECTION.RAM_ONLY_COLLECTION}2`)).toEqual('updated test 2'); + expect(await StorageMock.getItem(`${ONYX_KEYS.COLLECTION.RAM_ONLY_COLLECTION}1`)).toBeNull(); + expect(await StorageMock.getItem(`${ONYX_KEYS.COLLECTION.RAM_ONLY_COLLECTION}2`)).toBeNull(); + }); }); describe('merge', () => { @@ -2593,6 +2655,21 @@ describe('Onyx', () => { }); }); }); + + it('should not save a RAM-only key to storage when using merge', async () => { + await Onyx.merge(ONYX_KEYS.RAM_ONLY_TEST_KEY, {someProperty: 'value'}); + + expect(cache.get(ONYX_KEYS.RAM_ONLY_TEST_KEY)).toEqual({someProperty: 'value'}); + expect(await StorageMock.getItem(ONYX_KEYS.RAM_ONLY_TEST_KEY)).toBeNull(); + }); + + it('should not save a RAM-only collection member to storage when using merge', async () => { + const collectionMemberKey = `${ONYX_KEYS.COLLECTION.RAM_ONLY_COLLECTION}1`; + await Onyx.merge(collectionMemberKey, {data: 'test'}); + + expect(cache.get(collectionMemberKey)).toEqual({data: 'test'}); + expect(await StorageMock.getItem(collectionMemberKey)).toBeNull(); + }); }); describe('set', () => { @@ -2619,6 +2696,40 @@ describe('Onyx', () => { expect(testKeyValue).toEqual(testData); }); }); + + it('should not save a RAM-only key to storage', async () => { + await Onyx.set(ONYX_KEYS.RAM_ONLY_TEST_KEY, 'test'); + + expect(cache.get(ONYX_KEYS.RAM_ONLY_TEST_KEY)).toEqual('test'); + expect(await StorageMock.getItem(ONYX_KEYS.RAM_ONLY_TEST_KEY)).toBeNull(); + }); + + it('should not save a member of a RAM-only collection to storage', async () => { + const collectionMemberKey = `${ONYX_KEYS.COLLECTION.RAM_ONLY_COLLECTION}1`; + await Onyx.set(collectionMemberKey, 'test'); + + expect(cache.get(collectionMemberKey)).toEqual('test'); + expect(await StorageMock.getItem(collectionMemberKey)).toBeNull(); + }); + }); + + describe('multiSet', () => { + it('should only save non RAM-only keys to storage', async () => { + const otherTestValue = 'non ram only value'; + const collectionMemberKey = `${ONYX_KEYS.COLLECTION.RAM_ONLY_COLLECTION}2`; + + await Onyx.multiSet({ + [ONYX_KEYS.OTHER_TEST]: otherTestValue, + [ONYX_KEYS.RAM_ONLY_TEST_KEY]: 'test value 1', + [collectionMemberKey]: 'test value 2', + }); + + expect(await StorageMock.getItem(ONYX_KEYS.OTHER_TEST)).toEqual(otherTestValue); + expect(cache.get(ONYX_KEYS.RAM_ONLY_TEST_KEY)).toEqual('test value 1'); + expect(await StorageMock.getItem(ONYX_KEYS.RAM_ONLY_TEST_KEY)).toBeNull(); + expect(cache.get(collectionMemberKey)).toEqual('test value 2'); + expect(await StorageMock.getItem(collectionMemberKey)).toBeNull(); + }); }); describe('setCollection', () => { @@ -2734,6 +2845,21 @@ describe('Onyx', () => { [routeB]: {name: 'Route B'}, }); }); + + it('should not save a RAM-only collection to storage', async () => { + const key1 = `${ONYX_KEYS.COLLECTION.RAM_ONLY_COLLECTION}1`; + const key2 = `${ONYX_KEYS.COLLECTION.RAM_ONLY_COLLECTION}2`; + + await Onyx.setCollection(ONYX_KEYS.COLLECTION.RAM_ONLY_COLLECTION, { + [key1]: 'test1', + [key2]: 'test2', + }); + + expect(cache.get(key1)).toEqual('test1'); + expect(cache.get(key2)).toEqual('test2'); + expect(await StorageMock.getItem(key1)).toBeNull(); + expect(await StorageMock.getItem(key2)).toBeNull(); + }); }); describe('skippable collection member ids', () => { @@ -2864,4 +2990,21 @@ describe('Onyx', () => { jest.restoreAllMocks(); }); }); + + describe('clear', () => { + it('should handle RAM-only keys with defaults correctly during clear', async () => { + // Set a value for RAM-only key + await Onyx.set(ONYX_KEYS.RAM_ONLY_TEST_KEY, 'some value'); + await Onyx.set(ONYX_KEYS.RAM_ONLY_WITH_INITIAL_VALUE, 'some other value'); + + await Onyx.clear(); + + // Verify it's not in storage + expect(await StorageMock.getItem(ONYX_KEYS.RAM_ONLY_TEST_KEY)).toBeNull(); + expect(await StorageMock.getItem(ONYX_KEYS.RAM_ONLY_WITH_INITIAL_VALUE)).toBeNull(); + // Verify cache state based on whether there's a default + expect(cache.get(ONYX_KEYS.RAM_ONLY_TEST_KEY)).toBeUndefined(); + expect(cache.get(ONYX_KEYS.RAM_ONLY_WITH_INITIAL_VALUE)).toEqual('default'); + }); + }); }); diff --git a/tests/unit/onyxUtilsTest.ts b/tests/unit/onyxUtilsTest.ts index 36e1dd0eb..3448c6add 100644 --- a/tests/unit/onyxUtilsTest.ts +++ b/tests/unit/onyxUtilsTest.ts @@ -74,11 +74,14 @@ const ONYXKEYS = { TEST_LEVEL_KEY: 'test_level_', TEST_LEVEL_LAST_KEY: 'test_level_last_', ROUTES: 'routes_', + RAM_ONLY_COLLECTION: 'ramOnlyCollection_', }, + RAM_ONLY_KEY: 'ramOnlyKey', }; Onyx.init({ keys: ONYXKEYS, + ramOnlyKeys: [ONYXKEYS.RAM_ONLY_KEY, ONYXKEYS.COLLECTION.RAM_ONLY_COLLECTION], }); beforeEach(() => Onyx.clear()); @@ -487,4 +490,30 @@ describe('OnyxUtils', () => { expect(retryOperationSpy).toHaveBeenCalledTimes(1); }); }); + + describe('isRamOnlyKey', () => { + it('should return true for RAM-only key', () => { + expect(OnyxUtils.isRamOnlyKey(ONYXKEYS.RAM_ONLY_KEY)).toBeTruthy(); + }); + + it('should return true for RAM-only collection', () => { + expect(OnyxUtils.isRamOnlyKey(ONYXKEYS.COLLECTION.RAM_ONLY_COLLECTION)).toBeTruthy(); + }); + + it('should return true for RAM-only collection member', () => { + expect(OnyxUtils.isRamOnlyKey(`${ONYXKEYS.COLLECTION.RAM_ONLY_COLLECTION}1`)).toBeTruthy(); + }); + + it('should return false for a normal key', () => { + expect(OnyxUtils.isRamOnlyKey(ONYXKEYS.TEST_KEY)).toBeFalsy(); + }); + + it('should return false for normal collection', () => { + expect(OnyxUtils.isRamOnlyKey(ONYXKEYS.COLLECTION.TEST_KEY)).toBeFalsy(); + }); + + it('should return false for normal collection member', () => { + expect(OnyxUtils.isRamOnlyKey(`${ONYXKEYS.COLLECTION.TEST_KEY}1`)).toBeFalsy(); + }); + }); });