Skip to content

Instantly share code, notes, and snippets.

@bennadel
Created July 20, 2016 12:13
Using ES6 Generators And Yield To Implement Asynchronous Workflows In JavaScript
function* poem() {
yield( Promise.resolve( "Roses are red" ) );
yield( Promise.resolve( "Violets are blue" ) );
yield( Promise.resolve( "I'm a schizophrenic" ) );
yield( Promise.resolve( "And so am I" ) );
}
// By invoking the generator function, we are given a generator object, which we can
// use to iterate through the yield-delimited portions of the generator function body.
var iterator = poem();
console.log( "== Start of Poem ==" );
// In this version of the code, the generator function is yielding data that is
// asynchronous in nature (Promises). The generator itself is still synchronous; but, we
// now have to be more conscious of the type of data that it is yielding. In this demo,
// since we're dealing with promises, we have to wait until all the promises have
// resolved before we can output the poem.
Promise
.all( [ ...iterator ] ) // Convert iterator to an array or yielded promises.
.then(
function handleResolve( lines ) {
for ( var line of lines ) {
console.log( line );
}
}
)
;
console.log( "== End of Poem ==" );
function* poem() {
yield( "Roses are red" );
yield( "Violets are blue" );
yield( "I'm a schizophrenic" );
yield( "And so am I" );
}
// By invoking the generator function, we are given a generator object, which we can
// use to iterate through the yield-delimited portions of the generator function body.
var iterator = poem();
// Invoke the first two iterations manually.
console.log( "== Start of Poem ==" );
console.log( iterator.next().value );
console.log( iterator.next().value );
// Invoke the next iterations implicitly with a for-loop.
// --
// NOTE: When we iterate with a for-of loop, we don't have to call `.value`. This is
// because the for-of loop is both calling .next() and binding the `.value` property to
// our iteration variable implicitly.
for ( var line of iterator ) {
console.log( line );
}
console.log( "== End of Poem ==" );
// I am a generator. Such steps! Much iteration! So confusion!
function* getUserFriendsGenerator( id ) {
var user = yield( getUser( id ) );
var friends = yield( getFriends( id ) );
return( [ user, friends ] );
}
// I get the user with the given Id. Returns a promise.
function getUser( id ) {
var promise = Promise.resolve({
id: id,
name: "Sarah"
});
return( promise );
}
// I get the friends for the user with the given Id. Returns a promise.
function getFriends( userID ) {
if ( ! userID ) {
throw( new Error( "InvalidArgument" ) );
}
var promise = Promise.resolve([
{
id: 201,
name: "Joanna"
},
{
id: 301,
name: "Tricia"
}
]);
return( promise );
}
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// I am a generator function. Such steps! Much iteration! So confusion!
function* getUserFriendsGenerator( id ) {
var user = yield( getUser( id ) );
var friends = yield( getFriends( id ) );
return( [ user, friends ] );
}
// I get the user and friends for the user with the given ID. Returns a promise.
function getUserFriends( id ) {
// Here, we are taking the generator function and wrapping it in an iteration
// proxy. The iteration proxy is capable of synchronously returning a promise
// while internally iterating over the resultant generator object asynchronously.
var workflowProxy = createPromiseWorkflow( getUserFriendsGenerator );
return( workflowProxy( id ) );
}
// Let's call our method that is running as a generator-proxy internally.
getUserFriends( 4 ).then(
function handleResult( value ) {
console.log( "getUserFriends() -- Result:" );
console.log( JSON.stringify( value, null, 2 ) );
},
function handleReject( reason ) {
console.log( "getUserFriends() -- Error:" );
console.log( reason );
}
);
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// On its own, a Generator Function produces a generator, which is just a function
// that can be executed, in steps, as an iterator; it doesn't have any implicit promise
// functionality. However, if a generator happens to yields promises during iteration,
// we can wrap that generator in a proxy and let the proxy pipe yielded values back
// into the next iteration of the generator. In this manner, the proxy can manage an
// internal promise chain that ultimately manifests as a single promise returned by
// the proxy.
function createPromiseWorkflow( generatorFunction ) {
// Return the proxy that is now lexically-bound to the generator function.
return( iterationProxy );
// I proxy the generator and "reduce" its iteration values down to a single value,
// represented by a promise. Returns a promise.
function iterationProxy() {
// When we call the generator function, the body of the generator is NOT
// executed. Instead, an iterator is returned that can iterate over the
// segments of the generator body, delineated by yield statements.
var iterator = generatorFunction.apply( this, arguments );
// function* () {
// var a = yield( getA() ); // (1)
// var b = yield( getB() ); // (2)
// return( [ a, b ] ); // (3)
// }
// When we initiate the iteration, we need to catch any errors that may occur
// before the first "yield". Such an error will short-circuit the process and
// result in a rejected promise.
try {
// When we call .next() here, we are kicking off the iteration of the
// generator produced by our generator function. The function will start
// executing and run until it hits the first "yield" statement (1), which
// will return, as its result, the value supplied to the "yield" statement.
// The .next() result will look like this:
// --
// {
// done: false,
// value: getA() // Passed to "yield"; may or may not be a Promise.
// }
// --
// We then pipe this result back into the next iteration of the generator.
return( pipeResultBackIntoGenerator( iterator.next() ) );
} catch ( error ) {
return( Promise.reject( error ) );
}
// I take the given iterator result, extract the value, and pipe it back into
// the next iteration. Returns a promise.
// --
// NOTE: This function calls itself recursively, building up a promise-chain
// that represents each generator iteration step.
function pipeResultBackIntoGenerator( iteratorResult ) {
if ( iteratorResult.done ) {
// If the generator is done iterating through its function body, we can
// return one final promise of the value that was returned from the
// generator function (3). The iteratorResult would look like this:
// --
// {
// done: true,
// value: [ a, b ]
// }
// --
// So, our return() statement here really is doing this:
// --
// return( Promise.resolve( [ a, b ] ) ); // (3)
return( Promise.resolve( iteratorResult.value ) );
}
// If the generator is NOT DONE iterating through its function body, we need
// to bridge the gap between the yields. We can do this by turning each step
// into a promise that can build on itself recursively.
var intermediaryPromise = Promise
// Normalize the value returned by the iterator in order to ensure that
// its a promise (so that we know it is "thenable").
.resolve( iteratorResult.value )
.then(
function handleResolve( value ) {
// Once the promise has returned with a value, we need to
// pipe that value back into the generator function, which is
// currently paused on a "yield" statement. When we call
// .next( value ) here, we are replacing the currently-paused
// "yield" with the given "value", and resuming the iteration.
// Essentially, this pre-yielded statement:
// --
// var a = yield( getA() ); // (1)
// --
// ... becomes this after we call .next( value ):
// --
// var a = value; // (1)
// --
// At this point, the generator function continues its execution
// until the next yield; or until it hits the a return (implicit
// or explicit).
return( pipeResultBackIntoGenerator( iterator.next( value ) ) );
// CAUTION: If iterator.next() throws an error that is not
// handled by the generator, it will cause an exception inside
// this resolution handler, which will cause the promise to be
// rejected.
},
function handleReject( reason ) {
// If the promise value from the previous step results in a
// rejection, we need to pipe that rejection back into the
// generator where the generator may or may not be able to handle
// it gracefully. When we call iterator.throw(), we resume the
// generator function with an error. If the generator function
// doesn't catch this error, it will bubble up right here and
// cause an error inside the of handleReject() function (which
// will lead to a rejected promise). However, if the generator
// function catches the error and returns a value, that value
// will be wrapped in an iterator result and piped back into the
// generator.
return( pipeResultBackIntoGenerator( iterator.throw( reason ) ) );
}
)
;
return( intermediaryPromise );
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment