/** * Client connection handler - manages connections to multiple servers */ import type { ServerConnection } from '../config/schema.js'; import { TCPClient } from '../protocol/socket.js'; import type { DeviceBindMessage, DeviceUnbindMessage, Message } from '../protocol/types.js'; import { MessageType } from '../protocol/types.js'; import { DeviceState } from '../types/common.js'; import { logger } from '../utils/logger.js'; import type { DeviceManager } from './device-manager.js'; interface ServerConnectionState { config: ServerConnection; client: TCPClient; connected: boolean; } /** * Connection handler for managing multiple server connections */ export class ConnectionHandler { private connections: Map = new Map(); private deviceManager: DeviceManager | null = null; constructor( private clientId: string, private servers: ServerConnection[] ) {} /** * Set device manager */ setDeviceManager(deviceManager: DeviceManager): void { this.deviceManager = deviceManager; } /** * Connect to all servers */ async connectToAllServers(): Promise { logger.info(`Connecting to ${this.servers.length} server(s)...`); for (const server of this.servers) { await this.connectToServer(server); } } /** * Connect to a specific server */ async connectToServer(server: ServerConnection): Promise { logger.info(`Connecting to server: ${server.name} (${server.address}:${server.port})`); const client = new TCPClient(server.address, server.port); // Store connection BEFORE setting up callbacks to avoid race condition this.connections.set(server.name, { config: server, client, connected: false, }); // Handle incoming messages client.onMessage((message) => { this.handleMessage(server.name, message); }); // Handle connection status client.onConnectionState((connected) => { const conn = this.connections.get(server.name); if (!conn) { logger.error( `Connection state callback: server ${server.name} not found in connections map` ); return; } logger.debug( `Connection state callback for ${server.name}: connected=${connected}, conn.connected=${conn.connected}, client.isConnected()=${client.isConnected()}` ); // Update connection state synchronously conn.connected = connected; if (connected) { logger.info(`✓ Connected to ${server.name}`); logger.debug( `About to send CONNECTION_STATUS_QUERY: conn.connected=${conn.connected}, client.isConnected()=${client.isConnected()}` ); // Request connection status after connecting this.sendMessage(server.name, { type: MessageType.CONNECTION_STATUS_QUERY, timestamp: Date.now(), clientId: this.clientId, }); } else { logger.warn(`Disconnected from ${server.name}`); // Detach all devices from this server when connection is lost if (this.deviceManager) { this.deviceManager.handleServerDisconnect(server.name); } } }); // Start connection await client.connect(); } /** * Send message to a specific server */ sendMessage(serverName: string, message: Message): void { const conn = this.connections.get(serverName); if (!conn) { logger.error(`Server not found: ${serverName}`); return; } logger.debug( `sendMessage to ${serverName}: conn.connected=${conn.connected}, client.isConnected()=${conn.client.isConnected()}` ); if (!conn.connected) { logger.warn(`Not connected to ${serverName}, message not sent`); return; } conn.client.send(message); } /** * Handle incoming message from a server */ private async handleMessage(serverName: string, message: Message): Promise { if (!this.deviceManager) { logger.warn('Device manager not set, ignoring message'); return; } switch (message.type) { case MessageType.DEVICE_BIND: this.deviceManager.handleDeviceBind(serverName, message as DeviceBindMessage); break; case MessageType.DEVICE_UNBIND: this.deviceManager.handleDeviceUnbind(serverName, message as DeviceUnbindMessage); break; case MessageType.DEVICE_STATUS_RESPONSE: logger.debug(`Device status response from ${serverName}`); break; case MessageType.CONNECTION_STATUS_RESPONSE: logger.info(`Connection status received from ${serverName}`); // Process available devices and attach them if ('devices' in message && Array.isArray(message.devices)) { const conn = this.connections.get(serverName); if (!conn) { logger.error(`Cannot process device list: server ${serverName} not found`); break; } for (const device of message.devices) { // Check if this device is in our config const deviceConfig = conn.config.devices.find((d) => d.path === device.path); if (!deviceConfig) { logger.debug(`Skipping device ${device.path} - not in our config`); continue; } if (device.state === DeviceState.BOUND) { // Device is available on server - informational only // Actual device binding will occur when server sends DEVICE_BIND message logger.info( `Device ${device.path} (${device.busId}) is available on ${serverName} (status only - waiting for bind message)` ); } } } break; case MessageType.DEVICE_LIST_RESPONSE: logger.info(`Device list received from ${serverName}`); // This would be handled by the wizard during configuration break; case MessageType.HEARTBEAT: logger.debug(`Heartbeat from ${serverName}`); break; default: logger.warn(`Unknown message type from ${serverName}: ${message.type}`); } } /** * Get connection status for all servers */ getConnectionStatus(): Map { const status = new Map(); for (const [name, conn] of this.connections) { status.set(name, conn.connected); } return status; } /** * Disconnect from all servers */ async disconnect(): Promise { logger.info('Disconnecting from all servers...'); for (const [name, conn] of this.connections) { logger.info(`Disconnecting from ${name}...`); await conn.client.disconnect(); } this.connections.clear(); } }