subtle om + core.async problems

277 views
Skip to first unread message

Alexander Hudek

unread,
Jul 23, 2014, 7:19:38 PM7/23/14
to clo...@googlegroups.com
I've encountered two subtle but serious problems using om with core.async.

The first one is illustrated by this code: 


First, one obvious solution here is to move the dump-chan inside the form state. 
However, it's written this way to illustrate the error which originally arose in a 
dialog component that took both an action channel that receives button presses
and a dialog content component. It exposed the action channel in this way in 
order to be flexible. 

I believe the cause of this error is that when you toggle the form in the demo 
to off, it unmounts the component. However, the go-block is still active and
listening to the channel. When you toggle the form back on, a new component
is created and mounted. Now you have two components listening on the same
channel!

The ideal solution might be to find a way to end the go block when the component
unmounts. This is easy to do on a case by case basis, but not easy to do in a
completely generic fashion. Here is one solution:


The second problem is easy to fix, however, I don't understand why it happens.


Instead of initializing the dump channel in the base components state, we initialize
it when creating the form component. As soon as you start typing into the form
the channel breaks. It was suggested on irc that this might be because of a 
re-render of base would create a new channel, but the form component will not
be re-mounted because it's already mounted. However, putting in some debug
code shows that there is no re-render of the base component occurring between
when the channel works and when it doesn't. 

I'm posting this here mostly to warn people to be wary of these situations. Of
course, if you have any suggestions or explanations for these I'd love to hear them.

Alex

Sean Corfield

unread,
Jul 23, 2014, 9:36:03 PM7/23/14
to clo...@googlegroups.com
You'll want to read this thread: https://groups.google.com/forum/#!topic/clojurescript/DHJvcGey8Sc

In particular:

"So if you have code that's like this, those components will want to clean up after themselves in IWillUnmount."

That should address your first problem?

I'm not sure what to suggest right now about the second problem.

Sean
signature.asc

Daniel Kersten

unread,
Jul 25, 2014, 5:26:55 AM7/25/14
to clo...@googlegroups.com
You could simplify your fix code a small bit by using go-loop and when, like this:

(go-loop []
  (let [[v ch] (alts! [dump-chan (om/get-state owner :exit-chan)])]
    (when (= ch dump-chan)
      (.log js/console "dumping state:")
      (.log js/console (pr-str (om/get-state owner)))
      (recur))))

This won't work in your code because my-form's parent owns dump-chan, but if a component owns the channel, an alternative approach is to simply close dump-chan in IWillUnmount and change your go block to something like this:

(go-loop []
  (when-let [v (<! dump-chan)]
    (.log js/console "dumping state:")
    (.log js/console (pr-str (om/get-state owner)))
    (recur)))

In my own code, I've abstracted my go blocks into om-tools mixins that handle the killing of go blocks.

I'm not sure why the second problem happens. As a general rule, I never create new resources in render (IMO render should be pure functional: app state and local state in -> new dom nodes out; but as the child stores the channel between calls to parents render, this breaks that "rule") and I've never had the issue.
Since base isn't being re-rendered in this case, I'm not sure why this would be a problem here.

Alexander K. Hudek

unread,
Jul 25, 2014, 2:13:48 PM7/25/14
to clo...@googlegroups.com
Could you describe your mixin a bit more? We’ve just written an om component and macro to help clean up go-blocks in cases like these. Several of our components take channels as input from parent components to allow for more sophisticated communication. We’ll post a link to this manager component today or tomorrow after we add some documentation.

Regarding the second problem, we don’t typically do that either. I was just being lazy when writing the error demo and ran into. Curious behaviour, but easy to avoid the problem.
--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@googlegroups.com
Note that posts from new members are moderated - please be patient with your first post.
To unsubscribe from this group, send email to
clojure+u...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
---
You received this message because you are subscribed to a topic in the Google Groups "Clojure" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/clojure/abczlIGvogk/unsubscribe.
To unsubscribe from this group and all its topics, send an email to clojure+u...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Daniel Kersten

unread,
Jul 25, 2014, 3:59:44 PM7/25/14
to clo...@googlegroups.com
The way I use mixins is really simple and might be a bit too specific to how I currently use channels I plan to explore the idea more, but haven't had the time yet. I'm sure you can build much more sophisticated abstractions.

I don't have the code in front of me right now, but the basic idea is as follows: Before om-tools, I had a subscribe function that wrapped core.async/sub. You pass it a keyword topic, owner and a function that takes the value as its argument. All messages on the channel take the form [topic value]. I also had a matching unsubscribe function that took owner and killed the go blocks created by all subscribe calls in that component. The problem I had was that it was very easy to forgot to call unsubscribe in IWillUnmount (plus the extra boilerplate was a bit annoying). So I wrote a mixin that looks something like this:

(defmixin subscriber
  (will-unmount [owner]
    (my-unsubscribe owner))
  (subscribe [owner topic callback]
    (my-subscribe owner topic callback)))

Which can then be used like this:

(defcomponentk a-component [owner]
  (:mixins subscriber)
  (did-mount [_]
    (.subscribe owner :foo (fn [v] ...)))
  (render [_]
    ...))


For your purposes, perhaps something like this would work:

(defmixin go-block-aware
  (init-state []
    {:exit-chan (async/chan)})
  (will-unmount [owner]
   (async/put! (om/get-state owner :exit-chan) true))

  (go [owner callback]
    (let [exit-chan (om/get-state owner :exit-chan)]
      (go-loop []
        (let [[v ch] (async/alts! [dump-chan exit-chan])]
          (when (= ch dump-chan)
            (callback v)
            (recur)))))))

And use like this:

(defcomponentk a-component [owner]
  (:mixins go-block-aware)
  (did-mount [_]
    (.go owner (fn [v] ...)))
  (render [_]
    ...))







You received this message because you are subscribed to the Google Groups "Clojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email to clojure+u...@googlegroups.com.

Dylan Butman

unread,
Jul 28, 2014, 1:11:46 PM7/28/14
to clo...@googlegroups.com
That looks great Dan!

here's a slightly different version that takes a supplied channel and kills the go on either component unmount or the supplied channel being closed 

(defmixin go-block-aware
  (init-state []
              {:chans {:mounted (async/chan)}})
  (will-unmount [owner]
                (async/close! (om/get-state owner [:chans :mounted])))

  (go [owner read-chan callback]
      (let [mounted (om/get-state owner [:chan :mounted])]
        (go-loop []
                 (when-some [v (first (async/alts! [read-chan exit-chan]))]
                   (callback v)
                   (recur))))))

I've used something like this before with mult/tap

(defn create-mults [chans]
  {:chans chans
   :mults (zipmap (keys chans) (map mult (vals chans)))})

(defn get-shared-chan [owner key]
  (om/get-shared owner [:chans key]))

(defn tap-shared-chan
  ([owner key] (tap-shared-chan owner key (async/chan)))
  ([owner key ch]
     (tap (om/get-shared owner [:mults key]) ch)))

(om/root app-view app-state {:target (.getElementById js/document "app")
                             :shared (create-mults {:nav (chan)
                                                    :events (chan)})})

and then in your component you could use something like what Dan suggests

(defcomponentk a-component [owner]
  (:mixins go-block-aware)
  (did-mount [_]
    (.go owner (tap-shared-chan owner :nav) (fn [v] ...)))
  (render [_]
    ...))

I usually then submit tuples to the shared channels and then destructure

(fn [[action & args]
    (case action
      :login (let [[id] args]
               (do-login id))])


Reply all
Reply to author
Forward
0 new messages