diff --git a/js/src/main/scala/cs214/webapp/client/StateMachineClientApp.scala b/js/src/main/scala/cs214/webapp/client/StateMachineClientApp.scala index 5c5993778d3dcd91ca76028ed4807b906f5424a3..3155739fb647b19ea2b1d56e013bc9a3a7dc9952 100644 --- a/js/src/main/scala/cs214/webapp/client/StateMachineClientApp.scala +++ b/js/src/main/scala/cs214/webapp/client/StateMachineClientApp.scala @@ -9,23 +9,61 @@ import org.scalajs.dom type Target = dom.Element +private class WebSocket(endpoint: String): + var socket: Option[dom.WebSocket] = None + + val MIN_DELAY_MS = 10d + val MAX_DELAY_MS = 5000d + var reconnect_delay_ms = MIN_DELAY_MS + + private var listeners: List[String => Unit] = Nil + def addListener(listener: String => Unit) = + listeners = listener :: listeners + + def send(str: String) = socket match + case Some(s) => s.send(str) + case None => dom.window.alert("Disconnected!") + + private def onmessage(event: dom.MessageEvent) = + listeners.foreach(_(event.data.toString)) + + private def reconnect(): Unit = + println("[ws] Attempting to connect") + val _socket = dom.WebSocket(endpoint) + + def onopen(event: dom.Event) = + reconnect_delay_ms = MIN_DELAY_MS + println("[ws] WebSocket connection opened") + def onclose(event: dom.CloseEvent) = + if socket == Some(_socket) then socket = None + println(s"[ws] WebSocket connection closed (${event.code}): ${event.reason}") + if event.code != 1000 then // Normal closure + dom.window.setTimeout(() => reconnect(), reconnect_delay_ms) + reconnect_delay_ms = math.min(1.5 * reconnect_delay_ms, MAX_DELAY_MS) + def onerror(event: dom.Event) = + println(s"[ws] WebSocket error") + _socket.close(3000) // Lowest custom error code + + _socket.onopen = evt => onopen(evt) + _socket.onclose = evt => onclose(evt) + _socket.onerror = evt => onerror(evt) + _socket.onmessage = msg => onmessage(msg) + socket = Some(_socket) + + reconnect() + abstract class WSClientApp extends ClientApp: WebClient.register(this) protected def init(userId: UserId, sendMessage: ujson.Value => Unit, target: Target): ClientAppInstance def init(instanceId: InstanceId, userId: UserId, endpoint: String, target: Target): ClientAppInstance = - val socket = new dom.WebSocket(endpoint) - socket.onopen = (event: dom.Event) => println("WebSocket connection opened") - socket.onclose = (event: dom.CloseEvent) => println(s"WebSocket connection closed: ${event.reason}") - socket.onerror = (event: dom.Event) => println(s"WebSocket error: ${event.`type`}") - + val socket = WebSocket(endpoint) val sendMessage = (js: ujson.Value) => socket.send(js.toString) val client = init(userId, sendMessage, target) - socket.onmessage = msg => - val js = ujson.read(msg.data.toString) + socket.addListener: msg => + val js = ujson.read(msg) client.onMessage(SocketResponseWire.decode(js).flatten) - client /** Instance of a client-side state machine application. diff --git a/jvm/src/main/scala/cs214/webapp/server/web/ServerApp.scala b/jvm/src/main/scala/cs214/webapp/server/web/ServerApp.scala index c468c3d588ad1c4f7862ba75b93fece96b127b89..78d90225a4a46407aa61615704f3975203f94bf2 100644 --- a/jvm/src/main/scala/cs214/webapp/server/web/ServerApp.scala +++ b/jvm/src/main/scala/cs214/webapp/server/web/ServerApp.scala @@ -100,7 +100,7 @@ private[web] abstract class ServerApp: for channels <- channels.values channel <- channels do - channel.send(cask.Ws.Close()) + channel.send(cask.Ws.Close(cask.Ws.Close.NormalClosure, "Shutdown")) /** Sends a message to a specific client. */ private def send(userId: UserId)(message: ujson.Value): Unit = instanceLock.synchronized: