shoreleave/shoreleave-remote

0.3.0


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

dependencies

org.clojure/clojure
1.4.0
shoreleave/shoreleave-core
0.3.0
shoreleave/shoreleave-browser
0.3.0

dev dependencies

lein-marginalia
0.7.1



(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))
               nil)]
    `(shoreleave.remotes.http-rpc/remote-callback ~(str sym)
                                                  ~(vec params)
                                                  ~func)))
(defmacro letrpc
  [bindings & body]
  (let [bindings (partition 2 bindings)]
    (reduce
      (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]))

Remotes

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.

`request`

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`

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)

`remote-callback`

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]
            [goog.net.EventType :as gevent]
            [shoreleave.browser.cookies :as cookies]
            [shoreleave.remotes.protocols :as r-protocols]))

Attention:

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

(def event-types
  {:on-complete goog.net.EventType.COMPLETE
   :on-success goog.net.EventType.SUCCESS
   :on-error goog.net.EventType.ERROR
   :on-timeout goog.net.EventType.TIMEOUT
   :on-ready goog.net.EventType.READY})
(def ^:dynamic *csrf-token-name* :__anti-forgery-token)

Generate a random string that is suitable for request IDs

(defn rand-id-str
  []
  (gstr/getRandomString))

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

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

Shape the routes accordingly for Closure's XHR calls

(defn parse-route
  [route]
  (cond
    (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
  [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})
    content-map))
(extend-protocol r-protocols/ITransportData

  string
  (-data-str [t] t)

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

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

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

  default
  (-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
  [d]
  (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]))

HTTP-RPC

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]))

vs

 (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 [goog.net.Jsonp :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 (goog.net.Jsonp. 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)

TransportData

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: https://github.com/bobby/trail

Enhanced to support uniform calling format and CSRF protection

(ns 
  shoreleave.remotes.request
  (:require [cljs.reader :as reader]
            [clojure.browser.event :as event]
            [goog.net.XhrManager :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})))
(extend-type goog.net.XhrManager

  event/EventType
  (event-types [this]
    (into {}
          (map
           (fn [[k v]]
             [(keyword (. k (toLowerCase)))
              v])
           (js->clj goog.net.EventType)))))
(def ^:private
  *xhr-manager*
  (goog.net.XhrManager. nil
                        nil
                        nil
                        0
                        5000))

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 goog.net.XhrManager.Event.

URLs/Routes can be expressed as "http://www.google.com" or as [:post "http://www.google.com"] 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
                    on-error]
             :or   {id (common/rand-id-str), retries 0}}]
  (let [[method uri] (common/parse-route route)]
    (try
      (add-responders id on-success on-error)
      (.send *xhr-manager*
             id
             uri
             method
             (when content (common/->data-str
                             (common/csrf-protected content method)))
             (clj->js headers)
             priority
             ;; This next one is a callback, and we could use it to get
             ;; rid of the atom and figure out success/failure ourselves
             nil
             retries)
      (catch js/Error e
        nil))))
(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 [goog.net.XhrIo :as xhr]
            [goog.events :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 \"http://www.google.com\" 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 (goog.net.XhrIo.)
        [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)))))