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);
    }