// Package ws provides WebSocket connection management for Cyrene. package ws import ( "encoding/json" "log" "net/http" "sync" "github.com/gorilla/websocket" ) var upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, CheckOrigin: func(r *http.Request) bool { return true }, } // Hub manages all active WebSocket connections. type Hub struct { mu sync.RWMutex clients map[*Client]bool register chan *Client unregister chan *Client broadcast chan []byte } // Client represents a single WebSocket connection. type Client struct { hub *Hub conn *websocket.Conn send chan []byte UserID string closed bool mu sync.Mutex } // Message represents a WebSocket message envelope. type Message struct { Type string `json:"type"` Content string `json:"content,omitempty"` SessionID string `json:"session_id,omitempty"` Timestamp int64 `json:"timestamp,omitempty"` Error string `json:"error,omitempty"` } // NewHub creates and starts a new WebSocket hub. func NewHub() *Hub { h := &Hub{ clients: make(map[*Client]bool), register: make(chan *Client), unregister: make(chan *Client), broadcast: make(chan []byte, 256), } go h.run() return h } func (h *Hub) run() { for { select { case client := <-h.register: h.mu.Lock() h.clients[client] = true h.mu.Unlock() log.Printf("[ws] client connected (%d total)", len(h.clients)) case client := <-h.unregister: h.mu.Lock() if _, ok := h.clients[client]; ok { delete(h.clients, client) close(client.send) } h.mu.Unlock() log.Printf("[ws] client disconnected (%d remaining)", len(h.clients)) case message := <-h.broadcast: h.mu.RLock() for client := range h.clients { select { case client.send <- message: default: // Client too slow, skip } } h.mu.RUnlock() } } } // HandleUpgrade upgrades an HTTP connection to WebSocket. func (h *Hub) HandleUpgrade(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Printf("[ws] upgrade error: %v", err) return } userID := r.URL.Query().Get("user_id") if userID == "" { userID = "anonymous" } client := &Client{ hub: h, conn: conn, send: make(chan []byte, 64), UserID: userID, } h.register <- client go client.writePump() go client.readPump() } func (c *Client) readPump() { defer func() { c.hub.unregister <- c c.conn.Close() }() for { _, msgBytes, err := c.conn.ReadMessage() if err != nil { break } var msg Message if err := json.Unmarshal(msgBytes, &msg); err != nil { continue } // Echo back for now (will route to AI-Core later) reply := Message{ Type: "response", Content: "echo: " + msg.Content, SessionID: msg.SessionID, } replyBytes, _ := json.Marshal(reply) c.send <- replyBytes } } func (c *Client) writePump() { defer c.conn.Close() for msg := range c.send { if err := c.conn.WriteMessage(websocket.TextMessage, msg); err != nil { return } } } // Broadcast sends a message to all connected clients. func (h *Hub) Broadcast(msg Message) error { data, err := json.Marshal(msg) if err != nil { return err } h.broadcast <- data return nil } // Count returns the number of connected clients. func (h *Hub) Count() int { h.mu.RLock() defer h.mu.RUnlock() return len(h.clients) } // UpgradeHandler returns an http.HandlerFunc for WebSocket upgrades. func (h *Hub) UpgradeHandler() http.HandlerFunc { return h.HandleUpgrade }