diff --git a/core/src/refx/hooks.cljs b/core/src/refx/hooks.cljs index 2b540af..cf0eab9 100644 --- a/core/src/refx/hooks.cljs +++ b/core/src/refx/hooks.cljs @@ -28,7 +28,7 @@ (react/useMemo (fn [] [(fn [callback] - (let [key (str "use-sub-" (swap! use-sub-counter inc))] + (let [key {::subs/index (swap! use-sub-counter inc)}] (subs/-add-listener sub key callback) #(subs/-remove-listener sub key))) (fn [] diff --git a/core/src/refx/subs.cljc b/core/src/refx/subs.cljc index 5e5d53b..9d83a01 100644 --- a/core/src/refx/subs.cljc +++ b/core/src/refx/subs.cljc @@ -76,6 +76,48 @@ (doseq [[_ sub] @sub-cache] (dispose! sub))) +(defonce ^:private listeners-state + (letfn [(comparator [a b] + (compare (::index a) (::index b)))] + (atom {:counter 0 :pending (sorted-map-by comparator)}))) + +(defn- invoke-listener + "This function is responsible for ensuring that signal listeners + (from DynamicSubs) are called before triggering regular listeners + (eg: added via use-sub hook). Listeners are triggered in the order they + were registered. This function ensures that one db update will only trigger + a single render." + [listener-key listener-fn] + (let [listener-fn-this-tick (atom nil)] + (swap! listeners-state (fn [state] + (let [new-state (update state :counter inc)] + (if (signal? listener-key) + ;; For the case of DynamicSub, we need to call its + ;; listener this tick to trigger dependent subs + (do (reset! listener-fn-this-tick listener-fn) + new-state) + (update new-state :pending assoc listener-key listener-fn))))) + + (when-let [f @listener-fn-this-tick] + (f)) + + (interop/next-tick + (fn [] + (let [{:keys [counter pending]} + (swap! listeners-state update :counter dec)] + + (when (zero? counter) + (doseq [[listener-key _] pending] + ;; Triggering a listener-fn can result in a subsequent sub's + ;; remove-listener to be called (which will remove it from pending). + ;; This check ensure it's still pending. + (let [listener-fn (atom nil)] + (swap! listeners-state (fn [state] + (reset! listener-fn (get-in state [:pending listener-key])) + (update state :pending dissoc listener-key))) + (when-let [f @listener-fn] + (f)))))))))) + (deftype Listeners [^:mutable listeners] Object (empty? [_] (empty? listeners)) @@ -84,8 +126,8 @@ (remove [_ k] (set! listeners (dissoc listeners k))) (notify [_] - (doseq [[_ f] listeners] - (f)))) + (doseq [[k f] listeners] + (invoke-listener k f)))) (defn- make-listeners [] (Listeners. nil)) @@ -122,6 +164,8 @@ (-add-listener [_ k f] (.add listeners k f)) (-remove-listener [this k] + (when-not (signal? k) + (swap! listeners-state update :pending dissoc k)) (.remove listeners k) (when (.empty? listeners) (sub-orphaned this))) @@ -200,6 +244,8 @@ (-add-listener [_ k f] (.add listeners k f)) (-remove-listener [this k] + (when-not (signal? k) + (swap! listeners-state update :pending dissoc k)) (.remove listeners k) (when (.empty? listeners) (sub-orphaned this))) diff --git a/core/test/refx/subs_test.cljs b/core/test/refx/subs_test.cljs index a7f6fa8..ca6cc20 100644 --- a/core/test/refx/subs_test.cljs +++ b/core/test/refx/subs_test.cljs @@ -104,3 +104,39 @@ (is (empty? @subs/sub-cache)) (done)) 10))))) + +(deftest listener-ordering + (let [source (atom 0)] + (subs/register :a (constantly source) identity) + (subs/register :b (constantly source) identity) + (subs/register :c (constantly (subs/sub [:b (subs/sub [:a])])) identity) + (subs/register :d (constantly [(subs/sub [:b]) (subs/sub [:c])]) identity) + (let [sub-a (subs/sub [:a]) + sub-b (subs/sub [:b]) + sub-c (subs/sub [:c]) + sub-d (subs/sub [:d]) + listener-count (atom 0) + listener-calls (atom []) + remove-listener-fns (atom '()) + add-listener! (fn [sub] + (let [key {::subs/index (swap! listener-count inc)}] + (subs/-add-listener sub key #(swap! listener-calls conj key)) + (swap! remove-listener-fns conj #(subs/-remove-listener sub key)))) + remove-listeners! (fn [] + (doseq [f @remove-listener-fns] + (f)))] + (add-listener! sub-a) + (add-listener! sub-b) + (add-listener! sub-c) + (add-listener! sub-d) + (reset! source 1) + (async done + (js/setTimeout (fn [] + (remove-listeners!) + (is (= @listener-calls + [{::subs/index 1} + {::subs/index 2} + {::subs/index 3} + {::subs/index 4}])) + (done)) + 10)))))