2015-03-04 13:00:36 +00:00
|
|
|
(ns re-frame.router
|
2016-07-20 08:57:46 +00:00
|
|
|
(:require [re-frame.events :refer [handle]]
|
2016-07-28 06:37:57 +00:00
|
|
|
[re-frame.interop :refer [after-render empty-queue next-tick]]
|
|
|
|
[re-frame.loggers :refer [console]]))
|
2015-03-04 13:00:36 +00:00
|
|
|
|
2015-11-02 11:37:46 +00:00
|
|
|
|
|
|
|
;; -- Router Loop ------------------------------------------------------------
|
2015-03-04 13:00:36 +00:00
|
|
|
;;
|
2016-05-29 12:47:25 +00:00
|
|
|
;; A call to "re-frame.core/dispatch" places an event on a queue for processing.
|
|
|
|
;; A short time later, the handler registered to handle this event will be run.
|
|
|
|
;; What follows is the implemtation of this process.
|
2016-05-20 12:36:35 +00:00
|
|
|
;;
|
2016-05-29 12:47:25 +00:00
|
|
|
;; The task is to process queued events in a perpetual loop, one after
|
|
|
|
;; the other, FIFO, calling the registered event-handler for each, being idle when
|
|
|
|
;; there are no events, and firing up when one arrives.
|
2015-03-04 13:00:36 +00:00
|
|
|
;;
|
2016-05-29 12:47:25 +00:00
|
|
|
;; But browsers only have a single thread of control and we must be
|
2015-11-04 14:00:45 +00:00
|
|
|
;; careful to not hog the CPU. When processing events one after another, we
|
|
|
|
;; must hand back control to the browser regularly, so it can redraw, process
|
|
|
|
;; websockets, etc. But not too regularly! If we are in a de-focused browser
|
|
|
|
;; tab, our app will be CPU throttled. Each time we get back control, we have
|
|
|
|
;; to process all queued events, or else something like a bursty websocket
|
|
|
|
;; (producing events) might overwhelm the queue. So there's a balance.
|
2015-03-04 13:00:36 +00:00
|
|
|
;;
|
2016-05-29 12:47:25 +00:00
|
|
|
;; The processing/handling of an event happens "asynchronously" sometime after
|
|
|
|
;; that event was enqueued via "dispatch". The original implementation of this router loop
|
|
|
|
;; used core.async. It was fairly simple, and it mostly worked, but it did not
|
|
|
|
;; give enough control. So now we hand-roll our own, finite-state-machine and all.
|
2015-03-04 13:00:36 +00:00
|
|
|
;;
|
2015-11-02 11:37:46 +00:00
|
|
|
;; The strategy is this:
|
2016-05-20 12:36:35 +00:00
|
|
|
;; - maintain a FIFO queue of `dispatched` events.
|
2015-11-02 12:43:19 +00:00
|
|
|
;; - when a new event arrives, "schedule" processing of this queue using
|
|
|
|
;; goog.async.nextTick, which means it will happen "very soon".
|
2016-05-29 12:47:25 +00:00
|
|
|
;; - when processing events, one after the other, do ALL the those currently
|
|
|
|
;; queued. Don't stop. Don't yield to the browser. Hog that CPU.
|
2016-05-20 12:36:35 +00:00
|
|
|
;; - but if any new events are dispatched during this cycle of processing,
|
|
|
|
;; don't do them immediately. Leave them queued. Yield first to the browser,
|
|
|
|
;; and do these new events in the next processing cycle. That way we drain
|
2015-11-04 14:00:45 +00:00
|
|
|
;; the queue up to a point, but we never hog the CPU forever. In
|
|
|
|
;; particular, we handle the case where handling one event will beget
|
|
|
|
;; another event. The freshly begat event will be handled next cycle,
|
2016-05-20 12:36:35 +00:00
|
|
|
;; with yielding in-between.
|
2016-05-29 12:47:25 +00:00
|
|
|
;; - In some cases, an event should not be handled until after the GUI has been
|
2016-05-20 12:36:35 +00:00
|
|
|
;; updated, i.e., after the next Reagent animation frame. In such a case,
|
2015-11-02 11:37:46 +00:00
|
|
|
;; the event should be dispatched with :flush-dom metadata like this:
|
2015-11-04 14:00:45 +00:00
|
|
|
;; (dispatch ^:flush-dom [:event-id other params])
|
|
|
|
;; Such an event will temporarily block all further processing because
|
2016-05-29 12:47:25 +00:00
|
|
|
;; events are processed sequentially: we handle one event completely
|
|
|
|
;; before we handle the ones behind it.
|
2015-03-04 13:00:36 +00:00
|
|
|
;;
|
2016-05-29 12:47:25 +00:00
|
|
|
;; Implementation Notes:
|
2015-11-04 02:33:50 +00:00
|
|
|
;; - queue processing can be in a number of states: scheduled, running, paused
|
2016-05-29 12:47:25 +00:00
|
|
|
;; etc. So it is modeled as a Finite State Machine.
|
2015-11-04 02:33:50 +00:00
|
|
|
;; See "-fsm-trigger" (below) for the states and transitions.
|
2015-11-02 11:37:46 +00:00
|
|
|
;; - the scheduling is done via "goog.async.nextTick" which is pretty quick
|
2016-05-20 14:44:54 +00:00
|
|
|
;; - when the event has :flush-dom metadata we schedule via
|
2016-05-29 12:47:25 +00:00
|
|
|
;; "reagent.core.after-render"
|
|
|
|
;; which will run event processing after the next Reagent animation frame.
|
2015-11-02 11:37:46 +00:00
|
|
|
;;
|
|
|
|
|
2016-06-24 11:31:12 +00:00
|
|
|
;; Events can have metadata which says to pause event processing.
|
2016-05-20 14:44:54 +00:00
|
|
|
;; event metadata -> "run later" functions
|
2015-11-04 13:34:23 +00:00
|
|
|
(def later-fns
|
2016-07-20 08:57:46 +00:00
|
|
|
{:flush-dom (fn [f] (after-render #(next-tick f))) ;; one tick after the end of the next annimation frame
|
|
|
|
:yield next-tick}) ;; almost immediately
|
2015-11-04 13:34:23 +00:00
|
|
|
|
2016-05-29 12:47:25 +00:00
|
|
|
|
2016-06-24 11:31:12 +00:00
|
|
|
;; Abstract representation of the Event Queue
|
2015-11-02 11:37:46 +00:00
|
|
|
(defprotocol IEventQueue
|
|
|
|
|
2016-06-03 14:13:41 +00:00
|
|
|
;; -- API
|
|
|
|
(push [this event])
|
2016-07-28 04:50:39 +00:00
|
|
|
(add-post-event-callback [this id callack])
|
2016-06-03 02:49:49 +00:00
|
|
|
(remove-post-event-callback [this f])
|
2015-12-06 12:19:11 +00:00
|
|
|
|
2016-05-29 12:47:25 +00:00
|
|
|
;; -- Implementation via a Finite State Machine
|
2015-11-02 11:37:46 +00:00
|
|
|
(-fsm-trigger [this trigger arg])
|
|
|
|
|
2016-05-29 12:47:25 +00:00
|
|
|
;; -- Finite State Machine actions
|
2015-11-02 11:37:46 +00:00
|
|
|
(-add-event [this event])
|
2016-06-03 14:13:41 +00:00
|
|
|
(-process-1st-event-in-queue [this])
|
2015-11-02 11:37:46 +00:00
|
|
|
(-run-next-tick [this])
|
|
|
|
(-run-queue [this])
|
|
|
|
(-exception [this ex])
|
2015-11-07 03:58:15 +00:00
|
|
|
(-pause [this later-fn])
|
2016-06-03 14:13:41 +00:00
|
|
|
(-resume [this])
|
|
|
|
(-call-post-event-callbacks[this event]))
|
2015-11-02 11:37:46 +00:00
|
|
|
|
|
|
|
|
2016-05-29 12:47:25 +00:00
|
|
|
;; Concrete implementation of IEventQueue
|
2016-07-20 08:57:46 +00:00
|
|
|
(deftype EventQueue [#?(:cljs ^:mutable fsm-state :clj ^:volatile-mutable fsm-state)
|
|
|
|
#?(:cljs ^:mutable queue :clj ^:volatile-mutable queue)
|
|
|
|
#?(:cljs ^:mutable post-event-callback-fns :clj ^:volatile-mutable post-event-callback-fns)]
|
2015-11-02 11:37:46 +00:00
|
|
|
IEventQueue
|
|
|
|
|
2015-12-06 12:19:11 +00:00
|
|
|
;; -- API ------------------------------------------------------------------
|
2016-05-29 12:47:25 +00:00
|
|
|
|
2016-06-03 14:13:41 +00:00
|
|
|
(push [this event] ;; presumably called by dispatch
|
2015-11-02 11:37:46 +00:00
|
|
|
(-fsm-trigger this :add-event event))
|
|
|
|
|
2016-07-28 04:50:39 +00:00
|
|
|
;; register a callback function which will be called after each event is processed
|
|
|
|
(add-post-event-callback [_ id callback-fn]
|
2016-07-28 06:37:57 +00:00
|
|
|
(if (contains? post-event-callback-fns id)
|
|
|
|
(console :warn "re-frame: overwriting existing post event call back with id: " id))
|
2016-07-28 04:50:39 +00:00
|
|
|
(->> (assoc post-event-callback-fns id callback-fn)
|
|
|
|
(set! post-event-callback-fns)))
|
2015-12-06 12:19:11 +00:00
|
|
|
|
2016-07-28 04:50:39 +00:00
|
|
|
(remove-post-event-callback [_ id]
|
2016-07-28 06:37:57 +00:00
|
|
|
(if-not (contains? post-event-callback-fns id)
|
|
|
|
(console :warn "re-frame: could not remove post event call back with id: " id)
|
|
|
|
(->> (dissoc post-event-callback-fns id)
|
|
|
|
(set! post-event-callback-fns))))
|
2016-06-03 02:49:49 +00:00
|
|
|
|
2015-12-06 12:19:11 +00:00
|
|
|
|
2016-05-29 12:47:25 +00:00
|
|
|
;; -- FSM Implementation ---------------------------------------------------
|
2016-06-03 02:49:49 +00:00
|
|
|
|
2015-12-06 12:19:11 +00:00
|
|
|
(-fsm-trigger
|
|
|
|
[this trigger arg]
|
|
|
|
|
2016-06-03 14:13:41 +00:00
|
|
|
;; The following "case" impliments the Finite State Machine.
|
|
|
|
;; Given a "trigger", and the existing FSM state, it computes the
|
|
|
|
;; new FSM state and the tranistion action (function).
|
2015-12-06 12:19:11 +00:00
|
|
|
|
2016-05-29 12:47:25 +00:00
|
|
|
(let [[new-fsm-state action-fn]
|
|
|
|
(case [fsm-state trigger]
|
2015-12-06 12:19:11 +00:00
|
|
|
|
2016-05-29 12:47:25 +00:00
|
|
|
;; You should read the following "case" as:
|
|
|
|
;; [current-FSM-state trigger] -> [new-FSM-state action-fn]
|
|
|
|
;;
|
2016-06-03 02:49:49 +00:00
|
|
|
;; So, for example, the next line should be interpreted as:
|
2016-05-29 12:47:25 +00:00
|
|
|
;; if you are in state ":idle" and a trigger ":add-event"
|
|
|
|
;; happens, then move the FSM to state ":scheduled" and execute
|
2016-06-03 02:49:49 +00:00
|
|
|
;; that two-part "do" fucntion.
|
2015-12-06 12:19:11 +00:00
|
|
|
[:idle :add-event] [:scheduled #(do (-add-event this arg)
|
|
|
|
(-run-next-tick this))]
|
|
|
|
|
2016-05-29 12:47:25 +00:00
|
|
|
;; State: :scheduled (the queue is scheduled to run, soon)
|
2015-12-06 12:19:11 +00:00
|
|
|
[:scheduled :add-event] [:scheduled #(-add-event this arg)]
|
|
|
|
[:scheduled :run-queue] [:running #(-run-queue this)]
|
|
|
|
|
2016-05-20 14:44:54 +00:00
|
|
|
;; State: :running (the queue is being processed one event after another)
|
2015-12-06 12:19:11 +00:00
|
|
|
[:running :add-event ] [:running #(-add-event this arg)]
|
|
|
|
[:running :pause ] [:paused #(-pause this arg)]
|
|
|
|
[:running :exception ] [:idle #(-exception this arg)]
|
|
|
|
[:running :finish-run] (if (empty? queue) ;; FSM guard
|
|
|
|
[:idle]
|
|
|
|
[:scheduled #(-run-next-tick this)])
|
|
|
|
|
2016-06-03 02:49:49 +00:00
|
|
|
;; State: :paused (:flush-dom metadata on an event has caused a temporary pause in processing)
|
2015-12-06 12:19:11 +00:00
|
|
|
[:paused :add-event] [:paused #(-add-event this arg)]
|
|
|
|
[:paused :resume ] [:running #(-resume this)]
|
|
|
|
|
2016-07-20 08:57:46 +00:00
|
|
|
(throw (ex-info (str "re-frame: router state transition not found. " fsm-state " " trigger)
|
|
|
|
{:fsm-state fsm-state, :trigger trigger})))]
|
2015-12-06 12:19:11 +00:00
|
|
|
|
2016-05-29 12:47:25 +00:00
|
|
|
;; The "case" above computed both the new FSM state, and the action. Now, make it happen.
|
|
|
|
(set! fsm-state new-fsm-state)
|
2015-12-06 12:19:11 +00:00
|
|
|
(when action-fn (action-fn))))
|
|
|
|
|
2015-11-02 11:37:46 +00:00
|
|
|
(-add-event
|
2016-06-03 14:13:41 +00:00
|
|
|
[_ event]
|
2015-11-02 11:37:46 +00:00
|
|
|
(set! queue (conj queue event)))
|
|
|
|
|
2016-06-03 14:13:41 +00:00
|
|
|
(-process-1st-event-in-queue
|
2015-11-02 11:37:46 +00:00
|
|
|
[this]
|
|
|
|
(let [event-v (peek queue)]
|
|
|
|
(try
|
|
|
|
(handle event-v)
|
2016-06-03 02:49:49 +00:00
|
|
|
(set! queue (pop queue))
|
2016-06-03 14:13:41 +00:00
|
|
|
(-call-post-event-callbacks this event-v)
|
2016-07-20 08:57:46 +00:00
|
|
|
(catch #?(:cljs :default :clj Exception) ex
|
2016-06-03 02:49:49 +00:00
|
|
|
(-fsm-trigger this :exception ex)))))
|
2015-11-02 11:37:46 +00:00
|
|
|
|
|
|
|
(-run-next-tick
|
|
|
|
[this]
|
2016-07-20 08:57:46 +00:00
|
|
|
(next-tick #(-fsm-trigger this :run-queue nil)))
|
2015-11-02 11:37:46 +00:00
|
|
|
|
|
|
|
;; Process all the events currently in the queue, but not any new ones.
|
|
|
|
;; Be aware that events might have metadata which will pause processing.
|
|
|
|
(-run-queue
|
|
|
|
[this]
|
2015-11-04 20:36:41 +00:00
|
|
|
(loop [n (count queue)]
|
|
|
|
(if (zero? n)
|
|
|
|
(-fsm-trigger this :finish-run nil)
|
2015-12-06 12:19:11 +00:00
|
|
|
(if-let [later-fn (some later-fns (-> queue peek meta keys))] ;; any metadata which causes pausing?
|
2015-11-07 03:58:15 +00:00
|
|
|
(-fsm-trigger this :pause later-fn)
|
2016-06-03 14:13:41 +00:00
|
|
|
(do (-process-1st-event-in-queue this)
|
2015-11-04 20:36:41 +00:00
|
|
|
(recur (dec n)))))))
|
2015-11-02 11:37:46 +00:00
|
|
|
|
2015-11-07 04:01:55 +00:00
|
|
|
(-exception
|
|
|
|
[_ ex]
|
2016-07-20 08:57:46 +00:00
|
|
|
(set! queue empty-queue) ;; purge the queue
|
2015-11-07 04:01:55 +00:00
|
|
|
(throw ex))
|
|
|
|
|
2015-11-07 03:58:15 +00:00
|
|
|
(-pause
|
2015-11-04 13:34:23 +00:00
|
|
|
[this later-fn]
|
2015-11-07 03:58:15 +00:00
|
|
|
(later-fn #(-fsm-trigger this :resume nil)))
|
2015-11-02 11:37:46 +00:00
|
|
|
|
2016-06-03 14:13:41 +00:00
|
|
|
(-call-post-event-callbacks
|
|
|
|
[_ event-v]
|
|
|
|
;; Call each registed post-event callback.
|
|
|
|
(doseq [callback post-event-callback-fns]
|
|
|
|
(callback event-v queue)))
|
|
|
|
|
2015-11-07 03:58:15 +00:00
|
|
|
(-resume
|
2015-11-02 11:37:46 +00:00
|
|
|
[this]
|
2016-06-03 14:13:41 +00:00
|
|
|
(-process-1st-event-in-queue this) ;; do the event which paused processing
|
2015-12-06 12:19:11 +00:00
|
|
|
(-run-queue this))) ;; do the rest of the queued events
|
2015-11-02 11:37:46 +00:00
|
|
|
|
|
|
|
|
|
|
|
;; ---------------------------------------------------------------------------
|
2016-05-29 12:47:25 +00:00
|
|
|
;; Event Queue
|
2016-06-24 11:31:12 +00:00
|
|
|
;; When "dispatch" is called, the event is added into this event queue. Later,
|
|
|
|
;; the queue will "run" and the event will be "handled" by the registered function.
|
2015-03-04 13:00:36 +00:00
|
|
|
;;
|
2016-07-28 08:52:35 +00:00
|
|
|
(def event-queue (->EventQueue :idle empty-queue {}))
|
2015-11-02 11:37:46 +00:00
|
|
|
|
|
|
|
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
;; Dispatching
|
|
|
|
;;
|
2015-03-04 13:00:36 +00:00
|
|
|
|
|
|
|
(defn dispatch
|
2016-05-29 12:47:25 +00:00
|
|
|
"Queue an event to be processed by the registered handler function.
|
2015-03-04 13:00:36 +00:00
|
|
|
|
|
|
|
Usage example:
|
2015-11-04 14:03:01 +00:00
|
|
|
(dispatch [:delete-item 42])"
|
2015-03-04 13:00:36 +00:00
|
|
|
[event-v]
|
2016-07-19 23:52:26 +00:00
|
|
|
(let [;; At the point of dispatch, we capture the current stack.
|
|
|
|
;; Attach this stack to the event as meta data so that later, if
|
|
|
|
;; anything goes wrong, we know where it came from.
|
|
|
|
;; To get a source mapped stack, we must get rid of the react frames
|
|
|
|
;; See https://github.com/Day8/re-frame/issues/164#issuecomment-233528154
|
2016-07-20 08:57:46 +00:00
|
|
|
stack #?(:cljs (->> (js/Error. (str "Event " (first event-v) " dispatched from here:"))
|
|
|
|
.-stack
|
|
|
|
clojure.string/split-lines
|
2016-07-21 02:42:25 +00:00
|
|
|
(remove #(re-find #"react\.inc\.js|\(native\)" %))
|
2016-07-20 08:57:46 +00:00
|
|
|
(clojure.string/join "\n"))
|
|
|
|
:clj "n/a")]
|
2016-07-19 04:46:33 +00:00
|
|
|
(if (nil? event-v)
|
2016-07-19 23:52:26 +00:00
|
|
|
(throw (ex-info "re-frame: you called \"dispatch\" without an event vector." {}))
|
2016-07-19 04:46:33 +00:00
|
|
|
(push event-queue (with-meta event-v {:stack stack}))))
|
2016-06-03 14:13:41 +00:00
|
|
|
nil) ;; Ensure nil return. See https://github.com/Day8/re-frame/wiki/Beware-Returning-False
|
2015-03-04 13:00:36 +00:00
|
|
|
|
|
|
|
|
|
|
|
(defn dispatch-sync
|
2016-06-03 02:49:49 +00:00
|
|
|
"Immediately process an event using the registered handler.
|
|
|
|
|
2016-06-03 14:13:41 +00:00
|
|
|
Generally, you shouldn't use this. Use \"dispatch\" instead. It
|
2016-06-03 02:49:49 +00:00
|
|
|
is an error to even try and use it within an event handler.
|
2015-03-06 01:44:22 +00:00
|
|
|
|
|
|
|
Usage example:
|
|
|
|
(dispatch-sync [:delete-item 42])"
|
2015-03-04 13:00:36 +00:00
|
|
|
[event-v]
|
2015-03-06 01:44:22 +00:00
|
|
|
(handle event-v)
|
2016-06-03 14:13:41 +00:00
|
|
|
(-call-post-event-callbacks event-queue event-v) ;; ugly hack. Just so post-event-callbacks get called
|
|
|
|
nil) ;; Ensure nil return. See https://github.com/Day8/re-frame/wiki/Beware-Returning-False
|