missionary

telekid 2025-10-14T17:32:17.100519Z

I'm trying to use missionary RAII to open and close portal on demand. This works: ๐Ÿงต

telekid 2025-10-24T02:10:44.857639Z

I've written this up in a bit more depth here: https://github.com/leonoel/missionary/issues/132

telekid 2025-10-14T17:32:30.535529Z

(def >portal
  (m/signal
   (m/observe
    (fn [cb]
      ;; TODO: Technically we shouldn't block here
      ;; TODO: Handle errors
      ((m/via m/blk ((requiring-resolve 'portal.api/open))) {} {})
      (cb :open)
      (fn []
        ((m/via m/blk ((requiring-resolve 'portal.api/close))) {} {}))))))

telekid 2025-10-14T17:35:14.205369Z

...but there are two things that feel off. 1. Manually running the tasks in m/observe smells weird 2. The cleanup thunk calls portal.api/close's arity-zero form. It would be preferrable to call the arity-1 form, which accepts a portal as an argument. However, I'm struggling to come up with a clean pattern for passing the constructed portal object to the cleanup thunk.

telekid 2025-10-14T17:36:03.292859Z

I've run into this in a few other places. I guess the broader question is, what is the idiomatic way of using m/observe to instantiate an object with a blocking constructor and destructor?

Absolute Negativity 2025-10-15T03:07:01.246199Z

I think this could work:

(m/?
  (m/reduce conj []
    (m/eduction (take 1)
      (m/ap
        (let [resource (m/? (m/via m/blk (prn :init!) :the-resource))]
          (m/amb= resource
            (try (m/? m/never)
              (finally (m/? (m/via m/blk (prn :cleanup!)))))))))))
;; outputs:
;; :init!
;; :cleanup!
;; => [:the-resource]
Hmm, doing Thread/sleep or other blocking calls in the cleanup task might be problematic, couldn't get prn output then.

telekid 2025-10-19T17:44:54.383109Z

Another cancellation puzzle. Here, resource b depends on resource a. Both resources are represented as flows:

(def a*
  (m/stream
   (m/ap
    (let [a :a]
      (m/amb= (do (println "Opening" a)
                  a)
              (try (m/? m/never)
                   (finally (println "Closing" a))))))))

(def b*
  (m/ap
   (let [a (m/?< a*)
         b [a :b]]
     (m/amb= (do
               (println "Opening" b)
               (m/amb))
             (try (m/? m/never)
                  (finally (println "Closing" b)))))))

(def cancel
  ((m/reduce prn {} b*) prn prn))

(cancel)
When I invoke (cancel), I want b to close before a (since the resource b depends on the existence of a ), but the opposite actually happens โ€“ a closes before b. It's almost as if my intuition about the supervision tree is backwards. I can't quite figure out how to get the behavior that I want. (Well, actually, I can if I inline b inside a, but then I lose some of the compositional properties that I'd like to hold onto.)

telekid 2025-10-17T13:16:59.702899Z

Hm, as @itsfah pointed out, it looks like x will never contain :c or :d.

leonoel 2025-10-17T13:19:04.906499Z

use m/compel on cleanup task to disable interruption

๐Ÿ’ก 1
telekid 2025-10-17T13:20:30.561849Z

telekid 2025-10-17T13:20:30.685329Z

Ah! Cool, this works

telekid 2025-10-17T13:21:29.811819Z

I've been wondering when I would use m/compel, that answers that.

telekid 2025-10-17T13:22:17.862799Z

It isn't entirely clear to me how that task receives the cancelation signal, but I'll do a bit of experimenting and see if I can build some intuition.

telekid 2025-10-17T13:23:58.853269Z

(One thing that I've found challenging about missionary is noticing and debugging cancelation. Some kind of tracing mechanism for that would be very useful. I'd love to get a sense of how the hyperfiddle team thinks about this.)

leonoel 2025-10-15T08:27:21.840849Z

yes that's the right pattern, blocking in the cleanup task is fine as long as it's in the m/via

telekid 2025-10-15T16:52:21.368849Z

Thank you both! This is great.

telekid 2025-10-15T16:57:14.350899Z

I would have never thought of the (m/? m/never) pattern, but it makes a lot of sense. You need some flow to throw during cancellation.