UNPKG

@supernick135/face-scanner-client

Version:

Node.js client library for ZKTeco face scanning devices integration with comprehensive API support

458 lines (386 loc) โ€ข 12.7 kB
#!/usr/bin/env node const WebSocket = require('ws'); const { EventEmitter } = require('events'); /** * Secure WebSocket Client with Authentication & Auto-Reconnection * Similar to ordering-service functionality */ class WebSocketClient extends EventEmitter { constructor(config, authManager) { super(); this.config = config; this.authManager = authManager; this.logger = authManager.logger; // Connection state this.ws = null; this.isConnected = false; this.isConnecting = false; this.reconnectAttempts = 0; this.maxReconnectAttempts = config.MAX_RECONNECT_ATTEMPTS || 5; this.reconnectDelay = config.RECONNECT_DELAY || 5000; // Ping/Pong for connection health this.pingInterval = null; this.pongTimeout = null; this.pingIntervalMs = config.PING_INTERVAL || 30000; this.lastPongTime = null; // Message handling this.messageQueue = []; this.isProcessingQueue = false; } /** * Connect to WebSocket server with authentication */ async connect() { if (this.isConnecting || this.isConnected) { this.logger.warn('โš ๏ธ Connection already in progress or established'); return; } try { this.isConnecting = true; // Ensure we have valid authentication if (!this.authManager.isValid()) { this.logger.info('๐Ÿ” Authenticating before connection...'); await this.authManager.authenticate(); } const apiKey = this.authManager.getAPIKey(); const params = new URLSearchParams({ token: apiKey, clientType: 'service', serviceName: this.config.SERVICE_NAME || 'ordering-service' }); const wsUrl = `${this.config.WS_SERVER_URL}?${params.toString()}`; this.logger.info('๐Ÿ”Œ Connecting to WebSocket server...', { baseUrl: this.config.WS_SERVER_URL, fullUrl: wsUrl, keyId: this.authManager.getStatus().keyId, clientType: 'service' }); this.ws = new WebSocket(wsUrl); this.setupEventListeners(); } catch (error) { this.isConnecting = false; this.logger.error('โŒ Connection failed:', error.message); this.emit('error', error); if (this.config.AUTO_RECONNECT) { this.scheduleReconnect(); } } } /** * Setup WebSocket event listeners */ setupEventListeners() { this.ws.on('open', () => { this.isConnected = true; this.isConnecting = false; this.reconnectAttempts = 0; this.logger.info('โœ… WebSocket connected successfully'); // Send authentication message immediately after connection try { const apiKey = this.authManager.getAPIKey(); const authMessage = { type: 'auth', payload: { apiKey: apiKey, clientType: 'service' }, timestamp: Date.now() }; this.ws.send(JSON.stringify(authMessage)); this.logger.info('๐Ÿ” Sent authentication message with API key'); } catch (error) { this.logger.error('โŒ Failed to send auth message:', error.message); } // Start ping/pong if enabled if (this.config.ENABLE_PING_PONG) { this.startPingPong(); } // Process queued messages this.processMessageQueue(); this.emit('connected'); }); this.ws.on('message', (data) => { try { const message = JSON.parse(data.toString()); this.handleMessage(message); } catch (error) { this.logger.warn('โš ๏ธ Received invalid JSON message:', data.toString()); } }); this.ws.on('close', (code, reason) => { this.isConnected = false; this.isConnecting = false; this.stopPingPong(); this.logger.warn('๐Ÿ”Œ WebSocket connection closed', { code, reason: reason.toString(), reconnectAttempts: this.reconnectAttempts }); this.emit('disconnected', { code, reason: reason.toString() }); // Auto-reconnect if configured if (this.config.AUTO_RECONNECT && this.reconnectAttempts < this.maxReconnectAttempts) { this.scheduleReconnect(); } }); this.ws.on('error', (error) => { this.logger.error('โŒ WebSocket error:', error.message); this.emit('error', error); }); // Handle pong responses this.ws.on('pong', () => { this.lastPongTime = Date.now(); if (this.pongTimeout) { clearTimeout(this.pongTimeout); this.pongTimeout = null; } if (this.config.DEBUG_MODE) { this.logger.debug('๐Ÿ“ Received pong from server'); } }); } /** * Handle incoming messages */ handleMessage(message) { if (this.config.DEBUG_MODE) { this.logger.debug('๐Ÿ“จ Received message:', message); } // Handle different message types switch (message.type) { case 'welcome': this.logger.info('๐Ÿ‘‹ Server welcome:', message.data); this.emit('welcome', message.data); break; case 'face_detected': this.logger.info('๐Ÿ‘ค Face detected:', message.data); this.emit('face_detected', message.data); break; case 'system_message': this.logger.info('๐Ÿ”” System message:', message.data); this.emit('system_message', message.data); break; case 'error': this.logger.error('โŒ Server error:', message.payload || message.data); this.emit('server_error', message.payload || message.data); break; case 'auth_success': this.logger.info('๐Ÿ” Authentication successful'); this.emit('auth_success', message.payload || message.data); break; case 'auth_error': this.logger.error('๐Ÿ”’ Authentication failed:', message.payload || message.data); this.emit('auth_error', message.payload || message.data); break; case 'room_update': this.logger.debug('๐Ÿ  Room update:', message.payload); this.emit('room_update', message.payload); break; default: this.logger.debug('๐Ÿ“‹ Unknown message type:', message.type); this.emit('message', message); } } /** * Send message to server */ send(message) { if (!this.isConnected) { this.logger.warn('โš ๏ธ Not connected, queuing message:', message.type); this.messageQueue.push(message); // Try to connect if not already connecting if (!this.isConnecting && this.config.AUTO_RECONNECT) { this.connect(); } return; } try { const messageStr = JSON.stringify(message); this.ws.send(messageStr); if (this.config.DEBUG_MODE) { this.logger.debug('๐Ÿ“ค Sent message:', message); } } catch (error) { this.logger.error('โŒ Failed to send message:', error.message); this.emit('error', error); } } /** * Send face scan event (service capability) */ sendFaceScan(faceData) { const message = { type: 'face_detected', payload: { studentId: faceData.studentId, confidence: faceData.confidence || 0.95, deviceId: this.config.SERVICE_NAME, schoolId: this.config.SERVICE_SCHOOL_ID, timestamp: Date.now(), // Added timestamp in payload metadata: { source: 'service-client', ...faceData.metadata } }, timestamp: Date.now() }; this.send(message); this.logger.info('๐Ÿ‘ค Sent face scan event:', { studentId: faceData.studentId, confidence: message.payload.confidence }); } /** * Join a specific room */ joinRoom(roomName) { const message = { type: 'join_room', payload: { room: roomName }, timestamp: Date.now() }; this.send(message); this.logger.info(`๐Ÿšช Joining room: ${roomName}`); } /** * Leave a specific room */ leaveRoom(roomName) { const message = { type: 'leave_room', payload: { room: roomName }, timestamp: Date.now() }; this.send(message); this.logger.info(`๐Ÿšช Leaving room: ${roomName}`); } /** * Send system message (placeholder - not supported by server) */ sendSystemMessage(messageText, targetRoom = null) { this.logger.info('๐Ÿ”” System message logging:', { message: messageText, room: targetRoom, source: this.config.SERVICE_NAME, note: 'system_message type not supported by server' }); } /** * Process queued messages when connection is restored */ processMessageQueue() { if (this.isProcessingQueue || this.messageQueue.length === 0) { return; } this.isProcessingQueue = true; this.logger.info(`๐Ÿ“ฌ Processing ${this.messageQueue.length} queued messages`); while (this.messageQueue.length > 0 && this.isConnected) { const message = this.messageQueue.shift(); this.send(message); } this.isProcessingQueue = false; } /** * Start ping/pong health check */ startPingPong() { if (this.pingInterval) { clearInterval(this.pingInterval); } this.pingInterval = setInterval(() => { if (this.isConnected && this.ws.readyState === WebSocket.OPEN) { // Set timeout for pong response this.pongTimeout = setTimeout(() => { this.logger.warn('โš ๏ธ Pong timeout - connection may be dead'); this.ws.terminate(); }, 5000); // 5 second pong timeout this.ws.ping(); if (this.config.DEBUG_MODE) { this.logger.debug('๐Ÿ“ Sent ping to server'); } } }, this.pingIntervalMs); } /** * Stop ping/pong health check */ stopPingPong() { if (this.pingInterval) { clearInterval(this.pingInterval); this.pingInterval = null; } if (this.pongTimeout) { clearTimeout(this.pongTimeout); this.pongTimeout = null; } } /** * Schedule reconnection attempt */ scheduleReconnect() { this.reconnectAttempts++; if (this.reconnectAttempts > this.maxReconnectAttempts) { this.logger.error(`โŒ Max reconnection attempts (${this.maxReconnectAttempts}) reached`); this.emit('max_reconnect_attempts_reached'); return; } const delay = this.reconnectDelay * this.reconnectAttempts; // Exponential backoff this.logger.info(`๐Ÿ”„ Scheduling reconnect attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`); setTimeout(() => { this.connect(); }, delay); } /** * Get connection status */ getStatus() { return { isConnected: this.isConnected, isConnecting: this.isConnecting, reconnectAttempts: this.reconnectAttempts, queuedMessages: this.messageQueue.length, lastPongTime: this.lastPongTime, authStatus: this.authManager.getStatus(), readyState: this.ws ? this.ws.readyState : null, readyStateString: this.ws ? this.getReadyStateString() : 'NO_CONNECTION' }; } /** * Get readable WebSocket ready state */ getReadyStateString() { if (!this.ws) return 'NO_CONNECTION'; switch (this.ws.readyState) { case WebSocket.CONNECTING: return 'CONNECTING'; case WebSocket.OPEN: return 'OPEN'; case WebSocket.CLOSING: return 'CLOSING'; case WebSocket.CLOSED: return 'CLOSED'; default: return 'UNKNOWN'; } } /** * Graceful disconnect */ disconnect() { this.logger.info('๐Ÿ”Œ Disconnecting WebSocket...'); this.stopPingPong(); if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.close(1000, 'Client requested disconnect'); } this.isConnected = false; this.isConnecting = false; } /** * Cleanup resources */ destroy() { this.disconnect(); this.removeAllListeners(); this.messageQueue = []; this.logger.info('๐Ÿงน WebSocket client destroyed'); } } module.exports = WebSocketClient;