NodeServer - Structure Analysis

Table of Contents

  1. Overview
  2. Technology Stack
  3. Architecture Patterns
  4. File Structure
  5. Code Organization
  6. Data Flow Architecture
  7. Connection Management
  8. Command Processing Pipeline
  9. Database Integration
  10. Push Notification System
  11. Multi-Tenancy Implementation
  12. State Management
  13. Concurrency Model
  14. 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

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.