Skip to content

Commit eef42ad

Browse files
committed
Merge branch 'main' into feature/structural-sharing-cache-pr-1
2 parents ccccbf1 + b351fa5 commit eef42ad

25 files changed

Lines changed: 639 additions & 176 deletions

lib/DevTools/RealDevTools.ts

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

34
const ERROR_LABEL = 'Onyx DevTools - Error: ';
45

@@ -76,7 +77,7 @@ class RealDevTools implements IDevTools {
7677
clearState(keysToPreserve: string[] = []): void {
7778
const newState = Object.entries(this.state).reduce((obj: Record<string, unknown>, [key, value]) => {
7879
// eslint-disable-next-line no-param-reassign
79-
obj[key] = keysToPreserve.includes(key) ? value : this.defaultState[key];
80+
obj[key] = keysToPreserve.some((preserveKey) => OnyxKeys.isKeyMatch(preserveKey, key)) ? value : this.defaultState[key];
8081
return obj;
8182
}, {});
8283

lib/Onyx.ts

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,13 @@ function init({
8383

8484
OnyxUtils.initStoreValues(keys, initialKeyStates, evictableKeys);
8585

86-
// Initialize all of our keys with data provided then give green light to any pending connections
87-
Promise.all([cache.addEvictableKeysToRecentlyAccessedList(OnyxKeys.isCollectionKey, OnyxUtils.getAllKeys), OnyxUtils.initializeWithDefaultKeyStates()]).then(
88-
OnyxUtils.getDeferredInitTask().resolve,
89-
);
86+
// Initialize all of our keys with data provided then give green light to any pending connections.
87+
// addEvictableKeysToRecentlyAccessedList must run after initializeWithDefaultKeyStates because
88+
// eager cache loading populates the key index (cache.setAllKeys) inside initializeWithDefaultKeyStates,
89+
// and the evictable keys list depends on that index being populated.
90+
OnyxUtils.initializeWithDefaultKeyStates()
91+
.then(() => cache.addEvictableKeysToRecentlyAccessedList(OnyxKeys.isCollectionKey, OnyxUtils.getAllKeys))
92+
.then(OnyxUtils.getDeferredInitTask().resolve);
9093
}
9194

9295
/**
@@ -238,7 +241,13 @@ function merge<TKey extends OnyxKey>(key: TKey, changes: OnyxMergeInput<TKey>):
238241

239242
try {
240243
const validChanges = mergeQueue[key].filter((change) => {
241-
const {isCompatible, existingValueType, newValueType} = utils.checkCompatibilityWithExistingValue(change, existingValue);
244+
const {isCompatible, existingValueType, newValueType, isEmptyArrayCoercion} = utils.checkCompatibilityWithExistingValue(change, existingValue);
245+
if (isEmptyArrayCoercion) {
246+
// Merging an object into an empty array isn't semantically correct, but we allow it
247+
// in case we accidentally encoded an empty object as an empty array in PHP. If you're
248+
// looking at a bugbot from this message, we're probably missing that key in OnyxKeys::KEYS_REQUIRING_EMPTY_OBJECT
249+
Logger.logAlert(`[ENSURE_BUGBOT] Onyx merge called on key "${key}" whose existing value is an empty array. Will coerce to object.`);
250+
}
242251
if (!isCompatible) {
243252
Logger.logAlert(logMessages.incompatibleUpdateAlert(key, 'merge', existingValueType, newValueType));
244253
}
@@ -261,8 +270,9 @@ function merge<TKey extends OnyxKey>(key: TKey, changes: OnyxMergeInput<TKey>):
261270
return Promise.resolve();
262271
}
263272

264-
return OnyxMerge.applyMerge(key, existingValue, validChanges).then(({mergedValue}) => {
273+
return OnyxMerge.applyMerge(key, existingValue, validChanges).then(({mergedValue, updatePromise}) => {
265274
OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.MERGE, key, changes, mergedValue);
275+
return updatePromise;
266276
});
267277
} catch (error) {
268278
Logger.logAlert(`An error occurred while applying merge for key: ${key}, Error: ${error}`);
@@ -339,7 +349,7 @@ function clear(keysToPreserve: OnyxKey[] = []): Promise<void> {
339349
// to null would cause unknown behavior)
340350
// 2.1 However, if a default key was explicitly set to null, we need to reset it to the default value
341351
for (const key of allKeys) {
342-
const isKeyToPreserve = keysToPreserve.includes(key);
352+
const isKeyToPreserve = keysToPreserve.some((preserveKey) => OnyxKeys.isKeyMatch(preserveKey, key));
343353
const isDefaultKey = key in defaultKeyStates;
344354

345355
// If the key is being removed or reset to default:
@@ -374,10 +384,20 @@ function clear(keysToPreserve: OnyxKey[] = []): Promise<void> {
374384
keysToBeClearedFromStorage.push(key);
375385
}
376386

387+
const updatePromises: Array<Promise<void>> = [];
388+
389+
// Notify the subscribers for each key/value group so they can receive the new values
390+
for (const [key, value] of Object.entries(keyValuesToResetIndividually)) {
391+
updatePromises.push(OnyxUtils.scheduleSubscriberUpdate(key, value));
392+
}
393+
for (const [key, value] of Object.entries(keyValuesToResetAsCollection)) {
394+
updatePromises.push(OnyxUtils.scheduleNotifyCollectionSubscribers(key, value.newValues, value.oldValues));
395+
}
396+
377397
// Exclude RAM-only keys to prevent them from being saved to storage
378398
const defaultKeyValuePairs = Object.entries(
379399
Object.keys(defaultKeyStates)
380-
.filter((key) => !keysToPreserve.includes(key) && !OnyxKeys.isRamOnlyKey(key))
400+
.filter((key) => !keysToPreserve.some((preserveKey) => OnyxKeys.isKeyMatch(preserveKey, key)) && !OnyxKeys.isRamOnlyKey(key))
381401
.reduce((obj: KeyValueMapping, key) => {
382402
// eslint-disable-next-line no-param-reassign
383403
obj[key] = defaultKeyStates[key];
@@ -392,14 +412,7 @@ function clear(keysToPreserve: OnyxKey[] = []): Promise<void> {
392412
.then(() => Storage.multiSet(defaultKeyValuePairs))
393413
.then(() => {
394414
DevTools.clearState(keysToPreserve);
395-
396-
// Notify the subscribers for each key/value group so they can receive the new values
397-
for (const [key, value] of Object.entries(keyValuesToResetIndividually)) {
398-
OnyxUtils.keyChanged(key, value);
399-
}
400-
for (const [key, value] of Object.entries(keyValuesToResetAsCollection)) {
401-
OnyxUtils.keysChanged(key, value.newValues, value.oldValues);
402-
}
415+
return Promise.all(updatePromises);
403416
});
404417
})
405418
.then(() => undefined);

lib/OnyxCache.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,6 @@ class OnyxCache {
451451
// Return a shallow copy to ensure React detects changes when items are added/removed
452452
return {...cachedCollection};
453453
}
454-
455454
}
456455

457456
const instance = new OnyxCache();

lib/OnyxMerge/index.native.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,21 @@ const applyMerge: ApplyMerge = <TKey extends OnyxKey, TValue extends OnyxInput<T
2727
OnyxUtils.logKeyChanged(OnyxUtils.METHOD.MERGE, key, mergedValue, hasChanged);
2828

2929
// This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
30-
OnyxUtils.broadcastUpdate(key, mergedValue as OnyxValue<TKey>, hasChanged);
30+
const updatePromise = OnyxUtils.broadcastUpdate(key, mergedValue as OnyxValue<TKey>, hasChanged);
3131

3232
const shouldSkipStorageOperations = !hasChanged || OnyxKeys.isRamOnlyKey(key);
3333

3434
// If the value has not changed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
3535
// If the key is marked as RAM-only, it should not be saved nor updated in the storage.
3636
if (shouldSkipStorageOperations) {
37-
return Promise.resolve({mergedValue});
37+
return Promise.resolve({mergedValue, updatePromise});
3838
}
3939

4040
// For native platforms we use `mergeItem` that will take advantage of JSON_PATCH and JSON_REPLACE SQL operations to
4141
// merge the object in a performant way.
4242
return Storage.mergeItem(key, batchedChanges as OnyxValue<TKey>, replaceNullPatches).then(() => ({
4343
mergedValue,
44+
updatePromise,
4445
}));
4546
};
4647

lib/OnyxMerge/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,20 @@ const applyMerge: ApplyMerge = <TKey extends OnyxKey, TValue extends OnyxInput<T
1919
OnyxUtils.logKeyChanged(OnyxUtils.METHOD.MERGE, key, mergedValue, hasChanged);
2020

2121
// This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
22-
OnyxUtils.broadcastUpdate(key, mergedValue as OnyxValue<TKey>, hasChanged);
22+
const updatePromise = OnyxUtils.broadcastUpdate(key, mergedValue as OnyxValue<TKey>, hasChanged);
2323

2424
const shouldSkipStorageOperations = !hasChanged || OnyxKeys.isRamOnlyKey(key);
2525

2626
// If the value has not changed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
2727
// If the key is marked as RAM-only, it should not be saved nor updated in the storage.
2828
if (shouldSkipStorageOperations) {
29-
return Promise.resolve({mergedValue});
29+
return Promise.resolve({mergedValue, updatePromise});
3030
}
3131

3232
// For web platforms we use `setItem` since the object was already merged with its changes before.
3333
return Storage.setItem(key, mergedValue as OnyxValue<TKey>).then(() => ({
3434
mergedValue,
35+
updatePromise,
3536
}));
3637
};
3738

lib/OnyxMerge/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {OnyxInput, OnyxKey} from '../types';
22

33
type ApplyMergeResult<TValue> = {
44
mergedValue: TValue;
5+
updatePromise: Promise<void>;
56
};
67

78
type ApplyMerge = <TKey extends OnyxKey, TValue extends OnyxInput<OnyxKey> | undefined, TChange extends OnyxInput<OnyxKey> | null>(

0 commit comments

Comments
 (0)