react-native-onyx 2.0.33 → 2.0.35

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/DevTools.js CHANGED
@@ -11,8 +11,8 @@ class DevTools {
11
11
  try {
12
12
  // We don't want to augment the window type in a library code, so we use type assertion instead
13
13
  // eslint-disable-next-line no-underscore-dangle, @typescript-eslint/no-explicit-any
14
- const reduxDevtools = window.__REDUX_DEVTOOLS_EXTENSION__;
15
- if ((options && options.remote) || typeof window === 'undefined' || !reduxDevtools) {
14
+ const reduxDevtools = typeof window === 'undefined' ? undefined : window.__REDUX_DEVTOOLS_EXTENSION__;
15
+ if ((options === null || options === void 0 ? void 0 : options.remote) || !reduxDevtools) {
16
16
  return;
17
17
  }
18
18
  return reduxDevtools.connect(options);
package/dist/Onyx.js CHANGED
@@ -36,6 +36,7 @@ const storage_1 = __importDefault(require("./storage"));
36
36
  const utils_1 = __importDefault(require("./utils"));
37
37
  const DevTools_1 = __importDefault(require("./DevTools"));
38
38
  const OnyxUtils_1 = __importDefault(require("./OnyxUtils"));
39
+ const logMessages_1 = __importDefault(require("./logMessages"));
39
40
  // Keeps track of the last connectionID that was used so we can keep incrementing it
40
41
  let lastConnectionID = 0;
41
42
  // Connections can be made before `Onyx.init`. They would wait for this task before resolving
@@ -191,6 +192,13 @@ function disconnect(connectionID, keyToRemoveFromEvictionBlocklist) {
191
192
  * @param value value to store
192
193
  */
193
194
  function set(key, value) {
195
+ // check if the value is compatible with the existing value in the storage
196
+ const existingValue = OnyxCache_1.default.getValue(key, false);
197
+ const { isCompatible, existingValueType, newValueType } = utils_1.default.checkCompatibilityWithExistingValue(value, existingValue);
198
+ if (!isCompatible) {
199
+ Logger.logAlert(logMessages_1.default.incompatibleUpdateAlert(key, 'set', existingValueType, newValueType));
200
+ return Promise.resolve();
201
+ }
194
202
  // If the value is null, we remove the key from storage
195
203
  const { value: valueAfterRemoving, wasRemoved } = OnyxUtils_1.default.removeNullValues(key, value);
196
204
  const valueWithoutNullValues = valueAfterRemoving;
@@ -275,7 +283,17 @@ function merge(key, changes) {
275
283
  try {
276
284
  // We first only merge the changes, so we can provide these to the native implementation (SQLite uses only delta changes in "JSON_PATCH" to merge)
277
285
  // We don't want to remove null values from the "batchedDeltaChanges", because SQLite uses them to remove keys from storage natively.
278
- const batchedDeltaChanges = OnyxUtils_1.default.applyMerge(undefined, mergeQueue[key], false);
286
+ const validChanges = mergeQueue[key].filter((change) => {
287
+ const { isCompatible, existingValueType, newValueType } = utils_1.default.checkCompatibilityWithExistingValue(change, existingValue);
288
+ if (!isCompatible) {
289
+ Logger.logAlert(logMessages_1.default.incompatibleUpdateAlert(key, 'merge', existingValueType, newValueType));
290
+ }
291
+ return isCompatible;
292
+ });
293
+ if (!validChanges.length) {
294
+ return undefined;
295
+ }
296
+ const batchedDeltaChanges = OnyxUtils_1.default.applyMerge(undefined, validChanges, false);
279
297
  // Case (1): When there is no existing value in storage, we want to set the value instead of merge it.
280
298
  // Case (2): The presence of a top-level `null` in the merge queue instructs us to drop the whole existing value.
281
299
  // In this case, we can't simply merge the batched changes with the existing value, because then the null in the merge queue would have no effect
@@ -358,8 +376,14 @@ function mergeCollection(collectionKey, collection) {
358
376
  return true;
359
377
  });
360
378
  const existingKeys = keys.filter((key) => persistedKeys.has(key));
379
+ const cachedCollectionForExistingKeys = OnyxUtils_1.default.getCachedCollection(collectionKey, existingKeys);
361
380
  const newKeys = keys.filter((key) => !persistedKeys.has(key));
362
381
  const existingKeyCollection = existingKeys.reduce((obj, key) => {
382
+ const { isCompatible, existingValueType, newValueType } = utils_1.default.checkCompatibilityWithExistingValue(mergedCollection[key], cachedCollectionForExistingKeys[key]);
383
+ if (!isCompatible) {
384
+ Logger.logAlert(logMessages_1.default.incompatibleUpdateAlert(key, 'mergeCollection', existingValueType, newValueType));
385
+ return obj;
386
+ }
363
387
  // eslint-disable-next-line no-param-reassign
364
388
  obj[key] = mergedCollection[key];
365
389
  return obj;
@@ -385,11 +409,13 @@ function mergeCollection(collectionKey, collection) {
385
409
  if (keyValuePairsForNewCollection.length > 0) {
386
410
  promises.push(storage_1.default.multiSet(keyValuePairsForNewCollection));
387
411
  }
412
+ // finalMergedCollection contains all the keys that were merged, without the keys of incompatible updates
413
+ const finalMergedCollection = Object.assign(Object.assign({}, existingKeyCollection), newCollection);
388
414
  // Prefill cache if necessary by calling get() on any existing keys and then merge original data to cache
389
415
  // and update all subscribers
390
416
  const promiseUpdate = Promise.all(existingKeys.map(OnyxUtils_1.default.get)).then(() => {
391
- OnyxCache_1.default.merge(mergedCollection);
392
- return OnyxUtils_1.default.scheduleNotifyCollectionSubscribers(collectionKey, mergedCollection);
417
+ OnyxCache_1.default.merge(finalMergedCollection);
418
+ return OnyxUtils_1.default.scheduleNotifyCollectionSubscribers(collectionKey, finalMergedCollection);
393
419
  });
394
420
  return Promise.all(promises)
395
421
  .catch((error) => OnyxUtils_1.default.evictStorageAndRetry(error, mergeCollection, collectionKey, mergedCollection))
@@ -103,7 +103,7 @@ declare function addToEvictionBlockList(key: OnyxKey, connectionID: number): voi
103
103
  * removed.
104
104
  */
105
105
  declare function addAllSafeEvictionKeysToRecentlyAccessedList(): Promise<void>;
106
- declare function getCachedCollection<TKey extends CollectionKeyBase>(collectionKey: TKey): NonNullable<OnyxCollection<KeyValueMapping[TKey]>>;
106
+ declare function getCachedCollection<TKey extends CollectionKeyBase>(collectionKey: TKey, collectionMemberKeys?: string[]): NonNullable<OnyxCollection<KeyValueMapping[TKey]>>;
107
107
  /**
108
108
  * When a collection of keys change, search for any callbacks matching the collection key and trigger those callbacks
109
109
  */
package/dist/OnyxUtils.js CHANGED
@@ -323,9 +323,9 @@ function addAllSafeEvictionKeysToRecentlyAccessedList() {
323
323
  });
324
324
  });
325
325
  }
326
- function getCachedCollection(collectionKey) {
327
- const collectionMemberKeys = Array.from(OnyxCache_1.default.getAllKeys()).filter((storedKey) => isCollectionMemberKey(collectionKey, storedKey));
328
- return collectionMemberKeys.reduce((prev, key) => {
326
+ function getCachedCollection(collectionKey, collectionMemberKeys) {
327
+ const resolvedCollectionMemberKeys = collectionMemberKeys || Array.from(OnyxCache_1.default.getAllKeys()).filter((storedKey) => isCollectionMemberKey(collectionKey, storedKey));
328
+ return resolvedCollectionMemberKeys.reduce((prev, key) => {
329
329
  const cachedValue = OnyxCache_1.default.getValue(key);
330
330
  if (!cachedValue) {
331
331
  return prev;
@@ -0,0 +1,4 @@
1
+ declare const logMessages: {
2
+ incompatibleUpdateAlert: (key: string, operation: string, existingValueType?: string, newValueType?: string) => string;
3
+ };
4
+ export default logMessages;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const logMessages = {
4
+ incompatibleUpdateAlert: (key, operation, existingValueType, newValueType) => {
5
+ return `Warning: Trying to apply "${operation}" with ${newValueType !== null && newValueType !== void 0 ? newValueType : 'unknown'} type to ${existingValueType !== null && existingValueType !== void 0 ? existingValueType : 'unknown'} type in the key "${key}"`;
6
+ },
7
+ };
8
+ exports.default = logMessages;
package/dist/utils.d.ts CHANGED
@@ -15,10 +15,17 @@ declare function fastMerge<TObject extends Record<string, unknown>>(target: TObj
15
15
  declare function removeNestedNullValues<TObject extends Record<string, unknown>>(value: unknown | unknown[] | TObject): Record<string, unknown> | unknown[] | null;
16
16
  /** Formats the action name by uppercasing and adding the key if provided. */
17
17
  declare function formatActionName(method: string, key?: OnyxKey): string;
18
+ /** validate that the update and the existing value are compatible */
19
+ declare function checkCompatibilityWithExistingValue(value: unknown, existingValue: unknown): {
20
+ isCompatible: boolean;
21
+ existingValueType?: string;
22
+ newValueType?: string;
23
+ };
18
24
  declare const _default: {
19
25
  isEmptyObject: typeof isEmptyObject;
20
26
  fastMerge: typeof fastMerge;
21
27
  formatActionName: typeof formatActionName;
22
28
  removeNestedNullValues: typeof removeNestedNullValues;
29
+ checkCompatibilityWithExistingValue: typeof checkCompatibilityWithExistingValue;
23
30
  };
24
31
  export default _default;
package/dist/utils.js CHANGED
@@ -98,4 +98,24 @@ function removeNestedNullValues(value) {
98
98
  function formatActionName(method, key) {
99
99
  return key ? `${method.toUpperCase()}/${key}` : method.toUpperCase();
100
100
  }
101
- exports.default = { isEmptyObject, fastMerge, formatActionName, removeNestedNullValues };
101
+ /** validate that the update and the existing value are compatible */
102
+ function checkCompatibilityWithExistingValue(value, existingValue) {
103
+ if (!existingValue || !value) {
104
+ return {
105
+ isCompatible: true,
106
+ };
107
+ }
108
+ const existingValueType = Array.isArray(existingValue) ? 'array' : 'non-array';
109
+ const newValueType = Array.isArray(value) ? 'array' : 'non-array';
110
+ if (existingValueType !== newValueType) {
111
+ return {
112
+ isCompatible: false,
113
+ existingValueType,
114
+ newValueType,
115
+ };
116
+ }
117
+ return {
118
+ isCompatible: true,
119
+ };
120
+ }
121
+ exports.default = { isEmptyObject, fastMerge, formatActionName, removeNestedNullValues, checkCompatibilityWithExistingValue };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-onyx",
3
- "version": "2.0.33",
3
+ "version": "2.0.35",
4
4
  "author": "Expensify, Inc.",
5
5
  "homepage": "https://expensify.com",
6
6
  "description": "State management for React Native",