Skip to content
59 changes: 59 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@ Any existing collection members not included in the new data will be removed.</p
Live RAM-only values and writes that have not reached storage are not included.
Treat the returned object and its nested values as read-only.</p>
</dd>
<dt><a href="#get">get(key)</a> ⇒</dt>
<dd><p>Reads the current value of an Onyx key once, without subscribing. Use <code>useOnyx()</code> or
<code>Onyx.connectWithoutView()</code> when the value has to stay current.</p>
<p>The read is served from the cache, which <code>init()</code> fills with the whole store, so it returns what a
subscription to the same key would deliver. The value is the cached one rather than a copy, so treat
it as read-only.</p>
</dd>
<dt><a href="#multiGet">multiGet(keys)</a> ⇒</dt>
<dd><p>Reads several Onyx keys at once, without subscribing. Use <code>useOnyx()</code> or <code>Onyx.connectWithoutView()</code> when
the values have to stay current.</p>
<p>Values come back in the order of the keys given, and each is what get() returns for its key. To read a
whole collection, pass the collection key rather than listing its members.</p>
</dd>
</dl>

<a name="init"></a>
Expand Down Expand Up @@ -280,3 +293,49 @@ Live RAM-only values and writes that have not reached storage are not included.
Treat the returned object and its nested values as read-only.

**Kind**: global function
<a name="get"></a>

## get(key) ⇒
Reads the current value of an Onyx key once, without subscribing. Use `useOnyx()` or
`Onyx.connectWithoutView()` when the value has to stay current.

The read is served from the cache, which `init()` fills with the whole store, so it returns what a
subscription to the same key would deliver. The value is the cached one rather than a copy, so treat
it as read-only.

**Kind**: global function
**Returns**: The current value, or `undefined` if the key has none.

| Param | Description |
| --- | --- |
| key | ONYXKEY to read, either a collection key or a single key |

**Example**
```js
const report = await Onyx.get(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
const allReports = await Onyx.get(ONYXKEYS.COLLECTION.REPORT);
```
<a name="multiGet"></a>

## multiGet(keys) ⇒
Reads several Onyx keys at once, without subscribing. Use `useOnyx()` or `Onyx.connectWithoutView()` when
the values have to stay current.

Values come back in the order of the keys given, and each is what get() returns for its key. To read a
whole collection, pass the collection key rather than listing its members.

**Kind**: global function
**Returns**: The values in the order of their keys, each `undefined` where a key has no value.

| Param | Description |
| --- | --- |
| keys | ONYXKEYS to read, in any mix of collection keys and single keys |

**Example**
```js
const [session, wallet, report] = await Onyx.multiGet([
ONYXKEYS.SESSION,
ONYXKEYS.WALLET,
`${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
]);
```
45 changes: 45 additions & 0 deletions lib/Onyx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,8 @@ function clear(keysToPreserve: OnyxKey[] = []): Promise<void> {
.then(() => {
DevTools.clearState(keysToPreserve);

onyxSubscriptionManager.resetCacheMissReports();

// Notify the subscribers for each key/value group so they can receive the new values
for (const [key, value] of Object.entries(keyValuesToResetIndividually)) {
OnyxUtils.notifyKey(key, value);
Expand Down Expand Up @@ -754,6 +756,47 @@ function exportState(): Promise<Record<OnyxKey, OnyxValue<OnyxKey>>> {
);
}

/**
* Reads the current value of an Onyx key once, without subscribing. Use `useOnyx()` or
* `Onyx.connectWithoutView()` when the value has to stay current.
*
* The read is served from the cache, which `init()` fills with the whole store, so it returns what a
* subscription to the same key would deliver. The value is the cached one rather than a copy, so treat
* it as read-only.
*
* @example
* const report = await Onyx.get(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
* const allReports = await Onyx.get(ONYXKEYS.COLLECTION.REPORT);
*
* @param key ONYXKEY to read, either a collection key or a single key
* @returns The current value, or `undefined` if the key has none.
*/
Comment thread
LukasMod marked this conversation as resolved.
function get<TKey extends OnyxKey>(key: TKey): Promise<OnyxValue<TKey>> {
return OnyxUtils.afterInit(() => Promise.resolve((onyxSubscriptionManager.getState(key) ?? undefined) as OnyxValue<TKey>));
}

/**
* Reads several Onyx keys at once, without subscribing. Use `useOnyx()` or `Onyx.connectWithoutView()` when
* the values have to stay current.
*
* Values come back in the order of the keys given, and each is what get() returns for its key. To read a
* whole collection, pass the collection key rather than listing its members.
*
* @example
* const [session, wallet, report] = await Onyx.multiGet([
* ONYXKEYS.SESSION,
* ONYXKEYS.WALLET,
* `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
* ]);
*
* @param keys ONYXKEYS to read, in any mix of collection keys and single keys
* @returns The values in the order of their keys, each `undefined` where a key has no value.
*/
function multiGet<const Keys extends readonly OnyxKey[]>(keys: Keys): Promise<{[Index in keyof Keys]: OnyxValue<Keys[Index]>}> {
// map() widens the key tuple to an array, so the per-slot value types survive only through the cast.
return OnyxUtils.afterInit(() => Promise.resolve(keys.map((key) => onyxSubscriptionManager.getState(key) ?? undefined) as {[Index in keyof Keys]: OnyxValue<Keys[Index]>}));
}

const Onyx = {
METHOD: OnyxUtils.METHOD,
connect,
Expand All @@ -768,6 +811,8 @@ const Onyx = {
clear,
exportState,
init,
get,
multiGet,
registerLogger: Logger.registerLogger,
};

Expand Down
40 changes: 39 additions & 1 deletion lib/OnyxSubscriptionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {CollectionKeyBase, KeyValueMapping, OnyxCollection, OnyxKey, OnyxVa
import * as Logger from './Logger';
import cache from './OnyxCache';
import OnyxKeys from './OnyxKeys';
import Storage from './storage';

/**
* Listener fired when an exact key's value changes.
Expand All @@ -15,6 +16,32 @@ type Listener<TKey extends OnyxKey = OnyxKey> = (value: OnyxValue<TKey>, key: TK
*/
type GenericListener = (value: unknown, key: OnyxKey) => void;

const reportedCacheMisses = new Set<OnyxKey>();

function reportCacheMissIfStorageHasValue(key: OnyxKey): void {
if (OnyxKeys.isRamOnlyKey(key) || !cache.getAllKeys().has(key) || cache.hasNullishStorageKey(key)) {
return;
}

const reportKey = OnyxKeys.getCollectionKey(key) ?? key;
if (reportedCacheMisses.has(reportKey)) {
return;
}
reportedCacheMisses.add(reportKey);

Storage.getItem(key)
.then((value) => {
if (value === undefined || value === null) {
return;
}

Logger.logHmmm(`Cache held no value for a key storage still has: ${reportKey}`);
})
.catch((error: unknown) => {
Logger.logInfo(`Could not check storage for a suspected cache miss on ${reportKey}. Error: ${String(error)}`);
});
}

/**
* OnyxSubscriptionManager is a registry for Onyx subscriptions.
* Subscriptions are stored in `keyListeners`, a flat map keyed by OnyxKey.
Expand All @@ -37,7 +64,13 @@ class OnyxSubscriptionManager {
if (OnyxKeys.isCollectionKey(key)) {
return cache.getCollectionData(key) as OnyxValue<TKey>;
}
return cache.get(key) as OnyxValue<TKey>;

const value = cache.get(key) as OnyxValue<TKey>;
if (value === undefined) {
reportCacheMissIfStorageHasValue(key);
}

return value;
}

/**
Expand Down Expand Up @@ -160,6 +193,11 @@ class OnyxSubscriptionManager {
*/
clearAll(): void {
this.keyListeners.clear();
this.resetCacheMissReports();
}

resetCacheMissReports(): void {
reportedCacheMisses.clear();
}

/**
Expand Down
24 changes: 24 additions & 0 deletions tests/perf-test/Onyx.perf-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,30 @@ describe('Onyx', () => {
});
});

describe('get', () => {
const COLLECTION_READS = 1000;

test('1k calls for the whole collection of 10k heavy objects', async () => {
await measureAsyncFunction(() => Promise.all(Array.from({length: COLLECTION_READS}, () => Onyx.get(collectionKey))), {
beforeEach: async () => {
await Onyx.multiSet(mockedReportActionsMap);
},
afterEach: clearOnyxAfterEachMeasure,
});
});

test('10k calls for individual collection members', async () => {
const memberKeys = Object.keys(mockedReportActionsMap);

await measureAsyncFunction(() => Promise.all(memberKeys.map((memberKey) => Onyx.get(memberKey))), {
beforeEach: async () => {
await Onyx.multiSet(mockedReportActionsMap);
},
afterEach: clearOnyxAfterEachMeasure,
});
});
});

describe('update', () => {
test('one call with 5k sets and 5k merges updates', async () => {
const changedReportActions = Object.fromEntries(Object.entries(mockedReportActionsMap).map(([k, v]) => [k, createRandomReportAction(Number(v.reportActionID))] as const));
Expand Down
56 changes: 56 additions & 0 deletions tests/types/get.ts
Comment thread
LukasMod marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import Onyx from '../../dist/Onyx';
import ONYX_KEYS from './setup';

async function readCollectionMember() {
const member = await Onyx.get(`${ONYX_KEYS.COLLECTION.TEST_KEY}1`);

if (!member) {
return;
}

// Annotation pins the draft to the read type because a spread alone drops readonly modifiers.
const draft: typeof member = {...member, list: member.list ? [...member.list] : undefined};
draft.str = 'mutated';
draft.list?.push('appended');

return draft;
}

async function readCollection() {
const collection = await Onyx.get(ONYX_KEYS.COLLECTION.TEST_KEY);

if (!collection) {
return;
}

const draft: typeof collection = {...collection};
draft.test_1 = {str: 'mutated'};

return draft;
}

async function writeBackWhatWasRead() {
const value = await Onyx.get(ONYX_KEYS.TEST_KEY);
await Onyx.set(ONYX_KEYS.TEST_KEY, value ?? null);
await Onyx.merge(ONYX_KEYS.TEST_KEY, value ?? null);

const member = await Onyx.get(`${ONYX_KEYS.COLLECTION.TEST_KEY}1`);

if (!member) {
return;
}

await Onyx.set(`${ONYX_KEYS.COLLECTION.TEST_KEY}1`, member);
await Onyx.merge(`${ONYX_KEYS.COLLECTION.TEST_KEY}1`, member);
}

async function writeBackWholeCollectionNeedsDefinedMembers() {
const collection = await Onyx.get(ONYX_KEYS.COLLECTION.TEST_KEY);

if (!collection) {
return;
}

// @ts-expect-error undefined members are not valid write input
await Onyx.setCollection(ONYX_KEYS.COLLECTION.TEST_KEY, collection);
}
46 changes: 46 additions & 0 deletions tests/types/multiGet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import Onyx from '../../dist/Onyx';
import ONYX_KEYS from './setup';

async function readsArePositional() {
const [single, collection] = await Onyx.multiGet([ONYX_KEYS.TEST_KEY, ONYX_KEYS.COLLECTION.TEST_KEY]);

if (!collection) {
return single;
}

collection.test_1 = {str: 'mutated'};

return single;
}

async function mixedKeysKeepTheirOwnTypes() {
const [single, member] = await Onyx.multiGet([ONYX_KEYS.TEST_KEY, `${ONYX_KEYS.COLLECTION.TEST_KEY}1`]);

if (!member) {
return single;
}

member.str = 'mutated';
member.list?.push('appended');

return [single, member] as const;
}

async function singleKeyListResolvesToTuple() {
const [single] = await Onyx.multiGet([ONYX_KEYS.TEST_KEY]);

return single;
}

async function writeBackWhatWasRead() {
const [single, member] = await Onyx.multiGet([ONYX_KEYS.TEST_KEY, `${ONYX_KEYS.COLLECTION.TEST_KEY}1`]);

await Onyx.set(ONYX_KEYS.TEST_KEY, single ?? null);

if (!member) {
return;
}

await Onyx.set(`${ONYX_KEYS.COLLECTION.TEST_KEY}1`, member);
await Onyx.merge(`${ONYX_KEYS.COLLECTION.TEST_KEY}1`, member);
}
4 changes: 3 additions & 1 deletion tests/types/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ type OnyxValues = {
[ONYX_KEYS.TEST_KEY]: string;
};

// The optional list member lets get.ts check that a value read back is mutable, because only a mutable
// array has push.
type OnyxCollectionValues = {
[ONYX_KEYS.COLLECTION.TEST_KEY]: {str: string};
[ONYX_KEYS.COLLECTION.TEST_KEY]: {str: string; list?: string[]};
};

declare module '../../dist/types' {
Expand Down
Loading
Loading