fix(dm): WebSocket heartbeat와 token 재연결을 보정한다

This commit is contained in:
2026-06-18 23:31:15 +09:00
parent a6485292e4
commit 8f69c1ab82
2 changed files with 190 additions and 5 deletions

View File

@@ -48,6 +48,8 @@ class DmChatRoomViewModel(
private var currentRealtimeToken: String = ""
private var currentRealtimeRoomId: Long = 0L
private var reconnectDisposable: Disposable? = null
private var heartbeatPingDisposable: Disposable? = null
private var heartbeatTimeoutDisposable: Disposable? = null
private var localMessageSequence: Long = 0L
private var requestSequence: Long = 0L
private val pendingRequestLocalIds = mutableMapOf<String, String>()
@@ -188,7 +190,16 @@ class DmChatRoomViewModel(
private fun connectRealtime(token: String) {
val roomId = currentRoomId
if (roomId <= 0L || isRealtimeConnected && currentRealtimeRoomId == roomId) return
if (roomId <= 0L) return
if (currentRealtimeToken.isNotEmpty() && currentRealtimeToken != token && currentRealtimeRoomId == roomId) {
stopHeartbeat()
repository.closeSocket()
isRealtimeJoining = false
isRealtimeConnected = false
currentRealtimeToken = ""
currentRealtimeRoomId = 0L
}
if (isRealtimeConnected && currentRealtimeRoomId == roomId) return
if (isRealtimeJoining && currentRealtimeRoomId == roomId) return
if (!shouldReconnectRealtime && currentRealtimeToken.isNotEmpty() && currentRealtimeRoomId == roomId) return
@@ -222,8 +233,12 @@ class DmChatRoomViewModel(
fun leaveRealtime() {
val roomId = currentRoomId
if (roomId <= 0L) return
val hasActiveSocket = currentRealtimeRoomId == roomId &&
(isRealtimeJoining || isRealtimeConnected || currentRealtimeToken.isNotEmpty())
if (!hasActiveSocket) return
shouldReconnectRealtime = false
stopHeartbeat()
currentRealtimeToken = ""
currentRealtimeRoomId = 0L
isRealtimeJoining = false
@@ -243,7 +258,7 @@ class DmChatRoomViewModel(
reconnectDisposable = reconnectScheduler.scheduleDirect(
{
scheduleRealtimeCallback {
if (shouldReconnectRealtime) connectRealtime(token = token)
if (shouldReconnectRealtime) connectRealtime(token = authToken().ifBlank { token })
}
},
RECONNECT_DELAY_MILLIS,
@@ -261,6 +276,7 @@ class DmChatRoomViewModel(
override fun onCleared() {
mainHandler.removeCallbacksAndMessages(null)
stopHeartbeat()
reconnectDisposable?.dispose()
reconnectDisposable = null
currentRealtimeRoomId = 0L
@@ -362,15 +378,65 @@ class DmChatRoomViewModel(
DmChatSocketEvent.Joined -> {
isRealtimeJoining = false
isRealtimeConnected = true
startHeartbeat()
syncLatestMessagesAfterReconnect(token = token)
}
is DmChatSocketEvent.Message -> handleRealtimeMessage(event.requestId, event.message)
is DmChatSocketEvent.SendAck -> handleSendAck(event.requestId, event.message)
is DmChatSocketEvent.Error -> event.requestId?.let { markPendingMessageFailed(it) }
DmChatSocketEvent.Pong -> Unit
DmChatSocketEvent.Pong -> clearHeartbeatTimeout()
}
}
private fun startHeartbeat() {
stopHeartbeat()
heartbeatPingDisposable = reconnectScheduler.schedulePeriodicallyDirect(
{
scheduleRealtimeCallback {
if (!isRealtimeConnected || !shouldReconnectRealtime) return@scheduleRealtimeCallback
val latestToken = authToken()
if (latestToken.isNotBlank() && latestToken != currentRealtimeToken) {
connectRealtime(token = latestToken)
return@scheduleRealtimeCallback
}
if (repository.sendPing()) scheduleHeartbeatTimeout()
}
},
HEARTBEAT_INTERVAL_MILLIS,
HEARTBEAT_INTERVAL_MILLIS,
TimeUnit.MILLISECONDS
).also { compositeDisposable.add(it) }
}
private fun scheduleHeartbeatTimeout() {
heartbeatTimeoutDisposable?.dispose()
heartbeatTimeoutDisposable = reconnectScheduler.scheduleDirect(
{
scheduleRealtimeCallback {
if (!isRealtimeConnected || !shouldReconnectRealtime) return@scheduleRealtimeCallback
isRealtimeJoining = false
isRealtimeConnected = false
stopHeartbeat()
repository.closeSocket()
scheduleRealtimeReconnect()
}
},
HEARTBEAT_TIMEOUT_MILLIS,
TimeUnit.MILLISECONDS
).also { compositeDisposable.add(it) }
}
private fun stopHeartbeat() {
heartbeatPingDisposable?.dispose()
heartbeatPingDisposable = null
clearHeartbeatTimeout()
}
private fun clearHeartbeatTimeout() {
heartbeatTimeoutDisposable?.dispose()
heartbeatTimeoutDisposable = null
}
private fun handleSendAck(requestId: String, message: DmChatMessageResponse) {
val localId = pendingRequestLocalIds.remove(requestId)
?: recentFailedRequestLocalIds.remove(requestId)
@@ -475,6 +541,8 @@ class DmChatRoomViewModel(
private companion object {
const val RECONNECT_DELAY_MILLIS = 3_000L
const val SEND_ACK_TIMEOUT_MILLIS = 10_000L
const val HEARTBEAT_INTERVAL_MILLIS = 30_000L
const val HEARTBEAT_TIMEOUT_MILLIS = 10_000L
}
}