Version 0.8.0-alpha10
This commit is contained in:
parent
f78541af59
commit
c8659d6770
|
@ -3,7 +3,7 @@
|
||||||
[cljs.spec :as s]))
|
[cljs.spec :as s]))
|
||||||
|
|
||||||
|
|
||||||
;; -- Spec -----------------------------------------------------------------
|
;; -- Spec --------------------------------------------------------------------
|
||||||
;;
|
;;
|
||||||
;; This is a clojure.spec specification which documents the structure of app-db
|
;; This is a clojure.spec specification which documents the structure of app-db
|
||||||
;; See: http://clojure.org/guides/spec
|
;; 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.
|
;; 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
|
(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.
|
(cljs.reader/read-string) ;; stored as an EDN map.
|
||||||
(into (sorted-map)) ;; map -> sorted-map
|
(into (sorted-map)) ;; map -> sorted-map
|
||||||
(hash-map :todos))) ;; access via the :todos key
|
(hash-map :todos))) ;; access via the :todos key
|
||||||
|
@ -65,5 +65,5 @@
|
||||||
(defn todos->local-store
|
(defn todos->local-store
|
||||||
"Puts todos into localStorage"
|
"Puts todos into localStorage"
|
||||||
[todos]
|
[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
|
||||||
|
|
||||||
|
|
|
@ -14,8 +14,7 @@
|
||||||
"throw an exception if db doesn't match the spec."
|
"throw an exception if db doesn't match the spec."
|
||||||
[a-spec db]
|
[a-spec db]
|
||||||
(when-not (s/valid? a-spec db)
|
(when-not (s/valid? a-spec db)
|
||||||
(throw (ex-info "spec check failed: " {:problems
|
(throw (ex-info (str "spec check failed: " (s/explain-str a-spec db)) {}))))
|
||||||
(s/explain-str a-spec db)}))))
|
|
||||||
|
|
||||||
;; Event handlers change state, that's their job. But what happens if there's
|
;; 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
|
;; a bug which corrupts app state in some subtle way? This interceptor is run after
|
||||||
|
|
|
@ -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."
|
: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"}
|
:license {:name "MIT"}
|
||||||
|
|
|
@ -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.))))
|
|
@ -3,11 +3,13 @@
|
||||||
[re-frame.events :as events]
|
[re-frame.events :as events]
|
||||||
[re-frame.subs :as subs]
|
[re-frame.subs :as subs]
|
||||||
[re-frame.fx :as fx]
|
[re-frame.fx :as fx]
|
||||||
|
[re-frame.cofx :as cofx]
|
||||||
[re-frame.router :as router]
|
[re-frame.router :as router]
|
||||||
[re-frame.loggers :as loggers]
|
[re-frame.loggers :as loggers]
|
||||||
[re-frame.registrar :as registrar]
|
[re-frame.registrar :as registrar]
|
||||||
[re-frame.interceptor :as interceptor :refer [base
|
[re-frame.interceptor :as interceptor]
|
||||||
db-handler->interceptor fx-handler->interceptor
|
[re-frame.std-interceptors :as std-interceptors :refer [db-handler->interceptor
|
||||||
|
fx-handler->interceptor
|
||||||
ctx-handler->interceptor]]))
|
ctx-handler->interceptor]]))
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,14 +18,14 @@
|
||||||
(def dispatch-sync router/dispatch-sync)
|
(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 clear all handlers
|
||||||
;; XXX add a push handlers for testing purposes
|
;; XXX for testing purposes, and a way to snapshot re-frame instance. Then re-instate
|
||||||
;; XXX Add ->interceptor assoc-coeffect etc to API
|
|
||||||
;; XXX on figwheel reload, should invalidate all re-frame subscriptions
|
;; XXX on figwheel reload, should invalidate all re-frame subscriptions
|
||||||
|
|
||||||
|
|
||||||
;; -- interceptor related
|
;; -- interceptor related
|
||||||
|
;; useful if you are writing your own interceptors
|
||||||
(def ->interceptor interceptor/->interceptor)
|
(def ->interceptor interceptor/->interceptor)
|
||||||
(def enqueue interceptor/enqueue)
|
(def enqueue interceptor/enqueue)
|
||||||
(def get-coeffect interceptor/get-coeffect)
|
(def get-coeffect interceptor/get-coeffect)
|
||||||
|
@ -33,15 +35,15 @@
|
||||||
|
|
||||||
|
|
||||||
;; -- standard interceptors
|
;; -- standard interceptors
|
||||||
(def debug interceptor/debug)
|
(def debug std-interceptors/debug)
|
||||||
(def path interceptor/path)
|
(def path std-interceptors/path)
|
||||||
(def enrich interceptor/enrich)
|
(def enrich std-interceptors/enrich)
|
||||||
(def trim-v interceptor/trim-v)
|
(def trim-v std-interceptors/trim-v)
|
||||||
(def after interceptor/after)
|
(def after std-interceptors/after)
|
||||||
(def on-changes interceptor/on-changes)
|
(def on-changes std-interceptors/on-changes)
|
||||||
|
|
||||||
|
|
||||||
;; -- subscribe
|
;; -- subscriptions: reading and writing
|
||||||
(def reg-sub-raw subs/register-raw)
|
(def reg-sub-raw subs/register-raw)
|
||||||
(def reg-sub subs/reg-sub)
|
(def reg-sub subs/reg-sub)
|
||||||
(def subscribe subs/subscribe)
|
(def subscribe subs/subscribe)
|
||||||
|
@ -50,40 +52,43 @@
|
||||||
(def reg-fx fx/register)
|
(def reg-fx fx/register)
|
||||||
(def clear-fx (partial registrar/clear-handlers fx/kind))
|
(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
|
;; -- Events
|
||||||
|
|
||||||
;; usage (clear-event! :some-id)
|
;; 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
|
(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` and an interceptor chain.
|
||||||
`db-handler` is a function: (db event) -> db
|
`db-handler` is a function: (db event) -> db
|
||||||
`interceptors` is a collection of interceptors, possibly nested (needs flattenting).
|
`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
|
`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]
|
([id db-handler]
|
||||||
(reg-event-db id nil db-handler))
|
(reg-event-db id nil db-handler))
|
||||||
([id interceptors 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
|
(defn reg-event-fx
|
||||||
([id fx-handler]
|
([id fx-handler]
|
||||||
(reg-event-fx id nil fx-handler))
|
(reg-event-fx id nil fx-handler))
|
||||||
([id interceptors 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
|
(defn reg-event-ctx
|
||||||
([id handler]
|
([id handler]
|
||||||
(reg-event-ctx id nil handler))
|
(reg-event-ctx id nil handler))
|
||||||
([id interceptors 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 -----
|
;; -- Logging -----
|
||||||
|
|
|
@ -2,32 +2,45 @@
|
||||||
(:require
|
(:require
|
||||||
[re-frame.router :as router]
|
[re-frame.router :as router]
|
||||||
[re-frame.db :refer [app-db]]
|
[re-frame.db :refer [app-db]]
|
||||||
|
[re-frame.interceptor :refer [->interceptor]]
|
||||||
|
[re-frame.interop :refer [set-timeout!]]
|
||||||
[re-frame.events :as events]
|
[re-frame.events :as events]
|
||||||
[re-frame.registrar :refer [get-handler clear-handlers register-handler]]
|
[re-frame.registrar :refer [get-handler clear-handlers register-handler]]
|
||||||
[re-frame.interop :refer [ratom? set-timeout!]]
|
|
||||||
[re-frame.loggers :refer [console]]))
|
[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 ------------------------------------------------------------
|
;; -- Registration ------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
(def kind :fx)
|
(def kind :fx)
|
||||||
(assert (re-frame.registrar/kinds kind))
|
(assert (re-frame.registrar/kinds kind))
|
||||||
|
(def register (partial register-handler kind))
|
||||||
|
|
||||||
(defn register
|
;; -- Interceptor -------------------------------------------------------------
|
||||||
[id handler-fn]
|
|
||||||
(register-handler kind id handler-fn))
|
|
||||||
|
|
||||||
|
(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 --------------------------------------
|
;; -- Standard Builtin Effects Handlers --------------------------------------
|
||||||
|
|
||||||
|
|
||||||
;; :dispatch-later
|
;; :dispatch-later
|
||||||
;;
|
;;
|
||||||
;; `dispatch` one or more events in given times on the future. Expects a collection
|
;; `dispatch` one or more events in given times on the future. Expects a collection
|
||||||
|
|
|
@ -2,16 +2,13 @@
|
||||||
(:require
|
(:require
|
||||||
[re-frame.interop :refer [ratom?]]
|
[re-frame.interop :refer [ratom?]]
|
||||||
[re-frame.loggers :refer [console]]
|
[re-frame.loggers :refer [console]]
|
||||||
[re-frame.interop :refer [empty-queue debug-enabled?]]
|
[re-frame.interop :refer [empty-queue debug-enabled?]]))
|
||||||
[re-frame.db :refer [app-db]]
|
|
||||||
[re-frame.registrar :as registrar]
|
|
||||||
[clojure.data :as data]))
|
|
||||||
|
|
||||||
|
|
||||||
|
;; XXX use defrecord ??
|
||||||
|
|
||||||
(def mandatory-interceptor-keys #{:name :after :before})
|
(def mandatory-interceptor-keys #{:name :after :before})
|
||||||
|
|
||||||
;; XXX use defrecord ??
|
|
||||||
|
|
||||||
(defn interceptor?
|
(defn interceptor?
|
||||||
[m]
|
[m]
|
||||||
|
@ -26,17 +23,12 @@
|
||||||
(if-let [unknown-keys (seq (clojure.set/difference
|
(if-let [unknown-keys (seq (clojure.set/difference
|
||||||
(-> m keys set)
|
(-> m keys set)
|
||||||
mandatory-interceptor-keys))]
|
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)
|
{:name (or name :unnamed)
|
||||||
:before before
|
:before before
|
||||||
:after after })
|
:after after })
|
||||||
|
|
||||||
;; -- Helpers ------------------------------------------------------------------------------------
|
;; -- Effect Helpers -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
(defn get-coeffect
|
|
||||||
[context key]
|
|
||||||
(get-in context [:coeffects key]))
|
|
||||||
|
|
||||||
(defn get-effect
|
(defn get-effect
|
||||||
([context key]
|
([context key]
|
||||||
|
@ -44,33 +36,29 @@
|
||||||
([context]
|
([context]
|
||||||
(:effects context)))
|
(:effects context)))
|
||||||
|
|
||||||
|
|
||||||
(defn assoc-effect
|
(defn assoc-effect
|
||||||
[context key value]
|
[context key value]
|
||||||
(assoc-in context [:effects key] value))
|
(assoc-in context [:effects key] value))
|
||||||
|
|
||||||
|
;; -- CoEffect Helpers ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(defn get-coeffect
|
||||||
|
[context key]
|
||||||
|
(get-in context [:coeffects key]))
|
||||||
|
|
||||||
(defn assoc-coeffect
|
(defn assoc-coeffect
|
||||||
[context key value]
|
[context key value]
|
||||||
(assoc-in context [:coeffects key] value))
|
(assoc-in context [:coeffects key] value))
|
||||||
|
|
||||||
|
|
||||||
;; -- Execute Interceptor Chain ------------------------------------------------------------------
|
;; -- Execute Interceptor Chain ------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
(defn- invoke-fn
|
(defn- invoke-interceptor-fn
|
||||||
[context interceptor direction]
|
[context interceptor direction]
|
||||||
(if-let [f (get interceptor direction)]
|
(if-let [f (get interceptor direction)]
|
||||||
(f context)
|
(f context)
|
||||||
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
|
(defn- invoke-interceptors
|
||||||
"Loop over all interceptors, calling `direction` function on each,
|
"Loop over all interceptors, calling `direction` function on each,
|
||||||
|
@ -80,31 +68,41 @@
|
||||||
|
|
||||||
Each iteration, the next interceptor to process is obtained from
|
Each iteration, the next interceptor to process is obtained from
|
||||||
context's `:queue`. After they are processed, interceptors are popped
|
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
|
After sufficient iteration, `:queue` will be empty, and `:stack` will
|
||||||
contain all interceptors processed.
|
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]
|
([context direction]
|
||||||
(loop [context context]
|
(loop [context context]
|
||||||
(let [queue (:queue context)] ;; future interceptors
|
(let [queue (:queue context)] ;; future interceptors
|
||||||
(if (empty? queue)
|
(if (empty? queue)
|
||||||
context
|
context
|
||||||
(let [interceptor (peek queue)
|
(let [interceptor (peek queue) ;; next interceptor to call
|
||||||
stack (:stack context)] ;; already completed interceptors
|
stack (:stack context)] ;; already completed interceptors
|
||||||
(recur (-> context
|
(recur (-> context
|
||||||
(assoc :queue (pop queue))
|
(assoc :queue (pop queue))
|
||||||
(assoc :stack (conj stack interceptor))
|
(assoc :stack (conj stack interceptor))
|
||||||
(invoke-fn interceptor direction)))))))))
|
(invoke-interceptor-fn interceptor direction)))))))))
|
||||||
|
|
||||||
|
|
||||||
(defn enqueue
|
(defn enqueue
|
||||||
"Adds a collection of interceptors to the end of context's execution queue.
|
"Add a collection of `interceptors` to the end of `context's` execution `:queue`.
|
||||||
Returns updated context.
|
Returns the updated `context`.
|
||||||
|
|
||||||
In advanced cases, where an interceptor itself wanted to add to the queue,
|
In an advanced case, this function would allow an interceptor could add new
|
||||||
it would call this function (on the context provided to it)"
|
interceptors to the `:queue` of a context."
|
||||||
[context interceptors]
|
[context interceptors]
|
||||||
(update context :queue
|
(update context :queue
|
||||||
(fnil into empty-queue)
|
(fnil into empty-queue)
|
||||||
|
@ -112,386 +110,81 @@
|
||||||
|
|
||||||
|
|
||||||
(defn- context
|
(defn- context
|
||||||
"Return a fresh context"
|
"Create a fresh context"
|
||||||
([event interceptors]
|
([event interceptors]
|
||||||
(-> {}
|
(-> {}
|
||||||
(assoc-coeffect :event event)
|
(assoc-coeffect :event event)
|
||||||
(enqueue interceptors)))
|
(enqueue interceptors)))
|
||||||
|
([event interceptors db] ;; only used in tests, probably a hack, remove ? XXX
|
||||||
([event interceptors db]
|
|
||||||
(-> (context event interceptors)
|
(-> (context event interceptors)
|
||||||
(assoc-coeffect :db db))))
|
(assoc-coeffect :db db))))
|
||||||
|
|
||||||
|
|
||||||
(defn- change-direction
|
(defn- change-direction
|
||||||
"Called on completion of `:before` processing, this function prepares/modifies
|
"Called on completion of `:before` processing, this function prepares/modifies
|
||||||
`context` for the backwards sweep of processing in which `:after` fns are
|
`context` for the backwards sweep of processing in which an interceptor
|
||||||
called.
|
chain's `:after` fns are called.
|
||||||
|
|
||||||
At this point in processing, the `:queue` is empty and `:stack` holds all
|
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`
|
the previously run interceptors. So this function enables the backwards walk
|
||||||
with the reverse of what's in `:stack`"
|
by priming `:queue` with what's currently in `:stack`"
|
||||||
[context]
|
[context]
|
||||||
(-> context
|
(-> context
|
||||||
(dissoc :queue)
|
(dissoc :queue)
|
||||||
(enqueue (-> context :stack ))))
|
(enqueue (:stack context))))
|
||||||
|
|
||||||
|
|
||||||
(defn execute
|
(defn execute
|
||||||
"Executes a queue of interceptors for a given event.
|
"Executes the given chain (coll) of interceptors.
|
||||||
|
|
||||||
An interceptor has this form:
|
Each interceptor has this form:
|
||||||
{:before (fn [context] ...) ;; identity would be a noop
|
{:before (fn [context] ...) ;; returns possibly modified context
|
||||||
:after (fn [context] ...)}
|
:after (fn [context] ...)} ;; `identity` would be a noop
|
||||||
|
|
||||||
Walk the queue of iterceptors from beginning to end calling the `:before` fn on
|
Walks the queue of iterceptors from beginning to end, calling the
|
||||||
each, then reverse direction, and walk backwards, calling the `:after` fn on each.
|
`: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:
|
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>}
|
:db <original contents of app-db>}
|
||||||
:effects {:db <new value for 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>
|
:queue <a collection of further interceptors>
|
||||||
:stack <a collection of interceptors already walked>}
|
:stack <a collection of interceptors already walked>}
|
||||||
|
|
||||||
`context` has `:coeffects` and `:effects` which, if this was a web server, would
|
`context` has `:coeffects` and `:effects` which, if this was a web
|
||||||
be somewhat anologous to `request` and `response`.
|
server, would be somewhat anologous to `request` and `response`
|
||||||
|
respectively.
|
||||||
|
|
||||||
`coeffects` contains information like `event` and the initial state of `db` - ie. the
|
`coeffects` will contain information like `event` and the initial
|
||||||
inputs required by the event handler (sitting presumably on the end of the chain),
|
state of `db` - ie. the inputs required by the event handler
|
||||||
while handler-required side effects are assoc-ed in `:effects` including, but not limited
|
(sitting presumably on the end of the chain), while handler-returned
|
||||||
to, new values for `db`.
|
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
|
The first few interceptor in a chain will likely have `:before`
|
||||||
which adds the current state of app-db into `:coeffects`. OR, it might instead
|
functions which \"prime\" the `context` by adding the event, and
|
||||||
add the connection for a DataScript database, or any other inputs required.
|
the current state of app-db into `:coeffects`. OR, it might instead
|
||||||
And subsequent interceptors may further add to coeffects via their :before too.
|
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
|
Equally, some interceptors in the chain will have `:after` fn
|
||||||
all the side effects accumulated into `:effects` including but, not limited to,
|
which can process the side effects accumulated into `:effects`
|
||||||
updates to app-db.
|
including but, not limited to, updates to app-db.
|
||||||
|
|
||||||
Through both stages (before and after), `context` contains a `:queue` of interceptors yet to be
|
Through both stages (before and after), `context` contains a `:queue`
|
||||||
processed, and a `:stack` of interceptors already done. In advanced cases,
|
of interceptors yet to be processed, and a `:stack` of interceptors
|
||||||
these values can be modified by the functions through which the context is threaded."
|
already done. In advanced cases, these values can be modified by the
|
||||||
|
functions through which the context is threaded."
|
||||||
[event-v interceptors]
|
[event-v interceptors]
|
||||||
(-> (context event-v interceptors)
|
(-> (context event-v interceptors)
|
||||||
(invoke-interceptors :before)
|
(invoke-interceptors :before)
|
||||||
change-direction
|
change-direction
|
||||||
(invoke-interceptors :after)))
|
(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)))))
|
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
(ns re-frame.registrar
|
(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?]]
|
(:require [re-frame.interop :refer [debug-enabled?]]
|
||||||
[re-frame.loggers :refer [console]]))
|
[re-frame.loggers :refer [console]]))
|
||||||
|
|
||||||
|
|
||||||
;; kinds of handlers
|
;; kinds of handlers
|
||||||
(def kinds #{:event :fx :sub})
|
(def kinds #{:event :fx :cofx :sub})
|
||||||
|
|
||||||
;; This atom contains a register of all handlers.
|
;; This atom contains a register of all handlers.
|
||||||
;; Is a map keyed first by kind (of handler), and then id.
|
;; Is a map keyed first by kind (of handler), and then id.
|
||||||
|
|
|
@ -199,7 +199,6 @@
|
||||||
|
|
||||||
(-call-post-event-callbacks
|
(-call-post-event-callbacks
|
||||||
[_ event-v]
|
[_ event-v]
|
||||||
;; Call each registed post-event callback.
|
|
||||||
(doseq [callback (vals post-event-callback-fns)]
|
(doseq [callback (vals post-event-callback-fns)]
|
||||||
(callback event-v queue)))
|
(callback event-v queue)))
|
||||||
|
|
||||||
|
|
|
@ -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)))))
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
(let [cache-key [query-v dynv]]
|
(let [cache-key [query-v dynv]]
|
||||||
;; when this reaction is nolonger being used, remove it from the cache
|
;; when this reaction is nolonger being used, remove it from the cache
|
||||||
(add-on-dispose! r #(do (swap! query->reaction dissoc cache-key)
|
(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
|
;; cache this reaction, so it can be used to deduplicate other, later "=" subscriptions
|
||||||
(swap! query->reaction assoc cache-key r)
|
(swap! query->reaction assoc cache-key r)
|
||||||
r)) ;; return the actual reaction
|
r)) ;; return the actual reaction
|
||||||
|
@ -54,18 +54,18 @@
|
||||||
"Returns a Reagent/reaction which contains a computation"
|
"Returns a Reagent/reaction which contains a computation"
|
||||||
([query-v]
|
([query-v]
|
||||||
(if-let [cached (cache-lookup 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)
|
cached)
|
||||||
(let [query-id (first-in-vector query-v)
|
(let [query-id (first-in-vector query-v)
|
||||||
handler-fn (get-handler kind query-id)]
|
handler-fn (get-handler kind query-id)]
|
||||||
(console :log "Subscription created: " query-v)
|
;(console :log "Subscription created: " query-v)
|
||||||
(if-not handler-fn
|
(if-not handler-fn
|
||||||
(console :error "re-frame: no subscription handler registered for: \"" query-id "\". Returning a nil subscription."))
|
(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)))))
|
(cache-and-return query-v [] (handler-fn app-db query-v)))))
|
||||||
|
|
||||||
([v dynv]
|
([v dynv]
|
||||||
(if-let [cached (cache-lookup 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)
|
cached)
|
||||||
(let [query-id (first-in-vector v)
|
(let [query-id (first-in-vector v)
|
||||||
handler-fn (get-handler kind query-id)]
|
handler-fn (get-handler kind query-id)]
|
||||||
|
@ -78,7 +78,7 @@
|
||||||
sub (make-reaction (fn [] (handler-fn app-db v @dyn-vals)))]
|
sub (make-reaction (fn [] (handler-fn app-db v @dyn-vals)))]
|
||||||
;; handler-fn returns a reaction which is then wrapped in the sub reaction
|
;; handler-fn returns a reaction which is then wrapped in the sub reaction
|
||||||
;; need to double deref it to get to the actual value.
|
;; 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)))))))))
|
(cache-and-return v dynv (make-reaction (fn [] @@sub)))))))))
|
||||||
|
|
||||||
;; -- Helper code for register-pure -------------------
|
;; -- Helper code for register-pure -------------------
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
(ns re-frame.interceptor-test
|
(ns re-frame.interceptor-test
|
||||||
(:require [cljs.test :refer-macros [is deftest]]
|
(:require [cljs.test :refer-macros [is deftest]]
|
||||||
[reagent.ratom :refer [atom]]
|
[reagent.ratom :refer [atom]]
|
||||||
[re-frame.interceptor :refer [context get-coeffect assoc-effect assoc-coeffect get-effect
|
[re-frame.interceptor :refer [context get-coeffect assoc-effect assoc-coeffect get-effect]]
|
||||||
trim-v path on-changes
|
[re-frame.std-interceptors :refer [trim-v path on-changes
|
||||||
db-handler->interceptor fx-handler->interceptor]]))
|
db-handler->interceptor fx-handler->interceptor]]))
|
||||||
|
|
||||||
(enable-console-print!)
|
(enable-console-print!)
|
||||||
|
|
Loading…
Reference in New Issue