i have written a polling service in AngularJS and want to start the service if my post request is done.But if I call the gui, the poll service is active.
i have try to implement a start function, end function and call the start() function if the post request is done.. but it doesnt work :/
My poll service :
.factory('NotificationPollService',
['$http', '$q', '$interval',
function ($http, $q, $interval) {
var deferred = $q.defer();
var notification = {};
notification.poller = $interval(function(id) {
$http.get('http://localhost:9999/v1/jmeter/id', {cache: false})
.success(function(data, status, headers, config) {
return data;
}, 10000);
});
notification.endPolling = function() {$interval.cancel(this.interval);};
}])
and the controller which i post the request
.controller('HomeController',
['$scope', '$rootScope', 'SendJmeterFile', 'NotificationPollService',
function ($scope, $rootScope, SendJmeterFile , NotificationPollService) {
$scope.upload = function() {
var customArtifacts = "";
var testDataBase = "";
if($scope.jmeterFile.customArtifact == undefined){
customArtifacts = null;
} else {customArtifacts = $scope.jmeterFile.customArtifact.base64}
if($scope.jmeterFile.testDataBase == undefined){
testDataBase = null;
} else {testDataBase = $scope.jmeterFile.testDataBase.base64}
SendJmeterFile.upload($scope.jmeterFile.jmeter.base64, customArtifacts, $scope.jmeterFile.customProperties, $scope.jmeterFile.instanceConfiguration, $scope.jmeterFile.instances, $scope.jmeterFile.providerID, testDataBase)
.then(function(data) {
alert("Daten erfolgreich verschickt!");
console.log(data);
NotificationPollService.poller(data.id)
//.then(function(data) {
/*if(data.status == "SETUP")
if(data.status == "TEST")
if(data.status == "DONE")
if(data.status == "ERROR")
}), function(data) {
})*/
}, function(data) {
alert("Fehler!");
console.log(data);
});
};
}])
One problem is that $interval() is called immediately upon injection into your controller. Your hunch to implement a 'Start' method or something similar was a good one - but you can probably simplify it even more by letting the factory return a function. Then you can just instantiate that function in your controller as many times as you need a Poller.
However, there are more problems. A promise can only be resolved once, and since you execute a HTTP request multiple times, my guess is that you want to be 'notified' of state changes until the state is marked as 'Done'. You're currently putting the responsibility for checking the state with the controller. If all you want is to be notified of "error" and "success" steps however, it is probably much better to let the Poller service be responsible for interpreting the state information that comes back from your service, and simply depend on standard promise behaviour in your controller. I opted to show an example of the latter case:
UPDATE: sample plnkr here: http://plnkr.co/edit/e7vqU82fqYGQuCwufPZN?p=preview
angular.module('MYMODULENAMEHERE')
.factory('NotificationPoller',
['$http', '$q', '$interval',
function ($http, $q, $interval) {
return poller;
function poller(id) {
var _this = this;
var deferred = $q.defer();
var interval = $interval(function() {
$http
// I assumed you actually want to use the value of 'id' in your
// url here, rather than just the word 'id'.
.get('http://localhost:9999/v1/jmeter/' + id, {cache: false})
.then(function(data, status, headers, config) {
// I commented out the following statement. It is meaningless because
// you can't do anything with the return value since it's an anonymous
// function you're returning it from. Instead, you probably meant to
// use the promise to return the data.
// return data;
if(data.status == "SETUP") {
deferred.notify(data);
}
else if(data.status == "TEST") {
deferred.notify(data);
}
else if(data.status == "DONE") {
_this.endPolling(); // Automatically stop polling when status is done
deferred.resolve(data);
}
else { // data.status == "ERROR" (or anything else that's not expected)
_this.endPolling(); // Automatically stop polling on error
deferred.reject(data);
}
}, function(data) {
_this.endPolling();
deferred.reject(data);
});
}, 10000);
this.endPolling = function() {
$interval.cancel(interval);
};
// Make the promise available to calling code
this.promise = deferred.promise;
};
}])
Now your controller can much more easily use your polling service. Here's an example of a stripped-down controller using your polling service, for clarity:
angular.module('MYMODULENAMEHERE')
.controller('HomeController', [
'NotificationPoller',
function(NotificationPoller) {
var some_id = 207810;
var poller = new NotificationPoller(some_id);
poller.promise.then(onSuccess, onError, onNotify);
function onSuccess(data) {
// data.status == "DONE"
};
function onError(data) {
// data.status == "ERROR"
};
function onNotify(data) {
// data.status == "TEST" || data.status == "SETUP"
};
}]);
As you see, the factory has received a little more responsibility this way, but your controller doesn't need to be aware of the details of all the statuses that the backend can send anymore. It just uses standard promises.
You try to call NotificationPollService.poller(data.id) which is Promise, actually, because previously in NotificationPollService you assigned notification.poller like so
notification.poller = $interval(function(id) {
$http.get('http://localhost:9999/v1/jmeter/id', {cache: false})
.success(function(data, status, headers, config) {
return data;
}, 10000);
});
Now your notification.poller is a return value of $interval function.
To make it work you should wrap the function so you could actually pass an id to it.
Related
Given 2 JSON url, how do I make sure the code has finished retrieving the data from a.json, then only start retrieving the data from b.json, then only run init function?
var aUrl = "a.json";
var bUrl = "b.json";
My attempt:
var app = angular.module('calendarApp', []);
app.controller('ctrl', function($scope, $http) {
$http.get(aUrl).success(function(data) { });
$http.get(bUrl).success(function(data) {
init()}
);
var init = function(){}
I faced the same issue in my initial days.
There are many ways of doing it exactly as suggested here.
You need to know below two things before exploring:
1. JavaScript is synchronous
Synchronous Example[Flow in sequence]:
console.log('1')
console.log('2')
console.log('3')
It logs 1 2 3.
Example of making service calls
1. $http.get(aUrl).success(function(data) { console.log('1.any time response returns') });
2. $http.get(bUrl).success(function(data) { console.log('2.mummy returns')};
So as single-threaded javascript will first make a call to your below code with $http.get(aUrl) which hits the url and processes to fetch the data from the background.
$http.get(aUrl).success(function(data) { console.log('1.any time response returns') });
But the key thing to notice here is $http.get(aUrl) requested above doesn't wait until the data is returned in success/error. It moves to the next request $http.get(bUrl) and we just can't predict which response comes earlier.
$http.get(bUrl).success(function(data) { console.log('2.mummy returns') }
Output might be either
1.any time response returns
2.mummy returns
or
2.mummy returns
1.any time response returns
So, to overcome this situation we follow asynchronous operations in various ways.
2. Asynchronous Calls
$http.get(aUrl)
.then(function(response){
console.log('inside the first then response');
console.log(response.data);
//executing the second request after we get the first request
//and returns the **outputResponse** which is captured in the next **then** block
return $http.get(bUrl);
})
.then(function(**outputResponse** ){
console.log('outputResponse generated from to the second bUrl');
//you can call init() here
});
Above code suffices your requirement.
Click for more info using $q in future
Click here to know why to use then instead of success.
Might not be the best or cleanest method but quickly making your code do what you want it to do I got:
var app = angular.module('calendarApp', []);
app.controller('ctrl', function($scope, $http) {
$http.get(aUrl).success(function(data) {
$http.get(bUrl).success(function(data) {
init()
}
});
);
var init = function(){}
You could create a service layer in which define the two methods. Then inject the service into your controller:
//Controller
YourService.getUrl(urlA).then(function(response) {
if(response != null && response.success == true){
// do something
}
YourService.getUrl(urlB).then(function(response) {
if(response != null && response.success == true){
// do something
init()
}
},
function errorCallback(response) {
console.log("Error YourService: getUrlB ---> ");
});
},
function errorCallback(response) {
console.log("Error YourService: getUrlA ---> ");
});
// Example of method in your service
this.getUrl = function(urlA) {
try{
var deferred = $q.defer();
$http({
method: 'GET',
url: getUrlA,
params: {},
responseType: "json",
cache: false
})
.success(function(data, status, headers, config) {
deferred.resolve(data);
})
.error(function(data, status, headers, config) {
deferred.reject(data);
});
return deferred.promise;
}catch(e){
/* */
console.log("Service: getUrl ---> " + e);
}
}
$http.get returns a promise, so you can do:
return $http.get(aUrl)
.then(function(result) {
return $http.get(bUrl);
})
.then(function(result) {
return init();
},
function (error) {
// do something with the error
});
I suggest to use AngularJS promises. Mainly it has the benefit of loading the data asynchronly at the same time without having to wait until the first request is finished. see: https://docs.angularjs.org/api/ng/service/$q
var promises = [];
var loadingJson = function(url){
var defer = $q.defer();
$http.get(url).then(function(results){
defer.resolve(results);
}, function(err){
defer.reject(err);
});
return defer.promise;
};
promise.push(loadingJson('example.com/1.json'));
promise.push(loadingJson('example.com/2.json'));
$q.all(promises).then(function(resultList){
// Your hanadling here, resultList contains the results of both API calls.
}, function(errList){
// Your error handling here.
});
I am using factory method to get user's current location. Below is my factory method:
app.factory('FactoryName', function($http, $q, $window){
'use strict';
function getCurrentPosition() {
var deferred = $q.defer();
if (!$window.navigator.geolocation) {
deferred.reject('Geolocation not supported.');
} else {
$window.navigator.geolocation.getCurrentPosition(
function (position) {
deferred.resolve(position);
},
function (err) {
deferred.reject(err);
});
}
return deferred.promise;
}
return {
getCurrentPosition: getCurrentPosition
};
});
Now inside controller I am getting user's current location as follows:
FactoryName.getCurrentPosition().then(UserLocationFound).catch(UserLocationNotFound);
var UserLocationFound = function(data, status) {
$scope.latitude = data.coords.latitude;
$scope.longitude = data.coords.longitude;
};
var UserLocationNotFound = function(data, error) {
console.log("location not found")
};
Lets say the url of this page is "domain.com/form" and when I directly go to this page then its working fine, "then" and "catch" are fired accordingly to the situation. But the problem arises when I go to this page from some other page(anchor tag linkage). In this case none of the events are fired. I am unable to understand why it is working in one case and not in the other.
I have a global error handler for my angular app which is written as an $http interceptor, but I'd like to take it a step further. What I'd like is for each $http call that fails (is rejected), any "chained" consumers of the promise should first try to resolve the error, and if it is STILL unresolved (not caught), THEN I'd like the global error handler to take over.
Use case is, my global error handler shows a growl "alert box" at the top of the screen. But I have a couple of modals that pop up, and I handle the errors explicitly there, showing an error message in the modal itself. So, essentially, this modal controller should mark the rejected promise as "handled". But since the interceptor always seems to be the first to run on an $http error, I can't figure out a way to do it.
Here is my interceptor code:
angular.module("globalErrors", ['angular-growl', 'ngAnimate'])
.factory("myHttpInterceptor", ['$q', '$log', '$location', '$rootScope', 'growl', 'growlMessages',
function ($q, $log, $location, $rootScope, growl, growlMessages) {
var numLoading = 0;
return {
request: function (config) {
if (config.showLoader !== false) {
numLoading++;
$rootScope.loading = true;
}
return config || $q.when(config)
},
response: function (response) {
if (response.config.showLoader !== false) {
numLoading--;
$rootScope.loading = numLoading > 0;
}
if(growlMessages.getAllMessages().length) { // clear messages on next success XHR
growlMessages.destroyAllMessages();
}
return response || $q.when(response);
},
responseError: function (rejection) {
//$log.debug("error with status " + rejection.status + " and data: " + rejection.data['message']);
numLoading--;
$rootScope.loading = numLoading > 0;
switch (rejection.status) {
case 401:
document.location = "/auth/login";
growl.error("You are not logged in!");
break;
case 403:
growl.error("You don't have the right to do this: " + rejection.data);
break;
case 0:
growl.error("No connection, internet is down?");
break;
default:
if(!rejection.handled) {
if (rejection.data && rejection.data['message']) {
var mes = rejection.data['message'];
if (rejection.data.errors) {
for (var k in rejection.data.errors) {
mes += "<br/>" + rejection.data.errors[k];
}
}
growl.error("" + mes);
} else {
growl.error("There was an unknown error processing your request");
}
}
break;
}
return $q.reject(rejection);
}
};
}]).config(function ($provide, $httpProvider) {
return $httpProvider.interceptors.push('myHttpInterceptor');
})
This is rough code of how I'd expect the modal promise call to look like:
$http.get('/some/url').then(function(c) {
$uibModalInstance.close(c);
}, function(resp) {
if(resp.data.errors) {
$scope.errors = resp.data.errors;
resp.handled = true;
return resp;
}
});
1. Solution (hacky way)
You can do that by creating a service doing that for you. Because promises are chain-able and you basically mark a property handled at the controller level, you should pass this promise to your service and it'll take care of the unhandled errors.
myService.check(
$http.get('url/to/the/endpoint')
.then( succCallback, errorCallback)
);
2. Solution (preferred way)
Or the better solution would be to create a wrapper for $http and do something like this:
myhttp.get('url/to/the/endpoint', successCallback, failedCallback);
function successCallback(){ ... }
function failedCallback(resp){
//optional solution, you can even say resp.handled = true
myhttp.setAsHandled(resp);
//do not forget to reject here, otherwise the chained promise will be recognised as a resolved promise.
$q.reject(resp);
}
Here the myhttp service call will apply the given success and failed callbacks and then it can chain his own faild callback and check if the handled property is true or false.
The myhttp service implementation (updated, added setAsHandled function which is just optional but it's a nicer solution since it keeps everything in one place (the attribute 'handled' easily changeable and in one place):
function myhttp($http){
var service = this;
service.setAsHandled = setAsHandled;
service.get = get;
function setAsHandled(resp){
resp.handled = true;
}
function get(url, successHandler, failedHandler){
$http.get(url)
.then(successHandler, failedHandler)
.then(null, function(resp){
if(resp.handled !== true){
//your awesome popup message triggers here.
}
})
}
}
3. Solution
Same as #2 but less code needed to achieve the same:
myhttp.get('url/to/the/endpoint', successCallback, failedCallback);
function successCallback(){ ... }
function failedCallback(resp){
//if you provide a failedCallback, and you still want to have your popup, then you need your reject.
$q.reject(resp);
}
Other example:
//since you didn't provide failed callback, it'll treat as a non-handled promise, and you'll have your popup.
myhttp.get('url/to/the/endpoint', successCallback);
function successCallback(){ ... }
The myhttp service implementation:
function myhttp($http){
var service = this;
service.get = get;
function get(url, successHandler, failedHandler){
$http.get(url)
.then(successHandler, failedHandler)
.then(null, function(){
//your awesome popup message triggers here.
})
}
}
Question:
From any controller, how can I call the getPages function, return the data back to the controller and replace the empty Page.details.refobject with the GET response data?
is it possible for this all to happen within the factory regardless of which controller calls the function?
app.factory('Pages', function($http, ENV){
var Pages = {};
Pages.details =
{
pages:
{
length: 0,
offsets: []
},
ref:
{
//data goes here on success
},
getPages: function($scope) {
return $http.get(ENV.apiEndpoint + '/' + $scope.storeSlug + '/pages.json?code=' + $scope.promoCode)
.success(function(data){
// I want this Pages.details.ref to be replaced on success of getPages
Pages.details.ref = data;
$scope.handlePagesSuccess(data);
return data;
})
.error(function(data, status){
// console.log('error:' + status);
});
}
}
return Pages;
});
Controllers:
this controller calls the init request
app.controller('RandomCtrl', function($scope, Pages){
var handleSuccess = function (data) {
$scope.data = data;
}
Pages.details.getPages($scope).success(handleSuccess);
})
Controller #2:
this controller just consumes a temp version of the request no relationship between the RandomCtrl. e.g this controller is typically a directive level controller where the theres no bubbling between a parent ctrl
app.controller('OtherCtrl', function($scope, Pages){
$scope.tempPage = Pages.details.ref;
})
it shouldnt matter where getPages is called from. I want ref to be replaced everytime getPages is called.
It seems like you are trying to manage state inside your factory, which probably is not a good idea. Also it is not a good idea to pass around $scope in factories. They should be limited to its own controller. You could instead cache the promise for the previous call made and based on a flag you could either return the cached promise or make the actual service call.
app.factory('Pages', function($http, ENV, $q){
var Pages = {};
var cachedPromise = {};
Pages.details =
{
pages:
{
length: 0,
offsets: []
},
getPages: function(request) {
//Get a request key to make sure you are returning right promise incase multiple product calls are made at the same time.
var reqKey = request.storeSlug + request.promoCode;
//if a call has already been made and there is a promise return it
if(cachedPromise[reqKey]) return cachedPromise[reqKey];
//Store the promise in the cache for lastCall retrieval
return cachedPromise[reqKey] = $http.get(ENV.apiEndpoint + '/' + request.storeSlug + '/pages.json?code=' + request.promoCode)
.then(function(result){
return result.data; //You can alter data and send as well
}, function(data, status){
return $q.reject('some error'); //or return some data
}).finally(function(){
//remove the cache from the map, once promise is resolved.
delete cachedPromise[reqKey];
});
}
}
return Pages;
});
In your first controller do:-
app.controller('RandomCtrl', function($scope, Pages){
//Build your request.
Pages.details.getPages(request).then(function (data) {
$scope.data = data;
});
});
In your second controller just do the same:-
app.controller('OtherCtrl', function($scope, Pages){
//Pass the flag to get the cached data.
Pages.details.getPages(request).then(function (data) {
$scope.tempPage = data;
});
});
I'm getting data from an async service inside my controller like this:
myApp.controller('myController', ['$scope', 'AsyncService',
function($scope, AsyncService) {
$scope.getData = function(query) {
return AsyncService.query(query).then(function(response) {
// Got success response, return promise
return response;
}, function(reason) {
// Got error, query again in one second
// ???
});
}
}]);
My questions:
How to query the service again when I get error from service without returning the promise.
Would it be better to do this in my service?
Thanks!
You can retry the request in the service itself, not the controller.
So, AsyncService.query can be something like:
AsyncService.query = function() {
var counter = 0
var queryResults = $q.defer()
function doQuery() {
$http({method: 'GET', url: 'https://example.com'})
.success(function(body) {
queryResults.resolve(body)
})
.error(function() {
if (counter < 3) {
doQuery()
counter++
}
})
}
return queryResults.promise
}
And you can get rid of your error function in the controller:
myApp.controller('myController', ['$scope', 'AsyncService',
function($scope, AsyncService) {
$scope.getData = function(query) {
return AsyncService.query(query).then(function(response) {
// Got success response
return response;
});
}
}
]);
This actually works:
angular.module('retry_request', ['ng'])
.factory('RetryRequest', ['$http', '$q', function($http, $q) {
return function(path) {
var MAX_REQUESTS = 3,
counter = 1,
results = $q.defer();
var request = function() {
$http({method: 'GET', url: path})
.success(function(response) {
results.resolve(response)
})
.error(function() {
if (counter < MAX_REQUESTS) {
request();
counter++;
} else {
results.reject("Could not load after multiple tries");
}
});
};
request();
return results.promise;
}
}]);
Then just an example of using it:
RetryRequest('/api/token').then(function(token) {
// ... do something
});
You have to require it when declaring your module:
angular.module('App', ['retry_request']);
And in you controller:
app.controller('Controller', function($scope, RetryRequest) {
...
});
If someone wants to improve it with some kind of backoff or random timing to retry the request, that will be even better. I wish one day something like that will be in Angular Core
I wrote an implementation with exponential backoff that doesn't use recursion (which would created nested stack frames, correct?) The way it's implemented has the cost of using multiple timers and it always creates all the stack frames for the make_single_xhr_call (even after success, instead of only after failure). I'm not sure if it's worth it (especially if the average case is a success) but it's food for thought.
I was worried about a race condition between calls but if javascript is single-threaded and has no context switches (which would allow one $http.success to be interrupted by another and allow it to execute twice), then we're good here, correct?
Also, I'm very new to angularjs and modern javascript so the conventions may be a little dirty also. Let me know what you think.
var app = angular.module("angular", []);
app.controller("Controller", ["$scope", "$http", "$timeout",
function($scope, $http, $timeout) {
/**
* Tries to make XmlHttpRequest call a few times with exponential backoff.
*
* The way this works is by setting a timeout for all the possible calls
* to make_single_xhr_call instantly (because $http is asynchronous) and
* make_single_xhr_call checks the global state ($scope.xhr_completed) to
* make sure another request was not already successful.
*
* With sleeptime = 0, inc = 1000, the calls will be performed around:
* t = 0
* t = 1000 (+1 second)
* t = 3000 (+2 seconds)
* t = 7000 (+4 seconds)
* t = 15000 (+8 seconds)
*/
$scope.repeatedly_xhr_call_until_success = function() {
var url = "/url/to/data";
$scope.xhr_completed = false
var sleeptime = 0;
var inc = 1000;
for (var i = 0, n = 5 ; i < n ; ++i) {
$timeout(function() {$scope.make_single_xhr_call(url);}, sleeptime);
sleeptime += inc;
inc = (inc << 1); // multiply inc by 2
}
};
/**
* Try to make a single XmlHttpRequest and do something with the data.
*/
$scope.make_single_xhr_call = function(url) {
console.log("Making XHR Request to " + url);
// avoid making the call if it has already been successful
if ($scope.xhr_completed) return;
$http.get(url)
.success(function(data, status, headers) {
// this would be later (after the server responded)-- maybe another
// one of the calls has already completed.
if ($scope.xhr_completed) return;
$scope.xhr_completed = true;
console.log("XHR was successful");
// do something with XHR data
})
.error(function(data, status, headers) {
console.log("XHR failed.");
});
};
}]);
Following this article Promises in AngularJS, Explained as a Cartoon
you need to retry only when the response comes under 5XX category
I have written a service called http which can be called by passing all http configs as
var params = {
method: 'GET',
url: URL,
data: data
}
then call the service method as follows:
<yourDefinedAngularFactory>.http(params, function(err, response) {});
http: function(config, callback) {
function request() {
var counter = 0;
var queryResults = $q.defer();
function doQuery(config) {
$http(config).success(function(response) {
queryResults.resolve(response);
}).error(function(response) {
if (response && response.status >= 500 && counter < 3) {
counter++;
console.log('retrying .....' + counter);
setTimeout(function() {
doQuery(config);
}, 3000 * counter);
} else {
queryResults.reject(response);
}
});
}
doQuery(config);
return queryResults.promise;
}
request(config).then(function(response) {
if (response) {
callback(response.errors, response.data);
} else {
callback({}, {});
}
}, function(response) {
if (response) {
callback(response.errors, response.data);
} else {
callback({}, {});
}
});
}
I ended up doing this a lot so I wrote a library to help address this problem : )
https://www.npmjs.com/package/reattempt-promise-function
In this example you could do something like
myApp.controller('myController', ['$scope', 'AsyncService',
function($scope, AsyncService) {
var dogsQuery = { family: canine };
$scope.online = true;
$scope.getDogs = function() {
return reattempt(AsyncService.query(dogsQuery)).then(function(dogs) {
$scope.online = true;
$scope.dogs = dogs;
}).catch(function() {
$scope.online = false;
});
}
}]);