From 8a4acc0cff7f2f4a4c402e5d2ac666c84ec255d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 12 Mar 2026 19:09:30 +0000 Subject: [PATCH 1/9] Remove cache eviction system --- API-INTERNAL.md | 47 ++-- README.md | 38 ---- lib/Onyx.ts | 12 +- lib/OnyxCache.ts | 174 +------------- lib/OnyxConnectionManager.ts | 58 +---- lib/OnyxSnapshotCache.ts | 2 +- lib/OnyxUtils.ts | 84 +------ lib/types.ts | 13 -- lib/useOnyx.ts | 29 +-- tests/perf-test/Onyx.perf-test.ts | 4 - tests/perf-test/OnyxCache.perf-test.ts | 28 --- .../OnyxConnectionManager.perf-test.ts | 36 --- tests/perf-test/OnyxUtils.perf-test.ts | 98 +------- tests/perf-test/useOnyx.perf-test.tsx | 1 - tests/unit/OnyxConnectionManagerTest.ts | 61 ----- tests/unit/cacheEvictionTest.ts | 59 ----- tests/unit/onyxCacheTest.tsx | 213 ------------------ tests/unit/onyxUtilsTest.ts | 10 - tests/unit/useOnyxTest.ts | 83 ------- 19 files changed, 51 insertions(+), 999 deletions(-) delete mode 100644 tests/unit/cacheEvictionTest.ts diff --git a/API-INTERNAL.md b/API-INTERNAL.md index 34ec4bf37..148f09b9a 100644 --- a/API-INTERNAL.md +++ b/API-INTERNAL.md @@ -17,6 +17,11 @@
getDeferredInitTask()

Getter - returns the deffered init task.

+
afterInit(action)
+

Executes an action after Onyx has been initialized. +If Onyx is already initialized, the action is executed immediately. +Otherwise, it waits for initialization to complete before executing.

+
getSkippableCollectionMemberIDs()

Getter - returns the skippable collection member IDs.

@@ -29,7 +34,7 @@
setSnapshotMergeKeys()

Setter - sets the snapshot merge keys allowlist.

-
initStoreValues(keys, initialKeyStates, evictableKeys)
+
initStoreValues(keys, initialKeyStates)

Sets the initial values for the Onyx store

reduceCollectionWithSelector()
@@ -106,10 +111,6 @@ If the requested key is a collection, it will return an object with all the coll
sendDataToConnection()

Sends the data obtained from the keys to the connection.

-
addKeyToRecentlyAccessedIfNeeded()
-

We check to see if this key is flagged as safe for eviction and add it to the recentlyAccessedKeys list so that when we -run out of storage the least recently accessed key can be removed.

-
getCollectionDataAndSendAsObject()

Gets the data for a given an array of matching keys, combines them into an object, and sends the result back to the subscriber.

@@ -128,11 +129,10 @@ subscriber callbacks receive the data in a different format than they normally e

Remove a key from Onyx and update the subscribers

retryOperation()
-

Handles storage operation failures based on the error type:

+

Handles storage operation failures by retrying the operation.

broadcastUpdate()
@@ -223,6 +223,20 @@ Getter - returns the default key states. Getter - returns the deffered init task. **Kind**: global function + + +## afterInit(action) ⇒ +Executes an action after Onyx has been initialized. +If Onyx is already initialized, the action is executed immediately. +Otherwise, it waits for initialization to complete before executing. + +**Kind**: global function +**Returns**: The result of the action + +| Param | Description | +| --- | --- | +| action | The action to execute after initialization | + ## getSkippableCollectionMemberIDs() @@ -249,7 +263,7 @@ Setter - sets the snapshot merge keys allowlist. **Kind**: global function -## initStoreValues(keys, initialKeyStates, evictableKeys) +## initStoreValues(keys, initialKeyStates) Sets the initial values for the Onyx store **Kind**: global function @@ -258,7 +272,6 @@ Sets the initial values for the Onyx store | --- | --- | | keys | `ONYXKEYS` constants object from Onyx.init() | | initialKeyStates | initial data to set when `init()` and `clear()` are called | -| evictableKeys | This is an array of keys (individual or collection patterns) that when provided to Onyx are flagged as "safe" for removal. | @@ -393,7 +406,7 @@ For example: - `getCollectionKey("sharedNVP_user_-1_something")` would return "sharedNVP_user_" **Kind**: global function -**Returns**: The plain collection key or throws an Error if the key is not a collection one. +**Returns**: The plain collection key or undefined if the key is not a collection one. | Param | Description | | --- | --- | @@ -444,13 +457,6 @@ keyChanged(key, value, subscriber => subscriber.initWithStoredValues === false) ## sendDataToConnection() Sends the data obtained from the keys to the connection. -**Kind**: global function - - -## addKeyToRecentlyAccessedIfNeeded() -We check to see if this key is flagged as safe for eviction and add it to the recentlyAccessedKeys list so that when we -run out of storage the least recently accessed key can be removed. - **Kind**: global function @@ -496,10 +502,9 @@ Remove a key from Onyx and update the subscribers ## retryOperation() -Handles storage operation failures based on the error type: -- Storage capacity errors: evicts data and retries the operation +Handles storage operation failures by retrying the operation. - Invalid data errors: logs an alert and throws an error -- Other errors: retries the operation +- Other errors: retries the operation up to MAX_STORAGE_OPERATION_RETRY_ATTEMPTS times **Kind**: global function diff --git a/README.md b/README.md index dd32a0e66..5473dbb6a 100644 --- a/README.md +++ b/README.md @@ -314,43 +314,6 @@ If a platform needs to use a separate library (like using MMVK for react-native) [Docs](./API.md) -# Cache Eviction - -Different platforms come with varying storage capacities and Onyx has a way to gracefully fail when those storage limits are encountered. When Onyx fails to set or modify a key the following steps are taken: -1. Onyx looks at a list of recently accessed keys (access is defined as subscribed to or modified) and locates the key that was least recently accessed -2. It then deletes this key and retries the original operation - -By default, Onyx will not evict anything from storage and will presume all keys are "unsafe" to remove unless explicitly told otherwise. - -**To flag a key as safe for removal:** -- Add the key to the `evictableKeys` option in `Onyx.init(options)` -- Implement `canEvict` in the Onyx config for each component subscribing to a key -- The key will only be deleted when all subscribers return `true` for `canEvict` - -e.g. -```js -Onyx.init({ - evictableKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS], -}); -``` - -```js -const ReportActionsView = ({reportID, isActiveReport}) => { - const [reportActions] = useOnyx( - `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}_`, - {canEvict: () => !isActiveReport} - ); - - return ( - - {/* Render with reportActions data */} - - ); -}; - -export default ReportActionsView; -``` - # Benchmarks Provide the `captureMetrics` boolean flag to `Onyx.init` to capture call statistics @@ -358,7 +321,6 @@ Provide the `captureMetrics` boolean flag to `Onyx.init` to capture call statist ```js Onyx.init({ keys: ONYXKEYS, - evictableKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS], captureMetrics: Config.BENCHMARK_ONYX, }); ``` diff --git a/lib/Onyx.ts b/lib/Onyx.ts index a508c4043..feb9a3dd8 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -34,8 +34,6 @@ import OnyxMerge from './OnyxMerge'; function init({ keys = {}, initialKeyStates = {}, - evictableKeys = [], - maxCachedKeysCount = 1000, shouldSyncMultipleInstances = !!global.localStorage, enablePerformanceMetrics = false, enableDevTools = true, @@ -76,16 +74,10 @@ function init({ }); } - if (maxCachedKeysCount > 0) { - cache.setRecentKeysLimit(maxCachedKeysCount); - } - - OnyxUtils.initStoreValues(keys, initialKeyStates, evictableKeys); + OnyxUtils.initStoreValues(keys, initialKeyStates); // 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( - OnyxUtils.getDeferredInitTask().resolve, - ); + OnyxUtils.initializeWithDefaultKeyStates().then(OnyxUtils.getDeferredInitTask().resolve); } /** diff --git a/lib/OnyxCache.ts b/lib/OnyxCache.ts index fb6d4f795..1e053948d 100644 --- a/lib/OnyxCache.ts +++ b/lib/OnyxCache.ts @@ -3,7 +3,6 @@ import bindAll from 'lodash/bindAll'; import type {ValueOf} from 'type-fest'; import utils from './utils'; import type {OnyxKey, OnyxValue} from './types'; -import * as Str from './Str'; // Task constants const TASK = { @@ -25,9 +24,6 @@ class OnyxCache { /** A list of keys where a nullish value has been fetched from storage before, but the key still exists in cache */ private nullishStorageKeys: Set; - /** Unique list of keys maintained in access order (most recent at the end) */ - private recentKeys: Set; - /** A map of cached values */ private storageMap: Record>; @@ -40,18 +36,6 @@ class OnyxCache { */ private pendingPromises: Map | OnyxKey[]>>; - /** Maximum size of the keys store din cache */ - private maxRecentKeysSize = 0; - - /** List of keys that are safe to remove when we reach max storage */ - private evictionAllowList: OnyxKey[] = []; - - /** Map of keys and connection arrays whose keys will never be automatically evicted */ - private evictionBlocklist: Record = {}; - - /** List of keys that have been directly subscribed to or recently modified from least to most recent */ - private recentlyAccessedKeys = new Set(); - /** Set of collection keys for fast lookup */ private collectionKeys = new Set(); @@ -61,7 +45,6 @@ class OnyxCache { constructor() { this.storageKeys = new Set(); this.nullishStorageKeys = new Set(); - this.recentKeys = new Set(); this.storageMap = {}; this.collectionData = {}; this.pendingPromises = new Map(); @@ -82,17 +65,7 @@ class OnyxCache { 'hasPendingTask', 'getTaskPromise', 'captureTask', - 'addToAccessedKeys', - 'removeLeastRecentlyUsedKeys', - 'setRecentKeysLimit', 'setAllKeys', - 'setEvictionAllowList', - 'getEvictionBlocklist', - 'isEvictableKey', - 'removeLastAccessedKey', - 'addLastAccessedKey', - 'addEvictableKeysToRecentlyAccessedList', - 'getKeyForEviction', 'setCollectionKeys', 'isCollectionKey', 'getCollectionKey', @@ -151,12 +124,8 @@ class OnyxCache { /** * Get a cached value from storage - * @param [shouldReindexCache] – This is an LRU cache, and by default accessing a value will make it become last in line to be evicted. This flag can be used to skip that and just access the value directly without side-effects. */ - get(key: OnyxKey, shouldReindexCache = true): OnyxValue { - if (shouldReindexCache) { - this.addToAccessedKeys(key); - } + get(key: OnyxKey): OnyxValue { return this.storageMap[key]; } @@ -166,7 +135,6 @@ class OnyxCache { */ set(key: OnyxKey, value: OnyxValue): OnyxValue { this.addKey(key); - this.addToAccessedKeys(key); // When a key is explicitly set in cache, we can remove it from the list of nullish keys, // since it will either be set to a non nullish value or removed from the cache completely. @@ -212,7 +180,6 @@ class OnyxCache { } this.storageKeys.delete(key); - this.recentKeys.delete(key); } /** @@ -233,7 +200,6 @@ class OnyxCache { for (const [key, value] of Object.entries(data)) { this.addKey(key); - this.addToAccessedKeys(key); const collectionKey = this.getCollectionKey(key); @@ -291,148 +257,12 @@ class OnyxCache { return returnPromise; } - /** Adds a key to the top of the recently accessed keys */ - addToAccessedKeys(key: OnyxKey): void { - this.recentKeys.delete(key); - this.recentKeys.add(key); - } - - /** Remove keys that don't fall into the range of recently used keys */ - removeLeastRecentlyUsedKeys(): void { - const numKeysToRemove = this.recentKeys.size - this.maxRecentKeysSize; - if (numKeysToRemove <= 0) { - return; - } - - const iterator = this.recentKeys.values(); - const keysToRemove: OnyxKey[] = []; - - const recentKeysArray = Array.from(this.recentKeys); - const mostRecentKey = recentKeysArray[recentKeysArray.length - 1]; - - let iterResult = iterator.next(); - while (!iterResult.done) { - const key = iterResult.value; - // Don't consider the most recently accessed key for eviction - // This ensures we don't immediately evict a key we just added - if (key !== undefined && key !== mostRecentKey && this.isEvictableKey(key)) { - keysToRemove.push(key); - } - iterResult = iterator.next(); - } - - for (const key of keysToRemove) { - delete this.storageMap[key]; - - // Remove from collection data cache if this is a collection member - const collectionKey = this.getCollectionKey(key); - if (collectionKey && this.collectionData[collectionKey]) { - delete this.collectionData[collectionKey][key]; - } - this.recentKeys.delete(key); - } - } - - /** Set the recent keys list size */ - setRecentKeysLimit(limit: number): void { - this.maxRecentKeysSize = limit; - } - /** Check if the value has changed */ hasValueChanged(key: OnyxKey, value: OnyxValue): boolean { - const currentValue = this.get(key, false); + const currentValue = this.get(key); return !deepEqual(currentValue, value); } - /** - * Sets the list of keys that are considered safe for eviction - * @param keys - Array of OnyxKeys that are safe to evict - */ - setEvictionAllowList(keys: OnyxKey[]): void { - this.evictionAllowList = keys; - } - - /** - * Get the eviction block list that prevents keys from being evicted - */ - getEvictionBlocklist(): Record { - return this.evictionBlocklist; - } - - /** - * Checks to see if this key has been flagged as safe for removal. - * @param testKey - Key to check - */ - isEvictableKey(testKey: OnyxKey): boolean { - return this.evictionAllowList.some((key) => this.isKeyMatch(key, testKey)); - } - - /** - * Check if a given key matches a pattern key - * @param configKey - Pattern that may contain a wildcard - * @param key - Key to test against the pattern - */ - private isKeyMatch(configKey: OnyxKey, key: OnyxKey): boolean { - const isCollectionKey = configKey.endsWith('_'); - return isCollectionKey ? Str.startsWith(key, configKey) : configKey === key; - } - - /** - * Remove a key from the recently accessed key list. - */ - removeLastAccessedKey(key: OnyxKey): void { - this.recentlyAccessedKeys.delete(key); - } - - /** - * Add a key to the list of recently accessed keys. The least - * recently accessed key should be at the head and the most - * recently accessed key at the tail. - */ - addLastAccessedKey(key: OnyxKey, isCollectionKey: boolean): void { - // Only specific keys belong in this list since we cannot remove an entire collection. - if (isCollectionKey || !this.isEvictableKey(key)) { - return; - } - - this.removeLastAccessedKey(key); - this.recentlyAccessedKeys.add(key); - } - - /** - * Take all the keys that are safe to evict and add them to - * the recently accessed list when initializing the app. This - * enables keys that have not recently been accessed to be - * removed. - * @param isCollectionKeyFn - Function to determine if a key is a collection key - * @param getAllKeysFn - Function to get all keys, defaults to Storage.getAllKeys - */ - addEvictableKeysToRecentlyAccessedList(isCollectionKeyFn: (key: OnyxKey) => boolean, getAllKeysFn: () => Promise>): Promise { - return getAllKeysFn().then((keys: Set) => { - for (const evictableKey of this.evictionAllowList) { - for (const key of keys) { - if (!this.isKeyMatch(evictableKey, key)) { - continue; - } - - this.addLastAccessedKey(key, isCollectionKeyFn(key)); - } - } - }); - } - - /** - * Finds a key that can be safely evicted - */ - getKeyForEviction(): OnyxKey | undefined { - for (const key of this.recentlyAccessedKeys) { - if (!this.evictionBlocklist[key]) { - return key; - } - } - return undefined; - } - /** * Set the collection keys for optimized storage */ diff --git a/lib/OnyxConnectionManager.ts b/lib/OnyxConnectionManager.ts index b9d8d56cd..447206647 100644 --- a/lib/OnyxConnectionManager.ts +++ b/lib/OnyxConnectionManager.ts @@ -4,7 +4,6 @@ import type {ConnectOptions} from './Onyx'; import OnyxUtils from './OnyxUtils'; import * as Str from './Str'; import type {CollectionConnectCallback, DefaultConnectCallback, DefaultConnectOptions, OnyxKey, OnyxValue} from './types'; -import cache from './OnyxCache'; import onyxSnapshotCache from './OnyxSnapshotCache'; type ConnectCallback = DefaultConnectCallback | CollectionConnectCallback; @@ -105,7 +104,7 @@ class OnyxConnectionManager { this.sessionID = Str.guid(); // Binds all public methods to prevent problems with `this`. - bindAll(this, 'generateConnectionID', 'fireCallbacks', 'connect', 'disconnect', 'disconnectAll', 'refreshSessionID', 'addToEvictionBlockList', 'removeFromEvictionBlockList'); + bindAll(this, 'generateConnectionID', 'fireCallbacks', 'connect', 'disconnect', 'disconnectAll', 'refreshSessionID'); } /** @@ -239,7 +238,6 @@ class OnyxConnectionManager { // If the connection's callbacks map is empty we can safely unsubscribe from the Onyx key. if (connectionMetadata.callbacks.size === 0) { OnyxUtils.unsubscribeFromKey(connectionMetadata.subscriptionID); - this.removeFromEvictionBlockList(connection); this.connectionsMap.delete(connection.id); } @@ -249,11 +247,8 @@ class OnyxConnectionManager { * Disconnect all subscribers from Onyx. */ disconnectAll(): void { - for (const [connectionID, connectionMetadata] of this.connectionsMap.entries()) { + for (const [, connectionMetadata] of this.connectionsMap.entries()) { OnyxUtils.unsubscribeFromKey(connectionMetadata.subscriptionID); - for (const callbackID of connectionMetadata.callbacks.keys()) { - this.removeFromEvictionBlockList({id: connectionID, callbackID}); - } } this.connectionsMap.clear(); @@ -271,55 +266,6 @@ class OnyxConnectionManager { // Clear snapshot cache when session refreshes to avoid stale cache issues onyxSnapshotCache.clear(); } - - /** - * Adds the connection to the eviction block list. Connections added to this list can never be evicted. - * */ - addToEvictionBlockList(connection: Connection): void { - if (!connection) { - Logger.logInfo(`[ConnectionManager] Attempted to add connection to eviction block list passing an undefined connection object.`); - return; - } - - const connectionMetadata = this.connectionsMap.get(connection.id); - if (!connectionMetadata) { - Logger.logInfo(`[ConnectionManager] Attempted to add connection to eviction block list but no connection was found.`); - return; - } - - const evictionBlocklist = cache.getEvictionBlocklist(); - if (!evictionBlocklist[connectionMetadata.onyxKey]) { - evictionBlocklist[connectionMetadata.onyxKey] = []; - } - - evictionBlocklist[connectionMetadata.onyxKey]?.push(`${connection.id}_${connection.callbackID}`); - } - - /** - * Removes a connection previously added to this list - * which will enable it to be evicted again. - */ - removeFromEvictionBlockList(connection: Connection): void { - if (!connection) { - Logger.logInfo(`[ConnectionManager] Attempted to remove connection from eviction block list passing an undefined connection object.`); - return; - } - - const connectionMetadata = this.connectionsMap.get(connection.id); - if (!connectionMetadata) { - Logger.logInfo(`[ConnectionManager] Attempted to remove connection from eviction block list but no connection was found.`); - return; - } - - const evictionBlocklist = cache.getEvictionBlocklist(); - evictionBlocklist[connectionMetadata.onyxKey] = - evictionBlocklist[connectionMetadata.onyxKey]?.filter((evictionKey) => evictionKey !== `${connection.id}_${connection.callbackID}`) ?? []; - - // Remove the key if there are no more subscribers. - if (evictionBlocklist[connectionMetadata.onyxKey]?.length === 0) { - delete evictionBlocklist[connectionMetadata.onyxKey]; - } - } } const connectionManager = new OnyxConnectionManager(); diff --git a/lib/OnyxSnapshotCache.ts b/lib/OnyxSnapshotCache.ts index 1f47039d3..04f261f99 100644 --- a/lib/OnyxSnapshotCache.ts +++ b/lib/OnyxSnapshotCache.ts @@ -60,7 +60,7 @@ class OnyxSnapshotCache { * - `selector`: Different selectors produce different results, so each selector needs its own cache entry * - `initWithStoredValues`: This flag changes the initial loading behavior and affects the returned fetch status * - * Other options like `canEvict`, `reuseConnection`, and `allowDynamicKey` don't affect the data transformation + * Other options like `reuseConnection` and `allowDynamicKey` don't affect the data transformation * or timing behavior of getSnapshot, so they're excluded from the cache key for better cache hit rates. */ registerConsumer(options: Pick, 'selector' | 'initWithStoredValues'>): string { diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 50402ea10..6f1b65061 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -51,20 +51,6 @@ const METHOD = { CLEAR: 'clear', } as const; -// IndexedDB errors that indicate storage capacity issues where eviction can help -const IDB_STORAGE_ERRORS = [ - 'quotaexceedederror', // Browser storage quota exceeded -] as const; - -// SQLite errors that indicate storage capacity issues where eviction can help -const SQLITE_STORAGE_ERRORS = [ - 'database or disk is full', // Device storage is full - 'disk I/O error', // File system I/O failure, often due to insufficient space or corrupted storage - 'out of memory', // Insufficient RAM or storage space to complete the operation -] as const; - -const STORAGE_ERRORS = [...IDB_STORAGE_ERRORS, ...SQLITE_STORAGE_ERRORS]; - // Max number of retries for failed storage operations const MAX_STORAGE_OPERATION_RETRY_ATTEMPTS = 5; @@ -185,9 +171,8 @@ function setSnapshotMergeKeys(keys: Set): 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. */ -function initStoreValues(keys: DeepRecord, initialKeyStates: Partial, evictableKeys: OnyxKey[]): void { +function initStoreValues(keys: DeepRecord, initialKeyStates: Partial): 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[]; @@ -201,9 +186,6 @@ function initStoreValues(keys: DeepRecord, initialKeyStates: Pa DevTools.initState(initialKeyStates); - // Let Onyx know about which keys are safe to evict - cache.setEvictionAllowList(evictableKeys); - // Set collection keys in cache for optimized storage cache.setCollectionKeys(onyxCollectionKeySet); @@ -757,13 +739,6 @@ function keyChanged( canUpdateSubscriber: (subscriber?: CallbackToStateMapping) => boolean = () => true, isProcessingCollectionUpdate = false, ): void { - // Add or remove this key from the recentlyAccessedKeys lists - if (value !== null) { - cache.addLastAccessedKey(key, isCollectionKey(key)); - } else { - cache.removeLastAccessedKey(key); - } - // We get the subscribers interested in the key that has just changed. If the subscriber's key is a collection key then we will // notify them if the key that changed is a collection member. Or if it is a regular key notify them when there is an exact match. // Given the amount of times this function is called we need to make sure we are not iterating over all subscribers every time. On the other hand, we don't need to @@ -846,22 +821,6 @@ function sendDataToConnection(mapping: CallbackToStateMapp (mapping as DefaultConnectOptions).callback?.(valueToPass, matchedKey as TKey); } -/** - * We check to see if this key is flagged as safe for eviction and add it to the recentlyAccessedKeys list so that when we - * run out of storage the least recently accessed key can be removed. - */ -function addKeyToRecentlyAccessedIfNeeded(key: TKey): void { - if (!cache.isEvictableKey(key)) { - return; - } - - // Add the key to recentKeys first (this makes it the most recent key) - cache.addToAccessedKeys(key); - - // Try to free some cache whenever we connect to a safe eviction key - cache.removeLeastRecentlyUsedKeys(); -} - /** * Gets the data for a given an array of matching keys, combines them into an object, and sends the result back to the subscriber. */ @@ -942,10 +901,9 @@ function reportStorageQuota(): Promise { } /** - * Handles storage operation failures based on the error type: - * - Storage capacity errors: evicts data and retries the operation + * Handles storage operation failures by retrying the operation. * - Invalid data errors: logs an alert and throws an error - * - Other errors: retries the operation + * - Other errors: retries the operation up to MAX_STORAGE_OPERATION_RETRY_ATTEMPTS times */ function retryOperation(error: Error, onyxMethod: TMethod, defaultParams: Parameters[0], retryAttempt: number | undefined): Promise { const currentRetryAttempt = retryAttempt ?? 0; @@ -958,36 +916,14 @@ function retryOperation(error: Error, on throw error; } - const errorMessage = error?.message?.toLowerCase?.(); - const errorName = error?.name?.toLowerCase?.(); - const isStorageCapacityError = STORAGE_ERRORS.some((storageError) => errorName?.includes(storageError) || errorMessage?.includes(storageError)); - if (nextRetryAttempt > MAX_STORAGE_OPERATION_RETRY_ATTEMPTS) { - Logger.logAlert(`Storage operation failed after 5 retries. Error: ${error}. onyxMethod: ${onyxMethod.name}.`); + Logger.logAlert(`Storage operation failed after ${MAX_STORAGE_OPERATION_RETRY_ATTEMPTS} retries. Error: ${error}. onyxMethod: ${onyxMethod.name}.`); + reportStorageQuota(); return Promise.resolve(); } - if (!isStorageCapacityError) { - // @ts-expect-error No overload matches this call. - return onyxMethod(defaultParams, nextRetryAttempt); - } - - // Find the first key that we can remove that has no subscribers in our blocklist - const keyForRemoval = cache.getKeyForEviction(); - if (!keyForRemoval) { - // If we have no acceptable keys to remove then we are possibly trying to save mission critical data. If this is the case, - // then we should stop retrying as there is not much the user can do to fix this. Instead of getting them stuck in an infinite loop we - // will allow this write to be skipped. - Logger.logAlert('Out of storage. But found no acceptable keys to remove.'); - return reportStorageQuota(); - } - - // Remove the least recently viewed key that is not currently being accessed and retry. - Logger.logInfo(`Out of storage. Evicting least recently accessed key (${keyForRemoval}) and retrying.`); - reportStorageQuota(); - // @ts-expect-error No overload matches this call. - return remove(keyForRemoval).then(() => onyxMethod(defaultParams, nextRetryAttempt)); + return onyxMethod(defaultParams, nextRetryAttempt); } /** @@ -998,8 +934,6 @@ function broadcastUpdate(key: TKey, value: OnyxValue // all updates regardless of value changes (indicated by initWithStoredValues set to false). if (hasChanged) { cache.set(key, value); - } else { - cache.addToAccessedKeys(key); } return scheduleSubscriberUpdate(key, value, (subscriber) => hasChanged || subscriber?.initWithStoredValues === false).then(() => undefined); @@ -1177,7 +1111,8 @@ function subscribeToKey(connectOptions: ConnectOptions addKeyToRecentlyAccessedIfNeeded(mapping.key)) + // FIXME: We need this otherwise some tests fail. + .then(() => undefined) .then(() => { // Performance improvement // If the mapping is connected to an onyx key that is not a collection @@ -1371,7 +1306,7 @@ function setWithRetry({key, value, options}: SetParams; - /** - * This is an array of keys (individual or collection patterns) that when provided to Onyx are flagged - * as "safe" for removal. Any components subscribing to these keys must also implement a canEvict option. See the README for more info. - */ - evictableKeys?: OnyxKey[]; - - /** - * Sets how many recent keys should we try to keep in cache - * Setting this to 0 would practically mean no cache - * We try to free cache when we connect to a safe eviction key - */ - maxCachedKeysCount?: number; - /** * Auto synchronize storage events between multiple instances * of Onyx running in different tabs/windows. Defaults to true for platforms that support local storage (web/desktop) diff --git a/lib/useOnyx.ts b/lib/useOnyx.ts index 05784d805..3e852b20d 100644 --- a/lib/useOnyx.ts +++ b/lib/useOnyx.ts @@ -15,11 +15,6 @@ import useLiveRef from './useLiveRef'; type UseOnyxSelector> = (data: OnyxValue | undefined) => TReturnValue; type UseOnyxOptions = { - /** - * Determines if this key in this subscription is safe to be evicted. - */ - canEvict?: boolean; - /** * 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. @@ -198,22 +193,6 @@ function useOnyx>( // eslint-disable-next-line react-hooks/exhaustive-deps }, [...dependencies]); - const checkEvictableKey = useCallback(() => { - if (options?.canEvict === undefined || !connectionRef.current) { - return; - } - - if (!OnyxCache.isEvictableKey(key)) { - throw new Error(`canEvict can't be used on key '${key}'. This key must explicitly be flagged as safe for removal by adding it to Onyx.init({evictableKeys: []}).`); - } - - if (options.canEvict) { - connectionManager.removeFromEvictionBlockList(connectionRef.current); - } else { - connectionManager.addToEvictionBlockList(connectionRef.current); - } - }, [key, options?.canEvict]); - // Tracks the last memoizedSelector reference that getSnapshot() has computed with. // When the selector changes, this mismatch forces getSnapshot() to re-evaluate // even if all other conditions (isFirstConnection, shouldGetCachedValue, key) are false. @@ -342,8 +321,6 @@ function useOnyx>( reuseConnection: options?.reuseConnection, }); - checkEvictableKey(); - return () => { if (!connectionRef.current) { return; @@ -355,7 +332,7 @@ function useOnyx>( onStoreChangeFnRef.current = null; }; }, - [key, options?.initWithStoredValues, options?.reuseConnection, checkEvictableKey], + [key, options?.initWithStoredValues, options?.reuseConnection], ); const getSnapshotDecorated = useMemo(() => { @@ -366,10 +343,6 @@ function useOnyx>( return decorateWithMetrics(getSnapshot, 'useOnyx.getSnapshot'); }, [getSnapshot]); - useEffect(() => { - checkEvictableKey(); - }, [checkEvictableKey]); - const result = useSyncExternalStore>(subscribe, getSnapshotDecorated); return result; diff --git a/tests/perf-test/Onyx.perf-test.ts b/tests/perf-test/Onyx.perf-test.ts index 4acc46648..85956f127 100644 --- a/tests/perf-test/Onyx.perf-test.ts +++ b/tests/perf-test/Onyx.perf-test.ts @@ -31,8 +31,6 @@ describe('Onyx', () => { beforeAll(async () => { Onyx.init({ keys: ONYXKEYS, - maxCachedKeysCount: 100000, - evictableKeys: [ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY], skippableCollectionMemberIDs: ['skippable-id'], }); }); @@ -139,8 +137,6 @@ describe('Onyx', () => { Onyx.init({ keys: ONYXKEYS, initialKeyStates: mockedReportActionsMap, - maxCachedKeysCount: 100000, - evictableKeys: [ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY], skippableCollectionMemberIDs: ['skippable-id'], }); diff --git a/tests/perf-test/OnyxCache.perf-test.ts b/tests/perf-test/OnyxCache.perf-test.ts index 72d9667f7..a100a4382 100644 --- a/tests/perf-test/OnyxCache.perf-test.ts +++ b/tests/perf-test/OnyxCache.perf-test.ts @@ -178,34 +178,6 @@ describe('OnyxCache', () => { }); }); - describe('addToAccessedKeys', () => { - test('one call adding one key', async () => { - await measureFunction(() => cache.addToAccessedKeys(mockedReportActionsKeys[0]), { - beforeEach: resetCacheBeforeEachMeasure, - }); - }); - }); - - describe('removeLeastRecentlyUsedKeys', () => { - test('one call removing 1000 keys', async () => { - await measureFunction(() => cache.removeLeastRecentlyUsedKeys(), { - beforeEach: async () => { - resetCacheBeforeEachMeasure(); - cache.setRecentKeysLimit(mockedReportActionsKeys.length - 1000); - for (const k of mockedReportActionsKeys) cache.addToAccessedKeys(k); - }, - }); - }); - }); - - describe('setRecentKeysLimit', () => { - test('one call', async () => { - await measureFunction(() => cache.setRecentKeysLimit(10000), { - beforeEach: resetCacheBeforeEachMeasure, - }); - }); - }); - describe('hasValueChanged', () => { const key = mockedReportActionsKeys[0]; const reportAction = mockedReportActionsMap[key]; diff --git a/tests/perf-test/OnyxConnectionManager.perf-test.ts b/tests/perf-test/OnyxConnectionManager.perf-test.ts index 284b4d0cb..4b5ead72c 100644 --- a/tests/perf-test/OnyxConnectionManager.perf-test.ts +++ b/tests/perf-test/OnyxConnectionManager.perf-test.ts @@ -46,8 +46,6 @@ describe('OnyxConnectionManager', () => { beforeAll(async () => { Onyx.init({ keys: ONYXKEYS, - maxCachedKeysCount: 100000, - evictableKeys: [ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY], skippableCollectionMemberIDs: ['skippable-id'], ramOnlyKeys: [ONYXKEYS.RAM_ONLY_TEST_KEY, ONYXKEYS.COLLECTION.RAM_ONLY_TEST_COLLECTION], }); @@ -147,38 +145,4 @@ describe('OnyxConnectionManager', () => { }); }); }); - - describe('addToEvictionBlockList', () => { - let connection: Connection | undefined; - - test('one call', async () => { - await measureFunction(() => connectionManager.addToEvictionBlockList(connection as Connection), { - beforeEach: async () => { - connection = connectionManager.connect({key: mockedReportActionsKeys[0], callback: jest.fn()}); - }, - afterEach: async () => { - connectionManager.removeFromEvictionBlockList(connection as Connection); - resetConectionManagerAfterEachMeasure(); - await clearOnyxAfterEachMeasure(); - }, - }); - }); - }); - - describe('removeFromEvictionBlockList', () => { - let connection: Connection | undefined; - - test('one call', async () => { - await measureFunction(() => connectionManager.removeFromEvictionBlockList(connection as Connection), { - beforeEach: async () => { - connection = connectionManager.connect({key: mockedReportActionsKeys[0], callback: jest.fn()}); - connectionManager.addToEvictionBlockList(connection as Connection); - }, - afterEach: async () => { - resetConectionManagerAfterEachMeasure(); - await clearOnyxAfterEachMeasure(); - }, - }); - }); - }); }); diff --git a/tests/perf-test/OnyxUtils.perf-test.ts b/tests/perf-test/OnyxUtils.perf-test.ts index 5a00d910e..6ed4b28e0 100644 --- a/tests/perf-test/OnyxUtils.perf-test.ts +++ b/tests/perf-test/OnyxUtils.perf-test.ts @@ -4,7 +4,6 @@ import createRandomReportAction, {getRandomReportActions} from '../utils/collect import type {Selector} from '../../lib'; import Onyx from '../../lib'; import StorageMock from '../../lib/storage'; -import OnyxCache from '../../lib/OnyxCache'; import OnyxUtils, {clearOnyxUtilsInternals} from '../../lib/OnyxUtils'; import type GenericCollection from '../utils/GenericCollection'; import type {OnyxUpdate} from '../../lib/Onyx'; @@ -29,8 +28,6 @@ const ONYXKEYS = { }, }; -const evictableKeys = [ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY]; - const initialKeyStates = {}; const generateTestSelector = () => @@ -52,8 +49,6 @@ describe('OnyxUtils', () => { beforeAll(async () => { Onyx.init({ keys: ONYXKEYS, - maxCachedKeysCount: 100000, - evictableKeys, initialKeyStates, skippableCollectionMemberIDs: ['skippable-id'], ramOnlyKeys: [ONYXKEYS.RAM_ONLY_TEST_KEY, ONYXKEYS.COLLECTION.RAM_ONLY_TEST_COLLECTION], @@ -99,9 +94,9 @@ describe('OnyxUtils', () => { ...getRandomReportActions(ONYXKEYS.COLLECTION.TEST_KEY_5), }; - await measureFunction(() => OnyxUtils.initStoreValues(ONYXKEYS, data, evictableKeys), { + await measureFunction(() => OnyxUtils.initStoreValues(ONYXKEYS, data), { afterEach: () => { - OnyxUtils.initStoreValues(ONYXKEYS, initialKeyStates, evictableKeys); + OnyxUtils.initStoreValues(ONYXKEYS, initialKeyStates); }, }); }); @@ -191,12 +186,6 @@ describe('OnyxUtils', () => { }); }); - describe('isSafeEvictionKey', () => { - test('one call checking one key', async () => { - await measureFunction(() => OnyxCache.isEvictableKey(`${ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY}entry1`)); - }); - }); - describe('tryGetCachedValue', () => { const key = `${collectionKey}0`; const reportAction = mockedReportActionsMap[`${collectionKey}0`]; @@ -224,50 +213,6 @@ describe('OnyxUtils', () => { }); }); - describe('removeLastAccessedKey', () => { - test('one call removing one key', async () => { - await measureFunction(() => OnyxCache.removeLastAccessedKey(`${collectionKey}5000`), { - beforeEach: async () => { - for (const key of mockedReportActionsKeys) OnyxCache.addLastAccessedKey(key, false); - }, - afterEach: async () => { - for (const key of mockedReportActionsKeys) OnyxCache.removeLastAccessedKey(key); - }, - }); - }); - }); - - describe('addLastAccessedKey', () => { - test('one call adding one key', async () => { - await measureFunction(() => OnyxCache.addLastAccessedKey(`${collectionKey}5000`, false), { - beforeEach: async () => { - for (const key of mockedReportActionsKeys) OnyxCache.addLastAccessedKey(key, false); - }, - afterEach: async () => { - for (const key of mockedReportActionsKeys) OnyxCache.removeLastAccessedKey(key); - }, - }); - }); - }); - - describe('addEvictableKeysToRecentlyAccessedList', () => { - const data = { - ...getRandomReportActions(collectionKey, 1000), - ...getRandomReportActions(ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY, 1000), - }; - const fakeMethodParameter = () => false; - const fakePromiseMethodParameter = () => Promise.resolve(new Set(Object.keys(data))); - - test('one call adding 1k keys', async () => { - await measureAsyncFunction(() => OnyxCache.addEvictableKeysToRecentlyAccessedList(fakeMethodParameter, fakePromiseMethodParameter), { - beforeEach: async () => { - await Onyx.multiSet(data); - }, - afterEach: clearOnyxAfterEachMeasure, - }); - }); - }); - describe('getCachedCollection', () => { test('one call retrieving a collection with 5k heavy objects', async () => { const data = { @@ -512,19 +457,11 @@ describe('OnyxUtils', () => { describe('retryOperation', () => { test('one call', async () => { - const error = new Error(); - const onyxMethod = jest.fn() as RetriableOnyxOperation; + const genericError = new Error('Generic storage error'); + const mockOnyxMethod = jest.fn().mockResolvedValue(undefined) as unknown as RetriableOnyxOperation; + Object.defineProperty(mockOnyxMethod, 'name', {value: 'mockOnyxMethod'}); - await measureAsyncFunction(() => OnyxUtils.retryOperation(error, onyxMethod, {key: '', value: null}, 1), { - beforeEach: async () => { - for (const key of mockedReportActionsKeys) OnyxCache.addLastAccessedKey(key, false); - OnyxCache.addLastAccessedKey(`${ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY}1`, false); - }, - afterEach: async () => { - for (const key of mockedReportActionsKeys) OnyxCache.removeLastAccessedKey(key); - OnyxCache.removeLastAccessedKey(`${ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY}1`); - }, - }); + await measureAsyncFunction(() => OnyxUtils.retryOperation(genericError, mockOnyxMethod, {key: `${collectionKey}0`, value: 'test'}, 0)); }); }); @@ -588,11 +525,11 @@ describe('OnyxUtils', () => { await measureAsyncFunction(() => OnyxUtils.initializeWithDefaultKeyStates(), { beforeEach: async () => { await StorageMock.multiSet(Object.entries(changedReportActions).map(([k, v]) => [k, v])); - OnyxUtils.initStoreValues(ONYXKEYS, mockedReportActionsMap, evictableKeys); + OnyxUtils.initStoreValues(ONYXKEYS, mockedReportActionsMap); }, afterEach: async () => { await clearOnyxAfterEachMeasure(); - OnyxUtils.initStoreValues(ONYXKEYS, initialKeyStates, evictableKeys); + OnyxUtils.initStoreValues(ONYXKEYS, initialKeyStates); }, }); }); @@ -719,12 +656,6 @@ describe('OnyxUtils', () => { }); }); - describe('getEvictionBlocklist', () => { - test('one call', async () => { - await measureFunction(() => OnyxCache.getEvictionBlocklist()); - }); - }); - describe('getSkippableCollectionMemberIDs', () => { test('one call', async () => { const skippableCollectionMemberIDs = OnyxUtils.getSkippableCollectionMemberIDs(); @@ -792,19 +723,6 @@ describe('OnyxUtils', () => { }); }); - describe('addKeyToRecentlyAccessedIfNeeded', () => { - test('one call', async () => { - const key = `${ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY}0`; - - await measureFunction(() => OnyxUtils.addKeyToRecentlyAccessedIfNeeded(key), { - afterEach: async () => { - OnyxCache.removeLastAccessedKey(key); - await clearOnyxAfterEachMeasure(); - }, - }); - }); - }); - describe('reduceCollectionWithSelector', () => { test('one call with 10k heavy objects', async () => { const selector = generateTestSelector(); diff --git a/tests/perf-test/useOnyx.perf-test.tsx b/tests/perf-test/useOnyx.perf-test.tsx index 963d5832d..6cbc4159e 100644 --- a/tests/perf-test/useOnyx.perf-test.tsx +++ b/tests/perf-test/useOnyx.perf-test.tsx @@ -57,7 +57,6 @@ describe('useOnyx', () => { beforeAll(async () => { Onyx.init({ keys: ONYXKEYS, - maxCachedKeysCount: 100000, ramOnlyKeys: [ONYXKEYS.RAM_ONLY_TEST_KEY], }); }); diff --git a/tests/unit/OnyxConnectionManagerTest.ts b/tests/unit/OnyxConnectionManagerTest.ts index 088c613d3..723df8aea 100644 --- a/tests/unit/OnyxConnectionManagerTest.ts +++ b/tests/unit/OnyxConnectionManagerTest.ts @@ -2,7 +2,6 @@ import {act} from '@testing-library/react-native'; import Onyx from '../../lib'; import type {Connection} from '../../lib/OnyxConnectionManager'; import connectionManager from '../../lib/OnyxConnectionManager'; -import OnyxCache from '../../lib/OnyxCache'; import StorageMock from '../../lib/storage'; import type GenericCollection from '../utils/GenericCollection'; import waitForPromisesToResolve from '../utils/waitForPromisesToResolve'; @@ -406,66 +405,6 @@ describe('OnyxConnectionManager', () => { }); }); - describe('addToEvictionBlockList / removeFromEvictionBlockList', () => { - it('should add and remove connections from the eviction block list correctly', async () => { - const evictionBlocklist = OnyxCache.getEvictionBlocklist(); - - connectionsMap.set('connectionID1', {subscriptionID: 0, onyxKey: ONYXKEYS.TEST_KEY, callbacks: new Map(), isConnectionMade: true}); - connectionsMap.get('connectionID1')?.callbacks.set('callbackID1', () => undefined); - connectionManager.addToEvictionBlockList({id: 'connectionID1', callbackID: 'callbackID1'}); - expect(evictionBlocklist[ONYXKEYS.TEST_KEY]).toEqual(['connectionID1_callbackID1']); - - connectionsMap.get('connectionID1')?.callbacks.set('callbackID2', () => undefined); - connectionManager.addToEvictionBlockList({id: 'connectionID1', callbackID: 'callbackID2'}); - expect(evictionBlocklist[ONYXKEYS.TEST_KEY]).toEqual(['connectionID1_callbackID1', 'connectionID1_callbackID2']); - - connectionsMap.set('connectionID2', {subscriptionID: 1, onyxKey: `${ONYXKEYS.COLLECTION.TEST_KEY}entry1`, callbacks: new Map(), isConnectionMade: true}); - connectionsMap.get('connectionID2')?.callbacks.set('callbackID3', () => undefined); - connectionManager.addToEvictionBlockList({id: 'connectionID2', callbackID: 'callbackID3'}); - expect(evictionBlocklist[`${ONYXKEYS.COLLECTION.TEST_KEY}entry1`]).toEqual(['connectionID2_callbackID3']); - - connectionManager.removeFromEvictionBlockList({id: 'connectionID2', callbackID: 'callbackID3'}); - expect(evictionBlocklist[`${ONYXKEYS.COLLECTION.TEST_KEY}entry1`]).toBeUndefined(); - - // inexistent callback ID, shouldn't do anything - connectionManager.removeFromEvictionBlockList({id: 'connectionID1', callbackID: 'callbackID1000'}); - expect(evictionBlocklist[ONYXKEYS.TEST_KEY]).toEqual(['connectionID1_callbackID1', 'connectionID1_callbackID2']); - - connectionManager.removeFromEvictionBlockList({id: 'connectionID1', callbackID: 'callbackID2'}); - expect(evictionBlocklist[ONYXKEYS.TEST_KEY]).toEqual(['connectionID1_callbackID1']); - - connectionManager.removeFromEvictionBlockList({id: 'connectionID1', callbackID: 'callbackID1'}); - expect(evictionBlocklist[ONYXKEYS.TEST_KEY]).toBeUndefined(); - - // inexistent connection ID, shouldn't do anything - expect(() => connectionManager.removeFromEvictionBlockList({id: 'connectionID0', callbackID: 'callbackID0'})).not.toThrow(); - }); - - it('should not throw any errors when passing an undefined connection or trying to access an inexistent one inside addToEvictionBlockList()', () => { - expect(connectionsMap.size).toEqual(0); - - expect(() => { - connectionManager.addToEvictionBlockList(undefined as unknown as Connection); - }).not.toThrow(); - - expect(() => { - connectionManager.addToEvictionBlockList({id: 'connectionID1', callbackID: 'callbackID1'}); - }).not.toThrow(); - }); - - it('should not throw any errors when passing an undefined connection or trying to access an inexistent one inside removeFromEvictionBlockList()', () => { - expect(connectionsMap.size).toEqual(0); - - expect(() => { - connectionManager.removeFromEvictionBlockList(undefined as unknown as Connection); - }).not.toThrow(); - - expect(() => { - connectionManager.removeFromEvictionBlockList({id: 'connectionID1', callbackID: 'callbackID1'}); - }).not.toThrow(); - }); - }); - describe('sourceValue parameter', () => { it('should pass the sourceValue parameter to collection callbacks when waitForCollectionCallback is true', async () => { const obj1 = {id: 'entry1_id', name: 'entry1_name'}; diff --git a/tests/unit/cacheEvictionTest.ts b/tests/unit/cacheEvictionTest.ts deleted file mode 100644 index 56585b503..000000000 --- a/tests/unit/cacheEvictionTest.ts +++ /dev/null @@ -1,59 +0,0 @@ -import StorageMock from '../../lib/storage'; -import Onyx from '../../lib'; -import waitForPromisesToResolve from '../utils/waitForPromisesToResolve'; - -const ONYX_KEYS = { - COLLECTION: { - TEST_KEY: 'test_', - }, -}; - -test('Cache eviction', () => { - const RECORD_TO_EVICT = 'evict'; - const RECORD_TO_ADD = 'add'; - const collection: Record = {}; - - // Given an evictable key previously set in storage - return StorageMock.setItem(`${ONYX_KEYS.COLLECTION.TEST_KEY}${RECORD_TO_EVICT}`, {test: 'evict'}) - .then(() => { - // When we initialize Onyx and mark the set collection key as a safeEvictionKey - Onyx.init({ - keys: ONYX_KEYS, - evictableKeys: [ONYX_KEYS.COLLECTION.TEST_KEY], - }); - - // And connect to this key - Onyx.connect({ - key: ONYX_KEYS.COLLECTION.TEST_KEY, - callback: (val, key) => { - if (!val) { - delete collection[key]; - } else { - collection[key] = val; - } - }, - }); - - return waitForPromisesToResolve(); - }) - .then(() => { - // Then it should populate our data with the key we will soon evict - expect(collection[`${ONYX_KEYS.COLLECTION.TEST_KEY}${RECORD_TO_EVICT}`]).toStrictEqual({test: 'evict'}); - - // When we set a new key we want to add and force the first attempt to fail - const originalSetItem = StorageMock.setItem; - const setItemMock = jest.fn(originalSetItem).mockImplementationOnce( - () => - new Promise((_resolve, reject) => { - reject(new Error('out of memory')); - }), - ); - StorageMock.setItem = setItemMock; - - return Onyx.set(`${ONYX_KEYS.COLLECTION.TEST_KEY}${RECORD_TO_ADD}`, {test: 'add'}).then(() => { - // Then our collection should no longer contain the evictable key - expect(collection[`${ONYX_KEYS.COLLECTION.TEST_KEY}${RECORD_TO_EVICT}`]).toBe(undefined); - expect(collection[`${ONYX_KEYS.COLLECTION.TEST_KEY}${RECORD_TO_ADD}`]).toStrictEqual({test: 'add'}); - }); - }); -}); diff --git a/tests/unit/onyxCacheTest.tsx b/tests/unit/onyxCacheTest.tsx index 5f2154287..2bfc2d81c 100644 --- a/tests/unit/onyxCacheTest.tsx +++ b/tests/unit/onyxCacheTest.tsx @@ -1,10 +1,7 @@ import type OnyxInstance from '../../lib/Onyx'; import type OnyxCache from '../../lib/OnyxCache'; import type {CacheTask} from '../../lib/OnyxCache'; -import type {Connection} from '../../lib/OnyxConnectionManager'; -import type MockedStorage from '../../lib/storage/__mocks__'; import type {InitOptions} from '../../lib/types'; -import generateRange from '../utils/generateRange'; import waitForPromisesToResolve from '../utils/waitForPromisesToResolve'; const MOCK_TASK = 'mockTask' as CacheTask; @@ -418,7 +415,6 @@ describe('Onyx', () => { describe('Onyx with Cache', () => { let Onyx: typeof OnyxInstance; - let StorageMock: typeof MockedStorage; /** @type OnyxCache */ let cache: typeof OnyxCache; @@ -434,8 +430,6 @@ describe('Onyx', () => { function initOnyx(overrides?: Partial) { Onyx.init({ keys: ONYX_KEYS, - evictableKeys: [ONYX_KEYS.COLLECTION.MOCK_COLLECTION], - maxCachedKeysCount: 10, ...overrides, }); @@ -452,216 +446,9 @@ describe('Onyx', () => { const OnyxModule = require('../../lib'); Onyx = OnyxModule.default; - StorageMock = require('../../lib/storage').default; - cache = require('../../lib/OnyxCache').default; }); - it('Should keep recently accessed items in cache', () => { - // Given Storage with 10 different keys - StorageMock.getItem.mockResolvedValue('"mockValue"'); - const range = generateRange(0, 10); - StorageMock.getAllKeys.mockResolvedValue(range.map((number) => `${ONYX_KEYS.COLLECTION.MOCK_COLLECTION}${number}`)); - let connections: Array<{key: string; connection: Connection}> = []; - - // Given Onyx is configured with max 5 keys in cache - return initOnyx({maxCachedKeysCount: 5}) - .then(() => { - // Given 10 connections for different keys - connections = range.map((number) => { - const key = `${ONYX_KEYS.COLLECTION.MOCK_COLLECTION}${number}`; - return { - key, - connection: Onyx.connect({key, callback: jest.fn()}), - }; - }); - }) - .then(waitForPromisesToResolve) - .then(() => { - // When a new connection for a safe eviction key happens - Onyx.connect({key: `${ONYX_KEYS.COLLECTION.MOCK_COLLECTION}10`, callback: jest.fn()}); - }) - .then(waitForPromisesToResolve) - .then(() => { - // The newly connected key should remain in cache - expect(cache.hasCacheForKey(`${ONYX_KEYS.COLLECTION.MOCK_COLLECTION}10`)).toBe(true); - - // With the updated implementation, all evictable keys are removed except the most recently added one - // Each time we connect to a safe eviction key, we remove all other evictable keys - for (const {key} of connections) { - expect(cache.hasCacheForKey(key)).toBe(false); - } - }); - }); - - it('Should clean cache when connections to eviction keys happen', () => { - // Given storage with some data - StorageMock.getItem.mockResolvedValue('"mockValue"'); - const range = generateRange(0, 10); - const keyPrefix = ONYX_KEYS.COLLECTION.MOCK_COLLECTION; - StorageMock.getAllKeys.mockResolvedValue(range.map((number) => `${keyPrefix}${number}`)); - let connections: Array<{key: string; connection: Connection}> = []; - - return initOnyx({ - maxCachedKeysCount: 3, - }) - .then(() => { - connections = range.map((number) => { - const key = `${keyPrefix}${number}`; - return { - key, - connection: Onyx.connect({key, callback: jest.fn()}), - }; - }); - }) - .then(waitForPromisesToResolve) - .then(() => { - Onyx.connect({key: `${keyPrefix}10`, callback: jest.fn()}); - }) - .then(waitForPromisesToResolve) - .then(() => { - // All previously connected evictable keys are removed - for (const {key} of connections) { - expect(cache.hasCacheForKey(key)).toBe(false); - } - - // Only the newly connected key should remain in cache - expect(cache.hasCacheForKey(`${keyPrefix}10`)).toBe(true); - }); - }); - - it('Should prioritize eviction of evictableKeys over non-evictable keys when cache limit is reached', () => { - const testKeys = { - ...ONYX_KEYS, - SAFE_FOR_EVICTION: 'evictable_', - NOT_SAFE_FOR_EVICTION: 'critical_', - }; - - const criticalKey1 = `${testKeys.NOT_SAFE_FOR_EVICTION}1`; - const criticalKey2 = `${testKeys.NOT_SAFE_FOR_EVICTION}2`; - const criticalKey3 = `${testKeys.NOT_SAFE_FOR_EVICTION}3`; - const evictableKey1 = `${testKeys.SAFE_FOR_EVICTION}1`; - const evictableKey2 = `${testKeys.SAFE_FOR_EVICTION}2`; - const evictableKey3 = `${testKeys.SAFE_FOR_EVICTION}3`; - const triggerKey = `${testKeys.SAFE_FOR_EVICTION}trigger`; - - StorageMock.getItem.mockResolvedValue('"mockValue"'); - const allKeys = [ - // Keys that should be evictable (these match the SAFE_FOR_EVICTION pattern) - evictableKey1, - evictableKey2, - evictableKey3, - triggerKey, - // Keys that should NOT be evictable - criticalKey1, - criticalKey2, - criticalKey3, - ]; - StorageMock.getAllKeys.mockResolvedValue(allKeys); - - return initOnyx({ - keys: testKeys, - maxCachedKeysCount: 3, - evictableKeys: [testKeys.SAFE_FOR_EVICTION], - }) - .then(() => { - // Verify keys are correctly identified as evictable or not - expect(cache.isEvictableKey?.(evictableKey1)).toBe(true); - expect(cache.isEvictableKey?.(evictableKey2)).toBe(true); - expect(cache.isEvictableKey?.(evictableKey3)).toBe(true); - expect(cache.isEvictableKey?.(triggerKey)).toBe(true); - expect(cache.isEvictableKey?.(criticalKey1)).toBe(false); - - // Connect to non-evictable keys first - Onyx.connect({key: criticalKey1, callback: jest.fn()}); - Onyx.connect({key: criticalKey2, callback: jest.fn()}); - Onyx.connect({key: criticalKey3, callback: jest.fn()}); - }) - .then(waitForPromisesToResolve) - .then(() => { - // Then connect to evictable keys - Onyx.connect({key: evictableKey1, callback: jest.fn()}); - Onyx.connect({key: evictableKey2, callback: jest.fn()}); - Onyx.connect({key: evictableKey3, callback: jest.fn()}); - }) - .then(waitForPromisesToResolve) - .then(() => { - // Trigger an eviction by connecting to a safe eviction key - Onyx.connect({key: triggerKey, callback: jest.fn()}); - }) - .then(waitForPromisesToResolve) - .then(() => { - // Previously connected evictable keys should be removed - expect(cache.hasCacheForKey(evictableKey1)).toBe(false); - expect(cache.hasCacheForKey(evictableKey2)).toBe(false); - expect(cache.hasCacheForKey(evictableKey3)).toBe(false); - - // Non-evictable keys should remain in cache - expect(cache.hasCacheForKey(criticalKey1)).toBe(true); - expect(cache.hasCacheForKey(criticalKey2)).toBe(true); - expect(cache.hasCacheForKey(criticalKey3)).toBe(true); - - // The trigger key should be in cache as it was just connected - expect(cache.hasCacheForKey(triggerKey)).toBe(true); - }); - }); - - it('Should not evict non-evictable keys even when cache limit is exceeded', () => { - const testKeys = { - ...ONYX_KEYS, - SAFE_FOR_EVICTION: 'evictable_', - NOT_SAFE_FOR_EVICTION: 'critical_', - }; - - const criticalKey1 = `${testKeys.NOT_SAFE_FOR_EVICTION}1`; - const criticalKey2 = `${testKeys.NOT_SAFE_FOR_EVICTION}2`; - const criticalKey3 = `${testKeys.NOT_SAFE_FOR_EVICTION}3`; - const evictableKey1 = `${testKeys.SAFE_FOR_EVICTION}1`; - // Additional trigger key for natural eviction - const triggerKey = `${testKeys.SAFE_FOR_EVICTION}trigger`; - - StorageMock.getItem.mockResolvedValue('"mockValue"'); - const allKeys = [ - evictableKey1, - triggerKey, - // Keys that should not be evicted - criticalKey1, - criticalKey2, - criticalKey3, - ]; - StorageMock.getAllKeys.mockResolvedValue(allKeys); - - return initOnyx({ - keys: testKeys, - maxCachedKeysCount: 2, - evictableKeys: [testKeys.SAFE_FOR_EVICTION], - }) - .then(() => { - Onyx.connect({key: criticalKey1, callback: jest.fn()}); // Should never be evicted - Onyx.connect({key: criticalKey2, callback: jest.fn()}); // Should never be evicted - Onyx.connect({key: criticalKey3, callback: jest.fn()}); // Should never be evicted - Onyx.connect({key: evictableKey1, callback: jest.fn()}); // Should be evicted when we connect to triggerKey - }) - .then(waitForPromisesToResolve) - .then(() => { - // Trigger eviction by connecting to another safe eviction key - Onyx.connect({key: triggerKey, callback: jest.fn()}); - }) - .then(waitForPromisesToResolve) - .then(() => { - // evictableKey1 should be evicted since it's an evictable key - expect(cache.hasCacheForKey(evictableKey1)).toBe(false); - - // Non-evictable keys should remain in cache - expect(cache.hasCacheForKey(criticalKey1)).toBe(true); - expect(cache.hasCacheForKey(criticalKey2)).toBe(true); - expect(cache.hasCacheForKey(criticalKey3)).toBe(true); - - // The trigger key should be in cache as it was just connected - expect(cache.hasCacheForKey(triggerKey)).toBe(true); - }); - }); - it('should save RAM-only keys', () => { const testKeys = { ...ONYX_KEYS, diff --git a/tests/unit/onyxUtilsTest.ts b/tests/unit/onyxUtilsTest.ts index 07ed29beb..13a2859c6 100644 --- a/tests/unit/onyxUtilsTest.ts +++ b/tests/unit/onyxUtilsTest.ts @@ -456,7 +456,6 @@ describe('OnyxUtils', () => { const retryOperationSpy = jest.spyOn(OnyxUtils, 'retryOperation'); const genericError = new Error('Generic storage error'); const invalidDataError = new Error("Failed to execute 'put' on 'IDBObjectStore': invalid data"); - const memoryError = new Error('out of memory'); it('should retry only one time if the operation is firstly failed and then passed', async () => { StorageMock.setItem = jest.fn(StorageMock.setItem).mockRejectedValueOnce(genericError).mockImplementation(StorageMock.setItem); @@ -481,15 +480,6 @@ describe('OnyxUtils', () => { await expect(Onyx.set(ONYXKEYS.TEST_KEY, {test: 'data'})).rejects.toThrow(invalidDataError); }); - - it('should not retry in case of storage capacity error and no keys to evict', async () => { - StorageMock.setItem = jest.fn().mockRejectedValue(memoryError); - - await Onyx.set(ONYXKEYS.TEST_KEY, {test: 'data'}); - - // Should only be called once since there are no evictable keys - expect(retryOperationSpy).toHaveBeenCalledTimes(1); - }); }); describe('isRamOnlyKey', () => { diff --git a/tests/unit/useOnyxTest.ts b/tests/unit/useOnyxTest.ts index 203b5f11a..b0243fcd6 100644 --- a/tests/unit/useOnyxTest.ts +++ b/tests/unit/useOnyxTest.ts @@ -1,7 +1,6 @@ import {act, renderHook} from '@testing-library/react-native'; import type {OnyxCollection, OnyxEntry, OnyxKey} from '../../lib'; import Onyx, {useOnyx} from '../../lib'; -import OnyxCache from '../../lib/OnyxCache'; import StorageMock from '../../lib/storage'; import type GenericCollection from '../utils/GenericCollection'; import waitForPromisesToResolve from '../utils/waitForPromisesToResolve'; @@ -23,7 +22,6 @@ const ONYXKEYS = { Onyx.init({ keys: ONYXKEYS, - evictableKeys: [ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY], skippableCollectionMemberIDs: ['skippable-id'], ramOnlyKeys: [ONYXKEYS.RAM_ONLY_KEY, ONYXKEYS.COLLECTION.RAM_ONLY_COLLECTION, ONYXKEYS.RAM_ONLY_WITH_INITIAL_VALUE], }); @@ -1098,85 +1096,4 @@ describe('useOnyx', () => { 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({evictableKeys: []}).`; - - beforeEach(() => { - jest.spyOn(console, 'error').mockImplementation(jest.fn); - }); - - afterEach(() => { - (console.error as unknown as jest.SpyInstance>).mockRestore(); - }); - - it('should throw an error when trying to set the "canEvict" property for a non-evictable key', async () => { - await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test'); - - try { - renderHook(() => useOnyx(ONYXKEYS.TEST_KEY, {canEvict: false})); - - await act(async () => waitForPromisesToResolve()); - - fail('Expected to throw an error.'); - } catch (e) { - expect((e as Error).message).toBe(error(ONYXKEYS.TEST_KEY)); - } - }); - - it('should add the connection to the blocklist when setting "canEvict" to false', async () => { - Onyx.mergeCollection(ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY, { - [`${ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY}entry1`]: {id: 'entry1_id', name: 'entry1_name'}, - } as GenericCollection); - - renderHook(() => useOnyx(`${ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY}entry1`, {canEvict: false})); - - await act(async () => waitForPromisesToResolve()); - - const evictionBlocklist = OnyxCache.getEvictionBlocklist(); - expect(evictionBlocklist[`${ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY}entry1`]).toHaveLength(1); - }); - - it('should handle removal/adding the connection to the blocklist properly when changing the evictable key to another', async () => { - Onyx.mergeCollection(ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY, { - [`${ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY}entry1`]: {id: 'entry1_id', name: 'entry1_name'}, - [`${ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY}entry2`]: {id: 'entry2_id', name: 'entry2_name'}, - } as GenericCollection); - - const {rerender} = renderHook((key: string) => useOnyx(key, {canEvict: false}), {initialProps: `${ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY}entry1` as string}); - - await act(async () => waitForPromisesToResolve()); - - const evictionBlocklist = OnyxCache.getEvictionBlocklist(); - expect(evictionBlocklist[`${ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY}entry1`]).toHaveLength(1); - expect(evictionBlocklist[`${ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY}entry2`]).toBeUndefined(); - - await act(async () => { - rerender(`${ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY}entry2`); - }); - - expect(evictionBlocklist[`${ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY}entry1`]).toBeUndefined(); - expect(evictionBlocklist[`${ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY}entry2`]).toHaveLength(1); - }); - - it('should remove the connection from the blocklist when setting "canEvict" to true', async () => { - Onyx.mergeCollection(ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY, { - [`${ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY}entry1`]: {id: 'entry1_id', name: 'entry1_name'}, - } as GenericCollection); - - const {rerender} = renderHook((canEvict: boolean) => useOnyx(`${ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY}entry1`, {canEvict}), {initialProps: false as boolean}); - - await act(async () => waitForPromisesToResolve()); - - const evictionBlocklist = OnyxCache.getEvictionBlocklist(); - expect(evictionBlocklist[`${ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY}entry1`]).toHaveLength(1); - - await act(async () => { - rerender(true); - }); - - expect(evictionBlocklist[`${ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY}entry1`]).toBeUndefined(); - }); - }); }); From dce6b40a35863ad762b76bc1d3d2134d8473e3b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 12 Mar 2026 21:04:38 +0000 Subject: [PATCH 2/9] Load all storage data into cache during init --- lib/OnyxUtils.ts | 191 ++++-------------- lib/storage/__mocks__/index.ts | 1 + lib/storage/index.ts | 6 + .../providers/IDBKeyValProvider/index.ts | 8 + lib/storage/providers/MemoryOnlyProvider.ts | 7 + lib/storage/providers/NoopProvider.ts | 7 + lib/storage/providers/SQLiteProvider.ts | 11 + lib/storage/providers/types.ts | 6 + tests/unit/OnyxConnectionManagerTest.ts | 31 ++- tests/unit/onyxMultiMergeWebStorageTest.ts | 184 ----------------- tests/unit/useOnyxTest.ts | 43 ++-- 11 files changed, 112 insertions(+), 383 deletions(-) delete mode 100644 tests/unit/onyxMultiMergeWebStorageTest.ts diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 6f1b65061..cfc6ce334 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -4,7 +4,7 @@ import _ from 'underscore'; import DevTools from './DevTools'; import * as Logger from './Logger'; import type Onyx from './Onyx'; -import cache, {TASK} from './OnyxCache'; +import cache from './OnyxCache'; import * as Str from './Str'; import Storage from './storage'; import type { @@ -247,143 +247,22 @@ function get>(key: TKey): P return Promise.resolve(cache.get(key) as TValue); } - // RAM-only keys should never read from storage (they may have stale persisted data - // from before the key was migrated to RAM-only). Mark as nullish so future get() calls - // short-circuit via hasCacheForKey and avoid re-running this branch. - if (isRamOnlyKey(key)) { - cache.addNullishStorageKey(key); - return Promise.resolve(undefined as TValue); - } - - const taskName = `${TASK.GET}:${key}` as const; - - // When a value retrieving task for this key is still running hook to it - if (cache.hasPendingTask(taskName)) { - return cache.getTaskPromise(taskName) as Promise; - } - - // 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; - } - - cache.set(key, val); - return val; - }) - .catch((err) => Logger.logInfo(`Unable to get item from persistent storage. Key: ${key} Error: ${err}`)); - - return cache.captureTask(taskName, promise) as Promise; + return Promise.resolve(undefined as TValue); } // multiGet the data first from the cache and then from the storage for the missing keys. function multiGet(keys: CollectionKeyBase[]): Promise>> { - // Keys that are not in the cache - const missingKeys: OnyxKey[] = []; - - // Tasks that are pending - const pendingTasks: Array>> = []; - - // Keys for the tasks that are pending - const pendingKeys: OnyxKey[] = []; - // Data to be sent back to the invoker const dataMap = new Map>(); - /** - * We are going to iterate over all the matching keys and check if we have the data in the cache. - * If we do then we add it to the data object. If we do not have them, then we check if there is a pending task - * for the key. If there is such task, then we add the promise to the pendingTasks array and the key to the pendingKeys - * array. If there is no pending task then we add the key to the missingKeys array. - * - * These missingKeys will be later used to multiGet the data from the storage. - */ for (const key of keys) { - // RAM-only keys should never read from storage as they may have stale persisted data - // from before the key was migrated to RAM-only. - if (isRamOnlyKey(key)) { - if (cache.hasCacheForKey(key)) { - dataMap.set(key, cache.get(key) as OnyxValue); - } - continue; - } - const cacheValue = cache.get(key) as OnyxValue; if (cacheValue) { dataMap.set(key, cacheValue); - continue; - } - - const pendingKey = `${TASK.GET}:${key}` as const; - if (cache.hasPendingTask(pendingKey)) { - pendingTasks.push(cache.getTaskPromise(pendingKey) as Promise>); - pendingKeys.push(key); - } else { - missingKeys.push(key); } } - return ( - Promise.all(pendingTasks) - // Wait for all the pending tasks to resolve and then add the data to the data map. - .then((values) => { - for (const [index, value] of values.entries()) { - dataMap.set(pendingKeys[index], value); - } - - return Promise.resolve(); - }) - // Get the missing keys using multiGet from the storage. - .then(() => { - if (missingKeys.length === 0) { - return Promise.resolve(undefined); - } - - return Storage.multiGet(missingKeys); - }) - // Add the data from the missing keys to the data map and also merge it to the cache. - .then((values) => { - if (!values || values.length === 0) { - return dataMap; - } - - // temp object is used to merge the missing data into the cache - const temp: OnyxCollection = {}; - for (const [key, value] of values) { - if (skippableCollectionMemberIDs.size) { - try { - const [, collectionMemberID] = splitCollectionMemberKey(key); - if (skippableCollectionMemberIDs.has(collectionMemberID)) { - // The key is a skippable one, so we skip this iteration. - continue; - } - } 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; - } - cache.merge(temp); - return dataMap; - }) - ); + return Promise.resolve(dataMap); } /** @@ -427,29 +306,7 @@ function deleteKeyBySubscriptions(subscriptionID: number) { /** Returns current key names stored in persisted storage */ function getAllKeys(): Promise> { - // When we've already read stored keys, resolve right away - const cachedKeys = cache.getAllKeys(); - if (cachedKeys.size > 0) { - return Promise.resolve(cachedKeys); - } - - // When a value retrieving task for all keys is still running hook to it - if (cache.hasPendingTask(TASK.GET_ALL_KEYS)) { - return cache.getTaskPromise(TASK.GET_ALL_KEYS) as Promise>; - } - - // Otherwise retrieve the keys from storage and capture a promise to aid concurrent usages - const promise = Storage.getAllKeys().then((keys) => { - // Filter out RAM-only keys from storage results as they may be stale entries - // from before the key was migrated to RAM-only. - const filteredKeys = keys.filter((key) => !isRamOnlyKey(key)); - cache.setAllKeys(filteredKeys); - - // return the updated set of keys - return cache.getAllKeys(); - }); - - return cache.captureTask(TASK.GET_ALL_KEYS, promise) as Promise>; + return Promise.resolve(cache.getAllKeys()); } /** @@ -1045,18 +902,44 @@ function mergeInternal | undefined, TChange ex * Merge user provided default key value pairs. */ function initializeWithDefaultKeyStates(): Promise { - // Filter out RAM-only keys from storage reads as they may have stale persisted data - // from before the key was migrated to RAM-only. - const keysToFetch = Object.keys(defaultKeyStates).filter((key) => !isRamOnlyKey(key)); - return Storage.multiGet(keysToFetch).then((pairs) => { - const existingDataAsObject = Object.fromEntries(pairs) as Record; + // Eagerly load the entire database into cache in a single batch read. + // This is faster than lazy-loading individual keys because: + // 1. One DB transaction instead of hundreds + // 2. All subsequent reads are synchronous cache hits + return Storage.getAll().then((pairs) => { + const allDataFromStorage: Record = {}; + + for (const [key, value] of pairs) { + // RAM-only keys should never be loaded from storage as they may have stale persisted data + // from before the key was migrated to RAM-only. + if (isRamOnlyKey(key)) { + continue; + } + allDataFromStorage[key] = value; + } + + // Load all storage data into cache silently (no subscriber notifications) + cache.setAllKeys(Object.keys(allDataFromStorage)); + cache.merge(allDataFromStorage); + + // Extract only the default key states from storage and merge with defaults + const defaultKeysFromStorage = Object.keys(defaultKeyStates).reduce((obj: Record, key) => { + if (key in allDataFromStorage) { + // eslint-disable-next-line no-param-reassign + obj[key] = allDataFromStorage[key]; + } + return obj; + }, {}); - const merged = utils.fastMerge(existingDataAsObject, defaultKeyStates, { + const merged = utils.fastMerge(defaultKeysFromStorage, defaultKeyStates, { shouldRemoveNestedNulls: true, }).result; cache.merge(merged ?? {}); - for (const [key, value] of Object.entries(merged ?? {})) keyChanged(key, value); + // Only notify subscribers for default key states — same as before. + // Other keys will be picked up by subscribers when they connect. + // TODO: Maybe we dont need this. + // for (const [key, value] of Object.entries(merged ?? {})) keyChanged(key, value); }); } diff --git a/lib/storage/__mocks__/index.ts b/lib/storage/__mocks__/index.ts index b4fcc31bd..a4456a7e7 100644 --- a/lib/storage/__mocks__/index.ts +++ b/lib/storage/__mocks__/index.ts @@ -16,6 +16,7 @@ const StorageMock = { removeItems: jest.fn(MemoryOnlyProvider.removeItems), clear: jest.fn(MemoryOnlyProvider.clear), getAllKeys: jest.fn(MemoryOnlyProvider.getAllKeys), + getAll: jest.fn(MemoryOnlyProvider.getAll), getDatabaseSize: jest.fn(MemoryOnlyProvider.getDatabaseSize), keepInstancesSync: jest.fn(), diff --git a/lib/storage/index.ts b/lib/storage/index.ts index 07e7f7536..be45bc41a 100644 --- a/lib/storage/index.ts +++ b/lib/storage/index.ts @@ -187,6 +187,11 @@ const storage: Storage = { */ getAllKeys: () => tryOrDegradePerformance(() => provider.getAllKeys()), + /** + * Returns all key-value pairs from storage in a single batch operation + */ + getAll: () => tryOrDegradePerformance(() => provider.getAll()), + /** * Gets the total bytes of the store */ @@ -220,6 +225,7 @@ GlobalSettings.addGlobalSettingsChangeListener(({enablePerformanceMetrics}) => { storage.removeItems = decorateWithMetrics(storage.removeItems, 'Storage.removeItems'); storage.clear = decorateWithMetrics(storage.clear, 'Storage.clear'); storage.getAllKeys = decorateWithMetrics(storage.getAllKeys, 'Storage.getAllKeys'); + storage.getAll = decorateWithMetrics(storage.getAll, 'Storage.getAll'); }); export default storage; diff --git a/lib/storage/providers/IDBKeyValProvider/index.ts b/lib/storage/providers/IDBKeyValProvider/index.ts index c140ed763..89b5077b8 100644 --- a/lib/storage/providers/IDBKeyValProvider/index.ts +++ b/lib/storage/providers/IDBKeyValProvider/index.ts @@ -4,6 +4,7 @@ import utils from '../../../utils'; import type StorageProvider from '../types'; import type {OnyxKey, OnyxValue} from '../../../types'; import createStore from './createStore'; +import type {StorageKeyValuePair} from '../types'; const DB_NAME = 'OnyxDB'; const STORE_NAME = 'keyvaluepairs'; @@ -109,6 +110,13 @@ const provider: StorageProvider = { return IDB.keys(provider.store); }, + getAll() { + if (!provider.store) { + throw new Error('Store not initialized!'); + } + + return IDB.entries(provider.store) as Promise; + }, getItem(key) { if (!provider.store) { throw new Error('Store not initialized!'); diff --git a/lib/storage/providers/MemoryOnlyProvider.ts b/lib/storage/providers/MemoryOnlyProvider.ts index 15e9c264d..1f92425b6 100644 --- a/lib/storage/providers/MemoryOnlyProvider.ts +++ b/lib/storage/providers/MemoryOnlyProvider.ts @@ -135,6 +135,13 @@ const provider: StorageProvider = { return Promise.resolve(_.keys(provider.store)); }, + /** + * Returns all key-value pairs from memory + */ + getAll() { + return Promise.resolve(Object.entries(provider.store) as StorageKeyValuePair[]); + }, + /** * Gets the total bytes of the store. * `bytesRemaining` will always be `Number.POSITIVE_INFINITY` since we don't have a hard limit on memory. diff --git a/lib/storage/providers/NoopProvider.ts b/lib/storage/providers/NoopProvider.ts index dc3bcb2af..677826cfb 100644 --- a/lib/storage/providers/NoopProvider.ts +++ b/lib/storage/providers/NoopProvider.ts @@ -87,6 +87,13 @@ const provider: StorageProvider = { return Promise.resolve([]); }, + /** + * Returns all key-value pairs from storage + */ + getAll() { + return Promise.resolve([]); + }, + /** * Gets the total bytes of the store. * `bytesRemaining` will always be `Number.POSITIVE_INFINITY` since we don't have a hard limit on memory. diff --git a/lib/storage/providers/SQLiteProvider.ts b/lib/storage/providers/SQLiteProvider.ts index afbc0f405..61de2cf7d 100644 --- a/lib/storage/providers/SQLiteProvider.ts +++ b/lib/storage/providers/SQLiteProvider.ts @@ -198,6 +198,17 @@ const provider: StorageProvider = { return (result ?? []) as StorageKeyList; }); }, + getAll() { + if (!provider.store) { + throw new Error('Store is not initialized!'); + } + + return provider.store.executeAsync('SELECT record_key, valueJSON FROM keyvaluepairs;').then(({rows}) => { + // eslint-disable-next-line no-underscore-dangle + const result = rows?._array.map((row) => [row.record_key, JSON.parse(row.valueJSON)]); + return (result ?? []) as StorageKeyValuePair[]; + }); + }, removeItem(key) { if (!provider.store) { throw new Error('Store is not initialized!'); diff --git a/lib/storage/providers/types.ts b/lib/storage/providers/types.ts index ab275e39c..8bd4e17e2 100644 --- a/lib/storage/providers/types.ts +++ b/lib/storage/providers/types.ts @@ -60,6 +60,12 @@ type StorageProvider = { */ getAllKeys: () => Promise; + /** + * Returns all key-value pairs from storage in a single batch operation. + * More efficient than getAllKeys + multiGet for loading the entire database. + */ + getAll: () => Promise; + /** * Removes given key and its value from storage */ diff --git a/tests/unit/OnyxConnectionManagerTest.ts b/tests/unit/OnyxConnectionManagerTest.ts index 723df8aea..24f7e1359 100644 --- a/tests/unit/OnyxConnectionManagerTest.ts +++ b/tests/unit/OnyxConnectionManagerTest.ts @@ -2,7 +2,6 @@ import {act} from '@testing-library/react-native'; import Onyx from '../../lib'; import type {Connection} from '../../lib/OnyxConnectionManager'; import connectionManager from '../../lib/OnyxConnectionManager'; -import StorageMock from '../../lib/storage'; import type GenericCollection from '../utils/GenericCollection'; import waitForPromisesToResolve from '../utils/waitForPromisesToResolve'; @@ -69,7 +68,7 @@ describe('OnyxConnectionManager', () => { describe('connect / disconnect', () => { it('should connect to a key and fire the callback with its value', async () => { - await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test'); + await Onyx.set(ONYXKEYS.TEST_KEY, 'test'); const callback1 = jest.fn(); const connection = connectionManager.connect({key: ONYXKEYS.TEST_KEY, callback: callback1}); @@ -87,7 +86,7 @@ describe('OnyxConnectionManager', () => { }); it('should connect two times to the same key and fire both callbacks with its value', async () => { - await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test'); + await Onyx.set(ONYXKEYS.TEST_KEY, 'test'); const callback1 = jest.fn(); const connection1 = connectionManager.connect({key: ONYXKEYS.TEST_KEY, callback: callback1}); @@ -119,10 +118,10 @@ describe('OnyxConnectionManager', () => { [`${ONYXKEYS.COLLECTION.TEST_KEY}entry1`]: obj1, [`${ONYXKEYS.COLLECTION.TEST_KEY}entry2`]: obj2, } as GenericCollection; - await StorageMock.multiSet([ - [`${ONYXKEYS.COLLECTION.TEST_KEY}entry1`, obj1], - [`${ONYXKEYS.COLLECTION.TEST_KEY}entry2`, obj2], - ]); + await Onyx.multiSet({ + [`${ONYXKEYS.COLLECTION.TEST_KEY}entry1`]: obj1, + [`${ONYXKEYS.COLLECTION.TEST_KEY}entry2`]: obj2, + }); const callback1 = jest.fn(); const connection1 = connectionManager.connect({key: ONYXKEYS.COLLECTION.TEST_KEY, callback: callback1}); @@ -151,7 +150,7 @@ describe('OnyxConnectionManager', () => { }); it('should connect to a key, connect some times more after first connection is made, and fire all subsequent callbacks immediately with its value', async () => { - await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test'); + await Onyx.set(ONYXKEYS.TEST_KEY, 'test'); const callback1 = jest.fn(); connectionManager.connect({key: ONYXKEYS.TEST_KEY, callback: callback1}); @@ -181,7 +180,7 @@ describe('OnyxConnectionManager', () => { }); it('should have the connection object already defined when triggering the callback of the second connection to the same key', async () => { - await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test'); + await Onyx.set(ONYXKEYS.TEST_KEY, 'test'); const callback1 = jest.fn(); connectionManager.connect({key: ONYXKEYS.TEST_KEY, callback: callback1}); @@ -208,7 +207,7 @@ describe('OnyxConnectionManager', () => { }); it('should create a separate connection to the same key when setting reuseConnection to false', async () => { - await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test'); + await Onyx.set(ONYXKEYS.TEST_KEY, 'test'); const callback1 = jest.fn(); const connection1 = connectionManager.connect({key: ONYXKEYS.TEST_KEY, callback: callback1}); @@ -223,7 +222,7 @@ describe('OnyxConnectionManager', () => { }); it('should create a separate connection to the same key when setting initWithStoredValues to false', async () => { - await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test'); + await Onyx.set(ONYXKEYS.TEST_KEY, 'test'); const callback1 = jest.fn(); const connection1 = connectionManager.connect({key: ONYXKEYS.TEST_KEY, initWithStoredValues: false, callback: callback1}); @@ -309,7 +308,7 @@ describe('OnyxConnectionManager', () => { }); it('should create a separate connection for the same key after a Onyx.clear() call', async () => { - await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test'); + await Onyx.set(ONYXKEYS.TEST_KEY, 'test'); const callback1 = jest.fn(); connectionManager.connect({key: ONYXKEYS.TEST_KEY, callback: callback1}); @@ -361,8 +360,8 @@ describe('OnyxConnectionManager', () => { describe('disconnectAll', () => { it('should disconnect all connections', async () => { - await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test'); - await StorageMock.setItem(ONYXKEYS.TEST_KEY_2, 'test2'); + await Onyx.set(ONYXKEYS.TEST_KEY, 'test'); + await Onyx.set(ONYXKEYS.TEST_KEY_2, 'test2'); const callback1 = jest.fn(); const connection1 = connectionManager.connect({key: ONYXKEYS.TEST_KEY, callback: callback1}); @@ -388,8 +387,8 @@ describe('OnyxConnectionManager', () => { describe('refreshSessionID', () => { it('should create a separate connection for the same key if the session ID changes', async () => { - await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test'); - await StorageMock.setItem(ONYXKEYS.TEST_KEY_2, 'test2'); + await Onyx.set(ONYXKEYS.TEST_KEY, 'test'); + await Onyx.set(ONYXKEYS.TEST_KEY_2, 'test2'); const connection1 = connectionManager.connect({key: ONYXKEYS.TEST_KEY, callback: jest.fn()}); diff --git a/tests/unit/onyxMultiMergeWebStorageTest.ts b/tests/unit/onyxMultiMergeWebStorageTest.ts deleted file mode 100644 index 84ea6c40d..000000000 --- a/tests/unit/onyxMultiMergeWebStorageTest.ts +++ /dev/null @@ -1,184 +0,0 @@ -import OnyxCache from '../../lib/OnyxCache'; -import waitForPromisesToResolve from '../utils/waitForPromisesToResolve'; -import Storage from '../../lib/storage'; -import type MockedStorage from '../../lib/storage/__mocks__'; -import type OnyxInstance from '../../lib/Onyx'; -import type GenericCollection from '../utils/GenericCollection'; - -const StorageMock = Storage as unknown as typeof MockedStorage; - -const ONYX_KEYS = { - COLLECTION: { - TEST_KEY: 'test_', - }, -}; - -const initialTestObject = {a: 'a'}; -const initialData = { - test_1: initialTestObject, - test_2: initialTestObject, - test_3: initialTestObject, -}; - -describe('Onyx.mergeCollection() and WebStorage', () => { - let Onyx: typeof OnyxInstance; - - beforeAll(() => { - Onyx = require('../../lib').default; - jest.useRealTimers(); - - Onyx.init({ - keys: ONYX_KEYS, - initialKeyStates: {}, - }); - }); - - afterEach(() => Onyx.clear()); - - it('merges two sets of data consecutively', () => { - StorageMock.setMockStore(initialData); - - // Given initial data in storage - expect(StorageMock.getMockStore().test_1).toEqual(initialTestObject); - expect(StorageMock.getMockStore().test_2).toEqual(initialTestObject); - expect(StorageMock.getMockStore().test_3).toEqual(initialTestObject); - - // And an empty cache values for the collection keys - expect(OnyxCache.get('test_1')).not.toBeDefined(); - expect(OnyxCache.get('test_2')).not.toBeDefined(); - expect(OnyxCache.get('test_3')).not.toBeDefined(); - - // When we merge additional data - const additionalDataOne = {b: 'b', c: 'c', e: [1, 2]}; - Onyx.mergeCollection(ONYX_KEYS.COLLECTION.TEST_KEY, { - test_1: additionalDataOne, - test_2: additionalDataOne, - test_3: additionalDataOne, - } as GenericCollection); - - // And call again consecutively with different data - const additionalDataTwo = {d: 'd', e: [2]}; - Onyx.mergeCollection(ONYX_KEYS.COLLECTION.TEST_KEY, { - test_1: additionalDataTwo, - test_2: additionalDataTwo, - test_3: additionalDataTwo, - } as GenericCollection); - - return waitForPromisesToResolve().then(() => { - const finalObject = { - a: 'a', - b: 'b', - c: 'c', - d: 'd', - e: [2], - }; - - // Then our new data should merge with the existing data in the cache - expect(OnyxCache.get('test_1')).toEqual(finalObject); - expect(OnyxCache.get('test_2')).toEqual(finalObject); - expect(OnyxCache.get('test_3')).toEqual(finalObject); - - // And the storage should reflect the same state - expect(StorageMock.getMockStore().test_1).toEqual(finalObject); - expect(StorageMock.getMockStore().test_2).toEqual(finalObject); - expect(StorageMock.getMockStore().test_3).toEqual(finalObject); - }); - }); - - it('cache updates correctly when accessed again if keys are removed or evicted', () => { - // Given empty storage - expect(StorageMock.getMockStore().test_1).toBeFalsy(); - expect(StorageMock.getMockStore().test_2).toBeFalsy(); - expect(StorageMock.getMockStore().test_3).toBeFalsy(); - - // And an empty cache values for the collection keys - expect(OnyxCache.get('test_1')).toBeFalsy(); - expect(OnyxCache.get('test_2')).toBeFalsy(); - expect(OnyxCache.get('test_3')).toBeFalsy(); - - // When we merge additional data and wait for the change - const data = {a: 'a', b: 'b'}; - Onyx.mergeCollection(ONYX_KEYS.COLLECTION.TEST_KEY, { - test_1: data, - test_2: data, - test_3: data, - } as GenericCollection); - - return waitForPromisesToResolve() - .then(() => { - // Then the cache and storage should match - expect(OnyxCache.get('test_1')).toEqual(data); - expect(OnyxCache.get('test_2')).toEqual(data); - expect(OnyxCache.get('test_3')).toEqual(data); - expect(StorageMock.getMockStore().test_1).toEqual(data); - expect(StorageMock.getMockStore().test_2).toEqual(data); - expect(StorageMock.getMockStore().test_3).toEqual(data); - - // When we drop all the cache keys (but do not modify the underlying storage) and merge another object - OnyxCache.drop('test_1'); - OnyxCache.drop('test_2'); - OnyxCache.drop('test_3'); - - const additionalData = {c: 'c'}; - Onyx.mergeCollection(ONYX_KEYS.COLLECTION.TEST_KEY, { - test_1: additionalData, - test_2: additionalData, - test_3: additionalData, - } as GenericCollection); - - return waitForPromisesToResolve(); - }) - .then(() => { - const finalObject = { - a: 'a', - b: 'b', - c: 'c', - }; - - // Then our new data should merge with the existing data in the cache - expect(OnyxCache.get('test_1')).toEqual(finalObject); - expect(OnyxCache.get('test_2')).toEqual(finalObject); - expect(OnyxCache.get('test_3')).toEqual(finalObject); - - // And the storage should reflect the same state - expect(StorageMock.getMockStore().test_1).toEqual(finalObject); - expect(StorageMock.getMockStore().test_2).toEqual(finalObject); - expect(StorageMock.getMockStore().test_3).toEqual(finalObject); - }); - }); - - it('setItem() and multiMerge()', () => { - // Onyx should be empty after clear() is called - expect(StorageMock.getMockStore()).toEqual({}); - - // Given no previous data and several calls to setItem and call to mergeCollection to update a given key - - // 1st call - Onyx.set('test_1', {a: 'a'}); - - // These merges will all queue together - Onyx.merge('test_1', {b: 'b'}); - Onyx.merge('test_1', {c: 'c'}); - - // 2nd call - Onyx.mergeCollection(ONYX_KEYS.COLLECTION.TEST_KEY, { - test_1: {d: 'd', e: 'e'}, - } as GenericCollection); - - // Last call - Onyx.merge('test_1', {f: 'f'}); - return waitForPromisesToResolve().then(() => { - const finalObject = { - a: 'a', - b: 'b', - c: 'c', - d: 'd', - e: 'e', - f: 'f', - }; - - expect(OnyxCache.get('test_1')).toEqual(finalObject); - expect(StorageMock.getMockStore().test_1).toEqual(finalObject); - }); - }); -}); diff --git a/tests/unit/useOnyxTest.ts b/tests/unit/useOnyxTest.ts index b0243fcd6..e6e5fd645 100644 --- a/tests/unit/useOnyxTest.ts +++ b/tests/unit/useOnyxTest.ts @@ -172,16 +172,11 @@ describe('useOnyx', () => { expect(result.current[1].status).toEqual('loaded'); }); - it('should initially return `undefined` while loading non-cached key, and then return value and loaded state', async () => { - await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test'); + it('should return value and loaded state immediately when the key is already cached', async () => { + await Onyx.set(ONYXKEYS.TEST_KEY, 'test'); const {result} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY)); - expect(result.current[0]).toBeUndefined(); - expect(result.current[1].status).toEqual('loading'); - - await act(async () => waitForPromisesToResolve()); - expect(result.current[0]).toEqual('test'); expect(result.current[1].status).toEqual('loaded'); }); @@ -217,6 +212,8 @@ describe('useOnyx', () => { }); it('should return loaded state after an Onyx.clear() call while connecting and loading from cache', async () => { + // Write directly to storage so the data is not in cache when Onyx.clear() is called + // TODO: Check if this test still makes sense await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test'); Onyx.clear(); @@ -239,7 +236,7 @@ describe('useOnyx', () => { }); it('should return updated state when connecting to the same regular key after an Onyx.clear() call', async () => { - await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test'); + await Onyx.set(ONYXKEYS.TEST_KEY, 'test'); const {result: result1} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY)); @@ -274,7 +271,7 @@ describe('useOnyx', () => { }); it('should return updated state when connecting to the same colection member key after an Onyx.clear() call', async () => { - await StorageMock.setItem(`${ONYXKEYS.COLLECTION.TEST_KEY}entry1`, 'test'); + await Onyx.set(`${ONYXKEYS.COLLECTION.TEST_KEY}entry1`, 'test'); const {result: result1} = renderHook(() => useOnyx(`${ONYXKEYS.COLLECTION.TEST_KEY}entry1`)); @@ -779,7 +776,7 @@ describe('useOnyx', () => { describe('initWithStoredValues', () => { it('should return `undefined` and loaded state, and after merge return updated value and loaded state', async () => { - await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test1'); + await Onyx.set(ONYXKEYS.TEST_KEY, 'test1'); const {result} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY, {initWithStoredValues: false})); @@ -795,7 +792,7 @@ describe('useOnyx', () => { }); it('should return `undefined` value and loaded state if using `selector`, and after merge return selected value and loaded state', async () => { - await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test1'); + await Onyx.set(ONYXKEYS.TEST_KEY, 'test1'); const {result} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY, { @@ -817,16 +814,11 @@ describe('useOnyx', () => { }); describe('multiple usage', () => { - it('should connect to a key and load the value into cache, and return the value loaded in the next hook call', async () => { - await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test'); + it('should connect to a key and return the cached value immediately, and return the same value in the next hook call', async () => { + await Onyx.set(ONYXKEYS.TEST_KEY, 'test'); const {result: result1} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY)); - expect(result1.current[0]).toBeUndefined(); - expect(result1.current[1].status).toEqual('loading'); - - await act(async () => waitForPromisesToResolve()); - expect(result1.current[0]).toEqual('test'); expect(result1.current[1].status).toEqual('loaded'); @@ -836,20 +828,12 @@ describe('useOnyx', () => { expect(result2.current[1].status).toEqual('loaded'); }); - it('should connect to a key two times while data is loading from the cache, and return the value loaded to both of them', async () => { - await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test'); + it('should connect to a key two times and return the cached value immediately to both of them', async () => { + await Onyx.set(ONYXKEYS.TEST_KEY, 'test'); const {result: result1} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY)); const {result: result2} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY)); - expect(result1.current[0]).toBeUndefined(); - expect(result1.current[1].status).toEqual('loading'); - - expect(result2.current[0]).toBeUndefined(); - expect(result2.current[1].status).toEqual('loading'); - - await act(async () => waitForPromisesToResolve()); - expect(result1.current[0]).toEqual('test'); expect(result1.current[1].status).toEqual('loaded'); @@ -858,7 +842,7 @@ describe('useOnyx', () => { }); it('"initWithStoredValues" should work correctly for the same key if more than one hook is using it', async () => { - await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test1'); + await Onyx.set(ONYXKEYS.TEST_KEY, 'test1'); const {result: result1} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY, {initWithStoredValues: false})); @@ -969,6 +953,7 @@ describe('useOnyx', () => { }); it('should always return undefined when subscribing to a skippable collection member id', async () => { + // TODO: Check if this test still makes sense await StorageMock.setItem(`${ONYXKEYS.COLLECTION.TEST_KEY}skippable-id`, 'skippable-id_value'); const {result} = renderHook(() => useOnyx(`${ONYXKEYS.COLLECTION.TEST_KEY}skippable-id`)); From ba345a2653c57645b6e5c9c56a651035cf133e01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 12 Mar 2026 21:17:14 +0000 Subject: [PATCH 3/9] Revert change --- lib/OnyxUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index cfc6ce334..19de9f916 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -938,8 +938,8 @@ function initializeWithDefaultKeyStates(): Promise { // Only notify subscribers for default key states — same as before. // Other keys will be picked up by subscribers when they connect. - // TODO: Maybe we dont need this. - // for (const [key, value] of Object.entries(merged ?? {})) keyChanged(key, value); + // FIXME: Maybe we dont need this, but some tests in E/App are failing if we remove it. + for (const [key, value] of Object.entries(merged ?? {})) keyChanged(key, value); }); } From 8bfb429976094c48ffdb4299bbcb2b05472b748d Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Fri, 13 Mar 2026 11:56:35 +0100 Subject: [PATCH 4/9] Add error handling for getAll() failure during init --- lib/OnyxUtils.ts | 72 ++++++++++++++++++++++++++++-------------------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 19de9f916..5954fece3 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -906,41 +906,53 @@ function initializeWithDefaultKeyStates(): Promise { // This is faster than lazy-loading individual keys because: // 1. One DB transaction instead of hundreds // 2. All subsequent reads are synchronous cache hits - return Storage.getAll().then((pairs) => { - const allDataFromStorage: Record = {}; - - for (const [key, value] of pairs) { - // RAM-only keys should never be loaded from storage as they may have stale persisted data - // from before the key was migrated to RAM-only. - if (isRamOnlyKey(key)) { - continue; + return Storage.getAll() + .then((pairs) => { + const allDataFromStorage: Record = {}; + for (const [key, value] of pairs) { + // RAM-only keys should never be loaded from storage as they may have stale persisted data + // from before the key was migrated to RAM-only. + if (isRamOnlyKey(key)) { + continue; + } + allDataFromStorage[key] = value; } - allDataFromStorage[key] = value; - } - // Load all storage data into cache silently (no subscriber notifications) - cache.setAllKeys(Object.keys(allDataFromStorage)); - cache.merge(allDataFromStorage); + // Load all storage data into cache silently (no subscriber notifications) + cache.setAllKeys(Object.keys(allDataFromStorage)); + cache.merge(allDataFromStorage); - // Extract only the default key states from storage and merge with defaults - const defaultKeysFromStorage = Object.keys(defaultKeyStates).reduce((obj: Record, key) => { - if (key in allDataFromStorage) { - // eslint-disable-next-line no-param-reassign - obj[key] = allDataFromStorage[key]; - } - return obj; - }, {}); + // Extract only the default key states from storage and merge with defaults + const defaultKeysFromStorage = Object.keys(defaultKeyStates).reduce((obj: Record, key) => { + if (key in allDataFromStorage) { + // eslint-disable-next-line no-param-reassign + obj[key] = allDataFromStorage[key]; + } + return obj; + }, {}); - const merged = utils.fastMerge(defaultKeysFromStorage, defaultKeyStates, { - shouldRemoveNestedNulls: true, - }).result; - cache.merge(merged ?? {}); + const merged = utils.fastMerge(defaultKeysFromStorage, defaultKeyStates, { + shouldRemoveNestedNulls: true, + }).result; + cache.merge(merged ?? {}); - // Only notify subscribers for default key states — same as before. - // Other keys will be picked up by subscribers when they connect. - // FIXME: Maybe we dont need this, but some tests in E/App are failing if we remove it. - for (const [key, value] of Object.entries(merged ?? {})) keyChanged(key, value); - }); + // Only notify subscribers for default key states — same as before. + // Other keys will be picked up by subscribers when they connect. + // FIXME: Maybe we dont need this, but some tests in E/App are failing if we remove it. + for (const [key, value] of Object.entries(merged ?? {})) keyChanged(key, value); + }) + .catch((error) => { + Logger.logAlert(`Failed to load data from storage during init. The app will boot with default key states only. Error: ${error}`); + + // Populate the key index so getAllKeys() returns correct results for default keys. + // Without this, subscribers that check getAllKeys() would see an empty set even + // though we have default values in cache. + cache.setAllKeys(Object.keys(defaultKeyStates)); + + // Boot with defaults so the app renders instead of deadlocking. + // Users will get a fresh-install experience but the app won't be bricked. + cache.merge(defaultKeyStates); + }); } /** From fa95648ad9d10276af36327b89cd38fc2bfe87b1 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 17 Mar 2026 09:09:45 +0100 Subject: [PATCH 5/9] remove storage mock usage from useOnyx perf tests --- tests/perf-test/useOnyx.perf-test.tsx | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/tests/perf-test/useOnyx.perf-test.tsx b/tests/perf-test/useOnyx.perf-test.tsx index 6cbc4159e..f70a8b050 100644 --- a/tests/perf-test/useOnyx.perf-test.tsx +++ b/tests/perf-test/useOnyx.perf-test.tsx @@ -4,7 +4,6 @@ import {Text, View} from 'react-native'; import {measureRenders} from 'reassure'; import type {FetchStatus, OnyxEntry, OnyxKey, OnyxValue, ResultMetadata, UseOnyxOptions} from '../../lib'; import Onyx, {useOnyx} from '../../lib'; -import StorageMock from '../../lib/storage'; import type {UseOnyxSelector} from '../../lib/useOnyx'; const ONYXKEYS = { @@ -81,13 +80,13 @@ describe('useOnyx', () => { }); /** - * Expected renders: 2. + * Expected renders: 1. */ - test('data in storage but not yet in cache', async () => { + test('data set via Onyx.set before render', async () => { const key = ONYXKEYS.TEST_KEY; await measureRenders(, { beforeEach: async () => { - await StorageMock.setItem(key, 'test'); + await Onyx.set(key, 'test'); }, scenario: async () => { await screen.findByText(dataMatcher(key, 'test')); @@ -209,7 +208,7 @@ describe('useOnyx', () => { />, { beforeEach: async () => { - await StorageMock.setItem(key, 'test'); + await Onyx.set(key, 'test'); }, scenario: async () => { await screen.findByText(dataMatcher(key, undefined)); @@ -223,9 +222,9 @@ describe('useOnyx', () => { describe('multiple calls', () => { /** - * Expected renders: 2. + * Expected renders: 1. */ - test('3 calls loading from storage', async () => { + test('3 calls loading from cache', async () => { function TestComponent() { const [testKeyData, testKeyMetadata] = useOnyx(ONYXKEYS.TEST_KEY); const [testKey2Data, testKey2Metadata] = useOnyx(ONYXKEYS.TEST_KEY_2); @@ -254,9 +253,9 @@ describe('useOnyx', () => { await measureRenders(, { beforeEach: async () => { - await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test'); - await StorageMock.setItem(ONYXKEYS.TEST_KEY_2, 'test2'); - await StorageMock.setItem(ONYXKEYS.TEST_KEY_3, 'test3'); + await Onyx.set(ONYXKEYS.TEST_KEY, 'test'); + await Onyx.set(ONYXKEYS.TEST_KEY_2, 'test2'); + await Onyx.set(ONYXKEYS.TEST_KEY_3, 'test3'); }, scenario: async () => { await screen.findByText(dataMatcher(ONYXKEYS.TEST_KEY, 'test')); From 8e1a33d1f691872da6c0fcd37f8bcbdcae0cd940 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 17 Mar 2026 08:34:09 +0000 Subject: [PATCH 6/9] Minor fixes --- API-INTERNAL.md | 40 ------------------------------------ lib/OnyxConnectionManager.ts | 2 +- lib/OnyxSnapshotCache.ts | 2 +- lib/OnyxUtils.ts | 7 +++---- tests/unit/useOnyxTest.ts | 11 ++++------ 5 files changed, 9 insertions(+), 53 deletions(-) diff --git a/API-INTERNAL.md b/API-INTERNAL.md index 148f09b9a..5c7fe9251 100644 --- a/API-INTERNAL.md +++ b/API-INTERNAL.md @@ -114,17 +114,6 @@ If the requested key is a collection, it will return an object with all the coll
getCollectionDataAndSendAsObject()

Gets the data for a given an array of matching keys, combines them into an object, and sends the result back to the subscriber.

-
prepareSubscriberUpdate(callback)
-

Delays promise resolution until the next macrotask to prevent race condition if the key subscription is in progress.

-
-
scheduleSubscriberUpdate()
-

Schedules an update that will be appended to the macro task queue (so it doesn't update the subscribers immediately).

-
-
scheduleNotifyCollectionSubscribers()
-

This method is similar to scheduleSubscriberUpdate but it is built for working specifically with collections -so that keysChanged() is triggered for the collection and not keyChanged(). If this was not done, then the -subscriber callbacks receive the data in a different format than they normally expect and it breaks code.

-
remove()

Remove a key from Onyx and update the subscribers

@@ -463,35 +452,6 @@ Sends the data obtained from the keys to the connection. ## getCollectionDataAndSendAsObject() Gets the data for a given an array of matching keys, combines them into an object, and sends the result back to the subscriber. -**Kind**: global function - - -## prepareSubscriberUpdate(callback) -Delays promise resolution until the next macrotask to prevent race condition if the key subscription is in progress. - -**Kind**: global function - -| Param | Description | -| --- | --- | -| callback | The keyChanged/keysChanged callback | - - - -## scheduleSubscriberUpdate() -Schedules an update that will be appended to the macro task queue (so it doesn't update the subscribers immediately). - -**Kind**: global function -**Example** -```js -scheduleSubscriberUpdate(key, value, subscriber => subscriber.initWithStoredValues === false) -``` - - -## scheduleNotifyCollectionSubscribers() -This method is similar to scheduleSubscriberUpdate but it is built for working specifically with collections -so that keysChanged() is triggered for the collection and not keyChanged(). If this was not done, then the -subscriber callbacks receive the data in a different format than they normally expect and it breaks code. - **Kind**: global function diff --git a/lib/OnyxConnectionManager.ts b/lib/OnyxConnectionManager.ts index 447206647..ff9436c80 100644 --- a/lib/OnyxConnectionManager.ts +++ b/lib/OnyxConnectionManager.ts @@ -247,7 +247,7 @@ class OnyxConnectionManager { * Disconnect all subscribers from Onyx. */ disconnectAll(): void { - for (const [, connectionMetadata] of this.connectionsMap.entries()) { + for (const connectionMetadata of this.connectionsMap.values()) { OnyxUtils.unsubscribeFromKey(connectionMetadata.subscriptionID); } diff --git a/lib/OnyxSnapshotCache.ts b/lib/OnyxSnapshotCache.ts index 04f261f99..09129c328 100644 --- a/lib/OnyxSnapshotCache.ts +++ b/lib/OnyxSnapshotCache.ts @@ -60,7 +60,7 @@ class OnyxSnapshotCache { * - `selector`: Different selectors produce different results, so each selector needs its own cache entry * - `initWithStoredValues`: This flag changes the initial loading behavior and affects the returned fetch status * - * Other options like `reuseConnection` and `allowDynamicKey` don't affect the data transformation + * Other options like `reuseConnection` don't affect the data transformation * or timing behavior of getSnapshot, so they're excluded from the cache key for better cache hit rates. */ registerConsumer(options: Pick, 'selector' | 'initWithStoredValues'>): string { diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 5451537a6..9d590aad6 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -739,8 +739,7 @@ function retryOperation(error: Error, on if (nextRetryAttempt > MAX_STORAGE_OPERATION_RETRY_ATTEMPTS) { Logger.logAlert(`Storage operation failed after ${MAX_STORAGE_OPERATION_RETRY_ATTEMPTS} retries. Error: ${error}. onyxMethod: ${onyxMethod.name}.`); - reportStorageQuota(); - return Promise.resolve(); + return reportStorageQuota(); } // @ts-expect-error No overload matches this call. @@ -872,7 +871,7 @@ function initializeWithDefaultKeyStates(): Promise { // 2. All subsequent reads are synchronous cache hits return Storage.getAll() .then((pairs) => { - const allDataFromStorage: Record = {}; + const allDataFromStorage: Record> = {}; for (const [key, value] of pairs) { // RAM-only keys should never be loaded from storage as they may have stale persisted data // from before the key was migrated to RAM-only. @@ -902,7 +901,6 @@ function initializeWithDefaultKeyStates(): Promise { // Only notify subscribers for default key states — same as before. // Other keys will be picked up by subscribers when they connect. - // FIXME: Maybe we dont need this, but some tests in E/App are failing if we remove it. for (const [key, value] of Object.entries(merged ?? {})) keyChanged(key, value); }) .catch((error) => { @@ -916,6 +914,7 @@ function initializeWithDefaultKeyStates(): Promise { // Boot with defaults so the app renders instead of deadlocking. // Users will get a fresh-install experience but the app won't be bricked. cache.merge(defaultKeyStates); + for (const [key, value] of Object.entries(defaultKeyStates)) keyChanged(key, value); }); } diff --git a/tests/unit/useOnyxTest.ts b/tests/unit/useOnyxTest.ts index 6d19c6002..086ae096b 100644 --- a/tests/unit/useOnyxTest.ts +++ b/tests/unit/useOnyxTest.ts @@ -107,18 +107,16 @@ describe('useOnyx', () => { }); it('should return loaded state after an Onyx.clear() call while connecting and loading from cache', async () => { - // Write directly to storage so the data is not in cache when Onyx.clear() is called - // TODO: Check if this test still makes sense - await StorageMock.setItem(ONYXKEYS.TEST_KEY, 'test'); + await Onyx.set(ONYXKEYS.TEST_KEY, 'test'); Onyx.clear(); const {result: result1} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY)); const {result: result2} = renderHook(() => useOnyx(ONYXKEYS.TEST_KEY)); - expect(result1.current[0]).toBeUndefined(); + expect(result1.current[0]).toEqual('test'); expect(result1.current[1].status).toEqual('loaded'); - expect(result2.current[0]).toBeUndefined(); + expect(result2.current[0]).toEqual('test'); expect(result2.current[1].status).toEqual('loaded'); Onyx.merge(ONYXKEYS.TEST_KEY, 'test2'); @@ -848,8 +846,7 @@ describe('useOnyx', () => { }); it('should always return undefined when subscribing to a skippable collection member id', async () => { - // TODO: Check if this test still makes sense - await StorageMock.setItem(`${ONYXKEYS.COLLECTION.TEST_KEY}skippable-id`, 'skippable-id_value'); + await Onyx.set(`${ONYXKEYS.COLLECTION.TEST_KEY}skippable-id`, 'skippable-id_value'); const {result} = renderHook(() => useOnyx(`${ONYXKEYS.COLLECTION.TEST_KEY}skippable-id`)); From b61292e102e4a46e99abd9eadcbb2ebfe0034aff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 17 Mar 2026 09:49:51 +0000 Subject: [PATCH 7/9] Add tests for cache load during initialisation --- tests/unit/onyxCacheTest.tsx | 85 ++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/tests/unit/onyxCacheTest.tsx b/tests/unit/onyxCacheTest.tsx index 2bfc2d81c..b6ae45791 100644 --- a/tests/unit/onyxCacheTest.tsx +++ b/tests/unit/onyxCacheTest.tsx @@ -1,6 +1,7 @@ import type OnyxInstance from '../../lib/Onyx'; import type OnyxCache from '../../lib/OnyxCache'; import type {CacheTask} from '../../lib/OnyxCache'; +import type StorageMockType from '../../lib/storage'; import type {InitOptions} from '../../lib/types'; import waitForPromisesToResolve from '../utils/waitForPromisesToResolve'; @@ -449,6 +450,90 @@ describe('Onyx', () => { cache = require('../../lib/OnyxCache').default; }); + describe('eager loading during initialisation', () => { + let StorageMock: typeof StorageMockType; + + beforeEach(() => { + StorageMock = require('../../lib/storage').default; + }); + + it('should load all storage data into cache during init', async () => { + await StorageMock.setItem(ONYX_KEYS.TEST_KEY, 'storageValue'); + await StorageMock.setItem(`${ONYX_KEYS.COLLECTION.MOCK_COLLECTION}1`, {id: 1, name: 'Item 1'}); + await StorageMock.setItem(`${ONYX_KEYS.COLLECTION.MOCK_COLLECTION}2`, {id: 2, name: 'Item 2'}); + await initOnyx(); + + expect(cache.getAllKeys().size).toBe(3); + expect(cache.get(ONYX_KEYS.TEST_KEY)).toBe('storageValue'); + expect(cache.get(`${ONYX_KEYS.COLLECTION.MOCK_COLLECTION}1`)).toEqual({id: 1, name: 'Item 1'}); + expect(cache.get(`${ONYX_KEYS.COLLECTION.MOCK_COLLECTION}2`)).toEqual({id: 2, name: 'Item 2'}); + }); + + it('should not load RAM-only keys from storage during init', async () => { + const testKeys = { + ...ONYX_KEYS, + RAM_ONLY_KEY: 'ramOnlyKey', + }; + + await StorageMock.setItem(testKeys.RAM_ONLY_KEY, 'staleValue'); + await StorageMock.setItem(ONYX_KEYS.TEST_KEY, 'normalValue'); + await initOnyx({keys: testKeys, ramOnlyKeys: [testKeys.RAM_ONLY_KEY]}); + + expect(cache.getAllKeys().size).toBe(1); + expect(cache.get(testKeys.RAM_ONLY_KEY)).toBeUndefined(); + expect(cache.get(ONYX_KEYS.TEST_KEY)).toBe('normalValue'); + }); + + it('should merge default key states with storage data during init', async () => { + await StorageMock.setItem(ONYX_KEYS.OTHER_TEST, {fromStorage: true}); + await initOnyx({ + initialKeyStates: { + [ONYX_KEYS.OTHER_TEST]: {fromDefault: true}, + }, + }); + + // Default key states are merged on top of storage data. + expect(cache.get(ONYX_KEYS.OTHER_TEST)).toEqual({fromStorage: true, fromDefault: true}); + }); + + it('should use default key states when storage data is not available for a key', async () => { + await StorageMock.clear(); + await initOnyx({ + initialKeyStates: { + [ONYX_KEYS.OTHER_TEST]: 42, + }, + }); + + expect(cache.get(ONYX_KEYS.OTHER_TEST)).toBe(42); + }); + + it('should gracefully handle Storage.getAll() failure and boot with defaults', async () => { + (StorageMock.getAll as jest.Mock).mockImplementationOnce(() => Promise.reject(new Error('Database corrupted'))); + + await initOnyx({ + initialKeyStates: { + [ONYX_KEYS.OTHER_TEST]: 42, + }, + }); + + expect(cache.getAllKeys().size).toBe(1); + expect(cache.get(ONYX_KEYS.OTHER_TEST)).toBe(42); + }); + + it('should populate cache key index with all storage keys during init', async () => { + await StorageMock.setItem(ONYX_KEYS.TEST_KEY, 'value1'); + await StorageMock.setItem(ONYX_KEYS.OTHER_TEST, 'value2'); + await StorageMock.setItem(`${ONYX_KEYS.COLLECTION.MOCK_COLLECTION}1`, {id: 1}); + await initOnyx(); + + const allKeys = cache.getAllKeys(); + expect(allKeys.size).toBe(3); + expect(allKeys.has(ONYX_KEYS.TEST_KEY)).toBe(true); + expect(allKeys.has(ONYX_KEYS.OTHER_TEST)).toBe(true); + expect(allKeys.has(`${ONYX_KEYS.COLLECTION.MOCK_COLLECTION}1`)).toBe(true); + }); + }); + it('should save RAM-only keys', () => { const testKeys = { ...ONYX_KEYS, From 150e4b5efe52d3c941bc234b41fcb0a9fa4768ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 17 Mar 2026 10:40:45 +0000 Subject: [PATCH 8/9] Remove duplicate test --- tests/perf-test/useOnyx.perf-test.tsx | 48 --------------------------- 1 file changed, 48 deletions(-) diff --git a/tests/perf-test/useOnyx.perf-test.tsx b/tests/perf-test/useOnyx.perf-test.tsx index f70a8b050..0c97506be 100644 --- a/tests/perf-test/useOnyx.perf-test.tsx +++ b/tests/perf-test/useOnyx.perf-test.tsx @@ -269,54 +269,6 @@ describe('useOnyx', () => { }); }); - /** - * Expected renders: 1. - */ - test('3 calls loading from cache', async () => { - function TestComponent() { - const [testKeyData, testKeyMetadata] = useOnyx(ONYXKEYS.TEST_KEY); - const [testKey2Data, testKey2Metadata] = useOnyx(ONYXKEYS.TEST_KEY_2); - const [testKey3Data, testKey3Metadata] = useOnyx(ONYXKEYS.TEST_KEY_3); - - return ( - - - - - - ); - } - - await measureRenders(, { - beforeEach: async () => { - await Onyx.set(ONYXKEYS.TEST_KEY, 'test'); - await Onyx.set(ONYXKEYS.TEST_KEY_2, 'test2'); - await Onyx.set(ONYXKEYS.TEST_KEY_3, 'test3'); - }, - scenario: async () => { - await screen.findByText(dataMatcher(ONYXKEYS.TEST_KEY, 'test')); - await screen.findByText(metadataStatusMatcher(ONYXKEYS.TEST_KEY, 'loaded')); - await screen.findByText(dataMatcher(ONYXKEYS.TEST_KEY_2, 'test2')); - await screen.findByText(metadataStatusMatcher(ONYXKEYS.TEST_KEY_2, 'loaded')); - await screen.findByText(dataMatcher(ONYXKEYS.TEST_KEY_3, 'test3')); - await screen.findByText(metadataStatusMatcher(ONYXKEYS.TEST_KEY_3, 'loaded')); - }, - afterEach: clearOnyxAfterEachMeasure, - }); - }); - /** * Expected renders: 2. */ From 2c251c8440823ffd182f4e90664c801bef48b468 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 17 Mar 2026 11:09:16 +0000 Subject: [PATCH 9/9] Fix Reassure tests --- tests/perf-test/OnyxUtils.perf-test.ts | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/tests/perf-test/OnyxUtils.perf-test.ts b/tests/perf-test/OnyxUtils.perf-test.ts index f3fab80c9..6d4f4f902 100644 --- a/tests/perf-test/OnyxUtils.perf-test.ts +++ b/tests/perf-test/OnyxUtils.perf-test.ts @@ -3,7 +3,6 @@ import {randBoolean} from '@ngneat/falso'; import createRandomReportAction, {getRandomReportActions} from '../utils/collections/reportActions'; import type {Selector} from '../../lib'; import Onyx from '../../lib'; -import StorageMock from '../../lib/storage'; import OnyxUtils, {clearOnyxUtilsInternals} from '../../lib/OnyxUtils'; import type GenericCollection from '../utils/GenericCollection'; import type {OnyxUpdate} from '../../lib/Onyx'; @@ -112,7 +111,7 @@ describe('OnyxUtils', () => { test('10k calls with heavy objects', async () => { await measureAsyncFunction(() => Promise.all(mockedReportActionsKeys.map((key) => OnyxUtils.get(key))), { beforeEach: async () => { - await StorageMock.multiSet(Object.entries(mockedReportActionsMap).map(([k, v]) => [k, v])); + await Onyx.multiSet(mockedReportActionsMap); }, afterEach: clearOnyxAfterEachMeasure, }); @@ -123,7 +122,7 @@ describe('OnyxUtils', () => { test('one call with 50k heavy objects', async () => { await measureAsyncFunction(() => OnyxUtils.getAllKeys(), { beforeEach: async () => { - await StorageMock.multiSet(Object.entries(mockedReportActionsMap).map(([k, v]) => [k, v])); + await Onyx.multiSet(mockedReportActionsMap); }, afterEach: clearOnyxAfterEachMeasure, }); @@ -463,7 +462,7 @@ describe('OnyxUtils', () => { await measureAsyncFunction(() => OnyxUtils.initializeWithDefaultKeyStates(), { beforeEach: async () => { - await StorageMock.multiSet(Object.entries(changedReportActions).map(([k, v]) => [k, v])); + await Onyx.multiSet(changedReportActions); OnyxUtils.initStoreValues(ONYXKEYS, mockedReportActionsMap); }, afterEach: async () => { @@ -481,15 +480,6 @@ describe('OnyxUtils', () => { }); describe('multiGet', () => { - test('one call getting 10k heavy objects from storage', async () => { - await measureAsyncFunction(() => OnyxUtils.multiGet(mockedReportActionsKeys), { - beforeEach: async () => { - await StorageMock.multiSet(Object.entries(mockedReportActionsMap).map(([k, v]) => [k, v])); - }, - afterEach: clearOnyxAfterEachMeasure, - }); - }); - test('one call getting 10k heavy objects from cache', async () => { await measureAsyncFunction(() => OnyxUtils.multiGet(mockedReportActionsKeys), { beforeEach: async () => { @@ -540,7 +530,7 @@ describe('OnyxUtils', () => { }, { beforeEach: async () => { - await StorageMock.multiSet(Object.entries(mockedReportActionsMap).map(([k, v]) => [k, v])); + await Onyx.multiSet(mockedReportActionsMap); }, afterEach: async () => { OnyxUtils.unsubscribeFromKey(subscriptionID); @@ -567,7 +557,7 @@ describe('OnyxUtils', () => { }, { beforeEach: async () => { - await StorageMock.multiSet(Object.entries(mockedReportActionsMap).map(([k, v]) => [k, v])); + await Onyx.multiSet(mockedReportActionsMap); }, afterEach: async () => { OnyxUtils.unsubscribeFromKey(subscriptionID);