Optimize server dispatcher

This commit is contained in:
Andres Gomez, Thomas (ITDV RL) 2025-05-13 09:48:51 +02:00
parent 5632ec7c85
commit a84c170396
12 changed files with 483 additions and 459 deletions

View file

@ -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> {
CoroutineScope(Job())
}
}
val engineDependencies

View file

@ -32,7 +32,7 @@ class AlterationService(
}
@Throws
fun save(
suspend fun save(
json: AlterationJson,
create: Boolean,
) {

View file

@ -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<List<Alteration>>(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<List<Alteration>> = 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<Alteration> {
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<AlterationJson>(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<AlterationJson>(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
})
}
}
}

View file

@ -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(

View file

@ -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<Campaign> = 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<CampaignJson>(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<CampaignJson>(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 {

View file

@ -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,

View file

@ -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<List<CharacterSheet>>(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<List<CharacterSheet>> = 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<CharacterSheet> {
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<CharacterSheetJson>(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<CharacterSheetJson>(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 })
}
}
}

View file

@ -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,
) {

View file

@ -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<Map<String, Inventory>>(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<Map<String, Inventory>> = 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<String, Inventory> {
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<InventoryJson>(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<InventoryJson>(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) }
}
}
}

View file

@ -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)
}
}

View file

@ -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<List<Item>>(emptyList())
init {
val scope = CoroutineScope(Dispatchers.IO + Job())
scope.launch {
updateItemsFlow()
}
@ -43,7 +42,7 @@ class ItemStore(
fun itemsFlow(): StateFlow<List<Item>> = 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<Item> {
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<ItemJson>(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<ItemJson>(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
})
}
}
}

View file

@ -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<Map<String, TagJson>>(emptyMap())
private val characterTagsFlow = MutableStateFlow<Map<String, TagJson>>(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<Map<String, TagJson>> = characterTagsFlow
fun itemTags(): StateFlow<Map<String, TagJson>> = 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<Map<String, TagJson>>,
file: File,
) {
@ -69,7 +68,7 @@ class TagStore(
}
@Throws(FileReadException::class, JsonConversionException::class)
private fun File.readTags(): List<TagJson> {
private suspend fun File.readTags(): List<TagJson> {
// 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<List<TagJson>>(data)
jsonSerializer.decodeFromString<List<TagJson>>(data)
} catch (exception: Exception) {
throw JsonConversionException(
root = exception
@ -90,10 +89,10 @@ class TagStore(
}
@Throws(JsonConversionException::class, FileWriteException::class)
private fun saveAlterationTags(tags: List<TagJson>) {
private suspend fun saveAlterationTags(tags: List<TagJson>) {
// convert the data to json format
val json = try {
this.json.encodeToString(tags)
this.jsonSerializer.encodeToString(tags)
} catch (exception: Exception) {
throw JsonConversionException(root = exception)
}