perf(core): reduce inter-service HTTP calls from 11 to 4 per move
- Add `postMoveStatus` batch method to `RuleSet` trait (default impl composes individual calls; `RuleSetRestAdapter` overrides with single HTTP round-trip) - Collapse 5 sequential rule checks in `GameEngine.executeMove` into one `postMoveStatus` call - Add `POST /api/rules/post-move-status` endpoint to rule-service - Add `exportCombined` to `IoServiceClient` and `POST /io/export/combined` endpoint to io-service, replacing two separate FEN/PGN HTTP calls - Fix `statusOf` to pattern-match on `WinReason` from `ctx.result` instead of making a redundant `isCheckmate` HTTP call - Remove duplicate `legalMoves` pre-validation in `GameResource.makeMove`; engine already validates and fires `InvalidMoveEvent` - Add `scalafix:off` guards for pre-existing `var`/`return` usage in `DefaultRules` hot-path code - Apply spotless formatting to previously unformatted files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,10 +7,10 @@ import org.eclipse.microprofile.rest.client.inject.RegisterRestClient
|
||||
case class CorePlayerInfo(id: String, displayName: String)
|
||||
case class CoreTimeControl(limitSeconds: Option[Int], incrementSeconds: Option[Int], daysPerMove: Option[Int])
|
||||
case class CoreCreateGameRequest(
|
||||
white: Option[CorePlayerInfo],
|
||||
black: Option[CorePlayerInfo],
|
||||
timeControl: Option[CoreTimeControl],
|
||||
mode: Option[String],
|
||||
white: Option[CorePlayerInfo],
|
||||
black: Option[CorePlayerInfo],
|
||||
timeControl: Option[CoreTimeControl],
|
||||
mode: Option[String],
|
||||
)
|
||||
case class CoreGameResponse(gameId: String)
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import io.quarkus.runtime.annotations.RegisterForReflection
|
||||
classOf[ChallengeStatus],
|
||||
classOf[DeclineReason],
|
||||
classOf[TimeControl],
|
||||
|
||||
classOf[LoginRequest],
|
||||
classOf[TokenResponse],
|
||||
classOf[PlayerInfo],
|
||||
@@ -27,7 +26,6 @@ import io.quarkus.runtime.annotations.RegisterForReflection
|
||||
classOf[ChallengeStatus],
|
||||
classOf[ChallengeColor],
|
||||
classOf[DeclineReason],
|
||||
|
||||
classOf[CorePlayerInfo],
|
||||
classOf[CoreTimeControl],
|
||||
classOf[CoreCreateGameRequest],
|
||||
|
||||
@@ -26,4 +26,4 @@ class Account extends PanacheEntityBase:
|
||||
var rating: Int = 1500
|
||||
|
||||
var createdAt: Instant = uninitialized
|
||||
// scalafix:on
|
||||
// scalafix:on
|
||||
|
||||
@@ -4,4 +4,4 @@ sealed trait TimeControl
|
||||
|
||||
object TimeControl:
|
||||
case class Clock(limit: Int, increment: Int) extends TimeControl
|
||||
case object Unlimited extends TimeControl
|
||||
case object Unlimited extends TimeControl
|
||||
|
||||
@@ -15,17 +15,17 @@ case class TimeControlDto(`type`: String, limit: Option[Int], increment: Option[
|
||||
case class ChallengeRequest(color: String, timeControl: TimeControlDto)
|
||||
|
||||
case class ChallengeDto(
|
||||
id: String,
|
||||
challenger: PlayerInfo,
|
||||
destUser: PlayerInfo,
|
||||
variant: String,
|
||||
color: String,
|
||||
timeControl: TimeControlDto,
|
||||
status: String,
|
||||
declineReason: Option[String],
|
||||
gameId: Option[String],
|
||||
createdAt: String,
|
||||
expiresAt: String,
|
||||
id: String,
|
||||
challenger: PlayerInfo,
|
||||
destUser: PlayerInfo,
|
||||
variant: String,
|
||||
color: String,
|
||||
timeControl: TimeControlDto,
|
||||
status: String,
|
||||
declineReason: Option[String],
|
||||
gameId: Option[String],
|
||||
createdAt: String,
|
||||
expiresAt: String,
|
||||
)
|
||||
|
||||
case class DeclineRequest(reason: Option[String])
|
||||
|
||||
+1
-4
@@ -40,7 +40,4 @@ class AccountRepository:
|
||||
.headOption
|
||||
|
||||
def findAll(): List[Account] =
|
||||
em.createQuery("FROM Account", classOf[Account])
|
||||
.getResultList
|
||||
.asScala
|
||||
.toList
|
||||
em.createQuery("FROM Account", classOf[Account]).getResultList.asScala.toList
|
||||
|
||||
@@ -63,8 +63,8 @@ class AccountResource:
|
||||
|
||||
private def toPublicDto(account: Account): PublicAccountDto =
|
||||
PublicAccountDto(
|
||||
id = account.id.toString,
|
||||
username = account.username,
|
||||
rating = account.rating,
|
||||
id = account.id.toString,
|
||||
username = account.username,
|
||||
rating = account.rating,
|
||||
createdAt = account.createdAt.toString,
|
||||
)
|
||||
|
||||
@@ -38,8 +38,8 @@ class ChallengeResource:
|
||||
case Left(error) =>
|
||||
val status = error match
|
||||
case ChallengeError.UserNotFound(_) | ChallengeError.ChallengerNotFound => Response.Status.NOT_FOUND
|
||||
case ChallengeError.CannotChallengeSelf => Response.Status.BAD_REQUEST
|
||||
case _ => Response.Status.CONFLICT
|
||||
case ChallengeError.CannotChallengeSelf => Response.Status.BAD_REQUEST
|
||||
case _ => Response.Status.CONFLICT
|
||||
Response.status(status).entity(ErrorDto(error.message)).build()
|
||||
|
||||
@GET
|
||||
|
||||
@@ -24,16 +24,14 @@ class AccountService:
|
||||
|
||||
@Transactional
|
||||
def register(req: RegisterRequest): Either[AccountError, Account] =
|
||||
if accountRepository.findByUsername(req.username).isDefined then
|
||||
Left(AccountError.UsernameTaken(req.username))
|
||||
else if accountRepository.findByEmail(req.email).isDefined then
|
||||
Left(AccountError.EmailAlreadyRegistered(req.email))
|
||||
if accountRepository.findByUsername(req.username).isDefined then Left(AccountError.UsernameTaken(req.username))
|
||||
else if accountRepository.findByEmail(req.email).isDefined then Left(AccountError.EmailAlreadyRegistered(req.email))
|
||||
else
|
||||
val account = new Account()
|
||||
account.username = req.username
|
||||
account.email = req.email
|
||||
account.username = req.username
|
||||
account.email = req.email
|
||||
account.passwordHash = BcryptUtil.bcryptHash(req.password)
|
||||
account.createdAt = Instant.now()
|
||||
account.createdAt = Instant.now()
|
||||
accountRepository.persist(account)
|
||||
Right(account)
|
||||
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
package de.nowchess.account.service
|
||||
|
||||
import de.nowchess.account.client.{CoreCreateGameRequest, CoreGameClient, CoreGameResponse, CorePlayerInfo, CoreTimeControl}
|
||||
import de.nowchess.account.client.{
|
||||
CoreCreateGameRequest,
|
||||
CoreGameClient,
|
||||
CoreGameResponse,
|
||||
CorePlayerInfo,
|
||||
CoreTimeControl,
|
||||
}
|
||||
import de.nowchess.account.domain.{Challenge, ChallengeColor, ChallengeStatus, DeclineReason}
|
||||
import de.nowchess.account.dto.{ChallengeDto, ChallengeListDto, ChallengeRequest, DeclineRequest, PlayerInfo, TimeControlDto}
|
||||
import de.nowchess.account.dto.{
|
||||
ChallengeDto,
|
||||
ChallengeListDto,
|
||||
ChallengeRequest,
|
||||
DeclineRequest,
|
||||
PlayerInfo,
|
||||
TimeControlDto,
|
||||
}
|
||||
import de.nowchess.account.error.ChallengeError
|
||||
import de.nowchess.account.repository.{AccountRepository, ChallengeRepository}
|
||||
import jakarta.enterprise.context.ApplicationScoped
|
||||
@@ -36,23 +49,21 @@ class ChallengeService:
|
||||
destUser <- accountRepository.findByUsername(destUsername).toRight(ChallengeError.UserNotFound(destUsername))
|
||||
challenger <- accountRepository.findById(challengerId).toRight(ChallengeError.ChallengerNotFound)
|
||||
_ <- Either.cond(challenger.id != destUser.id, (), ChallengeError.CannotChallengeSelf)
|
||||
_ <- Either.cond(
|
||||
challengeRepository.findDuplicateChallenge(challengerId, destUser.id).isEmpty,
|
||||
(),
|
||||
ChallengeError.DuplicateChallenge,
|
||||
)
|
||||
color <- parseColor(req.color)
|
||||
_ <- Either.cond(
|
||||
challengeRepository.findDuplicateChallenge(challengerId, destUser.id).isEmpty,
|
||||
(),
|
||||
ChallengeError.DuplicateChallenge,
|
||||
)
|
||||
color <- parseColor(req.color)
|
||||
yield
|
||||
val challenge = new Challenge()
|
||||
challenge.challenger = challenger
|
||||
challenge.destUser = destUser
|
||||
challenge.color = color
|
||||
challenge.status = ChallengeStatus.Created
|
||||
challenge.timeControlType = req.timeControl.`type`
|
||||
challenge.timeControlLimit =
|
||||
req.timeControl.limit.map(java.lang.Integer.valueOf).orNull
|
||||
challenge.timeControlIncrement =
|
||||
req.timeControl.increment.map(java.lang.Integer.valueOf).orNull
|
||||
val challenge = new Challenge()
|
||||
challenge.challenger = challenger
|
||||
challenge.destUser = destUser
|
||||
challenge.color = color
|
||||
challenge.status = ChallengeStatus.Created
|
||||
challenge.timeControlType = req.timeControl.`type`
|
||||
challenge.timeControlLimit = req.timeControl.limit.map(java.lang.Integer.valueOf).orNull
|
||||
challenge.timeControlIncrement = req.timeControl.increment.map(java.lang.Integer.valueOf).orNull
|
||||
challenge.createdAt = Instant.now()
|
||||
challenge.expiresAt = Instant.now().plus(24, ChronoUnit.HOURS)
|
||||
challengeRepository.persist(challenge)
|
||||
@@ -79,7 +90,7 @@ class ChallengeService:
|
||||
_ <- Either.cond(challenge.destUser.id == userId, (), ChallengeError.NotAuthorized)
|
||||
reason <- parseDeclineReason(req.reason)
|
||||
yield
|
||||
challenge.status = ChallengeStatus.Declined
|
||||
challenge.status = ChallengeStatus.Declined
|
||||
challenge.declineReason = reason.orNull
|
||||
challengeRepository.merge(challenge)
|
||||
challenge
|
||||
@@ -112,8 +123,8 @@ class ChallengeService:
|
||||
val challenger = CorePlayerInfo(challenge.challenger.id.toString, challenge.challenger.username)
|
||||
val destUser = CorePlayerInfo(challenge.destUser.id.toString, challenge.destUser.username)
|
||||
challenge.color match
|
||||
case ChallengeColor.White => (challenger, destUser)
|
||||
case ChallengeColor.Black => (destUser, challenger)
|
||||
case ChallengeColor.White => (challenger, destUser)
|
||||
case ChallengeColor.Black => (destUser, challenger)
|
||||
case ChallengeColor.Random =>
|
||||
if scala.util.Random.nextBoolean() then (challenger, destUser) else (destUser, challenger)
|
||||
|
||||
@@ -121,7 +132,7 @@ class ChallengeService:
|
||||
challenge.timeControlType match
|
||||
case "unlimited" => None
|
||||
case "correspondence" => Some(CoreTimeControl(None, None, challenge.timeControlLimitOpt))
|
||||
case _ => Some(CoreTimeControl(challenge.timeControlLimitOpt, challenge.timeControlIncrementOpt, None))
|
||||
case _ => Some(CoreTimeControl(challenge.timeControlLimitOpt, challenge.timeControlIncrementOpt, None))
|
||||
|
||||
private def parseColor(raw: String): Either[ChallengeError, ChallengeColor] =
|
||||
raw.toLowerCase match
|
||||
@@ -140,15 +151,15 @@ class ChallengeService:
|
||||
|
||||
def toDto(c: Challenge): ChallengeDto =
|
||||
ChallengeDto(
|
||||
id = c.id.toString,
|
||||
challenger = PlayerInfo(c.challenger.id.toString, c.challenger.username, c.challenger.rating),
|
||||
destUser = PlayerInfo(c.destUser.id.toString, c.destUser.username, c.destUser.rating),
|
||||
variant = "standard",
|
||||
color = c.color.toString.toLowerCase,
|
||||
id = c.id.toString,
|
||||
challenger = PlayerInfo(c.challenger.id.toString, c.challenger.username, c.challenger.rating),
|
||||
destUser = PlayerInfo(c.destUser.id.toString, c.destUser.username, c.destUser.rating),
|
||||
variant = "standard",
|
||||
color = c.color.toString.toLowerCase,
|
||||
timeControl = TimeControlDto(c.timeControlType, c.timeControlLimitOpt, c.timeControlIncrementOpt),
|
||||
status = c.status.toString.toLowerCase,
|
||||
status = c.status.toString.toLowerCase,
|
||||
declineReason = c.declineReasonOpt.map(_.toString.toLowerCase),
|
||||
gameId = c.gameIdOpt,
|
||||
createdAt = c.createdAt.toString,
|
||||
expiresAt = c.expiresAt.toString,
|
||||
gameId = c.gameIdOpt,
|
||||
createdAt = c.createdAt.toString,
|
||||
expiresAt = c.expiresAt.toString,
|
||||
)
|
||||
|
||||
+14
-2
@@ -156,8 +156,20 @@ class ChallengeResourceTest:
|
||||
val t1 = registerAndLogin("listUser1")
|
||||
registerAndLogin("listUser2")
|
||||
registerAndLogin("listUser3")
|
||||
authed(t1).contentType(ContentType.JSON).body(clockBody).when().post("/api/challenge/listUser2").`then`().statusCode(201)
|
||||
authed(t1).contentType(ContentType.JSON).body(clockBody).when().post("/api/challenge/listUser3").`then`().statusCode(201)
|
||||
authed(t1)
|
||||
.contentType(ContentType.JSON)
|
||||
.body(clockBody)
|
||||
.when()
|
||||
.post("/api/challenge/listUser2")
|
||||
.`then`()
|
||||
.statusCode(201)
|
||||
authed(t1)
|
||||
.contentType(ContentType.JSON)
|
||||
.body(clockBody)
|
||||
.when()
|
||||
.post("/api/challenge/listUser3")
|
||||
.`then`()
|
||||
.statusCode(201)
|
||||
authed(t1)
|
||||
.when()
|
||||
.get("/api/challenge")
|
||||
|
||||
Reference in New Issue
Block a user