Real-time Communication System¶
Relevant source files * index.js * src/sockets/socketHandler.js * views/soporte.ejs
Purpose and Scope¶
This document describes the Socket.IO-based real-time communication system that enables bidirectional messaging between users and administrators. The system implements a support chat feature with WebSocket connections, JWT-based authentication, room-based message routing, and database persistence.
For information about the HTTP API endpoints that complement this system, see API Endpoints. For the user interface implementation, see Support Chat System.
Sources: index.js, src/sockets/socketHandler.js, views/soporte.ejs
System Architecture¶
The real-time communication system consists of three primary components:
| Component | File | Responsibility |
|---|---|---|
| Socket.IO Server | index.js:9-12 | HTTP server creation and Socket.IO attachment |
| Socket Handler | src/sockets/socketHandler.js | WebSocket authentication, connection management, event handling |
| Client Interface | views/soporte.ejs:37-189 | Browser-side Socket.IO client and UI logic |
The system uses a dual-channel architecture where WebSocket connections handle real-time message delivery while HTTP endpoints provide historical message retrieval.
Sources: index.js, src/sockets/socketHandler.js
Socket.IO Server Initialization¶
flowchart TD
A["http.createServer(app)"]
B["socketIO(server)"]
C["setupSocket(io)"]
D["io.use(authMiddleware)"]
E["io.on('connection', handler)"]
F["server.listen(3000)"]
G["Express App"]
H["src/sockets/socketHandler.js"]
I["JWT Authentication"]
J["Event Handlers"]
K["Room Management"]
A --> B
B --> C
C --> D
D --> E
E --> F
G --> A
H --> C
I --> D
J --> E
K --> E
Diagram: Socket.IO Server Initialization Flow
The Socket.IO server is initialized in index.js L8-L12
by creating an HTTP server from the Express application and attaching Socket.IO to it. The setupSocket function from src/sockets/socketHandler.js
is called at index.js L57
to configure authentication middleware and event handlers before the server starts listening.
Sources: index.js:8-12, index.js:57
WebSocket Authentication¶
Authentication Middleware¶
The Socket.IO server implements authentication using JWT tokens stored in HTTP-only cookies. The authentication occurs at the connection level, not per-message.
sequenceDiagram
participant Client
participant Socket.IO Server
participant io.use middleware
participant jwt.verify
Client->>Socket.IO Server: WebSocket connection request
Socket.IO Server->>io.use middleware: Invoke authentication
io.use middleware->>io.use middleware: Extract cookies from headers
loop [Invalid token]
io.use middleware->>Client: Error: "No autenticado"
io.use middleware->>io.use middleware: Extract token from cookie
io.use middleware->>Client: Error: "Token no proporcionado"
io.use middleware->>jwt.verify: jwt.verify(token, JWT_SECRET)
jwt.verify->>io.use middleware: Error
io.use middleware->>Client: Error: "Token inválido"
jwt.verify->>io.use middleware: decoded payload
io.use middleware->>io.use middleware: Attach user to request
io.use middleware->>Socket.IO Server: next()
Socket.IO Server->>Client: Connection established
end
Diagram: WebSocket Authentication Sequence
The authentication middleware is registered at src/sockets/socketHandler.js L6-L32
It extracts the JWT token from the cookie header using regex matching at src/sockets/socketHandler.js L16
verifies it using jwt.verify at src/sockets/socketHandler.js L25
and attaches the decoded user information to socket.request.user at src/sockets/socketHandler.js L26
Key Implementation Details:
| Aspect | Implementation | Location |
|---|---|---|
| Cookie Extraction | Regex pattern /token=([^;]+)/ |
src/sockets/socketHandler.js L16 |
| Token Verification | jwt.verify(token, process.env.JWT_SECRET) |
src/sockets/socketHandler.js L25 |
| User Attachment | req.user = decoded |
src/sockets/socketHandler.js L26 |
| Error Handling | Returns Error object to reject connection |
src/sockets/socketHandler.js L12-L30 |
Sources: src/sockets/socketHandler.js:6-32
Room Management Strategy¶
Room Types and Assignment¶
The system implements a sophisticated room-based routing strategy using Socket.IO rooms for targeted message delivery.
flowchart TD
Connect["io.on('connection')"]
Extract["Extract user, name, rol from socket.request.user"]
Personal["socket.join('user:' + username)"]
AdminCheck["rol === 'admin'?"]
AdminRoom["socket.join('admins')"]
R1["user:alice<br>Members: alice"]
R2["user:bob<br>Members: bob"]
R3["user:admin1<br>Members: admin1"]
R4["admins<br>Members: admin1, admin2, admin3"]
End["End"]
Extract --> Personal
AdminCheck --> End
AdminRoom --> End
Personal --> R1
Personal --> R2
Personal --> R3
AdminRoom --> R4
subgraph subGraph2 ["Room Structure"]
R1
R2
R3
R4
end
subgraph subGraph1 ["Room Assignment Logic"]
Personal
AdminCheck
AdminRoom
Personal --> AdminCheck
AdminCheck --> AdminRoom
end
subgraph subGraph0 ["Connection Handler"]
Connect
Extract
Connect --> Extract
end
Diagram: Room Assignment Strategy
Every authenticated user is assigned to a personal room named user:{username} at src/sockets/socketHandler.js L42
If the user has the admin role, they additionally join the admins room at src/sockets/socketHandler.js L43
This dual-room membership for admins enables the message broadcasting pattern described below.
Room Naming Conventions:
| Room Pattern | Example | Purpose |
|---|---|---|
user:{username} |
user:alice |
Direct messages to specific user |
admins |
admins |
Broadcast to all online administrators |
Sources: src/sockets/socketHandler.js:36-43
Message Event Handling¶
Event: mensaje_privado¶
The mensaje_privado event handles sending messages between users. The routing logic differs based on sender role.
flowchart TD
Client["Client emits mensaje_privado<br>{para, mensaje}"]
Handler["Event handler receives event"]
Extract["de = socket.request.user.user"]
SendRecipient["io.to('user:' + para).emit('mensaje_recibido')"]
CheckRole["sender rol !== 'admin'?"]
SendAdmins["io.to('admins').emit('mensaje_recibido')"]
Persist["INSERT INTO mensajes<br>(de_usuario, para_usuario, mensaje)"]
Done["Done"]
Room1["user:recipient room"]
Room2["admins room"]
Client --> Handler
Handler --> Extract
Extract --> SendRecipient
SendRecipient --> CheckRole
CheckRole --> SendAdmins
CheckRole --> Persist
SendAdmins --> Persist
Persist --> Done
SendRecipient --> Room1
SendAdmins --> Room2
Diagram: mensaje_privado Event Routing Logic
The event handler at src/sockets/socketHandler.js L45-L63
implements the following logic:
- Extract sender: The sender username is extracted from
socket.request.user.userat src/sockets/socketHandler.js L46 - Direct delivery: Message is emitted to the recipient's personal room at src/sockets/socketHandler.js L48
- Admin notification: If sender is not admin, message is also broadcast to all admins at src/sockets/socketHandler.js L50-L52
- Persistence: Message is stored in the
mensajestable at src/sockets/socketHandler.js L55-L62
Event Payload Structures:
| Event | Direction | Payload | Example |
|---|---|---|---|
mensaje_privado |
Client → Server | {para: string, mensaje: string} |
{para: "admin", mensaje: "Hello"} |
mensaje_recibido |
Server → Client | {de: string, mensaje: string} |
{de: "alice", mensaje: "Hello"} |
Sources: src/sockets/socketHandler.js:45-63
Event: disconnect¶
The disconnect event is handled at src/sockets/socketHandler.js L65-L67
and logs the disconnection. Socket.IO automatically removes the socket from all rooms upon disconnection.
Sources: src/sockets/socketHandler.js:65-67
Database Persistence¶
Message Storage Schema¶
All messages are persisted to the mensajes table for historical retrieval via HTTP endpoints.
#mermaid-a19z30qjfth{font-family:ui-sans-serif,-apple-system,system-ui,Segoe UI,Helvetica;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-a19z30qjfth .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-a19z30qjfth .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-a19z30qjfth .error-icon{fill:#dddddd;}#mermaid-a19z30qjfth .error-text{fill:#222222;stroke:#222222;}#mermaid-a19z30qjfth .edge-thickness-normal{stroke-width:1px;}#mermaid-a19z30qjfth .edge-thickness-thick{stroke-width:3.5px;}#mermaid-a19z30qjfth .edge-pattern-solid{stroke-dasharray:0;}#mermaid-a19z30qjfth .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-a19z30qjfth .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-a19z30qjfth .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-a19z30qjfth .marker{fill:#999;stroke:#999;}#mermaid-a19z30qjfth .marker.cross{stroke:#999;}#mermaid-a19z30qjfth svg{font-family:ui-sans-serif,-apple-system,system-ui,Segoe UI,Helvetica;font-size:16px;}#mermaid-a19z30qjfth p{margin:0;}#mermaid-a19z30qjfth .entityBox{fill:#ffffff;stroke:#dddddd;}#mermaid-a19z30qjfth .relationshipLabelBox{fill:#dddddd;opacity:0.7;background-color:#dddddd;}#mermaid-a19z30qjfth .relationshipLabelBox rect{opacity:0.5;}#mermaid-a19z30qjfth .labelBkg{background-color:rgba(221, 221, 221, 0.5);}#mermaid-a19z30qjfth .edgeLabel .label{fill:#dddddd;font-size:14px;}#mermaid-a19z30qjfth .label{font-family:ui-sans-serif,-apple-system,system-ui,Segoe UI,Helvetica;color:#333;}#mermaid-a19z30qjfth .edge-pattern-dashed{stroke-dasharray:8,8;}#mermaid-a19z30qjfth .node rect,#mermaid-a19z30qjfth .node circle,#mermaid-a19z30qjfth .node ellipse,#mermaid-a19z30qjfth .node polygon{fill:#ffffff;stroke:#dddddd;stroke-width:1px;}#mermaid-a19z30qjfth .relationshipLine{stroke:#999;stroke-width:1;fill:none;}#mermaid-a19z30qjfth .marker{fill:none!important;stroke:#999!important;stroke-width:1;}#mermaid-a19z30qjfth :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}de_usuario referencespara_usuario referencesmensajesvarcharde_usuarioFKvarcharpara_usuarioFKtextmensajedatetimefechausuariosvarcharusuarioPKvarcharnombrevarcharrol
Diagram: Message Persistence Data Model
The SQL insertion occurs at src/sockets/socketHandler.js L55-L62
with the query:
INSERT INTO mensajes (de_usuario, para_usuario, mensaje) VALUES (?, ?, ?)
The fecha column uses a database default timestamp. Error handling logs failures but does not reject the WebSocket event.
Sources: src/sockets/socketHandler.js:55-62
Client-Side Implementation¶
Socket.IO Client Initialization¶
The client-side Socket.IO library is loaded at views/soporte.ejs L37
and initialized at views/soporte.ejs L39
:
const socket = io();
This establishes a WebSocket connection to the server with automatic authentication via cookies.
Sources: views/soporte.ejs:37-39
Admin Interface Logic¶
Administrators have access to a multi-conversation interface with user list and conversation switching.
flowchart TD
ConversationsObj["conversaciones = {}<br>Stores message arrays per user"]
CurrentUser["usuarioActual<br>Hidden input field"]
UserList["lista-usuarios<br>UL element"]
FetchUsers["GET /api/usuarios-conversaciones"]
PopulateList["agregarUsuarioLista() for each"]
LoadHistory["cargarHistorial() for each"]
RecvMsg["socket.on('mensaje_recibido')"]
SendMsg["formulario.submit"]
SwitchUser["li.click in user list"]
RenderConv["renderConversacion(usuario)<br>Clears and repopulates chat"]
AddMsg["agregarMensaje(de, mensaje, fecha)"]
LoadHistory --> ConversationsObj
RecvMsg --> ConversationsObj
RecvMsg --> RenderConv
SendMsg --> ConversationsObj
SendMsg --> RenderConv
SwitchUser --> RenderConv
subgraph subGraph3 ["Display Functions"]
RenderConv
AddMsg
RenderConv --> AddMsg
end
subgraph subGraph2 ["Event Handlers"]
RecvMsg
SendMsg
SwitchUser
end
subgraph Initialization ["Initialization"]
FetchUsers
PopulateList
LoadHistory
FetchUsers --> PopulateList
PopulateList --> LoadHistory
end
subgraph subGraph0 ["Admin UI State"]
ConversationsObj
CurrentUser
UserList
end
Diagram: Admin Chat Interface State Management
Key Functions:
| Function | Location | Purpose |
|---|---|---|
agregarUsuarioLista(usuario) |
views/soporte.ejs L89-L102 | Adds user to sidebar list with click handler |
cargarHistorial(usuario) |
views/soporte.ejs L115-L125 | Fetches message history from HTTP API |
renderConversacion(usuario) |
views/soporte.ejs L69-L86 | Clears chat and displays selected conversation |
agregarMensaje(de, mensaje, fecha) |
views/soporte.ejs L54-L60 | Appends message to chat DOM |
Visual Notification System:
The admin interface implements a visual notification system at views/soporte.ejs L136-L145
:
- Red + Bold: Indicates unread messages from user not currently selected
- Green + Bold: Indicates currently active conversation
- Normal: User with no new messages
Sources: views/soporte.ejs:62-163
User Interface Logic¶
Regular users have a simplified single-conversation interface that communicates with administrators.
sequenceDiagram
participant User Client
participant GET /api/mensajes/mios
participant Socket.IO
participant Server
User Client->>GET /api/mensajes/mios: Fetch message history
GET /api/mensajes/mios->>User Client: Return all messages
User Client->>User Client: Render messages in chat
loop [Real-time updates]
User Client->>Socket.IO: emit('mensaje_privado', {para: 'admin'})
Socket.IO->>Server: Send message
Server->>Socket.IO: emit('mensaje_recibido', {de, mensaje})
Socket.IO->>User Client: Display incoming message
end
Diagram: User Chat Interface Flow
Initialization:
The user interface loads message history at views/soporte.ejs L166-L171
via HTTP GET to /api/mensajes/mios.
Message Sending:
Users always send messages to "admin" (hardcoded) at views/soporte.ejs L184
The server routes these messages to the recipient's personal room and broadcasts to all admins via the admins room.
Event Listeners:
| Event | Location | Behavior |
|---|---|---|
socket.on("mensaje_recibido") |
views/soporte.ejs L174-L177 | Appends incoming message to chat |
formulario.submit |
views/soporte.ejs L180-L187 | Emits mensaje_privado event |
Sources: views/soporte.ejs:164-188
Integration with HTTP API¶
The real-time system is complemented by HTTP endpoints for historical message retrieval:
| Endpoint | Auth | Purpose | Used By |
|---|---|---|---|
GET /api/mensajes/mios |
User | Retrieve own message history | User interface initialization |
GET /api/mensajes?con={username} |
Admin | Retrieve conversation with specific user | Admin conversation switching |
GET /api/usuarios-conversaciones |
Admin | List all users with messages | Admin user list population |
These endpoints query the same mensajes table that WebSocket events write to, ensuring consistency between real-time and historical data.
Sources: views/soporte.ejs:105-112, views/soporte.ejs:116, views/soporte.ejs:166
Connection Lifecycle¶
stateDiagram-v2
[*] --> Disconnected : "JWT invalid/missing"
Disconnected --> Authenticating : "JWT invalid/missing"
Authenticating --> Authenticated : "JWT valid"
Authenticating --> Disconnected : "JWT invalid/missing"
Authenticated --> RoomAssignment : "Auth middleware passes"
RoomAssignment --> Connected : "Rooms joined"
Connected --> Disconnected : "disconnect event"
Disconnected --> [*] : "JWT invalid/missing"
Diagram: WebSocket Connection State Machine
The connection lifecycle follows these states:
- Disconnected: Initial state before connection
- Authenticating: JWT validation via middleware src/sockets/socketHandler.js L6-L32
- Room Assignment: User assigned to rooms src/sockets/socketHandler.js L42-L43
- Connected: Socket ready for event handling src/sockets/socketHandler.js L45-L67
Connections are automatically cleaned up by Socket.IO on disconnect, removing the socket from all rooms.
Sources: src/sockets/socketHandler.js:6-67
Error Handling¶
The system implements error handling at multiple levels:
| Error Type | Location | Handling Strategy |
|---|---|---|
| Missing cookies | src/sockets/socketHandler.js L10-L13 | Reject connection with error |
| Missing token | src/sockets/socketHandler.js L19-L22 | Reject connection with error |
| Invalid token | src/sockets/socketHandler.js L28-L31 | Reject connection with error |
| Database insertion failure | src/sockets/socketHandler.js L57-L61 | Log error, continue operation |
Database insertion errors are logged but do not disrupt the real-time message delivery, ensuring that communication continues even if persistence fails temporarily.
Sources: src/sockets/socketHandler.js:10-13, src/sockets/socketHandler.js:19-22, src/sockets/socketHandler.js:28-31, src/sockets/socketHandler.js:57-61