You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
294 lines
5.3 KiB
294 lines
5.3 KiB
|
|
/** |
|
* slice() reference. |
|
*/ |
|
|
|
var slice = Array.prototype.slice; |
|
|
|
/** |
|
* 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(function(){ |
|
done.call(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 TypeError('You may only yield a function, promise, generator, array, or object, ' |
|
+ 'but the following was passed: "' + String(ret.value) + '"')); |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* 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; |
|
var isArray = Array.isArray(obj); |
|
|
|
return function(done){ |
|
var keys = Object.keys(obj); |
|
var pending = keys.length; |
|
var results = isArray |
|
? new Array(pending) // predefine the array length |
|
: new obj.constructor(); |
|
var finished; |
|
|
|
if (!pending) { |
|
setImmediate(function(){ |
|
done(null, results) |
|
}); |
|
return; |
|
} |
|
|
|
// prepopulate object keys to preserve key ordering |
|
if (!isArray) { |
|
for (var i = 0; i < pending; i++) { |
|
results[keys[i]] = undefined; |
|
} |
|
} |
|
|
|
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; |
|
}); |
|
}
|
|
|