Storage expects array like: [["@MyApp_user", value_1], ["@MyApp_key", value_2]]
This method transforms an object like {'@MyApp_user': myUserValue, '@MyApp_key': myKeyValue}
@@ -464,14 +459,6 @@ whatever it is we attempted to do.
## broadcastUpdate()
Notifies subscribers and writes current value to cache
-**Kind**: global function
-
-
-## removeNullValues() ⇒
-Removes a key from storage if the value is null.
-Otherwise removes all nested null values in objects,
-if shouldRemoveNestedNulls is true and returns the object.
-
**Kind**: global function
**Returns**: The value without null values and a boolean "wasRemoved", which indicates if the key got removed completely
diff --git a/lib/Onyx.ts b/lib/Onyx.ts
index 51e655ac9..3039b611a 100644
--- a/lib/Onyx.ts
+++ b/lib/Onyx.ts
@@ -27,6 +27,7 @@ import type {
OnyxValue,
OnyxInput,
OnyxMethodMap,
+ MultiMergeReplaceNullPatches,
} from './types';
import OnyxUtils from './OnyxUtils';
import logMessages from './logMessages';
@@ -169,38 +170,31 @@ function set(key: TKey, value: OnyxSetInput): Promis
return Promise.resolve();
}
- // If the value is null, we remove the key from storage
- const {value: valueAfterRemoving, wasRemoved} = OnyxUtils.removeNullValues(key, value);
-
- const logSetCall = (hasChanged = true) => {
- // Logging properties only since values could be sensitive things we don't want to log
- Logger.logInfo(`set called for key: ${key}${_.isObject(value) ? ` properties: ${_.keys(value).join(',')}` : ''} hasChanged: ${hasChanged}`);
- };
-
- // Calling "OnyxUtils.removeNullValues" removes the key from storage and cache and updates the subscriber.
+ // If the change is null, we can just delete the key.
// Therefore, we don't need to further broadcast and update the value so we can return early.
- if (wasRemoved) {
- logSetCall();
+ if (value === null) {
+ OnyxUtils.remove(key);
+ Logger.logInfo(`set called for key: ${key} => null passed, so key was removed`);
return Promise.resolve();
}
- const valueWithoutNullValues = valueAfterRemoving as OnyxValue;
- const hasChanged = cache.hasValueChanged(key, valueWithoutNullValues);
+ const valueWithoutNestedNullValues = utils.removeNestedNullValues(value) as OnyxValue;
+ const hasChanged = cache.hasValueChanged(key, valueWithoutNestedNullValues);
- logSetCall(hasChanged);
+ Logger.logInfo(`set called for key: ${key}${_.isObject(value) ? ` properties: ${_.keys(value).join(',')}` : ''} hasChanged: ${hasChanged}`);
// This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
- const updatePromise = OnyxUtils.broadcastUpdate(key, valueWithoutNullValues, hasChanged);
+ const updatePromise = OnyxUtils.broadcastUpdate(key, valueWithoutNestedNullValues, hasChanged);
// If the value has not changed or the key got removed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
if (!hasChanged) {
return updatePromise;
}
- return Storage.setItem(key, valueWithoutNullValues)
- .catch((error) => OnyxUtils.evictStorageAndRetry(error, set, key, valueWithoutNullValues))
+ return Storage.setItem(key, valueWithoutNestedNullValues)
+ .catch((error) => OnyxUtils.evictStorageAndRetry(error, set, key, valueWithoutNestedNullValues))
.then(() => {
- OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.SET, key, valueWithoutNullValues);
+ OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.SET, key, valueWithoutNestedNullValues);
return updatePromise;
});
}
@@ -323,10 +317,10 @@ function merge(key: TKey, changes: OnyxMergeInput):
delete mergeQueue[key];
delete mergeQueuePromise[key];
- // Calling "OnyxUtils.remove" removes the key from storage and cache and updates the subscriber.
+ // If the last change is null, we can just delete the key.
// Therefore, we don't need to further broadcast and update the value so we can return early.
if (validChanges.at(-1) === null) {
- Logger.logInfo(`merge called for key: ${key} was removed`);
+ Logger.logInfo(`merge called for key: ${key} => null passed, so key was removed`);
OnyxUtils.remove(key);
return Promise.resolve();
}
@@ -376,7 +370,7 @@ function merge(key: TKey, changes: OnyxMergeInput):
function mergeCollection(
collectionKey: TKey,
collection: OnyxMergeCollectionInput,
- mergeReplaceNullPatches?: MixedOperationsQueue['mergeReplaceNullPatches'],
+ mergeReplaceNullPatches?: MultiMergeReplaceNullPatches,
): Promise {
if (!OnyxUtils.isValidNonEmptyCollectionForMerge(collection)) {
Logger.logInfo('mergeCollection() called with invalid or empty value. Skipping this update.');
@@ -449,7 +443,7 @@ function mergeCollection(
// When (multi-)merging the values with the existing values in storage,
// we don't want to remove nested null values from the data that we pass to the storage layer,
// because the storage layer uses them to remove nested keys from storage natively.
- const keyValuePairsForExistingCollection = OnyxUtils.prepareKeyValuePairsForStorage(existingKeyCollection, false);
+ const keyValuePairsForExistingCollection = OnyxUtils.prepareKeyValuePairsForStorage(existingKeyCollection, false, mergeReplaceNullPatches);
// We can safely remove nested null values when using (multi-)set,
// because we will simply overwrite the existing values in storage.
@@ -464,7 +458,7 @@ function mergeCollection(
// New keys will be added via multiSet while existing keys will be updated using multiMerge
// This is because setting a key that doesn't exist yet with multiMerge will throw errors
if (keyValuePairsForExistingCollection.length > 0) {
- promises.push(Storage.multiMerge(keyValuePairsForExistingCollection, mergeReplaceNullPatches));
+ promises.push(Storage.multiMerge(keyValuePairsForExistingCollection));
}
if (keyValuePairsForNewCollection.length > 0) {
diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts
index 1003d3ae1..e89769404 100644
--- a/lib/OnyxUtils.ts
+++ b/lib/OnyxUtils.ts
@@ -20,6 +20,7 @@ import type {
DefaultConnectOptions,
KeyValueMapping,
Mapping,
+ MultiMergeReplaceNullPatches,
OnyxCollection,
OnyxEntry,
OnyxInput,
@@ -35,6 +36,7 @@ import type {DeferredTask} from './createDeferredTask';
import createDeferredTask from './createDeferredTask';
import * as GlobalSettings from './GlobalSettings';
import decorateWithMetrics from './metrics';
+import type {StorageKeyValuePair} from './storage/providers/types';
// Method constants
const METHOD = {
@@ -1204,34 +1206,6 @@ function hasPendingMergeForKey(key: OnyxKey): boolean {
return !!mergeQueue[key];
}
-type RemoveNullValuesOutput | undefined> = {
- value: Value;
- wasRemoved: boolean;
-};
-
-/**
- * Removes a key from storage if the value is null.
- * Otherwise removes all nested null values in objects,
- * if shouldRemoveNestedNulls is true and returns the object.
- *
- * @returns The value without null values and a boolean "wasRemoved", which indicates if the key got removed completely
- */
-function removeNullValues | undefined>(key: OnyxKey, value: Value, shouldRemoveNestedNulls = true): RemoveNullValuesOutput {
- if (value === null) {
- remove(key);
- return {value, wasRemoved: true};
- }
-
- if (value === undefined) {
- return {value, wasRemoved: false};
- }
-
- // We can remove all null values in an object by merging it with itself
- // utils.fastMerge recursively goes through the object and removes all null values
- // Passing two identical objects as source and target to fastMerge will not change it, but only remove the null values
- return {value: shouldRemoveNestedNulls ? utils.removeNestedNullValues(value) : value, wasRemoved: false};
-}
-
/**
* Storage expects array like: [["@MyApp_user", value_1], ["@MyApp_key", value_2]]
* This method transforms an object like {'@MyApp_user': myUserValue, '@MyApp_key': myKeyValue}
@@ -1239,16 +1213,27 @@ function removeNullValues | undefined>(key: Ony
* @return an array of key - value pairs <[key, value]>
*/
-function prepareKeyValuePairsForStorage(data: Record>, shouldRemoveNestedNulls: boolean): Array<[OnyxKey, OnyxInput]> {
- return Object.entries(data).reduce]>>((pairs, [key, value]) => {
- const {value: valueAfterRemoving, wasRemoved} = removeNullValues(key, value, shouldRemoveNestedNulls);
+function prepareKeyValuePairsForStorage(
+ data: Record>,
+ shouldRemoveNestedNulls?: boolean,
+ replaceNullPatches?: MultiMergeReplaceNullPatches,
+): StorageKeyValuePair[] {
+ const pairs: StorageKeyValuePair[] = [];
+
+ Object.entries(data).forEach(([key, value]) => {
+ if (value === null) {
+ remove(key);
+ return;
+ }
+
+ const valueWithoutNestedNullValues = shouldRemoveNestedNulls ?? true ? utils.removeNestedNullValues(value) : value;
- if (!wasRemoved && valueAfterRemoving !== undefined) {
- pairs.push([key, valueAfterRemoving]);
+ if (valueWithoutNestedNullValues !== undefined) {
+ pairs.push([key, valueWithoutNestedNullValues, replaceNullPatches?.[key]]);
}
+ });
- return pairs;
- }, []);
+ return pairs;
}
function mergeChanges | undefined, TChange extends OnyxInput | undefined>(changes: TChange[], existingValue?: TValue): FastMergeResult {
@@ -1502,7 +1487,6 @@ const OnyxUtils = {
evictStorageAndRetry,
broadcastUpdate,
hasPendingMergeForKey,
- removeNullValues,
prepareKeyValuePairsForStorage,
mergeChanges,
mergeAndMarkChanges,
diff --git a/lib/storage/InstanceSync/index.web.ts b/lib/storage/InstanceSync/index.web.ts
index 67b309791..99a7fe325 100644
--- a/lib/storage/InstanceSync/index.web.ts
+++ b/lib/storage/InstanceSync/index.web.ts
@@ -5,7 +5,7 @@
*/
import type {OnyxKey} from '../../types';
import NoopProvider from '../providers/NoopProvider';
-import type {KeyList, OnStorageKeyChanged} from '../providers/types';
+import type {StorageKeyList, OnStorageKeyChanged} from '../providers/types';
import type StorageProvider from '../providers/types';
const SYNC_ONYX = 'SYNC_ONYX';
@@ -19,7 +19,7 @@ function raiseStorageSyncEvent(onyxKey: OnyxKey) {
global.localStorage.removeItem(SYNC_ONYX);
}
-function raiseStorageSyncManyKeysEvent(onyxKeys: KeyList) {
+function raiseStorageSyncManyKeysEvent(onyxKeys: StorageKeyList) {
onyxKeys.forEach((onyxKey) => {
raiseStorageSyncEvent(onyxKey);
});
@@ -54,12 +54,12 @@ const InstanceSync = {
multiSet: raiseStorageSyncManyKeysEvent,
mergeItem: raiseStorageSyncEvent,
clear: (clearImplementation: () => void) => {
- let allKeys: KeyList;
+ let allKeys: StorageKeyList;
// The keys must be retrieved before storage is cleared or else the list of keys would be empty
return storage
.getAllKeys()
- .then((keys: KeyList) => {
+ .then((keys: StorageKeyList) => {
allKeys = keys;
})
.then(() => clearImplementation())
diff --git a/lib/storage/providers/IDBKeyValProvider.ts b/lib/storage/providers/IDBKeyValProvider.ts
index c61fc851e..64d419a83 100644
--- a/lib/storage/providers/IDBKeyValProvider.ts
+++ b/lib/storage/providers/IDBKeyValProvider.ts
@@ -70,7 +70,7 @@ const provider: StorageProvider = {
}
return true;
- });
+ }) as Array<[IDBValidKey, unknown]>;
return setMany(pairsWithoutNull, idbKeyValStore);
},
diff --git a/lib/storage/providers/MemoryOnlyProvider.ts b/lib/storage/providers/MemoryOnlyProvider.ts
index 1af95a6e3..1510d20cb 100644
--- a/lib/storage/providers/MemoryOnlyProvider.ts
+++ b/lib/storage/providers/MemoryOnlyProvider.ts
@@ -1,7 +1,7 @@
import _ from 'underscore';
import utils from '../../utils';
import type StorageProvider from './types';
-import type {KeyValuePair} from './types';
+import type {StorageKeyValuePair} from './types';
import type {OnyxKey, OnyxValue} from '../../types';
type Store = Record>;
@@ -49,7 +49,7 @@ const provider: StorageProvider = {
new Promise((resolve) => {
this.getItem(key).then((value) => resolve([key, value]));
}),
- ) as Array>;
+ ) as Array>;
return Promise.all(getPromises);
},
diff --git a/lib/storage/providers/SQLiteProvider.ts b/lib/storage/providers/SQLiteProvider.ts
index fd3363f79..bdcde2b76 100644
--- a/lib/storage/providers/SQLiteProvider.ts
+++ b/lib/storage/providers/SQLiteProvider.ts
@@ -8,7 +8,7 @@ import {open} from 'react-native-quick-sqlite';
import type {FastMergeReplaceNullPatch} from '../../utils';
import utils from '../../utils';
import type StorageProvider from './types';
-import type {KeyList, KeyValuePairList} from './types';
+import type {StorageKeyList, StorageKeyValuePair} from './types';
const DB_NAME = 'OnyxDB';
let db: QuickSQLiteConnection;
@@ -61,7 +61,7 @@ const provider: StorageProvider = {
return db.executeAsync(command, keys).then(({rows}) => {
// eslint-disable-next-line no-underscore-dangle
const result = rows?._array.map((row) => [row.record_key, JSON.parse(row.valueJSON)]);
- return (result ?? []) as KeyValuePairList;
+ return (result ?? []) as StorageKeyValuePair[];
});
},
setItem(key, value) {
@@ -74,7 +74,7 @@ const provider: StorageProvider = {
}
return db.executeBatchAsync([['REPLACE INTO keyvaluepairs (record_key, valueJSON) VALUES (?, json(?));', stringifiedPairs]]).then(() => undefined);
},
- multiMerge(pairs, mergeReplaceNullPatches) {
+ multiMerge(pairs) {
const commands: SQLBatchTuple[] = [];
const patchQuery = `INSERT INTO keyvaluepairs (record_key, valueJSON)
@@ -93,13 +93,14 @@ const provider: StorageProvider = {
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < nonNullishPairs.length; i++) {
- const pair = nonNullishPairs[i];
- const value = JSON.stringify(pair[1], replacer);
- patchQueryArguments.push([pair[0], value]);
+ const [key, value, replaceNullPatches] = nonNullishPairs[i];
- const patches = mergeReplaceNullPatches?.[pair[0]] ?? [];
+ const valueAfterReplace = JSON.stringify(value, replacer);
+ patchQueryArguments.push([key, valueAfterReplace]);
+
+ const patches = replaceNullPatches ?? [];
if (patches.length > 0) {
- const queries = generateJSONReplaceSQLQueries(pair[0], patches);
+ const queries = generateJSONReplaceSQLQueries(key, patches);
if (queries.length > 0) {
replaceQueryArguments.push(...queries);
@@ -114,15 +115,15 @@ const provider: StorageProvider = {
return db.executeBatchAsync(commands).then(() => undefined);
},
- mergeItem(key, change) {
+ mergeItem(key, change, replaceNullPatches) {
// Since Onyx already merged the existing value with the changes, we can just set the value directly.
- return this.multiMerge([[key, change]]);
+ return this.multiMerge([[key, change, replaceNullPatches]]);
},
getAllKeys: () =>
db.executeAsync('SELECT record_key FROM keyvaluepairs;').then(({rows}) => {
// eslint-disable-next-line no-underscore-dangle
const result = rows?._array.map((row) => row.record_key);
- return (result ?? []) as KeyList;
+ return (result ?? []) as StorageKeyList;
}),
removeItem: (key) => db.executeAsync('DELETE FROM keyvaluepairs WHERE record_key = ?;', [key]).then(() => undefined),
removeItems: (keys) => {
diff --git a/lib/storage/providers/types.ts b/lib/storage/providers/types.ts
index fc3b3cade..db7525aa5 100644
--- a/lib/storage/providers/types.ts
+++ b/lib/storage/providers/types.ts
@@ -1,8 +1,8 @@
-import type {MixedOperationsQueue, OnyxKey, OnyxValue} from '../../types';
+import type {OnyxKey, OnyxValue} from '../../types';
+import type {FastMergeReplaceNullPatch} from '../../utils';
-type KeyValuePair = [OnyxKey, OnyxValue];
-type KeyList = OnyxKey[];
-type KeyValuePairList = KeyValuePair[];
+type StorageKeyValuePair = [key: OnyxKey, value: OnyxValue, replaceNullPatches?: FastMergeReplaceNullPatch[]];
+type StorageKeyList = OnyxKey[];
type DatabaseSize = {
bytesUsed: number;
@@ -28,7 +28,7 @@ type StorageProvider = {
/**
* Get multiple key-value pairs for the given array of keys in a batch
*/
- multiGet: (keys: KeyList) => Promise;
+ multiGet: (keys: StorageKeyList) => Promise;
/**
* Sets the value for a given key. The only requirement is that the value should be serializable to JSON string
@@ -38,23 +38,23 @@ type StorageProvider = {
/**
* Stores multiple key-value pairs in a batch
*/
- multiSet: (pairs: KeyValuePairList) => Promise;
+ multiSet: (pairs: StorageKeyValuePair[]) => Promise;
/**
* Multiple merging of existing and new values in a batch
*/
- multiMerge: (pairs: KeyValuePairList, mergeReplaceNullPatches?: MixedOperationsQueue['mergeReplaceNullPatches']) => Promise;
+ multiMerge: (pairs: StorageKeyValuePair[]) => Promise;
/**
* Merges an existing value with a new one
* @param change - the change to merge with the existing value
*/
- mergeItem: (key: TKey, change: OnyxValue) => Promise;
+ mergeItem: (key: TKey, change: OnyxValue, replaceNullPatches?: FastMergeReplaceNullPatch[]) => Promise;
/**
* Returns all keys available in storage
*/
- getAllKeys: () => Promise;
+ getAllKeys: () => Promise;
/**
* Removes given key and its value from storage
@@ -64,7 +64,7 @@ type StorageProvider = {
/**
* Removes given keys and their values from storage
*/
- removeItems: (keys: KeyList) => Promise;
+ removeItems: (keys: StorageKeyList) => Promise;
/**
* Clears absolutely everything from storage
@@ -83,4 +83,4 @@ type StorageProvider = {
};
export default StorageProvider;
-export type {KeyList, KeyValuePair, KeyValuePairList, OnStorageKeyChanged};
+export type {StorageKeyList, StorageKeyValuePair, OnStorageKeyChanged};
diff --git a/lib/types.ts b/lib/types.ts
index cd5556b89..7dbdb1ca7 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -486,12 +486,14 @@ type InitOptions = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type GenericFunction = (...args: any[]) => any;
+type MultiMergeReplaceNullPatches = {[TKey in OnyxKey]: FastMergeReplaceNullPatch[]};
+
/**
* Represents a combination of Merge and Set operations that should be executed in Onyx
*/
type MixedOperationsQueue = {
merge: OnyxInputKeyValueMapping;
- mergeReplaceNullPatches: {[TKey in OnyxKey]: FastMergeReplaceNullPatch[]};
+ mergeReplaceNullPatches: MultiMergeReplaceNullPatches;
set: OnyxInputKeyValueMapping;
};
@@ -533,5 +535,6 @@ export type {
OnyxValue,
Selector,
WithOnyxConnectOptions,
+ MultiMergeReplaceNullPatches,
MixedOperationsQueue,
};
From 8391cc7628ff629a513af4550bec0a01cc78d55f Mon Sep 17 00:00:00 2001
From: Christoph Pader
Date: Wed, 21 May 2025 17:19:24 +0200
Subject: [PATCH 19/20] keep performance merge logic on native
---
lib/Onyx.ts | 23 ++--------------------
lib/OnyxMerge.native.ts | 43 +++++++++++++++++++++++++++++++++++++++++
lib/OnyxMerge.ts | 35 +++++++++++++++++++++++++++++++++
lib/OnyxUtils.ts | 19 ++++++------------
4 files changed, 86 insertions(+), 34 deletions(-)
create mode 100644 lib/OnyxMerge.native.ts
create mode 100644 lib/OnyxMerge.ts
diff --git a/lib/Onyx.ts b/lib/Onyx.ts
index 3039b611a..5bee462a1 100644
--- a/lib/Onyx.ts
+++ b/lib/Onyx.ts
@@ -1,4 +1,3 @@
-/* eslint-disable no-continue */
import _ from 'underscore';
import lodashPick from 'lodash/pick';
import * as Logger from './Logger';
@@ -35,6 +34,7 @@ import type {Connection} from './OnyxConnectionManager';
import connectionManager from './OnyxConnectionManager';
import * as GlobalSettings from './GlobalSettings';
import decorateWithMetrics from './metrics';
+import OnyxMerge from './OnyxMerge.native';
/** Initialize the store with actions and listening for storage events */
function init({
@@ -325,26 +325,7 @@ function merge(key: TKey, changes: OnyxMergeInput):
return Promise.resolve();
}
- const {result: mergedValue} = OnyxUtils.mergeChanges(validChanges, existingValue);
-
- // 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, mergedValue);
-
- // Logging properties only since values could be sensitive things we don't want to log.
- Logger.logInfo(`merge called for key: ${key}${_.isObject(mergedValue) ? ` properties: ${_.keys(mergedValue).join(',')}` : ''} hasChanged: ${hasChanged}`);
-
- // This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
- const updatePromise = OnyxUtils.broadcastUpdate(key, mergedValue as OnyxValue, hasChanged);
-
- // If the value has not changed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
- if (!hasChanged) {
- return updatePromise;
- }
-
- return Storage.setItem(key, mergedValue as OnyxValue).then(() => {
- OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.MERGE, key, changes, mergedValue);
- return updatePromise;
- });
+ return OnyxMerge.applyMerge(key, existingValue, validChanges);
} catch (error) {
Logger.logAlert(`An error occurred while applying merge for key: ${key}, Error: ${error}`);
return Promise.resolve();
diff --git a/lib/OnyxMerge.native.ts b/lib/OnyxMerge.native.ts
new file mode 100644
index 000000000..f18589e77
--- /dev/null
+++ b/lib/OnyxMerge.native.ts
@@ -0,0 +1,43 @@
+import _ from 'underscore';
+import * as Logger from './Logger';
+import OnyxUtils from './OnyxUtils';
+import type {OnyxKey, OnyxValue} from './types';
+import cache from './OnyxCache';
+import Storage from './storage';
+
+function applyMerge(key: TKey, existingValue: OnyxValue, validChanges: unknown[]): Promise {
+ // If any of the changes is null, we need to discard the existing value.
+ const baseValue = validChanges.includes(null) ? undefined : existingValue;
+
+ // We first batch the changes into a single change with object removal marks,
+ // so that SQLite can merge the changes more efficiently.
+ const {result: batchedChanges, replaceNullPatches} = OnyxUtils.mergeAndMarkChanges(validChanges);
+
+ // We then merge the batched changes with the existing value, because we need to final merged value to broadcast to subscribers.
+ const {result: mergedValue} = OnyxUtils.mergeChanges([batchedChanges], baseValue);
+
+ // 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, mergedValue);
+
+ // Logging properties only since values could be sensitive things we don't want to log.
+ Logger.logInfo(`merge called for key: ${key}${_.isObject(mergedValue) ? ` properties: ${_.keys(mergedValue).join(',')}` : ''} hasChanged: ${hasChanged}`);
+
+ // This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
+ const updatePromise = OnyxUtils.broadcastUpdate(key, mergedValue as OnyxValue, hasChanged);
+
+ // If the value has not changed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
+ if (!hasChanged) {
+ return updatePromise;
+ }
+
+ return Storage.mergeItem(key, batchedChanges as OnyxValue, replaceNullPatches).then(() => {
+ OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.MERGE, key, validChanges, mergedValue);
+ return updatePromise;
+ });
+}
+
+const OnyxMerge = {
+ applyMerge,
+};
+
+export default OnyxMerge;
diff --git a/lib/OnyxMerge.ts b/lib/OnyxMerge.ts
new file mode 100644
index 000000000..bbf65515f
--- /dev/null
+++ b/lib/OnyxMerge.ts
@@ -0,0 +1,35 @@
+import _ from 'underscore';
+import * as Logger from './Logger';
+import OnyxUtils from './OnyxUtils';
+import type {OnyxKey, OnyxValue} from './types';
+import cache from './OnyxCache';
+import Storage from './storage';
+
+function applyMerge(key: TKey, existingValue: OnyxValue, validChanges: unknown[]): Promise {
+ const {result: mergedValue} = OnyxUtils.mergeChanges(validChanges, existingValue);
+
+ // 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, mergedValue);
+
+ // Logging properties only since values could be sensitive things we don't want to log.
+ Logger.logInfo(`merge called for key: ${key}${_.isObject(mergedValue) ? ` properties: ${_.keys(mergedValue).join(',')}` : ''} hasChanged: ${hasChanged}`);
+
+ // This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
+ const updatePromise = OnyxUtils.broadcastUpdate(key, mergedValue as OnyxValue, hasChanged);
+
+ // If the value has not changed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
+ if (!hasChanged) {
+ return updatePromise;
+ }
+
+ return Storage.setItem(key, mergedValue as OnyxValue).then(() => {
+ OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.MERGE, key, validChanges, mergedValue);
+ return updatePromise;
+ });
+}
+
+const OnyxMerge = {
+ applyMerge,
+};
+
+export default OnyxMerge;
diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts
index e89769404..9ac4e881b 100644
--- a/lib/OnyxUtils.ts
+++ b/lib/OnyxUtils.ts
@@ -1236,14 +1236,11 @@ function prepareKeyValuePairsForStorage(
return pairs;
}
-function mergeChanges | undefined, TChange extends OnyxInput | undefined>(changes: TChange[], existingValue?: TValue): FastMergeResult {
+function mergeChanges | undefined>(changes: TValue[], existingValue?: TValue): FastMergeResult {
return applyMerge('merge', changes, existingValue);
}
-function mergeAndMarkChanges | undefined, TChange extends OnyxInput | undefined>(
- changes: TChange[],
- existingValue?: TValue,
-): FastMergeResult {
+function mergeAndMarkChanges | undefined>(changes: TValue[], existingValue?: TValue): FastMergeResult {
return applyMerge('mark', changes, existingValue);
}
@@ -1253,11 +1250,7 @@ function mergeAndMarkChanges | undefined, TCha
* @param changes Array of changes that should be merged
* @param existingValue The existing value that should be merged with the changes
*/
-function applyMerge | undefined, TChange extends OnyxInput | undefined>(
- mode: 'merge' | 'mark',
- changes: TChange[],
- existingValue?: TValue,
-): FastMergeResult {
+function applyMerge | undefined>(mode: 'merge' | 'mark', changes: TValue[], existingValue?: TValue): FastMergeResult {
const lastChange = changes?.at(-1);
if (Array.isArray(lastChange)) {
@@ -1266,7 +1259,7 @@ function applyMerge | undefined, TChange exten
if (changes.some((change) => change && typeof change === 'object')) {
// Object values are then merged one after the other
- return changes.reduce>(
+ return changes.reduce>(
(modifiedData, change) => {
const options: FastMergeOptions = mode === 'merge' ? {shouldRemoveNestedNulls: true, objectRemovalMode: 'replace'} : {objectRemovalMode: 'mark'};
const {result, replaceNullPatches} = utils.fastMerge(modifiedData.result, change, options);
@@ -1279,7 +1272,7 @@ function applyMerge | undefined, TChange exten
return modifiedData;
},
{
- result: (existingValue ?? {}) as TChange,
+ result: (existingValue ?? {}) as TValue,
replaceNullPatches: [],
},
);
@@ -1287,7 +1280,7 @@ function applyMerge | undefined, TChange exten
// If we have anything else we can't merge it so we'll
// simply return the last value that was queued
- return {result: lastChange as TChange, replaceNullPatches: []};
+ return {result: lastChange as TValue, replaceNullPatches: []};
}
/**
From ed66dad9a1a3ecba57216c456e98784c9a942bf2 Mon Sep 17 00:00:00 2001
From: Christoph Pader
Date: Wed, 21 May 2025 17:35:31 +0200
Subject: [PATCH 20/20] fix: invalid param for `sendActionToDevTools`
---
lib/Onyx.ts | 7 +++--
.../index.native.ts} | 27 ++++++++++---------
lib/{OnyxMerge.ts => OnyxMerge/index.ts} | 27 ++++++++++---------
lib/OnyxMerge/types.ts | 10 +++++++
4 files changed, 43 insertions(+), 28 deletions(-)
rename lib/{OnyxMerge.native.ts => OnyxMerge/index.native.ts} (73%)
rename lib/{OnyxMerge.ts => OnyxMerge/index.ts} (66%)
create mode 100644 lib/OnyxMerge/types.ts
diff --git a/lib/Onyx.ts b/lib/Onyx.ts
index 5bee462a1..4e4e3c4dd 100644
--- a/lib/Onyx.ts
+++ b/lib/Onyx.ts
@@ -34,7 +34,7 @@ import type {Connection} from './OnyxConnectionManager';
import connectionManager from './OnyxConnectionManager';
import * as GlobalSettings from './GlobalSettings';
import decorateWithMetrics from './metrics';
-import OnyxMerge from './OnyxMerge.native';
+import OnyxMerge from './OnyxMerge/index.native';
/** Initialize the store with actions and listening for storage events */
function init({
@@ -325,7 +325,10 @@ function merge(key: TKey, changes: OnyxMergeInput):
return Promise.resolve();
}
- return OnyxMerge.applyMerge(key, existingValue, validChanges);
+ return OnyxMerge.applyMerge(key, existingValue, validChanges).then(({mergedValue, updatePromise}) => {
+ OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.MERGE, key, changes, mergedValue);
+ return updatePromise;
+ });
} catch (error) {
Logger.logAlert(`An error occurred while applying merge for key: ${key}, Error: ${error}`);
return Promise.resolve();
diff --git a/lib/OnyxMerge.native.ts b/lib/OnyxMerge/index.native.ts
similarity index 73%
rename from lib/OnyxMerge.native.ts
rename to lib/OnyxMerge/index.native.ts
index f18589e77..bf323b53d 100644
--- a/lib/OnyxMerge.native.ts
+++ b/lib/OnyxMerge/index.native.ts
@@ -1,11 +1,12 @@
import _ from 'underscore';
-import * as Logger from './Logger';
-import OnyxUtils from './OnyxUtils';
-import type {OnyxKey, OnyxValue} from './types';
-import cache from './OnyxCache';
-import Storage from './storage';
-
-function applyMerge(key: TKey, existingValue: OnyxValue, validChanges: unknown[]): Promise {
+import * as Logger from '../Logger';
+import OnyxUtils from '../OnyxUtils';
+import type {OnyxKey, OnyxValue} from '../types';
+import cache from '../OnyxCache';
+import Storage from '../storage';
+import type {ApplyMerge, ApplyMergeResult} from './types';
+
+const applyMerge: ApplyMerge = (key: TKey, existingValue: OnyxValue, validChanges: unknown[]): Promise => {
// If any of the changes is null, we need to discard the existing value.
const baseValue = validChanges.includes(null) ? undefined : existingValue;
@@ -27,14 +28,14 @@ function applyMerge(key: TKey, existingValue: OnyxValue, replaceNullPatches).then(() => {
- OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.MERGE, key, validChanges, mergedValue);
- return updatePromise;
- });
-}
+ return Storage.mergeItem(key, batchedChanges as OnyxValue, replaceNullPatches).then(() => ({
+ mergedValue,
+ updatePromise,
+ }));
+};
const OnyxMerge = {
applyMerge,
diff --git a/lib/OnyxMerge.ts b/lib/OnyxMerge/index.ts
similarity index 66%
rename from lib/OnyxMerge.ts
rename to lib/OnyxMerge/index.ts
index bbf65515f..ea53203dd 100644
--- a/lib/OnyxMerge.ts
+++ b/lib/OnyxMerge/index.ts
@@ -1,11 +1,12 @@
import _ from 'underscore';
-import * as Logger from './Logger';
-import OnyxUtils from './OnyxUtils';
-import type {OnyxKey, OnyxValue} from './types';
-import cache from './OnyxCache';
-import Storage from './storage';
-
-function applyMerge(key: TKey, existingValue: OnyxValue, validChanges: unknown[]): Promise {
+import * as Logger from '../Logger';
+import OnyxUtils from '../OnyxUtils';
+import type {OnyxKey, OnyxValue} from '../types';
+import cache from '../OnyxCache';
+import Storage from '../storage';
+import type {ApplyMerge, ApplyMergeResult} from './types';
+
+const applyMerge: ApplyMerge = (key: TKey, existingValue: OnyxValue, validChanges: unknown[]): Promise => {
const {result: mergedValue} = OnyxUtils.mergeChanges(validChanges, existingValue);
// In cache, we don't want to remove the key if it's null to improve performance and speed up the next merge.
@@ -19,14 +20,14 @@ function applyMerge(key: TKey, existingValue: OnyxValue).then(() => {
- OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.MERGE, key, validChanges, mergedValue);
- return updatePromise;
- });
-}
+ return Storage.setItem(key, mergedValue as OnyxValue).then(() => ({
+ mergedValue,
+ updatePromise,
+ }));
+};
const OnyxMerge = {
applyMerge,
diff --git a/lib/OnyxMerge/types.ts b/lib/OnyxMerge/types.ts
new file mode 100644
index 000000000..51405c9ac
--- /dev/null
+++ b/lib/OnyxMerge/types.ts
@@ -0,0 +1,10 @@
+import type {OnyxKey, OnyxValue} from '../types';
+
+type ApplyMergeResult = {
+ mergedValue: OnyxValue;
+ updatePromise: Promise;
+};
+
+type ApplyMerge = (key: TKey, existingValue: OnyxValue, validChanges: unknown[]) => Promise;
+
+export type {ApplyMerge, ApplyMergeResult};