Tom MacWright

tom@macwright.com

RPL (2) - Terrarium

Update: This post was published in 2014. As of 2017,
  • terrarium is no longer developed.
  • Building something similar to this is now my full-time job at Observable: we're building notebooks for JavaScript that support this kind of transparent evaluation as well as new tricks like order-independent code and hot replacement of functions and values.

Bark Bench Terrarium

CC-BY-NC joshleo

The core of rpl is terrarium. Terrarium is an alternative to JavaScript’s eval() function and node’s vm.runInNewContext(). Unlike these systems, rpl is designed to run code as full-fledged scripts and to manage its execution as a proper full-fledged process.

The precedecessor to rpl, called tmcw.github.io/mistakes, used eval() at its core - it would execute each 1..n subset of code text in eval() and display the result. While this worked very well for imperative code that didn’t use timing functions, but couldn’t handle async and timed code properly. This ruins the fun of JavaScript.

The problem is that some programs will finish executing in one pass:

var x = 10;

But other programs will never exit:

setInterval(function() {
  var x = 10;
}, 100);

JavaScript has a few different ways to run forever - setInterval, setTimeout, process.nextTick, and so on. It’s very difficult to ‘clear all intervals’ without brutal hacks.

The other problem of running code in a sandbox is scope: if you run something like eval('x = 10'), the variable x will now be defined in the outer scope. While node’s vm.runInContext method gives more control over scope access, there isn’t a browser equivalent with the same behavior.

Terrarium’s solution is true nesting in both cases - for node, it creates a module on the fly and runs it with child_process.fork to create a new subprocess with a stream for communication. In a browser, it creates a new HTML resource with the javascript code embedded and runs it in a new iframe with window.top as a communication mechanism.

This allows the sub-scripts in question to do anything normal code can do - in a browser, you can create elements and attach events to them, and in node, you can create web servers, access files, and all the rest. And all this without hacks necessary even in node core to make the default node REPL act like software.

var T = require('terrarium');
var terrarium = new T.Node(); // or T.Browser();
terrarium.on('data', function(d) {
  // instrumentation data
  terrarium.destroy();
});
terrarium.run('//=1');

Terrarium is a low-level API: you create a sandbox, write code to it, and receive instrumentation data. The API is identical between node and the browser.

var terrariumStream = require('terrarium-stream');
var terrarium = new terrariumStream.Node();
terrarium.on('data', function(d) {
  // instrumentation data
  terrarium.destroy();
});
terrarium.write({ value: '//=1' });
  • browser: iframe, communication via window.top
  • node: subprocess, communication via fork and streams

terrarium-stream is a slightly higher-level API that connects the node stream lifecycle - creating, reading, writing, destroying - to terrarium. Using terrarium-stream, rpl is able to interface with browser & node code and build an extremely simple websocket-based interface using shoe.