NodeServer - Structure Analysis¶
Table of Contents¶
- Overview
- Technology Stack
- Architecture Patterns
- File Structure
- Code Organization
- Data Flow Architecture
- Connection Management
- Command Processing Pipeline
- Database Integration
- Push Notification System
- Multi-Tenancy Implementation
- State Management
- Concurrency Model
- Scalability Analysis
Overview¶
The NodeServer is a monolithic WebSocket signaling server (~3000 lines of code in a single file) that serves as the real-time communication backbone for multiple telemedicine and learning management platforms.
Primary Responsibilities:
- WebSocket connection management
- Real-time messaging and presence
- WebRTC signaling for video/audio calls
- Push notification orchestration
- Multi-tenant database integration
Architectural Style:
- Event-driven (WebSocket message events)
- Callback-heavy (Node.js async patterns)
- Stateful (in-memory connection tracking)
- Monolithic (single-file implementation)
Technology Stack¶
Core Dependencies¶
| Package | Version | Purpose | Risk Level |
|---|---|---|---|
| ws | 7.2.1 | WebSocket server implementation | Low (mature) |
| mssql | 6.0.1 | MS SQL Server driver | Medium (old version) |
| firebase-admin | 8.9.1 | FCM push notifications | Medium (old version) |
| apn | 2.2.0 | Apple Push Notifications | Low |
| winston | 3.2.1 | Structured logging | Low |
| winston-daily-rotate-file | 4.4.1 | Log rotation | Low |
| yargs | 15.1.0 | CLI argument parsing | Low |
| jsonfile | 5.0.0 | JSON file reading | Low |
Security Observations:
- firebase-admin@8.9.1 is outdated (current: 12.x) - may have security vulnerabilities
- mssql@6.0.1 is outdated (current: 10.x) - missing performance improvements
- No dependency security scanning in package.json
Recommendations:
npm audit fix # Fix known vulnerabilities
npm update winston # Update logging
npm install firebase-admin@latest # Update Firebase SDK
npm install mssql@latest # Update SQL driver
Architecture Patterns¶
1. Monolithic Single-File Design¶
Current State:
server.js (2968 lines)
├── Constants & Enums
├── Configuration
├── Server Initialization
├── Connection Handling
├── Presence Mode Functions (800+ lines)
├── Collaboration Mode Functions (600+ lines)
├── Messaging Functions (400+ lines)
└── Utility Functions
Pros:
- Simple deployment (single file)
- No module resolution complexity
- Easy to understand data flow
Cons:
- Difficult to maintain (3000 LOC)
- No code reusability
- Testing challenges (no modularity)
- Merge conflicts in teams
- Difficult to refactor
Refactoring Recommendation:
src/
├── index.js (entry point)
├── config/
│ ├── constants.js (cCommand, cReason, etc.)
│ ├── database.js (per-tenant DB configs)
│ ├── firebase.js (FCM settings)
│ └── ssl.js (SSL cert loading)
├── handlers/
│ ├── presenceHandler.js (P_* commands)
│ ├── collaborationHandler.js (C_* commands)
│ └── messagingHandler.js (messaging functions)
├── services/
│ ├── authService.js (authentication logic)
│ ├── notificationService.js (FCM/APN)
│ └── dbService.js (SQL query wrappers)
├── models/
│ ├── User.js
│ ├── Collaboration.js
│ └── Message.js
└── utils/
├── logger.js
├── tokenGenerator.js
└── validators.js
2. Event-Driven Architecture¶
WebSocket Event Flow:
Client Connection
↓
wss.on('connection')
↓
Parse Query Params
↓
Route to Authentication
↓
client.on('message')
↓
Parse JSON Command
↓
Route to P/C Handler
↓
Process Command
↓
Send Response
↓
client.on('close')
↓
Cleanup Resources
Event Handlers:
wss.on('connection', (client, request) => {
// Initial connection setup
client.on('message', (data) => {
// Message routing
});
client.on('close', (code, data) => {
// Cleanup
});
client.on('error', (error) => {
// Error handling
});
client.on('pong', heartbeat);
});
Observations:
- No centralized event dispatcher (commands parsed inline)
- Heavy use of callbacks (callback hell potential)
- No Promise/async-await patterns (2018 code style)
Modernization Recommendation:
// Convert to async/await
async function handleMessage(client, data) {
const message = JSON.parse(data);
try {
switch(message.Command) {
case cCommand.P_AUTHENTICATE:
await handleAuthentication(client, message);
break;
// ...
}
} catch (error) {
await handleError(client, error);
}
}
// Use Promise-based SQL queries
async function authenticateUser(communicationKey) {
const request = new sql.Request(connection);
const result = await request
.input('verificationCode', sql.NVarChar(100), communicationKey)
.execute('COB_Authenticate_Get_User_List');
return result.recordsets[0];
}
3. State Management Pattern¶
In-Memory State:
var userList = []; // All authenticated users
var pUserConnectionList = []; // Presence connections
var cUserConnectionList = []; // Collaboration connections
var collaborationList = []; // Active sessions
var connection = null; // SQL connection (singleton)
State Object Structures:
User Object:
{
Id: 42,
UserInfo: {
Id: 42,
Fullname: "John Doe",
Email: "john@example.com",
Status: 2, // ONLINE
CollaborationStatus: 1, // AVAILABLE
NetworkStatus: 7, // NETWORK_OK
// Tenant-specific fields
PatientId?: 123,
ServiceProviderId?: 456,
IsPatient?: true
},
UserList: [/* Friend list */]
}
WebSocket Client Object:
{
Id: 1636540800000, // Timestamp-based ID
deviceId: "android_device_123",
clientPlatform: 2, // MOBILE_ANDROID
wsClient: WebSocket // ws library object
}
Connection List Object:
{
Id: 42, // User ID
wsClientList: [
{ Id: 123, deviceId: "android_1", wsClient: WebSocket },
{ Id: 456, deviceId: "web_1", wsClient: WebSocket }
]
}
Collaboration Object:
{
Key: "uuid-1234-5678",
Initiator: { /* User object */ },
ParticipantList: [{ /* User objects */ }],
BookingId: 789,
IsScheduled: 1,
StartTime: "2025-11-10 12:00:00",
TimeLimit: 60, // seconds
CountdownTimer: setTimeout() // Node.js timer reference
}
State Management Issues:
- No persistence (all in RAM)
- No Redis/external cache
- Server restart loses all connections
- Single-server limitation (can’t scale horizontally)
Scalability Recommendation:
// Use Redis for shared state
const Redis = require('ioredis');
const redis = new Redis();
// Store user presence in Redis
async function setUserOnline(userId, deviceId) {
await redis.hset(`user:${userId}`, 'status', 'ONLINE');
await redis.sadd(`user:${userId}:devices`, deviceId);
await redis.expire(`user:${userId}`, 3600); // 1 hour TTL
}
// Pub/Sub for multi-server communication
redis.subscribe('collaboration:events');
redis.on('message', (channel, message) => {
broadcastToLocalClients(JSON.parse(message));
});
File Structure¶
Project Layout¶
NodeServer/
├── server.js # Main server (2968 lines)
│ ├── Lines 1-150: Constants & Enums
│ ├── Lines 150-250: Configuration & Init
│ ├── Lines 250-400: Connection Handling
│ ├── Lines 400-2200: Presence Functions
│ ├── Lines 2200-2800: Collaboration Functions
│ └── Lines 2800-2968: Messaging Functions
│
├── package.json # Dependencies
├── package-lock.json # Dependency lock
├── config.json # Runtime config
│ └── { "enableLog": "true" }
│
├── restart.sh # Linux restart script
│
├── ssl/ # SSL certificates
│ ├── PSYTER_DEV/
│ │ ├── key_live.pem
│ │ └── cert_live.pem
│ ├── PSYTER_LIVE/
│ ├── QURAN_DEV/
│ ├── QURAN_LMS/
│ ├── ASSESSMENTSYSTEM_*/
│ └── ...
│
└── node_modules/ # Installed packages
No Modular Structure¶
- Missing: Separate files for handlers
- Missing: Test directory
- Missing: Environment config files
- Missing: Database migration scripts
- Missing: API documentation
Code Organization¶
Function Categorization¶
1. Presence Mode Functions (Prefix: P)
| Function | Lines | Purpose |
|---|---|---|
PProcessCommand |
~50 | Routes incoming presence commands |
PAuthenticate |
~40 | Communication key authentication |
PAuthenticateUserPass |
~40 | Username/password authentication |
PAuthenticateClientKey |
~30 | Client key authentication |
PAuthenticateSuccess |
~60 | Post-auth setup and broadcast |
PSignOut |
~20 | User logout and cleanup |
PSendToUser |
~20 | Direct message routing |
PStatus |
~30 | Status change handling |
PBroadCast |
~80 | Broadcast to friend lists |
PCollaborationBroadCast |
~40 | Call invitation broadcast |
PRejectCollaboration |
~10 | Call rejection handler |
PNoAnswerCollaboration |
~10 | Missed call handler |
PGetAllUser |
~40 | Fetch all users from DB |
PManageUserFriend |
~60 | Add/remove friends |
PUpdatePassword |
~40 | Password change |
PRegisterUser |
~50 | New user registration |
PSendMessage |
~80 | Chat message storage |
PGetNewMessageCount |
~40 | Unread count |
PGetUserConversationsList |
~50 | Conversation list |
PGetUserConversationMessageList |
~60 | Message history |
PUpdateMessageStatus |
~60 | Read receipts |
PSendCommand |
~40 | Send JSON to clients |
PClose |
~20 | Close presence connection |
2. Collaboration Mode Functions (Prefix: C)
| Function | Lines | Purpose |
|---|---|---|
CProcessCommand |
~100 | Routes collaboration commands |
CInitiate |
~80 | Join collaboration session |
CReInitiate |
~50 | Reconnect to session |
CCreateCollaboration |
~30 | Start new call |
CBroadCast |
~40 | Broadcast to session participants |
CCollaborationBroadCast |
~60 | Initiate call broadcast |
CPresenceBroadCast |
~40 | Update presence for collab users (commented out) |
CPresenceBroadCastAndNotify |
~10 | Presence + FCM notification |
CNotify |
~30 | Send FCM for collab events |
CBroadCastFCMNotification |
~50 | Send FCM to participants |
CLeaveCollaboration |
~10 | User leaves call |
CEndCollaboration |
~10 | End entire call |
CEndScreenShare |
~20 | Stop screen sharing |
CTimeLimit |
~40 | Set session time limit |
CConsumptionInsert |
~80 | Log call duration to DB |
CUpdateCount |
~40 | Update service count |
CClearCollaboration |
~20 | Remove from collaboration list |
CSendCommand |
~40 | Send JSON to collab clients |
CClose |
~10 | Close collaboration connection |
3. Utility Functions
| Function | Lines | Purpose |
|---|---|---|
setClientMode |
~500 | Set DB/Firebase config per tenant |
sqlConnect |
~20 | Establish SQL connection |
loadFCMAPISettings |
~10 | Initialize Firebase |
SendFCMNotification |
~100 | Send Firebase notification |
UniqueToken |
~10 | Generate UUID for collab keys |
logInfo |
~20 | Winston logging wrapper |
getUserInfo |
~20 | Format user for logging |
getUserId |
~20 | Extract user ID safely |
getBoolean |
~10 | Parse boolean from config |
getQueryVariable |
~15 | Parse URL query params |
getCurrentUTCDateTime |
~10 | UTC timestamp generator |
CleanSocketConnectionList |
~20 | Remove closed connections |
CleanOtherUserList |
~10 | Remove stale users |
OnUnExpectedConnectionClose |
~20 | Handle abrupt disconnects |
SetUserNetworkStatus |
~20 | Update network quality |
SetUserCollaborationStatus |
~15 | Update call availability |
IsPresenceConnectiionAvailable |
~20 | Check if user online |
validateJSONString |
~10 | Validate JSON input |
Coupling Analysis¶
Tight Coupling:
- All functions share global variables (userList, pUserConnectionList, etc.)
- Database configuration embedded in setClientMode()
- Firebase configuration embedded in setClientMode()
- No dependency injection
Example of Tight Coupling:
function PAuthenticate(command, client, data) {
// Directly accesses global:
// - connection (SQL)
// - userList
// - pUserConnectionList
var request = new sql.Request(connection); // Global
// ...
userList.push(userObject); // Global
pUserConnectionList.push(pUserConnectionObject); // Global
}
Refactoring to Dependency Injection:
class PresenceHandler {
constructor(dbConnection, userStore, logger) {
this.db = dbConnection;
this.users = userStore;
this.logger = logger;
}
async authenticate(client, data) {
const request = new sql.Request(this.db);
// ...
await this.users.addUser(userObject);
this.logger.info('User authenticated');
}
}
Data Flow Architecture¶
Connection Lifecycle¶
┌─────────────────────────────────────────────────────────┐
│ Client Connection │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ wss.on('connection', (client, request)) │
│ - Parse URL query parameters │
│ - Validate connectionMode │
└─────────────────────────────────────────────────────────┘
│
┌──────────────┴──────────────┐
│ │
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ Presence Mode (1) │ │ Collaboration (2) │
│ - Authentication │ │ - Join session │
│ - Status updates │ │ - WebRTC signals │
│ - Messaging │ │ - Call controls │
└─────────────────────┘ └─────────────────────┘
│ │
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ client.on('message', (data)) │
│ - Parse JSON │
│ - Extract Command, Message, FromUser, ToUserList │
└─────────────────────────────────────────────────────────┘
│
┌──────────────┴──────────────┐
│ │
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ PProcessCommand() │ │ CProcessCommand() │
│ - P_* commands │ │ - C_* commands │
└─────────────────────┘ └─────────────────────┘
│ │
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Business Logic Layer │
│ - Database queries (SQL Server) │
│ - State updates (userList, collaborationList) │
│ - Push notifications (FCM/APN) │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ PSendCommand() / CSendCommand() │
│ - Serialize to JSON │
│ - ws.send() to client(s) │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ client.on('close', (code, data)) │
│ - Cleanup connections │
│ - Broadcast sign-out │
│ - Clear collaboration │
└─────────────────────────────────────────────────────────┘
Message Routing Logic¶
Presence Commands:
Client → P_AUTHENTICATE
↓
Server queries DB (COB_Authenticate_Get_User_List)
↓
Server adds to userList, pUserConnectionList
↓
Server broadcasts P_SIGNIN to user's friends
↓
Server sends P_AUTHENTICATE response to client
Collaboration Commands:
Initiator → C_CREATE_COLLABORATION
↓
Server generates collaborationKey
↓
Server adds to collaborationList
↓
Server sends P_ACCEPT_COLLABORATION to participants (via FCM if offline)
↓
Participants connect with connectionMode=2&collaborationKey=...
↓
Server routes C_SDP, C_ICECANDIDATE between peers
↓
Initiator/Participant → C_END_COLLABORATION
↓
Server calls CConsumptionInsert() to log duration
↓
Server broadcasts C_END_COLLABORATION
↓
Server clears collaborationList entry
Chat Message Flow:
Sender → P_SEND_MESSAGE
↓
Server calls Message_InsertMessage stored procedure
↓
Server sends P_RECIEVE_MESSAGE to recipient (if online)
↓
Server sends PSendFCMNotification (if offline)
↓
Server sends P_SEND_MESSAGE response to sender
Connection Management¶
WebSocket Connection Storage¶
Two-Tier Storage:
// Tier 1: User-level aggregation
pUserConnectionList = [
{
Id: 42, // User ID
wsClientList: [/* Tier 2 */]
}
];
// Tier 2: Device-level connections
wsClientList = [
{
Id: 1636540800000, // Client ID (timestamp)
deviceId: "android_123",
clientPlatform: 2, // MOBILE_ANDROID
wsClient: WebSocket
}
];
Multi-Device Support:
- Same user can have multiple connections (different devices)
- Each device identified by deviceId query parameter
- Broadcasts sent to ALL user’s devices
Example:
// User 42 has 3 devices connected
pUserConnectionList = [
{
Id: 42,
wsClientList: [
{ deviceId: "android_phone", wsClient: WebSocket1 },
{ deviceId: "web_browser", wsClient: WebSocket2 },
{ deviceId: "android_tablet", wsClient: WebSocket3 }
]
}
];
// Broadcast to all 3 devices
PSendCommand(pUserConnection[0].wsClientList, message, command, reason);
Connection Cleanup Mechanisms¶
1. Heartbeat/Ping-Pong:
setInterval(function ping() {
wss.clients.forEach(function each(ws) {
if (ws.isAlive === false) {
OnUnExpectedConnectionClose(ws);
return ws.terminate();
}
ws.isAlive = false;
ws.ping(noop);
});
}, (30 * 60 * 1000)); // Every 30 minutes
2. Close Event Cleanup:
client.on('close', function (code, data) {
// Removes from pUserConnectionList/cUserConnectionList
// Broadcasts P_SIGNOUT if last device
CleanSocketConnectionList();
CleanOtherUserList();
});
3. Manual Cleanup Functions:
function CleanSocketConnectionList() {
// Filter out closed connections
pUserConnectionList.forEach(pUserConnection => {
pUserConnection.wsClientList = pUserConnection.wsClientList.filter(
p => p.wsClient.readyState === ws.OPEN
);
});
// Remove users with no active connections
pUserConnectionList = pUserConnectionList.filter(
p => p.wsClientList.length > 0
);
}
Cleanup Issues:
- Cleanup runs every 30 minutes (long delay)
- Abrupt disconnects may not trigger cleanup immediately
- Memory leak potential if cleanup fails
Recommendation:
- Reduce heartbeat interval to 5 minutes
- Add connection timeout (close after 10 min inactivity)
- Monitor connection counts in production
Command Processing Pipeline¶
Command Enum Structure¶
66 Commands Defined:
cCommand = {
// Presence Commands (P_*)
P_AUTHENTICATE: 1,
P_SIGNIN: 2,
P_SIGNOUT: 3,
P_CLOSE: 4,
P_CHAT: 5,
// ... 20 more presence commands
// Collaboration Commands (C_*)
C_INITIATE: 12,
C_CREATE_COLLABORATION: 13,
C_SDP: 16,
C_ICECANDIDATE: 17,
// ... 15 more collaboration commands
// Shared/Special
COMMAND_RECEIVED_ACK: 40,
P_FILE_START: 47,
P_FILE_DATA: 48,
P_FILE_END: 49,
// ...
};
Command Routing¶
Presence Mode Routing:
function PProcessCommand(client, data, command, fromUser, toUserList, ...) {
switch (command) {
case cCommand.P_AUTHENTICATE:
PAuthenticate(command, client, data);
break;
case cCommand.P_SIGNOUT:
PSignOut(client, fromUser.Id);
break;
case cCommand.P_CHAT:
PSendToUser(command, data, fromUser, toUserList);
break;
case cCommand.P_SEND_MESSAGE:
PSendMessage(client, command, data, fromUser, toUserList, clientInitiationTime);
break;
// ... 20+ more cases
default:
// No default handler! Silently ignored.
}
}
Collaboration Mode Routing:
function CProcessCommand(client, data, command, collaborationKey, ...) {
switch (command) {
case cCommand.C_INITIATE:
CInitiate(client, data, collaborationKey);
break;
case cCommand.C_CREATE_COLLABORATION:
CCreateCollaboration(client, command, fromUser, toUserList, ...);
break;
case cCommand.C_SDP:
case cCommand.C_ICECANDIDATE:
CBroadCast(command, data, fromUser, collaborationKey, cReason.SUCCESS);
break;
// ... 15+ more cases
}
}
Routing Issues:
- No default case (unknown commands silently ignored)
- No command validation (client can send any integer)
- No rate limiting per command
- No command-specific permissions
Recommendation:
function PProcessCommand(client, data, command, ...) {
// Validate command
if (!isValidPresenceCommand(command)) {
logInfo(`Invalid command: ${command}`, logType.ERROR);
PClose(client, 'Invalid command', cReason.INVALID_REQUEST);
return;
}
// Rate limiting
if (!checkRateLimit(client, command)) {
logInfo(`Rate limit exceeded for user ${client.UserId}`, logType.ERROR);
return;
}
// Permission check
if (!hasPermission(client, command)) {
PClose(client, 'Permission denied', cReason.NO_PERMISSION);
return;
}
// Route to handler
const handler = presenceHandlers[command];
if (handler) {
handler(client, data, ...);
} else {
logInfo(`No handler for command ${command}`, logType.ERROR);
}
}
Database Integration¶
Connection Management¶
Single Global Connection:
var connection = null; // SQL connection object
function sqlConnect() {
connection = sql.connect(dbConfig, function (err) {
if (err) {
logInfo("sql.connect --> err = " + err, logType.ERROR);
} else {
logInfo("Sql connection is established");
loadFCMAPISettings();
}
});
}
sql.on('error', err => {
logInfo("sql --> on(error) --> err = " + err, logType.ERROR);
});
Issues:
- No reconnection logic - If connection dies, server breaks
- No connection pooling - Single connection shared by all requests
- No timeout handling - Queries can hang indefinitely
Recommendation:
const sql = require('mssql');
const pool = new sql.ConnectionPool({
...dbConfig,
pool: {
max: 10,
min: 2,
idleTimeoutMillis: 30000
},
options: {
connectTimeout: 30000,
requestTimeout: 30000
}
});
async function connectDB() {
try {
await pool.connect();
logInfo("Database pool connected");
} catch (err) {
logInfo(`DB connection failed: ${err}`, logType.ERROR);
setTimeout(connectDB, 5000); // Retry after 5s
}
}
pool.on('error', err => {
logInfo(`Pool error: ${err}`, logType.ERROR);
});
Stored Procedure Calls¶
Pattern Used:
var request = new sql.Request(connection);
request
.input('verificationCode', sql.NVarChar(100), data.communicationKey)
.input('authenticateOnly', sql.Int, data.authenticateOnly)
.execute('COB_Authenticate_Get_User_List', (err, result) => {
if (err != null) {
logInfo("Error: " + err, logType.ERROR);
PClose(client, err.message, cReason.DATABASE_NOT_AVAILABLE);
return;
}
if (result !== undefined && result.recordsets.length > 1) {
var objUser = result.recordsets[0];
// Process result
}
});
Stored Procedures Called:
Authentication:
- COB_Authenticate (Client key auth)
- COB_Authenticate_Get_User_List (User auth + friend list)
- COB_Get_User_List (All users)
- COB_Get_Related_User_List (Friend list)
- COB_Create_User (User registration)
- COB_Update_Password (Password change)
- COB_Manage_User_Friend (Add/remove friend)
Collaboration:
- Mobile_Package_ConsumptionDetail_Update (Assessment System - log call duration)
- SP_ManageCareProvidersServiceCount (Psyter - update service count)
- SP_GetBookingDetailsById (Fetch booking info)
Messaging:
- Message_InsertMessage (Save message)
- Message_GetNewMessagesCount (Unread count)
- Message_GetUserConversationsList (Conversation list)
- Message_GetUserConversationMessages (Message history)
- Message_UpdateMessageStatus (Read receipts)
Query Safety:
- ✅ Parameterized queries - Uses request.input() (safe from SQL injection)
- ✅ Type enforcement - Specifies SQL data types (sql.NVarChar, sql.BigInt)
- ❌ No retry logic - Transient errors not handled
- ❌ No transaction management - Multi-step operations not atomic
Push Notification System¶
Firebase Cloud Messaging (FCM)¶
Initialization:
admin.initializeApp({
credential: admin.credential.cert({
projectId: fcmAPISettings.ProjectId,
clientEmail: fcmAPISettings.ClientEmail,
privateKey: fcmAPISettings.PrivateKey
}),
databaseURL: fcmAPISettings.DatabaseURL
});
Notification Structure:
function SendFCMNotification(topic, title, body, fromUser, toUser, collaborationKey, retryNotify, isPresenceNotification) {
var message = {
topic: topic, // Or condition with multiple topics
data: {
title: title,
body: body
},
android: {
priority: "high"
},
apns: {
payload: {
aps: {
alert: {
title: "Incoming call",
body: fromUser.Fullname
},
sound: "ringtone.mp3"
},
collaborationData: body
}
}
};
admin.messaging().send(message)
.then(response => { /* Success */ })
.catch(error => { /* Retry or mark unreachable */ });
}
Topic Naming:
// Psyter
if (toUser.IsPatient) {
topic = 'patient_' + toUser.PatientId + '~';
} else {
topic = 'doctor_' + toUser.ServiceProviderId + '~';
}
// Assessment Systems
if (toUser.IsStudent) {
topic = 'student_' + toUser.StudentId + '~';
} else {
topic = 'staff_' + toUser.StaffId + '~';
}
// Generic
topic = 'user_' + toUser.Id + '~';
Notification Scenarios:
1. Incoming Call: P_COLLABORATION command sent via FCM
2. Chat Message: P_RECIEVE_MESSAGE sent if user offline
3. Call Rejection: C_UNREACHABLE if FCM fails
Apple Push Notifications (APN)¶
Configuration:
var apnOptions = {
token: {
key: "ssl/AuthKey_L7XHN784FM.p8",
keyId: "L7XHN784FM",
teamId: "EV53AQE262"
},
production: false
};
apnProvider = new apn.Provider(apnOptions);
VoIP Notification:
var note = new apn.Notification();
note.expiry = Math.floor(Date.now() / 1000) + 3600;
note.payload = {
aps: {
alert: {
title: "Incoming call",
body: fromUser.Fullname
},
sound: "ringtone.mp3"
},
collaborationData: body
};
note.topic = "com.innotech-sa.Quran-University.voip";
apnProvider.send(note, deviceToken).then(result => {
// Handle result
});
APN Issues:
- Commented out - APN code is commented, not actively used
- Hardcoded device token - deviceToken variable hardcoded
- No error handling - Failed sends not logged
Multi-Tenancy Implementation¶
Tenant Configuration¶
12 Tenants Supported:
cClient = {
QURAN_DEV: 1,
QURAN_LMS: 2,
PSYTER_DEV: 3,
PSYTER_LIVE: 4,
ONE2ONE: 5,
ASSESSMENTSYSTEM_DEV: 6,
ASSESSMENTSYSTEM_AFU: 7,
ASSESSMENTSYSTEM_PSU: 8,
ASSESSMENTSYSTEM_KSU: 9,
ASSESSMENTSYSTEM_MBRU: 10,
ASSESSMENTSYSTEM_TAIF: 11,
NARAAKUM_DEV: 12
};
Tenant Isolation¶
Database Separation:
switch (currentClientMode) {
case cClient.PSYTER_DEV:
dbConfig = {
server: 'db.innotech-sa.com',
database: 'Psyter_v1'
};
break;
case cClient.PSYTER_LIVE:
dbConfig = {
server: '51.89.234.59',
database: 'Psyter'
};
break;
// ... 10 more cases
}
Firebase Project Separation:
switch (currentClientMode) {
case cClient.PSYTER_DEV:
fcmAPISettings = {
ProjectId: "psyterdev",
ClientEmail: "firebase-adminsdk-uh0r6@psyterdev.iam.gserviceaccount.com",
// ...
};
break;
// ... other tenants
}
SSL Certificate Separation:
var pkey = fs.readFileSync(`ssl/${getKeyByValue(cClient, currentClientMode)}/key_live.pem`);
var pcert = fs.readFileSync(`ssl/${getKeyByValue(cClient, currentClientMode)}/cert_live.pem`);
Port Assignment:
- QURAN_DEV: 2333
- QURAN_LMS: 1443
- PSYTER_DEV: 3333
- PSYTER_LIVE: 3223
- ONE2ONE: 4223
- ASSESSMENTSYSTEM_*: 5223-5228
- NARAAKUM_DEV: 6223
Tenant-Specific Logic¶
Assessment Systems - Student Login Restriction:
if ((currentClientMode == cClient.ASSESSMENTSYSTEM_DEV ||
currentClientMode == cClient.ASSESSMENTSYSTEM_AFU ||
/* ... other assessment modes */) &&
objUser[0].IsStudent) {
PClose(client, '', cReason.ALREADY_LOGGED_IN); // Deny duplicate login
return;
}
Psyter - Service Count Tracking:
if (currentClientMode == cClient.PSYTER_LIVE ||
currentClientMode == cClient.PSYTER_DEV) {
request.execute('SP_ManageCareProvidersServiceCount', ...);
}
Multi-Tenancy Issues¶
- No tenant validation - Client can’t connect to wrong tenant accidentally
- Configuration duplication - Each tenant’s config hardcoded
- No dynamic tenant addition - New tenant requires code change
- Shared codebase - Bug in one tenant affects all
Recommendation:
// Externalize to JSON config file
const tenants = require('./tenants.json');
{
"PSYTER_DEV": {
"id": 3,
"port": 3333,
"database": {
"server": "db.innotech-sa.com",
"database": "Psyter_v1",
"user": "PsyterUser"
},
"firebase": {
"projectId": "psyterdev",
"credentialsFile": "firebase/psyter-dev.json"
},
"ssl": {
"keyFile": "ssl/PSYTER_DEV/key.pem",
"certFile": "ssl/PSYTER_DEV/cert.pem"
}
},
// ... other tenants
}
State Management¶
In-Memory State Objects¶
Global Variables:
var userList = []; // All authenticated users
var pUserConnectionList = []; // Presence WebSocket connections
var cUserConnectionList = []; // Collaboration WebSocket connections
var collaborationList = []; // Active collaboration sessions
State Mutation Points¶
User State Changes:
- Login: PAuthenticateSuccess() → Adds to userList, pUserConnectionList
- Logout: PSignOut() → Removes from pUserConnectionList, userList
- Status Change: PStatus() → Updates userInfo.Status
- Network Status: SetUserNetworkStatus() → Updates userInfo.NetworkStatus
- Collaboration Status: SetUserCollaborationStatus() → Updates userInfo.CollaborationStatus
Collaboration State Changes:
- Create: CCreateCollaboration() → Adds to collaborationList
- Join: CInitiate() → Adds to cUserConnectionList
- Leave: CLeaveCollaboration() → Removes from cUserConnectionList, collaborationList
- Time Limit: CTimeLimit() → Sets CountdownTimer on collaboration object
State Persistence¶
Current State:
- ❌ No persistence - All state in RAM
- ❌ No snapshot/restore - Server restart loses everything
- ❌ No distributed state - Can’t run multiple instances
Impact of Server Restart:
1. All users disconnected
2. All active calls ended abruptly
3. Undelivered messages lost (if not yet persisted to DB)
4. User status reset to offline
Recommendation - Redis Integration:
const Redis = require('ioredis');
const redis = new Redis();
// Store user presence
async function addUserPresence(userId, userInfo) {
await redis.hset(`user:${userId}`, {
fullname: userInfo.Fullname,
status: userInfo.Status,
collaborationStatus: userInfo.CollaborationStatus
});
await redis.expire(`user:${userId}`, 3600);
}
// Store collaboration
async function createCollaboration(key, initiator, participants) {
await redis.hset(`collab:${key}`, {
initiator: JSON.stringify(initiator),
participants: JSON.stringify(participants),
startTime: Date.now()
});
}
// Pub/Sub for multi-server sync
redis.subscribe('user:status:*');
redis.on('message', (channel, message) => {
const [, , userId] = channel.split(':');
broadcastStatusChange(userId, JSON.parse(message));
});
Concurrency Model¶
Node.js Event Loop¶
Single-Threaded Architecture:
Event Loop (Single Thread)
↓
┌────────────────────────────┐
│ Incoming WebSocket Event │
├────────────────────────────┤
│ Parse Message │
│ Route to Handler │
│ Execute Business Logic │
│ Database I/O (async) │ ← Non-blocking
│ Push Notification (async) │ ← Non-blocking
│ Send Response │
└────────────────────────────┘
↓
Next Event in Queue
Concurrency Characteristics:
- ✅ Non-blocking I/O - Database and HTTP calls don’t block event loop
- ✅ Callback-based async - Uses callbacks (old Node.js style)
- ❌ Callback hell - Deep nesting in some functions
- ❌ No async/await - Misses modern Node.js patterns
Race Conditions¶
Potential Race Condition Example:
// Thread 1: User A sends collaboration request to User B
CCreateCollaboration(userA, [userB], ...);
↓
collaborationList.push(collaboration); // Step 1
// Thread 2: User B simultaneously creates collaboration to User A
CCreateCollaboration(userB, [userA], ...);
↓
collaborationList.push(collaboration); // Step 2
// Result: Two separate collaborations instead of one
No Mutual Exclusion:
- JavaScript is single-threaded, so no traditional race conditions
- However, callback ordering can cause logical races
- No distributed locking for multi-server scenarios
Recommendation:
// Use Redis for distributed locking
const Redlock = require('redlock');
const redlock = new Redlock([redis]);
async function createCollaboration(fromUser, toUserList) {
const participants = [fromUser, ...toUserList].sort((a, b) => a.Id - b.Id);
const lockKey = `collab:${participants.map(u => u.Id).join(':')}`;
const lock = await redlock.lock(lockKey, 1000); // 1s lock
try {
// Check if collaboration already exists
const existing = await redis.get(lockKey);
if (existing) {
return JSON.parse(existing);
}
// Create new collaboration
const collab = { Key: UniqueToken(), Initiator: fromUser, ... };
await redis.set(lockKey, JSON.stringify(collab), 'EX', 3600);
return collab;
} finally {
await lock.unlock();
}
}
Scalability Analysis¶
Current Limitations¶
1. Single-Server Architecture
- Bottleneck: All connections handled by one process
- Max Connections: ~10,000 (OS/hardware dependent)
- CPU Bound: JavaScript is single-threaded
- Memory Bound: All state in RAM of one server
2. No Load Balancing
- Can’t distribute load across multiple servers
- WebSocket sticky sessions required if load-balanced
3. No Horizontal Scaling
- State is local (not shared via Redis)
- No pub/sub for cross-server communication
- Adding servers would create isolated silos
Scalability Recommendations¶
Phase 1: Vertical Scaling (Short-term)
- Increase server RAM (16GB → 64GB)
- Use Node.js clustering (PM2 cluster mode)
- Optimize database queries (add indexes)
Phase 2: Horizontal Scaling (Long-term)
┌─────────────────┐
│ Load Balancer │ (NGINX with ip_hash for sticky sessions)
└────────┬────────┘
│
┌────┴────┬────────────┬─────────────┐
│ │ │ │
┌───▼───┐ ┌──▼────┐ ┌───▼────┐ ┌────▼────┐
│Node 1 │ │Node 2 │ │ Node 3 │ │ Node 4 │
└───┬───┘ └───┬───┘ └───┬────┘ └────┬────┘
│ │ │ │
└─────────┴────────────┴────────────┘
│
┌──────▼───────┐
│ Redis Cluster│ (Shared state + Pub/Sub)
└──────────────┘
Redis Pub/Sub Implementation:
// Node 1 publishes user status change
redis.publish('user:status:42', JSON.stringify({
userId: 42,
status: 'ONLINE',
timestamp: Date.now()
}));
// All nodes subscribe and update local state
redis.subscribe('user:status:*');
redis.on('message', (channel, message) => {
const data = JSON.parse(message);
updateLocalUserCache(data.userId, data.status);
broadcastToLocalClients(data);
});
Performance Metrics¶
Monitor These:
- WebSocket Connection Count: ccCount variable
- Memory Usage: process.memoryUsage().heapUsed
- Event Loop Lag: Measure with process.hrtime()
- Database Query Time: Log slow queries (>100ms)
- Message Throughput: Messages/second
Example Monitoring:
const EventLoopMonitor = require('event-loop-monitor');
EventLoopMonitor.on('data', (data) => {
if (data.delay > 100) { // >100ms lag
logInfo(`Event loop lag: ${data.delay}ms`, logType.ERROR);
}
});
setInterval(() => {
const mem = process.memoryUsage();
logInfo(`Memory: ${mem.heapUsed / 1024 / 1024}MB`, logType.INFO);
logInfo(`Connections: ${ccCount}`, logType.INFO);
}, 60000); // Every minute
Summary & Next Steps¶
Architecture Strengths¶
- ✅ Simple deployment (single file)
- ✅ WebSocket management robust
- ✅ Multi-tenant support
- ✅ Parameterized SQL queries (safe)
- ✅ Push notification integration
Critical Issues¶
- ❌ Monolithic structure (3000 LOC in one file)
- ❌ No horizontal scalability
- ❌ No state persistence
- ❌ Outdated dependencies (security risk)
- ❌ No automated tests
- ❌ Hardcoded credentials
Recommended Refactoring Steps¶
Phase 1: Safety & Security (Immediate)
1. Update dependencies (npm audit fix)
2. Externalize credentials to environment variables
3. Add input validation for all commands
4. Add rate limiting
Phase 2: Maintainability (1-2 months)
1. Split into modules (handlers, services, models)
2. Add TypeScript for type safety
3. Write unit tests (Mocha/Jest)
4. Add ESLint/Prettier
Phase 3: Scalability (3-6 months)
1. Integrate Redis for shared state
2. Add pub/sub for multi-server communication
3. Implement health check endpoints
4. Add Prometheus metrics
5. Set up horizontal scaling infrastructure
Phase 4: Operations (Ongoing)
1. Add monitoring/alerting (Grafana)
2. Create runbooks for common issues
3. Automate deployment (CI/CD)
4. Perform load testing
This structure analysis provides a complete technical understanding of the NodeServer architecture, identifying both strengths and critical improvement areas for the development team to prioritize.