All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.pro.respawn.flowmvi.debugger.client.DebugClientStore.kt Maven / Gradle / Ivy

Go to download

A Kotlin Multiplatform MVI library based on coroutines with a powerful plugin system

There is a newer version: 3.0.0
Show newest version
package pro.respawn.flowmvi.debugger.client

import com.benasher44.uuid.uuid4
import io.ktor.client.HttpClient
import io.ktor.client.plugins.websocket.DefaultClientWebSocketSession
import io.ktor.client.plugins.websocket.receiveDeserialized
import io.ktor.client.plugins.websocket.sendSerialized
import io.ktor.client.plugins.websocket.webSocketSession
import io.ktor.http.HttpMethod
import io.ktor.websocket.close
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.withTimeoutOrNull
import pro.respawn.flowmvi.api.ActionShareBehavior.Disabled
import pro.respawn.flowmvi.api.EmptyState
import pro.respawn.flowmvi.api.Store
import pro.respawn.flowmvi.debugger.model.ClientEvent
import pro.respawn.flowmvi.debugger.model.ClientEvent.StoreConnected
import pro.respawn.flowmvi.debugger.model.ServerEvent
import pro.respawn.flowmvi.dsl.store
import pro.respawn.flowmvi.logging.StoreLogLevel
import pro.respawn.flowmvi.logging.log
import pro.respawn.flowmvi.plugins.enableLogging
import pro.respawn.flowmvi.plugins.init
import pro.respawn.flowmvi.plugins.recover
import pro.respawn.flowmvi.plugins.reduce
import kotlin.time.Duration

internal typealias DebugClientStore = Store

internal fun debugClientStore(
    clientName: String,
    client: HttpClient,
    host: String,
    port: Int,
    reconnectionDelay: Duration,
    logEvents: Boolean = false,
) = store(EmptyState) {
    val id = uuid4()
    val session = MutableStateFlow(null)
    configure {
        name = "${clientName}Debugger"
        coroutineContext = Dispatchers.Default
        debuggable = true
        parallelIntents = false // ensure the order of events matches server's expectations
        actionShareBehavior = Disabled
        allowIdleSubscriptions = true
        onOverflow = BufferOverflow.DROP_OLDEST // drop old events in the queue
    }
    if (logEvents) enableLogging()
    recover {
        log(it)
        null
    }

    init {
        launchConnectionLoop(
            reconnectionDelay,
            onError = {
                session.update {
                    it?.close()
                    null
                }
            },
        ) {
            log(StoreLogLevel.Debug) { "Starting connection at $host:$port/$id" }
            client.webSocketSession(
                method = HttpMethod.Get,
                host = host,
                port = port,
                path = "/$id",
            ).apply {
                session.update {
                    it?.close()
                    this
                }
                sendSerialized(StoreConnected(clientName, id))
                log(StoreLogLevel.Debug) { "Established connection to ${call.request.url}" }
                awaitEvents {
                    log(StoreLogLevel.Debug) { "Received event: $it" }
                    when (it) {
                        is ServerEvent.Stop -> close()
                    }
                }
            }
        }
    }

    reduce { intent ->
        withTimeoutOrNull(reconnectionDelay) {
            session.filterNotNull().first().apply {
                sendSerialized(intent)
            }
        }
    }
}

private inline fun CoroutineScope.launchConnectionLoop(
    reconnectionDelay: Duration,
    crossinline onError: suspend (Exception) -> Unit,
    crossinline connect: suspend () -> Unit,
) = launch {
    while (isActive) {
        try {
            supervisorScope {
                connect()
                awaitCancellation()
            }
        } catch (e: CancellationException) {
            onError(e)
            throw e
        } catch (expected: Exception) {
            onError(expected)
        }
        delay(reconnectionDelay)
    }
}

private suspend inline fun DefaultClientWebSocketSession.awaitEvents(onEvent: (ServerEvent) -> Unit) {
    while (isActive) onEvent(receiveDeserialized())
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy