package eventDemo.app.command import eventDemo.app.command.command.GameCommand import eventDemo.app.entity.Player import eventDemo.app.event.GameEventBus import eventDemo.app.event.GameEventHandler import eventDemo.app.event.event.GameEvent import eventDemo.app.notification.CommandErrorNotification import eventDemo.app.notification.CommandSuccessNotification import eventDemo.app.notification.Notification import eventDemo.libs.command.CommandId import eventDemo.libs.command.CommandStreamChannel import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.withLoggingContext import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.channels.SendChannel import java.util.UUID import java.util.concurrent.ConcurrentHashMap /** * Listen [GameCommand] on [CommandStreamChannel], check the validity and execute an action. * * This action can be executing an action and produce a new [GameEvent] after verification. */ class GameCommandHandler( private val commandStreamChannel: CommandStreamChannel, private val eventHandler: GameEventHandler, private val runner: GameCommandActionRunner, eventBus: GameEventBus, listenerPriority: Int = DEFAULT_PRIORITY, ) { private val logger = KotlinLogging.logger { } private val eventCommandMap = EventCommandMap() companion object Config { const val DEFAULT_PRIORITY = 1000 } // subscribe to the event bus to send success notification after save the event. init { eventBus.subscribe(listenerPriority) { event: GameEvent -> eventCommandMap[event.eventId]?.apply { channel.sendSuccess(commandId)() } ?: logger.warn { "No Notification for event: $event" } } } /** * Run a command and publish the event. * * It restricts to run only once a command. * * If the command fail, send an [error notification][CommandErrorNotification], * if success, send a [success notification][CommandSuccessNotification] */ suspend fun handle( player: Player, incomingCommandChannel: ReceiveChannel, channelNotification: SendChannel, ) { commandStreamChannel.process(incomingCommandChannel) { command -> withLoggingContext("command" to command.toString()) { if (command.payload.player.id != player.id) { logger.warn { "Handle command Refuse, the player of the command is not the same" } channelNotification.sendError(command)("You are not the author of this command\n") } else { logger.info { "Handle command" } try { val eventBuilder = runner.run(command) eventHandler.handle(command.payload.aggregateId) { version -> eventBuilder(version) .also { eventCommandMap.set(it.eventId, channelNotification, command.id) } } } catch (e: CommandException) { logger.warn(e) { e.message } channelNotification.sendError(command)(e.message) } } } } } } private fun SendChannel.sendSuccess(commandId: CommandId): suspend () -> Unit = { val logger = KotlinLogging.logger { } CommandSuccessNotification(commandId = commandId) .also { notification -> withLoggingContext("notification" to notification.toString(), "commandId" to commandId.toString()) { logger.debug { "Notification SUCCESS sent" } send(notification) } } } private fun SendChannel.sendError(command: GameCommand): suspend (String) -> Unit = { val logger = KotlinLogging.logger { } CommandErrorNotification(message = it, command = command) .also { notification -> withLoggingContext("notification" to notification.toString(), "command" to command.toString()) { logger.warn { "Notification ERROR sent: ${notification.message}" } send(notification) } } } /** * Map to record the command that triggered the event. */ private class EventCommandMap { val map = ConcurrentHashMap() fun set( eventId: UUID, channel: SendChannel, commandId: CommandId, ) { map[eventId] = Output(channel, commandId) } operator fun get(eventId: UUID): Output? = map[eventId] data class Output( val channel: SendChannel, val commandId: CommandId, ) }