Move file and add interface to improve Archi Hexa

This commit is contained in:
2025-04-14 23:39:47 +02:00
parent 4739495980
commit 769d104040
77 changed files with 388 additions and 337 deletions
@@ -0,0 +1,11 @@
package eventDemo.adapter.infrastructureLayer.event
import eventDemo.business.entity.GameId
import eventDemo.business.event.GameEventBus
import eventDemo.business.event.event.GameEvent
import eventDemo.libs.event.EventBus
import eventDemo.libs.event.EventBusInMemory
class GameEventBusInMemory :
GameEventBus,
EventBus<GameEvent, GameId> by EventBusInMemory<GameEvent, GameId>()
@@ -0,0 +1,14 @@
package eventDemo.adapter.infrastructureLayer.event
import eventDemo.business.entity.GameId
import eventDemo.business.event.GameEventStore
import eventDemo.business.event.event.GameEvent
import eventDemo.libs.event.EventStore
import eventDemo.libs.event.EventStoreInMemory
/**
* A stream to publish and read the played card event.
*/
class GameEventStoreInMemory :
GameEventStore,
EventStore<GameEvent, GameId> by EventStoreInMemory()
@@ -0,0 +1,48 @@
package eventDemo.adapter.infrastructureLayer.event.projection
import eventDemo.business.entity.GameId
import eventDemo.business.event.GameEventHandler
import eventDemo.business.event.GameEventStore
import eventDemo.business.event.event.GameEvent
import eventDemo.business.event.projection.GameState
import eventDemo.business.event.projection.GameStateRepository
import eventDemo.business.event.projection.apply
import eventDemo.libs.event.projection.ProjectionSnapshotRepositoryInMemory
import eventDemo.libs.event.projection.SnapshotConfig
class GameStateRepositoryInMemory(
eventStore: GameEventStore,
eventHandler: GameEventHandler,
snapshotConfig: SnapshotConfig = SnapshotConfig(),
) : GameStateRepository {
private val projectionsSnapshot =
ProjectionSnapshotRepositoryInMemory(
eventStore = eventStore,
snapshotCacheConfig = snapshotConfig,
applyToProjection = GameState::apply,
initialStateBuilder = { aggregateId: GameId -> GameState(aggregateId) },
)
init {
eventHandler.registerProjectionBuilder { event ->
projectionsSnapshot.applyAndPutToCache(event)
}
}
/**
* Get the last version of the [GameState] from the all eventStream.
*
* It fetches it from the local cache if possible, otherwise it builds it.
*/
override fun getLast(gameId: GameId): GameState =
projectionsSnapshot.getLast(gameId)
/**
* 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.
*/
override fun getUntil(event: GameEvent): GameState =
projectionsSnapshot.getUntil(event)
}
@@ -0,0 +1,51 @@
package eventDemo.adapter.interfaceLayer
import eventDemo.business.command.GameCommandHandler
import eventDemo.business.entity.Player
import eventDemo.business.event.eventListener.PlayerNotificationEventListener
import eventDemo.business.notification.Notification
import eventDemo.libs.fromFrameChannel
import eventDemo.libs.toObjectChannel
import io.github.oshai.kotlinlogging.withLoggingContext
import io.ktor.server.application.ApplicationCall
import io.ktor.server.auth.authenticate
import io.ktor.server.auth.jwt.JWTPrincipal
import io.ktor.server.auth.principal
import io.ktor.server.routing.Route
import io.ktor.server.websocket.webSocket
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.launch
@DelicateCoroutinesApi
fun Route.gameWebSocket(
playerNotificationListener: PlayerNotificationEventListener,
commandHandler: GameCommandHandler,
) {
authenticate {
webSocket("/game") {
val currentPlayer = call.getPlayer()
val outgoingFrameChannel: SendChannel<Notification> = fromFrameChannel(outgoing)
withLoggingContext("currentPlayer" to currentPlayer.toString()) {
GlobalScope.launch {
commandHandler.handle(
currentPlayer,
toObjectChannel(incoming),
outgoingFrameChannel,
)
}
playerNotificationListener.startListening({ outgoingFrameChannel.trySendBlocking(it) }, currentPlayer)
}
}
}
}
private fun ApplicationCall.getPlayer() =
principal<JWTPrincipal>()!!.run {
Player(
id = payload.getClaim("playerid").asString(),
name = payload.getClaim("username").asString(),
)
}
@@ -0,0 +1,55 @@
package eventDemo.adapter.interfaceLayer
import eventDemo.business.entity.GameId
import eventDemo.business.event.projection.GameStateRepository
import eventDemo.configuration.GameIdSerializer
import io.ktor.http.HttpStatusCode
import io.ktor.resources.Resource
import io.ktor.server.application.call
import io.ktor.server.auth.authenticate
import io.ktor.server.resources.get
import io.ktor.server.response.respond
import io.ktor.server.routing.Route
import kotlinx.serialization.Serializable
@Serializable
@Resource("/game/{id}")
class Game(
@Serializable(with = GameIdSerializer::class)
val id: GameId,
) {
@Serializable
@Resource("card/last")
class Card(
val game: Game,
)
@Serializable
@Resource("state")
class State(
val game: Game,
)
}
/**
* API routes to read the game state.
*/
fun Route.readTheGameState(gameStateRepository: GameStateRepository) {
authenticate {
// Read the last played card on the game.
get<Game.Card> { body ->
gameStateRepository
.getLast(body.game.id)
.cardOnCurrentStack
?.card
?.let { call.respond(it) }
?: call.response.status(HttpStatusCode.BadRequest)
}
// Read the last played card on the game.
get<Game.State> { body ->
val state = gameStateRepository.getLast(body.game.id)
call.respond(state)
}
}
}