diff --git a/lib/Onyx.ts b/lib/Onyx.ts index eb79d42f..27751ae6 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -56,7 +56,13 @@ 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 + const isKeyCollectionMember = OnyxUtils.isCollectionMember(key); + + 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, 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);