Initial commit

This commit is contained in:
2026-06-29 20:14:06 +02:00
commit e0488f8081
21 changed files with 1840 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
/*
! /servers/websocket/actions/template.js A simple websocket action template
? This module is just a template for the websocket actions
*/
module.exports = (connectionManager, data, id) => {
};
+185
View File
@@ -0,0 +1,185 @@
/*
! /servers/websocket/connections.js Central WebSocket Connection Manager
? This module handles all websocket connections as a pool and automatically sends heartbeats
*/
// % import nodejs dependencies
const path = require('path');
// % import external dependencies
const rootDir = path.resolve(__dirname, '..', '..');
const { generatePrefixed } = require(path.resolve(rootDir, 'services', 'idgen.js'));
// % create a new connection mapping
let connections = new Map();
let registry = new Map();
// % set heartbeat settings
let heartbeatInterval = null;
const heartbeatTime = 5000; // 5 seconds
// $ add a connection
function add(socket, codename = null) {
// % generate a unique id for websocket connections
const id = generatePrefixed('ws');
// ~ add connection to connection pool
const conn = { socket, codename, lastHeartbeat: Date.now() };
connections.set(id, conn);
// ~ add registry keys if codename is provided
if (codename) registry.set(codename, id);
// ~ listen for 'pong' events from client to update heartbeat timestamp
socket.on('pong', () => conn.lastHeartbeat = Date.now());
// ~ error handling
socket.on('error', (err) => remove(id));
// ~ close connection
socket.on('close', () => remove(id));
// ~ return client id
return id;
}
// $ remove a connection by id
function remove(id) {
// % find id in connection map
const conn = connections.get(id);
// ~ close and remove connection
if (conn) {
// ~ cleanup registry if codename exists
if (conn.codename) registry.delete(conn.codename);
// ~ close connection
if (conn.socket.readyState === conn.socket.OPEN) conn.socket.close();
// ~ remove connection from pool
connections.delete(id);
}
}
// $ remove all connections
function removeAll() {
// ~ loop through connection mapping
for (const id of connections.keys()) {
remove(id);
}
}
// $ get a connection by id
function get(id) {
return connections.get(id);
}
// $ get a connection by codename
function getByCodename(codename) {
// % get id from codename
const id = registry.get(codename);
// ~ return connection
return id ? connections.get(id) : null;
}
// $ get all connections
function getAll() {
// % create a reference for all connections
const all = {};
// ~ loop through connections and fill "all" reference
for (const [id, data] of connections.entries()) {
all[id] = {
codename: data.codename,
lastHeartbeat: data.lastHeartbeat
};
}
// ~ return the connection reference
return all;
}
// $ send a message to a specific connection by id
function send(id, message) {
// % get id
const conn = connections.get(id);
// ~ check if connection exists and is open
if (conn && conn.socket.readyState === conn.socket.OPEN) {
conn.socket.send(JSON.stringify(message));
return true;
}
// ~ return immediately after a failure
return false;
}
// $ send a message to a specific connection by codename
function sendTo(codename, message) {
// % get id
const id = registry.get(codename);
// ~ send a message if id exists
if (id) return send(id, message);
// ~ immediately return if id not exists
return false;
}
// $ broadcast to all connections
function broadcast(message) {
// % serialize payload
const payload = JSON.stringify(message);
// ~ loop through all established connections
for (const conn of connections.values()) {
if (conn.socket.readyState === conn.socket.OPEN) {
conn.socket.send(payload);
}
}
}
// $ start the heartbeat system
function startBeat() {
if (heartbeatInterval) return;
heartbeatInterval = setInterval(() => {
const now = Date.now();
for (const [id, conn] of connections.entries()) {
// ~ send ping to active connections
if (conn.socket.readyState === conn.socket.OPEN) {
conn.socket.ping();
}
// ~ remove stale connections (no pong in 2 heartbeat intervals)
if (now - conn.lastHeartbeat > heartbeatTime * 2) {
remove(id);
}
}
}, heartbeatTime);
}
// $ stop the heartbeat system
function stopBeat() {
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
heartbeatInterval = null;
}
}
// § export the websocket connection module
module.exports = {
add,
remove,
removeAll,
get,
getByCodename,
getAll,
send,
sendTo,
broadcast,
startBeat,
stopBeat
};
+134
View File
@@ -0,0 +1,134 @@
/*
! /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 };