Skip to content

Commit 0e80d49

Browse files
committed
Merge branch 'main' into feature/sqliteprovider-improvements
2 parents 6929683 + 0c9172b commit 0e80d49

19 files changed

Lines changed: 906 additions & 376 deletions

.github/actionlint.yml

Lines changed: 0 additions & 8 deletions
This file was deleted.

API-INTERNAL.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,18 @@ is associated with a collection of keys.</p>
5858
<dt><a href="#isCollectionMember">isCollectionMember(key)</a> ⇒</dt>
5959
<dd><p>Checks if a given key is a collection member key (not just a collection key).</p>
6060
</dd>
61+
<dt><a href="#isRamOnlyKey">isRamOnlyKey(key)</a> ⇒</dt>
62+
<dd><p>Checks if a given key is a RAM-only key, RAM-only collection key, or a RAM-only collection member</p>
63+
<p>For example:</p>
64+
<p>For the following Onyx setup</p>
65+
<p>ramOnlyKeys: [&quot;ramOnlyKey&quot;, &quot;ramOnlyCollection_&quot;]</p>
66+
<ul>
67+
<li><code>isRamOnlyKey(&quot;ramOnlyKey&quot;)</code> would return true</li>
68+
<li><code>isRamOnlyKey(&quot;ramOnlyCollection_&quot;)</code> would return true</li>
69+
<li><code>isRamOnlyKey(&quot;ramOnlyCollection_1&quot;)</code> would return true</li>
70+
<li><code>isRamOnlyKey(&quot;someOtherKey&quot;)</code> would return false</li>
71+
</ul>
72+
</dd>
6173
<dt><a href="#splitCollectionMemberKey">splitCollectionMemberKey(key, collectionKey)</a> ⇒</dt>
6274
<dd><p>Splits a collection member key into the collection key part and the ID part.</p>
6375
</dd>
@@ -307,6 +319,29 @@ Checks if a given key is a collection member key (not just a collection key).
307319
| --- | --- |
308320
| key | The key to check |
309321

322+
<a name="isRamOnlyKey"></a>
323+
324+
## isRamOnlyKey(key) ⇒
325+
Checks if a given key is a RAM-only key, RAM-only collection key, or a RAM-only collection member
326+
327+
For example:
328+
329+
For the following Onyx setup
330+
331+
ramOnlyKeys: ["ramOnlyKey", "ramOnlyCollection_"]
332+
333+
- `isRamOnlyKey("ramOnlyKey")` would return true
334+
- `isRamOnlyKey("ramOnlyCollection_")` would return true
335+
- `isRamOnlyKey("ramOnlyCollection_1")` would return true
336+
- `isRamOnlyKey("someOtherKey")` would return false
337+
338+
**Kind**: global function
339+
**Returns**: true if key is a RAM-only key, RAM-only collection key, or a RAM-only collection member
340+
341+
| Param | Description |
342+
| --- | --- |
343+
| key | The key to check |
344+
310345
<a name="splitCollectionMemberKey"></a>
311346

312347
## splitCollectionMemberKey(key, collectionKey) ⇒

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,25 @@ Onyx.init({
460460
});
461461
```
462462

463+
### Using RAM-only keys
464+
465+
You can choose not to save certain keys on disk and keep them RAM-only, that way their values will reset with each session. You just have to pass an array of `ramOnlyKeys` to the `Onyx.init` method. You can mark entire collections as RAM-only by including the collection key (e.g., `ONYXKEYS.COLLECTION.TEMP_DATA`). This will make all members of that collection RAM-only. Individual collection member keys cannot be selectively marked as RAM-only.
466+
467+
```javascript
468+
import Onyx from 'react-native-onyx';
469+
470+
Onyx.init({
471+
keys: ONYXKEYS,
472+
ramOnlyKeys: [
473+
ONYXKEYS.RAM_ONLY_KEY_1,
474+
ONYXKEYS.RAM_ONLY_KEY_2,
475+
ONYXKEYS.COLLECTION.TEMP_DATA,
476+
],
477+
});
478+
```
479+
480+
> Note: RAM-only keys still consume memory and will remain in cache until explicitly cleared or until Onyx.clear() is called. Use them judiciously for truly ephemeral data.
481+
463482
### Usage
464483

465484
The extension interface is pretty simple, on the left sidebar you can see all the updates made to the local storage, in ascending order, and on the right pane you can see the whole the current state, payload of an action and the diff between the previous state and the current state after the action was triggered.

lib/Onyx.ts

Lines changed: 284 additions & 274 deletions
Large diffs are not rendered by default.

lib/OnyxCache.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ class OnyxCache {
5555
/** Set of collection keys for fast lookup */
5656
private collectionKeys = new Set<OnyxKey>();
5757

58+
/** Set of RAM-only keys for fast lookup */
59+
private ramOnlyKeys = new Set<OnyxKey>();
60+
5861
constructor() {
5962
this.storageKeys = new Set();
6063
this.nullishStorageKeys = new Set();
@@ -94,6 +97,8 @@ class OnyxCache {
9497
'isCollectionKey',
9598
'getCollectionKey',
9699
'getCollectionData',
100+
'setRamOnlyKeys',
101+
'isRamOnlyKey',
97102
);
98103
}
99104

@@ -474,6 +479,20 @@ class OnyxCache {
474479
// Return a shallow copy to ensure React detects changes when items are added/removed
475480
return {...cachedCollection};
476481
}
482+
483+
/**
484+
* Set the RAM-only keys for optimized storage
485+
*/
486+
setRamOnlyKeys(ramOnlyKeys: Set<OnyxKey>): void {
487+
this.ramOnlyKeys = ramOnlyKeys;
488+
}
489+
490+
/**
491+
* Check if a key is a RAM-only key
492+
*/
493+
isRamOnlyKey(key: OnyxKey): boolean {
494+
return this.ramOnlyKeys.has(key);
495+
}
477496
}
478497

479498
const instance = new OnyxCache();

lib/OnyxMerge/index.native.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,11 @@ const applyMerge: ApplyMerge = <TKey extends OnyxKey, TValue extends OnyxInput<T
2828
// This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
2929
const updatePromise = OnyxUtils.broadcastUpdate(key, mergedValue as OnyxValue<TKey>, hasChanged);
3030

31+
const shouldSkipStorageOperations = !hasChanged || OnyxUtils.isRamOnlyKey(key);
32+
3133
// If the value has not changed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
32-
if (!hasChanged) {
34+
// If the key is marked as RAM-only, it should not be saved nor updated in the storage.
35+
if (shouldSkipStorageOperations) {
3336
return Promise.resolve({mergedValue, updatePromise});
3437
}
3538

lib/OnyxMerge/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,11 @@ const applyMerge: ApplyMerge = <TKey extends OnyxKey, TValue extends OnyxInput<T
2020
// This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
2121
const updatePromise = OnyxUtils.broadcastUpdate(key, mergedValue as OnyxValue<TKey>, hasChanged);
2222

23+
const shouldSkipStorageOperations = !hasChanged || OnyxUtils.isRamOnlyKey(key);
24+
2325
// If the value has not changed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
24-
if (!hasChanged) {
26+
// If the key is marked as RAM-only, it should not be saved nor updated in the storage.
27+
if (shouldSkipStorageOperations) {
2528
return Promise.resolve({mergedValue, updatePromise});
2629
}
2730

lib/OnyxUtils.ts

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,21 @@ function getDeferredInitTask(): DeferredTask {
137137
return deferredInitTask;
138138
}
139139

140+
/**
141+
* Executes an action after Onyx has been initialized.
142+
* If Onyx is already initialized, the action is executed immediately.
143+
* Otherwise, it waits for initialization to complete before executing.
144+
*
145+
* @param action The action to execute after initialization
146+
* @returns The result of the action
147+
*/
148+
function afterInit<T>(action: () => Promise<T>): Promise<T> {
149+
if (deferredInitTask.isResolved) {
150+
return action();
151+
}
152+
return deferredInitTask.promise.then(action);
153+
}
154+
140155
/**
141156
* Getter - returns the skippable collection member IDs.
142157
*/
@@ -470,6 +485,35 @@ function isCollectionMember(key: OnyxKey): boolean {
470485
}
471486
}
472487

488+
/**
489+
* Checks if a given key is a RAM-only key, RAM-only collection key, or a RAM-only collection member
490+
*
491+
* For example:
492+
*
493+
* For the following Onyx setup
494+
*
495+
* ramOnlyKeys: ["ramOnlyKey", "ramOnlyCollection_"]
496+
*
497+
* - `isRamOnlyKey("ramOnlyKey")` would return true
498+
* - `isRamOnlyKey("ramOnlyCollection_")` would return true
499+
* - `isRamOnlyKey("ramOnlyCollection_1")` would return true
500+
* - `isRamOnlyKey("someOtherKey")` would return false
501+
*
502+
* @param key - The key to check
503+
* @returns true if key is a RAM-only key, RAM-only collection key, or a RAM-only collection member
504+
*/
505+
function isRamOnlyKey(key: OnyxKey): boolean {
506+
try {
507+
const collectionKey = getCollectionKey(key);
508+
// If collectionKey exists for a given key, check if it's a RAM-only key
509+
return cache.isRamOnlyKey(collectionKey);
510+
} catch {
511+
// If getCollectionKey throws, the key is not a collection member
512+
}
513+
514+
return cache.isRamOnlyKey(key);
515+
}
516+
473517
/**
474518
* Splits a collection member key into the collection key part and the ID part.
475519
* @param key - The collection member key to split.
@@ -869,6 +913,11 @@ function scheduleNotifyCollectionSubscribers<TKey extends OnyxKey>(
869913
function remove<TKey extends OnyxKey>(key: TKey, isProcessingCollectionUpdate?: boolean): Promise<void> {
870914
cache.drop(key);
871915
scheduleSubscriberUpdate(key, undefined as OnyxValue<TKey>, undefined, isProcessingCollectionUpdate);
916+
917+
if (isRamOnlyKey(key)) {
918+
return Promise.resolve();
919+
}
920+
872921
return Storage.removeItem(key).then(() => undefined);
873922
}
874923

@@ -1344,6 +1393,12 @@ function setWithRetry<TKey extends OnyxKey>({key, value, options}: SetParams<TKe
13441393
return updatePromise;
13451394
}
13461395

1396+
// If a key is a RAM-only key or a member of RAM-only collection, we skip the step that modifies the storage
1397+
if (isRamOnlyKey(key)) {
1398+
OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.SET, key, valueWithoutNestedNullValues);
1399+
return updatePromise;
1400+
}
1401+
13471402
return Storage.setItem(key, valueWithoutNestedNullValues)
13481403
.catch((error) => OnyxUtils.retryOperation(error, setWithRetry, {key, value: valueWithoutNestedNullValues, options}, retryAttempt))
13491404
.then(() => {
@@ -1394,7 +1449,13 @@ function multiSetWithRetry(data: OnyxMultiSetInput, retryAttempt?: number): Prom
13941449
return OnyxUtils.scheduleSubscriberUpdate(key, value);
13951450
});
13961451

1397-
return Storage.multiSet(keyValuePairsToSet)
1452+
const keyValuePairsToStore = keyValuePairsToSet.filter((keyValuePair) => {
1453+
const [key] = keyValuePair;
1454+
// Filter out the RAM-only key value pairs, as they should not be saved to storage
1455+
return !isRamOnlyKey(key);
1456+
});
1457+
1458+
return Storage.multiSet(keyValuePairsToStore)
13981459
.catch((error) => OnyxUtils.retryOperation(error, multiSetWithRetry, newData, retryAttempt))
13991460
.then(() => {
14001461
OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.MULTI_SET, undefined, newData);
@@ -1463,6 +1524,12 @@ function setCollectionWithRetry<TKey extends CollectionKeyBase>({collectionKey,
14631524

14641525
const updatePromise = OnyxUtils.scheduleNotifyCollectionSubscribers(collectionKey, mutableCollection, previousCollection);
14651526

1527+
// RAM-only keys are not supposed to be saved to storage
1528+
if (isRamOnlyKey(collectionKey)) {
1529+
OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.SET_COLLECTION, undefined, mutableCollection);
1530+
return updatePromise;
1531+
}
1532+
14661533
return Storage.multiSet(keyValuePairs)
14671534
.catch((error) => OnyxUtils.retryOperation(error, setCollectionWithRetry, {collectionKey, collection}, retryAttempt))
14681535
.then(() => {
@@ -1573,11 +1640,13 @@ function mergeCollectionWithPatches<TKey extends CollectionKeyBase>(
15731640

15741641
// New keys will be added via multiSet while existing keys will be updated using multiMerge
15751642
// This is because setting a key that doesn't exist yet with multiMerge will throw errors
1576-
if (keyValuePairsForExistingCollection.length > 0) {
1643+
// We can skip this step for RAM-only keys as they should never be saved to storage
1644+
if (!isRamOnlyKey(collectionKey) && keyValuePairsForExistingCollection.length > 0) {
15771645
promises.push(Storage.multiMerge(keyValuePairsForExistingCollection));
15781646
}
15791647

1580-
if (keyValuePairsForNewCollection.length > 0) {
1648+
// We can skip this step for RAM-only keys as they should never be saved to storage
1649+
if (!isRamOnlyKey(collectionKey) && keyValuePairsForNewCollection.length > 0) {
15811650
promises.push(Storage.multiSet(keyValuePairsForNewCollection));
15821651
}
15831652

@@ -1656,6 +1725,11 @@ function partialSetCollection<TKey extends CollectionKeyBase>({collectionKey, co
16561725

16571726
const updatePromise = scheduleNotifyCollectionSubscribers(collectionKey, mutableCollection, previousCollection);
16581727

1728+
if (isRamOnlyKey(collectionKey)) {
1729+
sendActionToDevTools(METHOD.SET_COLLECTION, undefined, mutableCollection);
1730+
return updatePromise;
1731+
}
1732+
16591733
return Storage.multiSet(keyValuePairs)
16601734
.catch((error) => retryOperation(error, partialSetCollection, {collectionKey, collection}, retryAttempt))
16611735
.then(() => {
@@ -1690,6 +1764,7 @@ const OnyxUtils = {
16901764
getMergeQueuePromise,
16911765
getDefaultKeyStates,
16921766
getDeferredInitTask,
1767+
afterInit,
16931768
initStoreValues,
16941769
sendActionToDevTools,
16951770
get,
@@ -1741,6 +1816,7 @@ const OnyxUtils = {
17411816
setWithRetry,
17421817
multiSetWithRetry,
17431818
setCollectionWithRetry,
1819+
isRamOnlyKey,
17441820
};
17451821

17461822
GlobalSettings.addGlobalSettingsChangeListener(({enablePerformanceMetrics}) => {

lib/createDeferredTask.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
type DeferredTask = {
22
promise: Promise<void>;
3-
resolve?: () => void;
3+
resolve: () => void;
4+
isResolved: boolean;
45
};
56

67
/**
@@ -9,11 +10,16 @@ type DeferredTask = {
910
* Useful when we want to wait for a tasks that is resolved from an external action
1011
*/
1112
export default function createDeferredTask(): DeferredTask {
12-
const deferred = {} as DeferredTask;
13+
const {promise, resolve: originalResolve} = Promise.withResolvers<void>();
1314

14-
deferred.promise = new Promise((res) => {
15-
deferred.resolve = res;
16-
});
15+
const deferred: DeferredTask = {
16+
promise,
17+
resolve: () => {
18+
deferred.isResolved = true;
19+
originalResolve();
20+
},
21+
isResolved: false,
22+
};
1723

1824
return deferred;
1925
}

lib/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ type ExpandOnyxKeys<TKey extends OnyxKey> = TKey extends CollectionKeyBase ? NoI
334334
* If a new method is added to OnyxUtils.METHOD constant, it must be added to OnyxMethodValueMap type.
335335
* Otherwise it will show static type errors.
336336
*/
337-
type OnyxUpdate<TKey extends OnyxKey = OnyxKey> = {
337+
type OnyxUpdate<TKey extends OnyxKey> = {
338338
// ⚠️ DO NOT CHANGE THIS TYPE, UNLESS YOU KNOW WHAT YOU ARE DOING. ⚠️
339339
[K in TKey]:
340340
| {onyxMethod: typeof OnyxUtils.METHOD.SET; key: ExpandOnyxKeys<K>; value: OnyxSetInput<K>}
@@ -426,6 +426,11 @@ type InitOptions = {
426426
*/
427427
skippableCollectionMemberIDs?: string[];
428428

429+
/**
430+
* Array of keys that when provided to Onyx are flagged as RAM-only keys, and thus are not saved to disk.
431+
*/
432+
ramOnlyKeys?: OnyxKey[];
433+
429434
/**
430435
* A list of field names that should always be merged into snapshot entries even if those fields are
431436
* missing in the snapshot. Snapshots are saved "views" of a key's data used to populate read-only

0 commit comments

Comments
 (0)