suspend
suspend is a small, experimental library for Node.js that uses ES6 language features to simplify asynchronous code interactions.
Specifically, suspend exposes a minimal API around ES6 generators that is expressly designed to work seamlessly with Node.js' existing callback conventions. This allows unobtrusive use of yield
execution semantics that works seamlessly with existing Node.js code bases. suspend uses 100% native JavaScript - no transpiling or library-wrapping required.
Quick Example
var suspend = require('suspend'),
fs = require('fs');
suspend(function* (resume) {
var data = yield fs.readFile(__filename, resume);
console.log(data[1].toString('utf8'));
})();
Installation
$ npm install suspend
Why Generators (and Why suspend
)?
ES6 Generators landed in V8 3.19, which means they're available in Node.js since v0.11.2. Generators are awesome because, among other things, they allow for "suspended execution" semantics using the yield
keyword.
To illustrate, consider the following example:
function* myGenerator() {
console.log('hello');
yield sleep(2000);
console.log('world');
}
var iterator = myGenerator();
iterator.next();
function sleep(ms) {
setTimeout(function() {
iterator.next();
}, ms);
}
While the syntax above leaves something to be desired, the 2 second pause between console.log('hello')
and console.log('world')
is incredibly significant. Prior to generators, JavaScript had absolutely no language constructs to facilitate suspended execution, which is why all asynchronous operations in Node.js use callbacks.
What suspend does, then, is provide a small abstraction around generators that is designed to "play nice" with Node.js' existing callback conventions. Here's the previous example modified to use suspend:
suspend(function* (resume) {
console.log('hello');
yield sleep(2000, resume);
console.log('world');
})();
function sleep(ms, cb) {
setTimeout(cb, ms);
}
Notice that not only is the suspend version much cleaner, but the sleep()
function no longer has to know about the iterator at all. In fact, we can remove sleep()
altogether at this point if we want:
suspend(function* (resume) {
console.log('hello');
yield setTimeout(resume, 2000);
console.log('world');
})();
Here's another way to think about it: suspend is "red light, green light" for asynchronous code execution. yield
means stop, and resume
means go.
Usage
Basic Overview
When you provide a generator reference to suspend()
, it returns a new function reference that acts as an "initializer":
var run = suspend(function* () {
...
});
The generator itself is then initialized by invoking the returned function:
run();
Assigning this initializer to a temporary variable is, of course, unnecessary. Instead, we can simply invoke it immediately:
suspend(function* () {
...
})();
Invoking the generator like this is intentionally made optional. Sometimes, just like with regular functions, you don't want it to run immediately. For example, you may want to wait for an event before beginning execution:
someEmitter.on('some-event', suspend(function* () {
...
}));
Now, given that the majority of the Node.js ecosystem uses callbacks to handle asynchronous operations, we need a way to easily interact with functions that expect a callback. This is where resume
comes into play:
suspend(function* (resume) {
var data = yield fs.readFile(__filename, resume);
})();
As can be seen, when the generator is initialized, it is passed a reference to resume
. resume
is nothing more than a reusable callback, bound to the resulting iterator, that is just barely smart enough to understand Node.js' callback conventions. All arguments passed to resume
become available in an array, which is the result of the yield assignment:
suspend(function* (resume) {
var data = yield fs.readFile(__filename, resume);
console.log(data[1].toString('utf8'));
})();
Any arguments passed to the initializer are passed to the generator as well, following the resume
parameter:
suspend(function* (resume, fileName) {
var data = yield fs.readFile(fileName, resume);
console.log(data[1].toString('utf8'));
})(__filename);
What about Parallel Execution, Mapping, Etc.?
More advanced flow constructs, in my opinion, already have pretty elegant solutions through libraries like async. While I haven't ruled out support for these types of features in suspend itself, for now it is designed to be much more minimal, allowing you to layer on top of your existing control-flow libraries of choice. For example, here's a modified snippet from the async README:
// async without suspend
async.map(['file1','file2','file3'], fs.stat, function(err, results){
// results is now an array of stats for each file
});
// async with suspend
var res = yield async.map(['file1','file2','file3'], fs.stat, resume);
This begins to illustrate why suspend is designed to interoperate with Node.js' existing callback semantics. The goal isn't to replace your existing solutions - the goal is to simply and unobtrusively make them even better.
Error Handling
Default Behavior
By default, suspend won't do anything fancy with errors. If Node.js conventions are followed, errors returned from asynchronous methods will be passed as the first argument to the resume
callback. suspend won't make any assumptions about this, and will simply return the error in the first index of the results array.
Using this default behavior, then, error handling is much the same as before:
fs.readFile(__filename, function(err, buffer) {
if (err) {
}
console.log(buffer.toString('utf8'));
});
var res = yield fs.readFile(__filename, resume);
if (res[0]) {
}
console.log(res[1].toString('utf8'));
Note: if the results array is driving you crazy, be sure to read to the end of the README.
Throw Behavior
If for whatever reason you prefer to work with thrown exceptions instead, simply set the throw
option to true
:
suspend(function* () {
try {
var res = yield fs.readFile(__filename, resume);
console.log(res[0].toString('utf8'));
} catch (err) {
}
}, { throw: true })();
When throw
is set to true
, if an error is passed to the resume
callback (well, any non-null first parameter) it will be thrown instead of returned in the results array. Also, since the error is no longer being returned in the results array, the non-error arguments begin at index 0 (instead of 1).
Hate the results array?
Me too, but bear with me...
Having to access results through an array is a bit of an eyesore. There is a method to this madness, though. This aspect of the API is eagerly waiting for destructuring assignment to be implemented in V8 (currently it has progressed to Draft ES6 Specification, just like generators).
Once destructuring assignment is available, using suspend becomes even cleaner:
var res = yield fs.readFile(__filename, resume);
var [err, buffer] = yield fs.readFile(__filename, resume);
So, as with all things, patience, and a whole lot of nagging. :)
Is suspend
Ready To Be Used?
Mmmm... probably not. Currently generators are only supported in unstable (v0.11.x) versions of Node.js, and suspend itself is very new. I'll be eating my own dog food with it in side projects, and I would much appreciate feedback from any early adopters. If you find anything or have any suggestions, please open an issue (or email me at jmar777@gmail.com)!
On a related note, suspend will adhere to SemVer-compliant version updates, so if you do happen to use it you won't have to worry about the rug being pulled out from under you.
License
The MIT License (MIT)
Copyright (c) 2013 Jeremy Martin
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.