Skip to content

Commit 8848f86

Browse files
VelikovPetarclaude
andauthored
Port V6 fix: Fix passing outdated Channel data for CidEvents to the ChatEventHandler.handleChatEvent. (#6385)
Co-authored-by: Claude <noreply@anthropic.com>
1 parent c69f4f4 commit 8848f86

4 files changed

Lines changed: 123 additions & 3 deletions

File tree

stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/logic/querychannels/internal/QueryChannelsLogic.kt

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -308,11 +308,21 @@ internal class QueryChannelsLogic(
308308

309309
internal suspend fun parseChatEventResults(chatEvents: List<ChatEvent>): List<EventHandlingResult> {
310310
val cids = chatEvents.filterIsInstance<CidEvent>().map { it.cid }.distinct()
311-
val cachedChannels = queryChannelsDatabaseLogic
312-
.selectChannels(cids).associateBy { it.cid }
311+
// Prefer in-memory per-channel state which has already been updated by the channel
312+
// event handlers. Fall back to DB for channels that are not currently active in memory.
313+
val inMemoryChannels = cids.mapNotNull { cid ->
314+
queryChannelsStateLogic.getActiveChannelState(cid)?.let { cid to it }
315+
}.toMap()
316+
val remainingCids = cids - inMemoryChannels.keys
317+
val dbChannels = if (remainingCids.isEmpty()) {
318+
emptyMap()
319+
} else {
320+
queryChannelsDatabaseLogic.selectChannels(remainingCids).associateBy { it.cid }
321+
}
322+
val resolvedChannels = inMemoryChannels + dbChannels
313323

314324
return chatEvents.map { event ->
315-
val channel = (event as? CidEvent)?.let { cachedChannels[it.cid] }
325+
val channel = (event as? CidEvent)?.let { resolvedChannels[it.cid] }
316326
queryChannelsStateLogic.handleChatEvent(event, channel)
317327
}
318328
}

stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/logic/querychannels/internal/QueryChannelsStateLogic.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,16 @@ internal class QueryChannelsStateLogic(
244244
mutableState.setChannels(newChannels)
245245
}
246246

247+
/**
248+
* Returns the current [Channel] snapshot from the in-memory per-channel state if the
249+
* channel is active, or `null` otherwise.
250+
*/
251+
internal fun getActiveChannelState(cid: String): Channel? {
252+
val (type, id) = cid.cidToTypeAndId()
253+
if (!stateRegistry.isActiveChannel(type, id)) return null
254+
return stateRegistry.channel(type, id).toChannel()
255+
}
256+
247257
/**
248258
* Refreshes member state in all channels from this query.
249259
*

stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/logic/querychannels/internal/QueryChannelsLogicTest.kt

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717
package io.getstream.chat.android.client.internal.state.plugin.logic.querychannels.internal
1818

1919
import io.getstream.chat.android.client.ChatClient
20+
import io.getstream.chat.android.client.api.event.EventHandlingResult
2021
import io.getstream.chat.android.client.api.models.QueryChannelsRequest
2122
import io.getstream.chat.android.client.api.state.QueryChannelsState
2223
import io.getstream.chat.android.client.query.QueryChannelsSpec
2324
import io.getstream.chat.android.client.query.pagination.AnyChannelPaginationRequest
25+
import io.getstream.chat.android.client.test.randomNewMessageEvent
2426
import io.getstream.chat.android.models.Channel
2527
import io.getstream.chat.android.models.FilterObject
2628
import io.getstream.chat.android.models.Filters
@@ -31,6 +33,7 @@ import io.getstream.chat.android.test.asCall
3133
import kotlinx.coroutines.flow.MutableStateFlow
3234
import kotlinx.coroutines.test.runTest
3335
import org.junit.Rule
36+
import org.junit.jupiter.api.Assertions.assertEquals
3437
import org.junit.jupiter.api.BeforeEach
3538
import org.junit.jupiter.api.Test
3639
import org.mockito.kotlin.any
@@ -283,4 +286,75 @@ internal class QueryChannelsLogicTest {
283286
}
284287

285288
// endregion
289+
290+
// region parseChatEventResults
291+
292+
@Test
293+
fun `parseChatEventResults should resolve channels from in-memory state and skip DB`() = runTest {
294+
// Given
295+
val channel = randomChannel(type = "messaging", id = "ch1")
296+
val event = randomNewMessageEvent(cid = channel.cid, channelType = "messaging", channelId = "ch1")
297+
val expectedResult = EventHandlingResult.Skip
298+
299+
whenever(queryChannelsStateLogic.getActiveChannelState(channel.cid)) doReturn channel
300+
whenever(queryChannelsStateLogic.handleChatEvent(eq(event), eq(channel))) doReturn expectedResult
301+
302+
// When
303+
val results = logic.parseChatEventResults(listOf(event))
304+
305+
// Then
306+
verify(queryChannelsDatabaseLogic, never()).selectChannels(any())
307+
assertEquals(listOf(expectedResult), results)
308+
}
309+
310+
@Test
311+
fun `parseChatEventResults should fall back to DB when channel is not active in memory`() = runTest {
312+
// Given
313+
val channel = randomChannel(type = "messaging", id = "ch1")
314+
val event = randomNewMessageEvent(cid = channel.cid, channelType = "messaging", channelId = "ch1")
315+
val expectedResult = EventHandlingResult.Skip
316+
317+
whenever(queryChannelsStateLogic.getActiveChannelState(channel.cid)) doReturn null
318+
whenever(queryChannelsDatabaseLogic.selectChannels(listOf(channel.cid))) doReturn listOf(channel)
319+
whenever(queryChannelsStateLogic.handleChatEvent(eq(event), eq(channel))) doReturn expectedResult
320+
321+
// When
322+
val results = logic.parseChatEventResults(listOf(event))
323+
324+
// Then
325+
verify(queryChannelsDatabaseLogic).selectChannels(listOf(channel.cid))
326+
assertEquals(listOf(expectedResult), results)
327+
}
328+
329+
@Test
330+
fun `parseChatEventResults should use mixed resolution - memory for active, DB for inactive`() = runTest {
331+
// Given
332+
val inMemoryChannel = randomChannel(type = "messaging", id = "active")
333+
val dbChannel = randomChannel(type = "messaging", id = "inactive")
334+
val event1 = randomNewMessageEvent(
335+
cid = inMemoryChannel.cid,
336+
channelType = "messaging",
337+
channelId = "active",
338+
)
339+
val event2 = randomNewMessageEvent(
340+
cid = dbChannel.cid,
341+
channelType = "messaging",
342+
channelId = "inactive",
343+
)
344+
345+
whenever(queryChannelsStateLogic.getActiveChannelState(inMemoryChannel.cid)) doReturn inMemoryChannel
346+
whenever(queryChannelsStateLogic.getActiveChannelState(dbChannel.cid)) doReturn null
347+
whenever(queryChannelsDatabaseLogic.selectChannels(listOf(dbChannel.cid))) doReturn listOf(dbChannel)
348+
whenever(queryChannelsStateLogic.handleChatEvent(any(), any())) doReturn EventHandlingResult.Skip
349+
350+
// When
351+
logic.parseChatEventResults(listOf(event1, event2))
352+
353+
// Then – only the inactive channel should be fetched from DB
354+
verify(queryChannelsDatabaseLogic).selectChannels(listOf(dbChannel.cid))
355+
verify(queryChannelsStateLogic).handleChatEvent(event1, inMemoryChannel)
356+
verify(queryChannelsStateLogic).handleChatEvent(event2, dbChannel)
357+
}
358+
359+
// endregion
286360
}

stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/logic/querychannels/internal/QueryChannelsStateLogicTest.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ import io.getstream.chat.android.test.TestCoroutineRule
3232
import kotlinx.coroutines.test.runTest
3333
import org.amshove.kluent.`should contain same`
3434
import org.junit.Rule
35+
import org.junit.jupiter.api.Assertions.assertEquals
36+
import org.junit.jupiter.api.Assertions.assertNull
3537
import org.junit.jupiter.api.Test
3638
import org.mockito.kotlin.any
3739
import org.mockito.kotlin.doReturn
@@ -133,4 +135,28 @@ internal class QueryChannelsStateLogicTest {
133135
queryChannelsSpec.cids `should contain same` setOf(testCid, channel1.cid, channel2.cid)
134136
verify(mutableState).setChannels(channels.associateBy { it.cid })
135137
}
138+
139+
@Test
140+
fun `getActiveChannelState should return channel when it is active in state registry`() {
141+
val channel = randomChannel(type = type, id = id)
142+
val channelState: ChannelState = mock {
143+
on(it.toChannel()) doReturn channel
144+
}
145+
146+
whenever(stateRegistry.isActiveChannel(type, id)) doReturn true
147+
whenever(stateRegistry.channel(type, id)) doReturn channelState
148+
149+
val result = queryChannelsStateLogic.getActiveChannelState(testCid)
150+
151+
assertEquals(channel, result)
152+
}
153+
154+
@Test
155+
fun `getActiveChannelState should return null when channel is not active in state registry`() {
156+
whenever(stateRegistry.isActiveChannel(type, id)) doReturn false
157+
158+
val result = queryChannelsStateLogic.getActiveChannelState(testCid)
159+
160+
assertNull(result)
161+
}
136162
}

0 commit comments

Comments
 (0)