Skip to content

Commit a1824c4

Browse files
Fix two possible bugs:
1. if an event triggered two rules, and if both rules have halt? true, then two halt events get dispatched 2. if two flows were used concurrently, AND they didn't supply an `:id`, then they would tread on each other's toes.
1 parent d0966b6 commit a1824c4

File tree

3 files changed

+75
-64
lines changed

3 files changed

+75
-64
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
## Async Control Flow In re-frame
1515

16-
Herein a re-frame [Effects Handler](https://github.com/Day8/re-frame/wiki/Effectful-Event-Handlers), keyed `:async-flow`,
16+
Herein a re-frame [Effects Handler](https://github.com/Day8/re-frame/docs), keyed `:async-flow`,
1717
which wrangles async tasks. It manages control flow at app boot time.
1818

1919
## Quick Start Guide

src/day8/re_frame/async_flow_fx.cljs

Lines changed: 59 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
[clojure.set :as set]
55
[day8.re-frame.forward-events-fx]))
66

7-
(def default-id :async/flow)
87

98
(defn seen-all-of?
109
[required-events seen-events]
@@ -24,47 +23,43 @@
2423
(filterv (fn [task] ((:when task) (:events task) now-seen-events)))))
2524

2625

26+
(def map-when->fn {:seen? seen-all-of?
27+
:seen-both? seen-all-of?
28+
:seen-all-of? seen-all-of?
29+
:seen-any-of? seen-any-of?})
30+
31+
(defn when->fn
32+
[when-kw]
33+
(if-let [when-fn (map-when->fn when-kw)]
34+
when-fn
35+
(re-frame/console :error "async-flow: got bad value for :when - " when-kw)))
36+
2737
(defn massage-rules
2838
"Massage the supplied rules as follows:
2939
- replace `:when` keyword value with a function implementing the predicate
3040
- ensure that only `:dispatch` or `:dispatch-n` is provided
31-
- add a unique :id, if one not already present
32-
- add halt event when :halt? present"
33-
[flow-id rules]
34-
(let [halt-event [flow-id :halt-flow]
35-
when->fn {:seen? seen-all-of?
36-
:seen-both? seen-all-of?
37-
:seen-all-of? seen-all-of?
38-
:seen-any-of? seen-any-of?}
39-
add-halt (fn [tasks halt?]
40-
; when rule represents stop, add `:halt-flow` as last event
41-
(if halt? (concat tasks [halt-event]) tasks))]
42-
(->> rules
43-
(map-indexed (fn [index {:keys [id when events dispatch dispatch-n halt?]}]
44-
(let [when-as-fn (when->fn when)
45-
_ (assert (cond [(some? dispatch) (some? dispatch-n)]
46-
[false false] true ; either or both can be nil
47-
[true false] true ; only dispatch provided
48-
[false true] true ; only dispatch-n provided
49-
false) "async-flow: rule can only specify one of :dispatch :dispatch-n")
50-
_ (assert (some? when-as-fn) (str "async-flow: found bad value for :when: " when))
51-
tasks (-> (cond
52-
dispatch (list dispatch)
53-
dispatch-n dispatch-n
54-
:else nil)
55-
(add-halt halt?))]
56-
{:id (or id index)
57-
:when when-as-fn
58-
:events (if (coll? events) (set events) #{events})
59-
:dispatch-n tasks}))))))
60-
61-
62-
;; -- Create Event Handler
41+
- add a unique :id, if one not already present"
42+
[rules]
43+
(->> rules
44+
(map-indexed (fn [index {:as rule :keys [id when events dispatch dispatch-n halt?]}]
45+
{:id (or id index)
46+
:halt? (or halt? false)
47+
:when (when->fn when)
48+
:events (if (coll? events) (set events) (hash-set events))
49+
:dispatch-n (cond
50+
dispatch-n (if dispatch
51+
(re-frame/console :error "async-flow: rule can only specify one of :dispatch and :dispatch-n. Got both: " rule)
52+
dispatch-n)
53+
dispatch (list dispatch)
54+
:else '())}))))
55+
56+
57+
;; -- Event Handler
6358

6459
(defn make-flow-event-handler
65-
"given a flow definitiion, returns an event handler which implements this definition"
60+
"Given a flow definitiion, returns an event handler which implements this definition"
6661
[{:keys [id db-path rules first-dispatch]}]
67-
(let [id (or id default-id)
62+
(let [
6863
;; Subject to db-path, state is either stored in app-db or in a local atom
6964
;; Two pieces of state are maintained:
7065
;; - the set of seen events
@@ -81,15 +76,16 @@
8176
(fn [db] (get-in db db-path))
8277
(fn [_] @local-store))
8378

84-
rules (massage-rules id rules)] ;; all of the events refered to in the rules
79+
rules (massage-rules rules)] ;; all of the events refered to in the rules
8580

8681
;; Return an event handler which will manage the flow.
8782
;; This event handler will receive 3 kinds of events:
8883
;; (dispatch [:id :setup])
8984
;; (dispatch [:id :halt-flow])
9085
;; (dispatch [:id [:forwarded :event :vector]])
9186
;;
92-
;; This event handler returns a map of effects.
87+
;; This event handler returns a map of effects - it expects to be registered using
88+
;; reg-event-fx
9389
;;
9490
(fn async-flow-event-hander
9591
[{:keys [db]} event-v]
@@ -113,31 +109,44 @@
113109
:forward-events {:unregister id}
114110
:deregister-event-handler id}
115111

116-
;; Here we are managig the flow.
117-
;; A new event has been forwarded to this handler. What does it mean?
118-
;; 1. does this new event mean we need to dispatch another?
112+
;; Here we are managing the flow.
113+
;; A new event has been forwarded, so work out what should happen:
114+
;; 1. does this new event mean we should dispatch another?
119115
;; 2. remember this event has happened
120116
(let [[_ [forwarded-event-id & args]] event-v
121117
{:keys [seen-events rules-fired]} (get-state db)
122118
new-seen-events (conj seen-events forwarded-event-id)
123119
ready-rules (startable-rules rules new-seen-events rules-fired)
120+
add-halt? (some :halt? ready-rules)
124121
ready-rules-ids (->> ready-rules (map :id) set)
125-
new-rules-fired (set/union rules-fired ready-rules-ids)]
122+
new-rules-fired (set/union rules-fired ready-rules-ids)
123+
new-dispatches (cond-> (mapcat :dispatch-n ready-rules)
124+
add-halt? vec
125+
add-halt? (conj [id :halt-flow]))]
126126
(merge
127127
{:db (set-state db new-seen-events new-rules-fired)}
128-
(when (seq ready-rules) {:dispatch-n (mapcat :dispatch-n ready-rules)})))))))
128+
(when (seq new-dispatches) {:dispatch-n new-dispatches})))))))
129129

130130

131-
;; -- Register effects handler with re-frame
131+
(defn- ensure-has-id
132+
"Ensure `flow` has an id.
133+
Return a vector of [id flow]"
134+
[flow]
135+
(if-let [id (:id flow)]
136+
[id flow]
137+
(let [new-id (keyword (str "async-flow/" (gensym "id-")))]
138+
[new-id (assoc flow :id new-id)])))
132139

133-
(defn flow->handler
134-
[{:as flow :keys [id] :or {id default-id}}]
135-
(re-frame/reg-event-fx
136-
id ;; add debug middleware if dp-path set ??? XXX
137-
(make-flow-event-handler flow))
138-
(re-frame/console :log "starting async-flow:" id)
139-
(re-frame/dispatch [id :setup]))
140140

141+
;; -- Effect handler
142+
143+
144+
(defn flow->handler
145+
"Action the given flow effect"
146+
[flow]
147+
(let [[id flow'] (ensure-has-id flow)]
148+
(re-frame/reg-event-fx id (make-flow-event-handler flow')) ;; register event handler
149+
(re-frame/dispatch [id :setup]))) ;; kicks things off
141150

142151
(re-frame/reg-fx
143152
:async-flow

test/day8/re_frame/async_flow_fx_test.cljs

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,28 +36,29 @@
3636

3737

3838
(deftest test-massage-rules
39-
(is (= (core/massage-rules :my-id [{:when :seen? :events :1 :dispatch [:2]}])
40-
(list {:id 0 :when core/seen-all-of? :events #{:1} :dispatch-n (list [:2])})))
39+
(is (= (core/massage-rules [{:when :seen? :events :1 :dispatch [:2]}])
40+
(list {:id 0 :when core/seen-all-of? :events #{:1} :halt? false :dispatch-n (list [:2])})))
4141

42-
(is (= (core/massage-rules :my-id [{:when :seen-both? :events [:1 :2] :halt? true}])
43-
(list {:id 0 :when core/seen-all-of? :events #{:1 :2} :dispatch-n (list [:my-id :halt-flow])})))
42+
(is (= (core/massage-rules [{:when :seen-both? :events [:1 :2] :halt? true}])
43+
(list {:id 0 :when core/seen-all-of? :events #{:1 :2} :halt? true :dispatch-n '()})))
4444

45-
(is (= (core/massage-rules :my-id [{:when :seen-any-of? :events #{:1 :2} :dispatch [:2] :halt? true}])
46-
(list {:id 0 :when core/seen-any-of? :events #{:1 :2} :dispatch-n (list [:2] [:my-id :halt-flow])}))))
45+
(is (= (core/massage-rules [{:when :seen-any-of? :events #{:1 :2} :dispatch [:2] :halt? true}])
46+
(list {:id 0 :when core/seen-any-of? :events #{:1 :2} :halt? true :dispatch-n (list [:2])}))))
4747

4848

4949
(deftest test-setup
50-
(let [flow {:first-dispatch [:1]
50+
(let [flow {:id :some-id
51+
:first-dispatch [:1]
5152
:rules [
5253
{:when :seen? :events :1 :dispatch [:2]}
5354
{:when :seen? :events :3 :halt? true}]}
5455
handler-fn (core/make-flow-event-handler flow)]
5556
(is (= (handler-fn {:db {}} [:dummy-id :setup])
5657
{:db {}
5758
:dispatch [:1]
58-
:forward-events {:register core/default-id
59+
:forward-events {:register :some-id
5960
:events #{:1 :3}
60-
:dispatch-to [core/default-id]}}))))
61+
:dispatch-to [:some-id]}}))))
6162

6263
(deftest test-forwarding
6364
(let [flow {:first-dispatch [:start]
@@ -76,7 +77,7 @@
7677
{:db {:p {:seen-events #{:33 :no}
7778
:rules-fired #{}}}}))
7879

79-
;; new event should not cause a new dispatch because task is already started (:id 0 is in ::rules-fired)
80+
;; new event should not cause a new dispatch because task is already started (:id 0 is in :rules-fired)
8081
(is (= (handler-fn
8182
{:db {:p {:seen-events #{:1}
8283
:rules-fired #{0}}}}
@@ -109,13 +110,14 @@
109110

110111

111112
(deftest test-halt1
112-
(let [flow {:first-dispatch [:1]
113+
(let [flow {:id :some-id
114+
:first-dispatch [:1]
113115
:rules []}
114116
handler-fn (core/make-flow-event-handler flow)]
115117
(is (= (handler-fn {:db {}} [:dummy-id :halt-flow])
116118
{ ;; :db {}
117-
:deregister-event-handler core/default-id
118-
:forward-events {:unregister core/default-id}}))))
119+
:deregister-event-handler :some-id
120+
:forward-events {:unregister :some-id}}))))
119121

120122

121123
;; Aggh. I don't have dissoc-in available to make this work.

0 commit comments

Comments
 (0)