Search code examples
clojureclojurescriptcore.asyncom

core.async pub/sub behaves odd in Om (clojurescript)


Why is the counter in the child component updating fine when I comment

(om/update-state! owner :clicked not) 

and not when I uncomment it in the parent component in the code below? The counter is updated by clicking the button.

What I'm trying to accomplish is a pub/sub mechanism so components can exchange messages in a decoupled fashion.

You can replicate it by making a new project with:

lein new mies-om om-channel-test

Then replace core.cljs with code below and run

lein cljsbuild auto

Visit the index.html page in a modern browser (for example the latest Chrome).

The code:

(ns om-channel-test.core
  (:require-macros [cljs.core.async.macros :refer (go)])
  (:require [om.core :as om :include-macros true]
            [om.dom :as dom :include-macros true]
            [cljs.core.async :refer [chan pub <! sub >! timeout put!]]))

(enable-console-print!)

(def app-state (atom {:text "Hello world!"}))

(def event-ch (chan))

(def event-pub
  (pub event-ch #(:topic %)))

(defn child [cursor owner]
  (reify
    om/IInitState
    (init-state [_]
      {:counter 0})
    om/IWillMount
    (will-mount [_]
      (go (loop [] (<! (om/get-state owner :subscriber))
                (println "message received")
                (om/update-state! owner :counter inc)
                (recur))))
    om/IRender
    (render [_]
      (println "rendering child")
      (dom/p nil (om/get-state owner :counter)))
    om/IWillUnmount
    (will-unmount [_]
      (println "unmount"))))

(defn parent [cursor owner]
  (om/component
   (println "rendering parent")
   (dom/div nil
            (dom/button #js {:onClick
                             #(do
                                #_(om/update-state! owner :clicked not)
                                (go (>! event-ch {:topic :wizard
                                                  :message "hello"})))}
                        "Click")
            (om/build child
                      cursor
                      {:init-state
                       {:subscriber
                        ((om/get-shared owner :create-subscriber) :wizard)}}))))

(om/root
 parent
 app-state
 {:target (. js/document (getElementById "app"))
  :shared {:create-subscriber (fn [topic]
                                (sub event-pub
                                     topic (chan)))
           :event-ch event-ch}})

Solution

  • Answered on https://groups.google.com/forum/#!topic/clojurescript/5rCTfnulNXI.

    With line 41 uncommented the following seems to happen:

    1. Parent component's state changed

    2. om/react "walks" the component tree in parent's render to see what should update

    3. on line 45 with om/build for the child component finds that the child component already exists, so there is no new component created nor mounted.

    4. However, "running"/calling om/build on line 45 created a new subscription to the event-pub through :subscriber/:create-subscriber in {:init-state ...}

    5. There will not be a new component created that would create a go-loop to consume from this new subscriber channel (there's no call to om/will-mount for a new component from line 22)

    6. Now event-pub has two subscribers but only one go-loop that consumes from a channel. The pub on :event-ch will block [1] [2]

    7. Weirdness on the page

    Seems you shouldn't have side-effects in the {:init-state ...} passed to om/build. Instead pass the event-pub to the child component via :init-state and create the sub chan together with the go-loop to consume from it.

    [1] http://clojure.github.io/core.async/#clojure.core.async/pub "Each item is distributed to all subs in parallel and synchronously, i.e. each sub must accept before the next item is distributed. Use buffering/windowing to prevent slow subs from holding up the pub."

    [2] Play around with buffering in the chan on line 57 to see this behavior change for a couple of clicks