Actions provide a data-oriented way to manipulate your system state in a controlled and highly observable way.
An action is a persistent collection of effects, and (optionally) a map of headers.
(require '[clj-arsenal.action :refer [act << <<ctx decide with-inj] :as action])
(act {:key ::my-action}
[:http (<< :http/post "api/example" :json {:foo 1 :bar 2})]
[:patch {:path [:foo] :change [:value (<< :http/response :get :foo)]}])
; -> #clj-arsenal.action/Action{:headers {:key ::my-action} :effects ...}
; it's just a record
When dispatched, an action's effects are executed in order;
with their injections (the <<
forms) within each effect
being resolved before execution.
A dispatcher is a function which takes an action, wraps it in a context map, and runs said context through a series of interceptors. A minimal dispatcher can be created like this:
(defn executor
[context [effect-kind & args :as effect]]
;; execute effect
context)
(defn injector
[context {:keys [kind args] :as injection}]
;; return injected value
)
(defn context-builder
[context-basis & args]
;; return a context map
context-basis)
(def dispatch!
(action/dispatcher [(action/errors) (action/effects executor injector)]
:context-builder context-builder))
(dispatch!
(act {:key ::my-action}
[:http (<< :http/post "api/example" :json {:foo 1 :bar 2})]
[:patch {:path [:foo] :change [:value (<< :http/response :get :foo)]}])
...extra-args)
Here's what happens when an action is dispatched:
context-builder
is called to create an initial dispatch context.
It's passed a minimal context-stub
map and any extra arguments passed
to dispatch!
. The context-stub
is a minimal context, which can be
modified as needed, or returned unmodified. The context-builder
is
optional, if omitted then the minimal context-stub
will be used as
the initial dispatch context.injector
, then executes the effect with executor
.
If either injector
or executor
return a chainable (i.e async value), then
this too will be resolved before continuing the dispatch.The executor
returns a new (or the same) context map, or a chainable that
resolves to a context map.
The injector
returns the value to be injected, or a chainable that resolves
to this value.
Interceptors are maps with any of (all are optional):
::action/enter
- called with the context map when entering the interceptor.
Returns a new (or the same) context map, or a chainable that resolves to
a context map.::action/leave
- called with the context map when entering the interceptor.
Returns a new (or the same) context map, or a chainable that resolves to
a context map.::action/name
- the interceptor name, used to identify it in errors and such.The context-stub
, which is the default dispatch context, has the following:
::action/action
- the action being dispatched.::action/pending-effects
- a FIFO queue of pending effects.::action/pending-enter
- a FIFO queue of interceptors to enter.::action/pending-leave
- a vector of interceptors that have
been entered, and need to be left.These entries must be available in the context for dispatch to continue; however they can be manipulated by middleware.
When an error is thrown (in middleware, injector, executor, etc)
the error is added to ::action/errors
(in the context map), and
::action/pending-enter
is cleared. The errors map is a nested
map of the form interceptor-name -> interceptor-stage -> error
.
Where interceptor-stage
is the stage the interceptor was in
when the error was thrown (either ::action/enter
or ::action/leave
).
Parts of an action can be made dynamic, computed based on context
or other injected values; via the built-in decide
function, which
yields a special injection.
(def my-action
(act {:key ::my-action}
[:http (<< :http/post "api/example" :json {:foo 1 :bar 2})]
(decide
(fn [{:keys [status errors] :as _response}]
(case status
"succcess"
[:dispatch success-act]
"failure"
[:dispatch failure-act {:errors errors}]))
(<< :http/response :get :foo))))
When the injection created by decide
is hit, its function is called
with the injected dependencies. The function's result is then injected
in turn.
The with-inj
macro provides some let-like syntax sugar around decide. Here's
how that looks:
(def my-action
(act {:key ::my-action}
[:http (<< :http/post "api/example" :json {:foo 1 :bar 2})]
(with-inj
[{:keys [status errors] :as _response} (<< :http/response :get :foo)]
(case status
"succcess"
[:dispatch success-act]
"failure"
[:dispatch failure-act {:errors errors}]))))
All injections have a 'depth'. When an action is dispatched, only injections
with a depth <= 0
will be resolved; any others will simply have their depth
decremented.
This allows for a kind of 'quoting' of injections. For example if you have
a :dispatch
effect which expects an action. If the action you give has
injections of its own, then they'd be resolved against the context of the
dispatching action (the one with the :dispatch
effect), instead of against
the new context created by dispatching the action, as might be intended.
So, to deal with situations like this, we have two utilities action/inc-depth
and action/dec-depth
. These will walk a form an increment or decrement any
injections found within it. In the previous example success-act
and failure-act
should probably be depth incremented.
(def my-action
(act {:key ::my-action}
[:http (<< :http/post "api/example" :json {:foo 1 :bar 2})]
(with-inj
[{:keys [status errors] :as _response} (<< :http/response :get :foo)]
(case status
"succcess"
[:dispatch (action/inc-depth success-act)]
"failure"
[:dispatch (action/dec-depth failure-act) {:errors errors}]))))
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close