Source: push.js

(function($, rf, jsf) {


  /**
   * Global object accessing general state of Push functionality.
   *
   * Push internally uses Atmosphere to handle connection.
   *
   * @module RichFaces.push
   */
  rf.push = {

    options: {
      transport: "websocket",
      fallbackTransport: "long-polling",
      logLevel: "info"
    },

    /**
     * topics that we subscribed to in current atmosphere connection
     */
    _subscribedTopics: {},

    /**
     * topics to be added to subscription during next invocation updateConnection
     */
    _addedTopics: {},

    /**
     * topics to be removed from subscription during next invocation updateConnection
     */
    _removedTopics: {},

    /**
     * Object that holds mapping of addresses to number of subscribed widgets
     *
     * { address: counter }
     *
     * If counter reaches 0, it is safe to unsubscribe given address.
     */
    _handlersCounter: {},

    /**
     * current Push session identifier
     */
    _pushSessionId: null,
    /**
     * sequence number which identifies which messages were already processed
     */
    _lastMessageNumber: -1,

    /**
     * the URL that handles Push subscriptions
     */
    _pushResourceUrl: null,
    /**
     * the URL that handles Atmosphere requests
     */
    _pushHandlerUrl: null,

    /**
     * Checks whether there are requests for adding or removing topics to the list of subscribed topics.
     *
     * If yes, it reconnects Atmosphere connection.
     *
     * @method updateConnection
     */
    updateConnection: function() {
      if ($.isEmptyObject(this._handlersCounter)) {
        this._disconnect();
      } else if (!$.isEmptyObject(this._addedTopics) || !$.isEmptyObject(this._removedTopics)) {
        this._disconnect();
        this._connect();
      }

      this._addedTopics = {};
      this._removedTopics = {};
    },

    /**
     * Increases number of subscriptions to given topic
     *
     * @method increaseSubscriptionCounters
     * @param topic
     */
    increaseSubscriptionCounters: function(topic) {
      if (isNaN(this._handlersCounter[topic]++)) {
        this._handlersCounter[topic] = 1;
        this._addedTopics[topic] = true;
      }
    },

    /**
     * Decreases number of subscriptions to given topic
     *
     * @method decreaseSubscriptionCounters
     * @param topic
     */
    decreaseSubscriptionCounters: function(topic) {
      if (--this._handlersCounter[topic] == 0) {
        delete this._handlersCounter[topic];
        this._removedTopics[topic] = true;
        this._subscribedTopics[topic] = false;
      }
    },

    /**
     * Setups the URL that handles Push subscriptions
     *
     * @method setPushResourceUrl
     * @param url
     */
    setPushResourceUrl: function(url) {
      this._pushResourceUrl = qualifyUrl(url);
    },

    /**
     * Setups the URL that handles Atmosphere requests
     *
     * @method setPushHandlerUrl
     * @param url
     */
    setPushHandlerUrl: function(url) {
      this._pushHandlerUrl = qualifyUrl(url);
    },

    /**
     * Handles messages transported using Atmosphere
     */
    _messageCallback: function(response) {
      if (response.state && response.state === "opening") {
          this._lastMessageNumber = -1;
          return;
      }
      var suspendMessageEndMarker = /^(<!--[^>]+-->\s*)+/;
      var messageTokenExpr = /<msg topic="([^"]+)" number="([^"]+)">([^<]*)<\/msg>/g;

      var dataString = response.responseBody.replace(suspendMessageEndMarker, "");
      if (dataString) {
        var messageToken;
        while (messageToken = messageTokenExpr.exec(dataString)) {
          if (!messageToken[1]) {
            continue;
          }

          var message = {
              topic: messageToken[1],
              number: parseInt(messageToken[2]),
              data: $.parseJSON(messageToken[3])
          };

          if (message.number <= this._lastMessageNumber) {
            continue;
          }

          var event = new jQuery.Event('push.push.RICH.' + message.topic, { rf: { data: message.data } });

          (function(event) {
            $(function() {
              $(document).trigger(event);
            });
          })(event);

          this._lastMessageNumber = message.number;
        }
      }
    },

    /**
     * Handles errors during Atmosphere initialization and transport
     */
    _errorCallback: function(response) {
      for (var address in this.newlySubcribed) {
        if (this.newlySubcribed.hasOwnProperty(address)) {
          this._subscribedTopics[address] = true;
          $(document).trigger('error.push.RICH.' + address, response);
        }
      }
    },

    /**
     * Initializes Atmosphere connection
     */
    _connect: function() {
      this.newlySubcribed = {};

      var topics = [];
      for (var address in this._handlersCounter) {
        if (this._handlersCounter.hasOwnProperty(address)) {
          topics.push(address);
          if (!this._subscribedTopics[address]) {
            this.newlySubcribed[address] = true;
          }
        }
      }

      var data = {
        'pushTopic': topics
      };

      if (this._pushSessionId) {
        data['forgetPushSessionId'] = this._pushSessionId;
      }

      //TODO handle request errors
      $.ajax({
        data: data,
        dataType: 'text',
        traditional: true,
        type: 'POST',
        url: this._pushResourceUrl,
        success: $.proxy(function(response) {
          var data = $.parseJSON(response);

          for (var address in data.failures) {
            $(document).trigger('error.push.RICH.' + address);
          }

          if (data.sessionId) {
            this._pushSessionId = data.sessionId;

            var url = this._pushHandlerUrl || this._pushResourceUrl;
            url += "?__richfacesPushAsync=1&pushSessionId="
            url += this._pushSessionId;

            var messageCallback = $.proxy(this._messageCallback, this);
            var errorCallback = $.proxy(this._errorCallback, this);

            $.atmosphere.subscribe(url, messageCallback, {
              transport: this.options.transport,
              fallbackTransport: this.options.fallbackTransport,
              logLevel: this.options.logLevel,
              onError: errorCallback
            });

            // fire subscribed events
            for (var address in this.newlySubcribed) {
              this._subscribedTopics[address] = true;
              $(document).trigger('subscribed.push.RICH.' + address);
            }
          }
        }, this)
      });
    },

    /**
     * Ends Atmosphere connection
     */
    _disconnect: function() {
      $.atmosphere.unsubscribe();
    }
  };

  /**
   * jQuery plugin which mades easy to bind the Push to the DOM element
   * and manage plugins lifecycle and event handling
   *
   * @function $.fn.richpush
   */

  $.fn.richpush = function( options ) {
    var widget = $.extend({}, $.fn.richpush);

    // for all selected elements
    return this.each(function() {
      widget.element = this;
      widget.options = $.extend({}, widget.options, options);
      widget.eventNamespace = '.push.RICH.' + widget.element.id;

      // call constructor
      widget._create();

      // listen for global DOM destruction event
      $(document).on('beforeDomClean' + widget.eventNamespace, function(event) {
        // is the push component under destructed DOM element (event target)?
        if (event.target && (event.target === widget.element || $.contains(event.target, widget.element))) {
          widget._destroy();
        }
      });
    });
  };

  $.extend($.fn.richpush, {

    options: {

      /**
       * Specifies which address (topic) will Push listen on
       *
       * @property address
       * @required
       */
      address: null,

      /**
       * Fired when Push is subscribed to the specified address
       *
       * @event subscribed
       */
      subscribed: null,

      /**
       * Triggered when Push receives data
       *
       * @event push
       */
      push: null,

      /**
       * Triggered when error is observed during Push initialization or communication
       *
       * @event error
       */
      error: null
    },

    _create: function() {
      var widget = this;

      this.address = this.options.address;

      this.handlers = {
        subscribed: null,
        push: null,
        error: null
      };

      $.each(this.handlers, function(eventName) {
        if (widget.options[eventName]) {

          var handler = function(event, data) {
            if (data) {
              $.extend(event, {
                rf: {
                  data: data
                }
              });
            }

            widget.options[eventName].call(widget.element, event);
          };

          widget.handlers[eventName] = handler;
          $(document).on(eventName + widget.eventNamespace + '.' + widget.address, handler);
        }
      });

      rf.push.increaseSubscriptionCounters(this.address);
    },

    _destroy: function() {
      rf.push.decreaseSubscriptionCounters(this.address);
      $(document).off(this.eventNamespace);
    }

  });

  /*
   * INITIALIZE PUSH
   *
   * when document is ready and when JSF errors happens
   */

  $(document).ready(function() {
    rf.push.updateConnection();
  });

  jsf.ajax.addOnEvent(jsfErrorHandler);
  jsf.ajax.addOnError(jsfErrorHandler);


  /* PRIVATE FUNCTIONS */

  function jsfErrorHandler(event) {
    if (event.type == 'event') {
      if (event.status != 'success') {
        return;
      }
    } else if (event.type != 'error') {
      return;
    }

    rf.push.updateConnection();
  }

  function qualifyUrl(url) {
    var result = url;
    if (url.charAt(0) == '/') {
      result = location.protocol + '//' + location.host + url;
    }
    return result;
  }

}(RichFaces.jQuery, RichFaces, jsf));