Using promises instead of callbacks

Hi guys, I’m currently working on the Twitch.tv viewer. I can retrieve the data from the API, but I feel like my method is very convoluted. What I’m doing at the minute is making AJAX requests in a loop and appending the usernames to the URL. The success function then pushes the response into a cache object. Then after that I check that the length of the list of responses matches the length of the list of usernames. If so I call my callback function. What I’m wondering is, is there a better way to do this, using promises? See my code below:

code
/**
 * This function will get all of the data from Twitch
 * @param dataCache {DataCache} instance of the dataCache
 * @param dataCache.getRequestDetails {function} get the details to be used for the request
 * @param dataCache.addResponse {function} push a response to the response array
 * @param dataCache.usernames {Array} the array of usernames
 * @param dataCache.getResponseData {function} get all of the responses
 * @param successCb {function} the callback to be called when the all of the data is retrieved
 */
function getFromTwitch(dataCache, successCb){
    var requestData = dataCache.getRequestDetails();
    //iterate through the usernames
    requestData['usernames'].forEach(function (elem) {
        //make a request to the URL with the username appended, using the request data
        getOneFromTwitch(requestData.url + elem, requestData.requestData, function (response) {
            dataCache.addResponse(response); // add the response to the response array
            if(requestData.usernames.length === dataCache.getResponseData().length){
                successCb();
            }
        })
    })

}
/**
 * Make a single AJAX request
 * @param url {string] the url to request
 * @param data {object} the parameters for the HTTP GET request
 * @param successCb {function} callback for successful data retrieval
 */
function getOneFromTwitch(url, data, successCb){
    $.ajax({
        url:  url,
        dataType: 'jsonp',
        data: data,
        success: successCb
    })
}
/**
 * The dataCache object, which will contain a set of constants required for the request, and the responses
 * @return {{getRequestDetails: getRequestDetails, addResponse: addResponse}}
 */
function DataCache(){
    var commonUsernames = ["ESL_SC2", "OgamingSC2", "cretetion", "freecodecamp", "storbeck", "habathcx", "RobotCaleb", "noobs2ninjas"];
    var baseUrl = 'https://api.twitch.tv/kraken/streams/';

    var requestData = {
        'format': 'json',
        'client_id': 'API_KEY_HERE'
    };

    var responseData = [];

    /**
     * Add a response to the array of responses
     * @param response {object}
     */
    function addResponse(response){
        responseData.push(response);
    }

    /**
     * Get the details required for the request
     * @return {{usernames: string[], url: string, requestData: {format: string, client_id: string}}}
     */
    function getRequestDetails(){
        return {
            'usernames': commonUsernames,
            'url': baseUrl,
            "requestData": requestData
        }
    }
    function getAllResponses(){
        return responseData;
    }
    var publicApi = {
        'getRequestDetails': getRequestDetails,
        'addResponse': addResponse,
        'getResponseData': getAllResponses
    };
    return publicApi;
}
$(document).ready(function () {
    var dataCache = new DataCache();
    getFromTwitch(dataCache, function () {
        console.log(dataCache.getResponseData())
    });

});

Thanks for any help you can give me!

2 Likes

Oooh … Code I can read, I like it!

I would suggest looking into Promise.all()
It accepts an iterable of promises (eg, an Array) and returns a promise that resolves when all promises in the iterable resolve, or rejects as soon as the first rejects.

So you could take your array of usernames and (forgive me for this mess, I’m on my phone) …

Promise.all( user_names.map( ajax_function( username) ))
.then( handle_after_load );

Just make sure that your ajax_function returns the xhr object or jquery’s Ajax object, etc. So that the array is populated with promises.

That should work … The then handler will be passed an array of results that will be populated in the same order as your original array of usernames.

EDIT: Unrelated tip … To save some typing, ES6 now supports creating an object from just variable names. So your public API I can be returned as

return {
   getRequestDetails,
   addRequest,
   getResponseData
};

The object returned will use the same property names as your variables. Just saves a bit of time.

3 Likes

Thanks for that, I’ll try it now. And also for the ES6 shortand for exposing public methods.

Do I need to import a separate library for these promises, or use ES6. Or are they part of the ES5 specification? My IDE is currently set up to target ES5, and I’m getting unresolved variable or type warnings for Promise.

EDIT:

I tried to refactor my code there. I changed my getFromTwitch method to:

Promise.all(requestData.usernames.map(function (username) {
        getOneFromTwitch(requestData.url + username, requestData.data, function(res){
            dataCache.addResponse(res);
        })
    })).then(successCb, function (err) {
        console.log(err);
    });

I don’t get any errors, but the success callback is being returned too early. I have a log statement that gets called as a result of the successCb being called inside Promise.then(). This returns an empty array. However, for testing purposes, I used setTimeout to log the contents of the array 10 seconds after the page loads and the data is present. So I’m assuming that the callback is getting called before the promises are resolved. What am I doing wrong?

According to MDN, they’re part of ES6, but they look like they have pretty good browser support. All current browsers show support for it, according to caniuse.com (At least they all show support for Promise, I can’t tell if they all support Promise.all(), but I would assume so as there are no gotchas listed on the page).

Oh,just saw your edit.

The map function you are passing to Promise.all needs to return an array of Promises (right now it is returning an array of undefineds - so you need a return statement that returns a promise. Make sure your getOneFromTwitch function returns the $.ajax object and then return getOneFromTwich inside the map function.

That way Promise.all can keep track of each individual Promise. The then method called when all Promises have resolved will be passed an array of results, which you can use to populate your dataCache. So I would say, don’t execute a callback in getOneFromTwitch . Instead iterate the array of results inside your then handler and populate the dataCache from each item.

This is based on my understanding of how I think it should work based on reading I’ve been doing lately - unfortunately I haven’t yet done this myself!

I look forward to hearing the results!

I’m going off of the MDN docs …

The thing that I am not sure of at the moment is how the resolved $.ajax/promise from getOneFromTwitch will know what data to pass back.

If it doesn’t work with a simple return $.ajax ... I would try

return new Promise( function( resolve, reject ){
$ajax({
       ...
       'success': ( data ){
          resolve( data );
       }
   });
});

As that should pass the data to the resolve handler …

1 Like

Yes! That worked. Ok, I am still using an empty callback inside Promise.then() to let my jQuery logic know that all promises have been resolved, and the responses have been added to the DataCache. The relevant parts of my code are below:


function getFromTwitch(dataCache, successCb){
    var requestData = dataCache.getRequestDetails();
    Promise.all(requestData.usernames.map(function (username) {
         return getOneFromTwitch(requestData.url + username, requestData.requestData)
    })).then(function (arrayOfResponses) {
        arrayOfResponses.forEach(function (elem) {
            dataCache.addResponse(elem);
        });
        successCb(); //trigger a console.log() in document.ready
    });

}

function getOneFromTwitch(url, data){
    return $.ajax({
        url:  url,
        dataType: 'jsonp',
        data: data
    })
}


Nice! Glad that worked … Your thread is great timing. I left off tonight with notes on my project that are directly related to refactoring some caching code, so I’m glad to see this working!

And now I should probably try to sleep … it’s 4:30am here … Too bad I’m wide awake …

1 Like