blob: 9ea3a39dac4e1732e5416057d876dbfd8fe0b5cc [file] [log] [blame]
/**
* toString() reference.
*/
var toString = Object.prototype.toString;
/**
* slice() reference.
*/
var slice = Array.prototype.slice;
/**
* setImmediate() with fallback to process.nextTick() for node v0.8.x.
*/
var setImmediate = global.setImmediate || process.nextTick;
/**
* Expose `co`.
*/
module.exports = co;
/**
* Wrap the given generator `fn` and
* return a thunk.
*
* @param {Function} fn
* @return {Function}
* @api public
*/
function co(fn) {
var isGenFun = isGeneratorFunction(fn);
return function (done) {
var ctx = this;
// in toThunk() below we invoke co()
// with a generator, so optimize for
// this case
var gen = fn;
// we only need to parse the arguments
// if gen is a generator function.
if (isGenFun) {
var args = slice.call(arguments), len = args.length;
var hasCallback = len && 'function' == typeof args[len - 1];
done = hasCallback ? args.pop() : error;
gen = fn.apply(this, args);
} else {
done = done || error;
}
next();
// #92
// wrap the callback in a setImmediate
// so that any of its errors aren't caught by `co`
function exit(err, res) {
setImmediate(done.bind(ctx, err, res));
}
function next(err, res) {
var ret;
// multiple args
if (arguments.length > 2) res = slice.call(arguments, 1);
// error
if (err) {
try {
ret = gen.throw(err);
} catch (e) {
return exit(e);
}
}
// ok
if (!err) {
try {
ret = gen.next(res);
} catch (e) {
return exit(e);
}
}
// done
if (ret.done) return exit(null, ret.value);
// normalize
ret.value = toThunk(ret.value, ctx);
// run
if ('function' == typeof ret.value) {
var called = false;
try {
ret.value.call(ctx, function(){
if (called) return;
called = true;
next.apply(ctx, arguments);
});
} catch (e) {
setImmediate(function(){
if (called) return;
called = true;
next(e);
});
}
return;
}
// invalid
next(new Error('yield a function, promise, generator, array, or object'));
}
}
}
/**
* Convert `obj` into a normalized thunk.
*
* @param {Mixed} obj
* @param {Mixed} ctx
* @return {Function}
* @api private
*/
function toThunk(obj, ctx) {
if (isGeneratorFunction(obj)) {
return co(obj.call(ctx));
}
if (isGenerator(obj)) {
return co(obj);
}
if (isPromise(obj)) {
return promiseToThunk(obj);
}
if ('function' == typeof obj) {
return obj;
}
if (isObject(obj) || Array.isArray(obj)) {
return objectToThunk.call(ctx, obj);
}
return obj;
}
/**
* Convert an object of yieldables to a thunk.
*
* @param {Object} obj
* @return {Function}
* @api private
*/
function objectToThunk(obj){
var ctx = this;
return function(done){
var keys = Object.keys(obj);
var pending = keys.length;
var results = new obj.constructor();
var finished;
if (!pending) {
setImmediate(function(){
done(null, results)
});
return;
}
for (var i = 0; i < keys.length; i++) {
run(obj[keys[i]], keys[i]);
}
function run(fn, key) {
if (finished) return;
try {
fn = toThunk(fn, ctx);
if ('function' != typeof fn) {
results[key] = fn;
return --pending || done(null, results);
}
fn.call(ctx, function(err, res){
if (finished) return;
if (err) {
finished = true;
return done(err);
}
results[key] = res;
--pending || done(null, results);
});
} catch (err) {
finished = true;
done(err);
}
}
}
}
/**
* Convert `promise` to a thunk.
*
* @param {Object} promise
* @return {Function}
* @api private
*/
function promiseToThunk(promise) {
return function(fn){
promise.then(function(res) {
fn(null, res);
}, fn);
}
}
/**
* Check if `obj` is a promise.
*
* @param {Object} obj
* @return {Boolean}
* @api private
*/
function isPromise(obj) {
return obj && 'function' == typeof obj.then;
}
/**
* Check if `obj` is a generator.
*
* @param {Mixed} obj
* @return {Boolean}
* @api private
*/
function isGenerator(obj) {
return obj && 'function' == typeof obj.next && 'function' == typeof obj.throw;
}
/**
* Check if `obj` is a generator function.
*
* @param {Mixed} obj
* @return {Boolean}
* @api private
*/
function isGeneratorFunction(obj) {
return obj && obj.constructor && 'GeneratorFunction' == obj.constructor.name;
}
/**
* Check for plain object.
*
* @param {Mixed} val
* @return {Boolean}
* @api private
*/
function isObject(val) {
return val && Object == val.constructor;
}
/**
* Throw `err` in a new stack.
*
* This is used when co() is invoked
* without supplying a callback, which
* should only be for demonstrational
* purposes.
*
* @param {Error} err
* @api private
*/
function error(err) {
if (!err) return;
setImmediate(function(){
throw err;
});
}