From 0e182a26659499e54102fc1c2e90d8579bf64398 Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Sat, 15 Aug 2015 21:44:36 +0200 Subject: [PATCH 01/44] decouple re-frame and reagent+core.async This rewrite was motivated by https://github.com/Day8/re-frame/pull/106. In essence re-frame provides a transducer: state, event -> state. This transducer is able to apply algorithmic transformations on series of events and build final state up. This allows complete decoupling of re-frame handlers, subscriptions and middlewares logic from mechanisms how events are queued, processed and how are their effects applied to app-db. This "bare re-frame" is implemented in frame.cljs. For maximal flexibility bare re-frame must be pure and have no special knowledge of app-db, reagent/ratom and core.async. It must be possible to create multiple independent instances of bare re-frame. For convenience scaffold.cljs is re-implementing original re-frame functionality of v0.4.1 using those new bare primitives. This is exposed as public api via core.cljs. App developer can opt-in using default implementation by requiring re-frame.core. In this scenario re-frame provides a single app-db which is reagent's ratom and runs single event loop where events are dispatched and processed via core.async channel. A single re-frame instance is created and kept in re-frame.core/app-frame atom. Plus developer gets access to the original imperative API to manipulate them. App developer is also free to pick/implement other means of employing bare re-frame by not requiring re-frame.core directly and to build own scaffold around bare re-frame primitives. --- examples/simple/checkouts/re-frame | 1 + examples/simple/project.clj | 30 ++--- project.clj | 72 +++++------ src/re_frame/core.cljs | 80 +++++------- src/re_frame/db.cljs | 11 -- src/re_frame/frame.cljs | 91 ++++++++++++++ src/re_frame/handlers.cljs | 94 -------------- src/re_frame/logging.cljs | 17 +++ src/re_frame/middleware.cljs | 167 +++++++++++-------------- src/re_frame/router.cljs | 93 -------------- src/re_frame/scaffold.cljs | 192 +++++++++++++++++++++++++++++ src/re_frame/subs.cljs | 34 ----- src/re_frame/undo.cljs | 173 -------------------------- src/re_frame/utils.cljs | 49 ++------ test/re-frame/middleware.cljs | 41 ------ test/re_frame/test/core.cljs | 34 +++++ test/re_frame/test/frame.cljs | 76 ++++++++++++ test/re_frame/test/middleware.cljs | 32 +++++ 18 files changed, 604 insertions(+), 683 deletions(-) create mode 120000 examples/simple/checkouts/re-frame delete mode 100644 src/re_frame/db.cljs create mode 100644 src/re_frame/frame.cljs delete mode 100644 src/re_frame/handlers.cljs create mode 100644 src/re_frame/logging.cljs delete mode 100644 src/re_frame/router.cljs create mode 100644 src/re_frame/scaffold.cljs delete mode 100644 src/re_frame/subs.cljs delete mode 100644 src/re_frame/undo.cljs delete mode 100644 test/re-frame/middleware.cljs create mode 100644 test/re_frame/test/core.cljs create mode 100644 test/re_frame/test/frame.cljs create mode 100644 test/re_frame/test/middleware.cljs diff --git a/examples/simple/checkouts/re-frame b/examples/simple/checkouts/re-frame new file mode 120000 index 000000000..1b20c9fb8 --- /dev/null +++ b/examples/simple/checkouts/re-frame @@ -0,0 +1 @@ +../../../ \ No newline at end of file diff --git a/examples/simple/project.clj b/examples/simple/project.clj index 072390a9f..47bb77109 100644 --- a/examples/simple/project.clj +++ b/examples/simple/project.clj @@ -1,32 +1,32 @@ (defproject simple-re-frame "0.4.0" - :dependencies [[org.clojure/clojure "1.6.0"] - [org.clojure/clojurescript "0.0-3208"] + :dependencies [[org.clojure/clojure "1.7.0"] + [org.clojure/clojurescript "1.7.107"] [reagent "0.5.0"] [re-frame "0.4.1"] - [figwheel "0.2.6"]] + [figwheel "0.3.7"]] :plugins [[lein-cljsbuild "1.0.5"] - [lein-figwheel "0.2.6"]] + [lein-figwheel "0.3.7"]] :hooks [leiningen.cljsbuild] - :profiles {:dev {:cljsbuild - {:builds {:client {:source-paths ["devsrc"] - :compiler {:main simpleexample.dev - :asset-path "js" - :optimizations :none - :source-map true - :source-map-timestamp true}}}}} + :profiles {:dev {:cljsbuild + {:builds {:client {:source-paths ["devsrc"] + :compiler {:main simpleexample.dev + :asset-path "js" + :optimizations :none + :source-map true + :source-map-timestamp true}}}}} :prod {:cljsbuild - {:builds {:client {:compiler {:optimizations :advanced - :elide-asserts true - :pretty-print false}}}}}} + {:builds {:client {:compiler {:optimizations :advanced + :elide-asserts true + :pretty-print false}}}}}} :figwheel {:repl false} :clean-targets ^{:protect false} ["resources/public/js"] - :cljsbuild {:builds {:client {:source-paths ["src"] + :cljsbuild {:builds {:client {:source-paths ["checkouts/re-frame/src" "src"] :compiler {:output-dir "resources/public/js" :output-to "resources/public/js/client.js"}}}}) diff --git a/project.clj b/project.clj index 623ce0053..0b584814e 100644 --- a/project.clj +++ b/project.clj @@ -1,43 +1,43 @@ -(defproject re-frame "0.4.1" - :description "A Clojurescript MVC-like Framework For Writing SPAs Using Reagent." - :url "https://github.com/Day8/re-frame.git" - :license {:name "MIT"} - :dependencies [[org.clojure/clojure "1.6.0"] - [org.clojure/clojurescript "0.0-3211"] +(defproject re-frame "0.4.2-TRANSDUCERS" + :description "A Clojurescript MVC-like Framework For Writing SPAs Using Reagent." + :url "https://github.com/Day8/re_frame.git" + :license {:name "MIT"} + :dependencies [[org.clojure/clojure "1.7.0"] + [org.clojure/clojurescript "1.7.107"] [org.clojure/core.async "0.1.346.0-17112a-alpha"] [reagent "0.5.0"]] - :profiles {:debug {:debug true} - :dev {:dependencies [[spellhouse/clairvoyant "0.0-48-gf5e59d3"]] - :plugins [[lein-cljsbuild "1.0.5"] - [com.cemerick/clojurescript.test "0.3.3"]]}} - + :profiles {:debug {:debug true} + :dev {:dependencies [[spellhouse/clairvoyant "0.0-48-gf5e59d3"]] + :plugins [[lein-cljsbuild "1.0.5"] + [com.cemerick/clojurescript.test "0.3.3"]]}} :clean-targets [:target-path - "run/compiled/demo"] + "run/compiled"] :resource-paths ["run/resources"] - :jvm-opts ["-Xmx1g" "-XX:+UseConcMarkSweepGC"] ;; - :source-paths ["src"] - :test-paths ["test"] - - - :cljsbuild {:builds [{:id "test" ;; currently bogus, there is no demo or tests - :source-paths ["test"] - :compiler {:output-to "run/compiled/test.js" - :source-map "run/compiled/test.js.map" - :output-dir "run/compiled/test" - :optimizations :simple - :pretty-print true}}] - - :test-commands {"rhino" ["rhino" "-opt" "-1" :rhino-runner - "run/compiled/test.js"] - "slimer" ["xvfb-run" "-a" "slimerjs" :runner - "run/compiled/test.js"] - "phantom" ["phantomjs" ; doesn't work with phantomjs < 2.0.0 - :runner "run/compiled/test.js"]}} - - :aliases {"auto" ["do" "clean," "cljsbuild" "clean," "cljsbuild" "auto" "demo,"] - "once" ["do" "clean," "cljsbuild" "clean," "cljsbuild" "once" "demo,"] - "test-rhino" ["do" "clean," "cljsbuild" "once," "cljsbuild" "test" "rhino"] - "test-slimer" ["do" "clean," "cljsbuild" "once," "cljsbuild" "test" "slimer"] }) + :jvm-opts ["-Xmx1g" "-XX:+UseConcMarkSweepGC"] + :source-paths [] + :test-paths ["test"] + + + :cljsbuild {:builds [{:id "test" ;; currently bogus, there is no demo or tests + :source-paths ["src" "test"] + :compiler {;:main re-frame.test-runner + :output-to "run/compiled/test.js" + :source-map "run/compiled/test.js.map" + :output-dir "run/compiled/test" + ;:target :nodejs ;;; this target required for node, plus a *main* defined in the tests. + ;:hashbang false ;;; https://github.com/cemerick/clojurescript.test/issues/68#issuecomment-52981151 + :optimizations :simple ;; https://github.com/cemerick/clojurescript.test/issues/68 + :pretty-print true}}] + + :test-commands {"rhino" ["rhino" "-opt" "-1" :rhino-runner "run/compiled/test.js"] + "slimer" ["xvfb-run" "-a" "slimerjs" :runner "run/compiled/test.js"] + "phantom" ["phantomjs" :runner "run/compiled/test.js"]}} ; doesn't work with phantomjs < 2.0.0 + + :aliases {"auto" ["do" "clean," "cljsbuild" "clean," "cljsbuild" "auto" "demo,"] + "once" ["do" "clean," "cljsbuild" "clean," "cljsbuild" "once" "demo,"] + "test-rhino" ["do" "clean," "cljsbuild" "once," "cljsbuild" "test" "rhino"] + "test-slimer" ["do" "clean," "cljsbuild" "once," "cljsbuild" "test" "slimer"] + "test-phantom" ["do" "clean," "cljsbuild" "once," "cljsbuild" "test" "phantom"]}) diff --git a/src/re_frame/core.cljs b/src/re_frame/core.cljs index 726c13558..f022ad2fb 100644 --- a/src/re_frame/core.cljs +++ b/src/re_frame/core.cljs @@ -1,56 +1,34 @@ (ns re-frame.core - (:require - [re-frame.handlers :as handlers] - [re-frame.subs :as subs] - [re-frame.router :as router] - [re-frame.utils :as utils] - [re-frame.middleware :as middleware])) + (:require [re-frame.scaffold :as scaffold])) +; this file provides public API to default re-frame setup +; note: by including this namespace, you will get default app-db, default app-frame +; and start default router-loop automatically -;; -- API ------- - -(def dispatch router/dispatch) -(def dispatch-sync router/dispatch-sync) - -(def register-sub subs/register) -(def clear-sub-handlers! subs/clear-handlers!) -(def subscribe subs/subscribe) - - -(def clear-event-handlers! handlers/clear-handlers!) - - -(def pure middleware/pure) -(def debug middleware/debug) -(def undoable middleware/undoable) -(def path middleware/path) -(def enrich middleware/enrich) -(def trim-v middleware/trim-v) -(def after middleware/after) -(def log-ex middleware/log-ex) - - -;; ALPHA - EXPERIMENTAL MIDDLEWARE -(def on-changes middleware/on-changes) - - -;; -- Logging ----- -;; re-frame uses the logging functions: warn, log, error, group and groupEnd -;; By default, these functions map directly to the js/console implementations -;; But you can override with your own (set or subset): -;; (set-loggers! {:warn my-warn :log my-looger ...}) -(def set-loggers! utils/set-loggers!) - - -;; -- Convenience API ------- - -;; Almost 100% of handlers will be pure, so make it easy to -;; register with "pure" middleware in the correct (left-hand-side) position. -(defn register-handler - ([id handler] - (handlers/register-base id pure handler)) - ([id middleware handler] - (handlers/register-base id [pure middleware] handler))) - +(def app-db scaffold/app-db) ; the default instance of app-db +(def app-frame scaffold/app-frame) ; the default instance of re-frame +;; -- API ------- +(def router-loop scaffold/router-loop) +(def set-loggers! scaffold/set-loggers!) +(def register-sub scaffold/register-sub) +(def clear-sub-handlers! scaffold/clear-sub-handlers!) +(def subscribe scaffold/subscribe) +(def clear-event-handlers! scaffold/clear-event-handlers!) +(def dispatch scaffold/dispatch) +(def dispatch-sync scaffold/dispatch-sync) +(def register-handler scaffold/register-handler) + +;(def pure scaffold/pure) +(def debug scaffold/debug) +;(def undoable scaffold/undoable) +(def path scaffold/path) +(def enrich scaffold/enrich) +(def trim-v scaffold/trim-v) +(def after scaffold/after) +(def log-ex scaffold/log-ex) +(def on-changes scaffold/on-changes) + +;; start event processing +(scaffold/router-loop) diff --git a/src/re_frame/db.cljs b/src/re_frame/db.cljs deleted file mode 100644 index c6859b6fc..000000000 --- a/src/re_frame/db.cljs +++ /dev/null @@ -1,11 +0,0 @@ -(ns re-frame.db - (:require [reagent.core :as reagent])) - - -;; -- Application State -------------------------------------------------------------------------- -;; -;; Should not be accessed directly by application code -;; Read access goes through subscriptions. -;; Updates via event handlers. -(def app-db (reagent/atom {})) - diff --git a/src/re_frame/frame.cljs b/src/re_frame/frame.cljs new file mode 100644 index 000000000..0412caffe --- /dev/null +++ b/src/re_frame/frame.cljs @@ -0,0 +1,91 @@ +(ns re-frame.frame + (:require [re-frame.utils :refer [get-event-id get-subscription-id]])) + +; re-frame meat reimplemented in terms of pure functions (with help of transducers) + +(defn frame-xform + "Returns a transducer: state, event -> state. +This transducer reads event-id and applies matching handler on input state." + [handlers loggers db-selector] + (fn [rf] + (fn + ([] (rf)) + ([result] (rf result)) + ([state event] + (let [event-id (get-event-id event) + handler-fn (event-id handlers)] + (if (nil? handler-fn) + (let [error (:error loggers)] + (error "re-frame: no event handler registered for: \"" event-id "\". Ignoring.") + state) + (rf state (handler-fn (db-selector state) event)))))))) + + +; TODO: this should be implemented as deftype/defrecord in future +(defn frame-factory + "Constructs independent frame instance." + [handlers subscriptions loggers db-selector] + {:handlers handlers + :subscriptions subscriptions + :loggers loggers + :db-selector db-selector}) + +; see http://clojure.org/transducers +(defn get-frame-transducer [frame] + (frame-xform (:handlers frame) (:loggers frame) (:db-selector frame))) + +(defn subscribe + "Returns a reagent/reaction which observes state." + [frame subscription-spec] + (.log js/console "in-subscribe" frame) + (let [subscription-id (get-subscription-id subscription-spec) + handler-fn (get-in frame [:subscriptions subscription-id])] + (if (nil? handler-fn) + (let [error (get-in frame [:loggers :error])] + (error "re-frame: no subscription handler registered for: \"" subscription-id "\". Returning a nil subscription.")) + (apply handler-fn subscription-spec)))) + +; TODO: figure out what to do here, re-frame should not be aware of app-db here, this is for backward compatibility +(defn legacy-subscribe + "Returns a reagent/reaction which observes state." + [frame app-db subscription-spec] + (.log js/console "in-subscribe" frame) + (let [subscription-id (get-subscription-id subscription-spec) + handler-fn (get-in frame [:subscriptions subscription-id])] + (if (nil? handler-fn) + (let [error (get-in frame [:loggers :error])] + (error "re-frame: no subscription handler registered for: \"" subscription-id "\". Returning a nil subscription.")) + (handler-fn app-db subscription-spec)))) + +(defn register-subscription-handler + "Registers a handler function for an id." + [frame subscription-id handler-fn] + (let [existing-subscriptions (get frame :subscriptions)] + (if (contains? existing-subscriptions subscription-id) + (let [warn (get-in frame [:loggers :warn])] + (warn "re-frame: overwriting subscription-handler for: " subscription-id)))) + (assoc-in frame [:subscriptions subscription-id] handler-fn)) + +(defn clear-subscription-handlers + "Unregisters all subscription handlers." + [frame] + (assoc frame :subscriptions {})) + +(defn register-event-handler + "Register a handler for an event." + ([frame event-id handler-fn] + (let [existing-handlers (get frame :handlers)] + (if (contains? existing-handlers event-id) + (let [warn (get-in frame [:loggers :warn])] + (warn "re-frame: overwriting an event-handler for: " event-id))) + (assoc-in frame [:handlers event-id] handler-fn)))) + +(defn clear-event-handlers + "Unregisters all event handlers." + [frame] + (assoc frame :handlers {})) + +(defn set-loggers + "Resets loggers." + [frame new-loggers] + (assoc frame :loggers new-loggers)) diff --git a/src/re_frame/handlers.cljs b/src/re_frame/handlers.cljs deleted file mode 100644 index 7250e1cc0..000000000 --- a/src/re_frame/handlers.cljs +++ /dev/null @@ -1,94 +0,0 @@ -(ns re-frame.handlers - (:require [re-frame.db :refer [app-db]] - [re-frame.utils :refer [first-in-vector warn error]])) - - -;; -- composing middleware ----------------------------------------------------------------------- - - -(defn report-middleware-factories - "See https://github.com/Day8/re-frame/issues/65" - [v] - (letfn [(name-of-factory - [f] - (-> f meta :re-frame-factory-name)) - (factory-names-in - [v] - (remove nil? (map name-of-factory v)))] - (doseq [name (factory-names-in v)] - (error "re-frame: \"" name "\" used incorrectly. Must be used like this \"(" name " ...)\", whereas you just used \"" name "\".")))) - - -(defn comp-middleware - "Given a vector of middleware, filter out any nils, and use \"comp\" to compose the elements. - v can have nested vectors, and will be flattened before \"comp\" is applied. - For convienience, if v is a function (assumed to be middleware already), just return it. - Filtering out nils allows us to create Middleware conditionally like this: - (comp-middleware [pure (when debug? debug)]) ;; that 'when' might leave a nil - " - [v] - - (cond - (fn? v) v ;; assumed to be existing middleware - (seq? v) (let [v (remove nil? (flatten v))] - (report-middleware-factories v) - (apply comp v)) - :else (warn "re-frame: comp-middleware expects a vector, got: " v))) - - -;; -- the register of event handlers -------------------------------------------------------------- - -(def ^:private id->fn (atom {})) - - -(defn lookup-handler - [event-id] - (get @id->fn event-id)) - - -(defn clear-handlers! - "Unregister all event handlers" - [] - (reset! id->fn {})) - - -(defn register-base - "register a handler for an event. - This is low level and it is expected that \"re-frame.core/register-handler\" would - generally be used." - ([event-id handler-fn] - (when (contains? @id->fn event-id) - (warn "re-frame: overwriting an event-handler for: " event-id)) ;; allow it, but warn. - (swap! id->fn assoc event-id handler-fn)) - - ([event-id middleware handler-fn] - (let [mid-ware (comp-middleware middleware) ;; compose the middleware - midware+hfn (mid-ware handler-fn)] ;; wrap the handler in the middleware - (register-base event-id midware+hfn)))) - - - - -;; -- lookup and call ----------------------------------------------------------------------------- - -(def ^:dynamic *handling* nil) ;; remember what event we are currently handling - - -(defn handle - "Given an event vector, look up the handler, then call it. - By default, handlers are not assumed to be pure. They are called with - two paramters: - - the `app-db` atom - - the event vector - The handler is assumed to side-effect on `app-db` - the return value is ignored. - To write a pure handler, use the \"pure\" middleware when registering the handler." - [event-v] - (let [event-id (first-in-vector event-v) - handler-fn (lookup-handler event-id)] - (if (nil? handler-fn) - (error "re-frame: no event handler registered for: \"" event-id "\". Ignoring.") - (if *handling* - (error "re-frame: while handling \"" *handling* "\" dispatch-sync was called for \"" event-v "\". You can't call dispatch-sync in an event handler.") - (binding [*handling* event-v] - (handler-fn app-db event-v)))))) - diff --git a/src/re_frame/logging.cljs b/src/re_frame/logging.cljs new file mode 100644 index 000000000..45476f07c --- /dev/null +++ b/src/re_frame/logging.cljs @@ -0,0 +1,17 @@ +(ns re-frame.logging) + +;; -- Logging ----------------------------------------------------------------- +;; +;; re-frame internally uses a set of logging functions which, by default, +;; print to js/console. +;; Use set-loggers! if you want to change this default behaviour. +;; In production environment, you may want to capture exceptions and POST +;; them somewhere. to , you might want to override the way that exceptions are +;; handled by overridding "error" +;; +(def default-loggers + {:log #(.log js/console %) + :warn #(.warn js/console %) + :error #(.error js/console %) + :group #(if (.group js/console) (.group js/console %) (.log js/console %)) ;; group does not exist < IE 11 + :groupEnd #(when (.groupEnd js/console) (.groupEnd js/console))}) ;; groupEnd does not exist < IE 11 diff --git a/src/re_frame/middleware.cljs b/src/re_frame/middleware.cljs index 0ce0085c9..866bd5dc2 100644 --- a/src/re_frame/middleware.cljs +++ b/src/re_frame/middleware.cljs @@ -1,41 +1,9 @@ (ns re-frame.middleware - (:require - [reagent.ratom :refer [IReactiveAtom]] - [re-frame.undo :refer [store-now!]] - [re-frame.utils :refer [warn log group groupEnd error]] - [clojure.data :as data])) + (:require [clojure.data :as data])) ;; See docs in the Wiki: https://github.com/Day8/re-frame/wiki - -(defn pure - "Acts as an adaptor, allowing handlers to be writen as pure functions. - The re-frame router passes the `app-db` atom as the first parameter to any handler. - This middleware adapts that atom to be the value within the atom. - If you strip away the error/efficiency checks, this middleware is doing: - (reset! app-db (handler @app-db event-vec)) - You don't have to use this middleware directly. It is automatically applied to - your handler's middleware when you use \"register-handler\". - In fact, the only way to by-pass automatic use of \"pure\" in your middleware - is to use the low level registration function \"re-frame.handlers/register-handler-base\"" - [handler] - (fn pure-handler - [app-db event-vec] - (if (not (satisfies? IReactiveAtom app-db)) - (do - (if (map? app-db) - (warn "re-frame: Looks like \"pure\" is in the middleware pipeline twice. Ignoring.") - (warn "re-frame: \"pure\" middleware not given a Ratom. Got: " app-db)) - handler) ;; turn this into a noop handler - (let [db @app-db - new-db (handler db event-vec)] - (if (nil? new-db) - (error "re-frame: your pure handler returned nil. It should return the new db state.") - (if-not (identical? db new-db) - (reset! app-db new-db))))))) - - (defn log-ex "Middleware which catches and prints any handler-generated exceptions to console. Handlers are called from within a core.async go-loop, and core.async produces @@ -45,33 +13,39 @@ So this middleware catches and prints to stacktrace before the core.async sausage machine has done its work. " - [handler] - (fn log-ex-handler - [db v] - (warn "re-frame: use of \"log-ex\" is deprecated. You don't need it any more IF YOU ARE USING CHROME 44. Chrome now seems to now produce good stack traces.") - (try - (handler db v) - (catch :default e ;; ooops, handler threw - (do - (.error js/console (.-stack e)) - (throw e)))))) + [frame-atom] + (fn [handler] + (fn log-ex-handler + [db v] + ((get-in @frame-atom [:loggers :warn]) "re-frame: use of \"log-ex\" is deprecated. You don't need it any more IF YOU ARE USING CHROME 44. Chrome now seems to now produce good stack traces.") + (try + (handler db v) + (catch :default e ;; ooops, handler threw + (do + (.error js/console (.-stack e)) + (throw e))))))) (defn debug "Middleware which logs debug information to js/console for each event. Includes a clojure.data/diff of the db, before vs after, showing the changes caused by the event." - [handler] - (fn debug-handler - [db v] - (log "-- New Event ----------------------------------------------------") - (group "re-frame event: " v) - (let [new-db (handler db v) - diff (data/diff db new-db)] - (log "only before: " (first diff)) - (log "only after : " (second diff)) - (groupEnd) - new-db))) + [frame-atom] + (fn [handler] + (fn debug-handler [db v] + (let [frame @frame-atom + loggers (get frame :loggers) + log (get loggers :log) + group (get loggers :group) + groupEnd (get loggers :groupEnd)] + (log "-- New Event ----------------------------------------------------") + (group "re-frame event: " v) + (let [new-db (handler db v) + diff (data/diff db new-db)] + (log "only before: " (first diff)) + (log "only after : " (second diff)) + (groupEnd) + new-db))))) @@ -83,10 +57,11 @@ [db [x y z]] ;; <-- instead of [_ x y z] ....) " - [handler] - (fn trim-v-handler - [db v] - (handler db (vec (rest v))))) + [_frame-atom] + (fn [handler] + (fn trim-v-handler + [db v] + (handler db (vec (rest v)))))) ;; -- Middleware Factories ---------------------------------------------------- @@ -101,7 +76,7 @@ ;; ;; So, yeah, weird. -(def path +(defn path "A middleware factory which supplies a sub-tree of `db` to the handler. Works a bit like update-in. Supplies a narrowed data structure for the handler. Afterwards, grafts the result of the handler back into db. @@ -111,12 +86,13 @@ (path [:some :path] :to :here) (path [:some :path] [:to] :here) " - ^{:re-frame-factory-name "path"} + [frame-atom] + ;^{:re-frame-factory-name "path"} (fn path [& args] - (let [path (flatten args)] + (let [path (flatten args)] (when (empty? path) - (error "re-frame: \"path\" middleware given no params.")) + ((get-in @frame-atom [:loggers :error]) "re-frame: \"path\" middleware given no params.")) (fn path-middleware [handler] (fn path-handler @@ -124,29 +100,29 @@ (assoc-in db path (handler (get-in db path) v))))))) -(def undoable - "A Middleware factory which stores an undo checkpoint. - \"explanation\" can be either a string or a function. If it is a - function then must be: (db event-vec) -> string. - \"explanation\" can be nil. in which case \"\" is recorded. - " - ^{:re-frame-factory-name "undoable"} - (fn undoable - [explanation] - (fn undoable-middleware - [handler] - (fn undoable-handler - [db event-vec] - (let [explanation (cond - (fn? explanation) (explanation db event-vec) - (string? explanation) explanation - (nil? explanation) "" - :else (error "re-frame: \"undoable\" middleware given a bad parameter. Got: " explanation))] - (store-now! explanation) - (handler db event-vec)))))) - - -(def enrich +#_(def undoable + "A Middleware factory which stores an undo checkpoint. + \"explanation\" can be either a string or a function. If it is a + function then must be: (db event-vec) -> string. + \"explanation\" can be nil. in which case \"\" is recorded. + " + ^{:re-frame-factory-name "undoable"} + (fn undoable + [explanation] + (fn undoable-middleware + [handler] + (fn undoable-handler + [db event-vec] + (let [explanation (cond + (fn? explanation) (explanation db event-vec) + (string? explanation) explanation + (nil? explanation) "" + :else (error "re-frame: \"undoable\" middleware given a bad parameter. Got: " explanation))] + (store-now! explanation) + (handler db event-vec)))))) + + +(defn enrich "Middleware factory which runs a given function \"f\" in the after position. \"f\" is (db v) -> db Unlike \"after\" which is about side effects, \"enrich\" expects f to process and alter @@ -164,7 +140,8 @@ \"f\" would need to be both adding and removing the duplicate warnings. By applying \"f\" in middleware, we keep the handlers simple and yet we ensure this important step is not missed." - ^{:re-frame-factory-name "enrich"} + [_frame-atom] + ;^{:re-frame-factory-name "enrich"} (fn enrich [f] (fn enrich-middleware @@ -174,14 +151,15 @@ (f (handler db v) v))))) -(def after +(defn after "Middleware factory which runs a function \"f\" in the \"after handler\" position presumably for side effects. \"f\" is given the new value of \"db\". It's return value is ignored. Examples: \"f\" can run schema validation. Or write current state to localstorage. etc. In effect, \"f\" is meant to sideeffect. It gets no chance to change db. See \"enrich\" (if you need that.)" - ^{:re-frame-factory-name "after"} + [_frame-atom] + ;^{:re-frame-factory-name "after"} (fn after [f] (fn after-middleware @@ -189,13 +167,13 @@ (fn after-handler [db v] (let [new-db (handler db v)] - (f new-db v) ;; call f for side effects + (f new-db v) ;; call f for side effects new-db))))) ;; EXPERIMENTAL -(def on-changes +(defn on-changes "Middleware factory which acts a bit like \"reaction\" (but it flows into db , rather than out) It observes N inputs (paths into db) and if any of them change (as a result of the handler being run) then it runs 'f' to compute a new value, which is @@ -215,19 +193,20 @@ - call 'f' with the values extracted from [:a] [:b] - assoc the return value from 'f' into the path [:c] " - ^{:re-frame-factory-name "on-changes"} + [_frame-atom] + ;^{:re-frame-factory-name "on-changes"} (fn on-changes [f out-path & in-paths] (fn on-changed-middleware [handler] (fn on-changed-handler [db v] - (let [ ;; run the handler, computing a new generation of db + (let [;; run the handler, computing a new generation of db new-db (handler db v) ;; work out if any "inputs" have changed - new-ins (map #(get-in new-db %) in-paths) - old-ins (map #(get-in db %) in-paths) + new-ins (map #(get-in new-db %) in-paths) + old-ins (map #(get-in db %) in-paths) changed-ins? (some false? (map identical? new-ins old-ins))] ;; if one of the inputs has changed, then run 'f' diff --git a/src/re_frame/router.cljs b/src/re_frame/router.cljs deleted file mode 100644 index b4f6186a3..000000000 --- a/src/re_frame/router.cljs +++ /dev/null @@ -1,93 +0,0 @@ -(ns re-frame.router - (:refer-clojure :exclude [flush]) - (:require-macros [cljs.core.async.macros :refer [go-loop go]]) - (:require [reagent.core :refer [flush]] - [re-frame.handlers :refer [handle]] - [re-frame.utils :refer [warn error]] - [cljs.core.async :refer [chan put! f meta :re-frame-factory-name)) + (factory-names-in + [v] + (remove nil? (map name-of-factory v)))] + (doseq [name (factory-names-in v)] + ((get-in frame [:loggers :error]) "re-frame: \"" name "\" used incorrectly. Must be used like this \"(" name " ...)\", whereas you just used \"" name "\".")))) + +(defn comp-middleware + "Given a vector of middleware, filter out any nils, and use \"comp\" to compose the elements. + v can have nested vectors, and will be flattened before \"comp\" is applied. + For convienience, if v is a function (assumed to be middleware already), just return it. + Filtering out nils allows us to create Middleware conditionally like this: + (comp-middleware [pure (when debug? debug)]) ;; that 'when' might leave a nil + " + [frame v] + (cond + (fn? v) v ;; assumed to be existing middleware + (seq? v) (let [v (remove nil? (flatten v))] + (report-middleware-factories frame v) + (apply comp v)) + :else ((get-in frame [:loggers :warn]) "re-frame: comp-middleware expects a vector, got: " v))) + +(defn register-base + "register a handler for an event. + This is low level and it is expected that \"re-frame.core/register-handler\" would + generally be used." + ([event-id handler-fn] + (swap! app-frame #(frame/register-event-handler % event-id handler-fn))) + + ([event-id middleware handler-fn] + (let [mid-ware (comp-middleware @app-frame middleware) ;; compose the middleware + midware+hfn (mid-ware handler-fn)] ;; wrap the handler in the middleware + (register-base event-id midware+hfn)))) + +(defn register-handler + ([event-id handler] + (register-base event-id handler)) + ([event-id middleware handler] + (register-base event-id middleware handler))) + +;; -- The Event Conveyor Belt -------------------------------------------------------------------- +;; +;; Moves events from "dispatch" to the router loop. +;; Using core.async means we can have the aysnc handling of events. +;; +(def ^:private event-chan (chan)) ;; TODO: set buffer size? + +(defn purge-chan + "read all pending events from the channel and drop them on the floor" + [] + #_(loop [] ;; TODO commented out until poll! is a part of the core.asyc API + (when (go (poll! event-chan)) ;; progress: https://github.com/clojure/core.async/commit/d8047c0b0ec13788c1092f579f03733ee635c493 + (recur)))) + +;; -- router loop --------------------------------------------------------------------------------- +;; +;; In a perpetual loop, read events from "event-chan", and call the right handler. +;; +;; Because handlers occupy the CPU, before each event is handled, hand +;; back control to the browser, via a (fn (atom {})) - - -(defn clear-handlers! - "Unregisters all subscription handlers" - [] - (reset! key->fn {})) - - -(defn register - "Registers a handler function for an id" - [key-v handler-fn] - (if (contains? @key->fn key-v) - (warn "re-frame: overwriting subscription-handler for: " key-v)) ;; allow it, but warn. - (swap! key->fn assoc key-v handler-fn)) - - -(defn subscribe - "Returns a reagent/reaction which observes a part of app-db" - [v] - (let [key-v (first-in-vector v) - handler-fn (get @key->fn key-v)] - (if (nil? handler-fn) - (error "re-frame: no subscription handler registered for: \"" key-v "\". Returning a nil subscription.") - (handler-fn app-db v)))) - diff --git a/src/re_frame/undo.cljs b/src/re_frame/undo.cljs deleted file mode 100644 index 32f1519d8..000000000 --- a/src/re_frame/undo.cljs +++ /dev/null @@ -1,173 +0,0 @@ -(ns re-frame.undo - (:require-macros [reagent.ratom :refer [reaction]]) - (:require - [reagent.core :as reagent] - [re-frame.utils :refer [warn]] - [re-frame.db :refer [app-db]] - [re-frame.handlers :as handlers] - [re-frame.subs :as subs])) - - -;; -- History ------------------------------------------------------------------------------------- -;; -;; -(def ^:private max-undos "Maximum number of undo states maintained" (atom 50)) -(defn set-max-undos! - [n] - (reset! max-undos n)) - - -(def ^:private undo-list "A list of history states" (reagent/atom [])) -(def ^:private redo-list "A list of future states, caused by undoing" (reagent/atom [])) - -;; -- Explanations ----------------------------------------------------------- -;; -;; Each undo has an associated explanation which can be displayed to the user. -;; -;; Seems really ugly to have mirrored vectors, but ... -;; the code kinda falls out when you do. I'm feeling lazy. -(def ^:private app-explain "Mirrors app-db" (reagent/atom "")) -(def ^:private undo-explain-list "Mirrors undo-list" (reagent/atom [])) -(def ^:private redo-explain-list "Mirrors redo-list" (reagent/atom [])) - -(defn- clear-undos! - [] - (reset! undo-list []) - (reset! undo-explain-list [])) - - -(defn- clear-redos! - [] - (reset! redo-list []) - (reset! redo-explain-list [])) - - -(defn clear-history! - [] - (clear-undos!) - (clear-redos!) - (reset! app-explain "")) - - -(defn store-now! - "Stores the value currently in app-db, so the user can later undo" - [explanation] - (clear-redos!) - (reset! undo-list (vec (take - @max-undos - (conj @undo-list @app-db)))) - (reset! undo-explain-list (vec (take - @max-undos - (conj @undo-explain-list @app-explain)))) - (reset! app-explain explanation)) - - -(defn undos? - "Returns true if undos exist, false otherwise" - [] - (pos? (count @undo-list))) - -(defn redos? - "Returns true if redos exist, false otherwise" - [] - (pos? (count @redo-list))) - -(defn undo-explanations - "Returns list of undo descriptions or empty list if no undos" - [] - (if (undos?) - (conj @undo-explain-list @app-explain) - [])) - -;; -- subscriptions ----------------------------------------------------------------------------- - -(subs/register - :undos? - (fn handler - ; "return true if anything is stored in the undo list, otherwise false" - [_ _] - (reaction (undos?)))) - -(subs/register - :redos? - (fn handler - ; "return true if anything is stored in the redo list, otherwise false" - [_ _] - (reaction (redos?)))) - - -(subs/register - :undo-explanations - (fn handler - ; "return a vector of string explanations ordered oldest to most recent" - [_ _] - (reaction (undo-explanations)))) - -(subs/register - :redo-explanations - (fn handler - ; "returns a vector of string explanations ordered from most recent undo onward" - [_ _] - (reaction (deref redo-explain-list)))) - -;; -- event handlers ---------------------------------------------------------------------------- - - -(defn- undo - [undos cur redos] - (let [u @undos - r (cons @cur @redos)] - (reset! cur (last u)) - (reset! redos r) - (reset! undos (pop u)))) - -(defn- undo-n - "undo n steps or until we run out of undos" - [n] - (when (and (pos? n) (undos?)) - (undo undo-list app-db redo-list) - (undo undo-explain-list app-explain redo-explain-list) - (recur (dec n)))) - -(handlers/register-base ;; not a pure handler - :undo ;; usage: (dispatch [:undo n]) n is optional, defaults to 1 - (fn handler - [_ [_ n]] - (if-not (undos?) - (warn "re-frame: you did a (dispatch [:undo]), but there is nothing to undo.") - (undo-n (or n 1))))) - - -(defn- redo - [undos cur redos] - (let [u (conj @undos @cur) - r @redos] - (reset! cur (first r)) - (reset! redos (rest r)) - (reset! undos u))) - -(defn- redo-n - "redo n steps or until we run out of redos" - [n] - (when (and (pos? n) (redos?)) - (redo undo-list app-db redo-list) - (redo undo-explain-list app-explain redo-explain-list) - (recur (dec n)))) - -(handlers/register-base ;; not a pure handler - :redo ;; usage: (dispatch [:redo n]) - (fn handler ;; if n absent, defaults to 1 - [_ [_ n]] - (if-not (redos?) - (warn "re-frame: you did a (dispatch [:redo]), but there is nothing to redo.") - (redo-n (or n 1))))) - - -(handlers/register-base ;; not a pure handler - :purge-redos ;; usage: (dispatch [:purge-redo]) - (fn handler - [_ _] - (if-not (redos?) - (warn "re-frame: you did a (dispatch [:purge-redos]), but there is nothing to redo.") - (clear-redos!)))) - diff --git a/src/re_frame/utils.cljs b/src/re_frame/utils.cljs index 9bf6f20fd..8f1c6219c 100644 --- a/src/re_frame/utils.cljs +++ b/src/re_frame/utils.cljs @@ -1,46 +1,13 @@ -(ns re-frame.utils - (:require - [clojure.set :refer [difference]])) +(ns re-frame.utils) - -;; -- Logging ----------------------------------------------------------------- -;; -;; re-frame internally uses a set of logging functions which, by default, -;; print to js/console. -;; Use set-loggers! if you want to change this default behaviour. -;; In production environment, you may want to capture exceptions and POST -;; them somewhere. to , you might want to override the way that exceptions are -;; handled by overridding "error" -;; -(def default-loggers - {:log #(.log js/console %) - :warn #(.warn js/console %) - :error #(.error js/console %) - :group #(if (.group js/console) (.group js/console %) (.log js/console %)) ;; group does not exist < IE 11 - :groupEnd #(when (.groupEnd js/console) (.groupEnd js/console))}) ;; groupEnd does not exist < IE 11 - -;; holds the current set of loggers. -(def loggers (atom default-loggers)) - -(defn set-loggers! - "Change the set (subset?) of logging functions used by re-frame. - 'new-loggers' should be a map which looks like default-loggers" - [new-loggers] - (assert (empty? (difference (set (keys new-loggers)) (set (keys default-loggers)))) "Unknown keys in new-loggers") - (swap! loggers merge new-loggers)) - - -(defn log [& args] ((:log @loggers) (apply str args))) -(defn warn [& args] ((:warn @loggers) (apply str args))) -(defn group [& args] ((:group @loggers) (apply str args))) -(defn groupEnd [& args] ((:groupEnd @loggers) (apply str args))) -(defn error [& args] ((:error @loggers) (apply str args))) - -;; -- Misc -------------------------------------------------------------------- - -(defn first-in-vector +(defn get-event-id [v] (if (vector? v) (first v) - (error "re-frame: expected a vector event, but got: " v))) + (throw (js/Error. (str "re-frame: expected a vector event, but got: " v))))) +(defn get-subscription-id + [v] + (if (vector? v) + (first v) + (throw (js/Error. (str "re-frame: expected a vector subscription, but got: " v))))) diff --git a/test/re-frame/middleware.cljs b/test/re-frame/middleware.cljs deleted file mode 100644 index 12f07abcb..000000000 --- a/test/re-frame/middleware.cljs +++ /dev/null @@ -1,41 +0,0 @@ -(ns re-frame.test.middleware - (:require-macros [cemerick.cljs.test :refer (is deftest)]) - (:require [cemerick.cljs.test :as t] - [reagent.ratom :refer [atom]] - [re-frame.middleware :as middleware])) - -(enable-console-print!) - -(deftest pure - (let [db (atom {:a true}) - handler (fn [db [_ key value]] - (assoc db key value))] - ((middleware/pure handler) db [nil :a false]) - (is (= (:a @db) false)))) - -(deftest trim-v - (let [handler (fn [db vect] vect)] - (is (= (handler nil [:a :b :c]) - [:a :b :c])) - (is (= ((middleware/trim-v handler) nil [:a :b :c]) - [:b :c])))) - -(deftest path - (let [db {:a true} - handler (fn - [a [_]] - (not a))] - (let [new-db (((middleware/path [:a]) - handler) db [nil])] - (is (= (:a new-db) - false))))) - - -(deftest on-changes - (let [set-a (fn [db v] (assoc db :a v)) ;; handler - mid-ware (middleware/on-changes + [:c] [:a] [:b]) ;; middleware - wrapped (mid-ware set-a)] ;; wrapped middleware - (is (= (wrapped {:a 0 :b 2} 0) ;; no change in 'a' - {:a 0 :b 2})) - (is (= (wrapped {:a 4 :b 2} 0) ;; 'a' changed to 0 - {:c 2 :a 0 :b 2})))) ;; 'c' is a + b diff --git a/test/re_frame/test/core.cljs b/test/re_frame/test/core.cljs new file mode 100644 index 000000000..ba5604e48 --- /dev/null +++ b/test/re_frame/test/core.cljs @@ -0,0 +1,34 @@ +(ns re-frame.test.core + (:require-macros [cemerick.cljs.test :refer (is deftest testing done)]) + (:require [cemerick.cljs.test :as t] + [re-frame.core :as core] + [re-frame.frame :as frame] + [re-frame.logging :as logging])) + +(defn reinitialize! [] + ; TODO: figure out, how to force channel flush + (reset! core/app-db nil) + (reset! core/app-frame (frame/frame-factory nil nil logging/default-loggers deref))) + +(deftest modify-app-db-sync + (testing "modify app-db via handler (sync)" + (reinitialize!) + (is (= @core/app-db nil)) + (core/register-handler :modify-app (fn [app-db [_ data]] + (assoc app-db :modify-app-handler-was-here data))) + (core/dispatch-sync [:modify-app "something"]) + (is (= @core/app-db {:modify-app-handler-was-here "something"})))) + +(deftest ^:async modify-app-db-async + (testing "modify app-db via handler (async)" + (reinitialize!) + (is (= @core/app-db nil)) + (core/register-handler :modify-app (fn [app-db [_ data]] + (assoc app-db :modify-app-handler-was-here data))) + (core/register-handler :check (fn [app-db] + (is (= app-db @core/app-db)) + (is (= app-db {:modify-app-handler-was-here "something"})) + (done) + app-db)) + (core/dispatch [:modify-app "something"]) + (core/dispatch [:check]))) diff --git a/test/re_frame/test/frame.cljs b/test/re_frame/test/frame.cljs new file mode 100644 index 000000000..d124f7ccd --- /dev/null +++ b/test/re_frame/test/frame.cljs @@ -0,0 +1,76 @@ +(ns re-frame.test.frame + (:require-macros [cemerick.cljs.test :refer (is deftest testing)]) + (:require [cemerick.cljs.test :as t] + [re-frame.frame :as frame])) + +(def log-transcript (atom {})) + +(defn record-log-call [what args] + (swap! log-transcript (fn [transcript] + (update transcript what + (fn [record] + (-> (or record {}) + (update :counter inc) + (update :history conj (vec args)))))))) + +(defn last-log* [what] + (last (get-in @log-transcript [what :history]))) + +(def last-log (partial last-log* :log)) +(def last-warn (partial last-log* :warn)) +(def last-error (partial last-log* :error)) + +(def recording-loggers + {:log (fn [& args] (record-log-call :log args)) + :warn (fn [& args] (record-log-call :warn args)) + :error (fn [& args] (record-log-call :error args)) + :group (fn [& args] (record-log-call :group args)) + :groupEnd (fn [& args] (record-log-call :groupEnd args))}) + +(defn reset-log-recorder! [] + (reset! log-transcript {})) + +(defn make-empty-test-frame [] + (frame/frame-factory {} {} recording-loggers identity)) + +(defn transduce-single-event [frame event] + (let [reducing-fn (fn + ([result] result) + ([_old-state new-state] new-state))] + (let [xform (frame/get-frame-transducer frame)] + (transduce xform reducing-fn nil [event])))) + +(deftest frame-error-handling + (testing "invalid subscription" + (reset-log-recorder!) + (let [frame (make-empty-test-frame)] + (is (thrown-with-msg? js/Error #"expected a vector subscription, but got:" (frame/subscribe frame :non-vector))))) + (testing "subscribing to non-existent subscription handler" + (reset-log-recorder!) + (let [frame (make-empty-test-frame)] + (is (= (last-error) nil)) + (frame/subscribe frame [:subscription-which-does-not-exist]) + (is (= (last-error) ["re-frame: no subscription handler registered for: \"" :subscription-which-does-not-exist "\". Returning a nil subscription."]))))) + +(deftest frame-warning-handling + (testing "overwriting subscription handler" + (reset-log-recorder!) + (let [frame (make-empty-test-frame) + frame-with-some-handler (frame/register-subscription-handler frame :some-handler identity)] + (is (= (last-warn) nil)) + (frame/register-subscription-handler frame-with-some-handler :some-handler (fn [])) + (is (= (last-warn) ["re-frame: overwriting subscription-handler for: " :some-handler])))) + (testing "overwriting event handler" + (reset-log-recorder!) + (let [frame (make-empty-test-frame) + frame-with-some-handler (frame/register-event-handler frame :some-handler identity)] + (is (= (last-warn) nil)) + (frame/register-event-handler frame-with-some-handler :some-handler (fn [])) + (is (= (last-warn) ["re-frame: overwriting an event-handler for: " :some-handler]))))) + +(deftest frame-transduction + (testing "simple transduce" + (let [my-handler (fn [_state [event-id & args]] (str "result" event-id args)) + frame (-> (make-empty-test-frame) + (frame/register-event-handler :my-handler my-handler))] + (is (= (transduce-single-event frame [:my-handler 1 2]) "result:my-handler(1 2)"))))) diff --git a/test/re_frame/test/middleware.cljs b/test/re_frame/test/middleware.cljs new file mode 100644 index 000000000..55a32c1dd --- /dev/null +++ b/test/re_frame/test/middleware.cljs @@ -0,0 +1,32 @@ +(ns re-frame.test.middleware + (:require-macros [cemerick.cljs.test :refer (is deftest)]) + (:require [cemerick.cljs.test :as t] + [reagent.ratom :refer [atom]] + [re-frame.core :as re-frame])) + +(deftest trim-v + (let [handler (fn [db vect] vect)] + (is (= (handler nil [:a :b :c]) + [:a :b :c])) + (is (= ((re-frame/trim-v handler) nil [:a :b :c]) + [:b :c])))) + +(deftest path + (let [db {:a true} + handler (fn + [a [_]] + (not a))] + (let [new-db (((re-frame/path [:a]) + handler) db [nil])] + (is (= (:a new-db) + false))))) + + +(deftest on-changes + (let [set-a (fn [db v] (assoc db :a v)) ;; handler + mid-ware (re-frame/on-changes + [:c] [:a] [:b]) ;; api + wrapped (mid-ware set-a)] ;; wrapped api + (is (= (wrapped {:a 0 :b 2} 0) ;; no change in 'a' + {:a 0 :b 2})) + (is (= (wrapped {:a 4 :b 2} 0) ;; 'a' changed to 0 + {:c 2 :a 0 :b 2})))) ;; 'c' is a + b From a54eaf1436f3278f87ac8cc202fd4878d2966833 Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Mon, 17 Aug 2015 00:51:00 +0200 Subject: [PATCH 02/44] forgot to remove extra apply --- src/re_frame/frame.cljs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/re_frame/frame.cljs b/src/re_frame/frame.cljs index 0412caffe..485178d8a 100644 --- a/src/re_frame/frame.cljs +++ b/src/re_frame/frame.cljs @@ -43,7 +43,7 @@ This transducer reads event-id and applies matching handler on input state." (if (nil? handler-fn) (let [error (get-in frame [:loggers :error])] (error "re-frame: no subscription handler registered for: \"" subscription-id "\". Returning a nil subscription.")) - (apply handler-fn subscription-spec)))) + (handler-fn subscription-spec)))) ; TODO: figure out what to do here, re-frame should not be aware of app-db here, this is for backward compatibility (defn legacy-subscribe From 200aef2ebf1375b59714de402ebfffaedcd0c387 Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Mon, 17 Aug 2015 00:59:24 +0200 Subject: [PATCH 03/44] remove debug logging --- src/re_frame/frame.cljs | 2 -- src/re_frame/scaffold.cljs | 1 - 2 files changed, 3 deletions(-) diff --git a/src/re_frame/frame.cljs b/src/re_frame/frame.cljs index 485178d8a..3f80e4c8e 100644 --- a/src/re_frame/frame.cljs +++ b/src/re_frame/frame.cljs @@ -37,7 +37,6 @@ This transducer reads event-id and applies matching handler on input state." (defn subscribe "Returns a reagent/reaction which observes state." [frame subscription-spec] - (.log js/console "in-subscribe" frame) (let [subscription-id (get-subscription-id subscription-spec) handler-fn (get-in frame [:subscriptions subscription-id])] (if (nil? handler-fn) @@ -49,7 +48,6 @@ This transducer reads event-id and applies matching handler on input state." (defn legacy-subscribe "Returns a reagent/reaction which observes state." [frame app-db subscription-spec] - (.log js/console "in-subscribe" frame) (let [subscription-id (get-subscription-id subscription-spec) handler-fn (get-in frame [:subscriptions subscription-id])] (if (nil? handler-fn) diff --git a/src/re_frame/scaffold.cljs b/src/re_frame/scaffold.cljs index b8946041d..634f0fbd4 100644 --- a/src/re_frame/scaffold.cljs +++ b/src/re_frame/scaffold.cljs @@ -26,7 +26,6 @@ (swap! app-frame #(frame/set-loggers % new-loggers))) (defn register-sub [subscription-id handler-fn] - (.log js/console "register-sub" subscription-id handler-fn) (swap! app-frame #(frame/register-subscription-handler % subscription-id handler-fn))) (defn clear-sub-handlers! [] From 6bf1aba81e7a14d711e4a9253cc79fcb5efaf1c7 Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Mon, 17 Aug 2015 10:06:36 +0200 Subject: [PATCH 04/44] fix broken project repo url --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 0b584814e..3de725ed3 100644 --- a/project.clj +++ b/project.clj @@ -1,6 +1,6 @@ (defproject re-frame "0.4.2-TRANSDUCERS" :description "A Clojurescript MVC-like Framework For Writing SPAs Using Reagent." - :url "https://github.com/Day8/re_frame.git" + :url "https://github.com/Day8/re-frame.git" :license {:name "MIT"} :dependencies [[org.clojure/clojure "1.7.0"] [org.clojure/clojurescript "1.7.107"] From 6e9eefdd2281d618fdd1f0c28fc2028d1918f47e Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Mon, 17 Aug 2015 10:07:48 +0200 Subject: [PATCH 05/44] bump project version to 0.5.0 --- project.clj | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/project.clj b/project.clj index 3de725ed3..cba38a54d 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject re-frame "0.4.2-TRANSDUCERS" +(defproject re-frame "0.5.0-TRANSDUCERS" :description "A Clojurescript MVC-like Framework For Writing SPAs Using Reagent." :url "https://github.com/Day8/re-frame.git" :license {:name "MIT"} @@ -12,8 +12,7 @@ :plugins [[lein-cljsbuild "1.0.5"] [com.cemerick/clojurescript.test "0.3.3"]]}} - :clean-targets [:target-path - "run/compiled"] + :clean-targets [:target-path "run/compiled"] :resource-paths ["run/resources"] :jvm-opts ["-Xmx1g" "-XX:+UseConcMarkSweepGC"] @@ -23,12 +22,9 @@ :cljsbuild {:builds [{:id "test" ;; currently bogus, there is no demo or tests :source-paths ["src" "test"] - :compiler {;:main re-frame.test-runner - :output-to "run/compiled/test.js" + :compiler {:output-to "run/compiled/test.js" :source-map "run/compiled/test.js.map" :output-dir "run/compiled/test" - ;:target :nodejs ;;; this target required for node, plus a *main* defined in the tests. - ;:hashbang false ;;; https://github.com/cemerick/clojurescript.test/issues/68#issuecomment-52981151 :optimizations :simple ;; https://github.com/cemerick/clojurescript.test/issues/68 :pretty-print true}}] From b7f78b82a3d5f1dd58a9efdf8f6651da634e729b Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Mon, 17 Aug 2015 10:11:32 +0200 Subject: [PATCH 06/44] minor refinements in tests --- test/re_frame/test/core.cljs | 2 +- test/re_frame/test/frame.cljs | 6 +++--- test/re_frame/test/middleware.cljs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/re_frame/test/core.cljs b/test/re_frame/test/core.cljs index ba5604e48..2527f97b4 100644 --- a/test/re_frame/test/core.cljs +++ b/test/re_frame/test/core.cljs @@ -1,6 +1,6 @@ (ns re-frame.test.core (:require-macros [cemerick.cljs.test :refer (is deftest testing done)]) - (:require [cemerick.cljs.test :as t] + (:require [cemerick.cljs.test] [re-frame.core :as core] [re-frame.frame :as frame] [re-frame.logging :as logging])) diff --git a/test/re_frame/test/frame.cljs b/test/re_frame/test/frame.cljs index d124f7ccd..0196bc402 100644 --- a/test/re_frame/test/frame.cljs +++ b/test/re_frame/test/frame.cljs @@ -1,6 +1,6 @@ (ns re-frame.test.frame (:require-macros [cemerick.cljs.test :refer (is deftest testing)]) - (:require [cemerick.cljs.test :as t] + (:require [cemerick.cljs.test] [re-frame.frame :as frame])) (def log-transcript (atom {})) @@ -33,7 +33,7 @@ (defn make-empty-test-frame [] (frame/frame-factory {} {} recording-loggers identity)) -(defn transduce-single-event [frame event] +(defn process-single-event [frame event] (let [reducing-fn (fn ([result] result) ([_old-state new-state] new-state))] @@ -73,4 +73,4 @@ (let [my-handler (fn [_state [event-id & args]] (str "result" event-id args)) frame (-> (make-empty-test-frame) (frame/register-event-handler :my-handler my-handler))] - (is (= (transduce-single-event frame [:my-handler 1 2]) "result:my-handler(1 2)"))))) + (is (= (process-single-event frame [:my-handler 1 2]) "result:my-handler(1 2)"))))) diff --git a/test/re_frame/test/middleware.cljs b/test/re_frame/test/middleware.cljs index 55a32c1dd..1062870b8 100644 --- a/test/re_frame/test/middleware.cljs +++ b/test/re_frame/test/middleware.cljs @@ -1,6 +1,6 @@ (ns re-frame.test.middleware (:require-macros [cemerick.cljs.test :refer (is deftest)]) - (:require [cemerick.cljs.test :as t] + (:require [cemerick.cljs.test] [reagent.ratom :refer [atom]] [re-frame.core :as re-frame])) From 0f4fcd61db86b5fdc24019d9794ed451b8adacdc Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Mon, 17 Aug 2015 10:30:18 +0200 Subject: [PATCH 07/44] bump todomvc deps --- examples/todomvc/project.clj | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/examples/todomvc/project.clj b/examples/todomvc/project.clj index 40cb29e7c..76b616d84 100644 --- a/examples/todomvc/project.clj +++ b/examples/todomvc/project.clj @@ -1,29 +1,29 @@ (defproject todomvc-re-frame "0.4.0" - :dependencies [[org.clojure/clojure "1.6.0"] - [org.clojure/clojurescript "0.0-3208"] + :dependencies [[org.clojure/clojure "1.7.0"] + [org.clojure/clojurescript "1.7.107"] [reagent "0.5.0"] [re-frame "0.4.1"] - [figwheel "0.2.6"] + [figwheel "0.3.7"] [secretary "1.2.3"] [prismatic/schema "0.4.3"]] :plugins [[lein-cljsbuild "1.0.5"] - [lein-figwheel "0.2.6"]] + [lein-figwheel "0.3.7"]] :hooks [leiningen.cljsbuild] - :profiles {:dev {:cljsbuild - {:builds {:client {:source-paths ["devsrc"] - :compiler {:main todomvc.dev - :asset-path "js" - :optimizations :none - :source-map true - :source-map-timestamp true}}}}} + :profiles {:dev {:cljsbuild + {:builds {:client {:source-paths ["devsrc"] + :compiler {:main todomvc.dev + :asset-path "js" + :optimizations :none + :source-map true + :source-map-timestamp true}}}}} :prod {:cljsbuild - {:builds {:client {:compiler {:optimizations :advanced - :elide-asserts true - :pretty-print false}}}}}} + {:builds {:client {:compiler {:optimizations :advanced + :elide-asserts true + :pretty-print false}}}}}} :figwheel {:server-port 3450 :repl false} From 5ee36defdcdafdc919118da9cab2d000d4bf472e Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Mon, 17 Aug 2015 10:37:04 +0200 Subject: [PATCH 08/44] add checkouts so that todomvc uses local sources --- examples/todomvc/checkouts/re-frame | 1 + examples/todomvc/project.clj | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 120000 examples/todomvc/checkouts/re-frame diff --git a/examples/todomvc/checkouts/re-frame b/examples/todomvc/checkouts/re-frame new file mode 120000 index 000000000..a8a4f8c21 --- /dev/null +++ b/examples/todomvc/checkouts/re-frame @@ -0,0 +1 @@ +../../.. \ No newline at end of file diff --git a/examples/todomvc/project.clj b/examples/todomvc/project.clj index 76b616d84..b795aeb97 100644 --- a/examples/todomvc/project.clj +++ b/examples/todomvc/project.clj @@ -31,6 +31,6 @@ :clean-targets ^{:protect false} ["resources/public/js"] - :cljsbuild {:builds {:client {:source-paths ["src"] + :cljsbuild {:builds {:client {:source-paths ["checkouts/re-frame/src" "src"] :compiler {:output-dir "resources/public/js" :output-to "resources/public/js/client.js"}}}}) From 329a57ff73fb92cfd3cf6a365d922c911bb4f71c Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Mon, 17 Aug 2015 11:17:50 +0200 Subject: [PATCH 09/44] fix comp-middleware https://github.com/darwin/re-frame/commit/9326a411e7a76a997aee7c9b6b6bd21d47086ae0 --- src/re_frame/scaffold.cljs | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/re_frame/scaffold.cljs b/src/re_frame/scaffold.cljs index 634f0fbd4..fd50227c5 100644 --- a/src/re_frame/scaffold.cljs +++ b/src/re_frame/scaffold.cljs @@ -67,13 +67,16 @@ Filtering out nils allows us to create Middleware conditionally like this: (comp-middleware [pure (when debug? debug)]) ;; that 'when' might leave a nil " - [frame v] - (cond - (fn? v) v ;; assumed to be existing middleware - (seq? v) (let [v (remove nil? (flatten v))] - (report-middleware-factories frame v) - (apply comp v)) - :else ((get-in frame [:loggers :warn]) "re-frame: comp-middleware expects a vector, got: " v))) + [frame what] + (let [spec (if (seqable? what) (seq what) what)] + (cond + (fn? spec) spec ;; assumed to be existing middleware + (seq? spec) (let [middlewares (remove nil? (flatten spec))] + (report-middleware-factories frame middlewares) + (apply comp middlewares)) + :else (do + ((get-in frame [:loggers :warn]) "re-frame: comp-middleware expects a vector, got: " what) + nil)))) (defn register-base "register a handler for an event. @@ -83,15 +86,16 @@ (swap! app-frame #(frame/register-event-handler % event-id handler-fn))) ([event-id middleware handler-fn] - (let [mid-ware (comp-middleware @app-frame middleware) ;; compose the middleware - midware+hfn (mid-ware handler-fn)] ;; wrap the handler in the middleware - (register-base event-id midware+hfn)))) + (if-let [mid-ware (comp-middleware @app-frame middleware)] ;; compose the middleware + (register-base event-id (mid-ware handler-fn))))) ;; wrap the handler in the middleware (defn register-handler ([event-id handler] (register-base event-id handler)) ([event-id middleware handler] - (register-base event-id middleware handler))) + (if middleware + (register-base event-id middleware handler) + (register-base event-id handler)))) ;; -- The Event Conveyor Belt -------------------------------------------------------------------- ;; From b62e3abece7a4fb1d1c96d1d5398e52047c9da8e Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Mon, 17 Aug 2015 11:19:17 +0200 Subject: [PATCH 10/44] integrate cljs-devtools into todomvc --- examples/todomvc/devsrc/todomvc/dev.cljs | 7 +++++-- examples/todomvc/project.clj | 3 ++- examples/todomvc/src/todomvc/core.cljs | 6 +++--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/examples/todomvc/devsrc/todomvc/dev.cljs b/examples/todomvc/devsrc/todomvc/dev.cljs index f1e54379f..b7878ad54 100644 --- a/examples/todomvc/devsrc/todomvc/dev.cljs +++ b/examples/todomvc/devsrc/todomvc/dev.cljs @@ -1,6 +1,9 @@ (ns todomvc.dev (:require [todomvc.core :as todomvc] - [figwheel.client :as fw])) + [figwheel.client :as fw] + [devtools.core :as devtools])) -(fw/start {:on-jsload todomvc/main +(devtools/install!) + +(fw/start {:on-jsload todomvc/main :websocket-url "ws://localhost:3450/figwheel-ws"}) diff --git a/examples/todomvc/project.clj b/examples/todomvc/project.clj index b795aeb97..00117e9e9 100644 --- a/examples/todomvc/project.clj +++ b/examples/todomvc/project.clj @@ -5,7 +5,8 @@ [re-frame "0.4.1"] [figwheel "0.3.7"] [secretary "1.2.3"] - [prismatic/schema "0.4.3"]] + [prismatic/schema "0.4.3"] + [binaryage/devtools "0.3.0"]] :plugins [[lein-cljsbuild "1.0.5"] [lein-figwheel "0.3.7"]] diff --git a/examples/todomvc/src/todomvc/core.cljs b/examples/todomvc/src/todomvc/core.cljs index 40af060c9..56752e3ac 100644 --- a/examples/todomvc/src/todomvc/core.cljs +++ b/examples/todomvc/src/todomvc/core.cljs @@ -1,7 +1,7 @@ (ns todomvc.core (:require-macros [secretary.core :refer [defroute]]) (:require [goog.events :as events] - [reagent.core :as reagent :refer [atom]] + [reagent.core :as reagent] [re-frame.core :refer [dispatch dispatch-sync]] [secretary.core :as secretary] [todomvc.handlers] @@ -21,7 +21,7 @@ (def history (doto (History.) (events/listen EventType.NAVIGATE - (fn [event] (secretary/dispatch! (.-token event)))) + (fn [event] (secretary/dispatch! (.-token event)))) (.setEnabled true))) @@ -31,4 +31,4 @@ [] (dispatch-sync [:initialise-db]) (reagent/render [todomvc.views/todo-app] - (.getElementById js/document "app"))) + (.getElementById js/document "app"))) From 072ac104acba818557741ae9e26d34cdf7dc52f3 Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Mon, 17 Aug 2015 11:22:45 +0200 Subject: [PATCH 11/44] integrate cljs-devtools into simpleexample --- examples/simple/devsrc/simpleexample/dev.cljs | 5 ++++- examples/simple/project.clj | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/examples/simple/devsrc/simpleexample/dev.cljs b/examples/simple/devsrc/simpleexample/dev.cljs index 543863a65..1e9d09063 100644 --- a/examples/simple/devsrc/simpleexample/dev.cljs +++ b/examples/simple/devsrc/simpleexample/dev.cljs @@ -1,6 +1,9 @@ (ns simpleexample.dev (:require [simpleexample.core :as example] - [figwheel.client :as fw])) + [figwheel.client :as fw] + [devtools.core :as devtools])) + +(devtools/install!) (fw/start {:on-jsload example/run :websocket-url "ws://localhost:3449/figwheel-ws"}) diff --git a/examples/simple/project.clj b/examples/simple/project.clj index 47bb77109..bdc87713d 100644 --- a/examples/simple/project.clj +++ b/examples/simple/project.clj @@ -3,7 +3,8 @@ [org.clojure/clojurescript "1.7.107"] [reagent "0.5.0"] [re-frame "0.4.1"] - [figwheel "0.3.7"]] + [figwheel "0.3.7"] + [binaryage/devtools "0.3.0"]] :plugins [[lein-cljsbuild "1.0.5"] [lein-figwheel "0.3.7"]] From bae4df88ff9f8ff98fb00334943e633280ca872a Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Mon, 17 Aug 2015 11:30:36 +0200 Subject: [PATCH 12/44] move legacy-subscribe to scaffold --- src/re_frame/frame.cljs | 14 ++------------ src/re_frame/scaffold.cljs | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/re_frame/frame.cljs b/src/re_frame/frame.cljs index 3f80e4c8e..c8dab8bee 100644 --- a/src/re_frame/frame.cljs +++ b/src/re_frame/frame.cljs @@ -41,20 +41,10 @@ This transducer reads event-id and applies matching handler on input state." handler-fn (get-in frame [:subscriptions subscription-id])] (if (nil? handler-fn) (let [error (get-in frame [:loggers :error])] - (error "re-frame: no subscription handler registered for: \"" subscription-id "\". Returning a nil subscription.")) + (error "re-frame: no subscription handler registered for: \"" subscription-id "\". Returning a nil subscription.") + nil) (handler-fn subscription-spec)))) -; TODO: figure out what to do here, re-frame should not be aware of app-db here, this is for backward compatibility -(defn legacy-subscribe - "Returns a reagent/reaction which observes state." - [frame app-db subscription-spec] - (let [subscription-id (get-subscription-id subscription-spec) - handler-fn (get-in frame [:subscriptions subscription-id])] - (if (nil? handler-fn) - (let [error (get-in frame [:loggers :error])] - (error "re-frame: no subscription handler registered for: \"" subscription-id "\". Returning a nil subscription.")) - (handler-fn app-db subscription-spec)))) - (defn register-subscription-handler "Registers a handler function for an id." [frame subscription-id handler-fn] diff --git a/src/re_frame/scaffold.cljs b/src/re_frame/scaffold.cljs index fd50227c5..f4058a139 100644 --- a/src/re_frame/scaffold.cljs +++ b/src/re_frame/scaffold.cljs @@ -5,7 +5,8 @@ [reagent.core :as reagent] [re-frame.frame :as frame] [re-frame.logging :as logging] - [re-frame.middleware :as middleware])) + [re-frame.middleware :as middleware] + [re-frame.utils :as utils])) ; scaffold's responsibility is to implement re-frame 0.4.1 functionality on top reusable re-frame parts ; @@ -31,8 +32,17 @@ (defn clear-sub-handlers! [] (swap! app-frame #(frame/clear-subscription-handlers %))) +(defn legacy-subscribe [frame app-db-atom subscription-spec] + (let [subscription-id (utils/get-subscription-id subscription-spec) + handler-fn (get-in frame [:subscriptions subscription-id])] + (if (nil? handler-fn) + (let [error (get-in frame [:loggers :error])] + (error "re-frame: no subscription handler registered for: \"" subscription-id "\". Returning a nil subscription.") + nil) + (handler-fn app-db-atom subscription-spec)))) + (defn subscribe [subscription-spec] - (frame/legacy-subscribe @app-frame app-db subscription-spec)) + (legacy-subscribe @app-frame app-db subscription-spec)) (defn clear-event-handlers! [] (swap! app-frame #(frame/clear-event-handlers %))) From bd4602f2eba6b3b70c0ea7ab8e00fabe7dd0cafb Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Mon, 17 Aug 2015 11:37:12 +0200 Subject: [PATCH 13/44] get-frame-transducer does the work directly --- src/re_frame/frame.cljs | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/src/re_frame/frame.cljs b/src/re_frame/frame.cljs index c8dab8bee..cf5df8e4d 100644 --- a/src/re_frame/frame.cljs +++ b/src/re_frame/frame.cljs @@ -3,23 +3,24 @@ ; re-frame meat reimplemented in terms of pure functions (with help of transducers) -(defn frame-xform +; see http://clojure.org/transducers +(defn get-frame-transducer "Returns a transducer: state, event -> state. This transducer reads event-id and applies matching handler on input state." - [handlers loggers db-selector] - (fn [rf] - (fn - ([] (rf)) - ([result] (rf result)) - ([state event] - (let [event-id (get-event-id event) - handler-fn (event-id handlers)] - (if (nil? handler-fn) - (let [error (:error loggers)] - (error "re-frame: no event handler registered for: \"" event-id "\". Ignoring.") - state) - (rf state (handler-fn (db-selector state) event)))))))) - + [frame] + (let [{:keys [handlers loggers db-selector]} frame] + (fn [rf] + (fn + ([] (rf)) + ([result] (rf result)) + ([state event] + (let [event-id (get-event-id event) + handler-fn (event-id handlers)] + (if (nil? handler-fn) + (let [error (:error loggers)] + (error "re-frame: no event handler registered for: \"" event-id "\". Ignoring.") + state) + (rf state (handler-fn (db-selector state) event))))))))) ; TODO: this should be implemented as deftype/defrecord in future (defn frame-factory @@ -30,10 +31,6 @@ This transducer reads event-id and applies matching handler on input state." :loggers loggers :db-selector db-selector}) -; see http://clojure.org/transducers -(defn get-frame-transducer [frame] - (frame-xform (:handlers frame) (:loggers frame) (:db-selector frame))) - (defn subscribe "Returns a reagent/reaction which observes state." [frame subscription-spec] From dc0981aaf4e99f9be85c17a9945b4084fdbb7b0b Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Mon, 17 Aug 2015 12:01:22 +0200 Subject: [PATCH 14/44] provide no-loggers for convenience --- src/re_frame/logging.cljs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/re_frame/logging.cljs b/src/re_frame/logging.cljs index 45476f07c..ebeb63678 100644 --- a/src/re_frame/logging.cljs +++ b/src/re_frame/logging.cljs @@ -1,5 +1,7 @@ (ns re-frame.logging) +(defn no-op [& _]) + ;; -- Logging ----------------------------------------------------------------- ;; ;; re-frame internally uses a set of logging functions which, by default, @@ -15,3 +17,11 @@ :error #(.error js/console %) :group #(if (.group js/console) (.group js/console %) (.log js/console %)) ;; group does not exist < IE 11 :groupEnd #(when (.groupEnd js/console) (.groupEnd js/console))}) ;; groupEnd does not exist < IE 11 + +(def no-loggers + {:log no-op + :warn no-op + :error no-op + :group no-op + :groupEnd no-op} +) From c05223fef8b9a90148eaa814b4670deb70e85dd8 Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Mon, 17 Aug 2015 12:07:50 +0200 Subject: [PATCH 15/44] default loggers should apply all args --- src/re_frame/logging.cljs | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/src/re_frame/logging.cljs b/src/re_frame/logging.cljs index ebeb63678..8c335e8aa 100644 --- a/src/re_frame/logging.cljs +++ b/src/re_frame/logging.cljs @@ -2,6 +2,24 @@ (defn no-op [& _]) +(defn js-console-log [& args] + (.apply (.-log js/console) js/console (into-array args))) + +(defn js-console-warn [& args] + (.apply (.-warn js/console) js/console (into-array args))) + +(defn js-console-error [& args] + (.apply (.-error js/console) js/console (into-array args))) + +(defn js-console-group [& args] + (if (.-group js/console) ;; group does not exist < IE 11 + (.apply (.-group js/console) js/console (into-array args)) + (apply js-console-log args))) + +(defn js-console-group-end [& args] + (if (.-groupEnd js/console) ;; groupEnd does not exist < IE 11 + (.apply (.-groupEnd js/console) js/console (into-array args)))) + ;; -- Logging ----------------------------------------------------------------- ;; ;; re-frame internally uses a set of logging functions which, by default, @@ -12,16 +30,15 @@ ;; handled by overridding "error" ;; (def default-loggers - {:log #(.log js/console %) - :warn #(.warn js/console %) - :error #(.error js/console %) - :group #(if (.group js/console) (.group js/console %) (.log js/console %)) ;; group does not exist < IE 11 - :groupEnd #(when (.groupEnd js/console) (.groupEnd js/console))}) ;; groupEnd does not exist < IE 11 + {:log js-console-log + :warn js-console-warn + :error js-console-error + :group js-console-group + :groupEnd js-console-group-end}) (def no-loggers - {:log no-op - :warn no-op - :error no-op - :group no-op - :groupEnd no-op} -) + {:log no-op + :warn no-op + :error no-op + :group no-op + :groupEnd no-op}) From 2778d5a03ba0422d707bb7afa68a053e86886e8f Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Mon, 17 Aug 2015 12:11:10 +0200 Subject: [PATCH 16/44] switch frame implementation to use defrecord --- src/re_frame/frame.cljs | 43 +++++++++++++++++++++++++++++------ src/re_frame/scaffold.cljs | 2 +- src/re_frame/utils.cljs | 3 +++ test/re_frame/test/core.cljs | 2 +- test/re_frame/test/frame.cljs | 2 +- 5 files changed, 42 insertions(+), 10 deletions(-) diff --git a/src/re_frame/frame.cljs b/src/re_frame/frame.cljs index cf5df8e4d..a67939942 100644 --- a/src/re_frame/frame.cljs +++ b/src/re_frame/frame.cljs @@ -1,5 +1,6 @@ (ns re-frame.frame - (:require [re-frame.utils :refer [get-event-id get-subscription-id]])) + (:require [re-frame.utils :refer [get-event-id get-subscription-id simple-inflection]] + [re-frame.logging :as logging])) ; re-frame meat reimplemented in terms of pure functions (with help of transducers) @@ -22,14 +23,42 @@ This transducer reads event-id and applies matching handler on input state." state) (rf state (handler-fn (db-selector state) event))))))))) -; TODO: this should be implemented as deftype/defrecord in future +(defn frame-summary-description [frame] + (let [handlers-count (count (:handlers frame)) + subscriptions-count (count (:subscriptions frame))] + (str + handlers-count " " (simple-inflection "handler" handlers-count) ", " + subscriptions-count " " (simple-inflection "subscription" subscriptions-count)))) + +(defprotocol IFrame) + +(defrecord Frame [handlers subscriptions db-selector loggers] + IFrame + + IHash + (-hash [this] (goog/getUid this)) + + IPrintWithWriter + (-pr-writer [this writer opts] + (-write writer (str "#"))) + (defn frame-factory "Constructs independent frame instance." - [handlers subscriptions loggers db-selector] - {:handlers handlers - :subscriptions subscriptions - :loggers loggers - :db-selector db-selector}) + ([] (frame-factory nil)) + ([handlers] (frame-factory handlers nil)) + ([handlers subscriptions] (frame-factory handlers subscriptions deref)) + ([handlers subscriptions db-selector] (frame-factory handlers subscriptions db-selector logging/default-loggers)) + ([handlers subscriptions db-selector loggers] + {:pre [(or (map? handlers) (nil? handlers)) + (or (map? subscriptions) (nil? subscriptions)) + (fn? db-selector) + (map? loggers)]} + (Frame. handlers subscriptions db-selector loggers))) (defn subscribe "Returns a reagent/reaction which observes state." diff --git a/src/re_frame/scaffold.cljs b/src/re_frame/scaffold.cljs index f4058a139..78df011dd 100644 --- a/src/re_frame/scaffold.cljs +++ b/src/re_frame/scaffold.cljs @@ -19,7 +19,7 @@ (def app-db (reagent/atom nil)) ; the default instance of re-frame -(def app-frame (atom (frame/frame-factory nil nil logging/default-loggers deref))) +(def app-frame (atom (frame/frame-factory))) ; methods bellow operate on app-db and provide backward-compatible interface as was present in re-frame 0.4.1 diff --git a/src/re_frame/utils.cljs b/src/re_frame/utils.cljs index 8f1c6219c..f380ccf4c 100644 --- a/src/re_frame/utils.cljs +++ b/src/re_frame/utils.cljs @@ -11,3 +11,6 @@ (if (vector? v) (first v) (throw (js/Error. (str "re-frame: expected a vector subscription, but got: " v))))) + +(defn simple-inflection [base n] + (if (= n 1) base (str base "s"))) diff --git a/test/re_frame/test/core.cljs b/test/re_frame/test/core.cljs index 2527f97b4..e476f7e8d 100644 --- a/test/re_frame/test/core.cljs +++ b/test/re_frame/test/core.cljs @@ -8,7 +8,7 @@ (defn reinitialize! [] ; TODO: figure out, how to force channel flush (reset! core/app-db nil) - (reset! core/app-frame (frame/frame-factory nil nil logging/default-loggers deref))) + (reset! core/app-frame (frame/frame-factory))) (deftest modify-app-db-sync (testing "modify app-db via handler (sync)" diff --git a/test/re_frame/test/frame.cljs b/test/re_frame/test/frame.cljs index 0196bc402..d8aeb8c8c 100644 --- a/test/re_frame/test/frame.cljs +++ b/test/re_frame/test/frame.cljs @@ -31,7 +31,7 @@ (reset! log-transcript {})) (defn make-empty-test-frame [] - (frame/frame-factory {} {} recording-loggers identity)) + (frame/frame-factory nil nil identity recording-loggers)) (defn process-single-event [frame event] (let [reducing-fn (fn From 3d72b79c4a26ba9db33424ab14a568277714e26a Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Mon, 17 Aug 2015 12:22:06 +0200 Subject: [PATCH 17/44] enable figwheel nrepl in examples --- .gitignore | 1 + examples/simple/project.clj | 3 ++- examples/todomvc/project.clj | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index c13bd356f..e3c5fbb8e 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ compiled/ misc/ /examples/todomvc/resources/public/js/ /examples/simple/resources/public/js/ +figwheel_server.log diff --git a/examples/simple/project.clj b/examples/simple/project.clj index bdc87713d..ab842ca39 100644 --- a/examples/simple/project.clj +++ b/examples/simple/project.clj @@ -24,7 +24,8 @@ :elide-asserts true :pretty-print false}}}}}} - :figwheel {:repl false} + :figwheel {:server-port 3440 + :nrepl-port 3540} :clean-targets ^{:protect false} ["resources/public/js"] diff --git a/examples/todomvc/project.clj b/examples/todomvc/project.clj index 00117e9e9..277cdce84 100644 --- a/examples/todomvc/project.clj +++ b/examples/todomvc/project.clj @@ -27,7 +27,7 @@ :pretty-print false}}}}}} :figwheel {:server-port 3450 - :repl false} + :nrepl-port 3550} :clean-targets ^{:protect false} ["resources/public/js"] From addfb80f74a702ff1b8c793abfa49d874f379aee Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Mon, 17 Aug 2015 12:27:35 +0200 Subject: [PATCH 18/44] rename frame-factory to make-frame --- src/re_frame/frame.cljs | 10 +++++----- src/re_frame/scaffold.cljs | 3 +-- test/re_frame/test/core.cljs | 2 +- test/re_frame/test/frame.cljs | 2 +- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/re_frame/frame.cljs b/src/re_frame/frame.cljs index a67939942..852e518e1 100644 --- a/src/re_frame/frame.cljs +++ b/src/re_frame/frame.cljs @@ -47,12 +47,12 @@ This transducer reads event-id and applies matching handler on input state." (pr-writer (:subscriptions this) writer opts) (-write writer ">"))) -(defn frame-factory +(defn make-frame "Constructs independent frame instance." - ([] (frame-factory nil)) - ([handlers] (frame-factory handlers nil)) - ([handlers subscriptions] (frame-factory handlers subscriptions deref)) - ([handlers subscriptions db-selector] (frame-factory handlers subscriptions db-selector logging/default-loggers)) + ([] (make-frame nil)) + ([handlers] (make-frame handlers nil)) + ([handlers subscriptions] (make-frame handlers subscriptions deref)) + ([handlers subscriptions db-selector] (make-frame handlers subscriptions db-selector logging/default-loggers)) ([handlers subscriptions db-selector loggers] {:pre [(or (map? handlers) (nil? handlers)) (or (map? subscriptions) (nil? subscriptions)) diff --git a/src/re_frame/scaffold.cljs b/src/re_frame/scaffold.cljs index 78df011dd..dbdd3ab50 100644 --- a/src/re_frame/scaffold.cljs +++ b/src/re_frame/scaffold.cljs @@ -4,7 +4,6 @@ (:require [cljs.core.async :refer [chan put! Date: Mon, 17 Aug 2015 13:22:31 +0200 Subject: [PATCH 19/44] minor refinements in core tests --- test/re_frame/test/core.cljs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/test/re_frame/test/core.cljs b/test/re_frame/test/core.cljs index a6c51d9bf..61f7f8fa0 100644 --- a/test/re_frame/test/core.cljs +++ b/test/re_frame/test/core.cljs @@ -2,8 +2,7 @@ (:require-macros [cemerick.cljs.test :refer (is deftest testing done)]) (:require [cemerick.cljs.test] [re-frame.core :as core] - [re-frame.frame :as frame] - [re-frame.logging :as logging])) + [re-frame.frame :as frame])) (defn reinitialize! [] ; TODO: figure out, how to force channel flush @@ -14,8 +13,8 @@ (testing "modify app-db via handler (sync)" (reinitialize!) (is (= @core/app-db nil)) - (core/register-handler :modify-app (fn [app-db [_ data]] - (assoc app-db :modify-app-handler-was-here data))) + (core/register-handler :modify-app (fn [db [_ data]] + (assoc db :modify-app-handler-was-here data))) (core/dispatch-sync [:modify-app "something"]) (is (= @core/app-db {:modify-app-handler-was-here "something"})))) @@ -23,12 +22,12 @@ (testing "modify app-db via handler (async)" (reinitialize!) (is (= @core/app-db nil)) - (core/register-handler :modify-app (fn [app-db [_ data]] - (assoc app-db :modify-app-handler-was-here data))) - (core/register-handler :check (fn [app-db] - (is (= app-db @core/app-db)) - (is (= app-db {:modify-app-handler-was-here "something"})) + (core/register-handler :modify-app (fn [db [_ data]] + (assoc db :modify-app-handler-was-here data))) + (core/register-handler :check (fn [db] + (is (= db @core/app-db)) + (is (= db {:modify-app-handler-was-here "something"})) (done) - app-db)) + db)) (core/dispatch [:modify-app "something"]) (core/dispatch [:check]))) From 7a8156dfff40a032ccb111ffedb8c700aa62bc11 Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Mon, 17 Aug 2015 13:22:38 +0200 Subject: [PATCH 20/44] properly override protocol on defrecord thanks @nberger compiler was complaining and didn't overwrite existing protocol: WARNING: Protocol IPrintWithWriter implemented multiple times at line 35 src/re_frame/frame.cljs --- src/re_frame/frame.cljs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/re_frame/frame.cljs b/src/re_frame/frame.cljs index 852e518e1..6c60b60ab 100644 --- a/src/re_frame/frame.cljs +++ b/src/re_frame/frame.cljs @@ -33,18 +33,16 @@ This transducer reads event-id and applies matching handler on input state." (defprotocol IFrame) (defrecord Frame [handlers subscriptions db-selector loggers] - IFrame + IFrame) - IHash - (-hash [this] (goog/getUid this)) - - IPrintWithWriter +(extend-protocol IPrintWithWriter + Frame (-pr-writer [this writer opts] - (-write writer (str "#"))) (defn make-frame From 568cdd30dafb63c8f98c6d115833935bde07d927 Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Mon, 17 Aug 2015 13:50:44 +0200 Subject: [PATCH 21/44] move "handler returned nil" check into transducer --- src/re_frame/frame.cljs | 12 ++++++++---- src/re_frame/scaffold.cljs | 8 +++----- test/re_frame/test/frame.cljs | 12 ++++++++++-- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/re_frame/frame.cljs b/src/re_frame/frame.cljs index 6c60b60ab..04204ddb0 100644 --- a/src/re_frame/frame.cljs +++ b/src/re_frame/frame.cljs @@ -10,10 +10,10 @@ This transducer reads event-id and applies matching handler on input state." [frame] (let [{:keys [handlers loggers db-selector]} frame] - (fn [rf] + (fn [reducing-fn] (fn - ([] (rf)) - ([result] (rf result)) + ([] (reducing-fn)) + ([result] (reducing-fn result)) ([state event] (let [event-id (get-event-id event) handler-fn (event-id handlers)] @@ -21,7 +21,11 @@ This transducer reads event-id and applies matching handler on input state." (let [error (:error loggers)] (error "re-frame: no event handler registered for: \"" event-id "\". Ignoring.") state) - (rf state (handler-fn (db-selector state) event))))))))) + (if-let [new-db (handler-fn (db-selector state) event)] + (reducing-fn state new-db) + (let [error (:error loggers)] + (error "re-frame: your handler returned nil. It should return the new db state. Ignoring.") + state))))))))) (defn frame-summary-description [frame] (let [handlers-count (count (:handlers frame)) diff --git a/src/re_frame/scaffold.cljs b/src/re_frame/scaffold.cljs index dbdd3ab50..6b4f2b728 100644 --- a/src/re_frame/scaffold.cljs +++ b/src/re_frame/scaffold.cljs @@ -138,11 +138,9 @@ (let [reducing-fn (fn ([db-atom] db-atom) ; completion ([db-atom new-state] ; apply new-state to atom - (if (nil? new-state) - ((get-in frame-atom [:loggers :error]) "re-frame: your pure handler returned nil. It should return the new db state.") - (let [old-state @db-atom] - (if-not (identical? old-state new-state) - (reset! db-atom new-state)))) + (let [old-state @db-atom] + (if-not (identical? old-state new-state) + (reset! db-atom new-state))) db-atom))] (let [xform (frame/get-frame-transducer @frame-atom)] (transduce xform reducing-fn db-atom events)))) diff --git a/test/re_frame/test/frame.cljs b/test/re_frame/test/frame.cljs index 796f6598f..2e0e99486 100644 --- a/test/re_frame/test/frame.cljs +++ b/test/re_frame/test/frame.cljs @@ -41,7 +41,7 @@ (transduce xform reducing-fn nil [event])))) (deftest frame-error-handling - (testing "invalid subscription" + (testing "doing invalid subscription" (reset-log-recorder!) (let [frame (make-empty-test-frame)] (is (thrown-with-msg? js/Error #"expected a vector subscription, but got:" (frame/subscribe frame :non-vector))))) @@ -50,7 +50,15 @@ (let [frame (make-empty-test-frame)] (is (= (last-error) nil)) (frame/subscribe frame [:subscription-which-does-not-exist]) - (is (= (last-error) ["re-frame: no subscription handler registered for: \"" :subscription-which-does-not-exist "\". Returning a nil subscription."]))))) + (is (= (last-error) ["re-frame: no subscription handler registered for: \"" :subscription-which-does-not-exist "\". Returning a nil subscription."])))) + (testing "calling handler which returns nil" + (reset-log-recorder!) + (let [my-handler (fn [_state _] nil) + frame (-> (make-empty-test-frame) + (frame/register-event-handler :my-handler my-handler))] + (is (= (last-error) nil)) + (is (= (process-single-event frame [:my-handler]) nil)) + (is (= (last-error) ["re-frame: your handler returned nil. It should return the new db state. Ignoring."]))))) (deftest frame-warning-handling (testing "overwriting subscription handler" From 751ccfbacf4c68e19958a434e319f557ef786401 Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Mon, 17 Aug 2015 13:57:25 +0200 Subject: [PATCH 22/44] add test "calling a handler which does not exist" --- test/re_frame/test/frame.cljs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/test/re_frame/test/frame.cljs b/test/re_frame/test/frame.cljs index 2e0e99486..494d6d665 100644 --- a/test/re_frame/test/frame.cljs +++ b/test/re_frame/test/frame.cljs @@ -45,20 +45,26 @@ (reset-log-recorder!) (let [frame (make-empty-test-frame)] (is (thrown-with-msg? js/Error #"expected a vector subscription, but got:" (frame/subscribe frame :non-vector))))) - (testing "subscribing to non-existent subscription handler" + (testing "subscribing to a non-existent subscription handler" (reset-log-recorder!) (let [frame (make-empty-test-frame)] (is (= (last-error) nil)) (frame/subscribe frame [:subscription-which-does-not-exist]) (is (= (last-error) ["re-frame: no subscription handler registered for: \"" :subscription-which-does-not-exist "\". Returning a nil subscription."])))) - (testing "calling handler which returns nil" + (testing "calling a handler which returns nil" (reset-log-recorder!) (let [my-handler (fn [_state _] nil) frame (-> (make-empty-test-frame) (frame/register-event-handler :my-handler my-handler))] (is (= (last-error) nil)) (is (= (process-single-event frame [:my-handler]) nil)) - (is (= (last-error) ["re-frame: your handler returned nil. It should return the new db state. Ignoring."]))))) + (is (= (last-error) ["re-frame: your handler returned nil. It should return the new db state. Ignoring."])))) + (testing "calling a handler which does not exist" + (reset-log-recorder!) + (let [frame (make-empty-test-frame)] + (is (= (last-error) nil)) + (is (= (process-single-event frame [:non-existing-handler]) nil)) + (is (= (last-error) ["re-frame: no event handler registered for: \"" :non-existing-handler "\". Ignoring."]))))) (deftest frame-warning-handling (testing "overwriting subscription handler" From a56a6e7aeb5470ebd995eb0b3ab0c14a11fc79e5 Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Mon, 17 Aug 2015 14:19:12 +0200 Subject: [PATCH 23/44] give our transducer some love --- src/re_frame/frame.cljs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/re_frame/frame.cljs b/src/re_frame/frame.cljs index 04204ddb0..68cba255f 100644 --- a/src/re_frame/frame.cljs +++ b/src/re_frame/frame.cljs @@ -4,16 +4,19 @@ ; re-frame meat reimplemented in terms of pure functions (with help of transducers) -; see http://clojure.org/transducers +; see http://clojure.org/transducers[1] (defn get-frame-transducer "Returns a transducer: state, event -> state. -This transducer reads event-id and applies matching handler on input state." +This transducer resolves event-id, selects matching handler and calls it with old db state to produce a new db state. +Tranducer must have no knowledge of underlying app-db-atom, reagent, core.async or anything else out there. +All business of queuing events and application of their effects must be baked into reducing-fn and provided from outside +by the process doing actual transduction. See scaffold's transduce-by-resetting-atom for an example." [frame] (let [{:keys [handlers loggers db-selector]} frame] (fn [reducing-fn] (fn - ([] (reducing-fn)) - ([result] (reducing-fn result)) + ([] (reducing-fn)) ; transduction initialization, see [1] + ([result] (reducing-fn result)) ; transduction completion, see [1] ([state event] (let [event-id (get-event-id event) handler-fn (event-id handlers)] @@ -21,11 +24,14 @@ This transducer reads event-id and applies matching handler on input state." (let [error (:error loggers)] (error "re-frame: no event handler registered for: \"" event-id "\". Ignoring.") state) - (if-let [new-db (handler-fn (db-selector state) event)] - (reducing-fn state new-db) - (let [error (:error loggers)] - (error "re-frame: your handler returned nil. It should return the new db state. Ignoring.") - state))))))))) + (let [old-db (db-selector state) ; db-selector is responsible for retrieving actual db from current state + new-db (handler-fn old-db event)] ; calls selected handler (including all composed middlewares) + (if (nil? new-db) ; TODO: this test should be optional, there could be valid use-cases for nil db + (let [error (:error loggers)] + (error "re-frame: your handler returned nil. It should return the new db state. Ignoring.") + state) + (reducing-fn state new-db)))))))))) ; reducing function prepares new transduction state + (defn frame-summary-description [frame] (let [handlers-count (count (:handlers frame)) From 173e8248a818801b1e9ee2b1b6a15e8f13d1c650 Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Mon, 17 Aug 2015 14:21:57 +0200 Subject: [PATCH 24/44] move frame-summary-description to utils --- src/re_frame/frame.cljs | 10 +--------- src/re_frame/utils.cljs | 7 +++++++ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/re_frame/frame.cljs b/src/re_frame/frame.cljs index 68cba255f..945720e8c 100644 --- a/src/re_frame/frame.cljs +++ b/src/re_frame/frame.cljs @@ -1,5 +1,5 @@ (ns re-frame.frame - (:require [re-frame.utils :refer [get-event-id get-subscription-id simple-inflection]] + (:require [re-frame.utils :refer [get-event-id get-subscription-id simple-inflection frame-summary-description]] [re-frame.logging :as logging])) ; re-frame meat reimplemented in terms of pure functions (with help of transducers) @@ -32,14 +32,6 @@ by the process doing actual transduction. See scaffold's transduce-by-resetting- state) (reducing-fn state new-db)))))))))) ; reducing function prepares new transduction state - -(defn frame-summary-description [frame] - (let [handlers-count (count (:handlers frame)) - subscriptions-count (count (:subscriptions frame))] - (str - handlers-count " " (simple-inflection "handler" handlers-count) ", " - subscriptions-count " " (simple-inflection "subscription" subscriptions-count)))) - (defprotocol IFrame) (defrecord Frame [handlers subscriptions db-selector loggers] diff --git a/src/re_frame/utils.cljs b/src/re_frame/utils.cljs index f380ccf4c..aa1e6e19f 100644 --- a/src/re_frame/utils.cljs +++ b/src/re_frame/utils.cljs @@ -14,3 +14,10 @@ (defn simple-inflection [base n] (if (= n 1) base (str base "s"))) + +(defn frame-summary-description [frame] + (let [handlers-count (count (:handlers frame)) + subscriptions-count (count (:subscriptions frame))] + (str + handlers-count " " (simple-inflection "handler" handlers-count) ", " + subscriptions-count " " (simple-inflection "subscription" subscriptions-count)))) From a52d3600fcbdee9a87ce5f72a901167390baa545 Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Mon, 17 Aug 2015 15:03:50 +0200 Subject: [PATCH 25/44] add possibility to unregister subscription/event handlers --- src/re_frame/core.cljs | 2 ++ src/re_frame/frame.cljs | 34 ++++++++++++++++++++++++++-------- src/re_frame/scaffold.cljs | 6 ++++++ test/re_frame/test/frame.cljs | 30 +++++++++++++++++++++++++----- 4 files changed, 59 insertions(+), 13 deletions(-) diff --git a/src/re_frame/core.cljs b/src/re_frame/core.cljs index f022ad2fb..bd17fc008 100644 --- a/src/re_frame/core.cljs +++ b/src/re_frame/core.cljs @@ -13,12 +13,14 @@ (def router-loop scaffold/router-loop) (def set-loggers! scaffold/set-loggers!) (def register-sub scaffold/register-sub) +(def unregister-sub scaffold/unregister-sub) (def clear-sub-handlers! scaffold/clear-sub-handlers!) (def subscribe scaffold/subscribe) (def clear-event-handlers! scaffold/clear-event-handlers!) (def dispatch scaffold/dispatch) (def dispatch-sync scaffold/dispatch-sync) (def register-handler scaffold/register-handler) +(def unregister-handler scaffold/unregister-handler) ;(def pure scaffold/pure) (def debug scaffold/debug) diff --git a/src/re_frame/frame.cljs b/src/re_frame/frame.cljs index 945720e8c..e4b854593 100644 --- a/src/re_frame/frame.cljs +++ b/src/re_frame/frame.cljs @@ -15,9 +15,9 @@ by the process doing actual transduction. See scaffold's transduce-by-resetting- (let [{:keys [handlers loggers db-selector]} frame] (fn [reducing-fn] (fn - ([] (reducing-fn)) ; transduction initialization, see [1] + ([] (reducing-fn)) ; transduction init, see [1] ([result] (reducing-fn result)) ; transduction completion, see [1] - ([state event] + ([state event] ; transduction step, see [1] (let [event-id (get-event-id event) handler-fn (event-id handlers)] (if (nil? handler-fn) @@ -48,7 +48,7 @@ by the process doing actual transduction. See scaffold's transduce-by-resetting- (-write writer ">"))) (defn make-frame - "Constructs independent frame instance." + "Constructs an independent frame instance." ([] (make-frame nil)) ([handlers] (make-frame handlers nil)) ([handlers subscriptions] (make-frame handlers subscriptions deref)) @@ -72,18 +72,27 @@ by the process doing actual transduction. See scaffold's transduce-by-resetting- (handler-fn subscription-spec)))) (defn register-subscription-handler - "Registers a handler function for an id." + "Registers a subscription handler function for an id." [frame subscription-id handler-fn] (let [existing-subscriptions (get frame :subscriptions)] (if (contains? existing-subscriptions subscription-id) (let [warn (get-in frame [:loggers :warn])] - (warn "re-frame: overwriting subscription-handler for: " subscription-id)))) + (warn "re-frame: overwriting subscription handler for: " subscription-id)))) (assoc-in frame [:subscriptions subscription-id] handler-fn)) +(defn unregister-subscription-handler + "Unregisters subscription handler function previously registered via register-subscription-handler." + [frame subscription-id] + (let [existing-subscriptions (get frame :subscriptions)] + (if-not (contains? existing-subscriptions subscription-id) + (let [warn (get-in frame [:loggers :warn])] + (warn "re-frame: unregistering subscription handler \"" subscription-id "\" which does not exist.")))) + (update frame :subscriptions dissoc subscription-id)) + (defn clear-subscription-handlers "Unregisters all subscription handlers." [frame] - (assoc frame :subscriptions {})) + (assoc frame :subscriptions nil)) (defn register-event-handler "Register a handler for an event." @@ -91,13 +100,22 @@ by the process doing actual transduction. See scaffold's transduce-by-resetting- (let [existing-handlers (get frame :handlers)] (if (contains? existing-handlers event-id) (let [warn (get-in frame [:loggers :warn])] - (warn "re-frame: overwriting an event-handler for: " event-id))) + (warn "re-frame: overwriting an event handler for: " event-id))) (assoc-in frame [:handlers event-id] handler-fn)))) +(defn unregister-event-handler + "Unregisters event handler function previously registered via register-event-handler." + [frame event-id] + (let [existing-handlers (get frame :handlers)] + (if (contains? existing-handlers event-id) + (let [warn (get-in frame [:loggers :warn])] + (warn "re-frame: unregistering event handler \"" event-id "\" which does not exist.")))) + (update frame :handlers dissoc event-id)) + (defn clear-event-handlers "Unregisters all event handlers." [frame] - (assoc frame :handlers {})) + (assoc frame :handlers nil)) (defn set-loggers "Resets loggers." diff --git a/src/re_frame/scaffold.cljs b/src/re_frame/scaffold.cljs index 6b4f2b728..97a62d19e 100644 --- a/src/re_frame/scaffold.cljs +++ b/src/re_frame/scaffold.cljs @@ -28,6 +28,9 @@ (defn register-sub [subscription-id handler-fn] (swap! app-frame #(frame/register-subscription-handler % subscription-id handler-fn))) +(defn unregister-sub [subscription-id] + (swap! app-frame #(frame/unregister-subscription-handler % subscription-id))) + (defn clear-sub-handlers! [] (swap! app-frame #(frame/clear-subscription-handlers %))) @@ -106,6 +109,9 @@ (register-base event-id middleware handler) (register-base event-id handler)))) +(defn unregister-handler [event-id] + (swap! app-frame #(frame/unregister-event-handler % event-id))) + ;; -- The Event Conveyor Belt -------------------------------------------------------------------- ;; ;; Moves events from "dispatch" to the router loop. diff --git a/test/re_frame/test/frame.cljs b/test/re_frame/test/frame.cljs index 494d6d665..3c45616fa 100644 --- a/test/re_frame/test/frame.cljs +++ b/test/re_frame/test/frame.cljs @@ -38,9 +38,9 @@ ([result] result) ([_old-state new-state] new-state))] (let [xform (frame/get-frame-transducer frame)] - (transduce xform reducing-fn nil [event])))) + ((xform reducing-fn) nil event)))) -(deftest frame-error-handling +(deftest frame-errors (testing "doing invalid subscription" (reset-log-recorder!) (let [frame (make-empty-test-frame)] @@ -66,21 +66,41 @@ (is (= (process-single-event frame [:non-existing-handler]) nil)) (is (= (last-error) ["re-frame: no event handler registered for: \"" :non-existing-handler "\". Ignoring."]))))) -(deftest frame-warning-handling +(deftest frame-warnings (testing "overwriting subscription handler" (reset-log-recorder!) (let [frame (make-empty-test-frame) frame-with-some-handler (frame/register-subscription-handler frame :some-handler identity)] (is (= (last-warn) nil)) (frame/register-subscription-handler frame-with-some-handler :some-handler (fn [])) - (is (= (last-warn) ["re-frame: overwriting subscription-handler for: " :some-handler])))) + (is (= (last-warn) ["re-frame: overwriting subscription handler for: " :some-handler])))) (testing "overwriting event handler" (reset-log-recorder!) (let [frame (make-empty-test-frame) frame-with-some-handler (frame/register-event-handler frame :some-handler identity)] (is (= (last-warn) nil)) (frame/register-event-handler frame-with-some-handler :some-handler (fn [])) - (is (= (last-warn) ["re-frame: overwriting an event-handler for: " :some-handler]))))) + (is (= (last-warn) ["re-frame: overwriting an event handler for: " :some-handler])))) + (testing "unregistering subscription handler which does not exist" + (reset-log-recorder!) + (let [frame (make-empty-test-frame)] + (is (= (last-warn) nil)) + (frame/unregister-subscription-handler frame :non-existing-handler) + (is (= (last-warn) ["re-frame: unregistering subscription handler \"" :non-existing-handler "\" which does not exist."]))))) + +(deftest frame-event-handlers + (testing "unregister event handler" + (let [frame (-> (make-empty-test-frame) + (frame/register-event-handler :some-handler identity))] + (is (= (get-in frame [:handlers :some-handler]) identity)) + (is (= (get-in (frame/unregister-event-handler frame :some-handler) [:handlers :some-handler] ::not-found) ::not-found))))) + +(deftest frame-subscription-handlers + (testing "unregister subscription handler" + (let [frame (-> (make-empty-test-frame) + (frame/register-subscription-handler :some-handler identity))] + (is (= (get-in frame [:subscriptions :some-handler]) identity)) + (is (= (get-in (frame/unregister-subscription-handler frame :some-handler) [:subscriptions :some-handler] ::not-found) ::not-found))))) (deftest frame-transduction (testing "simple transduce" From 4137a0d69bcd5cf6d6f8af82049981435ca83cc3 Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Mon, 17 Aug 2015 15:11:02 +0200 Subject: [PATCH 26/44] in case of processing only one event we can use transducer directly --- src/re_frame/frame.cljs | 2 +- src/re_frame/scaffold.cljs | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/re_frame/frame.cljs b/src/re_frame/frame.cljs index e4b854593..a6b65927e 100644 --- a/src/re_frame/frame.cljs +++ b/src/re_frame/frame.cljs @@ -10,7 +10,7 @@ This transducer resolves event-id, selects matching handler and calls it with old db state to produce a new db state. Tranducer must have no knowledge of underlying app-db-atom, reagent, core.async or anything else out there. All business of queuing events and application of their effects must be baked into reducing-fn and provided from outside -by the process doing actual transduction. See scaffold's transduce-by-resetting-atom for an example." +by the process doing actual transduction. See scaffold's transduce-events-by-resetting-atom for an example." [frame] (let [{:keys [handlers loggers db-selector]} frame] (fn [reducing-fn] diff --git a/src/re_frame/scaffold.cljs b/src/re_frame/scaffold.cljs index 97a62d19e..f6d443e34 100644 --- a/src/re_frame/scaffold.cljs +++ b/src/re_frame/scaffold.cljs @@ -140,7 +140,8 @@ ;; (dispatch ^:flush-dom [:event-id other params]) ;; -(defn transduce-by-resetting-atom [frame-atom db-atom events] +; this is just a sample implementation, for reducing single event we use single-step transduce-event-by-resetting-atom +(defn transduce-events-by-resetting-atom [frame-atom db-atom events] (let [reducing-fn (fn ([db-atom] db-atom) ; completion ([db-atom new-state] ; apply new-state to atom @@ -151,6 +152,15 @@ (let [xform (frame/get-frame-transducer @frame-atom)] (transduce xform reducing-fn db-atom events)))) +(defn transduce-event-by-resetting-atom [frame-atom db-atom event] + (let [reducing-fn (fn [db-atom new-state] ; apply new-state to atom + (let [old-state @db-atom] + (if-not (identical? old-state new-state) + (reset! db-atom new-state))) + db-atom)] + (let [xform (frame/get-frame-transducer @frame-atom)] + ((xform reducing-fn) db-atom event)))) + (defn router-loop* [db-atom frame-atom] (go-loop [] (let [event ( Date: Mon, 17 Aug 2015 15:32:20 +0200 Subject: [PATCH 27/44] better logging helpers --- src/re_frame/frame.cljs | 28 ++++++++++++---------------- src/re_frame/logging.cljs | 7 +++++++ src/re_frame/middleware.cljs | 23 ++++++++++------------- src/re_frame/scaffold.cljs | 11 ++++++----- 4 files changed, 35 insertions(+), 34 deletions(-) diff --git a/src/re_frame/frame.cljs b/src/re_frame/frame.cljs index a6b65927e..88a5a5721 100644 --- a/src/re_frame/frame.cljs +++ b/src/re_frame/frame.cljs @@ -1,6 +1,6 @@ (ns re-frame.frame (:require [re-frame.utils :refer [get-event-id get-subscription-id simple-inflection frame-summary-description]] - [re-frame.logging :as logging])) + [re-frame.logging :as logging :refer [log warn error]])) ; re-frame meat reimplemented in terms of pure functions (with help of transducers) @@ -12,7 +12,7 @@ Tranducer must have no knowledge of underlying app-db-atom, reagent, core.async All business of queuing events and application of their effects must be baked into reducing-fn and provided from outside by the process doing actual transduction. See scaffold's transduce-events-by-resetting-atom for an example." [frame] - (let [{:keys [handlers loggers db-selector]} frame] + (let [{:keys [handlers db-selector]} frame] (fn [reducing-fn] (fn ([] (reducing-fn)) ; transduction init, see [1] @@ -21,14 +21,14 @@ by the process doing actual transduction. See scaffold's transduce-events-by-res (let [event-id (get-event-id event) handler-fn (event-id handlers)] (if (nil? handler-fn) - (let [error (:error loggers)] - (error "re-frame: no event handler registered for: \"" event-id "\". Ignoring.") + (do + (error frame "re-frame: no event handler registered for: \"" event-id "\". Ignoring.") state) (let [old-db (db-selector state) ; db-selector is responsible for retrieving actual db from current state new-db (handler-fn old-db event)] ; calls selected handler (including all composed middlewares) (if (nil? new-db) ; TODO: this test should be optional, there could be valid use-cases for nil db - (let [error (:error loggers)] - (error "re-frame: your handler returned nil. It should return the new db state. Ignoring.") + (do + (error frame "re-frame: your handler returned nil. It should return the new db state. Ignoring.") state) (reducing-fn state new-db)))))))))) ; reducing function prepares new transduction state @@ -66,8 +66,8 @@ by the process doing actual transduction. See scaffold's transduce-events-by-res (let [subscription-id (get-subscription-id subscription-spec) handler-fn (get-in frame [:subscriptions subscription-id])] (if (nil? handler-fn) - (let [error (get-in frame [:loggers :error])] - (error "re-frame: no subscription handler registered for: \"" subscription-id "\". Returning a nil subscription.") + (do + (error frame "re-frame: no subscription handler registered for: \"" subscription-id "\". Returning a nil subscription.") nil) (handler-fn subscription-spec)))) @@ -76,8 +76,7 @@ by the process doing actual transduction. See scaffold's transduce-events-by-res [frame subscription-id handler-fn] (let [existing-subscriptions (get frame :subscriptions)] (if (contains? existing-subscriptions subscription-id) - (let [warn (get-in frame [:loggers :warn])] - (warn "re-frame: overwriting subscription handler for: " subscription-id)))) + (warn frame "re-frame: overwriting subscription handler for: " subscription-id))) (assoc-in frame [:subscriptions subscription-id] handler-fn)) (defn unregister-subscription-handler @@ -85,8 +84,7 @@ by the process doing actual transduction. See scaffold's transduce-events-by-res [frame subscription-id] (let [existing-subscriptions (get frame :subscriptions)] (if-not (contains? existing-subscriptions subscription-id) - (let [warn (get-in frame [:loggers :warn])] - (warn "re-frame: unregistering subscription handler \"" subscription-id "\" which does not exist.")))) + (warn frame "re-frame: unregistering subscription handler \"" subscription-id "\" which does not exist."))) (update frame :subscriptions dissoc subscription-id)) (defn clear-subscription-handlers @@ -99,8 +97,7 @@ by the process doing actual transduction. See scaffold's transduce-events-by-res ([frame event-id handler-fn] (let [existing-handlers (get frame :handlers)] (if (contains? existing-handlers event-id) - (let [warn (get-in frame [:loggers :warn])] - (warn "re-frame: overwriting an event handler for: " event-id))) + (warn frame "re-frame: overwriting an event handler for: " event-id)) (assoc-in frame [:handlers event-id] handler-fn)))) (defn unregister-event-handler @@ -108,8 +105,7 @@ by the process doing actual transduction. See scaffold's transduce-events-by-res [frame event-id] (let [existing-handlers (get frame :handlers)] (if (contains? existing-handlers event-id) - (let [warn (get-in frame [:loggers :warn])] - (warn "re-frame: unregistering event handler \"" event-id "\" which does not exist.")))) + (warn frame "re-frame: unregistering event handler \"" event-id "\" which does not exist."))) (update frame :handlers dissoc event-id)) (defn clear-event-handlers diff --git a/src/re_frame/logging.cljs b/src/re_frame/logging.cljs index 8c335e8aa..91c224765 100644 --- a/src/re_frame/logging.cljs +++ b/src/re_frame/logging.cljs @@ -1,5 +1,12 @@ (ns re-frame.logging) +; logging helpers +(defn log [frame & args] (apply (:log (:loggers frame)) args)) +(defn warn [frame & args] (apply (:warn (:loggers frame)) args)) +(defn error [frame & args] (apply (:error (:loggers frame)) args)) +(defn group [frame & args] (apply (:group (:loggers frame)) args)) +(defn group-end [frame & args] (apply (:groupEnd (:loggers frame)) args)) + (defn no-op [& _]) (defn js-console-log [& args] diff --git a/src/re_frame/middleware.cljs b/src/re_frame/middleware.cljs index 866bd5dc2..a9db14d86 100644 --- a/src/re_frame/middleware.cljs +++ b/src/re_frame/middleware.cljs @@ -1,5 +1,6 @@ (ns re-frame.middleware - (:require [clojure.data :as data])) + (:require [clojure.data :as data] + [re-frame.logging :refer [log warn error group group-end]])) ;; See docs in the Wiki: https://github.com/Day8/re-frame/wiki @@ -17,7 +18,7 @@ (fn [handler] (fn log-ex-handler [db v] - ((get-in @frame-atom [:loggers :warn]) "re-frame: use of \"log-ex\" is deprecated. You don't need it any more IF YOU ARE USING CHROME 44. Chrome now seems to now produce good stack traces.") + (warn @frame-atom "re-frame: use of \"log-ex\" is deprecated. You don't need it any more IF YOU ARE USING CHROME 44. Chrome now seems to now produce good stack traces.") (try (handler db v) (catch :default e ;; ooops, handler threw @@ -33,18 +34,14 @@ [frame-atom] (fn [handler] (fn debug-handler [db v] - (let [frame @frame-atom - loggers (get frame :loggers) - log (get loggers :log) - group (get loggers :group) - groupEnd (get loggers :groupEnd)] - (log "-- New Event ----------------------------------------------------") - (group "re-frame event: " v) + (let [frame @frame-atom] + (log frame "-- New Event ----------------------------------------------------") + (group frame "re-frame event: " v) (let [new-db (handler db v) diff (data/diff db new-db)] - (log "only before: " (first diff)) - (log "only after : " (second diff)) - (groupEnd) + (log frame "only before: " (first diff)) + (log frame "only after : " (second diff)) + (group-end frame) new-db))))) @@ -92,7 +89,7 @@ [& args] (let [path (flatten args)] (when (empty? path) - ((get-in @frame-atom [:loggers :error]) "re-frame: \"path\" middleware given no params.")) + (error @frame-atom "re-frame: \"path\" middleware given no params.")) (fn path-middleware [handler] (fn path-handler diff --git a/src/re_frame/scaffold.cljs b/src/re_frame/scaffold.cljs index f6d443e34..9b47bdff4 100644 --- a/src/re_frame/scaffold.cljs +++ b/src/re_frame/scaffold.cljs @@ -5,6 +5,7 @@ [reagent.core :as reagent] [re-frame.frame :as frame] [re-frame.middleware :as middleware] + [re-frame.logging :refer [log warn error]] [re-frame.utils :as utils])) ; scaffold's responsibility is to implement re-frame 0.4.1 functionality on top reusable re-frame parts @@ -38,8 +39,8 @@ (let [subscription-id (utils/get-subscription-id subscription-spec) handler-fn (get-in frame [:subscriptions subscription-id])] (if (nil? handler-fn) - (let [error (get-in frame [:loggers :error])] - (error "re-frame: no subscription handler registered for: \"" subscription-id "\". Returning a nil subscription.") + (do + (error frame "re-frame: no subscription handler registered for: \"" subscription-id "\". Returning a nil subscription.") nil) (handler-fn app-db-atom subscription-spec)))) @@ -70,7 +71,7 @@ [v] (remove nil? (map name-of-factory v)))] (doseq [name (factory-names-in v)] - ((get-in frame [:loggers :error]) "re-frame: \"" name "\" used incorrectly. Must be used like this \"(" name " ...)\", whereas you just used \"" name "\".")))) + (error frame "re-frame: \"" name "\" used incorrectly. Must be used like this \"(" name " ...)\", whereas you just used \"" name "\".")))) (defn comp-middleware "Given a vector of middleware, filter out any nils, and use \"comp\" to compose the elements. @@ -87,7 +88,7 @@ (report-middleware-factories frame middlewares) (apply comp middlewares)) :else (do - ((get-in frame [:loggers :warn]) "re-frame: comp-middleware expects a vector, got: " what) + (warn frame "re-frame: comp-middleware expects a vector, got: " what) nil)))) (defn register-base @@ -199,7 +200,7 @@ " [frame-atom event-v] (if (nil? event-v) - ((get-in @frame-atom [:loggers :error]) "re-frame: \"dispatch\" is ignoring a nil event.") ;; nil would close the channel + (error @frame-atom "re-frame: \"dispatch\" is ignoring a nil event.") ;; nil would close the channel (put! event-chan event-v)) nil) ;; Ensure nil return. See https://github.com/Day8/re-frame/wiki/Beware-Returning-False From 049285b80723c0aa3b94086d9fb5fa73df26f4b8 Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Mon, 17 Aug 2015 15:41:29 +0200 Subject: [PATCH 28/44] return pure handler for compatibility reasons but issue a warning when used --- src/re_frame/core.cljs | 2 +- src/re_frame/legacy.cljs | 33 +++++++++++++++++++++++++++++++++ src/re_frame/scaffold.cljs | 2 ++ 3 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 src/re_frame/legacy.cljs diff --git a/src/re_frame/core.cljs b/src/re_frame/core.cljs index bd17fc008..4e6449140 100644 --- a/src/re_frame/core.cljs +++ b/src/re_frame/core.cljs @@ -22,7 +22,7 @@ (def register-handler scaffold/register-handler) (def unregister-handler scaffold/unregister-handler) -;(def pure scaffold/pure) +(def pure scaffold/pure) (def debug scaffold/debug) ;(def undoable scaffold/undoable) (def path scaffold/path) diff --git a/src/re_frame/legacy.cljs b/src/re_frame/legacy.cljs new file mode 100644 index 000000000..c297a0250 --- /dev/null +++ b/src/re_frame/legacy.cljs @@ -0,0 +1,33 @@ +(ns re-frame.legacy + (:require [reagent.ratom :refer [IReactiveAtom]] + [re-frame.logging :refer [log warn error group group-end]])) + +;; See docs in the Wiki: https://github.com/Day8/re-frame/wiki + +; this middleware is included for backward compatibility, it is not used anymore +(defn pure + "Acts as an adaptor, allowing handlers to be writen as pure functions. + The re-frame router passes the `app-db` atom as the first parameter to any handler. + This middleware adapts that atom to be the value within the atom. + If you strip away the error/efficiency checks, this middleware is doing: + (reset! app-db (handler @app-db event-vec)) + You don't have to use this middleware directly. It is automatically applied to + your handler's middleware when you use \"register-handler\". + In fact, the only way to by-pass automatic use of \"pure\" in your middleware + is to use the low level registration function \"re-frame.handlers/register-handler-base\"" + [frame-atom] + (fn [handler] + (fn pure-handler [app-db event-vec] + (warn @frame-atom "re-frame: pure handler should not be used anymore.") + (if (not (satisfies? IReactiveAtom app-db)) + (do + (if (map? app-db) + (warn @frame-atom "re-frame: Looks like \"pure\" is in the middleware pipeline twice. Ignoring.") + (warn @frame-atom "re-frame: \"pure\" middleware not given a Ratom. Got: " app-db)) + handler) ;; turn this into a noop handler + (let [db @app-db + new-db (handler db event-vec)] + (if (nil? new-db) + (error @frame-atom "re-frame: your pure handler returned nil. It should return the new db state.") + (if-not (identical? db new-db) + (reset! app-db new-db)))))))) diff --git a/src/re_frame/scaffold.cljs b/src/re_frame/scaffold.cljs index 9b47bdff4..c15903adb 100644 --- a/src/re_frame/scaffold.cljs +++ b/src/re_frame/scaffold.cljs @@ -4,6 +4,7 @@ (:require [cljs.core.async :refer [chan put! Date: Mon, 17 Aug 2015 15:50:25 +0200 Subject: [PATCH 29/44] fix a typo thanks @thenonameguy --- src/re_frame/frame.cljs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/re_frame/frame.cljs b/src/re_frame/frame.cljs index 88a5a5721..6e0df3e0a 100644 --- a/src/re_frame/frame.cljs +++ b/src/re_frame/frame.cljs @@ -8,7 +8,7 @@ (defn get-frame-transducer "Returns a transducer: state, event -> state. This transducer resolves event-id, selects matching handler and calls it with old db state to produce a new db state. -Tranducer must have no knowledge of underlying app-db-atom, reagent, core.async or anything else out there. +Transducer must have no knowledge of underlying app-db-atom, reagent, core.async or anything else out there. All business of queuing events and application of their effects must be baked into reducing-fn and provided from outside by the process doing actual transduction. See scaffold's transduce-events-by-resetting-atom for an example." [frame] From 9b318895b66749b577ed3e8e6d8e94e4f0743a0e Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Mon, 17 Aug 2015 16:02:55 +0200 Subject: [PATCH 30/44] DRY logger helpers as suggested by @thenonameguy --- src/re_frame/logging.cljs | 16 +++++++++++----- test/re_frame/test/logging.cljs | 10 ++++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 test/re_frame/test/logging.cljs diff --git a/src/re_frame/logging.cljs b/src/re_frame/logging.cljs index 91c224765..aa0b6afaa 100644 --- a/src/re_frame/logging.cljs +++ b/src/re_frame/logging.cljs @@ -1,11 +1,17 @@ (ns re-frame.logging) +(defn make-logger-for-key [logger-key] + (fn [frame & args] + (if-let [logger-fn (get-in frame [:loggers logger-key])] + (apply logger-fn args) + (throw (js/Error. (str "re-frame: missing logger \"" logger-key "\"")))))) + ; logging helpers -(defn log [frame & args] (apply (:log (:loggers frame)) args)) -(defn warn [frame & args] (apply (:warn (:loggers frame)) args)) -(defn error [frame & args] (apply (:error (:loggers frame)) args)) -(defn group [frame & args] (apply (:group (:loggers frame)) args)) -(defn group-end [frame & args] (apply (:groupEnd (:loggers frame)) args)) +(def log (make-logger-for-key :log)) +(def warn (make-logger-for-key :warn)) +(def error (make-logger-for-key :error)) +(def group (make-logger-for-key :group)) +(def group-end (make-logger-for-key :groupEnd)) (defn no-op [& _]) diff --git a/test/re_frame/test/logging.cljs b/test/re_frame/test/logging.cljs new file mode 100644 index 000000000..c760d46af --- /dev/null +++ b/test/re_frame/test/logging.cljs @@ -0,0 +1,10 @@ +(ns re-frame.test.logging + (:require-macros [cemerick.cljs.test :refer (is deftest testing)]) + (:require [cemerick.cljs.test] + [re-frame.frame :as frame] + [re-frame.logging :as logging])) + +(deftest frame-logging + (testing "supplying incomplete logger" + (let [frame (frame/make-frame nil nil identity {})] + (is (thrown-with-msg? js/Error #"re-frame: missing logger \":log\"" (logging/log frame "log this!")))))) From 5f4be3c9b1a3ee9e46aa7b15ee4799ebe5b99893 Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Mon, 17 Aug 2015 19:28:27 +0200 Subject: [PATCH 31/44] use defonce to make scaffold figwheel reloadable --- src/re_frame/scaffold.cljs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/re_frame/scaffold.cljs b/src/re_frame/scaffold.cljs index c15903adb..7a9990fb4 100644 --- a/src/re_frame/scaffold.cljs +++ b/src/re_frame/scaffold.cljs @@ -17,10 +17,10 @@ ; * router event queue is implemented using core.async channel ; the default instance of app-db implemented as ratom -(def app-db (reagent/atom nil)) +(defonce app-db (reagent/atom nil)) ; the default instance of re-frame -(def app-frame (atom (frame/make-frame))) +(defonce app-frame (atom (frame/make-frame))) ; methods bellow operate on app-db and provide backward-compatible interface as was present in re-frame 0.4.1 From 4a7873db0d0d14d2a05d63fb01efd5a48208bddd Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Mon, 17 Aug 2015 20:04:10 +0200 Subject: [PATCH 32/44] various tweaks to increase reusability of scaffold code lessons learned from plastic --- src/re_frame/frame.cljs | 4 +-- src/re_frame/scaffold.cljs | 60 ++++++++------------------------------ src/re_frame/utils.cljs | 35 +++++++++++++++++++++- 3 files changed, 48 insertions(+), 51 deletions(-) diff --git a/src/re_frame/frame.cljs b/src/re_frame/frame.cljs index 6e0df3e0a..371a1fd91 100644 --- a/src/re_frame/frame.cljs +++ b/src/re_frame/frame.cljs @@ -1,6 +1,6 @@ (ns re-frame.frame (:require [re-frame.utils :refer [get-event-id get-subscription-id simple-inflection frame-summary-description]] - [re-frame.logging :as logging :refer [log warn error]])) + [re-frame.logging :refer [log warn error default-loggers]])) ; re-frame meat reimplemented in terms of pure functions (with help of transducers) @@ -52,7 +52,7 @@ by the process doing actual transduction. See scaffold's transduce-events-by-res ([] (make-frame nil)) ([handlers] (make-frame handlers nil)) ([handlers subscriptions] (make-frame handlers subscriptions deref)) - ([handlers subscriptions db-selector] (make-frame handlers subscriptions db-selector logging/default-loggers)) + ([handlers subscriptions db-selector] (make-frame handlers subscriptions db-selector default-loggers)) ([handlers subscriptions db-selector loggers] {:pre [(or (map? handlers) (nil? handlers)) (or (map? subscriptions) (nil? subscriptions)) diff --git a/src/re_frame/scaffold.cljs b/src/re_frame/scaffold.cljs index 7a9990fb4..9310a6a2c 100644 --- a/src/re_frame/scaffold.cljs +++ b/src/re_frame/scaffold.cljs @@ -36,17 +36,17 @@ (defn clear-sub-handlers! [] (swap! app-frame #(frame/clear-subscription-handlers %))) -(defn legacy-subscribe [frame app-db-atom subscription-spec] +(defn legacy-subscribe [frame-atom app-db-atom subscription-spec] (let [subscription-id (utils/get-subscription-id subscription-spec) - handler-fn (get-in frame [:subscriptions subscription-id])] + handler-fn (get-in @frame-atom [:subscriptions subscription-id])] (if (nil? handler-fn) (do - (error frame "re-frame: no subscription handler registered for: \"" subscription-id "\". Returning a nil subscription.") + (error @frame-atom "re-frame: no subscription handler registered for: \"" subscription-id "\". Returning a nil subscription.") nil) (handler-fn app-db-atom subscription-spec)))) (defn subscribe [subscription-spec] - (legacy-subscribe @app-frame app-db subscription-spec)) + (legacy-subscribe app-frame app-db subscription-spec)) (defn clear-event-handlers! [] (swap! app-frame #(frame/clear-event-handlers %))) @@ -63,54 +63,18 @@ ;; -- composing middleware ----------------------------------------------------------------------- -(defn report-middleware-factories - "See https://github.com/Day8/re-frame/issues/65" - [frame v] - (letfn [(name-of-factory - [f] - (-> f meta :re-frame-factory-name)) - (factory-names-in - [v] - (remove nil? (map name-of-factory v)))] - (doseq [name (factory-names-in v)] - (error frame "re-frame: \"" name "\" used incorrectly. Must be used like this \"(" name " ...)\", whereas you just used \"" name "\".")))) - -(defn comp-middleware - "Given a vector of middleware, filter out any nils, and use \"comp\" to compose the elements. - v can have nested vectors, and will be flattened before \"comp\" is applied. - For convienience, if v is a function (assumed to be middleware already), just return it. - Filtering out nils allows us to create Middleware conditionally like this: - (comp-middleware [pure (when debug? debug)]) ;; that 'when' might leave a nil - " - [frame what] - (let [spec (if (seqable? what) (seq what) what)] - (cond - (fn? spec) spec ;; assumed to be existing middleware - (seq? spec) (let [middlewares (remove nil? (flatten spec))] - (report-middleware-factories frame middlewares) - (apply comp middlewares)) - :else (do - (warn frame "re-frame: comp-middleware expects a vector, got: " what) - nil)))) - (defn register-base "register a handler for an event. This is low level and it is expected that \"re-frame.core/register-handler\" would generally be used." - ([event-id handler-fn] - (swap! app-frame #(frame/register-event-handler % event-id handler-fn))) - - ([event-id middleware handler-fn] - (if-let [mid-ware (comp-middleware @app-frame middleware)] ;; compose the middleware - (register-base event-id (mid-ware handler-fn))))) ;; wrap the handler in the middleware - -(defn register-handler - ([event-id handler] - (register-base event-id handler)) - ([event-id middleware handler] - (if middleware - (register-base event-id middleware handler) - (register-base event-id handler)))) + ([app-frame-atom event-id handler-fn] + (swap! app-frame-atom #(frame/register-event-handler % event-id handler-fn))) + + ([app-frame-atom event-id middleware handler-fn] + (if-let [mid-ware (utils/compose-middleware @app-frame middleware)] ;; compose the middleware + (register-base app-frame-atom event-id (mid-ware handler-fn))))) ;; wrap the handler in the middleware + +(def register-handler (partial register-base app-frame)) (defn unregister-handler [event-id] (swap! app-frame #(frame/unregister-event-handler % event-id))) diff --git a/src/re_frame/utils.cljs b/src/re_frame/utils.cljs index aa1e6e19f..3c57349d4 100644 --- a/src/re_frame/utils.cljs +++ b/src/re_frame/utils.cljs @@ -1,4 +1,5 @@ -(ns re-frame.utils) +(ns re-frame.utils + (:require [re-frame.logging :refer [log warn error]])) (defn get-event-id [v] @@ -21,3 +22,35 @@ (str handlers-count " " (simple-inflection "handler" handlers-count) ", " subscriptions-count " " (simple-inflection "subscription" subscriptions-count)))) + +;; -- composing middleware ----------------------------------------------------------------------- + +(defn report-middleware-factories + "See https://github.com/Day8/re-frame/issues/65" + [frame v] + (letfn [(name-of-factory + [f] + (-> f meta :re-frame-factory-name)) + (factory-names-in + [v] + (remove nil? (map name-of-factory v)))] + (doseq [name (factory-names-in v)] + (error frame "re-frame: \"" name "\" used incorrectly. Must be used like this \"(" name " ...)\", whereas you just used \"" name "\".")))) + +(defn compose-middleware + "Given a vector of middleware, filter out any nils, and use \"comp\" to compose the elements. + v can have nested vectors, and will be flattened before \"comp\" is applied. + For convienience, if v is a function (assumed to be middleware already), just return it. + Filtering out nils allows us to create Middleware conditionally like this: + (comp-middleware [pure (when debug? debug)]) ;; that 'when' might leave a nil + " + [frame what] + (let [spec (if (seqable? what) (seq what) what)] + (cond + (fn? spec) spec ;; assumed to be existing middleware + (seq? spec) (let [middlewares (remove nil? (flatten spec))] + (report-middleware-factories frame middlewares) + (apply comp middlewares)) + :else (do + (warn frame "re-frame: comp-middleware expects a vector, got: " what) + nil)))) From f393cc4cf8c102592c5b038d62bdf39fa1104717 Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Mon, 17 Aug 2015 20:35:09 +0200 Subject: [PATCH 33/44] move transduce-event(s)-by-resetting-atom to utils and give it a better name --- src/re_frame/frame.cljs | 2 +- src/re_frame/scaffold.cljs | 25 ++----------------------- src/re_frame/utils.cljs | 21 +++++++++++++++++++++ 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/re_frame/frame.cljs b/src/re_frame/frame.cljs index 371a1fd91..08ac0c81e 100644 --- a/src/re_frame/frame.cljs +++ b/src/re_frame/frame.cljs @@ -10,7 +10,7 @@ This transducer resolves event-id, selects matching handler and calls it with old db state to produce a new db state. Transducer must have no knowledge of underlying app-db-atom, reagent, core.async or anything else out there. All business of queuing events and application of their effects must be baked into reducing-fn and provided from outside -by the process doing actual transduction. See scaffold's transduce-events-by-resetting-atom for an example." +by the process doing actual transduction. See utils/handle-events-and-apply-results-to-atom for an example." [frame] (let [{:keys [handlers db-selector]} frame] (fn [reducing-fn] diff --git a/src/re_frame/scaffold.cljs b/src/re_frame/scaffold.cljs index 9310a6a2c..fc72598aa 100644 --- a/src/re_frame/scaffold.cljs +++ b/src/re_frame/scaffold.cljs @@ -107,27 +107,6 @@ ;; (dispatch ^:flush-dom [:event-id other params]) ;; -; this is just a sample implementation, for reducing single event we use single-step transduce-event-by-resetting-atom -(defn transduce-events-by-resetting-atom [frame-atom db-atom events] - (let [reducing-fn (fn - ([db-atom] db-atom) ; completion - ([db-atom new-state] ; apply new-state to atom - (let [old-state @db-atom] - (if-not (identical? old-state new-state) - (reset! db-atom new-state))) - db-atom))] - (let [xform (frame/get-frame-transducer @frame-atom)] - (transduce xform reducing-fn db-atom events)))) - -(defn transduce-event-by-resetting-atom [frame-atom db-atom event] - (let [reducing-fn (fn [db-atom new-state] ; apply new-state to atom - (let [old-state @db-atom] - (if-not (identical? old-state new-state) - (reset! db-atom new-state))) - db-atom)] - (let [xform (frame/get-frame-transducer @frame-atom)] - ((xform reducing-fn) db-atom event)))) - (defn router-loop* [db-atom frame-atom] (go-loop [] (let [event ( Date: Mon, 17 Aug 2015 21:08:25 +0200 Subject: [PATCH 34/44] transducer factory: transducers are parametrized by db-selector --- src/re_frame/frame.cljs | 66 ++++++++++++++++++++++++--------- src/re_frame/scaffold.cljs | 4 +- src/re_frame/utils.cljs | 21 ----------- test/re_frame/test/frame.cljs | 4 +- test/re_frame/test/logging.cljs | 2 +- 5 files changed, 54 insertions(+), 43 deletions(-) diff --git a/src/re_frame/frame.cljs b/src/re_frame/frame.cljs index 08ac0c81e..5daa5bec1 100644 --- a/src/re_frame/frame.cljs +++ b/src/re_frame/frame.cljs @@ -4,22 +4,15 @@ ; re-frame meat reimplemented in terms of pure functions (with help of transducers) -; see http://clojure.org/transducers[1] -(defn get-frame-transducer - "Returns a transducer: state, event -> state. -This transducer resolves event-id, selects matching handler and calls it with old db state to produce a new db state. -Transducer must have no knowledge of underlying app-db-atom, reagent, core.async or anything else out there. -All business of queuing events and application of their effects must be baked into reducing-fn and provided from outside -by the process doing actual transduction. See utils/handle-events-and-apply-results-to-atom for an example." - [frame] - (let [{:keys [handlers db-selector]} frame] - (fn [reducing-fn] +(defn frame-transducer-factory [frame] ; <- returns a function which is able to build transducers + (fn [db-selector] ; <- returns a transducer parametrized with db-selector + (fn [reducing-fn] ; <- this is a transducer (fn ([] (reducing-fn)) ; transduction init, see [1] ([result] (reducing-fn result)) ; transduction completion, see [1] ([state event] ; transduction step, see [1] (let [event-id (get-event-id event) - handler-fn (event-id handlers)] + handler-fn (event-id (:handlers frame))] (if (nil? handler-fn) (do (error frame "re-frame: no event handler registered for: \"" event-id "\". Ignoring.") @@ -32,9 +25,19 @@ by the process doing actual transduction. See utils/handle-events-and-apply-resu state) (reducing-fn state new-db)))))))))) ; reducing function prepares new transduction state +; see http://clojure.org/transducers[1] +(defn get-frame-transducer + "Returns a transducer: state, event -> state. +This transducer resolves event-id, selects matching handler and calls it with old db state to produce a new db state. +Transducer must have no knowledge of underlying app-db-atom, reagent, core.async or anything else out there. +All business of queuing events and application of their effects must be baked into reducing-fn and provided from outside +by the process doing actual transduction. See event processing helpers below for an example." + ([frame] (get-frame-transducer frame identity)) + ([frame db-selector] ((frame-transducer-factory frame) db-selector))) + (defprotocol IFrame) -(defrecord Frame [handlers subscriptions db-selector loggers] +(defrecord Frame [handlers subscriptions loggers] IFrame) (extend-protocol IPrintWithWriter @@ -51,14 +54,12 @@ by the process doing actual transduction. See utils/handle-events-and-apply-resu "Constructs an independent frame instance." ([] (make-frame nil)) ([handlers] (make-frame handlers nil)) - ([handlers subscriptions] (make-frame handlers subscriptions deref)) - ([handlers subscriptions db-selector] (make-frame handlers subscriptions db-selector default-loggers)) - ([handlers subscriptions db-selector loggers] + ([handlers subscriptions] (make-frame handlers subscriptions default-loggers)) + ([handlers subscriptions loggers] {:pre [(or (map? handlers) (nil? handlers)) (or (map? subscriptions) (nil? subscriptions)) - (fn? db-selector) (map? loggers)]} - (Frame. handlers subscriptions db-selector loggers))) + (Frame. handlers subscriptions loggers))) (defn subscribe "Returns a reagent/reaction which observes state." @@ -117,3 +118,34 @@ by the process doing actual transduction. See utils/handle-events-and-apply-resu "Resets loggers." [frame new-loggers] (assoc frame :loggers new-loggers)) + +;; -- event processing ----------------------------------------------------------------------- + +(defn process-events-on-atom [frame db-atom events] + (let [reducing-fn (fn + ([db-atom] db-atom) ; completion + ([db-atom new-db] ; apply new-state to atom + (let [old-db @db-atom] + (if-not (identical? old-db new-db) + (reset! db-atom new-db))) + db-atom)) + xform (get-frame-transducer frame deref)] + (transduce xform reducing-fn db-atom events))) + +(defn process-events-on-atom-with-coallesced-write [frame db-atom events] + (let [reducing-fn (fn + ([final-db] (reset! db-atom final-db)) ; completion + ([_old-db new-db] new-db)) ; simply carry-on with new-state + xform (get-frame-transducer frame identity)] + (transduce xform reducing-fn @db-atom events))) + +(defn process-event [frame old-db event] + (let [reducing-fn (fn [_old-state new-state] new-state) + xform (get-frame-transducer frame identity)] + ((xform reducing-fn) old-db event))) + +(defn process-event-and-reset-atom [frame db-atom event] + (let [old-db @db-atom + new-db (process-event frame old-db event)] + (if-not (identical? old-db new-db) + (reset! db-atom new-db)))) diff --git a/src/re_frame/scaffold.cljs b/src/re_frame/scaffold.cljs index fc72598aa..75e764184 100644 --- a/src/re_frame/scaffold.cljs +++ b/src/re_frame/scaffold.cljs @@ -114,7 +114,7 @@ (do (reagent/flush) ( Date: Mon, 17 Aug 2015 22:49:31 +0200 Subject: [PATCH 35/44] add tests to exercise event processing --- src/re_frame/frame.cljs | 11 +++++--- src/re_frame/scaffold.cljs | 4 +-- test/re_frame/test/frame.cljs | 52 +++++++++++++++++++++++++---------- 3 files changed, 47 insertions(+), 20 deletions(-) diff --git a/src/re_frame/frame.cljs b/src/re_frame/frame.cljs index 5daa5bec1..b580f77be 100644 --- a/src/re_frame/frame.cljs +++ b/src/re_frame/frame.cljs @@ -133,18 +133,21 @@ by the process doing actual transduction. See event processing helpers below for (transduce xform reducing-fn db-atom events))) (defn process-events-on-atom-with-coallesced-write [frame db-atom events] - (let [reducing-fn (fn - ([final-db] (reset! db-atom final-db)) ; completion + (let [old-db @db-atom + reducing-fn (fn + ([final-db] + (if-not (identical? old-db final-db) + (reset! db-atom final-db))) ; completion ([_old-db new-db] new-db)) ; simply carry-on with new-state xform (get-frame-transducer frame identity)] - (transduce xform reducing-fn @db-atom events))) + (transduce xform reducing-fn old-db events))) (defn process-event [frame old-db event] (let [reducing-fn (fn [_old-state new-state] new-state) xform (get-frame-transducer frame identity)] ((xform reducing-fn) old-db event))) -(defn process-event-and-reset-atom [frame db-atom event] +(defn process-event-on-atom [frame db-atom event] (let [old-db @db-atom new-db (process-event frame old-db event)] (if-not (identical? old-db new-db) diff --git a/src/re_frame/scaffold.cljs b/src/re_frame/scaffold.cljs index 75e764184..06469c2b0 100644 --- a/src/re_frame/scaffold.cljs +++ b/src/re_frame/scaffold.cljs @@ -114,7 +114,7 @@ (do (reagent/flush) ( (make-empty-test-frame) (frame/register-event-handler :my-handler my-handler))] (is (= (last-error) nil)) - (is (= (process-single-event frame [:my-handler]) nil)) + (is (= (frame/process-event frame nil [:my-handler]) nil)) (is (= (last-error) ["re-frame: your handler returned nil. It should return the new db state. Ignoring."])))) (testing "calling a handler which does not exist" (reset-log-recorder!) (let [frame (make-empty-test-frame)] (is (= (last-error) nil)) - (is (= (process-single-event frame [:non-existing-handler]) nil)) + (is (= (frame/process-event frame nil [:non-existing-handler]) nil)) (is (= (last-error) ["re-frame: no event handler registered for: \"" :non-existing-handler "\". Ignoring."]))))) (deftest frame-warnings @@ -102,9 +95,40 @@ (is (= (get-in frame [:subscriptions :some-handler]) identity)) (is (= (get-in (frame/unregister-subscription-handler frame :some-handler) [:subscriptions :some-handler] ::not-found) ::not-found))))) -(deftest frame-transduction - (testing "simple transduce" - (let [my-handler (fn [_state [event-id & args]] (str "result" event-id args)) +(deftest frame-events + (testing "process event" + (let [my-handler (fn [state [& args]] (str "state:" state " args:" args)) frame (-> (make-empty-test-frame) - (frame/register-event-handler :my-handler my-handler))] - (is (= (process-single-event frame [:my-handler 1 2]) "result:my-handler(1 2)"))))) + (frame/register-event-handler :my-handler my-handler)) + result (frame/process-event frame "[initial state]" [:my-handler 1 2])] + (is (= result "state:[initial state] args:(:my-handler 1 2)")))) + (testing "process event on atom" + (let [db (atom 0) + reset-counter (atom 0) + _ (add-watch db ::watcher #(swap! reset-counter inc)) + add-handler (fn [state [_event-id num]] (+ state num)) + frame (-> (make-empty-test-frame) + (frame/register-event-handler :add add-handler))] + (frame/process-event-on-atom frame db [:add 100]) + (is (= @db 100)) + (is (= @reset-counter 1)))) + (testing "process multiple events on atom" + (let [db (atom 0) + reset-counter (atom 0) + _ (add-watch db ::watcher #(swap! reset-counter inc)) + add-handler (fn [state [_event-id num]] (+ state num)) + frame (-> (make-empty-test-frame) + (frame/register-event-handler :add add-handler))] + (frame/process-events-on-atom frame db [[:add 1] [:add 2]]) + (is (= @db 3)) + (is (= @reset-counter 2)))) + (testing "process multiple events on atom with coallesced write" + (let [db (atom 0) + reset-counter (atom 0) + _ (add-watch db ::watcher #(swap! reset-counter inc)) + add-handler (fn [state [_event-id num]] (+ state num)) + frame (-> (make-empty-test-frame) + (frame/register-event-handler :add add-handler))] + (frame/process-events-on-atom-with-coallesced-write frame db [[:add 1] [:add 2]]) + (is (= @db 3)) + (is (= @reset-counter 1))))) From e534e6a703a3926cbf2a9eaca9af73213222ae4d Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Mon, 17 Aug 2015 23:08:59 +0200 Subject: [PATCH 36/44] frame polishing --- src/re_frame/frame.cljs | 167 ++++++++++++++++++++++------------------ 1 file changed, 90 insertions(+), 77 deletions(-) diff --git a/src/re_frame/frame.cljs b/src/re_frame/frame.cljs index b580f77be..d6603253d 100644 --- a/src/re_frame/frame.cljs +++ b/src/re_frame/frame.cljs @@ -2,53 +2,14 @@ (:require [re-frame.utils :refer [get-event-id get-subscription-id simple-inflection frame-summary-description]] [re-frame.logging :refer [log warn error default-loggers]])) -; re-frame meat reimplemented in terms of pure functions (with help of transducers) - -(defn frame-transducer-factory [frame] ; <- returns a function which is able to build transducers - (fn [db-selector] ; <- returns a transducer parametrized with db-selector - (fn [reducing-fn] ; <- this is a transducer - (fn - ([] (reducing-fn)) ; transduction init, see [1] - ([result] (reducing-fn result)) ; transduction completion, see [1] - ([state event] ; transduction step, see [1] - (let [event-id (get-event-id event) - handler-fn (event-id (:handlers frame))] - (if (nil? handler-fn) - (do - (error frame "re-frame: no event handler registered for: \"" event-id "\". Ignoring.") - state) - (let [old-db (db-selector state) ; db-selector is responsible for retrieving actual db from current state - new-db (handler-fn old-db event)] ; calls selected handler (including all composed middlewares) - (if (nil? new-db) ; TODO: this test should be optional, there could be valid use-cases for nil db - (do - (error frame "re-frame: your handler returned nil. It should return the new db state. Ignoring.") - state) - (reducing-fn state new-db)))))))))) ; reducing function prepares new transduction state - -; see http://clojure.org/transducers[1] -(defn get-frame-transducer - "Returns a transducer: state, event -> state. -This transducer resolves event-id, selects matching handler and calls it with old db state to produce a new db state. -Transducer must have no knowledge of underlying app-db-atom, reagent, core.async or anything else out there. -All business of queuing events and application of their effects must be baked into reducing-fn and provided from outside -by the process doing actual transduction. See event processing helpers below for an example." - ([frame] (get-frame-transducer frame identity)) - ([frame db-selector] ((frame-transducer-factory frame) db-selector))) +; re-frame meat implemented in terms of pure functions (with help of transducers) (defprotocol IFrame) (defrecord Frame [handlers subscriptions loggers] IFrame) -(extend-protocol IPrintWithWriter - Frame - (-pr-writer [this writer opts] - (-write writer (str "#"))) +;; -- construction ----------------------------------------------------------------------------------------------------- (defn make-frame "Constructs an independent frame instance." @@ -61,16 +22,30 @@ by the process doing actual transduction. See event processing helpers below for (map? loggers)]} (Frame. handlers subscriptions loggers))) -(defn subscribe - "Returns a reagent/reaction which observes state." - [frame subscription-spec] - (let [subscription-id (get-subscription-id subscription-spec) - handler-fn (get-in frame [:subscriptions subscription-id])] - (if (nil? handler-fn) - (do - (error frame "re-frame: no subscription handler registered for: \"" subscription-id "\". Returning a nil subscription.") - nil) - (handler-fn subscription-spec)))) +;; -- event handlers --------------------------------------------------------------------------------------------------- + +(defn register-event-handler + "Register a handler for an event." + ([frame event-id handler-fn] + (let [existing-handlers (get frame :handlers)] + (if (contains? existing-handlers event-id) + (warn frame "re-frame: overwriting an event handler for: " event-id)) + (assoc-in frame [:handlers event-id] handler-fn)))) + +(defn unregister-event-handler + "Unregisters event handler function previously registered via register-event-handler." + [frame event-id] + (let [existing-handlers (get frame :handlers)] + (if (contains? existing-handlers event-id) + (warn frame "re-frame: unregistering event handler \"" event-id "\" which does not exist."))) + (update frame :handlers dissoc event-id)) + +(defn clear-event-handlers + "Unregisters all event handlers." + [frame] + (assoc frame :handlers nil)) + +;; -- subscriptions ---------------------------------------------------------------------------------------------------- (defn register-subscription-handler "Registers a subscription handler function for an id." @@ -93,39 +68,65 @@ by the process doing actual transduction. See event processing helpers below for [frame] (assoc frame :subscriptions nil)) -(defn register-event-handler - "Register a handler for an event." - ([frame event-id handler-fn] - (let [existing-handlers (get frame :handlers)] - (if (contains? existing-handlers event-id) - (warn frame "re-frame: overwriting an event handler for: " event-id)) - (assoc-in frame [:handlers event-id] handler-fn)))) - -(defn unregister-event-handler - "Unregisters event handler function previously registered via register-event-handler." - [frame event-id] - (let [existing-handlers (get frame :handlers)] - (if (contains? existing-handlers event-id) - (warn frame "re-frame: unregistering event handler \"" event-id "\" which does not exist."))) - (update frame :handlers dissoc event-id)) +(defn subscribe + "Returns a reagent/reaction which observes state." + [frame subscription-spec] + (let [subscription-id (get-subscription-id subscription-spec) + handler-fn (get-in frame [:subscriptions subscription-id])] + (if (nil? handler-fn) + (do + (error frame "re-frame: no subscription handler registered for: \"" subscription-id "\". Returning a nil subscription.") + nil) + (handler-fn subscription-spec)))) -(defn clear-event-handlers - "Unregisters all event handlers." - [frame] - (assoc frame :handlers nil)) +;; -- utilities -------------------------------------------------------------------------------------------------------- (defn set-loggers "Resets loggers." [frame new-loggers] (assoc frame :loggers new-loggers)) -;; -- event processing ----------------------------------------------------------------------- +;; -- transducers ------------------------------------------------------------------------------------------------------ +;; +;; see http://clojure.org/transducers[1] + +(defn frame-transducer-factory [frame] ; <- returns a function which is able to build transducers + (fn [db-selector] ; <- returns a transducer parametrized with db-selector + (fn [reducing-fn] ; <- this is a transducer + (fn + ([] (reducing-fn)) ; transduction init, see [1] + ([result] (reducing-fn result)) ; transduction completion, see [1] + ([state event] ; transduction step, see [1] + (let [event-id (get-event-id event) + handler-fn (event-id (:handlers frame))] + (if (nil? handler-fn) + (do + (error frame "re-frame: no event handler registered for: \"" event-id "\". Ignoring.") + state) + (let [old-db (db-selector state) ; db-selector is responsible for retrieving actual db from current state + new-db (handler-fn old-db event)] ; calls selected handler (including all composed middlewares) + (if (nil? new-db) ; TODO: this test should be optional, there could be valid use-cases for nil db + (do + (error frame "re-frame: your handler returned nil. It should return the new db state. Ignoring.") + state) + (reducing-fn state new-db)))))))))) ; reducing function prepares new transduction state + +(defn get-frame-transducer + "Returns a transducer: state, event -> state. +This transducer resolves event-id, selects matching handler and calls it with old db state to produce a new db state. +Transducer must have no knowledge of underlying app-db-atom, reagent, core.async or anything else out there. +All business of queuing events and application of their effects must be baked into reducing-fn and provided from outside +by the process doing actual transduction. See event processing helpers below for an example." + ([frame] (get-frame-transducer frame identity)) + ([frame db-selector] ((frame-transducer-factory frame) db-selector))) + +;; -- event processing ------------------------------------------------------------------------------------------------- (defn process-events-on-atom [frame db-atom events] (let [reducing-fn (fn ([db-atom] db-atom) ; completion - ([db-atom new-db] ; apply new-state to atom - (let [old-db @db-atom] + ([db-atom new-db] ; in each step + (let [old-db @db-atom] ; commit new-db to atom (if-not (identical? old-db new-db) (reset! db-atom new-db))) db-atom)) @@ -135,10 +136,10 @@ by the process doing actual transduction. See event processing helpers below for (defn process-events-on-atom-with-coallesced-write [frame db-atom events] (let [old-db @db-atom reducing-fn (fn - ([final-db] - (if-not (identical? old-db final-db) - (reset! db-atom final-db))) ; completion - ([_old-db new-db] new-db)) ; simply carry-on with new-state + ([final-db] ; completion + (if-not (identical? old-db final-db) ; commit final-db to atom + (reset! db-atom final-db))) + ([_old-db new-db] new-db)) ; simply carry-on with new-db as our new state xform (get-frame-transducer frame identity)] (transduce xform reducing-fn old-db events))) @@ -152,3 +153,15 @@ by the process doing actual transduction. See event processing helpers below for new-db (process-event frame old-db event)] (if-not (identical? old-db new-db) (reset! db-atom new-db)))) + +;; -- nice to have ----------------------------------------------------------------------------------------------------- + +(extend-protocol IPrintWithWriter + Frame + (-pr-writer [this writer opts] + (-write writer (str "#"))) From ff346a866be689be091f674f775cadb396ab1054 Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Tue, 18 Aug 2015 09:51:00 +0200 Subject: [PATCH 37/44] add frame/process-events --- src/re_frame/frame.cljs | 30 +++++++++++++++++++----------- test/re_frame/test/frame.cljs | 7 +++++++ 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/re_frame/frame.cljs b/src/re_frame/frame.cljs index d6603253d..6e86125e3 100644 --- a/src/re_frame/frame.cljs +++ b/src/re_frame/frame.cljs @@ -122,6 +122,25 @@ by the process doing actual transduction. See event processing helpers below for ;; -- event processing ------------------------------------------------------------------------------------------------- +(defn process-event [frame init-db event] + (let [reducing-fn (fn [_old-state new-state] new-state) + xform (get-frame-transducer frame identity)] + ((xform reducing-fn) init-db event))) + +(defn process-events [frame init-db events] + (let [reducing-fn (fn + ([db-states] db-states) ; completion + ([db-states new-db] ; in each step + (conj db-states new-db))) ; add new-db state to the vector + xform (get-frame-transducer frame last)] + (transduce xform reducing-fn [init-db] events))) + +(defn process-event-on-atom [frame db-atom event] + (let [old-db @db-atom + new-db (process-event frame old-db event)] + (if-not (identical? old-db new-db) + (reset! db-atom new-db)))) + (defn process-events-on-atom [frame db-atom events] (let [reducing-fn (fn ([db-atom] db-atom) ; completion @@ -143,17 +162,6 @@ by the process doing actual transduction. See event processing helpers below for xform (get-frame-transducer frame identity)] (transduce xform reducing-fn old-db events))) -(defn process-event [frame old-db event] - (let [reducing-fn (fn [_old-state new-state] new-state) - xform (get-frame-transducer frame identity)] - ((xform reducing-fn) old-db event))) - -(defn process-event-on-atom [frame db-atom event] - (let [old-db @db-atom - new-db (process-event frame old-db event)] - (if-not (identical? old-db new-db) - (reset! db-atom new-db)))) - ;; -- nice to have ----------------------------------------------------------------------------------------------------- (extend-protocol IPrintWithWriter diff --git a/test/re_frame/test/frame.cljs b/test/re_frame/test/frame.cljs index 636350415..eb22a90ed 100644 --- a/test/re_frame/test/frame.cljs +++ b/test/re_frame/test/frame.cljs @@ -102,6 +102,13 @@ (frame/register-event-handler :my-handler my-handler)) result (frame/process-event frame "[initial state]" [:my-handler 1 2])] (is (= result "state:[initial state] args:(:my-handler 1 2)")))) + (testing "process multiple events, get vector of states back" + (let [init-db 0 + add-handler (fn [state [_event-id num]] (+ state num)) + frame (-> (make-empty-test-frame) + (frame/register-event-handler :add add-handler)) + result (frame/process-events frame init-db [[:add 1] [:add 2] [:add 10]])] + (is (= result [0 1 3 13])))) (testing "process event on atom" (let [db (atom 0) reset-counter (atom 0) From 8f61e93d920e3ec0e9d91354af18de61a5caa866 Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Tue, 18 Aug 2015 09:55:31 +0200 Subject: [PATCH 38/44] move log recording helpers under test.utils.log-recording ns --- test/re_frame/test/frame.cljs | 60 ++++++--------------- test/re_frame/test/utils/log_recording.cljs | 28 ++++++++++ 2 files changed, 45 insertions(+), 43 deletions(-) create mode 100644 test/re_frame/test/utils/log_recording.cljs diff --git a/test/re_frame/test/frame.cljs b/test/re_frame/test/frame.cljs index eb22a90ed..113075340 100644 --- a/test/re_frame/test/frame.cljs +++ b/test/re_frame/test/frame.cljs @@ -1,60 +1,34 @@ (ns re-frame.test.frame (:require-macros [cemerick.cljs.test :refer (is deftest testing)]) (:require [cemerick.cljs.test] - [re-frame.frame :as frame])) + [re-frame.frame :as frame] + [re-frame.test.utils.log-recording :refer [recording-loggers reset-log-recorder! last-error last-warn last-log]])) -(def log-transcript (atom {})) - -(defn record-log-call [what args] - (swap! log-transcript (fn [transcript] - (update transcript what - (fn [record] - (-> (or record {}) - (update :counter inc) - (update :history conj (vec args)))))))) - -(defn last-log* [what] - (last (get-in @log-transcript [what :history]))) - -(def last-log (partial last-log* :log)) -(def last-warn (partial last-log* :warn)) -(def last-error (partial last-log* :error)) - -(def recording-loggers - {:log (fn [& args] (record-log-call :log args)) - :warn (fn [& args] (record-log-call :warn args)) - :error (fn [& args] (record-log-call :error args)) - :group (fn [& args] (record-log-call :group args)) - :groupEnd (fn [& args] (record-log-call :groupEnd args))}) - -(defn reset-log-recorder! [] - (reset! log-transcript {})) - -(defn make-empty-test-frame [] +(defn make-test-frame-with-log-recording [] (frame/make-frame nil nil recording-loggers)) (deftest frame-errors (testing "doing invalid subscription" (reset-log-recorder!) - (let [frame (make-empty-test-frame)] + (let [frame (make-test-frame-with-log-recording)] (is (thrown-with-msg? js/Error #"expected a vector subscription, but got:" (frame/subscribe frame :non-vector))))) (testing "subscribing to a non-existent subscription handler" (reset-log-recorder!) - (let [frame (make-empty-test-frame)] + (let [frame (make-test-frame-with-log-recording)] (is (= (last-error) nil)) (frame/subscribe frame [:subscription-which-does-not-exist]) (is (= (last-error) ["re-frame: no subscription handler registered for: \"" :subscription-which-does-not-exist "\". Returning a nil subscription."])))) (testing "calling a handler which returns nil" (reset-log-recorder!) (let [my-handler (fn [_state _] nil) - frame (-> (make-empty-test-frame) + frame (-> (make-test-frame-with-log-recording) (frame/register-event-handler :my-handler my-handler))] (is (= (last-error) nil)) (is (= (frame/process-event frame nil [:my-handler]) nil)) (is (= (last-error) ["re-frame: your handler returned nil. It should return the new db state. Ignoring."])))) (testing "calling a handler which does not exist" (reset-log-recorder!) - (let [frame (make-empty-test-frame)] + (let [frame (make-test-frame-with-log-recording)] (is (= (last-error) nil)) (is (= (frame/process-event frame nil [:non-existing-handler]) nil)) (is (= (last-error) ["re-frame: no event handler registered for: \"" :non-existing-handler "\". Ignoring."]))))) @@ -62,35 +36,35 @@ (deftest frame-warnings (testing "overwriting subscription handler" (reset-log-recorder!) - (let [frame (make-empty-test-frame) + (let [frame (make-test-frame-with-log-recording) frame-with-some-handler (frame/register-subscription-handler frame :some-handler identity)] (is (= (last-warn) nil)) (frame/register-subscription-handler frame-with-some-handler :some-handler (fn [])) (is (= (last-warn) ["re-frame: overwriting subscription handler for: " :some-handler])))) (testing "overwriting event handler" (reset-log-recorder!) - (let [frame (make-empty-test-frame) + (let [frame (make-test-frame-with-log-recording) frame-with-some-handler (frame/register-event-handler frame :some-handler identity)] (is (= (last-warn) nil)) (frame/register-event-handler frame-with-some-handler :some-handler (fn [])) (is (= (last-warn) ["re-frame: overwriting an event handler for: " :some-handler])))) (testing "unregistering subscription handler which does not exist" (reset-log-recorder!) - (let [frame (make-empty-test-frame)] + (let [frame (make-test-frame-with-log-recording)] (is (= (last-warn) nil)) (frame/unregister-subscription-handler frame :non-existing-handler) (is (= (last-warn) ["re-frame: unregistering subscription handler \"" :non-existing-handler "\" which does not exist."]))))) (deftest frame-event-handlers (testing "unregister event handler" - (let [frame (-> (make-empty-test-frame) + (let [frame (-> (make-test-frame-with-log-recording) (frame/register-event-handler :some-handler identity))] (is (= (get-in frame [:handlers :some-handler]) identity)) (is (= (get-in (frame/unregister-event-handler frame :some-handler) [:handlers :some-handler] ::not-found) ::not-found))))) (deftest frame-subscription-handlers (testing "unregister subscription handler" - (let [frame (-> (make-empty-test-frame) + (let [frame (-> (make-test-frame-with-log-recording) (frame/register-subscription-handler :some-handler identity))] (is (= (get-in frame [:subscriptions :some-handler]) identity)) (is (= (get-in (frame/unregister-subscription-handler frame :some-handler) [:subscriptions :some-handler] ::not-found) ::not-found))))) @@ -98,14 +72,14 @@ (deftest frame-events (testing "process event" (let [my-handler (fn [state [& args]] (str "state:" state " args:" args)) - frame (-> (make-empty-test-frame) + frame (-> (make-test-frame-with-log-recording) (frame/register-event-handler :my-handler my-handler)) result (frame/process-event frame "[initial state]" [:my-handler 1 2])] (is (= result "state:[initial state] args:(:my-handler 1 2)")))) (testing "process multiple events, get vector of states back" (let [init-db 0 add-handler (fn [state [_event-id num]] (+ state num)) - frame (-> (make-empty-test-frame) + frame (-> (make-test-frame-with-log-recording) (frame/register-event-handler :add add-handler)) result (frame/process-events frame init-db [[:add 1] [:add 2] [:add 10]])] (is (= result [0 1 3 13])))) @@ -114,7 +88,7 @@ reset-counter (atom 0) _ (add-watch db ::watcher #(swap! reset-counter inc)) add-handler (fn [state [_event-id num]] (+ state num)) - frame (-> (make-empty-test-frame) + frame (-> (make-test-frame-with-log-recording) (frame/register-event-handler :add add-handler))] (frame/process-event-on-atom frame db [:add 100]) (is (= @db 100)) @@ -124,7 +98,7 @@ reset-counter (atom 0) _ (add-watch db ::watcher #(swap! reset-counter inc)) add-handler (fn [state [_event-id num]] (+ state num)) - frame (-> (make-empty-test-frame) + frame (-> (make-test-frame-with-log-recording) (frame/register-event-handler :add add-handler))] (frame/process-events-on-atom frame db [[:add 1] [:add 2]]) (is (= @db 3)) @@ -134,7 +108,7 @@ reset-counter (atom 0) _ (add-watch db ::watcher #(swap! reset-counter inc)) add-handler (fn [state [_event-id num]] (+ state num)) - frame (-> (make-empty-test-frame) + frame (-> (make-test-frame-with-log-recording) (frame/register-event-handler :add add-handler))] (frame/process-events-on-atom-with-coallesced-write frame db [[:add 1] [:add 2]]) (is (= @db 3)) diff --git a/test/re_frame/test/utils/log_recording.cljs b/test/re_frame/test/utils/log_recording.cljs new file mode 100644 index 000000000..090a791d5 --- /dev/null +++ b/test/re_frame/test/utils/log_recording.cljs @@ -0,0 +1,28 @@ +(ns re-frame.test.utils.log-recording) + +(def log-transcript (atom {})) + +(defn record-log-call [what args] + (swap! log-transcript (fn [transcript] + (update transcript what + (fn [record] + (-> (or record {}) + (update :counter inc) + (update :history conj (vec args)))))))) + +(defn last-log* [what] + (last (get-in @log-transcript [what :history]))) + +(def last-log (partial last-log* :log)) +(def last-warn (partial last-log* :warn)) +(def last-error (partial last-log* :error)) + +(def recording-loggers + {:log (fn [& args] (record-log-call :log args)) + :warn (fn [& args] (record-log-call :warn args)) + :error (fn [& args] (record-log-call :error args)) + :group (fn [& args] (record-log-call :group args)) + :groupEnd (fn [& args] (record-log-call :groupEnd args))}) + +(defn reset-log-recorder! [] + (reset! log-transcript {})) From 911ab0f7d5d45c2a35d868aafeaf890749621c0a Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Tue, 18 Aug 2015 10:39:00 +0200 Subject: [PATCH 39/44] add tests to exercise triggering subscriptions --- src/re_frame/frame.cljs | 2 +- test/re_frame/test/core.cljs | 17 ++++++++++++++++- test/re_frame/test/frame.cljs | 36 ++++++++++++++++++++++++++++++----- 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/src/re_frame/frame.cljs b/src/re_frame/frame.cljs index 6e86125e3..38529183c 100644 --- a/src/re_frame/frame.cljs +++ b/src/re_frame/frame.cljs @@ -77,7 +77,7 @@ (do (error frame "re-frame: no subscription handler registered for: \"" subscription-id "\". Returning a nil subscription.") nil) - (handler-fn subscription-spec)))) + (apply handler-fn subscription-spec)))) ;; -- utilities -------------------------------------------------------------------------------------------------------- diff --git a/test/re_frame/test/core.cljs b/test/re_frame/test/core.cljs index 61f7f8fa0..bd5fd873c 100644 --- a/test/re_frame/test/core.cljs +++ b/test/re_frame/test/core.cljs @@ -1,5 +1,6 @@ (ns re-frame.test.core - (:require-macros [cemerick.cljs.test :refer (is deftest testing done)]) + (:require-macros [cemerick.cljs.test :refer (is deftest testing done)] + [reagent.ratom :refer [reaction run!]]) (:require [cemerick.cljs.test] [re-frame.core :as core] [re-frame.frame :as frame])) @@ -31,3 +32,17 @@ db)) (core/dispatch [:modify-app "something"]) (core/dispatch [:check]))) + +(deftest subscribing + (testing "register subscription handler and trigger it" + (reinitialize!) + (reset! core/app-db 0) + (let [target (atom nil) + db-adder (fn [db [_sub-id num]] (reaction (+ @db num))) + _ (core/register-sub :db-adder db-adder) + subscription (core/subscribe [:db-adder 10])] + (is (= @target nil)) + (run! (reset! target @subscription)) + (is (= @target 10)) + (swap! core/app-db inc) + (is (= @target 11))))) diff --git a/test/re_frame/test/frame.cljs b/test/re_frame/test/frame.cljs index 113075340..565ad5ce0 100644 --- a/test/re_frame/test/frame.cljs +++ b/test/re_frame/test/frame.cljs @@ -1,8 +1,10 @@ (ns re-frame.test.frame - (:require-macros [cemerick.cljs.test :refer (is deftest testing)]) + (:require-macros [cemerick.cljs.test :refer (is deftest testing)] + [reagent.ratom :refer [reaction run!]]) (:require [cemerick.cljs.test] [re-frame.frame :as frame] - [re-frame.test.utils.log-recording :refer [recording-loggers reset-log-recorder! last-error last-warn last-log]])) + [re-frame.test.utils.log-recording :refer [recording-loggers reset-log-recorder! last-error last-warn last-log]] + [reagent.core :as reagent])) (defn make-test-frame-with-log-recording [] (frame/make-frame nil nil recording-loggers)) @@ -60,16 +62,26 @@ (let [frame (-> (make-test-frame-with-log-recording) (frame/register-event-handler :some-handler identity))] (is (= (get-in frame [:handlers :some-handler]) identity)) - (is (= (get-in (frame/unregister-event-handler frame :some-handler) [:handlers :some-handler] ::not-found) ::not-found))))) + (is (= (get-in (frame/unregister-event-handler frame :some-handler) [:handlers :some-handler] ::not-found) ::not-found)))) + (testing "clear event handlers" + (let [frame (-> (make-test-frame-with-log-recording) + (frame/register-event-handler :some-handler identity))] + (is (= (count (get frame :handlers)) 1)) + (is (= (count (get (frame/clear-event-handlers frame) :handlers)) 0))))) (deftest frame-subscription-handlers (testing "unregister subscription handler" (let [frame (-> (make-test-frame-with-log-recording) (frame/register-subscription-handler :some-handler identity))] (is (= (get-in frame [:subscriptions :some-handler]) identity)) - (is (= (get-in (frame/unregister-subscription-handler frame :some-handler) [:subscriptions :some-handler] ::not-found) ::not-found))))) + (is (= (get-in (frame/unregister-subscription-handler frame :some-handler) [:subscriptions :some-handler] ::not-found) ::not-found)))) + (testing "clear subscription handlers" + (let [frame (-> (make-test-frame-with-log-recording) + (frame/register-subscription-handler :some-handler identity))] + (is (= (count (get frame :subscriptions)) 1)) + (is (= (count (get (frame/clear-subscription-handlers frame) :subscriptions)) 0))))) -(deftest frame-events +(deftest frame-processing-events (testing "process event" (let [my-handler (fn [state [& args]] (str "state:" state " args:" args)) frame (-> (make-test-frame-with-log-recording) @@ -113,3 +125,17 @@ (frame/process-events-on-atom-with-coallesced-write frame db [[:add 1] [:add 2]]) (is (= @db 3)) (is (= @reset-counter 1))))) + +(deftest frame-subscriptions + (testing "register subscription handler and trigger it" + (let [source (reagent/atom 0) + target (atom nil) + source-adder (fn [_sub-id num] (reaction (+ @source num))) + frame (-> (make-test-frame-with-log-recording) + (frame/register-subscription-handler :source-adder source-adder)) + subscription (frame/subscribe frame [:source-adder 10])] + (is (= @target nil)) + (run! (reset! target @subscription)) + (is (= @target 10)) + (swap! source inc) + (is (= @target 11))))) From df66bf5011b300365dfb2058708b643cf5f77341 Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Tue, 18 Aug 2015 10:41:39 +0200 Subject: [PATCH 40/44] when triggering subscription make sure sub-id matches --- test/re_frame/test/core.cljs | 4 +++- test/re_frame/test/frame.cljs | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/test/re_frame/test/core.cljs b/test/re_frame/test/core.cljs index bd5fd873c..067be78d7 100644 --- a/test/re_frame/test/core.cljs +++ b/test/re_frame/test/core.cljs @@ -38,7 +38,9 @@ (reinitialize!) (reset! core/app-db 0) (let [target (atom nil) - db-adder (fn [db [_sub-id num]] (reaction (+ @db num))) + db-adder (fn [db [sub-id num]] + (is (= sub-id :db-adder)) + (reaction (+ @db num))) _ (core/register-sub :db-adder db-adder) subscription (core/subscribe [:db-adder 10])] (is (= @target nil)) diff --git a/test/re_frame/test/frame.cljs b/test/re_frame/test/frame.cljs index 565ad5ce0..17599de0b 100644 --- a/test/re_frame/test/frame.cljs +++ b/test/re_frame/test/frame.cljs @@ -130,7 +130,9 @@ (testing "register subscription handler and trigger it" (let [source (reagent/atom 0) target (atom nil) - source-adder (fn [_sub-id num] (reaction (+ @source num))) + source-adder (fn [sub-id num] + (is (= sub-id :source-adder)) + (reaction (+ @source num))) frame (-> (make-test-frame-with-log-recording) (frame/register-subscription-handler :source-adder source-adder)) subscription (frame/subscribe frame [:source-adder 10])] From 22e91e1dde7f176d8cedc9093605ebdbed02de15 Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Tue, 18 Aug 2015 10:49:00 +0200 Subject: [PATCH 41/44] DRY definition of recording-loggers --- test/re_frame/test/utils/log_recording.cljs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/test/re_frame/test/utils/log_recording.cljs b/test/re_frame/test/utils/log_recording.cljs index 090a791d5..bd22af15a 100644 --- a/test/re_frame/test/utils/log_recording.cljs +++ b/test/re_frame/test/utils/log_recording.cljs @@ -2,27 +2,26 @@ (def log-transcript (atom {})) -(defn record-log-call [what args] +(defn record-log-call [key args] (swap! log-transcript (fn [transcript] - (update transcript what + (update transcript key (fn [record] (-> (or record {}) (update :counter inc) (update :history conj (vec args)))))))) -(defn last-log* [what] - (last (get-in @log-transcript [what :history]))) +(defn last-log* [key] + (last (get-in @log-transcript [key :history]))) (def last-log (partial last-log* :log)) (def last-warn (partial last-log* :warn)) (def last-error (partial last-log* :error)) +(defn make-log-recorder-for-key [key] + (fn [& args] (record-log-call key args))) + (def recording-loggers - {:log (fn [& args] (record-log-call :log args)) - :warn (fn [& args] (record-log-call :warn args)) - :error (fn [& args] (record-log-call :error args)) - :group (fn [& args] (record-log-call :group args)) - :groupEnd (fn [& args] (record-log-call :groupEnd args))}) + (into {} (map (fn [key] [key (make-log-recorder-for-key key)]) [:log :warn :error :group :groupEnd]))) (defn reset-log-recorder! [] (reset! log-transcript {})) From dc82af6fe73793598f9e62104747b79b7aa7f145 Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Wed, 19 Aug 2015 16:36:56 +0200 Subject: [PATCH 42/44] refactor resetting atoms https://github.com/Day8/re-frame/pull/107#discussion_r37421196 --- src/re_frame/frame.cljs | 22 +++++++++------------- src/re_frame/utils.cljs | 6 +++++- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/re_frame/frame.cljs b/src/re_frame/frame.cljs index 38529183c..d90672807 100644 --- a/src/re_frame/frame.cljs +++ b/src/re_frame/frame.cljs @@ -1,5 +1,5 @@ (ns re-frame.frame - (:require [re-frame.utils :refer [get-event-id get-subscription-id simple-inflection frame-summary-description]] + (:require [re-frame.utils :refer [get-event-id get-subscription-id simple-inflection frame-summary-description reset-if-changed!]] [re-frame.logging :refer [log warn error default-loggers]])) ; re-frame meat implemented in terms of pure functions (with help of transducers) @@ -111,6 +111,8 @@ state) (reducing-fn state new-db)))))))))) ; reducing function prepares new transduction state +; TODO: we should memoize this function, beause it will be usually called with same frame +; using something like https://github.com/clojure/core.memoize with LRU cache would be neat (defn get-frame-transducer "Returns a transducer: state, event -> state. This transducer resolves event-id, selects matching handler and calls it with old db state to produce a new db state. @@ -136,31 +138,25 @@ by the process doing actual transduction. See event processing helpers below for (transduce xform reducing-fn [init-db] events))) (defn process-event-on-atom [frame db-atom event] - (let [old-db @db-atom - new-db (process-event frame old-db event)] - (if-not (identical? old-db new-db) - (reset! db-atom new-db)))) + (let [new-db (process-event frame @db-atom event)] + (reset-if-changed! db-atom new-db))) (defn process-events-on-atom [frame db-atom events] (let [reducing-fn (fn ([db-atom] db-atom) ; completion ([db-atom new-db] ; in each step - (let [old-db @db-atom] ; commit new-db to atom - (if-not (identical? old-db new-db) - (reset! db-atom new-db))) + (reset-if-changed! db-atom new-db) ; commit new-db to atom db-atom)) xform (get-frame-transducer frame deref)] (transduce xform reducing-fn db-atom events))) (defn process-events-on-atom-with-coallesced-write [frame db-atom events] - (let [old-db @db-atom - reducing-fn (fn + (let [reducing-fn (fn ([final-db] ; completion - (if-not (identical? old-db final-db) ; commit final-db to atom - (reset! db-atom final-db))) + (reset-if-changed! db-atom final-db)) ; commit final-db to atom ([_old-db new-db] new-db)) ; simply carry-on with new-db as our new state xform (get-frame-transducer frame identity)] - (transduce xform reducing-fn old-db events))) + (transduce xform reducing-fn @db-atom events))) ;; -- nice to have ----------------------------------------------------------------------------------------------------- diff --git a/src/re_frame/utils.cljs b/src/re_frame/utils.cljs index 3c57349d4..11471b33c 100644 --- a/src/re_frame/utils.cljs +++ b/src/re_frame/utils.cljs @@ -23,6 +23,10 @@ handlers-count " " (simple-inflection "handler" handlers-count) ", " subscriptions-count " " (simple-inflection "subscription" subscriptions-count)))) +(defn reset-if-changed! [db-atom new-db-state] + (if-not (identical? @db-atom new-db-state) + (reset! db-atom new-db-state))) + ;; -- composing middleware ----------------------------------------------------------------------- (defn report-middleware-factories @@ -53,4 +57,4 @@ (apply comp middlewares)) :else (do (warn frame "re-frame: comp-middleware expects a vector, got: " what) - nil)))) + nil)))) \ No newline at end of file From 6ad431f095cf8507f4985640f17f974dd4fd5e9d Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Wed, 19 Aug 2015 16:40:40 +0200 Subject: [PATCH 43/44] mark side-effecting functions as such --- src/re_frame/frame.cljs | 6 +++--- src/re_frame/scaffold.cljs | 4 ++-- test/re_frame/test/frame.cljs | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/re_frame/frame.cljs b/src/re_frame/frame.cljs index d90672807..6f98c2817 100644 --- a/src/re_frame/frame.cljs +++ b/src/re_frame/frame.cljs @@ -137,11 +137,11 @@ by the process doing actual transduction. See event processing helpers below for xform (get-frame-transducer frame last)] (transduce xform reducing-fn [init-db] events))) -(defn process-event-on-atom [frame db-atom event] +(defn process-event-on-atom! [frame db-atom event] (let [new-db (process-event frame @db-atom event)] (reset-if-changed! db-atom new-db))) -(defn process-events-on-atom [frame db-atom events] +(defn process-events-on-atom! [frame db-atom events] (let [reducing-fn (fn ([db-atom] db-atom) ; completion ([db-atom new-db] ; in each step @@ -150,7 +150,7 @@ by the process doing actual transduction. See event processing helpers below for xform (get-frame-transducer frame deref)] (transduce xform reducing-fn db-atom events))) -(defn process-events-on-atom-with-coallesced-write [frame db-atom events] +(defn process-events-on-atom-with-coallesced-write! [frame db-atom events] (let [reducing-fn (fn ([final-db] ; completion (reset-if-changed! db-atom final-db)) ; commit final-db to atom diff --git a/src/re_frame/scaffold.cljs b/src/re_frame/scaffold.cljs index 06469c2b0..96141456e 100644 --- a/src/re_frame/scaffold.cljs +++ b/src/re_frame/scaffold.cljs @@ -114,7 +114,7 @@ (do (reagent/flush) ( (make-test-frame-with-log-recording) (frame/register-event-handler :add add-handler))] - (frame/process-event-on-atom frame db [:add 100]) + (frame/process-event-on-atom! frame db [:add 100]) (is (= @db 100)) (is (= @reset-counter 1)))) (testing "process multiple events on atom" @@ -112,7 +112,7 @@ add-handler (fn [state [_event-id num]] (+ state num)) frame (-> (make-test-frame-with-log-recording) (frame/register-event-handler :add add-handler))] - (frame/process-events-on-atom frame db [[:add 1] [:add 2]]) + (frame/process-events-on-atom! frame db [[:add 1] [:add 2]]) (is (= @db 3)) (is (= @reset-counter 2)))) (testing "process multiple events on atom with coallesced write" @@ -122,7 +122,7 @@ add-handler (fn [state [_event-id num]] (+ state num)) frame (-> (make-test-frame-with-log-recording) (frame/register-event-handler :add add-handler))] - (frame/process-events-on-atom-with-coallesced-write frame db [[:add 1] [:add 2]]) + (frame/process-events-on-atom-with-coallesced-write! frame db [[:add 1] [:add 2]]) (is (= @db 3)) (is (= @reset-counter 1))))) From 069b7cc85434e5da205b1ca7c74adb7e816c8d46 Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Wed, 19 Aug 2015 16:57:16 +0200 Subject: [PATCH 44/44] missing newline before EOF in utils.cljs https://github.com/darwin/re-frame/commit/dc82af6fe73793598f9e62104747b79b7aa7f145#commitcomment-12786626 --- src/re_frame/utils.cljs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/re_frame/utils.cljs b/src/re_frame/utils.cljs index 11471b33c..907fc0c76 100644 --- a/src/re_frame/utils.cljs +++ b/src/re_frame/utils.cljs @@ -57,4 +57,4 @@ (apply comp middlewares)) :else (do (warn frame "re-frame: comp-middleware expects a vector, got: " what) - nil)))) \ No newline at end of file + nil))))