@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
JavaScript
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;