diff --git a/API.md b/API.md
index e766e551f..3fc28c26c 100644
--- a/API.md
+++ b/API.md
@@ -58,6 +58,11 @@ value will be saved to storage after the default value.
Sets a collection by replacing all existing collection members with new values.
Any existing collection members not included in the new data will be removed.
+get()
+Reads a value out of the cache synchronously. A collection key reads every member.
+Not a subscription, so the value never updates: use useOnyx for anything rendered. Returns undefined for a
+key with no value, and for every key until init() has hydrated the cache.
+
@@ -257,3 +262,12 @@ Onyx.setCollection(ONYXKEYS.COLLECTION.REPORT, {
[`${ONYXKEYS.COLLECTION.REPORT}2`]: report2,
});
```
+
+
+## get()
+Reads a value out of the cache synchronously. A collection key reads every member.
+
+Not a subscription, so the value never updates: use `useOnyx` for anything rendered. Returns `undefined` for a
+key with no value, and for every key until `init()` has hydrated the cache.
+
+**Kind**: global function
diff --git a/lib/Onyx.ts b/lib/Onyx.ts
index 0a0d8abc9..60606ad6f 100644
--- a/lib/Onyx.ts
+++ b/lib/Onyx.ts
@@ -255,16 +255,17 @@ function merge(key: TKey, changes: OnyxMergeInput):
}
mergeQueue[key] = [changes];
- mergeQueuePromise[key] = OnyxUtils.get(key).then((valueFromGet) => {
+ mergeQueuePromise[key] = Promise.resolve().then(() => {
// Calls to Onyx.set after a merge will terminate the current merge process and clear the merge queue
if (mergeQueue[key] == null) {
return Promise.resolve();
}
// Other writers (notably Onyx.update's mergeCollection path, which doesn't participate in mergeQueue)
- // can land between get() resolving and this callback running. Applying the delta on top of the value
- // captured back then and broadcasting it would overwrite those writes wholesale, so re-read the cache.
- const existingValue = cache.hasCacheForKey(key) ? (cache.get(key) as OnyxInput | undefined) : valueFromGet;
+ // can land between this merge being queued and this callback running. Applying the delta on top of a
+ // value captured back then and broadcasting it would overwrite those writes wholesale, so read the
+ // existing value here, at merge application time.
+ const existingValue = OnyxUtils.get(key);
try {
const validChanges = mergeQueue[key].filter((change) => {
@@ -353,92 +354,98 @@ function clear(keysToPreserve: OnyxKey[] = []): Promise {
const defaultKeyStates = OnyxUtils.getDefaultKeyStates();
const initialKeys = Object.keys(defaultKeyStates);
- const promise = OnyxUtils.getAllKeys()
- .then((cachedKeys) => {
- cache.clearNullishStorageKeys();
-
- const keysToBeClearedFromStorage: OnyxKey[] = [];
- const keyValuesToResetIndividually: KeyValueMapping = {};
- // We need to store old and new values for collection keys to properly notify subscribers when clearing Onyx
- // because the notification process needs the old values in cache but at that point they will be already removed from it.
- const keyValuesToResetAsCollection: Record<
- OnyxKey,
- {oldValues: Record; newValues: Record}
- > = {};
-
- const allKeys = new Set([...cachedKeys, ...initialKeys]);
-
- // The only keys that should not be cleared are:
- // 1. Anything specifically passed in keysToPreserve (because some keys like language preferences, offline
- // status, or activeClients need to remain in Onyx even when signed out)
- // 2. Any keys with a default state (because they need to remain in Onyx as their default, and setting them
- // to null would cause unknown behavior)
- // 2.1 However, if a default key was explicitly set to null, we need to reset it to the default value
- for (const key of allKeys) {
- const isKeyToPreserve = keysToPreserve.some((preserveKey) => OnyxKeys.isKeyMatch(preserveKey, key));
- const isDefaultKey = key in defaultKeyStates;
-
- // If the key is being removed or reset to default:
- // 1. Update it in the cache
- // 2. Figure out whether it is a collection key or not,
- // since collection key subscribers need to be updated differently
- if (!isKeyToPreserve) {
- const oldValue = cache.get(key);
- const newValue = defaultKeyStates[key] ?? null;
- if (newValue !== oldValue) {
- cache.set(key, newValue);
-
- const collectionKey = OnyxKeys.getCollectionKey(key);
-
- if (collectionKey) {
- if (!keyValuesToResetAsCollection[collectionKey]) {
- keyValuesToResetAsCollection[collectionKey] = {oldValues: {}, newValues: {}};
- }
- keyValuesToResetAsCollection[collectionKey].oldValues[key] = oldValue;
- keyValuesToResetAsCollection[collectionKey].newValues[key] = newValue ?? undefined;
- } else {
- keyValuesToResetIndividually[key] = newValue ?? undefined;
- }
- }
- }
+ const cachedKeys = OnyxUtils.getAllKeys();
+ cache.clearNullishStorageKeys();
- if (isKeyToPreserve || isDefaultKey) {
- continue;
- }
+ // Clear pending merge queues so that any in-flight Onyx.merge() calls
+ // don't overwrite the default values we're about to set.
+ const mergeQueue = OnyxUtils.getMergeQueue();
+ const mergeQueuePromise = OnyxUtils.getMergeQueuePromise();
+ for (const key of Object.keys(mergeQueue)) {
+ delete mergeQueue[key];
+ delete mergeQueuePromise[key];
+ }
- // If it isn't preserved and doesn't have a default, we'll remove it
- keysToBeClearedFromStorage.push(key);
+ const keysToBeClearedFromStorage: OnyxKey[] = [];
+ const keyValuesToResetIndividually: KeyValueMapping = {};
+ // We need to store old and new values for collection keys to properly notify subscribers when clearing Onyx
+ // because the notification process needs the old values in cache but at that point they will be already removed from it.
+ const keyValuesToResetAsCollection: Record<
+ OnyxKey,
+ {oldValues: Record; newValues: Record}
+ > = {};
+
+ const allKeys = new Set([...cachedKeys, ...initialKeys]);
+
+ // The only keys that should not be cleared are:
+ // 1. Anything specifically passed in keysToPreserve (because some keys like language preferences, offline
+ // status, or activeClients need to remain in Onyx even when signed out)
+ // 2. Any keys with a default state (because they need to remain in Onyx as their default, and setting them
+ // to null would cause unknown behavior)
+ // 2.1 However, if a default key was explicitly set to null, we need to reset it to the default value
+ for (const key of allKeys) {
+ const isKeyToPreserve = keysToPreserve.some((preserveKey) => OnyxKeys.isKeyMatch(preserveKey, key));
+ const isDefaultKey = key in defaultKeyStates;
+
+ // If the key is being removed or reset to default:
+ // 1. Update it in the cache
+ // 2. Figure out whether it is a collection key or not,
+ // since collection key subscribers need to be updated differently
+ if (!isKeyToPreserve) {
+ const oldValue = cache.get(key);
+ const newValue = defaultKeyStates[key] ?? null;
+ if (newValue !== oldValue) {
+ cache.set(key, newValue);
+
+ const collectionKey = OnyxKeys.getCollectionKey(key);
+
+ if (collectionKey) {
+ if (!keyValuesToResetAsCollection[collectionKey]) {
+ keyValuesToResetAsCollection[collectionKey] = {oldValues: {}, newValues: {}};
+ }
+ keyValuesToResetAsCollection[collectionKey].oldValues[key] = oldValue;
+ keyValuesToResetAsCollection[collectionKey].newValues[key] = newValue ?? undefined;
+ } else {
+ keyValuesToResetIndividually[key] = newValue ?? undefined;
+ }
}
+ }
- // Exclude RAM-only keys to prevent them from being saved to storage
- const defaultKeyValuePairs = Object.entries(
- Object.keys(defaultKeyStates)
- .filter((key) => !keysToPreserve.some((preserveKey) => OnyxKeys.isKeyMatch(preserveKey, key)) && !OnyxKeys.isRamOnlyKey(key))
- .reduce((obj: KeyValueMapping, key) => {
- // eslint-disable-next-line no-param-reassign
- obj[key] = defaultKeyStates[key];
- return obj;
- }, {}),
- );
+ if (isKeyToPreserve || isDefaultKey) {
+ continue;
+ }
- // Remove only the items that we want cleared from storage, and reset others to default
- for (const key of keysToBeClearedFromStorage) cache.drop(key);
- return Storage.removeItems(keysToBeClearedFromStorage)
- .then(() => connectionManager.refreshSessionID())
- .then(() => Storage.multiSet(defaultKeyValuePairs))
- .then(() => {
- DevTools.clearState(keysToPreserve);
-
- // Notify the subscribers for each key/value group so they can receive the new values
- for (const [key, value] of Object.entries(keyValuesToResetIndividually)) {
- OnyxUtils.keyChanged(key, value);
- }
- for (const [key, value] of Object.entries(keyValuesToResetAsCollection)) {
- OnyxUtils.keysChanged(key, value.newValues, value.oldValues);
- }
- });
- })
- .then(() => undefined);
+ // If it isn't preserved and doesn't have a default, we'll remove it
+ keysToBeClearedFromStorage.push(key);
+ }
+
+ // Exclude RAM-only keys to prevent them from being saved to storage
+ const defaultKeyValuePairs = Object.entries(
+ Object.keys(defaultKeyStates)
+ .filter((key) => !keysToPreserve.some((preserveKey) => OnyxKeys.isKeyMatch(preserveKey, key)) && !OnyxKeys.isRamOnlyKey(key))
+ .reduce((obj: KeyValueMapping, key) => {
+ // eslint-disable-next-line no-param-reassign
+ obj[key] = defaultKeyStates[key];
+ return obj;
+ }, {}),
+ );
+
+ // Remove only the items that we want cleared from storage, and reset others to default
+ for (const key of keysToBeClearedFromStorage) cache.drop(key);
+ const promise = Storage.removeItems(keysToBeClearedFromStorage)
+ .then(() => connectionManager.refreshSessionID())
+ .then(() => Storage.multiSet(defaultKeyValuePairs))
+ .then(() => {
+ DevTools.clearState(keysToPreserve);
+
+ // Notify the subscribers for each key/value group so they can receive the new values
+ for (const [key, value] of Object.entries(keyValuesToResetIndividually)) {
+ OnyxUtils.keyChanged(key, value);
+ }
+ for (const [key, value] of Object.entries(keyValuesToResetAsCollection)) {
+ OnyxUtils.keysChanged(key, value.newValues, value.oldValues);
+ }
+ });
return cache.captureTask(TASK.CLEAR, promise) as Promise;
});
@@ -559,6 +566,11 @@ function update(data: Array>): Promise OnyxUtils.partialSetCollection({collectionKey, collection: batchedCollectionUpdates.set as OnyxSetCollectionInput}));
+ }
if (!utils.isEmptyObject(batchedCollectionUpdates.merge)) {
promises.push(() =>
OnyxUtils.mergeCollectionWithPatches({
@@ -568,9 +580,6 @@ function update(data: Array>): Promise OnyxUtils.partialSetCollection({collectionKey, collection: batchedCollectionUpdates.set as OnyxSetCollectionInput}));
- }
}
for (const [key, operations] of Object.entries(updateQueue)) {
@@ -611,11 +620,22 @@ function setCollection(collectionKey: TKey, coll
return OnyxUtils.afterInit(() => OnyxUtils.setCollectionWithRetry({collectionKey, collection}));
}
+/**
+ * Reads a value out of the cache synchronously. A collection key reads every member.
+ *
+ * Not a subscription, so the value never updates: use `useOnyx` for anything rendered. Returns `undefined` for a
+ * key with no value, and for every key until `init()` has hydrated the cache.
+ */
+function get(key: TKey): OnyxValue {
+ return OnyxUtils.tryGetCachedValue(key) as OnyxValue;
+}
+
const Onyx = {
METHOD: OnyxUtils.METHOD,
connect,
connectWithoutView,
disconnect,
+ get,
set,
multiSet,
merge,
diff --git a/lib/OnyxCache.ts b/lib/OnyxCache.ts
index e105a0ab6..012b0dd8c 100644
--- a/lib/OnyxCache.ts
+++ b/lib/OnyxCache.ts
@@ -346,18 +346,17 @@ class OnyxCache {
* @param isCollectionKeyFn - Function to determine if a key is a collection key
* @param getAllKeysFn - Function to get all keys, defaults to Storage.getAllKeys
*/
- addEvictableKeysToRecentlyAccessedList(isCollectionKeyFn: (key: OnyxKey) => boolean, getAllKeysFn: () => Promise>): Promise {
- return getAllKeysFn().then((keys: Set) => {
- for (const evictableKey of this.evictionAllowList) {
- for (const key of keys) {
- if (!OnyxKeys.isKeyMatch(evictableKey, key)) {
- continue;
- }
-
- this.addLastAccessedKey(key, isCollectionKeyFn(key));
+ addEvictableKeysToRecentlyAccessedList(isCollectionKeyFn: (key: OnyxKey) => boolean, getAllKeysFn: () => Set): void {
+ const keys = getAllKeysFn();
+ for (const evictableKey of this.evictionAllowList) {
+ for (const key of keys) {
+ if (!OnyxKeys.isKeyMatch(evictableKey, key)) {
+ continue;
}
+
+ this.addLastAccessedKey(key, isCollectionKeyFn(key));
}
- });
+ }
}
/**
diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts
index 400cfbb2b..a2c790bfd 100644
--- a/lib/OnyxUtils.ts
+++ b/lib/OnyxUtils.ts
@@ -4,7 +4,7 @@ import _ from 'underscore';
import DevTools from './DevTools';
import * as Logger from './Logger';
import type Onyx from './Onyx';
-import cache, {TASK} from './OnyxCache';
+import cache from './OnyxCache';
import OnyxKeys from './OnyxKeys';
import StorageCircuitBreaker from './StorageCircuitBreaker';
import Storage from './storage';
@@ -255,163 +255,23 @@ function reduceCollectionWithSelector(
}
/** Get some data from the store */
-function get>(key: TKey): Promise {
- // When we already have the value in cache - resolve right away
- if (cache.hasCacheForKey(key)) {
- return Promise.resolve(cache.get(key) as TValue);
- }
-
- // RAM-only keys should never read from storage (they may have stale persisted data
- // from before the key was migrated to RAM-only). Mark as nullish so future get() calls
- // short-circuit via hasCacheForKey and avoid re-running this branch.
- if (OnyxKeys.isRamOnlyKey(key)) {
- cache.addNullishStorageKey(key);
- return Promise.resolve(undefined as TValue);
- }
-
- const taskName = `${TASK.GET}:${key}` as const;
-
- // When a value retrieving task for this key is still running hook to it
- if (cache.hasPendingTask(taskName)) {
- return cache.getTaskPromise(taskName) as Promise;
- }
-
- // Otherwise retrieve the value from storage and capture a promise to aid concurrent usages
- const promise = Storage.getItem(key)
- .then((val) => {
- if (skippableCollectionMemberIDs.size) {
- try {
- const [, collectionMemberID] = OnyxKeys.splitCollectionMemberKey(key);
- if (skippableCollectionMemberIDs.has(collectionMemberID)) {
- // The key is a skippable one, so we set the value to undefined.
- // eslint-disable-next-line no-param-reassign
- val = undefined as OnyxValue;
- }
- } catch (e) {
- // The key is not a collection one or something went wrong during split, so we proceed with the function's logic.
- }
- }
-
- // Prefer cache over stale storage if a concurrent write populated it during the read.
- const cachedValue = cache.get(key) as TValue;
- if (cachedValue !== undefined) {
- return cachedValue;
- }
-
- if (val === undefined) {
- cache.addNullishStorageKey(key);
- return undefined;
- }
-
- cache.set(key, val);
- return val;
- })
- .catch((err) => Logger.logInfo(`Unable to get item from persistent storage. Key: ${key} Error: ${err}`));
-
- return cache.captureTask(taskName, promise) as Promise;
+function get>(key: TKey): TValue {
+ return cache.get(key) as TValue;
}
-// multiGet the data first from the cache and then from the storage for the missing keys.
-function multiGet(keys: CollectionKeyBase[]): Promise