Jazcash Jazcash - 4 months ago 15
Node.js Question

Concatenating API responses in the correct sequence asynchronously

So I'm building a simple wrapper around an API to fetch all results of a particular entity. The API method can only return up to 500 results at a time, but it's possible to retrieve all results using the

skip
parameter, which can be used to specify what index to start retrieving results from. The API also has a method which returns the number of results there are that exist in total.

I've spent some time battling using the
request
package, trying to come up with a way to concatenate all the results in order, then execute a callback which passes all the results through.

This is my code currently:

Donedone.prototype.getAllActiveIssues = function(callback){
var url = this.url;
request(url + `/issues/all_active.json?take=500`, function (error, response, body) {
if (!error && response.statusCode == 200) {
var data = JSON.parse(body);
var totalIssues = data.total_issues;
var issues = [];
for (let i=0; i < totalIssues; i+=500){
request(url + `/issues/all_active.json?skip=${i}&take=500`, function (error, response, body){
if (!error && response.statusCode == 200) {
console.log(JSON.parse(body).issues.length);
issues.concat(JSON.parse(body).issues);
console.log(issues); // returns [] on all occasions
//callback(issues);
} else{
console.log("AGHR");
}
});
}
} else {
console.log("ERROR IN GET ALL ACTIVE ISSUES");
}
});
};


So I'm starting off with an empty array,
issues
. I iterate through a for loop, each time increasing
i
by 500 and passing that as the
skip
param. As you can see, I'm logging the length of how many issues each response contains before concatenating them with the main
issues
variable.

The output, from a total of 869 results is this:

369
[]
500
[]


Why is my issues variable empty when I log it out? There are clearly results to concatenate with it.

A more general question: is this approach the best way to go about what I'm trying to achieve? I figured that even if my code did work, the nature of asynchronicity means it's entirely possible for the results to be concatenated in the wrong order.

Should I just use a synchronous request library?

Answer

Why is my issues variable empty when I log it out? There are clearly results to concatenate with it.

A main problem here is that .concat() returns a new array. It doesn't add items onto the existing array.

You can change this:

issues.concat(JSON.parse(body).issues);

to this:

issues = issues.concat(JSON.parse(body).issues);

to make sure you are retaining the new concatenated array. This is a very common mistake.


You also potentially have sequencing issues in your array because you are running a for loop which is starting a whole bunch of requests at the same time and results may or may not arrive back in the proper order. You will still get the proper total number of issues, but they may not be in the order requested. I don't know if that is a problem for you or not. If that is a problem, we can also suggest a fix for that.

A more general question: is this approach the best way to go about what I'm trying to achieve? I figured that even if my code did work, the nature of asynchronicity means it's entirely possible for the results to be concatenated in the wrong order.

Except for the ordering issue which can also be fixed, this is a reasonable way to do things. We would have to know more about your API to know if this is the most efficient way to use the API to get your results. Usually, you want to avoid making N repeated API calls to the same server and you'd rather make one API call to get all the results.

Should I just use a synchronous request library?

Absolutely not. node.js requires learning how to do asynchronous programming. It is a learning step for most people, but is how you get the best performance from node.js and should be learned and used.


Here's a way to collect all the results in reliable order using promises for synchronization and error propagation (which is hugely useful for async processing in node.js):

// promisify the request() function so it returns a promise
// whose fulfilled value is the request result
function requestP(url) {
    return new Promise(function(resolve, reject) {
        request(url, function(err, response, body) {
            if (err || response.statusCode !== 200) {
                reject({err: err, response: response});
            } else {
                resolve({response: response, body: body});
            }
        });
    });
}

Donedone.prototype.getAllActiveIssues = function() {
    var url = this.url;
    return requestP(url + `/issues/all_active.json?take=500`).then(function(results) {
        var data = JSON.parse(results.body);
        var totalIssues = data.total_issues;
        var promises = [];
        for (let i = 0; i < totalIssues; i+= 500) {
            promises.push(requestP(url + `/issues/all_active.json?skip=${i}&take=500`).then(function(results) {
                return JSON.parse(results.body).issues;                
            }));
        }
        return Promise.all(promises).then(function(results) {
            // results is an array of each chunk (which is itself an array) so we have an array of arrays
            // now concat all results in order
            return Array.prototype.concat.apply([], results);
        })
    });
}

xxx.getAllActiveIssues().then(function(issues) {
    // process issues here
}, function(err) {
    // process error here
})