asyncawait
Version:
async/await for node.js
156 lines (128 loc) • 5.87 kB
text/typescript
import Fiber = require('../fibers');
import Promise = require('bluebird');
import _ = require('lodash');
export = makeAwaitFunc;
/** Function for creating a specific variant of the await() function.
* @param {string} variant - Recognised values: undefined, 'in', 'top'.
*/
function makeAwaitFunc(variant?: string): any {
// Return an await function tailored to the given options.
switch (variant) {
case 'in': return getExtraInfo(traverseInPlace);
case 'top': return (n: number) => getExtraInfo(traverseInPlace, n);
default: return getExtraInfo(traverseClone);
}
}
/** Helper function for makeAwaitFunc(). */
function getExtraInfo(traverse: (o, visitor: Function) => void, topN?: number) {
return function await() {
// Ensure this function is executing inside a fiber.
if (!Fiber.current) {
throw new Error(
'await functions, yield functions, and value-returning suspendable ' +
'functions may only be called from inside a suspendable function. '
);
}
// Parse argument(s). If not a single argument, treat it like an array was passed in.
if (arguments.length === 1) {
var expr = arguments[0];
} else {
expr = new Array(arguments.length);
for (var i = 0; i < arguments.length; ++i) expr[i] = arguments[i];
traverse = traverseInPlace;
}
// Handle each supported 'awaitable' appropriately...
var fiber = Fiber.current;
if (expr && _.isFunction(expr.then)) {
// A promise: resume the coroutine with the resolved value, or throw the rejection value into it.
// NB: ensure the handlers return null to avoid bluebird 3.x warning 'a promise was created in a
// handler but none were returned from it'. This occurs if the next resumption of the suspendable
// function (i.e. in the client's code) creates a bluebird 3.x promise and then awaits it.
expr.then(val => (fiber.run(val), fiber = null), err => (fiber.throwInto(err), fiber = null));
}
else if (_.isFunction(expr)) {
// A thunk: resume the coroutine with the callback value, or throw the errback value into it.
expr((err, val) => { if (err) fiber.throwInto(err); else fiber.run(val); fiber = null; });
}
else if (_.isArray(expr) || _.isPlainObject(expr)) {
// An array or plain object: resume the coroutine with a deep clone of the array/object,
// where all contained promises and thunks have been replaced by their resolved values.
// NB: ensure handlers return null (see similar comment above).
var trackedPromises = [];
expr = traverse(expr, trackAndReplaceWithResolvedValue(trackedPromises));
if (!topN) {
Promise.all(trackedPromises).then(val => (fiber.run(expr), fiber = null), err => (fiber.throwInto(err), fiber = null));
} else {
Promise.some(trackedPromises, topN).then(val => (fiber.run(val), fiber = null), err => (fiber.throwInto(err), fiber = null));
}
} else {
// Anything else: resume the coroutine immediately with the value.
setImmediate(() => { fiber.run(expr); fiber = null; });
}
// Suspend the current fiber until the one of the above handlers resumes it again.
return Fiber.yield();
}
}
/** In-place (ie non-cloning) object traversal. */
function traverseInPlace(o, visitor: (obj, key) => void): any {
if (_.isArray(o)) {
var len = o.length;
for (var i = 0; i < len; ++i) {
traverseInPlace(o[i], visitor);
visitor(o, i);
}
} else if (_.isPlainObject(o)) {
for (var key in o) {
if (!o.hasOwnProperty(key)) continue;
traverseInPlace(o[key], visitor);
visitor(o, key);
}
}
return o;
}
/** Object traversal with cloning. */
function traverseClone(o, visitor: (obj, key) => void): any {
var result;
if (_.isArray(o)) {
var len = o.length;
result = new Array(len);
for (var i = 0; i < len; ++i) {
result[i] = traverseClone(o[i], visitor);
visitor(result, i);
}
} else if (_.isPlainObject(o)) {
result = {};
for (var key in o) {
if (o.hasOwnProperty(key)) {
result[key] = traverseClone(o[key], visitor);
visitor(result, key);
}
}
} else {
result = o;
}
return result;
}
/** Visitor function factory for handling thunks and promises in awaited object graphs. */
function trackAndReplaceWithResolvedValue(tracking: Promise<any>[]) {
// Return a visitor function closed over the specified tracking array.
return (obj, key) => {
// Get the value being visited, and return early if it's falsy.
var val = obj[key];
if (!val) return;
// If the value is a thunk, convert it to an equivalent promise.
if (_.isFunction(val)) val = thunkToPromise(val);
// If the value is a promise, add it to the tracking array, and replace it with its value when resolved.
if (_.isFunction(val.then)) {
tracking.push(val);
val.then(result => { obj[key] = result }, err => {});
}
}
}
/** Convert a thunk to a promise. */
function thunkToPromise(thunk: Function) {
return new Promise((resolve, reject) => {
var callback = (err, val) => (err ? reject(err) : resolve(val));
thunk(callback);
});
}