// Import dependencies const { WebSocket, WebSocketServer } = require("ws"); const express = require("express"); const dotenv = require("dotenv"); dotenv.config(); // Setup Environmental Variables const PORT = process.env.PORT || 3000; const PREFIX = process.env.PREFIX || ""; const PUBLIC = process.env.PUBLIC || ""; // Setup Express Router // Create the routes of the application // Here are two pages and a wildcard: // at / there is the index.html page, where to draw // at /destination there is the destination.html page, where to receive the drawings // the wildcard /* serves the static files from the public folder const router = express.Router(); const routes = (app) => { app.get("/", (req, res) => { res.render("index", { address: PREFIX, }); }); app.get("/destination", (req, res) => { res.render("destination", { address: PREFIX, }); }); app.get("/wander", (req, res)=>{ res.render("wander", { address: PREFIX, }) }); app.get("/*", (req, res) => { res.sendFile(req.url, { root: "public" }); }); return app; }; // Setup the Express server const server = express() .set("view engine", "html") .engine("html", require("hbs").__express) .use(PREFIX, routes(router, {})) .use(express.static("public")) .listen(PORT, () => console.log(`Listening on ${PORT}`)); // Setup the Websocket server on top of the Express one const wss = new WebSocketServer({ server, clientTracking: true }); // Global variables to manage the connected Destination and User clients let DESTINATIONS = new Set(); let USERS = new Set(); var theme = ""; // The message processor defines which function is associated to every websocket message type. // It map a type to a function, passing some optional parameters such as the message itself and the websocket client that sent it. // for example an incoming message like {type: hello} will trigger the registerDest(ws, msg) function. // In this way to add message types and functionalities gets easier, and avoid long chain of if-else statements. const messageProcessor = { default: (ws, msg) => unknownMsg(msg), hello: (ws, msg) => registerDest(ws, msg), drawings: (ws, msg) => toDest(msg), theme: (ws, msg) => ((theme = msg.theme), broadcast(msg)), }; // Message processor functions // Default function, to cactch unkown message types const unknownMsg = (msg) => { console.log("Unknown message type..."); console.log(msg); }; // Add the ws client in the destinations set // Removing it when it disconnets const registerDest = (ws, msg) => { console.log("Destination client connected"); DESTINATIONS.add(ws); ws.on("close", () => { DESTINATIONS.delete(ws); }); }; // Send a message to all the connected Destinations const toDest = (msg) => { let message = JSON.stringify(msg); DESTINATIONS.forEach((DESTINATION) => { if (DESTINATION?.readyState === WebSocket.OPEN) { DESTINATION.send(message); } }); }; // Send a message to all the connected Users const broadcast = (msg) => { let message = JSON.stringify(msg); for (const user of USERS.values()) { if (user?.readyState === WebSocket.OPEN && !DESTINATIONS.has(user)) { user.send(message); } } }; // Websocket events listener wss.on("connection", (ws) => { USERS.add(ws); ws.send(JSON.stringify({ type: "theme", theme: theme })); ws.on("message", (data) => { // Parse the incoming data safely let message; try { message = JSON.parse(data); } catch (e) {} // Call the message processor, eventually falling back to use the default function if the type is not defined. if (message) { (messageProcessor[message?.type] || messageProcessor.default)(ws, message); } }); ws.on("close", () => { USERS.delete(ws); }); });