From 48274f28803821d1a2f3cf1a0ec17428bf684fdd Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 10 Dec 2025 15:47:53 +0100 Subject: [PATCH 1/3] skip keyChanged notifications on collection update between tabs --- lib/Onyx.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/Onyx.ts b/lib/Onyx.ts index eb79d42f..38975aff 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -56,7 +56,19 @@ function init({ if (shouldSyncMultipleInstances) { Storage.keepInstancesSync?.((key, value) => { cache.set(key, value); - OnyxUtils.keyChanged(key, value as OnyxValue); + + // Check if this is a collection member key to prevent duplicate callbacks + // When a collection is updated, individual members sync separately to other tabs + // Setting isProcessingCollectionUpdate=true prevents triggering collection callbacks for each individual update + let isCollectionMember = false; + try { + OnyxUtils.getCollectionKey(key); + isCollectionMember = true; + } catch { + // Key is not a collection member + } + + OnyxUtils.keyChanged(key, value as OnyxValue, undefined, true, isCollectionMember); }); } From 6a166001cf6d6d7abfa6829d961cea5a0f41ff7e Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Thu, 11 Dec 2025 08:07:07 +0100 Subject: [PATCH 2/3] create isCollectionMember util --- lib/Onyx.ts | 10 ++-------- lib/OnyxUtils.ts | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/lib/Onyx.ts b/lib/Onyx.ts index 38975aff..27751ae6 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -60,15 +60,9 @@ function init({ // Check if this is a collection member key to prevent duplicate callbacks // When a collection is updated, individual members sync separately to other tabs // Setting isProcessingCollectionUpdate=true prevents triggering collection callbacks for each individual update - let isCollectionMember = false; - try { - OnyxUtils.getCollectionKey(key); - isCollectionMember = true; - } catch { - // Key is not a collection member - } + const isKeyCollectionMember = OnyxUtils.isCollectionMember(key); - OnyxUtils.keyChanged(key, value as OnyxValue, undefined, true, isCollectionMember); + OnyxUtils.keyChanged(key, value as OnyxValue, undefined, true, isKeyCollectionMember); }); } diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 58168e02..669eda86 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -478,6 +478,22 @@ function isCollectionMemberKey(collect return key.startsWith(collectionKey) && key.length > collectionKey.length; } +/** + * Checks if a given key is a collection member key (not just a collection key). + * @param key - The key to check + * @returns true if the key is a collection member, false otherwise + */ +function isCollectionMember(key: OnyxKey): boolean { + try { + const collectionKey = getCollectionKey(key); + // If the key is longer than the collection key, it's a collection member + return key.length > collectionKey.length; + } catch (e) { + // If getCollectionKey throws, the key is not a collection member + return false; + } +} + /** * Splits a collection member key into the collection key part and the ID part. * @param key - The collection member key to split. @@ -1694,6 +1710,7 @@ const OnyxUtils = { getCollectionKeys, isCollectionKey, isCollectionMemberKey, + isCollectionMember, splitCollectionMemberKey, isKeyMatch, tryGetCachedValue, From 55fa8eacd1a52e2b6f5bcf33ce12eadcae91d98c Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Thu, 11 Dec 2025 08:12:51 +0100 Subject: [PATCH 3/3] add unit tests for isCollectionMember --- tests/unit/onyxUtilsTest.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/unit/onyxUtilsTest.ts b/tests/unit/onyxUtilsTest.ts index b4f49e54..a390ed79 100644 --- a/tests/unit/onyxUtilsTest.ts +++ b/tests/unit/onyxUtilsTest.ts @@ -345,6 +345,36 @@ describe('OnyxUtils', () => { }); }); + describe('isCollectionMember', () => { + it('should return true for collection member keys', () => { + expect(OnyxUtils.isCollectionMember('test_123')).toBe(true); + expect(OnyxUtils.isCollectionMember('test_level_456')).toBe(true); + expect(OnyxUtils.isCollectionMember('test_level_last_789')).toBe(true); + expect(OnyxUtils.isCollectionMember('test_-1_something')).toBe(true); + expect(OnyxUtils.isCollectionMember('routes_abc')).toBe(true); + }); + + it('should return false for collection keys themselves', () => { + expect(OnyxUtils.isCollectionMember('test_')).toBe(false); + expect(OnyxUtils.isCollectionMember('test_level_')).toBe(false); + expect(OnyxUtils.isCollectionMember('test_level_last_')).toBe(false); + expect(OnyxUtils.isCollectionMember('routes_')).toBe(false); + }); + + it('should return false for non-collection keys', () => { + expect(OnyxUtils.isCollectionMember('test')).toBe(false); + expect(OnyxUtils.isCollectionMember('someRegularKey')).toBe(false); + expect(OnyxUtils.isCollectionMember('notACollection')).toBe(false); + expect(OnyxUtils.isCollectionMember('')).toBe(false); + }); + + it('should return false for invalid keys', () => { + expect(OnyxUtils.isCollectionMember('invalid_key_123')).toBe(false); + expect(OnyxUtils.isCollectionMember('notregistered_')).toBe(false); + expect(OnyxUtils.isCollectionMember('notregistered_123')).toBe(false); + }); + }); + describe('mergeChanges', () => { it("should return the last change if it's an array", () => { const {result} = OnyxUtils.mergeChanges([...testMergeChanges, [0, 1, 2]], testObject);