Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 97 additions & 85 deletions lib/Onyx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,12 +220,16 @@ function merge<TKey extends OnyxKey>(key: TKey, changes: OnyxMergeInput<TKey>):
}
mergeQueue[key] = [changes];

mergeQueuePromise[key] = OnyxUtils.get(key).then((existingValue) => {
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();
}

// Read the existing value at merge application time (not at queue time) so that
// any intervening synchronous cache updates (e.g. from mergeCollection) are picked up.
const existingValue = OnyxUtils.get(key);

try {
const validChanges = mergeQueue[key].filter((change) => {
const {isCompatible, existingValueType, newValueType, isEmptyArrayCoercion} = utils.checkCompatibilityWithExistingValue(change, existingValue);
Expand Down Expand Up @@ -313,92 +317,98 @@ function clear(keysToPreserve: OnyxKey[] = []): Promise<void> {
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<string, KeyValueMapping[OnyxKey] | undefined>; newValues: Record<string, KeyValueMapping[OnyxKey] | undefined>}
> = {};

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();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Load persisted key index before clearing storage

clear() now derives allKeys from OnyxUtils.getAllKeys(), which is cache-backed only. If the in-memory key index is incomplete (for example after the initializeWithDefaultKeyStates() fallback path when Storage.getAll() fails, or after out-of-band storage writes), those persisted keys never enter keysToBeClearedFromStorage, so Storage.removeItems(...) leaves stale data behind after a clear/sign-out. The previous implementation could repopulate keys from storage on demand, so this change introduces a data-retention regression.

Useful? React with 👍 / 👎.

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<string, KeyValueMapping[OnyxKey] | undefined>; newValues: Record<string, KeyValueMapping[OnyxKey] | undefined>}
> = {};

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<void>;
});
Expand Down Expand Up @@ -519,6 +529,11 @@ function update<TKey extends OnyxKey>(data: Array<OnyxUpdate<TKey>>): Promise<vo
},
);

// Set operations must run before merge operations so their cache writes are
// visible when mergeCollectionWithPatches reads previous values synchronously.
if (!utils.isEmptyObject(batchedCollectionUpdates.set)) {
promises.push(() => OnyxUtils.partialSetCollection({collectionKey, collection: batchedCollectionUpdates.set as OnyxSetCollectionInput<OnyxKey>}));
}
if (!utils.isEmptyObject(batchedCollectionUpdates.merge)) {
promises.push(() =>
OnyxUtils.mergeCollectionWithPatches({
Expand All @@ -529,9 +544,6 @@ function update<TKey extends OnyxKey>(data: Array<OnyxUpdate<TKey>>): Promise<vo
}),
);
}
if (!utils.isEmptyObject(batchedCollectionUpdates.set)) {
promises.push(() => OnyxUtils.partialSetCollection({collectionKey, collection: batchedCollectionUpdates.set as OnyxSetCollectionInput<OnyxKey>}));
}
}

for (const [key, operations] of Object.entries(updateQueue)) {
Expand Down
19 changes: 9 additions & 10 deletions lib/OnyxCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Set<OnyxKey>>): Promise<void> {
return getAllKeysFn().then((keys: Set<OnyxKey>) => {
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<OnyxKey>): 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));
}
});
}
}

/**
Expand Down
Loading
Loading