Aleph/Ring, SSE and live reloading

Hello Peeps,

I’m working on a Conduit app (https://www.realworld.how/) in clojure and htmx.

I’m replicating this live reloading setup I have on a similar project but built in go. It uses Server Side Events (SSE) to let the client know that the server has restarted.

I’m using aleph to enable streaming events seen here

copied here for posterity

(defn ->get-sse [on-start-ch]
  (fn get-sse [_]
    (let [out (chan 1)]
      (go
        ; (println "SSE: connection established")
        (>! out "data: connected\n\n")
        ; (println "SSE: waiting for update")
        ; wait for the on-start-ch
        (<! on-start-ch)
        ; (println "SSE: update received")
        (>! out "data: updated\n\n")
        ; (println "SSE: updated")
        (close! out))
      (->
        out
        (->source)
        (util/response)
        (util/content-type "text/event-stream")
        (util/header "Cache-Control" "no-cache")
        (util/header "Connection" "keep-alive")))))

here is the Go version for comparison (trigger warning: non-fp code):

// get SSE messages on connection and on update
// on update, send message to client, where the client will reload the page
// see ./hot-reload.templ
func (c *Controller) getSSE(fc *fiber.Ctx) error {
	fc.Set("Content-Type", "text/event-stream")
	fc.Set("Cache-Control", "no-cache")
	fc.Set("Connection", "keep-alive")

	fc.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) {
		c.log.Debug("SSE connection established")
		fmt.Fprintf(w, "data: connected\n\n")
		err := w.Flush()

		if err != nil {
			c.log.Error("Error while flushing", "error", err)
		}

		<-c.onStart
		fmt.Fprintf(w, "data: updated\n\n")
		c.log.Debug("SSE update message sent")

		err = w.Flush()
		if err != nil {
			c.log.Error("Error while flushing", "error", err)
		}
		return
	}))

	return nil
}

The go version works pretty flawlessly but the clojure version seems to take a while for the push the new event to the client on start. And if I force a reload of the page, the connected and on start events are sent at the same time causing another restart.

here is the js script which is the same on both:

      function initHotReload() {
        if (typeof EventSource !== 'undefined') {

          var source = new EventSource('/__hotreload');

          source.onmessage = function(event) {
            if (event.data === 'updated') {
              console.log('hotreload: updated');
              source.close();
              setTimeout(() => {
                window.location.reload();
              }, 500);
            } else if (event.data === 'connected') {
              console.log('hotreload: connected');
            } else {
              console.log('hotreload: unknown event', event);
            }
          };

          source.onerror = function(event) {
            console.log('hotreload: err', event.message);
            source.close();
            setTimeout(initHotReload, 1000);
          };
          window.onbeforeunload = function() {
            source.close();
          };
        } else {
          console.log('Your browser does not support server-sent events...');
        }
      }

      setTimeout(initHotReload, 1000);

Any idea why this would be? I probably misunderstand the core.async logic here. Seems like it should be similar…

EDIT:

Ok, so I believe it may be due to the server not closing the connection right away when the request to close is happening. It seems like the sse connection is still alive for about 2 secs after the reset has happened.

Ok, so the issue wasn’t with the code above but with the Aleph server. The default timeout is 15secs, setting this to 0 makes the live reload work flawlessly again!

I’ll have this set to 0 during dev…

Hi @elOsoMF . Glad you solved your issue. For future reference, we usually hang out at #aleph on the Clojure Slack. If you have any more issues, try there; you’ll probably get a faster response.