Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions lib/Onyx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ function merge<TKey extends OnyxKey>(key: TKey, changes: OnyxMergeInput<TKey>):
if (!validChanges.length) {
return Promise.resolve();
}
const batchedDeltaChanges = OnyxUtils.applyMerge(undefined, validChanges, false);
const batchedDeltaChanges = OnyxUtils.applyMerge(undefined, validChanges, false, true);

// Case (1): When there is no existing value in storage, we want to set the value instead of merge it.
// Case (2): The presence of a top-level `null` in the merge queue instructs us to drop the whole existing value.
Expand Down Expand Up @@ -350,7 +350,7 @@ function merge<TKey extends OnyxKey>(key: TKey, changes: OnyxMergeInput<TKey>):
// The "preMergedValue" will be directly "set" in storage instead of being merged
// Therefore we merge the batched changes with the existing value to get the final merged value that will be stored.
// We can remove null values from the "preMergedValue", because "null" implicates that the user wants to remove a value from storage.
const preMergedValue = OnyxUtils.applyMerge(shouldSetValue ? undefined : existingValue, [batchedDeltaChanges], true);
const preMergedValue = OnyxUtils.applyMerge(shouldSetValue ? undefined : existingValue, [batchedDeltaChanges], true, false);

// In cache, we don't want to remove the key if it's null to improve performance and speed up the next merge.
const hasChanged = cache.hasValueChanged(key, preMergedValue);
Expand Down Expand Up @@ -765,7 +765,7 @@ function update(data: OnyxUpdate[]): Promise<void> {
// Remove the collection-related key from the updateQueue so that it won't be processed individually.
delete updateQueue[key];

const updatedValue = OnyxUtils.applyMerge(undefined, operations, false);
const updatedValue = OnyxUtils.applyMerge(undefined, operations, false, true);
if (operations[0] === null) {
// eslint-disable-next-line no-param-reassign
queue.set[key] = updatedValue;
Expand All @@ -790,7 +790,7 @@ function update(data: OnyxUpdate[]): Promise<void> {
});

Object.entries(updateQueue).forEach(([key, operations]) => {
const batchedChanges = OnyxUtils.applyMerge(undefined, operations, false);
const batchedChanges = OnyxUtils.applyMerge(undefined, operations, false, true);

if (operations[0] === null) {
promises.push(() => set(key, batchedChanges));
Expand Down
3 changes: 2 additions & 1 deletion lib/OnyxUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1249,6 +1249,7 @@ function applyMerge<TValue extends OnyxInput<OnyxKey> | undefined, TChange exten
existingValue: TValue,
changes: TChange[],
shouldRemoveNestedNulls: boolean,
isBatchingMergeChanges: boolean,
): TChange {
const lastChange = changes?.at(-1);

Expand All @@ -1258,7 +1259,7 @@ function applyMerge<TValue extends OnyxInput<OnyxKey> | undefined, TChange exten

if (changes.some((change) => change && typeof change === 'object')) {
// Object values are then merged one after the other
return changes.reduce((modifiedData, change) => utils.fastMerge(modifiedData, change, shouldRemoveNestedNulls), (existingValue || {}) as TChange);
return changes.reduce((modifiedData, change) => utils.fastMerge(modifiedData, change, shouldRemoveNestedNulls, isBatchingMergeChanges), (existingValue || {}) as TChange);
}

// If we have anything else we can't merge it so we'll
Expand Down
27 changes: 19 additions & 8 deletions lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ function isMergeableObject(value: unknown): value is Record<string, unknown> {
* @param shouldRemoveNestedNulls - If true, null object values will be removed.
* @returns - The merged object.
*/
function mergeObject<TObject extends Record<string, unknown>>(target: TObject | unknown | null | undefined, source: TObject, shouldRemoveNestedNulls = true): TObject {
function mergeObject<TObject extends Record<string, unknown>>(
target: TObject | unknown | null | undefined,
source: TObject,
shouldRemoveNestedNulls = true,
isBatchingMergeChanges = false,
): TObject {
const destination: Record<string, unknown> = {};

const targetObject = isMergeableObject(target) ? target : undefined;
Expand Down Expand Up @@ -74,11 +79,17 @@ function mergeObject<TObject extends Record<string, unknown>>(target: TObject |
// remove nested null values from the merged object.
// If source value is any other value we need to set the source value it directly.
if (isMergeableObject(sourceValue)) {
// If the target value is null or undefined, we need to fallback to an empty object,
// so that we can still use "fastMerge" to merge the source value,
// to ensure that nested null values are removed from the merged object.
const targetValueWithFallback = (targetValue ?? {}) as TObject;
destination[key] = fastMerge(targetValueWithFallback, sourceValue, shouldRemoveNestedNulls);
if (isBatchingMergeChanges && targetValue === null) {
destination[key] = null;
} else {
// If the target value is null or undefined, we need to fallback to an empty object,
// so that we can still use "fastMerge" to merge the source value,
// to ensure that nested null values are removed from the merged object.
const targetValueWithFallback = (targetValue ?? {}) as TObject;
destination[key] = fastMerge(targetValueWithFallback, sourceValue, shouldRemoveNestedNulls, isBatchingMergeChanges);
}
} else if (isBatchingMergeChanges && destination[key] === null) {
destination[key] = null;
} else {
destination[key] = sourceValue;
}
Expand All @@ -95,15 +106,15 @@ function mergeObject<TObject extends Record<string, unknown>>(target: TObject |
* On native, when merging an existing value with new changes, SQLite will use JSON_PATCH, which removes top-level nullish values.
* To be consistent with the behaviour for merge, we'll also want to remove null values for "set" operations.
*/
function fastMerge<TValue>(target: TValue, source: TValue, shouldRemoveNestedNulls = true): TValue {
function fastMerge<TValue>(target: TValue, source: TValue, shouldRemoveNestedNulls = true, isBatchingMergeChanges = false): TValue {
// 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 source;
}

return mergeObject(target, source as Record<string, unknown>, shouldRemoveNestedNulls) as TValue;
return mergeObject(target, source as Record<string, unknown>, shouldRemoveNestedNulls, isBatchingMergeChanges) as TValue;
}

/** Deep removes the nested null values from the given value. */
Expand Down
Loading