This commit is contained in:
2025-10-03 00:15:02 +02:00
parent c9806d5fe9
commit 71e16acf9a
20 changed files with 599 additions and 419 deletions

94
internal/websocket/hub.go Normal file
View File

@@ -0,0 +1,94 @@
package websocket
import (
"context"
"log/slog"
"spotify-ws/internal/spotify"
"sync"
"golang.org/x/net/websocket"
)
// Hub manages the set of active clients and broadcasts messages.
type Hub struct {
clients map[*websocket.Conn]struct{}
mu sync.RWMutex
realtime bool
register chan *websocket.Conn
unregister chan *websocket.Conn
broadcast chan *spotify.CurrentlyPlaying
}
// NewHub creates a new Hub.
func NewHub(realtime bool) *Hub {
return &Hub{
clients: make(map[*websocket.Conn]struct{}),
realtime: realtime,
register: make(chan *websocket.Conn),
unregister: make(chan *websocket.Conn),
broadcast: make(chan *spotify.CurrentlyPlaying),
}
}
// Run starts the hub's event loop. It must be run in a separate goroutine.
func (h *Hub) Run(ctx context.Context) {
slog.Info("hub started")
defer slog.Info("hub stopped")
for {
select {
case <-ctx.Done():
h.closeAllConnections()
return
case client := <-h.register:
h.mu.Lock()
h.clients[client] = struct{}{}
h.mu.Unlock()
slog.Debug("client registered", "remoteAddr", client.RemoteAddr())
case client := <-h.unregister:
h.mu.Lock()
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
}
h.mu.Unlock()
slog.Debug("client unregistered", "remoteAddr", client.RemoteAddr())
case state := <-h.broadcast:
h.broadcastState(state)
}
}
}
// Broadcast sends a state update to all connected clients.
func (h *Hub) Broadcast(state *spotify.CurrentlyPlaying) {
h.broadcast <- state
}
// broadcastState handles the actual message sending.
func (h *Hub) broadcastState(state *spotify.CurrentlyPlaying) {
h.mu.RLock()
defer h.mu.RUnlock()
if len(h.clients) == 0 {
return
}
clientPayload := newPlaybackState(state, h.realtime)
for client := range h.clients {
go func(c *websocket.Conn) {
if err := websocket.JSON.Send(c, clientPayload); err != nil {
slog.Warn("failed to broadcast message", "error", err, "remoteAddr", c.RemoteAddr())
}
}(client)
}
}
// closeAllConnections closes all active client connections during shutdown.
func (h *Hub) closeAllConnections() {
h.mu.Lock()
defer h.mu.Unlock()
for client := range h.clients {
if err := client.Close(); err != nil {
slog.Warn("error closing client connection during shutdown", "error", err, "remoteAddr", client.RemoteAddr())
}
}
}