clojurescript

2025-11-14T17:03:34.412819Z

hey folks, I would love to have something as piotr-yuxuan/closeable-map {:mvn/version "0.36.2"} but for cljs

2025-11-14T17:03:47.059539Z

(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)])))))))

2025-11-14T17:03:53.969769Z

this looks like madness

2025-11-14T17:04:48.528099Z

is there any lib that does something like this?

2025-11-14T17:05:43.142899Z

(-> (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 %))))

p-himik 2025-11-16T11:21:42.892639Z

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)))))))

👍 1