I am trying to setup message notifications in an Angular/Rails app.
When the user logs in, I want to open a SSE connection which will subscribe to a Redis stream and push an alert to the client when the user gets a new message.
I have my SSE setup and working, but cannot figure out a way to reliably close the SSE connection when a user logs out. I am trying to build a service to handle SSE opening and closing:
angular.module('messagesApp')
.factory('StreamHandler', function(CookieHandler, MessageStream){
var StreamHandler = {
set: function(){
var user
user = CookieHandler.get();
MessageStream.get = function(){
var source = new EventSource('/api/v1/messages/count?id='+user.id)
return source
}
},
get: function(){
var source = MessageStream.get()
source.onmessage = function(event) {
//do something
}
},
kill: function(){
var source = MessageStream.get()
source.close()
}
}
return StreamHandler
})
I cannot figure out how to kill the stream that is opened in StreamHandler.set(). My attempt in the kill attribute does not work, possible because calling the getter actually creates a new stream?
I am open to other approaches: I just need a way to set and kill an EventSource stream on user login/logout.
The problem was that I was putting the function that creates an EventSource in my get attribute, instead of putting the actual EventSource object. A few changes makes it work:
.factory('StreamHandler', function(CookieHandler, MessageStream){
var StreamHandler = {
set: function(){
var user
user = CookieHandler.get();
var source = new EventSource('/api/v1/messages/count?id='+user.id)
MessageStream.get = source
},
get: function(){
var source = MessageStream.get
source.onmessage = function(event) {
console.log(event)
}
source.onerror = function(error) {
source.close()
}
},
kill: function(){
var source = MessageStream.get
source.close();
}
}
return StreamHandler
})
Look into Oboe.js - http://oboejs.com/examples
Using Oboe, I basically did (and I guess you don't need to inject $source and $http either in this case):
.factory('MyStreamingResource', ['$resource', '$http',
function($resource, $http) {
return {
stream: function(options, startFn, nodeFn, doneFn) {
oboe('//url/' + 'maybeSomeOptions/?maybe=' + options.passedAbove)
.start(startFn)
.node(options.path, nodeFn)
.done(doneFn);
}
};
}
]);
Then simply injected it and called from some controllers with:
MyStreamingResource.stream({
passedAbove: 'foo',
path: 'items.*'
},
// start callback
function(status, headers){
// console.dir(headers);
// this.abort(); // could be called from here too
},
// node callback (where your data is going to be streamed to)
function(data){
if(data !== null) {
console.dir(data);
//this.abort();
}
},
// done (if you really want to wait)
function(parsedJson){
// ...
});
Very similar to other services that you'd see with $http, but instead you have a few more callbacks to consider.
Oboe.js worked like a charm for me and the Golang go-json-rest package streaming a response (even with invalid JSON - which is quite common with streams since they aren't complete and won't have closing tags or will have extra commas, etc.). Just ensure the browser version of the script gets included on your page and you can just call it where ever. I spent a good while searching on how to do this with AngularJS, but there just didn't seem to be a straight forward facility for it. If it is at all possible.
Of course from here you could probably figure out how to make it work for your needs...Update something in the $scope, etc.
Related
I am fairly new to angularjs, and would like to ask a few questions.
I am working on a project where I need to get a form object from the server. The form is a complicated tree object with many layers, and I have created 4 different components/tabs to bind to the corresponding objects. I had created a Service to get the data.
angular.module('myService', ['restangular'])
.factory('FormService', ['Restangular', '$q', function(Restangular, $q) {
function FormService() {
var self = this;
self.form = null;
self.getForm = function getForm(id)
{
var deferred = $q.defer();
if (self.form !== null)
{
deferred.resolve(self.form);
console.log("Cache!");
}
else {
Restangular.one('form', id).get()
.then(function successCallback(response)
{
self.form = response;
deferred.resolve(response);
console.log("from server!");
}, function errorCallback(response) {
deferred.reject(response);
console.log("error, cannot resolve object");
});
}
return deferred.promise;
}
return new FormService();
}])
});
Then I had my components all with similar config below:
angular.module('page1Summary', ['formService']).component('page1Summary', {
templateUrl: 'page1-summary/page1-summary.template.html',
controller: ['FormService', function Page1SummaryController(FormService) {
var ctrl = this;
// ******* Init Params Start *********** //
this.$onInit = function() {
// init value when object ready
FormService.getForm()
.then(
/* on success */
function successCallback(data) {
console.log("page1-summary init");
ctrl.form = data;
console.log("page1-summary got the data");
},
/* on error */
function errorCallback(data)
{
console.log("failed to get form");
}
);
}
/* other stuff here */
}
I was printing either "cache!" or "from server" on the getForm service. So that I can figure out whether I am pulling the data from server or memory. However, everytime I refresh, the result is different. Sometimes, the data saved in the local variable in service, and got "cached", but sometimes, some of my pages will get the data "from server".
I would like to know what is going wrong? I thought only the first time the service would get from server, but it seems like it is not the case.
Can someone please help me out and point out what I did wrong?
Thanks in advance!
You are caching your result into self.form.
self.form is again a variable FormSerivce Factory member.
It will cache the result till you do not refresh the page.
Once you refresh the page the value in self.form will get reset just like all the other variable in your application.
What you want is instead of caching result in self.form, cache it in localstorage.
So you can get the result back even after your page refresh.
In our application, we have an search input field. Typically a request is sent while the user types (a la Google Instant) and the results are displayed.
Obviously, the following can happen:
User types, which results in ajaxRequest1
User continues typing, resulting in ajaxRequest2
results2 corresponding to ajaxRequest2 are received and displayed
After this, results1 corresponding to ajaxRequest1 are received. Obviously, since ajaxRequest2 was sent after ajaxRequest1, we only care about results2, not results1.
EDIT: The obvious answer here is "Use debounce". For reasons of confidentiality and brevity, I'll just say here that it won't work in our particular scenario. I know what debounce does and I have considered it.
In pseudo-code, we used to handle it like this:
$scope.onInput = function() {
var inputText = getInput();
SearchService.search(inputText).then(function(results) {
// only display if input hasn't changed since request was sent
if(inputText === getInput()) {
displayResults(results);
}
});
};
Since this involves a lot of boilerplate and looks ugly, we moved to a pattern where the SearchService manages things a bit better
$scope.onInput = function() {
var inputText = getInput();
SearchService.search(inputText).then(function(results) {
displayResults(results);
});
}
function SearchService() {
var cachedSearchDeferred;
this.search = function(inputText) {
if(cachedSearchDeferred) {
//means there's an unresolved promise corresponding to an older request
cachedSearchDeferred.reject();
}
var deferred = $q.deferred();
$http.post(...).then(function(response) {
// saves us having to check the deferred's state outside
cachedSearchDeferred = null;
deferred.resolve(response.data);
});
cachedSearchDeferred = deferred;
return deferred.promise;
}
}
This works fine. The SearchService creates a deferred containing the promise corresponding to the most recent call to SearchService.search. If another call is made to SearchService.search the old deferred is rejected and a new deferred is created corresponding to the new call.
Two questions:
Is this a good pattern to do what we need - essentially request locking? We want to ensure that only the most recent request's promise resolves successfully
If we had other SearchService methods that needed to behave similarly, then this deferred boilerplate needs to be inside every method. Is there a better way?
#Jayraj depends how sophisticated you want to make your http api. You can go very deep, but if I understand your question you are looking for a http timeout interceptor. Using Angular $httpProvider you can register a custom interceptor which needs to return a response and request.
I should note I've frankensteined this from pieces of different code bases so I don't take credit for code, but it is early morning and would need to go find the source in my libraries, but to help best practice directionally here goes.
ANGULAR.JS EXAMPLE
angular team give this example
$httpProvider.interceptors.push(function($q, dependency1, dependency2) {
return {
'request': function(config) {
// same as above
},
'response': function(response) {
// same as above
}
};
});
create a factory object that holds you http endpoint configuration i.e a config file that with a server component and an endpoint that identified the UID for the endpoint i.e. where does it go and who is sending it
(function() {
'use strict';
var config = {
server: {
url: null
},
endpoint: {
url: null,
uuid: null,
}
};
return angular.module('matrixme.config', [
]).constant('config', config);
})();
for brevity sake I will leave out the service provider code, but you will need to build an REST api service provider, which you then inject into all relevant classes. The provider will effectively configure your config object e.g. user, articles and will serve as home for api calls.
You create your own interceptor and inject as such:
(function() {
'use strict';
angular.module('matrixme.api', ['matrixme.config'])
.config(['$httpProvider', function($httpProvider) {
$httpProvider.interceptors.push('timeoutInterceptor');
}]);
})();
Build the injector before you inject :) I have not tested this but really answering your question of best practice. So this is directional, but you would then create your request and response. You can build multiple custom interceptors e.g. uuid, auth timeout, etc.
(function() {
'use strict';
TimeoutInterceptor.$inject = ['$timeout', '$q', '$rootScope', 'request'];
function TimeoutInterceptor($timeout, $q, $rootScope, request) {
return {
request: function(config) {
if ((config.url)) {
config._ttl = config._ttl ? Math.min(2000, config._ttl * 2) : 2000;
config.timeout = $timeout(function() {
config._isTimeout = true;
}, config._ttl);
}
return config;
},
response: function(response) {
if (response.config.timeout) {
$timeout.cancel(response.config.timeout);
$rootScope.serverStatus = 0;
}
return response;
},
};
}
angular.module('matrixme.api')
.factory('timeoutInterceptor', TimeoutInterceptor);
})();
It turns out there already exists a solution for this: RxJS. The example in their README is almost this exact scenario.
const $input = $('#input');
/* Only get the value from each key up */
var keyups = Rx.Observable.fromEvent($input, 'keyup')
.pluck('target', 'value')
.filter(text => text.length > 2 );
/* Now debounce the input for 500ms */
var debounced = keyups
.debounce(500 /* ms */);
/* Now get only distinct values, so we eliminate
the arrows and other control characters */
var distinct = debounced
.distinctUntilChanged();
/* Once that is created, we can tie together the
distinct throttled input and query the service.
In this case, we'll call flatMapLatest to get
the value and ensure we're not introducing any
out of order sequence calls. */
const suggestions = distinct
.flatMapLatest(() => {
// Do XHR and return a promise
// flatMapLatest will always use the latest one
});
There's also RxJS for Angular which adds things to the $scope object.
Let's say I have a service deal with Firebase operation:
angular.module('myApp').factory('taskOfferService', ['FURL', '$firebaseArray', '$firebaseObject', '$q', 'taskService', taskOfferService]);
function taskOfferService(FURL, $firebaseArray, $firebaseObject, $q, taskService) {
var ref = new Firebase(FURL);
var Offer = {
acceptOffer: function(taskId, offerId, runnerId) {
var offerRef = ref.child('taskOffers').child(taskId).child(offerId);
var taskUpdate = $q.defer();
offerRef.update({accepted: true}, function(error) {
if (error) {
console.log('Update offer accepted value failed!');
} else {
var taskRef = ref.child('tasks').child(taskId);
taskRef.update({status: "assigned", runner: runnerId}, function(error) {
if (error) {
console.log('Update task status failed!');
taskUpdate.reject(false);
} else {
taskUpdate.resolve(true);
}
});
}
});
return taskUpdate.promise;
},
};
return Offer;
}
})();
And I have a controller need to call this service and need to wait for a promise when update successful - to call the toaster.pop:
$scope.acceptOffer = function(offerId, runnerId) {
taskOfferService.acceptOffer($scope.selectedTask.$id, offerId, runnerId).then(function(){
toaster.pop('success', 'Offer is accepted.');
});
};
This code work and it is follow the suggestion from Firebase docs. But as you can see in my service, in order to get a promise, I need to use the update callback inside an update callback...My point is, Firebase SDK do not return a promise when done and I need to create this promise and it is A LOT OF CODES...
If I use firebaseObject (angularFire 1.0), it will run into issues listed here: saving new property overwrites firebase object
And using ANTIPATTERN according to #sinan, the code can be way clearer:
acceptOffer: function(taskId, offerId, runnerId) {
var o = $firebaseObject(ref.child('taskOffers').child(taskId).child(offerId));
o.$loaded().then(function(){
o.accepted = true;
o.$save().then(function(){
var oTask = $firebaseObject(ref.child('tasks').child(taskId));
oTask.$loaded().then(function(){
oTask.status = "assigned";
oTask.runner = runnerId;
oTask.$save();
});
})
},
The point is, using "ANTIPATTERN", I can utilize $save() - which return an promise so no $q service is need inside my firebase service. It looks a lot clearer in my opinion. Both method works.
BUT, according to doc:
"The $loaded() method should be used with care as it's only called once after initial load. Using it for anything but debugging is usually a poor practice."
I just find myself using $loaded A LOT! Please advice the best way to go about this.
Firebase has a JavaScript SDK that exposes the platform's functionality to JavaScript environments. AngularFire is a library on top of that SDK, that makes it easier to bind Firebase data to an AngularJS web interface.
Your example here is a plain data manipulation operation. You're not binding the data to the screen, so you should not need AngularFire. I also see no need for using promises.
As far as I can tell, this does the exact same thing as your last script:
acceptOffer: function(taskId, offerId, runnerId) {
var offer = ref.child('taskOffers').child(taskId).child(offerId);
offer.update({ accepted: true }, function() {
var task = ref.child('tasks').child(taskId);
task.update({ status: "unassigned", runner: runnerId });
});
}
Not only is this shorter, it also prevents downloading the data, just to then update it.
And the best part? Since AngularFire is built on top of the regular Firebase JavaScript SDK, they interoperate perfectly. So if in another somewhere you actually display the task or offer through an AngularJS view that watches to a $firebaseObject, it will show the updated value straight away.
Update
If you need to do something when the acceptOffer method is done saving, you can pass in a callback:
acceptOffer: function(taskId, offerId, runnerId, callback) {
var offer = ref.child('taskOffers').child(taskId).child(offerId);
offer.update({ accepted: true }, function(error) {
if (!error) {
var task = ref.child('tasks').child(taskId);
task.update({ status: "unassigned", runner: runnerId }, callback);
}
else {
callback(error)
}
});
}
You then invoke it like:
taskOfferService.acceptOffer($scope.selectedTask.$id, offerId, runnerId, function(error) {
if (!error) {
toaster.pop('success', 'Offer is accepted.');
}
else {
console.error('Something went wrong: '+error);
}
});
You could definitely also promisify the acceptOffer method. But this is not necessary.
I asked the wrong question yesterday (and got a goodanswer that worked), but am realizing it's not what I needed. I need to be able to retrieve JSON data (preferably once), store it, and access it throughout my service. The challenge I'm having is that all the examples I can find talk about using JSON and passing to the app/controller, whereas in this case I need to get it, check it, and then it dictates what my module/service does.
For instance, I have my App and Controller, and then I have a module such as (this is psuedo-code, not meant to run):
angular.module("myModule")
.service("myService1", function($q, myService2, $http) {
this.getModel = function() {
return {
title: "My Title",
desc: "My Desc"
options: function () {
if (condition A)
return "option1";
else
return "option2";
}
};
};
})
.service("myService2", function($q, $http) {
this.getCfgInfo = function () {
var defer = $q.defer();
$http.get("my/json/url").then(function(response) {
defer.resolve(response.data);
});
return defer.promise;
};
})
In this example, I'm wanting to get the JSON, and use it within myService1 for both literal values (title, desc) as well as for conditions (condition A within the if).
I know I can do something like this (thanks to Joel for helping yesterday):
service("myService1", function($q, myService2, $http) {
// get a promise object for the configuration info
var cfgProm = rtDataMapper.getCfgInfo()
this.getModel = function() {
return {
title: cfgProm.then(function(response) {
return response.JSON_NAME;
}),
and it works fine as I've got the title mapped back into my model and there is a watch(), but I'm stumped as to how I get, store, and use the JSON within the service itself as a conditional (i.e. if (condition A) where condition A is coming from the JSON. Trying to wrap these in .then() doesn't seem to make sense, or at least I can't figure out how to do it.
I'm new to Angular and am attempting to modify some code that was left to us. I'm guessing I don't need the myService2 just to get the JSON. Can anyone help point me in the right direction? I've spent several hours online but can't seem to find a relevant reference/example.
Thanks
Live demo (click).
I'm having the service immediately get the data when it is injected (that code will only run once no matter how many times you inject it). That's nice because you won't have to call a function to get the data - it's called for when creating the service.
Your service method that returns that data will need to return the promise of the data, of course, since you aren't guaranteed that it will have come through when you ask for it. You can pass arguments to that method to use to determine your conditions. All you need to do for that is use promise.then in the method and resolve the promise with the modified data. Since that method is returning the promise already, the modification will be updated on the resolve. See all of this below and in the demo.
var app = angular.module('myApp', []);
app.controller('myCtrl', function($scope, myService) {
myService.getData(15).then(function(data) {
$scope.myData = data;
});
});
app.factory('myService', function($q, $timeout) {
//this code only runs once when you first inject the service
//get data immediately
var deferred = $q.defer();
$timeout(function() { //simulate ajax call
var data = { //ajax response data
foo: 15,
bar: 'Some data!'
};
data = modifyData(data, 1);
deferred.resolve(data);
}, 500);
function modifyData(data, fooVal) {
if (data.foo === fooVal) {
data.baz = 'Conditional data!';
}
return data;
}
var myService = {
//data can be modified when it comes from the server,
//or any time you call this function
getData: function(fooVal) {
if (fooVal) { //if you want to modify the data
deferred.promise.then(function(data) {
data = modifyData(data, fooVal);
deferred.resolve(data);
});
}
return deferred.promise;
}
};
return myService;
});
I have defined two AngularJS services ... one is for the YouTube Player API, and other for the YouTube iFrame Data API. They look like this:
angular.module('myApp.services',[]).run(function() {
var tag = document.createElement('script');
tag.src = "//www.youtube.com/iframe_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
})
.factory('YtPlayerApi', ['$window', '$rootScope', function ($window, $rootScope) {
var ytplayer = {"playerId":null,
"playerObj":null,
"videoId":null,
"height":390,
"width":640};
$window.onYouTubeIframeAPIReady = function () {
$rootScope.$broadcast('loadedApi');
};
ytplayer.setPlayerId = function(elemId) {
this.playerId=elemId;
};
ytplayer.loadPlayer = function () {
this.playerObj = new YT.Player(this.playerId, {
height: this.height,
width: this.width,
videoId: this.videoId
});
};
return ytplayer;
}])
.factory('YtDataApi', ['appConfig','$http', function(cfg,$http){
var _params = {
key: cfg.youtubeKey
};
var api="https://www.googleapis.com/youtube/v3/";
var yt_resource = {"api":api};
yt_resource.search = function(query, parameters) {
var config = {
params: angular.extend(angular.copy(_params),
{maxResults: 10,
part: "snippet"}, parameters)
};
return $http.get(api + "search?q=" + query, config);
};
return yt_resource;
}]);
(also note that the 'setPlayerId' function of my player service is called by a custom directive ... but that's not important for my question).
So, here's the issue. I need to ensure that the Player API code is loaded before I set the video id and create the player, which is why I have it broadcasting the 'loadedApi' message. And this works great, if I then in my controller pass a hard-coded video id, like this:
function ReceiverCtrl($scope,$rootScope,$routeParams,ytplayer,ytdataapi) {
$scope.$on('loadedApi',function () {
ytplayer.videoId='voNEBqRZmBc';
ytplayer.loadPlayer();
});
}
However, my video IDs won't be determined until I make an API call with the data api service, so I ALSO have to ensure that the results of that call have come back. And that's where I'm running into problems ... if I do something like this:
$scope.$on('loadedApi',function () {
ytdataapi.search("Mad Men", {'topicId':$routeParams.topicId,
'type':'video',
'order':'viewCount'})
.success(function(apiresults) { // <-- this never gets triggered
console.log(apiresults); // <-- likewise, this obviously doesn't either
});
});
Then the interaction with the data service never happens for some reason. I know the data service works just fine, for when I un-nest it from the $on statement, it returns the api results. But sometimes latency makes it so that the results don't come back fast enough to use them in the player service. Any thoughts on what I can do to make the data search after receiving the message that the player API is ready, but still keep the two services as two separate services (because other controllers only use one or the other, so I don't want them dependent on each other at the service level)?
Figured it out; I had to call $scope.$apply(), like this:
function ReceiverCtrl($scope,$rootScope,$routeParams,ytplayer,ytdataapi) {
$scope.$on('loadedApi',function () {
ytdataapi.search("",{'topicId':$routeParams.topicId,'type':'video','maxResults':1,'order':'viewCount'}).success(function(apiresults) {
ytplayer.videoId=apiresults.items[0].id.videoId;
ytplayer.loadPlayer();
});
$scope.$apply();
});
}
Is there anyone who could shed light on why this works, though? $scope.$digest() also works ... but I thought those methods were only used when you need to update bindings because of some javascript code that Angular isn't aware of. Is the nesting I've got here doing that (I wouldn't think it should, as my ytdataapi service is using $http)?