Reputation: 78
New to Node.js here. I'm looking for the correct way to make N asynchronous API calls from within another function, and combining their results to use further downstream. In my case, N would be reasonably small and blocking for their execution not too bad.
In synchronous execution the implementation for combine() below should work.
If I only needed the results from one API call it would be straightforward to implement the following logic in a callback function supplied to callAPI(). Where I stumble is when I need all the results combined before before executing foo(total, [...args]).
I looked into async.whilst
but wasn't able to get that to work. I'm skeptical if that actually is the correct fit to my needs. I've also looked into Promises
which seems to be the correct lead but it would be nice to get reassurances before crawling into that cavernous rabbit hole. Be it that Promises
is the correct way, which module is the standard to use in Node.js
projects?
var http = require('http');
function callAPI(id) {
var options = {
host: 'example.com',
path: '/q/result/'.concat(id)
}
var req = http.get(options, (res) => {
var body = [];
res.on('data', (chunk) => {
body.push(chunk);
}).on('end', () => {
body = Buffer.concat(body).toString();
return body;
}).on('error', (err) => {
console.error(err);
});
});
}
function combine(inputs) {
var total = 0;
for (i=0; i < inputs.length; i++) {
total += callAPI(inputs[i]['id']);
};
console.log(total);
// call some function, foo(total, [...args])
}
Edit 1:
I attempted to follow samanime's answer below and modify the API call to return a Promise. See:
function callAPI(id) {
return Promise((resolve, reject) => {
var options = {
host: 'example.com',
path: '/q/result/'.concat(id)
}
var req = http.get(options, (res) => {
var body = [];
res.on('data', (chunk) => {
body.push(chunk);
}).on('end', () => {
body = Buffer.concat(body).toString();
resolve(body);
}).on('error', (err) => {
reject(err);
});
});
});
}
function combine(inputs) {
var combined = [];
for (i=0; i < inputs.length; i++) {
total += callAPI(inputs[i]['id']);
.then(result => {
combined.push(result);
});
};
var total = combined.reduce((a, b) => a + b, 0);
console.log(total);
// call some function, foo(total, [...args])
}
This seems to get me halfway there. If I console.log(combined)
inside the then()
block I can see the list building up with results from the API calls. However, I still can't access the complete combined
at the "end" of the for
loop. Can I attach a callback to something to run after the full list has been built? Is there a better way?
Edit 2 (My solution - per Patrick Roberts suggestion)
function callAPI(id) {
return Promise((resolve, reject) => {
var options = {
host: 'example.com',
path: '/q/result/'.concat(id)
}
var req = http.get(options, (res) => {
var body = [];
res.on('data', (chunk) => {
body.push(chunk);
}).on('end', () => {
body = parseInt(Buffer.concat(body));
resolve(body);
}).on('error', (err) => {
reject(err);
});
});
});
}
function combine(inputs) {
var combined = [];
Promise.all(inputs.map(input => callAPI(input.id)))
.then((combined) => {
var total = combined.reduce((a, b) => a + b, 0);
// foo(total, [...args])
});
};
Upvotes: 2
Views: 5150
Reputation: 51866
Your edit is looking a lot better, but try this:
function callAPI(id) {
return Promise((resolve, reject) => {
var options = {
host: 'example.com',
path: '/q/result/' + id
}
http.get(options, (res) => {
var body = [];
res.on('data', (chunk) => {
body.push(chunk);
}).on('end', () => {
body = Buffer.concat(body).toString();
resolve(body);
}).on('error', reject);
});
});
}
function combine(inputs) {
Promise.all(inputs.map(input => callAPI(input.id))).then((combined) => {
// completed array of bodies
console.log(combined);
// foo(combined.length, [...args]);
}).catch((error) => {
console.log(error);
});
}
Upvotes: 0
Reputation: 14194
I would add a counter that keeps track of remaining API calls. Whenever an API call finishes, decrement and if its 0, you're done.
const numCalls = 10;
let remaining = numCalls;
let data = [];
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min)) + min;
}
function ajax() {
// Simulate ajax with a setTimeout for random amount of time.
setTimeout(() => {
// This is the callback when calling http.get
data.push(getRandomInt(0, 10)); // Some data from server
if (--remaining <= 0) {
// Am I the last call? Use data.
console.log(data);
console.log(data.length);
}
}, getRandomInt(1000, 3000));
}
for (let i = 0; i < numCalls; i++) {
ajax();
}
Upvotes: -1
Reputation: 26537
It sounds like you can just chain together a bunch of promises, passing the data along.
Basically something like:
const combined = [];
asyncOne()
.then(result => { combined.push(result); return asyncTwo())
.then(result => { combined.push(result); return asyncThree())
// and so on
As long as each function returns a promise, you'll be all set.
If you want to run them in parallel, use Promise.all()
, which will do the same thing for you:
Promise.all([asyncOne(), asyncTwo(), asyncThree() /* , etc */])
.then(combined => /* combined is an array with the results of each */)
This is by far the preferred pattern for this sort of thing.
Upvotes: 3