167 lines
3.1 KiB
Go
167 lines
3.1 KiB
Go
package goblocks
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type Message struct {
|
|
Event string
|
|
Data any
|
|
}
|
|
|
|
type Client struct {
|
|
ID string
|
|
Send chan Message
|
|
Close chan struct{}
|
|
IsActive bool
|
|
}
|
|
|
|
type SSEBroker struct {
|
|
clients map[string]*Client
|
|
mu sync.RWMutex
|
|
ticker *time.Ticker
|
|
done chan struct{}
|
|
}
|
|
|
|
func newSSEBroker() *SSEBroker {
|
|
s := &SSEBroker{
|
|
clients: make(map[string]*Client),
|
|
ticker: time.NewTicker(time.Minute),
|
|
done: make(chan struct{}),
|
|
}
|
|
go s.run()
|
|
return s
|
|
}
|
|
|
|
func (s *SSEBroker) run() {
|
|
for {
|
|
select {
|
|
case <-s.ticker.C:
|
|
s.Broadcast("ticker")
|
|
case <-s.done:
|
|
s.ticker.Stop()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *SSEBroker) registerClient(id string) *Client {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
client := &Client{
|
|
ID: id,
|
|
Send: make(chan Message, 100),
|
|
Close: make(chan struct{}),
|
|
IsActive: true,
|
|
}
|
|
s.clients[id] = client
|
|
return client
|
|
}
|
|
|
|
func (s *SSEBroker) unregisterClient(id string) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
if client, exists := s.clients[id]; exists {
|
|
close(client.Close)
|
|
delete(s.clients, id)
|
|
}
|
|
}
|
|
|
|
func (s *SSEBroker) Broadcast(event string, data ...any) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
var dataValue any
|
|
if len(data) > 0 {
|
|
dataValue = data[0]
|
|
}
|
|
|
|
message := Message{
|
|
Event: event,
|
|
Data: dataValue,
|
|
}
|
|
|
|
for _, client := range s.clients {
|
|
if client.IsActive {
|
|
select {
|
|
case client.Send <- message:
|
|
default:
|
|
slog.Warn("client channel full, skipping message", "client_id", client.ID)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *SSEBroker) HandleSSE(w http.ResponseWriter, r *http.Request) {
|
|
slog.Debug("SSE called")
|
|
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
w.Header().Set("Connection", "keep-alive")
|
|
w.Header().Set("Transfer-Encoding", "chunked")
|
|
|
|
clientID := uuid.New().String()
|
|
client := s.registerClient(clientID)
|
|
defer s.unregisterClient(clientID)
|
|
|
|
flusher, ok := w.(http.Flusher)
|
|
if !ok {
|
|
http.Error(w, "Streaming unsupported!", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
slog.Info("SSE connection established", "client_id", clientID)
|
|
|
|
fmt.Fprintf(w, "event: connection\ndata: %s\n\n", clientID)
|
|
flusher.Flush()
|
|
|
|
clientGone := r.Context().Done()
|
|
|
|
for {
|
|
select {
|
|
case message := <-client.Send:
|
|
var data string
|
|
switch v := message.Data.(type) {
|
|
case string:
|
|
data = v
|
|
case map[string]any, []any:
|
|
jsonData, err := json.Marshal(v)
|
|
if err != nil {
|
|
slog.Error("error marshaling message", "error", err)
|
|
continue
|
|
}
|
|
data = string(jsonData)
|
|
default:
|
|
jsonData, err := json.Marshal(v)
|
|
if err != nil {
|
|
slog.Error("error marshaling message", "error", err)
|
|
continue
|
|
}
|
|
data = string(jsonData)
|
|
}
|
|
fmt.Fprintf(w, "event: %s\ndata: %s\n\n", message.Event, data)
|
|
case <-client.Close:
|
|
slog.Info("client closed", "client_id", clientID)
|
|
return
|
|
case <-clientGone:
|
|
slog.Info("client gone", "client_id", clientID)
|
|
return
|
|
}
|
|
|
|
flusher.Flush()
|
|
}
|
|
}
|
|
|
|
func (s *SSEBroker) Shutdown() {
|
|
close(s.done)
|
|
}
|