/** * Server connection manager for handling client connections */ import type { TCPServer } from '../protocol/socket.js'; import type { ConnectionStatusQueryMessage, DeviceListRequestMessage, DeviceStatusQueryMessage, Message, } from '../protocol/types.js'; import { MessageType } from '../protocol/types.js'; import type { DeviceInfo } from '../types/common.js'; import { DeviceState } from '../types/common.js'; import { logger } from '../utils/logger.js'; import type { DeviceManager } from './device-manager.js'; interface ClientConnection { id: string; address: string; connectedAt: Date; } /** * Connection manager */ export class ConnectionManager { private clients: Map = new Map(); constructor( private server: TCPServer, private deviceManager: DeviceManager, private serverAddress: string = 'localhost' ) { this.setupHandlers(); } /** * Setup message and connection handlers */ private setupHandlers(): void { // Handle client connections this.server.onClientConnect((clientId) => { this.clients.set(clientId, { id: clientId, address: clientId, connectedAt: new Date(), }); logger.info(`Client connected: ${clientId} (${this.clients.size} total)`); // Send DEVICE_BIND messages for all currently bound devices const boundDevices = this.deviceManager .getDevices() .filter((device) => device.state === DeviceState.BOUND); for (const device of boundDevices) { // Check if client is authorized for this device if (this.deviceManager.isClientAllowed(device.path, clientId)) { logger.info( `Sending bind message for ${device.path} to newly connected client ${clientId}` ); this.server.broadcast({ type: MessageType.DEVICE_BIND, timestamp: Date.now(), device, serverAddress: this.serverAddress, }); } } }); // Handle client disconnections this.server.onClientDisconnect((clientId) => { this.clients.delete(clientId); logger.info(`Client disconnected: ${clientId} (${this.clients.size} remaining)`); }); // Handle messages from clients // Note: We don't have clientId in the message handler yet, so we'll broadcast for now this.server.onMessage(async (message) => { await this.handleMessage(message); }); // Handle device state changes this.deviceManager.onStateChange(async (device, oldState) => { await this.handleDeviceStateChange(device, oldState); }); } /** * Handle incoming message from client */ private async handleMessage(message: Message): Promise { switch (message.type) { case MessageType.DEVICE_STATUS_QUERY: await this.handleDeviceStatusQuery(message); break; case MessageType.CONNECTION_STATUS_QUERY: await this.handleConnectionStatusQuery(message); break; case MessageType.DEVICE_LIST_REQUEST: await this.handleDeviceListRequest(message); break; case MessageType.HEARTBEAT: logger.debug('Heartbeat received'); break; default: logger.warn(`Unknown message type: ${(message as Message).type}`); } } /** * Handle device status query */ private async handleDeviceStatusQuery(message: DeviceStatusQueryMessage): Promise { const { device } = message; const deviceInfo = this.deviceManager.getDevice(device.path); this.server.broadcast({ type: MessageType.DEVICE_STATUS_RESPONSE, timestamp: Date.now(), device, available: deviceInfo?.state === DeviceState.BOUND, info: deviceInfo, }); } /** * Handle connection status query */ private async handleConnectionStatusQuery(message: ConnectionStatusQueryMessage): Promise { const devices = this.deviceManager.getDevices(); this.server.broadcast({ type: MessageType.CONNECTION_STATUS_RESPONSE, timestamp: Date.now(), devices, }); } /** * Handle device list request (for discovery) */ private async handleDeviceListRequest(message: DeviceListRequestMessage): Promise { const { clientId } = message; logger.info(`Device list requested by client: ${clientId}`); // Get devices authorized for this client const authorizedDevices = this.deviceManager.getDevicesForClient(clientId); // Send response back this.server.broadcast({ type: MessageType.DEVICE_LIST_RESPONSE, timestamp: Date.now(), devices: authorizedDevices, }); } /** * Handle device state change */ private async handleDeviceStateChange(device: DeviceInfo, oldState: DeviceState): Promise { logger.info( `Device state changed: ${device.vendorId}:${device.productId} ${oldState} -> ${device.state}` ); // Notify all authorized clients if (device.state === DeviceState.BOUND && oldState !== DeviceState.BOUND) { // Device became available - tell clients to bind this.notifyAuthorizedClients(device, { type: MessageType.DEVICE_BIND, timestamp: Date.now(), device, serverAddress: this.serverAddress, }); } else if (device.state === DeviceState.DISCONNECTED && oldState === DeviceState.BOUND) { // Device became unavailable - tell clients to unbind this.notifyAuthorizedClients(device, { type: MessageType.DEVICE_UNBIND, timestamp: Date.now(), device: { path: device.path, vendorId: device.vendorId, productId: device.productId, description: device.description, }, }); } } /** * Notify clients authorized to access a device */ private notifyAuthorizedClients(device: DeviceInfo, message: Message): void { // For now, broadcast to all clients // In production, should filter by client authorization this.server.broadcast(message); } /** * Get connected clients count */ getClientCount(): number { return this.clients.size; } }