I'm having trouble wrapping my head around promises. I'm using the Google Earth API to do a 'tour' of addresses. A tour is just an animation that lasts about a minute, and when one completes, the next should start.
Here's my function that does a tour:
var tourAddress = function (address) {
return tourService.getLatLong(address).then(function (coords) {
return tourService.getKmlForCoords(coords).then(function (kml) {
_ge.getTourPlayer().setTour(kml);
_ge.getTourPlayer().play();
var counter = 0;
var d = $q.defer();
var waitForTour = function () {
if (counter < _ge.getTourPlayer().getDuration()) {
++counter;
setTimeout(waitForTour, 1000);
} else {
d.resolve();
}
};
waitForTour();
return d.promise;
});
});
}
This seems to work pretty well. It starts the animation and returns a promise that resolves when the animation is complete. Now I have an array of addresses, and I want to do a tour foreach of them:
$scope.addresses.forEach(function (item) {
tourAddress(item.address).then(function(){
$log.log(item.address + " complete");
});
});
When i do this, they all execute at the same time (Google Earth does the animation for the last address) and they all complete at the same time. How do I chain these to fire after the previous one completes?
UPDATE
I used #phtrivier's great help to achieve it:
$scope.addresses.reduce(function (curr,next) {
return curr.then(function(){
return tourAddress(next.address)
});
}, Promise.resolve()).then(function(){
$log.log('all complete');
});
You're right, the requests are done immediately, because calling tourAddress(item.address) does a request and returns a Promise resolved when the request is done.
Since you're calling tourAddress in a loop, many Promises are generated, but they don't depend on each other.
What you want is to call tourAdress, take the returned Promise, wait for it to be resolved, and then call tourAddress again with another address, and so on.
Manually, you would have to write something like :
tourAddress(addresses[0])
.then(function () {
return tourAddress(addresses[1]);
})
.then(function () {
return tourAddress(addresses[2]);
})
... etc...
If you want to do that automatically (you're not the first one : How can I execute array of promises in sequential order?), you could try reducing the list of address to a single Promise that will do the whole chain.
_.reduce(addresses, function (memo, address) {
return memo.then(function (address) {
// This returns a Promise
return tourAddress(address);
});
}, Promise.resolve());
(This is pseudo-code that uses underscore, and bluebird, but it should be adaptable)
It's because you're doing things asynchronously so they'll all run at the same time. This answer should help you rewrite your loop so each address is run after the next one.
Asynchronous Loop of jQuery Deferreds (promises)
Related
I need to call three WS services before calling a local function depending on whether some variables are defined or not, but the function is getting called before the services get any response, because it could take some time. I've even tried with $timeout, but it does not work
$scope.$on('search', function (event, data) {
self.searchDto= data;
if (self.searchDto.userCode) {
self.searchByUserCode(self.searchDto.userCode).then(function (data) {
self.userCode= data.find(function (item) {
return item.mstId === self.searchDto.userCode;
});
});
}
if (self.searchDto.companyCode) {
self.serachByCompanyCode(self.searchDto.companyCode).then(function (data) {
self.companyCode= data.find(function (item) {
return item.mstId === self.searchDto.companyCode;
});
});
}
if (self.searchDto.jobCode) {
self.searchByJobCode(self.searchDto.jobCode).then(function (data) {
self.jobCode= data.find(function (item) {
return item.mstId === self.searchDto.jobCode;
});
});
}
//I tried with this timeout but it didnt work
$timeout(function () {
self.searchPeople();
}, 1000);
});
Does anyone have idea how the searchPeople method can be called after the WS responses?
Use promises and $q.all()
var promises = [];
promises.push(self.searchByUserCode(self.searchDto.userCode).then(function (data) {
self.userCode= data.find(function (item) {
return item.mstId === self.searchDto.userCode;
});
}));
.then() returns a promise. Do that for the 3 service calls and then wait for their completion
$q.all(promises).then(function(){
self.searchPeople();
})
I see that you might not call all of your services. $q.all() will wait for the promise you put in the array. Keep in mind it will also execute your call if none of your services has been executed, if you need at least one to be called, you might want to add a check for promises.length > 0 before $q.all().
That way, if you only call one of your services, the promises array will have one element and upon its completion, will call your local function.
Setting timeout is not a correct approaching here. One solution can be: you should put 3 WS nested and put the function call inside the last WS callback.
It also depends on how much arguments that your searchPeople need. If it only work with fully 3 arguments from WS calls, another solution is putting the function call in all 3 WS callback, and inside function searchPeople, you should add a condition statement to check if we have fully 3 argument before do searching
I have recently started using promises so I can, for example, wait until I update a database with x number of records before executing the next function. I find myself counting loop iterations in order to resolve the promise at the right time. For example:
var updateAccounts = function(accounts) {
var promise = new Promise(function(resolve, reject) {
var counter = 0;
accounts.forEach(function(account) {
db.collection('accounts').update({
name: account.name
}, {
$set: {
balance: account.balance
}
});
counter++
if (counter == accounts.length) {
resolve('accounts updated');
}
});
}
});
return promise;
}
Is there a way to wait until a loop is finished without counting the iterations? It just feels sort of hacky, but I'm not sure if it's really an issue or not.
According to MDN:
The Promise object represents the eventual completion (or failure) of an asynchronous operation, and its resulting value.
Basically this means that a Promise should track the outcome of a single async operation.
However, in your example, you've executed multiple async operations (i.e., db.collection('accounts').update(...)) inside one Promise. You've had to resort to an interation count to track completeness rather than relying on the promise to do that.
To resolve this issue (pun intended :D), each of your async requests should have their own Promise.
Since you have multiple updates and therefore multiple promises, you can use a Promise.all to capture when all the promises have completed successfully.
Here's a quick adaptation of your code example:
var updateAccounts = function(accounts) {
var promises = [];
accounts.forEach(function(account) {
var promise = new Promise(function(resolve, reject) {
db.collection('accounts').update({
name: account.name
}, {
$set: {
balance: account.balance
}
});
});
promises.push(promise);
});
Promise.all(promises).then(function(arrPromises) {
console.log("All promises resolved.");
}).catch(function(failedPromise) {
console.log("Something failed.");
});
}
The only thing missing from my example is invoking resolve after db.collection('accounts').update(...) executes successfully. After the update completes successfully you must invoke resolve, or if the update fails you must invoke reject. This is what tells the promise it has finished.
My code example won't work without that, but it's not clear from your original code example where one should specify a callback for db.collection('accounts').update(...) -- so I've omitted it.
You could try after the foreach?
var updateAccounts = function(accounts) {
var promise = new Promise(function(resolve, reject) {
...
accounts.forEach(function(account) {
...
});
<---Here<--
}
});
return promise;
}
It seems odd to be checking for completion of the foreach inside the loop. Maybe you want to make sure the accounts array has some elements? You could check it for zero or non-zero length outside the loop too.
I want to take a screenshot of a full webpage by capturing tiles of the viewport size. It's almost done, but I'm very new to promises and I'm looking for the correct way to do.
Here is my code. The problem is the call to client.execute(...).then(...) does not wait for itself between loop iterations. And the final 'end' neither waits for the previous 'then', that's why it's commented out.
...
var client = webdriverio.remote(options);
...
client
...
.then(function() {
var yTile = 0;
var heightCaptured = 0;
while(heightCaptured < documentSize.height) {
var tileFile = 'screenshot-' + yTile + '.png';
client
.execute(function(heightCaptured) {
window.scrollTo(0, heightCaptured);
}, heightCaptured)
.then(function() {
console.log('captured: ' + tileFile);
client.saveScreenshot('./' + tileFile);
return client;
});
heightCaptured += viewportSize.height;
yTile++;
}
})
//.client.end()
;
What is the correct way to use promises in this case?
Thanks.
You can't use a while look to chain an indeterminate number of async operations because the while loop will run to completion immediately, but you need the loop decisions to be made after each async execution.
Instead, you can create an internal function next() that returns a promise and call it repeatedly, chaining each to the previous until done and deciding within the loop whether to chain another call to next() by returning it inside a prior .then() handler or you can end the chain by just returning a regular value (not a promise).
...
var client = webdriverio.remote(options);
...
client
...
.then(function () {
var yTile = 0;
var heightCaptured = 0;
function next() {
if (heightCaptured < documentSize.height) {
var tileFile = 'screenshot-' + yTile + '.png';
// return promise to chain it automatically to prior promise
return client.execute(function (heightCaptured) {
window.scrollTo(0, heightCaptured);
}, heightCaptured).then(function () {
console.log('captured: ' + tileFile);
// increment state variables
heightCaptured += viewportSize.height;
yTile++;
// return this promise to so it is also chained properly
// when this is done, call next again in the .then() handler
return client.saveScreenshot('./' + tileFile).then(next);
});
} else {
// Done now, end the promise chain by returning a final value
// Might also consider returning yTile so the caller knows
// how many screen shots were saved
return client;
}
}
// start the loop
return next();
}).then(function () {
// done here
}, function (err) {
// error here
});
As a reference, if you are inside a .then() handler and you return a promise from the .then() handler, then that promise gets chained onto the previous promise. If you return a value instead, then the promise chain ends there and the value is returned as the final resolved value of the whole chain.
So, in this example, since next() returns a promise, you can repeatedly call return next(); from within the .then() handler and that will chain all your screenshots together into one sequential chain until you finally just return a value, not a promise and that will end the chain.
I understand using promises in simple scenarios but currently really confused on how to implement something when using a for loop and some updates to local sqlite database.
Code is as follows
surveyDataLayer.getSurveysToUpload().then(function(surveys) {
var q = $q.defer();
for (var item in surveys) {
var survey = surveys[item];
// created as a closure so i can pass in the current item due to async process
(function(survey) {
ajaxserviceAPI.postSurvey(survey).then(function(response) {
//from response update local database
surveyDataLayer.setLocalSurveyServerId(survey, response.result).then(function() {
q.resolve; // resolve promise - tried only doing this when last record also
})
});
})(survey) //pass in current survey used to pass in item into closure
}
return q.promise;
}).then(function() {
alert('Done'); // This never gets run
});
Any help or assistance would be appreciated. I'm probably struggling on how best to do async calls within loop which does another async call to update and then continue once completed.
at least promises have got me out of callback hell.
Cheers
This answer will get you laid at JS conferences (no guarantees though)
surveyDataLayer.getSurveysToUpload().then(function(surveys) {
return Promise.all(Object.keys(surveys).map(function(key) {
var survey = surveys[key];
return ajaxserviceAPI.postSurvey(survey).then(function(response){
return surveyDataLayer.setLocalSurveyServerId(survey, response.result);
});
}));
}).then(function() {
alert('Done');
});
This should work (explanations in comments):
surveyDataLayer.getSurveysToUpload().then(function(surveys) {
// array to store promises
var promises = [];
for (var item in surveys) {
var survey = surveys[item];
// created as a closure so i can pass in the current item due to async process
(function(survey) {
var promise = ajaxserviceAPI.postSurvey(survey).then(function(response){
//returning this promise (I hope it's a promise) will replace the promise created by *then*
return surveyDataLayer.setLocalSurveyServerId(survey, response.result);
});
promises.push(promise);
})(survey); //pass in current survey used to pass in item into closure
}
// wait for all promises to resolve. If one fails nothing resolves.
return $q.all(promises);
}).then(function() {
alert('Done');
});
Awesome tutorial: http://ponyfoo.com/articles/es6-promises-in-depth
You basically want to wait for all of them to finish before resolving getSurveysToUpload, yes? In that case, you can return $q.all() in your getSurveysToUpload().then()
For example (not guaranteed working code, but you should get an idea):
surveyDataLayer.getSurveysToUpload().then(function(surveys) {
var promises = [];
// This type of loop will not work in older IEs, if that's of any consideration to you
for (var item in surveys) {
var survey = surveys[item];
promises.push(ajaxserviceAPI.postSurvey(survey));
}
var allPromise = $q.all(promises)
.then(function(responses) {
// Again, we want to wait for the completion of all setLocalSurveyServerId calls
var promises = [];
for (var index = 0; index < responses.length; index++) {
var response = responses[index];
promises.push(surveyDataLayer.setLocalSurveyServerId(survey, response.result));
}
return $q.all(promises);
});
return allPromise;
}).then(function() {
alert('Done'); // This never gets run
});
I use mbostock/queue for queuing few async operation. It is more to rate limit (UI generate few events, where the backend can process it slowly), and also to make sure they are processed sequentially. I use it like
function request(d, cb) {
//some async oper
add.then(function(){
cb(null, "finished ")
})
}
var addQ = queue(1);
addQ.defer(request) //called by few req at higher rates generated by UI
I already uses angular.js $q for async operation. So, do I have to use mbostock/queue, or can I build a queue out of $q (which is in spirit https://github.com/kriskowal/q)
Thanks.
Basic $q Chain Example
Yes you can build a chained queue using Angular's $q! Here is an example that shows you how you could use recursion to create a queue of any length. Each post happens in succession (one after another). The second post will not start until the first post has finished.
This can be helpful when writing to databases. If the database does not have it's own queue on the backend, and you make multiple writes at the same time, you may find that not all of your data is saved!
I have added a Plunkr example to demonstrate this code in action.
$scope.setData = function (data) {
// This array will hold the n-length queue
var promiseStack = [];
// Create a new promise (don't fire it yet)
function newPromise (key, data) {
return function () {
var deferred = $q.defer();
var postData = {};
postData[key] = data;
// Post the the data ($http returns a promise)
$http.post($scope.postPath, postData)
.then(function (response) {
// When the $http promise resolves, we also
// resolve the queued promise that contains it
deferred.resolve(response);
}, function (reason) {
deferred.reject(reason);
});
return deferred.promise;
};
}
// Loop through data creating our queue of promises
for (var key in data) {
promiseStack.push(newPromise(key, data[key]));
}
// Fire the first promise in the queue
var fire = function () {
// If the queue has remaining items...
return promiseStack.length &&
// Remove the first promise from the array
// and execute it
promiseStack.shift()()
// When that promise resolves, fire the next
// promise in our queue
.then(function () {
return fire();
});
};
// Begin the queue
return fire();
};
You can use a simple function to begin your queue. For the sake of this demonstration, I am passing an object full of keys to a function that will split these keys into individual posts, then POST them to Henry's HTTP Post Dumping Server. (Thanks Henry!)
$scope.beginQueue = function () {
$scope.setData({
a: 0,
b: 1,
/* ... all the other letters of the alphabet ... */
y: 24,
z: 25
}).then(function () {
console.log('Everything was saved!');
}).catch(function (reason) {
console.warn(reason);
});
};
Here is a link to the Plunkr example if you would like to try out this code.
The short answer is no, you don't need an extra library. Promise.then() is sufficiently "atomic". The long answer is: it's worth making a queue() function to keep code DRY. Bluebird-promises seems pretty complete, but here's something based on AngularJS's $q.
If I was making .queue() I'd want it to handle errors as well.
Here's an angular service factory, and some use cases:
/**
* Simple promise factory
*/
angular.module('app').factory('P', function($q) {
var P = $q;
// Make a promise
P.then = function(obj) {
return $q.when(obj);
};
// Take a promise. Queue 'action'. On 'action' faulure, run 'error' and continue.
P.queue = function(promise, action, error) {
return promise.then(action).catch(error);
};
// Oook! Monkey patch .queue() onto a $q promise.
P.startQueue = function(obj) {
var promise = $q.when(obj);
promise.queue = function(action, error) {
return promise.then(action).catch(error);
};
return promise;
};
return P;
});
How to use it:
.run(function($state, YouReallyNeedJustQorP, $q, P) {
// Use a $q promise. Queue actions with P
// Make a regular old promise
var myPromise = $q.when('plain old promise');
// use P to queue an action on myPromise
P.queue(myPromise, function() { return console.log('myPromise: do something clever'); });
// use P to queue an action
P.queue(myPromise, function() {
throw console.log('myPromise: do something dangerous');
}, function() {
return console.log('myPromise: risks must be taken!');
});
// use P to queue an action
P.queue(myPromise, function() { return console.log('myPromise: never quit'); });
// Same thing, but make a special promise with P
var myQueue = P.startQueue(myPromise);
// use P to queue an action
myQueue.queue(function() { return console.log('myQueue: do something clever'); });
// use P to queue an action
myQueue.queue(function() {
throw console.log('myQueue: do something hard');
}, function() {
return console.log('myQueue: hard is interesting!');
});
// use P to queue an action
myQueue.queue(function() { return console.log('myQueue: no easy days'); });
Chained Promises
Angular's $q implementation allows you to chain promises, and then handle resolves of those promises according to your own logic. The methods are a bit different than mbostock/queue, but the intent is the same. Create a function that determines how your defered will be resolved (creating a promise), then make these available to a higher level controller/service for specific resolution handling.
Angular uses $q.defer() to return promise objects, which can then be called in the order you wish inside your application logic. (or even skipped, mutated, intercepted, etc...).
I'll throw down some code, but I found this 7 minute video at egghead.io to be the best short demo: https://egghead.io/lessons/angularjs-chained-promises, and it will do a FAR better job of explaining. Thomas (the presenter) builds a very small flight dashboard app that queues up weather and flight data, and processes that queue when a user queries their itenerary. ThomasBurleson/angularjs-FlightDashboard
I will be setting up a smaller demonstration on codepen, using the situation of 'eating at a restaurant' to demonstrate this concept: http://codepen.io/LongLiveCHIEF/pen/uLyHx
Code examples here:
https://gist.github.com/LongLiveCHIEF/4c5432d1c2fb2fdf937d