-
-
Notifications
You must be signed in to change notification settings - Fork 193
1 Getting started
See also here for full example projects 👈
Add the relevant dependency to your project:
Leiningen: [com.taoensso/sente "x-y-z"] ; or
deps.edn: com.taoensso/sente {:mvn/version "x-y-z"}
First make sure that you're using one of the supported web servers (PRs for additional server adapters welcome!).
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 ; Usually a .clj file
(:require
;; <other stuff>
[taoensso.sente :as sente] ; <--- Add this
[ring.middleware.anti-forgery :refer [wrap-anti-forgery]] ; <--- Recommended
;; Uncomment a web-server adapter --->
;; [taoensso.sente.server-adapters.http-kit :refer [get-sch-adapter]]
;; [taoensso.sente.server-adapters.immutant :refer [get-sch-adapter]]
;; [taoensso.sente.server-adapters.nginx-clojure :refer [get-sch-adapter]]
;; [taoensso.sente.server-adapters.aleph :refer [get-sch-adapter]]
))
;;; Add this: --->
(let [{:keys [ch-recv send-fn connected-uids
ajax-post-fn ajax-get-or-ws-handshake-fn]}
(sente/make-channel-socket-server! (get-sch-adapter) {})]
(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
(def connected-uids connected-uids) ; Watchable, read-only atom
)
(defroutes my-app-routes
;; <other stuff>
;;; Add these 2 entries: --->
(GET "/chsk" req (ring-ajax-get-or-ws-handshake req))
(POST "/chsk" req (ring-ajax-post req))
)
(def my-app
(-> my-app-routes
;; Add necessary Ring middleware:
ring.middleware.keyword-params/wrap-keyword-params
ring.middleware.params/wrap-params
ring.middleware.anti-forgery/wrap-anti-forgery
ring.middleware.session/wrap-session))
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.
Add a CSRF token somewhere in your HTML:
(let [csrf-token (force ring.middleware.anti-forgery/*anti-forgery-token*)]
[:div#sente-csrf-token {:data-csrf-token csrf-token}])
You'll setup something similar on the client side:
(ns my-client-side-ns ; Usually a .cljs file
(:require-macros
[cljs.core.async.macros :as asyncm :refer (go go-loop)])
(:require
;; <other stuff>
[cljs.core.async :as async :refer (<! >! put! chan)]
[taoensso.sente :as sente :refer (cb-success?)] ; <--- Add this
))
;;; Add this: --->
(def ?csrf-token
(when-let [el (.getElementById js/document "sente-csrf-token")]
(.getAttribute el "data-csrf-token")))
(let [{:keys [chsk ch-recv send-fn state]}
(sente/make-channel-socket-client!
"/chsk" ; Note the same path as before
?csrf-token
{: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
(def chsk-state state) ; Watchable, read-only atom
)
After setup, the client will automatically initiate a WebSocket or repeating long-polling connection to your server. Client<->server events are now ready to transmit over the ch-chsk
channel.
Last step: you'll want to hook your own event handlers up to this channel. Please see one of the example projects and/or API docs for details.
-
ch-recv
is a core.async channel that'll receiveevent-msg
s -
chsk-send!
is a(fn [event & [?timeout-ms ?cb-fn]])
for standard client>server req>resp calls
Let's compare some Ajax and Sente code for sending an event from the client to the server:
(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 [reply] ; Reply is arbitrary Clojure data
(if (sente/cb-success? reply) ; Checks for :chsk/closed, :chsk/timeout, :chsk/error
(do-something! reply)
(error-handler!))))
Note:
- The Ajax request is slow to initialize, and bulky (HTTP overhead)
- The Sente request is pre-initialized (usu. WebSocket), and lean (edn/Transit protocol)
-
ch-recv
is a core.async channel that'll receiveevent-msg
s -
chsk-send!
is a(fn [user-id event])
for async server>user PUSH calls
For asynchronously pushing an event from the server to the client:
- Ajax would require a clumsy long-polling setup, and wouldn't easily support users connected with multiple clients simultaneously
- Sente:
(chsk-send! "destination-user-id" [:some/alert-id <arb-clj-data-payload>])
Important: note that Sente intentionally offers server to user push rather than server>client push. A single user may have >=0 associated clients.
Term | Form |
---|---|
event |
[<ev-id> <?ev-data>] , e.g. [:my-app/some-req {:data "data"}]
|
server event-msg | {:keys [event id ?data send-fn ?reply-fn uid ring-req client-id]} |
client event-msg | {:keys [event id ?data send-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 |
- Clients use
chsk-send!
to sendevent
s to the server and optionally request a reply with timeout - Server uses
chsk-send!
to sendevent
s to all the clients (browser tabs, devices, etc.) of a particular connected user by his/her user-id. - The server can also use an
event-msg
's?reply-fn
to reply to a particular clientevent
using an arbitrary edn value
I recommend not using Sente to transfer large payloads (> 1MB).
The reason is that Sente will by default operate over a WebSocket when possible. This is great for realtime bidirectional communications, but it does mean that there's a bottleneck on that socket.
If a WebSocket connection is saturated dealing with a large transfer, other communications (e.g. notifications) won't be able to get through until the large transfer completes.
In the worst case (with very large payloads and/or very slow connections), this could even cause the client to disconnect due to an apparently unresponsive server.
Instead, I recommend using Sente only for small payloads (<= 1MB) and for signalling. For large payloads do the following:
- client->server: the client can just request the large payload over Ajax
- server->client: the server can signal the client to request the large payload over Ajax
(Sente includes a util to make Ajax requests very easy).
This way you're using the ideal tools for each job:
- Sente's realtime socket is reserved for realtime purposes
- Dedicated Ajax requests are used for large transfers, and have access to normal browser HTTP caching, etc.
Each time the client's channel socket state changes, a client-side :chsk/state
event will fire that you can watch for and handle like any other event.
The event form is [:chsk/state [<old-state-map> <new-state-map>]]
with the following possible state map keys:
Key | Value |
---|---|
:type | e/o #{:auto :ws :ajax}
|
:open? | Truthy iff chsk appears to be open (connected) now |
:ever-opened? | Truthy iff chsk handshake has ever completed successfully |
:first-open? | Truthy iff chsk just completed first successful handshake |
:uid | User id provided by server on handshake, or nil |
:csrf-token | CSRF token provided by server on handshake, or nil |
:handshake-data | Arb user data provided by server on handshake |
:last-ws-error | ?{:udt _ :ev <WebSocket-on-error-event>} |
:last-ws-close | ?{:udt _ :ev <WebSocket-on-close-event> :clean? _ :code _ :reason _} |
:last-close |
?{:udt _ :reason _} , with reason e/o #{nil :requested-disconnect :requested-reconnect :downgrading-ws-to-ajax :unexpected}
|
I recommend not using Sente to transfer large payloads (> 1MB).
The reason is that Sente will by default operate over a WebSocket when possible. This is great for realtime bidirectional communications, but:
- WebSockets aren't ideal for large data transfers, and
- You'll have a bottleneck on that socket
If a WebSocket connection is saturated dealing with a large transfer, other communications (e.g. notifications) won't be able to get through until the large transfer completes.
In the worst case (with very large payloads and/or very slow connections), this could even cause the client to disconnect due to an apparently unresponsive server.
Instead, I recommend using Sente only for small payloads (<= 1MB) and for signalling. For large payloads do the following:
- client->server: the client can just request the large payload over Ajax
- server->client: the server can signal the client to request the large payload over Ajax
(Sente includes a util to make Ajax requests very easy).
This way you're using the ideal tool for each job:
- Sente's realtime socket is reserved for realtime purposes
- Dedicated Ajax requests are used for large transfers, and have access to normal browser HTTP caching, etc.