-*- mode: org; mode: visual-line; -*-
This is a simple project for bringing up a ClojureScript network REPL to talk to Max 8 via a Node.js sub-process. This means that Max can be live-coded in ClojureScript, at least to the extent that the node.script
instance in Max is connected to interesting things in the patcher (via inlets, outlets, named dictionaries). We describe the support for CIDER inside Emacs, although other IDE toolchains are available and should work.
Max has a library object called node.script
which can fire up a Node.js process and attach it to the enclosing patcher document, so that the patcher can run Javascript code, and the Javascript code can manipulate things in the Max world.
Figwheel Main is a set of libraries which equips a Javascript client (a web browser, or Node.js) with a network connection to a Figwheel server; the latter runs in Clojure (on the JVM), hosts the ClojureScript cross-compiler, and supports a REPL (read-eval-print loop). When everything’s running, ClojureScript can be live-coded via the server, which compiles everything to Javascript and sends it through to the client. The server also tracks changes to source files, uploading code to the client as required.
When everything is running, the setup looks like this:
- Max with an instance of
node.script
and a Node.js process attached to it - A Figwheel Main server, which Max’s Node.js attaches to
- Emacs (or equivalent), attached to the server for live coding via CIDER
Most of this machinery is invisible (so in particular, the Max application code in ClojureScript for Max needs to know nothing about live coding support); Figwheel and CIDER augment the code automatically to tie everything together.
We were initially using the basic (but new) Clojure CLI tools, but a build on a new machine caused some breakage, so we’ve switched back to tried-and-trusted Leiningen. The Figwheel Main site has good documentation, though that is oriented towards Clojure CLI.
A very simple setup with a network REPL. In all the simple examples, the actual code is just sitting in scratch.cljs
. Below is the build configuration file, dev.cljs.edn
:
^{:watch-dirs ["src"]
:node-command "node"
:launch-node false}
{:target :nodejs
:main simple.core}
The target is :nodejs
. Also, in the meta-data, we’ve disabled :launch-node
since that’s done by Max. For testing (without the Max API) make this true
, in which case the value of :node-command
might need to be altered.
Note the symbolic link from simple.js
to the actual built target. This is needed because Max’s node.script
object doesn’t support complex file paths.
To build, invoke cider-jack-in-cljs
from Emacs while visiting scratch.cljs
. For ClojureScript REPL type, enter figwheel-main
. For figwheel-main build, enter :dev
.
Start the Max node process once the CIDER session has initialised and is waiting for a client connection.
A more sophisticated example, interacting with Max dictionaries. Since dictionary operations return Javascript promises, we’re using cljs-promises, which wraps them in ClojureScript’s core.async machinery. (TODO: that package is probably not needed any more, use <p!
instead.)
We can use NPM to wheel in WebSocket support for the REPL to replace the less efficient polling: there’s a message box to mash to do that in the Max patcher. (TODO: that, or something equivalent, might be available in CLJSJS, which would be neater.)
Quite a complex example for bringing up an OSC forwarder with logging into a Max dictionary (the OSC being handled internally in Node). The complexity is the bootstrapping of the components, since we want to read some configuration (addresses, ports) from a Max dictionary before we kick off - and that’s a deferred, promise-based call as well.
Features: forwards all incoming OSC to an output address and port (which can be manually edited in config.json
), while logging the data to a dictionary. The dictionary is keyed by OSC address, and the first argument is a running count of the number of occurrences of the address, followed by the actual arguments of the last-seen message.
(The cljs-promises package is obsolete; there’s now first-class support for promises in core.async.)
This example has been moved to its own repository.