selfie of Kenton

Kenton Vizdos

i make cool things in go. this is my dev log. note: it's not a technical masterpiece, I just like writing (sometimes poorly) about what I learn!

Learning About Server Sent Events

What led me to SSE #

Over the past few projects I've done, I've wanted a way of adding some basic real-time UI updates (think like in-app Notifications). "Traditionally," I would lean for WebSockets, but something just really felt odd using a full ws connection for only sending events from the server.

Wait!

"Sending events from the server".. hmm.. Server Sent Events to the rescue!

What is a Server Sent Event (SSE)? #

They are similar to WebSockets, as they let the server communicate with the frontend. But that's about it. The client cannot communicate with the server.

Some other pitfalls include:

For my use case, these will work okay. I'm down to risk the "old proxy" mess until it happens..

How do you send an SSE? #

This is one of the beauties. It is SO simple, here is a (highly barebones) example. The following code will send "data: hello!" ever 100ms:

 1mux.HandleFunc("GET /sse", func(w http.ResponseWriter, r *http.Request) {
 2	w.Header().Set("Content-Type", "text/event-stream")
 3	w.Header().Set("Cache-Control", "no-cache")
 4	w.Header().Set("Connection", "keep-alive")
 5
 6	for {
 7		fmt.Fprintf(w, "data: hello!\n\n")
 8		w.(http.Flusher).Flush()
 9		time.Sleep(100 * time.Millisecond)
10	}
11})

Some important parts to note:

Another reason to love Go (i'm sure this exists in other langs), the connection can be detected as closed nearly instantly:

 1for {
 2	select {
 3		case <-time.After(100 * time.Millisecond):
 4			fmt.Fprintf(w, "data: hello!\n\n")
 5			w.(http.Flusher).Flush()
 6		case <-r.Context().Done():
 7			log.Println("Connection closed!")
 8			return
 9	}
10}

Before we dig into event types, let's check out how easy it is to connect this into JS.

Listening to SSE in JS #

Okay, when you need to listen, the simplicity remains:

 1const sseSource = new EventSource("/sse");
 2
 3sseSource.onopen = (e) => {
 4  console.log("The connection has been established, ready for events.");
 5};
 6
 7sseSource.onmessage = (event) => {
 8	console.log(event.data)
 9};
10
11sseSource.onerror = (err) => {
12  console.error("sseSource:", err); // This will automatically try to reconnect, amazing!
13};

Yup, thats pretty much it! However, we can get even fancier.

Note: If you need to send a cross-URL request with cookies, use:

1const sseSource = new EventSource("/sse", {
2  withCredentials: true,
3});

Custom Events in SSE #

While sure, you could always encode an event type in the data: field, it's unnecessary!

There is an event: field in SSE that will be completely hookable in JS. For this demo, we'll specify an example_event:

 1for {
 2	select {
 3		case <-time.After(100 * time.Millisecond):
 4			fmt.Fprintf(w, "event: example_event\ndata: hello, custom event!\n\n") // the \n\n is incredibly important, that is the delimiter for events!
 5			w.(http.Flusher).Flush()
 6		case <-r.Context().Done():
 7			log.Println("Connection closed!")
 8			return
 9	}
10}

In JavaScript, we can hook into it with:

1sseSource.addEventListener("example_event", (event) => {
2  console.log("Example Event Data:", event.data);
3});

And bam! We can now just receive those individual events.

Good to know: The onmessage event still fires for any event that DOES NOT specify an event:

Specifying Reconnection Settings #

Reconnection settings can be modified with retry:. Specified in milliseconds, this field lets you configure how long to wait until a reconnection is tried. (remember to flush this event, heh)

Going Forward #

Sure, you'll still need state management on the server side (and probably a Pub/Sub provider for horizontally scaled services), but in my opinion, it's definitely worth it! There are also packages and the like to make this easier.

Enjoy working with SSEs, they are well supported (minus IE + Opera Mini) and very simple.