Add server & shared module and remove the serveur from the client app.

This commit is contained in:
Thomas Andres Gomez 2024-11-29 18:19:54 +01:00
parent fa87f05be6
commit 3419afbe59
47 changed files with 233 additions and 530 deletions

View file

@ -29,16 +29,15 @@ kotlin {
api(libs.koin.core)
implementation(libs.koin.compose)
implementation(libs.koin.compose.viewmodel)
// network.
// common
implementation(projects.shared)
// network
implementation(libs.kotlinx.serialization.json)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.cio)
implementation(libs.ktor.client.websockets)
implementation(libs.ktor.server.core)
implementation(libs.ktor.server.netty)
implementation(libs.ktor.server.websockets)
// shell
implementation(libs.lordcodes.turttle)
implementation(libs.turtle)
}
commonTest.dependencies {

View file

@ -136,7 +136,6 @@
<string name="network__host__label">host</string>
<string name="network__port__label">port</string>
<string name="network__socket__connect_action">Se connecter à la table</string>
<string name="network__socket__host_action">Héberger la table</string>
<string name="network__socket__disconnect_action">Se déconnecter</string>
<string name="network__socket__status_state">État de connexion : %1$s</string>
<string name="network__socket__status_connected">Connecté</string>

View file

@ -1,15 +1,17 @@
package com.pixelized.desktop.lwa.repository.network
//import com.pixelized.desktop.lwa.repository.network.helper.server
//import io.ktor.server.engine.EmbeddedServer
//import io.ktor.server.netty.NettyApplicationEngine
import com.pixelized.desktop.lwa.repository.network.helper.client
import com.pixelized.desktop.lwa.repository.network.helper.connectWebSocket
import com.pixelized.desktop.lwa.repository.network.helper.server
import com.pixelized.desktop.lwa.repository.network.protocol.Message
import com.pixelized.desktop.lwa.repository.network.protocol.MessageContent
import com.pixelized.desktop.lwa.utils.extention.decodeFromFrame
import com.pixelized.desktop.lwa.utils.extention.encodeToFrame
import com.pixelized.server.lwa.SERVER_PORT
import com.pixelized.server.lwa.protocol.Message
import com.pixelized.server.lwa.protocol.MessageContent
import io.ktor.client.HttpClient
import io.ktor.server.engine.EmbeddedServer
import io.ktor.server.netty.NettyApplicationEngine
import io.ktor.websocket.Frame
import io.ktor.websocket.readText
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -21,20 +23,17 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.encodeToJsonElement
typealias Server = EmbeddedServer<NettyApplicationEngine, NettyApplicationEngine.Configuration>
typealias Client = HttpClient
class NetworkRepository {
companion object {
const val DEFAULT_PORT = 16030
const val DEFAULT_PORT = SERVER_PORT
const val DEFAULT_HOST = "pixelized.freeboxos.fr"
}
private val scope = CoroutineScope(Dispatchers.IO)
private var networkJob: Job? = null
private var server: Server? = null
private var client: Client? = null
private val outgoingMessageBuffer = MutableSharedFlow<Message>()
@ -47,65 +46,14 @@ class NetworkRepository {
private val _status = MutableStateFlow(Status.DISCONNECTED)
val status: StateFlow<Status> get() = _status
private val _type = MutableStateFlow(Type.NONE)
val type: StateFlow<Type> get() = _type
fun onPlayerNameChange(player: String) {
_player.value = player
}
fun host(
port: Int,
) {
_type.value = Type.SERVER
_status.value = Status.CONNECTED
server = server(port = port) {
println("Server launched")
val job = launch {
// send local message to the clients
outgoingMessageBuffer.collect { message ->
send(Json.encodeToFrame(message = message))
}
}
runCatching {
// watching for clients incoming message
incoming.consumeEach { frame ->
if (frame is Frame.Text) {
val message = Json.decodeFromFrame(frame = frame)
incomingMessageBuffer.emit(message)
// broadcast to clients the message
outgoingMessageBuffer.emit(message)
}
}
}.onFailure { exception ->
// TODO
println("WebSocket exception: ${exception.localizedMessage}")
}.also {
job.cancel()
}
}
networkJob?.cancel()
networkJob = scope.launch {
try {
server?.start(wait = true)
} catch (exception: Exception) {
// TODO
println("WebSocket exception: ${exception.localizedMessage}")
} finally {
println("Server close")
_type.value = Type.NONE
_status.value = Status.DISCONNECTED
}
}
}
fun connect(
host: String,
port: Int,
onConnect: (Type) -> Unit = { },
onConnect: () -> Unit = { },
onFailure: (Exception) -> Unit = { },
onClose: () -> Unit = { },
) {
@ -115,9 +63,8 @@ class NetworkRepository {
networkJob = scope.launch {
try {
client?.connectWebSocket(host = host, port = port) {
_type.value = Type.CLIENT
_status.value = Status.CONNECTED
onConnect(Type.CLIENT)
onConnect()
val job = launch {
// send message to the server
@ -140,7 +87,6 @@ class NetworkRepository {
} catch (exception: Exception) {
onFailure(exception)
} finally {
_type.value = Type.NONE
_status.value = Status.DISCONNECTED
onClose()
}
@ -150,7 +96,6 @@ class NetworkRepository {
fun disconnect() {
networkJob?.cancel()
scope.launch {
server?.stop()
client?.close()
}
}
@ -165,10 +110,6 @@ class NetworkRepository {
)
// emit the message into the outgoing buffer
outgoingMessageBuffer.emit(message)
// emit the message into the incoming buffer IF we are the server
if (type.value == Type.SERVER) {
incomingMessageBuffer.emit(message)
}
}
}
@ -176,20 +117,4 @@ class NetworkRepository {
CONNECTED,
DISCONNECTED
}
enum class Type {
CLIENT,
SERVER,
NONE,
}
}
private fun Json.decodeFromFrame(frame: Frame.Text): Message {
val json = frame.readText()
return decodeFromString<Message>(json)
}
private fun Json.encodeToFrame(message: Message): Frame {
val json = encodeToJsonElement(message)
return Frame.Text(text = json.toString())
}

View file

@ -1,39 +0,0 @@
package com.pixelized.desktop.lwa.repository.network.helper
import io.ktor.server.application.install
import io.ktor.server.engine.EmbeddedServer
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import io.ktor.server.netty.NettyApplicationEngine
import io.ktor.server.routing.routing
import io.ktor.server.websocket.DefaultWebSocketServerSession
import io.ktor.server.websocket.WebSockets
import io.ktor.server.websocket.pingPeriod
import io.ktor.server.websocket.timeout
import io.ktor.server.websocket.webSocket
import kotlin.time.Duration.Companion.seconds
// https://ktor.io/docs/server-websockets.html#handle-multiple-session
fun server(
port: Int,
handler: suspend DefaultWebSocketServerSession.() -> Unit
): EmbeddedServer<NettyApplicationEngine, NettyApplicationEngine.Configuration> {
return embeddedServer(
factory = Netty,
port = port,
module = {
install(WebSockets) {
pingPeriod = 15.seconds
timeout = 15.seconds
maxFrameSize = Long.MAX_VALUE
masking = false
}
routing {
webSocket(
path = "/ws",
handler = handler,
)
}
},
)
}

View file

@ -1,9 +0,0 @@
package com.pixelized.desktop.lwa.repository.network.protocol
import kotlinx.serialization.Serializable
@Serializable
data class Message(
val from: String,
val value: MessageContent,
)

View file

@ -1,6 +0,0 @@
package com.pixelized.desktop.lwa.repository.network.protocol
import kotlinx.serialization.Serializable
@Serializable
sealed interface MessageContent

View file

@ -1,12 +0,0 @@
package com.pixelized.desktop.lwa.repository.network.protocol
import kotlinx.serialization.Serializable
@Serializable
data class RollMessage(
val skillLabel: String,
val resultLabel: String?,
val rollDifficulty: String?,
val rollValue: Int,
val rollSuccessLimit: Int?,
) : MessageContent

View file

@ -1,8 +1,8 @@
package com.pixelized.desktop.lwa.repository.roll_history
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.repository.network.protocol.Message
import com.pixelized.desktop.lwa.repository.network.protocol.RollMessage
import com.pixelized.server.lwa.protocol.Message
import com.pixelized.server.lwa.protocol.RollMessage
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharedFlow

View file

@ -1,14 +1,12 @@
package com.pixelized.desktop.lwa.screen.network
import com.pixelized.desktop.lwa.repository.network.NetworkRepository.Status
import com.pixelized.desktop.lwa.repository.network.NetworkRepository.Type
class NetworkFactory {
fun convertToUio(
player: String,
status: Status,
type: Type,
host: String,
port: Int,
): NetworkPageUio {
@ -18,7 +16,7 @@ class NetworkFactory {
port = "$port",
enableFields = status == Status.DISCONNECTED,
enableActions = status == Status.DISCONNECTED && player.isNotBlank() && host.isNotBlank() && port > 0,
enableCancel = type != Type.NONE && status == Status.CONNECTED
enableCancel = status == Status.CONNECTED
)
}
}

View file

@ -48,11 +48,9 @@ import lwacharactersheet.composeapp.generated.resources.network__player_name__la
import lwacharactersheet.composeapp.generated.resources.network__port__label
import lwacharactersheet.composeapp.generated.resources.network__socket__connect_action
import lwacharactersheet.composeapp.generated.resources.network__socket__disconnect_action
import lwacharactersheet.composeapp.generated.resources.network__socket__host_action
import lwacharactersheet.composeapp.generated.resources.network__title
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel
import org.koin.core.annotation.KoinExperimentalAPI
@Stable
data class NetworkPageUio(
@ -64,7 +62,6 @@ data class NetworkPageUio(
val enableCancel: Boolean,
)
@OptIn(KoinExperimentalAPI::class)
@Composable
fun NetworkPage(
viewModel: NetworkViewModel = koinViewModel(),
@ -91,7 +88,6 @@ fun NetworkPage(
onHostChange = viewModel::onHostChange,
onPortChange = viewModel::onPortChange,
onConnect = viewModel::connect,
onHost = viewModel::host,
onDisconnect = viewModel::disconnect,
)
}
@ -133,7 +129,6 @@ private fun NetworkContent(
onHostChange: (String) -> Unit,
onPortChange: (String) -> Unit,
onConnect: () -> Unit,
onHost: () -> Unit,
onDisconnect: () -> Unit,
) {
Scaffold(
@ -207,13 +202,6 @@ private fun NetworkContent(
Text(text = stringResource(Res.string.network__socket__connect_action))
}
TextButton(
enabled = player.value.enableActions,
onClick = onHost,
) {
Text(text = stringResource(Res.string.network__socket__host_action))
}
TextButton(
enabled = player.value.enableCancel,
onClick = onDisconnect,

View file

@ -20,7 +20,6 @@ class NetworkViewModel(
private val repository: NetworkRepository,
private val factory: NetworkFactory
) : ViewModel() {
private val host = mutableStateOf(NetworkRepository.DEFAULT_HOST)
private val port = mutableStateOf(NetworkRepository.DEFAULT_PORT)
@ -41,13 +40,11 @@ class NetworkViewModel(
get() {
val player = repository.player.collectAsState()
val status = repository.status.collectAsState()
val type = repository.type.collectAsState()
return remember {
derivedStateOf {
factory.convertToUio(
player = player.value,
status = status.value,
type = type.value,
host = host.value,
port = port.value,
)
@ -67,10 +64,6 @@ class NetworkViewModel(
this.host.value = host
}
fun host() {
repository.host(port = port.value)
}
fun connect() {
controller.show()
_isLoading.value = true

View file

@ -4,7 +4,7 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.desktop.lwa.repository.network.protocol.RollMessage
import com.pixelized.server.lwa.protocol.RollMessage
import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository
import kotlinx.coroutines.launch

View file

@ -0,0 +1,17 @@
package com.pixelized.desktop.lwa.utils.extention
import com.pixelized.server.lwa.protocol.Message
import io.ktor.websocket.Frame
import io.ktor.websocket.readText
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.encodeToJsonElement
fun Json.decodeFromFrame(frame: Frame.Text): Message {
val json = frame.readText()
return decodeFromString<Message>(json)
}
fun Json.encodeToFrame(message: Message): Frame {
val json = encodeToJsonElement(message)
return Frame.Text(text = json.toString())
}