feat(coordinator): add configurable coordinator settings and enhance WebSocket connection handling
Build & Test (NowChessSystems) TeamCity build failed
Build & Test (NowChessSystems) TeamCity build failed
This commit is contained in:
@@ -48,9 +48,12 @@ dependencies {
|
||||
|
||||
implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}"))
|
||||
implementation("io.quarkus:quarkus-websockets-next")
|
||||
implementation("io.quarkus:quarkus-jackson")
|
||||
implementation("io.quarkus:quarkus-arc")
|
||||
implementation("io.quarkus:quarkus-smallrye-jwt")
|
||||
implementation("io.quarkus:quarkus-config-yaml")
|
||||
implementation("io.quarkus:quarkus-smallrye-health")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-scala_3:${versions["JACKSON_SCALA"]!!}")
|
||||
implementation("org.redisson:redisson:${versions["REDISSON"]!!}")
|
||||
|
||||
testImplementation(platform("org.junit:junit-bom:${versions["JUNIT_BOM"]!!}"))
|
||||
|
||||
@@ -3,6 +3,9 @@ quarkus:
|
||||
port: 8084
|
||||
application:
|
||||
name: nowchess-ws
|
||||
swagger-ui:
|
||||
always-include: true
|
||||
path: /swagger-ui
|
||||
grpc:
|
||||
server:
|
||||
use-separate-server: false
|
||||
@@ -19,6 +22,12 @@ nowchess:
|
||||
host: localhost
|
||||
port: 6379
|
||||
prefix: nowchess
|
||||
mp:
|
||||
jwt:
|
||||
verify:
|
||||
publickey:
|
||||
location: keys/public.pem
|
||||
issuer: nowchess
|
||||
|
||||
"%deployed":
|
||||
nowchess:
|
||||
@@ -26,3 +35,9 @@ nowchess:
|
||||
host: ${REDIS_HOST}
|
||||
port: ${REDIS_PORT:6379}
|
||||
prefix: ${REDIS_PREFIX:nowchess}
|
||||
mp:
|
||||
jwt:
|
||||
verify:
|
||||
publickey:
|
||||
location: ${JWT_PUBLIC_KEY_PATH:keys/public.pem}
|
||||
issuer: nowchess
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxDsnsCAl0vQx7Vu9CLDZ
|
||||
g0SG05NgUzu9T+3DTEaHGq60T2uriO8BenwyvsF3BnDqTbKf4voohZ1DNfzdbT1J
|
||||
Fj8B62FrDmxcO+sp1/b5HUCJP6y2uSRCmzOHe5k7Pk1IEi72FgBpKXSRkFibRlVf
|
||||
634g7mgsPZAQ9PJEsv4Qvm05T9L6+Gmq6N3bMVLKRXs4RhDhaFbYH9GtUg1eI0yH
|
||||
YjGyRfqzW/nqVMstOLHt8CuPouq4p7eMzeDH3YHkxPm4GG5foCXMOd2DZrW0SCcr
|
||||
7dhFeNVWzQ2m53eOhBzNQX+v3pgjVStsePhBRt2LyGfwkNzmqDgqWsMzSHRMY+cn
|
||||
WQIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
@@ -0,0 +1,18 @@
|
||||
package de.nowchess.ws.config
|
||||
|
||||
import com.fasterxml.jackson.core.Version
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.module.scala.DefaultScalaModule
|
||||
import io.quarkus.jackson.ObjectMapperCustomizer
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class JacksonConfig extends ObjectMapperCustomizer:
|
||||
def customize(mapper: ObjectMapper): Unit =
|
||||
mapper.registerModule(new DefaultScalaModule() {
|
||||
override def version(): Version =
|
||||
// scalafix:off DisableSyntax.null
|
||||
new Version(2, 21, 1, null, "com.fasterxml.jackson.module", "jackson-module-scala")
|
||||
// scalafix:on DisableSyntax.null
|
||||
})
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package de.nowchess.ws.config
|
||||
|
||||
import de.nowchess.ws.resource.ConnectionMeta
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection
|
||||
|
||||
@RegisterForReflection(
|
||||
targets = Array(
|
||||
classOf[ConnectionMeta],
|
||||
),
|
||||
)
|
||||
class NativeReflectionConfig
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package de.nowchess.ws.resource
|
||||
|
||||
final case class ConnectionMeta(
|
||||
gameId: String,
|
||||
listenerId: Int,
|
||||
playerId: Option[String],
|
||||
)
|
||||
@@ -2,10 +2,13 @@ package de.nowchess.ws.resource
|
||||
|
||||
import de.nowchess.ws.config.RedisConfig
|
||||
import io.quarkus.websockets.next.*
|
||||
import io.smallrye.jwt.auth.principal.JWTParser
|
||||
import jakarta.inject.Inject
|
||||
import org.redisson.api.listener.MessageListener
|
||||
import org.redisson.api.RedissonClient
|
||||
|
||||
import scala.compiletime.uninitialized
|
||||
import scala.util.Try
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
@WebSocket(path = "/api/board/game/{gameId}/ws")
|
||||
@@ -17,9 +20,12 @@ class GameWebSocketResource:
|
||||
|
||||
@Inject
|
||||
var redisConfig: RedisConfig = uninitialized
|
||||
|
||||
@Inject
|
||||
var jwtParser: JWTParser = uninitialized
|
||||
// scalafix:on DisableSyntax.var
|
||||
|
||||
private val listenerIds = new ConcurrentHashMap[String, (String, Int)]()
|
||||
private val connections = new ConcurrentHashMap[String, ConnectionMeta]()
|
||||
|
||||
private def s2cTopic(gameId: String): String =
|
||||
s"${redisConfig.prefix}:game:$gameId:s2c"
|
||||
@@ -28,27 +34,41 @@ class GameWebSocketResource:
|
||||
s"${redisConfig.prefix}:game:$gameId:c2s"
|
||||
|
||||
@OnOpen
|
||||
def onOpen(connection: WebSocketConnection): Unit =
|
||||
val gameId = connection.pathParam("gameId")
|
||||
val topic = redisson.getTopic(s2cTopic(gameId))
|
||||
def onOpen(connection: WebSocketConnection, handshake: HandshakeRequest): Unit =
|
||||
val gameId = connection.pathParam("gameId")
|
||||
val playerId = Option(handshake.header("Authorization"))
|
||||
.filter(_.nonEmpty)
|
||||
.flatMap(token => Try(jwtParser.parse(token)).toOption)
|
||||
.map(_.getSubject)
|
||||
val topic = redisson.getTopic(s2cTopic(gameId))
|
||||
val listenerId = topic.addListener(
|
||||
classOf[String],
|
||||
new MessageListener[String]:
|
||||
def onMessage(channel: CharSequence, msg: String): Unit =
|
||||
connection.sendText(msg).subscribe().`with`(_ => (), _ => ()),
|
||||
)
|
||||
listenerIds.put(connection.id(), (gameId, listenerId))
|
||||
val connectedMsg = s"""{"type":"CONNECTED","gameId":"$gameId"}"""
|
||||
connections.put(connection.id(), ConnectionMeta(gameId, listenerId, playerId))
|
||||
val connectedMsg = playerId match
|
||||
case Some(pid) => s"""{"type":"CONNECTED","gameId":"$gameId","playerId":"$pid"}"""
|
||||
case None => s"""{"type":"CONNECTED","gameId":"$gameId"}"""
|
||||
redisson.getTopic(c2sTopic(gameId)).publish(connectedMsg)
|
||||
|
||||
@OnTextMessage
|
||||
def onTextMessage(connection: WebSocketConnection, message: String): Unit =
|
||||
Option(listenerIds.get(connection.id())).foreach { case (gameId, _) =>
|
||||
redisson.getTopic(c2sTopic(gameId)).publish(message)
|
||||
Option(connections.get(connection.id())).foreach { meta =>
|
||||
val enriched = meta.playerId match
|
||||
case Some(pid) => injectPlayerId(message, pid)
|
||||
case None => message
|
||||
redisson.getTopic(c2sTopic(meta.gameId)).publish(enriched)
|
||||
}
|
||||
|
||||
@OnClose
|
||||
def onClose(connection: WebSocketConnection): Unit =
|
||||
Option(listenerIds.remove(connection.id())).foreach { case (gameId, listenerId) =>
|
||||
redisson.getTopic(s2cTopic(gameId)).removeListener(listenerId)
|
||||
Option(connections.remove(connection.id())).foreach { meta =>
|
||||
redisson.getTopic(s2cTopic(meta.gameId)).removeListener(meta.listenerId)
|
||||
}
|
||||
|
||||
private def injectPlayerId(msg: String, pid: String): String =
|
||||
val trimmed = msg.trim
|
||||
if trimmed.endsWith("}") then trimmed.dropRight(1) + s""","playerId":"$pid"}"""
|
||||
else msg
|
||||
|
||||
Reference in New Issue
Block a user