Last month, we introduced the topic of recursion. As you work with recursion more and more, you will inevitably encounter scenarios where your recursive function returns a promise. This situation is often a little tricky the first few times a developer encounters it, so we thought we’d do a deep – dive article for any geodevs in similar boats. Read on to learn more!
What is a Promise?
Sometimes, a snippet of code will call a method or make an API call that will take a while to complete. If the code for a project required each call to complete and return results before continuing on to the next step, the load times could render an application nearly unusable. To help make applications more friendly, promises work by saying “you can go ahead and do other things; I’ll do this and get it back to you when it’s complete.”
Let’s look at a real world example. Imagine you are in a library preparing to do some research. You have ten books you need to request from the reference desk. In order to request a book, you have to write the book title on a slip of paper and hand it to a librarian, who will then fetch the book and give it to you. This represents calling the method or making a request to a REST endpoint and getting a response back.
Without promises, you would give the librarian one slip of paper, they would get the appropriate book, then you would give them the next slip, and so on. You would have to stay at the desk and couldn’t do anything else until you had all ten books. It isn’t a very efficient use of your time.
With promises, things go a little bit differently. There are ten librarians, and you give each one a different slip of paper. The librarians tell you they will get the books to you, and you go ahead to find a spot and set up your research area. As each librarian finds their book, they bring it to you, and you can begin looking up the information you need. (It is important to note you will not necessarily get the books in the same order you requested them.)
How do Promises Impact Recursion?
When working with a recursive method that returns a promise, the most important thing to remember is that each time the method is called, they create a new promise. All of those promises need to be resolved in order for the original call to be resolved.
Let’s look at a code example. Say you wanted to meet a friend one afternoon downtown and are planning to take a bus. There are a number of different bus routes that run downtown, and you will be doing different things before meeting your friend, so you want to know what three bus stops are closets to your meeting point. You write a method to take a meeting point and set distance and find the closest stops. If there aren’t three bus stops, it will expand the search area and call itself again. That would look something like this:
findClosestBusStop: function(meetingPoint, distance){
// This deferred will tell the code it can continue with other tasks while the closest bus stop is found.
var closestStopFound = new Deferred();
// The query parameters for each of the bus lines will be the same
var query = new Query();
query.where = "Route_Feature='Bus_Stop'";
query.outFields = ["*"];
query.geometry = meetingPoint;
query.distance = distance;
query.units = "feet";
// Check each bus route for the closest stops within the given distance
var busRoute1Queried = new Deferred();
var busRoute2Queried = new Deferred();
var busRoute3Queried = new Deferred();
var busRoute4Queried = new Deferred();
var allBusRoutesChecked = [busRoute1Queried, busRoute2Queried, busRoute3Queried, busRoute4Queried];
// Bonus challenge: These four queries do the same thing. Can you rewrite them more efficiently?
busRoutes1.queryFeatures(query).then(lang.hitch(this, function(features) {
busRoute1Queried.resolve(features);
}));
busRoutes2.queryFeatures(query).then(lang.hitch(this, function(features) {
busRoute2Queried.resolve(features);
}));
busRoutes3.queryFeatures(query).then(lang.hitch(this, function(features) {
busRoute3Queried.resolve(features);
}));
busRoutes4.queryFeatures(query).then(lang.hitch(this, function(features) {
busRoute4Queried.resolve(features);
}));
// Once all of the lines have been queried, combine the list of stops
Promise.all(allBusRoutesChecked).then(lang.hitch(this, function(busStops){
var combinedBusStops = busStops[0].concat(busStops[1]);
combinedBusStops = combinedBusStops.concat(busStops[2]);
// Check to see if there are at least 3 stops. If not, increase the distance and try again.
if(combinedBusStops.length > 3){
// Get the distance from the starting point for each bus stop
for (i=0; i<combinedBusStops.length; i++) {
var distanceFromStartingPoint = geometryEngine.distance(meetingPoint, combinedBusStops[i].geometry, "miles");
distanceFromStartingPoint = distanceFromStartingPoint.toFixed(2);
combinedBusStops[i].distanceFromStartingPoint = distanceFromStartingPoint;
}
// Sort the bus stops by closest-to-farthest distance
combinedBusStops.sort((a, b) => (parseFloat(a.distanceFromStartingPoint) > parseFloat(b.distanceFromStartingPoint)) ? 1 : -1);
// Return the three closest stops
closestStopFound.resolve(combinedBusStops.slice(0,3));
} else {
distance = query.distance + 40;
this.findClosestBusStop(meetingPoint, distance).then(lang.hitch(this, function(combinedBusStops){
// You have to resolve each of the previous promises to pass the results back up the chain
closestStopFound.resolve(combinedBusStops);
}));
}
}));
return closestStopFound;
}
This code snippet was written as if it would be in a custom widget for ArcGIS™ Web AppBuilder, and therefore uses Esri’s ArcGIS™ API for JavaScript 3.x. If you’d like your own custom widget, feel free to contact us.