Skip to content

Commit c5f9b1e

Browse files
authored
Merge pull request Expensify#761 from callstack-internal/feature/structural-sharing-cache-pr-1
[Structural Sharing] PR 1: Centralized key utilities with bidirectional indexes
2 parents b351fa5 + bb82ecc commit c5f9b1e

16 files changed

Lines changed: 756 additions & 550 deletions

API-INTERNAL.md

Lines changed: 19 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@
1717
<dt><a href="#getDeferredInitTask">getDeferredInitTask()</a></dt>
1818
<dd><p>Getter - returns the deffered init task.</p>
1919
</dd>
20+
<dt><a href="#afterInit">afterInit(action)</a> ⇒</dt>
21+
<dd><p>Executes an action after Onyx has been initialized.
22+
If Onyx is already initialized, the action is executed immediately.
23+
Otherwise, it waits for initialization to complete before executing.</p>
24+
</dd>
2025
<dt><a href="#getSkippableCollectionMemberIDs">getSkippableCollectionMemberIDs()</a></dt>
2126
<dd><p>Getter - returns the skippable collection member IDs.</p>
2227
</dd>
@@ -54,45 +59,6 @@ to the values for those keys (correctly typed) such as <code>[OnyxCollection&lt;
5459
<dt><a href="#getAllKeys">getAllKeys()</a></dt>
5560
<dd><p>Returns current key names stored in persisted storage</p>
5661
</dd>
57-
<dt><a href="#getCollectionKeys">getCollectionKeys()</a></dt>
58-
<dd><p>Returns set of all registered collection keys</p>
59-
</dd>
60-
<dt><a href="#isCollectionKey">isCollectionKey()</a></dt>
61-
<dd><p>Checks to see if the subscriber&#39;s supplied key
62-
is associated with a collection of keys.</p>
63-
</dd>
64-
<dt><a href="#isCollectionMember">isCollectionMember(key)</a> ⇒</dt>
65-
<dd><p>Checks if a given key is a collection member key (not just a collection key).</p>
66-
</dd>
67-
<dt><a href="#isRamOnlyKey">isRamOnlyKey(key)</a> ⇒</dt>
68-
<dd><p>Checks if a given key is a RAM-only key, RAM-only collection key, or a RAM-only collection member</p>
69-
<p>For example:</p>
70-
<p>For the following Onyx setup</p>
71-
<p>ramOnlyKeys: [&quot;ramOnlyKey&quot;, &quot;ramOnlyCollection_&quot;]</p>
72-
<ul>
73-
<li><code>isRamOnlyKey(&quot;ramOnlyKey&quot;)</code> would return true</li>
74-
<li><code>isRamOnlyKey(&quot;ramOnlyCollection_&quot;)</code> would return true</li>
75-
<li><code>isRamOnlyKey(&quot;ramOnlyCollection_1&quot;)</code> would return true</li>
76-
<li><code>isRamOnlyKey(&quot;someOtherKey&quot;)</code> would return false</li>
77-
</ul>
78-
</dd>
79-
<dt><a href="#splitCollectionMemberKey">splitCollectionMemberKey(key, collectionKey)</a> ⇒</dt>
80-
<dd><p>Splits a collection member key into the collection key part and the ID part.</p>
81-
</dd>
82-
<dt><a href="#isKeyMatch">isKeyMatch()</a></dt>
83-
<dd><p>Checks to see if a provided key is the exact configured key of our connected subscriber
84-
or if the provided key is a collection member key (in case our configured key is a &quot;collection key&quot;)</p>
85-
</dd>
86-
<dt><a href="#getCollectionKey">getCollectionKey(key)</a> ⇒</dt>
87-
<dd><p>Extracts the collection identifier of a given collection member key.</p>
88-
<p>For example:</p>
89-
<ul>
90-
<li><code>getCollectionKey(&quot;report_123&quot;)</code> would return &quot;report_&quot;</li>
91-
<li><code>getCollectionKey(&quot;report_&quot;)</code> would return &quot;report_&quot;</li>
92-
<li><code>getCollectionKey(&quot;report_-1_something&quot;)</code> would return &quot;report_&quot;</li>
93-
<li><code>getCollectionKey(&quot;sharedNVP_user_-1_something&quot;)</code> would return &quot;sharedNVP_user_&quot;</li>
94-
</ul>
95-
</dd>
9662
<dt><a href="#tryGetCachedValue">tryGetCachedValue()</a></dt>
9763
<dd><p>Tries to get a value from the cache. If the value is not present in cache it will return the default value or undefined.
9864
If the requested key is a collection, it will return an object with all the collection members.</p>
@@ -223,6 +189,20 @@ Getter - returns the default key states.
223189
Getter - returns the deffered init task.
224190

225191
**Kind**: global function
192+
<a name="afterInit"></a>
193+
194+
## afterInit(action) ⇒
195+
Executes an action after Onyx has been initialized.
196+
If Onyx is already initialized, the action is executed immediately.
197+
Otherwise, it waits for initialization to complete before executing.
198+
199+
**Kind**: global function
200+
**Returns**: The result of the action
201+
202+
| Param | Description |
203+
| --- | --- |
204+
| action | The action to execute after initialization |
205+
226206
<a name="getSkippableCollectionMemberIDs"></a>
227207

228208
## getSkippableCollectionMemberIDs()
@@ -312,93 +292,6 @@ Deletes a subscription ID associated with its corresponding key.
312292
Returns current key names stored in persisted storage
313293

314294
**Kind**: global function
315-
<a name="getCollectionKeys"></a>
316-
317-
## getCollectionKeys()
318-
Returns set of all registered collection keys
319-
320-
**Kind**: global function
321-
<a name="isCollectionKey"></a>
322-
323-
## isCollectionKey()
324-
Checks to see if the subscriber's supplied key
325-
is associated with a collection of keys.
326-
327-
**Kind**: global function
328-
<a name="isCollectionMember"></a>
329-
330-
## isCollectionMember(key) ⇒
331-
Checks if a given key is a collection member key (not just a collection key).
332-
333-
**Kind**: global function
334-
**Returns**: true if the key is a collection member, false otherwise
335-
336-
| Param | Description |
337-
| --- | --- |
338-
| key | The key to check |
339-
340-
<a name="isRamOnlyKey"></a>
341-
342-
## isRamOnlyKey(key) ⇒
343-
Checks if a given key is a RAM-only key, RAM-only collection key, or a RAM-only collection member
344-
345-
For example:
346-
347-
For the following Onyx setup
348-
349-
ramOnlyKeys: ["ramOnlyKey", "ramOnlyCollection_"]
350-
351-
- `isRamOnlyKey("ramOnlyKey")` would return true
352-
- `isRamOnlyKey("ramOnlyCollection_")` would return true
353-
- `isRamOnlyKey("ramOnlyCollection_1")` would return true
354-
- `isRamOnlyKey("someOtherKey")` would return false
355-
356-
**Kind**: global function
357-
**Returns**: true if key is a RAM-only key, RAM-only collection key, or a RAM-only collection member
358-
359-
| Param | Description |
360-
| --- | --- |
361-
| key | The key to check |
362-
363-
<a name="splitCollectionMemberKey"></a>
364-
365-
## splitCollectionMemberKey(key, collectionKey) ⇒
366-
Splits a collection member key into the collection key part and the ID part.
367-
368-
**Kind**: global function
369-
**Returns**: A tuple where the first element is the collection part and the second element is the ID part,
370-
or throws an Error if the key is not a collection one.
371-
372-
| Param | Description |
373-
| --- | --- |
374-
| key | The collection member key to split. |
375-
| collectionKey | The collection key of the `key` param that can be passed in advance to optimize the function. |
376-
377-
<a name="isKeyMatch"></a>
378-
379-
## isKeyMatch()
380-
Checks to see if a provided key is the exact configured key of our connected subscriber
381-
or if the provided key is a collection member key (in case our configured key is a "collection key")
382-
383-
**Kind**: global function
384-
<a name="getCollectionKey"></a>
385-
386-
## getCollectionKey(key) ⇒
387-
Extracts the collection identifier of a given collection member key.
388-
389-
For example:
390-
- `getCollectionKey("report_123")` would return "report_"
391-
- `getCollectionKey("report_")` would return "report_"
392-
- `getCollectionKey("report_-1_something")` would return "report_"
393-
- `getCollectionKey("sharedNVP_user_-1_something")` would return "sharedNVP_user_"
394-
395-
**Kind**: global function
396-
**Returns**: The plain collection key or throws an Error if the key is not a collection one.
397-
398-
| Param | Description |
399-
| --- | --- |
400-
| key | The collection key to process. |
401-
402295
<a name="tryGetCachedValue"></a>
403296

404297
## tryGetCachedValue()

lib/DevTools/RealDevTools.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type {IDevTools, DevtoolsOptions, DevtoolsConnection, ReduxDevtools} from './types';
2-
import OnyxUtils from '../OnyxUtils';
2+
import OnyxKeys from '../OnyxKeys';
33

44
const ERROR_LABEL = 'Onyx DevTools - Error: ';
55

@@ -77,7 +77,7 @@ class RealDevTools implements IDevTools {
7777
clearState(keysToPreserve: string[] = []): void {
7878
const newState = Object.entries(this.state).reduce((obj: Record<string, unknown>, [key, value]) => {
7979
// eslint-disable-next-line no-param-reassign
80-
obj[key] = keysToPreserve.some((preserveKey) => OnyxUtils.isKeyMatch(preserveKey, key)) ? value : this.defaultState[key];
80+
obj[key] = keysToPreserve.some((preserveKey) => OnyxKeys.isKeyMatch(preserveKey, key)) ? value : this.defaultState[key];
8181
return obj;
8282
}, {});
8383

lib/Onyx.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type {
2323
SetOptions,
2424
} from './types';
2525
import OnyxUtils from './OnyxUtils';
26+
import OnyxKeys from './OnyxKeys';
2627
import logMessages from './logMessages';
2728
import type {Connection} from './OnyxConnectionManager';
2829
import connectionManager from './OnyxConnectionManager';
@@ -55,13 +56,13 @@ function init({
5556
OnyxUtils.setSkippableCollectionMemberIDs(new Set(skippableCollectionMemberIDs));
5657
OnyxUtils.setSnapshotMergeKeys(new Set(snapshotMergeKeys));
5758

58-
cache.setRamOnlyKeys(new Set<OnyxKey>(ramOnlyKeys));
59+
OnyxKeys.setRamOnlyKeys(new Set<OnyxKey>(ramOnlyKeys));
5960

6061
if (shouldSyncMultipleInstances) {
6162
Storage.keepInstancesSync?.((key, value) => {
6263
// RAM-only keys should never sync from storage as they may have stale persisted data
6364
// from before the key was migrated to RAM-only.
64-
if (OnyxUtils.isRamOnlyKey(key)) {
65+
if (OnyxKeys.isRamOnlyKey(key)) {
6566
return;
6667
}
6768

@@ -70,7 +71,7 @@ function init({
7071
// Check if this is a collection member key to prevent duplicate callbacks
7172
// When a collection is updated, individual members sync separately to other tabs
7273
// Setting isProcessingCollectionUpdate=true prevents triggering collection callbacks for each individual update
73-
const isKeyCollectionMember = OnyxUtils.isCollectionMember(key);
74+
const isKeyCollectionMember = OnyxKeys.isCollectionMember(key);
7475

7576
OnyxUtils.keyChanged(key, value as OnyxValue<typeof key>, undefined, isKeyCollectionMember);
7677
});
@@ -87,7 +88,7 @@ function init({
8788
// eager cache loading populates the key index (cache.setAllKeys) inside initializeWithDefaultKeyStates,
8889
// and the evictable keys list depends on that index being populated.
8990
OnyxUtils.initializeWithDefaultKeyStates()
90-
.then(() => cache.addEvictableKeysToRecentlyAccessedList(OnyxUtils.isCollectionKey, OnyxUtils.getAllKeys))
91+
.then(() => cache.addEvictableKeysToRecentlyAccessedList(OnyxKeys.isCollectionKey, OnyxUtils.getAllKeys))
9192
.then(OnyxUtils.getDeferredInitTask().resolve);
9293
}
9394

@@ -204,7 +205,7 @@ function merge<TKey extends OnyxKey>(key: TKey, changes: OnyxMergeInput<TKey>):
204205
const skippableCollectionMemberIDs = OnyxUtils.getSkippableCollectionMemberIDs();
205206
if (skippableCollectionMemberIDs.size) {
206207
try {
207-
const [, collectionMemberID] = OnyxUtils.splitCollectionMemberKey(key);
208+
const [, collectionMemberID] = OnyxKeys.splitCollectionMemberKey(key);
208209
if (skippableCollectionMemberIDs.has(collectionMemberID)) {
209210
// The key is a skippable one, so we set the new changes to undefined.
210211
// eslint-disable-next-line no-param-reassign
@@ -348,7 +349,7 @@ function clear(keysToPreserve: OnyxKey[] = []): Promise<void> {
348349
// to null would cause unknown behavior)
349350
// 2.1 However, if a default key was explicitly set to null, we need to reset it to the default value
350351
for (const key of allKeys) {
351-
const isKeyToPreserve = keysToPreserve.some((preserveKey) => OnyxUtils.isKeyMatch(preserveKey, key));
352+
const isKeyToPreserve = keysToPreserve.some((preserveKey) => OnyxKeys.isKeyMatch(preserveKey, key));
352353
const isDefaultKey = key in defaultKeyStates;
353354

354355
// If the key is being removed or reset to default:
@@ -361,7 +362,7 @@ function clear(keysToPreserve: OnyxKey[] = []): Promise<void> {
361362
if (newValue !== oldValue) {
362363
cache.set(key, newValue);
363364

364-
const collectionKey = OnyxUtils.getCollectionKey(key);
365+
const collectionKey = OnyxKeys.getCollectionKey(key);
365366

366367
if (collectionKey) {
367368
if (!keyValuesToResetAsCollection[collectionKey]) {
@@ -396,7 +397,7 @@ function clear(keysToPreserve: OnyxKey[] = []): Promise<void> {
396397
// Exclude RAM-only keys to prevent them from being saved to storage
397398
const defaultKeyValuePairs = Object.entries(
398399
Object.keys(defaultKeyStates)
399-
.filter((key) => !keysToPreserve.some((preserveKey) => OnyxUtils.isKeyMatch(preserveKey, key)) && !OnyxUtils.isRamOnlyKey(key))
400+
.filter((key) => !keysToPreserve.some((preserveKey) => OnyxKeys.isKeyMatch(preserveKey, key)) && !OnyxKeys.isRamOnlyKey(key))
400401
.reduce((obj: KeyValueMapping, key) => {
401402
// eslint-disable-next-line no-param-reassign
402403
obj[key] = defaultKeyStates[key];
@@ -499,8 +500,8 @@ function update<TKey extends OnyxKey>(data: Array<OnyxUpdate<TKey>>): Promise<vo
499500
// Group all the collection-related keys and update each collection in a single `mergeCollection` call.
500501
// This is needed to prevent multiple `mergeCollection` calls for the same collection and `merge` calls for the individual items of the said collection.
501502
// This way, we ensure there is no race condition in the queued updates of the same key.
502-
for (const collectionKey of OnyxUtils.getCollectionKeys()) {
503-
const collectionItemKeys = Object.keys(updateQueue).filter((key) => OnyxUtils.isKeyMatch(collectionKey, key));
503+
for (const collectionKey of OnyxKeys.getCollectionKeys()) {
504+
const collectionItemKeys = Object.keys(updateQueue).filter((key) => OnyxKeys.isKeyMatch(collectionKey, key));
504505
if (collectionItemKeys.length <= 1) {
505506
// If there are no items of this collection in the updateQueue, we should skip it.
506507
// If there is only one item, we should update it individually, therefore retain it in the updateQueue.

0 commit comments

Comments
 (0)