From 3e0dd930e0a3a31a6723af242226e2c61e602c04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pit-Claudel?= <clement.pit-claudel@epfl.ch> Date: Sat, 30 Nov 2024 02:07:50 +0100 Subject: [PATCH] client: Clean up implementation of Pause action and fix double wait * js/src/main/scala/cs214/webapp/client/StateMachineClientApp.scala: (processing): New Boolean indicating that an event loop is running. (timeout, setCooldown, tryProcessNextAction): Remove. (processActions): New function: start an event loop, setting `processing` to `true` while traversing `actionsQueue`. Recurse through a future to allow individual actions to delay the next iteration of the event loop. (processAction): Return a `Future` to let individual actions handle timeouts. (after): New function: Expose `setTimeout` as a Future. Reported-by: Simon Lefort <androz2091@gmail.com> --- .../webapp/client/StateMachineClientApp.scala | 64 ++++++++----------- 1 file changed, 25 insertions(+), 39 deletions(-) diff --git a/js/src/main/scala/cs214/webapp/client/StateMachineClientApp.scala b/js/src/main/scala/cs214/webapp/client/StateMachineClientApp.scala index 8f70c6b..3cab0db 100644 --- a/js/src/main/scala/cs214/webapp/client/StateMachineClientApp.scala +++ b/js/src/main/scala/cs214/webapp/client/StateMachineClientApp.scala @@ -7,6 +7,9 @@ import scala.util.{Failure, Success} import org.scalajs.dom import scalatags.JsDom.all.Frag +import scala.scalajs.concurrent.JSExecutionContext.Implicits.queue +import scala.concurrent.{Future, Promise} + type Target = dom.Element abstract class WSClientApp extends ClientApp: @@ -83,58 +86,41 @@ abstract class StateMachineClientAppInstance[Event, View]( case Failure(msg) => WebClient.crash(Exception(msg)) case Success(Failure(msg)) => renderError(msg.getMessage) case Success(Success(actions)) => actionsQueue.enqueueAll(actions) - tryProcessNextAction() + tryProcessActionsQueue() - /** The views to render */ + /** Actions waiting to be processed. */ private val actionsQueue: mutable.Queue[Action[ujson.Value]] = mutable.Queue() + private var processing = false - /** Minimal cooldown time between the render of two views */ - private var actionCooldownDelay = 0 - - /** The timestamp for the last render of a view */ - private var lastActionAppliedMs = System.currentTimeMillis() - - /** A call to the [[tryProcessNextAction()]] function scheduled */ - private var timeout: Option[SetTimeoutHandle] = None + private def tryProcessActionsQueue(): Unit = + if !processing then processActions() - /** Sets the [[actionCooldownDelay]] of the client to the specified value */ - private def setCooldown(ms: Int): Unit = actionCooldownDelay = ms - - /** Apply the next action from [[actionsQueue]] if [[actionCooldownDelay]] - * permits it. - */ - private def tryProcessNextAction(): Unit = - // If there are still views to render and if no call to this function is scheduled - if actionsQueue.nonEmpty && timeout.isEmpty then - // Then check if we are out of the cooldown delay - if System.currentTimeMillis() - lastActionAppliedMs > actionCooldownDelay then - // We can render the view, and dequeue it - processAction(actionsQueue.dequeue()) - // Continue try emptying the queue - tryProcessNextAction() - else - // We still have to wait, put a timeout to call the function later - timeout = Some(setTimeout(actionCooldownDelay) { - // First remove the timeout so that, if necessary, - // the next function call can create a new one - timeout = None - // Then try to render next views - tryProcessNextAction() - }) + private def processActions(): Unit = + require(!processing) + def loop: Unit = + processing = actionsQueue.nonEmpty + if processing then + processAction(actionsQueue.dequeue()).andThen(_ => loop) + loop /** Execute a single action sent by the server. */ - private def processAction(jsonAction: Action[ujson.Value]): Unit = - lastActionAppliedMs = System.currentTimeMillis() - setCooldown(0) + private def processAction(jsonAction: Action[ujson.Value]): Future[Unit] = jsonAction match case Action.Alert(msg) => dom.window.alert(msg) + after(0) case Action.Pause(durationMs) => - setCooldown(durationMs) + after(durationMs) case Action.Render(js) => - // Step1: The client receives the view sent by the server here wire.viewFormat.decode(js) match case Failure(exception) => renderError(exception.getMessage) case Success(jsonView) => target.replaceChildren: this.render(userId, jsonView).render + after(0) + + /* Like `setTimeout(interval)`, but return a `Future`. */ + private def after(interval: Double): Future[Unit] = + val pr = Promise[Unit]() + setTimeout(interval)(pr.success(())) + pr.future -- GitLab