From a84c170396f1b5810519da8a2c3e1c57c5356772 Mon Sep 17 00:00:00 2001 From: "Andres Gomez, Thomas (ITDV RL)" Date: Tue, 13 May 2025 09:48:51 +0200 Subject: [PATCH] Optimize server dispatcher --- server/src/main/kotlin/Module.kt | 5 + .../lwa/model/alteration/AlterationService.kt | 2 +- .../lwa/model/alteration/AlterationStore.kt | 181 +++++++------- .../lwa/model/campaign/CampaignService.kt | 14 +- .../lwa/model/campaign/CampaignStore.kt | 104 ++++---- .../model/character/CharacterSheetService.kt | 12 +- .../model/character/CharacterSheetStore.kt | 193 +++++++-------- .../lwa/model/inventory/InventoryService.kt | 16 +- .../lwa/model/inventory/InventoryStore.kt | 166 ++++++------- .../server/lwa/model/item/ItemService.kt | 4 +- .../server/lwa/model/item/ItemStore.kt | 228 +++++++++--------- .../server/lwa/model/tag/TagStore.kt | 17 +- 12 files changed, 483 insertions(+), 459 deletions(-) diff --git a/server/src/main/kotlin/Module.kt b/server/src/main/kotlin/Module.kt index e2ca61a..2b0345d 100644 --- a/server/src/main/kotlin/Module.kt +++ b/server/src/main/kotlin/Module.kt @@ -12,6 +12,8 @@ import com.pixelized.server.lwa.model.tag.TagService import com.pixelized.server.lwa.model.tag.TagStore import com.pixelized.server.lwa.server.Engine import com.pixelized.shared.lwa.utils.PathProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import org.koin.core.module.dsl.createdAtStart import org.koin.core.module.dsl.singleOf import org.koin.dsl.module @@ -29,6 +31,9 @@ val toolsDependencies single { PathProvider(appName = "LwaServer") } + factory { + CoroutineScope(Job()) + } } val engineDependencies diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationService.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationService.kt index e450290..91bb091 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationService.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationService.kt @@ -32,7 +32,7 @@ class AlterationService( } @Throws - fun save( + suspend fun save( json: AlterationJson, create: Boolean, ) { diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationStore.kt index 58a1113..2414213 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationStore.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/alteration/AlterationStore.kt @@ -12,11 +12,11 @@ import com.pixelized.shared.lwa.protocol.rest.APIResponse import com.pixelized.shared.lwa.utils.PathProvider 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.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import java.io.File import java.text.Collator @@ -24,15 +24,14 @@ import java.text.Collator class AlterationStore( private val pathProvider: PathProvider, private val factory: AlterationJsonFactory, - private val json: Json, + private val jsonSerializer: Json, + scope: CoroutineScope, ) { private val directory = File(pathProvider.alterationsPath()).also { it.mkdirs() } private val alterationFlow = MutableStateFlow>(emptyList()) init { - // build a coroutine scope for async calls - val scope = CoroutineScope(Dispatchers.IO + Job()) // load the initial data scope.launch { updateAlterationFlow() @@ -41,7 +40,7 @@ class AlterationStore( fun alterationsFlow(): StateFlow> = alterationFlow - fun updateAlterationFlow() { + suspend fun updateAlterationFlow() { alterationFlow.value = try { load() } catch (exception: Exception) { @@ -52,104 +51,110 @@ class AlterationStore( @Throws( FileReadException::class, + JsonCodingException::class, JsonConversionException::class, ) - private fun load( + private suspend fun load( directory: File = this.directory, ): List { - return directory - .listFiles() - ?.mapNotNull { file -> - val json = try { - file.readText(charset = Charsets.UTF_8) - } catch (exception: Exception) { - throw FileReadException(root = exception) + return withContext(Dispatchers.IO) { + directory + .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 alteration have been save, ignore this file. + if (json.isBlank()) { + return@mapNotNull null + } + // decode the file + val data = try { + jsonSerializer.decodeFromString(json) + } catch (exception: Exception) { + throw JsonCodingException(root = exception) + } + // parse the json string. + val alterations = try { + factory.convertFromJson(data) + } catch (exception: Exception) { + throw JsonConversionException(root = exception) + } + return@mapNotNull alterations } - // Guard, if the json is blank no alteration have been save, ignore this file. - if (json.isBlank()) { - return@mapNotNull null - } - // decode the file - val data = try { - this.json.decodeFromString(json) - } catch (exception: Exception) { - throw JsonCodingException(root = exception) - } - // parse the json string. - try { - factory.convertFromJson(data) - } catch (exception: Exception) { - throw JsonConversionException(root = exception) - } - } - ?.sortedWith(compareBy(Collator.getInstance()) { it.metadata.name }) - ?: emptyList() + ?.sortedWith(compareBy(Collator.getInstance()) { it.metadata.name }) + ?: emptyList() + } } @Throws( BusinessException::class, - JsonConversionException::class, - JsonCodingException::class, FileWriteException::class, + JsonCodingException::class, + JsonConversionException::class, ) - fun save( + suspend fun save( json: AlterationJson, create: Boolean, ) { - val file = alterationFile(id = json.id) - // Guard case on update alteration - if (create && file.exists()) { - throw BusinessException( - message = "Alteration already exist, creation is impossible.", - ) - } - // Transform the json into the model. - val alteration = try { - factory.convertFromJson(json) - } catch (exception: Exception) { - throw JsonConversionException(root = exception) - } - if (alteration.id.isEmpty()) { - throw BusinessException( - message = "Alteration 'id' is a mandatory field.", - code = APIResponse.ErrorCode.AlterationId, - ) - } - if (alteration.metadata.name.isEmpty()) { - throw BusinessException( - message = "Alteration 'name' is a mandatory field.", - code = APIResponse.ErrorCode.AlterationName, - ) - } - // Encode the json into a string. - val data = try { - this.json.encodeToString(json) - } catch (exception: Exception) { - throw JsonCodingException(root = exception) - } - // Write the alteration into a file. - try { - file.writeText( - text = data, - charset = Charsets.UTF_8, - ) - } catch (exception: Exception) { - throw FileWriteException(root = exception) - } - // Update the dataflow. - alterationFlow.update { alterations -> - val index = alterations.indexOfFirst { it.id == json.id } - alterations.toMutableList() - .also { - if (index >= 0) { - it[index] = alteration - } else { - it.add(alteration) + withContext(Dispatchers.IO) { + val file = alterationFile(id = json.id) + // Guard case on update alteration + if (create && file.exists()) { + throw BusinessException( + message = "Alteration already exist, creation is impossible.", + ) + } + // Transform the json into the model. + val alteration = try { + factory.convertFromJson(json) + } catch (exception: Exception) { + throw JsonConversionException(root = exception) + } + if (alteration.id.isEmpty()) { + throw BusinessException( + message = "Alteration 'id' is a mandatory field.", + code = APIResponse.ErrorCode.AlterationId, + ) + } + if (alteration.metadata.name.isEmpty()) { + throw BusinessException( + message = "Alteration 'name' is a mandatory field.", + code = APIResponse.ErrorCode.AlterationName, + ) + } + // Encode the json into a string. + val data = try { + jsonSerializer.encodeToString(json) + } catch (exception: Exception) { + throw JsonCodingException(root = exception) + } + // Write the alteration into a file. + try { + file.writeText( + text = data, + charset = Charsets.UTF_8, + ) + } catch (exception: Exception) { + throw FileWriteException(root = exception) + } + // Update the dataflow. + alterationFlow.update { alterations -> + val index = alterations.indexOfFirst { it.id == json.id } + alterations.toMutableList() + .also { + if (index >= 0) { + it[index] = alteration + } else { + it.add(alteration) + } } - } - .sortedWith(compareBy(Collator.getInstance()) { - it.metadata.name - }) + .sortedWith(compareBy(Collator.getInstance()) { + it.metadata.name + }) + } } } diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignService.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignService.kt index e71b8fa..7521cac 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignService.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignService.kt @@ -39,7 +39,7 @@ class CampaignService( } @Throws - fun addCharacter( + suspend fun addCharacter( characterSheetId: String, ) { // Check if the character is already in the campaign. @@ -57,7 +57,7 @@ class CampaignService( } @Throws - fun addNpc( + suspend fun addNpc( characterSheetId: String, ) { // Check if the character is already in the campaign. @@ -75,7 +75,7 @@ class CampaignService( } @Throws - fun removeCharacter( + suspend fun removeCharacter( characterSheetId: String, ) { // Check if the character is in the campaign. @@ -93,7 +93,7 @@ class CampaignService( } @Throws - fun removeNpc( + suspend fun removeNpc( characterSheetId: String, ) { // Check if the character is in the campaign. @@ -111,7 +111,7 @@ class CampaignService( } @Throws - fun setScene( + suspend fun setScene( scene: Campaign.Scene, ) { // save the campaign to the disk + update the flow. @@ -122,7 +122,7 @@ class CampaignService( // Data manipulation through WebSocket. - fun updateToggleParty() { + suspend fun updateToggleParty() { store.save( campaign = campaign.copy( options = campaign.options.copy( @@ -132,7 +132,7 @@ class CampaignService( ) } - fun updateToggleNpc() { + suspend fun updateToggleNpc() { store.save( campaign = campaign.copy( options = campaign.options.copy( diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignStore.kt index f612cea..63bc43d 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignStore.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/campaign/CampaignStore.kt @@ -15,21 +15,21 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import java.io.File class CampaignStore( private val pathProvider: PathProvider, private val factory: CampaignJsonFactory, - private val json: Json, + private val jsonSerializer: Json, + scope: CoroutineScope, ) { private val campaignFlow = MutableStateFlow(value = Campaign.empty()) init { // create the directory if needed. File(pathProvider.campaignPath()).also { it.mkdirs() } - // build a coroutine scope for async calls - val scope = CoroutineScope(Dispatchers.IO + Job()) // load the initial data scope.launch { updateCampaignFlow() @@ -38,7 +38,7 @@ class CampaignStore( fun campaignFlow(): StateFlow = campaignFlow - fun updateCampaignFlow() { + suspend fun updateCampaignFlow() { campaignFlow.value = try { load() } catch (exception: Exception) { @@ -52,29 +52,31 @@ class CampaignStore( JsonCodingException::class, JsonConversionException::class, ) - fun load(): Campaign { - val file = campaignFile() - // Read the campaign file. - val data = try { - file.readText(charset = Charsets.UTF_8) - } catch (exception: Exception) { - throw FileReadException(root = exception) + suspend fun load(): Campaign { + return withContext(Dispatchers.IO) { + val file = campaignFile() + // Read the campaign file. + val data = try { + file.readText(charset = Charsets.UTF_8) + } catch (exception: Exception) { + throw FileReadException(root = exception) + } + // Guard, if the file is empty we load a default campaign. + if (data.isBlank()) return@withContext Campaign.empty() + // Decode the json into a string. + val json = try { + jsonSerializer.decodeFromString(data) + } catch (exception: Exception) { + throw JsonCodingException(root = exception) + } + // Convert from the Json format + val campaign = try { + factory.convertFromJson(json) + } catch (exception: Exception) { + throw JsonConversionException(root = exception) + } + return@withContext campaign } - // Guard, if the file is empty we load a default campaign. - if (data.isBlank()) return Campaign.empty() - // Decode the json into a string. - val json = try { - this.json.decodeFromString(data) - } catch (exception: Exception) { - throw JsonCodingException(root = exception) - } - // Convert from the Json format - val campaign = try { - factory.convertFromJson(json) - } catch (exception: Exception) { - throw JsonConversionException(root = exception) - } - return campaign } @Throws( @@ -82,31 +84,33 @@ class CampaignStore( JsonCodingException::class, FileWriteException::class, ) - fun save(campaign: Campaign) { - // Transform the json into the model. - val json = try { - factory.convertToJson(campaign = campaign) - } catch (exception: Exception) { - throw JsonConversionException(root = exception) + suspend fun save(campaign: Campaign) { + withContext(Dispatchers.IO) { + // Transform the json into the model. + val json = try { + factory.convertToJson(campaign = campaign) + } catch (exception: Exception) { + throw JsonConversionException(root = exception) + } + // Encode the json into a string. + val data = try { + jsonSerializer.encodeToString(json) + } catch (exception: Exception) { + throw JsonCodingException(root = exception) + } + // Write the file + try { + val file = campaignFile() + file.writeText( + text = data, + charset = Charsets.UTF_8, + ) + } catch (exception: Exception) { + throw FileWriteException(root = exception) + } + // Update the dataflow. + campaignFlow.update { campaign } } - // Encode the json into a string. - val data = try { - this.json.encodeToString(json) - } catch (exception: Exception) { - throw JsonCodingException(root = exception) - } - // Write the file - try { - val file = campaignFile() - file.writeText( - text = data, - charset = Charsets.UTF_8, - ) - } catch (exception: Exception) { - throw FileWriteException(root = exception) - } - // Update the dataflow. - campaignFlow.update { campaign } } private fun campaignFile(): File { diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetService.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetService.kt index 5005af7..a02611e 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetService.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetService.kt @@ -55,7 +55,7 @@ class CharacterSheetService( } @Throws - fun deleteCharacterSheet( + suspend fun deleteCharacterSheet( characterSheetId: String, ) { characterStore.delete( @@ -65,7 +65,7 @@ class CharacterSheetService( // Data manipulation through WebSocket. - fun updateAlteration( + suspend fun updateAlteration( characterSheetId: String, alterationId: String, active: Boolean, @@ -97,7 +97,7 @@ class CharacterSheetService( } } - fun updateDamage( + suspend fun updateDamage( characterSheetId: String, damage: Int, ) { @@ -110,7 +110,7 @@ class CharacterSheetService( } } - fun updateDiminished( + suspend fun updateDiminished( characterSheetId: String, diminished: Int, ) { @@ -123,7 +123,7 @@ class CharacterSheetService( } } - fun updateFatigue( + suspend fun updateFatigue( characterSheetId: String, fatigue: Int, ) { @@ -136,7 +136,7 @@ class CharacterSheetService( } } - fun updateCharacterSkillUsage( + suspend fun updateCharacterSkillUsage( characterSheetId: String, skillId: String, used: Boolean, diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetStore.kt index f2b3545..3cd0b3d 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetStore.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/character/CharacterSheetStore.kt @@ -12,11 +12,11 @@ import com.pixelized.shared.lwa.protocol.rest.APIResponse import com.pixelized.shared.lwa.utils.PathProvider 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.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import java.io.File import java.text.Collator @@ -24,14 +24,13 @@ import java.text.Collator class CharacterSheetStore( private val pathProvider: PathProvider, private val factory: CharacterSheetJsonFactory, - private val json: Json, + private val jsonSerializer: Json, + scope: CoroutineScope, ) { private val directory = File(pathProvider.characterStorePath()).also { it.mkdirs() } private val characterSheetsFlow = MutableStateFlow>(value = emptyList()) init { - // build a coroutine scope for async calls - val scope = CoroutineScope(Dispatchers.IO + Job()) // load the initial data scope.launch { updateCharacterFlow() @@ -40,7 +39,7 @@ class CharacterSheetStore( fun characterSheetsFlow(): StateFlow> = characterSheetsFlow - fun updateCharacterFlow() { + suspend fun updateCharacterFlow() { characterSheetsFlow.value = try { load() } catch (exception: Exception) { @@ -54,112 +53,118 @@ class CharacterSheetStore( JsonCodingException::class, JsonConversionException::class, ) - fun load( + suspend fun load( directory: File = this.directory, ): List { - return directory - .listFiles() - ?.mapNotNull { file -> - val json = try { - file.readText(charset = Charsets.UTF_8) - } catch (exception: Exception) { - throw FileReadException(root = exception) + return withContext(Dispatchers.IO) { + directory + .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 + } + // decode the file + val data = try { + jsonSerializer.decodeFromString(json) + } catch (exception: Exception) { + throw JsonCodingException(root = exception) + } + // parse the json string. + try { + factory.convertFromJson(data) + } catch (exception: Exception) { + throw JsonConversionException(root = exception) + } } - // Guard, if the json is blank no character have been save, ignore this file. - if (json.isBlank()) { - return@mapNotNull null - } - // decode the file - val data = try { - this.json.decodeFromString(json) - } catch (exception: Exception) { - throw JsonCodingException(root = exception) - } - // parse the json string. - try { - factory.convertFromJson(data) - } catch (exception: Exception) { - throw JsonConversionException(root = exception) - } - } - ?.sortedWith(compareBy(Collator.getInstance()) { it.name }) - ?: emptyList() + ?.sortedWith(compareBy(Collator.getInstance()) { it.name }) + ?: emptyList() + } } @Throws( BusinessException::class, - JsonConversionException::class, - JsonCodingException::class, FileWriteException::class, + JsonCodingException::class, + JsonConversionException::class, ) - fun save( + suspend fun save( sheet: CharacterSheet, create: Boolean, ) { - val file = characterSheetFile(id = sheet.id) - // Guard case on update alteration - if (create && file.exists()) { - throw BusinessException(message = "Character already exist, creation is impossible.") - } - // Transform the json into the model. - val json = try { - factory.convertToJson(sheet = sheet) - } catch (exception: Exception) { - throw JsonConversionException(root = exception) - } - // Encode the json into a string. - val data = try { - this.json.encodeToString(json) - } catch (exception: Exception) { - throw JsonCodingException(root = exception) - } - // write the character file. - try { - file.writeText( - text = data, - charset = Charsets.UTF_8, - ) - } catch (exception: Exception) { - throw FileWriteException(root = exception) - } - // Update the dataflow. - characterSheetsFlow.update { characters -> - characters.toMutableList() - .also { data -> - val index = data.indexOfFirst { it.id == sheet.id } - if (index >= 0) { - data[index] = sheet - } else { - data.add(sheet) + withContext(Dispatchers.IO) { + val file = characterSheetFile(id = sheet.id) + // Guard case on update alteration + if (create && file.exists()) { + throw BusinessException(message = "Character already exist, creation is impossible.") + } + // Transform the json into the model. + val json = try { + factory.convertToJson(sheet = sheet) + } catch (exception: Exception) { + throw JsonConversionException(root = exception) + } + // Encode the json into a string. + val data = try { + jsonSerializer.encodeToString(json) + } catch (exception: Exception) { + throw JsonCodingException(root = exception) + } + // write the character file. + try { + file.writeText( + text = data, + charset = Charsets.UTF_8, + ) + } catch (exception: Exception) { + throw FileWriteException(root = exception) + } + // Update the dataflow. + characterSheetsFlow.update { characters -> + characters.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 - }) + .sortedWith(compareBy(Collator.getInstance()) { + it.name + }) + } } } @Throws(BusinessException::class) - fun delete(characterSheetId: String) { - val file = characterSheetFile(id = characterSheetId) - // Guard case on the file existence. - if (file.exists().not()) { - throw BusinessException( - message = "Character file with id:$characterSheetId doesn't not exist.", - code = APIResponse.ErrorCode.CharacterSheetId - ) - } - // Guard case on the file deletion - if (file.delete().not()) { - throw BusinessException( - message = "Character file have not been deleted for unknown reason.", - ) - } - // Update the data model with the deleted character. - characterSheetsFlow.update { characters -> - characters.toMutableList() - .also { data -> data.removeIf { it.id == characterSheetId } } - .sortedWith(compareBy(Collator.getInstance()) { it.name }) + suspend fun delete(characterSheetId: String) { + withContext(Dispatchers.IO) { + val file = characterSheetFile(id = characterSheetId) + // Guard case on the file existence. + if (file.exists().not()) { + throw BusinessException( + message = "Character file with id:$characterSheetId doesn't not exist.", + code = APIResponse.ErrorCode.CharacterSheetId + ) + } + // Guard case on the file deletion + if (file.delete().not()) { + throw BusinessException( + message = "Character file have not been deleted for unknown reason.", + ) + } + // Update the data model with the deleted character. + characterSheetsFlow.update { characters -> + characters.toMutableList() + .also { data -> data.removeIf { it.id == characterSheetId } } + .sortedWith(compareBy(Collator.getInstance()) { it.name }) + } } } diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryService.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryService.kt index 5aa75d4..9c0ba11 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryService.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryService.kt @@ -40,7 +40,7 @@ class InventoryService( } @Throws - fun updatePurse( + suspend fun updatePurse( purse: ApiPurseJson, ) { val inventory = inventory( @@ -59,7 +59,7 @@ class InventoryService( } @Throws - fun save( + suspend fun save( inventoryJson: InventoryJson, create: Boolean, ) { @@ -70,12 +70,12 @@ class InventoryService( } @Throws - fun delete(characterSheetId: String) { + suspend fun delete(characterSheetId: String) { inventoryStore.delete(characterSheetId = characterSheetId) } @Throws - fun createInventoryItem( + suspend fun createInventoryItem( characterSheetId: String, itemId: String, count: Float, @@ -107,7 +107,7 @@ class InventoryService( } @Throws - fun changeInventoryItemCount( + suspend fun changeInventoryItemCount( characterSheetId: String, inventoryId: String, count: Float, @@ -145,7 +145,7 @@ class InventoryService( } @Throws - fun consumeInventoryItem( + suspend fun consumeInventoryItem( characterSheetId: String, inventoryId: String, ) { @@ -185,7 +185,7 @@ class InventoryService( } @Throws - fun equipInventoryItem( + suspend fun equipInventoryItem( characterSheetId: String, inventoryId: String, equip: Boolean, @@ -223,7 +223,7 @@ class InventoryService( } @Throws - fun deleteInventoryItem( + suspend fun deleteInventoryItem( characterSheetId: String, inventoryId: String, ) { diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryStore.kt index b9f3fed..4c8f32a 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryStore.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/inventory/InventoryStore.kt @@ -12,26 +12,24 @@ import com.pixelized.shared.lwa.protocol.rest.APIResponse import com.pixelized.shared.lwa.utils.PathProvider 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.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import java.io.File -import java.util.UUID class InventoryStore( private val pathProvider: PathProvider, private val factory: InventoryJsonFactory, - private val json: Json, + private val jsonSerializer: Json, + scope: CoroutineScope, ) { private val directory = File(pathProvider.inventoryPath()).also { it.mkdirs() } private val inventoryFlow = MutableStateFlow>(value = emptyMap()) init { - // build a coroutine scope for async calls - val scope = CoroutineScope(Dispatchers.IO + Job()) // load the initial data scope.launch { updateInventoryFlow() @@ -40,7 +38,7 @@ class InventoryStore( fun inventoryFlow(): StateFlow> = inventoryFlow - fun updateInventoryFlow() { + suspend fun updateInventoryFlow() { inventoryFlow.value = try { load() } catch (exception: Exception) { @@ -54,102 +52,104 @@ class InventoryStore( JsonCodingException::class, JsonConversionException::class, ) - private fun load( + private suspend fun load( directory: File = this.directory, ): Map { - return directory - .listFiles() - ?.mapNotNull { file -> - val json = try { - file.readText(charset = Charsets.UTF_8) - } catch (exception: Exception) { - throw FileReadException(root = exception) + return withContext(Dispatchers.IO) { + directory + .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 + } + // decode the file + val data = try { + jsonSerializer.decodeFromString(json) + } catch (exception: Exception) { + throw JsonCodingException(root = exception) + } + // parse the json string. + val inventory = try { + factory.convertFromJson(data) + } catch (exception: Exception) { + throw JsonConversionException(root = exception) + } + inventory.characterSheetId to inventory } - // Guard, if the json is blank no character have been save, ignore this file. - if (json.isBlank()) { - return@mapNotNull null - } - // decode the file - val data = try { - this.json.decodeFromString(json) - } catch (exception: Exception) { - throw JsonCodingException(root = exception) - } - // parse the json string. - val inventory = try { - factory.convertFromJson(data) - } catch (exception: Exception) { - throw JsonConversionException(root = exception) - } - inventory.characterSheetId to inventory - } - ?.toMap() - ?: emptyMap() + ?.toMap() + ?: emptyMap() + } } @Throws( BusinessException::class, - JsonConversionException::class, - JsonCodingException::class, FileWriteException::class, + JsonCodingException::class, + JsonConversionException::class, ) - fun save( + suspend fun save( inventory: Inventory, create: Boolean, ) { - val file = inventoryFile(id = inventory.characterSheetId) - - if (create && file.exists()) { - throw BusinessException(message = "Inventory already exist, creation is impossible.") - } - - val json = try { - factory.convertToJson(inventory = inventory) - } catch (exception: Exception) { - throw JsonConversionException(root = exception) - } - - val data = try { - this.json.encodeToString(json) - } catch (exception: Exception) { - throw JsonCodingException(root = exception) - } - - try { - file.writeText( - text = data, - charset = Charsets.UTF_8, + withContext(Dispatchers.IO) { + val file = inventoryFile( + id = inventory.characterSheetId ) - } catch (exception: Exception) { - throw FileWriteException(root = exception) - } - - inventoryFlow.update { flow -> - flow.toMutableMap().also { data -> - data[inventory.characterSheetId] = inventory + if (create && file.exists()) { + throw BusinessException(message = "Inventory already exist, creation is impossible.") + } + val json = try { + factory.convertToJson(inventory = inventory) + } catch (exception: Exception) { + throw JsonConversionException(root = exception) + } + val data = try { + jsonSerializer.encodeToString(json) + } catch (exception: Exception) { + throw JsonCodingException(root = exception) + } + try { + file.writeText( + text = data, + charset = Charsets.UTF_8, + ) + } catch (exception: Exception) { + throw FileWriteException(root = exception) + } + inventoryFlow.update { flow -> + flow.toMutableMap().also { data -> + data[inventory.characterSheetId] = inventory + } } } } @Throws(BusinessException::class) - fun delete(characterSheetId: String) { - val file = inventoryFile(id = characterSheetId) - - if (file.exists().not()) { - throw BusinessException( - message = "Inventory file with id:$characterSheetId doesn't not exist.", - code = APIResponse.ErrorCode.CharacterSheetId + suspend fun delete(characterSheetId: String) { + withContext(Dispatchers.IO) { + val file = inventoryFile( + id = characterSheetId, ) - } - - if (file.delete().not()) { - throw BusinessException( - message = "Inventory file have not been deleted for unknown reason.", - ) - } - - inventoryFlow.update { characters -> - characters.toMutableMap().also { data -> data.remove(characterSheetId) } + if (file.exists().not()) { + throw BusinessException( + message = "Inventory file with id:$characterSheetId doesn't not exist.", + code = APIResponse.ErrorCode.CharacterSheetId + ) + } + if (file.delete().not()) { + throw BusinessException( + message = "Inventory file have not been deleted for unknown reason.", + ) + } + inventoryFlow.update { characters -> + characters.toMutableMap().also { data -> data.remove(characterSheetId) } + } } } diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemService.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemService.kt index b3d1aba..ed03b38 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemService.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemService.kt @@ -32,7 +32,7 @@ class ItemService( } @Throws - fun save( + suspend fun save( json: ItemJson, create: Boolean, ) { @@ -43,7 +43,7 @@ class ItemService( } @Throws - fun delete(itemId: String) { + suspend fun delete(itemId: String) { itemStore.delete(id = itemId) } } diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemStore.kt index cfea1ef..135977a 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemStore.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/item/ItemStore.kt @@ -12,11 +12,11 @@ import com.pixelized.shared.lwa.protocol.rest.APIResponse import com.pixelized.shared.lwa.utils.PathProvider 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.flow.update import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import java.io.File import java.text.Collator @@ -24,14 +24,13 @@ import java.text.Collator class ItemStore( private val pathProvider: PathProvider, private val factory: ItemJsonFactory, - private val json: Json, + private val jsonSerializer: Json, + scope: CoroutineScope, ) { private val directory = File(pathProvider.itemsPath()).also { it.mkdirs() } private val itemFlow = MutableStateFlow>(emptyList()) init { - val scope = CoroutineScope(Dispatchers.IO + Job()) - scope.launch { updateItemsFlow() } @@ -43,7 +42,7 @@ class ItemStore( fun itemsFlow(): StateFlow> = itemFlow - fun updateItemsFlow() { + suspend fun updateItemsFlow() { itemFlow.value = try { load() } catch (exception: Exception) { @@ -54,131 +53,138 @@ class ItemStore( @Throws( FileReadException::class, + JsonCodingException::class, JsonConversionException::class, ) - private fun load( + private suspend fun load( directory: File = this.directory, ): List { - return directory - .listFiles() - ?.mapNotNull { file -> - val json = try { - file.readText(charset = Charsets.UTF_8) - } catch (exception: Exception) { - throw FileReadException(root = exception) + return withContext(Dispatchers.IO) { + directory + .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 item have been save, ignore this file. + if (json.isBlank()) { + return@mapNotNull null + } + // decode the file + val data = try { + jsonSerializer.decodeFromString(json) + } catch (exception: Exception) { + throw JsonCodingException(root = exception) + } + // parse the json string. + try { + factory.convertFromJson(data) + } catch (exception: Exception) { + throw JsonConversionException(root = exception) + } } - // Guard, if the json is blank no item have been save, ignore this file. - if (json.isBlank()) { - return@mapNotNull null - } - // decode the file - val data = try { - this.json.decodeFromString(json) - } catch (exception: Exception) { - throw JsonCodingException(root = exception) - } - // parse the json string. - try { - factory.convertFromJson(data) - } catch (exception: Exception) { - throw JsonConversionException(root = exception) - } - } - ?.sortedWith(compareBy(Collator.getInstance()) { it.metadata.label }) - ?: emptyList() + ?.sortedWith(compareBy(Collator.getInstance()) { it.metadata.label }) + ?: emptyList() + } } @Throws( BusinessException::class, - JsonConversionException::class, - JsonCodingException::class, FileWriteException::class, + JsonCodingException::class, + JsonConversionException::class, ) - fun save( + suspend fun save( json: ItemJson, create: Boolean, ) { - val file = itemFile(id = json.id) - // Guard case on update alteration - if (create && file.exists()) { - throw BusinessException( - message = "Item already exist, creation is impossible.", - ) - } - // Transform the json into the model. - val item = try { - factory.convertFromJson(json) - } catch (exception: Exception) { - throw JsonConversionException(root = exception) - } - if (item.id.isEmpty()) { - throw BusinessException( - message = "Item 'id' is a mandatory field.", - code = APIResponse.ErrorCode.ItemId, - ) - } - if (item.metadata.label.isEmpty()) { - throw BusinessException( - message = "Item 'name' is a mandatory field.", - code = APIResponse.ErrorCode.ItemName, - ) - } - // Encode the json into a string. - val data = try { - this.json.encodeToString(json) - } catch (exception: Exception) { - throw JsonCodingException(root = exception) - } - // Write the alteration into a file. - try { - file.writeText( - text = data, - charset = Charsets.UTF_8, - ) - } catch (exception: Exception) { - throw FileWriteException(root = exception) - } - // Update the dataflow. - itemFlow.update { items -> - val index = items.indexOfFirst { it.id == json.id } - items.toMutableList() - .also { - if (index >= 0) { - it[index] = item - } else { - it.add(item) + withContext(Dispatchers.IO) { + val file = itemFile(id = json.id) + // Guard case on update alteration + if (create && file.exists()) { + throw BusinessException( + message = "Item already exist, creation is impossible.", + ) + } + // Transform the json into the model. + val item = try { + factory.convertFromJson(json) + } catch (exception: Exception) { + throw JsonConversionException(root = exception) + } + if (item.id.isEmpty()) { + throw BusinessException( + message = "Item 'id' is a mandatory field.", + code = APIResponse.ErrorCode.ItemId, + ) + } + if (item.metadata.label.isEmpty()) { + throw BusinessException( + message = "Item 'name' is a mandatory field.", + code = APIResponse.ErrorCode.ItemName, + ) + } + // Encode the json into a string. + val data = try { + jsonSerializer.encodeToString(json) + } catch (exception: Exception) { + throw JsonCodingException(root = exception) + } + // Write the alteration into a file. + try { + file.writeText( + text = data, + charset = Charsets.UTF_8, + ) + } catch (exception: Exception) { + throw FileWriteException(root = exception) + } + // Update the dataflow. + itemFlow.update { items -> + val index = items.indexOfFirst { it.id == json.id } + items.toMutableList() + .also { + if (index >= 0) { + it[index] = item + } else { + it.add(item) + } } - } - .sortedWith(compareBy(Collator.getInstance()) { - it.metadata.label - }) + .sortedWith(compareBy(Collator.getInstance()) { + it.metadata.label + }) + } } } @Throws(BusinessException::class) - fun delete(id: String) { - val file = itemFile(id = id) - // Guard case on the file existence. - if (file.exists().not()) { - throw BusinessException( - message = "Item doesn't not exist, deletion is impossible.", - ) - } - // Guard case on the file deletion - if (file.delete().not()) { - throw BusinessException( - message = "Item file have not been deleted for unknown reason.", - ) - } - // Update the data model with the deleted alteration. - itemFlow.update { items -> - items.toMutableList() - .also { item -> - item.removeIf { it.id == id } - } - .sortedWith(compareBy(Collator.getInstance()) { - it.metadata.label - }) + suspend fun delete(id: String) { + withContext(Dispatchers.IO) { + val file = itemFile(id = id) + // Guard case on the file existence. + if (file.exists().not()) { + throw BusinessException( + message = "Item doesn't not exist, deletion is impossible.", + ) + } + // Guard case on the file deletion + if (file.delete().not()) { + throw BusinessException( + message = "Item file have not been deleted for unknown reason.", + ) + } + // Update the data model with the deleted alteration. + itemFlow.update { items -> + items.toMutableList() + .also { item -> + item.removeIf { it.id == id } + } + .sortedWith(compareBy(Collator.getInstance()) { + it.metadata.label + }) + } } } diff --git a/server/src/main/kotlin/com/pixelized/server/lwa/model/tag/TagStore.kt b/server/src/main/kotlin/com/pixelized/server/lwa/model/tag/TagStore.kt index b87d48d..4fd02b7 100644 --- a/server/src/main/kotlin/com/pixelized/server/lwa/model/tag/TagStore.kt +++ b/server/src/main/kotlin/com/pixelized/server/lwa/model/tag/TagStore.kt @@ -20,7 +20,8 @@ private const val ITEM = "item" class TagStore( private val pathProvider: PathProvider, - private val json: Json, + private val jsonSerializer: Json, + scope: CoroutineScope, ) { private val alterationTagsFlow = MutableStateFlow>(emptyMap()) private val characterTagsFlow = MutableStateFlow>(emptyMap()) @@ -29,8 +30,6 @@ class TagStore( init { // make the file path. File(pathProvider.tagsPath()).mkdirs() - // build a coroutine scope for async calls - val scope = CoroutineScope(Dispatchers.IO + Job()) // load the initial data scope.launch { updateTagFlow() @@ -41,7 +40,7 @@ class TagStore( fun characterTags(): StateFlow> = characterTagsFlow fun itemTags(): StateFlow> = itemTagsFlow - fun updateTagFlow() { + suspend fun updateTagFlow() { update( flow = alterationTagsFlow, file = alterationFile(), @@ -56,7 +55,7 @@ class TagStore( ) } - private fun update( + private suspend fun update( flow: MutableStateFlow>, file: File, ) { @@ -69,7 +68,7 @@ class TagStore( } @Throws(FileReadException::class, JsonConversionException::class) - private fun File.readTags(): List { + private suspend fun File.readTags(): List { // read the file (force the UTF8 format) val data = try { readText(charset = Charsets.UTF_8) @@ -81,7 +80,7 @@ class TagStore( return emptyList() } return try { - json.decodeFromString>(data) + jsonSerializer.decodeFromString>(data) } catch (exception: Exception) { throw JsonConversionException( root = exception @@ -90,10 +89,10 @@ class TagStore( } @Throws(JsonConversionException::class, FileWriteException::class) - private fun saveAlterationTags(tags: List) { + private suspend fun saveAlterationTags(tags: List) { // convert the data to json format val json = try { - this.json.encodeToString(tags) + this.jsonSerializer.encodeToString(tags) } catch (exception: Exception) { throw JsonConversionException(root = exception) }