shoreleave/shoreleave-remote0.3.0A smarter client-side with ClojureScript : Shoreleave's rpc/xhr/jsonp facilities dependencies
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)) 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])) | ||||||||||||
RemotesA 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 | |||||||||||||
`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 | (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 ( | (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-RPCShoreleave'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 You will most likely use the
vs
| |||||||||||||
(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 - JSONPJSONP is a conveinent and widely supported way to make cross origin calls. Shoreleave's support is built Closure's JSONP Object. The Additional options are passed in as key'd args:
| |||||||||||||
(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) | |||||||||||||
TransportDataTo 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 | (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 URLs/Routes can be expressed as Other allowable keyword arguments are | (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 By default, if you don't specify an Additional options are passed in as key'd args:
| |||||||||||||
(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))))) | |||||||||||||