"core.async in the browser is sweet"

October 12, 2015

Recently I've built the new version of asciinema player using ClojureScript, Reagent and core.async. The experience was so pleasurable that the player, which was so far my least favorite part of asciinema (I never fully enjoyed building frontends in JavaScript), became my most favorite part of the whole project.

ClojureScript tooling is much, much better now than it was 2 or even 1 year ago, and with Figwheel interactive development experience went to "eleven". If you wanted to get your feet wet with ClojureScript there was never better time to do that (try lein new figwheel hello-world).

When re-building the player I found several neat use cases for core.async which you may find useful in your projects. Below I picked two of them, both accidentally related to time.

Let's start with requiring few pieces from Google Closure and core.async libraries which we'll need later:

(ns demo.core
  (:require-macros [cljs.core.async.macros :refer [go go-loop]])
  (:require [goog.dom :as dom]
            [goog.events :as events]
            [cljs.core.async :refer [put! chan <! >! timeout close!]]))

asciinema player models the animation as a sequence of frames, where each frame is a tuple of delay and data (screen diff). For each frame it waits for the number of seconds specified by the delay and then it updates the terminal by applying the screen diff to the current content of the screen.

Let's simplify the problem (and animation frame model): each frame is a tuple of delay and text to print.

(def frames [
  [1.2 "hell"]
  [0.1 "o"]
  [6.2 " "]
  [1.0 "world"]
  [2.0 "!"]
  ...
])

There's no blocking sleep function in JavaScript, so the usual way of printing each frame's text would be to use setTimeout with a function that prints the text and then schedules next printing with setTimeout again. You can imagine it's not very pretty. But the biggest problem with this solution is that the ceremony with setTimeout completely obscures the intent of the code.

How about this:

(let [data-chan (coll->chan frames)]
  (go-loop []
    (when-let [text (<! data-chan)]
      (print text)
      (recur))))

We loop over the data received from data-chan channel, printing text, breaking the loop when the channel is closed (when-let). It looks like an old school loop that iterates over the elements of a collection, printing them. The difference is that it's not a collection but core.async channel, so the values arrive "when they're ready".

So, when are they ready? Here's coll->chan function used above to convert a sequence of frames to a channel:

(defn coll->chan [coll]
  (let [ch (chan)]
    (go
      (loop [coll coll]
        (when-let [[delay data] (first coll)]
          (<! (timeout (* 1000 delay)))
          (>! ch data)
          (recur (rest coll))))
      (close! ch))
    ch))

This is similar to core.async's to-chan, which turns a collection into a channel and emits all collection values immediately. Here, we know that each collection value is a tuple containing delay and data to emit, so instead of emitting all values as quickly as possible coll->chan sleeps for necessary time before emitting data for a given frame.

In asciinema player coll->chan function is used not only for obtaining a channel of frames, but also for obtaining a channel of cursor blinks ((coll->chan (cycle [[0.5 false] [0.5 true]]))) and a channel of progress bar status updates ((coll->chan (repeat [0.3 true]))). It turned out to be a great abstraction for these time related problems.

The other problem for which core.async happened to be a great fit was detecting user activity. In fullscreen mode I wanted to show the title bar and progress bar when user moves the mouse but hide them 2 seconds after the last mouse move.

We can generalize this problem to: convert arbitrary input channel into an output channel, where output channel emits true after new values start showing up on input, then emits false after no new values show up on input for the specified amount of time, then again waits for new values on input and emits true and so on and so on.

Here's how we can do that:

(defn activity-chan [input msec]
  (let [out (chan)]
    (go-loop []
      ;; wait for activity on input channel
      (<! input)
      (>! out true)

      ;; wait for inactivity on input channel
      (loop []
        (let [t (timeout msec)
              [_ c] (alts! [input t])]
          (when (= c input)
            (recur))))
      (>! out false)

      (recur))
    out))

This function creates and returns an output channel and sends true/false to it according to the above specification.

Note, this assumes the input channel will never close (which may be true in case where you send DOM events onto input channel). If it is closed at some point then... infinite loop (how to make it channel-close-proof is an exercise for the reader).

With activity-chan function in hand, we can detect and print changes to user activity, defined as mouse movement here, with this piece of code:

(let [dom-element (dom/getElement "player")
      mouse-moves (chan)
      mouse-activity (activity-chan mouse-moves 2000)]
  (events/listen dom-element "mousemove" #(put! mouse-moves %))
  (go-loop []
    (print (<! mouse-activity))
    (recur)))

All "mousemove" events get sent to mouse-moves channel, which is feeding mouse-activity channel, from which we read (and print) in a loop. It prints true, false, true, false, true and so on.

These are are just two of many use cases for core.async in the browser. core.async is a really great tool for solving async problems and modeling process communication, and as you can see it can help with abstracting many UI problems, making the code concise and intent revealing.

Read more about clojure, clojurescript, core.async.
blog comments powered by Disqus