├── .gitignore ├── src └── async_await │ ├── core.cljs │ └── core.clj ├── deps.edn ├── test └── async_await │ └── core_test.cljs └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .cpcache 3 | out 4 | *.iml 5 | -------------------------------------------------------------------------------- /src/async_await/core.cljs: -------------------------------------------------------------------------------- 1 | (ns async-await.core 2 | (:require-macros [async-await.core])) 3 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:deps {org.clojure/clojure {:mvn/version "1.10.1"} 2 | org.clojure/clojurescript {:mvn/version "1.10.520"}} 3 | :aliases {:test {:extra-paths ["test"]}}} 4 | -------------------------------------------------------------------------------- /test/async_await/core_test.cljs: -------------------------------------------------------------------------------- 1 | (ns async-await.core-test 2 | (:require [cljs.test :as test :refer [deftest is]] 3 | [async-await.core :refer [async await await-all await-first]])) 4 | 5 | (deftest test-async 6 | (test/async done 7 | (-> (async 1) 8 | (.then #(is (= 1 %))) 9 | (.then #(done))))) 10 | 11 | (deftest test-await 12 | (test/async done 13 | (async 14 | (let [x (await 1)] 15 | (is (= 1 x)) 16 | (done))))) 17 | 18 | (deftest test-await-all 19 | (test/async done 20 | (async 21 | (let [xs (await-all [1 2 3])] 22 | (is (= [1 2 3] xs)) 23 | (done))))) 24 | 25 | (deftest test-await-first 26 | (test/async done 27 | (async 28 | (let [x (await-first [1 2 3])] 29 | (is (= 1 x)) 30 | (done))))) 31 | 32 | (deftest test-recursive-async-fn 33 | (test/async done 34 | (let [async-fn (fn async-fn* [n] 35 | (async 36 | (if (pos? n) 37 | (async-fn* (await (dec n))) 38 | (do (is (= 0 n)) 39 | n))))] 40 | (-> (async-fn 3) 41 | (.then #(done)))))) 42 | 43 | (defn -main [] 44 | (test/run-tests)) 45 | 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## What is this? 2 | Experimental ClojureScript's compiler extension that enables JavaScript's async/await through `async` blocks and `await, await-all & await-first` operators. 3 | 4 | Requires `:language-in` compiler option set to `:ecmascript-2017` or newer ECMAScript versions, this will set `:language-out` to the same version. When targeting JavaScript runtimes where `async/await` is not supported, set `:language-out` to appropriate version and Google Closure Compiler will compile it down into supported implementation. 5 | 6 | ## Usage 7 | ```clojure 8 | (require '[async-await.core :refer [async await]]) 9 | 10 | (defn http-get [url] 11 | (async 12 | (let [response (await (js/fetch url)) 13 | json (await (.json response))] 14 | (.log js/console json)))) 15 | ``` 16 | 17 | ## Limitations 18 | JavaScript's `await` statement is not allowed in non-async functions, this causes a problem because of ClojureScript's codegen. When translated into JavaScript, constructs like the one below emit self-invoking function in place of `do`. 19 | ```clojure 20 | (async 21 | (let [x (do (inc 1) (await 2))] 22 | x)) 23 | ``` 24 | 25 | Output: 26 | ```javascript 27 | (async function() { 28 | var x = (function() { 29 | 1 + 1; 30 | return await 2; 31 | })(); 32 | return x; 33 | })() 34 | ``` 35 | 36 | ## API 37 | - `async` wraps body into self-invoking JavaScript's async function, returns promise 38 | - `await` suspends execution of current async block and returns asynchronously resolved value 39 | - `await-all` same as `(seq (.all js/Promise coll))`, but for easier usage within async blocks 40 | - `await-first` same as `(.race js/Promise coll)`, but for easier usage within async blocks 41 | 42 | ## Tests 43 | ``` 44 | clojure -A:test -m cljs.main -m async-await.core-test -re node 45 | ``` 46 | -------------------------------------------------------------------------------- /src/async_await/core.clj: -------------------------------------------------------------------------------- 1 | (ns async-await.core 2 | (:refer-clojure :exclude [await]) 3 | (:require [cljs.analyzer :as ana] 4 | [cljs.compiler :as compiler])) 5 | 6 | (def ^:dynamic *in-async* false) 7 | 8 | (alter-var-root #'ana/specials #(conj % 'async* 'await*)) 9 | 10 | (defmethod ana/parse 'await* 11 | [op env [_ expr :as form] _ _] 12 | (when-not *in-async* 13 | (throw (ana/error env "Can't await outside of async block"))) 14 | (when (not= 2 (count form)) 15 | (throw (ana/error env "Wrong number of args to await"))) 16 | {:env env 17 | :op :await 18 | :children [:expr] 19 | :expr (ana/analyze env expr) 20 | :form form}) 21 | 22 | (defmethod ana/parse 'async* 23 | [op env [_ & exprs :as form] _ _] 24 | (binding [*in-async* true] 25 | (let [statements (ana/disallowing-recur 26 | (->> (butlast exprs) 27 | (mapv #(ana/analyze (assoc env :context :statement) %)))) 28 | ret (ana/disallowing-recur 29 | (ana/analyze (assoc env :context :return) (last exprs))) 30 | children [:statements :ret]] 31 | {:op :async 32 | :env env 33 | :form form 34 | :statements statements 35 | :ret ret 36 | :ret-tag 'js/Promise 37 | :children children}))) 38 | 39 | (defmethod compiler/emit* :await 40 | [{:keys [env expr]}] 41 | (when (= :return (:context env)) 42 | (compiler/emits "return ")) 43 | (compiler/emits "(await ") 44 | (compiler/emits (assoc-in expr [:env :context] :expr)) 45 | (compiler/emits ")")) 46 | 47 | (defmethod compiler/emit* :async 48 | [{:keys [statements ret env]}] 49 | (when (= :return (:context env)) 50 | (compiler/emits "return ")) 51 | (compiler/emitln "(async function (){") 52 | (doseq [s statements] 53 | (compiler/emitln s)) 54 | (compiler/emit ret) 55 | (compiler/emitln "})()")) 56 | 57 | ;; ====== Public API ====== 58 | 59 | (defmacro async 60 | "Wraps body into self-invoking JavaScript's async function, returns promise" 61 | [& body] 62 | `(~'async* ~@body)) 63 | 64 | (defmacro await 65 | "Suspends execution of current async block and returns asynchronously resolved value" 66 | [expr] 67 | `(~'await* ~expr)) 68 | 69 | (defmacro await-all 70 | "Same as (seq (.all js/Promise coll)), but for easier usage within async blocks" 71 | [coll] 72 | `(seq (~'await* (.all js/Promise ~coll)))) 73 | 74 | (defmacro await-first 75 | "Same as (.race js/Promise coll), but for easier usage within async blocks" 76 | [coll] 77 | `(~'await* (.race js/Promise ~coll))) 78 | --------------------------------------------------------------------------------