package infra import ( "context" "crypto/rand" "encoding/hex" "net/http" "time" "nhooyr.io/websocket" ) // WSHandler retorna un http.HandlerFunc que upgradea la conexion HTTP a WebSocket, // crea un WSClient, lo registra en el hub y lanza dos goroutines: // - readPump: lee mensajes del Conn y los publica al hub.Broadcast // - writePump: consume del client.Send y escribe al Conn // // El cliente se desregistra del hub cuando alguna de las pumps termina (cliente // desconectado, error de I/O, o canal cerrado). Asigna un ID hex aleatorio si // no se sobreescribe externamente. func WSHandler(hub *WSHub, origins []string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { conn, err := WSUpgrader(w, r, origins) if err != nil { return } client := &WSClient{ Hub: hub, Conn: conn, Send: make(chan []byte, 64), ID: randomID(), } hub.Register <- client // writePump go wsWritePump(client) // readPump (bloqueante en el handler para mantener viva la request) wsReadPump(client) } } // wsReadPump lee mensajes del Conn y los publica al hub.Broadcast. // Termina si Read retorna error (cliente desconectado o cerrado). // Al terminar, desregistra el cliente y cierra la conexion. func wsReadPump(client *WSClient) { defer func() { // Unregister no bloqueante: si el hub ya esta cerrado, no esperamos select { case client.Hub.Unregister <- client: case <-client.Hub.done: } _ = client.Conn.Close(websocket.StatusNormalClosure, "") }() ctx := context.Background() for { _, data, err := client.Conn.Read(ctx) if err != nil { return } // Encolar al hub.Broadcast sin bloquear si esta lleno select { case client.Hub.Broadcast <- data: default: // Hub saturado: dropear mensaje del cliente para no bloquear el read } } } // wsWritePump consume del canal Send del cliente y escribe al Conn. // Termina si el canal se cierra (hub desregistro al cliente) o si Write falla. func wsWritePump(client *WSClient) { for msg := range client.Send { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) err := client.Conn.Write(ctx, websocket.MessageText, msg) cancel() if err != nil { return } } } // randomID genera un identificador hex aleatorio de 16 caracteres (8 bytes). // No es criptograficamente perfecto para autenticacion — solo identificacion. func randomID() string { b := make([]byte, 8) if _, err := rand.Read(b); err != nil { return "anon" } return hex.EncodeToString(b) }