Created
July 20, 2016 12:13
Using ES6 Generators And Yield To Implement Asynchronous Workflows In JavaScript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ==" ); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ==" ); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 ] ); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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