Skip to content

1 Getting started

Peter Taoussanis edited this page Nov 3, 2023 · 3 revisions

See also here for full example projects 👈

Setup

Dependency

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"}

Server-side setup

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 and ring-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}])

Client-side setup

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
  )

Usage

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.

Client-side API

  • ch-recv is a core.async channel that'll receive event-msgs
  • 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)

Server-side API

  • ch-recv is a core.async channel that'll receive event-msgs
  • 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.

Types and terminology

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

Summary

  • Clients use chsk-send! to send events to the server and optionally request a reply with timeout
  • Server uses chsk-send! to send events 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 client event using an arbitrary edn value

Limitations

Large transfers

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.

Channel socket state

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}

Limitations

Large transfers

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:

  1. WebSockets aren't ideal for large data transfers, and
  2. 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.