230 lines
10 KiB
Clojure
230 lines
10 KiB
Clojure
(ns re-frame.subs
|
|
(:require
|
|
[re-frame.db :refer [app-db]]
|
|
[re-frame.interop :refer [add-on-dispose! debug-enabled? make-reaction ratom? deref? dispose! reagent-id]]
|
|
[re-frame.loggers :refer [console]]
|
|
[re-frame.utils :refer [first-in-vector]]
|
|
[re-frame.registrar :refer [get-handler clear-handlers register-handler]]
|
|
[re-frame.trace :as trace :include-macros true]))
|
|
|
|
(def kind :sub)
|
|
(assert (re-frame.registrar/kinds kind))
|
|
|
|
;; -- cache -------------------------------------------------------------------
|
|
;;
|
|
;; De-duplicate subscriptions. If two or more equal subscriptions
|
|
;; are concurrently active, we want only one handler running.
|
|
;; Two subscriptions are "equal" if their query vectors test "=".
|
|
(def query->reaction (atom {}))
|
|
|
|
(defn clear-subscription-cache!
|
|
"Runs on-dispose for all subscriptions we have in the subscription cache.
|
|
Used to force recreation of new subscriptions. Should only be necessary
|
|
in development.
|
|
|
|
The on-dispose functions for the subscriptions will remove themselves from the
|
|
cache.
|
|
|
|
Useful when reloading Figwheel code after a React exception, as React components
|
|
aren't cleaned up properly. This means a subscription's on-dispose function isn't
|
|
run when the components are destroyed. If a bad subscription caused your exception,
|
|
then you can't fix it without reloading your browser."
|
|
[]
|
|
(doseq [[k rxn] @query->reaction]
|
|
(dispose! rxn))
|
|
(if (not-empty @query->reaction)
|
|
(console :warn "Subscription cache should be empty after clearing it.")))
|
|
|
|
(defn clear-all-handlers!
|
|
"Unregisters all existing subscription handlers"
|
|
[]
|
|
(clear-handlers kind)
|
|
(clear-subscription-cache!))
|
|
|
|
(defn cache-and-return
|
|
"cache the reaction r"
|
|
[query-v dynv r]
|
|
(let [cache-key [query-v dynv]]
|
|
;; when this reaction is no longer being used, remove it from the cache
|
|
(add-on-dispose! r #(do (swap! query->reaction dissoc cache-key)
|
|
(trace/with-trace {:operation (first-in-vector query-v)
|
|
:op-type :sub/dispose
|
|
:tags {:query-v query-v
|
|
:reaction (reagent-id r)}}
|
|
nil)))
|
|
;; cache this reaction, so it can be used to deduplicate other, later "=" subscriptions
|
|
(swap! query->reaction assoc cache-key r)
|
|
(trace/merge-trace! {:tags {:reaction (reagent-id 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])))
|
|
|
|
|
|
;; -- subscribe -----------------------------------------------------
|
|
|
|
(defn subscribe
|
|
"Returns a Reagent/reaction which contains a computation"
|
|
([query-v]
|
|
(trace/with-trace {:operation (first-in-vector query-v)
|
|
:op-type :sub/create
|
|
:tags {:query-v query-v}}
|
|
(if-let [cached (cache-lookup query-v)]
|
|
(do
|
|
(trace/merge-trace! {:tags {:cached? true
|
|
:reaction (reagent-id cached)}})
|
|
cached)
|
|
|
|
(let [query-id (first-in-vector query-v)
|
|
handler-fn (get-handler kind query-id)]
|
|
(trace/merge-trace! {:tags {:cached? false}})
|
|
(if (nil? handler-fn)
|
|
(do (trace/merge-trace! {:error true})
|
|
(console :error (str "re-frame: no subscription handler registered for: \"" query-id "\". Returning a nil subscription.")))
|
|
(cache-and-return query-v [] (handler-fn app-db query-v)))))))
|
|
|
|
([v dynv]
|
|
(trace/with-trace {:operation (first-in-vector v)
|
|
:op-type :sub/create
|
|
:tags {:query-v v
|
|
:dyn-v dynv}}
|
|
(if-let [cached (cache-lookup v dynv)]
|
|
(do
|
|
(trace/merge-trace! {:tags {:cached? true
|
|
:reaction (reagent-id cached)}})
|
|
cached)
|
|
(let [query-id (first-in-vector v)
|
|
handler-fn (get-handler kind query-id)]
|
|
(trace/merge-trace! {:tags {:cached? false}})
|
|
(when debug-enabled?
|
|
(when-let [not-reactive (not-empty (remove ratom? dynv))]
|
|
(console :warn "re-frame: your subscription's dynamic parameters that don't implement IReactiveAtom:" not-reactive)))
|
|
(if (nil? handler-fn)
|
|
(do (trace/merge-trace! {:error true})
|
|
(console :error (str "re-frame: no subscription handler registered for: \"" query-id "\". Returning a nil subscription.")))
|
|
(let [dyn-vals (make-reaction (fn [] (mapv deref dynv)))
|
|
sub (make-reaction (fn [] (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.
|
|
;(console :log "Subscription created: " v dynv)
|
|
(cache-and-return v dynv (make-reaction (fn [] @@sub))))))))))
|
|
|
|
;; -- reg-sub -----------------------------------------------------------------
|
|
|
|
(defn- map-vals
|
|
"Returns a new version of 'm' in which 'f' has been applied to each value.
|
|
(map-vals inc {:a 4, :b 2}) => {:a 5, :b 3}"
|
|
[f m]
|
|
(into (empty m)
|
|
(map (fn [[k v]] [k (f v)]))
|
|
m))
|
|
|
|
|
|
(defn- deref-input-signals
|
|
[signals query-id]
|
|
(let [signals (cond
|
|
(sequential? signals) (map deref signals)
|
|
(map? signals) (map-vals deref signals)
|
|
(deref? signals) @signals
|
|
:else (console :error "re-frame: in the reg-sub for " query-id ", the input-signals function returns: " signals))]
|
|
(trace/merge-trace! {:tags {:input-signals (map reagent-id signals)}})
|
|
signals))
|
|
|
|
|
|
(defn reg-sub
|
|
"Associate the given `query id` with a handler function and an optional signal function.
|
|
|
|
There's 3 ways this function can be called
|
|
|
|
1. (reg-sub
|
|
:test-sub
|
|
(fn [db [_]] db))
|
|
The value in app-db is passed to the computation function as the 1st argument.
|
|
|
|
2. (reg-sub
|
|
:a-b-sub
|
|
(fn [q-vec d-vec]
|
|
[(subs/subscribe [:a-sub])
|
|
(subs/subscribe [:b-sub])])
|
|
(fn [[a b] [_]] {:a a :b b}))
|
|
|
|
Two functions provided. The 2nd is computation function, as before. The 1st
|
|
is returns what `input signals` should be provided to the computation. The
|
|
`input signals` function is called with two arguments: the query vector
|
|
and the dynamic vector. The return value can be singleton reaction or
|
|
a sequence of reactions.
|
|
|
|
3. (reg-sub
|
|
:a-b-sub
|
|
:<- [:a-sub]
|
|
:<- [:b-sub]
|
|
(fn [[a b] [_]] {:a a :b b}))```
|
|
This 3rd variation is just syntactic sugar for the 2nd. Pairs are supplied instead
|
|
of an `input signals` functions. `:<-` is supplied followed by the subscription
|
|
vector.
|
|
"
|
|
[query-id & args]
|
|
(let [computation-fn (last args)
|
|
input-args (butlast args) ;; may be empty, or one fn, or pairs of :<- / vector
|
|
err-header (str "re-frame: reg-sub for " query-id ", ")
|
|
inputs-fn (case (count input-args)
|
|
;; no `inputs` function provided - give the default
|
|
0 (fn
|
|
([_] app-db)
|
|
([_ _] app-db))
|
|
|
|
;; a single `inputs` fn
|
|
1 (let [f (first input-args)]
|
|
(when-not (fn? f)
|
|
(console :error err-header "2nd argument expected to be an inputs function, got:" f))
|
|
f)
|
|
|
|
;; one sugar pair
|
|
2 (let [[marker vec] input-args]
|
|
(when-not (= :<- marker)
|
|
(console :error err-header "expected :<-, got:" marker))
|
|
(fn inp-fn
|
|
([_] (subscribe vec))
|
|
([_ _] (subscribe vec))))
|
|
|
|
;; multiple sugar pairs
|
|
(let [pairs (partition 2 input-args)
|
|
markers (map first pairs)
|
|
vecs (map last pairs)]
|
|
(when-not (and (every? #{:<-} markers) (every? vector? vecs))
|
|
(console :error err-header "expected pairs of :<- and vectors, got:" pairs))
|
|
(fn inp-fn
|
|
([_] (map subscribe vecs))
|
|
([_ _] (map subscribe vecs)))))]
|
|
(register-handler
|
|
kind
|
|
query-id
|
|
(fn subs-handler-fn
|
|
([db query-vec]
|
|
(let [subscriptions (inputs-fn query-vec)
|
|
reaction-id (atom nil)
|
|
reaction (make-reaction
|
|
(fn [] (trace/with-trace {:operation (first-in-vector query-vec)
|
|
:op-type :sub/run
|
|
:tags {:query-v query-vec
|
|
:reaction @reaction-id}}
|
|
(computation-fn (deref-input-signals subscriptions query-id) query-vec))))]
|
|
(reset! reaction-id (reagent-id reaction))
|
|
reaction))
|
|
([db query-vec dyn-vec]
|
|
(let [subscriptions (inputs-fn query-vec dyn-vec)
|
|
reaction-id (atom nil)
|
|
reaction (make-reaction
|
|
(fn []
|
|
(trace/with-trace {:operation (first-in-vector query-vec)
|
|
:op-type :sub/run
|
|
:tags {:query-v query-vec
|
|
:dyn-v dyn-vec
|
|
:reaction @reaction-id}}
|
|
(computation-fn (deref-input-signals subscriptions query-id) query-vec dyn-vec))))]
|
|
(reset! reaction-id (reagent-id reaction))
|
|
reaction))))))
|