feat: tipos WSHub, WSClient, WSMessage, SSEEvent (issue 0011 fase 1)

This commit is contained in:
2026-04-18 17:10:28 +02:00
parent 95826cb14f
commit 0255207514
10 changed files with 263 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
package infra
// SSEEvent es un evento Server-Sent Events segun la spec W3C.
// Campos opcionales: si Event esta vacio se envia solo data,
// si ID esta vacio no se incluye campo id, Retry en ms (0 = omitir).
type SSEEvent struct {
Event string `json:"event"`
Data string `json:"data"`
ID string `json:"id"`
Retry int `json:"retry"`
}
+15
View File
@@ -0,0 +1,15 @@
package infra
import (
"nhooyr.io/websocket"
)
// WSClient representa una conexion WebSocket individual.
// Cada cliente tiene su propio canal de envio buffereado y una
// referencia al hub al que pertenece.
type WSClient struct {
Hub *WSHub
Conn *websocket.Conn
Send chan []byte
ID string
}
+69
View File
@@ -0,0 +1,69 @@
package infra
// 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{}
}
// 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)
}
return
case client := <-h.Register:
h.Clients[client] = true
case client := <-h.Unregister:
if _, ok := h.Clients[client]; ok {
delete(h.Clients, client)
close(client.Send)
}
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)
}
}
}
}
}
// 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)
}
}
+10
View File
@@ -0,0 +1,10 @@
package infra
// WSMessage es un mensaje tipado que viaja por WebSocket.
// El campo Type permite al receptor decidir como procesar el payload.
type WSMessage struct {
Type string `json:"type"`
Payload []byte `json:"payload"`
SenderID string `json:"sender_id"`
Ts int64 `json:"ts"`
}