/* ! /servers/websocket/server.js – Central Websocket Server Manager ? This module orchestrates the websocket lifecycle, handles connections, heartbeats and actions */ // % import nodejs packages const path = require('path'); const fs = require('fs'); const http = require('http'); const WebSocket = require('ws'); // % import external dependencies const rootDir = path.resolve(__dirname, '..', '..'); const { dirs } = require(path.resolve(rootDir, 'configuration', 'mapping.js')); const connectionManager = require(path.resolve(dirs.websocketDir, 'connections.js')); // % declare action directories const wsActionsDir = path.resolve(dirs.websocketDir, 'actions'); // % declare reference for actions const actions = {}; // % declare references for the websocket server const ip = process.env.INTERNAL_HOST_IP; const port = process.env.INTERNAL_WEBSOCKET_PORT; let server = null; // $ load all websocket actions recursively // ? The loader is loading all actions with a dot notation // ?? Usage: actionName || folder.actionName || folder.folder2.actionName .... async function loadActions(dir = wsActionsDir, prefix = '') { // ~ reset the action container to ensure a clean state during reloads for (const key in actions) delete actions[key]; // % read all content synchronously const files = fs.readdirSync(dir); // ~ loop through each file or folder found in the directory for (const file of files) { // % construct the absolute path to the current item const fullPath = path.resolve(dir, file); // ~ retrieve file system metadata const stat = fs.statSync(fullPath); // ~ check if item is a folder if (stat.isDirectory()) { // ~ recurse into subdirs and append the folder name to the prefix loadActions(fullPath, prefix + file + '.'); } else if (file.endsWith('.js')) { // ~ generate a unique action name by stripping the file extension and include the prefix const actionName = prefix + file.replace('.js', ''); // ~ invalidate the require cache to allow loading the updated code delete require.cache[require.resolve(fullPath)]; // ~ load the fresh module version into the actions registry actions[actionName] = require(fullPath); } } } // $ function to start the websocket server async function start() { // ~ initially load all websocket actions await loadActions(); try { // ~ set up a new websocket server const wss = new WebSocket.Server({ host: ip, port: port }); wss.on('connection', async (socket, req) => { // ~ authorize connection const url = new URL(req.url, `http://${req.headers.host}`); const key = url.searchParams.get('key'); const codename = url.searchParams.get('codename'); if (key !== process.env.INTERNAL_SECRET) { socket.close(1008, 'Unauthorized'); return; } // ~ get connection id and establish connection const id = await connectionManager.add(socket, codename); connectionManager.sendTo(codename, id); // ~ register websocket messages socket.on('message', async (data) => { // ~ parse json data const msg = JSON.parse(data); // % get user id and validate const valid = connectionManager.get(id); if (valid.socket.readyState !== valid.socket.OPEN) { connectionManager.remove(id); return; }; // % declare action type (e.g. folder.actionName) const type = msg.type; if (!type) { connectionManager.send(id, 'Action value is empty. Please declare an action value.'); return; }; // ~ set action handler const actionHandler = actions[msg.type]; if (!actionHandler) { connectionManager.send(id, `Action ${actionHandler} not found.`); return; } // ~ send action to handler try { await actionHandler(connectionManager, msg.data, id); } catch(e) { console.error('Error while trying to execute the websocket action!', e); return; } }); }); // ~ start the heartbeat system with the connection manager connectionManager.startBeat(); } catch(e) { console.error('Error while trying to start the websocket server', e); connectionManager.stopBeat(); } } // § export websocket server and functions module.exports = { start, loadActions };