Skip to content

Commit 1b7caac

Browse files
authored
Fix passing outdated Channel data for CidEvents to the ChatEventHandler.handleChatEvent. (#6381)
1 parent d0c2f78 commit 1b7caac

4 files changed

Lines changed: 123 additions & 3 deletions

File tree

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

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

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

312322
return chatEvents.map { event ->
313-
val channel = (event as? CidEvent)?.let { cachedChannels[it.cid] }
323+
val channel = (event as? CidEvent)?.let { resolvedChannels[it.cid] }
314324
queryChannelsStateLogic.handleChatEvent(event, channel)
315325
}
316326
}

stream-chat-android-state/src/main/java/io/getstream/chat/android/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-state/src/test/java/io/getstream/chat/android/state/plugin/logic/querychannels/internal/QueryChannelsLogicTest.kt

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,20 @@ import io.getstream.chat.android.client.ChatClient
2020
import io.getstream.chat.android.client.api.models.QueryChannelsRequest
2121
import io.getstream.chat.android.client.query.QueryChannelsSpec
2222
import io.getstream.chat.android.client.query.pagination.AnyChannelPaginationRequest
23+
import io.getstream.chat.android.client.test.randomNewMessageEvent
2324
import io.getstream.chat.android.models.Channel
2425
import io.getstream.chat.android.models.FilterObject
2526
import io.getstream.chat.android.models.Filters
2627
import io.getstream.chat.android.models.querysort.QuerySortByField
2728
import io.getstream.chat.android.randomChannel
29+
import io.getstream.chat.android.state.event.handler.chat.EventHandlingResult
2830
import io.getstream.chat.android.state.plugin.state.querychannels.QueryChannelsState
2931
import io.getstream.chat.android.test.TestCoroutineRule
3032
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-state/src/test/java/io/getstream/chat/android/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)