text: /*
   * NchanSubscriber
   * usage: var sub = new NchanSubscriber(url, opt);
   * 
   * opt = {
   *   subscriber: 'longpoll', 'eventsource', or 'websocket',
   *     //or an array of the above indicating subscriber type preference
   *   reconnect: undefined or 'session' or 'persist'
   *     //if the HTML5 sessionStore or localStore should be used to resume
   *     //connections interrupted by a page load
   *   shared: true or undefined
   *     //share connection to same subscriber url between browser 
   *     //windows and tabs using localStorage. In shared mode, 
   *     //only 1 running subscriber is allowed per url per window/tab.
   * }
   * 
   * sub.on("transportSetup", function(opt, subscriberName) {
   *   // opt is a hash/object - not all transports support all options equally. Only longpoll supports arbitrary headers
   *   // subscriberName is a string
   *   //
   *   // longpoll transport supports:
   *   //   opt.longpoll.pollDelay - delay in milliseconds between successful requests
   *   // eventsource transport supports:
   *   //   opt.eventsource.withCredentials - boolean enabling the withCredentials CORS setting
   * });
   * 
   * sub.on("transportNativeCreated", function(nativeTransportObject, subscriberName) {
   *   // nativeTransportObject is the native transport object and depends on the subscriber type
   *   // subscriberName is a string
   * });
   *
   * sub.on("transportNativeBeforeDestroy", function(nativeTransportObject, subscriberName) {
   *   // nativeTransportObject is the native transport object and depends on the subscriber type
   *   // subscriberName is a string
   * });
   *
   * sub.on("message", function(message, message_metadata) {
   *   // message is a string
   *   // message_metadata may contain 'id' and 'content-type'
   * });
   * 
   * sub.on('connect', function(evt) {
   *   //fired when first connected. 
   * });
   * 
   * sub.on('disconnect', function(evt) {
   *   // when disconnected.
   * });
   * 
   * sub.on('error', function(code, message) {
   *   //error callback. not sure about the parameters yet
   * });
   * 
   * sub.reconnect; // should subscriber try to reconnect? true by default.
   * sub.reconnectTimeout; //how long to wait to reconnect? does not apply to EventSource, which reconnects on its own.
   * sub.lastMessageId; //last message id. useful for resuming a connection without loss or repetition.
   * 
   * sub.start(); // begin (or resume) subscribing
   * sub.stop(); // stop subscriber. do not reconnect.
   */
  
  //Thanks Darren Whitlen ( @prawnsalad ) for your feedback
  
  
  ;(function (global, moduleName, factory) { // eslint-disable-line
    "use strict";
    /* eslint-disable no-undef */
    var newModule = factory(global);
    if (typeof module === "object" && module != null && module.exports) {
      module.exports = newModule;
    } else if (typeof define === "function" && define.amd) {
      define(function () { return newModule; });
    } else {
      global[moduleName] = newModule;
    }
    /* eslint-enable no-undef */
  })(typeof window !== "undefined" ? window : this, "NchanSubscriber", function factory(global, undefined) { // eslint-disable-line
  
    // https://github.com/yanatan16/nanoajax
    var nanoajax={};
    (function(){var e=["responseType","withCredentials","timeout","onprogress"];nanoajax.ajax=function(r,o){var a=r.headers||{},u=r.body,s=r.method||(u?"POST":"GET"),i=false;var f=t(r.cors);function l(e,t){return function(){if(!i){o(f.status===undefined?e:f.status,f.status===0?"Error":f.response||f.responseText||t,f);i=true}}}f.open(s,r.url,true);var d=f.onload=l(200);f.onreadystatechange=function(){if(f.readyState===4)d()};f.onerror=l(null,"Error");f.ontimeout=l(null,"Timeout");f.onabort=l(null,"Abort");if(u){n(a,"X-Requested-With","XMLHttpRequest");if(!global.FormData||!(u instanceof global.FormData)){n(a,"Content-Type","application/x-www-form-urlencoded")}}for(var p=0,c=e.length,g;p<c;p++){g=e[p];if(r[g]!==undefined)f[g]=r[g]}for(var g in a)f.setRequestHeader(g,a[g]);f.send(u);return f};function t(e){if(e&&global.XDomainRequest&&!/MSIE 1/.test(navigator.userAgent))return new XDomainRequest;if(global.XMLHttpRequest)return new XMLHttpRequest}function n(e,t,n){e[t]=e[t]||n}})(); // eslint-disable-line
  
  
    // https://github.com/component/emitter
    function Emitter(t){return t?mixin(t):void 0}function mixin(t){for(var e in Emitter.prototype)t[e]=Emitter.prototype[e];return t}Emitter.prototype.on=Emitter.prototype.addEventListener=function(t,e){return this._callbacks=this._callbacks||{},(this._callbacks["$"+t]=this._callbacks["$"+t]||[]).push(e),this},Emitter.prototype.once=function(t,e){function i(){this.off(t,i),e.apply(this,arguments)}return i.fn=e,this.on(t,i),this},Emitter.prototype.off=Emitter.prototype.removeListener=Emitter.prototype.removeAllListeners=Emitter.prototype.removeEventListener=function(t,e){if(this._callbacks=this._callbacks||{},0==arguments.length)return this._callbacks={},this;var i=this._callbacks["$"+t];if(!i)return this;if(1==arguments.length)return delete this._callbacks["$"+t],this;for(var r,s=0;s<i.length;s++)if(r=i[s],r===e||r.fn===e){i.splice(s,1);break}return this},Emitter.prototype.emit=function(t){this._callbacks=this._callbacks||{};var e=[].slice.call(arguments,1),i=this._callbacks["$"+t];if(i){i=i.slice(0);for(var r=0,s=i.length;s>r;++r)i[r].apply(this,e)}return this},Emitter.prototype.listeners=function(t){return this._callbacks=this._callbacks||{},this._callbacks["$"+t]||[]},Emitter.prototype.hasListeners=function(t){return!!this.listeners(t).length};// eslint-disable-line
  
    var ughbind = (Function.prototype.bind
      ? function ughbind(fn, thisObj) {
        return fn.bind(thisObj);
      }
      : function ughbind(fn, thisObj) {
        return function() {
          fn.apply(thisObj, arguments);
        };
      }
    );
  
    var sharedSubscriberTable={};
  
    "use strict"; 
    function NchanSubscriber(url, opt) {
      if(typeof window !== "undefined" && this === window) {
        throw "use 'new NchanSubscriber(...)' to initialize";
      }
      
      this.url = url;
      opt = opt || {};
      
      //which transport should i use?
      if(typeof opt === "string") {
        opt = {subscriber: opt}; 
      }
      if(opt.transport && !opt.subscriber) {
        opt.subscriber = opt.transport;
      }
      if(typeof opt.subscriber === "string") {
        opt.subscriber = [ opt.subscriber ];
      }
      this.desiredTransport = opt.subscriber;
      
      if(opt.shared) {
        if (!("localStorage" in global)) {
          throw "localStorage unavailable for use in shared NchanSubscriber";
        }
        
        var pre = "NchanSubscriber:" + this.url + ":shared:";
        var sharedKey = function(key) { return pre + key; };
        var localStorage = global.localStorage;
        this.shared = {
          id: "" + Math.random() + Math.random(),
          key: sharedKey,
          get: function(key) {
            return localStorage.getItem(sharedKey(key));
          },
          set: function(key, val) {
            return localStorage.setItem(sharedKey(key), val);
          },
          setWithId: ughbind(function(key, val) {
            return this.shared.set(key, "##" + this.shared.id + ":" + val);
          }, this),
          getWithId: ughbind(function(key) {
            return this.shared.stripIdFromVal(this.shared.get(key));
          }, this),
          stripIdFromVal: function(val) {
            if(!val) {
              return val;
            }
            var sep = val.indexOf(":");
            if(val[0]!=val[1] || val[0]!="#" || !sep) {
              //throw "not an event value with id";
              return val; //for backwards-compatibility
            }
            return val.substring(sep+1, val.length);
          },
          remove: function(key) {
            return localStorage.removeItem(sharedKey(key));
          },
          matchEventKey: ughbind(function(ev, key) {
            if(ev.storageArea && ev.storageArea != localStorage){
              return false;
            }
            return ev.key == sharedKey(key);
          }, this),
          matchEventKeyWithId: ughbind(function(ev, key) {
            if(this.shared.matchEventKey(ev, key)) {
              var val = ev.newValue;
              var sep = val.indexOf(":");
              if(val[0]!=val[1] || val[0]!="#" || !sep) {
                //throw "not an event value with id";
                return true; //for backwards-compatibility
              }
              var id = val.substring(2, sep);
              return (id != this.shared.id); //ignore own events (accomodations for IE. Fuckin' IE, even after all these years...)
            }
            else {
              return false;
            }
          }, this),
          setRole: ughbind(function(role) {
            //console.log(this.url, "set shared role to ", role);
            if(role == "master" && this.shared.role != "master") {
              var now = new Date().getTime()/1000;
              this.shared.setWithId("master:created", now);
              this.shared.setWithId("master:lastSeen", now);
            }
            if(role == "slave" && !this.lastMessageId) {
              this.lastMessageId = this.shared.get("msg:id");
            }
            this.shared.role = role;
            return this;
          }, this),
          
          demoteToSlave: ughbind(function() {
            //console.log("demote to slave");
            if(this.shared.role != "master") {
              throw "can't demote non-master to slave";
            }
            if(this.running) {
              this.stop();
              this.shared.setRole("slave");
              this.initializeTransport();
              this.start();
            }
            else {
              this.initializeTransport();
            }
          }, this),
          
          maybePromoteToMaster: ughbind(function() {
            if(!(this.running || this.starting)) {
              //console.log(this.url, "stopped Subscriber won't be promoted to master");
              return this;
            }
            if(this.shared.maybePromotingToMaster) {
              //console.log(this.url, " already maybePromotingToMaster");
              return;
            }
            this.shared.maybePromotingToMaster = true;
            
            //console.log(this.url, "maybe promote to master");
            var processRoll;
            
            var lotteryRoundDuration = 2000;
            var currentContenders = 0;
            
            //roll the dice
            var roll = Math.random();
            var bestRoll = roll;
            
            var checkRollInterval;
            var checkRoll = ughbind(function(dontProcess) {
              var latestSharedRollTime = parseFloat(this.shared.getWithId("lotteryTime"));
              var latestSharedRoll = parseFloat(this.shared.getWithId("lottery"));
              var notStale = !latestSharedRollTime || (latestSharedRollTime > (new Date().getTime() - lotteryRoundDuration * 2));
              if(notStale && latestSharedRoll && (!bestRoll || latestSharedRoll > bestRoll)) {
                bestRoll = latestSharedRoll;
              }
              if(!dontProcess) {
                processRoll();
              }
            }, this);
            
            checkRoll(true);
            this.shared.setWithId("lottery", roll);
            this.shared.setWithId("lotteryTime", new Date().getTime() / 1000);
            
            var rollCallback = ughbind(function(ev) {
              if(this.shared.matchEventKeyWithId(ev, "lottery") && ev.newValue) {
                currentContenders += 1;
                var newVal = parseFloat(this.shared.stripIdFromVal(ev.newValue));
                var oldVal = parseFloat(this.shared.stripIdFromVal(ev.oldValue));
                if(oldVal > newVal) {
                  this.shared.setWithId("lottery", oldVal);
                }
                
                if(!bestRoll || newVal >= bestRoll) {
                  //console.log("new bestRoll", newVal);
                  bestRoll = newVal;
                }
              }
            }, this);
            global.addEventListener("storage", rollCallback);
            
            var finish = ughbind(function() {
              //console.log("finish");
              this.shared.maybePromotingToMaster = false;
              //console.log(this.url, this.shared.role);
              global.removeEventListener("storage", rollCallback);
              if(checkRollInterval) {
                clearInterval(checkRollInterval);
              }
              if(this.shared && this.shared.role == "master") {
                this.shared.remove("lottery");
                this.shared.remove("lotteryTime");
              }
              if(this.running) {
                this.stop();
                this.initializeTransport();
                this.start();
              }
              else {
                this.initializeTransport();
                if(this.starting) {
                  this.start();
                }
              }
            }, this);
            
            processRoll = ughbind(function() {
              //console.log("roll, bestroll", roll, bestRoll);
              if(roll < bestRoll) {
                //console.log(this.url, "loser");
                this.shared.setRole("slave");
                finish();
              }
              else if(roll >= bestRoll) {
                //var now = new Date().getTime() / 1000;
                //var lotteryTime = parseFloat(this.shared.getWithId("lotteryTime"));
                //console.log(lotteryTime, now - lotteryRoundDuration/1000);
                if(currentContenders == 0) {
                  //console.log("winner, no more contenders!");
                  this.shared.setRole("master");
                  finish();
                }
                else {
                  //console.log("winning, but have contenders", currentContenders);
                  currentContenders = 0;
                }
              }
            }, this);
            
            checkRollInterval = global.setInterval(checkRoll, lotteryRoundDuration);
          }, this),
          masterCheckInterval: 10000
        };
      }
      
      this.lastMessageId = opt.id || opt.msgId;
      this.reconnect = typeof opt.reconnect == "undefined" ? true : opt.reconnect;
      this.reconnectTimeout = opt.reconnectTimeout || 1000;
      
      
      var saveConnectionState;
      if(!opt.reconnect) {
        saveConnectionState = function() {};
      }
      else {
        var index = "NchanSubscriber:" + url + ":lastMessageId";
        var storage;
        if(opt.reconnect == "persist") {
          storage = ("localStorage" in global) && global.localStorage;
          if(!storage)
            throw "can't use reconnect: 'persist' option: localStorage not available";
        }
        else if(opt.reconnect == "session") {
          storage = ("sessionStorage" in global) && global.sessionStorage;
          if(!storage)
            throw "can't use reconnect: 'session' option: sessionStorage not available";
        }
        else {
          throw "invalid 'reconnect' option value " + opt.reconnect;
        }
        saveConnectionState = ughbind(function(msgid) {
          if(this.shared && this.shared.role == "slave") return;
          storage.setItem(index, msgid);
        }, this);
        this.lastMessageId = storage.getItem(index);
      }
      
      var onUnloadEvent = ughbind(function() {
        if(this.running) {
          this.stop();
        }
        if(this.shared && this.shared.role == "master") {
          this.shared.setWithId("status", "disconnected");
        }
      }, this);
      global.addEventListener("beforeunload", onUnloadEvent, false);
      // swap `beforeunload` to `unload` after DOM is loaded
      global.addEventListener("DOMContentLoaded", function() {
        global.removeEventListener("beforeunload", onUnloadEvent, false);
        global.addEventListener("unload", onUnloadEvent, false);
      }, false);
      
      
      var notifySharedSubscribers;
      if(this.shared) {
        notifySharedSubscribers = ughbind(function(name, data) {
          if(this.shared.role != "master") {
            return;
          }
          
          if(name == "message") {
            this.shared.set("msg:id", data[1] && data[1].id || "");
            this.shared.set("msg:content-type", data[1] && data[1]["content-type"] || "");
            this.shared.set("msg", data[0]);
          }
          else if(name == "error") {
            //TODO 
          }
          else if(name == "connecting") {
            this.shared.setWithId("status", "connecting");
          }
          else if(name == "connect") {
            this.shared.setWithId("status", "connected");
          }
          else if(name == "reconnect") {
            this.shared.setWithId("status", "reconnecting");
          }
          else if(name == "disconnect") {
            this.shared.setWithId("status", "disconnected");
          }
        }, this);
      }
      else {
        notifySharedSubscribers = function(){};
      }
      
      var restartTimeoutIndex;
      var stopHandler = ughbind(function() {
        if(!restartTimeoutIndex && this.running && this.reconnect && !this.transport.reconnecting && !this.transport.doNotReconnect) {
          //console.log("stopHAndler reconnect plz", this.running, this.reconnect);
          notifySharedSubscribers("reconnect");
          restartTimeoutIndex = global.setTimeout(ughbind(function() {
            restartTimeoutIndex = null;
            this.stop();
            this.start();
          }, this), this.reconnectTimeout);
        }
        else {
          notifySharedSubscribers("disconnect");
        }
      }, this);
      
      this.on("message", function msg(msg, meta) {
        this.lastMessageId=meta.id;
        if(meta.id) {
          saveConnectionState(meta.id);
        }
        notifySharedSubscribers("message", [msg, meta]);
        //console.log(msg, meta);
      });
      this.on("error", function fail(code, text) {
        stopHandler(code, text);
        notifySharedSubscribers("error", [code, text]);
        //console.log("failure", code, text);
      });
      this.on("connect", function() {
        this.connected = true;
        notifySharedSubscribers("connect");
      });
      this.on("__disconnect", function fail(code, text) {
        this.connected = false;
        this.emit("disconnect", code, text);
        stopHandler(code, text);
        //console.log("__disconnect", code, text);
      });
    }
  
    Emitter(NchanSubscriber.prototype);
  
    var defaultTransportOptions = function() {
      var opt = {
        url: null,
        msgid: null,
        headers : {}
      };
      for(var n in NchanSubscriber.prototype.SubscriberClass) {
        if(n != "__slave") {
          opt[n]={};
        }
      }
      return opt;
    };
    
    NchanSubscriber.prototype.initializeTransport = function(possibleTransports) {
      if(possibleTransports) {
        this.desiredTransport = possibleTransports;
      }
      if(this.shared && this.shared.role == "slave") {
        this.transport = new this.SubscriberClass["__slave"](ughbind(this.emit, this)); //try it
      }
      else {
        var tryInitializeTransport = ughbind(function(name) {
          if(!this.SubscriberClass[name]) {
            throw "unknown subscriber type " + name;
          }
          try {
            this.transport = new this.SubscriberClass[name](ughbind(this.emit, this)); //try it
            return this.transport;
          } catch(err) { /*meh...*/ }
        }, this);
        
        var i;
        if(this.desiredTransport) {
          for(i=0; i<this.desiredTransport.length; i++) {
            if(tryInitializeTransport(this.desiredTransport[i])) {
              break;
            }
          }
        }
        else {
          for(i in this.SubscriberClass) {
            if (this.SubscriberClass.hasOwnProperty(i) && i[0] != "_" && tryInitializeTransport(i)) {
              break;
            }
          }
        }
      }
      if(! this.transport) {
        throw "can't use any transport type";
      }
    };
  
    var storageEventListener;
  
    NchanSubscriber.prototype.start = function() {
      if(this.running)
        throw "Can't start NchanSubscriber, it's already started.";
      
      this.starting = true;
      
      if(this.shared) {
        if(sharedSubscriberTable[this.url] && sharedSubscriberTable[this.url] != this) {
          throw "Only 1 shared subscriber allowed per url per window/tab.";
        }
        sharedSubscriberTable[this.url] = this;
        
        if(!this.shared.role) {
          var status = this.shared.getWithId("status");
          storageEventListener = ughbind(function(ev) {
            if(this.shared.matchEventKeyWithId(ev, "status")) {
              var newValue = this.shared.stripIdFromVal(ev.newValue);
              if(newValue == "disconnected") {
                if(this.shared.role == "slave") {
                  //play the promotion lottery
                  //console.log(this.url, "status changed to disconnected, maybepromotetomaster", ev.newValue, ev.oldValue);
                  this.shared.maybePromoteToMaster();
                }
                else if(this.shared.role == "master") {
                  //do nothing
                  //console.log(this.url, "current role is master, do nothing?...");
                }
              }
            }
            else if(this.shared.role == "master" && this.shared.matchEventKeyWithId(ev, "master:created") && ev.newValue) {
              //a new master has arrived. demote to slave.
              this.shared.demoteToSlave();
            }
          }, this);
          global.addEventListener("storage", storageEventListener);
          if(status == "disconnected") {
            //console.log(this.url, "status == disconnected, maybepromotetomaster");
            this.shared.maybePromoteToMaster();
          }
          else {
            this.shared.setRole(status ? "slave" : "master");
            this.initializeTransport();
          }
        }
        
        if(this.shared.role == "master") {
          this.shared.setWithId("status", "connecting");
          this.transport.listen(this.url, this.lastMessageId);
          this.running = true;
          delete this.starting;
          
          //master checkin interval
          this.shared.masterIntervalCheckID = global.setInterval(ughbind(function() {
            this.shared.setWithId("master:lastSeen", new Date().getTime() / 1000);
          }, this), this.shared.masterCheckInterval * 0.8);
        }
        else if(this.shared.role == "slave") {
          this.transport.listen(this.url, this.shared);
          this.running = true;
          delete this.starting;
          
          //slave check if master is around
          this.shared.masterIntervalCheckID = global.setInterval(ughbind(function() {
            var lastCheckin = parseFloat(this.shared.getWithId("master:lastSeen"));
            if(!lastCheckin || lastCheckin < (new Date().getTime() / 1000) - this.shared.masterCheckInterval / 1000) {
              //master hasn't checked in for too long. assume it's gone.
              this.shared.maybePromoteToMaster();
            }
          }, this), this.shared.masterCheckInterval);
        }
      }
      else {
        if(!this.transport) {
          this.initializeTransport();
        }
        this.transport.listen(this.url, this.lastMessageId);
        this.running = true;
        delete this.starting;
      }
      return this;
    };
  
    NchanSubscriber.prototype.stop = function() {
      if(!this.running)
        throw "Can't stop NchanSubscriber, it's not running.";
      
      this.running = false;
      if(storageEventListener) {
        global.removeEventListener("storage", storageEventListener);
      }
      this.transport.cancel();
      if(this.shared) {
        delete sharedSubscriberTable[this.url];
        if(this.shared.masterIntervalCheckID) {
          clearInterval(this.shared.masterIntervalCheckID);
          delete this.shared.masterIntervalCheckID;
        }
      }
      return this;
    };
  
    function addLastMsgIdToQueryString(url, msgid) {
      if(msgid) {
        var m = url.match(/(\?.*)$/);
        url += (m ? "&" : "?") + "last_event_id=" + encodeURIComponent(msgid);
      }
      return url;
    }
  
    NchanSubscriber.prototype.SubscriberClass = {
      "websocket": (function() {
        function WSWrapper(emit) {
          WebSocket;
          this.emit = emit;
          this.name = "websocket";
          this.opt = defaultTransportOptions()
          this.opt.headers["Sec-WebSocket-Protocol"]="ws+meta.nchan"
        };
  
        WSWrapper.prototype.setup = function() {
          this.emit("transportSetup", this.opt, this.name);
          var count = 0;
          var property;
          for ( property in this.opt.headers ) count++;
          if (count != 1 && "Sec-WebSocket-Protocol" in this.opt.headers) {
            throw "WebSocket only supports one header; Sec-WebSocket-Protocol";
          }
        };
  
        WSWrapper.prototype.websocketizeURL = function(url) {
          var m = url.match(/^((\w+:)?\/\/([^/]+))?(\/)?(.*)/);
          var protocol = m[2];
          var host = m[3];
          var absolute = m[4];
          var path = m[5];
          
          var loc;
          if(typeof window == "object") {
            loc = window.location;
          }
          else if(typeof document == "object") {
            loc = document.location;
          }
          
          if(!protocol && loc) {
            protocol = loc.protocol;
          }
          if(protocol == "https:") {
            protocol = "wss:";
          }
          else if(protocol == "http:") {
            protocol = "ws:";
          }
          else if(protocol != "ws:") {
            protocol = "wss:"; //default setting: secure, unless "ws:" explicitly specified
          }
          
          if(!host && loc) {
            host = loc.host;
          }
          
          if(!absolute) {
            path = loc ? loc.pathname.match(/(.*\/)[^/]*/)[1] + path : "/" + path;
          }
          else {
            path = "/" + path;
          }
          
          return protocol + "//" + host + path;
        };
        
        WSWrapper.prototype.listen = function(url, msgid) {
          if(this.listener) {
            throw "websocket already listening";
          }
          this.opt.url = url;
          this.opt.msgid = msgid;
          this.setup();
          url = this.websocketizeURL(this.opt.url);
          url = addLastMsgIdToQueryString(url, this.opt.msgid);
          //console.log(url);
          this.listener = new WebSocket(url, this.opt.headers["Sec-WebSocket-Protocol"]);
          var l = this.listener;
          this.emit("transportNativeCreated", l, this.name);
          l.onmessage = ughbind(function(evt) {
            if (evt.data instanceof Blob) {
              //extract header, preserve rest of blob.
              //let's assume the header ends in the first 255 chars
              var headerSlice = evt.data.slice(0, 255);
              var reader = new FileReader();
              reader.addEventListener("loadend", ughbind(function() {
                var m = reader.result.match(/^id: (.*)\n(content-type: (.*)\n)?\n/m);
                this.emit("message", evt.data.slice(m[0].length), {"id": m[1], "content-type": m[3]});
              }, this));
              reader.readAsText(headerSlice)
            } else {
              var m = evt.data.match(/^id: (.*)\n(content-type: (.*)\n)?\n/m);
              this.emit("message", evt.data.substr(m[0].length), {"id": m[1], "content-type": m[3]});
            }
          }, this);
          
          l.onopen = ughbind(function(evt) {
            this.emit("connect", evt);
            //console.log("connect", evt);
          }, this);
          
          l.onerror = ughbind(function(evt) {
            //console.log("error", evt);
            this.emit("error", evt, l);
            delete this.listener;
          }, this);
          
          l.onclose = ughbind(function(evt) {
            this.emit("__disconnect", evt);
            delete this.listener;
          }, this);
        };
        
        WSWrapper.prototype.cancel = function() {
          if(this.listener) {
            this.emit("transportNativeBeforeDestroy", this.listener, this.name);
            this.listener.close();
            delete this.listener;
          }
        };
        
        return WSWrapper;
      })(),
    
      "eventsource": (function() {
        function ESWrapper(emit) {
          EventSource;
          this.emit = emit;
          this.name = "eventsource";
          this.opt = defaultTransportOptions();
          this.opt.eventsource.withCredentials = false;
        }
  
        ESWrapper.prototype.setup = function() {
          this.emit("transportSetup", this.opt, this.name);
          var count = 0;
          var property;
          for ( property in this.opt.headers ) count++;
          if (count != 0) {
            throw "EventSource does not support headers";
          }
        };
  
        ESWrapper.prototype.listen= function(url, msgid) {
          if(this.listener) {
            throw "there's a ES listener running already";
          }
          this.opt.url = url;
          this.opt.msgid = msgid;
          this.setup();
          url = addLastMsgIdToQueryString(this.opt.url, this.opt.msgid);
          this.listener = new EventSource(url, this.opt.eventsource);
          var l = this.listener;
          this.emit("transportNativeCreated", l, this.name);
          l.onmessage = ughbind(function(evt){
            //console.log("message", evt);
            this.emit("message", evt.data, {id: evt.lastEventId});
          }, this);
          
          l.onopen = ughbind(function(evt) {
            this.reconnecting = false;
            //console.log("connect", evt);
            this.emit("connect", evt);
          }, this);
          
          l.onerror = ughbind(function(evt) {
            //EventSource will try to reconnect by itself
            //console.log("onerror", this.listener.readyState, evt);
            if(this.listener.readyState == EventSource.CONNECTING && !this.reconnecting) {
              if(!this.reconnecting) {
                this.reconnecting = true;
                this.emit("__disconnect", evt);
              }
            }
            else {
              this.emit("__disconnect", evt);
              //console.log('other __disconnect', evt);
            }
          }, this);
        };
        
        ESWrapper.prototype.cancel= function() {
          if(this.listener) {
            this.emit("transportNativeBeforeDestroy", this.listener, this.name);
            this.listener.close();
            delete this.listener;
          }
        };
        
        return ESWrapper;
      })(),
      
      "longpoll": (function () {
        function Longpoll(emit) {
          this.pollingRequest = null;
          this.nextRequestTimer = null;
          this.longPollStartTime = null;
          this.maxLongPollTime = 5*60*1000; //5 minutes
          this.emit = emit;
          this.name = "longpoll";
          this.opt = defaultTransportOptions();
          this.opt.longpoll.pollDelay = 0;
        }
  
        Longpoll.prototype.setup = function() {
          this.emit("transportSetup", this.opt, this.name);
          var count = 0;
          var property;
          for ( property in this.opt.headers ) count++;
        };
  
        Longpoll.prototype.listen = function(url, msgid) {
          if(this.req) {
            throw "already listening";
          }
          this.opt.url = url;
          this.opt.msgid = msgid;
          if(msgid) {
            this.opt.headers["Etag"] = msgid;
          }
          this.setup();
  
          var setHeader = ughbind(function(incoming, name) {
            if(incoming) { this.opt.headers[name]= incoming; }
          }, this);
  
          this.pollingRequest = ughbind(function() {
            if (this.req) {
              this.emit("transportNativeBeforeDestroy", this.req, this.name);
            }
            this.nextRequestTimer = null;
            this.reqStartTime = new Date().getTime();
            this.req = nanoajax.ajax({url: this.opt.url, headers: this.opt.headers}, requestCallback);
            this.emit("transportNativeCreated", this.req, this.name);
          }, this);
          var  requestCallback;
          requestCallback = ughbind(function (code, response_text, req) {
            setHeader(req.getResponseHeader("Last-Modified"), "If-Modified-Since");
            setHeader(req.getResponseHeader("Etag"), "If-None-Match");
            
            if(code >= 200 && code <= 210) {
              //legit reply
              var content_type = req.getResponseHeader("Content-Type");
              if (!this.parseMultipartMixedMessage(content_type, response_text, req)) {
                this.emit("message", response_text || "", {"content-type": content_type, "id": this.msgIdFromResponseHeaders(req)});
              }
              
              if (this.req) { //this check is needed because stop() may have been called in the message callback
                if (this.opt.longpoll.pollDelay == 0) {
                  this.pollingRequest();
                } else {
                  this.nextRequestTimer = global.setTimeout(this.pollingRequest, this.opt.longpoll.pollDelay);
                }
              }
            }
            else if((code == 0 && response_text == "Error" && req.readyState == 4) || (code === null && response_text != "Abort")) {
              //console.log("abort!!!");
              this.emit("__disconnect", code || 0, response_text);
              this.cancel();
            }
            else if(code !== null) {
              //HTTP error
              this.emit("error", code, response_text);
              this.cancel();
            }
            else {
              //don't care about abortions 
              this.cancel();
              this.emit("__disconnect");
              //console.log("abort!");
            }
          }, this);
          
          this.pollingRequest();
          this.emit("connect");
          
          return this;
        };
        
        Longpoll.prototype.parseMultipartMixedMessage = function(content_type, text, req) {
          var m = content_type && content_type.match(/^multipart\/mixed;\s+boundary=(.*)$/);
          if(!m) { 
            return false;
          }
          var boundary = m[1];
          
          var msgs = text.split("--" + boundary);
          if(msgs[0] != "" || !msgs[msgs.length-1].match(/--\r?\n/)) { throw "weird multipart/mixed split"; }
          
          msgs = msgs.slice(1, -1);
          for(var i in msgs) {
            m = msgs[i].match(/^(.*)\r?\n\r?\n([\s\S]*)\r?\n$/m);
            var hdrs = m[1].split("\n");
            
            var meta = {};
            for(var j in hdrs) {
              var hdr = hdrs[j].match(/^([^:]+):\s+(.*)/);
              if(hdr && hdr[1] == "Content-Type") {
                meta["content-type"] = hdr[2];
              }
            }
            
            if(i == msgs.length - 1) {
              meta["id"] = this.msgIdFromResponseHeaders(req);
            }
            this.emit("message", m[2], meta);
          }
          return true;
        };
        
        Longpoll.prototype.msgIdFromResponseHeaders = function(req) {
          var lastModified, etag;
          lastModified = req.getResponseHeader("Last-Modified");
          etag = req.getResponseHeader("Etag");
          if(lastModified) {
            return "" + Date.parse(lastModified)/1000 + ":" + (etag || "0");
          }
          else if(etag) {
            return etag;
          }
          else {
            return null;
          }
        };
        
        Longpoll.prototype.cancel = function() {
          if(this.req) {
            this.emit("transportNativeBeforeDestroy", this.req, this.name);
            this.req.abort();
            delete this.req;
          }
          this.cancelPendingPollRequest();
          return this; 
        };
  
        Longpoll.prototype.cancelPendingPollRequest = function() {
          if (this.nextRequestTimer) {
            global.clearTimeout(this.nextRequestTimer);
            this.nextRequestTimer = null;
          }
        };
  
        return Longpoll;
      })(),
      
      "__slave": (function() {
        function LocalStoreSlaveTransport(emit) {
          this.emit = emit;
          this.doNotReconnect = true;
          this.shared = null;
          this.name = "__slave";
          this.opt = defaultTransportOptions();
        }
  
        LocalStoreSlaveTransport.prototype.setup = function() {
          this.emit("transportSetup", this.opt, this.name);
          var count = 0;
          var property;
          for ( property in this.opt.headers ) count++;
          if (count != 0) {
            throw "__slave does not support headers";
          }
        };
  
        LocalStoreSlaveTransport.prototype.listen = function(url, shared) {
          this.shared = shared;
          this.opt.url = url;  
          this.setup();
          this.statusChangeChecker = ughbind(function(ev) {
            if(this.shared.matchEventKey(ev, "msg")) {
              var msgId = this.shared.get("msg:id");
              var contentType = this.shared.get("msg:content-type");
              var msg = this.shared.get("msg");
              this.emit("message", msg, {"id": msgId == "" ? undefined : msgId, "content-type": contentType == "" ? undefined : contentType});
            }
          }, this);
          global.addEventListener("storage", this.statusChangeChecker);
          //this.emit("transportNativeCreated", this, this.name);
        };
          
        LocalStoreSlaveTransport.prototype.cancel = function() {
          global.removeEventListener("storage", this.statusChangeChecker);
        };
        
        return LocalStoreSlaveTransport;
      })()
  
    };
  
    return NchanSubscriber;
  });
  