Adding a communication channel between server and clients using SSE
This commit is contained in:
+8
-4
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/deluan/navidrome/core/auth"
|
||||
"github.com/deluan/navidrome/log"
|
||||
"github.com/deluan/navidrome/model"
|
||||
"github.com/deluan/navidrome/server/events"
|
||||
"github.com/deluan/rest"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/httprate"
|
||||
@@ -18,12 +19,13 @@ import (
|
||||
)
|
||||
|
||||
type Router struct {
|
||||
ds model.DataStore
|
||||
mux http.Handler
|
||||
ds model.DataStore
|
||||
mux http.Handler
|
||||
broker events.Broker
|
||||
}
|
||||
|
||||
func New(ds model.DataStore) *Router {
|
||||
return &Router{ds: ds}
|
||||
func New(ds model.DataStore, broker events.Broker) *Router {
|
||||
return &Router{ds: ds, broker: broker}
|
||||
}
|
||||
|
||||
func (app *Router) Setup(path string) {
|
||||
@@ -68,6 +70,8 @@ func (app *Router) routes(path string) http.Handler {
|
||||
|
||||
// Keepalive endpoint to be used to keep the session valid (ex: while playing songs)
|
||||
r.Get("/keepalive/*", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"response":"ok"}`)) })
|
||||
|
||||
r.Handle("/events", app.broker)
|
||||
})
|
||||
|
||||
// Serve UI app assets
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package events
|
||||
|
||||
type Event interface {
|
||||
EventName() string
|
||||
}
|
||||
|
||||
type ScanStatus struct {
|
||||
Scanning bool `json:"scanning"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
func (s ScanStatus) EventName() string { return "scanStatus" }
|
||||
|
||||
type KeepAlive struct {
|
||||
TS int64 `json:"ts"`
|
||||
}
|
||||
|
||||
func (s KeepAlive) EventName() string { return "keepAlive" }
|
||||
@@ -0,0 +1,134 @@
|
||||
// Based on https://thoughtbot.com/blog/writing-a-server-sent-events-server-in-go
|
||||
package events
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/deluan/navidrome/log"
|
||||
)
|
||||
|
||||
type Broker interface {
|
||||
http.Handler
|
||||
SendMessage(event Event)
|
||||
}
|
||||
|
||||
type broker struct {
|
||||
// Events are pushed to this channel by the main events-gathering routine
|
||||
Notifier chan []byte
|
||||
|
||||
// New client connections
|
||||
newClients chan chan []byte
|
||||
|
||||
// Closed client connections
|
||||
closingClients chan chan []byte
|
||||
|
||||
// Client connections registry
|
||||
clients map[chan []byte]bool
|
||||
}
|
||||
|
||||
func NewBroker() Broker {
|
||||
// Instantiate a broker
|
||||
broker := &broker{
|
||||
Notifier: make(chan []byte, 1),
|
||||
newClients: make(chan chan []byte),
|
||||
closingClients: make(chan chan []byte),
|
||||
clients: make(map[chan []byte]bool),
|
||||
}
|
||||
|
||||
// Set it running - listening and broadcasting events
|
||||
go broker.listen()
|
||||
|
||||
return broker
|
||||
}
|
||||
|
||||
func (broker *broker) SendMessage(event Event) {
|
||||
pkg := struct {
|
||||
Event `json:"data"`
|
||||
Name string `json:"name"`
|
||||
}{}
|
||||
pkg.Name = event.EventName()
|
||||
pkg.Event = event
|
||||
data, _ := json.Marshal(pkg)
|
||||
broker.Notifier <- data
|
||||
}
|
||||
|
||||
func (broker *broker) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
// Make sure that the writer supports flushing.
|
||||
//
|
||||
flusher, ok := rw.(http.Flusher)
|
||||
|
||||
if !ok {
|
||||
http.Error(rw, "Streaming unsupported!", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
rw.Header().Set("Content-Type", "text/event-stream")
|
||||
rw.Header().Set("Cache-Control", "no-cache, no-transform")
|
||||
rw.Header().Set("Connection", "keep-alive")
|
||||
rw.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
|
||||
// Each connection registers its own message channel with the Broker's connections registry
|
||||
messageChan := make(chan []byte)
|
||||
|
||||
// Signal the broker that we have a new connection
|
||||
broker.newClients <- messageChan
|
||||
|
||||
// Remove this client from the map of connected clients
|
||||
// when this handler exits.
|
||||
defer func() {
|
||||
broker.closingClients <- messageChan
|
||||
}()
|
||||
|
||||
// Listen to connection close and un-register messageChan
|
||||
// notify := rw.(http.CloseNotifier).CloseNotify()
|
||||
notify := req.Context().Done()
|
||||
|
||||
go func() {
|
||||
<-notify
|
||||
broker.closingClients <- messageChan
|
||||
}()
|
||||
|
||||
for {
|
||||
// Write to the ResponseWriter
|
||||
// Server Sent Events compatible
|
||||
_, _ = fmt.Fprintf(rw, "data: %s\n\n", <-messageChan)
|
||||
|
||||
// Flush the data immediately instead of buffering it for later.
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func (broker *broker) listen() {
|
||||
keepAlive := time.NewTicker(15 * time.Second)
|
||||
defer keepAlive.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case s := <-broker.newClients:
|
||||
|
||||
// A new client has connected.
|
||||
// Register their message channel
|
||||
broker.clients[s] = true
|
||||
log.Debug("Client added", "numClients", len(broker.clients))
|
||||
case s := <-broker.closingClients:
|
||||
|
||||
// A client has dettached and we want to
|
||||
// stop sending them messages.
|
||||
delete(broker.clients, s)
|
||||
log.Debug("Removed client", "numClients", len(broker.clients))
|
||||
case event := <-broker.Notifier:
|
||||
|
||||
// We got a new event from the outside!
|
||||
// Send event to all connected clients
|
||||
for clientMessageChan := range broker.clients {
|
||||
clientMessageChan <- event
|
||||
}
|
||||
case ts := <-keepAlive.C:
|
||||
// Send a keep alive packet every 15 seconds
|
||||
broker.SendMessage(&KeepAlive{TS: ts.Unix()})
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user