App Engine Channel API and Angular JS
I’ve been using Google App Engine for a while now, and it is hard to go back to traditional java webapp development given how easy it is to deploy a demo version for a customer, and how great some of the tools provided in the App Engine SDK are.
Among those tools is the Channel API. It offers push messaging with only a few lines of code. There are some drawbacks (mainly regarding the reliability of the communications) but it still is a great tool to provide real-time notifications to the user.
I also recently discovered Angular JS. A great Javascript MVP framework (also by Google, it turns out). If you don’t know it already, have a look at their web site. The web-binding part is quite amazing.
Now, I read Angular JS provides some support for Comet notifications, but nothing for the Channel API. On a project I’m developping on my spare time there’s this need to send chat messages to various users.
Here’s how I did it with AngularJS and the Channel API :
- Create an AngularJS service to hold the list of the chats. This is not required but it is the recommended way to share an observable array (which means with two-way data binding) between controllers.
- Initiate the Channel API inside that service. Now there's the trick : for the modifications to the observable array to be pushed to the controllers, we need to use the $rootScope method.
So here’s how the service looks ( the chat repository which receives the chats from the Channel API is
AllChats
):
angular.module('hatChatServices', ['ngResource'])
.factory('Chat', function($resource){
return $resource('chats/:chatId', {}, {
query: {method:'GET', params:{chatId:'latest'}, isArray:true},
});
})
.factory('AllChats', function($rootScope,Chat){
var chatsList = Chat.query();
var unreadChats = {
"PENDING" : 0,
"APPROVED" : 0,
"ANSWERED" : 0
}
//that's where we connect
var socket = new SocketHandler();
socket.onMessage(function(data){
$rootScope.$apply(function () {
var newChat = new Chat(data);
angular.copy(removeChatIfAlreadyExists(newChat, chatsList), chatsList);
chatsList.push(newChat);
unreadChats[newChat.type]++;
});
});
return {
list : chatsList,
unreadChats : unreadChats
}
});
function removeChatIfAlreadyExists(chat, array){
var result = array.filter(function(potentialMatch){
return potentialMatch.id != chat.id;
})
return result;
}
And here’s the definition of the SocketHandler
:
var SocketHandler = function(){
this.messageCallback = function(){};
this.onMessage = function(callback){
var theCallback = function(message){
callback(JSON.parse(message.data));
}
if(this.channelSocket ==undefined){
this.messageCallback = theCallback;
}else{
this.channelSocket.onmessage = theCallback;
}
}
var context = this;
this.socketCreationCallback = function(channelData){
var channel = new goog.appengine.Channel(channelData.channelToken);
context.channelId = channelData.channelId;
var socket = channel.open();
socket.onerror = function(){
console.log("Channel error");
};
socket.onclose = function(){
console.log("Channel closed, reopening");
//We reopen the channel
context.messageCallback = context.channelSocket.onmessage;
context.channelSocket = undefined;
$.getJSON("chats/channel",context.socketCreationCallback);
};
context.channelSocket = socket;
console.log("Channel info received");
console.log(channelData.channelId);
context.channelSocket.onmessage = context.messageCallback;
};
$.getJSON("chats/channel",this.socketCreationCallback);
}