2015-03-04 13:00:36 +00:00
|
|
|
(ns re-frame.router
|
2015-11-04 13:07:33 +00:00
|
|
|
(:require [reagent.impl.batching :refer [do-later]]
|
2015-11-02 11:37:46 +00:00
|
|
|
[re-frame.handlers :refer [handle]]
|
2015-11-04 13:07:33 +00:00
|
|
|
[re-frame.utils :refer [error]]
|
2015-09-27 08:39:35 +00:00
|
|
|
[goog.async.nextTick]))
|
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
|
|
|
;;
|
2015-11-02 11:37:46 +00:00
|
|
|
;; Conceptually, the task is to process events in a perpetual loop, one after
|
2015-11-04 14:00:45 +00:00
|
|
|
;; the other, FIFO, calling the right event-handler for each, being idle when
|
|
|
|
;; there are no events, and firing up when one arrives, etc. The processing
|
|
|
|
;; of an event happens "asynchronously" sometime after the event is
|
|
|
|
;; dispatched.
|
2015-03-04 13:00:36 +00:00
|
|
|
;;
|
2015-11-02 11:37:46 +00:00
|
|
|
;; In practice, 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
|
|
|
;;
|
2015-11-04 14:00:45 +00:00
|
|
|
;; 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:
|
|
|
|
;; - maintain a 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".
|
|
|
|
;; - when processing events, do ALL the ones currently queued. Don't stop.
|
|
|
|
;; Don't yield to the browser. Hog that CPU.
|
2015-11-04 14:00:45 +00:00
|
|
|
;; - but if any new events arrive 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
|
|
|
|
;; 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,
|
|
|
|
;; with yielding in between.
|
2015-11-02 11:37:46 +00:00
|
|
|
;; - In some cases, an event should not be run until after the GUI has been
|
2015-11-04 14:00:45 +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
|
|
|
|
;; events are processed sequentially: we handle each event before we
|
|
|
|
;; handle the ones behind it.
|
2015-03-04 13:00:36 +00:00
|
|
|
;;
|
2015-11-02 11:37:46 +00:00
|
|
|
;; Implementation
|
2015-11-04 02:33:50 +00:00
|
|
|
;; - queue processing can be in a number of states: scheduled, running, paused
|
|
|
|
;; etc. So it is modeled explicitly as a FSM.
|
|
|
|
;; 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
|
|
|
|
;; - when the event has :dom-flush we schedule via "reagent.impl.batching.doLater"
|
2015-11-04 02:33:50 +00:00
|
|
|
;; which will run event processing after the next reagent animation frame.
|
2015-11-02 11:37:46 +00:00
|
|
|
;;
|
|
|
|
|
|
|
|
|
2015-11-04 13:34:23 +00:00
|
|
|
;; A map from event metadata keys to the corresponding "run later" functions
|
|
|
|
(def later-fns
|
|
|
|
{:flush-dom do-later ;; after next annimation frame
|
|
|
|
:yield goog.async.nextTick}) ;; almost immediately
|
|
|
|
|
2015-11-02 11:37:46 +00:00
|
|
|
(defprotocol IEventQueue
|
|
|
|
(enqueue [this event])
|
|
|
|
|
|
|
|
;; Finite State Machine transitions
|
|
|
|
(-fsm-trigger [this trigger arg])
|
|
|
|
|
|
|
|
;; Finite State Machine (FSM) actions
|
|
|
|
(-add-event [this event])
|
|
|
|
(-process-1st-event [this])
|
|
|
|
(-run-next-tick [this])
|
|
|
|
(-run-queue [this])
|
2015-11-04 13:34:23 +00:00
|
|
|
(-pause-run [this later-fn])
|
2015-11-02 11:37:46 +00:00
|
|
|
(-exception [this ex])
|
2015-11-04 06:11:42 +00:00
|
|
|
(-begin-resume [this]))
|
2015-11-02 11:37:46 +00:00
|
|
|
|
|
|
|
|
2015-11-02 12:43:19 +00:00
|
|
|
;; Want to understand this? Look at FSM in -fsm-trigger?
|
2015-11-02 11:37:46 +00:00
|
|
|
(deftype EventQueue [^:mutable fsm-state ^:mutable queue]
|
|
|
|
IEventQueue
|
|
|
|
|
|
|
|
(enqueue [this event]
|
|
|
|
(-fsm-trigger this :add-event event))
|
|
|
|
|
|
|
|
;; Finite State Machine "Actions"
|
|
|
|
(-add-event
|
|
|
|
[this event]
|
|
|
|
(set! queue (conj queue event)))
|
|
|
|
|
|
|
|
(-process-1st-event
|
|
|
|
[this]
|
|
|
|
(let [event-v (peek queue)]
|
|
|
|
(try
|
|
|
|
(handle event-v)
|
|
|
|
(catch :default ex
|
|
|
|
(-fsm-trigger this :exception ex)))
|
|
|
|
(set! queue (pop queue))))
|
|
|
|
|
|
|
|
(-run-next-tick
|
|
|
|
[this]
|
|
|
|
(goog.async.nextTick #(-fsm-trigger this :begin-run nil)))
|
|
|
|
|
|
|
|
(-exception
|
|
|
|
[_ ex]
|
|
|
|
(set! queue #queue []) ;; purge the queue
|
|
|
|
(throw ex))
|
|
|
|
|
|
|
|
;; 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]
|
|
|
|
(let [queue-length (count queue)]
|
|
|
|
(loop [n queue-length]
|
|
|
|
(if (zero? n)
|
|
|
|
(-fsm-trigger this :finish-run nil)
|
|
|
|
(let [event-v (peek queue)]
|
2015-11-04 13:34:23 +00:00
|
|
|
(if-let [later-fn (some later-fns (keys (meta event-v)))]
|
|
|
|
(-fsm-trigger this :pause-run later-fn)
|
2015-11-02 11:37:46 +00:00
|
|
|
(do (-process-1st-event this)
|
|
|
|
(recur (dec n)))))))))
|
|
|
|
|
|
|
|
(-pause-run
|
2015-11-04 13:34:23 +00:00
|
|
|
[this later-fn]
|
|
|
|
(later-fn #(-fsm-trigger this :begin-resume nil)))
|
2015-11-02 11:37:46 +00:00
|
|
|
|
2015-11-04 06:11:42 +00:00
|
|
|
(-begin-resume
|
2015-11-02 11:37:46 +00:00
|
|
|
[this]
|
|
|
|
(-process-1st-event this) ;; do the event which paused processing
|
2015-11-04 05:14:11 +00:00
|
|
|
(-fsm-trigger this :finish-resume nil)) ;; do the rest of the queued events
|
2015-11-02 11:37:46 +00:00
|
|
|
|
|
|
|
(-fsm-trigger
|
2015-11-04 13:55:17 +00:00
|
|
|
[this trigger arg]
|
2015-11-02 11:37:46 +00:00
|
|
|
|
|
|
|
;; work out new FSM state and action function for the transition
|
|
|
|
(let [[new-state action-fn]
|
2015-11-04 06:11:42 +00:00
|
|
|
(case [fsm-state trigger]
|
2015-11-02 11:37:46 +00:00
|
|
|
|
2015-11-04 14:01:56 +00:00
|
|
|
;; Here is the FSM
|
|
|
|
;; [current-state trigger] [new-state action-fn]
|
2015-11-02 11:37:46 +00:00
|
|
|
|
2015-11-04 14:01:56 +00:00
|
|
|
;; the queue is idle
|
2015-11-04 13:55:17 +00:00
|
|
|
[:quiescent :add-event] [:scheduled #(do (-add-event this arg)
|
|
|
|
(-run-next-tick this))]
|
2015-11-02 11:37:46 +00:00
|
|
|
|
|
|
|
;; processing has been already been scheduled to run in the future
|
2015-11-04 13:55:17 +00:00
|
|
|
[:scheduled :add-event] [:scheduled #(-add-event this arg)]
|
2015-11-02 11:37:46 +00:00
|
|
|
[:scheduled :begin-run] [:running #(-run-queue this)]
|
|
|
|
|
|
|
|
;; processing one event after another
|
2015-11-04 13:55:17 +00:00
|
|
|
[:running :add-event ] [:running #(-add-event this arg)]
|
|
|
|
[:running :pause-run ] [:paused #(-pause-run this arg)]
|
|
|
|
[:running :exception ] [:quiescent #(-exception this arg)]
|
|
|
|
[:running :finish-run] (if (empty? queue) ;; FSM guard
|
|
|
|
[:quiescent]
|
|
|
|
[:scheduled #(-run-next-tick this)])
|
2015-11-02 11:37:46 +00:00
|
|
|
|
|
|
|
;; event processing is paused - probably by :flush-dom metadata
|
2015-11-04 13:55:17 +00:00
|
|
|
[:paused :add-event ] [:paused #(-add-event this arg)]
|
|
|
|
[:paused :begin-resume] [:resuming #(-begin-resume this)]
|
2015-11-02 11:37:46 +00:00
|
|
|
|
2015-11-04 14:01:56 +00:00
|
|
|
;; processing the event that caused the queue to be paused
|
2015-11-04 13:55:17 +00:00
|
|
|
[:resuming :add-event ] [:resuming #(-add-event this arg)]
|
|
|
|
[:resuming :exception ] [:quiescent #(-exception this arg)]
|
2015-11-04 05:14:11 +00:00
|
|
|
[:resuming :finish-resume] [:running #(-run-queue this)]
|
2015-11-02 11:37:46 +00:00
|
|
|
|
|
|
|
(throw (str "re-frame: state transition not found. " fsm-state " " trigger)))]
|
|
|
|
|
|
|
|
;; change state and run the action fucntion
|
|
|
|
(set! fsm-state new-state)
|
|
|
|
(when action-fn (action-fn)))))
|
|
|
|
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
;; This is the global queue for events
|
|
|
|
;; When an event is dispatched, it is put into this queue. Later the queue
|
|
|
|
;; will "run" and the event will be "handled" by the registered event handler.
|
2015-03-04 13:00:36 +00:00
|
|
|
;;
|
2015-05-02 00:52:11 +00:00
|
|
|
|
2015-11-02 11:37:46 +00:00
|
|
|
(def event-queue (->EventQueue :quiescent #queue []))
|
|
|
|
|
|
|
|
|
|
|
|
;; ---------------------------------------------------------------------------
|
|
|
|
;; Dispatching
|
|
|
|
;;
|
2015-03-04 13:00:36 +00:00
|
|
|
|
|
|
|
(defn dispatch
|
2015-11-04 14:03:01 +00:00
|
|
|
"Queue an event to be processed by the registered handler.
|
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]
|
|
|
|
(if (nil? event-v)
|
2015-11-04 14:01:56 +00:00
|
|
|
(error "re-frame: \"dispatch\" is ignoring a nil event.")
|
2015-11-02 11:37:46 +00:00
|
|
|
(enqueue event-queue event-v))
|
|
|
|
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
|
2015-11-04 14:03:01 +00:00
|
|
|
"Send an event to be processed by the registered handler
|
|
|
|
immediately. Note: dispatch-sync may not be called while another
|
|
|
|
event is being handled.
|
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)
|
2015-11-02 11:37:46 +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
|
|
|
|