Skip to content

Commit 6f86313

Browse files
committed
Merge branch 'main' into feature/structural-sharing-cache-pr-3
2 parents 17fd83c + bcfee2c commit 6f86313

22 files changed

+254
-970
lines changed

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: 6 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,6 @@ class OnyxCache {
3535
/** A list of keys where a nullish value has been fetched from storage before, but the key still exists in cache */
3636
private nullishStorageKeys: Set<OnyxKey>;
3737

38-
/** Unique list of keys maintained in access order (most recent at the end) */
39-
private recentKeys: Set<OnyxKey>;
40-
4138
/** A map of cached values */
4239
private storageMap: Record<OnyxKey, OnyxValue<OnyxKey>>;
4340

@@ -47,15 +44,9 @@ class OnyxCache {
4744
*/
4845
private pendingPromises: Map<string, Promise<OnyxValue<OnyxKey> | OnyxKey[]>>;
4946

50-
/** Maximum size of the keys store din cache */
51-
private maxRecentKeysSize = 0;
52-
5347
/** List of keys that are safe to remove when we reach max storage */
5448
private evictionAllowList: OnyxKey[] = [];
5549

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

@@ -68,7 +59,6 @@ class OnyxCache {
6859
constructor() {
6960
this.storageKeys = new Set();
7061
this.nullishStorageKeys = new Set();
71-
this.recentKeys = new Set();
7262
this.storageMap = {};
7363
this.pendingPromises = new Map();
7464
this.collectionSnapshots = new Map();
@@ -90,12 +80,8 @@ class OnyxCache {
9080
'hasPendingTask',
9181
'getTaskPromise',
9282
'captureTask',
93-
'addToAccessedKeys',
94-
'removeLeastRecentlyUsedKeys',
95-
'setRecentKeysLimit',
9683
'setAllKeys',
9784
'setEvictionAllowList',
98-
'getEvictionBlocklist',
9985
'isEvictableKey',
10086
'removeLastAccessedKey',
10187
'addLastAccessedKey',
@@ -158,14 +144,8 @@ class OnyxCache {
158144
return this.storageMap[key] !== undefined || this.hasNullishStorageKey(key);
159145
}
160146

161-
/**
162-
* Get a cached value from storage
163-
* @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.
164-
*/
165-
get(key: OnyxKey, shouldReindexCache = true): OnyxValue<OnyxKey> {
166-
if (shouldReindexCache) {
167-
this.addToAccessedKeys(key);
168-
}
147+
/** Get a cached value from storage */
148+
get(key: OnyxKey): OnyxValue<OnyxKey> {
169149
return this.storageMap[key];
170150
}
171151

@@ -175,7 +155,6 @@ class OnyxCache {
175155
*/
176156
set(key: OnyxKey, value: OnyxValue<OnyxKey>): OnyxValue<OnyxKey> {
177157
this.addKey(key);
178-
this.addToAccessedKeys(key);
179158

180159
// When a key is explicitly set in cache, we can remove it from the list of nullish keys,
181160
// since it will either be set to a non nullish value or removed from the cache completely.
@@ -217,7 +196,6 @@ class OnyxCache {
217196
}
218197

219198
this.storageKeys.delete(key);
220-
this.recentKeys.delete(key);
221199
OnyxKeys.deregisterMemberKey(key);
222200
}
223201

@@ -234,7 +212,6 @@ class OnyxCache {
234212

235213
for (const [key, value] of Object.entries(data)) {
236214
this.addKey(key);
237-
this.addToAccessedKeys(key);
238215

239216
const collectionKey = OnyxKeys.getCollectionKey(key);
240217

@@ -314,52 +291,6 @@ class OnyxCache {
314291
return returnPromise;
315292
}
316293

317-
/** Adds a key to the top of the recently accessed keys */
318-
addToAccessedKeys(key: OnyxKey): void {
319-
this.recentKeys.delete(key);
320-
this.recentKeys.add(key);
321-
}
322-
323-
/** Remove keys that don't fall into the range of recently used keys */
324-
removeLeastRecentlyUsedKeys(): void {
325-
const numKeysToRemove = this.recentKeys.size - this.maxRecentKeysSize;
326-
if (numKeysToRemove <= 0) {
327-
return;
328-
}
329-
330-
const iterator = this.recentKeys.values();
331-
const keysToRemove: OnyxKey[] = [];
332-
333-
const recentKeysArray = Array.from(this.recentKeys);
334-
const mostRecentKey = recentKeysArray[recentKeysArray.length - 1];
335-
336-
let iterResult = iterator.next();
337-
while (!iterResult.done) {
338-
const key = iterResult.value;
339-
// Don't consider the most recently accessed key for eviction
340-
// This ensures we don't immediately evict a key we just added
341-
if (key !== undefined && key !== mostRecentKey && this.isEvictableKey(key)) {
342-
keysToRemove.push(key);
343-
}
344-
iterResult = iterator.next();
345-
}
346-
347-
for (const key of keysToRemove) {
348-
delete this.storageMap[key];
349-
350-
const collectionKey = OnyxKeys.getCollectionKey(key);
351-
if (collectionKey) {
352-
this.dirtyCollections.add(collectionKey);
353-
}
354-
this.recentKeys.delete(key);
355-
}
356-
}
357-
358-
/** Set the recent keys list size */
359-
setRecentKeysLimit(limit: number): void {
360-
this.maxRecentKeysSize = limit;
361-
}
362-
363294
/** Check if the value has changed. Uses reference equality as a fast path, falls back to deep equality. */
364295
hasValueChanged(key: OnyxKey, value: OnyxValue<OnyxKey>): boolean {
365296
const currentValue = this.storageMap[key];
@@ -377,13 +308,6 @@ class OnyxCache {
377308
this.evictionAllowList = keys;
378309
}
379310

380-
/**
381-
* Get the eviction block list that prevents keys from being evicted
382-
*/
383-
getEvictionBlocklist(): Record<OnyxKey, string[] | undefined> {
384-
return this.evictionBlocklist;
385-
}
386-
387311
/**
388312
* Checks to see if this key has been flagged as safe for removal.
389313
* @param testKey - Key to check
@@ -437,15 +361,12 @@ class OnyxCache {
437361
}
438362

439363
/**
440-
* Finds a key that can be safely evicted
364+
* Finds the least recently accessed key that can be safely evicted from storage.
441365
*/
442366
getKeyForEviction(): OnyxKey | undefined {
443-
for (const key of this.recentlyAccessedKeys) {
444-
if (!this.evictionBlocklist[key]) {
445-
return key;
446-
}
447-
}
448-
return undefined;
367+
// recentlyAccessedKeys is ordered from least to most recently accessed,
368+
// so the first element is the best candidate for eviction.
369+
return this.recentlyAccessedKeys.values().next().value;
449370
}
450371

451372
/**

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)