API docs | CHANGELOG | other Clojure libs | Twitter | contact/contributing | current (semantic) version:
[com.taoensso/sente "0.9.0"] ; < v1.0.0 API is subject to change
Sen-te (先手) is a Japanese Go term used to describe a play with such an overwhelming follow-up that it elicits an immediate response, thus leaving its player with the initiative.
Sente is a small client+server library that makes it easy to build reliable, high-performance realtime web applications with Clojure.
Or: We don't need no Socket.IO
Or: The missing piece in Clojure's web application story
Or: Clojure(Script) + core.async + WebSockets/Ajax = The Shiz
(I'd also recommend checking out James Henderson's Chord and Kevin Lynagh's jetty7-websockets-async as possible alternatives!)
- Bidirectional a/sync comms over both WebSockets and Ajax (auto-fallback).
- Robust: auto keep-alives, buffering, protocol selection, reconnects. It just works™.
- Efficient design incl. transparent event batching for low-bandwidth use, even over Ajax.
- Full, transparent support for edn over the wire (JSON, XML, and other arbitrary string-encoded formats may be used as edn strings).
- Tiny, simple API:
make-channel-socket!
and you're good to go. - Automatic, sensible support for users connected with multiple clients and/or devices simultaneously.
- Flexible model: use it anywhere you'd use WebSockets/Ajax/Socket.IO, etc.
- Standard Ring security model: auth as you like, HTTPS when available, CSRF support, etc.
- Fully documented, with examples.
- Small: ~600 lines of code for the entire client+server implementation.
- Supported servers: currently only http-kit, but easily extended. PRs welcome to add support for additional servers!
Protocol | client>server | client>server + ack/reply | server>user push |
---|---|---|---|
WebSockets | ✓ (native) | ✓ (emulated) | ✓ (native) |
Ajax | ✓ (emulated) | ✓ (native) | ✓ (emulated) |
So you can ignore the underlying protocol and deal directly with Sente's unified API. It's simple, and exposes the best of both WebSockets (bidirectionality + performance) and Ajax (optional evented ack/reply model).
Add the necessary dependency to your Leiningen project.clj
. This'll provide your project with both the client (ClojureScript) + server (Clojure) side library code:
[com.taoensso/sente "0.9.0"]
First, make sure you're using http-kit as your Clojure web server. If you're using the standard Ring server (Jetty), http-kit is almost a drop-in replacement.
Why http-kit? Besides being a great web server, it currently offers by far the best high-concurrency support which is something Sente needs to lean on for WebSocket and long-polling connections.
Somewhere in your web app's code you'll already have a routing mechanism in place for handling Ring requests by request URL. If you're using Compojure for example, you'll have something that looks like this:
(defroutes my-app
(GET "/" req (my-landing-pg-handler req))
(POST "/submit-form" req (my-form-submit-handler req)))
For Sente, we're going to add 2 new URLs and setup their handlers:
(ns my-server-side-routing-ns ; .clj
(:require
;; <other stuff>
[taoensso.sente :as sente] ; <--- Add this
))
;;; Add this: --->
(let [{:keys [ch-recv send-fn ajax-post-fn ajax-get-or-ws-handshake-fn]}
(sente/make-channel-socket! {})]
(def ring-ajax-post ajax-post-fn)
(def ring-ajax-get-or-ws-handshake ajax-get-or-ws-handshake-fn)
(def ch-chsk ch-recv) ; ChannelSocket's receive channel
(def chsk-send! send-fn) ; ChannelSocket's send API fn
)
(defroutes my-app
;; <other stuff>
;;; Add these 2 entries: --->
(GET "/chsk" req (ring-ajax-get-or-ws-handshake req))
(POST "/chsk" req (ring-ajax-post req))
)
The
ring-ajax-post
andring-ajax-get-or-ws-handshake
fns will automatically handle Ring GET and POST requests to our channel socket URL ("/chsk"
). Together these take care of the messy details of establishing + maintaining WebSocket or long-polling requests.
You'll setup something similar on the client side:
(ns my-client-side-ns ; .cljs
(:require-macros
[cljs.core.match.macros :refer (match)] ; Optional, useful
[cljs.core.async.macros :as asyncm :refer (go go-loop)])
(:require
;; <other stuff>
[cljs.core.match] ; Optional, useful
[cljs.core.async :as async :refer (<! >! put! chan)]
[taoensso.sente :as sente :refer (cb-success?)] ; <--- Add this
))
;;; Add this: --->
(let [{:keys [chsk ch-recv send-fn]}
(sente/make-channel-socket! "/chsk" ; Note the same path as before
{:type :auto ; e/o #{:auto :ajax :ws}
})]
(def chsk chsk)
(def ch-chsk ch-recv) ; ChannelSocket's receive channel
(def chsk-send! send-fn) ; ChannelSocket's send API fn
)
You're good to go! The client will automatically initiate a WebSocket or repeating long-polling connection to your server.
ch-recv
is a core.async channel that'll receiveevent
s.chsk-send!
is a(fn [event & [?timeout-ms ?cb-fn]])
. This is for standard client>server req>resp calls.
ch-recv
is a core.async channel that'll receiveevent-msg
s.chsk-send!
is a(fn [user-id event])
. This is for async server>user PUSH calls.
===============
Term | Form |
---|---|
event | [<ev-id> <?ev-data>] , e.g. [:my-app/some-req {:data "data"}] |
event-msg | {:ring-req _ :event _ :?reply-fn _} |
<ev-id> |
A namespaced keyword like :my-app/some-req |
<?ev-data> |
An optional arbitrary edn value like {:data "data"} |
:ring-req |
Ring map for Ajax request or WebSocket's initial handshake request |
:?reply-fn |
Present only when client requested a reply (otherwise logs a warning) |
- So clients can use
chsk-send!
to sendevent
s to the server. They can optionally request a reply, with timeout. - The server can likewise use
chsk-send!
to sendevent
s to all the clients (browser tabs, devices, etc.) of a particular connected user by his/heruser-id
. - The server can also use an
event-msg
's?reply-fn
to reply to a clientevent
using an arbitrary edn value.
It's worth noting that the server>user push
(chsk-send! <user-id> <event>)
takes a mandatory user-id argument. See the FAQ later for more info.
(jayq/ajax ; Using the jayq wrapper around jQuery
{:type :post :url "/some-url-on-server/"
:data {:name "Rich Hickey"
:type "Awesome"}
:timeout 8000
:success (fn [content text-status xhr]
(do-something! content))
:error (fn [xhr text-status] (error-handler!))})
(chsk-send! ; Using Sente
[:some/request-id {:name "Rich Hickey" :type "Awesome"}] ; event
8000 ; timeout
;; Optional callback:
(fn [edn-reply]
(if (sente/cb-success? edn-reply) ; Checks for :chsk/closed, :chsk/timeout, :chsk/error
(do-something! edn-reply)
(error-handler!))))
Some important differences to note:
- The Ajax request is slow to initialize, and bulky (HTTP overhead).
- The Sente request is pre-initialized (usu. WebSocket), and lean (edn protocol).
- Ajax would require clumsy long-polling setup, and wouldn't easily support users connected with multiple clients simultaneously.
- Sente:
(chsk-send! "destination-user-id" [:some/alert-id <edn-payload>])
.
For the server to push events, we need a destination. Traditionally we might push to a client (e.g. browser tab). But with modern rich web applications and the increasing use of multiple simultaneous devices (tablets, mobiles, etc.) - the value of a client push is diminishing. You'll often see applications (even by Google) struggling to deal with these cases.
Sente offers an out-the-box solution by pulling the concept of identity one level higher and dealing with unique users rather than clients. What constitutes a user is entirely at the discretion of each application:
- Each user-id may have zero or more connected clients at any given time.
- Each user-id may survive across clients (browser tabs, devices), and sessions.
Set the user's :uid
Ring session key to give him/her an identity.
If you want a simple per-session identity, generate a random uuid. If you want an identity that persists across sessions, try use something with semantic meaning that you may already have like a database-generated user-id, a login email address, a secure URL fragment, etc.
Note that user-ids are used only for server>user push. client>server requests don't take a user-id.
Sure! I use it with Reagent myself. Sente's just a client<->server comms mechanism.
Sente uses edn as an implementation detail of its transfer format. Anything sent with Sente will arrive at the other end as Clojure data.
Send a map, get a map. Send a vector, get a vector. Send a string, get a string.
And since JSON, XML, etc. are all string-encoded formats, using them with Sente is trivial: just send the encoded data as a string, and remember to decode it on the other end however you like.
Relative to network transfer times, the cost of (for example) json->edn->json->data
vs json->data
is negligable. It's also worth noting that the additional encoding isn't actually going to waste, it's buying you features implemented transparently by Sente like protocol negotiation and event batching. These can often outweigh any additional encoding cost.
However you like! If you don't have many events, a simple cond
will probably do. I use core.match myself since it's a nice fit and works well with both Clojure and ClojureScript. The reference example project has a fully-baked example.
Yup, it's automatic for both Ajax and WebSockets. If the page serving your JavaScript (ClojureScript) is running HTTPS, your Sente channel sockets will run over HTTPS and/or the WebSocket equivalent (WSS).
This is important. Sente has support, but you'll need to do a couple things on your end:
- Server-side: you'll need to use middleware like
ring-anti-forgery
to generate and check CSRF codes. Thering-ajax-post
handler should be covered (i.e. protected). - Client-side: you'll need to pass the page's csrf code to the
make-channel-socket!
constructor.
The reference example project has a fully-baked example.
There's a full reference example project in the repo. Call lein start-dev
in that dir to get a (headless) development repl that you can connect to with Cider (emacs) or your IDE.
Further instructions are provided in the relevant namespace.
If I've missed something here, feel free to open a GitHub issue or pop me an email!
lein start-dev
to get a (headless) development repl that you can connect to with Cider (emacs) or your IDE.
Please use the project's GitHub issues page for project questions/comments/suggestions/whatever (pull requests welcome!). Am very open to ideas if you have any!
Otherwise reach me (Peter Taoussanis) at taoensso.com or on Twitter. Cheers!
Copyright © 2012-2014 Peter Taoussanis. Distributed under the Eclipse Public License, the same as Clojure.