jarohen / chord

A library designed to bridge the gap between the triad of CLJ/CLJS, web-sockets and core.async.
439 stars 40 forks source link

A lightweight Clojure/ClojureScript library designed to bridge the gap between the triad of CLJ/CLJS, web-sockets and core.async.

** Usage

Include the following in your =project.clj=:

+BEGIN_SRC clojure

[jarohen/chord "0.8.1"]

+END_SRC

Chord now supports EDN, JSON, Transit and Fressian out of the box - please remove dependencies to 'chord-fressian' and 'chord-transit' if you have them. Thanks to [[https://github.com/lsnape][Luke Snape]], [[https://github.com/rosejn][Jeff Rose]] and [[https://github.com/tgetgood][Thomas Getgood]] for their help supporting these formats!

*** Example project

There is a simple example server/client project under the =example-project= directory. The client sends websocket messages to the server, that get echoed back to the client and written on the page.

You can run it with =lein dev= - an alias that starts up an http-kit server using [[https://github.com/james-henderson/lein-frodo][Frodo]] and automatically re-compiles the CLJS.

Once it is running - navigate to [[http://localhost:3000/]] and you should see =Send a message to the server:=

*** ClojureScript

Chord only has one function, =chord.client/ws-ch=, which takes a web-socket URL and returns a map, containing either =:ws-channel= or =:error=. When the connection opens successfully, this channel then returns a two-way channel that you can use to communicate with the web-socket server:

+BEGIN_SRC clojure

(:require [chord.client :refer [ws-ch]] [cljs.core.async :refer [<! >! put! close!]]) (:require-macros [cljs.core.async.macros :refer [go]])

(go (let [{:keys [ws-channel error]} (<! (ws-ch "ws://localhost:3000/ws"))] (if-not error (>! ws-channel "Hello server from client!") (js/console.log "Error:" (pr-str error)))))

+END_SRC

Messages that come from the server are received as a map with a =:message= key:

+BEGIN_SRC clojure

(go (let [{:keys [ws-channel]} (<! (ws-ch "ws://localhost:3000/ws")) {:keys [message]} (<! ws-channel)] (js/console.log "Got message from server:" (pr-str message))))

+END_SRC

Errors in the web-socket channel (i.e. if the server goes away) are returned as a map with an =:error= key:

+BEGIN_SRC clojure

(go (let [{:keys [ws-channel]} (<! (ws-ch "ws://localhost:3000/ws")) {:keys [message error]} (<! ws-channel)] (if error (js/console.log "Uh oh:" error) (js/console.log "Hooray! Message:" (pr-str message)))))

+END_SRC

As of 0.3.0, you can pass a =:format= option, to pass messages over the channel as EDN (default), as raw strings, or JSON (0.3.1). Valid formats are =#{:edn :json :json-kw :str :fressian :transit-json}=, defaulting to =:edn=.

(If you do use fressian, you'll need to require =chord.format.fressian=, in addition to the usual Chord namespaces)

+BEGIN_SRC clojure

(:require [cljs.core.async :as a]) (ws-ch "ws://localhost:3000/ws" {:format :json-kw})

+END_SRC

As of 0.2.1, you can configure the buffering of the channel by (optionally) passing custom read/write channels, as follows:

+BEGIN_SRC clojure

(:require [cljs.core.async :as a]) (ws-ch "ws://localhost:3000/ws" {:read-ch (a/chan (a/sliding-buffer 10)) :write-ch (a/chan 5)})

+END_SRC

By default, Chord uses unbuffered channels, like core.async itself.

*** Clojure

Chord wraps the websocket support provided by [[http://http-kit.github.io][http-kit]], a fast Clojure web server compatible with Ring.

N.B. Currently, Ring's standard Jetty adapter ~does not~ support Websockets. [[http://http-kit.github.io][http-kit]] is a Ring-compatible alternative.

Again, there's only one entry point to remember here: a wrapper around http-kit's =with-channel= macro. The only difference is that, rather than using http-kit's functions to interface with the channel, you can use core.async's primitives.

Chord's =with-channel= is used as follows:

+BEGIN_SRC clojure

(:require [chord.http-kit :refer [with-channel]] [org.httpkit.server :refer [run-server]] [clojure.core.async :refer [<! >! put! close! go]])

(defn your-handler [req] (with-channel req ws-ch (go (let [{:keys [message]} (<! ws-ch)] (prn "Message received:" message) (>! ws-ch "Hello client from server!") (close! ws-ch)))))

+END_SRC

This can take a =:format= option, and custom buffered read/write channels as well:

+BEGIN_SRC clojure

(require '[clojure.core.async :as a])

(defn your-handler [req] (with-channel req ws-ch {:read-ch (a/chan (a/dropping-buffer 10)) :format :str} ; again, :edn is default (go (let [{:keys [message]} (<! ws-ch)] (prn "Message received:" message) (>! ws-ch "Hello client from server!") (close! ws-ch)))))

+END_SRC

You can also use the =wrap-websocket-handler= middleware, which will put a =:ws-channel= key in the request map:

+BEGIN_SRC clojure

(require '[chord.http-kit :refer [wrap-websocket-handler]] '[org.httpkit.server :refer [run-server]] '[clojure.core.async :as a])

(defn your-handler [{:keys [ws-channel] :as req}] (go (let [{:keys [message]} (<! ws-channel)] (println "Message received:" message) (>! ws-channel "Hello client from server!") (close! ws-channel))))

(run-server (-> #'your-handler wrap-websocket-handler) {:port 3000})

+END_SRC

You can pass custom channels to =wrap-websocket-handler= as a second (optional) parameter:

+BEGIN_SRC clojure

(run-server (-> #'your-handler (wrap-websocket-handler {:read-ch ...})) {:port 3000})

+END_SRC

** Bug reports/pull requests/comments/suggestions etc?

Yes please! Please submit these in the traditional GitHub manner.

** Contributors

Chord's contributors are listed in the ChangeLog - thank you all for your help!

** License

Copyright © 2013-2015 James Henderson

Distributed under the Eclipse Public License, the same as Clojure.