-
Notifications
You must be signed in to change notification settings - Fork 0
/
lifecycle.clj
170 lines (146 loc) · 5.61 KB
/
lifecycle.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
(ns manifold.lifecycle
"
Convention based lifecycle management of asynchronous chains.
This implements a lightweight take on the ideas behind interceptors
and similar libraries.
Notable differences are:
- Manifold is the sole provided execution model for chains
- No chain manipulation can occur
- Guards and finalizer for steps are optional
- Exceptions during lifecycle steps stop the execution
"
(:require [spootnik.clock :as clock]
[clojure.spec.alpha :as s]
[manifold.deferred :as d]))
(defn ^:no-doc initialize-context
"Create a context which will be threaded between executions"
[{::keys [clock] :or {clock clock/wall-clock}} init]
(let [timestamp (clock/epoch clock)]
{::created-at timestamp
::updated-at timestamp
::result init
::index 0
::output []
::clock clock}))
(defn ^:no-doc augment-context
"Called after a step to record timing"
[{::keys [clock] :as context} id last-update]
(let [timestamp (clock/epoch clock)
timing (- timestamp last-update)]
(-> context
(update ::index inc)
(assoc ::updated-at timestamp)
(update ::steps assoc ::id id ::timing timing))))
(defn ^:no-doc rethrow-exception-fn
"When chains fail, augment the thrown exception with the current
context state"
[context]
(fn [e]
(let [data (ex-data e)
extra {:type :error/fault
::context context}]
(throw (ex-info (.getMessage e) (merge extra data) e)))))
(defn ^:no-doc prepare-step
"Coerce input to a step"
[input]
(cond
(var? input)
(let [{:keys [ns name]} (meta input)]
{::id (keyword (str ns) (str name))
::show-context? false
::handler (var-get input)})
(keyword? input)
{::id input
::handler input
::show-context? false}
(instance? clojure.lang.IFn input)
{::id (keyword (gensym "step"))
::handler input
::show-context? false}
(map? input) input
:else (throw (ex-info "invalid step definition"
{:error/type :error/invalid}))))
(defn ^:no-doc wrap-step-fn
"Wrap each lifecycle step. This yields a function which will run a
step's handler. If a guard is present, the execution of the function
will be dependent on the guard's success, if a finalizer is present,
it will be ran.
Any exception caught will be rethrown with the current context
state attached."
[{::keys [clock]}]
(bound-fn [{::keys [id handler guard finalizer show-context?] :as step}]
{:pre [(s/valid? ::step step)]}
(bound-fn [{::keys [result] :as context}]
(let [timestamp (clock/epoch clock)
context (augment-context context id timestamp)
input (if show-context? context result)
rethrow (rethrow-exception-fn context)]
(try
(if (or (nil? guard) (guard context))
(d/catch
(cond-> (handler input)
(not show-context?) (d/chain #(assoc context ::result %))
(some? finalizer) (d/chain finalizer))
rethrow)
input)
(catch Exception e
(rethrow e)))))))
(defn ^:no-doc prepare-steps
"Prepares chain function for each given step.
By default, the result is extracted out of a chain at
its end, `:manifold.lifecyle/raw-result?` set to true
in opts to `run` will ensure this is not the case"
[{::keys [raw-result?]} steps]
(cond-> (mapv prepare-step steps)
(not raw-result?)
(conj {::id ::result
::show-context? true
::handler ::result})))
(defn run
"
Run a series of potentially asynchronous steps in sequence
on an initial value (`init`0, threading each step's result to
the next step.
Steps are maps or the following keys:
[:manifold.lifecycle/id
:manifold.lifecycle/handler
:manifold.lifecycle/show-context?
:manifold.lifecycle/guard]
- `id` is the unique ID for a step
- `handler` is the function of the previous result
- `show-context?` determines whether the handler is fed
the context map or the result. When false, the output
is considered to be the result, not the context.
- `guard` is an optional predicate of the current context and previous
preventing execution of the step when yielding false
Steps can also be provided as functions or vars, in which case it is
assumed that `show-context?` is false for the step, and ID is inferred
or generated.
In the three-arity version, an extra options maps can
be provided, with the following keys:
[:manifold.lifecycle/clock
:manifold.lifecycle/raw-result?]
- `clock` is an optional implementation of `spootnik.clock/Clock`,
defaulting to the system's wall clock (`spootnik.clock/wall-clock`)
- `raw-result?` toggles extraction of the result out of the context
map, defaults to `false`
"
([init steps]
(run init steps {}))
([init steps opts]
(let [context (initialize-context opts init)]
(apply d/chain context
(->> (prepare-steps opts steps)
(map (wrap-step-fn context)))))))
;; Specs
;; =====
(s/def ::id keyword?)
(s/def ::handler #(instance? clojure.lang.IFn %))
(s/def ::guard #(instance? clojure.lang.IFn %))
(s/def ::description string?)
(s/def ::show-context? boolean?)
(s/def ::step (s/keys :req [::id ::handler ::show-context?]
:opt [::guard ::description]))
(s/def ::clock ::clock/clock)
(s/def ::raw-result? (s/nilable boolean?))
(s/def ::opts (s/keys :opt [::clock ::raw-result?]))