A smarter client-side with ClojureScript : Shoreleave's rpc/xhr/jsonp facilities



dev dependencies


(this space intentionally left almost blank)

Macros to smooth over the use of RPC

(ns shoreleave.remotes.macros)

The macro calls are preferred to the raw calls. Handling the "remote-name" correctly can be troublesome, and the macro ensures uniform handling.

(defmacro rpc
  [[sym & params] & [destruct & body]]
  (let [func (if destruct
               (if (some #{:on-success :on-error} body)
                 (reduce (fn [callback-map [k v-form]] (assoc callback-map k `(fn ~destruct ~v-form)))
                         {} (apply hash-map body))
                 `(fn ~destruct ~@body))
    `(shoreleave.remotes.http-rpc/remote-callback ~(str sym)
                                                  ~(vec params)
(defmacro letrpc
  [bindings & body]
  (let [bindings (partition 2 bindings)]
      (fn [prev [destruct func]]
        `(rpc ~func [~destruct] ~prev))
      `(do ~@body)
      (reverse bindings))))

Shoreleave's remote calling library

(ns shoreleave.remote
  (:require [shoreleave.remotes.request :as request]
            [shoreleave.remotes.jsonp :as jsonp]
            [shoreleave.remotes.http-rpc :as rpc]))


A major part of complex client-side applications is remote calling.

Shoreleave provides a consistent set of arguments across different types of calls: XmlHTTPRequests (xhr), pooled xhr (request), JSONP, and RPC. Additionally all calls to your own server are CSRF-protected if you're using the anti-forgery ring middleware.

jQuery remote calls are no longer support. You should use jayq directly if you need jQuery ajax calls.


This is an XHR request that uses a pool of XHR handlers You should always prefer to use this method over others

(def request request/request)


JSONP is an excellent way to make cross-origin calls without setting up security certificates. It relies upon you blindly evaluating the results, so you should only use it with sources you trust.

One great application is SOLR. You can setup a SOLR server and pull search results directly into your client with JSONP. You can see an example of the jsonp call in the DuckDuckGo service.

(def jsonp jsonp/jsonp)


The foundation of the RPC system is a remote-callback. This is a great way to expose server-side functionality to the client. A server's remote function is called, and the results are sent back over xhr. All forms of Clojure data are supported. Under the hood, remote-callback uses single xhr objects, not the request pool.

(def remote-callback rpc/remote-callback)

Common remote operations for packaging up calls

(ns shoreleave.remotes.common
  (:require [clojure.string :as cstr]
            [goog.Uri.QueryData :as query-data]
            [goog.structs :as structs]
            [goog.string :as gstr]
            [ :as gevent]
            [shoreleave.browser.cookies :as cookies]
            [shoreleave.remotes.protocols :as r-protocols]))


These are intended for internal use only. You should not use these directly.

(def event-types
(def ^:dynamic *csrf-token-name* :__anti-forgery-token)

Generate a random string that is suitable for request IDs

(defn rand-id-str

Given the keyword form of a request method (:post), return Closure acceptable form (an upper-cased string)

(defn ->url-method
  (cstr/upper-case (name m)))

Shape the routes accordingly for Closure's XHR calls

(defn parse-route
    (string? route) ["GET" route]
    (vector? route) (let [[m u] route]
                      [(->url-method m) u])
    :else ["GET" route]))

Liberate all client-side developers! Given a simple callback function, automatically pass it the response content from a remote call

(defn ->simple-callback
  (when callback
    (fn [req]
      (let [data (.getResponseText req)]
        (callback data)))))

For all POST requests, if ring-anti-forgery is used, pack the CSRF token into the content being sent to the server. Content is always sent to the server as a map (that later gets converted accordingly)

(defn csrf-protected
  [content-map method]
  (if-let [anti-forgery-token (and (= method "POST")
                                   (*csrf-token-name* cookies/cookies))]
    (merge content-map {*csrf-token-name* anti-forgery-token})
(extend-protocol r-protocols/ITransportData

  (-data-str [t] t)

  (-data-str [t] (str (query-data/createFromMap (structs/Map. (clj->js t)))))

  (-data-str [t] (str (query-data/createFromMap (structs/Map. (clj->js t)))))

  (-data-str [t] (str (query-data/createFromMap (structs/Map. (clj->js t)))))

  (-data-str [t]
  ;  (str (clj->js t))
    (str (query-data/createFromMap (structs/Map. (clj->js t))))))

Generate a query-data-string, given Clojure data (usually a hash-map or string)

(defn ->data-str
  (r-protocols/-data-str d))

Remote procedure calls over HTTP

(ns shoreleave.remotes.http-rpc
  (:require [shoreleave.remotes.xhr :as xhr]
            ;[goog.structs.PriorityPool :as priority]
            [cljs.reader :as reader]))


Shoreleave's HTTP-RPC is based on Chris Granger's fetch

Underneath, CSRF protection is automatically happening.

You can also set the resource url to something different (the default is "/_fetch"), but you must use (binding ...) forms on the client-side

You will most likely use the remote macros to make these calls. Here is an example:

 (srm/rpc  (ping)  [pong-response]
     (js/alert pong-response]))


 (srh/remote-callback "ping" [] #(js/alert %))
(def ^:dynamic *remote-uri* "/_shoreleave")

Call a remote-callback on the server. Arguments: remote - a string, the name of the remote on the server (eg. specified with a defremote) params - a vector, the parameters to pass to the remote function callback - a map or a function. The map specifies {:on-success some-f, :on-error another-f} otherwise, just a single function that will be called with on-complete is triggered extra-content - varlist of key-value pairs, extra-content to merge into the payload/content map.

(defn remote-callback
  [remote params callback & extra-content]
  (if (map? callback)
    (let [{:keys [on-success on-error]} callback] ;;TODO make xhr take *ANY* of the event triggers
      (xhr/xhr [:post *remote-uri*]
               :content (merge
                          {:remote remote
                           :params (pr-str params)}
                          (apply hash-map extra-content))
               :on-success (when on-success
                             (fn [data]
                               (let [data (if (= data "") "nil" data)]
                                 (on-success (reader/read-string data)))))
               :on-error (when on-error
                           (fn [data]
                             (let [data (if (= data "") "nil" data)]
                               (on-error (reader/read-string data)))))))
    (xhr/xhr [:post *remote-uri*]
             :content (merge
                        {:remote remote
                         :params (pr-str params)}
                        (apply hash-map extra-content))
             :on-success (when callback
                           (fn [data]
                             (let [data (if (= data "") "nil" data)]
                               (callback (reader/read-string data))))))))

Shoreleave's JSONP

(ns shoreleave.remotes.jsonp
  (:require [ :as jsonp]))

JSON with padding - JSONP

JSONP is a conveinent and widely supported way to make cross origin calls. Shoreleave's support is built Closure's JSONP Object.

The (jsonp ...) function takes one mandatory uri string argument.

Additional options are passed in as key'd args:

  • :on-success (fn [result] (js/console.log "This is a callback function for successful requests"))
  • :on-timeout - just like above
  • :content {:one-arg "Sending this to the server" :another 5}
  • :timeout-ms - the number of milliseconds until the call times out
  • :callback-value - hand-set the callback param value if your server requires something specific
  • :callback-name - the callback's name (there's usually no reason to set this)

    NOTE: - SOLR requires a callback-name of "json.wrf"

(defn jsonp [uri & opts]
  (let [{:keys [on-success on-timeout content callback-name callback-value timeout-ms]} opts
        req ( uri callback-name)
        data (when content (clj->js content))
        on-success (when on-success #(on-success (js->clj % :keywordize-keys true)))
        on-timeout (when on-timeout #(on-timeout (js->clj % :keywordize-keys true)))]
    (when timeout-ms (.setRequestTimeout req timeout-ms))
    (.send req data on-success on-timeout callback-value)))
(ns shoreleave.remotes.protocols)


To allow full and open interop with Google Closure's lower remote calls, (like XhrIo), the Shoreleave function to package up payloads/contents is a protocol

This can be extended or shaped for you application's needs. Out of the box, there is handling for hashmaps and strings.

That support/implementation can be found in remotes/common.cljs

(defprotocol ITransportData
  (-data-str [t]))

Make network requests.

Adapted from ClojureScript:One which is...

Adapted from Bobby Calderwood's Trail framework:

Enhanced to support uniform calling format and CSRF protection

  (:require [cljs.reader :as reader]
            [clojure.browser.event :as event]
            [ :as manager]
            [shoreleave.remotes.common :as common]))
(def ^:private responders (atom {}))
(defn- add-responders [id success error]
  (when (or success error)
    (swap! responders assoc id {:success success :error error})))

  (event-types [this]
    (into {}
           (fn [[k v]]
             [(keyword (. k (toLowerCase)))
(def ^:private
  ( nil

Asynchronously make a network request for the resource at url. If provided via the :on-success and :on-error keyword arguments, the appropriate one of on-success or on-error will be called on completion. They will be passed a map containing the keys :id, :body, :status, and :event. The entry for :event contains an instance of the

URLs/Routes can be expressed as "" or as [:post ""] The default method is GET.

Other allowable keyword arguments are :id, :content, :headers, :priority, and :retries. :id defaults to a random string, :retries defaults to 0.

(defn request
  [route & {:keys [id content headers priority retries on-success
             :or   {id (common/rand-id-str), retries 0}}]
  (let [[method uri] (common/parse-route route)]
      (add-responders id on-success on-error)
      (.send *xhr-manager*
             (when content (common/->data-str
                             (common/csrf-protected content method)))
             (clj->js headers)
             ;; This next one is a callback, and we could use it to get
             ;; rid of the atom and figure out success/failure ourselves
      (catch js/Error e
(defn- response-success [e]
  (when-let [{success :success} (get @responders (:id e))]
    (success e)
    (swap! responders dissoc (:id e))))
(defn- response-error [e]
  (when-let [{error :error} (get @responders (:id e))]
    (error e)
    (swap! responders dissoc (:id e))))
(defn- response-received
  [f e]
  (f {:id     (.-id e)
      :body   (.getResponse e/xhrIo)
      :status (.getStatus e/xhrIo)
      :event  e}))
(event/listen *xhr-manager* "success" (partial response-received response-success))
(event/listen *xhr-manager* "error"   (partial response-received response-error))

Shoreleave's XmlHttpRequest

(ns shoreleave.remotes.xhr
  (:require [ :as xhr]
            [ :as events]
            [shoreleave.remotes.common :as common]))

XMLHttpRequests - `xhr`

You are encouraged to use the xhr pool via the request call.

If you are in a situation where you need a hand-crafted one-off xhr call, use this function - a wrapper around Closure's XhrIo object

The (xhr ...) function takes a mandatory route argument, in the format [:method URL-str] -> [:get "/fetch-recent-results"] URLs/Routes can also be expressed as \"\" with a default GET method added

By default, if you don't specify an :on-error handler, errors will be logged to the console

Additional options are passed in as key'd args:

  • :on-success (fn [result] (js/console.log "This is a callback function for successful requests"))
  • :on-error (fn [result] (js/console.log "We failed."))
  • :content {:one-arg "Sending this to the server" :another 5}
  • :headers {} - additional header information

    If you need error handling, you must use (request ...)

(defn xhr [route & opts]
  (let [req (
        [method uri] (common/parse-route route)
        {:keys [on-success on-error content headers]} (apply hash-map opts)
        content (common/csrf-protected content method)
        data (common/->data-str content)
        suc-callback (common/->simple-callback on-success)
        err-callback (common/->simple-callback (or on-error #(js/console.log (str "XHR ERROR: " %))))]
    (when suc-callback
      (events/listen req (common/event-types :on-success) #(suc-callback req))
      (events/listen req (common/event-types :on-error) #(err-callback req)))
    (.send req uri method data (when headers (clj->js headers)))))