I need to be able to execute a series of asynchronous events in turn, but the execution of each depends on the result of the last. Is there anyway to achieve this dynamically? Consider the following code as an example of what I am trying to achieve.
$scope.queries = [
{
id: 1,
action: function(){
var deferred = $q.defer();
Service.something($.param(someData)).$promise.then(function(response){
deferred.resolve(response);
}, function(error){
deferred.reject(error);
});
return deferred.promise;
}
},{
id: 2,
action: function(){
var deferred = $q.defer();
Service.something($.param(someData)).$promise.then(function(response){
deferred.resolve(response);
}, function(error){
deferred.reject(error);
});
return deferred.promise;
}
},{
id: 3,
action: function(){
var deferred = $q.defer();
Service.something($.param(someData)).$promise.then(function(response){
deferred.resolve(response);
}, function(error){
deferred.reject(error);
});
return deferred.promise;
}
},
];
$scope.stop = false;
angular.forEach($scope.queries, function(query) {
if ($scope.stop === false) {
query.action().then(function(result){
//Everything is fine so we can continue to the next request
}, function(error) {
//This request produced an error so we need to stop
$scope.stop = true;
//Display error here
});
}
});
The main problem is how do I get the forEach to wait for the result of each action before it continues to the next?
Any help in understand this and finding a solution would be great. The only solution I can think of is manually chaining the three requests using chained .then()'s.
Thank you.
Usually when I want to run things in sequence, I use a for or foreach loop, promises are no different and require no magic, you just have to specify the continuation with .then
var promise = $q.when();
angular.forEach($scope.queries, function(query) {
promise = promise.then(function(){
// no need for scope.stop, a rejection will act like a throw and it'll
// stop executing code
return query.action();
});
});
If you want to know when they're all done, you can do:
promise.then(function(){
alert("All actions done!");
});
Note, your service code has the deferred anti pattern, you can convert code that looks like:
var deferred = $q.defer();
Service.something($.param(someData)).$promise.then(function(response){
deferred.resolve(response);
}, function(error){
deferred.reject(error);
});
return deferred.promise;
To simply do:
return Service.something($.param(someData)).$promise;
Related
I'm using promise but sometimes I can't get JSON from various reason in one case, how can I fire up done even if some of JSON is missing, with this code at the moment I'm having only failed message
$.when(
arrayResults[0] ? $.getJSON("url") : null,
arrayResults[1] ? $.getJSON("url") : null,
arrayResults[2] ? $.getJSON("url") : null
).done(function () { }).fail(function () {
console.log('Failed');
});
You can use deferred.always(cb):
$.when(
arrayResults[0] ? $.getJSON("url") : null,
arrayResults[1] ? $.getJSON("url") : null,
arrayResults[2] ? $.getJSON("url") : null
)
.done(function () { console.log('I will run when the promise was resolved') })
.fail(function () { console.log('I will run when the promise was rejected') })
.always(function() { console.log('I will always fire, regardless of previous results') })
See further information here: https://api.jquery.com/deferred.always/
If you are using jQuery v3+ it is Promises A+ compliant so you can add catch() to the request promise
Whenever you return from within a catch it resolves the prior promise and passes whatever you return to the next then() in the promise chain
function getData(url){
return $.getJSON(url)
.then(data=>data)
.catch(resolveFailure)
}
function resolveFailure(jqXhr) {
// return whatever you want here. I added the status in case that is of interest
// could return `false` or string or whatever
// can also log any issues back to server if needed
return {
error: true,
status: jqXhr.status,
statusText: jqXhr.statusText
};
}
var req = getData('https://api.myjson.com/bins/l9ywp'),
req2 = getData('https://httpbin.org/FAIL'),
req3 = getData('https://api.myjson.com/bins/l9ywp');
// could also replace `$.when with `Promise.all()`
$.when(req, req2, req3).then(function(r1, r2, r3) {
// validate the arguments based on whatever you return in the catch()
console.log('r1', r1);
console.log('r2', r2);// object returned from catch()
console.log('r3', r3);
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
I am a new developer, and am trying to check if a promise has not been resolved after 5 seconds. What is the most readable way to do this without creating a new directive/service/factory? this is what I tried, my main question is, am I setting the interval at the correct time, when the promise variable is instantiated?
$scope.promiseTimeLimit = false;
//if the promise does not return within 5 seconds, show a "service down" warning.
var promise = service.checkValidRailCar($scope.railcar.number, $scope.railcar.initials);
$interval(function () {
$scope.promiseTimeLimit = true
}, 5000)
if ($scope.promiseTimeLimit) {
$scope.message = {
content: [{
title: '',
msg: 'The Service Is Down'
}],
type: 'error'
};
return;
}
promise.then(function (data) {
if (!data.valid) {
$scope.invalidData = true;
$scope.message = {
content: [{
title: '',
msg: 'Invalid: This is not a valid data and can not be created.'
}],
type: 'error'
};
} else {
$scope.invalidData = false;
}
})
Yes, you have set the interval correctly after the promise is instantiated. But, I don't think you want to use $interval here. You may want to use $timeout. Callback function in $interval will be called again and again unless you clear the $interval. Callback function in $timeout will be called only once. If you want to check only once if the promise is resolved in 5 seconds, you should use $timeout.
The second thing is about the following code:
if ($scope.promiseTimeLimit) {
$scope.message = {
content: [{
title: '',
msg: 'The Service Is Down'
}],
type: 'error'
};
return;
}
You need to keep this code inside the $timeout function. For better understanding, I am changing the variable name $scope.promiseTimeLimit to $scope.promiseComplete and initialized it as $scope.promiseComplete = false.
$timeout(function () {
if(!$scope.promiseComplete) {
$scope.message = {
content: [{
title: '',
msg: 'The Service Is Down'
}],
type: 'error'
};
}
}, 5000);
Then, in the promise.then function(resolve), you need to set the $scope.promiseComplete to true.
promise.then(function (data) {
$scope.promiseComplete = true;
if (!data.valid) {
...
})
So, what is happening here? $timeout callback function will be called on or after 5 seconds. It will check if $scope.promiseComplete is false. If false, generate a warning message. If true, do nothing. In the promise.then, we set the $scope.promiseComplete to true. So, if promise.then function(resolve) sets $scope.promiseComplete to true before the $timeout callback function gets called, that means promise is completed before 5 seconds.
I have created a sample plunker to understand this behavior. You can have a look in HERE.
IMHO, a promise timeout should be implemented as a rejection and, like promisification, should be implmeneted at the lowest possible level.
In fact, an async timeout is effectively a promisification of window.setTimeout() in a race with whatever async service method you want to call.
You could write it like this, for example :
service.checkValidRailCarWithTimeout = function(t, railcarNumber, railcarInitials) {
// range checks
if(t === undefined || railcarNumber === undefined || railcarInitials === undefined) {
return $q.reject(new RangeError('Invalid parameter(s)'));
}
// race - timeout versus service call
return $q(function(resolve, reject) {
var tRef = window.setTimeout(function() {
reject(new Error('The Service Is Down');
}, t);
function kill() {
window.clearTimeout(tRef);
};
service.checkValidRailCar(railcarNumber, railcarInitials).then(resolve, reject).then(kill, kill);
});
};
And call as follows :
var promise = service.checkValidRailCarWithTimeout(5000, $scope.railcar.number, $scope.railcar.initials).then(function(data) {
$scope.invalidData = !data.valid;
if (!data.valid) {
throw new Error('Invalid: This is not a valid data and can not be created.');
}
}).catch(function(error) {
$scope.message = {
content: [{
title: '',
msg: error.message
}],
type: 'error'
};
});
Notes:
The need for $scope.promiseTimeLimit disappears
kill() is not strictly necessary but keeps things clean by clearing latent timouts that lost their race.
The $scope.message = ... expression will display whichever message is delivered by error
I'm newbie to AngularJs, and I'm in the process of writing my first unit test; to test the service I wrote a test that simply returns a single Json object. However, everytime I run the test I get the error stated in the title. I don't know what exactly is causing this! I tried reading on $apply and $digest and not sure if that's needed in my case, and if yes how; a simple plunker demo would be appreciated.
here is my code
service
var allBookss = [];
var filteredBooks = [];
/*Here we define our book model and REST api.*/
var Report = $resource('api/books/:id', {
id: '#id'
}, {
query: {
method: 'GET',
isArray: false
}
});
/*Retrive the requested book from the internal book list.*/
var getBook = function(bookId) {
var deferred = $q.defer();
if (bookId === undefined) {
deferred.reject('Error');
} else {
var books= $filter('filter')(allBooks, function(book) {
return (book.id == bookId);
});
if (books.length > 0) {
deferred.resolve(books[0]);//returns a single book object
} else {
deferred.reject('Error');
};
};
return deferred.promise;
};
test
describe('unit:bookService', function(){
beforeEach(module('myApp'));
var service, $httpBackend;
beforeEach(inject(function (_bookService_, _$httpBackend_) {
service = _bookService_;
$httpBackend = _$httpBackend_;
$httpBackend.when('GET', "/api/books/1").respond(200, {
"book": {
"id": "1",
"author": "James Spencer",
"edition": "2",
.....
}
});
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should return metadata for single report', function() {
service.getBook('1').then(function(response) {
expect(response.length).toEqual(1);
});
$httpBackend.flush();// error is in this line
});
});
error
Error: No pending request to flush !
at c:/myapp/bower_components/angular-mocks/angular-mocks.js:1439
at c:/myapptest/tests/bookTest.js:34
libs version
AngularJS v1.2.21
AngularJS-mock v1.2.21
I don't see where you're actually issuing a Report.query(). The getBook function just returns an unresolved promise that will never be resolved because nothing in the function is async.
You need to call Report.query via the book function with the promise resolved in the .then() (in the book function). After that, flush the http backend in the service.getBook().then() and do the expect.
Let's say my current route is /books and I make an $http call to get all of the books we want to show a user. Normally, the call would resolve quickly and the books would be ng-repeated into the DOM. When we have an error, though (such as a timeout or there are no books returned), we update a common, global view that will overlay the content view and display a message like, "There are no books available." The common view is handled via a service with methods like CommonView.showLoading(), CommonView.showError("There are no books available."), and CommonView.hide(), etc.
Recently, I discovered that if the $http is not resolved quickly, the user may leave and go to another route (maybe /dinosaurs). Eventually, when the $http ends up resolving or being rejected, the promise call to display that common, global view will happen, resulting in an error view being displayed when there shouldn't be one, and the error will make no sense to the user (ie, user is at /dinosaurs and the error screen pops up with "There are no books available.").
I've seen that you can cancel an $http with a timeout promise, but this still seems like it could lead to race conditions (maybe you call cancel after processing of the resolve() or reject() has begun). I think it would be messy to have to check that the current route matches the route the $http was initiated from.
It seems like there should be some standard way to destroy $http calls on a route change or from a controller's $destroy method. I'd really like to avoid adding a lot of conditionals all over my gigantic app.
I can't find a great way to stop the processing of my callback if it's already started, but here's the $http wrapper I made to try and stop delayed callbacks from getting called after route changes. It doesn't replicate all of the $http methods, just the ones I needed. I haven't fully tested it, either. I've only verified that it will work in normal conditions (normal bandwidth with standard calls, ie httpWrapper.get(url).success(cb).error(err)). Your mileage may vary.
angular.module('httpWrapper', []).provider('httpWrapper', function() {
this.$get = ['$rootScope','$http','$q', function($rootScope, $http, $q) {
var $httpWrapper = function(config) {
var deferred = $q.defer();
var hasChangedRoute = false;
var canceler = $q.defer();
var http = null;
var evListener = null;
var promise = deferred.promise;
if ((config || {}).timeout && typeof config.timeout === 'Object') {
// timeout promise already exists
canceler.promise = config.timeout;
} else {
angular.extend(config || {}, {
timeout: canceler.promise
});
}
http = $http(config)
.success(function(data, status, headers, config) {
// only call back if we haven't changed routes
if (!hasChangedRoute) {
deferred.resolve({data:data, status:status, headers:headers, config:config});
}
})
.error(function(data, status, headers, config) {
// only call back if we haven't changed routes
if (!hasChangedRoute) {
deferred.reject({data:data, status:status, headers:headers, config:config});
}
});
evListener = $rootScope.$on('$locationChangeStart', function(scope, next, current) {
hasChangedRoute = true;
canceler.resolve('killing http');
evListener(); // should unregister listener
})
promise.success = function(fn) {
promise.then(function(response) {
fn(response.data, response.status, response.headers, config);
});
return promise;
};
promise.error = function(fn) {
promise.then(null, function(response) {
fn(response.data, response.status, response.headers, config);
});
return promise;
}
return promise;
};
angular.forEach(['get', 'delete', 'head', 'jsonp'], function(method) {
$httpWrapper[method] = function(url, config) {
return $httpWrapper(
angular.extend(config || {}, {
method: method,
url: url
})
);
};
});
angular.forEach(['post', 'put'], function(method) {
$httpWrapper[method] = function(url, data, config) {
return $httpWrapper(
angular.extend(config || {}, {
method: method,
url: url,
data: data
})
);
};
});
return $httpWrapper;
}];
});
I am trying to implement the advanced push targeting from cloud code (background job) using parse.com service. I have added the day as a field in the Installation object.
I made it work if I have only one condition, i.e. day equals 1, using following snippet
var pushQuery = new Parse.Query(Parse.Installation);
pushQuery.equalTo("day",1);
Parse.Push.send({
where: pushQuery,
data: {
"content-available" : "1",
alert : "Message day 1!",
sound : "default"
}}, {
success: function() {
// Push was successful
},
error: function(error) {
// Handle error
}}).then(function() {
// Set the job's success status
status.success("Job finished successfully.");
}, function(error) {
// Set the job's error status
status.error("Uh oh, something went wrong.");
});
Reference: Push Notification Java Script guide
My next step is sending notifications to 20 queries (0 <= day < 20) and for each query send message according to day number. Calling function 20 times seems to me ugly, may I anyhow iterate, calling each time in loop Parse.Push.send function?
I solved my problem using Parse.Promise.when(promises)
Promises are a little bit magical, in that they let you chain them without nesting. If a callback for a promise returns a new promise, then the first one will not be resolved until the second one is. This lets you perform multiple actions without incurring the pyramid code you would get with callbacks.
function scheduleWordsForDay(day)
{
var pushQuery = new Parse.Query(Parse.Installation);
pushQuery.equalTo("day",day);
pushQuery.exists("deviceToken");
var promise = new Parse.Promise();
Parse.Push.send({
where: pushQuery,
data: {
alert : "word" + day
}}, { success: function() {
// Push was successful
},
error: function(error) {
}}).then (function(result){
//Marks this promise as fulfilled,
//firing any callbacks waiting on it.
promise.resolve(result);
}, function(error) {
//Marks this promise as fulfilled,
//firing any callbacks waiting on it.
promise.reject(error);
});
return promise;
}
Parse.Cloud.job("scheduleWordNotification", function(request, status)
{
var promiseArray = [];
for (var i = 0; i < 100; i++) {
var promise = scheduleWordsForDay(i);
promiseArray.push(promise);
}
//Returns a new promise that is
//fulfilled when all of the input promises are resolved.
Parse.Promise.when(promiseArray).then(function(result) {
console.log("success promise!!")
status.success("success promise!!");
}, function(error) {
console.error("Promise Error: " + error.message);
status.error(error);
});
});