Some bugs fixes. TODO: double disconnect when anonymous reloads page in Hall
[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 // Helper to safe-send some message through a (web-)socket:
17 function send(socket, message)
18 {
19 if (!!socket && socket.readyState == 1)
20 socket.send(JSON.stringify(message));
21 }
22
23 module.exports = function(wss) {
24 // Associative array page --> sid --> tmpId --> socket
25 // "page" is either "/" for hall or "/game/some_gid" for Game,
26 // tmpId is required if a same user (browser) has different tabs
27 let clients = {};
28 wss.on("connection", (socket, req) => {
29 const query = getJsonFromUrl(req.url);
30 const sid = query["sid"];
31 const tmpId = query["tmpId"];
32 const page = query["page"];
33 const notifyRoom = (page,code,obj={}) => {
34 if (!clients[page])
35 return;
36 Object.keys(clients[page]).forEach(k => {
37 Object.keys(clients[page][k]).forEach(x => {
38 if (k == sid && x == tmpId)
39 return;
40 send(clients[page][k][x], Object.assign({code:code, from:sid}, obj));
41 });
42 });
43 };
44 const deleteConnexion = () => {
45 if (!clients[page] || !clients[page][sid] || !clients[page][sid][tmpId])
46 return; //job already done
47 delete clients[page][sid][tmpId];
48 if (Object.keys(clients[page][sid]).length == 0)
49 {
50 delete clients[page][sid];
51 if (Object.keys(clients[page]) == 0)
52 delete clients[page];
53 }
54 };
55 const doDisconnect = () => {
56 deleteConnexion();
57 if (!clients[page] || !clients[page][sid])
58 {
59 // I effectively disconnected from this page:
60 notifyRoom(page, "disconnect");
61 if (page.indexOf("/game/") >= 0)
62 notifyRoom("/", "gdisconnect", {page:page});
63 }
64 };
65 const messageListener = (objtxt) => {
66 let obj = JSON.parse(objtxt);
67 switch (obj.code)
68 {
69 // Wait for "connect" message to notify connection to the room,
70 // because if game loading is slow the message listener might
71 // not be ready too early.
72 case "connect":
73 {
74 notifyRoom(page, "connect");
75 if (page.indexOf("/game/") >= 0)
76 notifyRoom("/", "gconnect", {page:page});
77 break;
78 }
79 case "disconnect":
80 // When page changes:
81 doDisconnect();
82 break;
83 case "killme":
84 {
85 // Self multi-connect: manual removal + disconnect
86 const doKill = (pg) => {
87 Object.keys(clients[pg][obj.sid]).forEach(x => {
88 send(clients[pg][obj.sid][x], {code: "killed"});
89 });
90 delete clients[pg][obj.sid];
91 };
92 const disconnectFromOtherConnexion = (pg,code,o={}) => {
93 Object.keys(clients[pg]).forEach(k => {
94 if (k != obj.sid)
95 {
96 Object.keys(clients[pg][k]).forEach(x => {
97 send(clients[pg][k][x], Object.assign({code:code, from:obj.sid}, o));
98 });
99 }
100 });
101 };
102 Object.keys(clients).forEach(pg => {
103 if (!!clients[pg][obj.sid])
104 {
105 doKill(pg);
106 disconnectFromOtherConnexion(pg, "disconnect");
107 if (pg.indexOf("/game/") >= 0 && !!clients["/"])
108 disconnectFromOtherConnexion("/", "gdisconnect", {page:pg});
109 }
110 });
111 break;
112 }
113 case "pollclients": //from Hall or Game
114 {
115 let sockIds = [];
116 Object.keys(clients[page]).forEach(k => {
117 // Avoid polling myself: no new information to get
118 if (k != sid)
119 sockIds.push(k);
120 });
121 send(socket, {code:"pollclients", sockIds:sockIds});
122 break;
123 }
124 case "pollclientsandgamers": //from Hall
125 {
126 let sockIds = [];
127 Object.keys(clients["/"]).forEach(k => {
128 // Avoid polling myself: no new information to get
129 if (k != sid)
130 sockIds.push({sid:k});
131 });
132 // NOTE: a "gamer" could also just be an observer
133 Object.keys(clients).forEach(p => {
134 if (p != "/")
135 {
136 Object.keys(clients[p]).forEach(k => {
137 if (k != sid)
138 sockIds.push({sid:k, page:p}); //page needed for gamers
139 });
140 }
141 });
142 send(socket, {code:"pollclientsandgamers", sockIds:sockIds});
143 break;
144 }
145
146 // Asking something: from is fully identified,
147 // but the requested resource can be from any tmpId (except current!)
148 case "askidentity":
149 case "asklastate":
150 case "askchallenge":
151 case "askgame":
152 case "askfullgame":
153 {
154 const pg = obj.page || page; //required for askidentity and askgame
155 const tmpIds = Object.keys(clients[pg][obj.target]);
156 if (obj.target == sid) //targetting myself
157 {
158 const idx_myTmpid = tmpIds.findIndex(x => x == tmpId);
159 if (idx_myTmpid >= 0)
160 tmpIds.splice(idx_myTmpid, 1);
161 }
162 const tmpId_idx = Math.floor(Math.random() * tmpIds.length);
163 send(clients[pg][obj.target][tmpIds[tmpId_idx]], {code:obj.code, from:[sid,tmpId,page]});
164 break;
165 }
166
167 // Some Hall events: target all tmpId's (except mine),
168 case "refusechallenge":
169 case "startgame":
170 Object.keys(clients[page][obj.target]).forEach(x => {
171 if (obj.target != sid || x != tmpId)
172 send(clients[page][obj.target][x], {code:obj.code, data:obj.data});
173 });
174 break;
175
176 // Notify all room: mostly game events
177 case "newchat":
178 case "newchallenge":
179 case "newgame":
180 case "deletechallenge":
181 case "newmove":
182 case "resign":
183 case "abort":
184 case "drawoffer":
185 case "draw":
186 notifyRoom(page, obj.code, {data:obj.data});
187 break;
188
189 // Passing, relaying something: from isn't needed,
190 // but target is fully identified (sid + tmpId)
191 case "challenge":
192 case "fullgame":
193 case "game":
194 case "identity":
195 case "lastate":
196 {
197 const pg = obj.target[2] || page; //required for identity and game
198 send(clients[pg][obj.target[0]][obj.target[1]], {code:obj.code, data:obj.data});
199 break;
200 }
201 }
202 };
203 const closeListener = () => {
204 // TODO: BUG: this is triggered twice when anonymous reloads page
205 // (+ registered users, everyone in Hall).
206 // For tab or browser closing:
207 doDisconnect();
208 };
209 // Update clients object: add new connexion
210 if (!clients[page])
211 clients[page] = {[sid]: {[tmpId]: socket}};
212 else if (!clients[page][sid])
213 clients[page][sid] = {[tmpId]: socket};
214 else
215 clients[page][sid][tmpId] = socket;
216 socket.on("message", messageListener);
217 socket.on("close", closeListener);
218 });
219 }