SkillAgentSearch skills...

Suspend

Callback-free control flow for Node using ES6 generators.

Install / Use

/learn @jmar777/Suspend
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

suspend

Generator-based control-flow for Node enabling asynchronous code without callbacks, transpiling, or selling your soul.

Suspend is designed to work seamlessly with Node's callback conventions, promises, and thunks, but is also compatible with code that follows other conventions.

Related reading for the generator-uninitiated: What's the Big Deal with Generators?

Note: Generators are a new feature in ES6 and are still hidden behind the --harmony-generators (or the more general --harmony) flag in V8:

$ node --harmony-generators your-script.js

Quick Examples

Working with Node-style callbacks:

var suspend = require('suspend'),
    resume = suspend.resume;

suspend(function*() {
    var data = yield fs.readFile(__filename, 'utf8', resume());
    console.log(data);
})();

Working with promises:

var suspend = require('suspend');

suspend(function*() {
    var user = yield UserModel.find({ username: 'jmar777' });
    console.log(user.favoriteColor);
})();

Working with thunks:

var suspend = require('suspend'),
    readFile = require('thunkify')(require('fs').readFile);

suspend(function*() {
    var package = JSON.parse(yield readFile('package.json', 'utf8'));
    console.log(package.name);
});

Installation

$ npm install suspend

Documentation

API

suspend.async(fn*)

Accepts a generator function fn*, and returns a wrapper function that follows Node's callback conventions. Note that the wrapper function requires a callback as the last parameter.

Example:

var readJsonFile = suspend.async(function*(fileName) {
    var rawFile = yield fs.readFile(fileName, 'utf8', suspend.resume());
    return JSON.parse(rawFile);
});

// the resulting function behaves like any other async function in node
readJsonFile('package.json', function(err, packageData) {
    console.log(packageData.name); // 'suspend'
});

Note that .async() lets you return your final result, instead of having to explicitly accept a callback parameter and pass the result manually. Likewise, any uncaught errors will be passed to the callback as the error argument (see the section on error handling for more information).


suspend.promise(fn*)

Accepts a generator function fn*, and returns a wrapper function that returns a Promise. If a value is returned (or the generator function completes with no explicit return value), the promise is resolved. If an error is thrown, then the promise is rejected.

Example:

var getFavoriteNumberByUsername = suspend.promise(function*(username) {
    var user = yield UserModel.find({ username: username });
    return user.favoriteColor;
});

// the resulting function exposes a promise API:
var promise = getFavoriteNumberByUsername('jmar777');

Note that the above example also demonstrates the ability to yield promises, which is documented below.


suspend.fn(fn*)

Accepts a generator function fn*, and returns a wrapper function that, unlike .async(), makes no assumptions regarding callback conventions. This makes .fn() useful for event handlers, setTimeout() functions, and other use cases that don't expect Node's typical asynchronous method signature.

Note: As a shorthand convenience, suspend(fn*) is an alias for suspend.fn(fn*).

Example:

var listener = suspend(function*(req, res) {
    // wait 2 seconds
    yield setTimeout(suspend.resume(), 2000);
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('Thanks for being patient!');
});

http.createServer(listener).listen(1337, '127.0.0.1');

suspend.run(fn*, [cb])

Accepts a generator function fn* and runs it immediately. If an optional callback cb is provided, any errors or return values will be passed to it.

Example:

suspend.run(function*() {
    var data = yield fs.readFile('file1', suspend.resume());
    yield fs.writeFile('file1-copy', data, suspend.resume());
}, function(err) {
    if (err) console.error(err);
});

suspend(fn*)

An alias for suspend.fn(fn*).

Suspending and Resuming Execution

yield

The yield keyword is a new language feature associated with generators in ES6. Whenever a yield is encountered, execution of the generator is "suspended" until something external tells it to resume.

In suspend, as the name implies, we use yield to suspend the generator while performing asynchronous operations, and then resume it once the operation is complete.

If you're using promises or thunks, suspend can resume for you automatically. However, given that the majority of the Node ecosystem relies on callbacks, suspend provides some simple mechanisms for interacting with callback-based code: resume() and resumeRaw().


suspend.resume()

A simple callback factory for interacting with Node-style asynchronous functions.

Example:

suspend(function*() {
    var data = yield fs.readFile(__filename, 'utf8', suspend.resume());
})();

As can be seen, resume() creates Node-style callbacks that know how to handle the results from the asynchronous operation and automatically resume the generator. If the first argument passed to the callback is an error (or any other truthy value), it will be thrown back in the generator body. Otherwise, the value of the second argument will be the result of the asynchronous operation.


suspend.resumeRaw()

While resume() knows how to intelligently handle Node-style callbacks, sometimes we have to work with code that doesn't follow these conventions. In these situations, resumeRaw() can be used. resumeRaw() makes no assumptions regarding what the arguments are, and simply provides the results back as an array.

Consider, for example, fs.exists(), a remnant from Node's early days and one of the few extant API methods that doesn't follow the error-first callback convention. While you shouldn't ever actually use fs.exists(), this is a good example of when you would use resumeRaw():

Example:

suspend(function*() {
    var data = yield fs.exists(__filename, suspend.resumeRaw());
    console.log(data); // [ true ]
})();

Promises

As was previously mentioned, suspend is also designed to play nice with promises. In fact, promises and suspend make a particularly nice combination, as it completely alleviates the need for callbacks (or resume()). To use suspend with promises, simply yield the promise itself.

Example:

suspend(function*() {
    var user = yield UserModel.find({ username: 'jmar777' });
})();

The above is an example of working with mongoose, which returns promises for asynchronous operations. If a yield expression evaluates to a "thenable", then suspend can figure out the rest.


Thunks

Thunks provide a nice, lightweight alternative to promises when working with generators. Sometimes referred to as continuables, thunks are simply functions returned from asynchronous operations, that accept a single node-style callback parameter. For creating "thunkified" versions of asynchronous functions, I recommend TJ Holowaychuk's thunkify module.

Example:

var readFile = thunkify(fs.readFile);

suspend(function*() {
    var data = yield readFile(__filename, 'utf8');
});

As can be seen, one must simply yield the thunk itself and suspend will appropriately handle the eventual result or error. Just like with promises, thunks and suspend make a particularly nice combination, as once again there's no need to pass in resume() or other callback mechanism into the async function itself.

Parallel Operations

While yielding is a convenient way to wait for an operation to complete, it does force things to be executed in series. Sometimes, however, we wish to do things in parallel. In suspend, parallel operations are made easy with fork() and join().

suspend.fork() and suspend.join()

Unlike resume(), which requires you to yield first, fork() creates a callback that will temporarily store the completion values until you subsequently yield on join(). This allows any arbitrary number of parallel operations to be "forked", without suspending execution until we are ready to use their results.

Example:

suspend(function*() {
    var fileNames = yield fs.readdir('test', suspend.resume());

    fileNames.forEach(function(fileName) {
        fs.readFile('test/' + fileName, 'utf8', suspend.fork());
    });

    var files = yield suspend.join();

    var numT
View on GitHub
GitHub Stars547
CategoryDevelopment
Updated1y ago
Forks22

Languages

JavaScript

Security Score

65/100

Audited on Mar 21, 2025

No findings