fix(chat): DM realtime 정리 누락을 보정한다
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
package kr.co.vividnext.sodalive.v2.main.chat.dm
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.orhanobut.logger.Logger
|
||||
@@ -47,6 +49,7 @@ class DmChatRoomViewModel(
|
||||
private var isDisconnecting: Boolean = false
|
||||
private var reconnectDisposable: Disposable? = null
|
||||
private var localMessageSequence: Long = 0L
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
private val _chatRoomStateLiveData = MutableLiveData<DmChatRoomUiState>()
|
||||
val chatRoomStateLiveData: LiveData<DmChatRoomUiState>
|
||||
@@ -252,7 +255,19 @@ class DmChatRoomViewModel(
|
||||
}
|
||||
|
||||
private fun scheduleRealtimeCallback(action: () -> Unit) {
|
||||
compositeDisposable.add(AndroidSchedulers.mainThread().scheduleDirect(action))
|
||||
if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||
action()
|
||||
} else {
|
||||
mainHandler.post { action() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
mainHandler.removeCallbacksAndMessages(null)
|
||||
reconnectDisposable?.dispose()
|
||||
reconnectDisposable = null
|
||||
repository.cancelRealtime()
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
private fun createRoomAndOpen(creatorId: Long) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.main.chat.dm
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.os.Looper
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
@@ -33,8 +34,11 @@ import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.Shadows.shadowOf
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import java.lang.reflect.Method
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@@ -386,6 +390,22 @@ class DmChatRoomViewModelTest {
|
||||
assertEquals(listOf("실시간"), state.messages.map { it.textMessage })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `realtime message callback은 완료된 Disposable을 누적하지 않는다`() {
|
||||
api.enqueueOpenSuccess(openResponse(roomId = 10L))
|
||||
viewModel.enter(roomId = 10L, creatorId = 0L)
|
||||
viewModel.connectRealtime()
|
||||
val beforeCallbackSize = viewModel.compositeDisposable.size()
|
||||
|
||||
realtimeClient.listener?.onMessage(message(messageId = 3L, textMessage = "실시간1"))
|
||||
realtimeClient.listener?.onMessage(message(messageId = 4L, textMessage = "실시간2"))
|
||||
realtimeClient.listener?.onMessage(message(messageId = 5L, textMessage = "실시간3"))
|
||||
|
||||
assertEquals(beforeCallbackSize, viewModel.compositeDisposable.size())
|
||||
val state = viewModel.chatRoomStateLiveData.requireValue() as DmChatRoomUiState.Content
|
||||
assertEquals(listOf(3L, 4L, 5L), state.messages.map { it.messageId })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `realtime listener callback은 main thread scheduler로 상태를 갱신한다`() {
|
||||
val source = projectFile(
|
||||
@@ -393,7 +413,8 @@ class DmChatRoomViewModelTest {
|
||||
).readText()
|
||||
|
||||
assertTrue(source.contains("scheduleRealtimeCallback"))
|
||||
assertTrue(source.contains("AndroidSchedulers.mainThread().scheduleDirect"))
|
||||
assertTrue(source.contains("Looper.myLooper() == Looper.getMainLooper()"))
|
||||
assertTrue(source.contains("mainHandler.post { action() }"))
|
||||
assertTrue(source.contains("scheduleRealtimeCallback { syncLatestMessagesAfterReconnect(token = token) }"))
|
||||
assertTrue(source.contains("scheduleRealtimeCallback { onRealtimeMessage(message) }"))
|
||||
}
|
||||
@@ -532,6 +553,56 @@ class DmChatRoomViewModelTest {
|
||||
assertEquals(listOf(1L), state.messages.map { it.messageId })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onCleared는 realtime 연결과 disposable을 정리한다`() {
|
||||
val pendingDisconnect = SingleSubject.create<ApiResponse<Boolean>>()
|
||||
api.enqueueOpenSuccess(openResponse(roomId = 10L))
|
||||
api.enqueueDisconnect(pendingDisconnect)
|
||||
viewModel.enter(roomId = 10L, creatorId = 0L)
|
||||
viewModel.connectRealtime()
|
||||
viewModel.disconnectRealtime()
|
||||
|
||||
viewModel.invokeOnCleared()
|
||||
|
||||
assertEquals(2, realtimeClient.cancelCalls)
|
||||
assertTrue(viewModel.compositeDisposable.isDisposed)
|
||||
assertTrue(pendingDisconnect.hasObservers().not())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onCleared는 예약된 realtime 재연결을 취소한다`() {
|
||||
api.enqueueOpenSuccess(openResponse(roomId = 10L))
|
||||
viewModel.enter(roomId = 10L, creatorId = 0L)
|
||||
viewModel.connectRealtime()
|
||||
|
||||
realtimeClient.listener?.onFailure(IllegalStateException("network"))
|
||||
viewModel.invokeOnCleared()
|
||||
reconnectScheduler.advanceTimeBy(3L, TimeUnit.SECONDS)
|
||||
|
||||
assertEquals(listOf(RealtimeConnectCall("test-token", 10L)), realtimeClient.connectCalls)
|
||||
assertEquals(1, realtimeClient.cancelCalls)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onCleared는 예약된 main handler callback 실행을 막는다`() {
|
||||
val postedLatch = CountDownLatch(1)
|
||||
api.enqueueOpenSuccess(openResponse(roomId = 10L))
|
||||
viewModel.enter(roomId = 10L, creatorId = 0L)
|
||||
viewModel.connectRealtime()
|
||||
|
||||
Thread {
|
||||
realtimeClient.listener?.onMessage(message(messageId = 99L, textMessage = "제거 대상"))
|
||||
postedLatch.countDown()
|
||||
}.start()
|
||||
assertTrue(postedLatch.await(2, TimeUnit.SECONDS))
|
||||
|
||||
viewModel.invokeOnCleared()
|
||||
shadowOf(Looper.getMainLooper()).idle()
|
||||
|
||||
val state = viewModel.chatRoomStateLiveData.requireValue() as DmChatRoomUiState.Content
|
||||
assertTrue(state.messages.isEmpty())
|
||||
}
|
||||
|
||||
private fun projectFile(relativePath: String): java.io.File {
|
||||
val candidates = listOf(java.io.File(relativePath), java.io.File("../$relativePath"))
|
||||
return candidates.firstOrNull { it.exists() }
|
||||
@@ -595,6 +666,12 @@ class DmChatRoomViewModelTest {
|
||||
removeObserver(observer)
|
||||
return value
|
||||
}
|
||||
|
||||
fun DmChatRoomViewModel.invokeOnCleared() {
|
||||
val method: Method = DmChatRoomViewModel::class.java.getDeclaredMethod("onCleared")
|
||||
method.isAccessible = true
|
||||
method.invoke(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user