"core.async in the browser is sweet"
October 12, 2015
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
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
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
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
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
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).
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 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.