152 lines
4.8 KiB
JavaScript
152 lines
4.8 KiB
JavaScript
import { logger } from '../logger/logger.js';
|
|
import * as protocol from '../protocol/messages.js';
|
|
|
|
/**
|
|
* Client represents a single active WebSocket connection wrapper.
|
|
* It encapsulates the raw connection, handles ping/pong heartbeat,
|
|
* parses messages, and routes events back to the coordinator Hub.
|
|
*/
|
|
export class Client {
|
|
/**
|
|
* Initializes a new client instance and sets up connection event listeners and heartbeat.
|
|
* @param {object} conn The raw WS socket connection instance from the 'ws' library.
|
|
* @param {object} hub The central Hub coordinator to process and route messages.
|
|
*/
|
|
constructor(conn, hub) {
|
|
this.conn = conn; // The underlying raw WebSocket connection
|
|
this.hub = hub; // Reference to the main coordinator hub
|
|
this.userID = ''; // Authenticated user ID (driver_id or passenger_id)
|
|
this.rideID = ''; // Associated ride ID
|
|
this.role = ''; // User's role: 'driver' or 'passenger'
|
|
this.isAlive = true; // Connection status flag updated by heartbeat pong responses
|
|
this.pongTimeout = null; // Timer waiting for a pong reply after sending a ping
|
|
|
|
/**
|
|
* Listener for incoming WebSocket message payloads.
|
|
* Enforces a hard read limit of 4096 bytes (4KB) to prevent Denial of Service (DoS) memory consumption.
|
|
*/
|
|
this.conn.on('message', (data) => {
|
|
if (data.length > 4096) {
|
|
logger.warn('payload_too_large', { remote_ip: this.remoteIP() });
|
|
this.send(protocol.newError(protocol.ErrPayloadTooLarge, 'Payload too large'));
|
|
this.close();
|
|
return;
|
|
}
|
|
// Pass the message to the hub for authentication and signaling routing
|
|
this.hub.handleMessage(this, data);
|
|
});
|
|
|
|
/**
|
|
* Listener for the 'pong' response frame.
|
|
* Indicates that the client is responsive. Clears the timeout timer.
|
|
*/
|
|
this.conn.on('pong', () => {
|
|
this.isAlive = true;
|
|
if (this.pongTimeout) {
|
|
clearTimeout(this.pongTimeout);
|
|
this.pongTimeout = null;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Listener for connection close event.
|
|
* Triggers cleanup of heartbeat timers and unregisters the client from the hub.
|
|
*/
|
|
this.conn.on('close', () => {
|
|
this.cleanup();
|
|
this.hub.unregister(this);
|
|
});
|
|
|
|
/**
|
|
* Listener for socket connection errors.
|
|
* Logs the error details and safely terminates the connection.
|
|
*/
|
|
this.conn.on('error', (err) => {
|
|
logger.error('websocket_error', {
|
|
user_id: this.userID,
|
|
ride_id: this.rideID,
|
|
error: err.message
|
|
});
|
|
this.close();
|
|
});
|
|
|
|
/**
|
|
* Heartbeat loop: Pings the client every 20 seconds.
|
|
* If the client does not respond with a pong within 10 seconds, the connection is considered dead and closed.
|
|
* This prevents resource leaks from half-open TCP connections.
|
|
*/
|
|
this.pingInterval = setInterval(() => {
|
|
this.isAlive = false;
|
|
try {
|
|
this.conn.ping();
|
|
} catch (err) {
|
|
this.close();
|
|
return;
|
|
}
|
|
|
|
// Schedule a timeout check for 10 seconds. Close connection if isAlive is still false.
|
|
this.pongTimeout = setTimeout(() => {
|
|
if (!this.isAlive) {
|
|
logger.warn('heartbeat_timeout', {
|
|
user_id: this.userID,
|
|
ride_id: this.rideID,
|
|
remote_ip: this.remoteIP()
|
|
});
|
|
this.close();
|
|
}
|
|
}, 10000);
|
|
}, 20000);
|
|
}
|
|
|
|
/**
|
|
* Sends a serialized JSON message down the WebSocket connection wire.
|
|
* Checks the connection readyState to ensure it is in the OPEN state before writing.
|
|
* @param {string} msg Serialized JSON string message payload.
|
|
*/
|
|
send(msg) {
|
|
if (this.conn.readyState === 1) { // WebSocket.OPEN state
|
|
try {
|
|
this.conn.send(msg);
|
|
} catch (err) {
|
|
logger.error('websocket_send_failed', { error: err.message });
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolves the remote IP address of the connected client.
|
|
* Falls back to 'unknown' if the connection socket is already closed or unavailable.
|
|
* @returns {string} The remote client's IP address.
|
|
*/
|
|
remoteIP() {
|
|
return this.conn._socket ? this.conn._socket.remoteAddress : 'unknown';
|
|
}
|
|
|
|
/**
|
|
* Safely terminates the WebSocket connection and releases local memory and timers.
|
|
*/
|
|
close() {
|
|
try {
|
|
this.conn.close();
|
|
} catch (err) {
|
|
// Ignored: connection might already be closed or unreachable
|
|
}
|
|
this.cleanup();
|
|
}
|
|
|
|
/**
|
|
* Cleans up and clears all scheduled interval pings and timeout check timers.
|
|
* This is critical to prevent CPU timer memory leaks when client closes.
|
|
*/
|
|
cleanup() {
|
|
if (this.pingInterval) {
|
|
clearInterval(this.pingInterval);
|
|
this.pingInterval = null;
|
|
}
|
|
if (this.pongTimeout) {
|
|
clearTimeout(this.pongTimeout);
|
|
this.pongTimeout = null;
|
|
}
|
|
}
|
|
}
|