hey folks, I would love to have something as piotr-yuxuan/closeable-map {:mvn/version "0.36.2"} but for cljs
(defmacro with-resource-map
"Executes a body of code with multiple asynchronous resources acquired in
parallel, ensuring all successfully acquired resources are closed afterward.
- bindings-map: A map of {sym [open-expr close-fn], ...}.
- sym: The symbol to bind the opened resource to.
- open-expr: An expression that returns a Promise which resolves to the resource.
- close-fn: A function that takes the resource and performs cleanup.
The macro acquires all resources in parallel using Promise.all and
guarantees that a close-fn is called for every resource that was
successfully acquired, even if other resources fail to open or the
body throws an error."
[bindings-map & body]
(let [;; Create a map of {symbol -> gensym'd atom name} for hygiene.
resource-atoms (into {} (for [k (keys bindings-map)]
[k (gensym "resource-atom-")]))
;; Extract symbols, open exprs, and close fns.
symbols (vec (keys bindings-map))
open-exprs (map #(first (get bindings-map %)) symbols)
close-fns (map #(second (get bindings-map %)) symbols)]
`(let [;; Define all the atoms, initialized to nil.
~@(mapcat (fn [sym] [sym `(atom nil)]) (vals resource-atoms))]
(-> (js/Promise.all
(clj->js
[~@(map-indexed
(fn [i open-expr]
;; For each open-expr, create a promise that, on success,
;; populates the corresponding atom and passes the result through.
`(-> ~open-expr
(.then (fn [res#]
(reset! ~(get resource-atoms (get symbols i)) res#)
res#)))) ; Return res# to pass it to Promise.all
open-exprs)]))
(.then (fn [results#]
;; Once all promises succeed, bind symbols to the atom values
;; and execute the body.
(let [~@(mapcat (fn [sym] [sym `(deref ~(get resource-atoms sym))]) symbols)]
~@body)))
(.finally (fn []
;; Finally, close every resource that was successfully acquired
;; (i.e., every non-nil atom).
(js/Promise.all
(clj->js
[~@(map-indexed
(fn [i close-fn]
`(when-let [resource# @~(get resource-atoms (get symbols i))]
(~close-fn resource#)))
close-fns)])))))))this looks like madness
is there any lib that does something like this?
(-> (with-resource-map
{auth [(init-auth-service {:valid-key false}) shutdown-auth-service] ; This one will fail
logger [(init-logger-service {:level :info}) shutdown-logger-service]}
(js/console.log "This body will not execute."))
(.then #(js/console.log "FINAL RESULT:" %))
(.catch #(js/console.error "FINAL ERROR:" (.-message %))))For future reference, please post multiple messages as a single message (editing could be used) or in a thread, so there are no multiple notifications for people and no risk of starting multiple threads. As for the question - FWIW a macro generating a mutable state seems like an overkill here so I'd probably do something like this instead:
(defn start-services [k->p+close-fn]
(-> (js/Promise.all (mapv (fn [[k [p close-fn]]]
(-> p
(.then (fn [result]
{:service {k result}
:close-fn (when close-fn {k close-fn})}))
(.catch (fn [err]
{:failure {k err}}))))
k->p+close-fn))
(.then (fn [results]
{:services (apply merge (keep :service results))
:close-fns (apply merge (keep :close-fn results))
:failures (apply merge (keep :failure results))}))))
(-> (start-services {:auth [(init-auth-service {:valid-key false})
shutdown-auth-service]
:logger [(init-logger-service {:level :info})
shutdown-logger-service]})
(.then (fn [{:keys [services close-fns failures]}]
(try
(when (empty? failures)
(let [{:keys [auth logger]} services]
...))
(finally
(doseq [f close-fns]
(f)))))))