package infra import "sync/atomic" // WSHub gestiona el ciclo de vida de conexiones WebSocket. // Mantiene un mapa de clientes activos y canales para registro, // desregistro y broadcast. Se ejecuta como goroutine via su Run(). type WSHub struct { Clients map[*WSClient]bool Broadcast chan []byte Register chan *WSClient Unregister chan *WSClient done chan struct{} count atomic.Int64 } // ClientCount retorna el numero de clientes registrados de forma thread-safe. // Util para metricas, dashboards y tests. func (h *WSHub) ClientCount() int { return int(h.count.Load()) } // NewWSHub crea un WSHub vacio con canales inicializados. // Ejecutar Run() en una goroutine para activar el loop de eventos. func NewWSHub() *WSHub { return &WSHub{ Clients: make(map[*WSClient]bool), Broadcast: make(chan []byte, 256), Register: make(chan *WSClient), Unregister: make(chan *WSClient), done: make(chan struct{}), } } // Run ejecuta el loop principal del hub. Atiende registros, desregistros // y broadcasts en un select. Bloqueante: lanzar como goroutine. // Para parar, llamar a Stop(). func (h *WSHub) Run() { for { select { case <-h.done: // Cerrar todos los Send y vaciar el mapa for client := range h.Clients { delete(h.Clients, client) close(client.Send) } h.count.Store(0) return case client := <-h.Register: h.Clients[client] = true h.count.Store(int64(len(h.Clients))) case client := <-h.Unregister: if _, ok := h.Clients[client]; ok { delete(h.Clients, client) close(client.Send) h.count.Store(int64(len(h.Clients))) } case msg := <-h.Broadcast: for client := range h.Clients { select { case client.Send <- msg: default: // Cliente lento: desconectar delete(h.Clients, client) close(client.Send) } } h.count.Store(int64(len(h.Clients))) } } } // Stop cierra el loop de Run() y limpia todos los clientes. // Idempotente — llamar mas de una vez no panica. func (h *WSHub) Stop() { select { case <-h.done: // Ya cerrado default: close(h.done) } }