diff --git a/backend/index.ts b/backend/index.ts index 98651a6..7a1d6bd 100644 --- a/backend/index.ts +++ b/backend/index.ts @@ -13,6 +13,7 @@ import votationRoutes from "./routes/votation"; import { parse } from "url"; import { lobbyWss } from "./wsServers/lobby"; import { organizerWss } from "./wsServers/organizer"; +import { startHeartbeatInterval } from "./utils/socketNotifier"; dotenv.config(); @@ -63,6 +64,9 @@ server.on("upgrade", function upgrade(request, socket, head) { } }); +// Start sending pings/Heartbeat to ws-connections to keep connections alive. +startHeartbeatInterval; + try { // Jest will start app itself when testing, and not run on port 3000 to avoid collisions. if (process.env.NODE_ENV !== "test") { diff --git a/backend/utils/socketNotifier.ts b/backend/utils/socketNotifier.ts index c4d809e..f53593c 100644 --- a/backend/utils/socketNotifier.ts +++ b/backend/utils/socketNotifier.ts @@ -17,40 +17,60 @@ type OrganizersByGroupSlug = { [key: string]: SocketList }; // This makes it possible to send messages to all logged in organizers of a group. export const organizerConnections: OrganizersByGroupSlug = {}; // Store all active participant connections, for access when sending messages about assembly. -export const lobbyConnections: SocketList = []; +export const lobbyConnections = new Map(); -export function storeLobbyConnectionByCookie( +const sendPing = (ws: WebSocket) => { + if (ws.readyState === WebSocket.OPEN) { + ws.ping(); + } +}; + +// Send ping to all participants to check if they are still connected and prevent the connection from closing. +export const startHeartbeatInterval = setInterval(() => { + lobbyConnections.forEach((ws: WebSocket) => { + sendPing(ws); + }); +}, 30000); // 30 seconds + +export const storeLobbyConnectionByCookie = ( ws: WebSocket, req: IncomingMessage -) { +) => { const ntnuiNo = NTNUINoFromRequest(req); if (ntnuiNo !== null) { // Notify about kicking out old connection if user already is connected. - if (typeof lobbyConnections[ntnuiNo] !== null) { + if (typeof lobbyConnections.get(ntnuiNo) !== null) { notifyOneParticipant(ntnuiNo, JSON.stringify({ status: "removed" })); } // Store socket connection on NTNUI ID. - lobbyConnections[ntnuiNo] = ws; + lobbyConnections.set(ntnuiNo, ws); } -} +}; -export function storeOrganizerConnectionByNTNUINo( +export const removeLobbyConnectionByCookie = (req: IncomingMessage) => { + const ntnuiNo = NTNUINoFromRequest(req); + if (ntnuiNo !== null) { + lobbyConnections.delete(ntnuiNo); + } +}; + +export const storeOrganizerConnectionByNTNUINo = ( ntnui_no: number, groupSlug: string, ws: WebSocket -) { +) => { if (!organizerConnections[groupSlug]) organizerConnections[groupSlug] = []; organizerConnections[groupSlug][ntnui_no] = ws; -} +}; export const notifyOneParticipant = (ntnui_no: number, message: string) => { - try { - lobbyConnections[ntnui_no].send(message); - } catch (error) { + const connection = lobbyConnections.get(ntnui_no); + if (connection) connection.send(message); + else { console.log( "Could not notify user " + ntnui_no + - ". Is there a problem with the socket URL? (Ignore if testing / dev has restarted)" + " (disconnected). Is there a problem with the socket URL? (Ignore if testing / dev has restarted)" ); } }; diff --git a/backend/wsServers/lobby.ts b/backend/wsServers/lobby.ts index 14a9c15..2084df4 100644 --- a/backend/wsServers/lobby.ts +++ b/backend/wsServers/lobby.ts @@ -1,9 +1,20 @@ import { WebSocketServer } from "ws"; -import { storeLobbyConnectionByCookie } from "../utils/socketNotifier"; +import { + removeLobbyConnectionByCookie, + storeLobbyConnectionByCookie, +} from "../utils/socketNotifier"; export const lobbyWss = new WebSocketServer({ noServer: true }); lobbyWss.on("connection", function connection(ws, req) { // Store connections to be able to send messages to specific users when needed. storeLobbyConnectionByCookie(ws, req); + + ws.on("pong", () => { + // The client responded to the ping, so the connection is still active. + }); + + ws.on("close", () => { + removeLobbyConnectionByCookie(req); + }); });