4

I would like to write something like a clock app. The state is basically a number which is repeatedly incremented. One way of doing it can be seen here.

(ns chest-example.core
  (:require [om.core :as om :include-macros true]
            [om.dom :as dom :include-macros true]
            [cljs.core.async :as async])
  (:require-macros [cljs.core.async.macros :refer [go]]))

(defonce app-state (atom {:time 0}))

(defn clock-view [data owner]
  (reify
    om/IRender
    (render [_]
      (dom/div nil (pr-str data)))))

(go (while true
  (async/<! (async/timeout 1000))
  (om/transact! (om/root-cursor app-state) :time inc)))

(defn main []
  (om/root
    clock-view
    app-state
    { :target (. js/document (getElementById "clock"))}))

The problem I have with this that this is not reloadable code. Once I refresh code through fig wheel the incrementing gets faster since there are several things updating the state.

I tried to experiment with various ideas (basically making different components to own the go statement code) but I was not able to come up with something that would work.

Does anyone have a neat solution for this or do I just have to stick with it during development?

Daniel Compton
  • 11,420
  • 4
  • 33
  • 55
  • The traditional thing is to make sure that you write unmount events for your Om components that clean up nicely. Which, granted, means structuring things quite differently. – Charles Duffy May 15 '15 at 23:38
  • ...which is to say: Start the routine in IWillMount, shut it down in IWillUnmount. – Charles Duffy May 15 '15 at 23:42
  • @CharlesDuffy Hey Charles. Thanks for suggestion. I tried that but was unssucessful partially because I am not sure how to "undo" the channel. Also I was kinda hoping for something even smarter. – Tomas Svarovsky May 16 '15 at 04:59
  • The typical approach is to have the loop read from *either* a timeout or a close channel [using `alts!` or similar]. If the close channel, well, closes, then you exit the loop, and in your IWillUnmount, you close it. – Charles Duffy May 16 '15 at 05:27

2 Answers2

2

You must tell the goroutine when to stop running. The easiest way to do this is sending close! to tell the goroutine:

(ns myproject.core
  ;; imports
  )

(def my-goroutine
  (go-loop []
    (when (async/<! (async/timeout 1000))
      (om/transact! (om/root-cursor app-state) :time inc)
      (recur)))))

;; put in your on-reload function for figwheel
(defn on-reload []
  (async/close! my-goroutine))

Any goroutine that runs in a loop needs to be signaled to stop on reload (via figwheel's :on-jsload config).

;; project.clj
(defproject ;; ...
  :figwheel {:on-jsload "myproject.core/on-reload"}
)

It's best to treat long-running goroutines like a resource that needs to be managed. In golang, it's a common pattern to treat long-running goroutines as processes/tombstones to ensure proper teardown. The same should be applied to core.async's goroutines.

Jeff
  • 4,695
  • 1
  • 30
  • 29
  • When you say "signalled to stop on reload", you mean through the figwheel.client/start {:on-jsload} function? – Odinodin May 17 '15 at 19:42
  • This sounds like what I need. Unfortunately the code does not seem to work for me. Is it dependent on some specific version of clojurescript? I am running on chestnut which I think is not exactly on latest version. – Tomas Svarovsky May 20 '15 at 00:23
  • It wasn't that exact code to make it work. I've updated the answer with more accurate code and what you need configure in your project.clj for figwheel. – Jeff May 23 '15 at 23:57
0

Ok. After reading suggestions I took a stab at implementing something myself. I cannot claim that it is the best solution so feedback is welcomed but it seems to work. Basically it does what Charles suggested. I wrapped it inside a component which has callbacks for when the component itself is added or removed. I think that this would be difficult to do with figwheel onload hook anyway.

The alts! is used so we can grab input from 2 channels. When the component is "removed" from the DOM it sends the :killed signal to the alts! which exits the loop.

The clock-controller does not render anything it is there basically just to keep the clock ticking and update the app-state which than can be consumed through a cursor by arbitrary other components.

(defn clock-controller [state owner]
  (reify
    om/IInitState
      (init-state [_]
        {:channel (async/chan)})
    om/IWillMount
    (will-mount [_]
      (go (loop []
        (let [c (om/get-state owner :channel)
              [v ch] (async/alts! [(async/timeout 1000) c])]
          (if (= v :killed)
            nil
            (do
              (om/transact! state :time (fn [x] (+ x 1)))
              (recur)))))))
    om/IWillUnmount
    (will-unmount [_]
      (let [c (om/get-state owner :channel)]
        (go
          (async/>! c :killed)
          (async/close! c))))
    om/IRender
    (render [_])))