diff --git a/app/src/main/java/de/krisenvorrat/app/data/sync/WebSocketClient.kt b/app/src/main/java/de/krisenvorrat/app/data/sync/WebSocketClient.kt index 72dcd25..c26229e 100644 --- a/app/src/main/java/de/krisenvorrat/app/data/sync/WebSocketClient.kt +++ b/app/src/main/java/de/krisenvorrat/app/data/sync/WebSocketClient.kt @@ -14,5 +14,6 @@ internal sealed interface WebSocketEvent { data object FullSyncRequired : WebSocketEvent data object Connected : WebSocketEvent data object Disconnected : WebSocketEvent + data class ConnectionFailed(val message: String) : WebSocketEvent data class NewMessage(val message: MessageDto) : WebSocketEvent } diff --git a/app/src/main/java/de/krisenvorrat/app/data/sync/WebSocketClientImpl.kt b/app/src/main/java/de/krisenvorrat/app/data/sync/WebSocketClientImpl.kt index b0b4c74..d845a8b 100644 --- a/app/src/main/java/de/krisenvorrat/app/data/sync/WebSocketClientImpl.kt +++ b/app/src/main/java/de/krisenvorrat/app/data/sync/WebSocketClientImpl.kt @@ -22,6 +22,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import javax.inject.Inject import javax.inject.Singleton +import kotlin.random.Random @Singleton internal class WebSocketClientImpl @Inject constructor() : WebSocketClient { @@ -40,6 +41,8 @@ internal class WebSocketClientImpl @Inject constructor() : WebSocketClient { connectionJob?.cancel() connectionJob = scope.launch { var backoffMs = INITIAL_BACKOFF_MS + var consecutiveFailures = 0 + var connectionFailedEmitted = false while (isActive) { try { val wsUrl = serverUrl.trimEnd('/') @@ -47,6 +50,8 @@ internal class WebSocketClientImpl @Inject constructor() : WebSocketClient { .replace("http://", "ws://") wsHttpClient.webSocket("$wsUrl/ws/sync?token=$accessToken") { backoffMs = INITIAL_BACKOFF_MS + consecutiveFailures = 0 + connectionFailedEmitted = false _events.emit(WebSocketEvent.Connected) for (frame in incoming) { if (frame is Frame.Text) { @@ -57,11 +62,20 @@ internal class WebSocketClientImpl @Inject constructor() : WebSocketClient { } catch (e: CancellationException) { break } catch (_: Exception) { - // connection failed – retry after backoff + consecutiveFailures++ + if (consecutiveFailures >= MAX_RETRIES && !connectionFailedEmitted) { + _events.emit( + WebSocketEvent.ConnectionFailed( + "Verbindung nach $MAX_RETRIES Versuchen fehlgeschlagen" + ) + ) + connectionFailedEmitted = true + } } if (!isActive) break _events.emit(WebSocketEvent.Disconnected) - delay(backoffMs) + val jitter = backoffMs * JITTER_FACTOR * (Random.nextDouble() * 2 - 1) + delay(backoffMs + jitter.toLong()) backoffMs = minOf(backoffMs * 2, MAX_BACKOFF_MS) } } @@ -100,6 +114,8 @@ internal class WebSocketClientImpl @Inject constructor() : WebSocketClient { private companion object { const val INITIAL_BACKOFF_MS = 2_000L const val MAX_BACKOFF_MS = 60_000L + const val MAX_RETRIES = 5 + const val JITTER_FACTOR = 0.25 val json = Json { ignoreUnknownKeys = true } } } diff --git a/app/src/main/java/de/krisenvorrat/app/ui/settings/SettingsViewModel.kt b/app/src/main/java/de/krisenvorrat/app/ui/settings/SettingsViewModel.kt index ab0017f..9e129e3 100644 --- a/app/src/main/java/de/krisenvorrat/app/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/de/krisenvorrat/app/ui/settings/SettingsViewModel.kt @@ -53,6 +53,12 @@ internal class SettingsViewModel @Inject constructor( when (event) { is WebSocketEvent.FullSyncRequired -> pullSync() is WebSocketEvent.InventoryUpdated -> pullSync() + is WebSocketEvent.ConnectionFailed -> { + _uiState.update { it.copy(syncStatus = SyncStatus.Error(event.message)) } + } + is WebSocketEvent.Connected -> { + _uiState.update { it.copy(syncStatus = SyncStatus.Idle) } + } else -> {} } }