0

I have a service that is written in Node and sends the request to get a list of teams for a specific city to another API service.

Request params:

  • city
  • page
  • per_page

Response body:

  • page
  • per_page
  • more
  • teams

per_page by default is set to 1000 but the number of teams is much greater than 1000 and some teams are not returned in the response. I need all the teams though. For some reasons I am not going to specify here, I do not want to change per_page param, but what I want to do is to send the first request and check if more in the response body is true (meaning there are more teams), I extract teams from the response body and add it to the final result, increase page param and send the request with params:

{
  "page": "2",
  "city": "chicago",
  "per_page": 1000, 
 }

I keep doing that until more header is false.

The service is written in Node and I use the request package for sending HTTP requests. Initially, I wanted to use while loop:

function teamsInCity(city, page, result, callback) {
 let more = true;

  while(more) {
  // send the request
  // extract teams into the result array
  // check if there is more
  // send another request
  // keep sending requests and appending teams until more is false
  } 
  // return the final result with all the teams
}

But that didn't work because while loop sends the request and keeps executing the next line of code without waiting for the response. So I solved it with recursion and it works. But I need it to be tail call optimized and I am not sure but I think Node doesn't support that but I might be wrong.

Is there a way to send requests inside of while loop without using async/await and promises?

Below is my working function with recursion:

function teamsInCity(city, page, result, callback) {
  request.get({
    url: 'my_service' + '/teams',
    qs: { 'city' : city, 'page': page, 'per_page': 1000 },
    json: true
  }, function(err, response, body) {
    if (err) { return callback(err); }

    result = result.concat(body.teams);

    if (body.more) {
      page++;
      return teamsInCity(city, page, result, callback);
    }
    return callback(null, result);
  });
}

And here is the route that calls teamsInCity()

router.get('/:city', function(req, res) {
  var data = {teams: []};
  myService.teamsInCity(req.params.city, 1, [], function(err, teams) {
    if (err) {
      logger.error('Error while retrieving teams in ' + req.params.city);
    }
    data.teams = _.sortBy(teams, 'name');
    res.send(data);
  });
});

Thank you!

Yashwardhan Pauranik
  • 5,370
  • 5
  • 42
  • 65
buterfly85
  • 163
  • 3
  • 11
  • 3
    _" without using async/await and promises?"_ any reason why not? – Phil Jul 15 '19 at 05:48
  • 1
    [a blog post](https://medium.com/trappedpanda/loops-asynchronous-functions-and-returning-collective-results-in-node-js-b7566285fb74) – Aritra Chakraborty Jul 15 '19 at 05:50
  • 2
    "But I need it to be tail call optimized" Your function is already tail call optimised – AvcS Jul 15 '19 at 06:05
  • @AvcS could you please elaborate more on your comment? According to [this post](https://stackoverflow.com/questions/23260390/node-js-tail-call-optimization-possible-or-not) V8 doesn't implement TCO. – buterfly85 Jul 15 '19 at 06:49

1 Answers1

2

You could use the async library, and the whilst function. I don't think this will make your Stack Overflow ;-)

For example:

const async = require("async");

function teamsInCity(city, callback) {
    let result = [];
    let page = 0;
    let more = true;
    async.whilst(
        () => more,
        (callback2) => {
            request.get({
                url: 'my_service' + '/teams',
                qs: { 'city' : city, 'page': page++, 'per_page': 1000 },
                json: true
            }, (err, response, body) => {
                if (err) { 
                    callback2(err); 
                    console.error(`teamsInCity: Error occurred: ${err.message}`);
                } else {
                    console.log(`teamsInCity: Page#: ${page}, Team count: ${body.teams.length}, More pages: ${body.more}`);
                    more = body.more;
                    result = result.concat(body.teams);
                    callback2(null, result);
                }
            })
        },
        (err, result) => {
            if (err) console.error("Final callback error: ", err.message);
            if (!err) console.log("Final callback result: ", result);
            callback(err, result);
        }
    );
}
Terry Lennox
  • 29,471
  • 5
  • 28
  • 40
  • thanks for your solution. It solves TCO problem but doesn't do what I need it to do. If I change `per_page` to 10, it will fetch the first page with 10 teams and return to the caller. In other words, it doesn't iterate. It will not send the requests to fetch the remaining 990. The service I send the request to makes one request to db for all the teams. It can be over 1000 teams. So I want to make multiple requests to the service with `per_page` set to 1000 until I fetch all the teams. – buterfly85 Jul 16 '19 at 06:07
  • Hey @buterfly85, thanks for testing the answer!! I wonder why it's not continuing with the calls.. normally that means the body.more property is falsey.. Could you add the log calls I've updated the answer with? I think this should be relatively simple to fix! – Terry Lennox Jul 16 '19 at 07:13
  • the log is `Final callback result: undefined`. Also I updated my post by adding the router that uses this function as a handler. I think for second argument in `async.whilst` it should be `() => {}` instead of `callback => {}`. I tried that and it returns the teams but it makes only one request though. It doesn't continue to iterate and send subsequent requests. – buterfly85 Jul 16 '19 at 17:13
  • Ah, yes well that is confusing, but actually the (callback) variable in the handler function is of a different scope to the root one. I would just give it a different name maybe. I'm still confused why the body.more variable isn't being assigned to the more variable! – Terry Lennox Jul 16 '19 at 17:33
  • even if `body.more` is not assigned to `more`, `more` would stay equal to `true` and then the next request would be sent anyway. But it looks like it isn't. – buterfly85 Jul 16 '19 at 18:01
  • So somehow false or more likely undefined is being assigned to more. If you could console.log the body variable it might help! – Terry Lennox Jul 16 '19 at 18:13
  • I logged `body.more` and `more` and the output for both was `true`. – buterfly85 Jul 16 '19 at 18:58
  • That's very strange then! I'm a bit stumped tbh! – Terry Lennox Jul 16 '19 at 19:19
  • I tried to google why it doesn't iterate but couldn't find anything useful – buterfly85 Jul 16 '19 at 20:18
  • And I've run this against a mock web service, everything works as expected. Something weird is going on I guess... – Terry Lennox Jul 16 '19 at 20:24
  • Maybe it has something to do with my environment or node version I use – buterfly85 Jul 16 '19 at 21:08
  • the same here on Mac – buterfly85 Jul 16 '19 at 21:36
  • I might try on an Os X VM, see if I can replicate what you're seeing – Terry Lennox Jul 16 '19 at 21:38
  • I also updated my post by adding the route that uses this function. – buterfly85 Jul 16 '19 at 21:45
  • 1
    never mind. For some reason, `async` version that was being installed was 1.5.0. I updated the version and it worked. So thank you a lot!!! – buterfly85 Jul 16 '19 at 21:57