generator-async

Generator-based asynchronous control flow for Node.js

Usage no npm install needed!

<script type="module">
  import generatorAsync from 'https://cdn.skypack.dev/generator-async';
</script>

README

Build Status

generator-async

Flexible generator-based asynchronous control flow for Node.js.

Introduction

With generator-async use both callback-based and promise-based libraries all together with a clean, consistent interface. Use the yield keyword before an asynchronous function call that would normally take a callback or return a promise, and skip the callback or call to then().

var async = require('generator-async');

// load `fs` with async's `require`
var fs = async.require('fs');

async.run(function*() {

    // read a file
    var passwd = yield fs.readFile('/etc/passwd');

    // then read another file
    var group = yield fs.readFile('/etc/group');

    // then spit out the contents of both files
    console.log(passwd, group);
});

In this example, the contents of /etc/passwd are read from disk and assigned to passwd. Program execution waits on the I/O and assignment, while the process event loop keeps on moving. That's in contrast to fs.readFileSync for example, where the entire process waits.

Notice fs was loaded through async.require, which wraps async methods to be compatible. If you'd rather not wrap, you can load modules directly like normal, and just refer to async.cb wherever a standard callback would be expected. See the Alternative Explicit Callback Interface section below for more.

Running in Parallel

Run functions concurrently with async.parallel and then yield in the same order.

async.run(function*() {

    // kick off read operations with async.parallel
    async.parallel( fs.readFile('/etc/passwd') );
    async.parallel( fs.readFile('/etc/group') );
    
    // yield in order
    var passwd = yield async.yield;
    var group = yield async.yield;
});

Examples

Working with Request

var request = async.require('request');

async.run(function*() {

    var data = yield request('http://www.google.com');
    console.log(data);
});

Working with Redis

var redis = async.require('redis');

async.run(function*() {

    var client = redis.createClient(6379, '172.17.42.1', {});

    yield client.hset("hash key", "hashtest 1", "some value");
    yield client.hset("hash key", "hashtest 2", "some other value");

    var data = yield client.hkeys("hash key");
    console.log(data);
});

Working with Express

Use generator functions directly as route handlers.

var express = async.require('express');
var fs = async.require('fs');

var app = express();

app.get('/', function*(req, res) {

    var data = yield fs.readFile('/etc/passwd');
    res.end(data);
});

Working with the file system

var fs = async.require('fs');
var mkdirp = async.require('mkdirp');
var rimraf = async.require('rimraf');
var path = require('path');
var assert = require('assert');

async.run(function*() {

    // come up with a random-ish nested dir name and see that it doesn't exist yet
    var name = path.join('mkdirp-' + parseInt(Math.random() * 10000), 'nested', 'dir');
    assert.equal(yield fs.exists(name), false);

    // create the new directory and see that it exists
    var err = yield mkdirp(name);
    assert.equal(yield fs.exists(name), true);

    // remove our nascent dir and see that it no longer exists
    yield rimraf(name);
    assert.equal(yield fs.exists(name), false);
});

Wrapping Classes

Wrapped classes that define methods as generator functions will automatically have those generator functions wrapped in a generator-async context, meaning they'll be invoked and executed when called by consumers.

Consider a Class representing a file:

var File = function() {
    this.initialize.apply(this, arguments);
};

File.prototype = {

    initialize: function(filename) {
        this.filename = filename;
    },
    read: function*() {
        var contents = yield fs.readFile(this.filename);
        return contents;
    },
    size: function*() {
        var stat = yield fs.stat(this.filename);
        return stat.size;
    }
};

File = async(File);

Consume this functionality in an async.run context:

async.run(function*() {
    var file = new File('/etc/passwd');
    var size = yield file.size();
    console.log(size);
});

Or, consume this functionality as-is just like normal with conventional callbacks.

var file = new File('/etc/passwd');
file.size(function(err, size) {
    console.log(size);
};

Alternative Explicit Callback Interface

Sometimes it's not feasible to wrap a module or method to be yieldable since it may have a non-standard callback scheme. Or you may prefer the more verbose interface in order to use the module directly and shed some of the intermediary magic. In that case, node's require like normal, and refer to async.cb where a callback is expected:

// require `fs` directly
var fs = require('fs');

async.run(function*() {
    // refer to async.cb where the callback would go
    var contents = yield fs.readFile('/etc/passwd', async.cb);
    console.log(contents);
});

Use async.cb where a standard node callback would be expected (a function that accepts err and data parameters). For non-standard callbacks refer to async.raw to get back all the values (and handle errors yourself).

API

async(input)

Implementation depends on the type of input. Given a function, or generator, returns a function that is both yieldable in an async.run context, and also compatible with a standard callback interface. Given an object or class, returns the input with each of its methods wrapped to be yieldable. Aliased as async.wrap.

async.require(module, [hints])

Imports a module Like node's native require, but also wraps that module so that its methods are yieldable in an async.run context with no callbacks necessary.

That's the goal at least. Wrapping involves making a heuristic best guess about which methods are asynchronous and what is their callback signature etc. So while it often works well, you may sometimes need to give hints about the makeup of the module. See module-async-map for more.

If you'd rather, feel free to use node's native require instead, and refer to async.cb where a callback is expected.

async.run(fn*)

Invokes and executes the supplied generator function.

async.proxy(fn)

Returns a wrapped version of the supplied function compatible to be run either in an async.run context or standard node callback style.

async.fn(fn*)

Returns a function that when called will invoke and execute the supplied generator function.

Collection Methods

Since the yield keyword is only valid directly inside of generator functions, we can't yield inside of stock Array methods, which might be exactly what you want to do sometimes. Instead, use these collection methods, which accept generator functions as iterator functions, so you can yield from within them. Underlying implementations courtesy of the fantastic async library, which has more documentation.

async.forEach(arr, fn*)

Applies fn* as an iterator to each item in arr, in parallel. Aliased as async.each.

var fs = async.require('fs');

async.run(function*() {

    var filenames = [...];
    var totalBytes = 0;

    yield async.forEach(filenames, function*(filename) {
        var stat = yield fs.stat(filename);
        totalBytes += stat.size;
        console.log(filename, stat.size);
    });

    console.log("Total bytes:", totalBytes);
});

async.eachLimit(arr, limit, fn*)

Same as async.forEach except that only limit iterators will be simultaneously running at any time.

async.map(arr, fn*)

Produces a new array of values by mapping each value in arr through the generator function.

async.run(function*() {

    var filenames = [...];

    var existences = yield async.map(filenames, function*(filename) {
        return yield fs.exists(filename);
    });

    console.log(existences); // => [ true, true, false, true, ... ]
});

async.mapLimit(arr, limit, fn*)

Same as async.map except that only limit iterators will be simultaneously running at any time.

async.filter(arr, fn*)

Returns a new array of all the values in arr which pass an async truth test.

async.run(function*() {

    var filenames = [...];

    var bigFiles = yield async.filter(filenames, function*(filename) {
        var stat = yield fs.stat(filename);
        return stat.size > 1024;
    });

    console.log("Big files:", bigFiles);
});

async.reject(arr, fn*)

The opposite of async.filter. Removes values that pass an async truth test.

async.reduce(arr, memo, fn*)

Reduces arr into a single value using an async iterator to return each successive step. memo is the initial state of the reduction. Runs in series.

async.reduceRight(arr, memo, fn*)

Same as async.reduce, only operates on arr in reverse order.

async.detect(arr, fn*)

Returns the first value in arr that passes an async truth test. The generator function is applied in parallel, meaning the first iterator to return true will itself be returned.

async.sortBy(arr, fn*)

Sorts a list by the results of running each arr value through an async generator function.

async.run(function*() {

    var filenames = [...];

    var sortedFiles = yield async.filter(filenames, function*(filename) {
        var stat = yield fs.stat(filename);
        return stat.size;
    });

    console.log("Sorted files:", sorted);
});

async.some(arr, fn*)

Returns true if at least one element in the arr satisfies an async test.

async.every(arr, fn*)

Returns true if every element in arr satisfies an async test.

async.concat(arr, fn*)

Applies iterator to each item in arr, concatenating the results. Returns the concatenated list.

History and Inspiration

This is an evolution of gx, with inspiration from other generator-based control flow libraries such as co, genny, galaxy, and suspend.

License

Copyright (c) 2015 David Chester

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.