Skip to content
No description, website, or topics provided.
Clojure Shell
Branch: master
Clone or download
Latest commit c01a2a5 Jan 12, 2020
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
script
src/com/wsscode/async
test/com/wsscode/async
.gitignore Initial commit Jan 6, 2020
README.adoc
deps.edn
package-lock.json
package.json Initial commit Jan 6, 2020
project.clj Bump Jan 12, 2020
shadow-cljs.edn

README.adoc

WSSCode Async Helpers

async cljdoc

Core.async is the standard way to handle async features in Clojure and ClojureScript programs.

Although core.async is built upon CSP, often (specially in CLJS) is desired to have something that’s more like Promises/Futures.

Core.async provides a promise-chan, which is a channel that has a promise-like semantic: after realization, any read on it will keep returning the same value. That’s helpful but this doesn’t cover the problem of error propagation, in a promise system it’s expected that errors can flow up to be captured by some higher level code.

This library provide some helpers to deal with this problem, and also:

  1. helpers to integrate with Javascript Promises in ClojureScript environments.

  2. helpers to write tests using core.async and cljs.test.

Setup

This library uses separated namespaces for Clojure and ClojureScript, when using the Clojure side, use the namespace com.wsscode.async.async-clj, and for ClojureScript use com.wsscode.async.async-cljs. Unless pointed otherwise, the features on both namespaces are the same.

If you want to use this with Clojure Common files, you can use the require like this:

(ns my-ns
  (:require [#?(:clj  com.wsscode.async.async-clj
                :cljs com.wsscode.async.async-cljs)
             :as wa
             :refer [go-promise <?]]))

Error propagation

To deal with error propagation, the trick is to return the error object as the channel value, but also throw that error when reading the channel. Let’s illustrate that with an example:

(ns my-ns
  (:require [com.wsscode.async.async-clj :refer [go-promise <?]))

(defn async-divide [x y]
  ; (1)
  (go-promise
    (/ x y)))

(defn run [d]
  (go-promise
    (try
      ; (2)
      (<? (async-divide 6 d))
      (catch Throwable _
        "ERROR"))))

(comment
  (<!! (run 2)) ; => 3 (3)
  (<!! (run 0)) ; => "ERROR" (4)
  )
  1. go-promise is similar to go, but will return a promise-channel, so in case this result gets passed to multiple readers, all of them will get the realized value or error. This also wraps the block in a try/catch, so if some exception happens it will get returned as the channel value.

  2. <? works like <!, but once it reads a value, it will check if it’s an error, and if so it will throw that error, propagating it up on the chain.

  3. propagate value back

  4. error propagated from async-divide trying to divide by zero

By following this pattern of error capture and send, this system ends up working in the same ways you would expect a promise, all while still in the same go blocks, making it compatible with standard core.async functionality. Later we will also talk about how to integrate with Javascript Promises in this system.

Nil returns

Another difference when using go-promise is that different from regular go, you can return nil as a value. Since the promise will always return the same value, a nil is not ambiguous.

ℹ️
In the implementation side, go-promise check if the value returned is nil, and if so it just closes the channel, making it an effective nil value.

Extra takers

We just saw the helper <? to do a take and check for errors, here is a list of other take helpers provided by this library:

  • <? - take and throw errors

  • <?! (clj only) - take and throw errors blocking on thread

  • <!p (cljs only) - take from JS Promise

  • <!maybe - if param is a channel, take from it, otherwise return its value

  • <!!maybe (clj only) - like <!maybe but blocking the thread

  • <?maybe - take and throw errors, return value if it’s not a channel

  • <?!maybe (clj only) - like <?maybe but blocking the thread

💡
in ClojureScript <?maybe can also take from Promises

Javascript Promises

While working in Javascript it’s common to need to handle Promises, to help with this there is a macro in this library that enables the read of JS promises as if they were core.async channels, the <!p helper:

(ns my-ns
  (:require [com.wsscode.async.async-cljs :refer [go-promise <? <!p]))

; (1)
(go-promise
  (-> (js/fetch "/") <!p
      (.text) <!p
      js/console.log))
  1. Read the index text of the current domain, note we are waiting for two different promises in this example, the first one for the fetch headers and the second to get the body text.

ℹ️
the way <!p works is by first converting the Promise into a core.async channel and them read on that channel, for core.async sake it’s channels all the way.

Note that this strategy allows the mixing of both core.async channels and promises in the same system, you can both park for channels or promises.

Javascript Async tests

Dealing with async tests in cljs.test can be annoying, the core doesn’t have any integration with core.async, neither it handles common problems like timing out a test. This library provides a helper called deftest-async that aims to facilitate the tests of async core using core.async. Example usage:

(ns com.wsscode.async.async-cljs-test
  (:require [clojure.test :refer [is are run-tests async testing deftest]]
            [com.wsscode.async.async-cljs :as wa :refer [deftest-async <! go]]))

(deftest-async my-test
  (is (= "foo" (<! (go "foo")))))

This macro will do a couple of things:

  1. It will wrap the body in a go-promise block, allowing the use of parking operations

  2. Try/catch this block, if any error happens (sync or async) that generates a test case that will fail with that error

  3. Add a 2 seconds timeout, if the go block doesn’t return in this time it will cancel and fail the test

You can configure the timeout duration, example:

(ns com.wsscode.async.async-cljs-test
  (:require [clojure.test :refer [is are run-tests async testing deftest]]
            [com.wsscode.async.async-cljs :as wa :refer [deftest-async <! go]]))

(deftest-async my-test
  {::wa/timeout 5000} ; 5 seconds timeout
  (is (= "foo" (<! (go "foo")))))
💡
if you want to use this helper with a different test constructor (from Workspaces or Devcards for example) you can use the wa/async-test helper instead

API

There are other minor helpers not mentioned in this document, but they all have documentation on the functions, to check it out see the cljdoc page of this library.

You can’t perform that action at this time.