r.Get("/clock", func(c *web.Ctx) error {
return c.SSE(func(s *sse.Stream) error {
t := time.NewTicker(time.Second)
defer t.Stop()
for {
select {
case <-s.Context().Done():
return nil
case now := <-t.C:
if err := s.Send("tick", now.Format(time.TimeOnly)); err != nil {
return err
}
}
}
})
})In plain net/http handlers use sse.Serve(w, r, fn).
Streams#
- Every event is flushed through
http.ResponseController, and the server'sWriteTimeoutis lifted for the stream. - A
: pingcomment goes out every 15s; change it withsse.Heartbeat(d). Stream.Context()ends when the client disconnects, whenfnreturns or when the app shuts down. The HTTP server signals open streams on shutdown, so it never waits on them.s.Send(event, data),s.SendJSON(event, v)ands.SendEvent(sse.Event{ID, Event, Data, Retry});s.LastEventID()reads the client'sLast-Event-IDafter a reconnect.- Field values lose CR and LF, so input cannot forge extra fields.
Hub#
sse.NewHub(broker) fans events out by topic:
hub := sse.NewHub(nil) // nil broker: this process only; pass the drivers/redis client for every instance
err := hub.Publish(ctx, "posts", sse.Event{Event: "created", Data: html})
r.Get("/posts/stream", func(c *web.Ctx) error {
return c.SSE(func(s *sse.Stream) error { return hub.Stream(s, "posts") })
})Subscribers that fall 64 events behind are dropped; the browser reconnects on its own.
With a broker, each topic holds one broker subscription while it has local subscribers on this instance; it is released when the last one leaves, so per-user topics (user:42) do not pile up Redis subscriptions. hub.Subscribe returns once the topic's broker subscription is up (at most 10 seconds), so a publish right after it reaches the stream; if the broker fails or does not answer in time, the stream's channel is closed (the browser reconnects and the next subscriber retries). A custom sse.Broker must close its channel and end the subscription when the Subscribe context is done.
With htmx#
The stream pairs with htmx's hx-sse: send rendered HTML fragments as event data and let htmx swap them in. See HTMX.
For named channels with authorization (private and presence channels), use broadcasting, which also serves over SSE.