Files
fn_registry/functions/infra/ws_handler.go
T

93 lines
2.5 KiB
Go

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)
}