-
-
Save rwaldron/2653470 to your computer and use it in GitHub Desktop.
sm.js with classes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment