Refactor project data to allow server handling.
This commit is contained in:
parent
3c8eecdab5
commit
1e5f0d88ae
58 changed files with 742 additions and 469 deletions
38
server/src/main/kotlin/Module.kt
Normal file
38
server/src/main/kotlin/Module.kt
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import com.pixelized.server.lwa.model.character.CharacterSheetRepository
|
||||
import com.pixelized.server.lwa.model.character.CharacterSheetStore
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
val serverModuleDependencies
|
||||
get() = listOf(
|
||||
parserDependencies,
|
||||
factoryDependencies,
|
||||
useCaseDependencies,
|
||||
storeDependencies,
|
||||
repositoryDependencies,
|
||||
)
|
||||
|
||||
val storeDependencies
|
||||
get() = module {
|
||||
singleOf(::CharacterSheetStore)
|
||||
}
|
||||
|
||||
val repositoryDependencies
|
||||
get() = module {
|
||||
singleOf(::CharacterSheetRepository)
|
||||
}
|
||||
|
||||
val factoryDependencies
|
||||
get() = module {
|
||||
|
||||
}
|
||||
|
||||
val parserDependencies
|
||||
get() = module {
|
||||
|
||||
}
|
||||
|
||||
val useCaseDependencies
|
||||
get() = module {
|
||||
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
package com.pixelized.server.lwa
|
||||
|
||||
import com.pixelized.server.lwa.server.LocalServer
|
||||
import com.pixelized.shared.lwa.sharedModuleDependencies
|
||||
import org.koin.core.context.startKoin
|
||||
import org.koin.java.KoinJavaComponent.inject
|
||||
|
||||
fun main() {
|
||||
LocalServer().create().start()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.pixelized.server.lwa.extention
|
||||
|
||||
import com.pixelized.server.lwa.protocol.Message
|
||||
import com.pixelized.shared.lwa.protocol.Message
|
||||
import io.ktor.websocket.Frame
|
||||
import io.ktor.websocket.readText
|
||||
import kotlinx.serialization.json.Json
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
package com.pixelized.server.lwa.model.character
|
||||
|
||||
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
class CharacterSheetRepository(
|
||||
store: CharacterSheetStore,
|
||||
) {
|
||||
private val scope = CoroutineScope(Dispatchers.IO + Job())
|
||||
|
||||
private val sheets = store.characterSheetFlow()
|
||||
.stateIn(
|
||||
scope = scope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = emptyList()
|
||||
)
|
||||
|
||||
fun characterSheetFlow(): StateFlow<List<CharacterSheet>> {
|
||||
return sheets
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
package com.pixelized.server.lwa.model.character
|
||||
|
||||
import com.pixelized.shared.lwa.characterStorePath
|
||||
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheet
|
||||
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheetJson
|
||||
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheetJsonFactory
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.File
|
||||
import java.text.Collator
|
||||
|
||||
class CharacterSheetStore(
|
||||
private val factory: CharacterSheetJsonFactory,
|
||||
private val jsonFormatter: Json,
|
||||
) {
|
||||
private val characterDirectory = File(characterStorePath()).also { it.mkdirs() }
|
||||
private val flow = MutableStateFlow<List<CharacterSheet>>(value = emptyList())
|
||||
|
||||
init {
|
||||
val scope = CoroutineScope(Dispatchers.IO + Job())
|
||||
scope.launch {
|
||||
flow.value = load()
|
||||
}
|
||||
}
|
||||
|
||||
fun characterSheetFlow(): StateFlow<List<CharacterSheet>> = flow
|
||||
|
||||
@Throws(
|
||||
CharacterSheetStoreException::class,
|
||||
FileWriteException::class,
|
||||
JsonConversionException::class,
|
||||
)
|
||||
fun save(sheet: CharacterSheet) {
|
||||
// convert the character sheet into json format.
|
||||
val json = try {
|
||||
factory.convertToJson(sheet = sheet).let(jsonFormatter::encodeToString)
|
||||
} catch (exception: Exception) {
|
||||
throw JsonConversionException(root = exception)
|
||||
}
|
||||
// write the character file.
|
||||
try {
|
||||
val file = characterSheetFile(id = sheet.id)
|
||||
file.writeText(
|
||||
text = json,
|
||||
charset = Charsets.UTF_8,
|
||||
)
|
||||
} catch (exception: Exception) {
|
||||
throw FileWriteException(root = exception)
|
||||
}
|
||||
// Update the dataflow.
|
||||
flow.value = flow.value
|
||||
.toMutableList()
|
||||
.also { data ->
|
||||
val index = data.indexOfFirst { it.id == sheet.id }
|
||||
if (index >= 0) {
|
||||
data[index] = sheet
|
||||
} else {
|
||||
data.add(sheet)
|
||||
}
|
||||
}
|
||||
.sortedWith(compareBy(Collator.getInstance()) { it.name })
|
||||
}
|
||||
|
||||
fun delete(id: String): Boolean {
|
||||
val file = characterSheetFile(id = id)
|
||||
flow.value = flow.value.toMutableList()
|
||||
.also { data ->
|
||||
data.removeIf { it.id == id }
|
||||
}
|
||||
.sortedBy {
|
||||
it.name
|
||||
}
|
||||
return file.delete()
|
||||
}
|
||||
|
||||
@Throws(
|
||||
CharacterSheetStoreException::class,
|
||||
FileReadException::class,
|
||||
JsonConversionException::class,
|
||||
)
|
||||
suspend fun load(): List<CharacterSheet> {
|
||||
return characterDirectory
|
||||
.listFiles()
|
||||
?.mapNotNull { file ->
|
||||
val json = try {
|
||||
file.readText(charset = Charsets.UTF_8)
|
||||
} catch (exception: Exception) {
|
||||
throw FileReadException(root = exception)
|
||||
}
|
||||
// Guard, if the json is blank no character have been save, ignore this file.
|
||||
if (json.isBlank()) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
try {
|
||||
val sheet = jsonFormatter.decodeFromString<CharacterSheetJson>(json)
|
||||
factory.convertFromJson(sheet)
|
||||
} catch (exception: Exception) {
|
||||
throw JsonConversionException(root = exception)
|
||||
}
|
||||
}
|
||||
?.sortedWith(compareBy(Collator.getInstance()) { it.name })
|
||||
?: emptyList()
|
||||
}
|
||||
|
||||
private fun characterSheetFile(id: String): File {
|
||||
return File("${characterStorePath()}${id}.json")
|
||||
}
|
||||
|
||||
sealed class CharacterSheetStoreException(root: Exception) : Exception(root)
|
||||
class JsonConversionException(root: Exception) : CharacterSheetStoreException(root)
|
||||
class FileWriteException(root: Exception) : CharacterSheetStoreException(root)
|
||||
class FileReadException(root: Exception) : CharacterSheetStoreException(root)
|
||||
}
|
||||
|
|
@ -1,16 +1,25 @@
|
|||
package com.pixelized.server.lwa.server
|
||||
|
||||
import com.pixelized.server.lwa.SERVER_PORT
|
||||
|
||||
import com.pixelized.server.lwa.extention.decodeFromFrame
|
||||
import com.pixelized.server.lwa.extention.encodeToFrame
|
||||
import com.pixelized.server.lwa.protocol.Message
|
||||
import com.pixelized.server.lwa.model.character.CharacterSheetRepository
|
||||
import com.pixelized.shared.lwa.SERVER_PORT
|
||||
import com.pixelized.shared.lwa.model.characterSheet.model.CharacterSheetJsonFactory
|
||||
import com.pixelized.shared.lwa.protocol.Message
|
||||
import com.pixelized.shared.lwa.sharedModuleDependencies
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
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.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.server.response.respond
|
||||
import io.ktor.server.response.respondText
|
||||
import io.ktor.server.routing.get
|
||||
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
|
||||
|
|
@ -21,6 +30,9 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.koin.ktor.ext.inject
|
||||
import org.koin.ktor.plugin.Koin
|
||||
import serverModuleDependencies
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
// https://ktor.io/docs/server-websockets.html#handle-multiple-session
|
||||
|
|
@ -28,33 +40,78 @@ typealias Server = EmbeddedServer<NettyApplicationEngine, NettyApplicationEngine
|
|||
|
||||
class LocalServer {
|
||||
private var server: Server? = null
|
||||
private val json = Json { explicitNulls = true }
|
||||
private val outgoingMessageBuffer = MutableSharedFlow<Message>()
|
||||
|
||||
fun create(): LocalServer {
|
||||
server = build {
|
||||
val job = launch {
|
||||
// send local message to the clients
|
||||
outgoingMessageBuffer.collect { message ->
|
||||
send(json.encodeToFrame(message))
|
||||
fun create(
|
||||
port: Int = SERVER_PORT, // 16030
|
||||
): LocalServer {
|
||||
server = embeddedServer(
|
||||
factory = Netty,
|
||||
port = port,
|
||||
module = {
|
||||
install(Koin) {
|
||||
modules(sharedModuleDependencies + serverModuleDependencies)
|
||||
}
|
||||
|
||||
val json by inject<Json>()
|
||||
install(ContentNegotiation) {
|
||||
json(json)
|
||||
}
|
||||
|
||||
install(WebSockets) {
|
||||
pingPeriod = 15.seconds
|
||||
timeout = 15.seconds
|
||||
maxFrameSize = Long.MAX_VALUE
|
||||
masking = false
|
||||
}
|
||||
|
||||
val repository by inject<CharacterSheetRepository>()
|
||||
val factory by inject<CharacterSheetJsonFactory>()
|
||||
routing {
|
||||
get(
|
||||
path = "/",
|
||||
body = {
|
||||
call.respondText(contentType = ContentType.Text.Html) {
|
||||
"<a href=\"http://127.0.0.1:16030/characters\">characters</a>"
|
||||
}
|
||||
}
|
||||
)
|
||||
get(
|
||||
path = "/characters",
|
||||
body = {
|
||||
val body = repository.characterSheetFlow().value.map(factory::convertToJson)
|
||||
call.respond(body)
|
||||
},
|
||||
)
|
||||
webSocket(
|
||||
path = "/ws",
|
||||
handler = {
|
||||
val job = launch {
|
||||
// send local message to the clients
|
||||
outgoingMessageBuffer.collect { message ->
|
||||
send(json.encodeToFrame(message))
|
||||
}
|
||||
}
|
||||
runCatching {
|
||||
// watching for clients incoming message
|
||||
incoming.consumeEach { frame ->
|
||||
if (frame is Frame.Text) {
|
||||
val message = Json.decodeFromFrame(frame = frame)
|
||||
println(message)
|
||||
// broadcast to clients the message
|
||||
outgoingMessageBuffer.emit(message)
|
||||
}
|
||||
}
|
||||
}.onFailure { exception ->
|
||||
println("WebSocket exception: ${exception.localizedMessage}")
|
||||
}.also {
|
||||
job.cancel()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
runCatching {
|
||||
// watching for clients incoming message
|
||||
incoming.consumeEach { frame ->
|
||||
if (frame is Frame.Text) {
|
||||
val message = Json.decodeFromFrame(frame = frame)
|
||||
println(message)
|
||||
// broadcast to clients the message
|
||||
outgoingMessageBuffer.emit(message)
|
||||
}
|
||||
}
|
||||
}.onFailure { exception ->
|
||||
println("WebSocket exception: ${exception.localizedMessage}")
|
||||
}.also {
|
||||
job.cancel()
|
||||
}
|
||||
}
|
||||
)
|
||||
return this
|
||||
}
|
||||
|
||||
|
|
@ -70,28 +127,4 @@ class LocalServer {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun build(
|
||||
port: Int = SERVER_PORT,
|
||||
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,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue