I have create a UserService as follows:
angular.module('nrApp').factory('userService', ['Restangular', 'UserModel', 'DSCacheFactory', function (Restangular, UserModel, DSCacheFactory) {
// Create a new cache called "profileCache"
var userCache = DSCacheFactory('userCache', {
maxAge: 3600000,
deleteOnExpire: 'aggressive',
storageMode: 'localStorage', // This cache will sync itself with `localStorage`.
onExpire: function (key, value) {
Restangular.oneUrl('users', key).get().then(function(data) {
userCache.put(key, data);
});
}
});
Restangular.extendModel('users', function(obj) {
return UserModel.mixInto(obj);
});
Restangular.addRequestInterceptor(function(element, operation, what, url) {
if(operation === 'get') {
debugger;
//Check the cache to see if the resource is already cached
var data = userCache.get(url);
//If cache object does exist, return it
if(data !== undefined) {
angular.extend(element, data);
}
return element;
}
});
Restangular.addResponseInterceptor(function(data, operation, what, url, response) {
//Cache the response from a get method
if(operation === 'get') {
debugger;
userCache.put(url, data);
}
//Unvalidate the cache when a 'put', 'post' and 'delete' is performed to update the cached version.
if (operation === 'put' || operation === 'post' || operation === 'delete') {
userCache.destroy();
}
return response;
});
return Restangular.service('users');
}]);
From the comments it can be seen that what I am trying to achieve is whenever a Get request is performed through this service using Restangular the local cache is checked and if the cache returns an object it is extended into the restangular element. The flow that want to achieve is that of cancelling the request to the sever when a cache object is found for that request.
However without any luck the addResponseInterceptor method still executes even though the object was found in the cache.
Are there any possible solutions to cancelling the request to the sever during a 'Get' request?
Thanks! :)
One way to go about it would be to cancel it via httpConfig. Restangular gives you httpConfig object as a parameter in the addFullRequestInterceptor method. You could use that like in the following:
RestangularProvider.addFullRequestInterceptor(function(element, operation, what, url, headers, params, httpConfig ) {
...
if found in cache {
var defer = $q.defer();
httpConfig.timeOut = defer.promise;
defer.resolve();
}
...
}
Hope this helps.
I solved the particular issue of returning cached data if available through an angular-cache CacheFactory instance by simply changing the httpConfig settings in the RequestInterceptor. Example shown below:
angular.module('App')
.factory('Countries', function (Restangular, CacheFactory, $q) {
var countryCache;
var countryService;
// Check to make sure the cache doesn't already exist
if (!CacheFactory.get('countryCache')) {
countryCache = CacheFactory('countryCache', { maxAge: 60 * 60 * 1000 });
}
if (!countryService) {
countryService = Restangular.service('countries');
Restangular.addFullRequestInterceptor(function(element, operation, what, url, headers, params, httpConfig) {
if (what === 'countries') {
switch (operation) {
case 'getList':
httpConfig.cache = countryCache;
break;
default:
break;
}
}
return {
element: element,
headers: headers,
params: params,
httpConfig: httpConfig
};
});
}
return countryService;
});
You can decorate $http to prevent multiple request to the same url. Restangular use $http, don't need to adding fullRequestIntercepter to cancel request, because this prevent request before send.
$provide.decorator('$http', function ($delegate, $cacheFactory, $rootScope) {
var $http = $delegate;
var customCache = $cacheFactory('customCache');
var wrapper = function () {
var key = arguments[0].url;
var requestPromise = customCache.get(key);
if (!requestPromise){
$rootScope.requestCount++;
requestPromise = $http.apply($http, arguments);
requestPromise.then(function(){
customCache.remove(key);
});
customCache.put(key, requestPromise)
}
return requestPromise;
};
Object.keys($http).filter(function (key) {
return (typeof $http[key] === 'function');
}).forEach(function (key) {
wrapper[key] = function () {
return $http[key].apply($http, arguments);
};
});
return wrapper;
});
Example here
Related
I have the following function (credit) that wraps an AngularJS $http function in a way that it invokes browser XHR when running on desktop, but invokes cordova-plugin-advanced-http if on mobile.
It seems that this works when I use $http({method:'get/post'}...) but doesn't work if I call the convenience shortcuts like $http.get(...)
Can someone suggest what modification I need to make?
$provide.decorator('$http', ['$delegate', '$q', function($delegate, $q) {
// create function which overrides $http function
var $http = $delegate;
var wrapper = function () {
var url = arguments[0].url;
var method = arguments[0].method;
var isOutgoingRequest = /^(http|https):\/\//.test(url);
if (window.cordova && isOutgoingRequest) {
console.log ("**** -->"+method+"<-- using native HTTP with:"+url);
var d = $q.defer();
var options = {
method: method,
data: arguments[0].data,
headers: arguments[0].headers,
timeout: arguments[0].timeout
};
cordova.plugin.http.sendRequest(url,options,
function (succ) {
console.log ("*** Inside native HTTP success with:"+JSON.stringify(succ));
try {
if (options.headers && options.headers['x-parse']=='text')
d.resolve({"data":succ.data});
else
d.resolve({"data":JSON.parse(succ.data)});
return d.promise;
}
catch (e) {
d.resolve({"data":succ.data});
return d.promise;
}
},
function (err) {
console.log ("*** Inside native HTTP error");
d.reject(err);
return d.promise;
});
return d.promise;
}
else {
console.log ("**** "+method+" using XHR HTTP for "+url);
return $http.apply($http, arguments);
}
};
Object.keys($http).filter(function (key) {
return (typeof $http[key] === 'function');
}).forEach(function (key) {
wrapper[key] = function () {
// Apply global changes to arguments, or perform other
// nefarious acts.
// console.log ("KEY="+key);
return $http[key].apply($http, arguments);
};
});
return wrapper;
}]);
If I understood your intent correctly, the way you're assigning the HTTP methods that hang off wrapper won't invoke the contents of your wrapper function.
Note that the parameters of the $http convenience functions vary.
Examples:
GET is described as: get(url, [config])
POST is described as: post(url, data, [config])
With the above in mind, here's one way of delegating back to your wrapper function that switches between XHR and the Cordova plugin when the $http convenience methods are used:
wrapper[key] = function () {
var url = arguments[0];
if (['get', 'delete', 'head', 'jsonp'].indexOf(key) !== -1) {
// arguments[1] == config
return wrapper(Object.assign({
method: key,
url: url,
}, arguments[1]));
} else {
// POST, PUT, PATCH
// arguments[1] == data
// arguments[2] == config
return wrapper(Object.assign({
data: arguments[1],
method: key,
url: url,
}, arguments[2]));
}
};
Here is a working solution I eventually arrived at.
// Wraps around $http that switches between browser XHR
// or cordova-advanced-http based on if cordova is available
// credits:
// a) https://www.exratione.com/2013/08/angularjs-wrapping-http-for-fun-and-profit/
// b) https://gist.github.com/adamreisnz/354364e2a58786e2be71
$provide.decorator('$http', ['$delegate', '$q', function($delegate, $q) {
// create function which overrides $http function
var $http = $delegate;
var wrapper = function () {
var url;
var method;
url = arguments[0].url;
method = arguments[0].method;
var isOutgoingRequest = /^(http|https):\/\//.test(url);
if (window.cordova && isOutgoingRequest) {
console.log ("**** -->"+method+"<-- using native HTTP with:"+encodeURI(url));
var d = $q.defer();
var options = {
method: method,
data: arguments[0].data,
headers: arguments[0].headers,
timeout: arguments[0].timeout,
responseType: arguments[0].responseType
};
cordova.plugin.http.sendRequest(encodeURI(url),options,
function (succ) {
// automatic JSON parse if no responseType: text
// fall back to text if JSON parse fails too
if (options.responseType =='text') {
// don't parse into JSON
d.resolve({"data":succ.data});
return d.promise;
}
else {
try {
d.resolve({"data":JSON.parse(succ.data)});
return d.promise;
}
catch (e) {
console.log ("*** Native HTTP response: JSON parsing failed for "+url+", returning text");
d.resolve({"data":succ.data});
return d.promise;
}
}
},
function (err) {
console.log ("*** Inside native HTTP error: "+JSON.stringify(err));
d.reject(err);
return d.promise;
});
return d.promise;
}
else { // not cordova, so lets go back to default http
console.log ("**** "+method+" using XHR HTTP for "+url);
return $http.apply($http, arguments);
}
};
// wrap around all HTTP methods
Object.keys($http).filter(function (key) {
return (typeof $http[key] === 'function');
}).forEach(function (key) {
wrapper[key] = function () {
return $http[key].apply($http, arguments);
};
});
// wrap convenience functions
$delegate.get = function (url,config) {
return wrapper(angular.extend(config || {}, {
method: 'get',
url: url
}));
};
$delegate.post = function (url,data,config) {
return wrapper(angular.extend(config || {}, {
method: 'post',
url: url,
data:data
}));
};
$delegate.delete = function (url,config) {
return wrapper(angular.extend(config || {}, {
method: 'delete',
url: url
}));
};
return wrapper;
}]);
Looking to get the right syntax to do the following :
1) empty object results = {}
2) first webservice call finished = results.webservice1 = data;
3) second webservice call finished = results.webservice2 = data;
4) Complete
I have something like this but syntax doesn't feel right
function getClaimSummary(filter) {
let deferred = $q.defer();
$http.post(configSettings.Api.GetClaimSummary, filter, { withCredentials : true })
.success(function(data){
deferred.resolve(data);
})
.error(function(error){
deferred.reject(error);
});
return deferred.promise;
}
function getPolicySummary(filter) {
let deferred = $q.defer();
$http.post(configSettings.Api.GetPolicySummary, filter, { withCredentials : true })
.success(function(data){
deferred.resolve(data);
})
.error(function(error){
deferred.reject(error);
});
return deferred.promise;
}
function calculateAccumulations(filter){
service.result = {};
//Get Claims Summary
getClaimSummary(filter).then(function(data){
service.result.claims = data;
}).then(getPolicySummary(filter).then(function(data){
service.result.policy = data;
showAccumulations();
}));
}
$http itself already returns a promise, so there's no need to create your own, you could also handle both promises at the same time instead of waiting for eachother like so:
function getClaimSummary(filter) {
return $http.post(configSettings.Api.GetClaimSummary, filter, { withCredentials : true });
}
function getPolicySummary(filter) {
return $http.post(configSettings.Api.GetPolicySummary, filter, { withCredentials : true });
}
function calculateAccumulations(filter){
service.result = {};
//Get Claims Summary
$q.all({
claims: getClaimSummary(filter),
policy: getPolicySummary(filter)
}).then(function (result) {
service.result = result;
});
}
You could even save some duplicate code doing it like so:
function fetchData(type, filter) {
return $http.post(configSettings.Api[type], filter, { withCredentials : true });
}
function calculateAccumulations(filter){
service.result = {};
//Get Claims Summary
$q.all({
claims: fetchData('GetClaimSummary', filter),
policy: getPolicySummary('GetPolicySummary', filter)
}).then(function (result) {
service.result = result;
});
}
More information about $q can be found here.
I have the next problem
I'm trying to caching a get request with $http but seems not working, the cache variable always get undefined
Sample code:
myApp.factory("sample", ["$http", "$q", "$cacheFactory", sample]);
function sample($http, $q, $cacheFactory) {
function getData() {
var url = "http://whatever ...";
return $http.get(url, {
params: {
Id: 10
},
cache: true
})
.then(function(response) {
// trying to get the cached data
var cache = $cacheFactory.get("$http");
var data = cache.get(url); // undefined -> ??
return response.data;
})
.catch(function(error) {
return $q.reject(error);
});
}
return {
getData: getData
};
}
The problem was with the URL which you are passing to get the cache.
This works.
myApp.factory("sample", ["$http", "$q", "$cacheFactory", sample]);
function sample($http, $q, $cacheFactory) {
function getData() {
var url = "http://whatever ...";
return $http.get(url, {
params: {
Id: 10
},
cache: true
})
.then(function(response) {
// trying to get the cached data
var cache = $cacheFactory.get("$http");
var data = cache.get(url+"?id=10"); // cacheFactory will store the cache data with full URL including params so your key should have the params
return response.data;
})
.catch(function(error) {
return $q.reject(error);
});
}
return {
getData: getData
};
}
cacheFactory will store the cache data with full URL including params so your key should have the params.
cache.get(url+"?id=10");
I have a method that calls an angular service and consequently makes an ajax request via the service. I need to make sure that if this is called several times, the previous request in aborted (if it hasn't already been resolved that is).
This method can get called multiple times. This method is actually from ngTable on ngTableParams:
getData = function($defer, params) {
myService.getRecord(params).then(function(res){
...
$defer.resolve(res.Records);
});
}
Here's the method on the service:
this.getRecords = function(params) {
...
return Restangular
.all('/api/records')
.post(filters);
};
If ngTable makes 3 calls I want the first 2 to be aborted (unless of course they returned so fast that it got resolved)
You can abort $http calls via the timeout config property, which can be a Promise, that aborts the request when resolved.
So in restangular, you can do this like
var abort = $q.defer();
Restangular.one('foos', 12345).withHttpConfig({timeout: abort.promise}).get();
abort.resolve();
To integrate it with your usecase, for example, you could have this in your service:
var abortGet;
this.getRecords = function(params) {
...
if (abortGet) abortGet.resolve();
abortGet = $q.defer();
return Restangular
.all('/api/records')
.withHttpConfig({timeout: abortGet.promise})
.post(filters);
}
This way calling getRecords always aborts the previous call if has not been resolved yet!
This is another approach if you want to abort all http requests when change the state for UI-router:
angular
.run(function(HttpHandlerSrv) {
HttpHandlerSrv.abortAllRequestsOn('$stateChangeSuccess');
HttpHandlerSrv.R.setFullRequestInterceptor(function(element, operation, route, url, headers, params, httpConfig) {
httpConfig = httpConfig || {};
if(httpConfig.timeout === undefined) {
httpConfig.timeout = HttpHandlerSrv.newTimeout();
}
return { element: element, params: params, headers: headers, httpConfig: httpConfig };
});
})
.factory('HttpHandlerSrv', HttpHandlerSrv);
/** ngInject */
function HttpHandlerSrv($q, $rootScope, Restangular) {
var requests = [];
return {
R: Restangular,
newTimeout: newTimeout,
abortAllRequests: abortAllRequests,
abortAllRequestsOn: abortAllRequestsOn
};
function newTimeout() {
var request = $q.defer();
requests.push(request);
return request.promise;
}
function abortAllRequests() {
angular.forEach(requests, function(request) {
request.resolve();
});
requests = [];
}
function abortAllRequestsOn(event) {
$rootScope.$on(event, function(event, newUrl, oldUrl) {
if(newUrl !== oldUrl) { abortAllRequests(); }
});
}
}
Given a Ajax request in AngularJS
$http.get("/backend/").success(callback);
what is the most effective way to cancel that request if another request is launched (same backend, different parameters for instance).
This feature was added to the 1.1.5 release via a timeout parameter:
var canceler = $q.defer();
$http.get('/someUrl', {timeout: canceler.promise}).success(successCallback);
// later...
canceler.resolve(); // Aborts the $http request if it isn't finished.
Cancelling Angular $http Ajax with the timeout property doesn't work in Angular 1.3.15.
For those that cannot wait for this to be fixed I'm sharing a jQuery Ajax solution wrapped in Angular.
The solution involves two services:
HttpService (a wrapper around the jQuery Ajax function);
PendingRequestsService (tracks the pending/open Ajax requests)
Here goes the PendingRequestsService service:
(function (angular) {
'use strict';
var app = angular.module('app');
app.service('PendingRequestsService', ["$log", function ($log) {
var $this = this;
var pending = [];
$this.add = function (request) {
pending.push(request);
};
$this.remove = function (request) {
pending = _.filter(pending, function (p) {
return p.url !== request;
});
};
$this.cancelAll = function () {
angular.forEach(pending, function (p) {
p.xhr.abort();
p.deferred.reject();
});
pending.length = 0;
};
}]);})(window.angular);
The HttpService service:
(function (angular) {
'use strict';
var app = angular.module('app');
app.service('HttpService', ['$http', '$q', "$log", 'PendingRequestsService', function ($http, $q, $log, pendingRequests) {
this.post = function (url, params) {
var deferred = $q.defer();
var xhr = $.ASI.callMethod({
url: url,
data: params,
error: function() {
$log.log("ajax error");
}
});
pendingRequests.add({
url: url,
xhr: xhr,
deferred: deferred
});
xhr.done(function (data, textStatus, jqXhr) {
deferred.resolve(data);
})
.fail(function (jqXhr, textStatus, errorThrown) {
deferred.reject(errorThrown);
}).always(function (dataOrjqXhr, textStatus, jqXhrErrorThrown) {
//Once a request has failed or succeeded, remove it from the pending list
pendingRequests.remove(url);
});
return deferred.promise;
}
}]);
})(window.angular);
Later in your service when you are loading data you would use the HttpService instead of $http:
(function (angular) {
angular.module('app').service('dataService', ["HttpService", function (httpService) {
this.getResources = function (params) {
return httpService.post('/serverMethod', { param: params });
};
}]);
})(window.angular);
Later in your code you would like to load the data:
(function (angular) {
var app = angular.module('app');
app.controller('YourController', ["DataService", "PendingRequestsService", function (httpService, pendingRequestsService) {
dataService
.getResources(params)
.then(function (data) {
// do stuff
});
...
// later that day cancel requests
pendingRequestsService.cancelAll();
}]);
})(window.angular);
Cancelation of requests issued with $http is not supported with the current version of AngularJS. There is a pull request opened to add this capability but this PR wasn't reviewed yet so it is not clear if its going to make it into AngularJS core.
If you want to cancel pending requests on stateChangeStart with ui-router, you can use something like this:
// in service
var deferred = $q.defer();
var scope = this;
$http.get(URL, {timeout : deferred.promise, cancel : deferred}).success(function(data){
//do something
deferred.resolve(dataUsage);
}).error(function(){
deferred.reject();
});
return deferred.promise;
// in UIrouter config
$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
//To cancel pending request when change state
angular.forEach($http.pendingRequests, function(request) {
if (request.cancel && request.timeout) {
request.cancel.resolve();
}
});
});
For some reason config.timeout doesn't work for me. I used this approach:
let cancelRequest = $q.defer();
let cancelPromise = cancelRequest.promise;
let httpPromise = $http.get(...);
$q.race({ cancelPromise, httpPromise })
.then(function (result) {
...
});
And cancelRequest.resolve() to cancel. Actually it doesn't not cancel a request but you don't get unnecessary response at least.
Hope this helps.
This enhances the accepted answer by decorating the $http service with an abort method as follows ...
'use strict';
angular.module('admin')
.config(["$provide", function ($provide) {
$provide.decorator('$http', ["$delegate", "$q", function ($delegate, $q) {
var getFn = $delegate.get;
var cancelerMap = {};
function getCancelerKey(method, url) {
var formattedMethod = method.toLowerCase();
var formattedUrl = encodeURI(url).toLowerCase().split("?")[0];
return formattedMethod + "~" + formattedUrl;
}
$delegate.get = function () {
var cancelerKey, canceler, method;
var args = [].slice.call(arguments);
var url = args[0];
var config = args[1] || {};
if (config.timeout == null) {
method = "GET";
cancelerKey = getCancelerKey(method, url);
canceler = $q.defer();
cancelerMap[cancelerKey] = canceler;
config.timeout = canceler.promise;
args[1] = config;
}
return getFn.apply(null, args);
};
$delegate.abort = function (request) {
console.log("aborting");
var cancelerKey, canceler;
cancelerKey = getCancelerKey(request.method, request.url);
canceler = cancelerMap[cancelerKey];
if (canceler != null) {
console.log("aborting", cancelerKey);
if (request.timeout != null && typeof request.timeout !== "number") {
canceler.resolve();
delete cancelerMap[cancelerKey];
}
}
};
return $delegate;
}]);
}]);
WHAT IS THIS CODE DOING?
To cancel a request a "promise" timeout must be set.
If no timeout is set on the HTTP request then the code adds a "promise" timeout.
(If a timeout is set already then nothing is changed).
However, to resolve the promise we need a handle on the "deferred".
We thus use a map so we can retrieve the "deferred" later.
When we call the abort method, the "deferred" is retrieved from the map and then we call the resolve method to cancel the http request.
Hope this helps someone.
LIMITATIONS
Currently this only works for $http.get but you can add code for $http.post and so on
HOW TO USE ...
You can then use it, for example, on state change, as follows ...
rootScope.$on('$stateChangeStart', function (event, toState, toParams) {
angular.forEach($http.pendingRequests, function (request) {
$http.abort(request);
});
});
here is a version that handles multiple requests, also checks for cancelled status in callback to suppress errors in error block. (in Typescript)
controller level:
requests = new Map<string, ng.IDeferred<{}>>();
in my http get:
getSomething(): void {
let url = '/api/someaction';
this.cancel(url); // cancel if this url is in progress
var req = this.$q.defer();
this.requests.set(url, req);
let config: ng.IRequestShortcutConfig = {
params: { id: someId}
, timeout: req.promise // <--- promise to trigger cancellation
};
this.$http.post(url, this.getPayload(), config).then(
promiseValue => this.updateEditor(promiseValue.data as IEditor),
reason => {
// if legitimate exception, show error in UI
if (!this.isCancelled(req)) {
this.showError(url, reason)
}
},
).finally(() => { });
}
helper methods
cancel(url: string) {
this.requests.forEach((req,key) => {
if (key == url)
req.resolve('cancelled');
});
this.requests.delete(url);
}
isCancelled(req: ng.IDeferred<{}>) {
var p = req.promise as any; // as any because typings are missing $$state
return p.$$state && p.$$state.value == 'cancelled';
}
now looking at the network tab, i see that it works beatuifully. i called the method 4 times and only the last one went through.
You can add a custom function to the $http service using a "decorator" that would add the abort() function to your promises.
Here's some working code:
app.config(function($provide) {
$provide.decorator('$http', function $logDecorator($delegate, $q) {
$delegate.with_abort = function(options) {
let abort_defer = $q.defer();
let new_options = angular.copy(options);
new_options.timeout = abort_defer.promise;
let do_throw_error = false;
let http_promise = $delegate(new_options).then(
response => response,
error => {
if(do_throw_error) return $q.reject(error);
return $q(() => null); // prevent promise chain propagation
});
let real_then = http_promise.then;
let then_function = function () {
return mod_promise(real_then.apply(this, arguments));
};
function mod_promise(promise) {
promise.then = then_function;
promise.abort = (do_throw_error_param = false) => {
do_throw_error = do_throw_error_param;
abort_defer.resolve();
};
return promise;
}
return mod_promise(http_promise);
}
return $delegate;
});
});
This code uses angularjs's decorator functionality to add a with_abort() function to the $http service.
with_abort() uses $http timeout option that allows you to abort an http request.
The returned promise is modified to include an abort() function. It also has code to make sure that the abort() works even if you chain promises.
Here is an example of how you would use it:
// your original code
$http({ method: 'GET', url: '/names' }).then(names => {
do_something(names));
});
// new code with ability to abort
var promise = $http.with_abort({ method: 'GET', url: '/names' }).then(
function(names) {
do_something(names));
});
promise.abort(); // if you want to abort
By default when you call abort() the request gets canceled and none of the promise handlers run.
If you want your error handlers to be called pass true to abort(true).
In your error handler you can check if the "error" was due to an "abort" by checking the xhrStatus property. Here's an example:
var promise = $http.with_abort({ method: 'GET', url: '/names' }).then(
function(names) {
do_something(names));
},
function(error) {
if (er.xhrStatus === "abort") return;
});