re-frame/src/re_frame/interceptor.cljc

199 lines
6.8 KiB
Plaintext
Raw Normal View History

(ns re-frame.interceptor
(:require
[re-frame.interop :refer [ratom?]]
[re-frame.loggers :refer [console]]
2016-08-08 05:17:01 +00:00
[re-frame.interop :refer [empty-queue debug-enabled?]]))
2016-08-08 05:17:01 +00:00
;; XXX use defrecord ??
(def mandatory-interceptor-keys #{:name :after :before})
(defn interceptor?
[m]
(and (map? m)
(= mandatory-interceptor-keys (-> m keys set))))
(defn ->interceptor
"Create an interceptor from named arguements"
[& {:as m :keys [name before after]}]
(when debug-enabled?
(if-let [unknown-keys (seq (clojure.set/difference
(-> m keys set)
mandatory-interceptor-keys))]
2016-08-08 05:17:01 +00:00
(console :error "re-frame: interceptor " name " has unknown keys: " unknown-keys)))
{:name (or name :unnamed)
:before before
:after after })
2016-08-08 05:17:01 +00:00
;; -- Effect Helpers -----------------------------------------------------------------------------
(defn get-effect
([context]
(:effects context))
([context key]
(get-in context [:effects key]))
([context key not-found]
(get-in context [:effects key] not-found)))
(defn assoc-effect
[context key value]
(assoc-in context [:effects key] value))
2016-08-08 05:17:01 +00:00
;; -- CoEffect Helpers ---------------------------------------------------------------------------
(defn get-coeffect
([context]
(:coeffects context))
([context key]
(get-in context [:coeffects key]))
([context key not-fount]
(get-in context [:coeffects key] not-fount)))
(defn assoc-coeffect
[context key value]
(assoc-in context [:coeffects key] value))
;; -- Execute Interceptor Chain ------------------------------------------------------------------
2016-08-08 05:17:01 +00:00
(defn- invoke-interceptor-fn
[context interceptor direction]
(if-let [f (get interceptor direction)]
(f context)
context))
(defn- invoke-interceptors
"Loop over all interceptors, calling `direction` function on each,
threading the value of `context` through every call.
`direction` is one of `:before` or `:after`.
Each iteration, the next interceptor to process is obtained from
context's `:queue`. After they are processed, interceptors are popped
2016-08-08 05:17:01 +00:00
from `:queue` and added to `:stack`.
After sufficient iteration, `:queue` will be empty, and `:stack` will
contain all interceptors processed.
2016-08-08 05:17:01 +00:00
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]
2016-08-08 05:17:01 +00:00
(let [queue (:queue context)] ;; future interceptors
(if (empty? queue)
context
2016-08-08 05:17:01 +00:00
(let [interceptor (peek queue) ;; next interceptor to call
2016-08-03 07:32:42 +00:00
stack (:stack context)] ;; already completed interceptors
(recur (-> context
(assoc :queue (pop queue))
(assoc :stack (conj stack interceptor))
2016-08-08 05:17:01 +00:00
(invoke-interceptor-fn interceptor direction)))))))))
(defn enqueue
2016-08-08 05:17:01 +00:00
"Add a collection of `interceptors` to the end of `context's` execution `:queue`.
Returns the updated `context`.
2016-08-08 05:17:01 +00:00
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)
interceptors))
(defn- context
2016-08-08 05:17:01 +00:00
"Create a fresh context"
([event interceptors]
(-> {}
(assoc-coeffect :event event)
(enqueue interceptors)))
2016-08-08 05:17:01 +00:00
([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
2016-08-08 05:17:01 +00:00
`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
2016-08-08 05:17:01 +00:00
the previously run interceptors. So this function enables the backwards walk
by priming `:queue` with what's currently in `:stack`"
[context]
(-> context
(dissoc :queue)
2016-08-08 05:17:01 +00:00
(enqueue (:stack context))))
(defn execute
2016-08-08 05:17:01 +00:00
"Executes the given chain (coll) of interceptors.
2016-08-08 05:17:01 +00:00
Each interceptor has this form:
{:before (fn [context] ...) ;; returns possibly modified context
:after (fn [context] ...)} ;; `identity` would be a noop
2016-08-08 05:17:01 +00:00
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.
2016-08-08 05:17:01 +00:00
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:
2016-08-08 05:17:01 +00:00
{:coeffects {:event [:a-query-id :some-param]
:db <original contents of app-db>}
2016-08-03 07:30:46 +00:00
:effects {:db <new value for app-db>
2016-08-08 05:17:01 +00:00
:dispatch [:an-event-id :param1]}
:queue <a collection of further interceptors>
:stack <a collection of interceptors already walked>}
2016-08-08 05:17:01 +00:00
`context` has `:coeffects` and `:effects` which, if this was a web
server, would be somewhat anologous to `request` and `response`
respectively.
2016-08-10 05:41:16 +00:00
`coeffects` will contain data like `event` and the initial
state of `db` - the inputs required by the event handler
2016-08-08 05:17:01 +00:00
(sitting presumably on the end of the chain), while handler-returned
2016-08-10 05:41:16 +00:00
side effects are put into `:effects` including, but not limited to,
2016-08-08 05:17:01 +00:00
new values for `db`.
2016-08-10 05:41:16 +00:00
The first few interceptors in a chain will likely have `:before`
2016-08-08 05:17:01 +00:00
functions which \"prime\" the `context` by adding the event, and
2016-08-10 05:41:16 +00:00
the current state of app-db into `:coeffects`. But interceptors can
add whatever they want to `:coeffect` - perhaps the event handler needs
some information from localstore, or a random number, or access to
a DataScript connection.
2016-08-08 05:17:01 +00:00
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."
[event-v interceptors]
(-> (context event-v interceptors)
(invoke-interceptors :before)
change-direction
(invoke-interceptors :after)))