2016-08-13 06:34:25 +00:00
|
|
|
## Introduction
|
|
|
|
|
2016-08-15 01:56:03 +00:00
|
|
|
This is an interceptors tutorial.
|
2016-08-13 06:34:25 +00:00
|
|
|
|
|
|
|
## Table Of Contents
|
|
|
|
|
|
|
|
- [Introduction](#introduction)
|
|
|
|
- [Table Of Contents](#table-of-contents)
|
|
|
|
- [Interceptors](#interceptors)
|
|
|
|
* [Why Interceptors?](#why-interceptors-)
|
|
|
|
* [What Do Interceptors Do?](#what-do-interceptors-do-)
|
|
|
|
* [Wait, I know That Pattern!](#wait--i-know-that-pattern-)
|
|
|
|
* [What's In The Pipeline?](#what-s-in-the-pipeline-)
|
|
|
|
* [Show Me](#show-me)
|
|
|
|
* [Handlers Are Interceptors Too](#handlers-are-interceptors-too)
|
|
|
|
- [Executing A Chain](#executing-a-chain)
|
|
|
|
* [The Links](#the-links)
|
|
|
|
* [What Is Context?](#what-is-context-)
|
|
|
|
* [Self Modifing](#self-modifing)
|
|
|
|
* [Credit](#credit)
|
|
|
|
* [Write An Interceptor](#write-an-interceptor)
|
|
|
|
* [Wrapping Handlers](#wrapping-handlers)
|
|
|
|
- [Summary](#summary)
|
|
|
|
- [Appendix](#appendix)
|
|
|
|
* [The Builtin Interceptors](#the-builtin-interceptors)
|
|
|
|
|
|
|
|
## Interceptors
|
2016-08-11 04:33:28 +00:00
|
|
|
### Why Interceptors?
|
|
|
|
|
2016-08-11 06:09:43 +00:00
|
|
|
Two reasons.
|
|
|
|
|
2016-08-11 13:24:39 +00:00
|
|
|
__First__, we want simple event handlers.
|
2016-08-11 04:33:28 +00:00
|
|
|
|
|
|
|
Interceptors can look after "cross-cutting" concerns like undo, tracing and validation.
|
|
|
|
They help us to factor out commonality, hide complexity and introduce further steps into the "Derived Data,
|
|
|
|
Flowing" story promoted by re-frame.
|
|
|
|
|
|
|
|
So, you'll want to use Interceptors - they're helpful.
|
|
|
|
|
2016-08-11 13:24:39 +00:00
|
|
|
__Second__, under the covers, Interceptors are the means by which
|
|
|
|
event handlers are executed (when you `dispatch`). You'll
|
|
|
|
want to understand how that happens.
|
2016-08-11 04:33:28 +00:00
|
|
|
|
2016-08-11 13:24:39 +00:00
|
|
|
### What Do Interceptors Do?
|
2016-08-11 04:33:28 +00:00
|
|
|
|
|
|
|
They wrap.
|
|
|
|
|
|
|
|
Specifically, they wrap event handlers.
|
|
|
|
|
2016-08-13 06:34:25 +00:00
|
|
|
Imagine your event handler is like a piece of ham. An interceptor would be
|
2016-08-11 04:33:28 +00:00
|
|
|
like bread either side of your ham, which makes a sandwich.
|
|
|
|
|
|
|
|
And two Interceptors, in a chain, would be like you put another
|
|
|
|
pair of bread slices around the outside of the existing sandwich to make
|
|
|
|
a sandwich of the sandwich. Now it is a very thick sandwich.
|
|
|
|
|
|
|
|
Interceptors wrap on both sides of a handler, layer after layer.
|
|
|
|
|
|
|
|
### Wait, I know That Pattern!
|
|
|
|
|
|
|
|
Interceptors implement middleware. But differently.
|
|
|
|
|
2016-08-11 13:24:39 +00:00
|
|
|
Traditional middleware - often seen in web servers - creates a data
|
2016-08-11 04:33:28 +00:00
|
|
|
processing pipeline via the nested composition of higher order functions.
|
|
|
|
The result is a "stack" of functions. Data flows through this pipeline,
|
|
|
|
first forwards from one end to the other, and then backwards.
|
|
|
|
|
|
|
|
Interceptors achieve the same outcome by assembling functions, as data,
|
|
|
|
in a collection (a chain, rather than a stack). Data can then be iteratively
|
|
|
|
pipelined, first forwards through the functions in the chain,
|
|
|
|
and then backwards along the same chain.
|
|
|
|
|
|
|
|
Because the interceptor pipeline is composed via data, rather than
|
|
|
|
higher order functions, it is a more flexible arrangement.
|
|
|
|
|
|
|
|
### What's In The Pipeline?
|
|
|
|
|
2016-08-11 13:24:39 +00:00
|
|
|
Data. It flows through the pipeline being progressively transformed.
|
2016-08-11 04:33:28 +00:00
|
|
|
|
|
|
|
Fine. But what data?
|
|
|
|
|
|
|
|
With a web server, the middleware "stack" progressively
|
|
|
|
transforms a `request` in one direction, and, then in the backwards
|
|
|
|
sweep, it progressively produces a `response`.
|
|
|
|
|
|
|
|
In re-frame, the forwards sweep progressively creates the `coeffects`
|
|
|
|
(inputs to the handler), while the backwards sweep processes the `effects`
|
|
|
|
(outputs from the handler).
|
|
|
|
|
|
|
|
I'll pause while you read that sentence again. That's the key
|
|
|
|
concept, right there.
|
|
|
|
|
2016-08-11 06:09:43 +00:00
|
|
|
### Show Me
|
2016-08-11 04:33:28 +00:00
|
|
|
|
|
|
|
You can provide a chain of interceptors when
|
2016-08-11 13:24:39 +00:00
|
|
|
you register an event handler.
|
2016-08-11 04:33:28 +00:00
|
|
|
|
|
|
|
Using a 3-arity registration function:
|
|
|
|
```clj
|
|
|
|
(reg-event-db
|
|
|
|
:some-id
|
|
|
|
[in1 in2] ;; <--- a chain of 2 interceptors
|
2016-08-11 06:09:43 +00:00
|
|
|
(fn [db v] ;; <-- the handler here, as before
|
2016-08-11 04:33:28 +00:00
|
|
|
....)))
|
|
|
|
```
|
|
|
|
|
2016-08-13 06:34:25 +00:00
|
|
|
> Each Event Handler can have its own tailored interceptor chain, provided at registration-time.
|
2016-08-11 04:33:28 +00:00
|
|
|
|
2016-08-11 13:24:39 +00:00
|
|
|
### Handlers Are Interceptors Too
|
2016-08-11 04:33:28 +00:00
|
|
|
|
2016-08-11 06:09:43 +00:00
|
|
|
You might see that registration above as associating `:some-id` with two things: (1) a chain of interceptors
|
2016-08-11 04:33:28 +00:00
|
|
|
and (2) a handler.
|
|
|
|
|
2016-08-11 06:09:43 +00:00
|
|
|
Except, the handler is turned into an interceptor too. (We'll see how later)
|
2016-08-11 04:33:28 +00:00
|
|
|
|
2016-08-11 06:09:43 +00:00
|
|
|
So `:some-id` is only associated with one thing: a 3-chain of interceptors,
|
2016-08-11 04:33:28 +00:00
|
|
|
with the handler wrapped in an interceptor and put on the end of the other two.
|
|
|
|
|
2016-08-13 06:34:25 +00:00
|
|
|
Except, the registration function itself, `reg-event-db`, actually takes this 3-chain
|
|
|
|
and inserts its own interceptors
|
2016-08-11 04:33:28 +00:00
|
|
|
(which do useful things) at the front (more on this soon too),
|
2016-08-13 06:34:25 +00:00
|
|
|
so ACTUALLY, there's about 5 interceptors in the chain.
|
2016-08-11 04:33:28 +00:00
|
|
|
|
2016-08-13 06:34:25 +00:00
|
|
|
So, ultimately, that event registration associates the event id `some-id`
|
|
|
|
with a chain of interceptors.
|
2016-08-11 06:09:43 +00:00
|
|
|
|
|
|
|
Later, when a `dispatch` for `:some-id` is done, that 5-chain of
|
2016-08-13 06:34:25 +00:00
|
|
|
interceptors will be "executed". And that's how events get handled.
|
|
|
|
|
2016-08-11 04:33:28 +00:00
|
|
|
|
2016-08-13 06:34:25 +00:00
|
|
|
## Executing A Chain
|
|
|
|
|
|
|
|
### The Links
|
2016-08-11 04:33:28 +00:00
|
|
|
|
|
|
|
Each interceptor has this form:
|
|
|
|
```clj
|
2016-08-11 06:09:43 +00:00
|
|
|
{:id :something ;; decorative only
|
2016-08-11 04:33:28 +00:00
|
|
|
:before (fn [context] ...) ;; returns possibly modified context
|
|
|
|
:after (fn [context] ...)} ;; `identity` would be a noop
|
|
|
|
```
|
|
|
|
|
|
|
|
That's essentially a map of two functions. Now imagine a vector of these maps - that's an
|
2016-08-13 06:34:25 +00:00
|
|
|
an interceptor chain.
|
2016-08-11 04:33:28 +00:00
|
|
|
|
2016-08-11 06:09:43 +00:00
|
|
|
To "execute" an interceptor chain:
|
2016-08-11 04:33:28 +00:00
|
|
|
1. create a `context` (a map, described below)
|
2016-08-11 06:09:43 +00:00
|
|
|
2. iterate forwards over the chain, calling the `:before` function on each interceptor
|
|
|
|
3. iterate over the chain in the opposite direction calling the `:after` function on each interceptor
|
2016-08-11 04:33:28 +00:00
|
|
|
|
|
|
|
Remember that the last interceptor in the chain is the handler itself (wrapped up to be the `:before`).
|
|
|
|
|
2016-08-11 06:09:43 +00:00
|
|
|
That's it. That's how an event gets handled.
|
|
|
|
|
|
|
|
|
|
|
|
### What Is Context?
|
2016-08-11 04:33:28 +00:00
|
|
|
|
2016-08-11 06:09:43 +00:00
|
|
|
Some data called a `context` is threaded through all the calls.
|
|
|
|
|
2016-08-11 13:24:39 +00:00
|
|
|
This value is passed as the argument to every `:before` and `:after`
|
|
|
|
function and they returned it, possibly modified.
|
2016-08-11 04:33:28 +00:00
|
|
|
|
2016-08-11 13:24:39 +00:00
|
|
|
A `context` is a map with this structure:
|
2016-08-11 04:33:28 +00:00
|
|
|
```clj
|
2016-08-11 06:09:43 +00:00
|
|
|
{:coeffects {:event [:some-id :some-param]
|
2016-08-11 04:33:28 +00:00
|
|
|
:db <original contents of app-db>}
|
2016-08-11 06:09:43 +00:00
|
|
|
|
2016-08-11 04:33:28 +00:00
|
|
|
:effects {:db <new value for app-db>
|
|
|
|
:dispatch [:an-event-id :param1]}
|
2016-08-11 06:09:43 +00:00
|
|
|
|
2016-08-11 04:33:28 +00:00
|
|
|
:queue <a collection of further interceptors>
|
|
|
|
:stack <a collection of interceptors already walked>}
|
|
|
|
```
|
|
|
|
|
|
|
|
`context` has `:coeffects` and `:effects` which, if this was a web
|
2016-08-11 06:09:43 +00:00
|
|
|
server, would be somewhat analogous to `request` and `response`
|
2016-08-11 04:33:28 +00:00
|
|
|
respectively.
|
|
|
|
|
2016-08-11 13:24:39 +00:00
|
|
|
`:coeffects` will contain the inputs required by the event handler
|
|
|
|
(sitting presumably on the end of the chain). So that's
|
|
|
|
data like the `:event` being processed, and the initial state of `db`. These are .
|
2016-08-11 04:33:28 +00:00
|
|
|
|
|
|
|
The handler-returned side effects are put into `:effects` including,
|
|
|
|
but not limited to, new values for `db`.
|
|
|
|
|
2016-08-11 06:09:43 +00:00
|
|
|
The first few interceptors in a chain (inserted by `reg-event-db`)
|
2016-08-11 04:33:28 +00:00
|
|
|
have `:before` functions which __prime__ the `:coeffects`
|
2016-08-11 06:09:43 +00:00
|
|
|
by adding in `:event`, and `:db`. Of course, other interceptors can
|
|
|
|
add further to `:coeffect`. Perhaps the event handler needs
|
2016-08-11 04:33:28 +00:00
|
|
|
data from localstore, or a random number, or a
|
|
|
|
DataScript connection. Interceptors can build up the coeffect, via their
|
|
|
|
`:before`.
|
|
|
|
|
|
|
|
Equally, some interceptors in the chain will have `:after` functions
|
2016-08-11 06:09:43 +00:00
|
|
|
which process the side effects accumulated into `:effects`
|
2016-08-11 04:33:28 +00:00
|
|
|
including but, not limited to, updates to app-db.
|
|
|
|
|
2016-08-13 06:34:25 +00:00
|
|
|
### Self Modifing
|
2016-08-11 04:33:28 +00:00
|
|
|
|
|
|
|
Through both stages (before and after), `context` contains a `:queue`
|
|
|
|
of interceptors yet to be processed, and a `:stack` of interceptors
|
2016-08-12 13:49:15 +00:00
|
|
|
already done.
|
2016-08-11 04:33:28 +00:00
|
|
|
|
2016-08-13 06:34:25 +00:00
|
|
|
In advanced cases, these values can be modified by the Interceptor
|
2016-08-11 04:33:28 +00:00
|
|
|
functions through which the `context` is threaded.
|
|
|
|
|
2016-08-11 06:09:43 +00:00
|
|
|
What I'm saying is that interceptors can be dynamically added
|
2016-08-13 06:34:25 +00:00
|
|
|
and removed from the `:queue` by existing Interceptors.
|
2016-08-11 04:33:28 +00:00
|
|
|
|
|
|
|
### Credit
|
|
|
|
|
2016-08-12 13:49:15 +00:00
|
|
|
> All truths are easy to understand once they are discovered <br>
|
|
|
|
> -- Galileo Galilei
|
2016-08-11 04:33:28 +00:00
|
|
|
|
2016-08-12 13:49:15 +00:00
|
|
|
This elegant and flexible arrangement was originally
|
|
|
|
designed by the talented
|
|
|
|
[Pedestal Team](https://github.com/pedestal/pedestal/blob/master/guides/documentation/service-interceptors.md). Thanks!
|
2016-08-11 04:33:28 +00:00
|
|
|
|
|
|
|
### Write An Interceptor
|
|
|
|
|
|
|
|
Dunno about you, but I'm easily offended by underscores.
|
|
|
|
|
|
|
|
If our components did this:
|
|
|
|
```clj
|
|
|
|
(dispatch [:delete-item 42])
|
|
|
|
```
|
|
|
|
|
|
|
|
We'd have to write this handler:
|
|
|
|
```clj
|
|
|
|
(def-event-db
|
|
|
|
:delete-item
|
|
|
|
(fn
|
|
|
|
[db [_ key-to-delete]] ;; <---- Arrgggghhh underscore
|
|
|
|
(dissoc db key-to-delete)))
|
|
|
|
```
|
|
|
|
|
2016-08-16 09:00:10 +00:00
|
|
|
Do you see it there? In the event destructuring!!! Almost mocking us with that
|
2016-08-11 06:09:43 +00:00
|
|
|
passive aggressive, understated thing it has going on!! Co-workers
|
2016-08-11 04:33:28 +00:00
|
|
|
have said I'm "being overly sensitive", perhaps even horizontalist, but
|
|
|
|
you can see it, right? Of course you can.
|
|
|
|
|
2016-08-16 09:00:10 +00:00
|
|
|
What a relief it would be to rid of it, but how? We'll write an interceptor: `trim-event`
|
2016-08-11 04:33:28 +00:00
|
|
|
|
|
|
|
Once we have written `trim-event`, our registration will change to look like this:
|
|
|
|
```clj
|
|
|
|
(def-event-db
|
|
|
|
:delete-item
|
|
|
|
[trim-event] ;; <--- interceptor added
|
|
|
|
(fn
|
|
|
|
[db [key-to-delete]] ;; <--- no leading underscore necessary
|
|
|
|
(dissoc db key-to-delete)))
|
|
|
|
```
|
|
|
|
|
2016-08-16 09:00:10 +00:00
|
|
|
`trim-event` will need to change the `:coeffects` map (within `context`). More specifically, it will be
|
2016-08-11 13:24:39 +00:00
|
|
|
changing the `:event` value within the `:coeffects`.
|
2016-08-11 04:33:28 +00:00
|
|
|
|
2016-08-11 13:24:39 +00:00
|
|
|
`:event` will start off as `[:delete-item 42]`, but will end up `[42]`. `trim-event` will remove that
|
2016-08-11 04:33:28 +00:00
|
|
|
leading `:delete-item` because, by the time the event is
|
|
|
|
being processed, we already know what id is has.
|
|
|
|
|
2016-08-11 06:09:43 +00:00
|
|
|
And, here it is:
|
2016-08-11 04:33:28 +00:00
|
|
|
```clj
|
|
|
|
(def trim-event
|
|
|
|
(re-frame.core/->interceptor
|
2016-08-11 13:24:39 +00:00
|
|
|
:id :trim-event
|
2016-08-11 04:33:28 +00:00
|
|
|
:before (fn [context]
|
|
|
|
(let [new-event (-> context
|
|
|
|
:coeffects
|
2016-08-11 06:09:43 +00:00
|
|
|
:event ;; extra event from coeffects
|
2016-08-11 04:33:28 +00:00
|
|
|
rest ;; remove first element
|
|
|
|
vec) ;; list->vector
|
|
|
|
(assoc-in context [:coeffects :event] new-event)))))
|
|
|
|
```
|
|
|
|
|
|
|
|
As you read this, look back to what a `context` looks like.
|
|
|
|
|
|
|
|
Notes:
|
2016-08-11 06:09:43 +00:00
|
|
|
1. We use `->interceptor` to create an interceptor (but it just a map)
|
2016-08-11 04:33:28 +00:00
|
|
|
2. Our interceptor only has a `:before` function
|
|
|
|
3. Our `:before` is given `context`. It modifies it and returns it.
|
|
|
|
4. There is no `:after` for this Interceptor. It has nothing to do
|
|
|
|
with the backwards processing flow of effects. It is concerned only
|
|
|
|
with coeffects in the forward flow.
|
|
|
|
|
2016-08-13 06:34:25 +00:00
|
|
|
####Wrapping Handlers
|
2016-08-11 04:33:28 +00:00
|
|
|
|
2016-08-11 06:09:43 +00:00
|
|
|
We're going well. Let's do an advanced wrapping.
|
2016-08-11 04:33:28 +00:00
|
|
|
|
2016-08-11 06:09:43 +00:00
|
|
|
How would you wrap a handler in an interceptor?
|
|
|
|
|
|
|
|
There's two kinds of handler:
|
2016-08-11 04:33:28 +00:00
|
|
|
- the `-db` variety registered by `reg-event-db`
|
|
|
|
- the `-fx` variety registered by `reg-event-fx`
|
|
|
|
|
2016-08-16 09:00:10 +00:00
|
|
|
Let's do a `-db` variety. This is what a `-db` handler looks like:
|
2016-08-11 06:09:43 +00:00
|
|
|
```clj
|
|
|
|
(fn [db event] ;; takes two params
|
|
|
|
(assoc db :flag true)) ;; returns a new db
|
|
|
|
```
|
|
|
|
|
2016-08-16 09:00:10 +00:00
|
|
|
And here is a function which turns a given handler into an interceptor:
|
2016-08-11 04:33:28 +00:00
|
|
|
```clj
|
|
|
|
(defn db-handler->interceptor
|
|
|
|
[db-handler-fn]
|
|
|
|
(->interceptor
|
|
|
|
:id :db-handler
|
|
|
|
:before (fn [context]
|
|
|
|
(let [{:keys [db event]} (:coeffects context) ;; extract db and event from coeffects
|
|
|
|
new-db (db-handler-fn db event)] ;; call the handler
|
|
|
|
(assoc-in context [:effects :db] new-db)))))) ;; put db back into :effects
|
|
|
|
```
|
|
|
|
|
|
|
|
|
2016-08-13 06:34:25 +00:00
|
|
|
## Summary
|
2016-08-11 04:33:28 +00:00
|
|
|
|
2016-08-11 06:09:43 +00:00
|
|
|
In this tutorial, we've learned:
|
|
|
|
|
2016-08-16 09:00:10 +00:00
|
|
|
__1.__ When you register an event handler, you can supply a collection of interceptors:
|
2016-08-11 06:09:43 +00:00
|
|
|
```
|
|
|
|
(reg-event-db
|
|
|
|
:some-id
|
|
|
|
[in1 in2] ;; <--- a chain of 2 interceptors
|
|
|
|
(fn [db v] ;; <-- real handler here
|
|
|
|
....)))
|
|
|
|
```
|
|
|
|
|
2016-08-16 09:00:10 +00:00
|
|
|
__2.__ When you registering an event handler, you are associating an event id with a chain of interceptors including:
|
|
|
|
- the ones your supply
|
|
|
|
- an extra one on the end, which wraps the handler itself
|
|
|
|
- a couple at the beginning of the chain, put there by the `reg-event-db` or `reg-event-fx`.
|
2016-08-11 06:09:43 +00:00
|
|
|
|
2016-08-16 09:00:10 +00:00
|
|
|
__3.__ Interceptors do interesting things:
|
|
|
|
- add to coeffects (data inputs to the handler)
|
|
|
|
- process side effects (returned by a handler)
|
2016-08-11 13:24:39 +00:00
|
|
|
- produce logs
|
2016-08-13 06:34:25 +00:00
|
|
|
- further process
|
2016-08-11 04:33:28 +00:00
|
|
|
|
2016-08-16 09:00:10 +00:00
|
|
|
In the next Tutorial, we'll look at (side) Effects in more depth. Later again, we'll look at Coeffects.
|
2016-08-11 04:33:28 +00:00
|
|
|
|
|
|
|
|
2016-08-13 06:34:25 +00:00
|
|
|
## Appendix
|
2016-08-11 04:33:28 +00:00
|
|
|
|
2016-08-13 06:34:25 +00:00
|
|
|
### The Builtin Interceptors
|
2016-08-11 04:33:28 +00:00
|
|
|
|
|
|
|
re-frame comes with some builtin Interceptors:
|
|
|
|
- __debug__: log each event as it is processed. Shows incremental [`clojure.data/diff`](https://clojuredocs.org/clojure.data/diff) reports.
|
|
|
|
- __trim-v__: a convenience. More readable handlers.
|
|
|
|
|
|
|
|
And some Interceptor factories (functions that return Interceptors):
|
|
|
|
- __enrich__: perform additional computations (validations?), after the handler has run. More derived data flowing.
|
|
|
|
- __after__: perform side effects, after a handler has run. Eg: use it to report if the data in `app-db` matches a schema.
|
|
|
|
- __path__: a convenience. Simplifies our handlers. Acts almost like `update-in`.
|
|
|
|
|
|
|
|
In addition, [a Library like re-frame-undo](https://github.com/Day8/re-frame-undo) provides an Interceptor
|
|
|
|
factory called `undoable` which checkpoints app state.
|
|
|
|
|
|
|
|
|
|
|
|
To use them, first require them:
|
|
|
|
```Clojure
|
|
|
|
(ns my.core
|
|
|
|
(:require
|
|
|
|
[re-frame.core :refer [debug path]])
|
|
|
|
```
|