From 3ba22436f2f989e0fab4e8ae8b009ad70a408c14 Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:29:23 +0200 Subject: [PATCH 1/2] Reduce redundant DB writes for users and channel configs --- .../DatabaseChannelConfigRepository.kt | 8 ++++++-- .../user/internal/DatabaseUserRepository.kt | 3 +++ .../repository/ChannelConfigRepositoryTest.kt | 19 +++++++++++++++++++ .../offline/repository/UserRepositoryTests.kt | 16 ++++++++++++++++ 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/domain/channelconfig/internal/DatabaseChannelConfigRepository.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/domain/channelconfig/internal/DatabaseChannelConfigRepository.kt index d3b7708637b..6f5a34acbdd 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/domain/channelconfig/internal/DatabaseChannelConfigRepository.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/domain/channelconfig/internal/DatabaseChannelConfigRepository.kt @@ -48,11 +48,15 @@ internal class DatabaseChannelConfigRepository( * Writes many [ChannelConfig] */ override suspend fun insertChannelConfigs(configs: Collection) { + // Channel configs are keyed by type, so a page of same-type channels yields many configs + // that collapse to one row. Dedup by type to avoid the redundant per-row writes. + val configsByType = configs.associateBy(ChannelConfig::type) + // update the local configs - channelConfigs += configs.associateBy(ChannelConfig::type) + channelConfigs += configsByType // insert into room db - channelConfigDao.insert(configs.map(ChannelConfig::toEntity)) + channelConfigDao.insert(configsByType.values.map(ChannelConfig::toEntity)) } /** diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/domain/user/internal/DatabaseUserRepository.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/domain/user/internal/DatabaseUserRepository.kt index c64b936404f..39fa85679a4 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/domain/user/internal/DatabaseUserRepository.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/domain/user/internal/DatabaseUserRepository.kt @@ -59,6 +59,9 @@ internal class DatabaseUserRepository( override suspend fun insertUsers(users: Collection) { if (users.isEmpty()) return val usersToInsert = users + // Use associateBy instead of distinctBy to keep the *last* occurrence of each user + .associateBy(User::id) + .values .filter { it != userCache[it.id] } .map { it.toEntity() } cacheUsers(users) diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/offline/repository/ChannelConfigRepositoryTest.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/offline/repository/ChannelConfigRepositoryTest.kt index 00bb22f66db..cbde874c516 100644 --- a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/offline/repository/ChannelConfigRepositoryTest.kt +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/offline/repository/ChannelConfigRepositoryTest.kt @@ -97,6 +97,25 @@ internal class ChannelConfigRepositoryTest { ) } + @Test + fun `When insert configs with duplicate types Should dedup keeping the last per type`() = runTest { + val first = randomChannelConfig(type = "messaging", config = randomConfig(name = "first")) + val last = randomChannelConfig(type = "messaging", config = randomConfig(name = "last")) + val other = randomChannelConfig(type = "livestream", config = randomConfig(name = "other")) + + sut.insertChannelConfigs(listOf(first, other, last)) + + verify(dao).insert( + argThat> { + size == 2 && + single { it.channelConfigInnerEntity.channelType == "messaging" } + .channelConfigInnerEntity.name == "last" && + single { it.channelConfigInnerEntity.channelType == "livestream" } + .channelConfigInnerEntity.name == "other" + }, + ) + } + @Test fun `Given config in cache When select Should return config`() = runTest { val config = randomChannelConfig(type = "messaging", config = randomConfig(name = "configName")) diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/offline/repository/UserRepositoryTests.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/offline/repository/UserRepositoryTests.kt index 01e590bccae..5920bb7403d 100644 --- a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/offline/repository/UserRepositoryTests.kt +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/offline/repository/UserRepositoryTests.kt @@ -51,6 +51,7 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.times @@ -131,6 +132,21 @@ internal class UserRepositoryTests { verify(userDao, never()).insertMany(any()) } + @Test + fun `When insert users with duplicate ids Should insert each user only once keeping the last`() = runTest { + val id = randomString() + val firstCopy = randomUser(id = id, name = "first") + val lastCopy = firstCopy.copy(name = "last") + val other = randomUser() + + sut.insertUsers(listOf(firstCopy, other, lastCopy)) + + val captor = argumentCaptor>() + verify(userDao).insertMany(captor.capture()) + captor.firstValue.map(UserEntity::id) shouldBeEqualTo listOf(id, other.id) + captor.firstValue.first { it.id == id }.name shouldBeEqualTo "last" + } + @Test fun `When insert current user Should insert entity with me id to dao`() = runTest { val user = randomUser( From 24c6e429699718e1f7afa1aaa80cb58095d03fa8 Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Mon, 22 Jun 2026 10:44:16 +0200 Subject: [PATCH 2/2] Update insertUsers log message --- .../domain/user/internal/DatabaseUserRepository.kt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/domain/user/internal/DatabaseUserRepository.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/domain/user/internal/DatabaseUserRepository.kt index 39fa85679a4..73940fd2700 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/domain/user/internal/DatabaseUserRepository.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/offline/repository/domain/user/internal/DatabaseUserRepository.kt @@ -58,15 +58,16 @@ internal class DatabaseUserRepository( */ override suspend fun insertUsers(users: Collection) { if (users.isEmpty()) return - val usersToInsert = users - // Use associateBy instead of distinctBy to keep the *last* occurrence of each user - .associateBy(User::id) - .values + // Use associateBy instead of distinctBy to keep the *last* occurrence of each user + val uniqueUsers = users.associateBy(User::id).values + val usersToInsert = uniqueUsers .filter { it != userCache[it.id] } .map { it.toEntity() } - cacheUsers(users) + cacheUsers(uniqueUsers) scope.launchWithMutex(dbMutex) { - logger.v { "[insertUsers] inserting ${usersToInsert.size} entities on DB, updated ${users.size} on cache" } + logger.v { + "[insertUsers] inserting ${usersToInsert.size} entities in DB, updated ${uniqueUsers.size} in cache" + } usersToInsert .takeUnless { it.isEmpty() } ?.let { userDao.insertMany(it) }