From 41d640a00763e1fdceaebd4cc369c1afc8cddfde Mon Sep 17 00:00:00 2001
From: arekm213
Date: Tue, 6 Oct 2026 17:51:25 +0200
Subject: [PATCH 1/9] Skip unchanged cached subtrees in removeNestedNullValues
and keep the cache null-free
---
lib/Onyx.ts | 8 ++-
lib/OnyxUtils.ts | 17 +++--
lib/utils.ts | 29 ++++++--
tests/unit/onyxTest.ts | 156 ++++++++++++++++++++++++++++++++++++++++
tests/unit/utilsTest.ts | 145 +++++++++++++++++++++++++++++++++++++
5 files changed, 344 insertions(+), 11 deletions(-)
diff --git a/lib/Onyx.ts b/lib/Onyx.ts
index 72c76c608..632b3b804 100644
--- a/lib/Onyx.ts
+++ b/lib/Onyx.ts
@@ -64,13 +64,16 @@ function init({
const individual: Array<[OnyxKey, OnyxEntry]> = [];
const collectionBatches = new Map>; previous: NonUndefined>}>();
- for (const [key, value] of pairs) {
+ for (const [key, valueFromStorage] of pairs) {
// RAM-only keys should never sync from storage as they may have stale persisted data
// from before the key was migrated to RAM-only.
if (OnyxKeys.isRamOnlyKey(key)) {
continue;
}
+ // Storage can hold nested nulls, but cached values must not.
+ const value = utils.removeNestedNullValues(valueFromStorage);
+
const collectionKey = OnyxKeys.getCollectionKey(key);
const isCollectionMember = !!collectionKey && OnyxKeys.isCollectionMemberKey(collectionKey, key);
@@ -489,7 +492,8 @@ function clear(keysToPreserve: OnyxKey[] = []): Promise {
// since collection key subscribers need to be updated differently
if (!isKeyToPreserve) {
const oldValue = cache.get(key);
- const newValue = defaultKeyStates[key] ?? null;
+ // Cached values must not hold nested nulls, so the default key state is cleaned first.
+ const newValue = utils.removeNestedNullValues(defaultKeyStates[key] ?? null);
if (newValue !== oldValue) {
cache.set(key, newValue);
diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts
index c7ddb5478..dda859eb2 100644
--- a/lib/OnyxUtils.ts
+++ b/lib/OnyxUtils.ts
@@ -300,8 +300,10 @@ function get>(key: TKey): P
return undefined;
}
- cache.set(key, val);
- return val;
+ // Storage can hold nested nulls (e.g. SQLite's JSON_REPLACE of marked objects), but cached values must not.
+ const valueWithoutNestedNullValues = utils.removeNestedNullValues(val) as TValue;
+ cache.set(key, valueWithoutNestedNullValues);
+ return valueWithoutNestedNullValues;
})
.catch((err) => Logger.logInfo(`Unable to get item from persistent storage. Key: ${key} Error: ${err}`));
@@ -402,10 +404,15 @@ function multiGet(keys: CollectionKeyBase[]): Promise
getDefaultKeyStates()
Getter - returns the default key states.
+getNullFreeDefaultKeyState()
+Getter - returns the default key state without nested nulls, cached so clear() reuses the same references.
+
getDeferredInitTask()
Getter - returns the deffered init task.
@@ -206,6 +209,12 @@ Getter - returns the merge queue promise.
## getDefaultKeyStates()
Getter - returns the default key states.
+**Kind**: global function
+
+
+## getNullFreeDefaultKeyState()
+Getter - returns the default key state without nested nulls, cached so clear() reuses the same references.
+
**Kind**: global function