changeset 99:f14558915187

bosh support
author Sonny Piers <sonny.piers@gmail.com>
date Tue, 12 Jun 2012 19:44:53 +0200
parents 6ec16b3e9cfc
children 3e124209821a
files bosh.js lightstring.js
diffstat 2 files changed, 380 insertions(+), 225 deletions(-) [+]
line wrap: on
line diff
new file mode 100644
--- /dev/null
+++ b/bosh.js
@@ -0,0 +1,215 @@
+'use strict';
+
+(function() {
+  Lightstring.BOSHConnection = function(aService) {
+    this.service = aService;
+    this.rid = 1337;
+    this.currentRequests = 0;
+    this.maxHTTPRetries = 5;
+    this.maxRequests = 2;
+    this.queue = [];
+  };
+  Lightstring.BOSHConnection.prototype = new EventEmitter();
+  Lightstring.BOSHConnection.prototype.open = function() {
+    var that = this;
+
+    var attrs = {
+      wait: '60',
+      hold: '1',
+      to: 'yuilop',
+      content: 'text/xml; charset=utf-8',
+      ver: '1.6',
+      'xmpp:version': '1.0',
+      'xmlns:xmpp': 'urn:xmpp:xbosh',
+    };
+
+    this.request(attrs, null, function(data) {
+      that.emit('open');
+      that.sid = data.getAttribute('sid');
+      that.maxRequests = data.getAttribute('maxRequests') || that.maxRequests;
+    });
+
+
+    this.on('in', function(stanza) {
+      if (stanza.localName === 'success') {
+        that.request({
+          'xmpp:restart': 'true',
+          'xmlns:xmpp': 'urn:xmpp:xbosh'
+        })
+      }
+    })
+  };
+  Lightstring.BOSHConnection.prototype.request = function(attrs, children, aOnSuccess, aOnError, aRetry) {
+    // if (children && children[0] && children[0].name === 'body') {
+    //   var body = children[0];
+    // }
+    // else {
+    //   var body = new ltx.Element('body');
+    //   if (children) {
+    //     if(util.isArray(children))
+    //       for (var k in children)
+    //         body.cnode(children[k]);
+    //     else
+    //       body.cnode(children);
+    //   }
+    // }
+
+    var body = '<body rid="' + this.rid++ + '"  xmlns="http://jabber.org/protocol/httpbind"/>';
+    var body = Lightstring.XML2DOM(body);
+
+    //sid
+    if (this.sid)
+      body.setAttribute('sid', this.sid);
+
+    //attributes on body
+    for (var i in attrs)
+      body.setAttribute(i, attrs[i]);
+
+    //children
+    for (var i in children)
+      body.appendChild(children[i]);
+
+
+
+    var retry = aRetry || 0;
+
+    var req = new XMLHttpRequest();
+    req.open('POST', this.service);
+
+
+    // req.upload.addEventListener("progress", updateProgress, false);
+    // req.upload.addEventListener("load", transferComplete, false);
+    // req.upload.addEventListener("error", transferFailed, false);
+    // req.upload.addEventListener("abort", transferCanceled, false);
+
+    // req.addEventListener("progress", updateProgress, false);
+    // req.addEventListener("load", transferComplete, false);
+    // req.addEventListener("error", transferFailed, false);
+    // req.addEventListener("abort", transferCanceled, false);
+
+    var that = this;
+    // req.responseType = 'document';
+    req.addEventListener("load", function() {
+      if (req.status < 200 || req.status >= 400) {
+        that.emit('error', "HTTP status " + req.status);
+        that.emit('close');
+        return;
+      }
+      that.currentRequests--;
+
+      var body = this.response;
+      that.emit('rawin', body);
+      var bodyEl = Lightstring.XML2DOM(body);
+      that.processResponse(bodyEl)
+      if (aOnSuccess)
+        aOnSuccess(bodyEl);
+
+    }, false);
+    // req.on('error', function(error) {
+    //   if (retry < that.maxHTTPRetries) {
+    //     that.request(attrs, children, aOnSuccess, aOnError, ++retry);
+    //   }
+    //   else {
+    //     that.emit('close');
+    //     that.emit('error', error);
+    //     if (aOnError)
+    //       aOnError(error);
+    //   }
+    // });
+    // this.emit('rawout', body.toString());
+
+    for(var i = 0; i < body.children.length; i++) {
+      var child = body.children[i];
+      that.emit('out', child);
+    }
+    this.emit('rawout', Lightstring.DOM2XML(body))
+
+    req.send(Lightstring.DOM2XML(body));
+    this.currentRequests++;
+  };
+  Lightstring.BOSHConnection.prototype.send = function(aData) {
+    if (!aData) {
+      var el = '';
+    }
+
+    else if(typeof aData == 'string') {
+      try {
+        var el = Lightstring.XML2DOM(aData);
+      }
+      catch(e) {
+        console.log(e);
+        console.log(aData);
+      }
+    }
+    else {
+      var el = aData.root();
+    }
+
+    var that = this;
+
+    this.queue.push(el);
+
+    setTimeout(this.mayRequest.bind(this), 0)
+
+  };
+  Lightstring.BOSHConnection.prototype.end = function(stanzas) {
+      var that = this;
+
+      stanzas = stanzas || [];
+      if (typeof stanzas !== 'array')
+        stanzas = [stanzas];
+
+      stanzas = this.queue.concat(stanzas);
+      this.queue = [];
+      this.request({type: 'terminate'}, stanzas,
+        function(err, bodyEl) {
+          that.emit('end');
+          that.emit('close');
+          delete that.sid;
+      });
+  };
+  Lightstring.BOSHConnection.prototype.processResponse = function(bodyEl) {
+    if (bodyEl && bodyEl.children) {
+      for(var i = 0; i < bodyEl.children.length; i++) {
+        var child = bodyEl.children[i];
+        this.emit('in', child);
+      }
+    }
+    if (bodyEl && bodyEl.getAttribute('type') === 'terminate') {
+      var condition = bodyEl.getAttribute('condition');
+      this.emit('error',
+        new Error(condition || "Session terminated"));
+      this.emit('close');
+    }
+  };
+  Lightstring.BOSHConnection.prototype.mayRequest = function() {
+    var canRequest =
+      this.sid && (this.currentRequests === 0 || ((this.queue.length > 0 && this.currentRequests < this.maxRequests))
+    );
+
+    if (!canRequest)
+      return;
+
+    var stanzas = this.queue;
+    this.queue = [];
+    //~ this.rid++;
+
+    var that = this;
+    this.request({}, stanzas,
+      //success
+      function(data) {
+        //if (data)
+          //that.processResponse(data);
+
+        setTimeout(that.mayRequest.bind(that), 0);
+
+      },
+      //error
+      function(error) {
+        that.emit('error', error);
+        that.emit('close');
+        delete that.sid;
+      }
+    );
+  };
+})();
\ No newline at end of file
--- a/lightstring.js
+++ b/lightstring.js
@@ -118,7 +118,7 @@ var Lightstring = {
 
 /**
  * @constructor Creates a new Lightstring connection
- * @param {String} [aService] The Websocket service URL.
+ * @param {String} [aService] The connection manager URL.
  * @memberOf Lightstring
  */
 Lightstring.Connection = function(aService) {
@@ -133,248 +133,188 @@ Lightstring.Connection = function(aServi
    */
   this.callbacks = {};
 };
-Lightstring.Connection.prototype = {
-  /**
-   * @function Create and open a websocket then go though the XMPP authentification process.
-   * @param {String} [aJid] The JID (Jabber id) to use.
-   * @param {String} [aPassword] The associated password.
-   */
-  connect: function(aJid, aPassword) {
-    this.emit('connecting');
-    this.jid = new Lightstring.JID(aJid);
-    if (aPassword)
-      this.password = aPassword;
-
-    if (!this.jid.bare)
-      return; //TODO: error
-    if (!this.service)
-      return; //TODO: error
-
-    function getProtocol(aURL) {
-      var a = document.createElement('a');
-      a.href = aURL;
-      return a.protocol.replace(':', '');
-    }
-    var protocol = getProtocol(this.service);
+Lightstring.Connection.prototype = new EventEmitter();
+/**
+ * @function Create and open a websocket then go though the XMPP authentification process.
+ * @param {String} [aJid] The JID (Jabber id) to use.
+ * @param {String} [aPassword] The associated password.
+ */
+Lightstring.Connection.prototype.connect = function(aJid, aPassword) {
+  this.emit('connecting');
+  this.jid = new Lightstring.JID(aJid);
+  if (aPassword)
+    this.password = aPassword;
 
-    if (protocol.match('http'))
-      this.connection = new Lightstring.BOSHConnection(this.service);
-    else if (protocol.match('ws'))
-      this.connection = new Lightstring.WebSocketConnection(this.service);
-
-    this.connection.connect();
-
-    var that = this;
-
-    this.connection.once('open', function() {
-      var stream = Lightstring.stanzas.stream.open(that.jid.domain);
-      that.connection.send(stream);
-      var stanza = {
-        XML: stream
-      };
-      that.emit('output', stanza);
-    });
-    this.connection.on('stanza', function(stanza) {
-      var stanza = new Lightstring.Stanza(stanza);
-
-      //FIXME: node-xmpp-bosh sends a self-closing stream:stream tag; it is wrong!
-      that.emit('input', stanza);
-
-      if (!stanza.DOM)
-        return;
-
-      var name = stanza.DOM.localName;
+  if (!this.jid.bare)
+    return; //TODO: error
+  if (!this.service)
+    return; //TODO: error
 
-      //Authentication
-      //FIXME SASL mechanisms and XMPP features can be both in a stream:features
-      if (name === 'features') {
-        //SASL mechanisms
-        if (stanza.DOM.firstChild.localName === 'mechanisms') {
-          stanza.mechanisms = [];
-          var nodes = stanza.DOM.getElementsByTagName('mechanism');
-          for (var i = 0; i < nodes.length; i++)
-            stanza.mechanisms.push(nodes[i].textContent);
-          that.emit('mechanisms', stanza);
-        }
-        //XMPP features
-        else {
-          //TODO: stanza.features
-          that.emit('features', stanza);
-        }
-      }
-      else if (name === 'challenge') {
-        that.emit('challenge', stanza);
-      }
-      else if (name === 'failure') {
-        that.emit('failure', stanza);
-      }
-      else if (name === 'success') {
-        that.emit('success', stanza);
-      }
+  function getProtocol(aURL) {
+    var a = document.createElement('a');
+    a.href = aURL;
+    return a.protocol.replace(':', '');
+  }
+  var protocol = getProtocol(this.service);
 
-      //Iq callbacks
-      else if (name === 'iq') {
-        var payload = stanza.DOM.firstChild;
-        if (payload)
-          that.emit('iq/' + payload.namespaceURI + ':' + payload.localName, stanza);
+  if (protocol.match('http'))
+    this.connection = new Lightstring.BOSHConnection(this.service);
+  else if (protocol.match('ws'))
+    this.connection = new Lightstring.WebSocketConnection(this.service);
+
+  this.connection.open();
 
-        var id = stanza.DOM.getAttribute('id');
-        if (!(id && id in that.callbacks))
-          return;
-
-        var type = stanza.DOM.getAttribute('type');
-        if (type !== 'result' && type !== 'error')
-          return; //TODO: warning
-
-        var callback = that.callbacks[id];
-        if (type === 'result' && callback.success)
-          callback.success.call(that, stanza);
-        else if (type === 'error' && callback.error)
-          callback.error.call(that, stanza);
-
-        delete that.callbacks[id];
-      }
+  var that = this;
 
-      else if (name === 'presence' || name === 'message') {
-        that.emit(name, stanza);
-      }
-    });
-  },
-  /**
-   * @function Send a message.
-   * @param {String|Object} aStanza The message to send.
-   * @param {Function} [aCallback] Executed on answer. (stanza must be iq)
-   */
-  send: function(aStanza, aSuccess, aError) {
-    if (!(aStanza instanceof Lightstring.Stanza))
-      var stanza = new Lightstring.Stanza(aStanza);
-    else
-      var stanza = aStanza;
+  this.connection.once('open', function() {
+    that.emit('open');
+  });
+  this.connection.on('out', function(stanza) {
+    that.emit('out', stanza);
+  });
+  this.connection.on('in', function(stanza) {
+    var stanza = new Lightstring.Stanza(stanza);
 
-    if (!stanza)
+    //FIXME: node-xmpp-bosh sends a self-closing stream:stream tag; it is wrong!
+    that.emit('stanza', stanza);
+
+    if (!stanza.DOM)
       return;
 
-    if (stanza.DOM.tagName === 'iq') {
-      var type = stanza.DOM.getAttribute('type');
-      if (type !== 'get' || type !== 'set')
-        ; //TODO: error
+    var name = stanza.DOM.localName;
 
-      var callback = {success: aSuccess, error: aError};
+    //Authentication
+    //FIXME SASL mechanisms and XMPP features can be both in a stream:features
+    if (name === 'features') {
+      //SASL mechanisms
+      if (stanza.DOM.firstChild.localName === 'mechanisms') {
+        stanza.mechanisms = [];
+        var nodes = stanza.DOM.getElementsByTagName('mechanism');
+        for (var i = 0; i < nodes.length; i++)
+          stanza.mechanisms.push(nodes[i].textContent);
+        that.emit('mechanisms', stanza);
+      }
+      //XMPP features
+      else {
+        //TODO: stanza.features
+        that.emit('features', stanza);
+      }
+    }
+    else if (name === 'challenge') {
+      that.emit('challenge', stanza);
+    }
+    else if (name === 'failure') {
+      that.emit('failure', stanza);
+    }
+    else if (name === 'success') {
+      that.emit('success', stanza);
+    }
+
+    //Iq callbacks
+    else if (name === 'iq') {
+      var payload = stanza.DOM.firstChild;
+      if (payload)
+        that.emit('iq/' + payload.namespaceURI + ':' + payload.localName, stanza);
 
       var id = stanza.DOM.getAttribute('id');
-      if (!id) {
-        var id = Lightstring.newId('sendiq:');
-        stanza.DOM.setAttribute('id', id);
-      }
+      if (!(id && id in that.callbacks))
+        return;
+
+      var type = stanza.DOM.getAttribute('type');
+      if (type !== 'result' && type !== 'error')
+        return; //TODO: warning
+
+      var callback = that.callbacks[id];
+      if (type === 'result' && callback.success)
+        callback.success.call(that, stanza);
+      else if (type === 'error' && callback.error)
+        callback.error.call(that, stanza);
+
+      delete that.callbacks[id];
+    }
 
-      this.callbacks[id] = callback;
-
+    else if (name === 'presence' || name === 'message') {
+      that.emit(name, stanza);
     }
-    else if (aSuccess || aError)
-      ; //TODO: warning (no callback without iq)
+  });
+};
+/**
+ * @function Send a message.
+ * @param {String|Object} aStanza The message to send.
+ * @param {Function} [aCallback] Executed on answer. (stanza must be iq)
+ */
+Lightstring.Connection.prototype.send = function(aStanza, aSuccess, aError) {
+  if (!(aStanza instanceof Lightstring.Stanza))
+    var stanza = new Lightstring.Stanza(aStanza);
+  else
+    var stanza = aStanza;
+
+  if (!stanza)
+    return;
+
+  if (stanza.DOM.tagName === 'iq') {
+    var type = stanza.DOM.getAttribute('type');
+    if (type !== 'get' || type !== 'set')
+      ; //TODO: error
+
+    var callback = {success: aSuccess, error: aError};
+
+    var id = stanza.DOM.getAttribute('id');
+    if (!id) {
+      var id = Lightstring.newId('sendiq:');
+      stanza.DOM.setAttribute('id', id);
+    }
+
+    this.callbacks[id] = callback;
+
+  }
+  else if (aSuccess || aError)
+    ; //TODO: warning (no callback without iq)
 
 
-    //FIXME this.socket.send(stanza.XML); (need some work on Lightstring.Stanza)
-    var fixme = Lightstring.DOM2XML(stanza.DOM);
-    stanza.XML = fixme;
-    this.connection.send(fixme);
-    this.emit('output', stanza);
-  },
-  /**
-   * @function Closes the XMPP stream and the socket.
-   */
-  disconnect: function() {
-    this.emit('disconnecting');
-    var stream = Lightstring.stanzas.stream.close();
-    this.socket.send(stream);
-    this.emit('XMLOutput', stream);
-    this.socket.close();
-  },
-  load: function() {
-    for (var i = 0; i < arguments.length; i++) {
-      var name = arguments[i];
-      if (!(name in Lightstring.plugins))
-        continue; //TODO: error
-
-      var plugin = Lightstring.plugins[name];
-
-      //Namespaces
-      for (var ns in plugin.namespaces)
-        Lightstring.ns[ns] = plugin.namespaces[ns];
-
-      //Stanzas
-      Lightstring.stanzas[name] = {};
-      for (var stanza in plugin.stanzas)
-        Lightstring.stanzas[name][stanza] = plugin.stanzas[stanza];
-
-      //Handlers
-      for (var handler in plugin.handlers)
-        this.on(handler, plugin.handlers[handler]);
-
-      //Methods
-      this[name] = {};
-      for (var method in plugin.methods)
-        this[name][method] = plugin.methods[method].bind(this);
+  //FIXME this.socket.send(stanza.XML); (need some work on Lightstring.Stanza)
+  var fixme = Lightstring.DOM2XML(stanza.DOM);
+  stanza.XML = fixme;
+  this.connection.send(fixme);
+  this.emit('output', stanza);
+};
+/**
+ * @function Closes the XMPP stream and the socket.
+ */
+Lightstring.Connection.prototype.disconnect = function() {
+  this.emit('disconnecting');
+  var stream = Lightstring.stanzas.stream.close();
+  this.socket.send(stream);
+  this.emit('XMLOutput', stream);
+  this.socket.close();
+};
+Lightstring.Connection.prototype.load = function() {
+  for (var i = 0; i < arguments.length; i++) {
+    var name = arguments[i];
+    if (!(name in Lightstring.plugins))
+      continue; //TODO: error
 
-      if (plugin.init)
-        plugin.init.apply(this);
-    }
-  },
-  /**
-   * @function Emits an event.
-   * @param {String} aName The event name.
-   * @param {Function|Array|Object} [aData] Data about the event.
-   */
-  emit: function(aName, aData) {
-    var handlers = this.handlers[aName];
-    if (!handlers)
-      return;
+    var plugin = Lightstring.plugins[name];
 
-    //Non-data events
-    if(!aData) {
-      for (var i = 0; i < handlers.length; i++)
-        handlers[i].call(this, aData);
-
-      return;
-    }
+    //Namespaces
+    for (var ns in plugin.namespaces)
+      Lightstring.ns[ns] = plugin.namespaces[ns];
 
-    //Non-iq events
-    if (aData && aData.DOM && aData.DOM.localName !== 'iq') {
-      for (var i = 0; i < handlers.length; i++)
-        handlers[i].call(this, aData);
-
-      return;
-    }
+    //Stanzas
+    Lightstring.stanzas[name] = {};
+    for (var stanza in plugin.stanzas)
+      Lightstring.stanzas[name][stanza] = plugin.stanzas[stanza];
 
-    //Iq events
-    var ret;
-    for (var i = 0; i < handlers.length; i++) {
-      ret = handlers[i].call(this, aData);
-      if (typeof ret !== 'boolean')
-        return; //TODO: error
-
-      if (ret)
-        return;
-    }
-
-    if (aData && aData.DOM) {
-      var type = aData.DOM.getAttribute('type');
-      if (type !== 'get' && type !== 'set')
-        return;
+    //Handlers
+    for (var handler in plugin.handlers)
+      this.on(handler, plugin.handlers[handler]);
 
-      var from = aData.DOM.getAttribute('from');
-      var id = aData.DOM.getAttribute('id');
-      this.send(Lightstring.stanzas.errors.iq(from, id, 'cancel', 'service-unavailable'));
-    }
-  },
-  /**
-   * @function Register an event handler.
-   * @param {String} aName The event name.
-   * @param {Function} aCallback The callback to call when the event is emitted.
-   */
-  on: function(aName, callback) {
-    if (!this.handlers[aName])
-      this.handlers[aName] = [];
-    this.handlers[aName].push(callback);
+    //Methods
+    this[name] = {};
+    for (var method in plugin.methods)
+      this[name][method] = plugin.methods[method].bind(this);
+
+    if (plugin.init)
+      plugin.init.apply(this);
   }
-};
+};
\ No newline at end of file