Hey everyone, I suspect I'm missing something very obvious. For exploration purposes I'm trying to use missionary to manage a datascript connection as the next level from the atom watchers. I can stand it up but I don't know where to conceptually put the datascript connection. If I put that reference in a global atom I feel like I'm using a side channel around missionary. Or am I worrying too much because if one starts a resource that listens to a tcp port, one can also affect that resource from outside missionary. Is there some simple default pattern? (Differently phrased: How are missionary resources supposed to be composed and accessed, the proper way to handle state in missionary's supervision model)
Maybe the tension in my mind is RAII lifecycle management and the "streaming" (emitting values over time) way to use missionary.
This is where I stopped: https://gist.github.com/bennyandresen/40038a2651d6636f4921b112490b0c6a (The goal that I didn't achieve was to compose or inject so that the handler is aware of datascript as a reference) I will be AFK the rest of the day, but thanks in advance!
For global resources, that are needed for the whole lifetime of the program, I would not overthink it and just use mount, component etc. For local resources, that are needed dynamically, you should have a look at m/observe E.g. some pseudo code:
(defn create-resource [!]
(let [r (new-resource)]
(! (get-state r)
(on-change r (fn [new-state] (! new-state)))
#(destroy r)))
(m/observe create-resource)
If you just need to keep track of some state an atom will often be sufficient with (m/watch).Your snippet looks fine at first glance (but I have no time to test). However, as said above, I would use mount and co to manage global things. It is simple and much more repl friendly. E.g. you can keep db state, when using in-mem db and other goodies.
I do think there's nothing wrong with using imperative code, but I also like to looking for functional ways of implementing things in case it might turn out to be simpler or easier to reuse. I note that your start-db is doing two things, creating a connection and attaching a listener to the connection. I'd try separating these, so that the function which is attaching the listener and returning a flow is passed the connection instead of creating it itself. Perhaps then start-system could create the connection, and conn could become a simple variable with the connection as its value instead of being an atom. Now you would also no longer would need the :init message to tunnel the connection value through the flow.
Earlier I had said:
> I'd try separating these
I missed that you were calling reset! on the connection, so my suggestion wouldn't apply to your code.
If you need to access a managed resource from outside of the process that is managing that resource, then using a side-channel is your only option.
To avoid this problem I usually try to compose my effects such that the processes that need resource X are supervised by the process managing resource X. Example :
(defn with-conn [schema f & args]
(m/sp (let [conn (d/create-conn schema)]
(try (m/? (apply f conn args))
(finally (reset! conn nil))))))
(defn start-db [conn]
(m/observe
(fn [!]
(let [tx-report-key (keyword (gensym "tx-listener"))]
(d/listen! conn tx-report-key !)
#(d/unlisten! conn tx-report-key)))))
(comment
(def schema {:name {:db/unique :db.unique/identity}})
(defn app [conn]
(m/join {}
(m/reduce (fn [_ x] (prn x)) nil (start-db conn))
(m/sp
(d/transact conn [{:name "Merlin"}])
(d/transact conn [{:name "Voldemort"}])
(d/transact conn [{:name "Sarah"}]))))
(def cancel ((with-conn schema app) prn prn))
)Thanks everyone. I'm not yet sure of the way to go, but this feedback helps. 🙂