/
core.cljs
255 lines (227 loc) · 9.59 KB
/
core.cljs
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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
(ns reacl-c.test-util.core
(:require [reacl-c.core :as core]
[reacl-c.base :as base]
[active.clojure.functions :as f]
[active.clojure.lens :as lens]
[cljs-async.core :as async]
[cljs-async.cljs.core :as async-cljs]))
(letfn [(conj-into [atom a]
(core/effect swap! atom conj a))]
(defn collect-actions
"Returns an item that is like `item`, but will capture all emitted actions and
conj them into the given atom instead."
[item atom]
(core/map-actions item (f/partial conj-into atom))))
(defn effect?
"Returns true if the given action is an effect action, and
optionally if it was created by the given effect function."
[a & [eff-defn]]
(and (base/effect? a)
(or (nil? eff-defn)
(and (base/simple-effect? a) (= (base/effect-f a) eff-defn))
(= (:reacl-c.core/effect-defn (meta a)) eff-defn))))
(defn simple-effect?
"Returns true for effects created by [[core/effect]]
or [[core/defn-effect]], but false for those created
from [[core/seq-effects]]."
[a & [eff-defn]]
(and (effect? a eff-defn)
(base/simple-effect? a)))
(defn effect-f
"Returns the function implementing the effect behind the given simple effect action. See [[simple-effect?]]."
[eff]
(assert (base/simple-effect? eff))
(base/effect-f eff))
(defn effect-args
"Returns the arguments for the function returned by [[effect-f]]."
[eff]
(assert (base/simple-effect? eff))
(base/effect-args eff))
(defn run-effect!
"Returns a tuple `[value ret]`. If an effect returns a [[reacl-c.core/return]]
value, then 'value' is the returned state, and 'ret' everything else.
For any other value, 'ret' is empty."
[eff]
(base/run-effect! eff))
(defn- subscribe-effect-1? [eff]
(effect? eff core/subscribe!))
(defn- subscribe-effect-fn
"The function passed to the subscription the given subscribe effect was generated from."
[eff]
(assert (subscribe-effect-1? eff))
(first (effect-args eff)))
(defn- subscribe-effect-args
"The arguments passed to the subscription the given subscribe effect was generated from."
[eff]
(assert (subscribe-effect-1? eff))
(second (effect-args eff)))
(defn- subscribe-effect-subscription [eff]
(assert (subscribe-effect-1? eff))
(let [subs-f (subscribe-effect-fn eff)
subs-args (subscribe-effect-args eff)]
;; but if subs if created via a call to to a defn-subscription fn, then it will look different
(if-let [f (core/subscription-from-defn-meta-key (meta eff))]
(apply f subs-args)
(apply core/subscription subs-f subs-args))))
(defn- subscribe-effect?
"Tests if the given effect, is one that is emitted by a subscription
equal to the given one on mount. This can be useful in unit tests."
([eff]
(subscribe-effect-1? eff))
([eff subs]
(and (effect? eff core/subscribe!)
(let [subs-f (subscribe-effect-fn eff)
subs-args (subscribe-effect-args eff)]
;; the first arg is the subs-f, the second arg it's user args.
;; creating a new subscription with same args, should be an = item then.
(or (= subs (apply core/subscription subs-f subs-args))
;; but if subs if created via a call to to a defn-subscription fn, then it will look different
(if-let [f (core/subscription-from-defn-meta-key (meta eff))]
(= subs (apply f subs-args))
false))))))
(defn- subscription-f-args [sub]
(when-let [[defn-f f args] (core/subscription-deconstruct sub)]
[f args]))
(defn- subscription-1?
[v]
(some? (core/subscription-deconstruct v)))
(defn subscription?
"Returns whether `item` is a subscription item, optinally also
checking if it was created by the given `defn-subscription` function
or with the given function `f` as its implementation."
([v]
(some? (core/subscription-deconstruct v)))
([v f]
(assert (ifn? f))
(when-let [[s-defn-f s-f s-args] (core/subscription-deconstruct v)]
(or (= f s-f)
(= f s-defn-f)))))
(defn subscription-f
"Returns the function implementing the given subscription item."
[sub]
(assert (subscription? sub))
(second (core/subscription-deconstruct sub)))
(defn subscription-args
"Returns extra arguments for the function implementing the given subscription item. See [[subscription-f]]"
[sub]
(assert (subscription? sub))
(second (rest (core/subscription-deconstruct sub))))
(defn ^{:arglists '([subscribe-effect f & args]
[subscribe-effect subscription])
:private true ;; use map-subscriptions
}
replace-subscription
"Given the subscribe effect of a subscription item, this returns a
modified effect that subscribes to the given subscription
instead. Alernatively, a function `f ` and args, that implement a
subscription can be given, corresponding
to [[core/subcription]]. Use this in a [[core/map-effects]] item, to
mock subscribe effects in a test setup."
[subscribe-effect f & args]
(assert (subscribe-effect? subscribe-effect))
(let [[f args] (or (subscription-f-args f)
[f args])]
(lens/overhaul subscribe-effect base/effect-args
(fn [[_ _ deliver! host action-mapper]]
;; Note: action-mapper contains the schema validation when specified in a defn-subscription.
[f args deliver! host identity]))))
(let [pre (fn [f eff]
(if-let [r (and (subscribe-effect? eff)
(f (subscribe-effect-subscription eff)))]
(replace-subscription eff r)
eff))]
(defn map-subscriptions
"Returns an item that replaces subscriptions in `item`. Note that
`item` itself remains unchanged; instead, whenever a subscription is
activated in `item`, `f` will be called with that subscription, and
if it returns a different subscription, then that is activated
instead. If it returns nil, then the original subscription will be
actived.
You can use [[subscription?]] and [[subscription-args]], if the
replacement depends on details of the subscription or for mapping a
whole group of subscriptions."
[item f]
(core/map-effects item (f/partial pre f))))
(defn run-subscription!
"Calls the function implementing the given subscription with the given
`deliver!` function, returning the stop function for it."
[sub deliver!]
(let [[f args] (subscription-f-args sub)]
(apply f deliver! args)))
(defn- run-subscription-async [sub]
;; starts the subscription and returns [stop-fn, value-promise]
;; where value-promise is promise of [value, next-value-promise] and
;; so on.
(let [first-value (async-cljs/promise)
next-value (atom first-value)
stop! (run-subscription! sub (fn deliver! [action]
(let [p @next-value
n (async-cljs/promise)]
(reset! next-value n)
(async-cljs/deliver p [action (async-cljs/async-deref n)]))))]
[stop! (async-cljs/async-deref first-value)]))
(defn- subscription-results-next [akku done? x]
(let [[v next-value] x
lst (swap! akku conj v)]
(if (done? lst)
lst
(-> next-value
(async/then (partial subscription-results-next akku done?))))))
(defn subscription-results
"Runs the given subscription, asynchronously waiting for at least the
given number of actions emitted by it (or the given timeout
elapses), and then stops the subscription. Returns a promise of the
sequence of actions."
[sub num-actions & [timeout-ms]]
(assert (number? num-actions))
(let [akku (atom [])
done? (fn [lst]
(>= (count lst) num-actions))
[stop! first-value] (run-subscription-async sub)
values (-> first-value
(async/then (partial subscription-results-next akku done?)))]
(-> (if timeout-ms
(async/race values
(-> (async/timeout timeout-ms)
(async/then (fn [_]
;; on timeout, return what we have to far.
@akku))))
values)
(async/finally stop!))))
(defn preventing-error-log
"Prevents a log message about an exception during the evaluation of
`thunk`, which occurs even when the error is handled in an error
boundary. If `thunk` returns a promise, then error logs are enabled
after that completes instead."
[thunk]
;; React does some fancy things with the browsers error
;; handler in DEV, and respects 'default prevented' in
;; that it does not log the error then (or is it the browser?)
(let [eh (fn [ev]
(.preventDefault ev))]
(js/window.addEventListener "error" eh)
;; and this suppressed the 'The above error occurred' log msg from React.
(let [pre js/console.error
restore-sync? (atom true)
restore (fn []
(set! js/console.error pre)
(js/window.removeEventListener "error" eh))]
(set! js/console.error (fn [& args] nil))
(try (let [x (thunk)]
(if (async/promise? x)
(do (reset! restore-sync? false)
(async/finally x restore))
x))
(finally
(when @restore-sync?
(restore)))))))
;; TODO
#_(def validate-schemas-async
(at/simple-async-fixture
(fn [init-done]
(if (s/fn-validation?)
(init-done (fn [done] (done)))
(do (s/set-fn-validation! true)
(init-done (fn [done]
(s/set-fn-validation! false)
(done))))))))