Skip to content

Pitfalls with (browser connected) ClojureScript REPLs

Valentin Waeselynck edited this page Oct 19, 2017 · 1 revision

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 and letsc 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:

Constraints to abide by:

  1. 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
  2. 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.

How to enforce these constraints?

Here are some strategies for running and using your ClojureScript REPL(s) to abide by these constraints:

Make sure all compilation tasks happen in the same process

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.

Beware of what browser tab your REPL runs in

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!")

Beware of page refresh

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).

When in doubt, clear the compilation cache

In Leiningen, this is done by lein clean.

Clone this wiki locally