react-native-onyx 3.0.48 → 3.0.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Onyx.js +18 -9
- package/dist/OnyxMerge/index.js +3 -2
- package/dist/OnyxMerge/index.native.js +3 -2
- package/dist/OnyxMerge/types.d.ts +1 -0
- package/dist/OnyxUtils.d.ts +17 -2
- package/dist/OnyxUtils.js +88 -41
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +10 -0
- package/package.json +1 -1
package/dist/Onyx.js
CHANGED
|
@@ -221,7 +221,13 @@ function merge(key, changes) {
|
|
|
221
221
|
}
|
|
222
222
|
try {
|
|
223
223
|
const validChanges = mergeQueue[key].filter((change) => {
|
|
224
|
-
const { isCompatible, existingValueType, newValueType } = utils_1.default.checkCompatibilityWithExistingValue(change, existingValue);
|
|
224
|
+
const { isCompatible, existingValueType, newValueType, isEmptyArrayCoercion } = utils_1.default.checkCompatibilityWithExistingValue(change, existingValue);
|
|
225
|
+
if (isEmptyArrayCoercion) {
|
|
226
|
+
// Merging an object into an empty array isn't semantically correct, but we allow it
|
|
227
|
+
// in case we accidentally encoded an empty object as an empty array in PHP. If you're
|
|
228
|
+
// looking at a bugbot from this message, we're probably missing that key in OnyxKeys::KEYS_REQUIRING_EMPTY_OBJECT
|
|
229
|
+
Logger.logAlert(`[ENSURE_BUGBOT] Onyx merge called on key "${key}" whose existing value is an empty array. Will coerce to object.`);
|
|
230
|
+
}
|
|
225
231
|
if (!isCompatible) {
|
|
226
232
|
Logger.logAlert(logMessages_1.default.incompatibleUpdateAlert(key, 'merge', existingValueType, newValueType));
|
|
227
233
|
}
|
|
@@ -240,8 +246,9 @@ function merge(key, changes) {
|
|
|
240
246
|
OnyxUtils_1.default.logKeyRemoved(OnyxUtils_1.default.METHOD.MERGE, key);
|
|
241
247
|
return Promise.resolve();
|
|
242
248
|
}
|
|
243
|
-
return OnyxMerge_1.default.applyMerge(key, existingValue, validChanges).then(({ mergedValue }) => {
|
|
249
|
+
return OnyxMerge_1.default.applyMerge(key, existingValue, validChanges).then(({ mergedValue, updatePromise }) => {
|
|
244
250
|
OnyxUtils_1.default.sendActionToDevTools(OnyxUtils_1.default.METHOD.MERGE, key, changes, mergedValue);
|
|
251
|
+
return updatePromise;
|
|
245
252
|
});
|
|
246
253
|
}
|
|
247
254
|
catch (error) {
|
|
@@ -340,6 +347,14 @@ function clear(keysToPreserve = []) {
|
|
|
340
347
|
// If it isn't preserved and doesn't have a default, we'll remove it
|
|
341
348
|
keysToBeClearedFromStorage.push(key);
|
|
342
349
|
}
|
|
350
|
+
const updatePromises = [];
|
|
351
|
+
// Notify the subscribers for each key/value group so they can receive the new values
|
|
352
|
+
for (const [key, value] of Object.entries(keyValuesToResetIndividually)) {
|
|
353
|
+
updatePromises.push(OnyxUtils_1.default.scheduleSubscriberUpdate(key, value));
|
|
354
|
+
}
|
|
355
|
+
for (const [key, value] of Object.entries(keyValuesToResetAsCollection)) {
|
|
356
|
+
updatePromises.push(OnyxUtils_1.default.scheduleNotifyCollectionSubscribers(key, value.newValues, value.oldValues));
|
|
357
|
+
}
|
|
343
358
|
// Exclude RAM-only keys to prevent them from being saved to storage
|
|
344
359
|
const defaultKeyValuePairs = Object.entries(Object.keys(defaultKeyStates)
|
|
345
360
|
.filter((key) => !keysToPreserve.includes(key) && !OnyxUtils_1.default.isRamOnlyKey(key))
|
|
@@ -356,13 +371,7 @@ function clear(keysToPreserve = []) {
|
|
|
356
371
|
.then(() => storage_1.default.multiSet(defaultKeyValuePairs))
|
|
357
372
|
.then(() => {
|
|
358
373
|
DevTools_1.default.clearState(keysToPreserve);
|
|
359
|
-
|
|
360
|
-
for (const [key, value] of Object.entries(keyValuesToResetIndividually)) {
|
|
361
|
-
OnyxUtils_1.default.keyChanged(key, value);
|
|
362
|
-
}
|
|
363
|
-
for (const [key, value] of Object.entries(keyValuesToResetAsCollection)) {
|
|
364
|
-
OnyxUtils_1.default.keysChanged(key, value.newValues, value.oldValues);
|
|
365
|
-
}
|
|
374
|
+
return Promise.all(updatePromises);
|
|
366
375
|
});
|
|
367
376
|
})
|
|
368
377
|
.then(() => undefined);
|
package/dist/OnyxMerge/index.js
CHANGED
|
@@ -13,16 +13,17 @@ const applyMerge = (key, existingValue, validChanges) => {
|
|
|
13
13
|
// Logging properties only since values could be sensitive things we don't want to log.
|
|
14
14
|
OnyxUtils_1.default.logKeyChanged(OnyxUtils_1.default.METHOD.MERGE, key, mergedValue, hasChanged);
|
|
15
15
|
// This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
|
|
16
|
-
OnyxUtils_1.default.broadcastUpdate(key, mergedValue, hasChanged);
|
|
16
|
+
const updatePromise = OnyxUtils_1.default.broadcastUpdate(key, mergedValue, hasChanged);
|
|
17
17
|
const shouldSkipStorageOperations = !hasChanged || OnyxUtils_1.default.isRamOnlyKey(key);
|
|
18
18
|
// If the value has not changed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
|
|
19
19
|
// If the key is marked as RAM-only, it should not be saved nor updated in the storage.
|
|
20
20
|
if (shouldSkipStorageOperations) {
|
|
21
|
-
return Promise.resolve({ mergedValue });
|
|
21
|
+
return Promise.resolve({ mergedValue, updatePromise });
|
|
22
22
|
}
|
|
23
23
|
// For web platforms we use `setItem` since the object was already merged with its changes before.
|
|
24
24
|
return storage_1.default.setItem(key, mergedValue).then(() => ({
|
|
25
25
|
mergedValue,
|
|
26
|
+
updatePromise,
|
|
26
27
|
}));
|
|
27
28
|
};
|
|
28
29
|
const OnyxMerge = {
|
|
@@ -19,17 +19,18 @@ const applyMerge = (key, existingValue, validChanges) => {
|
|
|
19
19
|
// Logging properties only since values could be sensitive things we don't want to log.
|
|
20
20
|
OnyxUtils_1.default.logKeyChanged(OnyxUtils_1.default.METHOD.MERGE, key, mergedValue, hasChanged);
|
|
21
21
|
// This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
|
|
22
|
-
OnyxUtils_1.default.broadcastUpdate(key, mergedValue, hasChanged);
|
|
22
|
+
const updatePromise = OnyxUtils_1.default.broadcastUpdate(key, mergedValue, hasChanged);
|
|
23
23
|
const shouldSkipStorageOperations = !hasChanged || OnyxUtils_1.default.isRamOnlyKey(key);
|
|
24
24
|
// If the value has not changed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
|
|
25
25
|
// If the key is marked as RAM-only, it should not be saved nor updated in the storage.
|
|
26
26
|
if (shouldSkipStorageOperations) {
|
|
27
|
-
return Promise.resolve({ mergedValue });
|
|
27
|
+
return Promise.resolve({ mergedValue, updatePromise });
|
|
28
28
|
}
|
|
29
29
|
// For native platforms we use `mergeItem` that will take advantage of JSON_PATCH and JSON_REPLACE SQL operations to
|
|
30
30
|
// merge the object in a performant way.
|
|
31
31
|
return storage_1.default.mergeItem(key, batchedChanges, replaceNullPatches).then(() => ({
|
|
32
32
|
mergedValue,
|
|
33
|
+
updatePromise,
|
|
33
34
|
}));
|
|
34
35
|
};
|
|
35
36
|
const OnyxMerge = {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { OnyxInput, OnyxKey } from '../types';
|
|
2
2
|
type ApplyMergeResult<TValue> = {
|
|
3
3
|
mergedValue: TValue;
|
|
4
|
+
updatePromise: Promise<void>;
|
|
4
5
|
};
|
|
5
6
|
type ApplyMerge = <TKey extends OnyxKey, TValue extends OnyxInput<OnyxKey> | undefined, TChange extends OnyxInput<OnyxKey> | null>(key: TKey, existingValue: TValue, validChanges: TChange[]) => Promise<ApplyMergeResult<TChange>>;
|
|
6
7
|
export type { ApplyMerge, ApplyMergeResult };
|
package/dist/OnyxUtils.d.ts
CHANGED
|
@@ -186,7 +186,7 @@ declare function keyChanged<TKey extends OnyxKey>(key: TKey, value: OnyxValue<TK
|
|
|
186
186
|
/**
|
|
187
187
|
* Sends the data obtained from the keys to the connection.
|
|
188
188
|
*/
|
|
189
|
-
declare function sendDataToConnection<TKey extends OnyxKey>(mapping: CallbackToStateMapping<TKey>, matchedKey: TKey | undefined): void;
|
|
189
|
+
declare function sendDataToConnection<TKey extends OnyxKey>(mapping: CallbackToStateMapping<TKey>, value: OnyxValue<TKey> | null, matchedKey: TKey | undefined): void;
|
|
190
190
|
/**
|
|
191
191
|
* We check to see if this key is flagged as safe for eviction and add it to the recentlyAccessedKeys list so that when we
|
|
192
192
|
* run out of storage the least recently accessed key can be removed.
|
|
@@ -196,6 +196,19 @@ declare function addKeyToRecentlyAccessedIfNeeded<TKey extends OnyxKey>(key: TKe
|
|
|
196
196
|
* Gets the data for a given an array of matching keys, combines them into an object, and sends the result back to the subscriber.
|
|
197
197
|
*/
|
|
198
198
|
declare function getCollectionDataAndSendAsObject<TKey extends OnyxKey>(matchingKeys: CollectionKeyBase[], mapping: CallbackToStateMapping<TKey>): void;
|
|
199
|
+
/**
|
|
200
|
+
* Schedules an update that will be appended to the macro task queue (so it doesn't update the subscribers immediately).
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* scheduleSubscriberUpdate(key, value, subscriber => subscriber.initWithStoredValues === false)
|
|
204
|
+
*/
|
|
205
|
+
declare function scheduleSubscriberUpdate<TKey extends OnyxKey>(key: TKey, value: OnyxValue<TKey>, canUpdateSubscriber?: (subscriber?: CallbackToStateMapping<OnyxKey>) => boolean, isProcessingCollectionUpdate?: boolean): Promise<void>;
|
|
206
|
+
/**
|
|
207
|
+
* This method is similar to scheduleSubscriberUpdate but it is built for working specifically with collections
|
|
208
|
+
* so that keysChanged() is triggered for the collection and not keyChanged(). If this was not done, then the
|
|
209
|
+
* subscriber callbacks receive the data in a different format than they normally expect and it breaks code.
|
|
210
|
+
*/
|
|
211
|
+
declare function scheduleNotifyCollectionSubscribers<TKey extends OnyxKey>(key: TKey, value: OnyxCollection<KeyValueMapping[TKey]>, previousValue?: OnyxCollection<KeyValueMapping[TKey]>): Promise<void>;
|
|
199
212
|
/**
|
|
200
213
|
* Remove a key from Onyx and update the subscribers
|
|
201
214
|
*/
|
|
@@ -211,7 +224,7 @@ declare function retryOperation<TMethod extends RetriableOnyxOperation>(error: E
|
|
|
211
224
|
/**
|
|
212
225
|
* Notifies subscribers and writes current value to cache
|
|
213
226
|
*/
|
|
214
|
-
declare function broadcastUpdate<TKey extends OnyxKey>(key: TKey, value: OnyxValue<TKey>, hasChanged?: boolean): void
|
|
227
|
+
declare function broadcastUpdate<TKey extends OnyxKey>(key: TKey, value: OnyxValue<TKey>, hasChanged?: boolean): Promise<void>;
|
|
215
228
|
declare function hasPendingMergeForKey(key: OnyxKey): boolean;
|
|
216
229
|
/**
|
|
217
230
|
* Storage expects array like: [["@MyApp_user", value_1], ["@MyApp_key", value_2]]
|
|
@@ -357,6 +370,8 @@ declare const OnyxUtils: {
|
|
|
357
370
|
sendDataToConnection: typeof sendDataToConnection;
|
|
358
371
|
getCollectionKey: typeof getCollectionKey;
|
|
359
372
|
getCollectionDataAndSendAsObject: typeof getCollectionDataAndSendAsObject;
|
|
373
|
+
scheduleSubscriberUpdate: typeof scheduleSubscriberUpdate;
|
|
374
|
+
scheduleNotifyCollectionSubscribers: typeof scheduleNotifyCollectionSubscribers;
|
|
360
375
|
remove: typeof remove;
|
|
361
376
|
reportStorageQuota: typeof reportStorageQuota;
|
|
362
377
|
retryOperation: typeof retryOperation;
|
package/dist/OnyxUtils.js
CHANGED
|
@@ -74,6 +74,8 @@ const MAX_STORAGE_OPERATION_RETRY_ATTEMPTS = 5;
|
|
|
74
74
|
// Key/value store of Onyx key and arrays of values to merge
|
|
75
75
|
let mergeQueue = {};
|
|
76
76
|
let mergeQueuePromise = {};
|
|
77
|
+
// Used to schedule subscriber update to the macro task queue
|
|
78
|
+
let nextMacrotaskPromise = null;
|
|
77
79
|
// Holds a mapping of all the React components that want their state subscribed to a store key
|
|
78
80
|
let callbackToStateMapping = {};
|
|
79
81
|
// Keeps a copy of the values of the onyx collection keys as a map for faster lookups
|
|
@@ -584,7 +586,6 @@ function keysChanged(collectionKey, partialCollection, partialPreviousCollection
|
|
|
584
586
|
// If they are subscribed to the collection key and using waitForCollectionCallback then we'll
|
|
585
587
|
// send the whole cached collection.
|
|
586
588
|
if (isSubscribedToCollectionKey) {
|
|
587
|
-
lastConnectionCallbackData.set(subscriber.subscriptionID, cachedCollection);
|
|
588
589
|
if (subscriber.waitForCollectionCallback) {
|
|
589
590
|
subscriber.callback(cachedCollection, subscriber.key, partialCollection);
|
|
590
591
|
continue;
|
|
@@ -667,7 +668,6 @@ function keyChanged(key, value, canUpdateSubscriber = () => true, isProcessingCo
|
|
|
667
668
|
cachedCollections[subscriber.key] = cachedCollection;
|
|
668
669
|
}
|
|
669
670
|
cachedCollection[key] = value;
|
|
670
|
-
lastConnectionCallbackData.set(subscriber.subscriptionID, cachedCollection);
|
|
671
671
|
subscriber.callback(cachedCollection, subscriber.key, { [key]: value });
|
|
672
672
|
continue;
|
|
673
673
|
}
|
|
@@ -682,32 +682,22 @@ function keyChanged(key, value, canUpdateSubscriber = () => true, isProcessingCo
|
|
|
682
682
|
/**
|
|
683
683
|
* Sends the data obtained from the keys to the connection.
|
|
684
684
|
*/
|
|
685
|
-
function sendDataToConnection(mapping, matchedKey) {
|
|
685
|
+
function sendDataToConnection(mapping, value, matchedKey) {
|
|
686
686
|
var _a, _b;
|
|
687
687
|
// If the mapping no longer exists then we should not send any data.
|
|
688
688
|
// This means our subscriber was disconnected.
|
|
689
689
|
if (!callbackToStateMapping[mapping.subscriptionID]) {
|
|
690
690
|
return;
|
|
691
691
|
}
|
|
692
|
-
// Always read the latest value from cache to avoid stale or duplicate data.
|
|
693
|
-
// For collection subscribers with waitForCollectionCallback, read the full collection.
|
|
694
|
-
// For individual key subscribers, read just that key's value.
|
|
695
|
-
let value;
|
|
696
|
-
if (isCollectionKey(mapping.key) && mapping.waitForCollectionCallback) {
|
|
697
|
-
const collection = getCachedCollection(mapping.key);
|
|
698
|
-
value = Object.keys(collection).length > 0 ? collection : undefined;
|
|
699
|
-
}
|
|
700
|
-
else {
|
|
701
|
-
value = OnyxCache_1.default.get(matchedKey !== null && matchedKey !== void 0 ? matchedKey : mapping.key);
|
|
702
|
-
}
|
|
703
692
|
// For regular callbacks, we never want to pass null values, but always just undefined if a value is not set in cache or storage.
|
|
704
|
-
|
|
693
|
+
const valueToPass = value === null ? undefined : value;
|
|
705
694
|
const lastValue = lastConnectionCallbackData.get(mapping.subscriptionID);
|
|
695
|
+
lastConnectionCallbackData.get(mapping.subscriptionID);
|
|
706
696
|
// If the value has not changed we do not need to trigger the callback
|
|
707
|
-
if (lastConnectionCallbackData.has(mapping.subscriptionID) &&
|
|
697
|
+
if (lastConnectionCallbackData.has(mapping.subscriptionID) && valueToPass === lastValue) {
|
|
708
698
|
return;
|
|
709
699
|
}
|
|
710
|
-
(_b = (_a = mapping).callback) === null || _b === void 0 ? void 0 : _b.call(_a,
|
|
700
|
+
(_b = (_a = mapping).callback) === null || _b === void 0 ? void 0 : _b.call(_a, valueToPass, matchedKey);
|
|
711
701
|
}
|
|
712
702
|
/**
|
|
713
703
|
* We check to see if this key is flagged as safe for eviction and add it to the recentlyAccessedKeys list so that when we
|
|
@@ -726,16 +716,50 @@ function addKeyToRecentlyAccessedIfNeeded(key) {
|
|
|
726
716
|
* Gets the data for a given an array of matching keys, combines them into an object, and sends the result back to the subscriber.
|
|
727
717
|
*/
|
|
728
718
|
function getCollectionDataAndSendAsObject(matchingKeys, mapping) {
|
|
729
|
-
multiGet(matchingKeys).then(() => {
|
|
730
|
-
|
|
719
|
+
multiGet(matchingKeys).then((dataMap) => {
|
|
720
|
+
const data = Object.fromEntries(dataMap.entries());
|
|
721
|
+
sendDataToConnection(mapping, data, mapping.key);
|
|
731
722
|
});
|
|
732
723
|
}
|
|
724
|
+
/**
|
|
725
|
+
* Delays promise resolution until the next macrotask to prevent race condition if the key subscription is in progress.
|
|
726
|
+
*
|
|
727
|
+
* @param callback The keyChanged/keysChanged callback
|
|
728
|
+
* */
|
|
729
|
+
function prepareSubscriberUpdate(callback) {
|
|
730
|
+
if (!nextMacrotaskPromise) {
|
|
731
|
+
nextMacrotaskPromise = new Promise((resolve) => {
|
|
732
|
+
setTimeout(() => {
|
|
733
|
+
nextMacrotaskPromise = null;
|
|
734
|
+
resolve();
|
|
735
|
+
}, 0);
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
return Promise.all([nextMacrotaskPromise, Promise.resolve().then(callback)]).then();
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Schedules an update that will be appended to the macro task queue (so it doesn't update the subscribers immediately).
|
|
742
|
+
*
|
|
743
|
+
* @example
|
|
744
|
+
* scheduleSubscriberUpdate(key, value, subscriber => subscriber.initWithStoredValues === false)
|
|
745
|
+
*/
|
|
746
|
+
function scheduleSubscriberUpdate(key, value, canUpdateSubscriber = () => true, isProcessingCollectionUpdate = false) {
|
|
747
|
+
return prepareSubscriberUpdate(() => keyChanged(key, value, canUpdateSubscriber, isProcessingCollectionUpdate));
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* This method is similar to scheduleSubscriberUpdate but it is built for working specifically with collections
|
|
751
|
+
* so that keysChanged() is triggered for the collection and not keyChanged(). If this was not done, then the
|
|
752
|
+
* subscriber callbacks receive the data in a different format than they normally expect and it breaks code.
|
|
753
|
+
*/
|
|
754
|
+
function scheduleNotifyCollectionSubscribers(key, value, previousValue) {
|
|
755
|
+
return prepareSubscriberUpdate(() => keysChanged(key, value, previousValue));
|
|
756
|
+
}
|
|
733
757
|
/**
|
|
734
758
|
* Remove a key from Onyx and update the subscribers
|
|
735
759
|
*/
|
|
736
760
|
function remove(key, isProcessingCollectionUpdate) {
|
|
737
761
|
OnyxCache_1.default.drop(key);
|
|
738
|
-
|
|
762
|
+
scheduleSubscriberUpdate(key, undefined, undefined, isProcessingCollectionUpdate);
|
|
739
763
|
if (isRamOnlyKey(key)) {
|
|
740
764
|
return Promise.resolve();
|
|
741
765
|
}
|
|
@@ -803,7 +827,7 @@ function broadcastUpdate(key, value, hasChanged) {
|
|
|
803
827
|
else {
|
|
804
828
|
OnyxCache_1.default.addToAccessedKeys(key);
|
|
805
829
|
}
|
|
806
|
-
|
|
830
|
+
return scheduleSubscriberUpdate(key, value, (subscriber) => hasChanged || (subscriber === null || subscriber === void 0 ? void 0 : subscriber.initWithStoredValues) === false).then(() => undefined);
|
|
807
831
|
}
|
|
808
832
|
function hasPendingMergeForKey(key) {
|
|
809
833
|
return !!mergeQueue[key];
|
|
@@ -978,7 +1002,7 @@ function subscribeToKey(connectOptions) {
|
|
|
978
1002
|
const matchedKey = isCollectionKey(mapping.key) && mapping.waitForCollectionCallback ? mapping.key : undefined;
|
|
979
1003
|
// Here we cannot use batching because the nullish value is expected to be set immediately for default props
|
|
980
1004
|
// or they will be undefined.
|
|
981
|
-
sendDataToConnection(mapping, matchedKey);
|
|
1005
|
+
sendDataToConnection(mapping, null, matchedKey);
|
|
982
1006
|
return;
|
|
983
1007
|
}
|
|
984
1008
|
// When using a callback subscriber we will either trigger the provided callback for each key we find or combine all values
|
|
@@ -991,15 +1015,15 @@ function subscribeToKey(connectOptions) {
|
|
|
991
1015
|
return;
|
|
992
1016
|
}
|
|
993
1017
|
// We did not opt into using waitForCollectionCallback mode so the callback is called for every matching key.
|
|
994
|
-
multiGet(matchingKeys).then(() => {
|
|
995
|
-
for (const key of
|
|
996
|
-
sendDataToConnection(mapping, key);
|
|
1018
|
+
multiGet(matchingKeys).then((values) => {
|
|
1019
|
+
for (const [key, val] of values.entries()) {
|
|
1020
|
+
sendDataToConnection(mapping, val, key);
|
|
997
1021
|
}
|
|
998
1022
|
});
|
|
999
1023
|
return;
|
|
1000
1024
|
}
|
|
1001
1025
|
// If we are not subscribed to a collection key then there's only a single key to send an update for.
|
|
1002
|
-
get(mapping.key).then(() => sendDataToConnection(mapping, mapping.key));
|
|
1026
|
+
get(mapping.key).then((val) => sendDataToConnection(mapping, val, mapping.key));
|
|
1003
1027
|
return;
|
|
1004
1028
|
}
|
|
1005
1029
|
console.error('Warning: Onyx.connect() was found without a callback');
|
|
@@ -1111,7 +1135,13 @@ function setWithRetry({ key, value, options }, retryAttempt) {
|
|
|
1111
1135
|
return Promise.resolve();
|
|
1112
1136
|
}
|
|
1113
1137
|
// Check if the value is compatible with the existing value in the storage
|
|
1114
|
-
const { isCompatible, existingValueType, newValueType } = utils_1.default.checkCompatibilityWithExistingValue(value, existingValue);
|
|
1138
|
+
const { isCompatible, existingValueType, newValueType, isEmptyArrayCoercion } = utils_1.default.checkCompatibilityWithExistingValue(value, existingValue);
|
|
1139
|
+
if (isEmptyArrayCoercion) {
|
|
1140
|
+
// Setting an object over empty array isn't semantically correct, but we allow it
|
|
1141
|
+
// in case we accidentally encoded an empty object as an empty array in PHP. If you're
|
|
1142
|
+
// looking at a bugbot from this message, we're probably missing that key in OnyxKeys::KEYS_REQUIRING_EMPTY_OBJECT
|
|
1143
|
+
Logger.logAlert(`[ENSURE_BUGBOT] Onyx setWithRetry called on key "${key}" whose existing value is an empty array. Will coerce to object.`);
|
|
1144
|
+
}
|
|
1115
1145
|
if (!isCompatible) {
|
|
1116
1146
|
Logger.logAlert(logMessages_1.default.incompatibleUpdateAlert(key, 'set', existingValueType, newValueType));
|
|
1117
1147
|
return Promise.resolve();
|
|
@@ -1127,20 +1157,21 @@ function setWithRetry({ key, value, options }, retryAttempt) {
|
|
|
1127
1157
|
const hasChanged = (options === null || options === void 0 ? void 0 : options.skipCacheCheck) ? true : OnyxCache_1.default.hasValueChanged(key, valueWithoutNestedNullValues);
|
|
1128
1158
|
OnyxUtils.logKeyChanged(OnyxUtils.METHOD.SET, key, value, hasChanged);
|
|
1129
1159
|
// This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
|
|
1130
|
-
OnyxUtils.broadcastUpdate(key, valueWithoutNestedNullValues, hasChanged);
|
|
1160
|
+
const updatePromise = OnyxUtils.broadcastUpdate(key, valueWithoutNestedNullValues, hasChanged);
|
|
1131
1161
|
// If the value has not changed and this isn't a retry attempt, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
|
|
1132
1162
|
if (!hasChanged && !retryAttempt) {
|
|
1133
|
-
return
|
|
1163
|
+
return updatePromise;
|
|
1134
1164
|
}
|
|
1135
1165
|
// If a key is a RAM-only key or a member of RAM-only collection, we skip the step that modifies the storage
|
|
1136
1166
|
if (isRamOnlyKey(key)) {
|
|
1137
1167
|
OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.SET, key, valueWithoutNestedNullValues);
|
|
1138
|
-
return
|
|
1168
|
+
return updatePromise;
|
|
1139
1169
|
}
|
|
1140
1170
|
return storage_1.default.setItem(key, valueWithoutNestedNullValues)
|
|
1141
1171
|
.catch((error) => OnyxUtils.retryOperation(error, setWithRetry, { key, value: valueWithoutNestedNullValues, options }, retryAttempt))
|
|
1142
1172
|
.then(() => {
|
|
1143
1173
|
OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.SET, key, valueWithoutNestedNullValues);
|
|
1174
|
+
return updatePromise;
|
|
1144
1175
|
});
|
|
1145
1176
|
}
|
|
1146
1177
|
/**
|
|
@@ -1170,16 +1201,16 @@ function multiSetWithRetry(data, retryAttempt) {
|
|
|
1170
1201
|
}, {});
|
|
1171
1202
|
}
|
|
1172
1203
|
const keyValuePairsToSet = OnyxUtils.prepareKeyValuePairsForStorage(newData, true);
|
|
1173
|
-
|
|
1204
|
+
const updatePromises = keyValuePairsToSet.map(([key, value]) => {
|
|
1174
1205
|
// When we use multiSet to set a key we want to clear the current delta changes from Onyx.merge that were queued
|
|
1175
1206
|
// before the value was set. If Onyx.merge is currently reading the old value from storage, it will then not apply the changes.
|
|
1176
1207
|
if (OnyxUtils.hasPendingMergeForKey(key)) {
|
|
1177
1208
|
delete OnyxUtils.getMergeQueue()[key];
|
|
1178
1209
|
}
|
|
1179
|
-
// Update cache and optimistically inform subscribers
|
|
1210
|
+
// Update cache and optimistically inform subscribers on the next tick
|
|
1180
1211
|
OnyxCache_1.default.set(key, value);
|
|
1181
|
-
|
|
1182
|
-
}
|
|
1212
|
+
return OnyxUtils.scheduleSubscriberUpdate(key, value);
|
|
1213
|
+
});
|
|
1183
1214
|
const keyValuePairsToStore = keyValuePairsToSet.filter((keyValuePair) => {
|
|
1184
1215
|
const [key] = keyValuePair;
|
|
1185
1216
|
// Filter out the RAM-only key value pairs, as they should not be saved to storage
|
|
@@ -1189,7 +1220,9 @@ function multiSetWithRetry(data, retryAttempt) {
|
|
|
1189
1220
|
.catch((error) => OnyxUtils.retryOperation(error, multiSetWithRetry, newData, retryAttempt))
|
|
1190
1221
|
.then(() => {
|
|
1191
1222
|
OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.MULTI_SET, undefined, newData);
|
|
1192
|
-
|
|
1223
|
+
return Promise.all(updatePromises);
|
|
1224
|
+
})
|
|
1225
|
+
.then(() => undefined);
|
|
1193
1226
|
}
|
|
1194
1227
|
/**
|
|
1195
1228
|
* Sets a collection by replacing all existing collection members with new values.
|
|
@@ -1242,16 +1275,17 @@ function setCollectionWithRetry({ collectionKey, collection }, retryAttempt) {
|
|
|
1242
1275
|
const previousCollection = OnyxUtils.getCachedCollection(collectionKey);
|
|
1243
1276
|
for (const [key, value] of keyValuePairs)
|
|
1244
1277
|
OnyxCache_1.default.set(key, value);
|
|
1245
|
-
|
|
1278
|
+
const updatePromise = OnyxUtils.scheduleNotifyCollectionSubscribers(collectionKey, mutableCollection, previousCollection);
|
|
1246
1279
|
// RAM-only keys are not supposed to be saved to storage
|
|
1247
1280
|
if (isRamOnlyKey(collectionKey)) {
|
|
1248
1281
|
OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.SET_COLLECTION, undefined, mutableCollection);
|
|
1249
|
-
return;
|
|
1282
|
+
return updatePromise;
|
|
1250
1283
|
}
|
|
1251
1284
|
return storage_1.default.multiSet(keyValuePairs)
|
|
1252
1285
|
.catch((error) => OnyxUtils.retryOperation(error, setCollectionWithRetry, { collectionKey, collection }, retryAttempt))
|
|
1253
1286
|
.then(() => {
|
|
1254
1287
|
OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.SET_COLLECTION, undefined, mutableCollection);
|
|
1288
|
+
return updatePromise;
|
|
1255
1289
|
});
|
|
1256
1290
|
});
|
|
1257
1291
|
}
|
|
@@ -1309,7 +1343,13 @@ function mergeCollectionWithPatches({ collectionKey, collection, mergeReplaceNul
|
|
|
1309
1343
|
const existingKeys = keys.filter((key) => persistedKeys.has(key));
|
|
1310
1344
|
const cachedCollectionForExistingKeys = getCachedCollection(collectionKey, existingKeys);
|
|
1311
1345
|
const existingKeyCollection = existingKeys.reduce((obj, key) => {
|
|
1312
|
-
const { isCompatible, existingValueType, newValueType } = utils_1.default.checkCompatibilityWithExistingValue(resultCollection[key], cachedCollectionForExistingKeys[key]);
|
|
1346
|
+
const { isCompatible, existingValueType, newValueType, isEmptyArrayCoercion } = utils_1.default.checkCompatibilityWithExistingValue(resultCollection[key], cachedCollectionForExistingKeys[key]);
|
|
1347
|
+
if (isEmptyArrayCoercion) {
|
|
1348
|
+
// Merging an object into an empty array isn't semantically correct, but we allow it
|
|
1349
|
+
// in case we accidentally encoded an empty object as an empty array in PHP. If you're
|
|
1350
|
+
// looking at a bugbot from this message, we're probably missing that key in OnyxKeys::KEYS_REQUIRING_EMPTY_OBJECT
|
|
1351
|
+
Logger.logAlert(`[ENSURE_BUGBOT] Onyx mergeCollection called on key "${key}" whose existing value is an empty array. Will coerce to object.`);
|
|
1352
|
+
}
|
|
1313
1353
|
if (!isCompatible) {
|
|
1314
1354
|
Logger.logAlert(logMessages_1.default.incompatibleUpdateAlert(key, 'mergeCollection', existingValueType, newValueType));
|
|
1315
1355
|
return obj;
|
|
@@ -1352,7 +1392,7 @@ function mergeCollectionWithPatches({ collectionKey, collection, mergeReplaceNul
|
|
|
1352
1392
|
// and update all subscribers
|
|
1353
1393
|
const promiseUpdate = previousCollectionPromise.then((previousCollection) => {
|
|
1354
1394
|
OnyxCache_1.default.merge(finalMergedCollection);
|
|
1355
|
-
|
|
1395
|
+
return scheduleNotifyCollectionSubscribers(collectionKey, finalMergedCollection, previousCollection);
|
|
1356
1396
|
});
|
|
1357
1397
|
return Promise.all(promises)
|
|
1358
1398
|
.catch((error) => retryOperation(error, mergeCollectionWithPatches, { collectionKey, collection: resultCollection, mergeReplaceNullPatches, isProcessingCollectionUpdate }, retryAttempt))
|
|
@@ -1405,15 +1445,16 @@ function partialSetCollection({ collectionKey, collection }, retryAttempt) {
|
|
|
1405
1445
|
const keyValuePairs = prepareKeyValuePairsForStorage(mutableCollection, true, undefined, true);
|
|
1406
1446
|
for (const [key, value] of keyValuePairs)
|
|
1407
1447
|
OnyxCache_1.default.set(key, value);
|
|
1408
|
-
|
|
1448
|
+
const updatePromise = scheduleNotifyCollectionSubscribers(collectionKey, mutableCollection, previousCollection);
|
|
1409
1449
|
if (isRamOnlyKey(collectionKey)) {
|
|
1410
1450
|
sendActionToDevTools(METHOD.SET_COLLECTION, undefined, mutableCollection);
|
|
1411
|
-
return;
|
|
1451
|
+
return updatePromise;
|
|
1412
1452
|
}
|
|
1413
1453
|
return storage_1.default.multiSet(keyValuePairs)
|
|
1414
1454
|
.catch((error) => retryOperation(error, partialSetCollection, { collectionKey, collection }, retryAttempt))
|
|
1415
1455
|
.then(() => {
|
|
1416
1456
|
sendActionToDevTools(METHOD.SET_COLLECTION, undefined, mutableCollection);
|
|
1457
|
+
return updatePromise;
|
|
1417
1458
|
});
|
|
1418
1459
|
});
|
|
1419
1460
|
}
|
|
@@ -1457,6 +1498,8 @@ const OnyxUtils = {
|
|
|
1457
1498
|
sendDataToConnection,
|
|
1458
1499
|
getCollectionKey,
|
|
1459
1500
|
getCollectionDataAndSendAsObject,
|
|
1501
|
+
scheduleSubscriberUpdate,
|
|
1502
|
+
scheduleNotifyCollectionSubscribers,
|
|
1460
1503
|
remove,
|
|
1461
1504
|
reportStorageQuota,
|
|
1462
1505
|
retryOperation,
|
|
@@ -1511,6 +1554,10 @@ GlobalSettings.addGlobalSettingsChangeListener(({ enablePerformanceMetrics }) =>
|
|
|
1511
1554
|
// @ts-expect-error Reassign
|
|
1512
1555
|
sendDataToConnection = (0, metrics_1.default)(sendDataToConnection, 'OnyxUtils.sendDataToConnection');
|
|
1513
1556
|
// @ts-expect-error Reassign
|
|
1557
|
+
scheduleSubscriberUpdate = (0, metrics_1.default)(scheduleSubscriberUpdate, 'OnyxUtils.scheduleSubscriberUpdate');
|
|
1558
|
+
// @ts-expect-error Reassign
|
|
1559
|
+
scheduleNotifyCollectionSubscribers = (0, metrics_1.default)(scheduleNotifyCollectionSubscribers, 'OnyxUtils.scheduleNotifyCollectionSubscribers');
|
|
1560
|
+
// @ts-expect-error Reassign
|
|
1514
1561
|
remove = (0, metrics_1.default)(remove, 'OnyxUtils.remove');
|
|
1515
1562
|
// @ts-expect-error Reassign
|
|
1516
1563
|
reportStorageQuota = (0, metrics_1.default)(reportStorageQuota, 'OnyxUtils.reportStorageQuota');
|
package/dist/utils.d.ts
CHANGED
|
@@ -46,6 +46,7 @@ declare function checkCompatibilityWithExistingValue(value: unknown, existingVal
|
|
|
46
46
|
isCompatible: boolean;
|
|
47
47
|
existingValueType?: string;
|
|
48
48
|
newValueType?: string;
|
|
49
|
+
isEmptyArrayCoercion?: boolean;
|
|
49
50
|
};
|
|
50
51
|
/**
|
|
51
52
|
* Picks entries from an object based on a condition.
|
package/dist/utils.js
CHANGED
|
@@ -142,6 +142,16 @@ function checkCompatibilityWithExistingValue(value, existingValue) {
|
|
|
142
142
|
isCompatible: true,
|
|
143
143
|
};
|
|
144
144
|
}
|
|
145
|
+
// PHP's associative arrays cannot distinguish between an empty list and an
|
|
146
|
+
// empty object, so it encodes both as []. A key that should hold an
|
|
147
|
+
// object may arrive from the server as [] and be stored that way. If
|
|
148
|
+
// we then try to MERGE an object into that key, the array-vs-object type check
|
|
149
|
+
// would normally block it. Since an empty array carries no data worth
|
|
150
|
+
// preserving, we treat it as compatible with an object update and coerce it.
|
|
151
|
+
const isObjectValue = typeof value === 'object' && !Array.isArray(value);
|
|
152
|
+
if (Array.isArray(existingValue) && existingValue.length === 0 && isObjectValue) {
|
|
153
|
+
return { isCompatible: true, isEmptyArrayCoercion: true };
|
|
154
|
+
}
|
|
145
155
|
const existingValueType = Array.isArray(existingValue) ? 'array' : 'non-array';
|
|
146
156
|
const newValueType = Array.isArray(value) ? 'array' : 'non-array';
|
|
147
157
|
if (existingValueType !== newValueType) {
|