feat(ui): Fixed polling, added JQuery Ajax

Fixed Race Condition problems with polling, added JQuery
This commit is contained in:
LQ63
2025-11-19 15:14:47 +01:00
parent e60fe7c98d
commit e2a5cb9614
6 changed files with 312 additions and 312 deletions

View File

@@ -172,8 +172,7 @@ class IngameController @Inject() (
optSession.foreach(_.lock.unlock())
if (result.isSuccess) {
Ok(Json.obj(
"status" -> "success",
"redirectUrl" -> routes.IngameController.game(gameId).url
"status" -> "success"
))
} else {
val throwable = result.failed.get
@@ -198,6 +197,11 @@ class IngameController @Inject() (
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
case _: NotInteractableException =>
BadRequest(Json.obj(
"status" -> "failure",
"errorMessage" -> throwable.getMessage
))
case _ =>
InternalServerError(Json.obj(
"status" -> "failure",

View File

@@ -1,10 +1,11 @@
package controllers
import auth.{AuthAction, AuthenticatedRequest}
import controllers.PollingController.{scheduler, timeoutDuration}
import de.knockoutwhist.cards.Hand
import logic.PodManager
import logic.game.{GameLobby, PollingEvents}
import logic.game.PollingEvents.{CardPlayed, LobbyUpdate, NewRound, ReloadEvent}
import logic.game.PollingEvents.{CardPlayed, LobbyCreation, LobbyUpdate, NewRound, ReloadEvent}
import model.sessions.UserSession
import model.users.User
import play.api.libs.json.{JsArray, JsValue, Json}
@@ -13,7 +14,12 @@ import util.WebUIUtils
import javax.inject.{Inject, Singleton}
import scala.concurrent.{ExecutionContext, Future}
import java.util.concurrent.{Executors, ScheduledExecutorService, TimeUnit}
import scala.concurrent.duration.*
object PollingController {
private val scheduler: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor()
private val timeoutDuration = 25.seconds
}
@Singleton
class PollingController @Inject() (
val cc: ControllerComponents,
@@ -96,25 +102,28 @@ class PollingController @Inject() (
}
}
// --- Main Polling Action ---
def polling(gameId: String): Action[AnyContent] = authAction.async { implicit request: AuthenticatedRequest[AnyContent] =>
val playerId = request.user.id
// 1. Safely look up the game
podManager.getGame(gameId) match {
case Some(game) =>
// 2. Short-Poll Check (Check for missed events)
if (game.getPollingState.nonEmpty) {
val event = game.getPollingState.dequeue()
val playerEventQueue = game.getEventsOfPlayer(playerId)
if (playerEventQueue.nonEmpty) {
val event = playerEventQueue.dequeue()
Future.successful(handleEvent(event, game, game.getUserSession(playerId)))
} else {
val eventPromise = game.registerWaiter(playerId)
val scheduledFuture = scheduler.schedule(
new Runnable {
override def run(): Unit =
eventPromise.tryFailure(new java.util.concurrent.TimeoutException("Polling Timeout"))
},
timeoutDuration.toMillis,
TimeUnit.MILLISECONDS
)
eventPromise.future.map { event =>
scheduledFuture.cancel(false)
game.removeWaiter(playerId)
handleEvent(event, game, game.getUserSession(playerId))
}.recover {
@@ -125,7 +134,6 @@ class PollingController @Inject() (
}
case None =>
// Game not found
Future.successful(NotFound("Game not found."))
}
}

View File

@@ -11,7 +11,7 @@ import de.knockoutwhist.player.{AbstractPlayer, PlayerFactory}
import de.knockoutwhist.rounds.{Match, Round, Trick}
import de.knockoutwhist.utils.events.{EventListener, SimpleEvent}
import exceptions.*
import logic.game.PollingEvents.{CardPlayed, LobbyUpdate, NewRound, ReloadEvent}
import logic.game.PollingEvents.{CardPlayed, LobbyCreation, LobbyUpdate, NewRound, ReloadEvent}
import model.sessions.{InteractionType, UserSession}
import model.users.User
@@ -27,21 +27,39 @@ class GameLobby private(
val name: String,
val maxPlayers: Int
) extends EventListener {
logic.addListener(this)
logic.createSession()
private val users: mutable.Map[UUID, UserSession] = mutable.Map()
private val pollingState: mutable.Queue[PollingEvents] = mutable.Queue()
private val eventsPerPlayer: mutable.Map[UUID, mutable.Queue[PollingEvents]] = mutable.Map()
private val waitingPromises: mutable.Map[UUID, ScalaPromise[PollingEvents]] = mutable.Map()
private val lock = new Object
lock.synchronized {
logic.addListener(this)
logic.createSession()
}
def registerWaiter(playerId: UUID): ScalaPromise[PollingEvents] = {
val promise = ScalaPromise[PollingEvents]()
waitingPromises.put(playerId, promise)
promise
lock.synchronized {
val queue = eventsPerPlayer.getOrElseUpdate(playerId, mutable.Queue())
if (queue.nonEmpty) {
val evt = queue.dequeue()
promise.success(evt)
promise
} else {
waitingPromises.put(playerId, promise)
promise
}
}
}
def removeWaiter(playerId: UUID): Unit = {
waitingPromises.remove(playerId)
lock.synchronized {
waitingPromises.remove(playerId)
}
}
def addUser(user: User): UserSession = {
@@ -72,7 +90,8 @@ class GameLobby private(
}
if (event.oldState == Lobby && event.newState == InGame) {
addToQueue(ReloadEvent)
}else {
return
} else {
addToQueue(ReloadEvent)
}
users.values.foreach(session => session.updatePlayer(event))
@@ -84,11 +103,29 @@ class GameLobby private(
}
private def addToQueue(event: PollingEvents): Unit = {
if (waitingPromises.nonEmpty) {
waitingPromises.values.foreach(_.success(event))
waitingPromises.clear()
} else {
pollingState.enqueue(event)
lock.synchronized {
users.keys.foreach { playerId =>
val q = eventsPerPlayer.getOrElseUpdate(playerId, mutable.Queue())
q.enqueue(event)
}
val waiterIds = waitingPromises.keys.toList
waiterIds.foreach { playerId =>
val q = eventsPerPlayer.getOrElseUpdate(playerId, mutable.Queue())
if (q.nonEmpty) {
val evt = q.dequeue()
val p = waitingPromises.remove(playerId)
p.foreach(_.success(evt))
}
}
}
waitingPromises.keys.foreach { playerId =>
val queue = eventsPerPlayer(playerId)
if (queue.nonEmpty) {
val promise = waitingPromises(playerId)
promise.success(queue.dequeue())
waitingPromises.remove(playerId)
}
}
}
@@ -218,8 +255,8 @@ class GameLobby private(
def getLogic: GameLogic = {
logic
}
def getPollingState: mutable.Queue[PollingEvents] = {
pollingState
def getEventsOfPlayer(playerId: UUID): mutable.Queue[PollingEvents] = {
eventsPerPlayer.getOrElseUpdate(playerId, mutable.Queue())
}
private def getPlayerBySession(userSession: UserSession): AbstractPlayer = {
val playerOption = getMatch.totalplayers.find(_.id == userSession.id)

View File

@@ -5,4 +5,5 @@ enum PollingEvents {
case NewRound
case ReloadEvent
case LobbyUpdate
case LobbyCreation
}

View File

@@ -25,5 +25,6 @@
<script src="@routes.JavaScriptRoutingController.javascriptRoutes()" type="text/javascript"></script>
<script src="@routes.Assets.versioned("javascripts/main.js")" type="text/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</body>
</html>