genny

A tiny ES6 (harmony) library for node 0.11.2+ that helps you use generators with node style callbacks.

Usage no npm install needed!

<script type="module">
  import genny from 'https://cdn.skypack.dev/genny';
</script>

README

genny

An ES6 (harmony) library for node 0.11.2 and up that helps you use generators with node style callbacks, similar to suspend

Benefits:

  • No need to wrap anything or use fn.bind. Works with regular callback-taking node functions.
  • Complete error stack traces
  • Compatible: also works with promises, thunks and arrays of promises/thunks.

usage examples

Spawn a generator task. From within your task, call your async functions with yield. Instead of a callback function, pass them a generated resume function:

genny.run(function* (resume) {
    console.log("Hello");
    yield setTimeout(resume(), 1000);
    console.log("World");
});

Genny automatically passes resume as the last argument to your generator. Its a constructor that can make resume callbacks.

The generator pauses when it encounters a yield, then resumes when the created resume callback is called by the async operation. If the callback was called with a value:

fn(null, value)

then the yield expression will return that value.

Example:

genny.run(function* (resume) {
    var data = yield fs.readFile("test.js", resume());
    console.log(data.toString())
});

errors

You can handle errors with try/catch, or as return results via resume.nothrow

genny.run(function* (resume) {
    // Throwing resume
    try {
        yield fs.readFile("test.js", resume());
    }
    catch (e) { // handle error
        console.error("Error reading file", e);
    }
    // Non-throwing resume, result is an array.
    var err_res = yield fs.readFile("test.js", resume.nothrow());
    if (err_res[0]) { // handle error
        console.error("Error reading file", err_res[0]);
    }
});

Alternatively, you can pass a callback argument to genny.run:

genny.run(function* (resume) {
    var data = yield fs.readFile("test.js", resume());
}, function(err) {
    // thrown error propagates here automatically
    // because it was not caught.
    if (err)
        console.error("Error reading file", err);
});

running things in parallel

If you need to run multiple operations in parallel, don't yield immediately:

genny.run(function* (resume) {
    fs.readFile("test.js", resume());
    fs.readFile("test2.js", resume());
    var file1 = yield, file2 = yield;
    return file1.toString() + file2.toString();
});

The order of yield results is guaranteed to be the same as the order of the resume() callback constructors. Feel free to use it in loops too:

genny.run(function* (resume) {
    // read files in parallel
    for (var k = 0; k < files.length; ++k)
        fs.readFile(file[k], resume());

    // wait for all of them to be read
    var content = [];
    for (var k = 0; k < files.length; ++k)
        content.push(yield);

});

You may also give yield a thunk (a function that take callback) or a promise

genny.run(function* () {
    var first = yield
      function(callback) { fs.readFile("test1.js", callback); };
    var files = yield [
      function(callback) { fs.readFile("test2.js", callback); },
      function(callback) { fs.readFile("test3.js", callback); },
    ];
    return first + files[0].toString() + files[1].toString();
});

or an array of these which will be run in parallel.

creating callback functions

You can also use genny.fn instead to create a function which can accept multiple arguments and a callback. The arguments will be passed to your generator, but instead of the callback, you will get genny's resume

var getLine = genny.fn(function* (file, number, resume) {
    var data = yield fs.readFile(file, resume());
    return data.toString().split('\n')[number];
});

getLine('test.js', 2, function(err, line) {
    // thrown error propagates here automagically
    // because it was not caught.
    // If the file actually exists, lineContent
    // will contain the second line
    if (err)
        console.error("Error reading line", err);
});

The result is a function that takes the specified arguments plus a standard node style callback. If you return a value at the end of your generator, it is passed as the result argument to the callback.

multi-argument callbacks, calling generators

If the async function calls the callback with more than 2 arguments, an array will be returned from the yield expression:

function returnsmore(callback) {
    callback(null, 'arg1', 'arg2');
}

genny.run(function* (resume) {
    var res = yield returnsmore(resume());
    var arg1 = res[0];
    var arg2 = res[1];
    var nothrowres = yield returnsmore(resume.nothrow());
    var err = res[0];
    var arg1 = res[1];
    var arg2 = res[2];
});

Use yield* and resume.gen() to call a genny-compatible generator:

yield* someGenerator(args..., resume.gen())

listeners and middleware

genny.fn creates a callback-taking node function which requires its last argument to be a callback. To create a listener function use genny.listener instead:

ee.on('event', genny.listener(function* (resume) { ... }));

Listeners currently ignore all errors and return values, but this may change in the future.

To create an express or connect middleware that properly forwards errors, use genny.middleware

app.get('/test', genny.middleware(function* (req, res, resume) {
    if (yield isAuth(req, resume.t))
        return true; // will call next()
    else
        throw new HttpError(401, "Unauthorized"); // will call next(err)

    // or use return; and next() will not be called.
});

debugging

genny comes with longStackSupport that enables you to trace errors across generators. Simply write:

require('genny').longStackSupport = true

to get stack traces like these:

Error: oops
    at Object._onImmediate (/home/spion/Documents/genny/test/index.js:10:12)
    at processImmediate [as _immediateCallback] (timers.js:325:15)
From generator:
    at innerGenerator1 (/home/spion/Documents/genny/test/index.js:136:26)
    at innerGenerator2 (/home/spion/Documents/genny/test/index.js:139:43)
    at innerGenerator3 (/home/spion/Documents/genny/test/index.js:142:43)
    at Test.completeStackTrace2 (/home/spion/Documents/genny/test/index.js:145:43)

for code like this:

function* innerGenerator1(resume) {
    yield errors(resume());
}
function* innerGenerator2(resume) {
    yield* innerGenerator1(resume.gen());
}
function* innerGenerator3(resume) {
    yield* innerGenerator2(resume.gen());
}
yield* innerGenerator3(resume.gen());

This results with CPU overhead of approximately 100% and memory overhead of approximately 80%.

In the future, the overhead will probably be eliminated in node but not in browsers.

more info

Look in test/index.js for more examples and tests.

thanks

jmar777 for his awesome suspend library which served as the base for genny

license

MIT