B4J=true Group=Default Group ModulesStructureVersion=1 Type=Class Version=10.3 @EndOfDesignText@ ' Handler class: StatsSSEHandler.b4j ' Gestiona y transmite en tiempo real las estadísticas del pool de conexiones vía Server-Sent Events (SSE). ' Opera en modo Singleton: una única instancia maneja todas las conexiones. Sub Class_Globals ' Almacena de forma centralizada a todos los clientes (navegadores) conectados. ' La clave es un ID único y el valor es el canal de comunicación (OutputStream). Private 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 ' Se ejecuta UNA SOLA VEZ cuando el servidor arranca, gracias al modo Singleton. Public Sub Initialize 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 ' Es el punto de entrada principal. Atiende todas las peticiones HTTP dirigidas a este handler. Sub Handle(req As ServletRequest, resp As ServletResponse) Log($"StatsTimerinicializado: ${StatsTimer.IsInitialized}, StatsTimer habilitado: ${StatsTimer.Enabled}"$) StatsTimer.Initialize("StatsTimer", 2000) StatsTimer.Enabled = True ' Filtro de seguridad: verifica si el usuario tiene una sesión autorizada. If req.GetSession.GetAttribute2("user_is_authorized", False) = False Then resp.SendRedirect("/login") Return End If ' Procesa únicamente las peticiones GET, que son las que usan los navegadores para iniciar una conexión SSE. If req.Method = "GET" Then ' Mantiene la petición activa de forma asíncrona para poder enviar datos en el futuro. Dim reqJO As JavaObject = req reqJO.RunMethod("startAsync", Null) ' Registra al nuevo cliente para que empiece a recibir eventos. SSE.AddTarget("stats", resp) Else ' Rechaza cualquier otro método HTTP (POST, PUT, etc.) con un error. resp.SendError(405, "Method Not Allowed") End If End Sub ' --- LÓGICA DE LOS TIMERS --- ' Evento del Timer #1 ("El Vigilante"): se dispara cada 5 segundos. Sub RemoveTimer_Tick ' Log("REMOVETIMER TICK") ' Optimización: si no hay nadie conectado, no hace nada. If Connections.Size = 0 Then Return ' Itera sobre todos los clientes para verificar si siguen activos. For Each key As String In Connections.Keys Try ' Envía un evento "ping" silencioso. Si la conexión está viva, no pasa nada. SSE.SendMessage(Connections.Get(key), "ping", "", 0, "") Catch ' Si el envío falla, la conexión está muerta. Se procede a la limpieza. Log("######################") Log("## Removing (timer cleanup): " & key) Log("######################") 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. 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 allPoolStats.Put(dbKey, connector.GetPoolStats) 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. SSE.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