On the way to multi-tabs support
[vchess.git] / server / sockets.js
index 3b16653..11f7d8e 100644 (file)
@@ -1,6 +1,7 @@
 const url = require('url');
 
 // Node version in Ubuntu 16.04 does not know about URL class
+// NOTE: url is already transformed, without ?xxx=yyy... parts
 function getJsonFromUrl(url)
 {
   const query = url.substr(2); //starts with "/?"
@@ -13,14 +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"];
-    // TODO: later, allow duplicate connections (shouldn't be much more complicated)
-    if (!!clients[sid])
-      return 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)
@@ -28,54 +29,136 @@ 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)));
         }
       });
     };
-    notifyRoom(query["page"], "connect"); //Hall or Game
-    if (query["page"].indexOf("/game/") >= 0)
-      notifyRoom("/", "connect"); //notify main hall
-    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][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 ||
-              // Consider that people playing are in Hall too:
-              (curPage == "/" && clients[k].page.indexOf("/game/") >= 0))
-            )}));
-          break;
-        case "pagechange":
-          notifyRoom(clients[sid].page, "disconnect");
-          if (clients[sid].page.indexOf("/game/") >= 0)
-            notifyRoom("/", "disconnect");
-          clients[sid].page = obj.page;
-          notifyRoom(obj.page, "connect");
-          if (obj.page.indexOf("/game/") >= 0)
-            notifyRoom("/", "connect");
+        {
+          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":
+        {
+          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[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
+          // 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)
             {
-              clients[k].sock.send(JSON.stringify(
-                {code:"askgame", from: sid}));
+              const gid = clients[k][x].page.match(regexpGid)[0];
+              if (!gameSids[gid])
+                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
+                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 sidIdx = L > 1
+              ? Math.floor(Math.random() * Math.floor(L))
+              : 0;
+            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}));
+          break;
+        case "fullgame":
+          clients[obj.target].sock.send(JSON.stringify(
+            {code:"fullgame", game:obj.game}));
           break;
         case "identity":
           clients[obj.target].sock.send(JSON.stringify(
@@ -110,11 +193,10 @@ module.exports = function(wss) {
           }
           break;
         case "newchat":
-          // WARNING: do not use query["page"], because the page may change
-          notifyRoom(clients[sid].page, "newchat",
-            {msg: obj.msg, name: obj.name});
+          notifyRoom(clients[sid].page, "newchat", {chat:obj.chat});
           break;
         // TODO: WebRTC instead in this case (most demanding?)
+        // --> 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}));
@@ -125,11 +207,11 @@ module.exports = function(wss) {
           break;
         case "resign":
           clients[obj.target].sock.send(JSON.stringify(
-            {code:"resign"}));
+            {code:"resign", side:obj.side}));
           break;
         case "abort":
           clients[obj.target].sock.send(JSON.stringify(
-            {code:"abort",msg:obj.msg}));
+            {code:"abort"}));
           break;
         case "drawoffer":
           clients[obj.target].sock.send(JSON.stringify(
@@ -137,16 +219,21 @@ module.exports = function(wss) {
           break;
         case "draw":
           clients[obj.target].sock.send(JSON.stringify(
-            {code:"draw"}));
+            {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("/", "disconnect"); //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);
   });
 }