Rework connection handling in the client
authorRaphaël Jakse <raphael.trivabble@jakse.fr>
Sun, 12 Apr 2020 18:14:19 +0000 (20:14 +0200)
committerRaphaël Jakse <raphael.trivabble@jakse.fr>
Sun, 12 Apr 2020 18:14:19 +0000 (20:14 +0200)
This commit introduces code to handle websockets.
This is disabled for now, support in the server is not ready.

l10n/js/fr.js
l10n/po/fr/trivabble.po
l10n/pot/trivabble.pot
trivabble.js

index 7943250b3c0cef7908b554863c89e85665a70a80..d4d0d6ca99df1b2affca973fabc3d1297dfd5cf0 100644 (file)
@@ -1 +1 @@
-(function(){var _=myAlert.l10n;_("fr","OK","OK");_("fr","Yes","Oui");_("fr","No","Non");_("fr","Cancel","Annuler");_=trivabble.l10n;_("fr","Your game is not over. Are you sure you want to leave now?","Votre partie n'est pas terminée. Êtes-vous sûr·e de vouloir la quitter ?");_("fr","You are about to leave the current game. To recover it, please note its number: {0}","Vous allez quitter la partie en cours. Pour la retrouver, notez bien son numéro : {0}");_("fr","Sorry, a problem just happened. The page must be reloaded. If the problem is not too serious, you should be able to keep playing normally. Otherwise, contact the person who is able to fix the problem. Click on “Yes” to reload the page.","Désolé, une erreur s'est produite. La page doit être chargée de nouveau et si le problème n'est pas très grave, vous devriez pouvoir continuer à jouer normalement. Sinon, contactez la personne qui devrait pouvoir régler le problème. Cliquez sur « Oui » pour recharger la page.");_("fr","To join a game, please give the number which is displayed on your adversary(ies)' screen.\nIf you do not know it, ask them.\n\nWarning: your adversary must not take your number, (s)he must keep his/her own. If you whish to recover your current game, please not the following number: {0}.","Pour rejoindre une partie, donnez le numéro qui s'affiche chez votre (vos) adversaire(s).\nSi vous ne le connaissez pas, demandez-lui (leur).\n\nAttention : votre adversaire ne doit pas prendre votre numéro, il doit garder le sien. Si vous souhaitez retrouver votre partie actuelle plus tard, gardez le numéro suivant : {0}.");_("fr","It seems your did not give a correct number, or you clicked on “Cancel”. As a result, the current game continues, if any. To join a game, click on “Join a game” again.","Il semblerait que vous n'avez pas donné un numéro correct, ou que vous avez cliqué sur « Annuler ». Par conséquent, la partie en cours continue, s'il y a une partie en cours. Pour rejoindre une partie, cliquez à nouveau sur « Rejoindre une partie ».");_("fr","To change your name, enter a new one. You can keep using your current name by cancelling. Please note that if you change your name and you have games in progress, you will not be able to keep playing them anymore unless you get back to your current name.","Pour changer de nom, saisissez-en un nouveau. Vous pouvez continuer à utiliser votre ancien nom en annulant. Veuillez noter que si vous changez de nom et que vous avez des parties en cours, vous ne pourrez plus jouer à celles-ci, sauf si vous reprenez votre ancien nom.");_("fr","You cannot take another tile: your rack is full.","Vous ne pouvez pas tirer de pièce : votre chevalet est complet.");_("fr","Are you sure you want to put all the tiles back in the bag (in order to play another game)?","Êtes-vous sûr·e de vouloir remettre toutes les lettres du jeu dans le sac (en vue de jouer une nouvelle partie) ?");_("fr","Are you sure you want to put all your tiles back in the bag?","Êtes-vous sûr·e de vouloir remettre toutes vos lettres dans le sac ?");_("fr","It seems your did not give your name. You need to do it for the game to run properly.","Il semblerait que vous n'avez pas renseigné votre nom. Il est nécessaire de le faire pour le bon déroulement du jeu.");_("fr","Hello! To begin, enter your name. Your adversaries will see this name when you play with them.","Bonjour ! Pour commencer, saisissez votre nom. Vos adversaires verront ce nom quand vous jouerez avec eux.");_("fr","Double\nLetter","Lettre\nDouble");_("fr","Double\nWord","Mot\nDouble");_("fr","Triple\nLetter","Lettre\nTriple");_("fr","Triple\nWord","Mot\nTriple");_("fr","Join adversaries","Rejoindre des adversaires");_("fr","You are:","Vous êtes :");_("fr","Change","Changer");_("fr","(name to give)","(nom à renseigner)");_("fr","Put back all the tiles\nin the bag","Remettre toutes les pièces\ndans le sac");_("fr","Number of your game:","Numéro de votre partie :");_("fr","(pending)","(en attente)");_("fr","Write a message to your adversaries here","Écrivez un message à vos adversaires ici");_("fr","Send message","Envoyer le message");_("fr","Sound of the tiles","Bruit des pièces");_("fr","Sound of messages","Bruit des messages");_("fr","Number of tiles in the bag:","Nombre de pièces dans le sac :");_("fr","Click on it to take one.","Cliquez dessus pour en prendre une.");_("fr","Put back all the tiles in the bag","Ranger toutes les pièces dans le sac");_("fr","Participant","Participant·e");_("fr","Rack","Chevalet");_("fr","Score","Score");_("fr","Waiting for other participants…","En attente des autres participants…");_("fr","Click on someone's score\nto change it.","Cliquez sur le score de quelqu'un\npour le changer.");_("fr","Put back all the tiles of\nyour rack in the bag","Remettre toutes les pièces du\nchevalet dans le sac");_("fr","Interface language:","Langue de l'interface :");_("fr","Sorry to disturb you, but we need to reload the game so you can continue playing. You will not lose anything except the messages in the chat. Are you ready?","Désolé pour le dérangement, mais nous avons besoin de recharger le jeu pour que vous poussiez continuer à jouer. Vous ne perdrez rien sauf les messages dans le chat. Êtes-vous prêt·e ?");_("fr","To continue playing, click here","Pour continuer à jouer, cliquez ici");_("fr","{0}: ","{0} : ");_("fr","Whoops, there is a problem. We are trying to fix it as soon as possible. Please wait a few seconds. If it persists, please contact the person who is able to fix the problem.","Oups, il y a un problème. Nous essayons de corriger cela le plus vite possible. Merci de bien vouloir patienter quelques secondes. Si cela persiste, n’hésitez pas à contacter la personne qui peut s’occuper de ce problème");_("fr","The problem is solved, sorry for the inconvenience!","Le problème est résolu. Désolé pour la gêne occasionnée !");if(trivabble.applyL10n){trivabble.applyL10n();}})();
\ No newline at end of file
+(function(){var _=myAlert.l10n;_("fr","OK","OK");_("fr","Yes","Oui");_("fr","No","Non");_("fr","Cancel","Annuler");_=trivabble.l10n;_("fr","Your game is not over. Are you sure you want to leave now?","Votre partie n'est pas terminée. Êtes-vous sûr·e de vouloir la quitter ?");_("fr","You are about to leave the current game. To recover it, please note its number: {0}","Vous allez quitter la partie en cours. Pour la retrouver, notez bien son numéro : {0}");_("fr","Sorry, a problem just happened. The page must be reloaded. If the problem is not too serious, you should be able to keep playing normally. Otherwise, contact the person who is able to fix the problem. Click on “Yes” to reload the page.","Désolé, une erreur s'est produite. La page doit être chargée de nouveau et si le problème n'est pas très grave, vous devriez pouvoir continuer à jouer normalement. Sinon, contactez la personne qui devrait pouvoir régler le problème. Cliquez sur « Oui » pour recharger la page.");_("fr","To join a game, please give the number which is displayed on your adversary(ies)' screen.\nIf you do not know it, ask them.\n\nWarning: your adversary must not take your number, (s)he must keep his/her own. If you whish to recover your current game, please not the following number: {0}.","Pour rejoindre une partie, donnez le numéro qui s'affiche chez votre (vos) adversaire(s).\nSi vous ne le connaissez pas, demandez-lui (leur).\n\nAttention : votre adversaire ne doit pas prendre votre numéro, il doit garder le sien. Si vous souhaitez retrouver votre partie actuelle plus tard, gardez le numéro suivant : {0}.");_("fr","It seems your did not give a correct number, or you clicked on “Cancel”. As a result, the current game continues, if any. To join a game, click on “Join a game” again.","Il semblerait que vous n'avez pas donné un numéro correct, ou que vous avez cliqué sur « Annuler ». Par conséquent, la partie en cours continue, s'il y a une partie en cours. Pour rejoindre une partie, cliquez à nouveau sur « Rejoindre une partie ».");_("fr","To change your name, enter a new one. You can keep using your current name by cancelling. Please note that if you change your name and you have games in progress, you will not be able to keep playing them anymore unless you get back to your current name.","Pour changer de nom, saisissez-en un nouveau. Vous pouvez continuer à utiliser votre ancien nom en annulant. Veuillez noter que si vous changez de nom et que vous avez des parties en cours, vous ne pourrez plus jouer à celles-ci, sauf si vous reprenez votre ancien nom.");_("fr","You cannot take another tile: your rack is full.","Vous ne pouvez pas tirer de pièce : votre chevalet est complet.");_("fr","Are you sure you want to put all the tiles back in the bag (in order to play another game)?","Êtes-vous sûr·e de vouloir remettre toutes les lettres du jeu dans le sac (en vue de jouer une nouvelle partie) ?");_("fr","Are you sure you want to put all your tiles back in the bag?","Êtes-vous sûr·e de vouloir remettre toutes vos lettres dans le sac ?");_("fr","It seems your did not give your name. You need to do it for the game to run properly.","Il semblerait que vous n'avez pas renseigné votre nom. Il est nécessaire de le faire pour le bon déroulement du jeu.");_("fr","Hello! To begin, enter your name. Your adversaries will see this name when you play with them.","Bonjour ! Pour commencer, saisissez votre nom. Vos adversaires verront ce nom quand vous jouerez avec eux.");_("fr","Double\nLetter","Lettre\nDouble");_("fr","Double\nWord","Mot\nDouble");_("fr","Triple\nLetter","Lettre\nTriple");_("fr","Triple\nWord","Mot\nTriple");_("fr","Join adversaries","Rejoindre des adversaires");_("fr","You are:","Vous êtes :");_("fr","Change","Changer");_("fr","(name to give)","(nom à renseigner)");_("fr","Put back all the tiles\nin the bag","Remettre toutes les pièces\ndans le sac");_("fr","Number of your game:","Numéro de votre partie :");_("fr","(pending)","(en attente)");_("fr","Write a message to your adversaries here","Écrivez un message à vos adversaires ici");_("fr","Send message","Envoyer le message");_("fr","Sound of the tiles","Bruit des pièces");_("fr","Sound of messages","Bruit des messages");_("fr","Number of tiles in the bag:","Nombre de pièces dans le sac :");_("fr","Click on it to take one.","Cliquez dessus pour en prendre une.");_("fr","Put back all the tiles in the bag","Ranger toutes les pièces dans le sac");_("fr","Participant","Participant·e");_("fr","Rack","Chevalet");_("fr","Score","Score");_("fr","Waiting for other participants…","En attente des autres participants…");_("fr","Click on someone's score\nto change it.","Cliquez sur le score de quelqu'un\npour le changer.");_("fr","Put back all the tiles of\nyour rack in the bag","Remettre toutes les pièces du\nchevalet dans le sac");_("fr","Interface language:","Langue de l'interface :");_("fr","Sorry to disturb you, but we need to reload the game so you can continue playing. You will not lose anything except the messages in the chat. Are you ready?","Désolé pour le dérangement, mais nous avons besoin de recharger le jeu pour que vous poussiez continuer à jouer. Vous ne perdrez rien sauf les messages dans le chat. Êtes-vous prêt·e ?");_("fr","To continue playing, click here","Pour continuer à jouer, cliquez ici");_("fr","{0}: ","{0} : ");_("fr","Whoops, there is a problem. We are trying to fix it as soon as possible. Please wait a few seconds. If it persists, please contact the person who is able to fix the problem.","Oups, il y a un problème. Nous essayons de corriger cela le plus vite possible. Merci de bien vouloir patienter quelques secondes. Si cela persiste, n’hésitez pas à contacter la personne qui peut s’occuper de ce problème");_("fr","The problem is solved, sorry for the inconvenience!","Le problème est résolu. Désolé pour la gêne occasionnée !");_("fr","It seems your browser went offline. Please reconnect to continue playing.","On dirait que votre navigateur est hors ligne. Merci de bien vouloir vous reconnecter pour continuer à jouer.");if(trivabble.applyL10n){trivabble.applyL10n();}})();
\ No newline at end of file
index dcfd920f6cd77eb4244597345dba3869bdc02176..5c1820ecce62939de5b702da6373294f2fcbc18d 100644 (file)
@@ -123,3 +123,6 @@ msgstr "Oups, il y a un problème. Nous essayons de corriger cela le plus vite p
 
 msgid "The problem is solved, sorry for the inconvenience!"
 msgstr "Le problème est résolu. Désolé pour la gêne occasionnée !"
+
+msgid "It seems your browser went offline. Please reconnect to continue playing."
+msgstr "On dirait que votre navigateur est hors ligne. Merci de bien vouloir vous reconnecter pour continuer à jouer."
index 17382de94e5fbcb9a1b158cbe38fbba715c2407d..a97a10373e3b87737dd951ea5835694e0bc62150 100644 (file)
@@ -129,3 +129,6 @@ msgstr ""
 
 msgid "The problem is solved, sorry for the inconvenience!"
 msgstr ""
+
+msgid  "It seems your browser went offline. Please reconnect to continue playing."
+msgstr ""
index 6bd993245144f880cf4ac719055775b32c6b10c9..bc28e16121d49bbd9ee96d647d824d9ec5e33f9d 100644 (file)
 (function () {
     "use strict";
 
-    const VERSION = 202004041733;
+    const VERSION = 202004121400;
     const POLLING_DELAY = 2000;
+    const ENABLE_WEBSOCKETS = false;
+    const ENABLE_EVENT_SOURCE = true;
 
     const _ = (window.libD && libD.l10n) ? libD.l10n() : function (s) {return s;};
 
     let participants;
     let name;
 
-    let lastDate = 0;
-
     let blockMove = 0;
     let needsRestart = false;
     const gameInProgress = false;
 
     let eventSource = null;
-    let blockEventSourceAutoRetry = false;
+    let webSocket = null;
+
     let connectionLostMessage = null;
-    let boundEventShowConnectionLost = false;
+    let boundEventShowConnectionLost = null;
+    let retryPollingTimeout = 0;
+    let pollingServer = false;
+    let connectionStopped = false;
 
     function mouseDown(ele, fun, stop) {
         const meth = stop ? "removeEventListener" : "addEventListener";
         return -1;
     }
 
-    function showConnectionLost() {
+    function showConnectionLost(msg) {
         unbindEventsShowConnectionLost();
 
         if (!connectionLostMessage) {
-            connectionLostMessage = chatMessage("", _("Whoops, there is a problem. We are trying to fix it as soon as possible. Please wait a few seconds. If it persists, please contact the person who is able to fix the problem."));
+            connectionLostMessage = chatMessage("", msg || _("Whoops, there is a problem. We are trying to fix it as soon as possible. Please wait a few seconds. If it persists, please contact the person who is able to fix the problem."));
             connectionLostMessage.classList.add("error");
         }
     }
 
     function unbindEventsShowConnectionLost() {
         if (boundEventShowConnectionLost) {
-            boundEventShowConnectionLost = false;
-            document.body.removeEventListener("mousemove", showConnectionLost);
-            document.body.removeEventListener("mousedown", showConnectionLost);
-            document.body.removeEventListener("touchstart", showConnectionLost);
+            document.body.removeEventListener("mousemove", boundEventShowConnectionLost);
+            document.body.removeEventListener("mousedown", boundEventShowConnectionLost);
+            document.body.removeEventListener("touchstart", boundEventShowConnectionLost);
+            boundEventShowConnectionLost = null;
         }
     }
 
 
     function connectionReady() {
         pollingReady = true;
-        blockEventSourceAutoRetry = false;
+        retriedImmediately = false;
+
         unbindEventsShowConnectionLost();
+
         if (connectionLostMessage) {
             connectionLostMessage.querySelector(".msg-content").textContent = _("The problem is solved, sorry for the inconvenience!");
-            connectionLostMessage.classList.replace("error", "ok");
+            connectionLostMessage.classList.remove("error");
+            connectionLostMessage.classList.add("ok");
 
             const removeMessage = removeElem.bind(null, connectionLostMessage);
             connectionLostMessage.addEventListener("click", removeMessage);
 
             if (newDest && newDest.getElementsByClassName("tile")[0]) {
                 let i = index + 1;
-let tile;
+                let tile;
 
                 while (playerLetters[i]) {
                     tile = playerLetters[i].getElementsByClassName("tile")[0];
@@ -513,7 +520,6 @@ let tile;
         );
     }
 
-    let pollingServer = false;
     let pollingReady = false;
     let retriedImmediately = false;
 
@@ -603,11 +609,7 @@ let tile;
         }
 
         if (data.stopping) {
-            blockEventSourceAutoRetry = true;
-            if (eventSource) {
-                eventSource.close();
-                eventSource = null;
-            }
+            stopConnection();
 
             retryPolling(
                 (typeof data.stopping === "number" && data.stopping > 0)
@@ -770,23 +772,35 @@ let tile;
             return;
         }
 
-        pollingServer = false;
         pollingReady = false;
-        blockEventSourceAutoRetry = false;
 
         if (!boundEventShowConnectionLost) {
-            boundEventShowConnectionLost = true;
+            boundEventShowConnectionLost = showConnectionLost.bind(null, null);
 
-            document.body.addEventListener("mousemove", showConnectionLost);
-            document.body.addEventListener("mousedown", showConnectionLost);
-            document.body.addEventListener("touchstart", showConnectionLost);
+            document.body.addEventListener("mousemove", boundEventShowConnectionLost);
+            document.body.addEventListener("mousedown", boundEventShowConnectionLost);
+            document.body.addEventListener("touchstart", boundEventShowConnectionLost);
+        }
+
+        if (retryPollingTimeout) {
+            return;
         }
 
         if (delay || retriedImmediately) {
-            setTimeout(sendCmds.bind(null), delay || POLLING_DELAY);
+            retryPollingTimeout = setTimeout(
+                function () {
+                    connectionStopped = false;
+                    pollingServer = false;
+                    startConnection();
+                    retryPollingTimeout = 0;
+                },
+                delay || POLLING_DELAY
+            );
         } else {
             retriedImmediately = true;
-            sendCmds();
+            connectionStopped = false;
+            pollingServer = false;
+            startConnection();
         }
     }
 
@@ -798,67 +812,109 @@ let tile;
         }
     }
 
-    function send(data) {
-        if (needsRestart) {
-            return;
+    function stopConnection() {
+        if (eventSource) {
+            eventSource.onerror = null;
+            eventSource.close();
+            eventSource = null;
         }
 
-        if (!pollingReady && (data.cmds && data.cmds.length)) {
-            sendCmds();
-            setTimeout(send.bind(null, data), POLLING_DELAY);
-            return;
+        if (webSocket) {
+            webSocket.onclose = null;
+            webSocket.onerror = null;
+            webSocket.close();
+            webSocket = null;
         }
 
-        let thisRequestIsPolling = false;
-        let currentIndex = 0;
-        let expectedLength = 0;
+        if (retryPollingTimeout) {
+            clearTimeout(retryPollingTimeout);
+            retryPollingTimeout = 0;
+        }
 
-        if (!data.cmds || !data.cmds.length) {
-            if (pollingServer) {
-                return;
-            }
+        connectionStopped = true;
+        pollingServer = false;
+        pollingReady = false;
+    }
 
-            pollingServer = true;
-            data.version = VERSION;
+    let blacklistWebsockets = false;
 
-            if (window.EventSource) {
-                eventSource = new EventSource("/:trivabble/sse/" + JSON.stringify(data));
+    function bindConnectionEvents(connection) {
+        connection.onopen = connectionReady;
 
-                eventSource.onopen = connectionReady;
+        connection.onmessage = function (e) {
+            connectionReady();
+            parseAndHandleReceivedData(e.data);
+        };
 
-                eventSource.onmessage = function (e) {
-                    connectionReady();
-                    parseAndHandleReceivedData(e.data);
-                };
+        connection.onerror = function (e) {
+            console.error("Connection error:", e);
+            if (connection === eventSource) {
+                retryPolling();
+            }
+        };
 
-                eventSource.onerror = function () {
-                    pollingReady = false;
-                    if (needsRestart) {
-                        eventSource.close();
-                    } else if (!blockEventSourceAutoRetry) {
-                        retryPolling();
-                    }
-                };
+        if (connection === webSocket) {
+            connection.onclose = function (e) {
+                webSocket = null;
+                if (e.code === 1002 || e.code === 1003) {
+                    // CLOSE_PROTOCOL_ERROR or CLOSE_UNSUPPORTED
+                    blacklistWebsockets = true;
+                    pollServerWithEventSource();
+                    return;
+                }
 
-                return;
-            }
+                console.log("Websocket connection has been closed", e);
+                retryPolling();
+            };
+        }
+    }
 
-            thisRequestIsPolling = true;
-        } else {
-            blockMove++;
+    function pollServerWithEventSource() {
+        if (ENABLE_EVENT_SOURCE && window.EventSource) {
+            eventSource = new EventSource("/:trivabble/sse/" + JSON.stringify(cmdsWithContext()));
+            bindConnectionEvents(eventSource);
+            return;
         }
 
+        pollServerWithXHR();
+    }
+
+    function pollServerWithWebSocket() {
+        if (ENABLE_WEBSOCKETS && !blacklistWebsockets && window.WebSocket) {
+            webSocket = new WebSocket(
+                (window.location.protocol === "http:" ? "ws://" : "wss://") +
+                window.location.host +
+                "/:trivabble/ws/" +
+                JSON.stringify(cmdsWithContext())
+            );
+
+            bindConnectionEvents(webSocket);
+            return;
+        }
+
+        pollServerWithEventSource();
+    }
+
+    function xhrRequest(data, onreadystatechange) {
         const xhr = new XMLHttpRequest();
 
         xhr.open("POST", "/:trivabble", true);
         xhr.setRequestHeader("Content-Type", "text/plain");
         xhr.send(JSON.stringify(data));
 
-        xhr.onreadystatechange = function () {
-            if (xhr.readyState === 2 && thisRequestIsPolling) {
+        xhr.onreadystatechange = onreadystatechange.bind(null, xhr);
+    }
+
+    function pollServerWithXHR() {
+        let currentIndex = 0;
+        let expectedLength = 0;
+
+        xhrRequest(cmdsWithContext(), function (xhr) {
+            if (xhr.readyState === 2) {
                 connectionReady();
-            } else if (xhr.readyState === 3 && thisRequestIsPolling) {
+            } else if (xhr.readyState === 3) {
                 connectionReady();
+
                 while (true) {
                     if (!expectedLength) {
                         let i = currentIndex;
@@ -892,20 +948,37 @@ let tile;
                             ), e);
                         }
 
-                        retriedImmediately = false;
                         handleReceivedData(msgs);
                     } else {
                         break;
                     }
                 }
             } else if (xhr.readyState === 4) {
-                if (thisRequestIsPolling) {
-                    retryPolling();
-                    return;
-                }
+                startConnection();
+            }
+        });
+    }
+
+    function startConnection() {
+        if (pollingServer || needsRestart || connectionStopped || navigator.onLine === false) {
+            return;
+        }
+
+        pollingServer = true;
+        pollServerWithWebSocket();
+    }
+
+    function sendXHR(cmds) {
+        if (needsRestart) {
+            return;
+        }
+
+        blockMove++;
 
+        xhrRequest(cmdsWithContext(cmds), function (xhr) {
+            if (xhr.readyState === 4) {
                 if (xhr.status === 0 || xhr.status >= 300) {
-                    setTimeout(send.bind(null, data), POLLING_DELAY);
+                    setTimeout(sendCmds.bind(null, cmds), POLLING_DELAY);
                     return;
                 }
 
@@ -913,19 +986,35 @@ let tile;
 
                 blockMove--;
 
-                if (!pollingServer) {
-                    sendCmds();
+                if (!pollingReady) {
+                    startConnection();
                 }
             }
-        };
+        });
     }
 
-    function sendCmds(cmds) {
-        send({
+    function cmdsWithContext(cmds) {
+        return {
             gameNumber: localStorage.trivabbleGameNumber || "",
             playerName: localStorage.trivabblePlayerName,
+            version: VERSION,
             cmds: cmds
-        });
+        };
+    }
+
+    function sendCmds(cmds) {
+        if (webSocket && webSocket.readyState === 1) {
+            webSocket.send(JSON.stringify(cmds));
+            return;
+        }
+
+        if (!pollingReady) {
+            startConnection();
+            setTimeout(sendCmds, POLLING_DELAY, cmds);
+            return;
+        }
+
+        sendXHR(cmds);
     }
 
     function joinGame() {
@@ -979,7 +1068,7 @@ let tile;
         if (number) {
             localStorage.trivabbleGameNumber = number;
         }
-        sendCmds();
+        startConnection();
     }
 
     let audioTileLoaded = false;
@@ -1027,9 +1116,7 @@ let tile;
         myConfirm(
             _("Are you sure you want to put all the tiles back in the bag (in order to play another game)?"),
             function () {
-                sendCmds(
-                    [{cmd: "resetGame"}]
-                );
+                sendCmds([{cmd: "resetGame"}]);
             }
         );
     }
@@ -1061,6 +1148,10 @@ let tile;
         };
 
         btn.onclick = function () {
+            if (!pollingReady) {
+                return;
+            }
+
             loadAudio();
             sendCmds([{
                 cmd: "msg",
@@ -1305,4 +1396,12 @@ let tile;
     trivabble.l10nError = trivabble.run;
 
     window.addEventListener("DOMContentLoaded", initLang);
+
+    window.addEventListener("offline", function () {
+        stopConnection();
+        showConnectionLost(_("It seems your browser went offline. Please reconnect to continue playing."));
+    });
+
+    window.addEventListener("online", startConnection);
+
 }());