Skip to content

Instantly share code, notes, and snippets.

@dherman
Created May 9, 2012 06:10
Show Gist options
  • Save dherman/2642306 to your computer and use it in GitHub Desktop.
Save dherman/2642306 to your computer and use it in GitHub Desktop.
sm.js with classes
/*
* Here is an implementation of an NPM module I recently wrote, done with ES6 classes.
* Is it a "betrayal" of the prototypal nature of JavaScript to use classes? Come on.
* Classes are a very common design pattern that's easy to implement with prototypes.
* And for some use cases, they're a good fit. Node's EventEmitter abstraction is a
* perfectly reasonable use of classes and inheritance: it's an abstract datatype that's
* user-extensible. So the 'events' module provides a base class with some built-in
* functionality that you can extend.
*
* And yes, it's a "class." Just check the docs:
*
* http://nodejs.org/api/events.html
*
* It irks me when people say "JavaScript doesn't have classes." It doesn't have a
* predefined construct in the language that's called classes, but it has all the
* elements that make it so easy to construct classes that they're one of the most
* common programming patterns. You don't have to use them everywhere -- God knows I
* sure don't -- but they're an idiomatic use of JavaScript. And if you compare this
* version to the actual implementation, you'll see that this version does a better
* job of saying what it means. It eliminates a lot of silly, repetitive boilerplate.
*
* https://github.com/dherman/sm.js/blob/master/lib/sm.js
*/
var cp = require("child_process");
var fs = require('fs');
var EventEmitter = require('events').EventEmitter;
var SEPARATOR = "<<<---SPIDERMONKEY-NODE-BRIDGE--->>>";
class SM extends EventEmitter {
constructor({ shell = "js" }) {
super();
// Create the child process.
this._proc = cp.spawn(shell, ["-i"]);
this._proc.stderr.setEncoding("utf8");
this._proc.stderr.on("data", this._data.bind(this));
this._proc.on("error", this._exit.bind(this));
this._proc.on("exit", this._exit.bind(this));
// is the child process still alive?
this._alive = true;
// callbacks waiting in turn for messages
this._waiters = [];
// buffered data from the child process's stderr
this._buffer = [];
}
// Internal methods
_data(str) {
var n = SEPARATOR.length;
var buf = this._buffer;
var m = buf.length;
// Look for a separator spanning the old and new chunks.
if (m > 0) {
var last = buf[m - 1];
var lastN = last.length;
var leftN = Math.min(lastN, n);
var left = last.substring(lastN - leftN);
var rightN = Math.min(str.length, n);
var right = str.substring(0, rightN);
var i = (left + right).indexOf(SEPARATOR);
// Found a spanning separator?
if (i > -1) {
// Remove the separator fragment from the old chun.
buf[m - 1] = last.substring(0, lastN - leftN + i);
// Receive the buffered message.
this._recv(buf.join(""));
buf.length = 0;
// last str
// --------------+ +---------------
// | |... | |........| |
// --------------+ +---------------
// ^ ^ ^ ^
// | | | |
// | +--+ | |
// lastN - leftN | | rightN
// +--+ |
// | |
// lastN - leftN + i n - leftN + i
// Remove the separator fragment from the new chunk.
str = str.substring(0, n - leftN + i);
}
}
// Look for complete messages.
var msgs = str.split(SEPARATOR);
// Found at least one separator?
if (msgs.length > 1) {
// buf msgs
// +---+---+---+---+ +---+---+---+---+---+---+---+
// | | | | | | | | | | | | |
// +---+---+---+---+ +---+---+---+---+---+---+---+
// \---------------------/ \-/ \-/ \-/ \-/ \-/ \-------....
// message msg msg msg msg msg message
// Reduce the current chunk to the remainder.
str = msgs.pop();
// Receive the buffered message.
buf.push(msgs[0]);
this._recv(buf.join(""));
buf.length = 0;
// Receive the additional messages.
for (var j = 1, m = msgs.length; j < m; j++)
this._recv(msgs[j]);
}
// Buffer the remaining bits of the current chunk.
buf.push(str);
}
_exit() {
this._alive = false;
this.emit("exit");
}
_send(src, request, callback) {
var waiters = this._waiters;
waiters.push({ request: request, callback: callback });
if (!this._alive) {
for (var i = 0, n = waiters.length; i < n; i++) {
waiters[i].callback.call(this, new Error("process exited"));
}
waiters.length = 0;
return;
}
this._proc.stdin.write(src);
this._proc.stdin.write("printErr(" + JSON.stringify(SEPARATOR) + ");\n");
}
_recv(msg) {
msg = msg.trim();
if (msg === "")
return;
var obj = JSON.parse(msg);
switch (obj.type) {
case "exit":
this.emit("message", obj, { close: true });
this._exit(obj.exitCode);
break;
case "return":
var waiter = this._waiters.shift();
this.emit("message", obj, waiter.request);
waiter.callback.call(this, null, obj.value);
break;
case "throw":
var waiter = this._waiters.shift();
this.emit("message", obj, waiter.request);
waiter.callback.call(this, obj.value);
break;
}
}
// Public methods
close() {
this._proc.stdin.end("printErr(" +
JSON.stringify('{ "type" : "exit", "exitCode" : 0 }') +
");\nprintErr(" +
JSON.stringify(SEPARATOR) +
");\n");
}
parse(src, callback) {
this._send(parseIPC(src), { parse: src }, callback);
}
parseFile(path, callback) {
this._send(parseFileIPC(path), { parseFile: path }, callback);
}
check(src, callback) {
this._send(checkIPC(src), { check: src }, callback);
}
checkFile(path, callback) {
this._send(checkFileIPC(path), { checkFile: path }, callback);
}
eval(src, callback) {
this._send(evalIPC(src), { eval: src }, callback);
}
load(path, callback) {
this._send(loadIPC(path), { load: path }, callback);
}
}
// Generate code for a pseudo-IPC using JSON.
function fakeIPC(expr) {
var cmd =
"try { " +
"printErr(JSON.stringify({ type: 'return', value: " + expr + " })); " +
"} catch (e) { " +
"printErr(JSON.stringify({ " +
"type: 'throw', " +
"value: { " +
"type: (e && e.constructor && e.constructor.name) || (typeof e), " +
"value: e, " +
"message: e && e.message, " +
"lineNumber: e && e.lineNumber " +
"} " +
"})); " +
"}";
return cmd;
}
// checking syntax (discards parse tree to minimize I/O)
function checkIPC(src) {
return fakeIPC("(Reflect.parse(" + JSON.stringify(src) + "), true)");
}
// checking syntax of a file (discards parse tree to minimize I/O)
function checkFileIPC(path) {
return fakeIPC("(Reflect.parse(snarf(" + JSON.stringify(path) + ")), true)");
}
// parse
function parseIPC(src) {
return fakeIPC("Reflect.parse(" + JSON.stringify(src) + ")");
}
// parse a file
function parseFileIPC(path) {
return fakeIPC("Reflect.parse(snarf(" + JSON.stringify(path) + "))");
}
// evaluate
function evalIPC(src) {
return fakeIPC("(1,eval)(" + JSON.stringify(src) + ")");
}
// load a file
function loadIPC(path) {
var quoted = JSON.stringify(path);
return fakeIPC("evalWithLocation(snarf(" + quoted + "), " + quoted + ", 1)");
}
module.exports = SM;
@subtleGradient
Copy link

I think the comma and semicolon reduction is the most compelling thing here ^_^

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment