Dragonmark Chat: core.async over the web August 30, 2014
It's the chat demo
Check out the demo!!
Way back when I was focused on Lift and explaining why Lift was different, I created the Lift chat app. The chat app was short, sweet, and highlighted how Lift was different.
As I've been working through the Dragonmark stuff, I decided to use the same Chat app as a demo. Why? 'cause the same concepts are present in Dragonmark... the abstraction of the cross-address-space plumbing.
core.async
across address spaces
The underlying "what the heck is it?" of Dragonmark is
Clojure's core.async
across address spaces. Clojure's immutable, simply serializable
data structures lend themselves very nicely to distribution.
CSP
does not assume sharing address spaces between processes. CSP
simply assumes message sending.
Browser to server and back again
Clojure and ClojureScript are very cool because the developer has fundamentally the same language on the server and in the browser. Clojure and ClojureScript share the same data types and the same functions for operating on that data.
Further, the ClojureScript core.async
implementation avoids
the call-back challenges of the current state of the art in
JavaScript land.
So, being able to distribute CSP channels across the browser and server and then send messages across the address spaces is a great concept... and with Dragonmark it just works.
Works great with Om
core.async
works great with the Om browser app framework.
Add Dragonmark to the mix and you get the same message handling
in the browser, except the messages come from a different
address space.
Show me some code
First, we'll take a look at the chat server:
(defn ^:service add-listener
"Adds a listener. Pass in a channel to get messages"
[the-chan]
(swap! listeners conj the-chan)
(async/put! the-chan (vec (take-last 30 @messages)))
true)
(defn ^:service remove-listener
"Remove the listener"
[the-chan]
(swap! listeners (fn [vec] (filterv #(not (= the-chan %)) vec)))
true)
(defn ^:service send-message
"Sends a message into the chat stream"
[msg]
(if (string? msg)
(do
(swap! messages conj msg)
(swap! listeners
#(vec (remove asyncp/closed? %)))
(mapv (fn [c] (async/put! c msg)) @listeners)
true)
false))
If you speak Clojure, the code is self-explanatory... except
for the ^:service
annotation. The ^:service
thing
tells Dragonmark that the function is part of a service
that can be invoked via the gofor
macro.
When the web server starts, a root message handler is created and the chat service is added to the root message handler:
(circ/gofor
[_ (add base {:channel chat-service
:public true
:service "chat"})]
(println "Added chat")
:error (println "Add error " &err))
Each time a new session and a new page gets created, a handler is also created. The page handler delegates to the session handler and the session handler delegates to the root handler. Delegation is forwarding messages that the given handler does not understand.
The browser looks up the service:
(circ/gofor
:let [other-root (circ/remote-root transport)]
[the-chat-server (locate-service other-root {:service "chat"})]
[_ (add-listener the-chat-server {:the-chan chat-listener})]
(reset! chat-server the-chat-server)
:error (.log js/console "Got error " &err " var " &var)
)
The remote-root
is the handler in the other address space.
The chat service is located with (locate-service other-root {:service "chat"})
and is placed in an atom.
The code also adds the chat-listener
channel to the
chat server's listeners.
The app handles messages from the chat service:
(go
(loop []
(let [info (async/<! chat-listener)]
(if (nil? info)
nil
(do
(if (sequential? info)
(swap! app-state assoc :chat info)
(swap! app-state #(update-in % [:chat]
(fn [x]
(->> (conj x info)
(take-last 30)
vec)))))
(recur))))))
And a chat message is sent to the server with:
(defn- send-chat
[data owner]
(let [the-node (om/get-node owner "chat-in")
chat-string (.-value the-node)
chat-server @chat-server]
(when (and
chat-server
(not (asyncp/closed? chat-server))
chat-string)
;; send the message to the chat server
(async/put! chat-server
{:msg chat-string
:_cmd "send-message"})
;; clear the input
(aset the-node "value" "")
))
)
So, there we have it.
We have code that may or may not be in a different address space. In fact, the chat server is implemented as both Clojure and ClojureScript... so it's totally possible to do all the prototyping of the app in the browser and then just switch to distributed mode.
A side note on "doing things right"
Timothy Baldridge took issue
with my assertion in the gofor
description that waiting for a reply from
a process is the "right way" to do core.async
.
Please let me clarify.
core.async
channels, like the actor model has a default type signature of Any => Unit
. What does that mean? It means you call the message send function (method) with any input and you get nothing back. Put another way, the message send function has no purpose other than to cause a side effect. But for the most part, in functional programming land, we try to
avoid side effects.
And in fact, sometimes we send a message to a channel or a series of messages to a series of channels and care about getting the result of computations from some or all of the channels.
Put another way, sometimes we want Any => T
where T
is a type that's not unit/void.
The gofor
macro allows a much more concise way of describing Any => T
with
automatic timeouts. Timeouts are a good thing because the remote channel may
no longer be processing messages.
When I said "right way" I meant it in the same way that the right
way to call open
in C
involves checking the return value for an error rather than just assuming
it works.
There are plenty of core.async
applications where Any => Unit
is desired
(please see David Nolen's excellent webinar on Om and core.async), then just put!
those messages.
But if you care about doing Any => T
right, Dragonmark and the
gofor
macro reduces the boilerplate in your code.
Where to?
Right now, the Dragonmark handler for browser/server communication is wired into the example code. The next step is to break the code out and make it a separate thing that can be used in your code.