feat(ui): Fixed polling, added JQuery Ajax
Fixed Race Condition problems with polling, added JQuery
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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."))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -5,4 +5,5 @@ enum PollingEvents {
|
||||
case NewRound
|
||||
case ReloadEvent
|
||||
case LobbyUpdate
|
||||
case LobbyCreation
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user