feat(redis): implement Redis integration for game state management and websocket communication
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
package de.nowchess.store.config
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty
|
||||
import scala.compiletime.uninitialized
|
||||
|
||||
@ApplicationScoped
|
||||
class RedisConfig:
|
||||
@ConfigProperty(name = "nowchess.redis.host", defaultValue = "localhost")
|
||||
var host: String = uninitialized
|
||||
|
||||
@ConfigProperty(name = "nowchess.redis.port", defaultValue = "6379")
|
||||
var port: Int = uninitialized
|
||||
|
||||
@ConfigProperty(name = "nowchess.redis.prefix", defaultValue = "nowchess")
|
||||
var prefix: String = uninitialized
|
||||
@@ -0,0 +1,29 @@
|
||||
package de.nowchess.store.config
|
||||
|
||||
import jakarta.annotation.PreDestroy
|
||||
import jakarta.enterprise.context.ApplicationScoped
|
||||
import jakarta.enterprise.inject.Produces
|
||||
import jakarta.inject.Inject
|
||||
import org.redisson.Redisson
|
||||
import org.redisson.api.RedissonClient
|
||||
import org.redisson.config.Config
|
||||
import scala.compiletime.uninitialized
|
||||
|
||||
@ApplicationScoped
|
||||
class RedissonProducer:
|
||||
@Inject
|
||||
var redisConfig: RedisConfig = uninitialized
|
||||
|
||||
@Produces
|
||||
@ApplicationScoped
|
||||
def redissonClient(): RedissonClient =
|
||||
val config = new Config()
|
||||
config.useSingleServer()
|
||||
.setAddress(s"redis://${redisConfig.host}:${redisConfig.port}")
|
||||
.setConnectionMinimumIdleSize(1)
|
||||
.setConnectTimeout(500)
|
||||
Redisson.create(config)
|
||||
|
||||
@PreDestroy
|
||||
def close(client: RedissonClient): Unit =
|
||||
client.shutdown()
|
||||
@@ -0,0 +1,82 @@
|
||||
package de.nowchess.store.domain
|
||||
|
||||
import io.quarkus.hibernate.orm.panache.PanacheEntityBase
|
||||
import jakarta.persistence.*
|
||||
import scala.compiletime.uninitialized
|
||||
import java.time.Instant
|
||||
|
||||
@Entity
|
||||
@Table(name = "game_records")
|
||||
class GameRecord extends PanacheEntityBase:
|
||||
// scalafix:off DisableSyntax.var
|
||||
@Id
|
||||
@Column(nullable = false)
|
||||
var gameId: String = uninitialized
|
||||
|
||||
@Column(nullable = false, columnDefinition = "TEXT")
|
||||
var fen: String = uninitialized
|
||||
|
||||
@Column(nullable = false, columnDefinition = "TEXT")
|
||||
var pgn: String = uninitialized
|
||||
|
||||
@Column(nullable = false)
|
||||
var moveCount: Int = 0
|
||||
|
||||
@Column(nullable = false)
|
||||
var createdAt: Instant = uninitialized
|
||||
|
||||
@Column(nullable = false)
|
||||
var updatedAt: Instant = uninitialized
|
||||
|
||||
// Player info
|
||||
@Column(nullable = false)
|
||||
var whiteId: String = uninitialized
|
||||
|
||||
@Column(nullable = false)
|
||||
var whiteName: String = uninitialized
|
||||
|
||||
@Column(nullable = false)
|
||||
var blackId: String = uninitialized
|
||||
|
||||
@Column(nullable = false)
|
||||
var blackName: String = uninitialized
|
||||
|
||||
@Column(nullable = false)
|
||||
var mode: String = uninitialized
|
||||
|
||||
// Time control
|
||||
@Column
|
||||
var limitSeconds: java.lang.Integer = uninitialized
|
||||
|
||||
@Column
|
||||
var incrementSeconds: java.lang.Integer = uninitialized
|
||||
|
||||
@Column
|
||||
var daysPerMove: java.lang.Integer = uninitialized
|
||||
|
||||
// Clock state
|
||||
@Column
|
||||
var whiteRemainingMs: java.lang.Long = uninitialized
|
||||
|
||||
@Column
|
||||
var blackRemainingMs: java.lang.Long = uninitialized
|
||||
|
||||
@Column
|
||||
var incrementMs: java.lang.Long = uninitialized
|
||||
|
||||
@Column
|
||||
var clockLastTickAt: java.lang.Long = uninitialized
|
||||
|
||||
@Column
|
||||
var clockMoveDeadline: java.lang.Long = uninitialized
|
||||
|
||||
@Column
|
||||
var clockActiveColor: String = uninitialized
|
||||
|
||||
// Game meta
|
||||
@Column(nullable = false)
|
||||
var resigned: Boolean = false
|
||||
|
||||
@Column
|
||||
var pendingDrawOffer: String = uninitialized
|
||||
// scalafix:on
|
||||
@@ -0,0 +1,24 @@
|
||||
package de.nowchess.store.redis
|
||||
|
||||
case class GameWritebackEventDto(
|
||||
gameId: String,
|
||||
fen: String,
|
||||
pgn: String,
|
||||
moveCount: Int,
|
||||
whiteId: String,
|
||||
whiteName: String,
|
||||
blackId: String,
|
||||
blackName: String,
|
||||
mode: String,
|
||||
resigned: Boolean,
|
||||
limitSeconds: Option[Int],
|
||||
incrementSeconds: Option[Int],
|
||||
daysPerMove: Option[Int],
|
||||
whiteRemainingMs: Option[Long],
|
||||
blackRemainingMs: Option[Long],
|
||||
incrementMs: Option[Long],
|
||||
clockLastTickAt: Option[Long],
|
||||
clockMoveDeadline: Option[Long],
|
||||
clockActiveColor: Option[String],
|
||||
pendingDrawOffer: Option[String],
|
||||
)
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package de.nowchess.store.redis
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import de.nowchess.store.service.GameWritebackService
|
||||
import jakarta.annotation.PostConstruct
|
||||
import jakarta.enterprise.context.ApplicationScoped
|
||||
import jakarta.inject.Inject
|
||||
import org.redisson.api.listener.MessageListener
|
||||
import org.redisson.api.RedissonClient
|
||||
import scala.compiletime.uninitialized
|
||||
import scala.util.Try
|
||||
|
||||
@ApplicationScoped
|
||||
class GameWritebackStreamListener:
|
||||
@Inject
|
||||
// scalafix:off DisableSyntax.var
|
||||
var redisson: RedissonClient = uninitialized
|
||||
@Inject var objectMapper: ObjectMapper = uninitialized
|
||||
@Inject var writebackService: GameWritebackService = uninitialized
|
||||
// scalafix:on
|
||||
|
||||
@PostConstruct
|
||||
def startListening(): Unit =
|
||||
val topic = redisson.getTopic("game-writeback")
|
||||
topic.addListener(classOf[String], new MessageListener[String]:
|
||||
def onMessage(channel: CharSequence, json: String): Unit =
|
||||
Try(objectMapper.readValue(json, classOf[GameWritebackEventDto]))
|
||||
.toOption
|
||||
.foreach(writebackService.writeBack)
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
package de.nowchess.store.repository
|
||||
|
||||
import de.nowchess.store.domain.GameRecord
|
||||
import jakarta.enterprise.context.ApplicationScoped
|
||||
import jakarta.inject.Inject
|
||||
import jakarta.persistence.EntityManager
|
||||
import scala.compiletime.uninitialized
|
||||
|
||||
@ApplicationScoped
|
||||
class GameRecordRepository:
|
||||
@Inject
|
||||
// scalafix:off DisableSyntax.var
|
||||
var em: EntityManager = uninitialized
|
||||
// scalafix:on
|
||||
|
||||
def findByGameId(gameId: String): Option[GameRecord] =
|
||||
Option(em.find(classOf[GameRecord], gameId))
|
||||
|
||||
def persist(record: GameRecord): Unit =
|
||||
em.persist(record)
|
||||
|
||||
def merge(record: GameRecord): Unit =
|
||||
em.merge(record)
|
||||
@@ -0,0 +1,23 @@
|
||||
package de.nowchess.store.resource
|
||||
|
||||
import de.nowchess.store.repository.GameRecordRepository
|
||||
import jakarta.enterprise.context.ApplicationScoped
|
||||
import jakarta.inject.Inject
|
||||
import jakarta.ws.rs.*
|
||||
import jakarta.ws.rs.core.{MediaType, Response}
|
||||
import scala.compiletime.uninitialized
|
||||
|
||||
@Path("/game")
|
||||
@ApplicationScoped
|
||||
class StoreGameResource:
|
||||
@Inject
|
||||
// scalafix:off DisableSyntax.var
|
||||
var repository: GameRecordRepository = uninitialized
|
||||
// scalafix:on
|
||||
|
||||
@GET
|
||||
@Path("/{gameId}")
|
||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||
def getGame(@PathParam("gameId") gameId: String): Response =
|
||||
repository.findByGameId(gameId)
|
||||
.fold(Response.status(404).build())(r => Response.ok(r).build())
|
||||
@@ -0,0 +1,69 @@
|
||||
package de.nowchess.store.service
|
||||
|
||||
import de.nowchess.store.domain.GameRecord
|
||||
import de.nowchess.store.redis.GameWritebackEventDto
|
||||
import de.nowchess.store.repository.GameRecordRepository
|
||||
import jakarta.enterprise.context.ApplicationScoped
|
||||
import jakarta.inject.Inject
|
||||
import jakarta.transaction.Transactional
|
||||
import scala.compiletime.uninitialized
|
||||
import java.time.Instant
|
||||
|
||||
@ApplicationScoped
|
||||
class GameWritebackService:
|
||||
@Inject
|
||||
// scalafix:off DisableSyntax.var
|
||||
var repository: GameRecordRepository = uninitialized
|
||||
// scalafix:on
|
||||
|
||||
@Transactional
|
||||
def writeBack(event: GameWritebackEventDto): Unit =
|
||||
repository.findByGameId(event.gameId) match
|
||||
case None =>
|
||||
val record = new GameRecord
|
||||
record.gameId = event.gameId
|
||||
record.fen = event.fen
|
||||
record.pgn = event.pgn
|
||||
record.moveCount = event.moveCount
|
||||
record.whiteId = event.whiteId
|
||||
record.whiteName = event.whiteName
|
||||
record.blackId = event.blackId
|
||||
record.blackName = event.blackName
|
||||
record.mode = event.mode
|
||||
record.resigned = event.resigned
|
||||
record.limitSeconds = event.limitSeconds.map(java.lang.Integer.valueOf).orNull
|
||||
record.incrementSeconds = event.incrementSeconds.map(java.lang.Integer.valueOf).orNull
|
||||
record.daysPerMove = event.daysPerMove.map(java.lang.Integer.valueOf).orNull
|
||||
record.whiteRemainingMs = event.whiteRemainingMs.map(java.lang.Long.valueOf).orNull
|
||||
record.blackRemainingMs = event.blackRemainingMs.map(java.lang.Long.valueOf).orNull
|
||||
record.incrementMs = event.incrementMs.map(java.lang.Long.valueOf).orNull
|
||||
record.clockLastTickAt = event.clockLastTickAt.map(java.lang.Long.valueOf).orNull
|
||||
record.clockMoveDeadline = event.clockMoveDeadline.map(java.lang.Long.valueOf).orNull
|
||||
record.clockActiveColor = event.clockActiveColor.orNull
|
||||
record.pendingDrawOffer = event.pendingDrawOffer.orNull
|
||||
record.createdAt = Instant.now()
|
||||
record.updatedAt = Instant.now()
|
||||
repository.persist(record)
|
||||
case Some(r) if event.moveCount > r.moveCount || event.pgn != r.pgn =>
|
||||
r.fen = event.fen
|
||||
r.pgn = event.pgn
|
||||
r.moveCount = event.moveCount
|
||||
r.whiteId = event.whiteId
|
||||
r.whiteName = event.whiteName
|
||||
r.blackId = event.blackId
|
||||
r.blackName = event.blackName
|
||||
r.mode = event.mode
|
||||
r.resigned = event.resigned
|
||||
r.limitSeconds = event.limitSeconds.map(java.lang.Integer.valueOf).orNull
|
||||
r.incrementSeconds = event.incrementSeconds.map(java.lang.Integer.valueOf).orNull
|
||||
r.daysPerMove = event.daysPerMove.map(java.lang.Integer.valueOf).orNull
|
||||
r.whiteRemainingMs = event.whiteRemainingMs.map(java.lang.Long.valueOf).orNull
|
||||
r.blackRemainingMs = event.blackRemainingMs.map(java.lang.Long.valueOf).orNull
|
||||
r.incrementMs = event.incrementMs.map(java.lang.Long.valueOf).orNull
|
||||
r.clockLastTickAt = event.clockLastTickAt.map(java.lang.Long.valueOf).orNull
|
||||
r.clockMoveDeadline = event.clockMoveDeadline.map(java.lang.Long.valueOf).orNull
|
||||
r.clockActiveColor = event.clockActiveColor.orNull
|
||||
r.pendingDrawOffer = event.pendingDrawOffer.orNull
|
||||
r.updatedAt = Instant.now()
|
||||
repository.merge(r)
|
||||
case _ => ()
|
||||
Reference in New Issue
Block a user