diff --git a/src/main/kotlin/eventDemo/app/event/event/CardIsPlayedEvent.kt b/src/main/kotlin/eventDemo/app/event/event/CardIsPlayedEvent.kt index 9375846..dc79777 100644 --- a/src/main/kotlin/eventDemo/app/event/event/CardIsPlayedEvent.kt +++ b/src/main/kotlin/eventDemo/app/event/event/CardIsPlayedEvent.kt @@ -3,6 +3,7 @@ package eventDemo.app.event.event import eventDemo.app.entity.Card import eventDemo.app.entity.GameId import eventDemo.app.entity.Player +import java.util.UUID /** * An [GameEvent] to represent a played card. @@ -11,4 +12,5 @@ data class CardIsPlayedEvent( override val gameId: GameId, val card: Card, val player: Player, + override val eventId: UUID = UUID.randomUUID(), ) : GameEvent diff --git a/src/main/kotlin/eventDemo/app/event/event/GameEvent.kt b/src/main/kotlin/eventDemo/app/event/event/GameEvent.kt index fb8c5c5..cacf37a 100644 --- a/src/main/kotlin/eventDemo/app/event/event/GameEvent.kt +++ b/src/main/kotlin/eventDemo/app/event/event/GameEvent.kt @@ -3,11 +3,13 @@ package eventDemo.app.event.event import eventDemo.app.entity.GameId import eventDemo.libs.event.Event import kotlinx.serialization.Serializable +import java.util.UUID /** * An [Event] of a Game. */ @Serializable sealed interface GameEvent : Event { + override val eventId: UUID override val gameId: GameId } diff --git a/src/main/kotlin/eventDemo/app/event/event/GameStartedEvent.kt b/src/main/kotlin/eventDemo/app/event/event/GameStartedEvent.kt index cdbcd1b..78f3c95 100644 --- a/src/main/kotlin/eventDemo/app/event/event/GameStartedEvent.kt +++ b/src/main/kotlin/eventDemo/app/event/event/GameStartedEvent.kt @@ -4,6 +4,7 @@ import eventDemo.app.entity.Deck import eventDemo.app.entity.GameId import eventDemo.app.entity.Player import eventDemo.app.entity.initHands +import java.util.UUID /** * This [GameEvent] is sent when all players are ready. @@ -13,6 +14,8 @@ data class GameStartedEvent( val firstPlayer: Player, val deck: Deck, ) : GameEvent { + override val eventId: UUID = UUID.randomUUID() + companion object { fun new( id: GameId, diff --git a/src/main/kotlin/eventDemo/app/event/event/NewPlayerEvent.kt b/src/main/kotlin/eventDemo/app/event/event/NewPlayerEvent.kt index 566898b..db51e3b 100644 --- a/src/main/kotlin/eventDemo/app/event/event/NewPlayerEvent.kt +++ b/src/main/kotlin/eventDemo/app/event/event/NewPlayerEvent.kt @@ -2,6 +2,7 @@ package eventDemo.app.event.event import eventDemo.app.entity.GameId import eventDemo.app.entity.Player +import java.util.UUID /** * An [GameEvent] to represent a new player joining the game. @@ -9,4 +10,6 @@ import eventDemo.app.entity.Player data class NewPlayerEvent( override val gameId: GameId, val player: Player, -) : GameEvent +) : GameEvent { + override val eventId: UUID = UUID.randomUUID() +} diff --git a/src/main/kotlin/eventDemo/app/event/event/PlayerChoseColorEvent.kt b/src/main/kotlin/eventDemo/app/event/event/PlayerChoseColorEvent.kt index 4218d24..5efd879 100644 --- a/src/main/kotlin/eventDemo/app/event/event/PlayerChoseColorEvent.kt +++ b/src/main/kotlin/eventDemo/app/event/event/PlayerChoseColorEvent.kt @@ -3,6 +3,7 @@ package eventDemo.app.event.event import eventDemo.app.entity.Card import eventDemo.app.entity.GameId import eventDemo.app.entity.Player +import java.util.UUID /** * This [GameEvent] is sent when a player chose a color. @@ -11,4 +12,6 @@ data class PlayerChoseColorEvent( override val gameId: GameId, val player: Player, val color: Card.Color, -) : GameEvent +) : GameEvent { + override val eventId: UUID = UUID.randomUUID() +} diff --git a/src/main/kotlin/eventDemo/app/event/event/PlayerHavePassEvent.kt b/src/main/kotlin/eventDemo/app/event/event/PlayerHavePassEvent.kt index f94747a..b854eb9 100644 --- a/src/main/kotlin/eventDemo/app/event/event/PlayerHavePassEvent.kt +++ b/src/main/kotlin/eventDemo/app/event/event/PlayerHavePassEvent.kt @@ -3,6 +3,7 @@ package eventDemo.app.event.event import eventDemo.app.entity.Card import eventDemo.app.entity.GameId import eventDemo.app.entity.Player +import java.util.UUID /** * This [GameEvent] is sent when a player can play. @@ -11,4 +12,6 @@ data class PlayerHavePassEvent( override val gameId: GameId, val player: Player, val takenCard: Card, -) : GameEvent +) : GameEvent { + override val eventId: UUID = UUID.randomUUID() +} diff --git a/src/main/kotlin/eventDemo/app/event/event/PlayerReadyEvent.kt b/src/main/kotlin/eventDemo/app/event/event/PlayerReadyEvent.kt index d3ac05b..ff5de53 100644 --- a/src/main/kotlin/eventDemo/app/event/event/PlayerReadyEvent.kt +++ b/src/main/kotlin/eventDemo/app/event/event/PlayerReadyEvent.kt @@ -2,6 +2,7 @@ package eventDemo.app.event.event import eventDemo.app.entity.GameId import eventDemo.app.entity.Player +import java.util.UUID /** * This [GameEvent] is sent when a player is ready. @@ -9,4 +10,6 @@ import eventDemo.app.entity.Player data class PlayerReadyEvent( override val gameId: GameId, val player: Player, -) : GameEvent +) : GameEvent { + override val eventId: UUID = UUID.randomUUID() +} diff --git a/src/main/kotlin/eventDemo/app/event/event/PlayerWinEvent.kt b/src/main/kotlin/eventDemo/app/event/event/PlayerWinEvent.kt index 35647ca..933f7b3 100644 --- a/src/main/kotlin/eventDemo/app/event/event/PlayerWinEvent.kt +++ b/src/main/kotlin/eventDemo/app/event/event/PlayerWinEvent.kt @@ -2,6 +2,7 @@ package eventDemo.app.event.event import eventDemo.app.entity.GameId import eventDemo.app.entity.Player +import java.util.UUID /** * This [GameEvent] is sent when a player is ready. @@ -9,4 +10,6 @@ import eventDemo.app.entity.Player data class PlayerWinEvent( override val gameId: GameId, val player: Player, -) : GameEvent +) : GameEvent { + override val eventId: UUID = UUID.randomUUID() +} diff --git a/src/main/kotlin/eventDemo/app/event/projection/GameStateBuilder.kt b/src/main/kotlin/eventDemo/app/event/projection/GameStateBuilder.kt index 11c30bf..2680761 100644 --- a/src/main/kotlin/eventDemo/app/event/projection/GameStateBuilder.kt +++ b/src/main/kotlin/eventDemo/app/event/projection/GameStateBuilder.kt @@ -12,28 +12,18 @@ import eventDemo.app.event.event.PlayerHavePassEvent import eventDemo.app.event.event.PlayerReadyEvent import eventDemo.app.event.event.PlayerWinEvent -fun GameId.buildStateFromEventStream(eventStream: GameEventStream): GameState = - buildStateFromEvents( - eventStream.readAll(this), - ) +fun GameId.buildStateFromEventStream(eventStream: GameEventStream): GameState { + val events = eventStream.readAll(this) + if (events.isEmpty()) return GameState(this) + return events.buildStateFromEvents() +} -/** - * Build the state to the specific event - */ -fun GameEvent.buildStateFromEventStreamTo(eventStream: GameEventStream): GameState = - gameId.buildStateFromEvents( - eventStream.readAll(gameId).takeWhile { it != this } + this, - ) - -private fun GameId.buildStateFromEvents(events: List): GameState = - events.fold(GameState(this)) { state, event -> - state.apply(event) - } - -fun List.buildStateFromEvents(): GameState = - fold(GameState(this.first().gameId)) { state, event -> +fun List.buildStateFromEvents(): GameState { + val gameId = this.firstOrNull()?.gameId ?: error("Cannot build GameState from an empty list") + return fold(GameState(gameId)) { state, event -> state.apply(event) } +} fun GameState.apply(event: GameEvent): GameState = let { state -> diff --git a/src/main/kotlin/eventDemo/app/event/projection/GameStateRepository.kt b/src/main/kotlin/eventDemo/app/event/projection/GameStateRepository.kt index a8daaaf..fc111b5 100644 --- a/src/main/kotlin/eventDemo/app/event/projection/GameStateRepository.kt +++ b/src/main/kotlin/eventDemo/app/event/projection/GameStateRepository.kt @@ -3,32 +3,92 @@ package eventDemo.app.event.projection import eventDemo.app.entity.GameId import eventDemo.app.event.GameEventHandler import eventDemo.app.event.GameEventStream +import eventDemo.app.event.event.GameEvent import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicInteger class GameStateRepository( private val eventStream: GameEventStream, eventHandler: GameEventHandler, + private val maxSnapshotCacheSize: Int = 20, ) { - private val projections: ConcurrentHashMap = ConcurrentHashMap() + private val projections: MutableMap = ConcurrentHashMap() + private val version: AtomicInteger = AtomicInteger(0) + private val projectionsSnapshot: MutableMap = ConcurrentHashMap() + private val sortedSnapshotByVersion: MutableMap = ConcurrentHashMap() init { eventHandler.registerProjectionBuilder { event -> val projection = projections[event.gameId] if (projection == null) { - event.gameId + event + .gameId .buildStateFromEventStream(eventStream) .update() } else { projection .apply(event) - .let { projections.put(it.gameId, it) } + .also { projections[it.gameId] = it } + .also { state -> + val newVersion = version.addAndGet(1) + saveSnapshot(event, state, newVersion) + removeOldSnapshot() + } } } } - fun get(gameId: GameId): GameState = gameId.buildStateFromEventStream(eventStream) + private fun removeOldSnapshot() { + if (projectionsSnapshot.size > maxSnapshotCacheSize) { + val numberToRemove = projectionsSnapshot.size - maxSnapshotCacheSize + sortedSnapshotByVersion + .toList() + .sortedBy { it.second } + .take(numberToRemove) + .toMap() + .keys + .forEach { event -> + sortedSnapshotByVersion.remove(event) + projectionsSnapshot.remove(event) + } + } + } + + private fun saveSnapshot( + event: GameEvent, + state: GameState, + newVersion: Int, + ) { + projectionsSnapshot[event] = state + sortedSnapshotByVersion[event] = newVersion + } + + /** + * Get the last version of the [GameState] from the all eventStream. + * + * It fetches it from the local cache if possible, otherwise it builds it. + */ + fun get(gameId: GameId): GameState = + projections[gameId] + ?: gameId.buildStateFromEventStream(eventStream) + + /** + * Get the [GameState] to the specific [event][GameEvent]. + * It does not contain the [events][GameEvent] it after this one. + * + * It fetches it from the local cache if possible, otherwise it builds it. + */ + fun getUntil(event: GameEvent): GameState = + projectionsSnapshot[event] + ?: event.buildStateFromEventStreamTo(eventStream) private fun GameState.update() { projections[gameId] = this } + + /** + * Build the state to the specific event + */ + private fun GameEvent.buildStateFromEventStreamTo(eventStream: GameEventStream): GameState = + run { eventStream.readAll(gameId).takeWhile { it != this } + this }.buildStateFromEvents() } diff --git a/src/main/kotlin/eventDemo/app/eventListener/GameEventPlayerNotificationListener.kt b/src/main/kotlin/eventDemo/app/eventListener/GameEventPlayerNotificationListener.kt index fa79753..b4f43e0 100644 --- a/src/main/kotlin/eventDemo/app/eventListener/GameEventPlayerNotificationListener.kt +++ b/src/main/kotlin/eventDemo/app/eventListener/GameEventPlayerNotificationListener.kt @@ -2,7 +2,6 @@ package eventDemo.app.eventListener import eventDemo.app.entity.Player import eventDemo.app.event.GameEventBus -import eventDemo.app.event.GameEventStream import eventDemo.app.event.event.CardIsPlayedEvent import eventDemo.app.event.event.GameEvent import eventDemo.app.event.event.GameStartedEvent @@ -11,7 +10,7 @@ import eventDemo.app.event.event.PlayerChoseColorEvent import eventDemo.app.event.event.PlayerHavePassEvent import eventDemo.app.event.event.PlayerReadyEvent import eventDemo.app.event.event.PlayerWinEvent -import eventDemo.app.event.projection.buildStateFromEventStreamTo +import eventDemo.app.event.projection.GameStateRepository import eventDemo.app.notification.PlayerAsJoinTheGameNotification import eventDemo.app.notification.PlayerAsPlayACardNotification import eventDemo.app.notification.PlayerHavePassNotification @@ -29,7 +28,7 @@ import kotlinx.coroutines.channels.trySendBlocking class GameEventPlayerNotificationListener( private val eventBus: GameEventBus, - private val eventStream: GameEventStream, + private val gameStateRepository: GameStateRepository, ) { private val logger = KotlinLogging.logger {} @@ -38,7 +37,7 @@ class GameEventPlayerNotificationListener( currentPlayer: Player, ) { eventBus.subscribe { event: GameEvent -> - val currentState = event.buildStateFromEventStreamTo(eventStream) + val currentState = gameStateRepository.getUntil(event) val notification = when (event) { is NewPlayerEvent -> { diff --git a/src/main/kotlin/eventDemo/app/eventListener/GameEventReactionListener.kt b/src/main/kotlin/eventDemo/app/eventListener/GameEventReactionListener.kt index 6613b11..4f0118e 100644 --- a/src/main/kotlin/eventDemo/app/eventListener/GameEventReactionListener.kt +++ b/src/main/kotlin/eventDemo/app/eventListener/GameEventReactionListener.kt @@ -2,18 +2,17 @@ package eventDemo.app.eventListener import eventDemo.app.event.GameEventBus import eventDemo.app.event.GameEventHandler -import eventDemo.app.event.GameEventStream import eventDemo.app.event.event.GameEvent import eventDemo.app.event.event.GameStartedEvent import eventDemo.app.event.event.PlayerWinEvent import eventDemo.app.event.projection.GameState -import eventDemo.app.event.projection.buildStateFromEventStreamTo +import eventDemo.app.event.projection.GameStateRepository import io.github.oshai.kotlinlogging.KotlinLogging class GameEventReactionListener( private val eventBus: GameEventBus, private val eventHandler: GameEventHandler, - private val eventStream: GameEventStream, + private val gameStateRepository: GameStateRepository, private val priority: Int = DEFAULT_PRIORITY, ) { companion object Config { @@ -24,7 +23,7 @@ class GameEventReactionListener( fun init() { eventBus.subscribe(priority) { event: GameEvent -> - val state = event.buildStateFromEventStreamTo(eventStream) + val state = gameStateRepository.getUntil(event) sendStartGameEvent(state, event) sendWinnerEvent(state, event) } @@ -71,7 +70,7 @@ class GameEventReactionListener( "reactionEvent" to reactionEvent, ) } - eventStream.publish(reactionEvent) + eventHandler.handle(reactionEvent) } } } diff --git a/src/main/kotlin/eventDemo/configuration/ConfigureDI.kt b/src/main/kotlin/eventDemo/configuration/ConfigureDI.kt index 2d1a700..84808bd 100644 --- a/src/main/kotlin/eventDemo/configuration/ConfigureDI.kt +++ b/src/main/kotlin/eventDemo/configuration/ConfigureDI.kt @@ -33,12 +33,8 @@ val appKoinModule = single { GameStateRepository(get(), get()) } - single { - GameEventHandler(get(), get()) - } - single { - GameCommandHandler(get(), get()) - } + singleOf(::GameEventHandler) + singleOf(::GameCommandHandler) singleOf(::GameEventPlayerNotificationListener) } diff --git a/src/main/kotlin/eventDemo/libs/event/Event.kt b/src/main/kotlin/eventDemo/libs/event/Event.kt index 33c06cb..1b9596b 100644 --- a/src/main/kotlin/eventDemo/libs/event/Event.kt +++ b/src/main/kotlin/eventDemo/libs/event/Event.kt @@ -15,5 +15,6 @@ interface AggregateId { * @see EventStream */ interface Event { + val eventId: UUID val gameId: ID }