1

This is my first stab at attempting to put together a node module and I am still trying to wrap my head around how to structure the asynchronous callbacks. This is a case in point. Right now I am trying to use featureService.getCount() and getting nothing in response. Using breakpoints, I know featureService.getUniqueIds() is working.

Since a callback is in there, I am assuming the reason why I am not getting a length back is the callback in getCount has not responded yet. After looking at this for most of the afternoon and not really coming up with a very good solution other than a recursive loop checking for the value to be populated with a timeout, I am asking for advice how to better structure my code to accomplish the task at hand.

I have read a bit about promises. Is this an applicable instance or even a viable solution? I really have no clue how to implement promises, but it makes logical sense in such an instance.

Obviously I am lost here. Thank you for any help you can offer.

var Client = require('node-rest-client').Client;
var client = new Client();

var featureService = function(endpoint){

    var uniqueIds;
    var count;

    // get list of unique id's
    this.getUniqueIds = function(){
        if (!uniqueIds) {
            var options = {
                parameters: {
                    f: 'json',
                    where: "OBJECTID LIKE '%'",
                    returnIdsOnly: 'true'
                }
            };
            client.get(endpoint + '/query', options, function(data, res){
                var dataObject = JSON.parse(data);
                var uniqueIds = dataObject.objectIds;
                return uniqueIds;
            });
        } else {
            return uniqueIds;
        }
    };

    // get feature count
    this.getCount = function(){

        // get uniqueIds
        uniqueIds = this.getUniqueIds();

        // get length of uniqueIds
        count = uniqueIds.length;
    };

    // get list of unique attribute values in a single field for typeahead
    this.getTypeaheadJson = function(field){};

    // find features in a field with a specific value
    this.find = function(field, value){};
};

var endpoint = 'http://services.arcgis.com/SgB3dZDkkUxpEHxu/arcgis/rest/services/aw_accesses_20140712b/FeatureServer/1';
var afs = new featureService(endpoint);
console.log(afs.getCount());

exports.featureService = featureService;

Now, after working it over some more and using request as in the bluebird documentation (I could not get the above module to work), I have this working, but cannot figure out how to get the calculated value to work with, the number of iterations.

var Promise = require("bluebird"),
    request = Promise.promisifyAll(require("request"));

var FeatureService = function(){

    // get count from rest endpoint
    var getCount = function(){
        var optionsCount = {
            url: endpoint + '/query',
            qs: {
                f: 'json',
                where: "OBJECTID LIKE '%'",
                returnCountOnly: 'true'
            }
        };
        return request.getAsync(optionsCount)
            .get(1)
            .then(JSON.parse)
            .get('count');
    };

    // get max record count for each call to rest endpoint
    var getMaxRecordCount = function(){
        var optionsCount = {
            url: endpoint,
            qs: {
                f: 'json'
            }
        };
        return request.getAsync(optionsCount)
            .get(1)
            .then(JSON.parse)
            .get('maxRecordCount');
    };

    // divide the total record count by the number of records returned per query to get the number of query iterations
    this.getQueryIterations = function(){
        getCount().then(function(count){
            getMaxRecordCount().then(function(maxCount){
                return  Math.ceil(count/maxCount);
            });
        });
    };

};

// url to test against
var endpoint = 'http://services.arcgis.com/SgB3dZDkkUxpEHxu/arcgis/rest/services/aw_accesses_20140712b/FeatureServer/1';

// create new feature service object instance
afs = new FeatureService();

// This seems like it should work, but only returns undefined
console.log(afs.getQueryIterations());

// this throws an error telling me "TypeError: Cannot call method 'then' of undefined"
//afs.getQueryIterations().then(function(iterCount){
//    console.log(iterCount);
//});
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
knu2xs
  • 910
  • 2
  • 11
  • 23

1 Answers1

2

Yes, use promises! They're a powerful tool, made for exactly this purpose, and with a decent library they're easy to use. In your case:

var Promise = require('bluebird'); // for example, the Bluebird libary
var Client = Promise.promisifyAll(require('node-rest-client').Client);
var client = new Client();

function FeatureService(endpoint) {

    var uniqueIds;
    var count;

    // get list of unique id's
    this.getUniqueIds = function(){
        if (!uniqueIds) { // by caching the promise itself, you won't do multiple requests
                          // even if the method is called again before the first returns
            uniqueIds = client.getAsync(endpoint + '/query', {
                parameters: {
                    f: 'json',
                    where: "OBJECTID LIKE '%'",
                    returnIdsOnly: 'true'
                }
            })
            .then(JSON.parse)
            .get("objectIds");
        }
        return uniqueIds;
    };

    // get feature count
    this.getCount = function(){
        if (!count)
            count = this.getUniqueIds() // returns a promise now!
                    .get("length");
        return count; // return a promise for the length
    };

    // get list of unique attribute values in a single field for typeahead
    this.getTypeaheadJson = function(field){};

    // find features in a field with a specific value
    this.find = function(field, value){};
};

var endpoint = 'http://services.arcgis.com/SgB3dZDkkUxpEHxu/arcgis/rest/services/aw_accesses_20140712b/FeatureServer/1';
var afs = new FeatureService(endpoint);
afs.getCount().then(function(count) {
    console.log(count);
}); // you will need to use a callback to do something with async results (always!)

exports.FeatureService = FeatureService;

Here, using Bluebird's Promise.promisifyAll, you can just use .getAsync() instead of .get(), and will get a promise for the result.


// divide the total record count by the number of records returned per query to get the number of query iterations
this.getQueryIterations = function(){
    getCount().then(function(count){
        getMaxRecordCount().then(function(maxCount){
            return  Math.ceil(count/maxCount);
        });
    });
};

That's the right idea! Only you always want to return something from .then handlers, so that the promise returned by the .then() call will resolve with that value.

// divide the total record count by the number of records returned per query to get the number of query iterations
this.getQueryIterations = function(){
    return getCount().then(function(count){
//  ^^^^^^ return the promise from the `getQueryIterations` method
        return getMaxRecordCount().then(function(maxCount){
//      ^^^^^^ return the promise for the iteration number
            return  Math.ceil(count/maxCount);
        });
    });
};

Now, you get back a promise for the innermost result, and this will work now:

afs.getQueryIterations().then(function(iterCount){
    console.log(iterCount);
});
Community
  • 1
  • 1
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • I spent most of the evening yesterday and a few hours this morning trying to get this to work for my purposes. I keep getting stuck in figuring out how to finally get out of the promises async loops. I need to be able to call ```FeatureService.getCount()``` and get the count returned. Thankfully I am using Git. I've tried so many different branches I've lost count. Any ideas on how to modify this to get what I am looking for? – knu2xs Aug 12 '14 at 14:57
  • No, it's absolutely impossible to return a number from `getCount`. Functions that use async functions need to be async themselves, and provide callback parameters or return promises. Returning a promise for the number (like my answer does) is the best you can get. Have a look at [Bluebird's collection methods](https://github.com/petkaantonov/bluebird/blob/master/API.md#collections) if you need to use `getCount()` in a loop. – Bergi Aug 12 '14 at 15:11
  • I dove into the Bluebird documentation and completely gutted my code to mimic Bluebird's examples using ```require``` and included the updated code above. The idea of promises, I am slowly starting to wrap my head around, but now I cannot figure out how to get the query iterations from the function, neither directly nor through a promise. Any ideas? – knu2xs Aug 13 '14 at 01:52
  • Now I just also tried to create new promise around the function detailed in the [documentation](https://github.com/petkaantonov/bluebird/blob/master/API.md#new-promisefunctionfunction-resolve-function-reject-resolver---promise), but this did not work either. @Bergi, thank you so much for your help. – knu2xs Aug 13 '14 at 02:04
  • Yeah, see the extension to my answer :-) As the docs say, you hardly ever need to manually create a promise yourself - they are very composable. Whenever you think a type of composition logic is missing, a) search harder b) file a bug - you shouldn't need to implement it yourself. – Bergi Aug 13 '14 at 12:57
  • I need to let this sink in a little to understand exactly what is going on. However, testing your suggestions locally yields exactly what I need to do. This definitely has me on the right track. Now I just need to read up on the topic a little more and just talk my way through the code a few times to fully comprehend what is going on. Thank you so much for your help and patience in walking me through this. I owe you big time. – knu2xs Aug 13 '14 at 18:49
  • Thanks for the extensive reply Bergi... but shouldn't it be `var client = Promise.promisifyAll(new Client())` ? – Jean-François Beauchef Jan 27 '16 at 16:11
  • @Jean-FrançoisBeauchef: Not necessarily, I think you need that only when there are instance methods to promisify. It'll promisify the prototype otherwise. – Bergi Jan 27 '16 at 16:15
  • @Bergi when I check the Client instance the way you wrote it, I don't see getAsync, postAsync, etc... I must be doing something wrong? – Jean-François Beauchef Jan 27 '16 at 16:16
  • 1
    …or they changed their API since this answer was written and confirmed to work the last time. Your usage is fine as well, though usually promisifying the class itself is enough. See also [the docs](http://bluebirdjs.com/docs/api/promisification.html) – Bergi Jan 27 '16 at 16:22