create bus and subscriber

This commit is contained in:
2025-03-04 23:02:07 +01:00
parent a6847353b2
commit f3ca94c97e
36 changed files with 885 additions and 234 deletions

View File

@@ -0,0 +1,73 @@
package eventDemo.app.actions
import eventDemo.app.actions.playNewCard.PlayCardCommand
import eventDemo.shared.command.GameCommandStream
import eventDemo.shared.entity.Player
import eventDemo.shared.event.CardIsPlayedEvent
import eventDemo.shared.event.GameEvent
import eventDemo.shared.event.GameEventStream
import eventDemo.shared.event.GameState
import eventDemo.shared.event.buildStateFromEventStream
import io.ktor.websocket.Frame
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
/**
* Listen [PlayCardCommand] on [GameCommandStream], check the validity and execute the action.
*
* This action can be executing an action and produce a new [GameEvent] after verification.
*/
class GameCommandHandler(
private val eventStream: GameEventStream,
incoming: ReceiveChannel<Frame>,
outgoing: SendChannel<Frame>,
) {
private val commandStream = GameCommandStream(incoming, outgoing)
private val playerNotifier = outgoing
/**
* Init the handler
*/
fun init(player: Player) {
CoroutineScope(Dispatchers.IO).launch {
commandStream.process {
if (it.payload.player.id != player.id) {
nack()
}
when (it) {
is PlayCardCommand -> {
// Check the command can be executed
val canBeExecuted =
it.payload.gameId
.buildStateFromEventStream(eventStream)
.commandCardCanBeExecuted(it)
if (canBeExecuted) {
eventStream.publish(
CardIsPlayedEvent(
it.payload.gameId,
it.payload.card,
it.payload.player,
),
)
} else {
runBlocking {
playerNotifier.send(Frame.Text("Command cannot be executed"))
}
}
}
}
}
}
}
}
private fun GameState.commandCardCanBeExecuted(command: PlayCardCommand): Boolean =
canBePlayThisCard(
command.payload.player,
command.payload.card,
)

View File

@@ -0,0 +1,22 @@
package eventDemo.app.actions
import eventDemo.libs.event.EventBus
import eventDemo.shared.GameId
import eventDemo.shared.event.GameEvent
import eventDemo.shared.toFrame
import io.ktor.websocket.Frame
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.runBlocking
class GameEventPlayerNotificationSubscriber(
private val eventBus: EventBus<GameEvent, GameId>,
private val outgoing: SendChannel<Frame>,
) {
fun init() {
eventBus.subscribe { event: GameEvent ->
runBlocking {
outgoing.send(event.toFrame())
}
}
}
}

View File

@@ -0,0 +1,27 @@
package eventDemo.app.actions
import eventDemo.libs.event.EventBus
import eventDemo.libs.event.EventStream
import eventDemo.shared.GameId
import eventDemo.shared.event.GameEvent
import eventDemo.shared.event.GameStartedEvent
import eventDemo.shared.event.buildStateFromEventStream
class GameEventReactionSubscriber(
private val eventBus: EventBus<GameEvent, GameId>,
private val eventStream: EventStream<GameEvent, GameId>,
) {
fun init() {
eventBus.subscribe { event: GameEvent ->
val state = event.id.buildStateFromEventStream(eventStream)
if (state.isReady) {
eventStream.publish(
GameStartedEvent.new(
state.gameId,
state.players,
),
)
}
}
}
}

View File

@@ -1,14 +1,15 @@
package eventDemo.app.actions.playNewCard
import eventDemo.libs.command.send
import eventDemo.plugins.GameIdSerializer
import eventDemo.shared.GameId
import eventDemo.shared.command.GameCommandStream
import eventDemo.shared.command.GameCommandStreamInMemory
import eventDemo.shared.entity.Card
import eventDemo.shared.entity.Game
import eventDemo.shared.entity.Player
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.auth.principal
import io.ktor.server.request.receive
import io.ktor.server.resources.post
import io.ktor.server.response.respondNullable
@@ -16,12 +17,11 @@ import io.ktor.server.routing.Routing
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import org.koin.ktor.ext.inject
@Serializable
@Resource("/game/{id}")
class GameRoute(
@Serializable(with = GameIdSerializer::class)
// @Serializable(with = GameIdSerializer::class)
val id: GameId,
) {
@Serializable
@@ -35,19 +35,27 @@ class GameRoute(
* API route to send a request to play card.
*/
fun Routing.playNewCard() {
val commandStream by inject<GameCommandStream>()
val commandStream = GameCommandStreamInMemory()
authenticate {
/*
* A player request to play a new card.
*
* It always returns [HttpStatusCode.OK], but it is not mean that card is already played!
*/
post<GameRoute.Card> {
val card = call.receive<Card>()
val name = call.principal<Player>()!!
launch(Dispatchers.Default) {
commandStream.send(
PlayCardCommand(
it.game.id,
name,
card,
),
)
}
/*
* A player request to play a new card.
*
* It always returns [HttpStatusCode.OK], but it is not mean that card is already played!
*/
post<GameRoute.Card> {
val card = call.receive<Card>()
launch(Dispatchers.Default) {
commandStream.send(PlayCardCommand(Game(it.game.id), card))
call.respondNullable<Any?>(HttpStatusCode.OK, null)
}
call.respondNullable<Any?>(HttpStatusCode.OK, null)
}
}

View File

@@ -2,8 +2,9 @@ package eventDemo.app.actions.playNewCard
import eventDemo.libs.command.Command
import eventDemo.libs.command.CommandId
import eventDemo.shared.GameId
import eventDemo.shared.entity.Card
import eventDemo.shared.entity.Game
import eventDemo.shared.entity.Player
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@@ -13,19 +14,32 @@ import kotlinx.serialization.Serializable
@Serializable
@SerialName("PlayCard")
data class PlayCardCommand(
val payload: Payload,
) : Command {
override val payload: Payload,
) : GameCommand {
constructor(
game: Game,
gameId: GameId,
player: Player,
card: Card,
) : this(Payload(game, card))
) : this(Payload(gameId, player, card))
override val name: String = "PlayCard"
override val id: CommandId = CommandId()
@Serializable
data class Payload(
val game: Game,
override val gameId: GameId,
override val player: Player,
val card: Card,
)
) : GameCommand.Payload
}
@Serializable
sealed interface GameCommand : Command {
val payload: Payload
@Serializable
sealed interface Payload {
val gameId: GameId
val player: Player
}
}

View File

@@ -1,30 +0,0 @@
package eventDemo.app.actions.playNewCard
import eventDemo.shared.command.GameCommandStream
import eventDemo.shared.event.CardIsPlayedEvent
import eventDemo.shared.event.GameEventStream
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
/**
* Listen [PlayCardCommand] on [GameCommandStream], check the validity and execute the action.
*
* This action produces a new [CardIsPlayedEvent]
*/
class PlayCardCommandHandler(
private val commandStream: GameCommandStream,
private val eventStream: GameEventStream,
) {
/**
* Init the handler
*/
fun init() {
CoroutineScope(Dispatchers.IO).launch {
commandStream.process {
// TODO check the command can be executed
eventStream.publish(CardIsPlayedEvent(it.payload.game.id, it.payload.card))
}
}
}
}