Skip to content

Commit bc9c52b

Browse files
committed
Merge remote-tracking branch 'upstream/main' into JKobrynski/feat/80091-prevent-storage-reads-for-ram-only-keys
2 parents edd14c6 + 40a3da8 commit bc9c52b

13 files changed

Lines changed: 69 additions & 282 deletions

API-INTERNAL.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,15 @@
2020
<dt><a href="#getSkippableCollectionMemberIDs">getSkippableCollectionMemberIDs()</a></dt>
2121
<dd><p>Getter - returns the skippable collection member IDs.</p>
2222
</dd>
23+
<dt><a href="#getSnapshotMergeKeys">getSnapshotMergeKeys()</a></dt>
24+
<dd><p>Getter - returns the snapshot merge keys allowlist.</p>
25+
</dd>
2326
<dt><a href="#setSkippableCollectionMemberIDs">setSkippableCollectionMemberIDs()</a></dt>
2427
<dd><p>Setter - sets the skippable collection member IDs.</p>
2528
</dd>
29+
<dt><a href="#setSnapshotMergeKeys">setSnapshotMergeKeys()</a></dt>
30+
<dd><p>Setter - sets the snapshot merge keys allowlist.</p>
31+
</dd>
2632
<dt><a href="#initStoreValues">initStoreValues(keys, initialKeyStates, evictableKeys)</a></dt>
2733
<dd><p>Sets the initial values for the Onyx store</p>
2834
</dd>
@@ -222,12 +228,24 @@ Getter - returns the deffered init task.
222228
## getSkippableCollectionMemberIDs()
223229
Getter - returns the skippable collection member IDs.
224230

231+
**Kind**: global function
232+
<a name="getSnapshotMergeKeys"></a>
233+
234+
## getSnapshotMergeKeys()
235+
Getter - returns the snapshot merge keys allowlist.
236+
225237
**Kind**: global function
226238
<a name="setSkippableCollectionMemberIDs"></a>
227239

228240
## setSkippableCollectionMemberIDs()
229241
Setter - sets the skippable collection member IDs.
230242

243+
**Kind**: global function
244+
<a name="setSnapshotMergeKeys"></a>
245+
246+
## setSnapshotMergeKeys()
247+
Setter - sets the snapshot merge keys allowlist.
248+
231249
**Kind**: global function
232250
<a name="initStoreValues"></a>
233251

README.md

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -206,21 +206,6 @@ export default App;
206206

207207
It's also beneficial to use a [selector](https://github.com/Expensify/react-native-onyx/blob/main/API.md#connectmapping--number) with the mapping in case you need to grab a single item in a collection (like a single report action).
208208

209-
### useOnyx()'s `canBeMissing` option
210-
211-
You must pass the `canBeMissing` configuration flag to `useOnyx` if you want the hook to log an alert when data is missing from Onyx store. Regarding usage in `Expensify/App` repo, if the component calling this is the one loading the data by calling an action, then you should set this to `true`. If the component calling this does not load the data then you should set it to `false`, which means that if the data is not there, it will log an alert, as it means we are using data that no one loaded and that's most probably a bug.
212-
213-
```javascript
214-
const Component = ({reportID}) => {
215-
// This hook will log an alert (via `Logger.logAlert()`) if `report` is `undefined`.
216-
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {canBeMissing: false});
217-
218-
// rest of the component's code.
219-
};
220-
221-
export default Component;
222-
```
223-
224209
## Collections
225210

226211
Collections allow keys with similar value types to be subscribed together by subscribing to the collection key. To define one, it must be included in the `ONYXKEYS.COLLECTION` object and it must be suffixed with an underscore. Member keys should use a unique identifier or index after the collection key prefix (e.g. `report_42`).

lib/Onyx.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -352,13 +352,7 @@ function clear(keysToPreserve: OnyxKey[] = []): Promise<void> {
352352
if (newValue !== oldValue) {
353353
cache.set(key, newValue);
354354

355-
let collectionKey: string | undefined;
356-
try {
357-
collectionKey = OnyxUtils.getCollectionKey(key);
358-
} catch (e) {
359-
// If getCollectionKey() throws an error it means the key is not a collection key.
360-
collectionKey = undefined;
361-
}
355+
const collectionKey = OnyxUtils.getCollectionKey(key);
362356

363357
if (collectionKey) {
364358
if (!keyValuesToResetAsCollection[collectionKey]) {

lib/OnyxCache.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -458,13 +458,13 @@ class OnyxCache {
458458
/**
459459
* Get the collection key for a given member key
460460
*/
461-
getCollectionKey(key: OnyxKey): OnyxKey | null {
461+
getCollectionKey(key: OnyxKey): OnyxKey | undefined {
462462
for (const collectionKey of this.collectionKeys) {
463463
if (key.startsWith(collectionKey) && key.length > collectionKey.length) {
464464
return collectionKey;
465465
}
466466
}
467-
return null;
467+
return undefined;
468468
}
469469

470470
/**

lib/OnyxSnapshotCache.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -60,19 +60,17 @@ 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
* - `allowStaleData`: Controls whether stale data can be returned during pending merges, affecting result timing
63-
* - `canBeMissing`: Determines logging behavior for missing data, but doesn't affect the actual data returned
6463
*
6564
* Other options like `canEvict`, `reuseConnection`, and `allowDynamicKey` don't affect the data transformation
6665
* or timing behavior of getSnapshot, so they're excluded from the cache key for better cache hit rates.
6766
*/
68-
registerConsumer<TKey extends OnyxKey, TReturnValue>(options: Pick<UseOnyxOptions<TKey, TReturnValue>, 'selector' | 'initWithStoredValues' | 'allowStaleData' | 'canBeMissing'>): string {
67+
registerConsumer<TKey extends OnyxKey, TReturnValue>(options: Pick<UseOnyxOptions<TKey, TReturnValue>, 'selector' | 'initWithStoredValues' | 'allowStaleData'>): string {
6968
const selectorID = options?.selector ? this.getSelectorID(options.selector) : 'no_selector';
7069

7170
// Create options hash without expensive JSON.stringify
7271
const initWithStoredValues = options?.initWithStoredValues ?? true;
7372
const allowStaleData = options?.allowStaleData ?? false;
74-
const canBeMissing = options?.canBeMissing ?? true;
75-
const cacheKey = `${selectorID}_${initWithStoredValues}_${allowStaleData}_${canBeMissing}`;
73+
const cacheKey = `${selectorID}_${initWithStoredValues}_${allowStaleData}`;
7674

7775
// Increment reference count for this cache key
7876
const currentCount = this.cacheKeyRefCounts.get(cacheKey) || 0;
@@ -133,12 +131,10 @@ class OnyxSnapshotCache {
133131
// Always invalidate the exact key
134132
this.snapshotCache.delete(keyToInvalidate);
135133

136-
try {
137-
// Check if the key is a collection member and invalidate the collection base key
138-
const collectionBaseKey = OnyxUtils.getCollectionKey(keyToInvalidate);
134+
// Check if the key is a collection member and invalidate the collection base key
135+
const collectionBaseKey = OnyxUtils.getCollectionKey(keyToInvalidate);
136+
if (collectionBaseKey) {
139137
this.snapshotCache.delete(collectionBaseKey);
140-
} catch (e) {
141-
// do nothing - this just means the key is not a collection member
142138
}
143139
}
144140

lib/OnyxUtils.ts

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -495,14 +495,9 @@ function isCollectionMemberKey<TCollectionKey extends CollectionKeyBase>(collect
495495
* @returns true if the key is a collection member, false otherwise
496496
*/
497497
function isCollectionMember(key: OnyxKey): boolean {
498-
try {
499-
const collectionKey = getCollectionKey(key);
500-
// If the key is longer than the collection key, it's a collection member
501-
return key.length > collectionKey.length;
502-
} catch (e) {
503-
// If getCollectionKey throws, the key is not a collection member
504-
return false;
505-
}
498+
const collectionKey = getCollectionKey(key);
499+
// If the key is longer than the collection key, it's a collection member
500+
return !!collectionKey && key.length > collectionKey.length;
506501
}
507502

508503
/**
@@ -523,12 +518,9 @@ function isCollectionMember(key: OnyxKey): boolean {
523518
* @returns true if key is a RAM-only key, RAM-only collection key, or a RAM-only collection member
524519
*/
525520
function isRamOnlyKey(key: OnyxKey): boolean {
526-
try {
527-
const collectionKey = getCollectionKey(key);
528-
// If collectionKey exists for a given key, check if it's a RAM-only key
521+
const collectionKey = getCollectionKey(key);
522+
if (collectionKey) {
529523
return cache.isRamOnlyKey(collectionKey);
530-
} catch {
531-
// If getCollectionKey throws, the key is not a collection member
532524
}
533525

534526
return cache.isRamOnlyKey(key);
@@ -550,8 +542,12 @@ function splitCollectionMemberKey<TKey extends CollectionKey, CollectionKeyType
550542
}
551543

552544
if (!collectionKey) {
545+
const resolvedKey = getCollectionKey(key);
546+
if (!resolvedKey) {
547+
throw new Error(`Invalid '${key}' key provided, only collection keys are allowed.`);
548+
}
553549
// eslint-disable-next-line no-param-reassign
554-
collectionKey = getCollectionKey(key);
550+
collectionKey = resolvedKey;
555551
}
556552

557553
return [collectionKey as CollectionKeyType, key.slice(collectionKey.length)];
@@ -575,9 +571,9 @@ function isKeyMatch(configKey: OnyxKey, key: OnyxKey): boolean {
575571
* - `getCollectionKey("sharedNVP_user_-1_something")` would return "sharedNVP_user_"
576572
*
577573
* @param key - The collection key to process.
578-
* @returns The plain collection key or throws an Error if the key is not a collection one.
574+
* @returns The plain collection key or undefined if the key is not a collection one.
579575
*/
580-
function getCollectionKey(key: CollectionKey): string {
576+
function getCollectionKey(key: CollectionKey): string | undefined {
581577
// Start by finding the position of the last underscore in the string
582578
let lastUnderscoreIndex = key.lastIndexOf('_');
583579

@@ -595,7 +591,7 @@ function getCollectionKey(key: CollectionKey): string {
595591
lastUnderscoreIndex = key.lastIndexOf('_', lastUnderscoreIndex - 1);
596592
}
597593

598-
throw new Error(`Invalid '${key}' key provided, only collection keys are allowed.`);
594+
return undefined;
599595
}
600596

601597
/**
@@ -774,13 +770,7 @@ function keyChanged<TKey extends OnyxKey>(
774770
// do the same in keysChanged, because we only call that function when a collection key changes, and it doesn't happen that often.
775771
// For performance reason, we look for the given key and later if don't find it we look for the collection key, instead of checking if it is a collection key first.
776772
let stateMappingKeys = onyxKeyToSubscriptionIDs.get(key) ?? [];
777-
let collectionKey: string | undefined;
778-
try {
779-
collectionKey = getCollectionKey(key);
780-
} catch (e) {
781-
// If getCollectionKey() throws an error it means the key is not a collection key.
782-
collectionKey = undefined;
783-
}
773+
const collectionKey = getCollectionKey(key);
784774

785775
if (collectionKey) {
786776
// Getting the collection key from the specific key because only collection keys were stored in the mapping.

lib/useOnyx.ts

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import * as GlobalSettings from './GlobalSettings';
99
import type {CollectionKeyBase, OnyxKey, OnyxValue} from './types';
1010
import usePrevious from './usePrevious';
1111
import decorateWithMetrics from './metrics';
12-
import * as Logger from './Logger';
1312
import onyxSnapshotCache from './OnyxSnapshotCache';
1413
import useLiveRef from './useLiveRef';
1514

@@ -43,14 +42,6 @@ type UseOnyxOptions<TKey extends OnyxKey, TReturnValue> = {
4342
*/
4443
allowDynamicKey?: boolean;
4544

46-
/**
47-
* If the component calling this is the one loading the data by calling an action, then you should set this to `true`.
48-
*
49-
* If the component calling this does not load the data then you should set it to `false`, which means that if the data
50-
* is not there, it will log an alert, as it means we are using data that no one loaded and that's most probably a bug.
51-
*/
52-
canBeMissing?: boolean;
53-
5445
/**
5546
* This will be used to subscribe to a subset of an Onyx key's data.
5647
* Using this setting on `useOnyx` can have very positive performance benefits because the component will only re-render
@@ -156,9 +147,8 @@ function useOnyx<TKey extends OnyxKey, TReturnValue = OnyxValue<TKey>>(
156147
selector: options?.selector,
157148
initWithStoredValues: options?.initWithStoredValues,
158149
allowStaleData: options?.allowStaleData,
159-
canBeMissing: options?.canBeMissing,
160150
}),
161-
[options?.selector, options?.initWithStoredValues, options?.allowStaleData, options?.canBeMissing],
151+
[options?.selector, options?.initWithStoredValues, options?.allowStaleData],
162152
);
163153

164154
useEffect(() => () => onyxSnapshotCache.deregisterConsumer(key, cacheKey), [key, cacheKey]);
@@ -242,8 +232,6 @@ function useOnyx<TKey extends OnyxKey, TReturnValue = OnyxValue<TKey>>(
242232
}
243233
}
244234

245-
let isOnyxValueDefined = true;
246-
247235
// We return the initial result right away during the first connection if `initWithStoredValues` is set to `false`.
248236
if (isFirstConnectionRef.current && options?.initWithStoredValues === false) {
249237
const result = resultRef.current;
@@ -262,10 +250,6 @@ function useOnyx<TKey extends OnyxKey, TReturnValue = OnyxValue<TKey>>(
262250
const selectedValue = memoizedSelector ? memoizedSelector(value) : value;
263251
newValueRef.current = (selectedValue ?? undefined) as TReturnValue | undefined;
264252

265-
// This flag is `false` when the original Onyx value (without selector) is not defined yet.
266-
// It will be used later to check if we need to log an alert that the value is missing.
267-
isOnyxValueDefined = value !== null && value !== undefined;
268-
269253
// We set this flag to `false` again since we don't want to get the newest cached value every time `getSnapshot()` is executed,
270254
// and only when `Onyx.connect()` callback is fired.
271255
shouldGetCachedValueRef.current = false;
@@ -317,21 +301,14 @@ function useOnyx<TKey extends OnyxKey, TReturnValue = OnyxValue<TKey>>(
317301
sourceValue: sourceValueRef.current,
318302
},
319303
];
320-
321-
// If `canBeMissing` is set to `false` and the Onyx value of that key is not defined,
322-
// we log an alert so it can be acknowledged by the consumer. Additionally, we won't log alerts
323-
// if there's a `Onyx.clear()` task in progress.
324-
if (options?.canBeMissing === false && newFetchStatus === 'loaded' && !isOnyxValueDefined && !OnyxCache.hasPendingTask(TASK.CLEAR)) {
325-
Logger.logAlert(`useOnyx returned no data for key with canBeMissing set to false for key ${key}`, {showAlert: true});
326-
}
327304
}
328305

329306
if (newFetchStatus !== 'loading') {
330307
onyxSnapshotCache.setCachedResult<UseOnyxResult<TReturnValue>>(key, cacheKey, resultRef.current);
331308
}
332309

333310
return resultRef.current;
334-
}, [options?.initWithStoredValues, options?.allowStaleData, options?.canBeMissing, key, memoizedSelector, cacheKey, previousKey]);
311+
}, [options?.initWithStoredValues, options?.allowStaleData, key, memoizedSelector, cacheKey, previousKey]);
335312

336313
const subscribe = useCallback(
337314
(onStoreChange: () => void) => {

0 commit comments

Comments
 (0)