cf2:CometD

Purpose

This component provides a JavaScript API that enables web applications to use CometD for asynchronous communication between a web client and a web server. CometD implements the Bayeux protocol that allows for transporting messages via named channels. Messages can be transferred from a server to a client, from a client to a server, and from a client to a client (via the server). Refer to CometD.org for more information.

The following API is based on the CometD 2 JavaScript API.

Note:

MCS does not support synchronous requests.

Exported Features

cf2:CometD

Imported Features

n/a

Messages and channels

A Bayeux message is a JSON encoded object transferred between a web client and a server. Each message contains a sequence of name value pairs that represent fields and values. Values may be strings, numbers, boolean values, or JSON objects or arrays. Messages can be transported using AJAX or the WebSocket protocol and are wrapped in a transport-specific envelop.

Messages are published via channels. Clients need to subscribe to channels in order to receive published messages. Channels are identified by their names. The channel name consists of an initial slash "/" character followed by an optional sequence of path segments separated by a slash character (e.g. /foo/foooo), and can contain wildcards (e.g. /foo/*). There are two special types of channels: meta channels and service channels. Meta channels are used by the Bayeux protocol itself. The name of a meta channel must start with /meta/. Service channels are used for client to server communication. The name of a service channel must start with /service/.

JavaScript

The V$.net.cometd object provides the following methods:

CometD(name)

The CometD object constructor.

Parameter Description Type
name The name of the newly created object. String
void configure(configuration)

Configures a communication channel with a Bayeux server. The configuration parameters are passed via an object that must contain a mandatory url field defining the URL of the Bayeux server.

Parameter Description Type
configuration The configuration parameters. Map

The following table lists the configuration parameters.

Name Description Type Default Options Use
appendMessageTypeToURL Indicates whether or not the Bayeux message type (i.e. handshake, connect, disconnect) should be appended to the URL of the Bayeux server. xs:boolean true false, true Optional
autoBatch Indicates whether or not multiple publishes that gets queued up should be sent as a single batch. xs:boolean false false, true Optional
backoffIncrement The number of milliseconds to use to increment the backoff time every time a connection with the server fails. The client will attempt to connect to the server after the backoff time elapses. xs:nonNegativeInteger 1000 Optional
logLevel The log level. xs:string info debug, info, warn Optional
maxBackoff The time, in milliseconds, after which the backoff time is not incremented anymore. xs:nonNegativeInteger 60000 Optional
maxConnections The maximum number of connections used to connect to the server. xs:nonNegativeInteger 2 Optional
maxNetworkDelay The maximum number of milliseconds to wait before aborting a request to the server. xs:nonNegativeInteger 10000 Optional
requestHeaders An object containing the request headers to be sent in every request, for example: {"Foo-Header":"Foo-Header-Value"}. xs:string {} Optional
reverseIncomingExtensions Indicates whether or not the incoming extensions should be called in reverse order with respect to the registration order. xs:boolean true false, true Optional
url The URL of the Bayeux server to connect to. xs:string Required
void init(configuration, handshakeProps)

Configures and establishes a communication channel with a Bayeux server.

A handshake message is used to negotiate the connection type, authentication and other parameters between the client and the server. After the client and the server agree on the connection conditions, the connection is established by sending a connection message.

Parameter Description Type
configuration The configuration parameters. Please see the description of the configure() method for the list of available parameters. Map
handshakeProps An optional object containing additional data to be merged with the handshake message. Map
void handshake(handshakeProps)

Initiates the Bayeux protocol handshake with a server.

Parameter Description Type
handshakeProps An optional object containing additional data to be merged with the handshake message. Map
void disconnect(sync, disconnectProps)

Disconnects from a Bayeux server.

Parameter Description Type
sync Indicates whether or not to attempt to perform a synchronous disconnect. Boolean
disconnectProps An optional object containing additional data to be merged with the disconnect message. Map
void startBatch()

Marks the start of a batch of messages to be sent to a server in a single request. Please note that messages are held in a queue until the endBatch() method is called, and therefore if the startBatch() method is called multiple times, the endBatch() method must be called the same number of times to close and send the batch of messages.

void endBatch()

Marks the end of a batch of messages to be sent to a server in a single request.

void batch(scope, callback)

Executes the given callback within startBatch() and endBatch() calls.

The callback can be a function, or a method of an object. In the first case, the scope parameter is ignored and the callback parameter is of type Function. In the latter case, the scope parameter is an object, and the callback is the name of the method provided by the object (i.e. the callback parameter is of type String).

Parameter Description Type
scope The scope of the callback. Object
callback The callback function to be executed within startBatch() and endBatch() calls. Function or String
Object addListener(channel, scope, callback)

Adds a listener to a Bayeux channel that executes the given callback every time a message arrives from the channel. The method returns a subscription object and can be called before the handshake() method. The addListener() method is intended to be used to listen to meta channels, but it may also be used to listen to service channels. This method should not be used with regular channels - the subscribe() method should be used instead.

Parameter Description Type
channel The channel the listener is interested in. String
scope An optional scope of the callback. Object
callback A callback function to execute when a message arrives. Refer to the description of the batch() method for more information. Function or String
void removeListener(subscription)

Removes an existing listener created with the addListener() method.

Parameter Description Type
subscription The listener to remove, i.e. the subscription object returned by the addListener() method. Object
void clearListeners()

Removes all listeners/subscriptions registered with addListener() or subscribe().

Object subscribe(channel, scope, callback, subscribeProps)

Subscribes to a channel. The given callback will be executed every time a message arrives from the channel. The subscribe() method can only be called after the handshake() method. This method is intended to be used to subscribe to regular channels, but may also be used to subscribe to service channels (the addListener() method is preferable). An attempt to subscribe to a meta channel will cause the server to return an error.

Parameter Description Type
channel The channel to subscribe to. String
scope An optional scope of the callback. Object
callback A callback function to call when a message arrives. Refer to the description of the batch() method for more information. Function or String
subscribeProps An optional object containing additional data to be merged with the subscribe message. Map
void unsubscribe(subscription, unsubscribeProps)

Removes an existing subscription obtained with the subscribe() method.

Parameter Description Type
subscription The subscription to remove, i.e. the subscription object returned by the subscribe method. Object
unsubscribeProps An optional object containing additional data to be merged with the unsubscribe message. Map
void clearSubscriptions()

Removes all subscriptions that were created using the subscribe() method, but does not remove the listeners added with the addListener() method.

void publish(channel, content, publishProps)

Publishes a message to a channel.

Parameter Description Type
channel The channel to publish the message to. String
content The content of the message. Map
publishProps An optional object containing additional data to be merged with the published message. Map
String getStatus()

Returns a string representing the status of the current communication channel. The following values can be returned:

Value Description
connected The connection is established and communication is possible.
connecting The connection has not yet been established.
disconnected The connection has been closed.
disconnecting The connection is closing down.
handshaking The client and the server are negotiating connection conditions, e.g. the connection type, authentication, etc.
boolean isDisconected()

Returns a boolean value indicating whether or not the current communication instance has been disconnected.

void setBackoffIncrement(period)

Sets the backoff period. It is used to increase the backoff time that specifies how long the client should wait before resending an unsuccessful or failed message. The default value of the period parameter is 1 second, which means that if there is a persistent failure the retries will happen after 1 second, then after 2 seconds, then after 3 seconds, etc.

Parameter Description Type
period The backoff period to set; in milliseconds. Integer
int getBackoffIncrement()

Returns the number of milliseconds used to increase the backoff time.

int getBackoffPeriod

Returns the backoff period that specifies how long the client should wait before resending an unsuccessful or failed message.

void setLogLevel(level)

Sets the log level. Valid values of the level parameter are: 'error', 'warn', 'info' and 'debug'; from less verbose to more verbose.

Parameter Description Type
level The log level. String
boolean registerExtension(name, extension)

Registers an extension. This method returns a boolean value of 'true' if the extension was registered; 'false' otherwise. An extension can modify a message just before it is sent, or immediately after it was received. An extension is a JavaScript object with four optional methods:

  • outgoing(message) - to be called just before a message is sent;

  • incoming(message) - to be called immediately after a message was received;

  • registered(name, cometd) - to be called when the extension is registered (cometd is a reference to the CometD object);

  • unregistered() - to be called when the extension is unregistered.

Parameter Description Type
name The name of the extension. String
extension The extension to register. Object
boolean unregisterExtension(name)

Unregisters an extension previously registered using the registerExtension() method. The method returns a boolean value of 'true' if the extension was unregistered; 'false' otherwise.

Parameter Description Type
name The name of the extension to unregister. String
Extension getExtension(name)

Returns the extension registered with the given name. If no extension with the given name has been registered, the method returns null.

Parameter Description Type
name The name of the extension to find. String
void send(message)

Sends a complete Bayeux message. Extensions can use this method to send messages, for example, to re-send a message that has already been sent but must be sent again for some reason.

Parameter Description Type
message The Bayeux message to send. Map
void receive(message)

Receives a message.

Parameter Description Type
message The message received from the server. Map
String getName()

Returns the name assigned to the current CometD object. If no name has been explicitly passed to the constructor, the method returns a string value of 'default'.

String getClientId()

Returns the ID of the client assigned by a Bayeux server during handshake. A client ID is an random sequence of alphanumeric characters generated by the server.

String getURL()

Returns the URL of the current Bayeux server.

Example

In the following example, the API is used to create a chat room client application. This example requires a CometD compliant server with the CometD 2.x demo servlet. The servlet can be downloaded from http://download.cometd.org. When the server receives a message from one of the chat clients, it immediately sends the message to the other clients. Clients communicate with the server using the following channels: service/members, service/privatechat, chat/demo and members/demo.

<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/2002/06/xhtml2"
  xmlns:ui="http://www.volantis.com/xmlns/2009/07/cf2/ui"
  xmlns:cf2="http://www.volantis.com/xmlns/2009/07/cf2"
  xmlns:cst="http://www.volantis.com/xmlns/2009/07/cf2/template"
  xmlns:mcs="http://www.volantis.com/xmlns/2006/01/xdime/mcs"
  xmlns:sel="http://www.w3.org/2004/06/diselect"
  xmlns:xforms="http://www.w3.org/2002/xforms"
  xmlns:si="http://www.volantis.com/xmlns/2006/01/xdime2/si"
  xmlns:event="http://www.w3.org/2001/xml-events">
  <head>
    <title>CometD</title>
    <style type="text/css">
      ui|button {
        padding: 2px 4px 2px 4px;
        background-color: black;
        margin-right: 4px;
        font-weight: bolder;
        color: white;
        font-family: sans-serif;
      }
      .content {
        background-color: #dddddd;
        height: 200px;
      }
      .title {
        padding: 3px;
      }
      .member-name {
        color: #000;
        padding-left: 5px;
      }
      #input {
        clear: both;
        padding: 0.2em;
      }
      .from {
        font-weight: bolder;
        color: #000;
        padding: 5px;
      }
      .text {
        color: #000;
      }
      .membership {
        color: #000;
        font-style: italic;
      }
      .msg-col {
        width: 70%;
      }
      .user-col {
        width: 30%;
      }
      .chat-table {
        width: 100%;
        background-color: #eaeaea;
      }
      #chat {
        height: 100%;
        overflow: auto;
      }
      #members {
        height: 100%;
        overflow: auto;
      }
    </style>
    <xforms:model id="chatForm">
      <xforms:instance>
        <si:instance>
          <si:item name="username"/>
          <si:item name="phrase"/>
          <si:item name="server">http://localhost:9080/cometd/cometd</si:item>
        </si:instance>
      </xforms:instance>
    </xforms:model>
    <sel:if expr="mcs:feature('cf2:CometD')">
      <mcs:script src="/scripts/cometd-chat.mscr"/>
      <mcs:handler id="disconnect-handler" type="text/javascript">if (chat.isConnected()) {chat.leave();}</mcs:handler>
      <event:listener observer="body" handler="#disconnect-handler" event="unload"/>
    </sel:if>
  </head>
  <body id="body">
    <div>
      <sel:select>
        <sel:when expr="mcs:feature('cf2:CometD')">
          <ui:box id="chatroom">
            <ui:prototype id="member">
              <cst:template id="member-tmpl">
                <div class="member-name"><cst:value path="Name"/></div>
              </cst:template>
            </ui:prototype>
            <ui:prototype id="message">
              <cst:template id="message-tmpl">
                <div>
                  <cst:switch path="Type">
                    <cst:case string="membership">
                      <span class="membership"><span class="from"><cst:value path="FromUser"
                          /></span><span class="text"><cst:value path="Text"/></span></span>
                    </cst:case>
                    <cst:case string="private">
                      <span class="private"><span class="from"><cst:value path="FromUser"
                          /></span><span class="text">[private] <cst:value path="Text"
                        /></span></span>
                    </cst:case>
                    <cst:otherwise>
                      <span class="from"><cst:value path="FromUser"/></span><span class="text"
                          ><cst:value path="Text"/></span>
                    </cst:otherwise>
                  </cst:switch>
                </div>
              </cst:template>
            </ui:prototype>
            <table class="chat-table">
              <tr>
                <td class="msg-col title">Messages:</td>
                <td class="user-col title">Users:</td>
              </tr>
              <tr>
                <td class="msg-col content"><ui:box id="chat"/></td>
                <td class="user-col content"><ui:box id="members"/></td>
              </tr>
            </table>
            <ui:box id="input">
              <ui:box id="join" initial-displayed-state="true">
                <table>
                  <tbody>
                    <tr>
                      <td/>
                      <td>Server</td>
                      <td>
                        <xforms:input id="server" model="chatForm" ref="server">
                          <xforms:label/>
                        </xforms:input>
                      </td>
                      <td/>
                    </tr>
                    <tr>
                      <td/>
                      <td>Enter Chat Nickname</td>
                      <td>
                        <xforms:input id="username" model="chatForm" ref="username">
                          <xforms:label/>
                        </xforms:input>
                      </td>
                      <td>
                        <ui:button id="joinButton">
                          <span>Join</span>
                          <cf2:on event="cf2:activate">
                            <cf2:param name="joined" property="joined#displayed"/>
                            <cf2:param name="join" property="join#displayed"/>
                            if(chat.join(V$E('username').value)) {
                              joined.set(true);
                              join.set(false); 
                            } else {
                              joined.set(false); join.set(true);
                            }</cf2:on>
                        </ui:button>
                      </td>
                    </tr>
                  </tbody>
                </table>
              </ui:box>
              <ui:box id="joined" initial-displayed-state="false"> Chat: <xforms:input id="phrase"
                  model="chatForm" ref="phrase">
                  <xforms:label/>
                </xforms:input>
                <ui:button id="sendButton">
                  <span>Send</span>
                  <cf2:on event="cf2:activate"> chat.send(); </cf2:on>
                </ui:button>
                <ui:button id="leaveButton">
                  <span>Leave</span>
                  <cf2:on event="cf2:activate" set="joined#displayed" boolean="false"/>
                  <cf2:on event="cf2:activate" set="join#displayed" boolean="true"/>
                  <cf2:on event="cf2:activate"> chat.leave(); </cf2:on>
                </ui:button>
              </ui:box>
            </ui:box>
          </ui:box>
          <mcs:br/>
          <div>Tip: Use username[,username2]::text to send private
            messages</div>
        </sel:when>
        <sel:otherwise>
          <div> Feature cf2:CometD is not supported on this device. </div>
        </sel:otherwise>
      </sel:select>
    </div>
    <div> This example requires CometD compliant server with demo servlet from CometD
      2.x distribution. CometD 2.x can be downloaded at <a href="http://download.cometd.org"
        >http://download.cometd.org</a>. The client communicates with the server using the following
      channels: "service/members", "service/privatechat", "chat/demo", "members/demo". </div>
  </body>
</html>

The cometd-chat.mscr script must import the cf2:CometD feature and contain the following JavaScript code:

V$C.starting(function(c){
  chat = new Chat(c);
});

Chat = V$.Class.create();

Chat.methods({
  initialize: function(c) {
    this._memberPrototype = c.get('member');
    this._membersContainer = c.get('members');
    this._messagePrototype = c.get('message');
    this._messagesContainer = c.get('chat');
    this._joined = c.get('joined');
    this._join = c.get('join');
    this._wasConnected = false;
    this._connected = false;
    this._lastHandshakeFailed = false;
    var _self = this;
    V$.net.cometd.addListener('/meta/handshake', function(message) {_self._metaHandshake(message);});
    V$.net.cometd.addListener('/meta/connect', function(message) {_self._metaConnect(message);});
  },

  join: function(username) {
    this._disconnecting = false;
    this._username = username;
    if (!this._username) {
      alert('Please enter a username');
      return false;
    }
    var cometdURL = V$E('server').value;
    if (cometdURL.length == 0) {
      alert('Please enter a server address');
      return false;
    }
    V$.net.cometd.websocketEnabled = true;
    V$.net.cometd.configure({
      url: cometdURL
    });
    V$.net.cometd.handshake();
      return true;
    },

    leave: function() {
      var _self = this;
      V$.net.cometd.batch(function() {
        V$.net.cometd.publish('/chat/demo', {
          user: _self._username,
          membership: 'leave',
          chat: _self._username + ' has left'
        });
        _self._unsubscribe();
      });
      V$.net.cometd.disconnect();
      this._clearMembers();
      this._username = null;
      this._lastUser = null;
      this._disconnecting = true;
    },

    send: function() {
      var phrase = V$E('phrase');
      var text = phrase.value;
      phrase.value = '';
      if (!text || !text.length) return;
      var colons = text.indexOf('::');
      var _self = this;
      if (colons > 0) {
        V$.net.cometd.publish('/service/privatechat', {
          room: '/chat/demo',
          user: _self._username,
          chat: text.substring(colons + 2),
          peer: text.substring(0, colons)
        });
      } else {
        V$.net.cometd.publish('/chat/demo', {
          user: _self._username,
          chat: text
        });
      }
    },

    receive: function(message) {
      var fromUser = message.data.user;
      var membership = message.data.membership;
      var text = message.data.chat;
      var isPrivate = (message.data.scope == 'private');
      if (!membership && fromUser == this._lastUser) {
        fromUser = '...';
      } else {
        this._lastUser = fromUser;
        fromUser += ':';
      }
      var chat = V$E('chat');
      if (membership) {
        this._lastUser = null;
      }
      this._addMessage(fromUser, text, membership, isPrivate);
      this._messagesContainer.inner().scrollTop = this._messagesContainer.inner().scrollHeight;
    },

    /**
     * Updates the members list.
     * This function is called when a message arrives on channel /chat/members
     */
    members: function(message) {
      this._clearMembers();
      var _self = this;
      message.data.v_forEach(function(name) {_self._addMember(name);});
    },
             
    isConnected: function() {
      return this._connected;
    },
        
    _addMember: function(name) {
      this._addData(this._memberPrototype, this._membersContainer, 'member-tmpl', {Name: name});
    },
        
    _addMessage: function(fromUser, text, membership, isPrivate) {
      var type = '';
      if (membership) {
        type = 'membership';
      }
      if (isPrivate) {
        type = 'private';
      }
      this._addData(this._messagePrototype, this._messagesContainer, 'message-tmpl', {FromUser: fromUser, Text: text, Type: type});
    },
        
    _addData: function(proto, container, templateName, data) {
      var containerElement = container.inner();
      var pi = proto.construct();
      var template = pi.get(templateName);
      var r = pi.outer();
      containerElement.appendChild(r);
      template.setData(data);
    },
        
    _clearMembers: function() {
      this._membersContainer.inner().innerHTML = '';
    },

    _unsubscribe: function() {
      if (this._chatSubscription) {
        V$.net.cometd.unsubscribe(this._chatSubscription);
      }
      this._chatSubscription = null;
      if (this._membersSubscription) {
        V$.net.cometd.unsubscribe(this._membersSubscription);
      }
      this._membersSubscription = null;
    },

    _subscribe: function() {
      var _self = this;
      this._chatSubscription = V$.net.cometd.subscribe('/chat/demo', function(message) {_self.receive(message);});
      this._membersSubscription = V$.net.cometd.subscribe('/members/demo', function(message) {_self.members(message);});
    },

    _connectionInitialized: function() {
      var _self = this;
      // first time connection for this client, so subscribe tell everybody.
      V$.net.cometd.batch(function() {
        _self._subscribe();
        V$.net.cometd.publish('/chat/demo', {
          user: _self._username,
          membership: 'join',
          chat: _self._username + ' has joined'
        });
      });
    },

    _connectionEstablished: function() {
      // connection establish (maybe not for first time), so just
      // tell local user and update membership
      this.receive({
        data: {
          user: 'system',
          chat: 'Connection to Server Opened'
        }
      });
      V$.net.cometd.publish('/service/members', {
        user: this._username,
        room: '/chat/demo'
      });
    },

    _connectionBroken: function() {
      this.receive({
        data: {
          user: 'system',
          chat: 'Connection to Server Broken'
        }
      });
      this._clearMembers();
    },

    _connectionClosed: function() {
      this.receive({
        data: {
          user: 'system',
          chat: 'Connection to Server Closed'
        }
      });
    },
             
    _metaHandshake: function(message) {
      if (message.successful) {
        this._connectionInitialized();
        this._lastHandshakeFailed = false;
        this._joined.setDisplayed(true);
        this._join.setDisplayed(false);
      } else if (!this._lastHandshakeFailed) {
        alert('Cannot connect to server');
        this._lastHandshakeFailed = true;
        this._joined.setDisplayed(false);
        this._join.setDisplayed(true);
      }
    },


    _metaConnect: function(message) {
      if (this._disconnecting) {
        this._connected = false;
        this._connectionClosed();
      } else {
        this._wasConnected = this._connected;
        this._connected = message.successful === true;
        if (!this._wasConnected && this._connected) {
          this._connectionEstablished();
        } else if (this._wasConnected && !this._connected) {
          this._connectionBroken();
        }
      }
    }
  }
);

Related topics