B4J=true Group=Default Group ModulesStructureVersion=1 Type=StaticCode Version=10.3 @EndOfDesignText@ ' Módulo para gestionar conexiones y transmisiones de Server-Sent Events (SSE). ' Declaración de variables globales a nivel de proceso. Sub Process_Globals ' 'Connections' es un mapa (diccionario) para almacenar las conexiones SSE activas. ' La clave será una combinación del 'path' y un GUID único, y el valor será el OutputStream de la respuesta. ' Se usará un 'ThreadSafeMap' para evitar problemas de concurrencia entre hilos. Dim Connections As Map ' Timer #1 ("El Vigilante"): Se encarga de detectar y eliminar conexiones muertas. Private RemoveTimer As Timer ' Timer #2 ("El Informante"): Se encarga de recolectar y enviar los datos de estadísticas. Dim StatsTimer As Timer Dim const UPDATE_INTERVAL_MS As Long = 2000 ' Intervalo de envío de estadísticas: 2 segundos. End Sub ' Subrutina de inicialización del módulo. Se llama una vez cuando el objeto es creado. public Sub Initialize() ' Crea el mapa 'Connections' como un mapa seguro para hilos (ThreadSafeMap). ' Esto es fundamental porque múltiples peticiones (hilos) pueden intentar agregar o remover conexiones simultáneamente. Connections = Main.srvr.CreateThreadSafeMap ' Inicializa el temporizador 'RemoveTimer' para que dispare el evento "RemoveTimer" cada 5000 milisegundos (5 segundos). RemoveTimer.Initialize("RemoveTimer", 5000) ' Habilita el temporizador para que comience a funcionar. RemoveTimer.Enabled = True Log("Stats SSE Handler Initialized (Singleton Mode)") ' Crea el mapa de conexiones, asegurando que sea seguro para el manejo de múltiples hilos. Connections = Main.srvr.CreateThreadSafeMap ' Configura y activa el timer para la limpieza de conexiones cada 5 segundos. ' NOTA: El EventName "RemoveTimer" debe coincidir con el nombre de la subrutina del tick. RemoveTimer.Initialize("RemoveTimer", 5000) RemoveTimer.Enabled = True ' Configura y activa el timer para el envío de estadísticas. ' NOTA: El EventName "StatsTimer" debe coincidir con el nombre de la subrutina del tick. StatsTimer.Initialize("StatsTimer", UPDATE_INTERVAL_MS) StatsTimer.Enabled = True End Sub ' Subrutina para agregar un nuevo cliente (target) al stream de eventos SSE. ' Se llama cuando un cliente se conecta al endpoint SSE. ' Registra formalmente a un nuevo cliente en el sistema. Sub AddTarget(path As String, resp As ServletResponse) ' Genera una clave única para esta conexión específica. Dim connectionKey As String = path & "|" & GetGUID Log("--- [SSE] Cliente conectado: " & connectionKey & " ---") ' Configura las cabeceras HTTP necesarias para que el navegador mantenga la conexión abierta. resp.ContentType = "text/event-stream" resp.SetHeader("Cache-Control", "no-cache") resp.SetHeader("Connection", "keep-alive") resp.CharacterEncoding = "UTF-8" resp.Status = 200 ' Añade al cliente y su canal de comunicación al mapa central. Connections.Put(connectionKey, resp.OutputStream) ' Envía un primer mensaje de bienvenida para confirmar la conexión. SendMessage(resp.OutputStream, "open", "Connection established", 0, connectionKey) End Sub ' Envía un mensaje a todos los clientes suscritos a un "path" específico. Sub Broadcast(Path As String, EventName As String, Message As String, Retry As Long) ' Itera sobre la lista de clientes activos. For Each key As String In Connections.Keys ' Log(key) ' Filtra para enviar solo a los clientes del path correcto (en este caso, "stats"). If key.StartsWith(Path & "|") Then Try ' Llama a la función de bajo nivel para enviar el mensaje formateado. SendMessage(Connections.Get(key), EventName, Message, Retry, DateTime.Now) Catch ' Si el envío falla, asume que el cliente se desconectó y lo elimina. Log("######################") Log("## Removing (broadcast failed): " & key) Log("######################") Connections.Remove(key) End Try End If Next End Sub ' Formatea y envía un único mensaje SSE a un cliente específico. Sub SendMessage(out As OutputStream, eventName As String, message As String, retry As Int, id As String) ' Construye el mensaje siguiendo el formato oficial del protocolo SSE. Dim sb As StringBuilder sb.Initialize sb.Append("id: " & id).Append(CRLF) sb.Append("event: " & eventName).Append(CRLF) If message <> "" Then sb.Append("data: " & message).Append(CRLF) End If If retry > 0 Then sb.Append("retry: " & retry).Append(CRLF) End If sb.Append(CRLF) ' El doble salto de línea final es obligatorio. ' Convierte el texto a bytes y lo escribe en el canal de comunicación del cliente. Dim Bytes() As Byte = sb.ToString.GetBytes("UTF-8") out.WriteBytes(Bytes, 0, Bytes.Length) out.Flush ' Fuerza el envío inmediato de los datos. End Sub ' Genera un Identificador Único Global (GUID) para cada conexión. Private Sub GetGUID() As String Dim jo As JavaObject Return jo.InitializeStatic("java.util.UUID").RunMethod("randomUUID", Null) End Sub ' Evento que se dispara cada vez que el 'RemoveTimer' completa su intervalo (cada 5 segundos). ' Su propósito es proactivamente limpiar conexiones muertas. Sub RemoveTimer_Tick ' Log("remove timer") ' Itera sobre todas las conexiones activas. For Each key As String In Connections.Keys ' Intenta enviar un mensaje de prueba ("ping" o "heartbeat") a cada cliente. Try ' Obtiene el OutputStream del cliente. Dim out As OutputStream = Connections.Get(key) ' Envía un evento de tipo "Test" sin datos. Si la conexión está viva, esto no hará nada visible. SendMessage(out, "Test", "", 0, "") Catch ' Si el 'SendMessage' falla, significa que el socket está cerrado (el cliente se desconectó). ' Registra en el log que se está eliminando una conexión muerta. Log("######################") Log("## Removing (timer): " & key) Log("######################") ' Elimina la conexión del mapa para liberar recursos. Connections.Remove(key) End Try Next End Sub ' Evento del Timer #2 ("El Informante"): se dispara cada 2 segundos. public Sub StatsTimer_Tick ' Optimización: si no hay nadie conectado, no realiza el trabajo pesado. ' Log($"Conexiones: ${Connections.Size}"$) If Connections.Size = 0 Then Return Try ' Prepara un mapa para almacenar las estadísticas recolectadas. Dim allPoolStats As Map allPoolStats.Initialize ' Bloquea el acceso a los conectores para leer sus datos de forma segura. Main.MainConnectorsLock.RunMethod("lock", Null) For Each dbKey As String In Main.listaDeCP Dim connector As RDCConnector If Main.Connectors.ContainsKey(dbKey) Then connector = Main.Connectors.Get(dbKey) If connector.IsInitialized Then ' 1. Obtener las métricas del pool C3P0 (incluye la configuración estática) Dim statsMap As Map = connector.GetPoolStats ' 2. OBTENER LAS MÉTRICAS DE LA CAPA DE APLICACIÓN DESDE EL CACHÉ GLOBAL Dim cachedAppStats As Map = Main.LatestPoolStats.GetDefault(dbKey, CreateMap()).As(Map) ' 3. FUSIONAR: Agregar el contador de peticiones activas al mapa que se va a enviar If cachedAppStats.ContainsKey("HandlerActiveRequests") Then statsMap.Put("HandlerActiveRequests", cachedAppStats.Get("HandlerActiveRequests")) End If allPoolStats.Put(dbKey, statsMap) Else allPoolStats.Put(dbKey, CreateMap("Error": "Conector no inicializado")) End If End If Next ' Libera el bloqueo para que otras partes del programa puedan usar los conectores. Main.MainConnectorsLock.RunMethod("unlock", Null) ' Convierte el mapa de estadísticas a un formato de texto JSON. Dim j As JSONGenerator j.Initialize(allPoolStats) Dim jsonStats As String = j.ToString ' Llama al "locutor" para enviar el JSON a todos los clientes conectados. Broadcast("stats", "stats_update", jsonStats, 0) Catch ' Captura y registra cualquier error que ocurra durante la recolección de datos. Log($"[SSE] Error CRÍTICO durante la adquisición de estadísticas: ${LastException.Message}"$) End Try End Sub