Skip to content

Commit

Permalink
NB Merged refactor [#12], [#59], [#60], [#66], [#67] (@ckarlsen), [#68]…
Browse files Browse the repository at this point in the history
…, [#69] (@hugoduncan), more

Completely refactored Sente's client<->server data handling for:
  * Greater consistency, simplicity, robustness.
  * Increased efficiency (smaller cb ids, leaner cb wrapping format).
  * Pluggable serialization.

The pluggable serialization brings optional support for Transit, incl.
JSON and MessagePack over Transit (thanks to @ckarlsen for getting the ball
rolling on this!).

An early, experimental FlexiPacker is provided that allows per-payload
format selection and simple heuristic-based auto format selection for highly
efficient client<->server data comms in a wide range of use cases. This will
likely become Sente's standard (if not default) package format in future.

Other changes in this squashed commit include:
  * General code review + clean-up.
  * Improved logging.
  * A new client-side `chsk-destroy!` API fn.
  * Improved client+server router support for component-style configs
    (thanks to @hugoduncan for this!): server+client side routers now
    both receive `event-msg`s, and those `event-msg`s contain more
    useful goodies.
  * Client-side router now traps+logs errors like the server-side router.
  * Improve reference example's compatibility with LightTable.
  • Loading branch information
ptaoussanis committed Sep 1, 2014
1 parent 69b6d7b commit 47c8f5c
Show file tree
Hide file tree
Showing 7 changed files with 921 additions and 596 deletions.
6 changes: 5 additions & 1 deletion example-project/project.clj
Expand Up @@ -27,7 +27,11 @@
[ring/ring-defaults "0.1.1"] ; Incl. `ring-anti-forgery`, etc.
[hiccup "1.0.5"] ; Optional, just for HTML
[org.clojure/core.match "0.2.2"] ; Optional but quite handly
]
;;
;;; Transit deps optional; may be used to aid perf. of larger data payloads
;;; (see reference example for details):
[com.cognitect/transit-clj "0.8.247"]
[com.cognitect/transit-cljs "0.8.184"]]

:plugins
[[lein-pprint "1.1.1"]
Expand Down
149 changes: 90 additions & 59 deletions example-project/src/example/my_app.cljx
Expand Up @@ -12,7 +12,7 @@
INSTRUCTIONS:
1. Call `lein start-dev` at your terminal.
2. Connect to development nREPL (port will be printed).
3. Evaluate this namespace and `(start-http-server!)` in this namespace.
3. Evaluate this namespace and `(start!)` in this namespace.
4. Open browser & point to local http server (port will be printed).
5. Observe browser's console + nREPL's std-out.
Expand All @@ -32,7 +32,10 @@
[clojure.core.async :as async :refer (<! <!! >! >!! put! chan go go-loop)]
[taoensso.timbre :as timbre]
[taoensso.sente :as sente]
[ring.middleware.anti-forgery :as ring-anti-forgery])
[ring.middleware.anti-forgery :as ring-anti-forgery]

;; Optional, for Transit encoding:
[taoensso.sente.packers.transit :as sente-transit])

#+cljs
(:require-macros
Expand All @@ -44,17 +47,26 @@
[cljs.core.match]
[cljs.core.async :as async :refer (<! >! put! chan)]
[taoensso.encore :as encore :refer (logf)]
[taoensso.sente :as sente :refer (cb-success?)]))
[taoensso.sente :as sente :refer (cb-success?)]

;; Optional, for Transit encoding:
[taoensso.sente.packers.transit :as sente-transit]))

;; #+clj (timbre/set-level! :trace)
;; (sente/set-logging-level! :trace)
#+clj (defn- logf [fmt & xs] (println (apply format fmt xs)))

;;;; Setup server-side chsk handlers -------------------------------------------
(def packer
"Defines our packing (serialization) format for client<->server comms."
;; :edn ; Default
(sente-transit/get-flexi-packer :edn) ; Experimental, needs Transit deps
)

;;;; Server-side setup

#+clj
(let [{:keys [ch-recv send-fn ajax-post-fn ajax-get-or-ws-handshake-fn
connected-uids]}
(sente/make-channel-socket! {})]
(sente/make-channel-socket! {:packer packer})]
(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
Expand Down Expand Up @@ -112,80 +124,65 @@
{:read-token (fn [req] (-> req :params :csrf-token))})]
(ring.middleware.defaults/wrap-defaults my-routes ring-defaults-config)))

#+clj (defonce http-server_ (atom nil))

#+clj
(defn stop-http-server! []
(when-let [current-server @http-server_]
(current-server :timeout 100)))

#+clj
(defn start-http-server! []
(let [s (http-kit-server/run-server (var my-ring-handler) {:port 0})
uri (format "http://localhost:%s/" (:local-port (meta s)))]
(stop-http-server!)
(logf "Http-kit server is running at `%s`" uri)
(.browse (java.awt.Desktop/getDesktop)
(java.net.URI. uri))
(reset! http-server_ s)))

(comment (start-http-server!))
;;;; Client-side setup

;;;; Setup client-side chsk handlers -------------------------------------------
(def ^:private random-chsk-type-for-fun (if (>= (rand) 0.5) :ajax :auto))

#+cljs
(let [{:keys [chsk ch-recv send-fn state]}
(sente/make-channel-socket! "/chsk" ; Note the same URL as before
{:type (if (>= (rand) 0.5) :ajax :auto)})]
{:type random-chsk-type-for-fun
:packer packer})]
(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
(def chsk-state state) ; Watchable, read-only atom
)

;;;; Setup routers -------------------------------------------------------------
;;;; Routing handlers

#+cljs (logf "ClojureScript appears to have loaded correctly.")
#+clj
(defn- event-msg-handler
[{:as ev-msg :keys [ring-req event ?reply-fn]} _]
(defn- event-msg-handler "Server-side event-msg handler."
[{:as ev-msg
:keys [event ring-req ?reply-fn push-fn ; ... Useful stuff in here
]}]
(let [session (:session ring-req)
uid (:uid session)
[id data :as ev] event]

(logf "Event: %s" ev)
(match [id data]
;; TODO: Match your events here, reply when appropriate <...>
:else
(do (logf "Unmatched event: %s" ev)
(when-not (:dummy-reply-fn? (meta ?reply-fn))
(?reply-fn {:umatched-event-as-echoed-from-from-server ev}))))))

#+clj
(defonce chsk-router
(sente/start-chsk-router-loop! event-msg-handler ch-chsk))
;; TODO: Match your events here, reply when appropriate <...>
:else
(do (logf "Unmatched event: %s" ev)
(when-not (:dummy-reply-fn (meta ?reply-fn))
(?reply-fn {:umatched-event-as-echoed-from-from-server ev}))))))

#+cljs
(defn- event-handler [[id data :as ev] _]
(logf "Event: %s" ev)
(match [id data]
;; TODO Match your events here <...>
[:chsk/state {:first-open? true}]
(logf "Channel socket successfully established!")
[:chsk/state new-state] (logf "Chsk state change: %s" new-state)
[:chsk/recv payload] (logf "Push event from server: %s" payload)
:else (logf "Unmatched event: %s" ev)))
(defn- event-msg-handler "Client-side event-msg handler."
[{:as ev-msg
:keys [event ch-recv send-fn ; ... Useful stuff in here
]}]
(let [[id data :as ev] event]

#+cljs
(defonce chsk-router
(sente/start-chsk-router-loop! event-handler ch-chsk))
(logf "Event: %s" ev)
(match [id data]
;; TODO Match your events here <...>
[:chsk/state {:first-open? true}]
(logf "Channel socket successfully established!")

[:chsk/state new-state] (logf "Chsk state change: %s" new-state)
[:chsk/recv payload] (logf "Push event from server: %s" payload)

:else (logf "Unmatched event: %s" ev))))

;;;; Example: broadcast server>user

;; As an example of push notifications, we'll setup a server loop to broadcast
;; an event to _all_ possible user-ids every 10 seconds:
#+clj
(defonce broadcaster
(defn start-broadcaster! []
(go-loop [i 0]
(<! (async/timeout 10000))
(println (format "Broadcasting server>user: %s" @connected-uids))
Expand All @@ -206,7 +203,7 @@

(comment (test-fast-server>user-pushes))

;;;; Setup client buttons
;;;; Client-side UI

#+cljs
(when-let [target-el (.getElementById js/document "btn1")]
Expand Down Expand Up @@ -238,11 +235,45 @@
;;; our channel socket to reconnect, thereby picking up the new
;;; session.

(encore/ajax-lite "/login" {:method :post
:params
{:user-id (str user-id)
:csrf-token (:csrf-token @chsk-state)}}
(fn [ajax-resp]
(logf "Ajax login response: %s" ajax-resp)))
(encore/ajax-lite "/login"
{:method :post
:params {:user-id (str user-id)
:csrf-token (:csrf-token @chsk-state)}}
(fn [ajax-resp] (logf "Ajax login response: %s" ajax-resp)))

(sente/chsk-reconnect! chsk)))))))

;;;; Init

#+clj (defonce http-server_ (atom nil))
#+clj
(defn stop-http-server! []
(when-let [stop-f @http-server_]
(stop-f :timeout 100)))

#+clj
(defn start-http-server! []
(stop-http-server!)
(let [s (http-kit-server/run-server (var my-ring-handler) {:port 0})
uri (format "http://localhost:%s/" (:local-port (meta s)))]
(reset! http-server_ s)
(logf "Http-kit server is running at `%s`" uri)
(.browse (java.awt.Desktop/getDesktop)
(java.net.URI. uri))))

#+clj (defonce router_ (atom nil))
#+cljs (def router_ (atom nil))
(defn stop-router! [] (when-let [stop-f @router_] (stop-f)))
(defn start-router! []
(stop-router!)
(reset! router_ (sente/start-chsk-router! ch-chsk event-msg-handler)))

(defn start! []
(start-router!)
#+clj (start-http-server!)
#+clj (start-broadcaster!))

#+cljs (start!)
;; #+clj (start!) ; Auto-start disabled for LightTable, etc.
(comment (start!)
(test-fast-server>user-pushes))
10 changes: 5 additions & 5 deletions project.clj
Expand Up @@ -15,10 +15,8 @@
[org.clojure/clojurescript "0.0-2322"]
[org.clojure/core.async "0.1.338.0-5c5012-alpha"]
[org.clojure/tools.reader "0.8.7"]
[com.taoensso/encore "1.7.1"]
[com.taoensso/encore "1.7.3"]
[com.taoensso/timbre "3.2.1"]
[com.cognitect/transit-clj "0.8.247"]
[com.cognitect/transit-cljs "0.8.184"]
[http-kit "2.1.19"]]

:plugins
Expand All @@ -30,8 +28,10 @@
:server-jvm {:jvm-opts ^:replace ["-server"]}
:1.6 {:dependencies [[org.clojure/clojure "1.6.0"]]}
:1.7 {:dependencies [[org.clojure/clojure "1.7.0-alpha1"]]}
:test {:dependencies [[expectations "2.0.9"]
[org.clojure/test.check "0.5.9"]
:test {:dependencies [[com.cognitect/transit-clj "0.8.247"]
[com.cognitect/transit-cljs "0.8.184"]
[expectations "2.0.9"]
[org.clojure/test.check "0.5.9"]
;; [com.cemerick/double-check "0.5.7"]
]
:plugins [[lein-expectations "0.0.8"]
Expand Down

0 comments on commit 47c8f5c

Please sign in to comment.