-
Notifications
You must be signed in to change notification settings - Fork 14
Pitfalls with (browser connected) ClojureScript REPLs
Supporting the wide variety of ClojureScript REPLs is a core design goal of scope-capture, which drove design choices such as not relying on eval
. However, due to the way ClojureScript REPLs work, there are some pitfalls to be aware of.
How scope-capture works:
-
spy
works by saving some static information at compile-time (during macro-expansion) and some dynamic information at run-time (during execution); in both cases, the information is saved to an in-memory database, i.e in the address space of the process that does compilation or execution (at the time of writing, this database is a global atom held by#'sc.impl.db/db
) -
defsc
andletsc
are macros, which use some of the saved static information at compile-time (when they macro-expand, typically they need the content of:sc.cs/local-names
), and use some of the saved dynamic information at run-time (when the code they generated executes, typically they need the content of:sc.cs/local-bindings
).
The trouble with ClojureScript is: compilation and execution happen in different processes. Even worse, oftentimes there are several processes that do compilation (e.g a file-watcher and a REPL), and several processes that do execution (e.g several browser tabs). Because these processes have disjoint address spaces, this can lead to the saved information being fragmented, and defsc
/ letsc
attempting to read information in a different address space than where spy
stored it, causing an error. Another issue is that since the in-process database is not durable, these compilation / execution processes need to stay live.
This leads to the following constraints:
- A
(defsc ...)
/(letsc ...)
block must be compiled by the same process that compiled the associated(spy ...)
block. Symptoms:No Code Site found with id -3
- A
(defsc ...)
/(letsc ...)
block must be executed by the same process that executed the associated(spy ...)
block. Symptoms::No Execution Point found with id 7
In particular, both the compilation process and the execution process must stay live between (spy ...)
and (defsc ...)
/ (letsc ...)
- no build process restart, no page refresh.
Here are some strategies for running and using your ClojureScript REPL(s) to abide by these constraints:
For instance, if you have one task that watches files and recompiles them when they change (as done by cljs.build.api/watch
), and one task that runs your ClojureScript REPL (as done by cljs.repl/repl
), make sure you start them from the same JVM process (typically in different Threads of the same JVM process).
For example, if you run your build with a bare Clojure script:
(require
'[cljs.build.api :as b]
'[cljs.repl :as repl]
'[cljs.repl.browser :as browser])
(def opts
{:main 'myapp.core
:output-to "out/myapp.js"
:output-dir "out"})
(println "Building...")
(b/build "src" opts)
(future
(println "Watching in another thread...")
(b/watch "src" opts))
(repl/repl (browser/repl-env)
:output-dir "out")
If you're using Figwheel, you're in luck: that's how it works by default, so you have nothing more to do.
You may be working with several browser tabs at the same time, which are as many execution processes. In this case, it may happen than a (spy ...)
block runs in several tabs, especially if you're using a file-watcher. However, when subsequently using defsc
/ letsc
from the REPL, you have to make sure that the execution of the (spy ...)
block you're interested in happened in the browser tab where your REPL runs!
You can find out which tab it is by running (js/alert "Here!")
Hot-code reloading is file, but traditional page refresh means losing the recorded runtime information (which may be fine if you're not planning on using it anymore).
In Leiningen, this is done by lein clean
.