I implemented Primus (Sockets) on my Server and would like to access it via the client, which uses AngularJS. I would like to be able to still use libraries like Restangular or the $resource from Angular. So IMHO the best way to achieve this is to extend the $http service, which is used by most libraries as the basis.
I would like this new service to be able to gracefully fall back to the normal $http, when there is no socket connection available.
In Pseudocode:
socketHttpService = function(config) {
if(socketEnabled) {
var message = buildMessageFromConfig();
primus.write(message);
return promise;
}
return $http(config);
}
Call it like you would $http
socketHttpService({method: 'GET', url: '/someUrl'}).then(function() {
// do whatever
});
My question is, how can i replace the standard $http service with this newly created one? Is there an elegant way, while still retaining the default $http behaviour?
In the meantime, I found a solution to the problem
.config(function($provide) {
$provide.decorator('$httpBackend', function($delegate, $q, $log, SocketService) {
// do not blast mock $httpBackend if it exists
if (angular.isDefined(angular.mock)) {
return $delegate;
}
var httpBackendSocket = function(method, url, post, callback, headers, timeout, withCredentials, responseType) {
if(SocketService.isOpen) {
console.log('open');
method = method.toLowerCase();
// we only know get, post, put, delete
if(method === 'get' || method === 'post' || method === 'put' || method === 'delete') {
// we can not handle the authentication links via sockets, so exclude them
if( url.substring( 0, '/api/v1/currentuser'.length ) !== '/api/v1/currentuser' &&
!angular.equals(url, '/api/v1/login') &&
!angular.equals(url, '/api/v1/logout') &&
!angular.equals(url, '/api/v1/session') ) {
var promise = SocketService.writeRest(method, url, post || {});
return promise.then(function promiseSuccess(response) {
return callback(response.status, response.data, response.headers, '');
}, function promiseError(response) {
// is caught via http handlers
// LATER: If error, retry with $httpBackend ($delegate)
return callback(response.status, response.data, response.headers, '');
});
}
}
}
return $delegate(method, url, post, callback, headers, timeout, withCredentials, responseType);
}
return httpBackendSocket;
});
})
Why? Because it feels like 5 times faster than http, because there is a standing connection and I am not losing any of the realtime options. It's like a cherry on top.
Kind Regards
Related
I have an service in my Angular app that is responsible for authorizing the user and returning the auth token back.
However, due to the async nature of $http, I cannot properly isolate the logic of my service. Instead I must return a promise and push the logic of interpreting the response to the caller of the service (single responsibility red flag).
Given this, it's made me reconsider whether I am thinking about this correctly. I come from a Java world where the majority of interactions are synchronous, so something isn't sitting well with me as I try to architect a clean design with single responsibility.
What am I missing here?
UPDATE
I realize the below will not work as intended, but that's my thought of how I'd like it to work at least:
app.service('AuthenticationService', ['$http', '$httpParamSerializerJQLike', function($http, $httpParamSerializerJQLike)
{
this.authServerBaseURL = "...";
this.clientId = "...";
this.authenticate = function(username, password)
{
var config =
{
headers:
{
"Content-Type" : 'application/x-www-form-urlencoded',
"Authorization" : "Basic " + btoa(this.clientId + ":")
}
}
var body = $httpParamSerializerJQLike(
{
grant_type : "password",
username : username,
password : password
});
return $http.post(this.authServerBaseURL + '/oauth/token', body, config).
success(function(data, status, headers, config)
{
return data.access_token;
}).
error(function(data, status, headers, config)
{
return false;
});
}
}]);
Update after you added code: Your thought process can work, see below. However, $http docs say not to use .success and .error. If you instead use .then, as in my examples below, it will work.
Assuming your code is something similar to this:
// AuthService
this.authenticate = function() {
return $http.post('http://example.com', body, config);
}
// Using it:
AuthService.authenticate().then(function(data) {
var token = data.access_token;
});
You can move the knowledge about how the data is extracted to the service like this:
// AuthService
this.authenticate = function() {
return $http.post('http://example.com', body, config).then(function(data) {
return data.access_token;
});
}
// Using it:
AuthService.authenticate().then(function(token) {
var token = token;
});
what happens here is that you make a new promise by calling .then on the $http promise, which is what is returned. The promises are chained, so the $http promise will resolve this new promise, which then resolves itself with the extracted token.
I am attempting to use angular-cache in the app.config section of my angular application as shown in this JSFiddle - http://jsfiddle.net/0b1jgwoj/
.config(function (componentFactoryProvider, Config, RestangularProvider, DSCacheFactory, $q) {
componentFactoryProvider.setViewPath(function (componentSnakeName, componentName) {
return 'components/' + componentSnakeName + '/' + componentSnakeName + '.html';
})
RestangularProvider.setBaseUrl(Config.API.path);
RestangularProvider.setRequestSuffix('.json');
var appCache = DSCacheFactory('appCache', {
maxAge: 3600000,
deleteOnExpire: 'aggressive',
storageMode: 'localStorage', // This cache will sync itself with `localStorage`.
onExpire: function (key, value) {
//Todo: implement logic to get data from server be it a collection or single object
}
});
//Intercept the Get Request and check for value in cache. If found cancel Get Request, if not continue get Request.
RestangularProvider.addFullRequestInterceptor(function (element, operation, what, url, headers, params, httpConfig) {
if(operation === 'get') {
debugger;
//Check the cache to see if the resource is already cached
var data = appCache.get(url);
//If cache object does exist, return it
if (data !== undefined) {
angular.extend(element, data);
var defer = $q.defer();
httpConfig.timeOut = defer.promise;
defer.resolve();
return {
headers: headers,
params: params,
element: element,
httpConfig: httpConfig
};
}
}
});
RestangularProvider.addResponseInterceptor(function(data, operation, what, url, response) {
//Cache the response from a get method
if(operation === 'get') {
debugger;
appCache.remove(url);
appCache.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') {
appCache.destroy();
}
return response;
});
})
Two errors arise (1) $q is not defined even though I have put it inside the DI list (2) DSCacheFactory is returning an Unknown Provider Error
Any ideas on how to solve these problems as it is important that this logic remains in .config() section since the restangular 'addFullRequestInterceptor' doesn't cancel the request in any other services but only the config section.
Thanks
Fixed the problem. Had to put the logic in the .run method as stated in this document https://github.com/mgonto/restangular#how-to-configure-them-globally
Basically what I'm trying to do is to set a prefix for all $http server requests (the server url). I tried to use an interceptor, but the problem is that this also affects the template requests:
$httpProvider.interceptors.push(function ($q) {
return {
'request': function (request) {
request.url = "http://localhost/"+request.url;
return request || $q.when(request);
}
}
});
=>
XMLHttpRequest cannot load http://localhost/templates/main.html
I thought about using my own provider (for example $myHttp) which inherit $http, but i don't know how to do this.
So what is a good solution for this?
If all of your templates are in the templates directory, you could just ignore those in your interceptor.
$httpProvider.interceptors.push(function ($q) {
return {
request: function (request) {
if (request.url.indexOf('templates') === -1) {
request.url = "http://localhost/" + request.url;
}
return request || $q.when(request);
}
}
});
I have the following code in one of my Controllers to handle a 401 gracefully:
ChannelsService.query(function(response) {
$scope.channels = response;
}, function(error) {
if (error.status == 401) {
$state.go('login');
}
});
and my corresponding service:
myServices.factory('ChannelsService', function ($resource) {
return $resource('/channels', {}, {
query: { method: 'GET', isArray: true },
create: { method: 'POST' }
})
});
I would like to know how to handle 401's globally so that I don't have to work this logic into every controller. Is it an interceptor that I need and if so could someone share some code?
Thanks
For purposes of global error handling, authentication, or any kind of synchronous or asynchronous pre-processing of request or postprocessing of responses, it is desirable to be able to intercept requests before they are handed to the server and responses before they are handed over to the application code that initiated these requests. The interceptors leverage the promise APIs to fulfill this need for both synchronous and asynchronous pre-processing.
You can add an interceptor to the $httpProvider when configuring your application
app.config(['$httpProvider', function($httpProvider) {
$httpProvider.interceptors.push(function($q) {
return {
'responseError': function(rejection){
var defer = $q.defer();
if(rejection.status == 401){
console.dir(rejection);
}
defer.reject(rejection);
return defer.promise;
}
};
});
}]);
As the name already suggests, this will intercept each request and call the provided function if there is a responseError (You could add interceptors for succeeded requests, too)
For further information, see the $http docs
I'm trying to figure out if it is possible to use a $http interceptor to cancel a request before it even happens.
There is a button that triggers a request but if the user double-clicks it I do not want the same request to get triggered twice.
Now, I realize that there's several ways to solve this, and we do already have a working solution where we wrap $http in a service that keeps track of requests that are currently pending and simply ignores new requests with the same method, url and data.
Basically this is the behaviour I am trying to do with an interceptor:
factory('httpService', ['$http', function($http) {
var pendingCalls = {};
var createKey = function(url, data, method) {
return method + url + JSON.stringify(data);
};
var send = function(url, data, method) {
var key = createKey(url, data, method);
if (pendingCalls[key]) {
return pendingCalls[key];
}
var promise = $http({
method: method,
url: url,
data: data
});
pendingCalls[key] = promise;
promise.finally(function() {
delete pendingCalls[key];
});
return promise;
};
return {
post: function(url, data) {
return send(url, data, 'POST');
}
}
}])
When I look at the API for $http interceptors it does not seem to be a way to achieve this. I have access to the config object but that's about it.
Am I attempting to step outside the boundaries of what interceptors can be used for here or is there a way to do it?
according to $http documentation, you can return your own config from request interceptor.
try something like this:
config(function($httpProvider) {
var cache = {};
$httpProvider.interceptors.push(function() {
return {
response : function(config) {
var key = createKey(config);
var cached = cache[key];
return cached ? cached : cached[key];
}
}
});
}
Very old question, but I'll give a shot to handle this situation.
If I understood correctly, you are trying to:
1 - Start a request and register something to refer back to it;
2 - If another request takes place, to the same endpoint, you want to retrieve that first reference and drop the request in it.
This might be handled by a request timeout in the $http config object. On the interceptor, you can verify it there's one registered on the current request, if not, you can setup one, keep a reference to it and handle if afterwards:
function DropoutInterceptor($injector) {
var $q = $q || $injector.get('$q');
var dropouts = {};
return {
'request': function(config) {
// I'm using the request's URL here to make
// this reference, but this can be bad for
// some situations.
if (dropouts.hasOwnProperty(config.url)) {
// Drop the request
dropouts[config.url].resolve();
}
dropouts[config.url] = $q.defer();
// If the request already have one timeout
// defined, keep it, othwerwise, set up ours.
config.timeout = config.timeout || dropouts[config.url];
return config;
},
'requestError': function(reason) {
delete dropouts[reason.config.url];
return $q.reject(reason);
},
'response': function(response) {
delete dropouts[response.config.url];
return response;
},
'responseError': function(reason) {
delete dropouts[reason.config.url];
return $q.reject(reason);
}
};
}