On the way to multi-tabs support
[vchess.git] / server / sockets.js
index fcaab83..11f7d8e 100644 (file)
@@ -14,20 +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);
     const sid = query["sid"];
-    if (!!clients[sid])
-    {
-      // Dummy messages listener: just send "duplicate" event on anything
-      // ('connect' events for Hall and Game, 'askfullgame' for observers)
-      return socket.on("message", objtxt => {
-        if (["connect","askfullgame"].includes(JSON.parse(objtxt).code))
-          socket.send(JSON.stringify({code:"duplicate"}));
-      });
-    }
-    clients[sid] = {sock: socket, page: query["page"]};
+    const tmpId = query["tmpId"];
     const notifyRoom = (page,code,obj={},excluded=[]) => {
       Object.keys(clients).forEach(k => {
         if (k in excluded)
@@ -35,94 +29,129 @@ 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)));
         }
       });
     };
-    // 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.
-    socket.on("message", objtxt => {
+    const messageListener = (objtxt) => {
       let obj = JSON.parse(objtxt);
       if (!!obj.target && !clients[obj.target])
         return; //receiver not connected, nothing we can do
       switch (obj.code)
       {
+        // 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;
         }
+        case "askgame":
+          clients[obj.target].sock.send(JSON.stringify(
+            {code:"askgame", from:sid}));
+          break;
         case "askfullgame":
           clients[obj.target].sock.send(JSON.stringify(
             {code:"askfullgame", from:sid}));
@@ -167,7 +196,7 @@ module.exports = function(wss) {
           notifyRoom(clients[sid].page, "newchat", {chat:obj.chat});
           break;
         // TODO: WebRTC instead in this case (most demanding?)
-        // --> At least do a "notifyRoom"
+        // --> Or else: at least do a "notifyRoom" (also for draw, resign...)
         case "newmove":
           clients[obj.target].sock.send(JSON.stringify(
             {code:"newmove", move:obj.move}));
@@ -193,13 +222,18 @@ module.exports = function(wss) {
             {code:"draw", message:obj.message}));
           break;
       }
-    });
-    socket.on("close", () => {
-      const page = clients[sid].page;
+    };
+    const closeListener = () => {
       delete clients[sid];
-      notifyRoom(page, "disconnect");
-      if (page.indexOf("/game/") >= 0)
-        notifyRoom("/", "gdisconnect"); //notify main hall
-    });
+    };
+    if (!!clients[sid])
+    {
+      // Turn off old sock through current client:
+      clients[sid].sock.send(JSON.stringify({code:"duplicate"}));
+    }
+    // Potentially replace current connection:
+    clients[sid] = {sock: socket, page: query["page"]};
+    socket.on("message", messageListener);
+    socket.on("close", closeListener);
   });
 }