Files
dumpsterChat/internal/gateway/hub.go
T
hobokenchicken bb5a56816b Phase 1 MVP: gateway, CRUD handlers, frontend components
Backend:
- WebSocket gateway (hub, client, events) with fanout broadcast
- Server CRUD handlers (create, list, get, update, delete)
- Channel CRUD handlers (create, list, get, update, delete)
- Message CRUD handlers (list with cursor pagination, create, update, delete)
- cmd/migrate standalone migration CLI (up/down)
- cmd/server wired to all handlers + WebSocket + static file serving

Frontend:
- Zustand stores: auth, server, channel, message, websocket
- API client with fetch wrapper
- Terminal-styled components: Layout, LoginForm, ChatArea, ChannelList, ServerBar, MemberList
- React Router with login and main routes
- Gruvbox dark palette throughout

Ops:
- Docker Compose with app service (multi-stage build)
- Caddyfile with WebSocket upgrade support
- Makefile for common tasks
2026-06-26 14:47:29 -04:00

91 lines
2.0 KiB
Go

package gateway
import (
"encoding/json"
"log/slog"
"sync"
)
// Hub maintains the set of active clients and broadcasts messages to them.
type Hub struct {
mu sync.RWMutex
clients map[*Client]struct{}
register chan *Client
unregister chan *Client
broadcast chan []byte
logger *slog.Logger
}
// NewHub creates a new Hub.
func NewHub(logger *slog.Logger) *Hub {
return &Hub{
clients: make(map[*Client]struct{}),
register: make(chan *Client),
unregister: make(chan *Client),
broadcast: make(chan []byte, 256),
logger: logger,
}
}
// Run starts the hub's main event loop. Call this in a goroutine.
func (h *Hub) Run() {
for {
select {
case client := <-h.register:
h.mu.Lock()
h.clients[client] = struct{}{}
h.mu.Unlock()
h.logger.Info("client connected", "user_id", client.UserID)
case client := <-h.unregister:
h.mu.Lock()
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
close(client.send)
}
h.mu.Unlock()
h.logger.Info("client disconnected", "user_id", client.UserID)
case message := <-h.broadcast:
h.mu.RLock()
for client := range h.clients {
select {
case client.send <- message:
default:
go func(c *Client) {
h.unregister <- c
}(client)
}
}
h.mu.RUnlock()
}
}
}
// Register queues a client for registration with the hub.
func (h *Hub) Register(client *Client) {
h.register <- client
}
// Unregister queues a client for removal from the hub.
func (h *Hub) Unregister(client *Client) {
h.unregister <- client
}
// BroadcastEvent serializes an Event and broadcasts it to all connected clients.
func (h *Hub) BroadcastEvent(event Event) {
data, err := json.Marshal(event)
if err != nil {
h.logger.Error("failed to marshal event", "type", event.Type, "error", err)
return
}
h.broadcast <- data
}
// ClientCount returns the number of connected clients.
func (h *Hub) ClientCount() int {
h.mu.RLock()
defer h.mu.RUnlock()
return len(h.clients)
}