Projection was now built on listener events

Create ProjectionBus and use it with listeners
add EventStream::getByVersion
This commit is contained in:
2025-04-14 23:39:56 +02:00
parent 908cc888ad
commit 8c1eabb9f5
22 changed files with 126 additions and 89 deletions
@@ -0,0 +1,7 @@
package eventDemo.business.event.projection
import eventDemo.business.entity.GameId
import eventDemo.libs.bus.Bus
import eventDemo.libs.event.projection.Projection
interface GameProjectionBus : Bus<Projection<GameId>>
@@ -5,6 +5,9 @@ import eventDemo.business.entity.Player
import eventDemo.libs.event.projection.Projection
import kotlinx.serialization.Serializable
/**
* This [projection][Projection] is used to list all current games
*/
@Serializable
data class GameList(
override val aggregateId: GameId,
@@ -4,9 +4,13 @@ import eventDemo.business.entity.Card
import eventDemo.business.entity.Deck
import eventDemo.business.entity.GameId
import eventDemo.business.entity.Player
import eventDemo.business.event.event.GameEvent
import eventDemo.libs.event.projection.Projection
import kotlinx.serialization.Serializable
/**
* This [projection][Projection] is used for manage a game and theirs [card][Card]
*/
@Serializable
data class GameState(
override val aggregateId: GameId,
@@ -20,6 +24,7 @@ data class GameState(
val deck: Deck = Deck(players),
val isStarted: Boolean = false,
val playerWins: Set<Player> = emptySet(),
val lastEvent: GameEvent? = null,
) : Projection<GameId> {
enum class Direction {
CLOCKWISE,
@@ -111,5 +111,6 @@ fun GameState.apply(event: GameEvent): GameState =
}
}.copy(
lastEventVersion = event.version,
lastEvent = event,
)
}
@@ -0,0 +1,146 @@
package eventDemo.business.event.projection.projectionListener
import eventDemo.business.entity.Card
import eventDemo.business.entity.GameId
import eventDemo.business.entity.Player
import eventDemo.business.event.event.CardIsPlayedEvent
import eventDemo.business.event.event.GameStartedEvent
import eventDemo.business.event.event.NewPlayerEvent
import eventDemo.business.event.event.PlayerChoseColorEvent
import eventDemo.business.event.event.PlayerHavePassEvent
import eventDemo.business.event.event.PlayerReadyEvent
import eventDemo.business.event.event.PlayerWinEvent
import eventDemo.business.event.projection.GameProjectionBus
import eventDemo.business.event.projection.gameState.GameState
import eventDemo.business.notification.ItsTheTurnOfNotification
import eventDemo.business.notification.Notification
import eventDemo.business.notification.PlayerAsJoinTheGameNotification
import eventDemo.business.notification.PlayerAsPlayACardNotification
import eventDemo.business.notification.PlayerHavePassNotification
import eventDemo.business.notification.PlayerWasChoseTheCardColorNotification
import eventDemo.business.notification.PlayerWasReadyNotification
import eventDemo.business.notification.PlayerWinNotification
import eventDemo.business.notification.TheGameWasStartedNotification
import eventDemo.business.notification.WelcomeToTheGameNotification
import eventDemo.business.notification.YourNewCardNotification
import io.github.oshai.kotlinlogging.KotlinLogging
import io.github.oshai.kotlinlogging.withLoggingContext
class PlayerNotificationListener(
private val projectionBus: GameProjectionBus,
) {
private val logger = KotlinLogging.logger {}
fun startListening(
outgoingNotification: (Notification) -> Unit,
currentPlayer: Player,
gameId: GameId,
) {
projectionBus.subscribe { currentState ->
if (currentState !is GameState) return@subscribe
if (currentState.aggregateId != gameId) return@subscribe
withLoggingContext("projection" to currentState.toString()) {
fun Notification.send() {
withLoggingContext("notification" to this.toString()) {
if (currentState.players.contains(currentPlayer)) {
// Only notify players who have already joined the game.
outgoingNotification(this)
logger.info { "Notification was SEND" }
} else {
// Rare use case, when a connexion is created with the channel,
// but the player was not already join in the game
logger.warn { "Notification was SKIP, no player on the game" }
}
}
}
fun sendNextTurnNotif() =
ItsTheTurnOfNotification(
player = currentState.currentPlayerTurn ?: error("No player turn defined"),
).send()
val event =
currentState.lastEvent
?: error("No last event in the GameState projection")
when (event) {
is NewPlayerEvent -> {
if (currentPlayer != event.player) {
PlayerAsJoinTheGameNotification(
player = event.player,
).send()
} else {
WelcomeToTheGameNotification(
players = currentState.players,
).send()
}
}
is CardIsPlayedEvent -> {
if (currentPlayer != event.player) {
PlayerAsPlayACardNotification(
player = event.player,
card = event.card,
).send()
}
if (event.card !is Card.AllColorCard) {
ItsTheTurnOfNotification(
player = currentState.currentPlayerTurn ?: error("No player turn defined"),
).send()
}
}
is GameStartedEvent -> {
TheGameWasStartedNotification(
hand =
event.deck.playersHands.getHand(currentPlayer)
?: error("You are not in the game"),
).send()
sendNextTurnNotif()
}
is PlayerChoseColorEvent -> {
if (currentPlayer != event.player) {
PlayerWasChoseTheCardColorNotification(
player = event.player,
color = event.color,
).send()
}
sendNextTurnNotif()
}
is PlayerHavePassEvent -> {
if (currentPlayer == event.player) {
YourNewCardNotification(
card = event.takenCard,
).send()
} else {
PlayerHavePassNotification(
player = event.player,
).send()
}
sendNextTurnNotif()
}
is PlayerReadyEvent -> {
if (currentPlayer != event.player) {
PlayerWasReadyNotification(
player = event.player,
).send()
}
}
is PlayerWinEvent -> {
PlayerWinNotification(
player = event.player,
).send()
}
}
}
}
}
}
@@ -0,0 +1,75 @@
package eventDemo.business.event.projection.projectionListener
import eventDemo.business.entity.GameId
import eventDemo.business.event.GameEventHandler
import eventDemo.business.event.event.GameStartedEvent
import eventDemo.business.event.event.PlayerWinEvent
import eventDemo.business.event.projection.GameProjectionBus
import eventDemo.business.event.projection.gameState.GameState
import eventDemo.libs.event.projection.Projection
import io.github.oshai.kotlinlogging.KotlinLogging
import io.github.oshai.kotlinlogging.withLoggingContext
import java.util.concurrent.ConcurrentSkipListSet
class ReactionListener(
private val projectionBus: GameProjectionBus,
private val eventHandler: GameEventHandler,
private val priority: Int = DEFAULT_PRIORITY,
) {
companion object Config {
const val DEFAULT_PRIORITY = -1000
val registeredListeners = ConcurrentSkipListSet<GameProjectionBus>()
}
private val logger = KotlinLogging.logger { }
fun init() {
if (registeredListeners.add(projectionBus)) {
projectionBus.subscribe(priority) { projection: Projection<GameId> ->
if (projection !is GameState) return@subscribe
withLoggingContext("projection" to projection.toString()) {
sendStartGameEvent(projection)
sendWinnerEvent(projection)
}
}
} else {
logger.error { "${this::class.java.simpleName} is already init for this bus" }
}
}
private fun sendStartGameEvent(state: GameState) {
if (state.isReady && !state.isStarted) {
val reactionEvent =
eventHandler.handle(state.aggregateId) {
GameStartedEvent.new(
id = state.aggregateId,
players = state.players,
version = it,
)
}
logger.atInfo {
message = "Reaction event was Send"
payload = mapOf("reactionEvent" to reactionEvent)
}
}
}
private fun sendWinnerEvent(state: GameState) {
val winner = state.playerHasNoCardLeft().firstOrNull()
if (winner != null) {
val reactionEvent =
eventHandler.handle(state.aggregateId) {
PlayerWinEvent(
aggregateId = state.aggregateId,
player = winner,
version = it,
)
}
logger.atInfo {
message = "Reaction event was Send"
payload = mapOf("reactionEvent" to reactionEvent)
}
}
}
}