diff --git a/API-INTERNAL.md b/API-INTERNAL.md
index f5e6f119b..1f7afb688 100644
--- a/API-INTERNAL.md
+++ b/API-INTERNAL.md
@@ -72,10 +72,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.
@@ -326,13 +322,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
diff --git a/README.md b/README.md
index d85b032f8..9d70edc9d 100644
--- a/README.md
+++ b/README.md
@@ -314,42 +314,24 @@ If a platform needs to use a separate library (like using MMVK for react-native)
[Docs](./API.md)
-# Cache Eviction
+# Storage 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
+1. Onyx looks at a list of evictable keys ordered by recent access and locates the least recently accessed one
+2. It then deletes this key from both cache and storage, and retries the original operation
+3. This process repeats up to 5 times until the write succeeds or no more evictable keys are available
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`
+**To flag a key as safe for removal**, add the key to the `evictableKeys` option in `Onyx.init(options)`:
-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;
-```
+Only individual (non-collection) keys matching the `evictableKeys` patterns will be considered for eviction. Collection keys themselves cannot be evicted — only their individual members can.
# Debug mode
diff --git a/lib/Onyx.ts b/lib/Onyx.ts
index 77ce04852..be5de3fcc 100644
--- a/lib/Onyx.ts
+++ b/lib/Onyx.ts
@@ -34,7 +34,6 @@ function init({
keys = {},
initialKeyStates = {},
evictableKeys = [],
- maxCachedKeysCount = 1000,
shouldSyncMultipleInstances = !!global.localStorage,
enableDevTools = true,
skippableCollectionMemberIDs = [],
@@ -69,10 +68,6 @@ function init({
});
}
- if (maxCachedKeysCount > 0) {
- cache.setRecentKeysLimit(maxCachedKeysCount);
- }
-
OnyxUtils.initStoreValues(keys, initialKeyStates, evictableKeys);
// Initialize all of our keys with data provided then give green light to any pending connections.
diff --git a/lib/OnyxCache.ts b/lib/OnyxCache.ts
index 0444de370..2c9332643 100644
--- a/lib/OnyxCache.ts
+++ b/lib/OnyxCache.ts
@@ -25,9 +25,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,22 +37,15 @@ 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();
constructor() {
this.storageKeys = new Set();
this.nullishStorageKeys = new Set();
- this.recentKeys = new Set();
this.storageMap = {};
this.collectionData = {};
this.pendingPromises = new Map();
@@ -76,12 +66,8 @@ class OnyxCache {
'hasPendingTask',
'getTaskPromise',
'captureTask',
- 'addToAccessedKeys',
- 'removeLeastRecentlyUsedKeys',
- 'setRecentKeysLimit',
'setAllKeys',
'setEvictionAllowList',
- 'getEvictionBlocklist',
'isEvictableKey',
'removeLastAccessedKey',
'addLastAccessedKey',
@@ -144,14 +130,8 @@ class OnyxCache {
return this.storageMap[key] !== undefined || this.hasNullishStorageKey(key);
}
- /**
- * 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 a cached value from storage */
+ get(key: OnyxKey): OnyxValue {
return this.storageMap[key];
}
@@ -161,7 +141,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.
@@ -207,7 +186,6 @@ class OnyxCache {
}
this.storageKeys.delete(key);
- this.recentKeys.delete(key);
OnyxKeys.deregisterMemberKey(key);
}
@@ -229,7 +207,6 @@ class OnyxCache {
for (const [key, value] of Object.entries(data)) {
this.addKey(key);
- this.addToAccessedKeys(key);
const collectionKey = OnyxKeys.getCollectionKey(key);
@@ -287,56 +264,9 @@ 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 = OnyxKeys.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);
}
@@ -348,13 +278,6 @@ class OnyxCache {
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
@@ -408,15 +331,12 @@ class OnyxCache {
}
/**
- * Finds a key that can be safely evicted
+ * Finds the least recently accessed key that can be safely evicted from storage.
*/
getKeyForEviction(): OnyxKey | undefined {
- for (const key of this.recentlyAccessedKeys) {
- if (!this.evictionBlocklist[key]) {
- return key;
- }
- }
- return undefined;
+ // recentlyAccessedKeys is ordered from least to most recently accessed,
+ // so the first element is the best candidate for eviction.
+ return this.recentlyAccessedKeys.values().next().value;
}
/**
diff --git a/lib/OnyxConnectionManager.ts b/lib/OnyxConnectionManager.ts
index 0d5792c87..2726efc26 100644
--- a/lib/OnyxConnectionManager.ts
+++ b/lib/OnyxConnectionManager.ts
@@ -5,7 +5,6 @@ import OnyxUtils from './OnyxUtils';
import OnyxKeys from './OnyxKeys';
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;
@@ -106,7 +105,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');
}
/**
@@ -240,7 +239,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);
}
@@ -250,11 +248,8 @@ class OnyxConnectionManager {
* Disconnect all subscribers from Onyx.
*/
disconnectAll(): void {
- for (const [connectionID, connectionMetadata] of this.connectionsMap.entries()) {
+ for (const connectionMetadata of this.connectionsMap.values()) {
OnyxUtils.unsubscribeFromKey(connectionMetadata.subscriptionID);
- for (const callbackID of connectionMetadata.callbacks.keys()) {
- this.removeFromEvictionBlockList({id: connectionID, callbackID});
- }
}
this.connectionsMap.clear();
@@ -272,55 +267,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 3d96cc858..d4921b716 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` and `reuseConnection` 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 bbcd65ce4..e777c9566 100644
--- a/lib/OnyxUtils.ts
+++ b/lib/OnyxUtils.ts
@@ -633,8 +633,8 @@ function keyChanged(
canUpdateSubscriber: (subscriber?: CallbackToStateMapping) => boolean = () => true,
isProcessingCollectionUpdate = false,
): void {
- // Add or remove this key from the recentlyAccessedKeys lists
- if (value !== null) {
+ // Add or remove this key from the recentlyAccessedKeys list
+ if (value !== null && value !== undefined) {
cache.addLastAccessedKey(key, OnyxKeys.isCollectionKey(key));
} else {
cache.removeLastAccessedKey(key);
@@ -742,22 +742,6 @@ function sendDataToConnection(mapping: CallbackToStateMapp
(mapping as DefaultConnectOptions).callback?.(value, 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.
*/
@@ -822,7 +806,7 @@ function retryOperation(error: Error, on
return onyxMethod(defaultParams, nextRetryAttempt);
}
- // Find the first key that we can remove that has no subscribers in our blocklist
+ // Find the least recently accessed evictable key that we can remove
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,
@@ -832,7 +816,7 @@ function retryOperation(error: Error, on
return reportStorageQuota();
}
- // Remove the least recently viewed key that is not currently being accessed and retry.
+ // Remove the least recently accessed key and retry.
Logger.logInfo(`Out of storage. Evicting least recently accessed key (${keyForRemoval}) and retrying.`);
reportStorageQuota();
@@ -848,8 +832,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);
}
keyChanged(key, value, (subscriber) => hasChanged || subscriber?.initWithStoredValues === false);
@@ -1084,7 +1066,10 @@ function subscribeToKey(connectOptions: ConnectOptions addKeyToRecentlyAccessedIfNeeded(mapping.key))
+ .then(() => {
+ // Track evictable keys in the recently accessed list for storage eviction
+ cache.addLastAccessedKey(mapping.key, OnyxKeys.isCollectionKey(mapping.key));
+ })
.then(() => {
// Performance improvement
// If the mapping is connected to an onyx key that is not a collection
@@ -1278,7 +1263,7 @@ function setWithRetry({key, value, options}: SetParams> = (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.
@@ -168,22 +163,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.
@@ -321,8 +300,6 @@ function useOnyx>(
reuseConnection: options?.reuseConnection,
});
- checkEvictableKey();
-
return () => {
if (!connectionRef.current) {
return;
@@ -334,13 +311,9 @@ function useOnyx>(
onStoreChangeFnRef.current = null;
};
},
- [key, options?.initWithStoredValues, options?.reuseConnection, checkEvictableKey],
+ [key, options?.initWithStoredValues, options?.reuseConnection],
);
- useEffect(() => {
- checkEvictableKey();
- }, [checkEvictableKey]);
-
const result = useSyncExternalStore>(subscribe, getSnapshot);
return result;
diff --git a/tests/perf-test/Onyx.perf-test.ts b/tests/perf-test/Onyx.perf-test.ts
index 4acc46648..196f728b6 100644
--- a/tests/perf-test/Onyx.perf-test.ts
+++ b/tests/perf-test/Onyx.perf-test.ts
@@ -31,7 +31,6 @@ describe('Onyx', () => {
beforeAll(async () => {
Onyx.init({
keys: ONYXKEYS,
- maxCachedKeysCount: 100000,
evictableKeys: [ONYXKEYS.COLLECTION.EVICTABLE_TEST_KEY],
skippableCollectionMemberIDs: ['skippable-id'],
});
@@ -139,7 +138,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..fc3e5c519 100644
--- a/tests/perf-test/OnyxConnectionManager.perf-test.ts
+++ b/tests/perf-test/OnyxConnectionManager.perf-test.ts
@@ -46,7 +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 +146,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 2fc05ed36..afd03df7c 100644
--- a/tests/perf-test/OnyxUtils.perf-test.ts
+++ b/tests/perf-test/OnyxUtils.perf-test.ts
@@ -53,7 +53,6 @@ describe('OnyxUtils', () => {
beforeAll(async () => {
Onyx.init({
keys: ONYXKEYS,
- maxCachedKeysCount: 100000,
evictableKeys,
initialKeyStates,
skippableCollectionMemberIDs: ['skippable-id'],
@@ -659,12 +658,6 @@ describe('OnyxUtils', () => {
});
});
- describe('getEvictionBlocklist', () => {
- test('one call', async () => {
- await measureFunction(() => OnyxCache.getEvictionBlocklist());
- });
- });
-
describe('getSkippableCollectionMemberIDs', () => {
test('one call', async () => {
const skippableCollectionMemberIDs = OnyxUtils.getSkippableCollectionMemberIDs();
@@ -732,19 +725,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 b570c8107..d32816e18 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';
@@ -453,66 +452,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 fdaed1d51..3622731e6 100644
--- a/tests/unit/onyxCacheTest.tsx
+++ b/tests/unit/onyxCacheTest.tsx
@@ -2,10 +2,8 @@ import type OnyxInstance from '../../lib/Onyx';
import type OnyxCache from '../../lib/OnyxCache';
import type {CacheTask} from '../../lib/OnyxCache';
import type OnyxKeysType from '../../lib/OnyxKeys';
-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;
@@ -437,7 +435,6 @@ describe('Onyx', () => {
Onyx.init({
keys: ONYX_KEYS,
evictableKeys: [ONYX_KEYS.COLLECTION.MOCK_COLLECTION],
- maxCachedKeysCount: 10,
...overrides,
});
@@ -461,217 +458,6 @@ describe('Onyx', () => {
OnyxKeys = require('../../lib/OnyxKeys').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,
- COLLECTION: {
- ...ONYX_KEYS.COLLECTION,
- SAFE_FOR_EVICTION: 'evictable_',
- NOT_SAFE_FOR_EVICTION: 'critical_',
- },
- };
-
- const criticalKey1 = `${testKeys.COLLECTION.NOT_SAFE_FOR_EVICTION}1`;
- const criticalKey2 = `${testKeys.COLLECTION.NOT_SAFE_FOR_EVICTION}2`;
- const criticalKey3 = `${testKeys.COLLECTION.NOT_SAFE_FOR_EVICTION}3`;
- const evictableKey1 = `${testKeys.COLLECTION.SAFE_FOR_EVICTION}1`;
- const evictableKey2 = `${testKeys.COLLECTION.SAFE_FOR_EVICTION}2`;
- const evictableKey3 = `${testKeys.COLLECTION.SAFE_FOR_EVICTION}3`;
- const triggerKey = `${testKeys.COLLECTION.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.COLLECTION.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,
- COLLECTION: {
- ...ONYX_KEYS.COLLECTION,
- SAFE_FOR_EVICTION: 'evictable_',
- NOT_SAFE_FOR_EVICTION: 'critical_',
- },
- };
-
- const criticalKey1 = `${testKeys.COLLECTION.NOT_SAFE_FOR_EVICTION}1`;
- const criticalKey2 = `${testKeys.COLLECTION.NOT_SAFE_FOR_EVICTION}2`;
- const criticalKey3 = `${testKeys.COLLECTION.NOT_SAFE_FOR_EVICTION}3`;
- const evictableKey1 = `${testKeys.COLLECTION.SAFE_FOR_EVICTION}1`;
- // Additional trigger key for natural eviction
- const triggerKey = `${testKeys.COLLECTION.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.COLLECTION.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);
- });
- });
-
describe('eager loading during initialisation', () => {
beforeEach(() => {
StorageMock = require('../../lib/storage').default;
diff --git a/tests/unit/onyxUtilsTest.ts b/tests/unit/onyxUtilsTest.ts
index bd4d1a3a3..f124639b6 100644
--- a/tests/unit/onyxUtilsTest.ts
+++ b/tests/unit/onyxUtilsTest.ts
@@ -5,6 +5,7 @@ import type {GenericDeepRecord} from '../types';
import utils from '../../lib/utils';
import type {Collection, OnyxCollection} from '../../lib/types';
import type GenericCollection from '../utils/GenericCollection';
+import OnyxCache from '../../lib/OnyxCache';
import StorageMock from '../../lib/storage';
import createDeferredTask from '../../lib/createDeferredTask';
import waitForPromisesToResolve from '../utils/waitForPromisesToResolve';
@@ -393,6 +394,149 @@ describe('OnyxUtils', () => {
// Should only be called once since there are no evictable keys
expect(retryOperationSpy).toHaveBeenCalledTimes(1);
});
+
+ it('should not re-add an evicted key to recentlyAccessedKeys after removal', async () => {
+ // Re-init with evictable keys so getKeyForEviction() has something to return
+ Object.assign(OnyxUtils.getDeferredInitTask(), createDeferredTask());
+ Onyx.init({
+ keys: ONYXKEYS,
+ evictableKeys: [ONYXKEYS.COLLECTION.TEST_KEY],
+ });
+ await waitForPromisesToResolve();
+
+ const evictableKey = `${ONYXKEYS.COLLECTION.TEST_KEY}1`;
+
+ await Onyx.set(evictableKey, {id: 1});
+ expect(OnyxCache.getKeyForEviction()).toBe(evictableKey);
+
+ await OnyxUtils.remove(evictableKey);
+ expect(OnyxCache.getKeyForEviction()).toBeUndefined();
+ });
+ });
+
+ describe('storage eviction', () => {
+ const diskFullError = new Error('database or disk is full');
+
+ // Use local references that get fresh instances after jest.resetModules()
+ let LocalOnyx: typeof Onyx;
+ let LocalOnyxUtils: typeof OnyxUtils;
+ let LocalOnyxCache: typeof OnyxCache;
+ let LocalStorageMock: typeof StorageMock;
+
+ // Reset all modules to get fresh singletons (OnyxCache, OnyxUtils, etc.)
+ // then re-init Onyx with evictableKeys configured
+ beforeEach(async () => {
+ jest.resetModules();
+
+ LocalOnyx = require('../../lib').default;
+ LocalOnyxUtils = require('../../lib/OnyxUtils').default;
+ LocalOnyxCache = require('../../lib/OnyxCache').default;
+ LocalStorageMock = require('../../lib/storage').default;
+
+ LocalOnyx.init({
+ keys: ONYXKEYS,
+ evictableKeys: [ONYXKEYS.COLLECTION.TEST_KEY],
+ });
+ await waitForPromisesToResolve();
+ });
+
+ it('should evict the least recently accessed evictable key on storage capacity error and retry successfully', async () => {
+ const key1 = `${ONYXKEYS.COLLECTION.TEST_KEY}1`;
+ const key2 = `${ONYXKEYS.COLLECTION.TEST_KEY}2`;
+
+ await LocalOnyx.set(key1, {id: 1});
+ await LocalOnyx.set(key2, {id: 2});
+ expect(LocalOnyxCache.hasCacheForKey(key1)).toBe(true);
+ expect(LocalOnyxCache.hasCacheForKey(key2)).toBe(true);
+
+ // Fail once with capacity error, then succeed
+ LocalStorageMock.setItem = jest.fn(LocalStorageMock.setItem).mockRejectedValueOnce(diskFullError).mockImplementation(LocalStorageMock.setItem);
+
+ await LocalOnyx.set(ONYXKEYS.TEST_KEY, {test: 'data'});
+
+ // key1 was least recently accessed, so it should have been evicted
+ expect(LocalOnyxCache.hasCacheForKey(key1)).toBe(false);
+ // key2 was more recently accessed, so it should still be in cache
+ expect(LocalOnyxCache.hasCacheForKey(key2)).toBe(true);
+ // The write that triggered the error should have succeeded on retry
+ expect(LocalOnyxCache.get(ONYXKEYS.TEST_KEY)).toEqual({test: 'data'});
+ });
+
+ it('should evict the least recently accessed key first (LRU order)', async () => {
+ const key1 = `${ONYXKEYS.COLLECTION.TEST_KEY}1`;
+ const key2 = `${ONYXKEYS.COLLECTION.TEST_KEY}2`;
+ const key3 = `${ONYXKEYS.COLLECTION.TEST_KEY}3`;
+
+ // Set in order: key1, key2, key3
+ await LocalOnyx.set(key1, {id: 1});
+ await LocalOnyx.set(key2, {id: 2});
+ await LocalOnyx.set(key3, {id: 3});
+
+ // Now access key1 again so it becomes most recent
+ await LocalOnyx.merge(key1, {id: 1, updated: true});
+
+ // LRU order should now be: key2 (least recent), key3, key1 (most recent)
+ expect(LocalOnyxCache.getKeyForEviction()).toBe(key2);
+ });
+
+ it('should not evict non-evictable keys', async () => {
+ const evictableKey = `${ONYXKEYS.COLLECTION.TEST_KEY}1`;
+
+ await LocalOnyx.set(evictableKey, {id: 1});
+ await LocalOnyx.set(ONYXKEYS.TEST_KEY, {test: 'not evictable'});
+
+ // The evictable key should be a candidate for eviction
+ expect(LocalOnyxCache.isEvictableKey(evictableKey)).toBe(true);
+ // The non-evictable key should NOT be a candidate
+ expect(LocalOnyxCache.isEvictableKey(ONYXKEYS.TEST_KEY)).toBe(false);
+
+ // Evict it
+ await LocalOnyxUtils.remove(evictableKey);
+
+ // No more evictable candidates
+ expect(LocalOnyxCache.getKeyForEviction()).toBeUndefined();
+ // Non-evictable key should still be in cache
+ expect(LocalOnyxCache.get(ONYXKEYS.TEST_KEY)).toEqual({test: 'not evictable'});
+ });
+
+ it('should not add collection keys to eviction candidates, only their members', async () => {
+ const memberKey = `${ONYXKEYS.COLLECTION.TEST_KEY}1`;
+
+ await LocalOnyx.set(memberKey, {id: 1});
+
+ // The member key should be evictable
+ expect(LocalOnyxCache.getKeyForEviction()).toBe(memberKey);
+
+ // Attempting to add the collection key directly should be filtered out
+ LocalOnyxCache.addLastAccessedKey(ONYXKEYS.COLLECTION.TEST_KEY, true);
+
+ // Should still return the member key, not the collection key
+ expect(LocalOnyxCache.getKeyForEviction()).toBe(memberKey);
+ });
+
+ it('should seed evictable keys from storage at init', async () => {
+ // Set up storage with pre-existing evictable keys before init
+ jest.resetModules();
+
+ LocalOnyx = require('../../lib').default;
+ LocalOnyxCache = require('../../lib/OnyxCache').default;
+ const storage = require('../../lib/storage').default;
+
+ await storage.setItem(`${ONYXKEYS.COLLECTION.TEST_KEY}pre1`, {id: 'pre1'});
+ await storage.setItem(`${ONYXKEYS.COLLECTION.TEST_KEY}pre2`, {id: 'pre2'});
+
+ // Init — addEvictableKeysToRecentlyAccessedList should seed them
+ LocalOnyx.init({
+ keys: ONYXKEYS,
+ evictableKeys: [ONYXKEYS.COLLECTION.TEST_KEY],
+ });
+ await waitForPromisesToResolve();
+
+ // Pre-existing keys should be available for eviction without being explicitly accessed
+ const keyForEviction = LocalOnyxCache.getKeyForEviction();
+ expect(keyForEviction).toBeDefined();
+ expect(keyForEviction?.startsWith(ONYXKEYS.COLLECTION.TEST_KEY)).toBe(true);
+ });
});
describe('afterInit', () => {
diff --git a/tests/unit/useOnyxTest.ts b/tests/unit/useOnyxTest.ts
index c2fc2e999..5dddb1d1c 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';
@@ -14,7 +13,6 @@ const ONYXKEYS = {
COLLECTION: {
TEST_KEY: 'test_',
TEST_KEY_2: 'test2_',
- EVICTABLE_TEST_KEY: 'evictable_test_',
RAM_ONLY_COLLECTION: 'ramOnlyCollection_',
},
RAM_ONLY_KEY: 'ramOnlyKey',
@@ -23,7 +21,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],
});
@@ -1226,85 +1223,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();
- });
- });
});