Skip to content

Commit 101ad28

Browse files
committed
Merge branch 'main' into fix/improve-onyxConnectionManager
2 parents 0c80c84 + bcfee2c commit 101ad28

22 files changed

Lines changed: 255 additions & 972 deletions

API-INTERNAL.md

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,6 @@ If the requested key is a collection, it will return an object with all the coll
7272
<dt><a href="#sendDataToConnection">sendDataToConnection()</a></dt>
7373
<dd><p>Sends the data obtained from the keys to the connection.</p>
7474
</dd>
75-
<dt><a href="#addKeyToRecentlyAccessedIfNeeded">addKeyToRecentlyAccessedIfNeeded()</a></dt>
76-
<dd><p>We check to see if this key is flagged as safe for eviction and add it to the recentlyAccessedKeys list so that when we
77-
run out of storage the least recently accessed key can be removed.</p>
78-
</dd>
7975
<dt><a href="#getCollectionDataAndSendAsObject">getCollectionDataAndSendAsObject()</a></dt>
8076
<dd><p>Gets the data for a given an array of matching keys, combines them into an object, and sends the result back to the subscriber.</p>
8177
</dd>
@@ -326,13 +322,6 @@ keyChanged(key, value, subscriber => subscriber.initWithStoredValues === false)
326322
## sendDataToConnection()
327323
Sends the data obtained from the keys to the connection.
328324

329-
**Kind**: global function
330-
<a name="addKeyToRecentlyAccessedIfNeeded"></a>
331-
332-
## addKeyToRecentlyAccessedIfNeeded()
333-
We check to see if this key is flagged as safe for eviction and add it to the recentlyAccessedKeys list so that when we
334-
run out of storage the least recently accessed key can be removed.
335-
336325
**Kind**: global function
337326
<a name="getCollectionDataAndSendAsObject"></a>
338327

README.md

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -314,42 +314,24 @@ If a platform needs to use a separate library (like using MMVK for react-native)
314314

315315
[Docs](./API.md)
316316

317-
# Cache Eviction
317+
# Storage Eviction
318318

319319
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:
320-
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
321-
2. It then deletes this key and retries the original operation
320+
1. Onyx looks at a list of evictable keys ordered by recent access and locates the least recently accessed one
321+
2. It then deletes this key from both cache and storage, and retries the original operation
322+
3. This process repeats up to 5 times until the write succeeds or no more evictable keys are available
322323

323324
By default, Onyx will not evict anything from storage and will presume all keys are "unsafe" to remove unless explicitly told otherwise.
324325

325-
**To flag a key as safe for removal:**
326-
- Add the key to the `evictableKeys` option in `Onyx.init(options)`
327-
- Implement `canEvict` in the Onyx config for each component subscribing to a key
328-
- The key will only be deleted when all subscribers return `true` for `canEvict`
326+
**To flag a key as safe for removal**, add the key to the `evictableKeys` option in `Onyx.init(options)`:
329327

330-
e.g.
331328
```js
332329
Onyx.init({
333330
evictableKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS],
334331
});
335332
```
336333

337-
```js
338-
const ReportActionsView = ({reportID, isActiveReport}) => {
339-
const [reportActions] = useOnyx(
340-
`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}_`,
341-
{canEvict: () => !isActiveReport}
342-
);
343-
344-
return (
345-
<View>
346-
{/* Render with reportActions data */}
347-
</View>
348-
);
349-
};
350-
351-
export default ReportActionsView;
352-
```
334+
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.
353335

354336
# Debug mode
355337

lib/Onyx.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ function init({
3434
keys = {},
3535
initialKeyStates = {},
3636
evictableKeys = [],
37-
maxCachedKeysCount = 1000,
3837
shouldSyncMultipleInstances = !!global.localStorage,
3938
enableDevTools = true,
4039
skippableCollectionMemberIDs = [],
@@ -69,10 +68,6 @@ function init({
6968
});
7069
}
7170

72-
if (maxCachedKeysCount > 0) {
73-
cache.setRecentKeysLimit(maxCachedKeysCount);
74-
}
75-
7671
OnyxUtils.initStoreValues(keys, initialKeyStates, evictableKeys);
7772

7873
// Initialize all of our keys with data provided then give green light to any pending connections.

lib/OnyxCache.ts

Lines changed: 7 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ class OnyxCache {
2525
/** A list of keys where a nullish value has been fetched from storage before, but the key still exists in cache */
2626
private nullishStorageKeys: Set<OnyxKey>;
2727

28-
/** Unique list of keys maintained in access order (most recent at the end) */
29-
private recentKeys: Set<OnyxKey>;
30-
3128
/** A map of cached values */
3229
private storageMap: Record<OnyxKey, OnyxValue<OnyxKey>>;
3330

@@ -40,22 +37,15 @@ class OnyxCache {
4037
*/
4138
private pendingPromises: Map<string, Promise<OnyxValue<OnyxKey> | OnyxKey[]>>;
4239

43-
/** Maximum size of the keys store din cache */
44-
private maxRecentKeysSize = 0;
45-
4640
/** List of keys that are safe to remove when we reach max storage */
4741
private evictionAllowList: OnyxKey[] = [];
4842

49-
/** Map of keys and connection arrays whose keys will never be automatically evicted */
50-
private evictionBlocklist: Record<OnyxKey, string[] | undefined> = {};
51-
5243
/** List of keys that have been directly subscribed to or recently modified from least to most recent */
5344
private recentlyAccessedKeys = new Set<OnyxKey>();
5445

5546
constructor() {
5647
this.storageKeys = new Set();
5748
this.nullishStorageKeys = new Set();
58-
this.recentKeys = new Set();
5949
this.storageMap = {};
6050
this.collectionData = {};
6151
this.pendingPromises = new Map();
@@ -76,12 +66,8 @@ class OnyxCache {
7666
'hasPendingTask',
7767
'getTaskPromise',
7868
'captureTask',
79-
'addToAccessedKeys',
80-
'removeLeastRecentlyUsedKeys',
81-
'setRecentKeysLimit',
8269
'setAllKeys',
8370
'setEvictionAllowList',
84-
'getEvictionBlocklist',
8571
'isEvictableKey',
8672
'removeLastAccessedKey',
8773
'addLastAccessedKey',
@@ -144,14 +130,8 @@ class OnyxCache {
144130
return this.storageMap[key] !== undefined || this.hasNullishStorageKey(key);
145131
}
146132

147-
/**
148-
* Get a cached value from storage
149-
* @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.
150-
*/
151-
get(key: OnyxKey, shouldReindexCache = true): OnyxValue<OnyxKey> {
152-
if (shouldReindexCache) {
153-
this.addToAccessedKeys(key);
154-
}
133+
/** Get a cached value from storage */
134+
get(key: OnyxKey): OnyxValue<OnyxKey> {
155135
return this.storageMap[key];
156136
}
157137

@@ -161,7 +141,6 @@ class OnyxCache {
161141
*/
162142
set(key: OnyxKey, value: OnyxValue<OnyxKey>): OnyxValue<OnyxKey> {
163143
this.addKey(key);
164-
this.addToAccessedKeys(key);
165144

166145
// When a key is explicitly set in cache, we can remove it from the list of nullish keys,
167146
// since it will either be set to a non nullish value or removed from the cache completely.
@@ -207,7 +186,6 @@ class OnyxCache {
207186
}
208187

209188
this.storageKeys.delete(key);
210-
this.recentKeys.delete(key);
211189
OnyxKeys.deregisterMemberKey(key);
212190
}
213191

@@ -229,7 +207,6 @@ class OnyxCache {
229207

230208
for (const [key, value] of Object.entries(data)) {
231209
this.addKey(key);
232-
this.addToAccessedKeys(key);
233210

234211
const collectionKey = OnyxKeys.getCollectionKey(key);
235212

@@ -287,56 +264,9 @@ class OnyxCache {
287264
return returnPromise;
288265
}
289266

290-
/** Adds a key to the top of the recently accessed keys */
291-
addToAccessedKeys(key: OnyxKey): void {
292-
this.recentKeys.delete(key);
293-
this.recentKeys.add(key);
294-
}
295-
296-
/** Remove keys that don't fall into the range of recently used keys */
297-
removeLeastRecentlyUsedKeys(): void {
298-
const numKeysToRemove = this.recentKeys.size - this.maxRecentKeysSize;
299-
if (numKeysToRemove <= 0) {
300-
return;
301-
}
302-
303-
const iterator = this.recentKeys.values();
304-
const keysToRemove: OnyxKey[] = [];
305-
306-
const recentKeysArray = Array.from(this.recentKeys);
307-
const mostRecentKey = recentKeysArray[recentKeysArray.length - 1];
308-
309-
let iterResult = iterator.next();
310-
while (!iterResult.done) {
311-
const key = iterResult.value;
312-
// Don't consider the most recently accessed key for eviction
313-
// This ensures we don't immediately evict a key we just added
314-
if (key !== undefined && key !== mostRecentKey && this.isEvictableKey(key)) {
315-
keysToRemove.push(key);
316-
}
317-
iterResult = iterator.next();
318-
}
319-
320-
for (const key of keysToRemove) {
321-
delete this.storageMap[key];
322-
323-
// Remove from collection data cache if this is a collection member
324-
const collectionKey = OnyxKeys.getCollectionKey(key);
325-
if (collectionKey && this.collectionData[collectionKey]) {
326-
delete this.collectionData[collectionKey][key];
327-
}
328-
this.recentKeys.delete(key);
329-
}
330-
}
331-
332-
/** Set the recent keys list size */
333-
setRecentKeysLimit(limit: number): void {
334-
this.maxRecentKeysSize = limit;
335-
}
336-
337267
/** Check if the value has changed */
338268
hasValueChanged(key: OnyxKey, value: OnyxValue<OnyxKey>): boolean {
339-
const currentValue = this.get(key, false);
269+
const currentValue = this.get(key);
340270
return !deepEqual(currentValue, value);
341271
}
342272

@@ -348,13 +278,6 @@ class OnyxCache {
348278
this.evictionAllowList = keys;
349279
}
350280

351-
/**
352-
* Get the eviction block list that prevents keys from being evicted
353-
*/
354-
getEvictionBlocklist(): Record<OnyxKey, string[] | undefined> {
355-
return this.evictionBlocklist;
356-
}
357-
358281
/**
359282
* Checks to see if this key has been flagged as safe for removal.
360283
* @param testKey - Key to check
@@ -408,15 +331,12 @@ class OnyxCache {
408331
}
409332

410333
/**
411-
* Finds a key that can be safely evicted
334+
* Finds the least recently accessed key that can be safely evicted from storage.
412335
*/
413336
getKeyForEviction(): OnyxKey | undefined {
414-
for (const key of this.recentlyAccessedKeys) {
415-
if (!this.evictionBlocklist[key]) {
416-
return key;
417-
}
418-
}
419-
return undefined;
337+
// recentlyAccessedKeys is ordered from least to most recently accessed,
338+
// so the first element is the best candidate for eviction.
339+
return this.recentlyAccessedKeys.values().next().value;
420340
}
421341

422342
/**

lib/OnyxConnectionManager.ts

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

1110
type ConnectCallback = DefaultConnectCallback<OnyxKey> | CollectionConnectCallback<OnyxKey>;
@@ -106,7 +105,7 @@ class OnyxConnectionManager {
106105
this.sessionID = Str.guid();
107106

108107
// Binds all public methods to prevent problems with `this`.
109-
bindAll(this, 'generateConnectionID', 'fireCallbacks', 'connect', 'disconnect', 'disconnectAll', 'refreshSessionID', 'addToEvictionBlockList', 'removeFromEvictionBlockList');
108+
bindAll(this, 'generateConnectionID', 'fireCallbacks', 'connect', 'disconnect', 'disconnectAll', 'refreshSessionID');
110109
}
111110

112111
/**
@@ -240,7 +239,6 @@ class OnyxConnectionManager {
240239
// If the connection's callbacks map is empty we can safely unsubscribe from the Onyx key.
241240
if (connectionMetadata.callbacks.size === 0) {
242241
OnyxUtils.unsubscribeFromKey(connectionMetadata.subscriptionID);
243-
this.removeFromEvictionBlockList(connection);
244242

245243
this.connectionsMap.delete(connection.id);
246244
}
@@ -250,11 +248,8 @@ class OnyxConnectionManager {
250248
* Disconnect all subscribers from Onyx.
251249
*/
252250
disconnectAll(): void {
253-
for (const [connectionID, connectionMetadata] of this.connectionsMap.entries()) {
251+
for (const connectionMetadata of this.connectionsMap.values()) {
254252
OnyxUtils.unsubscribeFromKey(connectionMetadata.subscriptionID);
255-
for (const callbackID of connectionMetadata.callbacks.keys()) {
256-
this.removeFromEvictionBlockList({id: connectionID, callbackID});
257-
}
258253
}
259254

260255
this.connectionsMap.clear();
@@ -272,55 +267,6 @@ class OnyxConnectionManager {
272267
// Clear snapshot cache when session refreshes to avoid stale cache issues
273268
onyxSnapshotCache.clear();
274269
}
275-
276-
/**
277-
* Adds the connection to the eviction block list. Connections added to this list can never be evicted.
278-
* */
279-
addToEvictionBlockList(connection: Connection): void {
280-
if (!connection) {
281-
Logger.logInfo(`[ConnectionManager] Attempted to add connection to eviction block list passing an undefined connection object.`);
282-
return;
283-
}
284-
285-
const connectionMetadata = this.connectionsMap.get(connection.id);
286-
if (!connectionMetadata) {
287-
Logger.logInfo(`[ConnectionManager] Attempted to add connection to eviction block list but no connection was found.`);
288-
return;
289-
}
290-
291-
const evictionBlocklist = cache.getEvictionBlocklist();
292-
if (!evictionBlocklist[connectionMetadata.onyxKey]) {
293-
evictionBlocklist[connectionMetadata.onyxKey] = [];
294-
}
295-
296-
evictionBlocklist[connectionMetadata.onyxKey]?.push(`${connection.id}_${connection.callbackID}`);
297-
}
298-
299-
/**
300-
* Removes a connection previously added to this list
301-
* which will enable it to be evicted again.
302-
*/
303-
removeFromEvictionBlockList(connection: Connection): void {
304-
if (!connection) {
305-
Logger.logInfo(`[ConnectionManager] Attempted to remove connection from eviction block list passing an undefined connection object.`);
306-
return;
307-
}
308-
309-
const connectionMetadata = this.connectionsMap.get(connection.id);
310-
if (!connectionMetadata) {
311-
Logger.logInfo(`[ConnectionManager] Attempted to remove connection from eviction block list but no connection was found.`);
312-
return;
313-
}
314-
315-
const evictionBlocklist = cache.getEvictionBlocklist();
316-
evictionBlocklist[connectionMetadata.onyxKey] =
317-
evictionBlocklist[connectionMetadata.onyxKey]?.filter((evictionKey) => evictionKey !== `${connection.id}_${connection.callbackID}`) ?? [];
318-
319-
// Remove the key if there are no more subscribers.
320-
if (evictionBlocklist[connectionMetadata.onyxKey]?.length === 0) {
321-
delete evictionBlocklist[connectionMetadata.onyxKey];
322-
}
323-
}
324270
}
325271

326272
const connectionManager = new OnyxConnectionManager();

lib/OnyxSnapshotCache.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ class OnyxSnapshotCache {
6060
* - `selector`: Different selectors produce different results, so each selector needs its own cache entry
6161
* - `initWithStoredValues`: This flag changes the initial loading behavior and affects the returned fetch status
6262
*
63-
* Other options like `canEvict` and `reuseConnection` don't affect the data transformation
63+
* Other options like `reuseConnection` don't affect the data transformation
6464
* or timing behavior of getSnapshot, so they're excluded from the cache key for better cache hit rates.
6565
*/
6666
registerConsumer<TKey extends OnyxKey, TReturnValue>(options: Pick<UseOnyxOptions<TKey, TReturnValue>, 'selector' | 'initWithStoredValues'>): string {

0 commit comments

Comments
 (0)