Version 0.8.0-alpha10

This commit is contained in:
Mike Thompson 2016-08-08 15:17:01 +10:00
parent f78541af59
commit c8659d6770
12 changed files with 508 additions and 433 deletions

View File

@ -3,7 +3,7 @@
[cljs.spec :as s]))
;; -- Spec -----------------------------------------------------------------
;; -- Spec --------------------------------------------------------------------
;;
;; This is a clojure.spec specification which documents the structure of app-db
;; See: http://clojure.org/guides/spec
@ -52,12 +52,12 @@
;; But we are not to load the setting for the "showing" filter. Just the todos.
;;
(def lsk "todos-reframe") ;; localstore key
(def ls-key "todos-reframe") ;; localstore key
(defn localstore->todos
"Read in todos from LS, and process into a map we can merge into app-db."
"Read in todos from localstore, and process into a map we can merge into app-db."
[]
(some->> (.getItem js/localStorage lsk)
(some->> (.getItem js/localStorage ls-key)
(cljs.reader/read-string) ;; stored as an EDN map.
(into (sorted-map)) ;; map -> sorted-map
(hash-map :todos))) ;; access via the :todos key
@ -65,5 +65,5 @@
(defn todos->local-store
"Puts todos into localStorage"
[todos]
(.setItem js/localStorage lsk (str todos))) ;; sorted-map writen as an EDN map
(.setItem js/localStorage ls-key (str todos))) ;; sorted-map writen as an EDN map

View File

@ -14,8 +14,7 @@
"throw an exception if db doesn't match the spec."
[a-spec db]
(when-not (s/valid? a-spec db)
(throw (ex-info "spec check failed: " {:problems
(s/explain-str a-spec db)}))))
(throw (ex-info (str "spec check failed: " (s/explain-str a-spec db)) {}))))
;; Event handlers change state, that's their job. But what happens if there's
;; a bug which corrupts app state in some subtle way? This interceptor is run after

View File

@ -1,4 +1,4 @@
(defproject re-frame "0.8.0-SNAPSHOT"
(defproject re-frame "0.8.0"
:description "A Clojurescript MVC-like Framework For Writing SPAs Using Reagent."
:url "https://github.com/Day8/re-frame.git"
:license {:name "MIT"}

92
src/re_frame/cofx.cljc Normal file
View File

@ -0,0 +1,92 @@
(ns re-frame.cofx
(:require
[re-frame.db :refer [app-db]]
[re-frame.interceptor :refer [->interceptor]]
[re-frame.registrar :refer [get-handler clear-handlers register-handler]]
[re-frame.loggers :refer [console]]))
;; -- Registration ------------------------------------------------------------
(def kind :cofx)
(assert (re-frame.registrar/kinds kind))
(def register (partial register-handler kind))
;; -- Interceptor -------------------------------------------------------------
(defn coeffect
"An interceptor which adds to a `context's` `:coeffects`.
`coeffects` are the input resources required by an event handler
to perform its job. The two most obvious ones are `db` and `event`.
But sometimes a handler might need other resources.
Perhaps a handler needs a random number or a GUID or the current datetime.
Perhaps it needs access to an in-memory DataScript database.
If the handler directly access these resources, it stops being as
pure. It immedaitely becomes harder to test, etc.
So the necessary resources are \"injected\" into the `coeffect` (map)
given the handler.
Given one or more `ids`, this function will iterately lookup the
registered coeffect handlers (via `reg-cofx`) and call each of them
giving the current `:coeffect` as an argument, and expecting a
modified coeffect to be returned.
"
;; Why? We want our handlers to be pure. If a handler calls `js/Date.` then
;; it stops being as pure. It is harder to test.
;;
;; And what if a handler needs a random number? Or a GUID? These kinds of input resources are
;; termed `coeffects` (sometimes called side-causes).
[& ids]
(->interceptor
:name :coeffects
:before (fn coeffects-before
[context]
(let [orig-coeffect (:coeffects context)
run-id (fn [coeffect id]
((get-handler kind id) coeffect))
new-coeffect (reduce run-id orig-coeffect ids)]
(assoc context :coeffects new-coeffect)))))
;; -- Standard Builtin CoEffects Handlers --------------------------------------------------------
;; :db
;;
;; Adds to coeffects the value in `app-db`, under the key `:db`
(register
:db
(fn db-coeffects-handler
[coeffects]
(assoc coeffects :db @app-db)))
;; this interceptor is so commonly used that we reify it
(def add-db (coeffect :db))
;; XXX what about a coeffect which reads LocalStore. Use in todomvc example.
;; -- Example ------------------------------------------------------------------------------------
;; An example coeffect handler, which adds the current datetime under
;; the `:now` key.
;;
;; Example Usage:
;; (reg-event-fx ;; `-fx` variant used in registration
;; :some-id
;; [(coeffect :now) debug] ;; notice (coeffect :now) is in interceptors
;; (fn [coeffect event] ;; -fx handlers are given the entire coeffect as 1st argument
;; (let [dt (:now coeffect)] ;; `:now` is available
;; ...)))
;;
;; As a further exercise, consider how you would write a `random` interceptor which adds a random
;; number into a handler's coeffect?
#_(register
:now
(fn now-coeffects-handler
[coeffects]
(assoc coeffect :now (js/Date.))))

View File

@ -1,14 +1,16 @@
(ns re-frame.core
(:require
[re-frame.events :as events]
[re-frame.subs :as subs]
[re-frame.fx :as fx]
[re-frame.router :as router]
[re-frame.loggers :as loggers]
[re-frame.registrar :as registrar]
[re-frame.interceptor :as interceptor :refer [base
db-handler->interceptor fx-handler->interceptor
ctx-handler->interceptor]]))
[re-frame.events :as events]
[re-frame.subs :as subs]
[re-frame.fx :as fx]
[re-frame.cofx :as cofx]
[re-frame.router :as router]
[re-frame.loggers :as loggers]
[re-frame.registrar :as registrar]
[re-frame.interceptor :as interceptor]
[re-frame.std-interceptors :as std-interceptors :refer [db-handler->interceptor
fx-handler->interceptor
ctx-handler->interceptor]]))
;; -- dispatch
@ -16,14 +18,14 @@
(def dispatch-sync router/dispatch-sync)
;; XXX move certain API functions up here - to get code completion and docs
;; XXX move certain API functions up to this core level - to get code completion and docs
;; XXX add a clear all handlers
;; XXX add a push handlers for testing purposes
;; XXX Add ->interceptor assoc-coeffect etc to API
;; XXX for testing purposes, and a way to snapshot re-frame instance. Then re-instate
;; XXX on figwheel reload, should invalidate all re-frame subscriptions
;; -- interceptor related
;; useful if you are writing your own interceptors
(def ->interceptor interceptor/->interceptor)
(def enqueue interceptor/enqueue)
(def get-coeffect interceptor/get-coeffect)
@ -33,57 +35,60 @@
;; -- standard interceptors
(def debug interceptor/debug)
(def path interceptor/path)
(def enrich interceptor/enrich)
(def trim-v interceptor/trim-v)
(def after interceptor/after)
(def on-changes interceptor/on-changes)
(def debug std-interceptors/debug)
(def path std-interceptors/path)
(def enrich std-interceptors/enrich)
(def trim-v std-interceptors/trim-v)
(def after std-interceptors/after)
(def on-changes std-interceptors/on-changes)
;; -- subscribe
;; -- subscriptions: reading and writing
(def reg-sub-raw subs/register-raw)
(def reg-sub subs/reg-sub)
(def subscribe subs/subscribe)
;; -- effects
;; -- effects
(def reg-fx fx/register)
(def clear-fx (partial registrar/clear-handlers fx/kind))
;; -- coeffects
(def reg-cofx cofx/register)
(def clear-cofx (partial registrar/clear-handlers cofx/kind))
;; -- Events
;; usage (clear-event! :some-id)
(def clear-event! (partial registrar/clear-handlers events/kind)) ;; XXX name with !
(def clear-all-events! (partial registrar/clear-handlers events/kind)) ;; XXX name with !
(defn reg-event-db
"Register the given `id`, typically a kewyword, with the combination of
"Register the given `id`, typically a keyword, with the combination of
`db-handler` and an interceptor chain.
`db-handler` is a function: (db event) -> db
`interceptors` is a collection of interceptors, possibly nested (needs flattenting).
`db-handler` is wrapped in an interceptor and added to the end of the chain, so in the end
there is only a chain."
there is only a chain.
The necessary effects and coeffects handler are added to the front of the
interceptor chain. These interceptors ensure that app-db is available and updated."
([id db-handler]
(reg-event-db id nil db-handler))
([id interceptors db-handler]
(events/register id [base interceptors (db-handler->interceptor db-handler)])))
(events/register id [cofx/add-db fx/do-effects interceptors (db-handler->interceptor db-handler)])))
(defn reg-event-fx
([id fx-handler]
(reg-event-fx id nil fx-handler))
([id interceptors fx-handler]
(events/register id [base interceptors (fx-handler->interceptor fx-handler)])))
(events/register id [cofx/add-db fx/do-effects interceptors (fx-handler->interceptor fx-handler)])))
(defn reg-event-ctx
([id handler]
(reg-event-ctx id nil handler))
([id interceptors handler]
(events/register id [base interceptors (ctx-handler->interceptor handler)])))
(events/register id [cofx/add-db fx/do-effects interceptors (ctx-handler->interceptor handler)])))
;; -- Logging -----

View File

@ -2,32 +2,45 @@
(:require
[re-frame.router :as router]
[re-frame.db :refer [app-db]]
[re-frame.interceptor :refer [->interceptor]]
[re-frame.interop :refer [set-timeout!]]
[re-frame.events :as events]
[re-frame.registrar :refer [get-handler clear-handlers register-handler]]
[re-frame.interop :refer [ratom? set-timeout!]]
[re-frame.loggers :refer [console]]))
;; ---- Spec schema -----------------------------------------------------------
;; TODO use Spec to validate events for :dispatch and :dispatch-n when clj 1.9
;; e.g. (when (= :cljs.spec/invalid (s/conform ::event val))
;; (console :error (s/explain-str ::event val)))
;; (s/def ::event (s/and vector? (complement empty?)))
;; (s/def ::events-n (s/coll-off ::event))
;; -- Registration ------------------------------------------------------------
(def kind :fx)
(assert (re-frame.registrar/kinds kind))
(def register (partial register-handler kind))
(defn register
[id handler-fn]
(register-handler kind id handler-fn))
;; -- Interceptor -------------------------------------------------------------
(def do-effects
"An interceptor which performs a `context's` (side) `:effects`.
For every key in the `:effects` map, call the handler previously
registered via `reg-fx`.
So, if `:effects` was:
{:dispatch [:hello 42]
:db {...}
:undo \"set flag\"}
call the registered effects handlers for each of the map's keys:
`:dispatch`, `:undo` and `:db`."
(->interceptor
:name :do-effects
:after (fn do-effects-after
[context]
(->> (:effects context)
(map (fn [[key val]]
(if-let [effect-fn (get-handler kind key)]
(effect-fn val))))
doall))))
;; -- Standard Builtin Effects Handlers --------------------------------------
;; :dispatch-later
;;
;; `dispatch` one or more events in given times on the future. Expects a collection

View File

@ -2,16 +2,13 @@
(:require
[re-frame.interop :refer [ratom?]]
[re-frame.loggers :refer [console]]
[re-frame.interop :refer [empty-queue debug-enabled?]]
[re-frame.db :refer [app-db]]
[re-frame.registrar :as registrar]
[clojure.data :as data]))
[re-frame.interop :refer [empty-queue debug-enabled?]]))
;; XXX use defrecord ??
(def mandatory-interceptor-keys #{:name :after :before})
;; XXX use defrecord ??
(defn interceptor?
[m]
@ -26,17 +23,12 @@
(if-let [unknown-keys (seq (clojure.set/difference
(-> m keys set)
mandatory-interceptor-keys))]
(console :error "re-frame: unknown interceptor keys: " unknown-keys)))
(console :error "re-frame: interceptor " name " has unknown keys: " unknown-keys)))
{:name (or name :unnamed)
:before before
:after after })
;; -- Helpers ------------------------------------------------------------------------------------
(defn get-coeffect
[context key]
(get-in context [:coeffects key]))
;; -- Effect Helpers -----------------------------------------------------------------------------
(defn get-effect
([context key]
@ -44,33 +36,29 @@
([context]
(:effects context)))
(defn assoc-effect
[context key value]
(assoc-in context [:effects key] value))
;; -- CoEffect Helpers ---------------------------------------------------------------------------
(defn get-coeffect
[context key]
(get-in context [:coeffects key]))
(defn assoc-coeffect
[context key value]
(assoc-in context [:coeffects key] value))
;; -- Execute Interceptor Chain ------------------------------------------------------------------
(defn- invoke-fn
(defn- invoke-interceptor-fn
[context interceptor direction]
(if-let [f (get interceptor direction)]
(f context)
context))
;; XXX on figwheel reload, should invalidate all re-frame subscriptions
(defn clean-context
[context]
(if (map? context)
(dissoc context :stack :queue)
context))
(defn- invoke-interceptors
"Loop over all interceptors, calling `direction` function on each,
@ -80,31 +68,41 @@
Each iteration, the next interceptor to process is obtained from
context's `:queue`. After they are processed, interceptors are popped
from `:queue` and added to into `:stack`.
from `:queue` and added to `:stack`.
After sufficient iteration, `:queue` will be empty, and `:stack` will
contain all interceptors processed.
Returns updated context."
Returns updated `context`. Ie. the `context` which has been threaded
through all interceptor functions.
Generally speaking, an interceptor's `:before` fucntion will (if present)
add to a `context's` `:coeffect`, while it's `:after` function
will modify the `context`'s `:effect`. Very approximately.
But because all interceptor functions are given `context`, and can
return a modified version of it, the way is clear for an interceptor
to introspect the stack or queue, or even modify the queue
(add new interceptors via `enqueue`?). This is a very fluid arrangement."
([context direction]
(loop [context context]
(let [queue (:queue context)] ;; future interceptors
(let [queue (:queue context)] ;; future interceptors
(if (empty? queue)
context
(let [interceptor (peek queue)
(let [interceptor (peek queue) ;; next interceptor to call
stack (:stack context)] ;; already completed interceptors
(recur (-> context
(assoc :queue (pop queue))
(assoc :stack (conj stack interceptor))
(invoke-fn interceptor direction)))))))))
(invoke-interceptor-fn interceptor direction)))))))))
(defn enqueue
"Adds a collection of interceptors to the end of context's execution queue.
Returns updated context.
"Add a collection of `interceptors` to the end of `context's` execution `:queue`.
Returns the updated `context`.
In advanced cases, where an interceptor itself wanted to add to the queue,
it would call this function (on the context provided to it)"
In an advanced case, this function would allow an interceptor could add new
interceptors to the `:queue` of a context."
[context interceptors]
(update context :queue
(fnil into empty-queue)
@ -112,386 +110,81 @@
(defn- context
"Return a fresh context"
"Create a fresh context"
([event interceptors]
(-> {}
(assoc-coeffect :event event)
(enqueue interceptors)))
([event interceptors db]
([event interceptors db] ;; only used in tests, probably a hack, remove ? XXX
(-> (context event interceptors)
(assoc-coeffect :db db))))
(defn- change-direction
"Called on completion of `:before` processing, this function prepares/modifies
`context` for the backwards sweep of processing in which `:after` fns are
called.
`context` for the backwards sweep of processing in which an interceptor
chain's `:after` fns are called.
At this point in processing, the `:queue` is empty and `:stack` holds all
interceptors. To enable a backwards walk, the job is to prime the `:queue`
with the reverse of what's in `:stack`"
the previously run interceptors. So this function enables the backwards walk
by priming `:queue` with what's currently in `:stack`"
[context]
(-> context
(dissoc :queue)
(enqueue (-> context :stack ))))
(enqueue (:stack context))))
(defn execute
"Executes a queue of interceptors for a given event.
"Executes the given chain (coll) of interceptors.
An interceptor has this form:
{:before (fn [context] ...) ;; identity would be a noop
:after (fn [context] ...)}
Each interceptor has this form:
{:before (fn [context] ...) ;; returns possibly modified context
:after (fn [context] ...)} ;; `identity` would be a noop
Walk the queue of iterceptors from beginning to end calling the `:before` fn on
each, then reverse direction, and walk backwards, calling the `:after` fn on each.
Walks the queue of iterceptors from beginning to end, calling the
`:before` fn on each, then reverse direction and walk backwards,
calling the `:after` fn on each.
The last interceptor in the chain presumably wraps an event handler fn.
The last interceptor in the chain presumably wraps an event
handler fn. So the overall goal of the process is to \"handle
the given event\".
Thread a `context` through all calls. `context` has this form:
{:coeffects {:event event
{:coeffects {:event [:a-query-id :some-param]
:db <original contents of app-db>}
:effects {:db <new value for app-db>
:dispatch [:something]} ;; example of other effects
:dispatch [:an-event-id :param1]}
:queue <a collection of further interceptors>
:stack <a collection of interceptors already walked>}
`context` has `:coeffects` and `:effects` which, if this was a web server, would
be somewhat anologous to `request` and `response`.
`context` has `:coeffects` and `:effects` which, if this was a web
server, would be somewhat anologous to `request` and `response`
respectively.
`coeffects` contains information like `event` and the initial state of `db` - ie. the
inputs required by the event handler (sitting presumably on the end of the chain),
while handler-required side effects are assoc-ed in `:effects` including, but not limited
to, new values for `db`.
`coeffects` will contain information like `event` and the initial
state of `db` - ie. the inputs required by the event handler
(sitting presumably on the end of the chain), while handler-returned
side effects are assoc-ed in `:effects` including, but not limited to,
new values for `db`.
The first interceptor in a chain will likely have a :before function
which adds the current state of app-db into `:coeffects`. OR, it might instead
add the connection for a DataScript database, or any other inputs required.
And subsequent interceptors may further add to coeffects via their :before too.
The first few interceptor in a chain will likely have `:before`
functions which \"prime\" the `context` by adding the event, and
the current state of app-db into `:coeffects`. OR, it might instead
add the connection for a DataScript database, or any other inputs
required.
Equally, this same first interceptor will likely have an `:after` fn which can process
all the side effects accumulated into `:effects` including but, not limited to,
updates to app-db.
Equally, some interceptors in the chain will have `:after` fn
which can process the side effects accumulated into `:effects`
including but, not limited to, updates to app-db.
Through both stages (before and after), `context` contains a `:queue` of interceptors yet to be
processed, and a `:stack` of interceptors already done. In advanced cases,
these values can be modified by the functions through which the context is threaded."
Through both stages (before and after), `context` contains a `:queue`
of interceptors yet to be processed, and a `:stack` of interceptors
already done. In advanced cases, these values can be modified by the
functions through which the context is threaded."
[event-v interceptors]
(-> (context event-v interceptors)
(invoke-interceptors :before)
change-direction
(invoke-interceptors :after)))
;; -- Standard Interceptors -----------------------------------------------------------------------
;; these could be in their own library
(def base
"An interceptor which injects/extracts the value of app-db intto/from a context.
Used for XXXX "
(->interceptor
:name :base
:before (fn base-before
[context]
(assoc-coeffect context :db @app-db)) ;; a coeffect for the handler
:after (fn base-after
[context]
(->> (:effects context)
(map (fn [[key val]]
(if-let [effect-fn (registrar/get-handler :fx key)] ;; XXX shouldn't be using raw :fx
(effect-fn val))))
doall))))
;; XXX how to stub this out for testing purposes??
#_(def now
"An example interceptor (of dubious utility) which is an example of adding
to a handler's coeffects. This interceptor adds the current datetime to coeffects under
the `:now` key.
Why? We want out handlers to be as pure as possible. If a handler calls `js/Date.` then
it stops being as pure. What if it needs a random number? These kinds of needed
\"inputs\" are referred to `coeffects` (sometimes called side-causes).
usage:
(reg-event-fx ;; notice use of `-fx` registration
:some-id
[i1 i2 now] ;; notice use of `now` as one of the handler's interceptors
(fn [world event] ;; world is the handler's coeffect
(let [dt (:now world)] ;; `:now` is available becaue `now` put it there.
...)))
As an exercise, consider how you would write a `random` interceptor which adds a random
number into a handler's coeffect?
"
(->interceptor
:name :now
:before (fn now-before
[context]
(assoc-coeffect context :now (js/Date.)))))
(def debug
"An interceptor which logs data about the handling of an event.
Includes a `clojure.data/diff` of the db, before vs after, showing the changes
caused by the event handler.
You'd typically want this interceptor after (to the right of) any path interceptor.
Warning: calling clojure.data/diff on large, complex data structures can be slow.
So, you won't want this interceptor present in production code. See the todomvc
example to see how to exclude interceptors from production code."
(->interceptor
:name :debug
:before (fn debug-before
[context]
#_(console :log "Handling re-frame event: " (-> context :coeffects :event))
context)
:after (fn debug-after
[context]
(let [event (get-coeffect context :event)
orig-db (get-coeffect context :db)
new-db (get-effect context :db)]
(if-not new-db
(do
(console :log "no app-db changes caused by: " event)
context)
(let [[only-before only-after] (data/diff orig-db new-db) ;; diff between effe
db-changed? (or (some? only-before) (some? only-after))]
(if db-changed?
(do (console :group "db clojure.data/diff for: " event)
(console :log "only before: " only-before)
(console :log "only after : " only-after)
(console :groupEnd))
(console :log "no app-db changes caused by: " event))
context))))))
(def trim-v
"An interceptor which removes the first element of the event vector, allowing you to write
more aesthetically pleasing db handlers. No leading underscore on the event-v!
Your event handlers will look like this:
(defn my-handler
[db [x y z]] ;; <-- instead of [_ x y z]
....)"
(->interceptor
:name :trim-v
:before (fn trimv-before
[context]
(->> (get-coeffect context :event)
rest
vec
(assoc-coeffect context :event)))))
;; -- Interceptor Factories - PART 1 ---------------------------------------------------------------
;;
;; These factories wrap the 3 kinds of handlers.
;;
(defn db-handler->interceptor
"Returns an interceptor which wraps the kind of event handler given to `reg-event-db`.
These handlers take two arguments; `db` and `event`, and they return `db`.
(fn [db event]
....)
So, the interceptor wraps the given handler:
1. extracts two coeffects (from context): db and event
2. calls handler-fn
3. stores the db result back into context's effects"
[handler-fn]
(->interceptor
:name :db-handler
:before (fn db-handler-before
[context]
(let [{:keys [db event]} (:coeffects context)]
(->> (handler-fn db event)
(assoc-effect context :db))))))
(defn fx-handler->interceptor
"Returns an interceptor which wraps the kind of event handler given to `reg-event-fx`.
These handlers take two arguments; `world` and `event`, and they return `effects`.
(fn [world event]
{:db ...
:dispatch ...})
Wrap handler in an interceptor so it can be added to (the RHS) of a chain:
1. extracts necessary coeffects
2. call handler-fn
3. stores the result backinto the effects"
[handler-fn]
(->interceptor
:name :fx-handler
:before (fn fx-handler-before
[context]
(let [{:keys [event] :as coeffects} (:coeffects context)]
(->> (handler-fn coeffects event)
(assoc context :effects))))))
(defn ctx-handler->interceptor
"Returns an interceptor which wraps the kind of event handler given to `reg-event-ctx`.
These advanced handlers take one argument: `context` and they return a modified `context`.
Example:
(fn [context]
(enqueue context [more interceptors]))"
[handler-fn]
(->interceptor
:name :ctx-handler
:before handler-fn))
;; -- Interceptors Factories - PART 2 ------------------------------------------------------------
;;
;; I.e. functions which return interceptors
;;
(defn path
"An interceptor factory which supplies a sub-path of `:db` to the handler.
Is somewhat annologous to `update-in`. It grafts the return value from the handler
back into db.
Usage:
(path :some :path)
(path [:some :path])
(path [:some :path] :to :here)
(path [:some :path] [:to] :here)
Implementation:
- in :before, store the original db in within `context`
- ib :after, re-establish original db with modification
- remember: path may be used twice within the one interceptor chain.
"
[& args]
(let [path (flatten args)
db-store-key :re-frame-path/original-dbs] ;; this is where, within `context`, we store the original db
(when (empty? path)
(console :error "re-frame: \"path\" interceptor given no params."))
(->interceptor
:name :path
:before (fn
[context]
(let [original-db (get-coeffect context :db)]
(-> context
(update db-store-key conj original-db)
(assoc-coeffect :db (get-in original-db path)))))
:after (fn [context]
(let [db-store (db-store-key context)
original-db (peek db-store)
new-store (pop db-store)
full-db (->> (get-effect context :db)
(assoc-in original-db path))]
(-> context
(assoc db-store-key new-store)
(assoc-effect :db full-db)))))))
;; XXX in todomvc what about a coeffect which is the value in LocalStore ??
(defn enrich
"Interceptor factory which runs a given function \"f\" in the \"after handler\"
position. \"f\" is (db v) -> db
Unlike the \"after\" inteceptor which is only about side effects, \"enrich\"
expects f to process and alter the :db coeffect in some useful way, contributing
to the derived data, flowing vibe.
Imagine that todomvc needed to do duplicate detection - if any two todos had
the same text, then highlight their background, and report them in a warning
down the bottom.
Almost any action (edit text, add new todo, remove a todo) requires a
complete reassesment of duplication errors and warnings. Eg: that edit
update might have introduced a new duplicate or removed one. Same with a
todo removal.
And to perform this enrichment, a function has to inspect all the todos,
possibly set flags on each, and set some overall list of duplicates.
And this duplication check might just be one check among many.
\"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."
[f]
(->interceptor
:name :enrich
:after (fn enrich-after
[context]
(let [event (get-coeffect context :event)
db (get-effect context :db)]
(->> (f db event)
(assoc-effect context :db))))))
(defn after
"Interceptor factory which runs a given function `f` in the \"after\"
position presumably for side effects.
`f` is called with two arguments: the `effects` value of `:db` and the event. It's return
value is ignored so `f` can only side-effect. Example uses:
- `f` runs schema validation (reporting any errors found)
- `f` writes some aspect of db to localstorage."
[f]
(->interceptor
:name :after
:after (fn after-after
[context]
(let [db (get-effect context :db)
event (get-coeffect context :event)]
(f db event) ;; call f for side effects
context)))) ;; context is unchanged
(defn on-changes
"Interceptor factory which acts a bit like `reaction` (but it flows into `db`, rather than out)
It observes N paths in `db` and if any of them test not indentical? to their previous value
(as a result of a handler being run) then it runs 'f' to compute a new value, which is
then assoced into the given `out-path` within `db`.
Usage:
(defn my-f
[a-val b-val]
... some computation on a and b in here)
(on-changes my-f [:c] [:a] [:b])
Put this Interceptor on the right handlers (ones which might change :a or :b).
It will:
- call 'f' each time the value at path [:a] or [:b] changes
- call 'f' with the values extracted from [:a] [:b]
- assoc the return value from 'f' into the path [:c]
"
[f out-path & in-paths]
(->interceptor
:name :enrich
:after (fn on-change-after
[context]
(let [new-db (get-effect context :db)
old-db (get-coeffect context :db)
;; work out if any "inputs" have changed
new-ins (map #(get-in new-db %) in-paths)
old-ins (map #(get-in old-db %) in-paths)
changed-ins? (some false? (map identical? new-ins old-ins))]
;; if one of the inputs has changed, then run 'f'
(if changed-ins?
(->> (apply f new-ins)
(assoc-in new-db out-path)
(assoc-effect context :db))
context)))))

View File

@ -1,10 +1,13 @@
(ns re-frame.registrar
"In many places, re-frame asks you to associate an `id` (keyword)
with a `handler` (fucntion). This namespace contains the
central registry of such associations."
(:require [re-frame.interop :refer [debug-enabled?]]
[re-frame.loggers :refer [console]]))
;; kinds of handlers
(def kinds #{:event :fx :sub})
(def kinds #{:event :fx :cofx :sub})
;; This atom contains a register of all handlers.
;; Is a map keyed first by kind (of handler), and then id.

View File

@ -199,14 +199,13 @@
(-call-post-event-callbacks
[_ event-v]
;; Call each registed post-event callback.
(doseq [callback (vals post-event-callback-fns)]
(callback event-v queue)))
(-resume
[this]
(-process-1st-event-in-queue this) ;; do the event which paused processing
(-run-queue this))) ;; do the rest of the queued events
(-run-queue this))) ;; do the rest of the queued events
;; ---------------------------------------------------------------------------

View File

@ -0,0 +1,271 @@
(ns re-frame.std-interceptors
"contains re-frame supplied, standard interceptors"
(:require
[re-frame.interceptor :refer [->interceptor get-effect get-coeffect assoc-coeffect assoc-effect]]
[re-frame.loggers :refer [console]]
[re-frame.registrar :as registrar]
[re-frame.db :refer [app-db]]
[clojure.data :as data]))
;; XXX provide a way to set what handler should be called when there is no registered handler.
;; by default this handler will simply print out a message saying no handler was found.
(def debug
"An interceptor which logs data about the handling of an event.
Includes a `clojure.data/diff` of the db, before vs after, showing
the changes caused by the event handler.
You'd typically want this interceptor after (to the right of) any
path interceptor.
Warning: calling clojure.data/diff on large, complex data structures
can be slow. So, you won't want this interceptor present in production
code. See the todomvc example to see how to exclude interceptors from
production code."
(->interceptor
:name :debug
:before (fn debug-before
[context]
#_(console :log "Handling re-frame event: " (-> context :coeffects :event))
context)
:after (fn debug-after
[context]
(let [event (get-coeffect context :event)
orig-db (get-coeffect context :db)
new-db (get-effect context :db)]
(if-not new-db
(do
(console :log "no app-db changes caused by: " event)
context)
(let [[only-before only-after] (data/diff orig-db new-db) ;; diff between effe
db-changed? (or (some? only-before) (some? only-after))]
(if db-changed?
(do (console :group "db clojure.data/diff for: " event)
(console :log "only before: " only-before)
(console :log "only after : " only-after)
(console :groupEnd))
(console :log "no app-db changes caused by: " event))
context))))))
(def trim-v
"An interceptor which removes the first element of the event vector, allowing you to write
more aesthetically pleasing db handlers. No leading underscore on the event-v!
Your event handlers will look like this:
(defn my-handler
[db [x y z]] ;; <-- instead of [_ x y z]
....)"
(->interceptor
:name :trim-v
:before (fn trimv-before
[context]
(->> (get-coeffect context :event)
rest
vec
(assoc-coeffect context :event)))))
;; -- Interceptor Factories - PART 1 ---------------------------------------------------------------
;;
;; These 3 factories wrap the 3 kinds of handlers.
;;
(defn db-handler->interceptor
"Returns an interceptor which wraps the kind of event handler given to `reg-event-db`.
These handlers take two arguments; `db` and `event`, and they return `db`.
(fn [db event]
....)
So, the interceptor wraps the given handler:
1. extracts two `:coeffects` keys: db and event
2. calls handler-fn
3. stores the db result back into context's `:effects`"
[handler-fn]
(->interceptor
:name :db-handler
:before (fn db-handler-before
[context]
(let [{:keys [db event]} (:coeffects context)]
(->> (handler-fn db event)
(assoc-effect context :db))))))
(defn fx-handler->interceptor
"Returns an interceptor which wraps the kind of event handler given to `reg-event-fx`.
These handlers take two arguments; `world` and `event`, and they return `effects`.
(fn [world event]
{:db ...
:dispatch ...})
Wrap handler in an interceptor so it can be added to (the RHS) of a chain:
1. extracts `:coeffects`
2. call handler-fn giving coeffects
3. stores the result back into the `:effects`"
[handler-fn]
(->interceptor
:name :fx-handler
:before (fn fx-handler-before
[context]
(let [{:keys [event] :as coeffects} (:coeffects context)]
(->> (handler-fn coeffects event)
(assoc context :effects))))))
(defn ctx-handler->interceptor
"Returns an interceptor which wraps the kind of event handler given to `reg-event-ctx`.
These advanced handlers take one argument: `context` and they return a modified `context`.
Example:
(fn [context]
(enqueue context [more interceptors]))"
[handler-fn]
(->interceptor
:name :ctx-handler
:before handler-fn))
;; -- Interceptors Factories - PART 2 ------------------------------------------------------------
(defn path
"An interceptor factory which supplies a sub-path of `:db` to the handler.
It's action is somewhat annologous to `update-in`. It grafts the return
value from the handler back into db.
Usage:
(path :some :path)
(path [:some :path])
(path [:some :path] :to :here)
(path [:some :path] [:to] :here)
Remember: `path` may be used multiple times within the one interceptor chain.
"
[& args]
(let [path (flatten args)
db-store-key :re-frame-path/original-dbs] ;; this is where, within `context`, we store the original db
(when (empty? path)
(console :error "re-frame: \"path\" interceptor given no params."))
(->interceptor
:name :path
:before (fn
[context]
(let [original-db (get-coeffect context :db)]
(-> context
(update db-store-key conj original-db)
(assoc-coeffect :db (get-in original-db path)))))
:after (fn [context]
(let [db-store (db-store-key context)
original-db (peek db-store)
new-db-store (pop db-store)
full-db (->> (get-effect context :db)
(assoc-in original-db path))]
(-> context
(assoc db-store-key new-db-store)
(assoc-effect :db full-db)))))))
(defn enrich
"Interceptor factory which runs a given function \"f\" in the \"after handler\"
position. \"f\" is (db v) -> db
Unlike the \"after\" inteceptor which is only about side effects, \"enrich\"
expects f to process and alter the :db coeffect in some useful way, contributing
to the derived data, flowing vibe.
Imagine that todomvc needed to do duplicate detection - if any two todos had
the same text, then highlight their background, and report them in a warning
down the bottom.
Almost any action (edit text, add new todo, remove a todo) requires a
complete reassesment of duplication errors and warnings. Eg: that edit
update might have introduced a new duplicate or removed one. Same with a
todo removal.
And to perform this enrichment, a function has to inspect all the todos,
possibly set flags on each, and set some overall list of duplicates.
And this duplication check might just be one check among many.
\"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."
[f]
(->interceptor
:name :enrich
:after (fn enrich-after
[context]
(let [event (get-coeffect context :event)
db (get-effect context :db)]
(->> (f db event)
(assoc-effect context :db))))))
(defn after
"Interceptor factory which runs a given function `f` in the \"after\"
position presumably for side effects.
`f` is called with two arguments: the `effects` value of `:db` and the event. It's return
value is ignored so `f` can only side-effect. Example uses:
- `f` runs schema validation (reporting any errors found)
- `f` writes some aspect of db to localstorage."
[f]
(->interceptor
:name :after
:after (fn after-after
[context]
(let [db (get-effect context :db)
event (get-coeffect context :event)]
(f db event) ;; call f for side effects
context)))) ;; context is unchanged
(defn on-changes
"Interceptor factory which acts a bit like `reaction` (but it flows into `db`, rather than out)
It observes N paths in `db` and if any of them test not indentical? to their previous value
(as a result of a handler being run) then it runs 'f' to compute a new value, which is
then assoced into the given `out-path` within `db`.
Usage:
(defn my-f
[a-val b-val]
... some computation on a and b in here)
(on-changes my-f [:c] [:a] [:b])
Put this Interceptor on the right handlers (ones which might change :a or :b).
It will:
- call 'f' each time the value at path [:a] or [:b] changes
- call 'f' with the values extracted from [:a] [:b]
- assoc the return value from 'f' into the path [:c]
"
[f out-path & in-paths]
(->interceptor
:name :enrich
:after (fn on-change-after
[context]
(let [new-db (get-effect context :db)
old-db (get-coeffect context :db)
;; work out if any "inputs" have changed
new-ins (map #(get-in new-db %) in-paths)
old-ins (map #(get-in old-db %) in-paths)
changed-ins? (some false? (map identical? new-ins old-ins))]
;; if one of the inputs has changed, then run 'f'
(if changed-ins?
(->> (apply f new-ins)
(assoc-in new-db out-path)
(assoc-effect context :db))
context)))))

View File

@ -36,7 +36,7 @@
(let [cache-key [query-v dynv]]
;; when this reaction is nolonger being used, remove it from the cache
(add-on-dispose! r #(do (swap! query->reaction dissoc cache-key)
(console :log "Removing subscription: " cache-key))) ;; XXX remove console debug
#_(console :log "Removing subscription: " cache-key))) ;; XXX remove console debug
;; cache this reaction, so it can be used to deduplicate other, later "=" subscriptions
(swap! query->reaction assoc cache-key r)
r)) ;; return the actual reaction
@ -54,18 +54,18 @@
"Returns a Reagent/reaction which contains a computation"
([query-v]
(if-let [cached (cache-lookup query-v)]
(do (console :log "Using cached subscription: " query-v)
(do ;(console :log "Using cached subscription: " query-v)
cached)
(let [query-id (first-in-vector query-v)
handler-fn (get-handler kind query-id)]
(console :log "Subscription created: " query-v)
;(console :log "Subscription created: " query-v)
(if-not handler-fn
(console :error "re-frame: no subscription handler registered for: \"" query-id "\". Returning a nil subscription."))
(cache-and-return query-v [] (handler-fn app-db query-v)))))
([v dynv]
(if-let [cached (cache-lookup v dynv)]
(do (console :log "Using cached subscription: " v " and " dynv)
(do ;(console :log "Using cached subscription: " v " and " dynv)
cached)
(let [query-id (first-in-vector v)
handler-fn (get-handler kind query-id)]
@ -78,7 +78,7 @@
sub (make-reaction (fn [] (handler-fn app-db v @dyn-vals)))]
;; handler-fn returns a reaction which is then wrapped in the sub reaction
;; need to double deref it to get to the actual value.
(console :log "Subscription created: " v dynv)
;(console :log "Subscription created: " v dynv)
(cache-and-return v dynv (make-reaction (fn [] @@sub)))))))))
;; -- Helper code for register-pure -------------------

View File

@ -1,8 +1,8 @@
(ns re-frame.interceptor-test
(:require [cljs.test :refer-macros [is deftest]]
[reagent.ratom :refer [atom]]
[re-frame.interceptor :refer [context get-coeffect assoc-effect assoc-coeffect get-effect
trim-v path on-changes
[re-frame.interceptor :refer [context get-coeffect assoc-effect assoc-coeffect get-effect]]
[re-frame.std-interceptors :refer [trim-v path on-changes
db-handler->interceptor fx-handler->interceptor]]))
(enable-console-print!)