refactoring: Masive refactor to build the V2
Tests / build (push) Successful in 7m14s
Tests / lint (push) Successful in 8m35s
Tests / test (push) Failing after 11m0s

This commit is contained in:
2026-07-30 23:01:50 +02:00
parent b313b39cf4
commit e2d7942c7e
260 changed files with 5049 additions and 4753 deletions
@@ -0,0 +1,5 @@
package eventDemo.contexts.game.application.command.handlers
class CommandException(
override val message: String,
) : Exception(message)
@@ -0,0 +1,31 @@
package eventDemo.contexts.game.application.command.handlers
import eventDemo.contexts.game.application.command.models.GameCommand
import eventDemo.contexts.game.application.command.models.JoinTheGameCommand
import eventDemo.contexts.game.application.command.models.PlayCardCommand
import eventDemo.contexts.game.application.command.models.ReadyToPlayCommand
import eventDemo.contexts.game.application.command.models.TakeCartFromDrawPileCommand
import eventDemo.contexts.game.domain.game.GameId
import java.util.Collections
class GameCommandHandlerDispatcher(
private val playCardHandler: PlayCardHandler,
private val readyToPlayHandler: ReadyToPlayHandler,
private val joinTheGameHandler: JoinTheGameHandler,
private val takeCartFromDrawPileHandler: TakeCartFromDrawPileHandler,
) {
companion object {
val lock: MutableMap<GameId, String> = Collections.synchronizedMap(mutableMapOf())
}
fun dispatch(command: GameCommand) {
synchronized(lock.getOrPut(command.payload.aggregateId) { command.payload.aggregateId.toString() }) {
when (command) {
is JoinTheGameCommand -> joinTheGameHandler.handle(command)
is ReadyToPlayCommand -> readyToPlayHandler.handle(command)
is PlayCardCommand -> playCardHandler.handle(command)
is TakeCartFromDrawPileCommand -> takeCartFromDrawPileHandler.handle(command)
}
}
}
}
@@ -0,0 +1,66 @@
package eventDemo.contexts.game.application.command.handlers
import eventDemo.contexts.game.application.command.models.GameCommand
import eventDemo.contexts.game.application.eventStores.GameRepository
import eventDemo.contexts.game.application.ports.GameEventBus
import eventDemo.contexts.game.domain.events.GameEvent
import eventDemo.contexts.game.domain.game.gameState.Game
import eventDemo.libs.command.Command
import eventDemo.libs.eventSource.eventStore.VersionConflictException
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlin.reflect.KClass
sealed interface CommandHandler<C : Command> {
fun handle(command: C)
}
abstract class GameEventManager(
private val gameRepository: GameRepository,
private val gameEventBus: GameEventBus,
) {
private val logger = KotlinLogging.logger {}
fun GameCommand.getGame(): Game =
gameRepository.get(payload.aggregateId) ?: error("Game not found")
fun GameEvent.getGame(): Game =
gameRepository.get(aggregateId) ?: error("Game not found")
@Throws(VersionConflictException::class)
protected fun Game.saveEvents(): Game {
gameRepository.save(this)
return this
}
protected fun Game.publishEvents(): Game {
gameEventBus.publish(recordedEvents)
return this
}
protected fun <G : Game> Game.isStatusOrFail(
kClass: KClass<G>,
message: String,
): G {
if (kClass.isInstance(this)) {
return this as G
} else {
throw CommandException(message)
}
}
protected fun <T> retry(
mapAttempts: Int = 5,
block: () -> T,
): T =
try {
block()
} catch (e: VersionConflictException) {
if (mapAttempts > 0) {
logger.warn { "retry after version conflict (attempts left: $mapAttempts)" }
retry(mapAttempts - 1, block)
} else {
logger.error { "Version conflict retry failed" }
throw e
}
}
}
@@ -0,0 +1,31 @@
package eventDemo.contexts.game.application.command.handlers
import eventDemo.contexts.auth.application.eventStores.UserRepository
import eventDemo.contexts.game.application.command.models.JoinTheGameCommand
import eventDemo.contexts.game.application.eventStores.GameRepository
import eventDemo.contexts.game.application.ports.GameEventBus
import eventDemo.contexts.game.domain.game.gameState.GameCreated
/**
* A command to perform an action to play a new card
*/
class JoinTheGameHandler(
gameRepository: GameRepository,
gameEventBus: GameEventBus,
private val userRepository: UserRepository,
) : GameEventManager(gameRepository, gameEventBus),
CommandHandler<JoinTheGameCommand> {
override fun handle(command: JoinTheGameCommand) {
val user = userRepository.get(command.userId) ?: error("User with id ${command.userId} doesn't exist")
retry {
command
.getGame()
.isStatusOrFail(GameCreated::class, "The game is started")
.userJoinTheGame(
userId = command.userId,
name = user.username,
).saveEvents()
.publishEvents()
}
}
}
@@ -0,0 +1,27 @@
package eventDemo.contexts.game.application.command.handlers
import eventDemo.contexts.game.application.command.models.PlayCardCommand
import eventDemo.contexts.game.application.eventStores.GameRepository
import eventDemo.contexts.game.application.ports.GameEventBus
import eventDemo.contexts.game.domain.game.gameState.GameStarted
/**
* A command to perform an action to play a new card
*/
class PlayCardHandler(
gameRepository: GameRepository,
gameEventBus: GameEventBus,
) : GameEventManager(gameRepository, gameEventBus),
CommandHandler<PlayCardCommand> {
override fun handle(command: PlayCardCommand) {
command
.getGame()
.isStatusOrFail(GameStarted::class, "The game is not started")
.playTheCard(
card = command.payload.card,
playerId = command.payload.playerId,
chosenColor = command.payload.chosenColor,
).saveEvents()
.publishEvents()
}
}
@@ -0,0 +1,24 @@
package eventDemo.contexts.game.application.command.handlers
import eventDemo.contexts.game.application.command.models.ReadyToPlayCommand
import eventDemo.contexts.game.application.eventStores.GameRepository
import eventDemo.contexts.game.application.ports.GameEventBus
import eventDemo.contexts.game.domain.game.gameState.GameCreated
/**
* A command to set as ready to play
*/
class ReadyToPlayHandler(
gameRepository: GameRepository,
gameEventBus: GameEventBus,
) : GameEventManager(gameRepository, gameEventBus),
CommandHandler<ReadyToPlayCommand> {
override fun handle(command: ReadyToPlayCommand) {
command
.getGame()
.isStatusOrFail(GameCreated::class, "The game is started")
.setReadyPlayer(command.payload.playerId)
.saveEvents()
.publishEvents()
}
}
@@ -0,0 +1,26 @@
package eventDemo.contexts.game.application.command.handlers
import eventDemo.contexts.game.application.command.models.TakeCartFromDrawPileCommand
import eventDemo.contexts.game.application.eventStores.GameRepository
import eventDemo.contexts.game.application.ports.GameEventBus
import eventDemo.contexts.game.domain.game.gameState.GameStarted
/**
* A command to draw card on draw pile.
*
* Is can be triggered when you cannot play any card in your hand.
*/
class TakeCartFromDrawPileHandler(
gameRepository: GameRepository,
gameEventBus: GameEventBus,
) : GameEventManager(gameRepository, gameEventBus),
CommandHandler<TakeCartFromDrawPileCommand> {
override fun handle(command: TakeCartFromDrawPileCommand) {
command
.getGame()
.isStatusOrFail(GameStarted::class, "The game is not started")
.playerTakeCartFromDrawPile(command.payload.playerId, 1)
.saveEvents()
.publishEvents()
}
}
@@ -0,0 +1,19 @@
package eventDemo.contexts.game.application.command.models
import eventDemo.contexts.game.domain.game.GameId
import eventDemo.contexts.game.infrastructure.persistence.serializers.GameIdSerializer
import eventDemo.libs.command.Command
import eventDemo.sharedKernel.UserId
import kotlinx.serialization.Serializable
@Serializable
sealed interface GameCommand : Command {
val userId: UserId
val payload: Payload
@Serializable
sealed interface Payload {
@Serializable(with = GameIdSerializer::class)
val aggregateId: GameId
}
}
@@ -0,0 +1,24 @@
package eventDemo.contexts.game.application.command.models
import eventDemo.contexts.game.domain.game.GameId
import eventDemo.contexts.game.infrastructure.persistence.serializers.GameIdSerializer
import eventDemo.libs.command.CommandId
import eventDemo.sharedKernel.UserId
import kotlinx.serialization.Serializable
/**
* A command to perform an action to play a new card
*/
@Serializable
data class JoinTheGameCommand(
override val userId: UserId,
override val payload: Payload,
) : GameCommand {
override val id: CommandId = CommandId()
@Serializable
data class Payload(
@Serializable(with = GameIdSerializer::class)
override val aggregateId: GameId,
) : GameCommand.Payload
}
@@ -0,0 +1,31 @@
package eventDemo.contexts.game.application.command.models
import eventDemo.contexts.game.domain.game.Card
import eventDemo.contexts.game.domain.game.GameId
import eventDemo.contexts.game.domain.game.Player
import eventDemo.contexts.game.infrastructure.persistence.serializers.GameIdSerializer
import eventDemo.contexts.game.infrastructure.persistence.serializers.PlayerIdSerializer
import eventDemo.libs.command.CommandId
import eventDemo.sharedKernel.UserId
import kotlinx.serialization.Serializable
/**
* A command to perform an action to play a new card
*/
@Serializable
data class PlayCardCommand(
override val userId: UserId,
override val payload: Payload,
) : GameCommand {
override val id: CommandId = CommandId()
@Serializable
data class Payload(
@Serializable(with = GameIdSerializer::class)
override val aggregateId: GameId,
@Serializable(with = PlayerIdSerializer::class)
val playerId: Player.PlayerId,
val card: Card,
val chosenColor: Card.Color?,
) : GameCommand.Payload
}
@@ -0,0 +1,28 @@
package eventDemo.contexts.game.application.command.models
import eventDemo.contexts.game.domain.game.GameId
import eventDemo.contexts.game.domain.game.Player
import eventDemo.contexts.game.infrastructure.persistence.serializers.GameIdSerializer
import eventDemo.contexts.game.infrastructure.persistence.serializers.PlayerIdSerializer
import eventDemo.libs.command.CommandId
import eventDemo.sharedKernel.UserId
import kotlinx.serialization.Serializable
/**
* A command to set as ready to play
*/
@Serializable
data class ReadyToPlayCommand(
override val userId: UserId,
override val payload: Payload,
) : GameCommand {
override val id: CommandId = CommandId()
@Serializable
data class Payload(
@Serializable(with = GameIdSerializer::class)
override val aggregateId: GameId,
@Serializable(with = PlayerIdSerializer::class)
val playerId: Player.PlayerId,
) : GameCommand.Payload
}
@@ -0,0 +1,28 @@
package eventDemo.contexts.game.application.command.models
import eventDemo.contexts.game.domain.game.GameId
import eventDemo.contexts.game.domain.game.Player
import eventDemo.contexts.game.infrastructure.persistence.serializers.GameIdSerializer
import eventDemo.contexts.game.infrastructure.persistence.serializers.PlayerIdSerializer
import eventDemo.libs.command.CommandId
import eventDemo.sharedKernel.UserId
import kotlinx.serialization.Serializable
/**
* A command to perform an action to play a new card
*/
@Serializable
data class TakeCartFromDrawPileCommand(
override val userId: UserId,
override val payload: Payload,
) : GameCommand {
override val id: CommandId = CommandId()
@Serializable
data class Payload(
@Serializable(with = GameIdSerializer::class)
override val aggregateId: GameId,
@Serializable(with = PlayerIdSerializer::class)
val playerId: Player.PlayerId,
) : GameCommand.Payload
}