stream.go
101 lines1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
package application
import (
"fmt"
"net/http"
"strings"
)
// Streamer sends Server-Sent Events. Pair it with HTMX's SSE extension for
// real-time updates. Create one with c.Stream(w), which sets the SSE headers and
// flushes immediately, then push events with Send, SendData, or Render.
//
// func (c *Home) Live(w http.ResponseWriter, r *http.Request) {
// s := c.Stream(w)
// for {
// select {
// case <-r.Context().Done():
// return
// case msg := <-updates:
// s.Send("messages", "<div>"+msg+"</div>")
// }
// }
// }
//
// <div hx-ext="sse" sse-connect="/live" sse-swap="messages">Loading...</div>
type Streamer struct {
w http.ResponseWriter
flusher http.Flusher
app *App
}
// Stream begins an SSE response: it sets the event-stream headers, disables proxy
// buffering, and flushes to establish the connection.
func (c *BaseController) Stream(w http.ResponseWriter) *Streamer {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("X-Accel-Buffering", "no")
flusher, _ := w.(http.Flusher)
if flusher != nil {
flusher.Flush()
}
return &Streamer{w: w, flusher: flusher, app: c.App}
}
// Send emits a named SSE event, matching HTMX's sse-swap. Returns an error if the
// client has disconnected.
func (s *Streamer) Send(event, data string) error {
if _, err := fmt.Fprintf(s.w, "event: %s\n", event); err != nil {
return err
}
if err := s.writeData(data); err != nil {
return err
}
s.flush()
return nil
}
// SendData emits an unnamed ("message") SSE event — for JSON or data consumed by
// JavaScript rather than swapped by HTMX.
func (s *Streamer) SendData(data string) error {
if err := s.writeData(data); err != nil {
return err
}
s.flush()
return nil
}
// Render renders a template to HTML and emits it as a named SSE event — the
// common HTMX pattern where the server produces the final markup. Newlines are
// collapsed to spaces (safe for HTML, required by the SSE line protocol).
func (s *Streamer) Render(event, templateName string, data any) error {
html, err := s.app.RenderToString(templateName, data)
if err != nil {
return err
}
html = strings.ReplaceAll(html, "\n", " ")
if _, err = fmt.Fprintf(s.w, "event: %s\ndata: %s\n\n", event, html); err != nil {
return err
}
s.flush()
return nil
}
// writeData writes each line of data with the SSE "data:" prefix.
func (s *Streamer) writeData(data string) error {
for _, line := range strings.Split(data, "\n") {
if _, err := fmt.Fprintf(s.w, "data: %s\n", line); err != nil {
return err
}
}
_, err := fmt.Fprint(s.w, "\n")
return err
}
func (s *Streamer) flush() {
if s.flusher != nil {
s.flusher.Flush()
}
}