refactoring: Masive refactor to build the V2
This commit is contained in:
+37
@@ -0,0 +1,37 @@
|
||||
package eventDemo.contexts.game.application.channels
|
||||
|
||||
import eventDemo.contexts.game.application.command.models.GameCommand
|
||||
import eventDemo.contexts.game.application.notification.CommandSubscriber
|
||||
import eventDemo.contexts.game.application.notification.EventToNotificationSubscriber
|
||||
import eventDemo.contexts.game.application.notification.models.Notification
|
||||
import eventDemo.contexts.game.domain.game.GameId
|
||||
import eventDemo.sharedKernel.UserId
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.channels.ReceiveChannel
|
||||
import kotlinx.coroutines.channels.SendChannel
|
||||
|
||||
class GameChannelsSubscriber(
|
||||
private val eventToNotificationSubscriber: EventToNotificationSubscriber,
|
||||
private val commandSubscriber: CommandSubscriber,
|
||||
) {
|
||||
@DelicateCoroutinesApi
|
||||
fun subscribePlayerToGameChannels(
|
||||
gameId: GameId,
|
||||
userId: UserId,
|
||||
incomingCommandChannel: ReceiveChannel<GameCommand>,
|
||||
sendNotificationChannel: SendChannel<Notification>,
|
||||
) {
|
||||
val sub =
|
||||
eventToNotificationSubscriber.subscribeToEventsAndSendNotification(
|
||||
gameId = gameId,
|
||||
currentUserId = userId,
|
||||
outgoingFrameChannel = sendNotificationChannel,
|
||||
)
|
||||
|
||||
commandSubscriber
|
||||
.subscribe(
|
||||
currentUserId = userId,
|
||||
incomingFrameChannel = incomingCommandChannel,
|
||||
).invokeOnCompletion { sub.close() }
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package eventDemo.contexts.game.application.command.handlers
|
||||
|
||||
class CommandException(
|
||||
override val message: String,
|
||||
) : Exception(message)
|
||||
+31
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+66
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -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()
|
||||
}
|
||||
}
|
||||
+24
@@ -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()
|
||||
}
|
||||
}
|
||||
+26
@@ -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
|
||||
}
|
||||
}
|
||||
+24
@@ -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
|
||||
}
|
||||
+28
@@ -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
|
||||
}
|
||||
+28
@@ -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
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package eventDemo.contexts.game.application.eventStores
|
||||
|
||||
import eventDemo.contexts.game.application.ports.GameEventStore
|
||||
import eventDemo.contexts.game.domain.game.GameId
|
||||
import eventDemo.contexts.game.domain.game.gameState.Game
|
||||
import eventDemo.libs.eventSource.eventStore.VersionConflictException
|
||||
|
||||
class GameEventStoreRepository(
|
||||
val eventStore: GameEventStore,
|
||||
) : GameRepository {
|
||||
override fun get(id: GameId): Game? {
|
||||
val events =
|
||||
eventStore
|
||||
.getStream(id)
|
||||
.readAll()
|
||||
if (events.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
return events.let { Game.loadFromHistory(it) }
|
||||
}
|
||||
|
||||
@Throws(VersionConflictException::class)
|
||||
override fun save(game: Game) {
|
||||
eventStore.append(game.recordedEvents)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package eventDemo.contexts.game.application.eventStores
|
||||
|
||||
import eventDemo.contexts.game.domain.game.GameId
|
||||
import eventDemo.contexts.game.domain.game.gameState.Game
|
||||
import eventDemo.contexts.game.domain.game.gameState.GameCreated
|
||||
import eventDemo.contexts.game.domain.game.gameState.GameInit
|
||||
import eventDemo.libs.eventSource.eventStore.VersionConflictException
|
||||
|
||||
interface GameRepository {
|
||||
fun get(id: GameId): Game?
|
||||
|
||||
@Throws(VersionConflictException::class)
|
||||
fun save(game: Game)
|
||||
|
||||
fun getOrCreate(gameId: GameId): Game =
|
||||
get(gameId) ?: create(gameId)
|
||||
|
||||
fun create(gameId: GameId = GameId()): GameCreated =
|
||||
GameInit.createNewGame(gameId).also { save(it) }
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package eventDemo.contexts.game.application.logging
|
||||
|
||||
import io.github.oshai.kotlinlogging.withLoggingContext
|
||||
|
||||
inline fun <T> withLoggingContext(
|
||||
vararg pair: Pair<LoggingContextKeys, *>,
|
||||
body: () -> T,
|
||||
): T =
|
||||
withLoggingContext(
|
||||
*pair
|
||||
.map {
|
||||
it.first.name to it.second.toString()
|
||||
}.toTypedArray(),
|
||||
restorePrevious = true,
|
||||
body = body,
|
||||
)
|
||||
|
||||
// inline fun withLoggingContext(
|
||||
// vararg pair: Pair<LoggingContextKeys, *>,
|
||||
// body: () -> Unit,
|
||||
// ) =
|
||||
// withLoggingContext(
|
||||
// *pair
|
||||
// .map {
|
||||
// it.first.name to it.second.toString()
|
||||
// }.toTypedArray(),
|
||||
// restorePrevious = true,
|
||||
// body = body,
|
||||
// )
|
||||
@@ -0,0 +1,9 @@
|
||||
package eventDemo.contexts.game.application.logging
|
||||
|
||||
enum class LoggingContextKeys {
|
||||
CurrentUserId,
|
||||
Notification,
|
||||
Game,
|
||||
Event,
|
||||
Command,
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
package eventDemo.contexts.game.application.notification
|
||||
|
||||
import eventDemo.contexts.game.application.notification.models.ItsTheTurnOfNotification
|
||||
import eventDemo.contexts.game.application.notification.models.Notification
|
||||
import eventDemo.contexts.game.application.notification.models.PilesShuffledNotification
|
||||
import eventDemo.contexts.game.application.notification.models.PlayerAsJoinTheGameNotification
|
||||
import eventDemo.contexts.game.application.notification.models.PlayerAsPlayACardNotification
|
||||
import eventDemo.contexts.game.application.notification.models.PlayerHavePassNotification
|
||||
import eventDemo.contexts.game.application.notification.models.PlayerWasReadyNotification
|
||||
import eventDemo.contexts.game.application.notification.models.PlayerWinNotification
|
||||
import eventDemo.contexts.game.application.notification.models.TheGameWasStartedNotification
|
||||
import eventDemo.contexts.game.application.notification.models.WelcomeToTheGameNotification
|
||||
import eventDemo.contexts.game.application.notification.models.YourNewCardNotification
|
||||
import eventDemo.contexts.game.domain.events.CardIsPlayedEvent
|
||||
import eventDemo.contexts.game.domain.events.DrawFilledWithDiscardEvent
|
||||
import eventDemo.contexts.game.domain.events.GameCreatedEvent
|
||||
import eventDemo.contexts.game.domain.events.GameEvent
|
||||
import eventDemo.contexts.game.domain.events.GameStartedEvent
|
||||
import eventDemo.contexts.game.domain.events.NewPlayerEvent
|
||||
import eventDemo.contexts.game.domain.events.PlayerActionEvent
|
||||
import eventDemo.contexts.game.domain.events.PlayerHaveDrawCardEvent
|
||||
import eventDemo.contexts.game.domain.events.PlayerReadyEvent
|
||||
import eventDemo.contexts.game.domain.events.PlayerWinEvent
|
||||
import eventDemo.contexts.game.domain.game.gameState.Game
|
||||
import eventDemo.contexts.game.domain.game.gameState.GameStarted
|
||||
import eventDemo.sharedKernel.UserId
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.github.oshai.kotlinlogging.withLoggingContext
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
fun GameEvent.toNotification(
|
||||
game: Game,
|
||||
currentUserId: UserId,
|
||||
): Iterable<Notification> =
|
||||
Iterable {
|
||||
iterator {
|
||||
context(iterator: SequenceScope<Notification>)
|
||||
suspend fun Notification.send() {
|
||||
withLoggingContext("notification" to (this).toString()) {
|
||||
logger.info { "Notification sent" }
|
||||
iterator.yield(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun PlayerActionEvent.isFromCurrentUser(): Boolean =
|
||||
game.players.get(currentUserId).id == playerId
|
||||
|
||||
when (this@toNotification) {
|
||||
is GameCreatedEvent -> {
|
||||
// Nothing to send
|
||||
}
|
||||
|
||||
is DrawFilledWithDiscardEvent -> {
|
||||
PilesShuffledNotification().send()
|
||||
}
|
||||
|
||||
is NewPlayerEvent -> {
|
||||
if (this@toNotification.player.userId != currentUserId) {
|
||||
PlayerAsJoinTheGameNotification(
|
||||
player = this@toNotification.player,
|
||||
).send()
|
||||
} else {
|
||||
WelcomeToTheGameNotification(
|
||||
players = game.players,
|
||||
).send()
|
||||
}
|
||||
}
|
||||
|
||||
is CardIsPlayedEvent -> {
|
||||
PlayerAsPlayACardNotification(
|
||||
playerId = this@toNotification.playerId,
|
||||
card = this@toNotification.card,
|
||||
).send()
|
||||
|
||||
if (game is GameStarted) {
|
||||
ItsTheTurnOfNotification(
|
||||
player = game.nextPlayer,
|
||||
).send()
|
||||
}
|
||||
}
|
||||
|
||||
is GameStartedEvent -> {
|
||||
TheGameWasStartedNotification(
|
||||
hand =
|
||||
game.players
|
||||
.get(currentUserId)
|
||||
.hand.cards,
|
||||
).send()
|
||||
|
||||
if (game is GameStarted) {
|
||||
ItsTheTurnOfNotification(player = game.nextPlayer)
|
||||
.send()
|
||||
}
|
||||
}
|
||||
|
||||
is PlayerHaveDrawCardEvent -> {
|
||||
if (this@toNotification.isFromCurrentUser()) {
|
||||
YourNewCardNotification(
|
||||
cards = this@toNotification.takenCards,
|
||||
).send()
|
||||
} else {
|
||||
PlayerHavePassNotification(
|
||||
playerId = this@toNotification.playerId,
|
||||
).send()
|
||||
}
|
||||
|
||||
if (game is GameStarted) {
|
||||
ItsTheTurnOfNotification(player = game.nextPlayer)
|
||||
.send()
|
||||
}
|
||||
}
|
||||
|
||||
is PlayerReadyEvent -> {
|
||||
PlayerWasReadyNotification(
|
||||
playerId = this@toNotification.playerId,
|
||||
).send()
|
||||
}
|
||||
|
||||
is PlayerWinEvent -> {
|
||||
PlayerWinNotification(
|
||||
playerId = this@toNotification.playerId,
|
||||
).send()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package eventDemo.contexts.game.application.notification
|
||||
|
||||
import eventDemo.contexts.game.application.command.handlers.GameCommandHandlerDispatcher
|
||||
import eventDemo.contexts.game.application.command.models.GameCommand
|
||||
import eventDemo.contexts.game.application.eventStores.GameRepository
|
||||
import eventDemo.contexts.game.application.logging.LoggingContextKeys.Command
|
||||
import eventDemo.contexts.game.application.logging.LoggingContextKeys.CurrentUserId
|
||||
import eventDemo.contexts.game.application.logging.LoggingContextKeys.Event
|
||||
import eventDemo.contexts.game.application.logging.LoggingContextKeys.Game
|
||||
import eventDemo.contexts.game.application.logging.LoggingContextKeys.Notification
|
||||
import eventDemo.contexts.game.application.logging.withLoggingContext
|
||||
import eventDemo.contexts.game.application.notification.models.Notification
|
||||
import eventDemo.contexts.game.application.ports.GameEventBus
|
||||
import eventDemo.contexts.game.domain.game.GameId
|
||||
import eventDemo.libs.bus.Bus
|
||||
import eventDemo.libs.command.CommandUnicityChecker
|
||||
import eventDemo.sharedKernel.UserId
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.ReceiveChannel
|
||||
import kotlinx.coroutines.channels.SendChannel
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class EventToNotificationSubscriber(
|
||||
private val gameEventBus: GameEventBus,
|
||||
private val gameRepository: GameRepository,
|
||||
) {
|
||||
fun subscribeToEventsAndSendNotification(
|
||||
gameId: GameId,
|
||||
currentUserId: UserId,
|
||||
outgoingFrameChannel: SendChannel<Notification>,
|
||||
): Bus.Subscription =
|
||||
withLoggingContext(CurrentUserId to currentUserId) {
|
||||
gameEventBus.subscribe { event ->
|
||||
val game = gameRepository.get(gameId) ?: error("Game not found")
|
||||
withLoggingContext(Event to event, Game to game) {
|
||||
event
|
||||
.toNotification(
|
||||
game = game,
|
||||
currentUserId = currentUserId,
|
||||
).forEach { notification ->
|
||||
withLoggingContext(Notification to notification) {
|
||||
outgoingFrameChannel.trySendBlocking(notification)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CommandSubscriber(
|
||||
private val gameCommandHandlerDispatcher: GameCommandHandlerDispatcher,
|
||||
) {
|
||||
private val controller = CommandUnicityChecker<GameCommand>()
|
||||
|
||||
@DelicateCoroutinesApi
|
||||
fun subscribe(
|
||||
currentUserId: UserId,
|
||||
incomingFrameChannel: ReceiveChannel<GameCommand>,
|
||||
): Job =
|
||||
GlobalScope.launch {
|
||||
for (command in incomingFrameChannel) {
|
||||
withLoggingContext(CurrentUserId to currentUserId, Command to command) {
|
||||
controller.runOnlyOnce(command) {
|
||||
gameCommandHandlerDispatcher.dispatch(command)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package eventDemo.contexts.game.application.notification.models
|
||||
|
||||
import eventDemo.contexts.game.domain.game.Player
|
||||
import eventDemo.libs.serializer.UUIDSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.UUID
|
||||
|
||||
@Serializable
|
||||
data class ItsTheTurnOfNotification(
|
||||
@Serializable(with = UUIDSerializer::class)
|
||||
override val id: UUID = UUID.randomUUID(),
|
||||
val player: Player,
|
||||
) : Notification
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package eventDemo.contexts.game.application.notification.models
|
||||
|
||||
import eventDemo.libs.serializer.UUIDSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.UUID
|
||||
|
||||
@Serializable
|
||||
sealed interface Notification {
|
||||
@Serializable(with = UUIDSerializer::class)
|
||||
val id: UUID
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package eventDemo.contexts.game.application.notification.models
|
||||
|
||||
import eventDemo.libs.serializer.UUIDSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.UUID
|
||||
|
||||
@Serializable
|
||||
data class PilesShuffledNotification(
|
||||
@Serializable(with = UUIDSerializer::class)
|
||||
override val id: UUID = UUID.randomUUID(),
|
||||
) : Notification
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package eventDemo.contexts.game.application.notification.models
|
||||
|
||||
import eventDemo.contexts.game.domain.game.Player
|
||||
import eventDemo.libs.serializer.UUIDSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.UUID
|
||||
|
||||
@Serializable
|
||||
data class PlayerAsJoinTheGameNotification(
|
||||
@Serializable(with = UUIDSerializer::class)
|
||||
override val id: UUID = UUID.randomUUID(),
|
||||
val player: Player,
|
||||
) : Notification
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package eventDemo.contexts.game.application.notification.models
|
||||
|
||||
import eventDemo.contexts.game.domain.game.Card
|
||||
import eventDemo.contexts.game.domain.game.Player
|
||||
import eventDemo.libs.serializer.UUIDSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.UUID
|
||||
|
||||
@Serializable
|
||||
data class PlayerAsPlayACardNotification(
|
||||
@Serializable(with = UUIDSerializer::class)
|
||||
override val id: UUID = UUID.randomUUID(),
|
||||
val playerId: Player.PlayerId,
|
||||
val card: Card,
|
||||
) : Notification
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package eventDemo.contexts.game.application.notification.models
|
||||
|
||||
import eventDemo.contexts.game.domain.game.Player
|
||||
import eventDemo.libs.serializer.UUIDSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.UUID
|
||||
|
||||
@Serializable
|
||||
data class PlayerHavePassNotification(
|
||||
@Serializable(with = UUIDSerializer::class)
|
||||
override val id: UUID = UUID.randomUUID(),
|
||||
val playerId: Player.PlayerId,
|
||||
) : Notification
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package eventDemo.contexts.game.application.notification.models
|
||||
|
||||
import eventDemo.contexts.game.domain.game.Player
|
||||
import eventDemo.libs.serializer.UUIDSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.UUID
|
||||
|
||||
@Serializable
|
||||
data class PlayerWasReadyNotification(
|
||||
@Serializable(with = UUIDSerializer::class)
|
||||
override val id: UUID = UUID.randomUUID(),
|
||||
val playerId: Player.PlayerId,
|
||||
) : Notification
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package eventDemo.contexts.game.application.notification.models
|
||||
|
||||
import eventDemo.contexts.game.domain.game.Player
|
||||
import eventDemo.libs.serializer.UUIDSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.UUID
|
||||
|
||||
@Serializable
|
||||
data class PlayerWinNotification(
|
||||
@Serializable(with = UUIDSerializer::class)
|
||||
override val id: UUID = UUID.randomUUID(),
|
||||
val playerId: Player.PlayerId,
|
||||
) : Notification
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package eventDemo.contexts.game.application.notification.models
|
||||
|
||||
import eventDemo.contexts.game.domain.game.Card
|
||||
import eventDemo.libs.serializer.UUIDSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.UUID
|
||||
|
||||
@Serializable
|
||||
data class TheGameWasStartedNotification(
|
||||
@Serializable(with = UUIDSerializer::class)
|
||||
override val id: UUID = UUID.randomUUID(),
|
||||
val hand: Set<Card>,
|
||||
) : Notification
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package eventDemo.contexts.game.application.notification.models
|
||||
|
||||
import eventDemo.contexts.game.domain.game.Player
|
||||
import eventDemo.libs.serializer.UUIDSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.UUID
|
||||
|
||||
@Serializable
|
||||
data class WelcomeToTheGameNotification(
|
||||
@Serializable(with = UUIDSerializer::class)
|
||||
override val id: UUID = UUID.randomUUID(),
|
||||
val players: Set<Player>,
|
||||
) : Notification
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package eventDemo.contexts.game.application.notification.models
|
||||
|
||||
import eventDemo.contexts.game.domain.game.Card
|
||||
import eventDemo.libs.serializer.UUIDSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.UUID
|
||||
|
||||
@Serializable
|
||||
data class YourNewCardNotification(
|
||||
@Serializable(with = UUIDSerializer::class)
|
||||
override val id: UUID = UUID.randomUUID(),
|
||||
val cards: Set<Card>,
|
||||
) : Notification
|
||||
@@ -0,0 +1,6 @@
|
||||
package eventDemo.contexts.game.application.ports
|
||||
|
||||
import eventDemo.contexts.game.domain.events.GameEvent
|
||||
import eventDemo.libs.bus.Bus
|
||||
|
||||
interface GameEventBus : Bus<GameEvent>
|
||||
@@ -0,0 +1,7 @@
|
||||
package eventDemo.contexts.game.application.ports
|
||||
|
||||
import eventDemo.contexts.game.domain.events.GameEvent
|
||||
import eventDemo.contexts.game.domain.game.GameId
|
||||
import eventDemo.libs.eventSource.eventStore.EventStore
|
||||
|
||||
interface GameEventStore : EventStore<GameEvent, GameId>
|
||||
@@ -0,0 +1,14 @@
|
||||
package eventDemo.domain.event.projection
|
||||
|
||||
import eventDemo.contexts.game.infrastructure.persistence.projections.models.GameList
|
||||
|
||||
interface GameListRepository {
|
||||
fun getList(
|
||||
limit: Int = 100,
|
||||
offset: Int = 0,
|
||||
): List<GameList>
|
||||
|
||||
fun save(gameList: GameList)
|
||||
|
||||
fun subscribeToBus()
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package eventDemo.contexts.game.application.ports
|
||||
|
||||
import eventDemo.contexts.game.infrastructure.persistence.projections.models.GameProjection
|
||||
import eventDemo.libs.bus.Bus
|
||||
|
||||
interface GameProjectionBus : Bus<GameProjection>
|
||||
@@ -0,0 +1,55 @@
|
||||
package eventDemo.contexts.game.application.projections
|
||||
|
||||
import eventDemo.contexts.game.domain.events.CardIsPlayedEvent
|
||||
import eventDemo.contexts.game.domain.events.DrawFilledWithDiscardEvent
|
||||
import eventDemo.contexts.game.domain.events.GameCreatedEvent
|
||||
import eventDemo.contexts.game.domain.events.GameEvent
|
||||
import eventDemo.contexts.game.domain.events.GameStartedEvent
|
||||
import eventDemo.contexts.game.domain.events.NewPlayerEvent
|
||||
import eventDemo.contexts.game.domain.events.PlayerHaveDrawCardEvent
|
||||
import eventDemo.contexts.game.domain.events.PlayerReadyEvent
|
||||
import eventDemo.contexts.game.domain.events.PlayerWinEvent
|
||||
import eventDemo.contexts.game.infrastructure.persistence.projections.models.GameList
|
||||
|
||||
fun GameList.applyEvent(event: GameEvent): GameList =
|
||||
when (event) {
|
||||
is GameCreatedEvent -> {
|
||||
this
|
||||
}
|
||||
|
||||
is NewPlayerEvent -> {
|
||||
copy(
|
||||
players = players + event.player,
|
||||
status = GameList.Status.OPENING,
|
||||
)
|
||||
}
|
||||
|
||||
is GameStartedEvent -> {
|
||||
copy(
|
||||
status = GameList.Status.IS_STARTED,
|
||||
)
|
||||
}
|
||||
|
||||
is PlayerWinEvent -> {
|
||||
copy(
|
||||
winners = winners,
|
||||
status = GameList.Status.FINISH,
|
||||
)
|
||||
}
|
||||
|
||||
is CardIsPlayedEvent -> {
|
||||
this
|
||||
}
|
||||
|
||||
is PlayerHaveDrawCardEvent -> {
|
||||
this
|
||||
}
|
||||
|
||||
is PlayerReadyEvent -> {
|
||||
this
|
||||
}
|
||||
|
||||
is DrawFilledWithDiscardEvent -> {
|
||||
this
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package eventDemo.contexts.game.application.reaction
|
||||
|
||||
import eventDemo.contexts.game.application.command.handlers.GameEventManager
|
||||
import eventDemo.contexts.game.application.eventStores.GameRepository
|
||||
import eventDemo.contexts.game.application.logging.LoggingContextKeys
|
||||
import eventDemo.contexts.game.application.logging.withLoggingContext
|
||||
import eventDemo.contexts.game.application.ports.GameEventBus
|
||||
import eventDemo.contexts.game.domain.game.gameState.Game
|
||||
import eventDemo.contexts.game.domain.game.gameState.GameCreated
|
||||
import eventDemo.contexts.game.domain.game.gameState.GameStarted
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import java.util.concurrent.ConcurrentSkipListSet
|
||||
|
||||
class ReactionListener(
|
||||
gameRepository: GameRepository,
|
||||
private val gameEventBus: GameEventBus,
|
||||
) : GameEventManager(gameRepository, gameEventBus) {
|
||||
private companion object Config {
|
||||
val registeredListeners = ConcurrentSkipListSet<GameEventBus>()
|
||||
}
|
||||
|
||||
private val logger = KotlinLogging.logger { }
|
||||
|
||||
fun subscribeToBus() {
|
||||
if (registeredListeners.add(gameEventBus)) {
|
||||
gameEventBus.subscribe { event ->
|
||||
val game = event.getGame()
|
||||
withLoggingContext(LoggingContextKeys.Game to game) {
|
||||
sendStartGameEvent(game)
|
||||
sendWinnerEvent(game)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
"${this::class.simpleName} is already init for this bus".let {
|
||||
logger.error { it }
|
||||
error(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendStartGameEvent(game: Game) {
|
||||
if (game is GameCreated && game.allPlayerIsReady) {
|
||||
game
|
||||
.startGame()
|
||||
.saveEvents()
|
||||
.publishEvents()
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendWinnerEvent(game: Game) {
|
||||
if (game is GameStarted && game.lastPlayerId != null) {
|
||||
val lastPlayerWin =
|
||||
game
|
||||
.players
|
||||
.get(game.lastPlayerId)
|
||||
.hand.size == 0
|
||||
if (lastPlayerWin) {
|
||||
game
|
||||
.playerWin(game.lastPlayerId)
|
||||
.saveEvents()
|
||||
.publishEvents()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user