Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions API-INTERNAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
<dt><a href="#setSkippableCollectionMemberIDs">setSkippableCollectionMemberIDs()</a></dt>
<dd><p>Setter - sets the skippable collection member IDs.</p>
</dd>
<dt><a href="#initStoreValues">initStoreValues(keys, initialKeyStates, safeEvictionKeys)</a></dt>
<dt><a href="#initStoreValues">initStoreValues(keys, initialKeyStates, evictableKeys)</a></dt>
<dd><p>Sets the initial values for the Onyx store</p>
</dd>
<dt><a href="#maybeFlushBatchUpdates">maybeFlushBatchUpdates()</a></dt>
Expand Down Expand Up @@ -71,7 +71,7 @@ is associated with a collection of keys.</p>
<dd><p>Checks to see if a provided key is the exact configured key of our connected subscriber
or if the provided key is a collection member key (in case our configured key is a &quot;collection key&quot;)</p>
</dd>
<dt><a href="#isSafeEvictionKey">isSafeEvictionKey()</a></dt>
<dt><a href="#isEvictableKey">isEvictableKey()</a></dt>
<dd><p>Checks to see if this key has been flagged as safe for removal.</p>
</dd>
<dt><a href="#getCollectionKey">getCollectionKey(key)</a> ⇒</dt>
Expand All @@ -96,7 +96,7 @@ If the requested key is a collection, it will return an object with all the coll
recently accessed key should be at the head and the most
recently accessed key at the tail.</p>
</dd>
<dt><a href="#addAllSafeEvictionKeysToRecentlyAccessedList">addAllSafeEvictionKeysToRecentlyAccessedList()</a></dt>
<dt><a href="#addEvictableKeysToRecentlyAccessedList">addEvictableKeysToRecentlyAccessedList()</a></dt>
<dd><p>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
Expand Down Expand Up @@ -213,7 +213,7 @@ Setter - sets the skippable collection member IDs.
**Kind**: global function
<a name="initStoreValues"></a>

## initStoreValues(keys, initialKeyStates, safeEvictionKeys)
## initStoreValues(keys, initialKeyStates, evictableKeys)
Sets the initial values for the Onyx store

**Kind**: global function
Expand All @@ -222,7 +222,7 @@ 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 |
| safeEvictionKeys | This is an array of keys (individual or collection patterns) that when provided to Onyx are flagged as "safe" for removal. |
| evictableKeys | This is an array of keys (individual or collection patterns) that are eligible for automatic removal when storage limits are reached. |

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

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

**Kind**: global function
<a name="isSafeEvictionKey"></a>
<a name="isEvictableKey"></a>

## isSafeEvictionKey()
## isEvictableKey()
Checks to see if this key has been flagged as safe for removal.

**Kind**: global function
Expand Down Expand Up @@ -364,9 +364,9 @@ recently accessed key should be at the head and the most
recently accessed key at the tail.

**Kind**: global function
<a name="addAllSafeEvictionKeysToRecentlyAccessedList"></a>
<a name="addEvictableKeysToRecentlyAccessedList"></a>

## addAllSafeEvictionKeysToRecentlyAccessedList()
## addEvictableKeysToRecentlyAccessedList()
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
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -388,14 +388,14 @@ Different platforms come with varying storage capacities and Onyx has a way to g
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 `safeEvictionKeys` option in `Onyx.init(options)`
- 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({
safeEvictionKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS],
evictableKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this require any migration?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only thing we need to do is just change the parameter name in the app. I am also thinking we should add more keys in the App which are evictable. Like NYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS, ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES, ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS tested it already and there seems to not be any disadvantages

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok thanks, feel free to work on the bump PR

});
```

Expand Down Expand Up @@ -423,7 +423,7 @@ Provide the `captureMetrics` boolean flag to `Onyx.init` to capture call statist
```js
Onyx.init({
keys: ONYXKEYS,
safeEvictionKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS],
evictableKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS],
captureMetrics: Config.BENCHMARK_ONYX,
});
```
Expand Down
8 changes: 5 additions & 3 deletions lib/Onyx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import decorateWithMetrics from './metrics';
function init({
keys = {},
initialKeyStates = {},
safeEvictionKeys = [],
evictableKeys = [],
maxCachedKeysCount = 1000,
shouldSyncMultipleInstances = !!global.localStorage,
debugSetState = false,
Expand Down Expand Up @@ -70,10 +70,12 @@ function init({
cache.setRecentKeysLimit(maxCachedKeysCount);
}

OnyxUtils.initStoreValues(keys, initialKeyStates, safeEvictionKeys);
OnyxUtils.initStoreValues(keys, initialKeyStates, evictableKeys);

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

/**
Expand Down
126 changes: 119 additions & 7 deletions lib/OnyxCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ 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 = {
Expand Down Expand Up @@ -39,6 +40,15 @@ class OnyxCache {
/** 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<OnyxKey, string[] | undefined> = {};

/** List of keys that have been directly subscribed to or recently modified from least to most recent */
private recentlyAccessedKeys: OnyxKey[] = [];

constructor() {
this.storageKeys = new Set();
this.nullishStorageKeys = new Set();
Expand All @@ -62,9 +72,17 @@ class OnyxCache {
'hasPendingTask',
'getTaskPromise',
'captureTask',
'addToAccessedKeys',
'removeLeastRecentlyUsedKeys',
'setRecentKeysLimit',
'setAllKeys',
'setEvictionAllowList',
'getEvictionBlocklist',
'isEvictableKey',
'removeLastAccessedKey',
'addLastAccessedKey',
'addEvictableKeysToRecentlyAccessedList',
'getKeyForEviction',
);
}

Expand Down Expand Up @@ -219,19 +237,29 @@ class OnyxCache {

/** Remove keys that don't fall into the range of recently used keys */
removeLeastRecentlyUsedKeys(): void {
let numKeysToRemove = this.recentKeys.size - this.maxRecentKeysSize;
const numKeysToRemove = this.recentKeys.size - this.maxRecentKeysSize;
if (numKeysToRemove <= 0) {
return;
}

const iterator = this.recentKeys.values();
const temp = [];
while (numKeysToRemove > 0) {
const value = iterator.next().value;
temp.push(value);
numKeysToRemove--;
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 temp) {
for (const key of keysToRemove) {
delete this.storageMap[key];
this.recentKeys.delete(key);
}
Expand All @@ -246,6 +274,90 @@ class OnyxCache {
hasValueChanged(key: OnyxKey, value: OnyxValue<OnyxKey>): boolean {
return !deepEqual(this.storageMap[key], 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<OnyxKey, string[] | undefined> {
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;
}
Comment on lines +301 to +309
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a copy of OnyxUtils.isKeyMatch? Can't we reuse instead?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We cannot import OnyxUtils to OnyxCache as this produces dependency cycle

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's please extract this to another file that both OnyxUtils and OnyxCache can import, to avoid duplicate code

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could think about splitting up the OnyxUtils file in general, because at the moment it's around ~1500 lines long and there's not that much inter-dependent state between the functions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But maybe let's do that in a follow-up PR 😅


/**
* Remove a key from the recently accessed key list.
*/
removeLastAccessedKey(key: OnyxKey): void {
this.recentlyAccessedKeys = this.recentlyAccessedKeys.filter((recentlyAccessedKey) => recentlyAccessedKey !== 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.push(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<Set<OnyxKey>>): Promise<void> {
return getAllKeysFn().then((keys: Set<OnyxKey>) => {
this.evictionAllowList.forEach((evictableKey) => {
keys.forEach((key: OnyxKey) => {
if (!this.isKeyMatch(evictableKey, key)) {
return;
}

this.addLastAccessedKey(key, isCollectionKeyFn(key));
});
});
});
}

/**
* Finds a key that can be safely evicted
*/
getKeyForEviction(): OnyxKey | undefined {
return this.recentlyAccessedKeys.find((key) => !this.evictionBlocklist[key]);
}
}

const instance = new OnyxCache();
Expand Down
5 changes: 3 additions & 2 deletions lib/OnyxConnectionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import OnyxUtils from './OnyxUtils';
import * as Str from './Str';
import type {CollectionConnectCallback, DefaultConnectCallback, DefaultConnectOptions, OnyxKey, OnyxValue} from './types';
import utils from './utils';
import cache from './OnyxCache';

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

Expand Down Expand Up @@ -285,7 +286,7 @@ class OnyxConnectionManager {
return;
}

const evictionBlocklist = OnyxUtils.getEvictionBlocklist();
const evictionBlocklist = cache.getEvictionBlocklist();
if (!evictionBlocklist[connectionMetadata.onyxKey]) {
evictionBlocklist[connectionMetadata.onyxKey] = [];
}
Expand All @@ -309,7 +310,7 @@ class OnyxConnectionManager {
return;
}

const evictionBlocklist = OnyxUtils.getEvictionBlocklist();
const evictionBlocklist = cache.getEvictionBlocklist();
evictionBlocklist[connectionMetadata.onyxKey] =
evictionBlocklist[connectionMetadata.onyxKey]?.filter((evictionKey) => evictionKey !== `${connection.id}_${connection.callbackID}`) ?? [];

Expand Down
Loading