The sci.async
namespace offers async evaluation for ClojureScript. This is
work in progress. Please try it out and provide feedback.
Difference with the synchronous evaluation in sci.core
:
(ns ...)
forms are evaluated asynchronously - loading can be configured
via the :async-load-fn
option.require
can be made async using scia/require
, see Require.Code examples below use promesa to make working with promises more convenient.
(ns example
(:require [promesa.core :as p]
[sci.async :as scia]
[sci.core :as sci]))
(defn async-load-fn
[{:keys [libname ctx]}]
(p/resolved
(case libname
my.lazy-ns
(let [mlns (sci/create-ns 'my.lazy-ns)
lazy-greet (fn [x] (str "Hello " x "!"))
lazy-ns {'lazy-greet (sci/new-var 'lazy-greet lazy-greet {:ns mlns})}]
(sci/add-namespace! ctx libname lazy-ns)
;; empty map return value, SCI will still process `:as` and `:refer`
{}))))
(def ctx (sci/init {:async-load-fn async-load-fn}))
(def code "
(ns foo (:require [my.lazy-ns :refer [lazy-greet]]))
(lazy-greet \"Michiel\")
")
(p/let [result (scia/eval-string* ctx code)]
(println result)) ;; prints: "Hello Michiel!"
In this example we lazy load a namespace into the SCI context. Note that the
functions mapped in this namespace may come in asynchronously e.g. via an http
request. The :async-load-fn
is used to process the (:require ..)
part of the
ns
form. In our implementation we use sci/add-namespace!
to mutate the SCI
context and then we return an empty map indicating that SCI will process :as
and :refer
for us. This can be prevented by return a map with :handled true
like in the below example.
Additionally supported return values:
:source "..."
, CLJS source to be evaluatedBy default require
is synchronous in SCI. The sci.async
namespace contains
an asynchronous require
which can be substituted in a context:
(ns example
(:require [promesa.core :as p]
[sci.async :as scia]
[sci.core :as sci]))
(def async-load-fn (fn [{:keys [libname]}]
(p/resolved
(case libname
acme.foo {:source "(ns acme.foo) (defn the-fn [] :hello)"}
acme.bar {:source "(ns acme.bar) (defn the-fn [] :bye)"}))))
;; Substitute scia/require for clojure.core/require:
(def ctx (sci/init {:namespaces {'clojure.core {'require scia/require}}
:async-load-fn async-load-fn}))
(p/let [res (scia/eval-string* ctx "(require '[acme.foo :as foo]) (foo/the-fn)")
_ (assert (= :hello res))
res (scia/eval-string* ctx "(require '[acme.bar :as bar]) (bar/the-fn)")
_ (assert (= :bye res))])
Like the ns
form, a top level require
is handled as if it happened
synchronously: the next expression is scheduled after the require finishes.
SCI does not limit require
to occur at the beginning of a file, it may be used
anywhere, as long as the result is visible as a top level value:
(ns foo)
(+ 1 2 3)
(require '[some.lazy-ns :as slns])
(slns/the-fn)
(when (odd? 3)
(require '[some.other-lazy-ns]))
NOTE: the below code can be replaced by using
sci/add-js-lib!
from SCI > 0.6.37. See docs.
(ns example
(:require
[clojure.string :as str]
[goog.object :as gobject]
[promesa.core :as p]
[sci.async :as scia]
[sci.core :as sci]))
(defn async-load-fn
[{:keys [libname opts ctx ns]}]
(let [[libname suffix] (str/split libname "$")]
(case libname
"some_js_lib"
(p/let [js-lib (p/resolved #js {:add +
:subtract -
:multiply *
:default *})]
(let [js-lib (if suffix
(gobject/getValueByKeys js-lib (.split suffix "."))
js-lib)
munged (symbol (munge libname))]
;; register class globally in context
(sci/add-class! ctx munged js-lib)
(let [{:keys [as refer]} opts]
(when as
;; import class in current namespace with reference to globally
;; registed class
(sci/add-import! ctx ns munged as))
(when refer
(doseq [sym refer]
(let [prop (gobject/get js-lib sym)
sub-libname (str munged "$" prop)]
;; register sub-library globally
(sci/add-class! ctx sub-libname prop)
;; add import to sub-library in current namespace
(sci/add-import! ctx ns sub-libname sym))))))
{:handled true}))))
(def ctx (sci/init {:async-load-fn async-load-fn
;; async require override
:namespaces {'clojure.core {'require scia/require}}
;; allow JS interop globally
:classes {'js goog/global :allow :all}}))
;; allow printing
(sci/alter-var-root sci/print-fn (constantly *print-fn*))
(def code-1
"
(ns example (:require [\"some_js_lib\" :as my-lib :refer [subtract]]))
[(my-lib/add 1 2) (subtract 3 2)]
")
;; Library property namespaces:
(def code-2 "
(require '[\"some_js_lib$default\" :as awesome])
(require '[\"some_js_lib$add\" :as add])
[(awesome 3 2) (add 4 5)]
")
(p/let [result (scia/eval-string* ctx code-1)
_ (println "Result:" result) ;; [3 1]
result (scia/eval-string* ctx code-2)
_ (println "Result:" result) ;; [5 9]
]
)
In this example we simulate loading a JavaScript library asynchronously. In
practise the library could come in via an asynchronous HTTP Request, etc. but
here we just simulate it by returning a promise with JavaScript object that has
a couple of functions. In the async load fn we register the JS library as a
global class in the context and as an import in the current namespace. The
:handled true
key/value in the return value indicates that SCI will handle :refer
and :as
, because we already did that ourselves in the async-load-fn
.
In the code-2
fragment we use library property namespaces. These were
introduced in ClojureScript 1.10.844
. You can read about that
here.
We support that in async-load-fn
by splitting the libname on $
and getting
the properties out of the JS library, then registering that as a class and
import.
To preserve namespace state, use scia/eval-string+
which is similar to
scia/eval-string*
but returns a map with :val
and :ns
in a promise. To
keep the namespace state of the previous evaluation intact, you can feed the
entire return value back into the next evaluation:
(p/let [ctx (sci/init {})
{:keys [_ ns] :as ret} (scia/eval-string+ ctx "(ns foo)")
_ (is (= "foo" (str ns)))
{:keys [val ns]} (scia/eval-string+ ctx "(defn foo [] :hello) (foo/foo)" ret)
_ (is (= :hello val))
_ (is (= "foo" (str ns)))
;; no passing of previous state: ns is back to user again:
{:keys [val ns]} (scia/eval-string+ ctx "(defn bar []) (symbol #'bar)")
_ (is (= 'user/bar val))
_ (is (= "user" (str ns)))])
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close