announcements

borkdude 2026-02-10T20:32:52.488759Z

https://github.com/babashka/scittle: execute Clojure(Script) directly from browser script tags via SCI! v0.8.31 (2026-01-10) • Support async/await. See https://github.com/babashka/sci/blob/master/doc/async-await.md. • Implement js/import not using eval • Support this-as • nREPL: print #<Promise value> when a promise is evaluated

🔥 14
1
henrik 2026-02-10T21:18:23.483309Z

Reannouncing https://github.com/multiplyco/quiescent, this time for ClojureScript. Quiescent is a Clojure/ClojureScript library for composable async tasks with structured concurrency and parent-child and chain cancellation. Quiescent has been extended to ClojureScript, with the same composability and coordination semantics. Seamless interop with JavaScript promises is provided (similar to futures and CompletableFuture in CLJ). Return arbitrary Clojure data structures containing JS promises or Quiescent tasks to await them in parallel:

(q/task
  {:this  (js/fetch …)
   :that  (js/fetch …)
   :other (q/task …)})
To extend cancellation semantics into AbortController-aware JavaScript APIs (like fetch), an https://github.com/multiplyco/quiescent?tab=readme-ov-file#abortsignal-integration verb is provided. A new https://github.com/multiplyco/quiescent?tab=readme-ov-file#controlling-parallelism verb provides cross-platform concurrency limiting, with reentrant-aware permit management to help prevent deadlocks in recursive functions. For CLJ, performance has been improved under bursty scenarios. I’ve measured 100k (noop) tasks starting, running, and settling in ~30ms on my computer, which equates to roughly 0.3µs per task (YMMV).

👀 4
🎉 5
flowthing 2026-02-11T09:15:28.040839Z

Interesting! I tried this out, but I'm somewhat confused on the behavior related to platform threads and parking. It seems to me that even derefing a q/task (i.e. a task backed by a virtual thread) throws, but only if the task isn't complete when I dereference it.

λ clj -Srepro -Sdeps '{:deps {co.multiply/quiescent {:mvn/version "0.2.4"}}}'
Clojure 1.12.4
user=> (require '[co.multiply.quiescent :as q])
nil
user=> (def my-task (q/task "done"))
#'user/my-task
user=> (deref my-task)
"done"
vs.
λ clj -Srepro -Sdeps '{:deps {co.multiply/quiescent {:mvn/version "0.2.4"}}}'
Clojure 1.12.4
user=> (require '[co.multiply.quiescent :as q])
nil
user=> @(q/task (Thread/sleep Long/MAX_VALUE) "done")
Execution error (IllegalStateException) at co.multiply.machine-latch.impl/assert-virtual! (impl.clj:24).
Cannot park platform thread. Await from virtual thread instead.
Or am I holding it wrong?

henrik 2026-02-11T12:08:01.801389Z

@magnars Yeah, here’s the explanation: https://github.com/multiplyco/quiescent?tab=readme-ov-file#general-notes @flowthing Well spotted. This is because dereferencing != parking. It’s literally parking that is discouraged, not dereferencing itself. If you evaluate eg.:

(let [numbers (vec (range 1000))]
  @(qfor [n numbers]
     (q n)))
You’ll be allowed to, even without (q/throw-on-platform-park! false), because all operations in the form are synchronous.

Casey 2026-02-11T12:46:45.555489Z

This is very nice to see! The fact that you were able to support jvm and js runtime with (nearly) the same API is heartening, it means cljd could also be supported in the future (dart having similar semantics to js).

👍 1
flowthing 2026-02-11T13:21:38.888609Z

I understand the difference between dereferencing and parking, but I don't understand how to use the library to wait for a number of tasks (backed by virtual threads) to complete without calling (q/throw-on-platform-park! false) first.

henrik 2026-02-11T13:46:51.747619Z

Ah, sorry, I misunderstood your question. Ideally, you wouldn’t park at all. For example, if you take an API call on behalf of a user: • The user clicks some button • You issue an API call • Some post-processing on the response perhaps • Write to a DB • React on DB write somehow to show on screen for user Even if the flow should branch at some point, or branch then join, etc., it’s causal: one thing leads to another (although eventual). So we don’t need to park, we just need to trigger the next step once the former completes. Since most of the code in our codebase does something of that character (although not necessarily triggered by a user, it could be entirely backend), blocking has virtually always been accidental on our part, and has at least one time caused a major problem in production. That’s why, in this library, I opted for being very explicit about it. If you really want to block the main thread, you have three options: • https://cljdoc.org/d/co.multiply/quiescent/0.2.4/api/co.multiply.quiescent#throw-on-platform-park! - mostly useful for REPL stuff, I wouldn’t use it in production. • https://cljdoc.org/d/co.multiply/quiescent/0.2.4/api/co.multiply.quiescent#deref-cpu - allows you to park on a platform thread for a particular callsite (having it be an explicit verb makes it straightforward to audit). • -Dco.multiply.machine-latch.assert-virtual=false (property; set in eg. deps.edn) - elides the check entirely, and all bets are off. The last option can be used for REPL development as well. If you make eg. a :dev alias, you can set the property to be true for all REPL sessions, provided they use the dev alias. Though, this might mean that you write code which then doesn’t work when you run the tests/run it in production. I normally ad-hoc throw-on-platform-park! instead.

2026-02-11T06:38:53.065559Z

Nice work! 😊 Please do note that there already is a clojurescript library named Quiescent, by Luke VanderHart https://github.com/levand/quiescent

👍 1