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

This commit is contained in:
2026-04-18 17:10:28 +02:00
parent 3bc2d2573d
commit 2313f1aa55
10 changed files with 263 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
---
name: SSEEvent
lang: go
domain: infra
version: "1.0.0"
algebraic: product
definition: |
type SSEEvent struct {
Event string `json:"event"`
Data string `json:"data"`
ID string `json:"id"`
Retry int `json:"retry"`
}
description: "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 y dejar el default del browser ~3000ms)."
tags: [sse, event, server-sent-events, infra, realtime]
uses_types: []
file_path: "functions/infra/sse_event.go"
---
## Ejemplo
```go
ev := SSEEvent{
Event: "metrics_update",
ID: "42",
Data: `{"cpu": 23.4, "mem": 87.1}`,
}
SSESend(w, ev)
```
Wire format generado:
```
event: metrics_update
id: 42
data: {"cpu": 23.4, "mem": 87.1}
```
## Notas
Tipo producto con campos opcionales. `Data` puede contener saltos de linea — el formateador los traduce a multiples lineas `data:` segun la spec. `Retry` solo se envia si es > 0; cuando se envia, indica al cliente cuantos ms esperar antes de reconectar.
Para enviar solo un comentario keepalive (no un evento), no se usa este tipo: se escribe `: keepalive\n\n` directamente al writer.
+38
View File
@@ -0,0 +1,38 @@
---
name: WSClient
lang: go
domain: infra
version: "1.0.0"
algebraic: product
definition: |
type WSClient struct {
Hub *WSHub
Conn *websocket.Conn
Send chan []byte
ID string
}
description: "Conexion WebSocket individual gestionada por un WSHub. Cada cliente tiene su propio canal Send buffereado para entregar mensajes en orden y un ID para identificarlo en broadcasts y handlers."
tags: [websocket, client, connection, server, infra, realtime]
uses_types: [WSHub_go_infra]
file_path: "functions/infra/ws_client.go"
---
## Ejemplo
```go
client := &WSClient{
Hub: hub,
Conn: wsConn,
Send: make(chan []byte, 64),
ID: "user-42",
}
hub.Register <- client
```
## Notas
Tipo producto. El canal `Send` debe ser buffereado (tipico 64-256) para evitar que un cliente lento bloquee el broadcast del hub. `Conn` usa `nhooyr.io/websocket` que soporta `context.Context` nativamente. `ID` es texto libre — la app que monta el handler decide su semantica (UUID, username, session id, etc.).
Cada cliente tiene normalmente dos goroutines internas:
- readPump: lee del Conn y publica al hub.Broadcast (o procesa con callback)
- writePump: consume del Send y escribe al Conn
+36
View File
@@ -0,0 +1,36 @@
---
name: WSHub
lang: go
domain: infra
version: "1.0.0"
algebraic: product
definition: |
type WSHub struct {
Clients map[*WSClient]bool
Broadcast chan []byte
Register chan *WSClient
Unregister chan *WSClient
}
description: "Hub que 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 Run() y se compone con ws_handler para servir conexiones."
tags: [websocket, hub, server, broadcast, infra, realtime]
uses_types: [WSClient_go_infra]
file_path: "functions/infra/ws_hub.go"
---
## Ejemplo
```go
hub := NewWSHub()
go hub.Run()
defer hub.Stop()
routes := []Route{
{Method: "GET", Path: "/ws", Handler: WSHandler(hub, []string{"*"})},
}
```
## Notas
Tipo producto. El campo `done` interno (no exportado) controla el cierre limpio. `Broadcast` es buffereado (256) para no bloquear emisores. Cada cliente lento se desconecta automaticamente si su canal `Send` se llena durante un broadcast — esta es la garantia anti-backpressure: un cliente que no consume no afecta a los demas.
El loop `Run()` es de un solo thread escribiendo al mapa, asi que no hace falta mutex. Para parar limpiamente: `hub.Stop()` cierra `done`, libera todos los `client.Send` y termina el loop.
+37
View File
@@ -0,0 +1,37 @@
---
name: WSMessage
lang: go
domain: infra
version: "1.0.0"
algebraic: product
definition: |
type WSMessage struct {
Type string `json:"type"`
Payload []byte `json:"payload"`
SenderID string `json:"sender_id"`
Ts int64 `json:"ts"`
}
description: "Mensaje tipado que viaja por WebSocket entre cliente y servidor. El campo Type permite al receptor decidir como procesar el payload. Incluye SenderID y timestamp para trazabilidad."
tags: [websocket, message, protocol, infra, realtime]
uses_types: []
file_path: "functions/infra/ws_message.go"
---
## Ejemplo
```go
msg := WSMessage{
Type: "chat",
Payload: []byte(`{"text":"hola"}`),
SenderID: "user-1",
Ts: time.Now().UnixMilli(),
}
data, _ := json.Marshal(msg)
WSBroadcast(hub, data)
```
## Notas
Tipo producto. Las tags `json:` permiten serializar directamente a JSON para enviar por el wire. `Payload` es `[]byte` para permitir tanto JSON anidado como datos binarios codificados en base64. `Ts` en milisegundos epoch para compatibilidad con `Date.now()` en el browser.
No es obligatorio usar este tipo — apps que necesiten un protocolo distinto pueden enviar bytes arbitrarios via `WSBroadcast` o `WSSend`. Es solo un convenio recomendado para protocolos chat-like.