});
}
});
- // NOTE: store.initialize(this.$route.path); doesn't work
- store.initialize(window.location.href.split("#")[1].split("?")[0]);
+ store.initialize();
},
}).$mount("#app");
]
});
-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;
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 =
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)
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",
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;
import { ArrayFun } from "@/utils/array";
import { processModalClick } from "@/utils/modalClick";
import { getScoreMessage } from "@/utils/scoring";
+import params from "@/parameters";
export default {
name: 'my-game',
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: {
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));
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];
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;
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,
&& 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":
// 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:
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;
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":
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);
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}));
}
});
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});
}
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,
}));
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}));
}
});
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
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,
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});
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";
timeControl: localStorage.getItem("timeControl") || "",
},
newChat: "",
+ conn: null,
+ page: "",
+ tempId: "", //to distinguish several tabs
};
},
watch: {
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",
);
// 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")]
)}
);
},
+ beforeDestroy: function() {
+ this.conn.send(JSON.stringify({code:"disconnect",page:this.page}));
+ },
methods: {
// Helpers:
filterChallenges: function(type) {
},
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}
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)
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":
// 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,
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;
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":
}
else
{
- this.st.conn.send(JSON.stringify({
+ this.conn.send(JSON.stringify({
code: "refusechallenge",
cid: c.id, target: c.from.sid}));
}
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}));
}
};
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,
}
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)
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)));
}
});
};
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;
}
}
};
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])
{