10) { // Modo autenticado con token $userId = 'token_' . substr(md5($token), 0, 8); $authenticated = true; } } // Si aún no está autenticado, usar modo global if (!$authenticated) { // Modo global: recibir eventos broadcast a todos $userId = 'global'; error_log('SSE: Conexión en modo global (sin autenticación específica)'); } // IMPORTANTE: Enviar evento connected ANTES de hacer cualquier otra cosa error_log("SSE: Enviando evento connected (userId: {$userId})"); sendSSEEvent('connected', [ 'timestamp' => time(), 'user_id' => $userId, 'mode' => $authenticated ? 'authenticated' : 'global', 'status' => 'ready' ]); // Ahora sí, conectar a BD $db = Database::getInstance(); // Obtener último timestamp conocido por el cliente $lastEventId = $_SERVER['HTTP_LAST_EVENT_ID'] ?? null; $lastCheck = time(); $connectionStart = time(); $maxConnectionTime = 1800; // 30 minutos máximo (aumentado para evitar desconexiones frecuentes) // Loop principal - revisar eventos cada 2 segundos while (true) { // Verificar tiempo máximo de conexión (evitar conexiones eternas) if (time() - $connectionStart > $maxConnectionTime) { error_log("SSE: Conexión alcanzó tiempo máximo ({$maxConnectionTime}s), cerrando..."); sendSSEEvent('timeout', ['message' => 'Conexión reiniciándose', 'reconnect' => true]); break; } // Verificar si la conexión sigue activa if (connection_aborted()) { error_log("SSE: Cliente desconectado (userId: {$userId})"); break; } // Leer eventos pendientes del archivo temporal $pendingEvents = readPendingEvents($userId); foreach ($pendingEvents as $event) { sendSSEEvent( $event['type'] ?? 'message', $event['data'] ?? [], $event['id'] ?? null ); } // Revisar nuevos mensajes en BD cada 3 segundos if (time() - $lastCheck >= 3) { // Mantener track de mensajes ya enviados para evitar duplicados static $lastMessageId = 0; static $lastConversationCheck = null; if ($lastConversationCheck === null) { // Primera vez: obtener el ID más alto y timestamp actual de la BD, sin enviar nada $maxMsg = $db->fetch("SELECT MAX(id) as max_id FROM conversations"); $lastMessageId = $maxMsg['max_id'] ?? 0; // Usar el timestamp de la BD para evitar problemas de zona horaria $dbTime = $db->fetch("SELECT NOW() as now"); $lastConversationCheck = $dbTime['now'] ?? date('Y-m-d H:i:s'); } else { // 1. Verificar mensajes NUEVOS (solo los que tienen ID mayor al último visto) $recentMessages = $db->fetchAll( "SELECT c.*, u.phone_number, u.name as user_name FROM conversations c INNER JOIN users u ON c.user_id = u.id WHERE c.id > ? AND c.direction = 'incoming' ORDER BY c.id ASC LIMIT 10", [$lastMessageId] ); foreach ($recentMessages as $msg) { sendSSEEvent('new_message', [ 'message_id' => $msg['id'], 'user_id' => $msg['user_id'], 'phone_number' => $msg['phone_number'], 'user_name' => $msg['user_name'], 'content' => $msg['message_content'], 'message_type' => $msg['message_type'] ?? 'text', 'direction' => $msg['direction'], 'created_at' => $msg['created_at'], 'timestamp' => time() ]); // Actualizar último ID visto if ($msg['id'] > $lastMessageId) { $lastMessageId = $msg['id']; } } // 2. Verificar si hay nuevas conversaciones desde la última revisión $recentConversations = $db->fetchAll( "SELECT DISTINCT c.user_id, u.phone_number, u.name, MAX(c.created_at) as last_message_time, COUNT(*) as message_count FROM conversations c INNER JOIN users u ON c.user_id = u.id WHERE c.created_at > ? GROUP BY c.user_id ORDER BY last_message_time DESC LIMIT 5", [$lastConversationCheck] ); foreach ($recentConversations as $conv) { sendSSEEvent('new_conversation', [ 'user_id' => $conv['user_id'], 'phone_number' => $conv['phone_number'], 'name' => $conv['name'], 'message_count' => $conv['message_count'], 'timestamp' => $conv['last_message_time'] ]); // Actualizar timestamp if ($conv['last_message_time'] > $lastConversationCheck) { $lastConversationCheck = $conv['last_message_time']; } } } // Fin del else (primera inicialización vs verificaciones posteriores) // Verificar notificaciones NUEVAS (solo las creadas desde la última verificación) // NO enviar notificaciones guardadas - solo las que llegan en tiempo real try { static $lastNotificationCheck = null; if ($lastNotificationCheck === null) { // Primera vez: usar el timestamp de la BD, NO enviar nada $dbTimeNotif = $db->fetch("SELECT NOW() as now"); $lastNotificationCheck = $dbTimeNotif['now'] ?? date('Y-m-d H:i:s'); } else { // Verificaciones posteriores: solo notificaciones NUEVAS $notifications = $db->fetchAll( "SELECT id, user_id, type, message, data, is_read, created_at FROM notifications WHERE created_at > ? ORDER BY created_at ASC LIMIT 10", [$lastNotificationCheck] ); if ($notifications && count($notifications) > 0) { foreach ($notifications as $notification) { sendSSEEvent('notification', $notification); // Actualizar timestamp para evitar duplicados if ($notification['created_at'] > $lastNotificationCheck) { $lastNotificationCheck = $notification['created_at']; } } } } } catch (Exception $e) { error_log('SSE: Error leyendo notificaciones: ' . $e->getMessage()); } $lastCheck = time(); } // Enviar heartbeat cada 30 segundos para mantener conexión viva if (time() % 30 === 0) { sendSSEEvent('heartbeat', ['timestamp' => time()]); } // Esperar 2 segundos antes de la siguiente verificación sleep(2); } } catch (Exception $e) { error_log("SSE Error: " . $e->getMessage()); error_log("SSE Error trace: " . $e->getTraceAsString()); sendSSEEvent('error', [ 'message' => $e->getMessage(), 'code' => $e->getCode() ]); }