From 73f9f28d3a349b51f5526a60b6b6f1dbb4ca2d8c Mon Sep 17 00:00:00 2001 From: Chris Truter Date: Mon, 16 Feb 2015 23:22:15 +0200 Subject: [PATCH 1/5] Fighting chaos with CLJX Middleware not being inserted correctly, and having to repeat plugins as dependencies for some reason --- project.clj | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/project.clj b/project.clj index 89e00b8..43fb75a 100644 --- a/project.clj +++ b/project.clj @@ -10,16 +10,25 @@ [org.clojure/core.async "0.1.346.0-17112a-alpha"] [com.firebase/firebase-client-jvm "2.2.0"] - [cljsjs/firebase "2.1.2-1"]] + [cljsjs/firebase "2.1.2-1"] + + [com.keminglabs/cljx "0.5.0" :exclusions [com.cemerick/piggieback]] + [com.cemerick/piggieback "0.1.5"]] :deploy-repositories [["releases" :clojars]] - :plugins [[lein-cljsbuild "1.0.3" :scope "test"] - [com.cemerick/clojurescript.test "0.3.1" :scope "test"]] + :repl-options {:nrepl-middleware + [cljx.repl-middleware/wrap-cljx + cemerick.piggieback/wrap-cljs-repl]} - :repl-options {:nrepl-middleware [cljx.repl-middleware/wrap-cljx]} + :plugins [[lein-cljsbuild "1.0.3" :scope "test"] + [com.cemerick/clojurescript.test "0.3.1" :scope "test"] + [com.keminglabs/cljx "0.5.0" :exclusions [com.cemerick/piggieback]] + [com.cemerick/piggieback "0.1.5"]] :jar-exclusions [#"\.cljx|\.swp|\.swo|\.DS_Store"] + :auto-clean false + :profiles {:dev {:dependencies [[om "0.7.3"]] :plugins [[com.keminglabs/cljx "0.5.0"]]}} From e361703512e4aff5280874ab47b11748d33673e2 Mon Sep 17 00:00:00 2001 From: Chris Truter Date: Mon, 16 Feb 2015 23:45:03 +0200 Subject: [PATCH 2/5] Super crude CLJX wrapping Probably have `core` -> `impl` -> `common`, with a macro to do the bulk imports back into `core`. Yeugh! --- project.clj | 4 +- src/sunog/{async.cljs => async.cljx} | 4 +- src/sunog/core.cljs | 241 --------------------- src/sunog/core.cljx | 175 +++++++++++++++ src/sunog/{clojure/core.clj => impl.clj} | 92 +++----- src/sunog/impl.cljs | 106 +++++++++ src/sunog/{registry.cljs => registry.cljx} | 2 +- src/sunog/utils.cljx | 21 ++ test/sunog/core_test.clj | 3 +- test/sunog/core_test.cljs | 23 +- 10 files changed, 350 insertions(+), 321 deletions(-) rename src/sunog/{async.cljs => async.cljx} (95%) delete mode 100644 src/sunog/core.cljs create mode 100644 src/sunog/core.cljx rename src/sunog/{clojure/core.clj => impl.clj} (78%) create mode 100644 src/sunog/impl.cljs rename src/sunog/{registry.cljs => registry.cljx} (92%) create mode 100644 src/sunog/utils.cljx diff --git a/project.clj b/project.clj index 43fb75a..1425a96 100644 --- a/project.clj +++ b/project.clj @@ -38,13 +38,13 @@ :aot [sunog.clojure.android-stub] :cljsbuild {:builds [{:id "om-value-changes" - :source-paths ["examples/cljs/om-value-changes/src" "src"] + :source-paths ["examples/cljs/om-value-changes/src" "src" "target/classes"] :compiler {:output-to "examples/cljs/om-value-changes/main.js" :output-dir "examples/cljs/om-value-changes/out" :source-map true :optimizations :none }} {:id "test" - :source-paths ["src", "test"] + :source-paths ["src", "test", "target/classes"] :notify-command ["phantomjs" :cljs.test/runner "target/cljs/test.js"] :compiler {:output-to "target/cljs/test.js" :optimizations :whitespace diff --git a/src/sunog/async.cljs b/src/sunog/async.cljx similarity index 95% rename from src/sunog/async.cljs rename to src/sunog/async.cljx index 60a6ea0..93a7744 100644 --- a/src/sunog/async.cljs +++ b/src/sunog/async.cljx @@ -1,7 +1,7 @@ (ns sunog.async (:require [sunog.core :as p] - [cljs.core.async :refer [! chan put! close!]]) - (:require-macros [cljs.core.async.macros :refer [go go-loop]])) + #+clj [clojure.core.async :refer [! chan put! close!]] + #+cljs [cljs.core.async :refer [! chan put! close!]])) (defn with-chan "Call a function with a fresh channel, then return the channel" diff --git a/src/sunog/core.cljs b/src/sunog/core.cljs deleted file mode 100644 index 1e8e1f5..0000000 --- a/src/sunog/core.cljs +++ /dev/null @@ -1,241 +0,0 @@ -(ns sunog.core - (:refer-clojure :exclude [get-in set! reset! conj! swap! dissoc! deref parents key]) - (:require cljsjs.firebase - [clojure.string :as str] - [sunog.registry :refer [register-listener]])) - -;; constants - -(def child-events - [:child-added - :child-changed - :child-moved - :child-removed]) - -(def all-events - (conj child-events :value)) - -(def undefined) ;; firebase methods do not take kindly to nil callbacks - -(defn no-op [& _]) - -(def SERVER_TIMESTAMP js/Firebase.ServerValue.TIMESTAMP) - -;; utils - -;; FIXME: camel-case keys? -;; hydrate to custom vectors to preserve rich keys? -;; preserve sets (don't coerce to vector) -;; similarly preserve keywords as values - -(defn- hydrate [js-val] - (js->clj js-val :keywordize-keys true)) - -(defn- serialize [clj-val] - (clj->js clj-val)) - -(defn key - "Last segment in reference or snapshot path" - [ref] - (.key ref)) - -(defn- value [snapshot] - (hydrate (.val snapshot))) - -(defn- kebab->underscore [keyword] - (-> keyword name (str/replace "-" "_"))) - -(defn- underscore->kebab [string] - (-> string (str/replace "_" "-") keyword)) - -(defn- korks->path [korks] - (if (sequential? korks) - (str/join "/" (map name korks)) - (when korks (name korks)))) - -(defn- wrap-snapshot [snapshot] - ;; TODO: enhance with snapshot protocol - [(key snapshot) (value snapshot)]) - -;; references - -(defn connect - "Create a reference for firebase" - [url] - (js/Firebase. url)) - -(defn get-in - "Obtain child reference from base by following korks" - [ref korks] - (let [path (korks->path korks)] - (if-not (seq path) ref (.child ref path)))) - -(defn parent - "Immediate ancestor of reference, if any" - [ref] - (and ref (.parent ref))) - -(defn parents - "Probably don't need this. Or maybe we want more zipper nav (siblings, in-order, etc)" - [ref] - (take-while identity (iterate parent (parent ref)))) - -;; - -(defonce connected (atom true)) - -(defn disconnect! [] - (.goOffline js/Firebase) - (clojure.core/reset! connected false)) - -(defn reconnect! [] - (.goOnline js/Firebase) - (clojure.core/reset! connected true)) - -(defn check-connected? - "Returns boolean around whether client is set to synchronise with server. - Says nothing about actual connectivity." - [] - @connected) - -;; FIXME: find a better abstraction -;; https://github.com/crisptrutski/sunog/issues/4 - -(defn on-disconnect - "Return an on" - [ref] - (.onDisconnect ref)) - -(defn cancel [ref-disconnect & [cb]] - (.cancel ref-disconnect (or cb undefined))) - -;; -------------------- -;; auth - -(defn build-opts [session-only?] - (if session-only? - #js {:remember "sessionOnly"} - undefined)) - -(defn- wrap-auth-cb [cb] - (if cb - (fn [err info] - (cb err (hydrate info))) - undefined)) - -(defn auth [ref email password & [cb session-only?]] - (.authWithPassword - ref - #js {:email email, :password password} - (wrap-auth-cb cb) - (build-opts session-only?))) - -(defn auth-anon [ref & [cb session-only?]] - (.authAnonymously - ref - (wrap-auth-cb cb) - (build-opts session-only?))) - -(defn auth-info - "Returns a map of uid, provider, token, expires - or nil if there is no session" - [ref] - (hydrate (.getAuth ref))) - -;; onAuth and offAuth are not wrapped yet - -(defn unauth [ref] - (.unauth ref)) - -;; -------------------- -;; getters 'n setters - -(defn deref [ref cb] - (.once ref "value" (comp cb value))) - -(defn reset! [ref val & [cb]] - (.set ref (serialize val) (or cb undefined))) - -(defn reset-with-priority! [ref val priority & [cb]] - (.setWithPriority ref (serialize val) priority (or cb undefined))) - -(defn merge! [ref val & [cb]] - (.update ref (serialize val) (or cb undefined))) - -(defn conj! [ref val & [cb]] - (.push ref (serialize val) (or cb undefined))) - -(defn- extract-cb [args] - (if (and (>= 2 (count args)) - (= (first (take-last 2 args)) :callback)) - [(last args) (drop-last 2 args)] - [nil args])) - -(defn swap! [ref f & args] - (let [[cb args] (extract-cb args) - f' #(-> % hydrate ((fn [x] (apply f x args))) serialize)] - (.transaction ref f' (or cb undefined)))) - -(defn dissoc! [ref & [cb]] - (.remove ref (or cb undefined))) - -(def remove! dissoc!) - -(defn set-priority! [ref priority & [cb]] - (.setPriority ref priority (or cb undefined))) - -;; nested variants - -(defn deref-in [ref korks & [cb]] - (deref (get-in ref korks) cb)) - -(defn reset-in! [ref korks val & [cb]] - (reset! (get-in ref korks) val cb)) - -(defn reset-with-priority-in! [ref korks val priority & [cb]] - (reset-with-priority! (get-in ref korks) val priority cb)) - -(defn merge-in! [ref korks val & [cb]] - (merge! (get-in ref korks) val cb)) - -(defn conj-in! [ref korks val & [cb]] - (conj! (get-in ref korks) val cb)) - -(defn swap-in! [ref korks f & [cb]] - (swap! (get-in ref korks) f cb)) - -(defn dissoc-in! [ref korks & [cb]] - (dissoc! (get-in ref korks) cb)) - -(def remove-in! remove-in!) - -(defn set-priority-in! [ref korks priority & [cb]] - (set-priority! (get-in ref korks) priority cb)) - -;; ------------------ -;; subscriptions - -(defn listen-to - "Subscribe to notifications of given type" - ([ref type cb] - (assert (some #{type} all-events) (str "Unknown type: " type)) - (let [type (kebab->underscore type)] - (let [cb' (comp cb wrap-snapshot) - unsub! #(.off ref type cb')] - (.on ref type cb') - (register-listener ref type unsub!) - unsub!))) - ([ref korks type cb] - (listen-to (get-in ref korks) type cb))) - -(defn listen-children - "Subscribe to all children notifications on a reference, and return an unsubscribe" - ([ref cb] - (let [cbs (->> child-events - (map (fn [type] #(vector type %))) - (map #(comp cb %))) - unsubs (doall (map listen-to (repeat ref) child-events cbs))] - (fn [] - (doseq [unsub! unsubs] - (unsub!))))) - ([ref korks cb] - (listen-children (get-in ref korks) cb))) diff --git a/src/sunog/core.cljx b/src/sunog/core.cljx new file mode 100644 index 0000000..58be26e --- /dev/null +++ b/src/sunog/core.cljx @@ -0,0 +1,175 @@ +(ns sunog.core + (:refer-clojure :exclude [get-in set! reset! conj! swap! dissoc! deref parents key]) + (:require [clojure.string :as str] + [sunog.impl :as impl])) + +;; constants + +(def child-events + [:child-added + :child-changed + :child-moved + :child-removed]) + +(def all-events + (conj child-events :value)) + +(def SERVER_TIMESTAMP impl/SERVER_TIMESTAMP) + +;; FIXME: camel-case keys? +;; hydrate to custom vectors to preserve rich keys? +;; preserve sets (don't coerce to vector) +;; similarly preserve keywords as values + +(def hydrate impl/hydrate) + +(def serialize impl/serialize) + +(def key impl/key) + +(def value impl/value) + +(defn- wrap-snapshot [snapshot] + ;; TODO: enhance with snapshot protocol + [(key snapshot) (value snapshot)]) + +;; references + +(def connect impl/connect) + +(def get-in impl/get-in) + +(def parent impl/parent) + +(defn parents + "Probably don't need this. Or maybe we want more zipper nav (siblings, in-order, etc)" + [ref] + (take-while identity (iterate parent (parent ref)))) + +;; + +(defonce connected (atom true)) + +(defn disconnect! [] + #+cljs (.goOffline js/Firebase) + (clojure.core/reset! connected false)) + +(defn reconnect! [] + #+cljs (.goOnline js/Firebase) + (clojure.core/reset! connected true)) + +(defn check-connected? + "Returns boolean around whether client is set to synchronise with server. + Says nothing about actual connectivity." + [] + @connected) + +;; FIXME: find a better abstraction +;; https://github.com/crisptrutski/sunog/issues/4 + +(defn on-disconnect + "Return an on" + [ref] + #+cljs (.onDisconnect ref)) + +(defn cancel [ref-disconnect & [cb]] + #+cljs (.cancel ref-disconnect (or cb impl/undefined))) + +;; -------------------- +;; auth + +(defn build-opts [session-only?] + #+cljs (if session-only? + #js {:remember "sessionOnly"} + impl/undefined)) + +(defn- wrap-auth-cb [cb] + #+cljs (if cb + (fn [err info] + (cb err (hydrate info))) + impl/undefined)) + +(defn auth [ref email password & [cb session-only?]] + #+cljs (.authWithPassword + ref + #js {:email email, :password password} + (wrap-auth-cb cb) + (build-opts session-only?))) + +(defn auth-anon [ref & [cb session-only?]] + #+cljs (.authAnonymously + ref + (wrap-auth-cb cb) + (build-opts session-only?))) + +(defn auth-info + "Returns a map of uid, provider, token, expires - or nil if there is no session" + [ref] + #+cljs (hydrate (.getAuth ref))) + +;; onAuth and offAuth are not wrapped yet + +(defn unauth [ref] + #+cljs (.unauth ref)) + +;; -------------------- +;; getters 'n setters + +(def deref impl/deref) + +(def reset! impl/reset!) + +(defn reset-with-priority! [ref val priority & [cb]] + #+cljs (.setWithPriority ref (serialize val) priority (or cb impl/undefined))) + +(def merge! impl/merge!) + +(def conj! impl/conj!) + +(def swap! impl/swap!) + +(def dissoc! impl/dissoc!) + +(def remove! dissoc!) + +(defn set-priority! [ref priority & [cb]] + #+cljs (.setPriority ref priority (or cb impl/undefined))) + +;; nested variants + +(defn deref-in [ref korks & [cb]] + (deref (get-in ref korks) cb)) + +(defn reset-in! [ref korks val & [cb]] + (reset! (get-in ref korks) val cb)) + +(defn reset-with-priority-in! [ref korks val priority & [cb]] + (reset-with-priority! (get-in ref korks) val priority cb)) + +(defn merge-in! [ref korks val & [cb]] + (merge! (get-in ref korks) val cb)) + +(defn conj-in! [ref korks val & [cb]] + (conj! (get-in ref korks) val cb)) + +(defn swap-in! [ref korks f & [cb]] + (swap! (get-in ref korks) f cb)) + +(defn dissoc-in! [ref korks & [cb]] + (dissoc! (get-in ref korks) cb)) + +(def remove-in! remove-in!) + +(defn set-priority-in! [ref korks priority & [cb]] + (set-priority! (get-in ref korks) priority cb)) + +;; ------------------ +;; subscriptions + +(defn listen-to + ([ref type cb] (impl/listen-to ref type cb)) + ([ref korks type cb] (listen-to (get-in ref korks) type cb))) + +(defn listen-children + ([ref cb] (impl/listen-children ref cb)) + ([ref korks cb] (listen-children (get-in ref korks) cb))) diff --git a/src/sunog/clojure/core.clj b/src/sunog/impl.clj similarity index 78% rename from src/sunog/clojure/core.clj rename to src/sunog/impl.clj index b8daee9..bf41ba4 100644 --- a/src/sunog/clojure/core.clj +++ b/src/sunog/impl.clj @@ -1,8 +1,5 @@ -(ns sunog.clojure.core +(ns sunog.impl (:refer-clojure :exclude [get-in set! reset! conj! swap! dissoc! deref parents key]) - (:require [clojure.core.async :refer [chan put!]] - [clojure.string :as str] - [clojure.walk :as walk]) (:import [com.firebase.client ServerValue Firebase @@ -12,39 +9,46 @@ ValueEventListener ChildEventListener Transaction - Transaction$Handler])) + Transaction$Handler]) + (:require [clojure.core.async :refer [chan put!]] + [clojure.string :as str] + [clojure.walk :as walk])) -(def child-events - [:child-added - :child-changed - :child-moved - :child-removed]) +;; who doesn't like circular refs? -(def all-events - (conj child-events :value)) +(def all-events sunog.core/all-events) +(def child-events sunog.core/child-events) ;; TODO: review + unsubscribe listeners ;; TODO: server time -;; TODO: parents (inherit) ;; TODO: connect/discconet/on-disconnect ;; TODO: auth -;; TODO: listen-children -(defn serialize [v] - (if (map? v) - (walk/stringify-keys v) - v)) +(defn- wrap-cb [cb] + (reify com.firebase.client.Firebase$CompletionListener + (^void onComplete [this ^FirebaseError err ^Firebase ref] + (if err (throw err) (cb ref))))) +;; + +(def SERVER_TIMESTAMP ServerValue/TIMESTAMP) (defn hydrate [v] (if (map? v) (walk/keywordize-keys v) v)) -(defn- wrap-cb [cb] - (reify com.firebase.client.Firebase$CompletionListener - (^void onComplete [this ^FirebaseError err ^Firebase ref] - (if err (throw err) (cb ref))))) -;; +(defn serialize [v] + (if (map? v) + (walk/stringify-keys v) + v)) + +(defn key [ref] (.getKey ref)) + +(defn value [snapshot] + (hydrate (.getValue snapshot))) + +(defn wrap-snapshot [^DataSnapshot d] + [(key d) (value d)]) (defn connect [url] (Firebase. url)) @@ -55,18 +59,16 @@ (name korks))] (.child root path))) -(defn key [ref] (.getKey ref)) - (defn parent [ref] (.getParent ref)) -(defn reify-value-listener [cb] +(defn- reify-value-listener [cb] (reify ValueEventListener (^void onDataChange [_ ^DataSnapshot ds] - (cb [(.getKey ds) (hydrate (.getValue ds))])) + (cb (wrap-snapshot ds))) (^void onCancelled [_ ^FirebaseError err] (throw err)))) -(defn value [ref cb] +(defn deref [ref cb] (.addListenerForSingleValue ref (reify-value-listener cb))) (defn reset! [ref val & [cb]] @@ -104,35 +106,11 @@ (.removeValue ref) (.removeValue ref (wrap-cb cb)))) -(def remove! dissoc) - (defn set-priority [ref priority & [cb]] (if-not cb (.setPriority ref priority) (.setPriority ref priority (wrap-cb cb)))) -;; - -(defn reset-in! [ref korks val & [cb]] - (reset! (get-in ref korks) val cb)) - -(defn conj-in! [ref korks val & [cb]] - (conj! (get-in ref korks) val cb)) - -(defn swap-in! [ref korks f & args] - (apply swap! (get-in ref korks) f args)) - -(defn merge-in! [ref korks val & [cb]] - (merge! (get-in ref korks) val cb)) - -(defn dissoc-in! [ref korks & [cb]] - (dissoc! (get-in ref korks) cb)) - -(def remove-in! dissoc-in!) - -(defn- wrap-snapshot [^DataSnapshot d] - [(.getKey d) (hydrate (.getValue d))]) - (defn reify-child-listener [{:keys [added changed moved removed]}] (reify ChildEventListener (^void onChildAdded [_ ^DataSnapshot d ^String previous-child-name] @@ -166,13 +144,3 @@ (map (fn [type] #(vector type %))) (map #(comp cb %))))] (.addChildEventListener ref (reify-child-listener cbs))))) - -(defn listen-to< [ref type] - (let [ch (chan)] - (listen-to ref type #(if % (put! ch %))) - ch)) - -(defn listen-children< [ref] - (let [ch (chan)] - (listen-children ref #(if % (put! ch %))) - ch)) diff --git a/src/sunog/impl.cljs b/src/sunog/impl.cljs new file mode 100644 index 0000000..f704fc7 --- /dev/null +++ b/src/sunog/impl.cljs @@ -0,0 +1,106 @@ +(ns sunog.impl + (:refer-clojure :exclude [get-in set! reset! conj! swap! dissoc! deref parents key]) + (:require cljsjs.firebase + [sunog.utils :as utils] + [sunog.registry :refer [register-listener]])) + +(def undefined) ;; firebase methods do not take kindly to nil callbacks + +(def SERVER_TIMESTAMP js/Firebase.ServerValue.TIMESTAMP) + +;; circular refs not working, we repeat + +(declare key) +(declare value) + +(def child-events + [:child-added + :child-changed + :child-moved + :child-removed]) + +(def all-events + (conj child-events :value)) + +(defn- wrap-snapshot [snapshot] + ;; TODO: enhance with snapshot protocol + [(key snapshot) (value snapshot)]) + +;; + +(defn hydrate [js-val] + (js->clj js-val :keywordize-keys true)) + +(defn serialize [clj-val] + (clj->js clj-val)) + +(defn key + "Last segment in reference or snapshot path" + [ref] + (.key ref)) + +(defn value [snapshot] + (hydrate (.val snapshot))) + +(defn connect + "Create a reference for firebase" + [url] + (js/Firebase. url)) + +(defn get-in + "Obtain child reference from base by following korks" + [ref korks] + (let [path (utils/korks->path korks)] + (if-not (seq path) ref (.child ref path)))) + +(defn parent + "Immediate ancestor of reference, if any" + [ref] + (and ref (.parent ref))) + +;; + +(defn deref [ref cb] + (.once ref "value" (comp cb value))) + +(defn reset! [ref val & [cb]] + (.set ref (serialize val) (or cb undefined))) + +(defn merge! [ref val & [cb]] + (.update ref (serialize val) (or cb undefined))) + +(defn conj! [ref val & [cb]] + (.push ref (serialize val) (or cb undefined))) + +(defn swap! [ref f & args] + (let [[cb args] (utils/extract-cb args) + f' #(-> % hydrate ((fn [x] (apply f x args))) serialize)] + (.transaction ref f' (or cb undefined)))) + +(defn dissoc! [ref & [cb]] + (.remove ref (or cb undefined))) + +;; ------------------ +;; subscriptions + +(defn listen-to + "Subscribe to notifications of given type" + [ref type cb] + (assert (some #{type} all-events) (str "Unknown type: " type)) + (let [type (utils/kebab->underscore type)] + (let [cb' (comp cb wrap-snapshot) + unsub! #(.off ref type cb')] + (.on ref type cb') + (register-listener ref type unsub!) + unsub!))) + +(defn listen-children + "Subscribe to all children notifications on a reference, and return an unsubscribe" + [ref cb] + (let [cbs (->> child-events + (map (fn [type] #(vector type %))) + (map #(comp cb %))) + unsubs (doall (map listen-to (repeat ref) child-events cbs))] + (fn [] + (doseq [unsub! unsubs] + (unsub!))))) diff --git a/src/sunog/registry.cljs b/src/sunog/registry.cljx similarity index 92% rename from src/sunog/registry.cljs rename to src/sunog/registry.cljx index 4c54e28..69b35b3 100644 --- a/src/sunog/registry.cljs +++ b/src/sunog/registry.cljx @@ -20,6 +20,6 @@ 1 (do (call-all! (flatten-vals (get @unsubs ref))) (swap! unsubs dissoc ref)) 2 (do (call-all! (flatten-vals (get-in @unsubs [ref type]))) - (swap! unsubs update ref #(dissoc % type))) + (swap! unsubs update-in [ref] #(dissoc % type))) 3 (do (unsub!) (swap! unsubs update-in [ref type] #(disj % unsub!)))))) diff --git a/src/sunog/utils.cljx b/src/sunog/utils.cljx new file mode 100644 index 0000000..7373a9a --- /dev/null +++ b/src/sunog/utils.cljx @@ -0,0 +1,21 @@ +(ns sunog.utils + (:require [clojure.string :as str])) + +(defn- kebab->underscore [keyword] + (-> keyword name (str/replace "-" "_"))) + +(defn- underscore->kebab [string] + (-> string (str/replace "_" "-") keyword)) + +(defn- korks->path [korks] + (if (sequential? korks) + (str/join "/" (map name korks)) + (when korks (name korks)))) + +(defn no-op [& _]) + +(defn extract-cb [args] + (if (and (>= 2 (count args)) + (= (first (take-last 2 args)) :callback)) + [(last args) (drop-last 2 args)] + [nil args])) diff --git a/test/sunog/core_test.clj b/test/sunog/core_test.clj index e83edfa..6528fe3 100644 --- a/test/sunog/core_test.clj +++ b/test/sunog/core_test.clj @@ -1,8 +1,9 @@ (ns sunog.core-test (:require [clojure.test :refer :all] - [sunog.clojure.core :as p])) + [sunog.core :as p])) (def firebase-url "https://blazing-fire-1915.firebaseio.com") +(def firebase-url "https://luminous-torch-5788.firebaseio.com/") (deftest serialize-test ;; keywords -> strings diff --git a/test/sunog/core_test.cljs b/test/sunog/core_test.cljs index 2a18b33..cfce32d 100644 --- a/test/sunog/core_test.cljs +++ b/test/sunog/core_test.cljs @@ -3,7 +3,8 @@ [cljs.core.async.macros :refer [go go-loop]]) (:require [cemerick.cljs.test :as t] [cljs.core.async :refer [ ref p/on-disconnect p/remove!) ref)) -;; utils - (deftest serialize-hydrate-test (is (= {:a 1, :b ["b" "a"]} (p/hydrate (p/serialize {"a" 1, "b" #{:a :b}}))))) (deftest kebab->underscore-test - (is (= "a_cromulent_name" (p/kebab->underscore :a-cromulent-name)))) + (is (= "a_cromulent_name" (utils/kebab->underscore :a-cromulent-name)))) (deftest underscore->kebab-test - (is (= :a-tasty-skewer (p/underscore->kebab "a_tasty_skewer")))) + (is (= :a-tasty-skewer (utils/underscore->kebab "a_tasty_skewer")))) (deftest korks->path-test - (is (= nil (p/korks->path nil))) - (is (= "" (p/korks->path ""))) - (is (= "" (p/korks->path []))) - (is (= "a" (p/korks->path :a))) - (is (= "a" (p/korks->path ["a"]))) - (is (= "a/b" (p/korks->path "a/b"))) - (is (= "a/b" (p/korks->path [:a :b])))) + (is (= nil (utils/korks->path nil))) + (is (= "" (utils/korks->path ""))) + (is (= "" (utils/korks->path []))) + (is (= "a" (utils/korks->path :a))) + (is (= "a" (utils/korks->path ["a"]))) + (is (= "a/b" (utils/korks->path "a/b"))) + (is (= "a/b" (utils/korks->path [:a :b])))) (deftest key-parent-get-in-test (let [root (p/connect firebase-url) From 6615f91bd4bacec3b16f58b1b02c2bbba16942f9 Mon Sep 17 00:00:00 2001 From: Chris Truter Date: Tue, 17 Feb 2015 00:05:29 +0200 Subject: [PATCH 3/5] Starting down a dark macro path.. --- src/potemkin/namespaces.clj | 102 ++++++++++++++++++++++++++++++++++++ src/sunog/common.cljx | 10 ++++ src/sunog/core.cljx | 92 +++++++++++++------------------- src/sunog/impl.clj | 1 - src/sunog/impl.cljs | 3 ++ 5 files changed, 152 insertions(+), 56 deletions(-) create mode 100644 src/potemkin/namespaces.clj create mode 100644 src/sunog/common.cljx diff --git a/src/potemkin/namespaces.clj b/src/potemkin/namespaces.clj new file mode 100644 index 0000000..053468f --- /dev/null +++ b/src/potemkin/namespaces.clj @@ -0,0 +1,102 @@ + +(ns potemkin.namespaces) + +(defn link-vars + "Makes sure that all changes to `src` are reflected in `dst`." + [src dst] + (add-watch src dst + (fn [_ src old new] + (alter-var-root dst (constantly @src)) + (alter-meta! dst merge (dissoc (meta src) :name))))) + +(defmacro import-fn + "Given a function in another namespace, defines a function with the + same name in the current namespace. Argument lists, doc-strings, + and original line-numbers are preserved." + ([sym] + `(import-fn ~sym nil)) + ([sym name] + (let [vr (resolve sym) + m (meta vr) + n (or name (:name m)) + arglists (:arglists m) + protocol (:protocol m)] + (when-not vr + (throw (IllegalArgumentException. (str "Don't recognize " sym)))) + (when (:macro m) + (throw (IllegalArgumentException. + (str "Calling import-fn on a macro: " sym)))) + `(do + (def ~(with-meta n {:protocol protocol}) (deref ~vr)) + (alter-meta! (var ~n) merge (dissoc (meta ~vr) :name)) + (link-vars ~vr (var ~n)) + ~vr)))) + +(defmacro import-macro + "Given a macro in another namespace, defines a macro with the same + name in the current namespace. Argument lists, doc-strings, and + original line-numbers are preserved." + ([sym] + `(import-macro ~sym nil)) + ([sym name] + (let [vr (resolve sym) + m (meta vr) + n (or name (with-meta (:name m) {})) + arglists (:arglists m)] + (when-not vr + (throw (IllegalArgumentException. (str "Don't recognize " sym)))) + (when-not (:macro m) + (throw (IllegalArgumentException. + (str "Calling import-macro on a non-macro: " sym)))) + `(do + (def ~n ~(resolve sym)) + (alter-meta! (var ~n) merge (dissoc (meta ~vr) :name)) + (.setMacro (var ~n)) + (link-vars ~vr (var ~n)) + ~vr)))) + +(defmacro import-def + "Given a regular def'd var from another namespace, defined a new var with the + same name in the current namespace." + ([sym] + `(import-def ~sym nil)) + ([sym name] + (let [vr (resolve sym) + m (meta vr) + n (or name (:name m)) + n (with-meta n (if (:dynamic m) {:dynamic true} {})) + nspace (:ns m)] + (when-not vr + (throw (IllegalArgumentException. (str "Don't recognize " sym)))) + `(do + (def ~n @~vr) + (alter-meta! (var ~n) merge (dissoc (meta ~vr) :name)) + (link-vars ~vr (var ~n)) + ~vr)))) + +(defmacro import-vars + "Imports a list of vars from other namespaces." + [& syms] + (let [unravel (fn unravel [x] + (if (sequential? x) + (->> x + rest + (mapcat unravel) + (map + #(symbol + (str (first x) + (when-let [n (namespace %)] + (str "." n))) + (name %)))) + [x])) + syms (mapcat unravel syms)] + `(do + ~@(map + (fn [sym] + (let [vr (resolve sym) + m (meta vr)] + (cond + (:macro m) `(import-macro ~sym) + (:arglists m) `(import-fn ~sym) + :else `(import-def ~sym)))) + syms)))) diff --git a/src/sunog/common.cljx b/src/sunog/common.cljx new file mode 100644 index 0000000..23bfa47 --- /dev/null +++ b/src/sunog/common.cljx @@ -0,0 +1,10 @@ +(ns sunog.common) + +(def child-events + [:child-added + :child-changed + :child-moved + :child-removed]) + +(def all-events + (conj child-events :value)) diff --git a/src/sunog/core.cljx b/src/sunog/core.cljx index 58be26e..314c10f 100644 --- a/src/sunog/core.cljx +++ b/src/sunog/core.cljx @@ -1,46 +1,51 @@ (ns sunog.core (:refer-clojure :exclude [get-in set! reset! conj! swap! dissoc! deref parents key]) (:require [clojure.string :as str] - [sunog.impl :as impl])) + [sunog.impl :as impl]) + (#+clj :require #+cljs :require-macros [potemkin.namespaces :refer [import-vars]])) ;; constants -(def child-events - [:child-added - :child-changed - :child-moved - :child-removed]) - -(def all-events - (conj child-events :value)) - -(def SERVER_TIMESTAMP impl/SERVER_TIMESTAMP) - -;; FIXME: camel-case keys? -;; hydrate to custom vectors to preserve rich keys? -;; preserve sets (don't coerce to vector) -;; similarly preserve keywords as values - -(def hydrate impl/hydrate) - -(def serialize impl/serialize) - -(def key impl/key) - -(def value impl/value) +(import-vars + [sunog.common + + all-events + child-events]) + +(import-vars + [sunog.impl + + SERVER_TIMESTAMP + connect + ;; snapshots + key + value + ;; FIXME: camel-case keys? + ;; hydrate to custom vectors to preserve rich keys? + ;; preserve sets (don't coerce to vector) + ;; similarly preserve keywords as values + hydrate + serialize + ;; navigation + get-in + parent + ;; getters / setters + deref + reset + reset-with-priority! + merge! + conj! + swap! + dissoc! + remove! + set-priority!]) + +;; circular defs from common to impl :( (defn- wrap-snapshot [snapshot] ;; TODO: enhance with snapshot protocol [(key snapshot) (value snapshot)]) -;; references - -(def connect impl/connect) - -(def get-in impl/get-in) - -(def parent impl/parent) - (defn parents "Probably don't need this. Or maybe we want more zipper nav (siblings, in-order, etc)" [ref] @@ -112,29 +117,6 @@ (defn unauth [ref] #+cljs (.unauth ref)) -;; -------------------- -;; getters 'n setters - -(def deref impl/deref) - -(def reset! impl/reset!) - -(defn reset-with-priority! [ref val priority & [cb]] - #+cljs (.setWithPriority ref (serialize val) priority (or cb impl/undefined))) - -(def merge! impl/merge!) - -(def conj! impl/conj!) - -(def swap! impl/swap!) - -(def dissoc! impl/dissoc!) - -(def remove! dissoc!) - -(defn set-priority! [ref priority & [cb]] - #+cljs (.setPriority ref priority (or cb impl/undefined))) - ;; nested variants (defn deref-in [ref korks & [cb]] diff --git a/src/sunog/impl.clj b/src/sunog/impl.clj index bf41ba4..64cc19f 100644 --- a/src/sunog/impl.clj +++ b/src/sunog/impl.clj @@ -20,7 +20,6 @@ (def child-events sunog.core/child-events) ;; TODO: review + unsubscribe listeners -;; TODO: server time ;; TODO: connect/discconet/on-disconnect ;; TODO: auth diff --git a/src/sunog/impl.cljs b/src/sunog/impl.cljs index f704fc7..88359b6 100644 --- a/src/sunog/impl.cljs +++ b/src/sunog/impl.cljs @@ -66,6 +66,9 @@ (defn reset! [ref val & [cb]] (.set ref (serialize val) (or cb undefined))) +(defn reset-with-priority! [ref val priority & [cb]] + (.setWithPriority ref (serialize val) priority (or cb impl/undefined))) + (defn merge! [ref val & [cb]] (.update ref (serialize val) (or cb undefined))) From 477cd0fab57a40e7f7093aaacd8ff52503f6e775 Mon Sep 17 00:00:00 2001 From: Chris Truter Date: Wed, 18 Feb 2015 18:15:54 +0200 Subject: [PATCH 4/5] Take a direct, full-on-CLJX approach --- src/potemkin/namespaces.clj | 102 ------- src/sunog/common.cljx | 10 - src/sunog/core.cljx | 266 +++++++++++++++--- src/sunog/impl.clj | 145 ---------- src/sunog/impl.cljs | 109 ------- src/sunog/utils.cljx | 2 +- ...{registry_test.cljs => registry_test.cljx} | 5 +- test/sunog/core_test.clj | 2 +- 8 files changed, 227 insertions(+), 414 deletions(-) delete mode 100644 src/potemkin/namespaces.clj delete mode 100644 src/sunog/common.cljx delete mode 100644 src/sunog/impl.clj delete mode 100644 src/sunog/impl.cljs rename test/{registry_test.cljs => registry_test.cljx} (88%) diff --git a/src/potemkin/namespaces.clj b/src/potemkin/namespaces.clj deleted file mode 100644 index 053468f..0000000 --- a/src/potemkin/namespaces.clj +++ /dev/null @@ -1,102 +0,0 @@ - -(ns potemkin.namespaces) - -(defn link-vars - "Makes sure that all changes to `src` are reflected in `dst`." - [src dst] - (add-watch src dst - (fn [_ src old new] - (alter-var-root dst (constantly @src)) - (alter-meta! dst merge (dissoc (meta src) :name))))) - -(defmacro import-fn - "Given a function in another namespace, defines a function with the - same name in the current namespace. Argument lists, doc-strings, - and original line-numbers are preserved." - ([sym] - `(import-fn ~sym nil)) - ([sym name] - (let [vr (resolve sym) - m (meta vr) - n (or name (:name m)) - arglists (:arglists m) - protocol (:protocol m)] - (when-not vr - (throw (IllegalArgumentException. (str "Don't recognize " sym)))) - (when (:macro m) - (throw (IllegalArgumentException. - (str "Calling import-fn on a macro: " sym)))) - `(do - (def ~(with-meta n {:protocol protocol}) (deref ~vr)) - (alter-meta! (var ~n) merge (dissoc (meta ~vr) :name)) - (link-vars ~vr (var ~n)) - ~vr)))) - -(defmacro import-macro - "Given a macro in another namespace, defines a macro with the same - name in the current namespace. Argument lists, doc-strings, and - original line-numbers are preserved." - ([sym] - `(import-macro ~sym nil)) - ([sym name] - (let [vr (resolve sym) - m (meta vr) - n (or name (with-meta (:name m) {})) - arglists (:arglists m)] - (when-not vr - (throw (IllegalArgumentException. (str "Don't recognize " sym)))) - (when-not (:macro m) - (throw (IllegalArgumentException. - (str "Calling import-macro on a non-macro: " sym)))) - `(do - (def ~n ~(resolve sym)) - (alter-meta! (var ~n) merge (dissoc (meta ~vr) :name)) - (.setMacro (var ~n)) - (link-vars ~vr (var ~n)) - ~vr)))) - -(defmacro import-def - "Given a regular def'd var from another namespace, defined a new var with the - same name in the current namespace." - ([sym] - `(import-def ~sym nil)) - ([sym name] - (let [vr (resolve sym) - m (meta vr) - n (or name (:name m)) - n (with-meta n (if (:dynamic m) {:dynamic true} {})) - nspace (:ns m)] - (when-not vr - (throw (IllegalArgumentException. (str "Don't recognize " sym)))) - `(do - (def ~n @~vr) - (alter-meta! (var ~n) merge (dissoc (meta ~vr) :name)) - (link-vars ~vr (var ~n)) - ~vr)))) - -(defmacro import-vars - "Imports a list of vars from other namespaces." - [& syms] - (let [unravel (fn unravel [x] - (if (sequential? x) - (->> x - rest - (mapcat unravel) - (map - #(symbol - (str (first x) - (when-let [n (namespace %)] - (str "." n))) - (name %)))) - [x])) - syms (mapcat unravel syms)] - `(do - ~@(map - (fn [sym] - (let [vr (resolve sym) - m (meta vr)] - (cond - (:macro m) `(import-macro ~sym) - (:arglists m) `(import-fn ~sym) - :else `(import-def ~sym)))) - syms)))) diff --git a/src/sunog/common.cljx b/src/sunog/common.cljx deleted file mode 100644 index 23bfa47..0000000 --- a/src/sunog/common.cljx +++ /dev/null @@ -1,10 +0,0 @@ -(ns sunog.common) - -(def child-events - [:child-added - :child-changed - :child-moved - :child-removed]) - -(def all-events - (conj child-events :value)) diff --git a/src/sunog/core.cljx b/src/sunog/core.cljx index 314c10f..3b2a321 100644 --- a/src/sunog/core.cljx +++ b/src/sunog/core.cljx @@ -1,56 +1,200 @@ (ns sunog.core (:refer-clojure :exclude [get-in set! reset! conj! swap! dissoc! deref parents key]) + #+clj + (:import [com.firebase.client + ServerValue + Firebase + FirebaseError + MutableData + DataSnapshot + ValueEventListener + ChildEventListener + Transaction + Transaction$Handler]) (:require [clojure.string :as str] - [sunog.impl :as impl]) - (#+clj :require #+cljs :require-macros [potemkin.namespaces :refer [import-vars]])) + [sunog.utils :as utils] + [sunog.registry :refer [register-listener]] + #+clj [clojure.walk :as walk] + #+cljs cljsjs.firebase)) + +;; TODO: JVM register + unsubscribe listeners +;; TODO: JVM connect/discconet/on-disconnect +;; TODO: JVM auth ;; constants -(import-vars - [sunog.common - - all-events - child-events]) - -(import-vars - [sunog.impl - - SERVER_TIMESTAMP - connect - ;; snapshots - key - value - ;; FIXME: camel-case keys? - ;; hydrate to custom vectors to preserve rich keys? - ;; preserve sets (don't coerce to vector) - ;; similarly preserve keywords as values - hydrate - serialize - ;; navigation - get-in - parent - ;; getters / setters - deref - reset - reset-with-priority! - merge! - conj! - swap! - dissoc! - remove! - set-priority!]) - -;; circular defs from common to impl :( +(def child-events + [:child-added + :child-changed + :child-moved + :child-removed]) + +(def all-events + (conj child-events :value)) + +#+cljs (def undefined) ;; firebase methods do not take kindly to nil callbacks + +(def SERVER_TIMESTAMP + #+clj ServerValue/TIMESTAMP + #+cljs js/Firebase.ServerValue.TIMESTAMP) + +;; helpers + +(declare wrap-snapshot) +(declare hydrate) +(declare reset!) + +#+clj +(defn- wrap-cb [cb] + (reify com.firebase.client.Firebase$CompletionListener + (^void onComplete [this ^FirebaseError err ^Firebase ref] + (if err (throw err) (cb ref))))) + +#+clj +(defn- reify-value-listener [cb] + (reify ValueEventListener + (^void onDataChange [_ ^DataSnapshot ds] + (cb (wrap-snapshot ds))) + (^void onCancelled [_ ^FirebaseError err] + (throw err)))) + +#+clj +(defn- build-tx-handler [f args cb] + (reify Transaction$Handler + (^com.firebase.client.Transaction$Result doTransaction [_ ^MutableData d] + (let [current (hydrate (.getValue d))] + (reset! d (apply f current args)) + (Transaction/success d))) + (^void onComplete [_ ^FirebaseError error, ^boolean committed, ^DataSnapshot d] + (if (and cb (not error) committed) + (cb (hydrate (.getValue d))))))) + +#+clj +(defn reify-child-listener [{:keys [added changed moved removed]}] + (reify ChildEventListener + (^void onChildAdded [_ ^DataSnapshot d ^String previous-child-name] + (if added (added (wrap-snapshot d)))) + (^void onChildChanged [_ ^DataSnapshot d ^String previous-child-name] + (if changed (changed (wrap-snapshot d)))) + (^void onChildMoved [_ ^DataSnapshot d ^String previous-child-name] + (if moved (moved (wrap-snapshot d)))) + (^void onChildRemoved [_ ^DataSnapshot d] + (if removed (removed (wrap-snapshot d)))) + (^void onCancelled [_ ^FirebaseError err] + (throw err)))) + +#+clj +(defn- strip-prefix [type] + (-> type name (str/replace #"^.+\-" "") keyword)) + + +(defn hydrate [v] + #+clj (if (map? v) (walk/keywordize-keys v) v) + #+cljs (js->clj v :keywordize-keys true)) + +(defn serialize [v] + #+clj (if (map? v) (walk/stringify-keys v) v) + #+cljs (clj->js v)) + +(defn key + "Last segment in reference or snapshot path" + [ref] + #+clj (.getKey ref) + #+cljs (.key ref)) + +(defn value + "Data stored within snapshot" + [snapshot] + (hydrate + #+clj (.getValue snapshot) + #+cljs (.val snapshot))) (defn- wrap-snapshot [snapshot] ;; TODO: enhance with snapshot protocol [(key snapshot) (value snapshot)]) +;; API + +(defn connect + "Create a reference for firebase" + [url] + #+clj (Firebase. url) + #+cljs (js/Firebase. url)) + +(defn get-in + "Obtain child reference from base by following korks" + [ref korks] + (let [path (utils/korks->path korks)] + (if-not (seq path) ref (.child ref path)))) + +(defn parent + "Immediate ancestor of reference, if any" + [ref] + (and ref + #+clj (.getParent ref) + #+cljs (.parent ref))) + (defn parents "Probably don't need this. Or maybe we want more zipper nav (siblings, in-order, etc)" [ref] (take-while identity (iterate parent (parent ref)))) +(defn deref [ref cb] + #+clj (.addListenerForSingleValue ref (reify-value-listener cb)) + #+cljs (.once ref "value" (comp cb value))) + +(defn reset! [ref val & [cb]] + #+clj + (if-not cb + (.setValue ref (serialize val)) + (.setValue ref (serialize val) (wrap-cb cb))) + #+cljs (.set ref (serialize val) (or cb undefined))) + +;; FIXME: support for JVM +#+cljs +(defn reset-with-priority! [ref val priority & [cb]] + (.setWithPriority ref (serialize val) priority (or cb undefined))) + +(defn merge! [ref val & [cb]] + #+clj + (if-not cb + (.updateChildren ref (serialize val)) + (.updateChildren ref (serialize val) (wrap-cb cb))) + #+cljs + (.update ref (serialize val) (or cb undefined))) + +(defn conj! [ref val & [cb]] + #+clj (reset! (.push ref) val cb) + #+cljs (.push ref (serialize val) (or cb undefined))) + +(defn swap! + "Update value atomically, with local optimistic writes" + [ref f & args] + (let [[cb args] (utils/extract-cb args)] + #+clj + (.runTransaction ref (build-tx-handler f args cb) true) + #+cljs + (let [f' #(-> % hydrate ((fn [x] (apply f x args))) serialize)] + (.transaction ref f' (or cb undefined))))) + +(defn dissoc! [ref & [cb]] + #+clj + (if-not cb + (.removeValue ref) + (.removeValue ref (wrap-cb cb))) + #+cljs + (.remove ref (or cb undefined))) + +(def remove! dissoc!) + +(defn set-priority! [ref priority & [cb]] + #+clj + (if-not cb + (.setPriority ref priority) + (.setPriority ref priority (wrap-cb cb))) + #+cljs + (.setPriority ref priority (or cb undefined))) + ;; (defonce connected (atom true)) @@ -78,7 +222,7 @@ #+cljs (.onDisconnect ref)) (defn cancel [ref-disconnect & [cb]] - #+cljs (.cancel ref-disconnect (or cb impl/undefined))) + #+cljs (.cancel ref-disconnect (or cb undefined))) ;; -------------------- ;; auth @@ -86,13 +230,13 @@ (defn build-opts [session-only?] #+cljs (if session-only? #js {:remember "sessionOnly"} - impl/undefined)) + undefined)) (defn- wrap-auth-cb [cb] #+cljs (if cb (fn [err info] (cb err (hydrate info))) - impl/undefined)) + undefined)) (defn auth [ref email password & [cb session-only?]] #+cljs (.authWithPassword @@ -125,6 +269,7 @@ (defn reset-in! [ref korks val & [cb]] (reset! (get-in ref korks) val cb)) +#+cljs (defn reset-with-priority-in! [ref korks val priority & [cb]] (reset-with-priority! (get-in ref korks) val priority cb)) @@ -148,10 +293,43 @@ ;; ------------------ ;; subscriptions +(defn- -listen-to [ref type cb] + (assert (some #{type} all-events) (str "Unknown type: " type)) + #+clj + (if-not (some #{type} child-events) + (.addValueEventListener ref (reify-value-listener cb)) + (.addChildEventListener ref (reify-child-listener + (hash-map (strip-prefix type) cb)))) + #+cljs + (let [type (utils/kebab->underscore type)] + (let [cb' (comp cb wrap-snapshot) + unsub! #(.off ref type cb')] + (.on ref type cb') + (register-listener ref type unsub!) + unsub!))) + +(defn- -listen-children [ref cb] + #+clj + (let [bases (map strip-prefix child-events) + cbs (zipmap bases (->> child-events + (map (fn [type] #(vector type %))) + (map #(comp cb %))))] + (.addChildEventListener ref (reify-child-listener cbs))) + #+cljs + (let [cbs (->> child-events + (map (fn [type] #(vector type %))) + (map #(comp cb %))) + unsubs (doall (map -listen-to (repeat ref) child-events cbs))] + (fn [] + (doseq [unsub! unsubs] + (unsub!))))) + (defn listen-to - ([ref type cb] (impl/listen-to ref type cb)) - ([ref korks type cb] (listen-to (get-in ref korks) type cb))) + "Subscribe to notifications of given type" + ([ref type cb] (-listen-to ref type cb)) + ([ref korks type cb] (-listen-to (get-in ref korks) type cb))) (defn listen-children - ([ref cb] (impl/listen-children ref cb)) - ([ref korks cb] (listen-children (get-in ref korks) cb))) + "Subscribe to all children notifications on a reference, and return an unsubscribe" + ([ref cb] (-listen-children ref cb)) + ([ref korks cb] (-listen-children (get-in ref korks) cb))) diff --git a/src/sunog/impl.clj b/src/sunog/impl.clj deleted file mode 100644 index 64cc19f..0000000 --- a/src/sunog/impl.clj +++ /dev/null @@ -1,145 +0,0 @@ -(ns sunog.impl - (:refer-clojure :exclude [get-in set! reset! conj! swap! dissoc! deref parents key]) - (:import [com.firebase.client - ServerValue - Firebase - FirebaseError - MutableData - DataSnapshot - ValueEventListener - ChildEventListener - Transaction - Transaction$Handler]) - (:require [clojure.core.async :refer [chan put!]] - [clojure.string :as str] - [clojure.walk :as walk])) - -;; who doesn't like circular refs? - -(def all-events sunog.core/all-events) -(def child-events sunog.core/child-events) - -;; TODO: review + unsubscribe listeners -;; TODO: connect/discconet/on-disconnect -;; TODO: auth - -(defn- wrap-cb [cb] - (reify com.firebase.client.Firebase$CompletionListener - (^void onComplete [this ^FirebaseError err ^Firebase ref] - (if err (throw err) (cb ref))))) -;; - -(def SERVER_TIMESTAMP ServerValue/TIMESTAMP) - -(defn hydrate [v] - (if (map? v) - (walk/keywordize-keys v) - v)) - -(defn serialize [v] - (if (map? v) - (walk/stringify-keys v) - v)) - -(defn key [ref] (.getKey ref)) - -(defn value [snapshot] - (hydrate (.getValue snapshot))) - -(defn wrap-snapshot [^DataSnapshot d] - [(key d) (value d)]) - -(defn connect [url] - (Firebase. url)) - -(defn get-in [root korks] - (let [path (if (sequential? korks) - (str/join "/" (map name korks)) - (name korks))] - (.child root path))) - -(defn parent [ref] (.getParent ref)) - -(defn- reify-value-listener [cb] - (reify ValueEventListener - (^void onDataChange [_ ^DataSnapshot ds] - (cb (wrap-snapshot ds))) - (^void onCancelled [_ ^FirebaseError err] - (throw err)))) - -(defn deref [ref cb] - (.addListenerForSingleValue ref (reify-value-listener cb))) - -(defn reset! [ref val & [cb]] - (if-not cb - (.setValue ref (serialize val)) - (.setValue ref (serialize val) (wrap-cb cb)))) - -(defn conj! - ([ref val & [cb]] - (reset! (.push ref) val cb))) - -(defn- build-tx-handler [f args cb] - (reify Transaction$Handler - (^com.firebase.client.Transaction$Result doTransaction [_ ^MutableData d] - (let [current (hydrate (.getValue d))] - (reset! d (apply f current args)) - (Transaction/success d))) - (^void onComplete [_ ^FirebaseError error, ^boolean committed, ^DataSnapshot d] - (if (and cb (not error) committed) - (cb (hydrate (.getValue d))))))) - -(defn swap! - "Update value atomically, with local optimistic writes" - [ref f & args] - (let [cb nil #_"extract this like in CLJS case"] - (.runTransaction ref (build-tx-handler f args cb) true))) - -(defn merge! [ref val & [cb]] - (if-not cb - (.updateChildren ref (serialize val)) - (.updateChildren ref (serialize val) (wrap-cb cb)))) - -(defn dissoc! [ref & [cb]] - (if-not cb - (.removeValue ref) - (.removeValue ref (wrap-cb cb)))) - -(defn set-priority [ref priority & [cb]] - (if-not cb - (.setPriority ref priority) - (.setPriority ref priority (wrap-cb cb)))) - -(defn reify-child-listener [{:keys [added changed moved removed]}] - (reify ChildEventListener - (^void onChildAdded [_ ^DataSnapshot d ^String previous-child-name] - (if added (added (wrap-snapshot d)))) - (^void onChildChanged [_ ^DataSnapshot d ^String previous-child-name] - (if changed (changed (wrap-snapshot d)))) - (^void onChildMoved [_ ^DataSnapshot d ^String previous-child-name] - (if moved (moved (wrap-snapshot d)))) - (^void onChildRemoved [_ ^DataSnapshot d] - (if removed (removed (wrap-snapshot d)))) - (^void onCancelled [_ ^FirebaseError err] - (throw err)))) - -(defn- strip-prefix [type] - (-> type name (str/replace #"^.+\-" "") keyword)) - -(defn listen-to - ([ref type cb] - (assert (some #{type} all-events) (str "Unknown type: " type)) - (if-not (some #{type} child-events) - (.addValueEventListener ref (reify-value-listener cb)) - (.addChildEventListener ref (reify-child-listener - (hash-map (strip-prefix type) cb))))) - ([ref korks type cb] - (listen-to (get-in ref korks) type cb))) - -(defn listen-children - ([ref cb] - (let [bases (map strip-prefix child-events) - cbs (zipmap bases (->> child-events - (map (fn [type] #(vector type %))) - (map #(comp cb %))))] - (.addChildEventListener ref (reify-child-listener cbs))))) diff --git a/src/sunog/impl.cljs b/src/sunog/impl.cljs deleted file mode 100644 index 88359b6..0000000 --- a/src/sunog/impl.cljs +++ /dev/null @@ -1,109 +0,0 @@ -(ns sunog.impl - (:refer-clojure :exclude [get-in set! reset! conj! swap! dissoc! deref parents key]) - (:require cljsjs.firebase - [sunog.utils :as utils] - [sunog.registry :refer [register-listener]])) - -(def undefined) ;; firebase methods do not take kindly to nil callbacks - -(def SERVER_TIMESTAMP js/Firebase.ServerValue.TIMESTAMP) - -;; circular refs not working, we repeat - -(declare key) -(declare value) - -(def child-events - [:child-added - :child-changed - :child-moved - :child-removed]) - -(def all-events - (conj child-events :value)) - -(defn- wrap-snapshot [snapshot] - ;; TODO: enhance with snapshot protocol - [(key snapshot) (value snapshot)]) - -;; - -(defn hydrate [js-val] - (js->clj js-val :keywordize-keys true)) - -(defn serialize [clj-val] - (clj->js clj-val)) - -(defn key - "Last segment in reference or snapshot path" - [ref] - (.key ref)) - -(defn value [snapshot] - (hydrate (.val snapshot))) - -(defn connect - "Create a reference for firebase" - [url] - (js/Firebase. url)) - -(defn get-in - "Obtain child reference from base by following korks" - [ref korks] - (let [path (utils/korks->path korks)] - (if-not (seq path) ref (.child ref path)))) - -(defn parent - "Immediate ancestor of reference, if any" - [ref] - (and ref (.parent ref))) - -;; - -(defn deref [ref cb] - (.once ref "value" (comp cb value))) - -(defn reset! [ref val & [cb]] - (.set ref (serialize val) (or cb undefined))) - -(defn reset-with-priority! [ref val priority & [cb]] - (.setWithPriority ref (serialize val) priority (or cb impl/undefined))) - -(defn merge! [ref val & [cb]] - (.update ref (serialize val) (or cb undefined))) - -(defn conj! [ref val & [cb]] - (.push ref (serialize val) (or cb undefined))) - -(defn swap! [ref f & args] - (let [[cb args] (utils/extract-cb args) - f' #(-> % hydrate ((fn [x] (apply f x args))) serialize)] - (.transaction ref f' (or cb undefined)))) - -(defn dissoc! [ref & [cb]] - (.remove ref (or cb undefined))) - -;; ------------------ -;; subscriptions - -(defn listen-to - "Subscribe to notifications of given type" - [ref type cb] - (assert (some #{type} all-events) (str "Unknown type: " type)) - (let [type (utils/kebab->underscore type)] - (let [cb' (comp cb wrap-snapshot) - unsub! #(.off ref type cb')] - (.on ref type cb') - (register-listener ref type unsub!) - unsub!))) - -(defn listen-children - "Subscribe to all children notifications on a reference, and return an unsubscribe" - [ref cb] - (let [cbs (->> child-events - (map (fn [type] #(vector type %))) - (map #(comp cb %))) - unsubs (doall (map listen-to (repeat ref) child-events cbs))] - (fn [] - (doseq [unsub! unsubs] - (unsub!))))) diff --git a/src/sunog/utils.cljx b/src/sunog/utils.cljx index 7373a9a..ca6187f 100644 --- a/src/sunog/utils.cljx +++ b/src/sunog/utils.cljx @@ -7,7 +7,7 @@ (defn- underscore->kebab [string] (-> string (str/replace "_" "-") keyword)) -(defn- korks->path [korks] +(defn korks->path [korks] (if (sequential? korks) (str/join "/" (map name korks)) (when korks (name korks)))) diff --git a/test/registry_test.cljs b/test/registry_test.cljx similarity index 88% rename from test/registry_test.cljs rename to test/registry_test.cljx index c1adddd..39ff68a 100644 --- a/test/registry_test.cljs +++ b/test/registry_test.cljx @@ -1,6 +1,7 @@ (ns sunog.registry-test - (:require-macros [cemerick.cljs.test :refer [is deftest done]]) - (:require [cemerick.cljs.test :as t] + #+cljs (:require-macros [cemerick.cljs.test :refer [is deftest done]]) + (:require #+cljs [cemerick.cljs.test :as t] + #+clj [clojure.test :as t :refer [deftest is]] [sunog.registry :as r])) (reset! r/unsubs {}) diff --git a/test/sunog/core_test.clj b/test/sunog/core_test.clj index 6528fe3..8d36055 100644 --- a/test/sunog/core_test.clj +++ b/test/sunog/core_test.clj @@ -2,7 +2,7 @@ (:require [clojure.test :refer :all] [sunog.core :as p])) -(def firebase-url "https://blazing-fire-1915.firebaseio.com") +;; (def firebase-url "https://blazing-fire-1915.firebaseio.com") (def firebase-url "https://luminous-torch-5788.firebaseio.com/") (deftest serialize-test From ec8fed5bf545f75f7aa56742665ce69f555b8e95 Mon Sep 17 00:00:00 2001 From: Chris Truter Date: Wed, 18 Feb 2015 18:52:39 +0200 Subject: [PATCH 5/5] Remove problematic test-runner CLJX means bringing in pdo or something, and didn't seem to work properly --- project.clj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/project.clj b/project.clj index 1425a96..d0d1317 100644 --- a/project.clj +++ b/project.clj @@ -30,10 +30,10 @@ :auto-clean false :profiles {:dev {:dependencies [[om "0.7.3"]] - :plugins [[com.keminglabs/cljx "0.5.0"]]}} - - :aliases {"test-all" ["do" "test," "cljsbuild" "once" "test"] - "auto-test" ["do" "clean," "cljsbuild" "auto" "test"]} + :plugins [[com.keminglabs/cljx "0.5.0"]] + :aliases {"test-all" ["do" "cljx" "once," + "test," + "cljsbuild" "once" "test"]}}} :aot [sunog.clojure.android-stub]