Skip to content

Commit 20f9c12

Browse files
authored
Merge pull request #629 from callstack-internal/test-for-keys-eviction-implementation
Fix cache key eviction
2 parents b902cf9 + b380086 commit 20f9c12

16 files changed

Lines changed: 353 additions & 175 deletions

API-INTERNAL.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
<dt><a href="#setSkippableCollectionMemberIDs">setSkippableCollectionMemberIDs()</a></dt>
2727
<dd><p>Setter - sets the skippable collection member IDs.</p>
2828
</dd>
29-
<dt><a href="#initStoreValues">initStoreValues(keys, initialKeyStates, safeEvictionKeys)</a></dt>
29+
<dt><a href="#initStoreValues">initStoreValues(keys, initialKeyStates, evictableKeys)</a></dt>
3030
<dd><p>Sets the initial values for the Onyx store</p>
3131
</dd>
3232
<dt><a href="#maybeFlushBatchUpdates">maybeFlushBatchUpdates()</a></dt>
@@ -71,7 +71,7 @@ is associated with a collection of keys.</p>
7171
<dd><p>Checks to see if a provided key is the exact configured key of our connected subscriber
7272
or if the provided key is a collection member key (in case our configured key is a &quot;collection key&quot;)</p>
7373
</dd>
74-
<dt><a href="#isSafeEvictionKey">isSafeEvictionKey()</a></dt>
74+
<dt><a href="#isEvictableKey">isEvictableKey()</a></dt>
7575
<dd><p>Checks to see if this key has been flagged as safe for removal.</p>
7676
</dd>
7777
<dt><a href="#getCollectionKey">getCollectionKey(key)</a> ⇒</dt>
@@ -96,7 +96,7 @@ If the requested key is a collection, it will return an object with all the coll
9696
recently accessed key should be at the head and the most
9797
recently accessed key at the tail.</p>
9898
</dd>
99-
<dt><a href="#addAllSafeEvictionKeysToRecentlyAccessedList">addAllSafeEvictionKeysToRecentlyAccessedList()</a></dt>
99+
<dt><a href="#addEvictableKeysToRecentlyAccessedList">addEvictableKeysToRecentlyAccessedList()</a></dt>
100100
<dd><p>Take all the keys that are safe to evict and add them to
101101
the recently accessed list when initializing the app. This
102102
enables keys that have not recently been accessed to be
@@ -213,7 +213,7 @@ Setter - sets the skippable collection member IDs.
213213
**Kind**: global function
214214
<a name="initStoreValues"></a>
215215

216-
## initStoreValues(keys, initialKeyStates, safeEvictionKeys)
216+
## initStoreValues(keys, initialKeyStates, evictableKeys)
217217
Sets the initial values for the Onyx store
218218

219219
**Kind**: global function
@@ -222,7 +222,7 @@ Sets the initial values for the Onyx store
222222
| --- | --- |
223223
| keys | `ONYXKEYS` constants object from Onyx.init() |
224224
| initialKeyStates | initial data to set when `init()` and `clear()` are called |
225-
| safeEvictionKeys | This is an array of keys (individual or collection patterns) that when provided to Onyx are flagged as "safe" for removal. |
225+
| evictableKeys | This is an array of keys (individual or collection patterns) that are eligible for automatic removal when storage limits are reached. |
226226

227227
<a name="maybeFlushBatchUpdates"></a>
228228

@@ -319,9 +319,9 @@ Checks to see if a provided key is the exact configured key of our connected sub
319319
or if the provided key is a collection member key (in case our configured key is a "collection key")
320320

321321
**Kind**: global function
322-
<a name="isSafeEvictionKey"></a>
322+
<a name="isEvictableKey"></a>
323323

324-
## isSafeEvictionKey()
324+
## isEvictableKey()
325325
Checks to see if this key has been flagged as safe for removal.
326326

327327
**Kind**: global function
@@ -364,9 +364,9 @@ recently accessed key should be at the head and the most
364364
recently accessed key at the tail.
365365

366366
**Kind**: global function
367-
<a name="addAllSafeEvictionKeysToRecentlyAccessedList"></a>
367+
<a name="addEvictableKeysToRecentlyAccessedList"></a>
368368

369-
## addAllSafeEvictionKeysToRecentlyAccessedList()
369+
## addEvictableKeysToRecentlyAccessedList()
370370
Take all the keys that are safe to evict and add them to
371371
the recently accessed list when initializing the app. This
372372
enables keys that have not recently been accessed to be

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -388,14 +388,14 @@ Different platforms come with varying storage capacities and Onyx has a way to g
388388
By default, Onyx will not evict anything from storage and will presume all keys are "unsafe" to remove unless explicitly told otherwise.
389389

390390
**To flag a key as safe for removal:**
391-
- Add the key to the `safeEvictionKeys` option in `Onyx.init(options)`
391+
- Add the key to the `evictableKeys` option in `Onyx.init(options)`
392392
- Implement `canEvict` in the Onyx config for each component subscribing to a key
393393
- The key will only be deleted when all subscribers return `true` for `canEvict`
394394

395395
e.g.
396396
```js
397397
Onyx.init({
398-
safeEvictionKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS],
398+
evictableKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS],
399399
});
400400
```
401401

@@ -423,7 +423,7 @@ Provide the `captureMetrics` boolean flag to `Onyx.init` to capture call statist
423423
```js
424424
Onyx.init({
425425
keys: ONYXKEYS,
426-
safeEvictionKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS],
426+
evictableKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS],
427427
captureMetrics: Config.BENCHMARK_ONYX,
428428
});
429429
```

lib/Onyx.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import decorateWithMetrics from './metrics';
3838
function init({
3939
keys = {},
4040
initialKeyStates = {},
41-
safeEvictionKeys = [],
41+
evictableKeys = [],
4242
maxCachedKeysCount = 1000,
4343
shouldSyncMultipleInstances = !!global.localStorage,
4444
debugSetState = false,
@@ -70,10 +70,12 @@ function init({
7070
cache.setRecentKeysLimit(maxCachedKeysCount);
7171
}
7272

73-
OnyxUtils.initStoreValues(keys, initialKeyStates, safeEvictionKeys);
73+
OnyxUtils.initStoreValues(keys, initialKeyStates, evictableKeys);
7474

7575
// Initialize all of our keys with data provided then give green light to any pending connections
76-
Promise.all([OnyxUtils.addAllSafeEvictionKeysToRecentlyAccessedList(), OnyxUtils.initializeWithDefaultKeyStates()]).then(OnyxUtils.getDeferredInitTask().resolve);
76+
Promise.all([cache.addEvictableKeysToRecentlyAccessedList(OnyxUtils.isCollectionKey, OnyxUtils.getAllKeys), OnyxUtils.initializeWithDefaultKeyStates()]).then(
77+
OnyxUtils.getDeferredInitTask().resolve,
78+
);
7779
}
7880

7981
/**

lib/OnyxCache.ts

Lines changed: 119 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import bindAll from 'lodash/bindAll';
33
import type {ValueOf} from 'type-fest';
44
import utils from './utils';
55
import type {OnyxKey, OnyxValue} from './types';
6+
import * as Str from './Str';
67

78
// Task constants
89
const TASK = {
@@ -39,6 +40,15 @@ class OnyxCache {
3940
/** Maximum size of the keys store din cache */
4041
private maxRecentKeysSize = 0;
4142

43+
/** List of keys that are safe to remove when we reach max storage */
44+
private evictionAllowList: OnyxKey[] = [];
45+
46+
/** Map of keys and connection arrays whose keys will never be automatically evicted */
47+
private evictionBlocklist: Record<OnyxKey, string[] | undefined> = {};
48+
49+
/** List of keys that have been directly subscribed to or recently modified from least to most recent */
50+
private recentlyAccessedKeys: OnyxKey[] = [];
51+
4252
constructor() {
4353
this.storageKeys = new Set();
4454
this.nullishStorageKeys = new Set();
@@ -62,9 +72,17 @@ class OnyxCache {
6272
'hasPendingTask',
6373
'getTaskPromise',
6474
'captureTask',
75+
'addToAccessedKeys',
6576
'removeLeastRecentlyUsedKeys',
6677
'setRecentKeysLimit',
6778
'setAllKeys',
79+
'setEvictionAllowList',
80+
'getEvictionBlocklist',
81+
'isEvictableKey',
82+
'removeLastAccessedKey',
83+
'addLastAccessedKey',
84+
'addEvictableKeysToRecentlyAccessedList',
85+
'getKeyForEviction',
6886
);
6987
}
7088

@@ -219,19 +237,29 @@ class OnyxCache {
219237

220238
/** Remove keys that don't fall into the range of recently used keys */
221239
removeLeastRecentlyUsedKeys(): void {
222-
let numKeysToRemove = this.recentKeys.size - this.maxRecentKeysSize;
240+
const numKeysToRemove = this.recentKeys.size - this.maxRecentKeysSize;
223241
if (numKeysToRemove <= 0) {
224242
return;
225243
}
244+
226245
const iterator = this.recentKeys.values();
227-
const temp = [];
228-
while (numKeysToRemove > 0) {
229-
const value = iterator.next().value;
230-
temp.push(value);
231-
numKeysToRemove--;
246+
const keysToRemove: OnyxKey[] = [];
247+
248+
const recentKeysArray = Array.from(this.recentKeys);
249+
const mostRecentKey = recentKeysArray[recentKeysArray.length - 1];
250+
251+
let iterResult = iterator.next();
252+
while (!iterResult.done) {
253+
const key = iterResult.value;
254+
// Don't consider the most recently accessed key for eviction
255+
// This ensures we don't immediately evict a key we just added
256+
if (key !== undefined && key !== mostRecentKey && this.isEvictableKey(key)) {
257+
keysToRemove.push(key);
258+
}
259+
iterResult = iterator.next();
232260
}
233261

234-
for (const key of temp) {
262+
for (const key of keysToRemove) {
235263
delete this.storageMap[key];
236264
this.recentKeys.delete(key);
237265
}
@@ -246,6 +274,90 @@ class OnyxCache {
246274
hasValueChanged(key: OnyxKey, value: OnyxValue<OnyxKey>): boolean {
247275
return !deepEqual(this.storageMap[key], value);
248276
}
277+
278+
/**
279+
* Sets the list of keys that are considered safe for eviction
280+
* @param keys - Array of OnyxKeys that are safe to evict
281+
*/
282+
setEvictionAllowList(keys: OnyxKey[]): void {
283+
this.evictionAllowList = keys;
284+
}
285+
286+
/**
287+
* Get the eviction block list that prevents keys from being evicted
288+
*/
289+
getEvictionBlocklist(): Record<OnyxKey, string[] | undefined> {
290+
return this.evictionBlocklist;
291+
}
292+
293+
/**
294+
* Checks to see if this key has been flagged as safe for removal.
295+
* @param testKey - Key to check
296+
*/
297+
isEvictableKey(testKey: OnyxKey): boolean {
298+
return this.evictionAllowList.some((key) => this.isKeyMatch(key, testKey));
299+
}
300+
301+
/**
302+
* Check if a given key matches a pattern key
303+
* @param configKey - Pattern that may contain a wildcard
304+
* @param key - Key to test against the pattern
305+
*/
306+
private isKeyMatch(configKey: OnyxKey, key: OnyxKey): boolean {
307+
const isCollectionKey = configKey.endsWith('_');
308+
return isCollectionKey ? Str.startsWith(key, configKey) : configKey === key;
309+
}
310+
311+
/**
312+
* Remove a key from the recently accessed key list.
313+
*/
314+
removeLastAccessedKey(key: OnyxKey): void {
315+
this.recentlyAccessedKeys = this.recentlyAccessedKeys.filter((recentlyAccessedKey) => recentlyAccessedKey !== key);
316+
}
317+
318+
/**
319+
* Add a key to the list of recently accessed keys. The least
320+
* recently accessed key should be at the head and the most
321+
* recently accessed key at the tail.
322+
*/
323+
addLastAccessedKey(key: OnyxKey, isCollectionKey: boolean): void {
324+
// Only specific keys belong in this list since we cannot remove an entire collection.
325+
if (isCollectionKey || !this.isEvictableKey(key)) {
326+
return;
327+
}
328+
329+
this.removeLastAccessedKey(key);
330+
this.recentlyAccessedKeys.push(key);
331+
}
332+
333+
/**
334+
* Take all the keys that are safe to evict and add them to
335+
* the recently accessed list when initializing the app. This
336+
* enables keys that have not recently been accessed to be
337+
* removed.
338+
* @param isCollectionKeyFn - Function to determine if a key is a collection key
339+
* @param getAllKeysFn - Function to get all keys, defaults to Storage.getAllKeys
340+
*/
341+
addEvictableKeysToRecentlyAccessedList(isCollectionKeyFn: (key: OnyxKey) => boolean, getAllKeysFn: () => Promise<Set<OnyxKey>>): Promise<void> {
342+
return getAllKeysFn().then((keys: Set<OnyxKey>) => {
343+
this.evictionAllowList.forEach((evictableKey) => {
344+
keys.forEach((key: OnyxKey) => {
345+
if (!this.isKeyMatch(evictableKey, key)) {
346+
return;
347+
}
348+
349+
this.addLastAccessedKey(key, isCollectionKeyFn(key));
350+
});
351+
});
352+
});
353+
}
354+
355+
/**
356+
* Finds a key that can be safely evicted
357+
*/
358+
getKeyForEviction(): OnyxKey | undefined {
359+
return this.recentlyAccessedKeys.find((key) => !this.evictionBlocklist[key]);
360+
}
249361
}
250362

251363
const instance = new OnyxCache();

lib/OnyxConnectionManager.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import OnyxUtils from './OnyxUtils';
55
import * as Str from './Str';
66
import type {CollectionConnectCallback, DefaultConnectCallback, DefaultConnectOptions, OnyxKey, OnyxValue} from './types';
77
import utils from './utils';
8+
import cache from './OnyxCache';
89

910
type ConnectCallback = DefaultConnectCallback<OnyxKey> | CollectionConnectCallback<OnyxKey>;
1011

@@ -285,7 +286,7 @@ class OnyxConnectionManager {
285286
return;
286287
}
287288

288-
const evictionBlocklist = OnyxUtils.getEvictionBlocklist();
289+
const evictionBlocklist = cache.getEvictionBlocklist();
289290
if (!evictionBlocklist[connectionMetadata.onyxKey]) {
290291
evictionBlocklist[connectionMetadata.onyxKey] = [];
291292
}
@@ -309,7 +310,7 @@ class OnyxConnectionManager {
309310
return;
310311
}
311312

312-
const evictionBlocklist = OnyxUtils.getEvictionBlocklist();
313+
const evictionBlocklist = cache.getEvictionBlocklist();
313314
evictionBlocklist[connectionMetadata.onyxKey] =
314315
evictionBlocklist[connectionMetadata.onyxKey]?.filter((evictionKey) => evictionKey !== `${connection.id}_${connection.callbackID}`) ?? [];
315316

0 commit comments

Comments
 (0)