diff --git a/src/models/timer.js b/src/models/timer.js index c73dfe6c2..8afbad119 100644 --- a/src/models/timer.js +++ b/src/models/timer.js @@ -5,13 +5,12 @@ const { Schema } = mongoose; const timerSchema = new Schema({ userId: { type: Schema.Types.ObjectId, required: true, ref: "userProfile" }, - lastAccess: { type: Date, default: Date.now }, + startAt: { type: Date, default: Date.now }, time: { type: Number, default: 900000 }, - countdown: { type: Boolean, default: true }, goal: { type: Number, default: 900000 }, - paused: { type: Boolean, default: true }, + paused: { type: Boolean, default: false }, forcedPause: { type: Boolean, default: false }, - stopped: { type: Boolean, default: false }, + started: { type: Boolean, default: false }, }); module.exports = mongoose.model("newTimer", timerSchema, "newTimers"); diff --git a/src/routes/dashboardRouter.js b/src/routes/dashboardRouter.js index 33275597c..664c1c802 100644 --- a/src/routes/dashboardRouter.js +++ b/src/routes/dashboardRouter.js @@ -3,7 +3,6 @@ const express = require('express'); const route = function () { const controller = require('../controllers/dashBoardController')(); - const Dashboardrouter = express.Router(); Dashboardrouter.route('/dashboard/:userId') diff --git a/src/websockets/TimerService/clientsHandler.js b/src/websockets/TimerService/clientsHandler.js new file mode 100644 index 000000000..6990ead71 --- /dev/null +++ b/src/websockets/TimerService/clientsHandler.js @@ -0,0 +1,283 @@ +/* eslint-disable no-multi-assign */ +/* eslint-disable radix */ +const moment = require('moment'); +const Timer = require('../../models/timer'); +const logger = require('../../startup/logger'); + +/** + * Here we get the timer. + * If the timer already exists in memory, we return it. + * If it doesn't exist, we try to get it from MongoDB. + * If it doesn't exist in MongoDB, we create it and save it to MongoDB. + * Then we save it to memory and return it. + */ +export const getClient = async (clients, userId) => { + // In case of there is already a connection that is open for this user + // for example user open a new connection + if (!clients.has(userId)) { + try { + let timer = await Timer.findOne({ userId }); + if (!timer) timer = await Timer.create({ userId }); + clients.set(userId, timer); + } catch (e) { + logger.logException(e); + throw new Error( + 'Something happened when trying to retrieve timer from mongo', + ); + } + } + return clients.get(userId); +}; + +/** + * Save client info to database + * Save under these conditions: + * connection is normally closed (paused and closed); + * connection is forced-paused (timer still on and connection closed) + * message: STOP_TIMER + */ +export const saveClient = async (client) => { + try { + await Timer.findOneAndUpdate({ userId: client.userId }, client); + } catch (e) { + logger.logException(e); + throw new Error( + `Something happened when trying to save user timer to mongo, Error: ${e}`, + ); + } +}; + +/** + * This is the contract between client and server. + * The client can send one of the following messages to the server: + */ +export const action = { + START_TIMER: 'START_TIMER', + PAUSE_TIMER: 'PAUSE_TIMER', + STOP_TIMER: 'STOP_TIMER', + CLEAR_TIMER: 'CLEAR_TIMER', + SET_GOAL: 'SET_GOAL=', + ADD_GOAL: 'ADD_TO_GOAL=', + REMOVE_GOAL: 'REMOVE_FROM_GOAL=', + FORCED_PAUSE: 'FORCED_PAUSE', + ACK_FORCED: 'ACK_FORCED', +}; + +const MAX_HOURS = 5; +const MIN_MINS = 1; + +const updatedTimeSinceStart = (client) => { + if (!client.started) return client.goal; + const now = moment.utc(); + const startAt = moment(client.startAt); + const timePassed = moment.duration(now.diff(startAt)).asMilliseconds(); + const updatedTime = client.time - timePassed; + return updatedTime > 0 ? updatedTime : 0; +}; + +/** + * Here we start the timer, if it is not already started. + * We set the last access time to now, and set the paused and stopped flags to false. + * If the timer was paused, we need to check if it was paused by the user or by the server. + * If it was paused by the server, we need to set the forcedPause flag to true. + */ +const startTimer = (client) => { + client.startAt = moment.utc(); + client.paused = false; + if (!client.started) { + client.started = true; + client.time = client.goal; + } + if (client.forcedPause) client.forcedPause = false; +}; + +/** + * Here we pause the timer, if it is not already paused. + * We get the total elapsed time since the last access, and set it as the new time. + * We set the last access time to now, and set the paused flag to true. + * If the timer was paused by the server, we need to set the forcedPause flag to true. + * It'll only be triggered when the user closes the connection sudenlly or lacks of ACKs. + */ +const pauseTimer = (client, forced = false) => { + client.time = updatedTimeSinceStart(client); + client.startAt = moment.invalid(); + client.paused = true; + if (forced) client.forcedPause = true; +}; + +// Here we acknowledge the forced pause. To prevent the modal for beeing displayed again. +const ackForcedPause = (client) => { + client.forcedPause = false; + client.paused = true; + client.startAt = moment.invalid(); +}; + +/** + * Here we stop the timer. + * We pause the timer and set the stopped flag to true. + */ +const stopTimer = (client) => { + client.startAt = moment.invalid(); + client.started = false; + client.pause = false; + client.forcedPause = false; +}; + +/** + * Here we clear the timer. + * We pause the timer and check it's mode to set the time to 0 or the goal. + * Then we set the stopped flag to false. + */ +const clearTimer = (client) => { + stopTimer(client); + client.goal = moment.duration(2, 'hours').asMilliseconds(); + client.time = client.goal; +}; + +// Here we set the goal and time to the goal time. +/** + * Here we set the goal. + * if timer has not started, we set both time and goal to the new goal + * if timer has started, we calculate the passed time and remove that from new goal + * and if passed time is greater than new goal, then set time to 0, but this should + * not be prohibited by frontend. + */ +const setGoal = (client, msg) => { + const newGoal = parseInt(msg.split('=')[1]); + if (!client.started) { + client.goal = newGoal; + client.time = newGoal; + } else { + const passedTime = client.goal - client.time; + if (passedTime >= newGoal) { + client.time = 0; + client.goal = passedTime; + } else { + client.time = newGoal - passedTime; + client.goal = newGoal; + } + } +}; + +/** + * Here we add the goal time. + * Each addition add 15min + * First we get the goal time from the message. + * Then we add it to the current goal time and set it as the new goal time. + * We also add it to the current time and set it as the new time. + */ +const addGoal = (client, msg) => { + const duration = parseInt(msg.split('=')[1]); + const goalAfterAddition = moment + .duration(client.goal) + .add(duration, 'milliseconds') + .asHours(); + + if (goalAfterAddition > MAX_HOURS) return; + + client.goal = moment + .duration(client.goal) + .add(duration, 'milliseconds') + .asMilliseconds() + .toFixed(); + client.time = moment + .duration(client.time) + .add(duration, 'milliseconds') + .asMilliseconds() + .toFixed(); +}; + +/** + * Here we try to remove a goal time. + * First we get the goal time from the message. + * Then we subtract it from the current goal time and set it as the new goal time. + * We also subtract it from the current time and set it as the new time. + * If the new goal time is less than 15 minutes, we don't do anything. + * If the new time is less than 0, we set it to 0. + */ +const removeGoal = (client, msg) => { + const duration = parseInt(msg.split('=')[1]); + const goalAfterRemoval = moment + .duration(client.goal) + .subtract(duration, 'milliseconds') + .asMinutes(); + const timeAfterRemoval = moment + .duration(client.time) + .subtract(duration, 'milliseconds') + .asMinutes(); + + if (goalAfterRemoval < MIN_MINS || timeAfterRemoval < 0) return; + + client.goal = moment + .duration(client.goal) + .subtract(duration, 'milliseconds') + .asMilliseconds() + .toFixed(); + client.time = moment + .duration(client.time) + .subtract(duration, 'milliseconds') + .asMilliseconds() + .toFixed(); +}; + + +/** + * Here is were we handle the messages. + * First we check if the user is in memory, if not, we throw an error. + * Then we parse the request and check which action it is and call the corresponding function. + * If we don't have a match, we just return an error. + * The only operation that we write to Mongo it's the stop timer. Other operations are just in memory. + * So the slowest part of the app is the save to Mongo. + * Then we update the current client in hash map and return the response. + */ +export const handleMessage = async (msg, clients, userId) => { + if (!clients.has(userId)) { + throw new Error('It should have this user in memory'); + } + + const client = clients.get(userId); + let resp = null; + + const req = msg.toString(); + switch (req) { + case action.START_TIMER: + startTimer(client); + break; + case req.match(/SET_GOAL=/i)?.input: + setGoal(client, req); + break; + case req.match(/ADD_TO_GOAL=/i)?.input: + addGoal(client, req); + break; + case req.match(/REMOVE_FROM_GOAL=/i)?.input: + removeGoal(client, req); + break; + case action.PAUSE_TIMER: + pauseTimer(client); + break; + case action.FORCED_PAUSE: + pauseTimer(client, true); + break; + case action.ACK_FORCED: + ackForcedPause(client); + break; + case action.CLEAR_TIMER: + clearTimer(client); + break; + case action.STOP_TIMER: + stopTimer(client); + break; + + default: + resp = { + ...client, + error: `Unknown operation ${req}, please use one of ${action}`, + }; + break; + } + + saveClient(client); + clients.set(userId, client); + if (resp === null) resp = client; + return JSON.stringify(resp); +}; diff --git a/src/websockets/TimerService/connectionsHandler.js b/src/websockets/TimerService/connectionsHandler.js new file mode 100644 index 000000000..6658321bf --- /dev/null +++ b/src/websockets/TimerService/connectionsHandler.js @@ -0,0 +1,50 @@ +const WebSocket = require('ws'); + +/** + * Here we insert the new connection to the connections map. + * If the user is not in the map, we create a new entry with the user id as key and the connection as value. + * Else we just push the connection to the array of connections. + */ +export function insertNewUser(connections, userId, wsConn) { + const userConnetions = connections.get(userId); + if (!userConnetions) connections.set(userId, [wsConn]); + else userConnetions.push(wsConn); +} + +/** + *Here we remove the connection from the connections map. + *If the user is not in the map, we do nothing. + *Else we remove the connection from the array of connections. + *If the array is empty, we delete the user from the map. + */ +export function removeConnection(connections, userId, connToRemove) { + const userConnetions = connections.get(userId); + if (!userConnetions) return; + + const newConns = userConnetions.filter(conn => conn !== connToRemove); + if (newConns.length === 0) connections.delete(userId); + else connections.set(userId, newConns); +} + +/** + * Here we broadcast the message to all the connections that are connected to the same user. + * We check if the connection is open before sending the message. + */ +export function broadcastToSameUser(connections, userId, data) { + const userConnetions = connections.get(userId); + if (!userConnetions) return; + userConnetions.forEach((conn) => { + if (conn.readyState === WebSocket.OPEN) conn.send(data); + }); +} + +/** + * Here we check if there is another connection to the same user. + * If there is, we return true. + * Else we return false. + */ +export function hasOtherConn(connections, userId, anotherConn) { + if (!connections.has(userId)) return false; + const userConnections = connections.get(userId); + return userConnections.some(con => con !== anotherConn && con.readyState === WebSocket.OPEN); +} diff --git a/src/websockets/TimerService/index.js b/src/websockets/TimerService/index.js deleted file mode 100644 index 9eac199ce..000000000 --- a/src/websockets/TimerService/index.js +++ /dev/null @@ -1,308 +0,0 @@ -/* eslint-disable no-multi-assign */ -/* eslint-disable radix */ -const moment = require('moment'); -const Timer = require('../../models/timer'); -const logger = require('../../startup/logger'); - -/* -This is the contract between client and server. -The client can send one of the following messages to the server: -*/ -export const action = { - START_TIMER: 'START_TIMER', - PAUSE_TIMER: 'PAUSE_TIMER', - STOP_TIMER: 'STOP_TIMER', - GET_TIMER: 'GET_TIMER', - CLEAR_TIMER: 'CLEAR_TIMER', - SWITCH_MODE: 'SWITCH_MODE', - SET_GOAL: 'SET_GOAL=', - ADD_GOAL: 'ADD_GOAL=', - REMOVE_GOAL: 'REMOVE_GOAL=', - FORCED_PAUSE: 'FORCED_PAUSE', - ACK_FORCED: 'ACK_FORCED', -}; - -/* -Here we get the total elapsed time since the last access. -Since we have two modes for the timer, countdown and stopwatch, -we need to know which one is active to calculate the total elapsed time. -If the timer is in countdown mode, we need to subtract the elapsed time from the total time. -if this total time is less than 0, we set it to 0. -If the timer is in stopwatch mode, -we need to add the elapsed time since the last access to the total time. -we then return the total -*/ -const getTotalElapsedTime = (client) => { - const now = moment(); - const lastAccess = moment(client.lastAccess); - const elapSinceLastAccess = moment.duration(now.diff(lastAccess)); - const time = moment.duration(moment(client.time)); - - let total; - if (client.countdown) { - total = time.subtract(elapSinceLastAccess, 'milliseconds'); - if (total.asMilliseconds() < 0) { - total = moment.duration(0); - } - } else total = elapSinceLastAccess.add(client.time, 'milliseconds'); - - return total; -}; - -/* -Here we start the timer, if it is not already started. -We set the last access time to now, and set the paused and stopped flags to false. -If the timer was paused, we need to check if it was paused by the user or by the server. -If it was paused by the server, we need to set the forcedPause flag to true. -*/ -const startTimer = (client) => { - if (!client.paused) { - client.time = getTotalElapsedTime(client).asMilliseconds().toFixed(); - client.lastAccess = moment(); - } - - if (client.paused) { - client.lastAccess = moment(); - client.stopped = false; - client.paused = false; - if (client.forcedPause) client.forcedPause = false; - } -}; - -/* -Here we pause the timer, if it is not already paused. -We get the total elapsed time since the last access, and set it as the new time. -We set the last access time to now, and set the paused flag to true. -If the timer was paused by the server, we need to set the forcedPause flag to true. -It'll only be triggered when the user closes the connection sudenlly or lacks of ACKs. -*/ -const pauseTimer = (client, forced = false) => { - if (!client.paused) { - client.time = getTotalElapsedTime(client).asMilliseconds().toFixed(); - client.lastAccess = moment(); - client.paused = true; - if (forced) client.forcedPause = true; - } -}; - -// Here we acknowledge the forced pause. To prevent the modal for beeing displayed again. -const ackForcedPause = (client) => { - client.forcedPause = false; -}; - -/* -Here we clear the timer. -We pause the timer and check it's mode to set the time to 0 or the goal. -Then we set the stopped flag to false. -*/ -const clearTimer = (client) => { - pauseTimer(client); - client.time = client.countdown ? client.goal : 0; - client.stopped = false; -}; - -/* -Here we stop the timer. -We pause the timer and set the stopped flag to true. -*/ -const stopTimer = (client) => { - pauseTimer(client); - client.stopped = true; -}; - -/* -Here we switch the timer mode. -We pause the timer and check it's mode to set the time to 0 or the goal. -*/ -const switchMode = (client) => { - client.countdown = !client.countdown; - client.time = client.countdown ? client.goal : 0; - client.paused = true; -}; - -// Here we get the goal time from the message. -const getGoal = msg => parseInt(msg.split('=')[1]); - -// Here we set the goal and time to the goal time. -const setGoal = (client, msg) => { - const goal = getGoal(msg); - client.goal = client.time = goal; -}; - -const goalOver10Hours = (client, time) => { - const goal = moment.duration(client.goal).add(time, 'milliseconds').asHours(); - return goal > 10; -}; - -/* -Here we add the goal time. -First we get the goal time from the message. -Then we add it to the current goal time and set it as the new goal time. -We also add it to the current time and set it as the new time. -*/ -const addGoal = (client, msg) => { - const goal = getGoal(msg); - if (goalOver10Hours(client, goal)) return; - - if (!client.paused) { - client.time = getTotalElapsedTime(client).asMilliseconds().toFixed(); - client.lastAccess = moment(); - } - - client.goal = moment - .duration(client.goal) - .add(goal, 'milliseconds') - .asMilliseconds() - .toFixed(); - client.time = moment - .duration(client.time) - .add(goal, 'milliseconds') - .asMilliseconds() - .toFixed(); -}; - -/* - * Here we check if the goal time is less than 15 minutes. - * */ -const goalLessThan15min = (client, time) => { - const goal = moment - .duration(client.goal) - .subtract(time, 'milliseconds') - .asMinutes(); - return goal < 15; -}; - -/* - * Here we try to remove a goal time. - * First we get the goal time from the message. - * Then we subtract it from the current goal time and set it as the new goal time. - * We also subtract it from the current time and set it as the new time. - * If the new goal time is less than 15 minutes, we don't do anything. - * If the new time is less than 0, we set it to 0. - * */ -const removeGoal = (client, msg) => { - const goal = getGoal(msg); - if (goalLessThan15min(client, goal)) return; - - if (!client.paused) { - client.time = getTotalElapsedTime(client).asMilliseconds().toFixed(); - client.lastAccess = moment(); - } - - client.goal = moment - .duration(client.goal) - .subtract(goal, 'milliseconds') - .asMilliseconds() - .toFixed(); - const time = moment - .duration(client.time) - .subtract(goal, 'milliseconds') - .asMilliseconds() - .toFixed(); - client.time = time < 0 ? 0 : time; -}; - -/* -Here we get the timer. -If the timer already exists in memory, we return it. -If it doesn't exist, we try to get it from MongoDB. -If it doesn't exist in MongoDB, we create it and save it to MongoDB. -Then we save it to memory and return it. -*/ -export const getTimer = async (clientsMap, userId) => { - if (clientsMap.has(userId)) return; - - try { - let timer = await Timer.findOne({ userId }); - if (!timer) timer = await Timer.create({ userId }); - clientsMap.set(userId, timer); - } catch (e) { - logger.logException(e); - throw new Error( - 'Something happened when trying to retrieve timer from mongo', - ); - } -}; - -// Here we just save the timer to MongoDB. -const saveClient = async (client) => { - try { - await Timer.findOneAndUpdate({ userId: client.userId }, client); - } catch (e) { - logger.logException(e); - throw new Error( - 'Something happened when trying to save user timer to mongo', - ); - } -}; - -/* -Here is were we handle the messages. -First we check if the user is in memory, if not, we throw an error. -Then we parse the request and check which action it is and call the corresponding function. -If we don't have a match, we just return an error. -The only operation that we write to Mongo it's the stop timer. Other operations are just in memory. -So the slowest part of the app is the save to Mongo. -Then we update the current client in hash map and return the response. -*/ -export const handleMessage = async (msg, clientsMap, userId) => { - if (!clientsMap.has(userId)) { - throw new Error('It should have this user in memory'); - } - - const client = clientsMap.get(userId); - let resp = null; - - const req = msg.toString(); - switch (req) { - case action.GET_TIMER: - break; - case action.START_TIMER: - startTimer(client); - break; - case action.SWITCH_MODE: - switchMode(client); - break; - case req.match(/SET_GOAL=/i)?.input: - setGoal(client, req); - break; - case req.match(/ADD_GOAL=/i)?.input: - addGoal(client, req); - break; - case req.match(/REMOVE_GOAL=/i)?.input: - removeGoal(client, req); - break; - case action.PAUSE_TIMER: - pauseTimer(client); - break; - case action.FORCED_PAUSE: - pauseTimer(client, true); - break; - case action.ACK_FORCED: - ackForcedPause(client); - break; - case action.CLEAR_TIMER: - clearTimer(client); - break; - case action.STOP_TIMER: - stopTimer(client); - break; - - default: - resp = { - ...client, - error: `Unknown operation ${req}, please use one of ${action}`, - }; - break; - } - - if (req === action.STOP_TIMER) { - await saveClient(client).catch((err) => { - resp = { ...client, error: err }; - }); - } - - clientsMap.set(userId, client); - if (resp === null) resp = client; - return JSON.stringify(resp); -}; diff --git a/src/websockets/index.js b/src/websockets/index.js index a733dff25..5da85f729 100644 --- a/src/websockets/index.js +++ b/src/websockets/index.js @@ -7,15 +7,25 @@ const WebSocket = require("ws"); const moment = require("moment"); const jwt = require("jsonwebtoken"); const config = require("../config"); -const { getTimer, handleMessage, action } = require("./TimerService/"); - -/* -Here we authenticate the user. -We get the token from the headers and try to verify it. -If it fails, we throw an error. -Else we check if the token is valid and if it is, we return the user id. +const { + insertNewUser, + removeConnection, + broadcastToSameUser, + hasOtherConn, +} = require("./TimerService/connectionsHandler"); +const { + getClient, + handleMessage, + action, +} = require("./TimerService/clientsHandler"); + +/** +* Here we authenticate the user. +* We get the token from the headers and try to verify it. +* If it fails, we throw an error. +* Else we check if the token is valid and if it is, we return the user id. */ -export const authenticate = (req, res) => { +const authenticate = (req, res) => { const authToken = req.headers?.["sec-websocket-protocol"]; let payload = ""; try { @@ -37,68 +47,13 @@ export const authenticate = (req, res) => { res(null, payload.userid); }; -/* - * Here we insert the new connection to the connections map. - * If the user is not in the map, we create a new entry with the user id as key and the connection as value. - * Else we just push the connection to the array of connections. - */ -const insertNewUser = (connections, userId, wsConn) => { - const userConnetions = connections.get(userId); - if (!userConnetions) connections.set(userId, [wsConn]); - else userConnetions.push(wsConn); -}; - -/* - *Here we remove the connection from the connections map. - *If the user is not in the map, we do nothing. - *Else we remove the connection from the array of connections. - *If the array is empty, we delete the user from the map. - */ -const removeConnection = (connections, userId, connToRemove) => { - const userConnetions = connections.get(userId); - if (!userConnetions) return; - - const newConns = userConnetions.filter(conn => conn !== connToRemove); - if (newConns.length === 0) connections.delete(userId); - else connections.set(userId, newConns); -}; - -/* - * Here we broadcast the message to all the connections that are connected to the same user. - * We check if the connection is open before sending the message. - */ -const broadcastToSameUser = (connections, userId, data) => { - const userConnetions = connections.get(userId); - if (!userConnetions) return; - userConnetions.forEach((conn) => { - if (conn.readyState === WebSocket.OPEN) conn.send(data); - }); -}; - -/* - * Here we check if there is another connection to the same user. - * If there is, we return true. - * Else we return false. - */ -const checkOtherConn = (connections, anotherConn, userId) => { - const userConnetions = connections.get(userId); - if (!userConnetions) return false; - for (const con of userConnetions) { - if (con !== anotherConn && con.readyState === WebSocket.OPEN) return true; - } - return false; -}; - -/* -Here we start the timer service. -First we create a map to store the clients and start the Websockets Server. -Then we set the upgrade event listener to the Express Server, authenticate the user and -if it is valid, we add the user id to the request and handle the upgrade and emit the connection event. +/** +* Here we start the timer service. +* First we create a map to store the clients and start the Websockets Server. +* Then we set the upgrade event listener to the Express Server, authenticate the user and +* if it is valid, we add the user id to the request and handle the upgrade and emit the connection event. */ export default async (expServer) => { - const clients = new Map(); - const connections = new Map(); - const wss = new WebSocket.Server({ noServer: true, path: "/timer-service", @@ -118,64 +73,66 @@ export default async (expServer) => { }); }); - /* - For each new connection we start a timer of 5min to check if the connection is alive. - If it is, we then repeat the process. If it is not, we terminate the connection. - */ + const clients = new Map(); // { userId: timerInfo } + const connections = new Map(); // { userId: connections[] } + wss.on("connection", async (ws, req) => { ws.isAlive = true; + const { userId } = req; + ws.on("pong", () => { ws.isAlive = true; }); - const { userId } = req; - insertNewUser(connections, userId, ws); - /* + /** * Here we get the timer from memory or from the database and send it to the client. - * We don't broadcast it */ - await getTimer(clients, userId); - ws.send(await handleMessage(action.GET_TIMER, clients, userId)); + const clientTimer = await getClient(clients, userId); + ws.send(JSON.stringify(clientTimer)); - /* - Here we handle the messages from the client. - And we broadcast the response to all the clients that are connected to the same user. + /** + * Here we handle the messages from the client. + * And we broadcast the response to all the clients that are connected to the same user. */ ws.on("message", async (data) => { const resp = await handleMessage(data, clients, userId); broadcastToSameUser(connections, userId, resp); }); - /* - Here we handle the close event. - If there is another connection to the same user, we don't do anything. - Else he is the last connection and we do a forced pause if need be. - This may happen if the user closes all the tabs or the browser or he lost connection with - the service - We then remove the connection from the connections map. + /** + * Here we handle the close event. + * If there is another connection to the same user, we don't do anything. + * Else he is the last connection and we do a forced pause if need be. + * This may happen if the user closes all the tabs or the browser or he lost connection with + * the service + * We then remove the connection from the connections map. */ ws.on("close", async () => { - if (!checkOtherConn(connections, ws, userId)) { - await handleMessage(action.FORCED_PAUSE, clients, userId); + if (!hasOtherConn(connections, userId, ws)) { + const client = clients.get(userId); + if (client.started && !client.paused) { + await handleMessage(action.FORCED_PAUSE, clients, userId); + } } removeConnection(connections, userId, ws); }); }); - // The function to check if the connection is alive - const interval = setInterval(async () => { - wss.clients.forEach(async (ws) => { - if (ws.isAlive === false) return ws.terminate(); - + // For each new connection we start a time interval of 1min to check if the connection is alive. + // change to 1min before push + const interval = setInterval(() => { + wss.clients.forEach((ws) => { + if (ws.isAlive === false) { + return ws.close(); + } ws.isAlive = false; ws.ping(); }); - }, 3000000); + }, 10000); - // Here we just clear the interval when the server closes - wss.on("close", () => { + wss.on('close', () => { clearInterval(interval); });