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; } } }