react-native-onyx 2.0.117 → 2.0.118

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/types.d.ts CHANGED
@@ -3,6 +3,7 @@ import type { BuiltIns } from 'type-fest/source/internal';
3
3
  import type OnyxUtils from './OnyxUtils';
4
4
  import type { WithOnyxInstance, WithOnyxState } from './withOnyx/types';
5
5
  import type { OnyxMethod } from './OnyxUtils';
6
+ import type { FastMergeReplaceNullPatch } from './utils';
6
7
  /**
7
8
  * Utility type that excludes `null` from the type `TValue`.
8
9
  */
@@ -412,11 +413,19 @@ type InitOptions = {
412
413
  fullyMergedSnapshotKeys?: string[];
413
414
  };
414
415
  type GenericFunction = (...args: any[]) => any;
416
+ /**
417
+ * Represents a record where the key is a collection member key and the value is a list of
418
+ * tuples that we'll use to replace the nested objects of that collection member record with something else.
419
+ */
420
+ type MultiMergeReplaceNullPatches = {
421
+ [TKey in OnyxKey]: FastMergeReplaceNullPatch[];
422
+ };
415
423
  /**
416
424
  * Represents a combination of Merge and Set operations that should be executed in Onyx
417
425
  */
418
426
  type MixedOperationsQueue = {
419
427
  merge: OnyxInputKeyValueMapping;
428
+ mergeReplaceNullPatches: MultiMergeReplaceNullPatches;
420
429
  set: OnyxInputKeyValueMapping;
421
430
  };
422
- export type { BaseConnectOptions, Collection, CollectionConnectCallback, CollectionConnectOptions, CollectionKey, CollectionKeyBase, ConnectOptions, CustomTypeOptions, DeepRecord, DefaultConnectCallback, DefaultConnectOptions, ExtractOnyxCollectionValue, GenericFunction, InitOptions, Key, KeyValueMapping, Mapping, NonNull, NonUndefined, OnyxInputKeyValueMapping, NullishDeep, OnyxCollection, OnyxEntry, OnyxKey, OnyxInputValue, OnyxCollectionInputValue, OnyxInput, OnyxSetInput, OnyxMultiSetInput, OnyxMergeInput, OnyxMergeCollectionInput, OnyxMethod, OnyxMethodMap, OnyxUpdate, OnyxValue, Selector, WithOnyxConnectOptions, MixedOperationsQueue, };
431
+ export type { BaseConnectOptions, Collection, CollectionConnectCallback, CollectionConnectOptions, CollectionKey, CollectionKeyBase, ConnectOptions, CustomTypeOptions, DeepRecord, DefaultConnectCallback, DefaultConnectOptions, ExtractOnyxCollectionValue, GenericFunction, InitOptions, Key, KeyValueMapping, Mapping, NonNull, NonUndefined, OnyxInputKeyValueMapping, NullishDeep, OnyxCollection, OnyxEntry, OnyxKey, OnyxInputValue, OnyxCollectionInputValue, OnyxInput, OnyxSetInput, OnyxMultiSetInput, OnyxMergeInput, OnyxMergeCollectionInput, OnyxMethod, OnyxMethodMap, OnyxUpdate, OnyxValue, Selector, WithOnyxConnectOptions, MultiMergeReplaceNullPatches, MixedOperationsQueue, };
package/dist/utils.d.ts CHANGED
@@ -1,16 +1,42 @@
1
1
  import type { ConnectOptions, OnyxInput, OnyxKey } from './types';
2
2
  type EmptyObject = Record<string, never>;
3
3
  type EmptyValue = EmptyObject | null | undefined;
4
- /** Checks whether the given object is an object and not null/undefined. */
5
- declare function isEmptyObject<T>(obj: T | EmptyValue): obj is EmptyValue;
4
+ /**
5
+ * A tuple where the first value is the path to the nested object that contains the
6
+ * internal `ONYX_INTERNALS__REPLACE_OBJECT_MARK` flag, and the second value is the data we want to replace
7
+ * in that path.
8
+ *
9
+ * This tuple will be used in SQLiteProvider to replace the nested object using `JSON_REPLACE`.
10
+ * */
11
+ type FastMergeReplaceNullPatch = [string[], unknown];
12
+ type FastMergeOptions = {
13
+ /** If true, null object values will be removed. */
14
+ shouldRemoveNestedNulls?: boolean;
15
+ /**
16
+ * If set to "mark", we will mark objects that are set to null instead of simply removing them,
17
+ * so that we can batch changes together, without losing information about the object removal.
18
+ * If set to "replace", we will completely replace the marked objects with the new value instead of merging them.
19
+ */
20
+ objectRemovalMode?: 'mark' | 'replace' | 'none';
21
+ };
22
+ type FastMergeMetadata = {
23
+ /** The list of tuples that will be used in SQLiteProvider to replace the nested objects using `JSON_REPLACE`. */
24
+ replaceNullPatches: FastMergeReplaceNullPatch[];
25
+ };
26
+ type FastMergeResult<TValue> = {
27
+ /** The result of the merge. */
28
+ result: TValue;
29
+ /** The list of tuples that will be used in SQLiteProvider to replace the nested objects using `JSON_REPLACE`. */
30
+ replaceNullPatches: FastMergeReplaceNullPatch[];
31
+ };
6
32
  /**
7
33
  * Merges two objects and removes null values if "shouldRemoveNestedNulls" is set to true
8
34
  *
9
35
  * We generally want to remove null values from objects written to disk and cache, because it decreases the amount of data stored in memory and on disk.
10
- * On native, when merging an existing value with new changes, SQLite will use JSON_PATCH, which removes top-level nullish values.
11
- * To be consistent with the behaviour for merge, we'll also want to remove null values for "set" operations.
12
36
  */
13
- declare function fastMerge<TValue>(target: TValue, source: TValue, shouldRemoveNestedNulls?: boolean): TValue;
37
+ declare function fastMerge<TValue>(target: TValue, source: TValue, options?: FastMergeOptions, metadata?: FastMergeMetadata, basePath?: string[]): FastMergeResult<TValue>;
38
+ /** Checks whether the given object is an object and not null/undefined. */
39
+ declare function isEmptyObject<T>(obj: T | EmptyValue): obj is EmptyValue;
14
40
  /** Deep removes the nested null values from the given value. */
15
41
  declare function removeNestedNullValues<TValue extends OnyxInput<OnyxKey> | null>(value: TValue): TValue;
16
42
  /** Formats the action name by uppercasing and adding the key if provided. */
@@ -42,13 +68,15 @@ declare function omit<TValue>(obj: Record<string, TValue>, condition: string | s
42
68
  */
43
69
  declare function hasWithOnyxInstance<TKey extends OnyxKey>(mapping: ConnectOptions<TKey>): unknown;
44
70
  declare const _default: {
45
- isEmptyObject: typeof isEmptyObject;
46
71
  fastMerge: typeof fastMerge;
72
+ isEmptyObject: typeof isEmptyObject;
47
73
  formatActionName: typeof formatActionName;
48
74
  removeNestedNullValues: typeof removeNestedNullValues;
49
75
  checkCompatibilityWithExistingValue: typeof checkCompatibilityWithExistingValue;
50
76
  pick: typeof pick;
51
77
  omit: typeof omit;
52
78
  hasWithOnyxInstance: typeof hasWithOnyxInstance;
79
+ ONYX_INTERNALS__REPLACE_OBJECT_MARK: string;
53
80
  };
54
81
  export default _default;
82
+ export type { FastMergeResult, FastMergeReplaceNullPatch, FastMergeOptions };
package/dist/utils.js CHANGED
@@ -1,25 +1,42 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- /** Checks whether the given object is an object and not null/undefined. */
4
- function isEmptyObject(obj) {
5
- return typeof obj === 'object' && Object.keys(obj || {}).length === 0;
6
- }
7
- // Mostly copied from https://medium.com/@lubaka.a/how-to-remove-lodash-performance-improvement-b306669ad0e1
3
+ const ONYX_INTERNALS__REPLACE_OBJECT_MARK = 'ONYX_INTERNALS__REPLACE_OBJECT_MARK';
8
4
  /**
9
- * Checks whether the given value can be merged. It has to be an object, but not an array, RegExp or Date.
5
+ * Merges two objects and removes null values if "shouldRemoveNestedNulls" is set to true
6
+ *
7
+ * We generally want to remove null values from objects written to disk and cache, because it decreases the amount of data stored in memory and on disk.
10
8
  */
11
- function isMergeableObject(value) {
12
- const isNonNullObject = value != null ? typeof value === 'object' : false;
13
- return isNonNullObject && !(value instanceof RegExp) && !(value instanceof Date) && !Array.isArray(value);
9
+ function fastMerge(target, source, options, metadata, basePath = []) {
10
+ var _a, _b;
11
+ if (!metadata) {
12
+ // eslint-disable-next-line no-param-reassign
13
+ metadata = {
14
+ replaceNullPatches: [],
15
+ };
16
+ }
17
+ // We have to ignore arrays and nullish values here,
18
+ // otherwise "mergeObject" will throw an error,
19
+ // because it expects an object as "source"
20
+ if (Array.isArray(source) || source === null || source === undefined) {
21
+ return { result: source, replaceNullPatches: metadata.replaceNullPatches };
22
+ }
23
+ const optionsWithDefaults = {
24
+ shouldRemoveNestedNulls: (_a = options === null || options === void 0 ? void 0 : options.shouldRemoveNestedNulls) !== null && _a !== void 0 ? _a : false,
25
+ objectRemovalMode: (_b = options === null || options === void 0 ? void 0 : options.objectRemovalMode) !== null && _b !== void 0 ? _b : 'none',
26
+ };
27
+ const mergedValue = mergeObject(target, source, optionsWithDefaults, metadata, basePath);
28
+ return { result: mergedValue, replaceNullPatches: metadata.replaceNullPatches };
14
29
  }
15
30
  /**
16
31
  * Merges the source object into the target object.
17
32
  * @param target - The target object.
18
33
  * @param source - The source object.
19
- * @param shouldRemoveNestedNulls - If true, null object values will be removed.
34
+ * @param options - The options for the merge.
35
+ * @param metadata - The metadata for the merge.
36
+ * @param basePath - The base path for the merge.
20
37
  * @returns - The merged object.
21
38
  */
22
- function mergeObject(target, source, shouldRemoveNestedNulls = true) {
39
+ function mergeObject(target, source, options, metadata, basePath) {
23
40
  const destination = {};
24
41
  const targetObject = isMergeableObject(target) ? target : undefined;
25
42
  // First we want to copy over all keys from the target into the destination object,
@@ -27,66 +44,67 @@ function mergeObject(target, source, shouldRemoveNestedNulls = true) {
27
44
  // If "shouldRemoveNestedNulls" is true, we want to remove null values from the merged object
28
45
  // and therefore we need to omit keys where either the source or target value is null.
29
46
  if (targetObject) {
30
- // eslint-disable-next-line no-restricted-syntax, guard-for-in
31
- for (const key in targetObject) {
32
- const sourceValue = source === null || source === void 0 ? void 0 : source[key];
33
- const targetValue = targetObject === null || targetObject === void 0 ? void 0 : targetObject[key];
34
- // If "shouldRemoveNestedNulls" is true, we want to remove null values from the merged object.
35
- // Therefore, if either target or source value is null, we want to prevent the key from being set.
36
- // targetValue should techincally never be "undefined", because it will always be a value from cache or storage
37
- // and we never set "undefined" there. Still, if there targetValue is undefined we don't want to set
38
- // the key explicitly to prevent loose undefined values in objects in cache and storage.
39
- const isSourceOrTargetNull = targetValue === undefined || targetValue === null || sourceValue === null;
40
- const shouldOmitTargetKey = shouldRemoveNestedNulls && isSourceOrTargetNull;
41
- if (!shouldOmitTargetKey) {
42
- destination[key] = targetValue;
47
+ Object.keys(targetObject).forEach((key) => {
48
+ const targetProperty = targetObject === null || targetObject === void 0 ? void 0 : targetObject[key];
49
+ const sourceProperty = source === null || source === void 0 ? void 0 : source[key];
50
+ // If "shouldRemoveNestedNulls" is true, we want to remove (nested) null values from the merged object.
51
+ // If either the source or target value is null, we want to omit the key from the merged object.
52
+ const shouldOmitNullishProperty = options.shouldRemoveNestedNulls && (targetProperty === null || sourceProperty === null);
53
+ if (targetProperty === undefined || shouldOmitNullishProperty) {
54
+ return;
43
55
  }
44
- }
56
+ destination[key] = targetProperty;
57
+ });
45
58
  }
46
59
  // After copying over all keys from the target object, we want to merge the source object into the destination object.
47
- // eslint-disable-next-line no-restricted-syntax, guard-for-in
48
- for (const key in source) {
49
- const sourceValue = source === null || source === void 0 ? void 0 : source[key];
50
- const targetValue = targetObject === null || targetObject === void 0 ? void 0 : targetObject[key];
51
- // If undefined is passed as the source value for a key, we want to generally ignore it.
52
- // If "shouldRemoveNestedNulls" is set to true and the source value is null,
53
- // we don't want to set/merge the source value into the merged object.
54
- const shouldIgnoreNullSourceValue = shouldRemoveNestedNulls && sourceValue === null;
55
- const shouldOmitSourceKey = sourceValue === undefined || shouldIgnoreNullSourceValue;
56
- if (!shouldOmitSourceKey) {
57
- // If the source value is a mergable object, we want to merge it into the target value.
58
- // If "shouldRemoveNestedNulls" is true, "fastMerge" will recursively
59
- // remove nested null values from the merged object.
60
- // If source value is any other value we need to set the source value it directly.
61
- if (isMergeableObject(sourceValue)) {
62
- // If the target value is null or undefined, we need to fallback to an empty object,
63
- // so that we can still use "fastMerge" to merge the source value,
64
- // to ensure that nested null values are removed from the merged object.
65
- const targetValueWithFallback = (targetValue !== null && targetValue !== void 0 ? targetValue : {});
66
- destination[key] = fastMerge(targetValueWithFallback, sourceValue, shouldRemoveNestedNulls);
67
- }
68
- else {
69
- destination[key] = sourceValue;
70
- }
60
+ Object.keys(source).forEach((key) => {
61
+ let targetProperty = targetObject === null || targetObject === void 0 ? void 0 : targetObject[key];
62
+ const sourceProperty = source === null || source === void 0 ? void 0 : source[key];
63
+ // If "shouldRemoveNestedNulls" is true, we want to remove (nested) null values from the merged object.
64
+ // If the source value is null, we want to omit the key from the merged object.
65
+ const shouldOmitNullishProperty = options.shouldRemoveNestedNulls && sourceProperty === null;
66
+ if (sourceProperty === undefined || shouldOmitNullishProperty) {
67
+ return;
71
68
  }
72
- }
69
+ // If the source value is not a mergable object, we need to set the key directly.
70
+ if (!isMergeableObject(sourceProperty)) {
71
+ destination[key] = sourceProperty;
72
+ return;
73
+ }
74
+ // If "shouldMarkRemovedObjects" is enabled and the previous merge change (targetProperty) is null,
75
+ // it means we want to fully replace this object when merging the batched changes with the Onyx value.
76
+ // To achieve this, we first mark these nested objects with an internal flag.
77
+ // When calling fastMerge again with "mark" removal mode, the marked objects will be removed.
78
+ if (options.objectRemovalMode === 'mark' && targetProperty === null) {
79
+ targetProperty = { [ONYX_INTERNALS__REPLACE_OBJECT_MARK]: true };
80
+ metadata.replaceNullPatches.push([[...basePath, key], Object.assign({}, sourceProperty)]);
81
+ }
82
+ // Later, when merging the batched changes with the Onyx value, if a nested object of the batched changes
83
+ // has the internal flag set, we replace the entire destination object with the source one and remove
84
+ // the flag.
85
+ if (options.objectRemovalMode === 'replace' && sourceProperty[ONYX_INTERNALS__REPLACE_OBJECT_MARK]) {
86
+ // We do a spread here in order to have a new object reference and allow us to delete the internal flag
87
+ // of the merged object only.
88
+ const sourcePropertyWithoutMark = Object.assign({}, sourceProperty);
89
+ delete sourcePropertyWithoutMark.ONYX_INTERNALS__REPLACE_OBJECT_MARK;
90
+ destination[key] = sourcePropertyWithoutMark;
91
+ return;
92
+ }
93
+ destination[key] = fastMerge(targetProperty, sourceProperty, options, metadata, [...basePath, key]).result;
94
+ });
73
95
  return destination;
74
96
  }
97
+ /** Checks whether the given object is an object and not null/undefined. */
98
+ function isEmptyObject(obj) {
99
+ return typeof obj === 'object' && Object.keys(obj || {}).length === 0;
100
+ }
75
101
  /**
76
- * Merges two objects and removes null values if "shouldRemoveNestedNulls" is set to true
77
- *
78
- * We generally want to remove null values from objects written to disk and cache, because it decreases the amount of data stored in memory and on disk.
79
- * On native, when merging an existing value with new changes, SQLite will use JSON_PATCH, which removes top-level nullish values.
80
- * To be consistent with the behaviour for merge, we'll also want to remove null values for "set" operations.
102
+ * Checks whether the given value can be merged. It has to be an object, but not an array, RegExp or Date.
103
+ * Mostly copied from https://medium.com/@lubaka.a/how-to-remove-lodash-performance-improvement-b306669ad0e1.
81
104
  */
82
- function fastMerge(target, source, shouldRemoveNestedNulls = true) {
83
- // We have to ignore arrays and nullish values here,
84
- // otherwise "mergeObject" will throw an error,
85
- // because it expects an object as "source"
86
- if (Array.isArray(source) || source === null || source === undefined) {
87
- return source;
88
- }
89
- return mergeObject(target, source, shouldRemoveNestedNulls);
105
+ function isMergeableObject(value) {
106
+ const isNonNullObject = value != null ? typeof value === 'object' : false;
107
+ return isNonNullObject && !(value instanceof RegExp) && !(value instanceof Date) && !Array.isArray(value);
90
108
  }
91
109
  /** Deep removes the nested null values from the given value. */
92
110
  function removeNestedNullValues(value) {
@@ -192,4 +210,14 @@ function omit(obj, condition) {
192
210
  function hasWithOnyxInstance(mapping) {
193
211
  return 'withOnyxInstance' in mapping && mapping.withOnyxInstance;
194
212
  }
195
- exports.default = { isEmptyObject, fastMerge, formatActionName, removeNestedNullValues, checkCompatibilityWithExistingValue, pick, omit, hasWithOnyxInstance };
213
+ exports.default = {
214
+ fastMerge,
215
+ isEmptyObject,
216
+ formatActionName,
217
+ removeNestedNullValues,
218
+ checkCompatibilityWithExistingValue,
219
+ pick,
220
+ omit,
221
+ hasWithOnyxInstance,
222
+ ONYX_INTERNALS__REPLACE_OBJECT_MARK,
223
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-onyx",
3
- "version": "2.0.117",
3
+ "version": "2.0.118",
4
4
  "author": "Expensify, Inc.",
5
5
  "homepage": "https://expensify.com",
6
6
  "description": "State management for React Native",