- A recent version of Emacs (the one used to write this document initially was 24.5.1);
- Clojure and leiningen;
- Emacs configured to talk to a Clojure REPL (preferably using CIDER);
First run scripts/brepl
and access http://localhost:9000 on a browser to
have access to the REPL running code in the browser environment. This is
needed for some js functions (like setTimeout
) to work.
Configure org-babel
(you must have ob-clojure
installed):
(require 'org)
(org-babel-do-load-languages
'org-babel-load-languages
'((clojure . t)))
After that, make sure you have CIDER connected to the REPL and run the following to connect to a headless browser (make sure you have PhantomJS installed).
(cemerick.piggieback/cljs-repl :repl-env (cemerick.austin/exec-env))
To test if the setup was successful, check if running the following block shows the message “2” in the minibuffer.
(+ 1 1)
To generate the source code, use hit C-c C-v C-t
for the tangle step.
Unfortunately, due to JavaScript’s limitation, there’s no way to block the main “thread”, so the examples are a bit harder to visualize. Stuff is still printed on the REPL, but results can’t be collected to this document.
First, lets import the necessary dependencies to the namespace.
(ns re-interval.core
(:require [cljs.core.async :refer [chan timeout <! >! alts! close! put!]]
[re-frame.core :refer [register-handler dispatch]])
(:require-macros [cljs.core.async.macros :refer [go go-loop]]))
The simplest interval, without any way to control it other than directly manipulating the code, is as follows:
(go-loop [ticks-left 3]
(if-not (zero? ticks-left)
(do (<! (timeout 100))
(print "tick...")
(recur (dec ticks-left)))))
To communicate with the thread, we have to create a control channel.
(let [control-channel (chan)]
;; controlled thread
(go-loop [cmd (<! control-channel)]
(when (= cmd :tick)
(print "tick..."))
(when-not (= cmd :exit)
(recur (<! control-channel))))
;; controller
(go-loop [ticks-left 3]
(if-not (zero? ticks-left)
(do (<! (timeout 100))
(>! control-channel :tick)
(recur (dec ticks-left)))
(>! control-channel :exit))))
But that’s a bit useless, as the controller is calling the shots on the time
interval. A more useful controller would send start
and stop
commands to
the timer. We will use alts!
to combine timeout
and control-channel
. The
following example is an interval that exits on any command received. A second
go block is created to simulate an asynchronous controller. We will wrap it
all in a creator, and return only the control channel so we can have as many
intervals as we want. To make it even more useful, make-interval
receives a
function to be called when it’s time to tick.
(defn interval
"Returns a control channel to an interval. Commands are:
- `:start`: Starts counting time. If counter is already active, does nothing;
- `:restart`: Starts counting time from 0. If stopped, will start anyway.
- `:stop`: Stops the counter. Next time `:start` is called, counter starts from zero.
Any other command issued will be treated as an exit signal."
[timeout-in-msecs f & args]
(let [control-channel (chan)]
;; controlled thread - will exit on any command received
(go-loop [channels [control-channel]]
(let [[cmd ch] (alts! channels)]
(if(identical? ch control-channel) ;; a command was received
(case cmd
:start (recur [control-channel
(or (second channels)
(timeout timeout-in-msecs))])
:restart (recur [control-channel
(timeout timeout-in-msecs)])
:stop (recur [control-channel])
nil) ;; will exit on unknown command
(do ;; timeout channel happened, so it's time to tick and reset counter
(apply f args)
(recur [control-channel (timeout timeout-in-msecs)])))))
control-channel))
A basic controller can be created as follows.
(let [control-channel (interval 300 #(print "tick..."))]
(go
(print "controller> wait before starting")
(<! (timeout 600))
(print "controller> starting")
(>! control-channel :start)
(print "controller> waiy before stopping")
(<! (timeout 1200))
(print "controller> stopping")
(>! control-channel :stop)
(print "controller> exiting")
(close! control-channel)))
We’re now able to make it interact with re-frame. We want to control the
intervals using re-frame’s events. Event handlers should be registered
according to a prefix. For instance, if :foo
is passed as the controller’s
prefix, the interval would be controlled by :foo/start
, :foo/stop
and
:foo/restart
. An event :foo/tick
will be dispatched every time the
interval ticks.
(defn register-interval-handlers
[k-pref middleware timeout-in-msecs]
(let [pref (name k-pref)
control-channel (interval timeout-in-msecs
dispatch [(keyword pref "tick")])]
(doseq [action [:start :stop :restart :exit]]
(register-handler
(keyword pref (name action))
middleware
(fn [db _]
(put! control-channel action)
db)))))