On the way to multi-tabs support
authorBenjamin Auder <benjamin.auder@somewhere>
Mon, 10 Feb 2020 18:16:41 +0000 (19:16 +0100)
committerBenjamin Auder <benjamin.auder@somewhere>
Mon, 10 Feb 2020 18:16:41 +0000 (19:16 +0100)
client/src/main.js
client/src/router.js
client/src/store.js
client/src/views/Game.vue
client/src/views/Hall.vue
server/sockets.js

index 2d855b8..06b050b 100644 (file)
@@ -22,7 +22,6 @@ new Vue({
         });
       }
     });
-    // NOTE: store.initialize(this.$route.path); doesn't work
-    store.initialize(window.location.href.split("#")[1].split("?")[0]);
+    store.initialize();
   },
 }).$mount("#app");
index daa9619..b2efee7 100644 (file)
@@ -61,14 +61,4 @@ const router = new Router({
   ]
 });
 
-router.beforeEach((to, from, next) => {
-  window.scrollTo(0, 0);
-  if (!!store.state.conn) //uninitialized at first page
-  {
-    // Notify WebSockets server (TODO: path or fullPath?)
-    store.state.conn.send(JSON.stringify({code: "pagechange", page: to.path}));
-  }
-  next();
-});
-
 export default router;
index a1e432f..bcee338 100644 (file)
@@ -1,6 +1,5 @@
 import { ajax } from "./utils/ajax";
 import { getRandString } from "./utils/alea";
-import params from "./parameters"; //for socket connection
 
 // Global store: see https://medium.com/fullstackio/managing-state-in-vue-js-23a0352b1c87
 export const store =
@@ -9,12 +8,11 @@ export const store =
     variants: [],
     tr: {},
     user: {},
-    conn: null,
     settings: {},
     lang: "",
   },
   socketCloseListener: null,
-  initialize(page) {
+  initialize() {
     ajax("/variants", "GET", res => { this.state.variants = res.variantArray; });
     let mysid = localStorage["mysid"];
     if (!mysid)
@@ -38,15 +36,6 @@ export const store =
       this.state.user.email = res.email;
       this.state.user.notify = res.notify;
     });
-    const supportedLangs = ["en","es","fr"];
-    this.state.lang = localStorage["lang"] ||
-      (supportedLangs.includes(navigator.language)
-        ? navigator.language
-        : "en");
-    this.setTranslations();
-    // Initialize connection (even if the current page doesn't need it)
-    this.state.conn = new WebSocket(params.socketUrl + "/?sid=" + mysid +
-      "&page=" + encodeURIComponent(page));
     // Settings initialized with values from localStorage
     this.state.settings = {
       bcolor: localStorage.getItem("bcolor") || "lichess",
@@ -54,12 +43,12 @@ export const store =
       hints: localStorage.getItem("hints") == "true",
       highlight: localStorage.getItem("highlight") == "true",
     };
-    this.socketCloseListener = () => {
-      // Next line may fail at first, but should retry and eventually success (TODO?)
-      this.state.conn = new WebSocket(params.socketUrl + "/?sid=" + mysid +
-        "&page=" + encodeURIComponent(page));
-    };
-    this.state.conn.onclose = this.socketCloseListener;
+    const supportedLangs = ["en","es","fr"];
+    this.state.lang = localStorage["lang"] ||
+      (supportedLangs.includes(navigator.language)
+        ? navigator.language
+        : "en");
+    this.setTranslations();
   },
   updateSetting: function(propName, value) {
     this.state.settings[propName] = value;
index 022e3cb..177f9eb 100644 (file)
@@ -44,6 +44,7 @@ import { extractTime } from "@/utils/timeControl";
 import { ArrayFun } from "@/utils/array";
 import { processModalClick } from "@/utils/modalClick";
 import { getScoreMessage } from "@/utils/scoring";
+import params from "@/parameters";
 
 export default {
   name: 'my-game',
@@ -70,6 +71,9 @@ export default {
       lastate: undefined, //used if opponent send lastate before game is ready
       repeat: {}, //detect position repetition
       newChat: "",
+      conn: null,
+      page: "",
+      tempId: "", //to distinguish several tabs
     };
   },
   watch: {
@@ -114,20 +118,26 @@ export default {
     this.$set(this.people, my.sid, {id:my.id, name:my.name});
     this.gameRef.id = this.$route.params["id"];
     this.gameRef.rid = this.$route.query["rid"]; //may be undefined
-    // Define socket .onmessage() and .onclose() events:
-    this.st.conn.onmessage = this.socketMessageListener;
+    // Initialize connection
+    this.page = this.$route.path;
+    const connexionString = params.socketUrl +
+      "/?sid=" + this.st.user.sid +
+      "&tmpId=" + this.tempId +
+      "&page=" + encodeURIComponent(this.page);
+    this.conn = new WebSocket(connexionString);
+    this.conn.onmessage = this.socketMessageListener;
     const socketCloseListener = () => {
-      store.socketCloseListener(); //reinitialize connexion (in store.js)
-      this.st.conn.addEventListener('message', this.socketMessageListener);
-      this.st.conn.addEventListener('close', socketCloseListener);
+      this.conn = new WebSocket(connexionString);
+      this.conn.addEventListener('message', this.socketMessageListener);
+      this.conn.addEventListener('close', socketCloseListener);
     };
-    this.st.conn.onclose = socketCloseListener;
+    this.conn.onclose = socketCloseListener;
     // Socket init required before loading remote game:
     const socketInit = (callback) => {
-      if (!!this.st.conn && this.st.conn.readyState == 1) //1 == OPEN state
+      if (!!this.conn && this.conn.readyState == 1) //1 == OPEN state
         callback();
       else //socket not ready yet (initial loading)
-        this.st.conn.onopen = callback;
+        this.conn.onopen = callback;
     };
     if (!this.gameRef.rid) //game stored locally or on server
       this.loadGame(null, () => socketInit(this.roomInit));
@@ -143,13 +153,16 @@ export default {
     document.getElementById("chatWrap").addEventListener(
       "click", processModalClick);
   },
+  beforeDestroy: function() {
+    this.conn.send(JSON.stringify({code:"disconnect",page:this.page}));
+  },
   methods: {
     // O.1] Ask server for room composition:
     roomInit: function() {
       // Notify the room only now that I connected, because
       // messages might be lost otherwise (if game loading is slow)
-      this.st.conn.send(JSON.stringify({code:"connect"}));
-      this.st.conn.send(JSON.stringify({code:"pollclients"}));
+      this.conn.send(JSON.stringify({code:"connect"}));
+      this.conn.send(JSON.stringify({code:"pollclients"}));
     },
     isConnected: function(index) {
       const player = this.game.players[index];
@@ -165,7 +178,7 @@ export default {
       switch (data.code)
       {
         case "duplicate":
-          this.st.conn.send(JSON.stringify({code:"duplicate",
+          this.conn.send(JSON.stringify({code:"duplicate",
             page:"/game/" + this.game.id}));
           alert(this.st.tr["This tab is now offline"]);
           break;
@@ -176,14 +189,14 @@ export default {
               return;
             this.$set(this.people, sid, {id:0, name:""});
             // Ask only identity
-            this.st.conn.send(JSON.stringify({code:"askidentity", target:sid}));
+            this.conn.send(JSON.stringify({code:"askidentity", target:sid}));
           });
           break;
         case "askidentity":
           // Request for identification: reply if I'm not anonymous
           if (this.st.user.id > 0)
           {
-            this.st.conn.send(JSON.stringify({code:"identity",
+            this.conn.send(JSON.stringify({code:"identity",
               user: {
                 // NOTE: decompose to avoid revealing email
                 name: this.st.user.name,
@@ -201,7 +214,7 @@ export default {
             && this.game.type == "live" && this.game.score == "*"
             && this.game.players.some(p => p.sid == data.user.sid))
           {
-            this.st.conn.send(JSON.stringify({code:"asklastate", target:data.user.sid}));
+            this.conn.send(JSON.stringify({code:"asklastate", target:data.user.sid}));
           }
           break;
         case "asklastate":
@@ -212,7 +225,7 @@ export default {
             // Send our "last state" informations to opponent
             const L = this.game.moves.length;
             const myIdx = ["w","b"].indexOf(this.game.mycolor);
-            this.st.conn.send(JSON.stringify({
+            this.conn.send(JSON.stringify({
               code: "lastate",
               target: data.from,
               state:
@@ -245,7 +258,7 @@ export default {
               timeControl: this.game.timeControl,
               score: this.game.score,
             };
-            this.st.conn.send(JSON.stringify({code:"game",
+            this.conn.send(JSON.stringify({code:"game",
               game:myGame, target:data.from}));
           }
           break;
@@ -284,7 +297,7 @@ export default {
           this.drawOffer = "received";
           break;
         case "askfullgame":
-          this.st.conn.send(JSON.stringify({code:"fullgame",
+          this.conn.send(JSON.stringify({code:"fullgame",
             game:this.game, target:data.from}));
           break;
         case "fullgame":
@@ -293,7 +306,7 @@ export default {
           break;
         case "connect":
           this.$set(this.people, data.from, {name:"", id:0});
-          this.st.conn.send(JSON.stringify({code:"askidentity", target:data.from}));
+          this.conn.send(JSON.stringify({code:"askidentity", target:data.from}));
           break;
         case "disconnect":
           this.$delete(this.people, data.from);
@@ -332,7 +345,7 @@ export default {
         Object.keys(this.people).forEach(sid => {
           if (sid != this.st.user.sid)
           {
-            this.st.conn.send(JSON.stringify({code:"draw",
+            this.conn.send(JSON.stringify({code:"draw",
               message:message, target:sid}));
           }
         });
@@ -347,7 +360,7 @@ export default {
         this.drawOffer = "sent";
         Object.keys(this.people).forEach(sid => {
           if (sid != this.st.user.sid)
-            this.st.conn.send(JSON.stringify({code:"drawoffer", target:sid}));
+            this.conn.send(JSON.stringify({code:"drawoffer", target:sid}));
         });
         GameStorage.update(this.gameRef.id, {drawOffer: this.game.mycolor});
       }
@@ -359,7 +372,7 @@ export default {
       Object.keys(this.people).forEach(sid => {
         if (sid != this.st.user.sid)
         {
-          this.st.conn.send(JSON.stringify({
+          this.conn.send(JSON.stringify({
             code: "abort",
             target: sid,
           }));
@@ -372,7 +385,7 @@ export default {
       Object.keys(this.people).forEach(sid => {
         if (sid != this.st.user.sid)
         {
-          this.st.conn.send(JSON.stringify({code:"resign",
+          this.conn.send(JSON.stringify({code:"resign",
             side:this.game.mycolor, target:sid}));
         }
       });
@@ -514,7 +527,7 @@ export default {
       if (!!this.gameRef.rid)
       {
         // Remote live game: forgetting about callback func... (TODO: design)
-        this.st.conn.send(JSON.stringify(
+        this.conn.send(JSON.stringify(
           {code:"askfullgame", target:this.gameRef.rid}));
       }
       else
@@ -561,7 +574,7 @@ export default {
         Object.keys(this.people).forEach(sid => {
           if (sid != this.st.user.sid)
           {
-            this.st.conn.send(JSON.stringify({
+            this.conn.send(JSON.stringify({
               code: "newmove",
               target: sid,
               move: sendMove,
@@ -641,7 +654,7 @@ export default {
       document.getElementById("chatBtn").style.backgroundColor = "#e2e2e2";
     },
     processChat: function(chat) {
-      this.st.conn.send(JSON.stringify({code:"newchat", chat:chat}));
+      this.conn.send(JSON.stringify({code:"newchat", chat:chat}));
       // NOTE: anonymous chats in corr games are not stored on server (TODO?)
       if (this.game.type == "corr" && this.st.user.id > 0)
         GameStorage.update(this.gameRef.id, {chat: chat});
index 2395f60..e53dd56 100644 (file)
@@ -78,6 +78,7 @@ import { store } from "@/store";
 import { checkChallenge } from "@/data/challengeCheck";
 import { ArrayFun } from "@/utils/array";
 import { ajax } from "@/utils/ajax";
+import params from "@/parameters";
 import { getRandString, shuffle } from "@/utils/alea";
 import Chat from "@/components/Chat.vue";
 import GameList from "@/components/GameList.vue";
@@ -109,6 +110,9 @@ export default {
         timeControl: localStorage.getItem("timeControl") || "",
       },
       newChat: "",
+      conn: null,
+      page: "",
+      tempId: "", //to distinguish several tabs
     };
   },
   watch: {
@@ -135,7 +139,8 @@ export default {
   created: function() {
     // Always add myself to players' list
     const my = this.st.user;
-    this.$set(this.people, my.sid, {id:my.id, name:my.name});
+    this.tempId = getRandString();
+    this.$set(this.people, my.sid, {id:my.id, name:my.name, tmpId: [this.tempId]});
     // Ask server for current corr games (all but mines)
     ajax(
       "/games",
@@ -199,23 +204,25 @@ export default {
     );
     // 0.1] Ask server for room composition:
     const funcPollClients = () => {
-      // Same strategy as in Game.vue: send connection
-      // after we're sure WebSocket is initialized
-      this.st.conn.send(JSON.stringify({code:"connect"}));
-      this.st.conn.send(JSON.stringify({code:"pollclients"}));
-      this.st.conn.send(JSON.stringify({code:"pollgamers"}));
+      this.conn.send(JSON.stringify({code:"connect"}));
+      this.conn.send(JSON.stringify({code:"pollclients"}));
+      this.conn.send(JSON.stringify({code:"pollgamers"}));
     };
-    if (!!this.st.conn && this.st.conn.readyState == 1) //1 == OPEN state
-      funcPollClients();
-    else //socket not ready yet (initial loading)
-      this.st.conn.onopen = funcPollClients;
-    this.st.conn.onmessage = this.socketMessageListener;
+    // Initialize connection
+    this.page = this.$route.path;
+    const connexionString = params.socketUrl +
+      "/?sid=" + this.st.user.sid +
+      "&tmpId=" + this.tempId +
+      "&page=" + encodeURIComponent(this.page);
+    this.conn = new WebSocket(connexionString);
+    this.conn.onopen = funcPollClients;
+    this.conn.onmessage = this.socketMessageListener;
     const socketCloseListener = () => {
-      store.socketCloseListener(); //reinitialize connexion (in store.js)
-      this.st.conn.addEventListener('message', this.socketMessageListener);
-      this.st.conn.addEventListener('close', socketCloseListener);
+      this.conn = new WebSocket(connexionString);
+      this.conn.addEventListener('message', this.socketMessageListener);
+      this.conn.addEventListener('close', socketCloseListener);
     };
-    this.st.conn.onclose = socketCloseListener;
+    this.conn.onclose = socketCloseListener;
   },
   mounted: function() {
     [document.getElementById("infoDiv"),document.getElementById("newgameDiv")]
@@ -226,6 +233,9 @@ export default {
       )}
     );
   },
+  beforeDestroy: function() {
+    this.conn.send(JSON.stringify({code:"disconnect",page:this.page}));
+  },
   methods: {
     // Helpers:
     filterChallenges: function(type) {
@@ -260,11 +270,11 @@ export default {
     },
     processChat: function(chat) {
       // When received on server, this will trigger a "notifyRoom"
-      this.st.conn.send(JSON.stringify({code:"newchat", chat: chat}));
+      this.conn.send(JSON.stringify({code:"newchat", chat: chat}));
     },
     sendSomethingTo: function(to, code, obj, warnDisconnected) {
       const doSend = (code, obj, sid) => {
-        this.st.conn.send(JSON.stringify(Object.assign(
+        this.conn.send(JSON.stringify(Object.assign(
           {code: code},
           obj,
           {target: sid}
@@ -307,8 +317,8 @@ export default {
       switch (data.code)
       {
         case "duplicate":
-          this.st.conn.send(JSON.stringify({code:"duplicate", page:"/"}));
-          this.st.conn.send = () => {};
+          this.conn.send(JSON.stringify({code:"duplicate", page:"/"}));
+          this.conn.send = () => {};
           alert(this.st.tr["This tab is now offline"]);
           break;
         // 0.2] Receive clients list (just socket IDs)
@@ -316,8 +326,8 @@ export default {
           data.sockIds.forEach(sid => {
             this.$set(this.people, sid, {id:0, name:""});
             // Ask identity and challenges
-            this.st.conn.send(JSON.stringify({code:"askidentity", target:sid}));
-            this.st.conn.send(JSON.stringify({code:"askchallenge", target:sid}));
+            this.conn.send(JSON.stringify({code:"askidentity", target:sid}));
+            this.conn.send(JSON.stringify({code:"askchallenge", target:sid}));
           });
           break;
         case "pollgamers":
@@ -325,16 +335,16 @@ export default {
           // and gamers, but is it necessary?
           data.sockIds.forEach(sid => {
             this.$set(this.people, sid, {id:0, name:"", gamer:true});
-            this.st.conn.send(JSON.stringify({code:"askidentity", target:sid}));
+            this.conn.send(JSON.stringify({code:"askidentity", target:sid}));
           });
           // Also ask current games to all playing peers (TODO: some design issue)
-          this.st.conn.send(JSON.stringify({code:"askgames"}));
+          this.conn.send(JSON.stringify({code:"askgames"}));
           break;
         case "askidentity":
           // Request for identification: reply if I'm not anonymous
           if (this.st.user.id > 0)
           {
-            this.st.conn.send(JSON.stringify({code:"identity",
+            this.conn.send(JSON.stringify({code:"identity",
               user: {
                 // NOTE: decompose to avoid revealing email
                 name: this.st.user.name,
@@ -382,7 +392,7 @@ export default {
               timeControl: c.timeControl,
               added: c.added,
             };
-            this.st.conn.send(JSON.stringify({code:"challenge",
+            this.conn.send(JSON.stringify({code:"challenge",
               chall:myChallenge, target:data.from}));
           }
           break;
@@ -451,11 +461,11 @@ export default {
         case "connect":
         case "gconnect":
           this.$set(this.people, data.from, {name:"", id:0, gamer:data.code[0]=='g'});
-          this.st.conn.send(JSON.stringify({code:"askidentity", target:data.from}));
+          this.conn.send(JSON.stringify({code:"askidentity", target:data.from}));
           if (data.code == "connect")
-            this.st.conn.send(JSON.stringify({code:"askchallenge", target:data.from}));
+            this.conn.send(JSON.stringify({code:"askchallenge", target:data.from}));
           else
-            this.st.conn.send(JSON.stringify({code:"askgame", target:data.from}));
+            this.conn.send(JSON.stringify({code:"askgame", target:data.from}));
           break;
         case "disconnect":
         case "gdisconnect":
@@ -595,7 +605,7 @@ export default {
         }
         else
         {
-          this.st.conn.send(JSON.stringify({
+          this.conn.send(JSON.stringify({
             code: "refusechallenge",
             cid: c.id, target: c.from.sid}));
         }
@@ -639,7 +649,7 @@ export default {
       const tryNotifyOpponent = () => {
         if (!!oppsid) //opponent is online
         {
-          this.st.conn.send(JSON.stringify({code:"newgame",
+          this.conn.send(JSON.stringify({code:"newgame",
             gameInfo:gameInfo, target:oppsid, cid:c.id}));
         }
       };
@@ -666,7 +676,7 @@ export default {
       Object.keys(this.people).forEach(sid => {
         if (![this.st.user.sid,oppsid].includes(sid))
         {
-          this.st.conn.send(JSON.stringify({code:"game",
+          this.conn.send(JSON.stringify({code:"game",
             game: { //minimal game info:
               id: gameInfo.id,
               players: gameInfo.players,
index 64b180d..11f7d8e 100644 (file)
@@ -14,12 +14,14 @@ function getJsonFromUrl(url)
 }
 
 module.exports = function(wss) {
-  let clients = {}; //associative array sid --> socket
+  // Associative array sid --> tmpId --> {socket, page},
+  // "page" is either "/" for hall or "/game/some_gid" for Game,
+  // tmpId is required if a same user (browser) has different tabs
+  let clients = {};
   wss.on("connection", (socket, req) => {
     const query = getJsonFromUrl(req.url);
-    if (query["page"] != "/" && query["page"].indexOf("/game/") < 0)
-      return; //other tabs don't need to be connected
     const sid = query["sid"];
+    const tmpId = query["tmpId"];
     const notifyRoom = (page,code,obj={},excluded=[]) => {
       Object.keys(clients).forEach(k => {
         if (k in excluded)
@@ -27,7 +29,7 @@ module.exports = function(wss) {
         if (k != sid && clients[k].page == page)
         {
           clients[k].sock.send(JSON.stringify(Object.assign(
-            {code:code, from:sid}, obj)));
+            {code:code, from:[sid,tmpId]}, obj)));
         }
       });
     };
@@ -37,93 +39,112 @@ module.exports = function(wss) {
         return; //receiver not connected, nothing we can do
       switch (obj.code)
       {
-        case "duplicate":
-          // Turn off message listening, and send disconnect if needed:
-          socket.removeListener("message", messageListener);
-          socket.removeListener("close", closeListener);
-          // From obj.page to clients[sid].page (TODO: unclear)
-          if (clients[sid].page != obj.page)
-          {
-            notifyRoom(obj.page, "disconnect");
-            if (obj.page.indexOf("/game/") >= 0)
-              notifyRoom("/", "gdisconnect");
-          }
-          break;
         // Wait for "connect" message to notify connection to the room,
         // because if game loading is slow the message listener might
         // not be ready too early.
         case "connect":
         {
-          const curPage = clients[sid].page;
+          const curPage = clients[sid][tmpId].page;
           notifyRoom(curPage, "connect"); //Hall or Game
           if (curPage.indexOf("/game/") >= 0)
             notifyRoom("/", "gconnect"); //notify main hall
           break;
         }
+        case "disconnect":
+        {
+          const oldPage = obj.page;
+          notifyRoom(oldPage, "disconnect"); //Hall or Game
+          if (oldPage.indexOf("/game/") >= 0)
+            notifyRoom("/", "gdisconnect"); //notify main hall
+          break;
+        }
         case "pollclients":
         {
-          const curPage = clients[sid].page;
-          socket.send(JSON.stringify({code:"pollclients",
-            sockIds: Object.keys(clients).filter(k =>
-              k != sid && clients[k].page == curPage
-            )}));
+          const curPage = clients[sid][tmpId].page;
+          let sockIds = {}; //result, object sid ==> [tmpIds]
+          Object.keys(clients).forEach(k => {
+            Object.keys(clients[k]).forEach(x => {
+              if ((k != sid || x != tmpId)
+                && clients[k][x].page == curPage)
+              {
+                if (!sockIds[k])
+                  sockIds[k] = [x];
+                else
+                  sockIds[k].push(x);
+              }
+            });
+          });
+          socket.send(JSON.stringify({code:"pollclients", sockIds:sockIds}));
           break;
         }
         case "pollgamers":
-          socket.send(JSON.stringify({code:"pollgamers",
-            sockIds: Object.keys(clients).filter(k =>
-              k != sid && clients[k].page.indexOf("/game/") >= 0
-            )}));
-          break;
-        case "pagechange":
-          // page change clients[sid].page --> obj.page
-          // TODO: some offline rooms don't need to receive disconnect event
-          notifyRoom(clients[sid].page, "disconnect");
-          if (clients[sid].page.indexOf("/game/") >= 0)
-            notifyRoom("/", "gdisconnect");
-          clients[sid].page = obj.page;
-          // No need to notify connection: it's self-sent in .vue file
-          //notifyRoom(obj.page, "connect");
-          if (obj.page.indexOf("/game/") >= 0)
-            notifyRoom("/", "gconnect");
+        {
+          let sockIds = {};
+          Object.keys(clients).forEach(k => {
+            Object.keys(clients[k]).forEach(x => {
+              if ((k != sid || x != tmpId)
+                && clients[k][x].page.indexOf("/game/") >= 0)
+              {
+                if (!sockIds[k])
+                  sockIds[k] = [x];
+                else
+                  sockIds[k].push(x);
+              }
+            });
+          });
+          socket.send(JSON.stringify({code:"pollgamers", sockIds:sockIds}));
           break;
+        }
         case "askidentity":
-          clients[obj.target].sock.send(JSON.stringify(
-            {code:"askidentity",from:sid}));
+        {
+          // Identity only depends on sid, so select a tmpId at random
+          const tmpIds = Object.keys(clients[obj.target]);
+          const tmpId_idx = Math.floor(Math.random() * tmpIds.length);
+          clients[obj.target][tmpIds[tmpId_idx]].sock.send(JSON.stringify(
+            {code:"askidentity",from:[sid,tmpId]}));
           break;
+        }
         case "asklastate":
-          clients[obj.target].sock.send(JSON.stringify(
-            {code:"asklastate",from:sid}));
+          clients[obj.target[0]][obj.target[1]].sock.send(JSON.stringify(
+            {code:"asklastate",from:[sid,tmpId]}));
           break;
         case "askchallenge":
-          clients[obj.target].sock.send(JSON.stringify(
-            {code:"askchallenge",from:sid}));
+          clients[obj.target[0]][obj.target[1]].sock.send(JSON.stringify(
+            {code:"askchallenge",from:[sid,tmpId]}));
           break;
         case "askgames":
         {
           // Check all clients playing, and send them a "askgame" message
-          let gameSids = {}; //game ID --> [sid1, sid2]
+          // game ID --> [ sid1 --> array of tmpIds, sid2 --> array of tmpIds]
+          let gameSids = {};
           const regexpGid = /\/[a-zA-Z0-9]+$/;
           Object.keys(clients).forEach(k => {
-            if (k != sid && clients[k].page.indexOf("/game/") >= 0)
+            Object.keys(clients[k]).forEach(x => {
+              if ((k != sid || x != tmpId)
+                && clients[k][x].page.indexOf("/game/") >= 0)
             {
-              const gid = clients[k].page.match(regexpGid)[0];
+              const gid = clients[k][x].page.match(regexpGid)[0];
               if (!gameSids[gid])
-                gameSids[gid] = [k];
+                gameSids[gid] = [{k: [x]}];
+              else if (k == Object.keys(gameSids[gid][0])[0])
+                gameSids[gid][0][k].push(x);
+              else if (gameSids[gid].length == 1)
+                gameSids[gid].push({k: [x]});
               else
-                gameSids[gid].push(k);
+                Object.values(gameSids[gid][1]).push(x);
             }
           });
           // Request only one client out of 2 (TODO: this is a bit heavy)
           // Alt: ask game to all, and filter later?
           Object.keys(gameSids).forEach(gid => {
             const L = gameSids[gid].length;
-            const idx = L > 1
+            const sidIdx = L > 1
               ? Math.floor(Math.random() * Math.floor(L))
               : 0;
-            const rid = gameSids[gid][idx];
-            clients[rid].sock.send(JSON.stringify(
-              {code:"askgame", from: sid}));
+            const tmpIdx = Object.values(gameSids[gid][sidIdx]
+            const rid = gameSids[gid][sidIdx][tmpIdx];
+            clients[sidIdx][tmpIdx].sock.send(JSON.stringify(
+              {code:"askgame", from: [sid,tmpId]}));
           });
           break;
         }
@@ -203,11 +224,7 @@ module.exports = function(wss) {
       }
     };
     const closeListener = () => {
-      const page = clients[sid].page;
       delete clients[sid];
-      notifyRoom(page, "disconnect");
-      if (page.indexOf("/game/") >= 0)
-        notifyRoom("/", "gdisconnect"); //notify main hall
     };
     if (!!clients[sid])
     {