Experimental multi-tabs support (TODO: prevent multi-connect)
[vchess.git] / server / sockets.js
1 const url = require('url');
2
3 // Node version in Ubuntu 16.04 does not know about URL class
4 // NOTE: url is already transformed, without ?xxx=yyy... parts
5 function getJsonFromUrl(url)
6 {
7 const query = url.substr(2); //starts with "/?"
8 let result = {};
9 query.split("&").forEach((part) => {
10 const item = part.split("=");
11 result[item[0]] = decodeURIComponent(item[1]);
12 });
13 return result;
14 }
15
16 module.exports = function(wss) {
17 // Associative array page --> sid --> tmpId --> socket
18 // "page" is either "/" for hall or "/game/some_gid" for Game,
19 // tmpId is required if a same user (browser) has different tabs
20 let clients = {};
21 wss.on("connection", (socket, req) => {
22 const query = getJsonFromUrl(req.url);
23 const sid = query["sid"];
24 const tmpId = query["tmpId"];
25 const page = query["page"];
26 const notifyRoom = (page,code,obj={}) => {
27 Object.keys(clients[page]).forEach(k => {
28 Object.keys(clients[page][k]).forEach(x => {
29 if (k == sid && x == tmpId)
30 return;
31 clients[page][k][x].send(JSON.stringify(Object.assign(
32 {code:code, from:sid}, obj)));
33 });
34 });
35 };
36 const deleteConnexion = () => {
37 if (!clients[page] || !clients[page][sid] || !clients[page][sid][tmpId])
38 return; //job already done
39 delete clients[page][sid][tmpId];
40 if (Object.keys(clients[page][sid]).length == 0)
41 {
42 delete clients[page][sid];
43 if (Object.keys(clients[page]) == 0)
44 delete clients[page];
45 }
46 };
47 const messageListener = (objtxt) => {
48 let obj = JSON.parse(objtxt);
49 if (!!obj.target)
50 {
51 // Check if receiver is connected, because there may be some lag
52 // between a client disconnects and another notice.
53 if (Array.isArray(obj.target))
54 {
55 if (!clients[page][obj.target[0]] ||
56 !clients[page][obj.target[0]][obj.target[1]])
57 {
58 return;
59 }
60 }
61 else if (!clients[page][obj.target])
62 return;
63 }
64 switch (obj.code)
65 {
66 // Wait for "connect" message to notify connection to the room,
67 // because if game loading is slow the message listener might
68 // not be ready too early.
69 case "connect":
70 {
71 notifyRoom(page, "connect");
72 if (page.indexOf("/game/") >= 0)
73 notifyRoom("/", "gconnect", {page:page});
74 break;
75 }
76 case "disconnect":
77 // When page changes:
78 deleteConnexion();
79 if (!clients[page][sid])
80 {
81 // I effectively disconnected from this page:
82 notifyRoom(page, "disconnect");
83 if (page.indexOf("/game/") >= 0)
84 notifyRoom("/", "gdisconnect", {page:page});
85 }
86 break;
87 case "pollclients": //from Hall or Game
88 {
89 let sockIds = [];
90 Object.keys(clients[page]).forEach(k => {
91 // Poll myself if I'm on at least another tab (same page)
92 if (k != sid || Object.keys(clients["/"][k]).length >= 2)
93 sockIds.push(k);
94 });
95 socket.send(JSON.stringify({code:"pollclients", sockIds:sockIds}));
96 break;
97 }
98 case "pollclientsandgamers": //from Hall
99 {
100 let sockIds = [];
101 Object.keys(clients["/"]).forEach(k => {
102 // Poll myself if I'm on at least another tab (same page)
103 if (k != sid || Object.keys(clients["/"][k]).length >= 2)
104 sockIds.push({sid:k});
105 });
106 // NOTE: a "gamer" could also just be an observer
107 Object.keys(clients).forEach(p => {
108 if (p != "/")
109 {
110 Object.keys(clients[p]).forEach(k => {
111 if (k != sid)
112 sockIds.push({sid:k, page:p}); //page needed for gamers
113 });
114 }
115 });
116 socket.send(JSON.stringify({code:"pollclientsandgamers", sockIds:sockIds}));
117 break;
118 }
119
120 // Asking something: from is fully identified,
121 // but the requested resource can be from any tmpId (except current!)
122 case "askidentity":
123 case "asklastate":
124 case "askchallenge":
125 case "askgame":
126 case "askfullgame":
127 {
128 const tmpIds = Object.keys(clients[page][obj.target]);
129 if (obj.target == sid) //targetting myself
130 {
131 const idx_myTmpid = tmpIds.findIndex(x => x == tmpId);
132 if (idx_myTmpid >= 0)
133 tmpIds.splice(idx_myTmpid, 1);
134 }
135 const tmpId_idx = Math.floor(Math.random() * tmpIds.length);
136 clients[page][obj.target][tmpIds[tmpId_idx]].send(
137 JSON.stringify({code:obj.code, from:[sid,tmpId]}));
138 break;
139 }
140
141 // Some Hall events: target all tmpId's (except mine),
142 case "refusechallenge":
143 case "startgame":
144 Object.keys(clients[page][obj.target]).forEach(x => {
145 if (obj.target != sid || x != tmpId)
146 {
147 clients[page][obj.target][x].send(JSON.stringify(
148 {code:obj.code, data:obj.data}));
149 }
150 });
151 break;
152
153 // Notify all room: mostly game events
154 case "newchat":
155 case "newchallenge":
156 case "newgame":
157 case "deletechallenge":
158 case "newmove":
159 case "resign":
160 case "abort":
161 case "drawoffer":
162 case "draw":
163 notifyRoom(page, obj.code, {data:obj.data});
164 break;
165
166 // Passing, relaying something: from isn't needed,
167 // but target is fully identified (sid + tmpId)
168 case "challenge":
169 case "fullgame":
170 case "game":
171 case "identity":
172 case "lastate":
173 clients[page][obj.target[0]][obj.target[1]].send(JSON.stringify(
174 {code:obj.code, data:obj.data}));
175 break;
176 }
177 };
178 const closeListener = () => {
179 // For tab or browser closing:
180 deleteConnexion();
181 };
182 // Update clients object: add new connexion
183 if (!clients[page])
184 clients[page] = {[sid]: {[tmpId]: socket}};
185 else if (!clients[page][sid])
186 clients[page][sid] = {[tmpId]: socket};
187 else
188 clients[page][sid][tmpId] = socket;
189 socket.on("message", messageListener);
190 socket.on("close", closeListener);
191 });
192 }