JSPM

  • Created
  • Published
  • Downloads 900874
  • Score
    100M100P100Q178043F

Asynchronous control-flow with deferred and promises

Package Exports

  • deferred
  • deferred/lib/async-to-promise
  • deferred/lib/deferred
  • deferred/lib/is-promise
  • deferred/lib/promise

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (deferred) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

Asynchronous JavaScript with deferred and promises

Promises simple, straightforward and powerful way. It was build with less is more mantra in mind, API consist of just 7 functions which should give all you need to configure complicated asynchronous control flow.

This work is highly inspired by other deferred/promise implementations, in particular Q by Kris Kowal.

## Installation

It's plain EcmaScript, but out of the box currently works only with node & npm (due to it's CommonJS package):

$ npm install deferred

For browser or other environments it needs to be bundled with few dependencies from es5-ext project (code states specifically which). Browser ready files will be available in near future.

## Deferred/Promise concept ### Basics

Straight to the point: when there's work to do that doesn't return immediately (asynchronous) deferred object is created and promise (deferred.promise) is returned to the world. When finally value is obtained, deferred is resolved with it deferred.resolve(value). At that point all promise observers (added via deferred.promise.then) are notified with value of fulfilled promise.

Example:

var deferred = require('deferred');

var later = function () {
  var d = deferred();
    setTimeout(function () {
        d.resolve(1);
    }, 1000);
    return d.promise;
};

later().then(function (n) {
    console.log(n); // 1
});

then takes callback and returns another promise. Returned promise will resolve with value that is a result of callback function, this way, promises can be chained:

later()
    .then(function (n) {
        var d = deferred();
        setTimeout(function () {
            d.resolve(n + 1);
        }, 1000);
        return d.promise;
    })
    .then(function (n) {
        console.log(n); // 2
    });

Callback passed to then may return anything, it may also be regular synchronous function:

later()
    .then(function (n) {
        return n + 1;
    })
    .then(function (n) {
        console.log(n); // 2
    });

Promises can be nested. If promise resolves with another promise, it's not really resolved. It's resolved only when final promise returns real value:

var count = 0;
var laterNested = function fn (value) {
  var d = deferred();
    setTimeout(function () {
        value *= 2;
        d.resolve((++count === 3) ? value : fn(value));
    }, 1000);
    return d.promise;
};

laterNested(1).then(function (n) {
    console.log(n); // 8
});

Promise can be resolved only once, and callbacks passed to then are also called only once, no exceptions. For deeper insight into this concept, and to better understand design decisions please see Kris Kowal design notes, it's well worth read.

### Error handling

Promise is rejected when it's resolved with an error, same way if callback passed to then throws exception it becomes resolution of promise returned by then. To handle error, pass second callback to then:

later()
    .then(function (n) {
        throw new Error('error!')
    })
    .then(function () {
        // never called
    }, function (e) {
        // handle error;
    });

When there is no error callback passed, error is silent. To expose error, end chain with .end(), then error that broke the chain will be thrown:

later()
    .then(function (n) {
        throw new Error('error!')
    })
    .then(function (n) {
        // never executed
    })
    .end(); // throws error!

end takes optional handler so instead of throwing, error can be handled other way. Behavior is exactly same as when passing second callback to then:

later()
    .then(function (n) {
        throw new Error('error!')
    })
    .end(function (e) {
        // handle error!
    });
## Asynchronous functions as promises

There is a known convention in JavaScript for working with asynchronous calls. Following approach is widely used within node.js:

var afunc = function (x, y, callback) {
    setTimeout(function () {
        try {
            callback(null, x + y);
        } catch (e) {
            callback(e);
        }
    }, 1000);
};

Asynchronous function receives callback argument. Callback handles both error and success. There's easy way to turn such functions into promises and take advantage of promise design. There's deferred.asyncToPromise for that, let's use shorter name:

var a2p = deferred.asyncToPromise;

// we can also import it individually:
a2p = require('deferred/lib/async-to-promise');

This method can be used in various ways.
First way is to assign it directly to asynchronous method:

afunc.a2p = a2p;

afunc.a2p(3, 4).then(function (n) {
    console.log(n); // 7
});

Second way is more traditional (I personally favor this one as it doesn't touch asynchronous function):

a2p = a2p.call;

a2p(afunc, 3, 4).then(function (n) {
    console.log(n); // 7
});

Third way is to bind method for later execution. We'll use ba2p name for that:

var ba2p = require('deferred/lib/async-to-promise').bind;

var abinded = ba2p(afunc, 3, 4);

// somewhere in other context:
abinded().then(function (n) {
    console.log(n); // 7
});

Note that this way of using it is not perfectly safe. We need to be sure that abinded will be called without any not expected arguments, if it's the case, then it won't execute as expected, see:

abinded(7, 4); // TypeError: number is not a function.

Node.js example.
Reading file, changing it's content and writing under different name:

var fs   = require('fs');

a2p(fs.readFile, __filename, 'utf-8')
    .then(function (content) {
        // change content
        return content;
    })
    .then(ba2p(fs.writeFile, __filename + '.changed'))
    .end();
## Control-flow, sophisticated chaining

There are three dedicated methods for constructing flow chain. They're avaiable on deferred:

deferred.join(...);
deferred.all(...);
deferred.first(...);

// let's access them directly:
var join = deferred.join;
var all = deferred.all;
var first = deferred.first;

join(p1, p2, p3, ...).then(...);
all(p1, p2, p3, ...).then(...);
first(p1, p2, p3, ...).then(...);

As with other API methods, they can also be imported individually:

var join  = require('deferred/lib/chain/join')
  , all   = require('deferred/lib/chain/all')
  , first = require('deferred/lib/chain/first');

Chain methods take arguments of any type and internally distinguish between promises, functions and others. Call them with list of arguments or an array:

join(p1, p2, p3);
join([p1, p2, p3]); // same behavior
### join(...)

join returns promise which resolves with an array of resolved values of all arguments. Values may be anything, also errors (rejected promises, functions that thrown errors, errors itself). Returned promise always fulfills, never rejects.

### all(...)

Same as join, with that difference that all arguments need to be succesful. If there's any error, chain execution is stopped (following functions are not called), and promise is rejected with error that broke the chain. In succesful case returned promise value is same as with join, array of results.

### first(...)

Fulfills with first succesfully resolved argument. If all arguments fail, then promise rejects with error that occurred last.

### Non promise arguments

As mentioned above, chain methods take any arguments, not only promises. Function arguments are called with previous argument, if one resolved succesfully. If previous argument failed then function is never called. Error that rejected previous argument becomes also result of following function within returned result array. Any other values (neither promises or functions) are treated as if they were values of resolved promises.

### Examples:

Regular control-flow

Previous read/write file example written with all:

all(
    a2p(fs.readFile, __filename, 'utf-8'),
    function (content) {
        // change content
        return content;
    },
    ba2p(fs.writeFile, __filename + '.changed')
).end();

Concat all JavaScript files in given directory and save it to lib.js:

all(
    // Read all filenames in given path
    a2p(fs.readdir, __dirname),

    // Filter *.js files
    function (files) {
        return files.filter(function (name) {
            return (name.slice(-3) === '.js');
        });
    },

    // Read files content
    function (files) {
        return join(files.map(function (name) {
            return a2p(fs.readFile, name, 'utf-8');
        }));
    },

    // Concat into one string
    function (data) {
        return data.join("\n");
    },

    // Write to lib.js
    ba2p(fs.writeFile, __dirname + '/lib.js')
).end();

We can shorten it a bit with introduction of functional sugar, it's out of scope of this library but I guess worth an example:

var invoke = require('es5-ext/lib/Function/invoke');

all(
    // Read all filenames in given path
    a2p(fs.readdir, __dirname),

    // Filter *.js files
    invoke('filter', function (name) {
        return (name.slice(-3) === '.js');
    }),

    // Read files content
    invoke('map', function (name) {
        return a2p(fs.readFile, name, 'utf-8');
    }), join,

    // Concat into one string
    invoke('join', "\n"),

    // Write to lib.js
    ba2p(fs.writeFile, __dirname + '/lib.js')
).end();

invoke implementation can be found in es5-ext project: https://github.com/medikoo/es5-ext/blob/master/lib/Function/invoke.js

#### Asynchronous loop

Let's say we're after content that is paginated over many pages on some website (like search results). We don't know how many pages it spans. We only know by reading page n whether page n + 1 exists.

First things first. Simple download function, it downloads page at given path from predefinied domain and returns promise:

var http = require('http');

var getPage = function (path) {
    var d = deferred();

    http.get({
        host: 'www.example.com',
        path: path
    }, function(res) {
        res.setEncoding('utf-8');
        var content = "";
        res.on('data', function (data) {
            content += data;
        });
        res.on('end', function () {
            d.resolve(content);
        });
    }).on('error', d.resolve);

    return d.promise;
};

Deferred loop:

var n = 1, result;
getPage('/page/' + n++)
    .then(function process (content) {
        // populate result
        // decide whether we need to download next page
        if (isNextPage) {
            return getPage('/page/' + n++).then(process);
        } else {
            return result;
        }
    })
    .then(function (result) {
        // play with final result
    }).end();

We can also make it with all:

var n = 1, result;
all(
    getPage('/page/' + n++),
    function process (content) {
        // populate result
        // decide whether we need to download next page
        if (isNextPage) {
            return getPage('/page/' + n++).then(process);
        } else {
            return result;
        }
    },
    function (result) {
        // play with final result
    }
).end();
### Comparision to other solutions that take non promise approach

Following are examples from documentation of other solutions rewritten deferred/promise way. You'll be the judge, which solution you find more powerful and friendly.

#### Step -> https://github.com/creationix/step

First example from Step README, using chained promises:

all(
    a2p(fs.readFile, __filename, 'utf-8'),
    function capitalize (txt) {
        return txt.toUpperCase();
    },
    function showIt (newTxt) {
        console.log(newTxt);
    }
).end();

Again we can make it even more concise with functional sugar:

all(
    a2p(fs.readFile, __filename, 'utf-8'),
    invoke('toUpperCase'),
    console.log
).end();
#### Async -> https://github.com/caolan/async ##### async.series:
all(
    function () {
        // do some stuff ...
        return 'one';
    },
    function () {
        // do some more stuff
        return 'two';
    }
)
.then(function (results) {
    // results is now equal to ['one', 'two']
},
function (err) {
    // handle err
});
##### async.paralles:

For parallel execution we pass already initialized promises:

all(
    promise1,
    a2p(asyncFunc, arg1, arg2),
    promise2
)
.then(function (results) {
    // results are resolved values of promise1, asyncFunc and promise2
},
function (err) {
    // handle err
});
##### async.waterfall:

Resolved values are always passed to following functions, so again we have it out of a box:

all(
    function () {
        return ['one', 'two'];
    },
    function (args) {
        return 'three';
    },
    function (arg1) {
        // arg1 now equals 'three'
    }
);
##### async.auto

It's a question of combining all chains. First example from docs:

all(
    all(
        a2p(get_data),
        a2p(make_folder)
    ),
    ba2p(write_file)
    ba2p(email_link)
).end();
##### async.whilst, async.until

See Asynchronous loop example, it shows how easily loops can be configured.

##### async.forEach, async.map, async.filter ..etc.

Asynchronous handlers for array iterators, forEach and map:

all(arr.map(function (item) {
    // logic
    return promise;
})
.then(function (results) {
    // deal with results
    // if it's forEach than results are obsolete
})
.end();

I decided not to implement array iterator functions in this library, for two reasons, first is as you see above - it's very easy and straightforward to setup them with provided chain methods, second it's unlikely we need most of them.