clojurescript

weavejester 2026-01-07T12:05:37.219199Z

What's the best way of mapping over a collection with a function that returns a Promise? In my case I have a collection of encrypted messages that I want to decrypt, but the SubtleCrypto.decrypt method returns a Promise. The messages contain a sequential ID so I'm not worried about preserving order, as I can always sort them later. My current best idea is to convert the Promises into channels and then use clojure.core.async/merge, but is there a better way?

p-himik 2026-01-07T12:10:39.318919Z

I'd just use js/Promise.all.

borkdude 2026-01-07T12:10:56.506279Z

You can just use map and then Promise.all to turn the promises in a single Promise of an array of values

weavejester 2026-01-07T12:11:55.858639Z

Does that work with Clojure-specific collections, then? Like a seq returned from map?

borkdude 2026-01-07T12:12:18.709019Z

I think it does

borkdude 2026-01-07T12:12:31.747759Z

If not you could turn it into an array first

weavejester 2026-01-07T12:12:56.125809Z

That's true. I'll test it out. Thanks!

p-himik 2026-01-07T12:13:03.282909Z

It does work, yes.

weavejester 2026-01-07T12:26:44.982959Z

Currently I have a map function like:

(map #(update % :data decrypt key) messages)
I guess I'd need to put the update in a .then
(js/Promise.all
 (map (fn [{:keys [data] :as msg}]
        (-> (decrypt data key)
            (.then #(assoc msg :data %))))
      messages))
Or make a promised-update function:
(defn promised-update [m k f & args]
  (-> (apply f (m k) args) (.then #(assoc m k %)))
Feels kinda monad-y.

borkdude 2026-01-07T12:37:01.669789Z

Promises are basically monads except not in JS since you can't nest a promise, it automatically unnests. Anyway that's a tangent

💯 1
Harold 2026-01-07T13:42:39.191219Z

Out of curiosity, what is the next step in the process? What are you doing with the decrypted messages in this case?

weavejester 2026-01-07T13:44:43.345069Z

Rendering them on the page. The server stores only the encrypted messages, which are then decrypted client-side.

Harold 2026-01-07T13:48:48.486159Z

I see - I would say that https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled is also worth knowing about. And @borkdude's ongoing work around await will also likely be relevant; ideally a beautiful (not overly indented and .then-laden) piece of code could support decrypting the messages separately and individually and put them in the frontend db for rendering as they became ready without waiting for them all to be done and succeed. It's a good real-world case.

weavejester 2026-01-07T13:50:36.068699Z

What's the difference between all and allSettled?

borkdude 2026-01-07T13:51:10.886259Z

.allSettled returns an array of objects that contain a value and an error status. .allSettled runs until all promises are handled. .all returns as soon as one of the promises fails with just one error

borkdude 2026-01-07T13:51:48.602669Z

Promise.allSettled([
  Promise.resolve(33),
  new Promise((resolve) => setTimeout(() => resolve(66), 0)),
  99,
  Promise.reject(new Error("an error")),
]).then((values) => console.log(values));

// [
//   { status: 'fulfilled', value: 33 },
//   { status: 'fulfilled', value: 66 },
//   { status: 'fulfilled', value: 99 },
//   { status: 'rejected', reason: Error: an error }
// ]

weavejester 2026-01-07T13:51:55.969209Z

Thanks for the explanation!

Harold 2026-01-07T17:25:07.874109Z

(defn- ^:async decrypt&
  [{:keys [data] :as msg}]
  (-> (assoc msg :data (await (decrypt data key)))
      (put-in-frontend-db!)))

(run! decrypt& messages)
🤔

borkdude 2026-01-07T17:32:22.812639Z

run! doesn't return a promise and isn't async aware.

Harold 2026-01-07T17:39:16.241209Z

Does it matter? It's only for the side effects, here.

p-himik 2026-01-07T17:40:47.906949Z

Depends on whether or not the order is important and whether or not partial results are OK.

👍 1
Harold 2026-01-07T17:41:30.273009Z

Great thoughts @p-himik... @weavejester mentioned above that they have sequence numbers, so order is not important --- and partial results being OK seems good to me here in case decrypt does anything weird.

borkdude 2026-01-07T17:42:34.759059Z

It probably matters since you probably want to do something after the results finish. Anyway, depends on context

Harold 2026-01-07T17:44:00.549599Z

The idea is that the put-in-frontend-db! function would take the promises and handle them.

weavejester 2026-01-07T17:44:17.246959Z

I tend to prefer keeping everything functional up until the last moment. The code I ended up with looks like:

(defn- promised-update [m k f & args]
  (-> (apply f (get m k) args) (.then #(assoc m k %))))

(defn- decrypt-messages [key messages]
  (js/Promise.all (map #(promised-update % :data crypto/decrypt key) messages)))

(defn fetch-messages [key start end]
  (let [query (m/assoc-some {} :start start :end end)]
    (go (let [messages  (-> (<! (http/get "logs" query)) :body :logs)
              decrypted (<p! (decrypt-messages key messages))]
          (m/index-by :id decrypted)))))

(defn update-messages!
  ([store key] (update-messages! store key nil nil))
  ([store key start end]
   (go (let [mesgs (<! (fetch-messages key start end))]
         (swap! store update :messages into mesgs)))))

weavejester 2026-01-07T17:46:38.969769Z

The messages are stored in a sorted-map, ordered by :id, which is a sequential integer added by the server.

p-himik 2026-01-07T17:47:43.219319Z

Ahh, if you already use core.async then it might make sense to rely on it more. I myself prefer to avoid it as much as possible on the frontend.

Harold 2026-01-07T17:47:49.457039Z

Ah, if you're already using core.async, then https://github.com/funcool/promesa is probably also worth consideration.

Harold 2026-01-07T17:48:18.179099Z

🤝 @p-himik

😁 1
weavejester 2026-01-07T17:50:58.648039Z

Thanks for the library tip, @hhausman. I'm using core.async because I also have a websocket that listens for incoming events. I use HTTP to retrieve historic/missing logs as then I can preload them in a parallel connection.

Harold 2026-01-07T17:52:06.307179Z

That's cool - I imagine that's a lot of fun to program... We don't get to use core.async because there is a perception that it bloats our js artifacts.

p-himik 2026-01-07T17:55:10.430549Z

It definitely used to, haven't checked in a while. It is (or was) also harder to debug. And go loses ^js metadata. All of that is not a deal breaker by itself, but so far the implementations of my async needs haven't been too ugly without core.async.

👍 1
borkdude 2026-01-07T17:57:51.567309Z

Wait until async/await gets merged, then we can rebuild the IOC go macro using that and it will be much smaller (I've heard)

👍 3
😀 1