Introduce de-duplicated subscriptions.

This commit is contained in:
Mike Thompson 2016-05-29 23:23:17 +10:00
parent 32ab35a436
commit 3d4e8cde8f
2 changed files with 98 additions and 32 deletions

View File

@ -1,11 +1,38 @@
## 0.8.0 (XXX)
Headline:
- re-frame subscriptions are now de-duplicated. As a result, some Signal graphs will be much more
efficient. The new behaviour better matches programmer intuitions about what "should" happen.
*Explanation*
Each subscription causes a handler to execute, producing
a reactive stream of updates. Two calls to `(subscribe [:some :query])` results in two copies of the same
subscription handler running, each delivering a stream of updates. Now, if these two subscriptions
were running at the same time, this would be inefficient. Both handlers would be
doing the same computations and delivering the same stream of updates. Unnecessary, duplicate work.
Starting with this version, this sort of duplication has been eliminated. Two, or more, concurrent
subscriptions for the same query will now source reactive updates from the one executing handler.
So, how do we know if two subscriptions are "the same". Answer: two subscriptions
are the same if their query vectors test `=` to each other.
So, these two subscriptions are *not* "the same": `[:some-event 42]` `[:some-event "blah"]`. Even
though they involve the same event id, `:some-event`, the query vectors do not test `=`.
Breaking:
- this version requires reagent 0.6.0 or later. It won't work with 0.5.N.
- requires Reagent >= 0.6.0 or later. It won't work with <= Reagent 0.5.2.
- XXX undo extracted and put into sister library
Improvements
- `debug` middleware logs a single log line instead of a group if there is no difference in app-db between
before and after running the handler.
- XXXX middleware for spec checking of event vectors
- XXXX better subscriptions of subscriptions
- XXX spec definitions of what subscriptions deliver ??
- when an event-handler makes no change to `app-db`, the `debug` middleware now logs a
single line saying so, rather than a "group". Makes it slightly easier to grok
the absence of change.
## 0.7.0 (2016-03-14)

View File

@ -1,46 +1,85 @@
(ns re-frame.subs
(:require
[reagent.ratom :refer [make-reaction] :refer-macros [reaction run!]]
[reagent.ratom :as ratom :refer [make-reaction] :refer-macros [reaction]]
[re-frame.db :refer [app-db]]
[re-frame.utils :refer [first-in-vector warn error]]))
;; maps from handler-id to handler-fn
(def ^:private key->fn (atom {}))
;; -- Subscription Handler Lookup and Registration --------------------------------------------------
;; Maps from a query-id to a handler function.
(def ^:private qid->fn (atom {}))
(defn clear-handlers!
"Unregisters all subscription handlers"
"Unregisters all existing subscription handlers"
[]
(reset! key->fn {}))
(reset! qid->fn {}))
(defn register
"Registers a handler function for an id"
[key-v handler-fn]
(if (contains? @key->fn key-v)
(warn "re-frame: overwriting subscription-handler for: " key-v)) ;; allow it, but warn.
(swap! key->fn assoc key-v handler-fn))
"Registers a subscription handler function for an query id"
[query-id handler-fn]
(if (contains? @qid->fn query-id)
(warn "re-frame: overwriting subscription handler for: " query-id)) ;; allow it, but warn. Happens on figwheel reloads.
(swap! qid->fn assoc query-id handler-fn))
;; -- Subscription cache -----------------------------------------------------
;;
;; De-duplicate subscriptions. If two or more identical subscriptions
;; are concurrently active, we want only one handler running.
;; Two subscriptions are "identical" if their query vectors
;; test "=".
(def ^:private query->reaction (atom {}))
(defn cache-and-return
"cache the reaction r"
[v dynv r]
(let [cache-key [v dynv]]
;; when this reaction is nolonger being used, remove it from the cache
(ratom/add-on-dispose! r #(do (swap! query->reaction dissoc cache-key)
(warn "Removing subscription: " cache-key)))
;; cache this reaction, so it can be used for identical subscriptions
(swap! query->reaction assoc cache-key r)
r)) ;; return the actual reaction
(defn cache-lookup
([query-v]
(cache-lookup query-v []))
([query-v dyn-v]
(get @query->reaction [query-v dyn-v])))
;; -- Subscription cache -----------------------------------------------------
(defn subscribe
"Returns a reagent/reaction which observes a part of app-db"
"Returns a Reagent/reaction which contains a computation"
([v]
(let [key-v (first-in-vector v)
handler-fn (get @key->fn key-v)]
(if (nil? handler-fn)
(error "re-frame: no subscription handler registered for: \"" key-v "\". Returning a nil subscription.")
(handler-fn app-db v))))
(if-let [cached (cache-lookup v)]
(do (warn "Using cached subscription: " v)
cached)
(let [query-id (first-in-vector v)
handler-fn (get @qid->fn query-id)]
(warn "Subscription crerated: " v)
(if-not handler-fn
(error "re-frame: no subscription handler registered for: \"" query-id "\". Returning a nil subscription."))
(cache-and-return v [] (handler-fn app-db v)))))
([v dynv]
(let [key-v (first-in-vector v)
handler-fn (get @key->fn key-v)]
(when ^boolean js/goog.DEBUG
(when-let [not-reactive (seq (remove #(implements? reagent.ratom/IReactiveAtom %) dynv))]
(warn "re-frame: dynv contained parameters that don't implement IReactiveAtom: " not-reactive)))
(if (nil? handler-fn)
(error "re-frame: no subscription handler registered for: \"" key-v "\". Returning a nil subscription.")
(let [dyn-vals (reaction (mapv deref dynv))
sub (reaction (handler-fn app-db v @dyn-vals))]
;; handler-fn returns a reaction which is then wrapped in the sub reaction
;; need to double deref it to get to the actual value.
(reaction @@sub))))))
(if-let [cached (cache-lookup v dynv)]
(do (warn "Using cached subscription: " v " and " dynv)
cached)
(let [query-id (first-in-vector v)
handler-fn (get @qid->fn query-id)]
(when ^boolean js/goog.DEBUG
(when-let [not-reactive (remove #(implements? reagent.ratom/IReactiveAtom %) dynv)]
(warn "re-frame: your subscription's dynamic parameters that don't implement IReactiveAtom: " not-reactive)))
(if (nil? handler-fn)
(error "re-frame: no subscription handler registered for: \"" query-id "\". Returning a nil subscription.")
(let [dyn-vals (reaction (mapv deref dynv))
sub (reaction (handler-fn app-db v @dyn-vals))]
;; handler-fn returns a reaction which is then wrapped in the sub reaction
;; need to double deref it to get to the actual value.
(warn "Subscription created: " v dynv)
(cache-and-return v dynv (reaction @@sub))))))))