From a4b2e14897389a571101f346035fc2885a04ea76 Mon Sep 17 00:00:00 2001 From: gered Date: Sun, 8 May 2016 14:34:22 -0400 Subject: [PATCH] add clojurescript client-side browserchannel api --- chat-demo/src/chat_demo/client.cljs | 77 ++++------ clj-browserchannel-server/project.clj | 6 +- .../net/thegeez/browserchannel/client.cljs | 140 ++++++++++++++++++ 3 files changed, 174 insertions(+), 49 deletions(-) create mode 100644 clj-browserchannel-server/src/net/thegeez/browserchannel/client.cljs diff --git a/chat-demo/src/chat_demo/client.cljs b/chat-demo/src/chat_demo/client.cljs index 0a8f5d0..f9eb98d 100644 --- a/chat-demo/src/chat_demo/client.cljs +++ b/chat-demo/src/chat_demo/client.cljs @@ -1,70 +1,53 @@ (ns chat-demo.client (:require + [net.thegeez.browserchannel.client :as browserchannel] [dommy.core :as dom :refer-macros [by-id]] - goog.net.BrowserChannel goog.events.KeyHandler [goog.events.KeyCodes :as key-codes] [goog.events :as events])) -(defonce channel (goog.net.BrowserChannel.)) +(enable-console-print!) (defn say [text] - (.sendMap channel (clj->js {:msg text}))) + (browserchannel/send-map {:msg text})) (defn toggle-element [elem] (if (dom/attr elem :disabled) (dom/remove-attr! elem :disabled) (dom/set-attr! elem :disabled))) -(defn enable-chat [] - (let [msg-input (by-id "msg-input") - send-button (by-id "send-button") - send-message (fn [e] - (say (dom/value msg-input)) - (dom/set-value! msg-input ""))] - (toggle-element msg-input) - (toggle-element send-button) - (events/listen - (goog.events.KeyHandler. msg-input) - "key" - (fn [e] - (when (= (.-keyCode e) key-codes/ENTER) - (send-message e)))) - (events/listen - send-button - "click" - send-message))) +(def handler + {:on-open + (fn [] + (let [msg-input (by-id "msg-input") + send-button (by-id "send-button") + send-message (fn [e] + (say (dom/value msg-input)) + (dom/set-value! msg-input ""))] + (toggle-element msg-input) + (toggle-element send-button) + (events/listen + (goog.events.KeyHandler. msg-input) + "key" + (fn [e] + (when (= (.-keyCode e) key-codes/ENTER) + (send-message e)))) + (events/listen + send-button + "click" + send-message))) -(defn handler [] - (let [h (goog.net.BrowserChannel.Handler.)] - (set! (.-channelOpened h) - (fn [channel] - (enable-chat))) - (set! (.-channelHandleArray h) - (fn [channel data] - (let [data (js->clj data) - msg (get data "msg")] - (dom/append! (by-id "room") - (-> (dom/create-element "div") - (dom/set-text! (str "MSG::" msg))))))) - h)) + :on-receive + (fn [data] + (let [msg (get data "msg")] + (dom/append! (by-id "room") + (-> (dom/create-element "div") + (dom/set-text! (str "MSG::" msg))))))}) (defn ^:export run [] (events/listen js/window "unload" (fn [] - (.disconnect channel) (events/removeAll))) - ; disable logging - (doto (.. channel getChannelDebug getLogger) - (.setLevel goog.debug.Logger.Level.OFF)) - - ; or if you would like to see a ton of browserchannel logging output, uncomment this - #_(doto (.. channel getChannelDebug getLogger) - (.setLevel goog.debug.Logger.Level.FINER) - (.addHandler #(js/console.log %))) - - (doto channel - (.setHandler (handler)) - (.connect "/channel/test" "/channel/bind"))) \ No newline at end of file + (browserchannel/init! handler)) \ No newline at end of file diff --git a/clj-browserchannel-server/project.clj b/clj-browserchannel-server/project.clj index 98f8aa0..7d6e212 100644 --- a/clj-browserchannel-server/project.clj +++ b/clj-browserchannel-server/project.clj @@ -1,7 +1,9 @@ (defproject gered/clj-browserchannel-server "0.2.2" :description "BrowserChannel server implementation in Clojure" :dependencies [[ring/ring-core "1.4.0"] - [org.clojure/data.json "0.2.6"]] + [org.clojure/data.json "0.2.6"] + [prismatic/dommy "1.1.0"]] :profiles {:provided {:dependencies - [[org.clojure/clojure "1.8.0"]]}}) + [[org.clojure/clojure "1.8.0"] + [org.clojure/clojurescript "1.8.51"]]}}) diff --git a/clj-browserchannel-server/src/net/thegeez/browserchannel/client.cljs b/clj-browserchannel-server/src/net/thegeez/browserchannel/client.cljs new file mode 100644 index 0000000..1822ac1 --- /dev/null +++ b/clj-browserchannel-server/src/net/thegeez/browserchannel/client.cljs @@ -0,0 +1,140 @@ +(ns net.thegeez.browserchannel.client + (:require + [dommy.core :refer-macros [sel1]] + goog.net.BrowserChannel + goog.net.BrowserChannel.Handler + [goog.net.BrowserChannel.State :as bc-state] + [goog.events :as events] + [goog.debug.Logger.Level :as log-level])) + +(defonce channel (goog.net.BrowserChannel.)) + +(def default-options + {:base "/channel" + :allow-chunked-mode? true + :allow-host-prefix? true + :fail-fast? false + :max-back-channel-retries 3 + :max-forward-channel-retries 2 + :forward-channel-request-timeout (* 20 1000) + :verbose-logging? false}) + +; see: https://google.github.io/closure-library/api/source/closure/goog/net/browserchannel.js.src.html#l521 +(def ^:private bch-error-enum-to-keyword + {0 :ok + 2 :request-failed + 4 :logged-out + 5 :no-data + 6 :unknown-session-id + 7 :stop + 8 :network + 9 :blocked + 10 :bad-data + 11 :bad-response + 12 :active-x-blocked}) + +(defn- queued-map->clj + [queued-map] + {:context (aget queued-map "context") + :map (js->clj (aget queued-map "map")) + :map-id (aget queued-map "mapId")}) + +(defn- array-of-queued-map->clj + [queued-map-array] + (mapv queued-map->clj queued-map-array)) + +(defn channel-state [] + (.getState channel)) + +(defn connected? + [] + (= (channel-state) bc-state/OPENED)) + +(defn set-debug-log! + [level & [f]] + (doto (.. channel getChannelDebug getLogger) + (.setLevel level) + (.addHandler (or f #(js/console.log %))))) + +(defn send-map + [m & [{:keys [on-success]}]] + (.sendMap channel (clj->js m) {:on-success on-success})) + +(defn connect! + [& [{:keys [base] :as options}]] + (let [state (channel-state)] + (if (or (= state bc-state/CLOSED) + (= state bc-state/INIT)) + (.connect channel + (str base "/test") + (str base "/bind"))))) + +(defn disconnect! + [] + (if-not (= (channel-state) bc-state/CLOSED) + (.disconnect channel))) + +(defn- get-anti-forgery-token + [] + (if-let [tag (sel1 "meta[name='anti-forgery-token']")] + (.-content tag))) + +(defn ->handler + [{:keys [on-open on-close on-receive on-sent on-error]}] + (let [handler (goog.net.BrowserChannel.Handler.)] + (set! (.-channelOpened handler) + (fn [ch] + (if on-open + (on-open)))) + (set! (.-channelClosed handler) + (fn [ch pending undelivered] + (if on-close + (on-close (array-of-queued-map->clj pending) + (array-of-queued-map->clj undelivered))))) + (set! (.-channelHandleArray handler) + (fn [ch m] + (if on-receive + (on-receive (js->clj m))))) + (set! (.-channelSuccess handler) + (fn [ch delivered] + (if on-sent + (on-sent (array-of-queued-map->clj delivered))) + (doseq [m delivered] + (let [{:keys [on-success] :as context} (aget m "context")] + (if on-success + (on-success)))))) + (set! (.-channelError handler) + (fn [ch error-code] + (if on-error + (on-error (get bch-error-enum-to-keyword error-code :unknown))))) + handler)) + +(defn- apply-options! + [options] + (set-debug-log! (if (:verbose-logging? options) log-level/FINER log-level/OFF)) + (.setAllowChunkedMode channel (boolean (:allow-chunked-mode? options))) + (.setAllowHostPrefix channel (boolean (:allow-host-prefix? options))) + (.setFailFast channel (boolean (:fail-fast? options))) + (.setForwardChannelMaxRetries channel (:max-forward-channel-retries options)) + (.setForwardChannelRequestTimeout channel (:forward-channel-request-timeout options)) + ;; HACK: this is relying on changing a value for a setting that google's + ;; documentation lists as private. however, it is a fairly important + ;; setting to be able to change, so i think it's worth the risk... + (set! goog.net.BrowserChannel/BACK_CHANNEL_MAX_RETRIES (:max-back-channel-retries options))) + +(defn init! + [handler & [options]] + (let [options (merge default-options options)] + (events/listen + js/window "unload" + (fn [] + (disconnect!))) + + (.setHandler channel (->handler handler)) + (apply-options! options) + (let [csrf-token (get-anti-forgery-token) + headers (merge + (:headers options) + (if csrf-token {"X-CSRF-Token" csrf-token}))] + (.setExtraHeaders channel (clj->js headers))) + (connect! options))) \ No newline at end of file