179 lines
4.0 KiB
Go
179 lines
4.0 KiB
Go
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
package devserver
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const eventsPath = "/__himesan/events"
|
|
|
|
// Diagnostic is a compiler/build diagnostic suitable for the development
|
|
// browser overlay. The CLI may map its compiler's native diagnostics through
|
|
// Options.MapDiagnostics without coupling this package to the compiler.
|
|
type Diagnostic struct {
|
|
Path string `json:"path,omitempty"`
|
|
Line int `json:"line,omitempty"`
|
|
Column int `json:"column,omitempty"`
|
|
Code string `json:"code,omitempty"`
|
|
Message string `json:"message"`
|
|
Severity string `json:"severity,omitempty"`
|
|
}
|
|
|
|
// Event is delivered both to Options.OnEvent and to connected browser clients.
|
|
// Type is currently one of "ready", "reload", or "diagnostic".
|
|
type Event struct {
|
|
Type string `json:"type"`
|
|
Phase string `json:"phase,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
Diagnostics []Diagnostic `json:"diagnostics,omitempty"`
|
|
At time.Time `json:"at"`
|
|
}
|
|
|
|
type eventHub struct {
|
|
mu sync.Mutex
|
|
subscribers map[chan Event]struct{}
|
|
latest *Event
|
|
closed bool
|
|
}
|
|
|
|
func newEventHub() *eventHub {
|
|
return &eventHub{subscribers: make(map[chan Event]struct{})}
|
|
}
|
|
|
|
func (h *eventHub) publish(event Event) {
|
|
if event.At.IsZero() {
|
|
event.At = time.Now().UTC()
|
|
}
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
if h.closed {
|
|
return
|
|
}
|
|
if event.Type == "diagnostic" {
|
|
copy := event
|
|
h.latest = ©
|
|
} else if event.Type == "reload" {
|
|
h.latest = nil
|
|
}
|
|
for subscriber := range h.subscribers {
|
|
select {
|
|
case subscriber <- event:
|
|
default:
|
|
// Reload and diagnostic events are snapshots, not a log. Replace the
|
|
// oldest queued snapshot so a slow browser still receives the newest
|
|
// state transition.
|
|
select {
|
|
case <-subscriber:
|
|
default:
|
|
}
|
|
select {
|
|
case subscriber <- event:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (h *eventHub) subscribe() (<-chan Event, func()) {
|
|
updates := make(chan Event, 8)
|
|
h.mu.Lock()
|
|
if h.closed {
|
|
close(updates)
|
|
h.mu.Unlock()
|
|
return updates, func() {}
|
|
}
|
|
h.subscribers[updates] = struct{}{}
|
|
if h.latest != nil {
|
|
updates <- *h.latest
|
|
}
|
|
h.mu.Unlock()
|
|
|
|
var once sync.Once
|
|
return updates, func() {
|
|
once.Do(func() {
|
|
h.mu.Lock()
|
|
if _, ok := h.subscribers[updates]; ok {
|
|
delete(h.subscribers, updates)
|
|
close(updates)
|
|
}
|
|
h.mu.Unlock()
|
|
})
|
|
}
|
|
}
|
|
|
|
func (h *eventHub) close() {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
if h.closed {
|
|
return
|
|
}
|
|
h.closed = true
|
|
for subscriber := range h.subscribers {
|
|
close(subscriber)
|
|
delete(h.subscribers, subscriber)
|
|
}
|
|
}
|
|
|
|
func (h *eventHub) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
w.Header().Set("Allow", http.MethodGet)
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
flusher, ok := w.(http.Flusher)
|
|
if !ok {
|
|
http.Error(w, "streaming is unavailable", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
|
|
w.Header().Set("Cache-Control", "no-store")
|
|
w.Header().Set("Connection", "keep-alive")
|
|
w.Header().Set("X-Accel-Buffering", "no")
|
|
w.WriteHeader(http.StatusOK)
|
|
if err := writeSSE(w, Event{Type: "ready", At: time.Now().UTC()}); err != nil {
|
|
return
|
|
}
|
|
flusher.Flush()
|
|
|
|
updates, unsubscribe := h.subscribe()
|
|
defer unsubscribe()
|
|
heartbeat := time.NewTicker(15 * time.Second)
|
|
defer heartbeat.Stop()
|
|
for {
|
|
select {
|
|
case <-r.Context().Done():
|
|
return
|
|
case event, ok := <-updates:
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := writeSSE(w, event); err != nil {
|
|
return
|
|
}
|
|
flusher.Flush()
|
|
case <-heartbeat.C:
|
|
if _, err := fmt.Fprint(w, ": heartbeat\n\n"); err != nil {
|
|
return
|
|
}
|
|
flusher.Flush()
|
|
}
|
|
}
|
|
}
|
|
|
|
func writeSSE(w http.ResponseWriter, event Event) error {
|
|
payload, err := json.Marshal(event)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := fmt.Fprintf(w, "event: %s\n", event.Type); err != nil {
|
|
return err
|
|
}
|
|
_, err = fmt.Fprintf(w, "data: %s\n\n", payload)
|
|
return err
|
|
}
|