UNPKG

react-native-onyx

Version:

State management for React Native

224 lines (223 loc) 10.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const ONYX_INTERNALS__REPLACE_OBJECT_MARK = 'ONYX_INTERNALS__REPLACE_OBJECT_MARK'; /** * Merges two objects and removes null values if "shouldRemoveNestedNulls" is set to true * * 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. */ function fastMerge(target, source, options, metadata, basePath = []) { var _a, _b; if (!metadata) { // eslint-disable-next-line no-param-reassign metadata = { replaceNullPatches: [], }; } // We have to ignore arrays and nullish values here, // otherwise "mergeObject" will throw an error, // because it expects an object as "source" if (Array.isArray(source) || source === null || source === undefined) { return { result: source, replaceNullPatches: metadata.replaceNullPatches }; } const optionsWithDefaults = { shouldRemoveNestedNulls: (_a = options === null || options === void 0 ? void 0 : options.shouldRemoveNestedNulls) !== null && _a !== void 0 ? _a : false, objectRemovalMode: (_b = options === null || options === void 0 ? void 0 : options.objectRemovalMode) !== null && _b !== void 0 ? _b : 'none', }; const mergedValue = mergeObject(target, source, optionsWithDefaults, metadata, basePath); return { result: mergedValue, replaceNullPatches: metadata.replaceNullPatches }; } /** * Merges the source object into the target object. * @param target - The target object. * @param source - The source object. * @param options - The options for the merge. * @param metadata - The metadata for the merge. * @param basePath - The base path for the merge. * @returns - The merged object. */ function mergeObject(target, source, options, metadata, basePath) { const destination = {}; const targetObject = isMergeableObject(target) ? target : undefined; // First we want to copy over all keys from the target into the destination object, // in case "target" is a mergable object. // If "shouldRemoveNestedNulls" is true, we want to remove null values from the merged object // and therefore we need to omit keys where either the source or target value is null. if (targetObject) { Object.keys(targetObject).forEach((key) => { const targetProperty = targetObject === null || targetObject === void 0 ? void 0 : targetObject[key]; const sourceProperty = source === null || source === void 0 ? void 0 : source[key]; // If "shouldRemoveNestedNulls" is true, we want to remove (nested) null values from the merged object. // If either the source or target value is null, we want to omit the key from the merged object. const shouldOmitNullishProperty = options.shouldRemoveNestedNulls && (targetProperty === null || sourceProperty === null); if (targetProperty === undefined || shouldOmitNullishProperty) { return; } destination[key] = targetProperty; }); } // After copying over all keys from the target object, we want to merge the source object into the destination object. Object.keys(source).forEach((key) => { let targetProperty = targetObject === null || targetObject === void 0 ? void 0 : targetObject[key]; const sourceProperty = source === null || source === void 0 ? void 0 : source[key]; // If "shouldRemoveNestedNulls" is true, we want to remove (nested) null values from the merged object. // If the source value is null, we want to omit the key from the merged object. const shouldOmitNullishProperty = options.shouldRemoveNestedNulls && sourceProperty === null; if (sourceProperty === undefined || shouldOmitNullishProperty) { return; } // If the source value is not a mergable object, we need to set the key directly. if (!isMergeableObject(sourceProperty)) { destination[key] = sourceProperty; return; } // If "shouldMarkRemovedObjects" is enabled and the previous merge change (targetProperty) is null, // it means we want to fully replace this object when merging the batched changes with the Onyx value. // To achieve this, we first mark these nested objects with an internal flag. // When calling fastMerge again with "mark" removal mode, the marked objects will be removed. if (options.objectRemovalMode === 'mark' && targetProperty === null) { targetProperty = { [ONYX_INTERNALS__REPLACE_OBJECT_MARK]: true }; metadata.replaceNullPatches.push([[...basePath, key], Object.assign({}, sourceProperty)]); } // Later, when merging the batched changes with the Onyx value, if a nested object of the batched changes // has the internal flag set, we replace the entire destination object with the source one and remove // the flag. if (options.objectRemovalMode === 'replace' && sourceProperty[ONYX_INTERNALS__REPLACE_OBJECT_MARK]) { // We do a spread here in order to have a new object reference and allow us to delete the internal flag // of the merged object only. const sourcePropertyWithoutMark = Object.assign({}, sourceProperty); delete sourcePropertyWithoutMark.ONYX_INTERNALS__REPLACE_OBJECT_MARK; destination[key] = sourcePropertyWithoutMark; return; } destination[key] = fastMerge(targetProperty, sourceProperty, options, metadata, [...basePath, key]).result; }); return destination; } /** Checks whether the given object is an object and not null/undefined. */ function isEmptyObject(obj) { return typeof obj === 'object' && Object.keys(obj || {}).length === 0; } /** * Checks whether the given value can be merged. It has to be an object, but not an array, RegExp or Date. * Mostly copied from https://medium.com/@lubaka.a/how-to-remove-lodash-performance-improvement-b306669ad0e1. */ function isMergeableObject(value) { const isNonNullObject = value != null ? typeof value === 'object' : false; return isNonNullObject && !(value instanceof RegExp) && !(value instanceof Date) && !Array.isArray(value); } /** Deep removes the nested null values from the given value. */ function removeNestedNullValues(value) { if (value === null || value === undefined || typeof value !== 'object') { return value; } if (Array.isArray(value)) { return [...value]; } const result = {}; // eslint-disable-next-line no-restricted-syntax, guard-for-in for (const key in value) { const propertyValue = value[key]; if (propertyValue === null || propertyValue === undefined) { // eslint-disable-next-line no-continue continue; } if (typeof propertyValue === 'object' && !Array.isArray(propertyValue)) { const valueWithoutNestedNulls = removeNestedNullValues(propertyValue); result[key] = valueWithoutNestedNulls; } else { result[key] = propertyValue; } } return result; } /** Formats the action name by uppercasing and adding the key if provided. */ function formatActionName(method, key) { return key ? `${method.toUpperCase()}/${key}` : method.toUpperCase(); } /** validate that the update and the existing value are compatible */ function checkCompatibilityWithExistingValue(value, existingValue) { if (!existingValue || !value) { return { isCompatible: true, }; } const existingValueType = Array.isArray(existingValue) ? 'array' : 'non-array'; const newValueType = Array.isArray(value) ? 'array' : 'non-array'; if (existingValueType !== newValueType) { return { isCompatible: false, existingValueType, newValueType, }; } return { isCompatible: true, }; } /** * Filters an object based on a condition and an inclusion flag. * * @param obj - The object to filter. * @param condition - The condition to apply. * @param include - If true, include entries that match the condition; otherwise, exclude them. * @returns The filtered object. */ function filterObject(obj, condition, include) { const result = {}; const entries = Object.entries(obj); for (const [key, value] of entries) { let shouldInclude; if (Array.isArray(condition)) { shouldInclude = condition.includes(key); } else if (typeof condition === 'string') { shouldInclude = key === condition; } else { shouldInclude = condition([key, value]); } if (include ? shouldInclude : !shouldInclude) { result[key] = value; } } return result; } /** * Picks entries from an object based on a condition. * * @param obj - The object to pick entries from. * @param condition - The condition to determine which entries to pick. * @returns The object containing only the picked entries. */ function pick(obj, condition) { return filterObject(obj, condition, true); } /** * Omits entries from an object based on a condition. * * @param obj - The object to omit entries from. * @param condition - The condition to determine which entries to omit. * @returns The object containing only the remaining entries after omission. */ function omit(obj, condition) { return filterObject(obj, condition, false); } /** * Whether the connect options has the `withOnyxInstance` property defined, that is, it's used by the `withOnyx()` HOC. */ function hasWithOnyxInstance(mapping) { return 'withOnyxInstance' in mapping && mapping.withOnyxInstance; } exports.default = { fastMerge, isEmptyObject, formatActionName, removeNestedNullValues, checkCompatibilityWithExistingValue, pick, omit, hasWithOnyxInstance, ONYX_INTERNALS__REPLACE_OBJECT_MARK, };