From 21b75fa15eaae7891310d4e191dc8fec2c8e48b6 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Fri, 18 Nov 2011 10:08:39 -0800 Subject: [PATCH 0001/1403] Initial import --- .gitignore | 2 + .npmignore | 1 + README.md | 153 ++++ lib/engine-client.js | 24 + lib/engine.js | 312 ++++++++ lib/event-emitter.js | 170 +++++ lib/parser.js | 102 +++ lib/transport.js | 163 ++++ lib/transports/flashsocket.js | 189 +++++ lib/transports/index.js | 8 + lib/transports/polling-jsonp.js | 120 +++ lib/transports/polling-xhr.js | 270 +++++++ lib/transports/polling.js | 158 ++++ lib/transports/websocket.js | 133 ++++ lib/util.js | 89 +++ package.json | 5 + support/should.js | 1238 +++++++++++++++++++++++++++++++ support/web-socket-js | 1 + test/engine-client.js | 12 + 19 files changed, 3150 insertions(+) create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 README.md create mode 100644 lib/engine-client.js create mode 100644 lib/engine.js create mode 100644 lib/event-emitter.js create mode 100644 lib/parser.js create mode 100644 lib/transport.js create mode 100644 lib/transports/flashsocket.js create mode 100644 lib/transports/index.js create mode 100644 lib/transports/polling-jsonp.js create mode 100644 lib/transports/polling-xhr.js create mode 100644 lib/transports/polling.js create mode 100644 lib/transports/websocket.js create mode 100644 lib/util.js create mode 100644 package.json create mode 100644 support/should.js create mode 160000 support/web-socket-js create mode 100644 test/engine-client.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..fd4f2b06 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +.DS_Store diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..296b2ccf --- /dev/null +++ b/.npmignore @@ -0,0 +1 @@ +support/ diff --git a/README.md b/README.md new file mode 100644 index 00000000..498dae89 --- /dev/null +++ b/README.md @@ -0,0 +1,153 @@ + +# Engine.IO clien + +This is the client for [Engine](http://github.com/learnboost/engine.io), the +implementation of transport-based cross-browser/cross-device bi-directional +communication layer for [Socket.IO](http://github.com/learnboost/socket.io). + +## Hello World + +```html + + +``` + +## Features + +- Lightweight + - Lazyloads Flash transport +- Isomorphic with WebSocket API +- Written for node, runs on browser thanks to + [browserbuild](http://github.com/learnboost/browserbuild) + - Maximizes code readability / maintenance. + - Simplifies testing. +- Transports are independent of `Engine` + - Easy to debug + - Easy to unit test +- Runs inside HTML5 WebWorker + +## API + +

+ +### Top-level + +These are exposed in the `io` global namespace (in the browser), or by +`require('engine-client')` (in Node.JS). + +#### Properties + +- `version` _(String)_: protocol revision number +- `Engine` _(Function)_: client constructor + +### Engine + +The client class. _Inherits from EventEmitter_. + +#### Properties + +- `onopen` (_Function_) + - `open` event handler +- `onmessage` (_Function_) + - `message` event handler +- `onclose` (_Function_) + - `message` event handler + +#### Events + +- `open` + - Fired upon successful connection. +- `message` + - Fired when data is received from the server. + - **Arguments** + - `String`: utf-8 encoded data +- `close` + - Fired upon disconnection. +- `error` + - Fired when an error occurs. + +#### Methods + +- **constructor** + - Initializes the client + - **Parameters** + - `Object`: optional, options object + - **Options** + - `host` (`String`): host name (`localhost`) + - `port` (`Number`): port name (`80`) + - `path` (`String`): path name + - `query` (`String`): optional query string addition (eg: + `a=b&c=hello+world`) + - `secure` (`Boolean): whether the connection is secure + - `upgrade` (`Boolean`): defaults to true, whether the client should try + to upgrade the transport from long-polling to something better. + - `forceJSONP` (`Boolean`): forces JSONP for polling transport. + - `transports` (`Array`): a list of transports to try (in order). + Defaults to `['polling', 'websocket', 'flashsocket']`. `Engine` + always attempts to connect directly with the first one, provided the + feature detection test for it passes. +- `send` + - Sends a message to the server + - **Parameters** + - `String`: data to send +- `close` + - Disconnects the client. + +## Tests + +`engine.io-client` is used to test +[engine](http://github.com/learnboost/engine.io) + +## Support + +The support channels for `engine.io-client` are the same as `socket.io`: + - irc.freenode.net **#socket.io** + - [Google Groups](http://groups.google.com/group/socket_io) + - [Website](http://socket.io) + +## Development + +To contribute patches, run tests or benchmarks, make sure to clone the +repository: + +``` +git clone git://github.com/LearnBoost/engine.io-client.git +``` + +Then: + +``` +cd engine.io-client +npm install +``` + +## License + +(The MIT License) + +Copyright (c) 2011 Guillermo Rauch <guillermo@learnboost.com> + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/engine-client.js b/lib/engine-client.js new file mode 100644 index 00000000..06d232d0 --- /dev/null +++ b/lib/engine-client.js @@ -0,0 +1,24 @@ + +/** + * Client version. + * + * @api public. + */ + +exports.engineVersion = '0.1.0'; + +/** + * Protocol version. + * + * @api public. + */ + +exports.engineProtocol = 1; + +/** + * Engine constructor. + * + * @api public. + */ + +exports.Engine = require('./engine'); diff --git a/lib/engine.js b/lib/engine.js new file mode 100644 index 00000000..c3b4e0ea --- /dev/null +++ b/lib/engine.js @@ -0,0 +1,312 @@ + +/** + * Module exports. + */ + +module.exports = exports = Engine; + +/** + * Export Transport. + */ + +exports.Transport = require('./transport'); + +/** + * Export transports + */ + +var transports = exports.transports = require('./transports'); + +/** + * Export utils. + */ + +exports.util = require('./util') + +/** + * Engine constructor. + * + * @param {Object} options + * @api public + */ + +function Engine (opts) { + opts = opts || {}; + + this.host = opts.host || opts.hostname || 'localhost'; + this.port = opts.port || 80; + this.upgrade = false !== opts.upgrade; + this.path = opts.path || '/engine.io' + this.forceJSONP = !!opts.forceJSONP; + this.transports = opts.transports || ['polling', 'websocket', 'flashsocket']; + this.readyState = ''; + + // handle inline function events (eg: `onopen`) + var evs = ['open', 'message', 'close'] + , self = this + + for (var i = 0, l = evs.length; i < l; i++) { + (function (ev) { + self.on(ev, function () { + if (self['on' + ev]) { + self['on' + ev].apply(this, arguments); + } + }); + })(evs[i]); + } + + this.init(); +}; + +/** + * Initializes transport to use and starts probe. + * + * @api private + */ + +Engine.prototype.init = function () { + // use the first transport always for the first try + this.setTransport(this.transports[0]); + + var self = this; + + // whether we should perform a probe + if (this.upgrade && this.transports.length > 1) { + var probeTransports = this.transports.slice(1) + , probes = [] + + function abort () { + for (var i = 0, l = probes.length; i++) { + probes[i].close(); + } + } + + for (var i = 0, l = probeTransports.length; i < l; i++) { + (function (i) { + var id = probeTransports[i] + + probes.push(this.probe(id, function (err) { + probes.splice(i, 1); + + if (err) { + self.emit('error', err); + self.log.debug('probing transport "%s" failed', id); + } else { + self.setTransport(probeTransports[i]); + abort(); + } + })); + })(); + } + } + + // flush write buffers + function flush () { + if (self.writeBuffer.length) { + // make sure to transfer the buffer to the transport + self.transport.buffer = true; + for (var i = 0, l = self.writeBuffer.length; i < l; i++) { + self.transport.send(self.writeBuffer[i]); + } + self.transport.flush(); + } + } + + this.on('open', flush); + this.on('upgrade', flush); +}; + +/** + * Sets the current transport. Disables the existing one (if any). + * + * @api private + */ + +Engine.prototype.setTransport = function (id) { + var self = this; + + function set () { + // make sure to set upgrading state + self.upgrading = false; + + // set up transport + self.transportId = id; + self.transport = new transports[id]({ + host: self.host + , port: self.port + , secure: self.secure + , path: self.path + , query: 'transport=' + id + (self.query ? '&' + self.query : '') + , forceJSONP: self.forceJSONP + }); + + // emit upgrade event + self.emit('upgrade', id); + + // set up transport listeners + self.transport.on('data', function (data) { + self.onMessage(parser.decodePacket(data)); + }); + self.transport.on('close', function () { + self.onClose(); + }); + self.transport.open(); + }; + + if (this.transport) { + // upgrade transports + if (!this.transport.pause) { + this.emit('error', new Error('Transport "' + this.transportId + + '" can\'t be upgraded.')); + return; + } + + this.upgrading = true; + this.transport.pause(set); + } else { + // first open + this.readyState = 'opening'; + set(); + } +}; + +/** + * Probes a tranposrt + * + * @param {String} transport id + * @param {Function} callback + * @api private + */ + +Engine.prototype.probe = function (id, fn) { + this.log.debug('probing transport "%s"', id); + + var transport = new transports[id]({ + host: this.host + , port: this.port + , secure: this.secure + , path: this.path + , query: 'transport=' id + }); + + transport.once('open', function () { + transport.write(parser.encodePacket('probe')); + transport.once('data', function (data) { + if ('probe' == parser.decodePacket(data).type) { + fn(); + } else { + var err = new Error('probe fail'); + err.transport = id; + fn(err); + } + }); + }); + + return transport; +}; + +/** + * Opens the connection + * + * @api public + */ + +Engine.prototype.open = function () { + if ('' == this.readyState || 'closed' == this.readyState) { + this.transport.open() + } + + return this; +}; + +/** + * Called when connection is deemed open. + * + * @api public + */ + +Engine.prototype.onOpen = function () { + this.readyState = 'open'; + this.emit('open'); +}; + +/** + * Handles a message. + * + * @api private + */ + +Engine.prototype.onMessage = function (msg) { + switch (msg.type) { + case 'open': + this.onOpen(); + break; + + case 'heartbeat': + this.writePacket('heartbeat'); + break; + } +}; + +/** + * Sends a message. + * + * @param {String} message. + * @return {Engine} for chaining. + * @api public + */ + +Engine.prototype.send = function (msg) { + this.writePacket('message', msg); + return this; +}; + +/** + * Encodes a packet and writes it out. + * + * @param {String} packet type. + * @param {String} data. + * @api private + */ + +Engine.prototype.writePacket = function (type, data) { + this.write(parser.encodePacket(type, data)); +}; + +/** + * Writes data. + * + * @api private + */ + +Engine.prototype.write = function (data) { + if ('open' != this.readyState || this.upgrading) { + this.writeBuffer.push(data); + } else { + this.transport.send(data); + } +}; + +/** + * Closes the connection. + * + * @api private + */ + +Engine.prototype.close = function () { + if ('opening' == this.readyState || 'open' == this.readyState) { + this.transport.close(); + } + return this; +}; + +/** + * Called upon transport close. + * + * @api private + */ + +Engine.prototype.onClose = function () { + this.readyState = 'closed'; + this.emit('close'); +}; diff --git a/lib/event-emitter.js b/lib/event-emitter.js new file mode 100644 index 00000000..3de5641b --- /dev/null +++ b/lib/event-emitter.js @@ -0,0 +1,170 @@ + +/** + * Module exports. + */ + +module.exports = EventEmitter; + +/** + * Event emitter constructor. + * + * @api public. + */ + +function EventEmitter () {}; + +/** + * Adds a listener + * + * @api public + */ + +EventEmitter.prototype.on = function (name, fn) { + if (!this.$events) { + this.$events = {}; + } + + if (!this.$events[name]) { + this.$events[name] = fn; + } else if (io.util.isArray(this.$events[name])) { + this.$events[name].push(fn); + } else { + this.$events[name] = [this.$events[name], fn]; + } + + return this; +}; + +EventEmitter.prototype.addListener = EventEmitter.prototype.on; + +/** + * Adds a volatile listener. + * + * @api public + */ + +EventEmitter.prototype.once = function (name, fn) { + var self = this; + + function on () { + self.removeListener(name, on); + fn.apply(this, arguments); + }; + + on.listener = fn; + this.on(name, on); + + return this; +}; + +/** + * Removes a listener. + * + * @api public + */ + +EventEmitter.prototype.removeListener = function (name, fn) { + if (this.$events && this.$events[name]) { + var list = this.$events[name]; + + if (io.util.isArray(list)) { + var pos = -1; + + for (var i = 0, l = list.length; i < l; i++) { + if (list[i] === fn || (list[i].listener && list[i].listener === fn)) { + pos = i; + break; + } + } + + if (pos < 0) { + return this; + } + + list.splice(pos, 1); + + if (!list.length) { + delete this.$events[name]; + } + } else if (list === fn || (list.listener && list.listener === fn)) { + delete this.$events[name]; + } + } + + return this; +}; + +/** + * Removes all listeners for an event. + * + * @api public + */ + +EventEmitter.prototype.removeAllListeners = function (name) { + if (name === undefined) { + this.$events = {}; + return this; + } + + if (this.$events && this.$events[name]) { + this.$events[name] = null; + } + + return this; +}; + +/** + * Gets all listeners for a certain event. + * + * @api publci + */ + +EventEmitter.prototype.listeners = function (name) { + if (!this.$events) { + this.$events = {}; + } + + if (!this.$events[name]) { + this.$events[name] = []; + } + + if (!io.util.isArray(this.$events[name])) { + this.$events[name] = [this.$events[name]]; + } + + return this.$events[name]; +}; + +/** + * Emits an event. + * + * @api public + */ + +EventEmitter.prototype.emit = function (name) { + if (!this.$events) { + return false; + } + + var handler = this.$events[name]; + + if (!handler) { + return false; + } + + var args = Array.prototype.slice.call(arguments, 1); + + if ('function' == typeof handler) { + handler.apply(this, args); + } else if (io.util.isArray(handler)) { + var listeners = handler.slice(); + + for (var i = 0, l = listeners.length; i < l; i++) { + listeners[i].apply(this, args); + } + } else { + return false; + } + + return true; +}; diff --git a/lib/parser.js b/lib/parser.js new file mode 100644 index 00000000..3f3039d7 --- /dev/null +++ b/lib/parser.js @@ -0,0 +1,102 @@ + +/** + * Packet types. + */ + +var packets = exports.packets = { + 'open': 0 + , 'close': 1 + , 'heartbeat': 2 + , 'message': 3 + , 'probe': 4 + , 'error': 5 + , 'noop': 6 +}; + +var packetslist = Object.keys(packets); + +/** + * Encodes a packet. + * + * @api private + */ + +exports.encodePacket = function (type, data) { + var encoded = packets[type] + + // data fragment is optional + if ('string' == typeof data) { + encoded += ':' + data; + } + + return encoded; +}; + +/** + * Decodes a packet. + * + * @return {Object} with `type` and `data` (if any) + * @api private + */ + +exports.decodePacket = function (data) { + if (~data.indexOf(':')) { + var pieces = data.split(':'); + return { type: packetslist[pieces[0]], data: pieces[1] }; + } else { + return { type: packetslist[data] }; + } +}; + +/** + * Encodes multiple messages (payload). + * + * @param {Array} messages + * @api private + */ + +exports.encodePayload = function (packets) { + var encoded = ''; + + if (packets.length == 1) { + return packets[0]; + } + + for (var i = 0, l = packets.length; i < l; i++) { + encoded += '\ufffd' + packets[i].length + '\ufffd' + packets[i] + } + + return encoded; +}; + +/* + * Decodes data when a payload is maybe expected. + * + * @param {String} data + * @return {Array} messages + * @api public + */ + +exports.decodePayload = function (data) { + if (undefined == data || null == data) { + return []; + } + + if (data[0] == '\ufffd') { + var ret = []; + + for (var i = 1, length = ''; i < data.length; i++) { + if (data[i] == '\ufffd') { + ret.push(data.substr(i + 1).substr(0, length)); + i += Number(length) + 1; + length = ''; + } else { + length += data[i]; + } + } + + return ret; + } else { + return [data]; + } +} diff --git a/lib/transport.js b/lib/transport.js new file mode 100644 index 00000000..b5225879 --- /dev/null +++ b/lib/transport.js @@ -0,0 +1,163 @@ + +/** + * Module dependencies. + */ + +var util = require('./util') + , EventEmitter = require('./event-emitter') + +/** + * Module exports. + */ + +module.exports = Transport; + +/** + * Transport abstract constructor. + * + * @param {Object} opts. + * @api private + */ + +function Transport (opts) { + this.options = opts; + this.readyState = ''; +}; + +/** + * Inherits from EventEmitter. + */ + +util.inherits(Transport, EventEmitter); + +/** + * Whether to buffer outgoing data. + * + * @api public + */ + +Transport.prototype.buffer = true; + +/** + * Emits an error. + * + * @param {String} str + * @return {Transport} for chaining + * @api public + */ + +Transport.prototype.error = function (str) { + this.emit('error', new Error(str)); + return this; +}; + +/** + * Opens the transport. + * + * @api public + */ + +Transport.prototype.open = function () { + if ('closed' == this.readyState || '' == this.readyState) { + this.readyState = 'opening'; + this.doOpen(); + } + + return this; +}; + +/** + * Closes the transport. + * + * @api private + */ + +Transport.prototype.close = function () { + if ('opening' == this.readyState || 'open' == this.readyState) { + this.doClose(); + this.onClose(); + } + + return this; +}; + +/** + * Sends a message. + * + * @param {String} data + * @api public + */ + +Transport.prototype.send = function (data) { + if (this.readyState != 'open') { + this.error('write error'); + } else { + if (this.buffer) { + if (!this.writeBuffer) { + this.writeBuffer = []; + } + + this.writeBuffer.push(data); + return this; + } + + var self = this; + this.buffer = true; + this.write(data, function () { + self.flush(); + }); + } + + return this; +}; + +/** + * Flushes the buffer. + * + * @api private + */ + +Transport.prototype.flush = function () { + this.buffer = false; + this.emit('flush'); + + if (this.writeBuffer.length) { + this.writeMany(self.writeBuffer); + this.writeBuffer = []; + } + + return this; +}; + +/** + * Called upon open + * + * @api private + */ + +Transport.prototype.onOpen = function () { + this.readyState = 'open'; + this.emit('open'); +}; + +/** + * Called with data. + * + * @param {String} data + * @api private + */ + +Transport.prototype.onData = function (data) { + this.emit('data', data); +}; + +/** + * Called upon close. + * + * @api private + */ + +Transport.prototype.onClose = function () { + this.readyState = 'closed'; + this.emit('close'); +}; diff --git a/lib/transports/flashsocket.js b/lib/transports/flashsocket.js new file mode 100644 index 00000000..64b53cbd --- /dev/null +++ b/lib/transports/flashsocket.js @@ -0,0 +1,189 @@ + +/** + * Module dependencies. + */ + +var WebSocket = require('./websocket') + , util = require('../util') + +/** + * Module exports. + */ + +module.exports = FlashWS; + +/** + * FlashWS constructor. + * + * @param {Engine} engine instance. + * @api public + */ + +function FlashWS (options) { + WebSocket.call(this, options); +}; + +/** + * Inherits from WebSocket. + */ + +util.inherits(FlashWS, WebSocket); + +/** + * Opens the transport. + * + * @api public + */ + +FlashWS.prototype.doOpen = function () { + if (!check) { + // let the probe timeout + return; + } + + var base = io.enginePath + '/support/web-socket-js/' + , self = this + + // TODO: proxy logging to client logger + WEB_SOCKET_LOGGER = function () { } + WEB_SOCKET_SWF_LOCATION = base + '/WebSocketMainInsecure.swf'; + + $script.ready([base + 'swfobject.js', base + 'web_socket.js'], function () { + FlashWs.prototype.doOpen.call(self); + }); +}; + +/** + * Feature detection for FlashSocket. + * + * @return {Boolean} whether this transport is available. + * @api public + */ + +function check () { + // if node + return false; + // end + + for (var i = 0, l = navigator.plugins.length; i < l; i++) { + if (navigator.plugins[i].indexOf('Shockwave Flash')) { + return true; + } + } + + return false; +}; + +/** + * Dependency injection helper. + * @license MIT - Copyright Dustin Diaz - Jacob Thornton - 2011 + */ + +var $script = (function () { + var win = this, doc = document + , head = doc.getElementsByTagName('head')[0] + , validBase = /^https?:\/\// + , old = win.$script, list = {}, ids = {}, delay = {}, scriptpath + , scripts = {}, s = 'string', f = false + , push = 'push', domContentLoaded = 'DOMContentLoaded', readyState = 'readyState' + , addEventListener = 'addEventListener', onreadystatechange = 'onreadystatechange' + + function every(ar, fn, i) { + for (i = 0, j = ar.length; i < j; ++i) if (!fn(ar[i])) return f + return 1 + } + function each(ar, fn) { + every(ar, function(el) { + return !fn(el) + }) + } + + if (!doc[readyState] && doc[addEventListener]) { + doc[addEventListener](domContentLoaded, function fn() { + doc.removeEventListener(domContentLoaded, fn, f) + doc[readyState] = 'complete' + }, f) + doc[readyState] = 'loading' + } + + function $script(paths, idOrDone, optDone) { + paths = paths[push] ? paths : [paths] + var idOrDoneIsDone = idOrDone && idOrDone.call + , done = idOrDoneIsDone ? idOrDone : optDone + , id = idOrDoneIsDone ? paths.join('') : idOrDone + , queue = paths.length + function loopFn(item) { + return item.call ? item() : list[item] + } + function callback() { + if (!--queue) { + list[id] = 1 + done && done() + for (var dset in delay) { + every(dset.split('|'), loopFn) && !each(delay[dset], loopFn) && (delay[dset] = []) + } + } + } + setTimeout(function () { + each(paths, function(path) { + if (scripts[path]) { + id && (ids[id] = 1) + return scripts[path] == 2 && callback() + } + scripts[path] = 1 + id && (ids[id] = 1) + create(!validBase.test(path) && scriptpath ? scriptpath + path + '.js' : path, callback) + }) + }, 0) + return $script + } + + function create(path, fn) { + var el = doc.createElement('script') + , loaded = f + el.onload = el.onerror = el[onreadystatechange] = function () { + if ((el[readyState] && !(/^c|loade/.test(el[readyState]))) || loaded) return; + el.onload = el[onreadystatechange] = null + loaded = 1 + scripts[path] = 2 + fn() + } + el.async = 1 + el.src = path + head.insertBefore(el, head.firstChild) + } + + $script.get = create + + $script.order = function (scripts, id, done) { + (function callback(s) { + s = scripts.shift() + if (!scripts.length) $script(s, id, done) + else $script(s, callback) + }()) + } + + $script.path = function(p) { + scriptpath = p + } + $script.ready = function(deps, ready, req) { + deps = deps[push] ? deps : [deps] + var missing = []; + !each(deps, function(dep) { + list[dep] || missing[push](dep); + }) && every(deps, function(dep) {return list[dep]}) ? + ready() : !function(key) { + delay[key] = delay[key] || [] + delay[key][push](ready) + req && req(missing) + }(deps.join('|')) + return $script + } + + $script.noConflict = function () { + win.$script = old; + return this + } + + return $script +}); diff --git a/lib/transports/index.js b/lib/transports/index.js new file mode 100644 index 00000000..8de0155b --- /dev/null +++ b/lib/transports/index.js @@ -0,0 +1,8 @@ + +/** + * Export transports. + */ + +exports.polling = require('./polling'); +exports.websocket = require('./websocket'); +exports.flashsocket = require('./flashsocket'); diff --git a/lib/transports/polling-jsonp.js b/lib/transports/polling-jsonp.js new file mode 100644 index 00000000..4f14fabf --- /dev/null +++ b/lib/transports/polling-jsonp.js @@ -0,0 +1,120 @@ + +/** + * Module requirements. + */ + +var Transport = require('../transport') + , Polling = require('./polling') + , util = require('../util') + +/** + * Noop. + */ + +function empty () { } + +/** + * Module exports. + */ + +module.exports = JSONPPolling; + +/** + * JSONP Polling constructor. + * + * @param {Object} opts. + * @api public + */ + +function JSONPPolling (opts) { + Transport.call(this, opts); + this.setIndex(); +}; + +/** + * Inherits from Polling. + */ + +util.inherits(JSONPPolling, Polling); + +/** + * Sets JSONP global callback. + * + * @api private + */ + +JSONPPolling.prototype.setIndex = function () { + var self = this; + + // if we have an index already, set it to empy + if (undefined != this.index) { + io.j[this.index] = empty; + } + + this.index = io.j.length; + io.j.push(function (msg) { + self.onData(msg); + }); +}; + +/** + * Opens the socket. + * + * @api private + */ + +JSONPPolling.prototype.doOpen = function () { + var self = this; + util.defer(function () { + Polling.prototype.doOpen.call(self); + }); +}; + +/** + * Closes the socket + * + * @api private + */ + +JSONPPolling.prototype.doClose = function () { + this.setIndex(); + + if (this.script) { + this.script.parentNode.removeChild(this.script); + } +}; + +/** + * Starts a poll cycle. + * + * @api private + */ + +JSONPPolling.prototype.doPoll = function () { + var self = this + , script = document.createElement('script') + , query = io.util.query( + this.socket.options.query + , 't='+ (+new Date) + '&i=' + this.index + ); + + if (this.script) { + this.script.parentNode.removeChild(this.script); + this.script = null; + } + + script.async = true; + script.src = this.prepareUrl() + query; + + var insertAt = document.getElementsByTagName('script')[0] + insertAt.parentNode.insertBefore(script, insertAt); + this.script = script; + + if (util.ua.gecko) { + setTimeout(function () { + var iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + document.body.removeChild(iframe); + }, 100); + } +}; diff --git a/lib/transports/polling-xhr.js b/lib/transports/polling-xhr.js new file mode 100644 index 00000000..c24fcb40 --- /dev/null +++ b/lib/transports/polling-xhr.js @@ -0,0 +1,270 @@ + +/** + * Module requirements. + */ + +var Transport = require('../transport') + , Polling = require('./polling') + , EventEmitter = require('../event-emitter') + , util = require('../util') + , global = this + +/** + * Module exports. + */ + +module.exports = XHRPolling; +module.exports.Request = Request; + +/** + * Empty function + */ + +function empty () { } + +/** + * XHR Polling constructor. + * + * @param {Object} opts. + * @api public + */ + +function XHRPolling (opts) { + Transport.call(this, opts); + // if browser + this.xd = opts.host != global.location.hostname + || global.location.port != opts.port; + //end +}; + +/** + * Inherits from Polling. + */ + +util.inherits(XHRPolling, Polling); + +/** + * Opens the socket + * + * @api private + */ + +XHRPolling.prototype.doOpen = function () { + var self = this; + util.defer(function () { + Polling.prototype.open.call(self); + }); +}; + +/** + * Closes the socket. + * + * @api private + */ + +XHRPolling.prototype.doClose = function () { + if (this.pollXhr) { + this.pollXhr.abort(); + } + if (this.sendXhr) { + this.sendXhr.abort(); + } +}; + +/** + * Creates a request. + * + * @param {String} method + * @api private + */ + +XHR.prototype.request = function (opts) { + opts.uri = this.uri(); + opts.xd = this.xd; + var req = new Request(opts); + req.on('error', function () { + self.close(); + }); + return req; +}; + +/** + * Sends data. + * + * @param {String} data to send. + * @param {Function} called upon flush. + * @api private + */ + +XHR.prototype.write = function (data, fn) { + var req = this.request({ method: 'POST', data: data }) + , self = this + + req.on('success', fn); +}; + +/** + * Starts a poll cycle. + * + * @api private + */ + +XHRPolling.prototype.doPoll = function () { + this.pollXhr = this.request(); +}; + +/** + * Request constructor + * + * @param {Object} options + * @api public + */ + +function Request (opts) { + this.method = opts.method || 'GET'; + this.uri = opts.uri; + this.xd = !!opts.xd; + this.async = false !== opts.async; + this.data = undefined != opts.data ? opts.data : null; + this.create(); +} + +/** + * Inherits from Polling. + */ + +util.inherits(Request, EventEmitter); + +/** + * Creates the XHR object and sends the request. + * + * @api private + */ + +Request.prototype.create = function () { + var xhr = this.xhr = util.request(this.xd); + this.xhr.open(this.method, this.uri, this.async); + + if ('POST' == this.method) { + try { + if (xhr.setRequestHeader) { + // xmlhttprequest + xhr.setRequestHeader('Content-type', 'text/plain;charset=UTF-8'); + } else { + // xdomainrequest + xhr.contentType = 'text/plain'; + } + } catch (e) {} + } + + if (this.xd && this.xhr instanceof XDomainRequest) { + this.xhr.onerror = function () { + self.onError(); + }; + this.xhr.onload = function () { + self.onData(xhr.responseText); + }; + this.xhr.onprogress = empty; + } else { + this.xhr.onreadystatechange = function () { + try { + if (xhr.readyState != 4) return; + + if (200 == xhr.status) { + self.onData(xhr.responseText); + } else { + self.onError(); + } + } catch () { + self.onError(); + } + }; + } + + this.xhr.send(this.data); + + if (global.ActiveXObject) { + this.index = Request.requestsCount++; + Request.requests[this.index] = this; + } +}; + +/** + * Called upon successful response. + * + * @api private + */ + +Request.prototype.onSuccess = function () { + this.emit('success'); + this.cleanup(); +} + +/** + * Called if we have data. + * + * @api private + */ + +Request.prototype.onData = function (data) { + this.emit('data', data); + this.onSuccess(); +} + +/** + * Called upon error. + * + * @api private + */ + +Request.prototype.onError = function () { + this.emit('error'); + this.cleanup(); +} + +/** + * Cleans up house. + * + * @api private + */ + +Request.prototype.cleanup = function () { + // xmlhttprequest + this.xhr.onreadystatechange = empty; + + // xdomainrequest + this.xhr.onload = this.xhr.onerror = empty; + + try { + this.xhr.abort(); + } catch(e) {} + + if (global.ActiveXObject) { + delete Browser.requests[this.index]; + } + + this.xhr = null; +} + +/** + * Aborts the request. + * + * @api public + */ + +Request.prototype.abort = function () { + this.cleanup(); +}; + +if (global.ActiveXObject) { + Request.requestsCount = 0; + Request.requests = {}; + + global.attachEvent('onunload', function () { + for (var i in Request.requests) { + if (Request.requests.hasOwnProperty(i)) { + Request.requests[i].abort(); + } + } + }); +} diff --git a/lib/transports/polling.js b/lib/transports/polling.js new file mode 100644 index 00000000..44eeded8 --- /dev/null +++ b/lib/transports/polling.js @@ -0,0 +1,158 @@ + +/** + * Module dependencies. + */ + +var Transport = require('../transport') + , XHR = require('./polling-xhr') + , JSON = require('./polling-json') + , util = require('../util') + , parser = require('../parser') + , global = this + +/** + * Module exports. + */ + +module.exports = Polling; + +/** + * Polling transport polymorphic constructor. + * Decides on xhr vs jsonp based on feature detection. + * + * @api public + */ + +function Polling (opts) { + var xd; + + if (global.location) { + xd = opts.host != global.location.hostname || global.location.port != opts.port; + } + + var xhr = request(xd); + + if (xhr && !opts.forceJSONP) { + // if we support xhr + return new XHR; + } else { + return new JSONP; + } +}; + +/** + * Inherits from Transport. + */ + +util.inherits(Polling, Transport); + +/** + * Sets a callback for the next poll. + * + * @api private + */ + +Polling.prototype.nextPoll = function (fn) { + this.onNextPoll = fn; + return this; +}; + +/** + * Opens the socket (triggers polling). + * + * @api private + */ + +Polling.prototype.doOpen = function () { + this.poll(); +}; + +/** + * Pauses polling. + * + * @param {Function} callback upon flush + * @api private + */ + +Polling.prototype.pause = function (onFlush) { + this.paused = true; + + var pending = 0; + + if (this.polling) { + pending++; + this.once('data', function () { + --pending || onFlush(); + } + } + + if (this.buffer) { + pending++; + this.once('flush', function () { + --pending || onFlush(); + }); + } +}; + +/** + * Starts polling cycle. + * + * @api public + */ + +Polling.prototype.poll = function () { + if (!this.paused) { + this.polling = true; + this.doPoll(); + this.emit('poll'); + } +}; + +/** + * Pauses polling. + * + * @param {Function} callback when buffers are flushed + * @api private + */ + +Polling.prototype.pause = function (fn) { + this.paused = true; + this.removeAllListeners(); +}; + +/** + * Overloads onData to detect payloads. + * + * @api private + */ + +Polling.prototype.onData = function (data) { + if ('open' != this.readyState) { + this.onOpen(); + } + + var packets = parser.decodePayload(data); + + for (var i = 0, l = packets.length; i < l; i++) { + Transport.prototype.onData.call(this, packets[i]); + } + + // if we got data we're not polling + this.polling = false; + + // trigger next poll + this.poll(); + + return ret; +}; + +/** + * Writes a packets payload. + * + * @param {Array} data packets + * @api private + */ + +Polling.prototype.writeMany = function (packets) { + this.write(parser.encodePayload(packets)); +}; diff --git a/lib/transports/websocket.js b/lib/transports/websocket.js new file mode 100644 index 00000000..0b6e2873 --- /dev/null +++ b/lib/transports/websocket.js @@ -0,0 +1,133 @@ + +/** + * Module dependencies. + */ + +var Transport = require('../transport') + , util = require('../util') + , global = this + +/** + * Module exports. + */ + +module.exports = WS; + +/** + * WebSocket transport constructor. + * + * @api {Object} connection options + * @api public + */ + +function WS (opts) { + Transport.call(this, opts); +}; + +/** + * Inherits from Transport. + */ + +util.inherits(WS, Transport); + +/** + * Opens socket. + * + * @api private + */ + +WS.prototype.doOpen = function () { + if (!check()) { + // let probe timeout + return; + } + + this.socket = new ws()(this.uri); + this.socket.onopen = function () { + self.onOpen(); + }; + this.socket.onclose = function () { + self.onClose(); + }; + this.socket.ondata = function (ev) { + self.onData(ev.data); + }; +}; + +/** + * Writes data to socket. + * + * @param {String} data. + * @param {Function} flush callback. + * @api private + */ + +WS.prototype.write = function (data, fn) { + this.socket.send(data); + fn(); +}; + +/** + * Writes a packets payload. + * + * @param {Array} data packets + * @api private + */ + +Polling.prototype.writeMany = function (packets) { + for (var i = 0, l = packets.length; i < l; i++) { + this.write(packets[0]); + } +}; + +/** + * Closes socket. + * + * @api private + */ + +WS.prototype.doClose = function () { + this.socket.close(); +}; + +/** + * Generates uri for connection. + * + * @api private + */ + +WS.prototype.uri = function () { + return [ + this.options.secure ? 'wss' : 'ws' + , this.options.host + , ':' + , this.options.port + , this.options.path + , this.options.query + ].join('') +}; + +/** + * Getter for WS constructor. + * + * @api private + */ + +function ws () { + // if node + return require('easy-websocket'); + // end + + return global.WebSocket || global.MozWebSocket; +} + +/** + * Feature detection for WebSocket. + * + * @return {Boolean} whether this transport is available. + * @api public + */ + +function check () { + return !!ws(); +} diff --git a/lib/util.js b/lib/util.js new file mode 100644 index 00000000..d4ab665c --- /dev/null +++ b/lib/util.js @@ -0,0 +1,89 @@ + +/** + * Inheritance. + * + * @param {Function} ctor a + * @param {Function} ctor b + * @api public + */ + +exports.inherits = function inherits (a, b) { + function c () { } + c.prototype = b.prototype; + a.prototype = new c; +} + +/** + * UA / engines detection namespace. + * + * @namespace + */ + +util.ua = {}; + +/** + * Whether the UA supports CORS for XHR. + * + * @api public + */ + +util.ua.hasCORS = 'undefined' != typeof XMLHttpRequest && (function () { + try { + var a = new XMLHttpRequest(); + } catch (e) { + return false; + } + + return a.withCredentials != undefined; +})(); + +/** + * Detect webkit. + * + * @api public + */ + +util.ua.webkit = 'undefined' != typeof navigator && + /webkit/i.test(navigator.userAgent); + +/** + * Detect gecko. + * + * @api public + */ + +util.ua.gecko = 'undefined' != typeof navigator && + /gecko/i.test(navigator.userAgent); + +// end + +/** + * XHR request helper. + * + * @param {Boolean} whether we need xdomain + * @api private + */ + +util.request = function request (xdomain) { + // if node + var XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest; + return new XMLHttpRequest(); + // end + + if (xdomain && 'undefined' != typeof XDomainRequest) { + return new XDomainRequest(); + } + + // XMLHttpRequest can be disabled on IE + try { + if ('undefined' != typeof XMLHttpRequest && (!xdomain || util.ua.hasCORS)) { + return new XMLHttpRequest(); + } + } catch (e) { } + + if (!xdomain) { + try { + return new ActiveXObject('Microsoft.XMLHTTP'); + } catch(e) { } + } +}; diff --git a/package.json b/package.json new file mode 100644 index 00000000..a7a83b43 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "name": "engine-client" + , "description": "Client for the realtime Engine" + , "version": "0.0.1" +} diff --git a/support/should.js b/support/should.js new file mode 100644 index 00000000..2a8fb976 --- /dev/null +++ b/support/should.js @@ -0,0 +1,1238 @@ + +/** + * should.js by TJ Holowaychuk (MIT), adapted to run in browser and node. + */ + +(function (should) { + + if ('undefined' != typeof exports) { + module.exports = exports = should = require('assert'); + } + + /** + * Expose constructor. + */ + + should.Assertion = Assertion; + + /** + * Possible assertion flags. + */ + + var flags = { + not: ['be', 'have', 'include'] + , an: ['instance'] + , and: ['be', 'have', 'include', 'an'] + , be: ['an'] + , have: ['an', 'own'] + , include: ['an'] + , not: ['include', 'have', 'be'] + , own: [] + , instance: [] + }; + + /** + * Extend Object.prototype. + */ + + if ('object' == typeof process) { + Object.defineProperty( + Object.prototype + , 'should' + , { + get: function () { + var self = this.valueOf() + , fn = function () { + return new Assertion(self); + }; + + if ('undefined' != typeof exports) { + fn.__proto__ = exports; + fn.exports = exports; + } + + return fn; + } + , enumerable: false + } + ); + } else { + Object.prototype.should = function () { + return new Assertion(this.valueOf()); + }; + } + + /** + * Constructor + * + * @api private + */ + + function Assertion (obj) { + if (obj !== undefined) { + this.obj = obj; + this.flags = {}; + + var $flags = keys(flags); + + for (var i = 0, l = $flags.length; i < l; i++) { + this[$flags[i]] = new FlaggedAssertion(this, $flags[i]); + } + } + }; + + /** + * Performs an assertion + * + * @api private + */ + + Assertion.prototype.assert = function (truth, msg, error) { + var msg = this.flags.not ? error : msg + , ok = this.flags.not ? !truth : truth; + + if (!ok) { + throw new Error(msg); + } + + this.flags = {}; + }; + + /** + * Checks if the value is true + * + * @api public + */ + + Assertion.prototype.be_true = function () { + this.assert( + this.obj === true + , 'expected ' + i(this.obj) + ' to be true' + , 'expected ' + i(this.obj) + ' to not be true'); + return this; + }; + + /** + * Checks if the value is true + * + * @api public + */ + + Assertion.prototype.be_false = function () { + this.assert( + this.obj === false + , 'expected ' + i(this.obj) + ' to be false' + , 'expected ' + i(this.obj) + ' to not be false' + ); + return this; + }; + + /** + * Check if the value is truthy + * + * @api public + */ + + Assertion.prototype.ok = function () { + this.assert( + this.obj == true + , 'expected ' + i(this.obj) + ' to be true' + , 'expected ' + i(this.obj) + ' to not be true'); + }; + + /** + * Checks if the array is empty. + * + * @api public + */ + + Assertion.prototype.empty = function () { + this.obj.should().have.property('length'); + this.assert( + 0 === this.obj.length + , 'expected ' + i(this.obj) + ' to be empty' + , 'expected ' + i(this.obj) + ' to not be empty'); + return this; + }; + + /** + * Checks if the obj is arguments. + * + * @api public + */ + + Assertion.prototype.arguments = function () { + this.assert( + '[object Arguments]' == Object.prototype.toString.call(this.obj) + , 'expected ' + i(this.obj) + ' to be arguments' + , 'expected ' + i(this.obj) + ' to not be arguments'); + return this; + }; + + /** + * Checks if the obj exactly equals another. + * + * @api public + */ + + Assertion.prototype.equal = function (obj) { + this.assert( + obj === this.obj + , 'expected ' + i(this.obj) + ' to equal ' + i(obj) + , 'expected ' + i(this.obj) + ' to not equal ' + i(obj)); + return this; + }; + + /** + * Checks if the obj sortof equals another. + * + * @api public + */ + + Assertion.prototype.eql = function (obj) { + this.assert( + should.eql(obj, this.obj) + , 'expected ' + i(this.obj) + ' to sort of equal ' + i(obj) + , 'expected ' + i(this.obj) + ' to sort of not equal ' + i(obj)); + return this; + }; + + /** + * Assert within start to finish (inclusive). + * + * @param {Number} start + * @param {Number} finish + * @api public + */ + + Assertion.prototype.within = function (start, finish) { + var range = start + '..' + finish; + this.assert( + this.obj >= start && this.obj <= finish + , 'expected ' + i(this.obj) + ' to be within ' + range + , 'expected ' + i(this.obj) + ' to not be within ' + range); + return this; + }; + + /** + * Assert typeof. + * + * @api public + */ + + Assertion.prototype.a = function (type) { + this.assert( + type == typeof this.obj + , 'expected ' + i(this.obj) + ' to be a ' + type + , 'expected ' + i(this.obj) + ' not to be a ' + type); + return this; + }; + + /** + * Assert instanceof. + * + * @api public + */ + + Assertion.prototype.of = function (constructor) { + var name = constructor.name; + this.assert( + this.obj instanceof constructor + , 'expected ' + i(this.obj) + ' to be an instance of ' + name + , 'expected ' + i(this.obj) + ' not to be an instance of ' + name); + return this; + }; + + /** + * Assert numeric value above _n_. + * + * @param {Number} n + * @api public + */ + + Assertion.prototype.greaterThan = + Assertion.prototype.above = function (n) { + this.assert( + this.obj > n + , 'expected ' + i(this.obj) + ' to be above ' + n + , 'expected ' + i(this.obj) + ' to be below ' + n); + return this; + }; + + /** + * Assert numeric value below _n_. + * + * @param {Number} n + * @api public + */ + + Assertion.prototype.lessThan = + Assertion.prototype.below = function (n) { + this.assert( + this.obj < n + , 'expected ' + i(this.obj) + ' to be below ' + n + , 'expected ' + i(this.obj) + ' to be above ' + n); + return this; + }; + + /** + * Assert string value matches _regexp_. + * + * @param {RegExp} regexp + * @api public + */ + + Assertion.prototype.match = function (regexp) { + this.assert( + regexp.exec(this.obj) + , 'expected ' + i(this.obj) + ' to match ' + regexp + , 'expected ' + i(this.obj) + ' not to match ' + regexp); + return this; + }; + + /** + * Assert property "length" exists and has value of _n_. + * + * @param {Number} n + * @api public + */ + + Assertion.prototype.length = function (n) { + this.obj.should().have.property('length'); + var len = this.obj.length; + this.assert( + n == len + , 'expected ' + i(this.obj) + ' to have a length of ' + n + ' but got ' + len + , 'expected ' + i(this.obj) + ' to not have a length of ' + len); + return this; + }; + + /** + * Assert substring. + * + * @param {String} str + * @api public + */ + + Assertion.prototype.string = function(str){ + this.obj.should().be.a('string'); + this.assert( + ~this.obj.indexOf(str) + , 'expected ' + i(this.obj) + ' to include ' + i(str) + , 'expected ' + i(this.obj) + ' to not include ' + i(str)); + return this; + }; + + /** + * Assert inclusion of object. + * + * @param {Object} obj + * @api public + */ + + Assertion.prototype.object = function(obj){ + this.obj.should().be.a('object'); + var included = true; + for (var key in obj) { + if (obj.hasOwnProperty(key) && !should.eql(obj[key], this.obj[key])) { + included = false; + break; + } + } + this.assert( + included + , 'expected ' + i(this.obj) + ' to include ' + i(obj) + , 'expected ' + i(this.obj) + ' to not include ' + i(obj)); + return this; + }; + + /** + * Assert property _name_ exists, with optional _val_. + * + * @param {String} name + * @param {Mixed} val + * @api public + */ + + Assertion.prototype.property = function (name, val) { + if (this.flags.own) { + this.assert( + this.obj.hasOwnProperty(name) + , 'expected ' + i(this.obj) + ' to have own property ' + i(name) + , 'expected ' + i(this.obj) + ' to not have own property ' + i(name)); + return this; + } + + if (this.flags.not && undefined !== val) { + if (undefined === this.obj[name]) { + throw new Error(i(this.obj) + ' has no property ' + i(name)); + } + } else { + this.assert( + undefined !== this.obj[name] + , 'expected ' + i(this.obj) + ' to have a property ' + i(name) + , 'expected ' + i(this.obj) + ' to not have a property ' + i(name)); + } + + if (undefined !== val) { + this.assert( + val === this.obj[name] + , 'expected ' + i(this.obj) + ' to have a property ' + i(name) + + ' of ' + i(val) + ', but got ' + i(this.obj[name]) + , 'expected ' + i(this.obj) + ' to not have a property ' + i(name) + + ' of ' + i(val)); + } + + this.obj = this.obj[name]; + return this; + }; + + /** + * Assert that the array contains _obj_. + * + * @param {Mixed} obj + * @api public + */ + + Assertion.prototype.contain = function (obj) { + this.obj.should().be.an.instance.of(Array); + this.assert( + ~indexOf(this.obj, obj) + , 'expected ' + i(this.obj) + ' to contain ' + i(obj) + , 'expected ' + i(this.obj) + ' to not contain ' + i(obj)); + return this; + }; + + /** + * Assert exact keys or inclusion of keys by using + * the `.include` modifier. + * + * @param {Array|String ...} keys + * @api public + */ + + Assertion.prototype.key = + Assertion.prototype.keys = function (keys) { + var str + , ok = true; + + keys = keys instanceof Array + ? keys + : Array.prototype.slice.call(arguments); + + if (!keys.length) throw new Error('keys required'); + + var actual = keys(this.obj) + , len = keys.length; + + // Inclusion + ok = every(keys, function(key){ + return ~indexOf(actual, key); + }); + + // Strict + if (!this.flags.not && !this.flags.include) { + ok = ok && keys.length == actual.length; + } + + // Key string + if (len > 1) { + keys = map(keys, function(key){ + return i(key); + }); + var last = keys.pop(); + str = keys.join(', ') + ', and ' + last; + } else { + str = i(keys[0]); + } + + // Form + str = (len > 1 ? 'keys ' : 'key ') + str; + + // Have / include + str = (this.flag.include ? 'include ' : 'have ') + str; + + // Assertion + this.assert( + ok + , 'expected ' + i(this.obj) + ' to ' + str + , 'expected ' + i(this.obj) + ' to not ' + str); + + return this; + }; + + /** + * Assertion with a flag. + * + * @api private + */ + + function FlaggedAssertion (parent, flag) { + this.parent = parent; + this.obj = parent.obj; + + this.flag = flag; + this.flags = {}; + this.flags[flag] = true; + + for (var i in parent.flags) { + if (parent.flags.hasOwnProperty(i)) { + this.flags[i] = true; + } + } + + var $flags = flags[flag]; + + for (var i = 0, l = $flags.length; i < l; i++) { + this[$flags[i]] = new FlaggedAssertion(this, $flags[i]); + } + }; + + /** + * Inherits from assertion + */ + + FlaggedAssertion.prototype = new Assertion; + + /** + * Array every compatibility + * + * @see bit.ly/5Fq1N2 + * @api public + */ + + function every (arr, fn, thisObj) { + var scope = thisObj || window; + for (var i = 0, j = arr.length; i < j; ++i) { + if (!fn.call(scope, arr[i], i, arr)) { + return false; + } + } + return true; + }; + + /** + * Array indexOf compatibility. + * + * @see bit.ly/a5Dxa2 + * @api public + */ + + function indexOf (arr, o, i) { + if (Array.prototype.indexOf) { + return Array.prototype.indexOf.call(arr, o, i); + } + + for (var j = arr.length, i = i < 0 ? i + j < 0 ? 0 : i + j : i || 0 + ; i < j && arr[i] !== o; i++); + + return j <= i ? -1 : i; + }; + + /** + * Inspects an object. + * + * @see taken from node.js `util` module (copyright Joyent, MIT license) + * @api private + */ + + function i (obj, showHidden, depth) { + var seen = []; + + function stylize (str) { + return str; + }; + + function format (value, recurseTimes) { + // Provide a hook for user-specified inspect functions. + // Check that value is an object with an inspect function on it + if (value && typeof value.inspect === 'function' && + // Filter out the util module, it's inspect function is special + value !== exports && + // Also filter out any prototype objects using the circular check. + !(value.constructor && value.constructor.prototype === value)) { + return value.inspect(recurseTimes); + } + + // Primitive types cannot have properties + switch (typeof value) { + case 'undefined': + return stylize('undefined', 'undefined'); + + case 'string': + var simple = '\'' + json.stringify(value).replace(/^"|"$/g, '') + .replace(/'/g, "\\'") + .replace(/\\"/g, '"') + '\''; + return stylize(simple, 'string'); + + case 'number': + return stylize('' + value, 'number'); + + case 'boolean': + return stylize('' + value, 'boolean'); + } + // For some reason typeof null is "object", so special case here. + if (value === null) { + return stylize('null', 'null'); + } + + // Look up the keys of the object. + var visible_keys = keys(value); + var $keys = showHidden ? Object.getOwnPropertyNames(value) : visible_keys; + + // Functions without properties can be shortcutted. + if (typeof value === 'function' && $keys.length === 0) { + if (isRegExp(value)) { + return stylize('' + value, 'regexp'); + } else { + var name = value.name ? ': ' + value.name : ''; + return stylize('[Function' + name + ']', 'special'); + } + } + + // Dates without properties can be shortcutted + if (isDate(value) && $keys.length === 0) { + return stylize(value.toUTCString(), 'date'); + } + + var base, type, braces; + // Determine the object type + if (isArray(value)) { + type = 'Array'; + braces = ['[', ']']; + } else { + type = 'Object'; + braces = ['{', '}']; + } + + // Make functions say that they are functions + if (typeof value === 'function') { + var n = value.name ? ': ' + value.name : ''; + base = (isRegExp(value)) ? ' ' + value : ' [Function' + n + ']'; + } else { + base = ''; + } + + // Make dates with properties first say the date + if (isDate(value)) { + base = ' ' + value.toUTCString(); + } + + if ($keys.length === 0) { + return braces[0] + base + braces[1]; + } + + if (recurseTimes < 0) { + if (isRegExp(value)) { + return stylize('' + value, 'regexp'); + } else { + return stylize('[Object]', 'special'); + } + } + + seen.push(value); + + var output = map($keys, function(key) { + var name, str; + if (value.__lookupGetter__) { + if (value.__lookupGetter__(key)) { + if (value.__lookupSetter__(key)) { + str = stylize('[Getter/Setter]', 'special'); + } else { + str = stylize('[Getter]', 'special'); + } + } else { + if (value.__lookupSetter__(key)) { + str = stylize('[Setter]', 'special'); + } + } + } + if (indexOf(visible_keys, key) < 0) { + name = '[' + key + ']'; + } + if (!str) { + if (indexOf(seen, value[key]) < 0) { + if (recurseTimes === null) { + str = format(value[key]); + } else { + str = format(value[key], recurseTimes - 1); + } + if (str.indexOf('\n') > -1) { + if (isArray(value)) { + str = map(str.split('\n'), function(line) { + return ' ' + line; + }).join('\n').substr(2); + } else { + str = '\n' + map(str.split('\n'), function(line) { + return ' ' + line; + }).join('\n'); + } + } + } else { + str = stylize('[Circular]', 'special'); + } + } + if (typeof name === 'undefined') { + if (type === 'Array' && key.match(/^\d+$/)) { + return str; + } + name = json.stringify('' + key); + if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) { + name = name.substr(1, name.length - 2); + name = stylize(name, 'name'); + } else { + name = name.replace(/'/g, "\\'") + .replace(/\\"/g, '"') + .replace(/(^"|"$)/g, "'"); + name = stylize(name, 'string'); + } + } + + return name + ': ' + str; + }); + + seen.pop(); + + var numLinesEst = 0; + var length = reduce(output, function(prev, cur) { + numLinesEst++; + if (indexOf(cur, '\n') >= 0) numLinesEst++; + return prev + cur.length + 1; + }, 0); + + if (length > 50) { + output = braces[0] + + (base === '' ? '' : base + '\n ') + + ' ' + + output.join(',\n ') + + ' ' + + braces[1]; + + } else { + output = braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1]; + } + + return output; + } + return format(obj, (typeof depth === 'undefined' ? 2 : depth)); + }; + + function isArray (ar) { + return ar instanceof Array || + Object.prototype.toString.call(ar) == '[object Array]'; + }; + + function isRegExp(re) { + var s = '' + re; + return re instanceof RegExp || // easy case + // duck-type for context-switching evalcx case + typeof(re) === 'function' && + re.constructor.name === 'RegExp' && + re.compile && + re.test && + re.exec && + s.match(/^\/.*\/[gim]{0,3}$/); + }; + + function isDate(d) { + if (d instanceof Date) return true; + return false; + }; + + function keys (obj) { + if (Object.keys) { + return Object.keys(obj); + } + + var keys = []; + + for (var i in obj) { + if (obj.hasOwnProperty(i)) { + keys.push(i); + } + } + + return keys; + } + + function map (arr, mapper, that) { + if (Array.prototype.map) { + return Array.prototype.map.call(arr, mapper, that); + } + + var other= new Array(arr.length); + + for (var i= 0, n = arr.length; i= 2) { + var rv = arguments[1]; + } else { + do { + if (i in this) { + rv = this[i++]; + break; + } + + // if array contains no values, no initial value to return + if (++i >= len) + throw new TypeError(); + } while (true); + } + + for (; i < len; i++) { + if (i in this) + rv = fun.call(null, rv, this[i], i, this); + } + + return rv; + }; + + /** + * Strict equality + * + * @api public + */ + + should.equal = function (a, b) { + if (a !== b) { + should.fail('expected ' + i(a) + ' to equal ' + i(b)); + } + }; + + /** + * Fails with msg + * + * @param {String} msg + * @api public + */ + + should.fail = function (msg) { + throw new Error(msg); + }; + + /** + * Asserts deep equality + * + * @see taken from node.js `assert` module (copyright Joyent, MIT license) + * @api private + */ + + should.eql = function eql (actual, expected) { + // 7.1. All identical values are equivalent, as determined by ===. + if (actual === expected) { + return true; + } else if ('undefined' != typeof Buffer + && Buffer.isBuffer(actual) && Buffer.isBuffer(expected)) { + if (actual.length != expected.length) return false; + + for (var i = 0; i < actual.length; i++) { + if (actual[i] !== expected[i]) return false; + } + + return true; + + // 7.2. If the expected value is a Date object, the actual value is + // equivalent if it is also a Date object that refers to the same time. + } else if (actual instanceof Date && expected instanceof Date) { + return actual.getTime() === expected.getTime(); + + // 7.3. Other pairs that do not both pass typeof value == "object", + // equivalence is determined by ==. + } else if (typeof actual != 'object' && typeof expected != 'object') { + return actual == expected; + + // 7.4. For all other Object pairs, including Array objects, equivalence is + // determined by having the same number of owned properties (as verified + // with Object.prototype.hasOwnProperty.call), the same set of keys + // (although not necessarily the same order), equivalent values for every + // corresponding key, and an identical "prototype" property. Note: this + // accounts for both named and indexed properties on Arrays. + } else { + return objEquiv(actual, expected); + } + } + + function isUndefinedOrNull (value) { + return value === null || value === undefined; + } + + function isArguments (object) { + return Object.prototype.toString.call(object) == '[object Arguments]'; + } + + function objEquiv (a, b) { + if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) + return false; + // an identical "prototype" property. + if (a.prototype !== b.prototype) return false; + //~~~I've managed to break Object.keys through screwy arguments passing. + // Converting to array solves the problem. + if (isArguments(a)) { + if (!isArguments(b)) { + return false; + } + a = pSlice.call(a); + b = pSlice.call(b); + return should.eql(a, b); + } + try{ + var ka = keys(a), + kb = keys(b), + key, i; + } catch (e) {//happens when one is a string literal and the other isn't + return false; + } + // having the same number of owned properties (keys incorporates hasOwnProperty) + if (ka.length != kb.length) + return false; + //the same set of keys (although not necessarily the same order), + ka.sort(); + kb.sort(); + //~~~cheap key test + for (i = ka.length - 1; i >= 0; i--) { + if (ka[i] != kb[i]) + return false; + } + //equivalent values for every corresponding key, and + //~~~possibly expensive deep test + for (i = ka.length - 1; i >= 0; i--) { + key = ka[i]; + if (!should.eql(a[key], b[key])) + return false; + } + return true; + } + + var json = (function () { + "use strict"; + + if ('object' == typeof JSON && JSON.parse && JSON.stringify) { + return { + parse: nativeJSON.parse + , stringify: nativeJSON.stringify + } + } + + var JSON = {}; + + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + function date(d, key) { + return isFinite(d.valueOf()) ? + d.getUTCFullYear() + '-' + + f(d.getUTCMonth() + 1) + '-' + + f(d.getUTCDate()) + 'T' + + f(d.getUTCHours()) + ':' + + f(d.getUTCMinutes()) + ':' + + f(d.getUTCSeconds()) + 'Z' : null; + }; + + var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + gap, + indent, + meta = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' + }, + rep; + + + function quote(string) { + + // If the string contains no control characters, no quote characters, and no + // backslash characters, then we can safely slap some quotes around it. + // Otherwise we must also replace the offending characters with safe escape + // sequences. + + escapable.lastIndex = 0; + return escapable.test(string) ? '"' + string.replace(escapable, function (a) { + var c = meta[a]; + return typeof c === 'string' ? c : + '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : '"' + string + '"'; + } + + + function str(key, holder) { + + // Produce a string from holder[key]. + + var i, // The loop counter. + k, // The member key. + v, // The member value. + length, + mind = gap, + partial, + value = holder[key]; + + // If the value has a toJSON method, call it to obtain a replacement value. + + if (value instanceof Date) { + value = date(key); + } + + // If we were called with a replacer function, then call the replacer to + // obtain a replacement value. + + if (typeof rep === 'function') { + value = rep.call(holder, key, value); + } + + // What happens next depends on the value's type. + + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + + // JSON numbers must be finite. Encode non-finite numbers as null. + + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + + // If the value is a boolean or null, convert it to a string. Note: + // typeof null does not produce 'null'. The case is included here in + // the remote chance that this gets fixed someday. + + return String(value); + + // If the type is 'object', we might be dealing with an object or an array or + // null. + + case 'object': + + // Due to a specification blunder in ECMAScript, typeof null is 'object', + // so watch out for that case. + + if (!value) { + return 'null'; + } + + // Make an array to hold the partial results of stringifying this object value. + + gap += indent; + partial = []; + + // Is the value an array? + + if (Object.prototype.toString.apply(value) === '[object Array]') { + + // The value is an array. Stringify every element. Use null as a placeholder + // for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + + // Join all of the elements together, separated with commas, and wrap them in + // brackets. + + v = partial.length === 0 ? '[]' : gap ? + '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : + '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + + // If the replacer is an array, use it to select the members to be stringified. + + if (rep && typeof rep === 'object') { + length = rep.length; + for (i = 0; i < length; i += 1) { + if (typeof rep[i] === 'string') { + k = rep[i]; + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } else { + + // Otherwise, iterate through all of the keys in the object. + + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } + + // Join all of the member texts together, separated with commas, + // and wrap them in braces. + + v = partial.length === 0 ? '{}' : gap ? + '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : + '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + } + + // If the JSON object does not yet have a stringify method, give it one. + + JSON.stringify = function (value, replacer, space) { + + // The stringify method takes a value and an optional replacer, and an optional + // space parameter, and returns a JSON text. The replacer can be a function + // that can replace values, or an array of strings that will select the keys. + // A default replacer method can be provided. Use of the space parameter can + // produce text that is more easily readable. + + var i; + gap = ''; + indent = ''; + + // If the space parameter is a number, make an indent string containing that + // many spaces. + + if (typeof space === 'number') { + for (i = 0; i < space; i += 1) { + indent += ' '; + } + + // If the space parameter is a string, it will be used as the indent string. + + } else if (typeof space === 'string') { + indent = space; + } + + // If there is a replacer, it must be a function or an array. + // Otherwise, throw an error. + + rep = replacer; + if (replacer && typeof replacer !== 'function' && + (typeof replacer !== 'object' || + typeof replacer.length !== 'number')) { + throw new Error('JSON.stringify'); + } + + // Make a fake root object containing our value under the key of ''. + // Return the result of stringifying the value. + + return str('', {'': value}); + }; + + // If the JSON object does not yet have a parse method, give it one. + + JSON.parse = function (text, reviver) { + // The parse method takes a text and an optional reviver function, and returns + // a JavaScript value if the text is a valid JSON text. + + var j; + + function walk(holder, key) { + + // The walk method is used to recursively walk the resulting structure so + // that modifications can be made. + + var k, v, value = holder[key]; + if (value && typeof value === 'object') { + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + } + + + // Parsing happens in four stages. In the first stage, we replace certain + // Unicode characters with escape sequences. JavaScript handles many characters + // incorrectly, either silently deleting them, or treating them as line endings. + + text = String(text); + cx.lastIndex = 0; + if (cx.test(text)) { + text = text.replace(cx, function (a) { + return '\\u' + + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }); + } + + // In the second stage, we run the text against regular expressions that look + // for non-JSON patterns. We are especially concerned with '()' and 'new' + // because they can cause invocation, and '=' because it can cause mutation. + // But just to be safe, we want to reject all unexpected forms. + + // We split the second stage into 4 regexp operations in order to work around + // crippling inefficiencies in IE's and Safari's regexp engines. First we + // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we + // replace all simple value tokens with ']' characters. Third, we delete all + // open brackets that follow a colon or comma or that begin the text. Finally, + // we look to see that the remaining characters are only whitespace or ']' or + // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + + if (/^[\],:{}\s]*$/ + .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') + .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') + .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + + // In the third stage we use the eval function to compile the text into a + // JavaScript structure. The '{' operator is subject to a syntactic ambiguity + // in JavaScript: it can begin a block or an object literal. We wrap the text + // in parens to eliminate the ambiguity. + + j = eval('(' + text + ')'); + + // In the optional fourth stage, we recursively walk the new structure, passing + // each name/value pair to a reviver function for possible transformation. + + return typeof reviver === 'function' ? + walk({'': j}, '') : j; + } + + // If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError('JSON.parse'); + }; + + return JSON; + })(); + +})('undefined' != typeof exports ? exports : (should = {})); diff --git a/support/web-socket-js b/support/web-socket-js new file mode 160000 index 00000000..14023075 --- /dev/null +++ b/support/web-socket-js @@ -0,0 +1 @@ +Subproject commit 14023075bfa7350a3a2268cb6d7e38e046b24bcf diff --git a/test/engine-client.js b/test/engine-client.js new file mode 100644 index 00000000..3d27c9fc --- /dev/null +++ b/test/engine-client.js @@ -0,0 +1,12 @@ + +describe('engine-client', function () { + + it('should expose version number', function () { + io.engineVersion.should().match(/); + }); + + it('should expose protocol number', function () { + + }); + +}); From f5aacb7347623498f76b3c6fb50ae6082363ad7a Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 27 Nov 2011 10:21:55 -0800 Subject: [PATCH 0002/1403] Expose utils locally. --- lib/engine.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/engine.js b/lib/engine.js index c3b4e0ea..84b6f5be 100644 --- a/lib/engine.js +++ b/lib/engine.js @@ -21,7 +21,7 @@ var transports = exports.transports = require('./transports'); * Export utils. */ -exports.util = require('./util') +var util = exports.util = require('./util') /** * Engine constructor. From e96e0252ffada4e96cc10b9c6bc4abd64589bdc8 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 27 Nov 2011 10:22:19 -0800 Subject: [PATCH 0003/1403] Handle `ws`/`ws` URIs. --- lib/engine.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/engine.js b/lib/engine.js index 84b6f5be..bc5f29f1 100644 --- a/lib/engine.js +++ b/lib/engine.js @@ -31,6 +31,13 @@ var util = exports.util = require('./util') */ function Engine (opts) { + if ('string' == typeof opts) { + var uri = util.parseUri(opts); + opts.host = uri.host; + opts.secure = uri.scheme == 'wss' + opts.port = uri.port || (opts.secure ? 443 : 80); + } + opts = opts || {}; this.host = opts.host || opts.hostname || 'localhost'; From 0ae78039009f12d580f8f2b227f9eb75e835ec28 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 27 Nov 2011 10:22:50 -0800 Subject: [PATCH 0004/1403] Added `createTransport` method. --- lib/engine.js | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/lib/engine.js b/lib/engine.js index bc5f29f1..a519a269 100644 --- a/lib/engine.js +++ b/lib/engine.js @@ -48,21 +48,28 @@ function Engine (opts) { this.transports = opts.transports || ['polling', 'websocket', 'flashsocket']; this.readyState = ''; - // handle inline function events (eg: `onopen`) - var evs = ['open', 'message', 'close'] - , self = this + this.open(); +}; - for (var i = 0, l = evs.length; i < l; i++) { - (function (ev) { - self.on(ev, function () { - if (self['on' + ev]) { - self['on' + ev].apply(this, arguments); - } - }); - })(evs[i]); - } +/** + * Creates transport of the given type. + * + * @api {String} transport name + * @return {Transport} + * @api private + */ - this.init(); +Engine.prototype.createTransport = function (name) { + var transport = new transports[name]({ + host: self.host + , port: self.port + , secure: self.secure + , path: self.path + '/' + name + , query: self.query ? '&' + self.query : '' + , forceJSONP: self.forceJSONP + }, engine); + + return transport; }; /** From 153d59aa2123a99405f072a29bf4f0f5f2732565 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 27 Nov 2011 10:23:14 -0800 Subject: [PATCH 0005/1403] Simplified init mechanism. --- lib/engine.js | 61 +++++++++++++++------------------------------------ 1 file changed, 18 insertions(+), 43 deletions(-) diff --git a/lib/engine.js b/lib/engine.js index a519a269..2abd5c60 100644 --- a/lib/engine.js +++ b/lib/engine.js @@ -78,56 +78,31 @@ Engine.prototype.createTransport = function (name) { * @api private */ -Engine.prototype.init = function () { - // use the first transport always for the first try - this.setTransport(this.transports[0]); - +Engine.prototype.open = function () { var self = this; - // whether we should perform a probe - if (this.upgrade && this.transports.length > 1) { - var probeTransports = this.transports.slice(1) - , probes = [] + this.readyState = 'opening'; - function abort () { - for (var i = 0, l = probes.length; i++) { - probes[i].close(); - } - } + // use the first transport always for the first try + var transport = this.createTransport(this.transports[0]); + transport.open(); + transport.once('open', function () { + this.setTransport(transport); + }); + + // if the engine is closed before transport opened, abort it + this.once('close', function () { + transport.close(); + }); + + // whether we should perform a probe + if (this.upgrade && this.transports.length > 1 && transport.pause) { + var probeTransports = this.transports.slice(1); for (var i = 0, l = probeTransports.length; i < l; i++) { - (function (i) { - var id = probeTransports[i] - - probes.push(this.probe(id, function (err) { - probes.splice(i, 1); - - if (err) { - self.emit('error', err); - self.log.debug('probing transport "%s" failed', id); - } else { - self.setTransport(probeTransports[i]); - abort(); - } - })); - })(); + this.probe(probeTransports[i]); } } - - // flush write buffers - function flush () { - if (self.writeBuffer.length) { - // make sure to transfer the buffer to the transport - self.transport.buffer = true; - for (var i = 0, l = self.writeBuffer.length; i < l; i++) { - self.transport.send(self.writeBuffer[i]); - } - self.transport.flush(); - } - } - - this.on('open', flush); - this.on('upgrade', flush); }; /** From 0b27f857586364fefa15c3e36d902a3c77f7c45a Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 27 Nov 2011 10:23:31 -0800 Subject: [PATCH 0006/1403] Simplified `setTransport`. --- lib/engine.js | 103 ++++++++++++++++++++------------------------------ 1 file changed, 42 insertions(+), 61 deletions(-) diff --git a/lib/engine.js b/lib/engine.js index 2abd5c60..186fb57c 100644 --- a/lib/engine.js +++ b/lib/engine.js @@ -111,87 +111,68 @@ Engine.prototype.open = function () { * @api private */ -Engine.prototype.setTransport = function (id) { +Engine.prototype.setTransport = function (transport) { var self = this; - function set () { - // make sure to set upgrading state - self.upgrading = false; + // set up transport + this.transport = transport; - // set up transport - self.transportId = id; - self.transport = new transports[id]({ - host: self.host - , port: self.port - , secure: self.secure - , path: self.path - , query: 'transport=' + id + (self.query ? '&' + self.query : '') - , forceJSONP: self.forceJSONP - }); - - // emit upgrade event - self.emit('upgrade', id); - - // set up transport listeners - self.transport.on('data', function (data) { - self.onMessage(parser.decodePacket(data)); - }); - self.transport.on('close', function () { + // set up transport listeners + transport + .on('message', function (data) { + self.onMessage(msg); + }) + .on('close', function () { self.onClose(); - }); - self.transport.open(); - }; - - if (this.transport) { - // upgrade transports - if (!this.transport.pause) { - this.emit('error', new Error('Transport "' + this.transportId - + '" can\'t be upgraded.')); - return; - } - - this.upgrading = true; - this.transport.pause(set); - } else { - // first open - this.readyState = 'opening'; - set(); - } + }) }; /** - * Probes a tranposrt + * Probes a transport. * - * @param {String} transport id - * @param {Function} callback + * @param {String} transport name * @api private */ -Engine.prototype.probe = function (id, fn) { - this.log.debug('probing transport "%s"', id); +Engine.prototype.probe = function (name) { + this.log.debug('probing transport "%s"', name); - var transport = new transports[id]({ - host: this.host - , port: this.port - , secure: this.secure - , path: this.path - , query: 'transport=' id - }); + var transport = this.createTransport(name) + , self = this transport.once('open', function () { transport.write(parser.encodePacket('probe')); - transport.once('data', function (data) { - if ('probe' == parser.decodePacket(data).type) { - fn(); + transport.once('message', function (message) { + if ('probe' == message.type) { + self.upgrading = true; + self.emit('upgrading', name); + + self.transport.pause(function () { + self.setTransport(self.transport); + self.upgrading = false; + self.emit('upgrade', name); + }); } else { - var err = new Error('probe fail'); - err.transport = id; - fn(err); + var err = new Error('probe error'); + err.transport = transport.name; + self.emit('error', err); } }); }); - return transport; + transport.open(); + + // if closed prematurely, abort probe + this.once('close', function () { + transport.close(); + }); + + // if another probe suceeds, abort this one + this.once('upgrading', function (to) { + if (to != name) { + transport.close(); + } + }); }; /** From fc9d9f25c56577c5483f688d76bbf2107032a00e Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 27 Nov 2011 10:23:47 -0800 Subject: [PATCH 0007/1403] Handle `onopen` property manually. --- lib/engine.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/engine.js b/lib/engine.js index 186fb57c..acaa4188 100644 --- a/lib/engine.js +++ b/lib/engine.js @@ -198,6 +198,7 @@ Engine.prototype.open = function () { Engine.prototype.onOpen = function () { this.readyState = 'open'; this.emit('open'); + this.onopen && this.onopen.call(this); }; /** From c00744c78f262e5bb4fbd7d282b27c62a121ab86 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 27 Nov 2011 10:28:06 -0800 Subject: [PATCH 0008/1403] Added onMessage handlers. --- lib/engine.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/engine.js b/lib/engine.js index acaa4188..fb2f88a1 100644 --- a/lib/engine.js +++ b/lib/engine.js @@ -209,12 +209,26 @@ Engine.prototype.onOpen = function () { Engine.prototype.onMessage = function (msg) { switch (msg.type) { + case 'noop': + break; + case 'open': this.onOpen(); break; - case 'heartbeat': - this.writePacket('heartbeat'); + case 'ping': + this.writePacket('pong'); + break; + + case 'error': + var err = new Error('server error'); + err.code = msg.data; + this.emit('error', err); + break; + + case 'message': + this.emit('message', msg.data); + this.onmessage && this.onmessage.call(this, msg.data); break; } }; From 60a52e209d7df458c0df0016f9387d14c3ee93d5 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 27 Nov 2011 10:28:20 -0800 Subject: [PATCH 0009/1403] Abstracted buffer flushing logic into its own fn. --- lib/engine.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lib/engine.js b/lib/engine.js index fb2f88a1..de0ea4ee 100644 --- a/lib/engine.js +++ b/lib/engine.js @@ -233,6 +233,26 @@ Engine.prototype.onMessage = function (msg) { } }; +/** + * Flush write buffers. + * + * @api private + */ + +Engine.prototype.flush = function () { + if (this.writeBuffer.length) { + // make sure to transfer the buffer to the transport + this.transport.buffer = true; + + for (var i = 0, l = this.writeBuffer.length; i < l; i++) { + this.transport.send(this.writeBuffer[i]); + } + + // force transport flush + this.transport.flush(); + } +}; + /** * Sends a message. * From d9f8fa494faed9aaf1c4e0846cfda77d869a7f00 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 27 Nov 2011 10:28:59 -0800 Subject: [PATCH 0010/1403] Improved `close`. --- lib/engine.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/engine.js b/lib/engine.js index de0ea4ee..3831b380 100644 --- a/lib/engine.js +++ b/lib/engine.js @@ -300,8 +300,13 @@ Engine.prototype.write = function (data) { Engine.prototype.close = function () { if ('opening' == this.readyState || 'open' == this.readyState) { - this.transport.close(); + if (this.transport) { + this.transport.close(); + } + + this.onClose(); } + return this; }; From ecb288598eb1623ddeddc169292f138146f30a35 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 27 Nov 2011 10:29:08 -0800 Subject: [PATCH 0011/1403] Added onclose handler. --- lib/engine.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/engine.js b/lib/engine.js index 3831b380..61305230 100644 --- a/lib/engine.js +++ b/lib/engine.js @@ -319,4 +319,5 @@ Engine.prototype.close = function () { Engine.prototype.onClose = function () { this.readyState = 'closed'; this.emit('close'); + this.onclose && this.onclose.call(this); }; From bd04f2771a879e60ae1cea1fba954d5b075f4d51 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 27 Nov 2011 10:29:30 -0800 Subject: [PATCH 0012/1403] Added WebSocket API compatibility to EventEmitter. --- lib/event-emitter.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/event-emitter.js b/lib/event-emitter.js index 3de5641b..75b2268c 100644 --- a/lib/event-emitter.js +++ b/lib/event-emitter.js @@ -168,3 +168,11 @@ EventEmitter.prototype.emit = function (name) { return true; }; + +/** + * Compatibility with WebSocket + */ + +EventEmitter.prototype.addEventListener = EventEmitter.prototype.on; +EventEmitter.prototype.removeEventListener = EventEmitter.prototype.removeListener; +EventEmitter.prototype.dispatchEvent = EventEmitter.prototype.emit; From bd0601a66e7e809d88357ed74d4d3a37ff279006 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 27 Nov 2011 10:29:46 -0800 Subject: [PATCH 0013/1403] Improved message types - heartbeat is now a ping/pong - probing is now a ping/pong --- lib/parser.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/parser.js b/lib/parser.js index 3f3039d7..ee62736a 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -4,13 +4,13 @@ */ var packets = exports.packets = { - 'open': 0 - , 'close': 1 - , 'heartbeat': 2 - , 'message': 3 - , 'probe': 4 - , 'error': 5 - , 'noop': 6 + 'open': 1 + , 'close': 2 + , 'ping': 3 + , 'pong': 4 + , 'message': 5 + , 'error': 6 + , 'noop': 7 }; var packetslist = Object.keys(packets); From f77420c3173723b201ee205f3eac7625eecec5d4 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 27 Nov 2011 10:34:16 -0800 Subject: [PATCH 0014/1403] Gave up, we need an Engine reference from transport. --- lib/transport.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/transport.js b/lib/transport.js index b5225879..f1d4ddc0 100644 --- a/lib/transport.js +++ b/lib/transport.js @@ -15,12 +15,14 @@ module.exports = Transport; /** * Transport abstract constructor. * - * @param {Object} opts. + * @param {Object} options. + * @param {Engine} optional, owner Engine instance. * @api private */ -function Transport (opts) { +function Transport (opts, engine) { this.options = opts; + this.engine = engine; this.readyState = ''; }; From 5435b043ff801a41ec827f0e518e32cbb3950519 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 27 Nov 2011 10:37:54 -0800 Subject: [PATCH 0015/1403] Make sure Transport#error emits a `transport error` --- lib/transport.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/transport.js b/lib/transport.js index f1d4ddc0..7cc67ec9 100644 --- a/lib/transport.js +++ b/lib/transport.js @@ -16,13 +16,11 @@ module.exports = Transport; * Transport abstract constructor. * * @param {Object} options. - * @param {Engine} optional, owner Engine instance. * @api private */ -function Transport (opts, engine) { +function Transport (opts) { this.options = opts; - this.engine = engine; this.readyState = ''; }; @@ -48,8 +46,10 @@ Transport.prototype.buffer = true; * @api public */ -Transport.prototype.error = function (str) { - this.emit('error', new Error(str)); +Transport.prototype.error = function (code) { + var err = new Error('transport error'); + err.code = code; + this.emit('error', code); return this; }; From 64f371477b20e14ac00b5a904362cd4a63751e73 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 27 Nov 2011 10:38:17 -0800 Subject: [PATCH 0016/1403] Improved error message --- lib/transport.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/transport.js b/lib/transport.js index 7cc67ec9..da5b47a5 100644 --- a/lib/transport.js +++ b/lib/transport.js @@ -92,7 +92,7 @@ Transport.prototype.close = function () { Transport.prototype.send = function (data) { if (this.readyState != 'open') { - this.error('write error'); + this.error('not open'); } else { if (this.buffer) { if (!this.writeBuffer) { From 6c0a7146413ad0acf65fbc047b51dbf1ccebc8f9 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 27 Nov 2011 10:38:26 -0800 Subject: [PATCH 0017/1403] Removed `data` event and do parsing in transport. Unfortunately, it makes our API less clean to depend on specific parsing done by the Transport when clearly it doesnt belong there. The problem is that XHR polling *needs* specific messages to compensate for lack of features such as the ability to signal a connection close. Since we're already doing peaking of message types in the polling transport, we want to avoid the performance penalty of double parsing at the polling level and later at the Engine level, and we sacrifice a little bit of API purity. --- lib/transport.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/transport.js b/lib/transport.js index da5b47a5..e7862cc2 100644 --- a/lib/transport.js +++ b/lib/transport.js @@ -150,7 +150,7 @@ Transport.prototype.onOpen = function () { */ Transport.prototype.onData = function (data) { - this.emit('data', data); + this.onMessage(parser.decodePacket(data)); }; /** From 5a2580ec7f2548e9f43591472462a941aa1b9087 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 27 Nov 2011 11:25:15 -0800 Subject: [PATCH 0018/1403] Added reusable noop --- lib/transports/flashsocket.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/transports/flashsocket.js b/lib/transports/flashsocket.js index 64b53cbd..f4ba194d 100644 --- a/lib/transports/flashsocket.js +++ b/lib/transports/flashsocket.js @@ -12,6 +12,12 @@ var WebSocket = require('./websocket') module.exports = FlashWS; +/** + * Noop. + */ + +function empty () { } + /** * FlashWS constructor. * From 73263b98bc2b997d42a8003b455f934221cfa0b6 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 27 Nov 2011 11:25:22 -0800 Subject: [PATCH 0019/1403] Added transport name to proto. --- lib/transports/flashsocket.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/transports/flashsocket.js b/lib/transports/flashsocket.js index f4ba194d..97fc3a57 100644 --- a/lib/transports/flashsocket.js +++ b/lib/transports/flashsocket.js @@ -35,6 +35,14 @@ function FlashWS (options) { util.inherits(FlashWS, WebSocket); +/** + * Transport name. + * + * @api public + */ + +FlashWS.prototype.name = 'flashsocket'; + /** * Opens the transport. * From 6360ebd3cc5560e3068e223420f569942a820246 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 27 Nov 2011 11:25:33 -0800 Subject: [PATCH 0020/1403] Proxy web-socket-js debugging to our own --- lib/transports/flashsocket.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/transports/flashsocket.js b/lib/transports/flashsocket.js index 97fc3a57..81b0322c 100644 --- a/lib/transports/flashsocket.js +++ b/lib/transports/flashsocket.js @@ -58,11 +58,18 @@ FlashWS.prototype.doOpen = function () { var base = io.enginePath + '/support/web-socket-js/' , self = this - // TODO: proxy logging to client logger - WEB_SOCKET_LOGGER = function () { } - WEB_SOCKET_SWF_LOCATION = base + '/WebSocketMainInsecure.swf'; + function log (type) { + return function (msg) { + return self.log(type, msg); + } + }; - $script.ready([base + 'swfobject.js', base + 'web_socket.js'], function () { + // TODO: proxy logging to client logger + WEB_SOCKET_LOGGER = { log: log('debug'), error: log('error') }; + WEB_SOCKET_SWF_LOCATION = base + '/WebSocketMainInsecure.swf'; + WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR = true; + + load(base + 'swfobject.js', base + 'web_socket.js', function () { FlashWs.prototype.doOpen.call(self); }); }; From 1286e41301d02306d48b1349626de830a7c24e97 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 27 Nov 2011 11:26:27 -0800 Subject: [PATCH 0021/1403] Fixed last commit --- lib/transports/flashsocket.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/transports/flashsocket.js b/lib/transports/flashsocket.js index 81b0322c..329c1b19 100644 --- a/lib/transports/flashsocket.js +++ b/lib/transports/flashsocket.js @@ -60,7 +60,7 @@ FlashWS.prototype.doOpen = function () { function log (type) { return function (msg) { - return self.log(type, msg); + return self.log[type](msg); } }; From 9ad1f7f1587fc74cfc205220d8e24cdbff2a39ad Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 27 Nov 2011 11:26:52 -0800 Subject: [PATCH 0022/1403] Simplified script loader. --- lib/transports/flashsocket.js | 143 ++++++++++------------------------ 1 file changed, 43 insertions(+), 100 deletions(-) diff --git a/lib/transports/flashsocket.js b/lib/transports/flashsocket.js index 329c1b19..3eb51fc0 100644 --- a/lib/transports/flashsocket.js +++ b/lib/transports/flashsocket.js @@ -96,115 +96,58 @@ function check () { }; /** - * Dependency injection helper. - * @license MIT - Copyright Dustin Diaz - Jacob Thornton - 2011 + * Lazy loading of scripts. + * Based on $script by Dustin Diaz - MIT */ -var $script = (function () { - var win = this, doc = document - , head = doc.getElementsByTagName('head')[0] - , validBase = /^https?:\/\// - , old = win.$script, list = {}, ids = {}, delay = {}, scriptpath - , scripts = {}, s = 'string', f = false - , push = 'push', domContentLoaded = 'DOMContentLoaded', readyState = 'readyState' - , addEventListener = 'addEventListener', onreadystatechange = 'onreadystatechange' +var scripts = {}; - function every(ar, fn, i) { - for (i = 0, j = ar.length; i < j; ++i) if (!fn(ar[i])) return f - return 1 - } - function each(ar, fn) { - every(ar, function(el) { - return !fn(el) - }) - } +/** + * Injects a script. Keeps tracked of injected ones. + * + * @param {String} path + * @param {Function} callback + * @api private + */ - if (!doc[readyState] && doc[addEventListener]) { - doc[addEventListener](domContentLoaded, function fn() { - doc.removeEventListener(domContentLoaded, fn, f) - doc[readyState] = 'complete' - }, f) - doc[readyState] = 'loading' - } +function create (path, fn) { + if (scripts[path]) return fn(); - function $script(paths, idOrDone, optDone) { - paths = paths[push] ? paths : [paths] - var idOrDoneIsDone = idOrDone && idOrDone.call - , done = idOrDoneIsDone ? idOrDone : optDone - , id = idOrDoneIsDone ? paths.join('') : idOrDone - , queue = paths.length - function loopFn(item) { - return item.call ? item() : list[item] + var el = doc.createElement('script') + , loaded = false + + el.onload = el.onreadystatechange = function () { + var rs = el.readyState; + + if ((!rs || rs == 'loaded' || rs == 'complete') && !loaded) { + el.onload = el.onreadystatechange = null; + loaded = 1; + // prevent double execution across multiple instances + scripts[path] = true; + fn(); } - function callback() { - if (!--queue) { - list[id] = 1 - done && done() - for (var dset in delay) { - every(dset.split('|'), loopFn) && !each(delay[dset], loopFn) && (delay[dset] = []) - } - } - } - setTimeout(function () { - each(paths, function(path) { - if (scripts[path]) { - id && (ids[id] = 1) - return scripts[path] == 2 && callback() - } - scripts[path] = 1 - id && (ids[id] = 1) - create(!validBase.test(path) && scriptpath ? scriptpath + path + '.js' : path, callback) - }) - }, 0) - return $script - } + }; - function create(path, fn) { - var el = doc.createElement('script') - , loaded = f - el.onload = el.onerror = el[onreadystatechange] = function () { - if ((el[readyState] && !(/^c|loade/.test(el[readyState]))) || loaded) return; - el.onload = el[onreadystatechange] = null - loaded = 1 - scripts[path] = 2 - fn() - } - el.async = 1 - el.src = path - head.insertBefore(el, head.firstChild) - } + el.async = 1; + el.src = path; - $script.get = create + head.insertBefore(el, head.firstChild); +}; - $script.order = function (scripts, id, done) { - (function callback(s) { - s = scripts.shift() - if (!scripts.length) $script(s, id, done) - else $script(s, callback) - }()) - } +/** + * Loads scripts and fires a callback. + * + * @param {String} path (can be multiple parameters) + * @param {Function} callback + */ - $script.path = function(p) { - scriptpath = p - } - $script.ready = function(deps, ready, req) { - deps = deps[push] ? deps : [deps] - var missing = []; - !each(deps, function(dep) { - list[dep] || missing[push](dep); - }) && every(deps, function(dep) {return list[dep]}) ? - ready() : !function(key) { - delay[key] = delay[key] || [] - delay[key][push](ready) - req && req(missing) - }(deps.join('|')) - return $script - } +function load () { + var total = arguments.length - 1 + , fn = arguments[total] - $script.noConflict = function () { - win.$script = old; - return this + for (var i = 0, l = total; i < l; i++) { + create(arguments[i], function () { + --total || fn(); + }); } - - return $script -}); +}; From d1d691fdff9444c54baa895fcce8b74fbe186ce9 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 27 Nov 2011 11:27:07 -0800 Subject: [PATCH 0023/1403] Added jsonp transport name --- lib/transports/polling-jsonp.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/transports/polling-jsonp.js b/lib/transports/polling-jsonp.js index 4f14fabf..5c9f27bd 100644 --- a/lib/transports/polling-jsonp.js +++ b/lib/transports/polling-jsonp.js @@ -37,6 +37,14 @@ function JSONPPolling (opts) { util.inherits(JSONPPolling, Polling); +/** + * Transport name. + * + * @api public + */ + +JSONPPolling.prototype.name = 'polling-jsonp'; + /** * Sets JSONP global callback. * @@ -44,13 +52,12 @@ util.inherits(JSONPPolling, Polling); */ JSONPPolling.prototype.setIndex = function () { - var self = this; - // if we have an index already, set it to empy if (undefined != this.index) { io.j[this.index] = empty; } + var self = this; this.index = io.j.length; io.j.push(function (msg) { self.onData(msg); From 60e8782dac7a907ab41c65863d2a8b0e6cc0662f Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 27 Nov 2011 11:27:20 -0800 Subject: [PATCH 0024/1403] Added name to polling XHR, cleaned up. --- lib/transports/polling-xhr.js | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/lib/transports/polling-xhr.js b/lib/transports/polling-xhr.js index c24fcb40..b4aba10a 100644 --- a/lib/transports/polling-xhr.js +++ b/lib/transports/polling-xhr.js @@ -31,10 +31,11 @@ function empty () { } function XHRPolling (opts) { Transport.call(this, opts); - // if browser - this.xd = opts.host != global.location.hostname - || global.location.port != opts.port; - //end + + if (global.location) { + this.xd = opts.host != global.location.hostname + || global.location.port != opts.port; + } }; /** @@ -43,6 +44,14 @@ function XHRPolling (opts) { util.inherits(XHRPolling, Polling); +/** + * Transport name. + * + * @api public + */ + +XHRPolling.prototype.name = 'polling-xhr'; + /** * Opens the socket * @@ -66,6 +75,7 @@ XHRPolling.prototype.doClose = function () { if (this.pollXhr) { this.pollXhr.abort(); } + if (this.sendXhr) { this.sendXhr.abort(); } @@ -83,7 +93,7 @@ XHR.prototype.request = function (opts) { opts.xd = this.xd; var req = new Request(opts); req.on('error', function () { - self.close(); + self.error('connection error'); }); return req; }; From 889797db90454ed7f749d603322b0460830a3f73 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 27 Nov 2011 11:27:41 -0800 Subject: [PATCH 0025/1403] Style cleanup --- lib/transports/polling.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/transports/polling.js b/lib/transports/polling.js index 44eeded8..8f60b310 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -27,13 +27,13 @@ function Polling (opts) { var xd; if (global.location) { - xd = opts.host != global.location.hostname || global.location.port != opts.port; + xd = opts.host != global.location.hostname + || global.location.port != opts.port; } var xhr = request(xd); if (xhr && !opts.forceJSONP) { - // if we support xhr return new XHR; } else { return new JSONP; From bff32b2463f8f2971da5efe50ab85a69dadcea12 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 27 Nov 2011 11:27:51 -0800 Subject: [PATCH 0026/1403] Write a ping packet after opening polling. --- lib/transports/polling.js | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/lib/transports/polling.js b/lib/transports/polling.js index 8f60b310..2733e5c3 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -47,24 +47,15 @@ function Polling (opts) { util.inherits(Polling, Transport); /** - * Sets a callback for the next poll. - * - * @api private - */ - -Polling.prototype.nextPoll = function (fn) { - this.onNextPoll = fn; - return this; -}; - -/** - * Opens the socket (triggers polling). + * Opens the socket (triggers polling). We write a PING message to determine + * when the transport is open. * * @api private */ Polling.prototype.doOpen = function () { this.poll(); + this.write(parser.encodePacket('ping')); }; /** From 2bb5d0b00da5e0867e081f1ebbd17e3f81000469 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 27 Nov 2011 11:28:03 -0800 Subject: [PATCH 0027/1403] Do peaking of messages in polling transport: - if the transport readyState is `opening`, expect a PONG packet - if we get close, abort ongoing requests --- lib/transports/polling.js | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/lib/transports/polling.js b/lib/transports/polling.js index 2733e5c3..42844c83 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -99,18 +99,6 @@ Polling.prototype.poll = function () { } }; -/** - * Pauses polling. - * - * @param {Function} callback when buffers are flushed - * @api private - */ - -Polling.prototype.pause = function (fn) { - this.paused = true; - this.removeAllListeners(); -}; - /** * Overloads onData to detect payloads. * @@ -118,14 +106,30 @@ Polling.prototype.pause = function (fn) { */ Polling.prototype.onData = function (data) { - if ('open' != this.readyState) { - this.onOpen(); - } - var packets = parser.decodePayload(data); for (var i = 0, l = packets.length; i < l; i++) { - Transport.prototype.onData.call(this, packets[i]); + // for polling, we actually need to peak at the messages + var decoded = parser.decodePacket(packets[i]); + + // if its a PONG to our initial ping we consider the connection open + if ('opening' == this.readyState) { + if ('pong' == decoded.type) { + this.onOpen(); + continue; + } else { + this.error('protocol violation'); + return; + } + } + + // if its a close packet, we close the ongoing requests + if ('close' == decoded.type) { + this.doClose(); + } + + // otherwise bypass onData and handle the message + this.onMessage(decoded); } // if we got data we're not polling From 8dd5a6dbabbf74d403666fe453eb7be7c0e7fba3 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 27 Nov 2011 11:29:16 -0800 Subject: [PATCH 0028/1403] Also make sure close is triggered. --- lib/transports/polling.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/transports/polling.js b/lib/transports/polling.js index 42844c83..ebef12a5 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -126,6 +126,7 @@ Polling.prototype.onData = function (data) { // if its a close packet, we close the ongoing requests if ('close' == decoded.type) { this.doClose(); + this.onClose(); } // otherwise bypass onData and handle the message From 4c0e69e458b552d2d99e87227e938c5c6deef356 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 27 Nov 2011 11:29:43 -0800 Subject: [PATCH 0029/1403] To close polling transports, we must write a message. --- lib/transports/polling.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/transports/polling.js b/lib/transports/polling.js index ebef12a5..5d36ca18 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -138,8 +138,16 @@ Polling.prototype.onData = function (data) { // trigger next poll this.poll(); +}; - return ret; +/** + * For polling, send a close packet. + * + * @api private + */ + +Polling.prototype.doClose = function () { + this.write(parser.encodePacket('close')); }; /** From edc76f322aba8b09ec512c5cca09ae958c4270c6 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 27 Nov 2011 11:30:12 -0800 Subject: [PATCH 0030/1403] Added transport name to websocket --- lib/transports/websocket.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/transports/websocket.js b/lib/transports/websocket.js index 0b6e2873..d82f3bdf 100644 --- a/lib/transports/websocket.js +++ b/lib/transports/websocket.js @@ -30,6 +30,14 @@ function WS (opts) { util.inherits(WS, Transport); +/** + * Transport name. + * + * @api public + */ + +WS.prototype.name = 'websocket'; + /** * Opens socket. * From 7489fa4378196ee95305456dc42539b93afe08ff Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 27 Nov 2011 11:30:28 -0800 Subject: [PATCH 0031/1403] Added parseUri to util, cleaned up. --- lib/util.js | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/lib/util.js b/lib/util.js index d4ab665c..5d52483e 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1,4 +1,10 @@ +/** + * engine.io-client + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + /** * Inheritance. * @@ -55,8 +61,6 @@ util.ua.webkit = 'undefined' != typeof navigator && util.ua.gecko = 'undefined' != typeof navigator && /gecko/i.test(navigator.userAgent); -// end - /** * XHR request helper. * @@ -87,3 +91,29 @@ util.request = function request (xdomain) { } catch(e) { } } }; + +/** + * Parses an URI + * + * @author Steven Levithan (MIT license) + * @api public + */ + +var re = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/; + +var parts = [ + 'source', 'protocol', 'authority', 'userInfo', 'user', 'password', 'host' + , 'port', 'relative', 'path', 'directory', 'file', 'query', 'anchor' +]; + +exports.parseUri = function (str) { + var m = re.exec(str || '') + , uri = {} + , i = 14; + + while (i--) { + uri[parts[i]] = m[i] || ''; + } + + return uri; +}; From c7ecd93295460da3fa4ef23313d6a0c3cb7d61ea Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 27 Nov 2011 11:31:00 -0800 Subject: [PATCH 0032/1403] Added util tests. --- test/util.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 test/util.js diff --git a/test/util.js b/test/util.js new file mode 100644 index 00000000..968ee62c --- /dev/null +++ b/test/util.js @@ -0,0 +1,31 @@ + +/** + * Test dependencies. + */ + +var util = require('../lib/util') + +/** + * Tests. + */ + +describe('util', function () { + + describe('parse uri', function () { + var http = util.parseUri('http://google.com') + , https = util.parseUri('https://www.google.com:80') + , query = util.parseUri('google.com:8080/foo/bar?foo=bar'); + + http.protocol.should().eql('http'); + http.port.should().eql(''); + http.host.should().eql('google.com'); + https.protocol.should().eql('https'); + https.port.should().eql('80'); + https.host.should().eql('www.google.com'); + query.port.should().eql('8080'); + query.query.should().eql('foo=bar'); + query.path.should().eql('/foo/bar'); + query.relative.should().eql('/foo/bar?foo=bar'); + }); + +}); From 26d80e2fe8c25af5132af2f8b52807c4180a7752 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sat, 3 Dec 2011 14:02:05 -0800 Subject: [PATCH 0033/1403] Renamed global from `io` to `eio` to avoid confusion. Start renaming constructor `Engine` to `Socket` --- lib/engine-client.js | 8 ++++---- lib/{engine.js => socket.js} | 0 2 files changed, 4 insertions(+), 4 deletions(-) rename lib/{engine.js => socket.js} (100%) diff --git a/lib/engine-client.js b/lib/engine-client.js index 06d232d0..03778878 100644 --- a/lib/engine-client.js +++ b/lib/engine-client.js @@ -5,7 +5,7 @@ * @api public. */ -exports.engineVersion = '0.1.0'; +exports.version = '0.1.0'; /** * Protocol version. @@ -13,12 +13,12 @@ exports.engineVersion = '0.1.0'; * @api public. */ -exports.engineProtocol = 1; +exports.protocol = 1; /** - * Engine constructor. + * Socket constructor. * * @api public. */ -exports.Engine = require('./engine'); +exports.Socket = require('./socket'); diff --git a/lib/engine.js b/lib/socket.js similarity index 100% rename from lib/engine.js rename to lib/socket.js From 1fc5d132b66aa510b473dab9db054dc079a1e2f8 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sat, 3 Dec 2011 14:03:00 -0800 Subject: [PATCH 0034/1403] Change constructor name from Engine to Socket. --- lib/socket.js | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/socket.js b/lib/socket.js index 61305230..2bf215e0 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -3,7 +3,7 @@ * Module exports. */ -module.exports = exports = Engine; +module.exports = exports = Socket; /** * Export Transport. @@ -24,13 +24,13 @@ var transports = exports.transports = require('./transports'); var util = exports.util = require('./util') /** - * Engine constructor. + * Socket constructor. * * @param {Object} options * @api public */ -function Engine (opts) { +function Socket (opts) { if ('string' == typeof opts) { var uri = util.parseUri(opts); opts.host = uri.host; @@ -59,7 +59,7 @@ function Engine (opts) { * @api private */ -Engine.prototype.createTransport = function (name) { +Socket.prototype.createTransport = function (name) { var transport = new transports[name]({ host: self.host , port: self.port @@ -78,7 +78,7 @@ Engine.prototype.createTransport = function (name) { * @api private */ -Engine.prototype.open = function () { +Socket.prototype.open = function () { var self = this; this.readyState = 'opening'; @@ -111,7 +111,7 @@ Engine.prototype.open = function () { * @api private */ -Engine.prototype.setTransport = function (transport) { +Socket.prototype.setTransport = function (transport) { var self = this; // set up transport @@ -134,7 +134,7 @@ Engine.prototype.setTransport = function (transport) { * @api private */ -Engine.prototype.probe = function (name) { +Socket.prototype.probe = function (name) { this.log.debug('probing transport "%s"', name); var transport = this.createTransport(name) @@ -181,7 +181,7 @@ Engine.prototype.probe = function (name) { * @api public */ -Engine.prototype.open = function () { +Socket.prototype.open = function () { if ('' == this.readyState || 'closed' == this.readyState) { this.transport.open() } @@ -195,7 +195,7 @@ Engine.prototype.open = function () { * @api public */ -Engine.prototype.onOpen = function () { +Socket.prototype.onOpen = function () { this.readyState = 'open'; this.emit('open'); this.onopen && this.onopen.call(this); @@ -207,7 +207,7 @@ Engine.prototype.onOpen = function () { * @api private */ -Engine.prototype.onMessage = function (msg) { +Socket.prototype.onMessage = function (msg) { switch (msg.type) { case 'noop': break; @@ -239,7 +239,7 @@ Engine.prototype.onMessage = function (msg) { * @api private */ -Engine.prototype.flush = function () { +Socket.prototype.flush = function () { if (this.writeBuffer.length) { // make sure to transfer the buffer to the transport this.transport.buffer = true; @@ -257,11 +257,11 @@ Engine.prototype.flush = function () { * Sends a message. * * @param {String} message. - * @return {Engine} for chaining. + * @return {Socket} for chaining. * @api public */ -Engine.prototype.send = function (msg) { +Socket.prototype.send = function (msg) { this.writePacket('message', msg); return this; }; @@ -274,7 +274,7 @@ Engine.prototype.send = function (msg) { * @api private */ -Engine.prototype.writePacket = function (type, data) { +Socket.prototype.writePacket = function (type, data) { this.write(parser.encodePacket(type, data)); }; @@ -284,7 +284,7 @@ Engine.prototype.writePacket = function (type, data) { * @api private */ -Engine.prototype.write = function (data) { +Socket.prototype.write = function (data) { if ('open' != this.readyState || this.upgrading) { this.writeBuffer.push(data); } else { @@ -298,7 +298,7 @@ Engine.prototype.write = function (data) { * @api private */ -Engine.prototype.close = function () { +Socket.prototype.close = function () { if ('opening' == this.readyState || 'open' == this.readyState) { if (this.transport) { this.transport.close(); @@ -316,7 +316,7 @@ Engine.prototype.close = function () { * @api private */ -Engine.prototype.onClose = function () { +Socket.prototype.onClose = function () { this.readyState = 'closed'; this.emit('close'); this.onclose && this.onclose.call(this); From 0ac8ac72ac2987c64428044ffab5ab1694e8cc3c Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sat, 3 Dec 2011 14:06:53 -0800 Subject: [PATCH 0035/1403] Fixed jsdoc block. --- lib/transports/flashsocket.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/transports/flashsocket.js b/lib/transports/flashsocket.js index 3eb51fc0..591b5bff 100644 --- a/lib/transports/flashsocket.js +++ b/lib/transports/flashsocket.js @@ -21,7 +21,6 @@ function empty () { } /** * FlashWS constructor. * - * @param {Engine} engine instance. * @api public */ From 6c6630e3d3217bf8fc0dc3e15dfd7cc775ceff17 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sat, 3 Dec 2011 14:08:07 -0800 Subject: [PATCH 0036/1403] Fixed README. --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 498dae89..9743c3c4 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ communication layer for [Socket.IO](http://github.com/learnboost/socket.io). ```html + + + + + + + + + + + + + + +
+ + diff --git a/test/support/expect.js b/test/support/expect.js new file mode 120000 index 00000000..2520ed50 --- /dev/null +++ b/test/support/expect.js @@ -0,0 +1 @@ +../../node_modules/expect.js/expect.js \ No newline at end of file diff --git a/test/support/jquery.js b/test/support/jquery.js new file mode 100644 index 00000000..8ccd0ea7 --- /dev/null +++ b/test/support/jquery.js @@ -0,0 +1,9266 @@ +/*! + * jQuery JavaScript Library v1.7.1 + * http://jquery.com/ + * + * Copyright 2011, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * Copyright 2011, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * + * Date: Mon Nov 21 21:11:03 2011 -0500 + */ +(function( window, undefined ) { + +// Use the correct document accordingly with window argument (sandbox) +var document = window.document, + navigator = window.navigator, + location = window.location; +var jQuery = (function() { + +// Define a local copy of jQuery +var jQuery = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' + return new jQuery.fn.init( selector, context, rootjQuery ); + }, + + // Map over jQuery in case of overwrite + _jQuery = window.jQuery, + + // Map over the $ in case of overwrite + _$ = window.$, + + // A central reference to the root jQuery(document) + rootjQuery, + + // A simple way to check for HTML strings or ID strings + // Prioritize #id over to avoid XSS via location.hash (#9521) + quickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/, + + // Check if a string has a non-whitespace character in it + rnotwhite = /\S/, + + // Used for trimming whitespace + trimLeft = /^\s+/, + trimRight = /\s+$/, + + // Match a standalone tag + rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/, + + // JSON RegExp + rvalidchars = /^[\],:{}\s]*$/, + rvalidescape = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, + rvalidtokens = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, + rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, + + // Useragent RegExp + rwebkit = /(webkit)[ \/]([\w.]+)/, + ropera = /(opera)(?:.*version)?[ \/]([\w.]+)/, + rmsie = /(msie) ([\w.]+)/, + rmozilla = /(mozilla)(?:.*? rv:([\w.]+))?/, + + // Matches dashed string for camelizing + rdashAlpha = /-([a-z]|[0-9])/ig, + rmsPrefix = /^-ms-/, + + // Used by jQuery.camelCase as callback to replace() + fcamelCase = function( all, letter ) { + return ( letter + "" ).toUpperCase(); + }, + + // Keep a UserAgent string for use with jQuery.browser + userAgent = navigator.userAgent, + + // For matching the engine and version of the browser + browserMatch, + + // The deferred used on DOM ready + readyList, + + // The ready event handler + DOMContentLoaded, + + // Save a reference to some core methods + toString = Object.prototype.toString, + hasOwn = Object.prototype.hasOwnProperty, + push = Array.prototype.push, + slice = Array.prototype.slice, + trim = String.prototype.trim, + indexOf = Array.prototype.indexOf, + + // [[Class]] -> type pairs + class2type = {}; + +jQuery.fn = jQuery.prototype = { + constructor: jQuery, + init: function( selector, context, rootjQuery ) { + var match, elem, ret, doc; + + // Handle $(""), $(null), or $(undefined) + if ( !selector ) { + return this; + } + + // Handle $(DOMElement) + if ( selector.nodeType ) { + this.context = this[0] = selector; + this.length = 1; + return this; + } + + // The body element only exists once, optimize finding it + if ( selector === "body" && !context && document.body ) { + this.context = document; + this[0] = document.body; + this.selector = selector; + this.length = 1; + return this; + } + + // Handle HTML strings + if ( typeof selector === "string" ) { + // Are we dealing with HTML string or an ID? + if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = quickExpr.exec( selector ); + } + + // Verify a match, and that no context was specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) { + context = context instanceof jQuery ? context[0] : context; + doc = ( context ? context.ownerDocument || context : document ); + + // If a single string is passed in and it's a single tag + // just do a createElement and skip the rest + ret = rsingleTag.exec( selector ); + + if ( ret ) { + if ( jQuery.isPlainObject( context ) ) { + selector = [ document.createElement( ret[1] ) ]; + jQuery.fn.attr.call( selector, context, true ); + + } else { + selector = [ doc.createElement( ret[1] ) ]; + } + + } else { + ret = jQuery.buildFragment( [ match[1] ], [ doc ] ); + selector = ( ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment ).childNodes; + } + + return jQuery.merge( this, selector ); + + // HANDLE: $("#id") + } else { + elem = document.getElementById( match[2] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id !== match[2] ) { + return rootjQuery.find( selector ); + } + + // Otherwise, we inject the element directly into the jQuery object + this.length = 1; + this[0] = elem; + } + + this.context = document; + this.selector = selector; + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || rootjQuery ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) { + return rootjQuery.ready( selector ); + } + + if ( selector.selector !== undefined ) { + this.selector = selector.selector; + this.context = selector.context; + } + + return jQuery.makeArray( selector, this ); + }, + + // Start with an empty selector + selector: "", + + // The current version of jQuery being used + jquery: "1.7.1", + + // The default length of a jQuery object is 0 + length: 0, + + // The number of elements contained in the matched element set + size: function() { + return this.length; + }, + + toArray: function() { + return slice.call( this, 0 ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num == null ? + + // Return a 'clean' array + this.toArray() : + + // Return just the object + ( num < 0 ? this[ this.length + num ] : this[ num ] ); + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems, name, selector ) { + // Build a new jQuery matched element set + var ret = this.constructor(); + + if ( jQuery.isArray( elems ) ) { + push.apply( ret, elems ); + + } else { + jQuery.merge( ret, elems ); + } + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + ret.context = this.context; + + if ( name === "find" ) { + ret.selector = this.selector + ( this.selector ? " " : "" ) + selector; + } else if ( name ) { + ret.selector = this.selector + "." + name + "(" + selector + ")"; + } + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function( callback, args ) { + return jQuery.each( this, callback, args ); + }, + + ready: function( fn ) { + // Attach the listeners + jQuery.bindReady(); + + // Add the callback + readyList.add( fn ); + + return this; + }, + + eq: function( i ) { + i = +i; + return i === -1 ? + this.slice( i ) : + this.slice( i, i + 1 ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ), + "slice", slice.call(arguments).join(",") ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map(this, function( elem, i ) { + return callback.call( elem, i, elem ); + })); + }, + + end: function() { + return this.prevObject || this.constructor(null); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: [].sort, + splice: [].splice +}; + +// Give the init function the jQuery prototype for later instantiation +jQuery.fn.init.prototype = jQuery.fn; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[0] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction(target) ) { + target = {}; + } + + // extend jQuery itself if only one argument is passed + if ( length === i ) { + target = this; + --i; + } + + for ( ; i < length; i++ ) { + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) != null ) { + // Extend the base object + for ( name in options ) { + src = target[ name ]; + copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { + if ( copyIsArray ) { + copyIsArray = false; + clone = src && jQuery.isArray(src) ? src : []; + + } else { + clone = src && jQuery.isPlainObject(src) ? src : {}; + } + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend({ + noConflict: function( deep ) { + if ( window.$ === jQuery ) { + window.$ = _$; + } + + if ( deep && window.jQuery === jQuery ) { + window.jQuery = _jQuery; + } + + return jQuery; + }, + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Hold (or release) the ready event + holdReady: function( hold ) { + if ( hold ) { + jQuery.readyWait++; + } else { + jQuery.ready( true ); + } + }, + + // Handle when the DOM is ready + ready: function( wait ) { + // Either a released hold or an DOMready/load event and not yet ready + if ( (wait === true && !--jQuery.readyWait) || (wait !== true && !jQuery.isReady) ) { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( !document.body ) { + return setTimeout( jQuery.ready, 1 ); + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.fireWith( document, [ jQuery ] ); + + // Trigger any bound ready events + if ( jQuery.fn.trigger ) { + jQuery( document ).trigger( "ready" ).off( "ready" ); + } + } + }, + + bindReady: function() { + if ( readyList ) { + return; + } + + readyList = jQuery.Callbacks( "once memory" ); + + // Catch cases where $(document).ready() is called after the + // browser event has already occurred. + if ( document.readyState === "complete" ) { + // Handle it asynchronously to allow scripts the opportunity to delay ready + return setTimeout( jQuery.ready, 1 ); + } + + // Mozilla, Opera and webkit nightlies currently support this event + if ( document.addEventListener ) { + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", jQuery.ready, false ); + + // If IE event model is used + } else if ( document.attachEvent ) { + // ensure firing before onload, + // maybe late but safe also for iframes + document.attachEvent( "onreadystatechange", DOMContentLoaded ); + + // A fallback to window.onload, that will always work + window.attachEvent( "onload", jQuery.ready ); + + // If IE and not a frame + // continually check to see if the document is ready + var toplevel = false; + + try { + toplevel = window.frameElement == null; + } catch(e) {} + + if ( document.documentElement.doScroll && toplevel ) { + doScrollCheck(); + } + } + }, + + // See test/unit/core.js for details concerning isFunction. + // Since version 1.3, DOM methods and functions like alert + // aren't supported. They return false on IE (#2968). + isFunction: function( obj ) { + return jQuery.type(obj) === "function"; + }, + + isArray: Array.isArray || function( obj ) { + return jQuery.type(obj) === "array"; + }, + + // A crude way of determining if an object is a window + isWindow: function( obj ) { + return obj && typeof obj === "object" && "setInterval" in obj; + }, + + isNumeric: function( obj ) { + return !isNaN( parseFloat(obj) ) && isFinite( obj ); + }, + + type: function( obj ) { + return obj == null ? + String( obj ) : + class2type[ toString.call(obj) ] || "object"; + }, + + isPlainObject: function( obj ) { + // Must be an Object. + // Because of IE, we also have to check the presence of the constructor property. + // Make sure that DOM nodes and window objects don't pass through, as well + if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { + return false; + } + + try { + // Not own constructor property must be Object + if ( obj.constructor && + !hasOwn.call(obj, "constructor") && + !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { + return false; + } + } catch ( e ) { + // IE8,9 Will throw exceptions on certain host objects #9897 + return false; + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + + var key; + for ( key in obj ) {} + + return key === undefined || hasOwn.call( obj, key ); + }, + + isEmptyObject: function( obj ) { + for ( var name in obj ) { + return false; + } + return true; + }, + + error: function( msg ) { + throw new Error( msg ); + }, + + parseJSON: function( data ) { + if ( typeof data !== "string" || !data ) { + return null; + } + + // Make sure leading/trailing whitespace is removed (IE can't handle it) + data = jQuery.trim( data ); + + // Attempt to parse using the native JSON parser first + if ( window.JSON && window.JSON.parse ) { + return window.JSON.parse( data ); + } + + // Make sure the incoming data is actual JSON + // Logic borrowed from http://json.org/json2.js + if ( rvalidchars.test( data.replace( rvalidescape, "@" ) + .replace( rvalidtokens, "]" ) + .replace( rvalidbraces, "")) ) { + + return ( new Function( "return " + data ) )(); + + } + jQuery.error( "Invalid JSON: " + data ); + }, + + // Cross-browser xml parsing + parseXML: function( data ) { + var xml, tmp; + try { + if ( window.DOMParser ) { // Standard + tmp = new DOMParser(); + xml = tmp.parseFromString( data , "text/xml" ); + } else { // IE + xml = new ActiveXObject( "Microsoft.XMLDOM" ); + xml.async = "false"; + xml.loadXML( data ); + } + } catch( e ) { + xml = undefined; + } + if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) { + jQuery.error( "Invalid XML: " + data ); + } + return xml; + }, + + noop: function() {}, + + // Evaluates a script in a global context + // Workarounds based on findings by Jim Driscoll + // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context + globalEval: function( data ) { + if ( data && rnotwhite.test( data ) ) { + // We use execScript on Internet Explorer + // We use an anonymous function so that context is window + // rather than jQuery in Firefox + ( window.execScript || function( data ) { + window[ "eval" ].call( window, data ); + } )( data ); + } + }, + + // Convert dashed to camelCase; used by the css and data modules + // Microsoft forgot to hump their vendor prefix (#9572) + camelCase: function( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase(); + }, + + // args is for internal usage only + each: function( object, callback, args ) { + var name, i = 0, + length = object.length, + isObj = length === undefined || jQuery.isFunction( object ); + + if ( args ) { + if ( isObj ) { + for ( name in object ) { + if ( callback.apply( object[ name ], args ) === false ) { + break; + } + } + } else { + for ( ; i < length; ) { + if ( callback.apply( object[ i++ ], args ) === false ) { + break; + } + } + } + + // A special, fast, case for the most common use of each + } else { + if ( isObj ) { + for ( name in object ) { + if ( callback.call( object[ name ], name, object[ name ] ) === false ) { + break; + } + } + } else { + for ( ; i < length; ) { + if ( callback.call( object[ i ], i, object[ i++ ] ) === false ) { + break; + } + } + } + } + + return object; + }, + + // Use native String.trim function wherever possible + trim: trim ? + function( text ) { + return text == null ? + "" : + trim.call( text ); + } : + + // Otherwise use our own trimming functionality + function( text ) { + return text == null ? + "" : + text.toString().replace( trimLeft, "" ).replace( trimRight, "" ); + }, + + // results is for internal usage only + makeArray: function( array, results ) { + var ret = results || []; + + if ( array != null ) { + // The window, strings (and functions) also have 'length' + // Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930 + var type = jQuery.type( array ); + + if ( array.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( array ) ) { + push.call( ret, array ); + } else { + jQuery.merge( ret, array ); + } + } + + return ret; + }, + + inArray: function( elem, array, i ) { + var len; + + if ( array ) { + if ( indexOf ) { + return indexOf.call( array, elem, i ); + } + + len = array.length; + i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; + + for ( ; i < len; i++ ) { + // Skip accessing in sparse arrays + if ( i in array && array[ i ] === elem ) { + return i; + } + } + } + + return -1; + }, + + merge: function( first, second ) { + var i = first.length, + j = 0; + + if ( typeof second.length === "number" ) { + for ( var l = second.length; j < l; j++ ) { + first[ i++ ] = second[ j ]; + } + + } else { + while ( second[j] !== undefined ) { + first[ i++ ] = second[ j++ ]; + } + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, inv ) { + var ret = [], retVal; + inv = !!inv; + + // Go through the array, only saving the items + // that pass the validator function + for ( var i = 0, length = elems.length; i < length; i++ ) { + retVal = !!callback( elems[ i ], i ); + if ( inv !== retVal ) { + ret.push( elems[ i ] ); + } + } + + return ret; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var value, key, ret = [], + i = 0, + length = elems.length, + // jquery objects are treated as arrays + isArray = elems instanceof jQuery || length !== undefined && typeof length === "number" && ( ( length > 0 && elems[ 0 ] && elems[ length -1 ] ) || length === 0 || jQuery.isArray( elems ) ) ; + + // Go through the array, translating each of the items to their + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + + // Go through every key on the object, + } else { + for ( key in elems ) { + value = callback( elems[ key ], key, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + } + + // Flatten any nested arrays + return ret.concat.apply( [], ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // Bind a function to a context, optionally partially applying any + // arguments. + proxy: function( fn, context ) { + if ( typeof context === "string" ) { + var tmp = fn[ context ]; + context = fn; + fn = tmp; + } + + // Quick check to determine if target is callable, in the spec + // this throws a TypeError, but we will just return undefined. + if ( !jQuery.isFunction( fn ) ) { + return undefined; + } + + // Simulated bind + var args = slice.call( arguments, 2 ), + proxy = function() { + return fn.apply( context, args.concat( slice.call( arguments ) ) ); + }; + + // Set the guid of unique handler to the same of original handler, so it can be removed + proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++; + + return proxy; + }, + + // Mutifunctional method to get and set values to a collection + // The value/s can optionally be executed if it's a function + access: function( elems, key, value, exec, fn, pass ) { + var length = elems.length; + + // Setting many attributes + if ( typeof key === "object" ) { + for ( var k in key ) { + jQuery.access( elems, k, key[k], exec, fn, value ); + } + return elems; + } + + // Setting one attribute + if ( value !== undefined ) { + // Optionally, function values get executed if exec is true + exec = !pass && exec && jQuery.isFunction(value); + + for ( var i = 0; i < length; i++ ) { + fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass ); + } + + return elems; + } + + // Getting an attribute + return length ? fn( elems[0], key ) : undefined; + }, + + now: function() { + return ( new Date() ).getTime(); + }, + + // Use of jQuery.browser is frowned upon. + // More details: http://docs.jquery.com/Utilities/jQuery.browser + uaMatch: function( ua ) { + ua = ua.toLowerCase(); + + var match = rwebkit.exec( ua ) || + ropera.exec( ua ) || + rmsie.exec( ua ) || + ua.indexOf("compatible") < 0 && rmozilla.exec( ua ) || + []; + + return { browser: match[1] || "", version: match[2] || "0" }; + }, + + sub: function() { + function jQuerySub( selector, context ) { + return new jQuerySub.fn.init( selector, context ); + } + jQuery.extend( true, jQuerySub, this ); + jQuerySub.superclass = this; + jQuerySub.fn = jQuerySub.prototype = this(); + jQuerySub.fn.constructor = jQuerySub; + jQuerySub.sub = this.sub; + jQuerySub.fn.init = function init( selector, context ) { + if ( context && context instanceof jQuery && !(context instanceof jQuerySub) ) { + context = jQuerySub( context ); + } + + return jQuery.fn.init.call( this, selector, context, rootjQuerySub ); + }; + jQuerySub.fn.init.prototype = jQuerySub.fn; + var rootjQuerySub = jQuerySub(document); + return jQuerySub; + }, + + browser: {} +}); + +// Populate the class2type map +jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +}); + +browserMatch = jQuery.uaMatch( userAgent ); +if ( browserMatch.browser ) { + jQuery.browser[ browserMatch.browser ] = true; + jQuery.browser.version = browserMatch.version; +} + +// Deprecated, use jQuery.browser.webkit instead +if ( jQuery.browser.webkit ) { + jQuery.browser.safari = true; +} + +// IE doesn't match non-breaking spaces with \s +if ( rnotwhite.test( "\xA0" ) ) { + trimLeft = /^[\s\xA0]+/; + trimRight = /[\s\xA0]+$/; +} + +// All jQuery objects should point back to these +rootjQuery = jQuery(document); + +// Cleanup functions for the document ready method +if ( document.addEventListener ) { + DOMContentLoaded = function() { + document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + jQuery.ready(); + }; + +} else if ( document.attachEvent ) { + DOMContentLoaded = function() { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( document.readyState === "complete" ) { + document.detachEvent( "onreadystatechange", DOMContentLoaded ); + jQuery.ready(); + } + }; +} + +// The DOM ready check for Internet Explorer +function doScrollCheck() { + if ( jQuery.isReady ) { + return; + } + + try { + // If IE is used, use the trick by Diego Perini + // http://javascript.nwbox.com/IEContentLoaded/ + document.documentElement.doScroll("left"); + } catch(e) { + setTimeout( doScrollCheck, 1 ); + return; + } + + // and execute any waiting functions + jQuery.ready(); +} + +return jQuery; + +})(); + + +// String to Object flags format cache +var flagsCache = {}; + +// Convert String-formatted flags into Object-formatted ones and store in cache +function createFlags( flags ) { + var object = flagsCache[ flags ] = {}, + i, length; + flags = flags.split( /\s+/ ); + for ( i = 0, length = flags.length; i < length; i++ ) { + object[ flags[i] ] = true; + } + return object; +} + +/* + * Create a callback list using the following parameters: + * + * flags: an optional list of space-separated flags that will change how + * the callback list behaves + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible flags: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( flags ) { + + // Convert flags from String-formatted to Object-formatted + // (we check in cache first) + flags = flags ? ( flagsCache[ flags ] || createFlags( flags ) ) : {}; + + var // Actual callback list + list = [], + // Stack of fire calls for repeatable lists + stack = [], + // Last fire value (for non-forgettable lists) + memory, + // Flag to know if list is currently firing + firing, + // First callback to fire (used internally by add and fireWith) + firingStart, + // End of the loop when firing + firingLength, + // Index of currently firing callback (modified by remove if needed) + firingIndex, + // Add one or several callbacks to the list + add = function( args ) { + var i, + length, + elem, + type, + actual; + for ( i = 0, length = args.length; i < length; i++ ) { + elem = args[ i ]; + type = jQuery.type( elem ); + if ( type === "array" ) { + // Inspect recursively + add( elem ); + } else if ( type === "function" ) { + // Add if not in unique mode and callback is not in + if ( !flags.unique || !self.has( elem ) ) { + list.push( elem ); + } + } + } + }, + // Fire callbacks + fire = function( context, args ) { + args = args || []; + memory = !flags.memory || [ context, args ]; + firing = true; + firingIndex = firingStart || 0; + firingStart = 0; + firingLength = list.length; + for ( ; list && firingIndex < firingLength; firingIndex++ ) { + if ( list[ firingIndex ].apply( context, args ) === false && flags.stopOnFalse ) { + memory = true; // Mark as halted + break; + } + } + firing = false; + if ( list ) { + if ( !flags.once ) { + if ( stack && stack.length ) { + memory = stack.shift(); + self.fireWith( memory[ 0 ], memory[ 1 ] ); + } + } else if ( memory === true ) { + self.disable(); + } else { + list = []; + } + } + }, + // Actual Callbacks object + self = { + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + var length = list.length; + add( arguments ); + // Do we need to add the callbacks to the + // current firing batch? + if ( firing ) { + firingLength = list.length; + // With memory, if we're not firing then + // we should call right away, unless previous + // firing was halted (stopOnFalse) + } else if ( memory && memory !== true ) { + firingStart = length; + fire( memory[ 0 ], memory[ 1 ] ); + } + } + return this; + }, + // Remove a callback from the list + remove: function() { + if ( list ) { + var args = arguments, + argIndex = 0, + argLength = args.length; + for ( ; argIndex < argLength ; argIndex++ ) { + for ( var i = 0; i < list.length; i++ ) { + if ( args[ argIndex ] === list[ i ] ) { + // Handle firingIndex and firingLength + if ( firing ) { + if ( i <= firingLength ) { + firingLength--; + if ( i <= firingIndex ) { + firingIndex--; + } + } + } + // Remove the element + list.splice( i--, 1 ); + // If we have some unicity property then + // we only need to do this once + if ( flags.unique ) { + break; + } + } + } + } + } + return this; + }, + // Control if a given callback is in the list + has: function( fn ) { + if ( list ) { + var i = 0, + length = list.length; + for ( ; i < length; i++ ) { + if ( fn === list[ i ] ) { + return true; + } + } + } + return false; + }, + // Remove all callbacks from the list + empty: function() { + list = []; + return this; + }, + // Have the list do nothing anymore + disable: function() { + list = stack = memory = undefined; + return this; + }, + // Is it disabled? + disabled: function() { + return !list; + }, + // Lock the list in its current state + lock: function() { + stack = undefined; + if ( !memory || memory === true ) { + self.disable(); + } + return this; + }, + // Is it locked? + locked: function() { + return !stack; + }, + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + if ( stack ) { + if ( firing ) { + if ( !flags.once ) { + stack.push( [ context, args ] ); + } + } else if ( !( flags.once && memory ) ) { + fire( context, args ); + } + } + return this; + }, + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + // To know if the callbacks have already been called at least once + fired: function() { + return !!memory; + } + }; + + return self; +}; + + + + +var // Static reference to slice + sliceDeferred = [].slice; + +jQuery.extend({ + + Deferred: function( func ) { + var doneList = jQuery.Callbacks( "once memory" ), + failList = jQuery.Callbacks( "once memory" ), + progressList = jQuery.Callbacks( "memory" ), + state = "pending", + lists = { + resolve: doneList, + reject: failList, + notify: progressList + }, + promise = { + done: doneList.add, + fail: failList.add, + progress: progressList.add, + + state: function() { + return state; + }, + + // Deprecated + isResolved: doneList.fired, + isRejected: failList.fired, + + then: function( doneCallbacks, failCallbacks, progressCallbacks ) { + deferred.done( doneCallbacks ).fail( failCallbacks ).progress( progressCallbacks ); + return this; + }, + always: function() { + deferred.done.apply( deferred, arguments ).fail.apply( deferred, arguments ); + return this; + }, + pipe: function( fnDone, fnFail, fnProgress ) { + return jQuery.Deferred(function( newDefer ) { + jQuery.each( { + done: [ fnDone, "resolve" ], + fail: [ fnFail, "reject" ], + progress: [ fnProgress, "notify" ] + }, function( handler, data ) { + var fn = data[ 0 ], + action = data[ 1 ], + returned; + if ( jQuery.isFunction( fn ) ) { + deferred[ handler ](function() { + returned = fn.apply( this, arguments ); + if ( returned && jQuery.isFunction( returned.promise ) ) { + returned.promise().then( newDefer.resolve, newDefer.reject, newDefer.notify ); + } else { + newDefer[ action + "With" ]( this === deferred ? newDefer : this, [ returned ] ); + } + }); + } else { + deferred[ handler ]( newDefer[ action ] ); + } + }); + }).promise(); + }, + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + if ( obj == null ) { + obj = promise; + } else { + for ( var key in promise ) { + obj[ key ] = promise[ key ]; + } + } + return obj; + } + }, + deferred = promise.promise({}), + key; + + for ( key in lists ) { + deferred[ key ] = lists[ key ].fire; + deferred[ key + "With" ] = lists[ key ].fireWith; + } + + // Handle state + deferred.done( function() { + state = "resolved"; + }, failList.disable, progressList.lock ).fail( function() { + state = "rejected"; + }, doneList.disable, progressList.lock ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( firstParam ) { + var args = sliceDeferred.call( arguments, 0 ), + i = 0, + length = args.length, + pValues = new Array( length ), + count = length, + pCount = length, + deferred = length <= 1 && firstParam && jQuery.isFunction( firstParam.promise ) ? + firstParam : + jQuery.Deferred(), + promise = deferred.promise(); + function resolveFunc( i ) { + return function( value ) { + args[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value; + if ( !( --count ) ) { + deferred.resolveWith( deferred, args ); + } + }; + } + function progressFunc( i ) { + return function( value ) { + pValues[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value; + deferred.notifyWith( promise, pValues ); + }; + } + if ( length > 1 ) { + for ( ; i < length; i++ ) { + if ( args[ i ] && args[ i ].promise && jQuery.isFunction( args[ i ].promise ) ) { + args[ i ].promise().then( resolveFunc(i), deferred.reject, progressFunc(i) ); + } else { + --count; + } + } + if ( !count ) { + deferred.resolveWith( deferred, args ); + } + } else if ( deferred !== firstParam ) { + deferred.resolveWith( deferred, length ? [ firstParam ] : [] ); + } + return promise; + } +}); + + + + +jQuery.support = (function() { + + var support, + all, + a, + select, + opt, + input, + marginDiv, + fragment, + tds, + events, + eventName, + i, + isSupported, + div = document.createElement( "div" ), + documentElement = document.documentElement; + + // Preliminary tests + div.setAttribute("className", "t"); + div.innerHTML = "
a"; + + all = div.getElementsByTagName( "*" ); + a = div.getElementsByTagName( "a" )[ 0 ]; + + // Can't get basic test support + if ( !all || !all.length || !a ) { + return {}; + } + + // First batch of supports tests + select = document.createElement( "select" ); + opt = select.appendChild( document.createElement("option") ); + input = div.getElementsByTagName( "input" )[ 0 ]; + + support = { + // IE strips leading whitespace when .innerHTML is used + leadingWhitespace: ( div.firstChild.nodeType === 3 ), + + // Make sure that tbody elements aren't automatically inserted + // IE will insert them into empty tables + tbody: !div.getElementsByTagName("tbody").length, + + // Make sure that link elements get serialized correctly by innerHTML + // This requires a wrapper element in IE + htmlSerialize: !!div.getElementsByTagName("link").length, + + // Get the style information from getAttribute + // (IE uses .cssText instead) + style: /top/.test( a.getAttribute("style") ), + + // Make sure that URLs aren't manipulated + // (IE normalizes it by default) + hrefNormalized: ( a.getAttribute("href") === "/a" ), + + // Make sure that element opacity exists + // (IE uses filter instead) + // Use a regex to work around a WebKit issue. See #5145 + opacity: /^0.55/.test( a.style.opacity ), + + // Verify style float existence + // (IE uses styleFloat instead of cssFloat) + cssFloat: !!a.style.cssFloat, + + // Make sure that if no value is specified for a checkbox + // that it defaults to "on". + // (WebKit defaults to "" instead) + checkOn: ( input.value === "on" ), + + // Make sure that a selected-by-default option has a working selected property. + // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) + optSelected: opt.selected, + + // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) + getSetAttribute: div.className !== "t", + + // Tests for enctype support on a form(#6743) + enctype: !!document.createElement("form").enctype, + + // Makes sure cloning an html5 element does not cause problems + // Where outerHTML is undefined, this still works + html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav>", + + // Will be defined later + submitBubbles: true, + changeBubbles: true, + focusinBubbles: false, + deleteExpando: true, + noCloneEvent: true, + inlineBlockNeedsLayout: false, + shrinkWrapBlocks: false, + reliableMarginRight: true + }; + + // Make sure checked status is properly cloned + input.checked = true; + support.noCloneChecked = input.cloneNode( true ).checked; + + // Make sure that the options inside disabled selects aren't marked as disabled + // (WebKit marks them as disabled) + select.disabled = true; + support.optDisabled = !opt.disabled; + + // Test to see if it's possible to delete an expando from an element + // Fails in Internet Explorer + try { + delete div.test; + } catch( e ) { + support.deleteExpando = false; + } + + if ( !div.addEventListener && div.attachEvent && div.fireEvent ) { + div.attachEvent( "onclick", function() { + // Cloning a node shouldn't copy over any + // bound event handlers (IE does this) + support.noCloneEvent = false; + }); + div.cloneNode( true ).fireEvent( "onclick" ); + } + + // Check if a radio maintains its value + // after being appended to the DOM + input = document.createElement("input"); + input.value = "t"; + input.setAttribute("type", "radio"); + support.radioValue = input.value === "t"; + + input.setAttribute("checked", "checked"); + div.appendChild( input ); + fragment = document.createDocumentFragment(); + fragment.appendChild( div.lastChild ); + + // WebKit doesn't clone checked state correctly in fragments + support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Check if a disconnected checkbox will retain its checked + // value of true after appended to the DOM (IE6/7) + support.appendChecked = input.checked; + + fragment.removeChild( input ); + fragment.appendChild( div ); + + div.innerHTML = ""; + + // Check if div with explicit width and no margin-right incorrectly + // gets computed margin-right based on width of container. For more + // info see bug #3333 + // Fails in WebKit before Feb 2011 nightlies + // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right + if ( window.getComputedStyle ) { + marginDiv = document.createElement( "div" ); + marginDiv.style.width = "0"; + marginDiv.style.marginRight = "0"; + div.style.width = "2px"; + div.appendChild( marginDiv ); + support.reliableMarginRight = + ( parseInt( ( window.getComputedStyle( marginDiv, null ) || { marginRight: 0 } ).marginRight, 10 ) || 0 ) === 0; + } + + // Technique from Juriy Zaytsev + // http://perfectionkills.com/detecting-event-support-without-browser-sniffing/ + // We only care about the case where non-standard event systems + // are used, namely in IE. Short-circuiting here helps us to + // avoid an eval call (in setAttribute) which can cause CSP + // to go haywire. See: https://developer.mozilla.org/en/Security/CSP + if ( div.attachEvent ) { + for( i in { + submit: 1, + change: 1, + focusin: 1 + }) { + eventName = "on" + i; + isSupported = ( eventName in div ); + if ( !isSupported ) { + div.setAttribute( eventName, "return;" ); + isSupported = ( typeof div[ eventName ] === "function" ); + } + support[ i + "Bubbles" ] = isSupported; + } + } + + fragment.removeChild( div ); + + // Null elements to avoid leaks in IE + fragment = select = opt = marginDiv = div = input = null; + + // Run tests that need a body at doc ready + jQuery(function() { + var container, outer, inner, table, td, offsetSupport, + conMarginTop, ptlm, vb, style, html, + body = document.getElementsByTagName("body")[0]; + + if ( !body ) { + // Return for frameset docs that don't have a body + return; + } + + conMarginTop = 1; + ptlm = "position:absolute;top:0;left:0;width:1px;height:1px;margin:0;"; + vb = "visibility:hidden;border:0;"; + style = "style='" + ptlm + "border:5px solid #000;padding:0;'"; + html = "
" + + "" + + "
"; + + container = document.createElement("div"); + container.style.cssText = vb + "width:0;height:0;position:static;top:0;margin-top:" + conMarginTop + "px"; + body.insertBefore( container, body.firstChild ); + + // Construct the test element + div = document.createElement("div"); + container.appendChild( div ); + + // Check if table cells still have offsetWidth/Height when they are set + // to display:none and there are still other visible table cells in a + // table row; if so, offsetWidth/Height are not reliable for use when + // determining if an element has been hidden directly using + // display:none (it is still safe to use offsets if a parent element is + // hidden; don safety goggles and see bug #4512 for more information). + // (only IE 8 fails this test) + div.innerHTML = "
t
"; + tds = div.getElementsByTagName( "td" ); + isSupported = ( tds[ 0 ].offsetHeight === 0 ); + + tds[ 0 ].style.display = ""; + tds[ 1 ].style.display = "none"; + + // Check if empty table cells still have offsetWidth/Height + // (IE <= 8 fail this test) + support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); + + // Figure out if the W3C box model works as expected + div.innerHTML = ""; + div.style.width = div.style.paddingLeft = "1px"; + jQuery.boxModel = support.boxModel = div.offsetWidth === 2; + + if ( typeof div.style.zoom !== "undefined" ) { + // Check if natively block-level elements act like inline-block + // elements when setting their display to 'inline' and giving + // them layout + // (IE < 8 does this) + div.style.display = "inline"; + div.style.zoom = 1; + support.inlineBlockNeedsLayout = ( div.offsetWidth === 2 ); + + // Check if elements with layout shrink-wrap their children + // (IE 6 does this) + div.style.display = ""; + div.innerHTML = "
"; + support.shrinkWrapBlocks = ( div.offsetWidth !== 2 ); + } + + div.style.cssText = ptlm + vb; + div.innerHTML = html; + + outer = div.firstChild; + inner = outer.firstChild; + td = outer.nextSibling.firstChild.firstChild; + + offsetSupport = { + doesNotAddBorder: ( inner.offsetTop !== 5 ), + doesAddBorderForTableAndCells: ( td.offsetTop === 5 ) + }; + + inner.style.position = "fixed"; + inner.style.top = "20px"; + + // safari subtracts parent border width here which is 5px + offsetSupport.fixedPosition = ( inner.offsetTop === 20 || inner.offsetTop === 15 ); + inner.style.position = inner.style.top = ""; + + outer.style.overflow = "hidden"; + outer.style.position = "relative"; + + offsetSupport.subtractsBorderForOverflowNotVisible = ( inner.offsetTop === -5 ); + offsetSupport.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== conMarginTop ); + + body.removeChild( container ); + div = container = null; + + jQuery.extend( support, offsetSupport ); + }); + + return support; +})(); + + + + +var rbrace = /^(?:\{.*\}|\[.*\])$/, + rmultiDash = /([A-Z])/g; + +jQuery.extend({ + cache: {}, + + // Please use with caution + uuid: 0, + + // Unique for each copy of jQuery on the page + // Non-digits removed to match rinlinejQuery + expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ), + + // The following elements throw uncatchable exceptions if you + // attempt to add expando properties to them. + noData: { + "embed": true, + // Ban all objects except for Flash (which handle expandos) + "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000", + "applet": true + }, + + hasData: function( elem ) { + elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; + return !!elem && !isEmptyDataObject( elem ); + }, + + data: function( elem, name, data, pvt /* Internal Use Only */ ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var privateCache, thisCache, ret, + internalKey = jQuery.expando, + getByName = typeof name === "string", + + // We have to handle DOM nodes and JS objects differently because IE6-7 + // can't GC object references properly across the DOM-JS boundary + isNode = elem.nodeType, + + // Only DOM nodes need the global jQuery cache; JS object data is + // attached directly to the object so GC can occur automatically + cache = isNode ? jQuery.cache : elem, + + // Only defining an ID for JS objects if its cache already exists allows + // the code to shortcut on the same path as a DOM node with no cache + id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey, + isEvents = name === "events"; + + // Avoid doing any more work than we need to when trying to get data on an + // object that has no data at all + if ( (!id || !cache[id] || (!isEvents && !pvt && !cache[id].data)) && getByName && data === undefined ) { + return; + } + + if ( !id ) { + // Only DOM nodes need a new unique ID for each element since their data + // ends up in the global cache + if ( isNode ) { + elem[ internalKey ] = id = ++jQuery.uuid; + } else { + id = internalKey; + } + } + + if ( !cache[ id ] ) { + cache[ id ] = {}; + + // Avoids exposing jQuery metadata on plain JS objects when the object + // is serialized using JSON.stringify + if ( !isNode ) { + cache[ id ].toJSON = jQuery.noop; + } + } + + // An object can be passed to jQuery.data instead of a key/value pair; this gets + // shallow copied over onto the existing cache + if ( typeof name === "object" || typeof name === "function" ) { + if ( pvt ) { + cache[ id ] = jQuery.extend( cache[ id ], name ); + } else { + cache[ id ].data = jQuery.extend( cache[ id ].data, name ); + } + } + + privateCache = thisCache = cache[ id ]; + + // jQuery data() is stored in a separate object inside the object's internal data + // cache in order to avoid key collisions between internal data and user-defined + // data. + if ( !pvt ) { + if ( !thisCache.data ) { + thisCache.data = {}; + } + + thisCache = thisCache.data; + } + + if ( data !== undefined ) { + thisCache[ jQuery.camelCase( name ) ] = data; + } + + // Users should not attempt to inspect the internal events object using jQuery.data, + // it is undocumented and subject to change. But does anyone listen? No. + if ( isEvents && !thisCache[ name ] ) { + return privateCache.events; + } + + // Check for both converted-to-camel and non-converted data property names + // If a data property was specified + if ( getByName ) { + + // First Try to find as-is property data + ret = thisCache[ name ]; + + // Test for null|undefined property data + if ( ret == null ) { + + // Try to find the camelCased property + ret = thisCache[ jQuery.camelCase( name ) ]; + } + } else { + ret = thisCache; + } + + return ret; + }, + + removeData: function( elem, name, pvt /* Internal Use Only */ ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var thisCache, i, l, + + // Reference to internal data cache key + internalKey = jQuery.expando, + + isNode = elem.nodeType, + + // See jQuery.data for more information + cache = isNode ? jQuery.cache : elem, + + // See jQuery.data for more information + id = isNode ? elem[ internalKey ] : internalKey; + + // If there is already no cache entry for this object, there is no + // purpose in continuing + if ( !cache[ id ] ) { + return; + } + + if ( name ) { + + thisCache = pvt ? cache[ id ] : cache[ id ].data; + + if ( thisCache ) { + + // Support array or space separated string names for data keys + if ( !jQuery.isArray( name ) ) { + + // try the string as a key before any manipulation + if ( name in thisCache ) { + name = [ name ]; + } else { + + // split the camel cased version by spaces unless a key with the spaces exists + name = jQuery.camelCase( name ); + if ( name in thisCache ) { + name = [ name ]; + } else { + name = name.split( " " ); + } + } + } + + for ( i = 0, l = name.length; i < l; i++ ) { + delete thisCache[ name[i] ]; + } + + // If there is no data left in the cache, we want to continue + // and let the cache object itself get destroyed + if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) { + return; + } + } + } + + // See jQuery.data for more information + if ( !pvt ) { + delete cache[ id ].data; + + // Don't destroy the parent cache unless the internal data object + // had been the only thing left in it + if ( !isEmptyDataObject(cache[ id ]) ) { + return; + } + } + + // Browsers that fail expando deletion also refuse to delete expandos on + // the window, but it will allow it on all other JS objects; other browsers + // don't care + // Ensure that `cache` is not a window object #10080 + if ( jQuery.support.deleteExpando || !cache.setInterval ) { + delete cache[ id ]; + } else { + cache[ id ] = null; + } + + // We destroyed the cache and need to eliminate the expando on the node to avoid + // false lookups in the cache for entries that no longer exist + if ( isNode ) { + // IE does not allow us to delete expando properties from nodes, + // nor does it have a removeAttribute function on Document nodes; + // we must handle all of these cases + if ( jQuery.support.deleteExpando ) { + delete elem[ internalKey ]; + } else if ( elem.removeAttribute ) { + elem.removeAttribute( internalKey ); + } else { + elem[ internalKey ] = null; + } + } + }, + + // For internal use only. + _data: function( elem, name, data ) { + return jQuery.data( elem, name, data, true ); + }, + + // A method for determining if a DOM node can handle the data expando + acceptData: function( elem ) { + if ( elem.nodeName ) { + var match = jQuery.noData[ elem.nodeName.toLowerCase() ]; + + if ( match ) { + return !(match === true || elem.getAttribute("classid") !== match); + } + } + + return true; + } +}); + +jQuery.fn.extend({ + data: function( key, value ) { + var parts, attr, name, + data = null; + + if ( typeof key === "undefined" ) { + if ( this.length ) { + data = jQuery.data( this[0] ); + + if ( this[0].nodeType === 1 && !jQuery._data( this[0], "parsedAttrs" ) ) { + attr = this[0].attributes; + for ( var i = 0, l = attr.length; i < l; i++ ) { + name = attr[i].name; + + if ( name.indexOf( "data-" ) === 0 ) { + name = jQuery.camelCase( name.substring(5) ); + + dataAttr( this[0], name, data[ name ] ); + } + } + jQuery._data( this[0], "parsedAttrs", true ); + } + } + + return data; + + } else if ( typeof key === "object" ) { + return this.each(function() { + jQuery.data( this, key ); + }); + } + + parts = key.split("."); + parts[1] = parts[1] ? "." + parts[1] : ""; + + if ( value === undefined ) { + data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]); + + // Try to fetch any internally stored data first + if ( data === undefined && this.length ) { + data = jQuery.data( this[0], key ); + data = dataAttr( this[0], key, data ); + } + + return data === undefined && parts[1] ? + this.data( parts[0] ) : + data; + + } else { + return this.each(function() { + var self = jQuery( this ), + args = [ parts[0], value ]; + + self.triggerHandler( "setData" + parts[1] + "!", args ); + jQuery.data( this, key, value ); + self.triggerHandler( "changeData" + parts[1] + "!", args ); + }); + } + }, + + removeData: function( key ) { + return this.each(function() { + jQuery.removeData( this, key ); + }); + } +}); + +function dataAttr( elem, key, data ) { + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + + var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); + + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + jQuery.isNumeric( data ) ? parseFloat( data ) : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch( e ) {} + + // Make sure we set the data so it isn't changed later + jQuery.data( elem, key, data ); + + } else { + data = undefined; + } + } + + return data; +} + +// checks a cache object for emptiness +function isEmptyDataObject( obj ) { + for ( var name in obj ) { + + // if the public data object is empty, the private is still empty + if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { + continue; + } + if ( name !== "toJSON" ) { + return false; + } + } + + return true; +} + + + + +function handleQueueMarkDefer( elem, type, src ) { + var deferDataKey = type + "defer", + queueDataKey = type + "queue", + markDataKey = type + "mark", + defer = jQuery._data( elem, deferDataKey ); + if ( defer && + ( src === "queue" || !jQuery._data(elem, queueDataKey) ) && + ( src === "mark" || !jQuery._data(elem, markDataKey) ) ) { + // Give room for hard-coded callbacks to fire first + // and eventually mark/queue something else on the element + setTimeout( function() { + if ( !jQuery._data( elem, queueDataKey ) && + !jQuery._data( elem, markDataKey ) ) { + jQuery.removeData( elem, deferDataKey, true ); + defer.fire(); + } + }, 0 ); + } +} + +jQuery.extend({ + + _mark: function( elem, type ) { + if ( elem ) { + type = ( type || "fx" ) + "mark"; + jQuery._data( elem, type, (jQuery._data( elem, type ) || 0) + 1 ); + } + }, + + _unmark: function( force, elem, type ) { + if ( force !== true ) { + type = elem; + elem = force; + force = false; + } + if ( elem ) { + type = type || "fx"; + var key = type + "mark", + count = force ? 0 : ( (jQuery._data( elem, key ) || 1) - 1 ); + if ( count ) { + jQuery._data( elem, key, count ); + } else { + jQuery.removeData( elem, key, true ); + handleQueueMarkDefer( elem, type, "mark" ); + } + } + }, + + queue: function( elem, type, data ) { + var q; + if ( elem ) { + type = ( type || "fx" ) + "queue"; + q = jQuery._data( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !q || jQuery.isArray(data) ) { + q = jQuery._data( elem, type, jQuery.makeArray(data) ); + } else { + q.push( data ); + } + } + return q || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + fn = queue.shift(), + hooks = {}; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + } + + if ( fn ) { + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + jQuery._data( elem, type + ".run", hooks ); + fn.call( elem, function() { + jQuery.dequeue( elem, type ); + }, hooks ); + } + + if ( !queue.length ) { + jQuery.removeData( elem, type + "queue " + type + ".run", true ); + handleQueueMarkDefer( elem, type, "queue" ); + } + } +}); + +jQuery.fn.extend({ + queue: function( type, data ) { + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + } + + if ( data === undefined ) { + return jQuery.queue( this[0], type ); + } + return this.each(function() { + var queue = jQuery.queue( this, type, data ); + + if ( type === "fx" && queue[0] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + }); + }, + dequeue: function( type ) { + return this.each(function() { + jQuery.dequeue( this, type ); + }); + }, + // Based off of the plugin by Clint Helfers, with permission. + // http://blindsignals.com/index.php/2009/07/jquery-delay/ + delay: function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; + type = type || "fx"; + + return this.queue( type, function( next, hooks ) { + var timeout = setTimeout( next, time ); + hooks.stop = function() { + clearTimeout( timeout ); + }; + }); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, object ) { + if ( typeof type !== "string" ) { + object = type; + type = undefined; + } + type = type || "fx"; + var defer = jQuery.Deferred(), + elements = this, + i = elements.length, + count = 1, + deferDataKey = type + "defer", + queueDataKey = type + "queue", + markDataKey = type + "mark", + tmp; + function resolve() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + } + while( i-- ) { + if (( tmp = jQuery.data( elements[ i ], deferDataKey, undefined, true ) || + ( jQuery.data( elements[ i ], queueDataKey, undefined, true ) || + jQuery.data( elements[ i ], markDataKey, undefined, true ) ) && + jQuery.data( elements[ i ], deferDataKey, jQuery.Callbacks( "once memory" ), true ) )) { + count++; + tmp.add( resolve ); + } + } + resolve(); + return defer.promise(); + } +}); + + + + +var rclass = /[\n\t\r]/g, + rspace = /\s+/, + rreturn = /\r/g, + rtype = /^(?:button|input)$/i, + rfocusable = /^(?:button|input|object|select|textarea)$/i, + rclickable = /^a(?:rea)?$/i, + rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i, + getSetAttribute = jQuery.support.getSetAttribute, + nodeHook, boolHook, fixSpecified; + +jQuery.fn.extend({ + attr: function( name, value ) { + return jQuery.access( this, name, value, true, jQuery.attr ); + }, + + removeAttr: function( name ) { + return this.each(function() { + jQuery.removeAttr( this, name ); + }); + }, + + prop: function( name, value ) { + return jQuery.access( this, name, value, true, jQuery.prop ); + }, + + removeProp: function( name ) { + name = jQuery.propFix[ name ] || name; + return this.each(function() { + // try/catch handles cases where IE balks (such as removing a property on window) + try { + this[ name ] = undefined; + delete this[ name ]; + } catch( e ) {} + }); + }, + + addClass: function( value ) { + var classNames, i, l, elem, + setClass, c, cl; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).addClass( value.call(this, j, this.className) ); + }); + } + + if ( value && typeof value === "string" ) { + classNames = value.split( rspace ); + + for ( i = 0, l = this.length; i < l; i++ ) { + elem = this[ i ]; + + if ( elem.nodeType === 1 ) { + if ( !elem.className && classNames.length === 1 ) { + elem.className = value; + + } else { + setClass = " " + elem.className + " "; + + for ( c = 0, cl = classNames.length; c < cl; c++ ) { + if ( !~setClass.indexOf( " " + classNames[ c ] + " " ) ) { + setClass += classNames[ c ] + " "; + } + } + elem.className = jQuery.trim( setClass ); + } + } + } + } + + return this; + }, + + removeClass: function( value ) { + var classNames, i, l, elem, className, c, cl; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).removeClass( value.call(this, j, this.className) ); + }); + } + + if ( (value && typeof value === "string") || value === undefined ) { + classNames = ( value || "" ).split( rspace ); + + for ( i = 0, l = this.length; i < l; i++ ) { + elem = this[ i ]; + + if ( elem.nodeType === 1 && elem.className ) { + if ( value ) { + className = (" " + elem.className + " ").replace( rclass, " " ); + for ( c = 0, cl = classNames.length; c < cl; c++ ) { + className = className.replace(" " + classNames[ c ] + " ", " "); + } + elem.className = jQuery.trim( className ); + + } else { + elem.className = ""; + } + } + } + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var type = typeof value, + isBool = typeof stateVal === "boolean"; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( i ) { + jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); + }); + } + + return this.each(function() { + if ( type === "string" ) { + // toggle individual class names + var className, + i = 0, + self = jQuery( this ), + state = stateVal, + classNames = value.split( rspace ); + + while ( (className = classNames[ i++ ]) ) { + // check each className given, space seperated list + state = isBool ? state : !self.hasClass( className ); + self[ state ? "addClass" : "removeClass" ]( className ); + } + + } else if ( type === "undefined" || type === "boolean" ) { + if ( this.className ) { + // store className if set + jQuery._data( this, "__className__", this.className ); + } + + // toggle whole className + this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; + } + }); + }, + + hasClass: function( selector ) { + var className = " " + selector + " ", + i = 0, + l = this.length; + for ( ; i < l; i++ ) { + if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) { + return true; + } + } + + return false; + }, + + val: function( value ) { + var hooks, ret, isFunction, + elem = this[0]; + + if ( !arguments.length ) { + if ( elem ) { + hooks = jQuery.valHooks[ elem.nodeName.toLowerCase() ] || jQuery.valHooks[ elem.type ]; + + if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { + return ret; + } + + ret = elem.value; + + return typeof ret === "string" ? + // handle most common string cases + ret.replace(rreturn, "") : + // handle cases where value is null/undef or number + ret == null ? "" : ret; + } + + return; + } + + isFunction = jQuery.isFunction( value ); + + return this.each(function( i ) { + var self = jQuery(this), val; + + if ( this.nodeType !== 1 ) { + return; + } + + if ( isFunction ) { + val = value.call( this, i, self.val() ); + } else { + val = value; + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + } else if ( typeof val === "number" ) { + val += ""; + } else if ( jQuery.isArray( val ) ) { + val = jQuery.map(val, function ( value ) { + return value == null ? "" : value + ""; + }); + } + + hooks = jQuery.valHooks[ this.nodeName.toLowerCase() ] || jQuery.valHooks[ this.type ]; + + // If set returns undefined, fall back to normal setting + if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { + this.value = val; + } + }); + } +}); + +jQuery.extend({ + valHooks: { + option: { + get: function( elem ) { + // attributes.value is undefined in Blackberry 4.7 but + // uses .value. See #6932 + var val = elem.attributes.value; + return !val || val.specified ? elem.value : elem.text; + } + }, + select: { + get: function( elem ) { + var value, i, max, option, + index = elem.selectedIndex, + values = [], + options = elem.options, + one = elem.type === "select-one"; + + // Nothing was selected + if ( index < 0 ) { + return null; + } + + // Loop through all the selected options + i = one ? index : 0; + max = one ? index + 1 : options.length; + for ( ; i < max; i++ ) { + option = options[ i ]; + + // Don't return options that are disabled or in a disabled optgroup + if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) && + (!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) { + + // Get the specific value for the option + value = jQuery( option ).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + // Fixes Bug #2551 -- select.val() broken in IE after form.reset() + if ( one && !values.length && options.length ) { + return jQuery( options[ index ] ).val(); + } + + return values; + }, + + set: function( elem, value ) { + var values = jQuery.makeArray( value ); + + jQuery(elem).find("option").each(function() { + this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; + }); + + if ( !values.length ) { + elem.selectedIndex = -1; + } + return values; + } + } + }, + + attrFn: { + val: true, + css: true, + html: true, + text: true, + data: true, + width: true, + height: true, + offset: true + }, + + attr: function( elem, name, value, pass ) { + var ret, hooks, notxml, + nType = elem.nodeType; + + // don't get/set attributes on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + if ( pass && name in jQuery.attrFn ) { + return jQuery( elem )[ name ]( value ); + } + + // Fallback to prop when attributes are not supported + if ( typeof elem.getAttribute === "undefined" ) { + return jQuery.prop( elem, name, value ); + } + + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + + // All attributes are lowercase + // Grab necessary hook if one is defined + if ( notxml ) { + name = name.toLowerCase(); + hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook ); + } + + if ( value !== undefined ) { + + if ( value === null ) { + jQuery.removeAttr( elem, name ); + return; + + } else if ( hooks && "set" in hooks && notxml && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + elem.setAttribute( name, "" + value ); + return value; + } + + } else if ( hooks && "get" in hooks && notxml && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + + ret = elem.getAttribute( name ); + + // Non-existent attributes return null, we normalize to undefined + return ret === null ? + undefined : + ret; + } + }, + + removeAttr: function( elem, value ) { + var propName, attrNames, name, l, + i = 0; + + if ( value && elem.nodeType === 1 ) { + attrNames = value.toLowerCase().split( rspace ); + l = attrNames.length; + + for ( ; i < l; i++ ) { + name = attrNames[ i ]; + + if ( name ) { + propName = jQuery.propFix[ name ] || name; + + // See #9699 for explanation of this approach (setting first, then removal) + jQuery.attr( elem, name, "" ); + elem.removeAttribute( getSetAttribute ? name : propName ); + + // Set corresponding property to false for boolean attributes + if ( rboolean.test( name ) && propName in elem ) { + elem[ propName ] = false; + } + } + } + } + }, + + attrHooks: { + type: { + set: function( elem, value ) { + // We can't allow the type property to be changed (since it causes problems in IE) + if ( rtype.test( elem.nodeName ) && elem.parentNode ) { + jQuery.error( "type property can't be changed" ); + } else if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { + // Setting the type on a radio button after the value resets the value in IE6-9 + // Reset value to it's default in case type is set after value + // This is for element creation + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + }, + // Use the value property for back compat + // Use the nodeHook for button elements in IE6/7 (#1954) + value: { + get: function( elem, name ) { + if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { + return nodeHook.get( elem, name ); + } + return name in elem ? + elem.value : + null; + }, + set: function( elem, value, name ) { + if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { + return nodeHook.set( elem, value, name ); + } + // Does not return so that setAttribute is also used + elem.value = value; + } + } + }, + + propFix: { + tabindex: "tabIndex", + readonly: "readOnly", + "for": "htmlFor", + "class": "className", + maxlength: "maxLength", + cellspacing: "cellSpacing", + cellpadding: "cellPadding", + rowspan: "rowSpan", + colspan: "colSpan", + usemap: "useMap", + frameborder: "frameBorder", + contenteditable: "contentEditable" + }, + + prop: function( elem, name, value ) { + var ret, hooks, notxml, + nType = elem.nodeType; + + // don't get/set properties on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + + if ( notxml ) { + // Fix name and attach hooks + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + return ( elem[ name ] = value ); + } + + } else { + if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + return elem[ name ]; + } + } + }, + + propHooks: { + tabIndex: { + get: function( elem ) { + // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set + // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + var attributeNode = elem.getAttributeNode("tabindex"); + + return attributeNode && attributeNode.specified ? + parseInt( attributeNode.value, 10 ) : + rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? + 0 : + undefined; + } + } + } +}); + +// Add the tabIndex propHook to attrHooks for back-compat (different case is intentional) +jQuery.attrHooks.tabindex = jQuery.propHooks.tabIndex; + +// Hook for boolean attributes +boolHook = { + get: function( elem, name ) { + // Align boolean attributes with corresponding properties + // Fall back to attribute presence where some booleans are not supported + var attrNode, + property = jQuery.prop( elem, name ); + return property === true || typeof property !== "boolean" && ( attrNode = elem.getAttributeNode(name) ) && attrNode.nodeValue !== false ? + name.toLowerCase() : + undefined; + }, + set: function( elem, value, name ) { + var propName; + if ( value === false ) { + // Remove boolean attributes when set to false + jQuery.removeAttr( elem, name ); + } else { + // value is true since we know at this point it's type boolean and not false + // Set boolean attributes to the same name and set the DOM property + propName = jQuery.propFix[ name ] || name; + if ( propName in elem ) { + // Only set the IDL specifically if it already exists on the element + elem[ propName ] = true; + } + + elem.setAttribute( name, name.toLowerCase() ); + } + return name; + } +}; + +// IE6/7 do not support getting/setting some attributes with get/setAttribute +if ( !getSetAttribute ) { + + fixSpecified = { + name: true, + id: true + }; + + // Use this for any attribute in IE6/7 + // This fixes almost every IE6/7 issue + nodeHook = jQuery.valHooks.button = { + get: function( elem, name ) { + var ret; + ret = elem.getAttributeNode( name ); + return ret && ( fixSpecified[ name ] ? ret.nodeValue !== "" : ret.specified ) ? + ret.nodeValue : + undefined; + }, + set: function( elem, value, name ) { + // Set the existing or create a new attribute node + var ret = elem.getAttributeNode( name ); + if ( !ret ) { + ret = document.createAttribute( name ); + elem.setAttributeNode( ret ); + } + return ( ret.nodeValue = value + "" ); + } + }; + + // Apply the nodeHook to tabindex + jQuery.attrHooks.tabindex.set = nodeHook.set; + + // Set width and height to auto instead of 0 on empty string( Bug #8150 ) + // This is for removals + jQuery.each([ "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + set: function( elem, value ) { + if ( value === "" ) { + elem.setAttribute( name, "auto" ); + return value; + } + } + }); + }); + + // Set contenteditable to false on removals(#10429) + // Setting to empty string throws an error as an invalid value + jQuery.attrHooks.contenteditable = { + get: nodeHook.get, + set: function( elem, value, name ) { + if ( value === "" ) { + value = "false"; + } + nodeHook.set( elem, value, name ); + } + }; +} + + +// Some attributes require a special call on IE +if ( !jQuery.support.hrefNormalized ) { + jQuery.each([ "href", "src", "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + get: function( elem ) { + var ret = elem.getAttribute( name, 2 ); + return ret === null ? undefined : ret; + } + }); + }); +} + +if ( !jQuery.support.style ) { + jQuery.attrHooks.style = { + get: function( elem ) { + // Return undefined in the case of empty string + // Normalize to lowercase since IE uppercases css property names + return elem.style.cssText.toLowerCase() || undefined; + }, + set: function( elem, value ) { + return ( elem.style.cssText = "" + value ); + } + }; +} + +// Safari mis-reports the default selected property of an option +// Accessing the parent's selectedIndex property fixes it +if ( !jQuery.support.optSelected ) { + jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, { + get: function( elem ) { + var parent = elem.parentNode; + + if ( parent ) { + parent.selectedIndex; + + // Make sure that it also works with optgroups, see #5701 + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + return null; + } + }); +} + +// IE6/7 call enctype encoding +if ( !jQuery.support.enctype ) { + jQuery.propFix.enctype = "encoding"; +} + +// Radios and checkboxes getter/setter +if ( !jQuery.support.checkOn ) { + jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + get: function( elem ) { + // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified + return elem.getAttribute("value") === null ? "on" : elem.value; + } + }; + }); +} +jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], { + set: function( elem, value ) { + if ( jQuery.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); + } + } + }); +}); + + + + +var rformElems = /^(?:textarea|input|select)$/i, + rtypenamespace = /^([^\.]*)?(?:\.(.+))?$/, + rhoverHack = /\bhover(\.\S+)?\b/, + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|contextmenu)|click/, + rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + rquickIs = /^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/, + quickParse = function( selector ) { + var quick = rquickIs.exec( selector ); + if ( quick ) { + // 0 1 2 3 + // [ _, tag, id, class ] + quick[1] = ( quick[1] || "" ).toLowerCase(); + quick[3] = quick[3] && new RegExp( "(?:^|\\s)" + quick[3] + "(?:\\s|$)" ); + } + return quick; + }, + quickIs = function( elem, m ) { + var attrs = elem.attributes || {}; + return ( + (!m[1] || elem.nodeName.toLowerCase() === m[1]) && + (!m[2] || (attrs.id || {}).value === m[2]) && + (!m[3] || m[3].test( (attrs[ "class" ] || {}).value )) + ); + }, + hoverHack = function( events ) { + return jQuery.event.special.hover ? events : events.replace( rhoverHack, "mouseenter$1 mouseleave$1" ); + }; + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + add: function( elem, types, handler, data, selector ) { + + var elemData, eventHandle, events, + t, tns, type, namespaces, handleObj, + handleObjIn, quick, handlers, special; + + // Don't attach events to noData or text/comment nodes (allow plain objects tho) + if ( elem.nodeType === 3 || elem.nodeType === 8 || !types || !handler || !(elemData = jQuery._data( elem )) ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + events = elemData.events; + if ( !events ) { + elemData.events = events = {}; + } + eventHandle = elemData.handle; + if ( !eventHandle ) { + elemData.handle = eventHandle = function( e ) { + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ? + jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : + undefined; + }; + // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events + eventHandle.elem = elem; + } + + // Handle multiple events separated by a space + // jQuery(...).bind("mouseover mouseout", fn); + types = jQuery.trim( hoverHack(types) ).split( " " ); + for ( t = 0; t < types.length; t++ ) { + + tns = rtypenamespace.exec( types[t] ) || []; + type = tns[1]; + namespaces = ( tns[2] || "" ).split( "." ).sort(); + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend({ + type: type, + origType: tns[1], + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + quick: quickParse( selector ), + namespace: namespaces.join(".") + }, handleObjIn ); + + // Init the event handler queue if we're the first + handlers = events[ type ]; + if ( !handlers ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener/attachEvent if the special events handler returns false + if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + // Bind the global event handler to the element + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle, false ); + + } else if ( elem.attachEvent ) { + elem.attachEvent( "on" + type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + // Nullify elem to prevent memory leaks in IE + elem = null; + }, + + global: {}, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + + var elemData = jQuery.hasData( elem ) && jQuery._data( elem ), + t, tns, type, origType, namespaces, origCount, + j, events, special, handle, eventType, handleObj; + + if ( !elemData || !(events = elemData.events) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = jQuery.trim( hoverHack( types || "" ) ).split(" "); + for ( t = 0; t < types.length; t++ ) { + tns = rtypenamespace.exec( types[t] ) || []; + type = origType = tns[1]; + namespaces = tns[2]; + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector? special.delegateType : special.bindType ) || type; + eventType = events[ type ] || []; + origCount = eventType.length; + namespaces = namespaces ? new RegExp("(^|\\.)" + namespaces.split(".").sort().join("\\.(?:.*\\.)?") + "(\\.|$)") : null; + + // Remove matching events + for ( j = 0; j < eventType.length; j++ ) { + handleObj = eventType[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !namespaces || namespaces.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { + eventType.splice( j--, 1 ); + + if ( handleObj.selector ) { + eventType.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( eventType.length === 0 && origCount !== eventType.length ) { + if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) { + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + handle = elemData.handle; + if ( handle ) { + handle.elem = null; + } + + // removeData also checks for emptiness and clears the expando if empty + // so use it instead of delete + jQuery.removeData( elem, [ "events", "handle" ], true ); + } + }, + + // Events that are safe to short-circuit if no handlers are attached. + // Native DOM events should not be added, they may have inline handlers. + customEvent: { + "getData": true, + "setData": true, + "changeData": true + }, + + trigger: function( event, data, elem, onlyHandlers ) { + // Don't do events on text and comment nodes + if ( elem && (elem.nodeType === 3 || elem.nodeType === 8) ) { + return; + } + + // Event object or event type + var type = event.type || event, + namespaces = [], + cache, exclusive, i, cur, old, ontype, special, handle, eventPath, bubbleType; + + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } + + if ( type.indexOf( "!" ) >= 0 ) { + // Exclusive events trigger only for the exact event (no namespaces) + type = type.slice(0, -1); + exclusive = true; + } + + if ( type.indexOf( "." ) >= 0 ) { + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split("."); + type = namespaces.shift(); + namespaces.sort(); + } + + if ( (!elem || jQuery.event.customEvent[ type ]) && !jQuery.event.global[ type ] ) { + // No jQuery handlers for this event type, and it can't have inline handlers + return; + } + + // Caller can pass in an Event, Object, or just an event type string + event = typeof event === "object" ? + // jQuery.Event object + event[ jQuery.expando ] ? event : + // Object literal + new jQuery.Event( type, event ) : + // Just the event type (string) + new jQuery.Event( type ); + + event.type = type; + event.isTrigger = true; + event.exclusive = exclusive; + event.namespace = namespaces.join( "." ); + event.namespace_re = event.namespace? new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.)?") + "(\\.|$)") : null; + ontype = type.indexOf( ":" ) < 0 ? "on" + type : ""; + + // Handle a global trigger + if ( !elem ) { + + // TODO: Stop taunting the data cache; remove global events and always attach to document + cache = jQuery.cache; + for ( i in cache ) { + if ( cache[ i ].events && cache[ i ].events[ type ] ) { + jQuery.event.trigger( event, data, cache[ i ].handle.elem, true ); + } + } + return; + } + + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data != null ? jQuery.makeArray( data ) : []; + data.unshift( event ); + + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + eventPath = [[ elem, special.bindType || type ]]; + if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + cur = rfocusMorph.test( bubbleType + type ) ? elem : elem.parentNode; + old = null; + for ( ; cur; cur = cur.parentNode ) { + eventPath.push([ cur, bubbleType ]); + old = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( old && old === elem.ownerDocument ) { + eventPath.push([ old.defaultView || old.parentWindow || window, bubbleType ]); + } + } + + // Fire handlers on the event path + for ( i = 0; i < eventPath.length && !event.isPropagationStopped(); i++ ) { + + cur = eventPath[i][0]; + event.type = eventPath[i][1]; + + handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + // Note that this is a bare JS function and not a jQuery handler + handle = ontype && cur[ ontype ]; + if ( handle && jQuery.acceptData( cur ) && handle.apply( cur, data ) === false ) { + event.preventDefault(); + } + } + event.type = type; + + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { + + if ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) && + !(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) { + + // Call a native DOM method on the target with the same name name as the event. + // Can't use an .isFunction() check here because IE6/7 fails that test. + // Don't do default actions on window, that's where global variables be (#6170) + // IE<9 dies on focus/blur to hidden element (#1486) + if ( ontype && elem[ type ] && ((type !== "focus" && type !== "blur") || event.target.offsetWidth !== 0) && !jQuery.isWindow( elem ) ) { + + // Don't re-trigger an onFOO event when we call its FOO() method + old = elem[ ontype ]; + + if ( old ) { + elem[ ontype ] = null; + } + + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + elem[ type ](); + jQuery.event.triggered = undefined; + + if ( old ) { + elem[ ontype ] = old; + } + } + } + } + + return event.result; + }, + + dispatch: function( event ) { + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( event || window.event ); + + var handlers = ( (jQuery._data( this, "events" ) || {} )[ event.type ] || []), + delegateCount = handlers.delegateCount, + args = [].slice.call( arguments, 0 ), + run_all = !event.exclusive && !event.namespace, + handlerQueue = [], + i, j, cur, jqcur, ret, selMatch, matched, matches, handleObj, sel, related; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[0] = event; + event.delegateTarget = this; + + // Determine handlers that should run if there are delegated events + // Avoid disabled elements in IE (#6911) and non-left-click bubbling in Firefox (#3861) + if ( delegateCount && !event.target.disabled && !(event.button && event.type === "click") ) { + + // Pregenerate a single jQuery object for reuse with .is() + jqcur = jQuery(this); + jqcur.context = this.ownerDocument || this; + + for ( cur = event.target; cur != this; cur = cur.parentNode || this ) { + selMatch = {}; + matches = []; + jqcur[0] = cur; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + sel = handleObj.selector; + + if ( selMatch[ sel ] === undefined ) { + selMatch[ sel ] = ( + handleObj.quick ? quickIs( cur, handleObj.quick ) : jqcur.is( sel ) + ); + } + if ( selMatch[ sel ] ) { + matches.push( handleObj ); + } + } + if ( matches.length ) { + handlerQueue.push({ elem: cur, matches: matches }); + } + } + } + + // Add the remaining (directly-bound) handlers + if ( handlers.length > delegateCount ) { + handlerQueue.push({ elem: this, matches: handlers.slice( delegateCount ) }); + } + + // Run delegates first; they may want to stop propagation beneath us + for ( i = 0; i < handlerQueue.length && !event.isPropagationStopped(); i++ ) { + matched = handlerQueue[ i ]; + event.currentTarget = matched.elem; + + for ( j = 0; j < matched.matches.length && !event.isImmediatePropagationStopped(); j++ ) { + handleObj = matched.matches[ j ]; + + // Triggered event must either 1) be non-exclusive and have no namespace, or + // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). + if ( run_all || (!event.namespace && !handleObj.namespace) || event.namespace_re && event.namespace_re.test( handleObj.namespace ) ) { + + event.data = handleObj.data; + event.handleObj = handleObj; + + ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) + .apply( matched.elem, args ); + + if ( ret !== undefined ) { + event.result = ret; + if ( ret === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + return event.result; + }, + + // Includes some event props shared by KeyEvent and MouseEvent + // *** attrChange attrName relatedNode srcElement are not normalized, non-W3C, deprecated, will be removed in 1.8 *** + props: "attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), + + fixHooks: {}, + + keyHooks: { + props: "char charCode key keyCode".split(" "), + filter: function( event, original ) { + + // Add which for key events + if ( event.which == null ) { + event.which = original.charCode != null ? original.charCode : original.keyCode; + } + + return event; + } + }, + + mouseHooks: { + props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), + filter: function( event, original ) { + var eventDoc, doc, body, + button = original.button, + fromElement = original.fromElement; + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && original.clientX != null ) { + eventDoc = event.target.ownerDocument || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + + event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); + } + + // Add relatedTarget, if necessary + if ( !event.relatedTarget && fromElement ) { + event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && button !== undefined ) { + event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + } + + return event; + } + }, + + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } + + // Create a writable copy of the event object and normalize some properties + var i, prop, + originalEvent = event, + fixHook = jQuery.event.fixHooks[ event.type ] || {}, + copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; + + event = jQuery.Event( originalEvent ); + + for ( i = copy.length; i; ) { + prop = copy[ --i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Fix target property, if necessary (#1925, IE 6/7/8 & Safari2) + if ( !event.target ) { + event.target = originalEvent.srcElement || document; + } + + // Target should not be a text node (#504, Safari) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + // For mouse/key events; add metaKey if it's not there (#3368, IE6/7/8) + if ( event.metaKey === undefined ) { + event.metaKey = event.ctrlKey; + } + + return fixHook.filter? fixHook.filter( event, originalEvent ) : event; + }, + + special: { + ready: { + // Make sure the ready event is setup + setup: jQuery.bindReady + }, + + load: { + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + + focus: { + delegateType: "focusin" + }, + blur: { + delegateType: "focusout" + }, + + beforeunload: { + setup: function( data, namespaces, eventHandle ) { + // We only want to do this special case on windows + if ( jQuery.isWindow( this ) ) { + this.onbeforeunload = eventHandle; + } + }, + + teardown: function( namespaces, eventHandle ) { + if ( this.onbeforeunload === eventHandle ) { + this.onbeforeunload = null; + } + } + } + }, + + simulate: function( type, elem, event, bubble ) { + // Piggyback on a donor event to simulate a different one. + // Fake originalEvent to avoid donor's stopPropagation, but if the + // simulated event prevents default then we do the same on the donor. + var e = jQuery.extend( + new jQuery.Event(), + event, + { type: type, + isSimulated: true, + originalEvent: {} + } + ); + if ( bubble ) { + jQuery.event.trigger( e, null, elem ); + } else { + jQuery.event.dispatch.call( elem, e ); + } + if ( e.isDefaultPrevented() ) { + event.preventDefault(); + } + } +}; + +// Some plugins are using, but it's undocumented/deprecated and will be removed. +// The 1.7 special event interface should provide all the hooks needed now. +jQuery.event.handle = jQuery.event.dispatch; + +jQuery.removeEvent = document.removeEventListener ? + function( elem, type, handle ) { + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle, false ); + } + } : + function( elem, type, handle ) { + if ( elem.detachEvent ) { + elem.detachEvent( "on" + type, handle ); + } + }; + +jQuery.Event = function( src, props ) { + // Allow instantiation without the 'new' keyword + if ( !(this instanceof jQuery.Event) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false || + src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +function returnFalse() { + return false; +} +function returnTrue() { + return true; +} + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + preventDefault: function() { + this.isDefaultPrevented = returnTrue; + + var e = this.originalEvent; + if ( !e ) { + return; + } + + // if preventDefault exists run it on the original event + if ( e.preventDefault ) { + e.preventDefault(); + + // otherwise set the returnValue property of the original event to false (IE) + } else { + e.returnValue = false; + } + }, + stopPropagation: function() { + this.isPropagationStopped = returnTrue; + + var e = this.originalEvent; + if ( !e ) { + return; + } + // if stopPropagation exists run it on the original event + if ( e.stopPropagation ) { + e.stopPropagation(); + } + // otherwise set the cancelBubble property of the original event to true (IE) + e.cancelBubble = true; + }, + stopImmediatePropagation: function() { + this.isImmediatePropagationStopped = returnTrue; + this.stopPropagation(); + }, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse +}; + +// Create mouseenter/leave events using mouseover/out and event-time checks +jQuery.each({ + mouseenter: "mouseover", + mouseleave: "mouseout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var target = this, + related = event.relatedTarget, + handleObj = event.handleObj, + selector = handleObj.selector, + ret; + + // For mousenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || (related !== target && !jQuery.contains( target, related )) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +}); + +// IE submit delegation +if ( !jQuery.support.submitBubbles ) { + + jQuery.event.special.submit = { + setup: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Lazy-add a submit handler when a descendant form may potentially be submitted + jQuery.event.add( this, "click._submit keypress._submit", function( e ) { + // Node name check avoids a VML-related crash in IE (#9807) + var elem = e.target, + form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; + if ( form && !form._submit_attached ) { + jQuery.event.add( form, "submit._submit", function( event ) { + // If form was submitted by the user, bubble the event up the tree + if ( this.parentNode && !event.isTrigger ) { + jQuery.event.simulate( "submit", this.parentNode, event, true ); + } + }); + form._submit_attached = true; + } + }); + // return undefined since we don't need an event listener + }, + + teardown: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Remove delegated handlers; cleanData eventually reaps submit handlers attached above + jQuery.event.remove( this, "._submit" ); + } + }; +} + +// IE change delegation and checkbox/radio fix +if ( !jQuery.support.changeBubbles ) { + + jQuery.event.special.change = { + + setup: function() { + + if ( rformElems.test( this.nodeName ) ) { + // IE doesn't fire change on a check/radio until blur; trigger it on click + // after a propertychange. Eat the blur-change in special.change.handle. + // This still fires onchange a second time for check/radio after blur. + if ( this.type === "checkbox" || this.type === "radio" ) { + jQuery.event.add( this, "propertychange._change", function( event ) { + if ( event.originalEvent.propertyName === "checked" ) { + this._just_changed = true; + } + }); + jQuery.event.add( this, "click._change", function( event ) { + if ( this._just_changed && !event.isTrigger ) { + this._just_changed = false; + jQuery.event.simulate( "change", this, event, true ); + } + }); + } + return false; + } + // Delegated event; lazy-add a change handler on descendant inputs + jQuery.event.add( this, "beforeactivate._change", function( e ) { + var elem = e.target; + + if ( rformElems.test( elem.nodeName ) && !elem._change_attached ) { + jQuery.event.add( elem, "change._change", function( event ) { + if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { + jQuery.event.simulate( "change", this.parentNode, event, true ); + } + }); + elem._change_attached = true; + } + }); + }, + + handle: function( event ) { + var elem = event.target; + + // Swallow native change events from checkbox/radio, we already triggered them above + if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { + return event.handleObj.handler.apply( this, arguments ); + } + }, + + teardown: function() { + jQuery.event.remove( this, "._change" ); + + return rformElems.test( this.nodeName ); + } + }; +} + +// Create "bubbling" focus and blur events +if ( !jQuery.support.focusinBubbles ) { + jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { + + // Attach a single capturing handler while someone wants focusin/focusout + var attaches = 0, + handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); + }; + + jQuery.event.special[ fix ] = { + setup: function() { + if ( attaches++ === 0 ) { + document.addEventListener( orig, handler, true ); + } + }, + teardown: function() { + if ( --attaches === 0 ) { + document.removeEventListener( orig, handler, true ); + } + } + }; + }); +} + +jQuery.fn.extend({ + + on: function( types, selector, data, fn, /*INTERNAL*/ one ) { + var origFn, type; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + // ( types-Object, data ) + data = selector; + selector = undefined; + } + for ( type in types ) { + this.on( type, selector, data, types[ type ], one ); + } + return this; + } + + if ( data == null && fn == null ) { + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return this; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return this.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + }); + }, + one: function( types, selector, data, fn ) { + return this.on.call( this, types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + if ( types && types.preventDefault && types.handleObj ) { + // ( event ) dispatched jQuery.Event + var handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace? handleObj.type + "." + handleObj.namespace : handleObj.type, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + // ( types-object [, selector] ) + for ( var type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each(function() { + jQuery.event.remove( this, types, fn, selector ); + }); + }, + + bind: function( types, data, fn ) { + return this.on( types, null, data, fn ); + }, + unbind: function( types, fn ) { + return this.off( types, null, fn ); + }, + + live: function( types, data, fn ) { + jQuery( this.context ).on( types, this.selector, data, fn ); + return this; + }, + die: function( types, fn ) { + jQuery( this.context ).off( types, this.selector || "**", fn ); + return this; + }, + + delegate: function( selector, types, data, fn ) { + return this.on( types, selector, data, fn ); + }, + undelegate: function( selector, types, fn ) { + // ( namespace ) or ( selector, types [, fn] ) + return arguments.length == 1? this.off( selector, "**" ) : this.off( types, selector, fn ); + }, + + trigger: function( type, data ) { + return this.each(function() { + jQuery.event.trigger( type, data, this ); + }); + }, + triggerHandler: function( type, data ) { + if ( this[0] ) { + return jQuery.event.trigger( type, data, this[0], true ); + } + }, + + toggle: function( fn ) { + // Save reference to arguments for access in closure + var args = arguments, + guid = fn.guid || jQuery.guid++, + i = 0, + toggler = function( event ) { + // Figure out which function to execute + var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i; + jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 ); + + // Make sure that clicks stop + event.preventDefault(); + + // and execute the function + return args[ lastToggle ].apply( this, arguments ) || false; + }; + + // link all the functions, so any of them can unbind this click handler + toggler.guid = guid; + while ( i < args.length ) { + args[ i++ ].guid = guid; + } + + return this.click( toggler ); + }, + + hover: function( fnOver, fnOut ) { + return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); + } +}); + +jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + + "change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) { + + // Handle event binding + jQuery.fn[ name ] = function( data, fn ) { + if ( fn == null ) { + fn = data; + data = null; + } + + return arguments.length > 0 ? + this.on( name, null, data, fn ) : + this.trigger( name ); + }; + + if ( jQuery.attrFn ) { + jQuery.attrFn[ name ] = true; + } + + if ( rkeyEvent.test( name ) ) { + jQuery.event.fixHooks[ name ] = jQuery.event.keyHooks; + } + + if ( rmouseEvent.test( name ) ) { + jQuery.event.fixHooks[ name ] = jQuery.event.mouseHooks; + } +}); + + + +/*! + * Sizzle CSS Selector Engine + * Copyright 2011, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * More information: http://sizzlejs.com/ + */ +(function(){ + +var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, + expando = "sizcache" + (Math.random() + '').replace('.', ''), + done = 0, + toString = Object.prototype.toString, + hasDuplicate = false, + baseHasDuplicate = true, + rBackslash = /\\/g, + rReturn = /\r\n/g, + rNonWord = /\W/; + +// Here we check if the JavaScript engine is using some sort of +// optimization where it does not always call our comparision +// function. If that is the case, discard the hasDuplicate value. +// Thus far that includes Google Chrome. +[0, 0].sort(function() { + baseHasDuplicate = false; + return 0; +}); + +var Sizzle = function( selector, context, results, seed ) { + results = results || []; + context = context || document; + + var origContext = context; + + if ( context.nodeType !== 1 && context.nodeType !== 9 ) { + return []; + } + + if ( !selector || typeof selector !== "string" ) { + return results; + } + + var m, set, checkSet, extra, ret, cur, pop, i, + prune = true, + contextXML = Sizzle.isXML( context ), + parts = [], + soFar = selector; + + // Reset the position of the chunker regexp (start from head) + do { + chunker.exec( "" ); + m = chunker.exec( soFar ); + + if ( m ) { + soFar = m[3]; + + parts.push( m[1] ); + + if ( m[2] ) { + extra = m[3]; + break; + } + } + } while ( m ); + + if ( parts.length > 1 && origPOS.exec( selector ) ) { + + if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { + set = posProcess( parts[0] + parts[1], context, seed ); + + } else { + set = Expr.relative[ parts[0] ] ? + [ context ] : + Sizzle( parts.shift(), context ); + + while ( parts.length ) { + selector = parts.shift(); + + if ( Expr.relative[ selector ] ) { + selector += parts.shift(); + } + + set = posProcess( selector, set, seed ); + } + } + + } else { + // Take a shortcut and set the context if the root selector is an ID + // (but not if it'll be faster if the inner selector is an ID) + if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && + Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { + + ret = Sizzle.find( parts.shift(), context, contextXML ); + context = ret.expr ? + Sizzle.filter( ret.expr, ret.set )[0] : + ret.set[0]; + } + + if ( context ) { + ret = seed ? + { expr: parts.pop(), set: makeArray(seed) } : + Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); + + set = ret.expr ? + Sizzle.filter( ret.expr, ret.set ) : + ret.set; + + if ( parts.length > 0 ) { + checkSet = makeArray( set ); + + } else { + prune = false; + } + + while ( parts.length ) { + cur = parts.pop(); + pop = cur; + + if ( !Expr.relative[ cur ] ) { + cur = ""; + } else { + pop = parts.pop(); + } + + if ( pop == null ) { + pop = context; + } + + Expr.relative[ cur ]( checkSet, pop, contextXML ); + } + + } else { + checkSet = parts = []; + } + } + + if ( !checkSet ) { + checkSet = set; + } + + if ( !checkSet ) { + Sizzle.error( cur || selector ); + } + + if ( toString.call(checkSet) === "[object Array]" ) { + if ( !prune ) { + results.push.apply( results, checkSet ); + + } else if ( context && context.nodeType === 1 ) { + for ( i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) { + results.push( set[i] ); + } + } + + } else { + for ( i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && checkSet[i].nodeType === 1 ) { + results.push( set[i] ); + } + } + } + + } else { + makeArray( checkSet, results ); + } + + if ( extra ) { + Sizzle( extra, origContext, results, seed ); + Sizzle.uniqueSort( results ); + } + + return results; +}; + +Sizzle.uniqueSort = function( results ) { + if ( sortOrder ) { + hasDuplicate = baseHasDuplicate; + results.sort( sortOrder ); + + if ( hasDuplicate ) { + for ( var i = 1; i < results.length; i++ ) { + if ( results[i] === results[ i - 1 ] ) { + results.splice( i--, 1 ); + } + } + } + } + + return results; +}; + +Sizzle.matches = function( expr, set ) { + return Sizzle( expr, null, null, set ); +}; + +Sizzle.matchesSelector = function( node, expr ) { + return Sizzle( expr, null, null, [node] ).length > 0; +}; + +Sizzle.find = function( expr, context, isXML ) { + var set, i, len, match, type, left; + + if ( !expr ) { + return []; + } + + for ( i = 0, len = Expr.order.length; i < len; i++ ) { + type = Expr.order[i]; + + if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { + left = match[1]; + match.splice( 1, 1 ); + + if ( left.substr( left.length - 1 ) !== "\\" ) { + match[1] = (match[1] || "").replace( rBackslash, "" ); + set = Expr.find[ type ]( match, context, isXML ); + + if ( set != null ) { + expr = expr.replace( Expr.match[ type ], "" ); + break; + } + } + } + } + + if ( !set ) { + set = typeof context.getElementsByTagName !== "undefined" ? + context.getElementsByTagName( "*" ) : + []; + } + + return { set: set, expr: expr }; +}; + +Sizzle.filter = function( expr, set, inplace, not ) { + var match, anyFound, + type, found, item, filter, left, + i, pass, + old = expr, + result = [], + curLoop = set, + isXMLFilter = set && set[0] && Sizzle.isXML( set[0] ); + + while ( expr && set.length ) { + for ( type in Expr.filter ) { + if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) { + filter = Expr.filter[ type ]; + left = match[1]; + + anyFound = false; + + match.splice(1,1); + + if ( left.substr( left.length - 1 ) === "\\" ) { + continue; + } + + if ( curLoop === result ) { + result = []; + } + + if ( Expr.preFilter[ type ] ) { + match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter ); + + if ( !match ) { + anyFound = found = true; + + } else if ( match === true ) { + continue; + } + } + + if ( match ) { + for ( i = 0; (item = curLoop[i]) != null; i++ ) { + if ( item ) { + found = filter( item, match, i, curLoop ); + pass = not ^ found; + + if ( inplace && found != null ) { + if ( pass ) { + anyFound = true; + + } else { + curLoop[i] = false; + } + + } else if ( pass ) { + result.push( item ); + anyFound = true; + } + } + } + } + + if ( found !== undefined ) { + if ( !inplace ) { + curLoop = result; + } + + expr = expr.replace( Expr.match[ type ], "" ); + + if ( !anyFound ) { + return []; + } + + break; + } + } + } + + // Improper expression + if ( expr === old ) { + if ( anyFound == null ) { + Sizzle.error( expr ); + + } else { + break; + } + } + + old = expr; + } + + return curLoop; +}; + +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +/** + * Utility function for retreiving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +var getText = Sizzle.getText = function( elem ) { + var i, node, + nodeType = elem.nodeType, + ret = ""; + + if ( nodeType ) { + if ( nodeType === 1 || nodeType === 9 ) { + // Use textContent || innerText for elements + if ( typeof elem.textContent === 'string' ) { + return elem.textContent; + } else if ( typeof elem.innerText === 'string' ) { + // Replace IE's carriage returns + return elem.innerText.replace( rReturn, '' ); + } else { + // Traverse it's children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + } else { + + // If no nodeType, this is expected to be an array + for ( i = 0; (node = elem[i]); i++ ) { + // Do not traverse comment nodes + if ( node.nodeType !== 8 ) { + ret += getText( node ); + } + } + } + return ret; +}; + +var Expr = Sizzle.selectors = { + order: [ "ID", "NAME", "TAG" ], + + match: { + ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, + CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, + NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/, + ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/, + TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/, + CHILD: /:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/, + POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/, + PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/ + }, + + leftMatch: {}, + + attrMap: { + "class": "className", + "for": "htmlFor" + }, + + attrHandle: { + href: function( elem ) { + return elem.getAttribute( "href" ); + }, + type: function( elem ) { + return elem.getAttribute( "type" ); + } + }, + + relative: { + "+": function(checkSet, part){ + var isPartStr = typeof part === "string", + isTag = isPartStr && !rNonWord.test( part ), + isPartStrNotTag = isPartStr && !isTag; + + if ( isTag ) { + part = part.toLowerCase(); + } + + for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) { + if ( (elem = checkSet[i]) ) { + while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {} + + checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ? + elem || false : + elem === part; + } + } + + if ( isPartStrNotTag ) { + Sizzle.filter( part, checkSet, true ); + } + }, + + ">": function( checkSet, part ) { + var elem, + isPartStr = typeof part === "string", + i = 0, + l = checkSet.length; + + if ( isPartStr && !rNonWord.test( part ) ) { + part = part.toLowerCase(); + + for ( ; i < l; i++ ) { + elem = checkSet[i]; + + if ( elem ) { + var parent = elem.parentNode; + checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false; + } + } + + } else { + for ( ; i < l; i++ ) { + elem = checkSet[i]; + + if ( elem ) { + checkSet[i] = isPartStr ? + elem.parentNode : + elem.parentNode === part; + } + } + + if ( isPartStr ) { + Sizzle.filter( part, checkSet, true ); + } + } + }, + + "": function(checkSet, part, isXML){ + var nodeCheck, + doneName = done++, + checkFn = dirCheck; + + if ( typeof part === "string" && !rNonWord.test( part ) ) { + part = part.toLowerCase(); + nodeCheck = part; + checkFn = dirNodeCheck; + } + + checkFn( "parentNode", part, doneName, checkSet, nodeCheck, isXML ); + }, + + "~": function( checkSet, part, isXML ) { + var nodeCheck, + doneName = done++, + checkFn = dirCheck; + + if ( typeof part === "string" && !rNonWord.test( part ) ) { + part = part.toLowerCase(); + nodeCheck = part; + checkFn = dirNodeCheck; + } + + checkFn( "previousSibling", part, doneName, checkSet, nodeCheck, isXML ); + } + }, + + find: { + ID: function( match, context, isXML ) { + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + return m && m.parentNode ? [m] : []; + } + }, + + NAME: function( match, context ) { + if ( typeof context.getElementsByName !== "undefined" ) { + var ret = [], + results = context.getElementsByName( match[1] ); + + for ( var i = 0, l = results.length; i < l; i++ ) { + if ( results[i].getAttribute("name") === match[1] ) { + ret.push( results[i] ); + } + } + + return ret.length === 0 ? null : ret; + } + }, + + TAG: function( match, context ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { + return context.getElementsByTagName( match[1] ); + } + } + }, + preFilter: { + CLASS: function( match, curLoop, inplace, result, not, isXML ) { + match = " " + match[1].replace( rBackslash, "" ) + " "; + + if ( isXML ) { + return match; + } + + for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) { + if ( elem ) { + if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n\r]/g, " ").indexOf(match) >= 0) ) { + if ( !inplace ) { + result.push( elem ); + } + + } else if ( inplace ) { + curLoop[i] = false; + } + } + } + + return false; + }, + + ID: function( match ) { + return match[1].replace( rBackslash, "" ); + }, + + TAG: function( match, curLoop ) { + return match[1].replace( rBackslash, "" ).toLowerCase(); + }, + + CHILD: function( match ) { + if ( match[1] === "nth" ) { + if ( !match[2] ) { + Sizzle.error( match[0] ); + } + + match[2] = match[2].replace(/^\+|\s*/g, ''); + + // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6' + var test = /(-?)(\d*)(?:n([+\-]?\d*))?/.exec( + match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" || + !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); + + // calculate the numbers (first)n+(last) including if they are negative + match[2] = (test[1] + (test[2] || 1)) - 0; + match[3] = test[3] - 0; + } + else if ( match[2] ) { + Sizzle.error( match[0] ); + } + + // TODO: Move to normal caching system + match[0] = done++; + + return match; + }, + + ATTR: function( match, curLoop, inplace, result, not, isXML ) { + var name = match[1] = match[1].replace( rBackslash, "" ); + + if ( !isXML && Expr.attrMap[name] ) { + match[1] = Expr.attrMap[name]; + } + + // Handle if an un-quoted value was used + match[4] = ( match[4] || match[5] || "" ).replace( rBackslash, "" ); + + if ( match[2] === "~=" ) { + match[4] = " " + match[4] + " "; + } + + return match; + }, + + PSEUDO: function( match, curLoop, inplace, result, not ) { + if ( match[1] === "not" ) { + // If we're dealing with a complex expression, or a simple one + if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) { + match[3] = Sizzle(match[3], null, null, curLoop); + + } else { + var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); + + if ( !inplace ) { + result.push.apply( result, ret ); + } + + return false; + } + + } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { + return true; + } + + return match; + }, + + POS: function( match ) { + match.unshift( true ); + + return match; + } + }, + + filters: { + enabled: function( elem ) { + return elem.disabled === false && elem.type !== "hidden"; + }, + + disabled: function( elem ) { + return elem.disabled === true; + }, + + checked: function( elem ) { + return elem.checked === true; + }, + + selected: function( elem ) { + // Accessing this property makes selected-by-default + // options in Safari work properly + if ( elem.parentNode ) { + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + parent: function( elem ) { + return !!elem.firstChild; + }, + + empty: function( elem ) { + return !elem.firstChild; + }, + + has: function( elem, i, match ) { + return !!Sizzle( match[3], elem ).length; + }, + + header: function( elem ) { + return (/h\d/i).test( elem.nodeName ); + }, + + text: function( elem ) { + var attr = elem.getAttribute( "type" ), type = elem.type; + // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) + // use getAttribute instead to test this case + return elem.nodeName.toLowerCase() === "input" && "text" === type && ( attr === type || attr === null ); + }, + + radio: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "radio" === elem.type; + }, + + checkbox: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "checkbox" === elem.type; + }, + + file: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "file" === elem.type; + }, + + password: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "password" === elem.type; + }, + + submit: function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && "submit" === elem.type; + }, + + image: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "image" === elem.type; + }, + + reset: function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && "reset" === elem.type; + }, + + button: function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && "button" === elem.type || name === "button"; + }, + + input: function( elem ) { + return (/input|select|textarea|button/i).test( elem.nodeName ); + }, + + focus: function( elem ) { + return elem === elem.ownerDocument.activeElement; + } + }, + setFilters: { + first: function( elem, i ) { + return i === 0; + }, + + last: function( elem, i, match, array ) { + return i === array.length - 1; + }, + + even: function( elem, i ) { + return i % 2 === 0; + }, + + odd: function( elem, i ) { + return i % 2 === 1; + }, + + lt: function( elem, i, match ) { + return i < match[3] - 0; + }, + + gt: function( elem, i, match ) { + return i > match[3] - 0; + }, + + nth: function( elem, i, match ) { + return match[3] - 0 === i; + }, + + eq: function( elem, i, match ) { + return match[3] - 0 === i; + } + }, + filter: { + PSEUDO: function( elem, match, i, array ) { + var name = match[1], + filter = Expr.filters[ name ]; + + if ( filter ) { + return filter( elem, i, match, array ); + + } else if ( name === "contains" ) { + return (elem.textContent || elem.innerText || getText([ elem ]) || "").indexOf(match[3]) >= 0; + + } else if ( name === "not" ) { + var not = match[3]; + + for ( var j = 0, l = not.length; j < l; j++ ) { + if ( not[j] === elem ) { + return false; + } + } + + return true; + + } else { + Sizzle.error( name ); + } + }, + + CHILD: function( elem, match ) { + var first, last, + doneName, parent, cache, + count, diff, + type = match[1], + node = elem; + + switch ( type ) { + case "only": + case "first": + while ( (node = node.previousSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + + if ( type === "first" ) { + return true; + } + + node = elem; + + case "last": + while ( (node = node.nextSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + + return true; + + case "nth": + first = match[2]; + last = match[3]; + + if ( first === 1 && last === 0 ) { + return true; + } + + doneName = match[0]; + parent = elem.parentNode; + + if ( parent && (parent[ expando ] !== doneName || !elem.nodeIndex) ) { + count = 0; + + for ( node = parent.firstChild; node; node = node.nextSibling ) { + if ( node.nodeType === 1 ) { + node.nodeIndex = ++count; + } + } + + parent[ expando ] = doneName; + } + + diff = elem.nodeIndex - last; + + if ( first === 0 ) { + return diff === 0; + + } else { + return ( diff % first === 0 && diff / first >= 0 ); + } + } + }, + + ID: function( elem, match ) { + return elem.nodeType === 1 && elem.getAttribute("id") === match; + }, + + TAG: function( elem, match ) { + return (match === "*" && elem.nodeType === 1) || !!elem.nodeName && elem.nodeName.toLowerCase() === match; + }, + + CLASS: function( elem, match ) { + return (" " + (elem.className || elem.getAttribute("class")) + " ") + .indexOf( match ) > -1; + }, + + ATTR: function( elem, match ) { + var name = match[1], + result = Sizzle.attr ? + Sizzle.attr( elem, name ) : + Expr.attrHandle[ name ] ? + Expr.attrHandle[ name ]( elem ) : + elem[ name ] != null ? + elem[ name ] : + elem.getAttribute( name ), + value = result + "", + type = match[2], + check = match[4]; + + return result == null ? + type === "!=" : + !type && Sizzle.attr ? + result != null : + type === "=" ? + value === check : + type === "*=" ? + value.indexOf(check) >= 0 : + type === "~=" ? + (" " + value + " ").indexOf(check) >= 0 : + !check ? + value && result !== false : + type === "!=" ? + value !== check : + type === "^=" ? + value.indexOf(check) === 0 : + type === "$=" ? + value.substr(value.length - check.length) === check : + type === "|=" ? + value === check || value.substr(0, check.length + 1) === check + "-" : + false; + }, + + POS: function( elem, match, i, array ) { + var name = match[2], + filter = Expr.setFilters[ name ]; + + if ( filter ) { + return filter( elem, i, match, array ); + } + } + } +}; + +var origPOS = Expr.match.POS, + fescape = function(all, num){ + return "\\" + (num - 0 + 1); + }; + +for ( var type in Expr.match ) { + Expr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\[]*\])(?![^\(]*\))/.source) ); + Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) ); +} + +var makeArray = function( array, results ) { + array = Array.prototype.slice.call( array, 0 ); + + if ( results ) { + results.push.apply( results, array ); + return results; + } + + return array; +}; + +// Perform a simple check to determine if the browser is capable of +// converting a NodeList to an array using builtin methods. +// Also verifies that the returned array holds DOM nodes +// (which is not the case in the Blackberry browser) +try { + Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType; + +// Provide a fallback method if it does not work +} catch( e ) { + makeArray = function( array, results ) { + var i = 0, + ret = results || []; + + if ( toString.call(array) === "[object Array]" ) { + Array.prototype.push.apply( ret, array ); + + } else { + if ( typeof array.length === "number" ) { + for ( var l = array.length; i < l; i++ ) { + ret.push( array[i] ); + } + + } else { + for ( ; array[i]; i++ ) { + ret.push( array[i] ); + } + } + } + + return ret; + }; +} + +var sortOrder, siblingCheck; + +if ( document.documentElement.compareDocumentPosition ) { + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) { + return a.compareDocumentPosition ? -1 : 1; + } + + return a.compareDocumentPosition(b) & 4 ? -1 : 1; + }; + +} else { + sortOrder = function( a, b ) { + // The nodes are identical, we can exit early + if ( a === b ) { + hasDuplicate = true; + return 0; + + // Fallback to using sourceIndex (in IE) if it's available on both nodes + } else if ( a.sourceIndex && b.sourceIndex ) { + return a.sourceIndex - b.sourceIndex; + } + + var al, bl, + ap = [], + bp = [], + aup = a.parentNode, + bup = b.parentNode, + cur = aup; + + // If the nodes are siblings (or identical) we can do a quick check + if ( aup === bup ) { + return siblingCheck( a, b ); + + // If no parents were found then the nodes are disconnected + } else if ( !aup ) { + return -1; + + } else if ( !bup ) { + return 1; + } + + // Otherwise they're somewhere else in the tree so we need + // to build up a full list of the parentNodes for comparison + while ( cur ) { + ap.unshift( cur ); + cur = cur.parentNode; + } + + cur = bup; + + while ( cur ) { + bp.unshift( cur ); + cur = cur.parentNode; + } + + al = ap.length; + bl = bp.length; + + // Start walking down the tree looking for a discrepancy + for ( var i = 0; i < al && i < bl; i++ ) { + if ( ap[i] !== bp[i] ) { + return siblingCheck( ap[i], bp[i] ); + } + } + + // We ended someplace up the tree so do a sibling check + return i === al ? + siblingCheck( a, bp[i], -1 ) : + siblingCheck( ap[i], b, 1 ); + }; + + siblingCheck = function( a, b, ret ) { + if ( a === b ) { + return ret; + } + + var cur = a.nextSibling; + + while ( cur ) { + if ( cur === b ) { + return -1; + } + + cur = cur.nextSibling; + } + + return 1; + }; +} + +// Check to see if the browser returns elements by name when +// querying by getElementById (and provide a workaround) +(function(){ + // We're going to inject a fake input element with a specified name + var form = document.createElement("div"), + id = "script" + (new Date()).getTime(), + root = document.documentElement; + + form.innerHTML = ""; + + // Inject it into the root element, check its status, and remove it quickly + root.insertBefore( form, root.firstChild ); + + // The workaround has to do additional checks after a getElementById + // Which slows things down for other browsers (hence the branching) + if ( document.getElementById( id ) ) { + Expr.find.ID = function( match, context, isXML ) { + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + + return m ? + m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? + [m] : + undefined : + []; + } + }; + + Expr.filter.ID = function( elem, match ) { + var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); + + return elem.nodeType === 1 && node && node.nodeValue === match; + }; + } + + root.removeChild( form ); + + // release memory in IE + root = form = null; +})(); + +(function(){ + // Check to see if the browser returns only elements + // when doing getElementsByTagName("*") + + // Create a fake element + var div = document.createElement("div"); + div.appendChild( document.createComment("") ); + + // Make sure no comments are found + if ( div.getElementsByTagName("*").length > 0 ) { + Expr.find.TAG = function( match, context ) { + var results = context.getElementsByTagName( match[1] ); + + // Filter out possible comments + if ( match[1] === "*" ) { + var tmp = []; + + for ( var i = 0; results[i]; i++ ) { + if ( results[i].nodeType === 1 ) { + tmp.push( results[i] ); + } + } + + results = tmp; + } + + return results; + }; + } + + // Check to see if an attribute returns normalized href attributes + div.innerHTML = ""; + + if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" && + div.firstChild.getAttribute("href") !== "#" ) { + + Expr.attrHandle.href = function( elem ) { + return elem.getAttribute( "href", 2 ); + }; + } + + // release memory in IE + div = null; +})(); + +if ( document.querySelectorAll ) { + (function(){ + var oldSizzle = Sizzle, + div = document.createElement("div"), + id = "__sizzle__"; + + div.innerHTML = "

"; + + // Safari can't handle uppercase or unicode characters when + // in quirks mode. + if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { + return; + } + + Sizzle = function( query, context, extra, seed ) { + context = context || document; + + // Only use querySelectorAll on non-XML documents + // (ID selectors don't work in non-HTML documents) + if ( !seed && !Sizzle.isXML(context) ) { + // See if we find a selector to speed up + var match = /^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec( query ); + + if ( match && (context.nodeType === 1 || context.nodeType === 9) ) { + // Speed-up: Sizzle("TAG") + if ( match[1] ) { + return makeArray( context.getElementsByTagName( query ), extra ); + + // Speed-up: Sizzle(".CLASS") + } else if ( match[2] && Expr.find.CLASS && context.getElementsByClassName ) { + return makeArray( context.getElementsByClassName( match[2] ), extra ); + } + } + + if ( context.nodeType === 9 ) { + // Speed-up: Sizzle("body") + // The body element only exists once, optimize finding it + if ( query === "body" && context.body ) { + return makeArray( [ context.body ], extra ); + + // Speed-up: Sizzle("#ID") + } else if ( match && match[3] ) { + var elem = context.getElementById( match[3] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id === match[3] ) { + return makeArray( [ elem ], extra ); + } + + } else { + return makeArray( [], extra ); + } + } + + try { + return makeArray( context.querySelectorAll(query), extra ); + } catch(qsaError) {} + + // qSA works strangely on Element-rooted queries + // We can work around this by specifying an extra ID on the root + // and working up from there (Thanks to Andrew Dupont for the technique) + // IE 8 doesn't work on object elements + } else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { + var oldContext = context, + old = context.getAttribute( "id" ), + nid = old || id, + hasParent = context.parentNode, + relativeHierarchySelector = /^\s*[+~]/.test( query ); + + if ( !old ) { + context.setAttribute( "id", nid ); + } else { + nid = nid.replace( /'/g, "\\$&" ); + } + if ( relativeHierarchySelector && hasParent ) { + context = context.parentNode; + } + + try { + if ( !relativeHierarchySelector || hasParent ) { + return makeArray( context.querySelectorAll( "[id='" + nid + "'] " + query ), extra ); + } + + } catch(pseudoError) { + } finally { + if ( !old ) { + oldContext.removeAttribute( "id" ); + } + } + } + } + + return oldSizzle(query, context, extra, seed); + }; + + for ( var prop in oldSizzle ) { + Sizzle[ prop ] = oldSizzle[ prop ]; + } + + // release memory in IE + div = null; + })(); +} + +(function(){ + var html = document.documentElement, + matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector; + + if ( matches ) { + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9 fails this) + var disconnectedMatch = !matches.call( document.createElement( "div" ), "div" ), + pseudoWorks = false; + + try { + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( document.documentElement, "[test!='']:sizzle" ); + + } catch( pseudoError ) { + pseudoWorks = true; + } + + Sizzle.matchesSelector = function( node, expr ) { + // Make sure that attribute selectors are quoted + expr = expr.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']"); + + if ( !Sizzle.isXML( node ) ) { + try { + if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) && !/!=/.test( expr ) ) { + var ret = matches.call( node, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || !disconnectedMatch || + // As well, disconnected nodes are said to be in a document + // fragment in IE 9, so check for that + node.document && node.document.nodeType !== 11 ) { + return ret; + } + } + } catch(e) {} + } + + return Sizzle(expr, null, null, [node]).length > 0; + }; + } +})(); + +(function(){ + var div = document.createElement("div"); + + div.innerHTML = "
"; + + // Opera can't find a second classname (in 9.6) + // Also, make sure that getElementsByClassName actually exists + if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) { + return; + } + + // Safari caches class attributes, doesn't catch changes (in 3.2) + div.lastChild.className = "e"; + + if ( div.getElementsByClassName("e").length === 1 ) { + return; + } + + Expr.order.splice(1, 0, "CLASS"); + Expr.find.CLASS = function( match, context, isXML ) { + if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) { + return context.getElementsByClassName(match[1]); + } + }; + + // release memory in IE + div = null; +})(); + +function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + + if ( elem ) { + var match = false; + + elem = elem[dir]; + + while ( elem ) { + if ( elem[ expando ] === doneName ) { + match = checkSet[elem.sizset]; + break; + } + + if ( elem.nodeType === 1 && !isXML ){ + elem[ expando ] = doneName; + elem.sizset = i; + } + + if ( elem.nodeName.toLowerCase() === cur ) { + match = elem; + break; + } + + elem = elem[dir]; + } + + checkSet[i] = match; + } + } +} + +function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + + if ( elem ) { + var match = false; + + elem = elem[dir]; + + while ( elem ) { + if ( elem[ expando ] === doneName ) { + match = checkSet[elem.sizset]; + break; + } + + if ( elem.nodeType === 1 ) { + if ( !isXML ) { + elem[ expando ] = doneName; + elem.sizset = i; + } + + if ( typeof cur !== "string" ) { + if ( elem === cur ) { + match = true; + break; + } + + } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) { + match = elem; + break; + } + } + + elem = elem[dir]; + } + + checkSet[i] = match; + } + } +} + +if ( document.documentElement.contains ) { + Sizzle.contains = function( a, b ) { + return a !== b && (a.contains ? a.contains(b) : true); + }; + +} else if ( document.documentElement.compareDocumentPosition ) { + Sizzle.contains = function( a, b ) { + return !!(a.compareDocumentPosition(b) & 16); + }; + +} else { + Sizzle.contains = function() { + return false; + }; +} + +Sizzle.isXML = function( elem ) { + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement; + + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; + +var posProcess = function( selector, context, seed ) { + var match, + tmpSet = [], + later = "", + root = context.nodeType ? [context] : context; + + // Position selectors must be done after the filter + // And so must :not(positional) so we move all PSEUDOs to the end + while ( (match = Expr.match.PSEUDO.exec( selector )) ) { + later += match[0]; + selector = selector.replace( Expr.match.PSEUDO, "" ); + } + + selector = Expr.relative[selector] ? selector + "*" : selector; + + for ( var i = 0, l = root.length; i < l; i++ ) { + Sizzle( selector, root[i], tmpSet, seed ); + } + + return Sizzle.filter( later, tmpSet ); +}; + +// EXPOSE +// Override sizzle attribute retrieval +Sizzle.attr = jQuery.attr; +Sizzle.selectors.attrMap = {}; +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; +jQuery.expr[":"] = jQuery.expr.filters; +jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; + + +})(); + + +var runtil = /Until$/, + rparentsprev = /^(?:parents|prevUntil|prevAll)/, + // Note: This RegExp should be improved, or likely pulled from Sizzle + rmultiselector = /,/, + isSimple = /^.[^:#\[\.,]*$/, + slice = Array.prototype.slice, + POS = jQuery.expr.match.POS, + // methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend({ + find: function( selector ) { + var self = this, + i, l; + + if ( typeof selector !== "string" ) { + return jQuery( selector ).filter(function() { + for ( i = 0, l = self.length; i < l; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + }); + } + + var ret = this.pushStack( "", "find", selector ), + length, n, r; + + for ( i = 0, l = this.length; i < l; i++ ) { + length = ret.length; + jQuery.find( selector, this[i], ret ); + + if ( i > 0 ) { + // Make sure that the results are unique + for ( n = length; n < ret.length; n++ ) { + for ( r = 0; r < length; r++ ) { + if ( ret[r] === ret[n] ) { + ret.splice(n--, 1); + break; + } + } + } + } + } + + return ret; + }, + + has: function( target ) { + var targets = jQuery( target ); + return this.filter(function() { + for ( var i = 0, l = targets.length; i < l; i++ ) { + if ( jQuery.contains( this, targets[i] ) ) { + return true; + } + } + }); + }, + + not: function( selector ) { + return this.pushStack( winnow(this, selector, false), "not", selector); + }, + + filter: function( selector ) { + return this.pushStack( winnow(this, selector, true), "filter", selector ); + }, + + is: function( selector ) { + return !!selector && ( + typeof selector === "string" ? + // If this is a positional selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + POS.test( selector ) ? + jQuery( selector, this.context ).index( this[0] ) >= 0 : + jQuery.filter( selector, this ).length > 0 : + this.filter( selector ).length > 0 ); + }, + + closest: function( selectors, context ) { + var ret = [], i, l, cur = this[0]; + + // Array (deprecated as of jQuery 1.7) + if ( jQuery.isArray( selectors ) ) { + var level = 1; + + while ( cur && cur.ownerDocument && cur !== context ) { + for ( i = 0; i < selectors.length; i++ ) { + + if ( jQuery( cur ).is( selectors[ i ] ) ) { + ret.push({ selector: selectors[ i ], elem: cur, level: level }); + } + } + + cur = cur.parentNode; + level++; + } + + return ret; + } + + // String + var pos = POS.test( selectors ) || typeof selectors !== "string" ? + jQuery( selectors, context || this.context ) : + 0; + + for ( i = 0, l = this.length; i < l; i++ ) { + cur = this[i]; + + while ( cur ) { + if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) { + ret.push( cur ); + break; + + } else { + cur = cur.parentNode; + if ( !cur || !cur.ownerDocument || cur === context || cur.nodeType === 11 ) { + break; + } + } + } + } + + ret = ret.length > 1 ? jQuery.unique( ret ) : ret; + + return this.pushStack( ret, "closest", selectors ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[0] && this[0].parentNode ) ? this.prevAll().length : -1; + } + + // index in selector + if ( typeof elem === "string" ) { + return jQuery.inArray( this[0], jQuery( elem ) ); + } + + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[0] : elem, this ); + }, + + add: function( selector, context ) { + var set = typeof selector === "string" ? + jQuery( selector, context ) : + jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ), + all = jQuery.merge( this.get(), set ); + + return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ? + all : + jQuery.unique( all ) ); + }, + + andSelf: function() { + return this.add( this.prevObject ); + } +}); + +// A painfully simple check to see if an element is disconnected +// from a document (should be improved, where feasible). +function isDisconnected( node ) { + return !node || !node.parentNode || node.parentNode.nodeType === 11; +} + +jQuery.each({ + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return jQuery.dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return jQuery.dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return jQuery.nth( elem, 2, "nextSibling" ); + }, + prev: function( elem ) { + return jQuery.nth( elem, 2, "previousSibling" ); + }, + nextAll: function( elem ) { + return jQuery.dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return jQuery.dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return jQuery.dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return jQuery.dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return jQuery.sibling( elem.parentNode.firstChild, elem ); + }, + children: function( elem ) { + return jQuery.sibling( elem.firstChild ); + }, + contents: function( elem ) { + return jQuery.nodeName( elem, "iframe" ) ? + elem.contentDocument || elem.contentWindow.document : + jQuery.makeArray( elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var ret = jQuery.map( this, fn, until ); + + if ( !runtil.test( name ) ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + ret = jQuery.filter( selector, ret ); + } + + ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret; + + if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) { + ret = ret.reverse(); + } + + return this.pushStack( ret, name, slice.call( arguments ).join(",") ); + }; +}); + +jQuery.extend({ + filter: function( expr, elems, not ) { + if ( not ) { + expr = ":not(" + expr + ")"; + } + + return elems.length === 1 ? + jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] : + jQuery.find.matches(expr, elems); + }, + + dir: function( elem, dir, until ) { + var matched = [], + cur = elem[ dir ]; + + while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { + if ( cur.nodeType === 1 ) { + matched.push( cur ); + } + cur = cur[dir]; + } + return matched; + }, + + nth: function( cur, result, dir, elem ) { + result = result || 1; + var num = 0; + + for ( ; cur; cur = cur[dir] ) { + if ( cur.nodeType === 1 && ++num === result ) { + break; + } + } + + return cur; + }, + + sibling: function( n, elem ) { + var r = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + r.push( n ); + } + } + + return r; + } +}); + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, keep ) { + + // Can't pass null or undefined to indexOf in Firefox 4 + // Set to 0 to skip string check + qualifier = qualifier || 0; + + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep(elements, function( elem, i ) { + var retVal = !!qualifier.call( elem, i, elem ); + return retVal === keep; + }); + + } else if ( qualifier.nodeType ) { + return jQuery.grep(elements, function( elem, i ) { + return ( elem === qualifier ) === keep; + }); + + } else if ( typeof qualifier === "string" ) { + var filtered = jQuery.grep(elements, function( elem ) { + return elem.nodeType === 1; + }); + + if ( isSimple.test( qualifier ) ) { + return jQuery.filter(qualifier, filtered, !keep); + } else { + qualifier = jQuery.filter( qualifier, filtered ); + } + } + + return jQuery.grep(elements, function( elem, i ) { + return ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep; + }); +} + + + + +function createSafeFragment( document ) { + var list = nodeNames.split( "|" ), + safeFrag = document.createDocumentFragment(); + + if ( safeFrag.createElement ) { + while ( list.length ) { + safeFrag.createElement( + list.pop() + ); + } + } + return safeFrag; +} + +var nodeNames = "abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|" + + "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", + rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, + rleadingWhitespace = /^\s+/, + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig, + rtagName = /<([\w:]+)/, + rtbody = /", "" ], + legend: [ 1, "
", "
" ], + thead: [ 1, "", "
" ], + tr: [ 2, "", "
" ], + td: [ 3, "", "
" ], + col: [ 2, "", "
" ], + area: [ 1, "", "" ], + _default: [ 0, "", "" ] + }, + safeFragment = createSafeFragment( document ); + +wrapMap.optgroup = wrapMap.option; +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +// IE can't serialize and + From 152fdd9864a4be2ae6039f111fc90a6aa22ede26 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Thu, 22 Dec 2011 15:46:40 -0800 Subject: [PATCH 0112/1403] Fixed transport initialization. --- lib/socket.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/socket.js b/lib/socket.js index 16bc4e2d..9d041c0a 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -32,6 +32,7 @@ function Socket (opts) { this.host = opts.host || opts.hostname || 'localhost'; this.port = opts.port || 80; + this.query = opts.query || ''; this.upgrade = false !== opts.upgrade; this.path = opts.path || '/engine.io' this.forceJSONP = !!opts.forceJSONP; @@ -59,12 +60,12 @@ util.inherits(Socket, EventEmitter); Socket.prototype.createTransport = function (name) { // debug: creating transport "%s", name var transport = new transports[name]({ - host: self.host - , port: self.port - , secure: self.secure - , path: self.path + '/' + name - , query: self.query ? '&' + self.query : '' - , forceJSONP: self.forceJSONP + host: this.host + , port: this.port + , secure: this.secure + , path: this.path + , query: 'transport=' + name + (this.query ? '&' + this.query : '') + , forceJSONP: this.forceJSONP }, engine); return transport; From e9d8de6e89cdad8981241dee1d6ca3add0f8d993 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Fri, 23 Dec 2011 11:20:44 -0800 Subject: [PATCH 0113/1403] Changed `query` option type. --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 3a63ef75..b92aa0b7 100644 --- a/README.md +++ b/README.md @@ -83,8 +83,7 @@ The client class. _Inherits from EventEmitter_. - `host` (`String`): host name (`localhost`) - `port` (`Number`): port name (`80`) - `path` (`String`): path name - - `query` (`String`): optional query string addition (eg: - `a=b&c=hello+world`) + - `query` (`Object`): optional query string addition (eg: `{ a: 'b' }`) - `secure` (`Boolean): whether the connection is secure - `upgrade` (`Boolean`): defaults to true, whether the client should try to upgrade the transport from long-polling to something better. From eebd224afe9572a08eea80be235daf378b5ef89b Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Fri, 23 Dec 2011 11:21:31 -0800 Subject: [PATCH 0114/1403] Added extra query parameters support to `createTransport` --- lib/socket.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/socket.js b/lib/socket.js index 9d041c0a..27501839 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -52,19 +52,32 @@ util.inherits(Socket, EventEmitter); /** * Creates transport of the given type. * - * @api {String} transport name + * @param {String} transport name + * @param {Object} extra query parameters * @return {Transport} * @api private */ -Socket.prototype.createTransport = function (name) { +Socket.prototype.createTransport = function (name, queryParams) { // debug: creating transport "%s", name + var query = this.query || {} + query.transport = name; + + // add extra query params, if any + if (queryParams) { + for (var i in queryParams) { + if (queryParams.hasOwnProperty(i)) { + query[i] = queryParams[i]; + } + } + } + var transport = new transports[name]({ host: this.host , port: this.port , secure: this.secure , path: this.path - , query: 'transport=' + name + (this.query ? '&' + this.query : '') + , query: query , forceJSONP: this.forceJSONP }, engine); From 973ff5cba1260a25a5f6cb78fdfa0130396bbde9 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Fri, 23 Dec 2011 11:21:53 -0800 Subject: [PATCH 0115/1403] Added `probe` qs component to probes. --- lib/socket.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/socket.js b/lib/socket.js index 27501839..7e8f964d 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -157,7 +157,7 @@ Socket.prototype.setTransport = function (transport) { Socket.prototype.probe = function (name) { // debug: probing transport "%s", name - var transport = this.createTransport(name) + var transport = this.createTransport(name, { probe: 1 }) , self = this transport.once('open', function () { From 1727950a72ed87cdf59e29766c5c360a7b22228a Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Fri, 23 Dec 2011 11:53:00 -0800 Subject: [PATCH 0116/1403] Changing write signature --- lib/transports/polling.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/transports/polling.js b/lib/transports/polling.js index 4c0686eb..c21629ca 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -55,7 +55,7 @@ util.inherits(Polling, Transport); Polling.prototype.doOpen = function () { this.poll(); - this.write(parser.encodePacket('ping')); + this.write({ type: 'ping' }); }; /** From 648cb5b6e385e49fbd1c944e2256a1e532f1a01b Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Fri, 23 Dec 2011 11:53:13 -0800 Subject: [PATCH 0117/1403] Fixed comment --- lib/transports/polling.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/transports/polling.js b/lib/transports/polling.js index c21629ca..247b488d 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -109,7 +109,7 @@ Polling.prototype.onData = function (data) { var packets = parser.decodePayload(data); for (var i = 0, l = packets.length; i < l; i++) { - // if its a PONG to our initial ping we consider the connection open + // if its a pong to our initial ping we consider the connection open if ('opening' == this.readyState) { if ('pong' == packets[i].type) { this.onOpen(); From 238d10645a2bec88798d221a6a97928545ed394f Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Fri, 23 Dec 2011 11:53:29 -0800 Subject: [PATCH 0118/1403] Changing write signature --- lib/transports/polling.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/transports/polling.js b/lib/transports/polling.js index 247b488d..5c7e3308 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -148,7 +148,7 @@ Polling.prototype.onData = function (data) { Polling.prototype.doClose = function () { // we mute existing buffers and bypass buffers - this.write(parser.encodePacket('close')); + this.write({ type: 'close' }); }; /** From b1e46b0ce96e943f05009d2ab56c8d9a9b47ade3 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Fri, 23 Dec 2011 11:54:22 -0800 Subject: [PATCH 0119/1403] Fixed writeMany --- lib/transports/polling.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/transports/polling.js b/lib/transports/polling.js index 5c7e3308..5b21f809 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -159,5 +159,6 @@ Polling.prototype.doClose = function () { */ Polling.prototype.writeMany = function (packets) { - this.write(parser.encodePayload(packets)); + this.doWrite(parser.encodePayload(packets)); +}; }; From f28b6c84aa54fa531e5d4aafe49b09e45100bddf Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Fri, 23 Dec 2011 11:54:52 -0800 Subject: [PATCH 0120/1403] Fixed Polling#write --- lib/transports/polling.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/transports/polling.js b/lib/transports/polling.js index 5b21f809..08160382 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -161,4 +161,15 @@ Polling.prototype.doClose = function () { Polling.prototype.writeMany = function (packets) { this.doWrite(parser.encodePayload(packets)); }; + +/** + * Writes a single-packet payload. + * + * @parma {Object} data packet + * @api private + */ + +Polling.prototype.write = function (packet) { + this.writeMany([packet]); +}; }; From 89f017579d61c8db69808e85633cbc72dc11f800 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Fri, 23 Dec 2011 11:55:09 -0800 Subject: [PATCH 0121/1403] Added Polling#uri --- lib/transports/polling.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/lib/transports/polling.js b/lib/transports/polling.js index 08160382..783d97ba 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -172,4 +172,29 @@ Polling.prototype.writeMany = function (packets) { Polling.prototype.write = function (packet) { this.writeMany([packet]); }; + +/** + * Generates uri for connection. + * + * @api private + */ + +Polling.prototype.uri = function () { + var opts = this.options + , query = util.qs(opts.query) + , schema = opts.secure ? 'https' : 'http' + , port = '' + + // avoid port if default for schema + if (('https' == schema && opts.port != 443) + || ('http' == schema && opts.port != 80)) { + port = ':' + opts.port; + } + + // prepend ? to query + if (query.length) { + query = '?' + query; + } + + return schema + opts.host + port + opts.path + query; }; From 4d0d06f771663e2a4a44961646d27a69056081bf Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Fri, 23 Dec 2011 12:02:31 -0800 Subject: [PATCH 0122/1403] Cleaned up WS#uri --- lib/transports/websocket.js | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/lib/transports/websocket.js b/lib/transports/websocket.js index 90b28b84..a2a6225d 100644 --- a/lib/transports/websocket.js +++ b/lib/transports/websocket.js @@ -105,14 +105,23 @@ WS.prototype.doClose = function () { */ WS.prototype.uri = function () { - return [ - this.options.secure ? 'wss' : 'ws' - , this.options.host - , ':' - , this.options.port - , this.options.path - , this.options.query - ].join('') + var opts = this.options + , query = util.qs(opts.query) + , schema = opts.secure ? 'wss' : 'ws' + , port = '' + + // avoid port if default for schema + if (('wss' == schema && opts.port != 443) + || ('ws' == schema && opts.port != 80)) { + port = ':' + opts.port; + } + + // prepend ? to query + if (query.length) { + query = '?' + query; + } + + return schema + opts.host + port + opts.path + query; }; /** From f3306ea1a1b9d3c3475f6011c2a00d801115ba3e Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Fri, 23 Dec 2011 12:03:01 -0800 Subject: [PATCH 0123/1403] Added util#qs --- lib/util.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lib/util.js b/lib/util.js index f3683b91..c83b009b 100644 --- a/lib/util.js +++ b/lib/util.js @@ -117,3 +117,23 @@ exports.parseUri = function (str) { return uri; }; + +/** + * Compiles a querystring + * + * @param {Object} + * @api public + */ + +exports.qs = function (obj) { + var str = ''; + + for (var i in obj) { + if (obj.hasOwnProperty(i)) { + if (str.length) str += '&'; + str += i + '=' + encodeURIComponent(obj[i]); + } + } + + return str; +}; From f0590b15007a78e60a57ffbf8186d03fdb1de546 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Fri, 23 Dec 2011 12:03:10 -0800 Subject: [PATCH 0124/1403] Added tests for qs#util --- test/util.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/util.js b/test/util.js index 8c2b7e0d..d9facfa2 100644 --- a/test/util.js +++ b/test/util.js @@ -28,4 +28,10 @@ describe('util', function () { expect(query.relative).to.be('/foo/bar?foo=bar'); }); + it('should construct a query string from an object', function () { + expect(util.qs({ a: 'b' })).to.be('a=b'); + expect(util.qs({ a: 'b', c: 'd' })).to.be('a=b&c=d'); + expect(util.qs({ a: 'b', c: 'tobi rocks' })).to.be('a=b&c=tobi%20rocks'); + }); + }); From d4521a3ea4e932ca524500b4cdb073a942e119e7 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Fri, 23 Dec 2011 22:14:41 -0800 Subject: [PATCH 0125/1403] Added pointer to main file in package.json. --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index b6a6908a..c997c1a2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "engine.io-client" , "description": "Client for the realtime Engine" + , "main": "./lib/engine.io-client" , "version": "0.1.0" , "devDependencies": { "mocha": "*" From 9274895848a44f006d06dfda2471943a4a172ea6 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sat, 24 Dec 2011 15:09:59 -0500 Subject: [PATCH 0126/1403] Naming change: transports emit `packet`s not `message`s. --- lib/socket.js | 18 +++++++++--------- lib/transport.js | 10 +++++++++- lib/transports/polling.js | 2 +- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/lib/socket.js b/lib/socket.js index 7e8f964d..75784dc0 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -140,8 +140,8 @@ Socket.prototype.setTransport = function (transport) { .on('open', function () { self.onOpen(); }) - .on('message', function (data) { - self.onMessage(msg); + .on('packet', function (packet) { + self.onPacket(packet); }) .on('close', function () { self.onClose(); @@ -228,14 +228,14 @@ Socket.prototype.onOpen = function () { }; /** - * Handles a message. + * Handles a packet. * * @api private */ -Socket.prototype.onMessage = function (msg) { - // debug: socket receive: type "%s" | data "%s", msg.type, msg.data - switch (msg.type) { +Socket.prototype.onPacket = function (packet) { + // debug: socket receive: type "%s" | data "%s", packet.type, packet.data + switch (packet.type) { case 'noop': break; @@ -245,13 +245,13 @@ Socket.prototype.onMessage = function (msg) { case 'error': var err = new Error('server error'); - err.code = msg.data; + err.code = packet.data; this.emit('error', err); break; case 'message': - this.emit('message', msg.data); - this.onmessage && this.onmessage.call(this, msg.data); + this.emit('message', packet.data); + this.onmessage && this.onmessage.call(this, packet.data); break; } }; diff --git a/lib/transport.js b/lib/transport.js index 0bc97c29..1998b2fd 100644 --- a/lib/transport.js +++ b/lib/transport.js @@ -147,7 +147,15 @@ Transport.prototype.onOpen = function () { */ Transport.prototype.onData = function (data) { - this.onMessage(parser.decodePacket(data)); + this.onPacket(parser.decodePacket(data)); +}; + +/** + * Called with a decoded packet. + */ + +Transport.prototype.onPacket = function (packet) { + this.emit('packet', packet); }; /** diff --git a/lib/transports/polling.js b/lib/transports/polling.js index 783d97ba..2b636325 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -130,7 +130,7 @@ Polling.prototype.onData = function (data) { } // otherwise bypass onData and handle the message - this.onMessage(packets[i]); + this.onPacket(packets[i]); } // if we got data we're not polling From 66eb07a90b50f8271df37e89331e9855d9436580 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Fri, 30 Dec 2011 16:10:25 -0500 Subject: [PATCH 0127/1403] Fixed XDomainRequest type check. --- lib/transports/polling-xhr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/transports/polling-xhr.js b/lib/transports/polling-xhr.js index 28d4ac70..df0e82b8 100644 --- a/lib/transports/polling-xhr.js +++ b/lib/transports/polling-xhr.js @@ -168,7 +168,7 @@ Request.prototype.create = function () { } catch (e) {} } - if (this.xd && this.xhr instanceof XDomainRequest) { + if (this.xd && global.XDomainRequest && this.xhr instanceof XDomainRequest) { this.xhr.onerror = function () { self.onError(); }; From efa41a245c434232f265dc0950a4bd16adae73d3 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Fri, 30 Dec 2011 16:10:37 -0500 Subject: [PATCH 0128/1403] Fixed; make sure to send cookies in cross domain requests. --- lib/transports/polling-xhr.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/transports/polling-xhr.js b/lib/transports/polling-xhr.js index df0e82b8..125186be 100644 --- a/lib/transports/polling-xhr.js +++ b/lib/transports/polling-xhr.js @@ -177,6 +177,7 @@ Request.prototype.create = function () { }; this.xhr.onprogress = empty; } else { + this.xhr.withCredentials = true; this.xhr.onreadystatechange = function () { try { if (xhr.readyState != 4) return; From 74c64863aa05fa5d18e163fa818f70f5027dcd4a Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sat, 31 Dec 2011 09:21:00 -0500 Subject: [PATCH 0129/1403] Added xmlhttprequest/ws dependencies. --- package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package.json b/package.json index c997c1a2..fe7ee727 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,10 @@ , "description": "Client for the realtime Engine" , "main": "./lib/engine.io-client" , "version": "0.1.0" + , "dependencies": { + "ws": "0.3.8" + , "xmlhttprequest": "1.3.0" + } , "devDependencies": { "mocha": "*" , "serve": "*" From 3d3d4218e99e7aa55367578d890e23661196f86b Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sat, 31 Dec 2011 09:21:13 -0500 Subject: [PATCH 0130/1403] Fixed websocket client dependency. --- lib/transports/websocket.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/transports/websocket.js b/lib/transports/websocket.js index a2a6225d..54362005 100644 --- a/lib/transports/websocket.js +++ b/lib/transports/websocket.js @@ -132,7 +132,7 @@ WS.prototype.uri = function () { function ws () { // if node - return require('easy-websocket'); + return require('ws'); // end return global.WebSocket || global.MozWebSocket; From d81aa11f8ba1237c35477871b580ab8cd55c650a Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sat, 31 Dec 2011 13:11:12 -0500 Subject: [PATCH 0131/1403] Removed duplicate Socket#open --- lib/socket.js | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/lib/socket.js b/lib/socket.js index 75784dc0..aa7100ef 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -199,20 +199,6 @@ Socket.prototype.probe = function (name) { }); }; -/** - * Opens the connection - * - * @api public - */ - -Socket.prototype.open = function () { - if ('' == this.readyState || 'closed' == this.readyState) { - this.transport.open() - } - - return this; -}; - /** * Called when connection is deemed open. * From ebe3eb6052dc4a2fe31dbf088e0b72fde9c9bd9e Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sat, 31 Dec 2011 13:11:55 -0500 Subject: [PATCH 0132/1403] Removed engine refernece passed to transport constructor. --- lib/socket.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/socket.js b/lib/socket.js index aa7100ef..9eb1b4c7 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -79,7 +79,7 @@ Socket.prototype.createTransport = function (name, queryParams) { , path: this.path , query: query , forceJSONP: this.forceJSONP - }, engine); + }); return transport; }; From 6222cba321b191bc12a1a5fc1d0fea1d06555909 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:12:33 -0800 Subject: [PATCH 0133/1403] Fixed event-emitter from socket.io --- lib/event-emitter.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/lib/event-emitter.js b/lib/event-emitter.js index 75b2268c..7cd4f1e0 100644 --- a/lib/event-emitter.js +++ b/lib/event-emitter.js @@ -26,7 +26,7 @@ EventEmitter.prototype.on = function (name, fn) { if (!this.$events[name]) { this.$events[name] = fn; - } else if (io.util.isArray(this.$events[name])) { + } else if (isArray(this.$events[name])) { this.$events[name].push(fn); } else { this.$events[name] = [this.$events[name], fn]; @@ -67,7 +67,7 @@ EventEmitter.prototype.removeListener = function (name, fn) { if (this.$events && this.$events[name]) { var list = this.$events[name]; - if (io.util.isArray(list)) { + if (isArray(list)) { var pos = -1; for (var i = 0, l = list.length; i < l; i++) { @@ -128,7 +128,7 @@ EventEmitter.prototype.listeners = function (name) { this.$events[name] = []; } - if (!io.util.isArray(this.$events[name])) { + if (!isArray(this.$events[name])) { this.$events[name] = [this.$events[name]]; } @@ -156,7 +156,7 @@ EventEmitter.prototype.emit = function (name) { if ('function' == typeof handler) { handler.apply(this, args); - } else if (io.util.isArray(handler)) { + } else if (isArray(handler)) { var listeners = handler.slice(); for (var i = 0, l = listeners.length; i < l; i++) { @@ -169,6 +169,17 @@ EventEmitter.prototype.emit = function (name) { return true; }; +/** + * Checks for Array type. + * + * @param {Object} object + * @api private + */ + +function isArray (obj) { + return '[object Array]' == Object.prototype.toString.call(obj); +}; + /** * Compatibility with WebSocket */ From cfcebef5a03320f9d274b99320ae0e3010d90dbe Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:12:48 -0800 Subject: [PATCH 0134/1403] Added support for both http/ws URIs in Socket constructor. --- lib/socket.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/socket.js b/lib/socket.js index 9eb1b4c7..d07f68c9 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -23,8 +23,9 @@ module.exports = Socket; function Socket (opts) { if ('string' == typeof opts) { var uri = util.parseUri(opts); + opts = arguments[1] || {}; opts.host = uri.host; - opts.secure = uri.scheme == 'wss' + opts.secure = uri.scheme == 'https' || uri.scheme == 'wss'; opts.port = uri.port || (opts.secure ? 443 : 80); } From 6b19d655ee21a6b7b6f72d43607378fe43a8b186 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:13:16 -0800 Subject: [PATCH 0135/1403] Make sure query object is cloned upon transport creation --- lib/socket.js | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/lib/socket.js b/lib/socket.js index d07f68c9..57000c30 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -33,7 +33,7 @@ function Socket (opts) { this.host = opts.host || opts.hostname || 'localhost'; this.port = opts.port || 80; - this.query = opts.query || ''; + this.query = opts.query || {}; this.upgrade = false !== opts.upgrade; this.path = opts.path || '/engine.io' this.forceJSONP = !!opts.forceJSONP; @@ -54,23 +54,17 @@ util.inherits(Socket, EventEmitter); * Creates transport of the given type. * * @param {String} transport name - * @param {Object} extra query parameters * @return {Transport} * @api private */ -Socket.prototype.createTransport = function (name, queryParams) { +Socket.prototype.createTransport = function (name) { // debug: creating transport "%s", name - var query = this.query || {} + var query = clone(this.query) query.transport = name; - // add extra query params, if any - if (queryParams) { - for (var i in queryParams) { - if (queryParams.hasOwnProperty(i)) { - query[i] = queryParams[i]; - } - } + if (this.sid) { + query.sid = this.sid; } var transport = new transports[name]({ @@ -85,6 +79,16 @@ Socket.prototype.createTransport = function (name, queryParams) { return transport; }; +function clone (obj) { + var o = {}; + for (var i in obj) { + if (obj.hasOwnProperty(i)) { + o[i] = obj[i]; + } + } + return o; +} + /** * Initializes transport to use and starts probe. * From 1cb04d41edfe7d9e815d198399d14c57b7ca08c7 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:13:34 -0800 Subject: [PATCH 0136/1403] Removed probes upon open --- lib/socket.js | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/lib/socket.js b/lib/socket.js index 57000c30..f7ebe805 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -96,30 +96,10 @@ function clone (obj) { */ Socket.prototype.open = function () { - var self = this; - this.readyState = 'opening'; - - // use the first transport always for the first try var transport = this.createTransport(this.transports[0]); transport.open(); - transport.once('open', function () { - this.setTransport(transport); - }); - - // if the engine is closed before transport opened, abort it - this.once('close', function () { - transport.close(); - }); - - // whether we should perform a probe - if (this.upgrade && this.transports.length > 1 && transport.pause) { - var probeTransports = this.transports.slice(1); - - for (var i = 0, l = probeTransports.length; i < l; i++) { - this.probe(probeTransports[i]); - } - } + this.setTransport(transport); }; /** From e442540a4525d684503a44b468ffea70acb23b48 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:14:09 -0800 Subject: [PATCH 0137/1403] Added error handling and closing reason to current socket transport --- lib/socket.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/socket.js b/lib/socket.js index f7ebe805..4e7dd0e0 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -121,15 +121,14 @@ Socket.prototype.setTransport = function (transport) { // set up transport listeners transport - // this already fired for an upgrade, so we don't have to worry - .on('open', function () { - self.onOpen(); - }) .on('packet', function (packet) { self.onPacket(packet); }) + .on('error', function (e) { + self.onError(e); + }) .on('close', function () { - self.onClose(); + self.onClose('transport close'); }) }; From 6fae597966656c8481527f3293ca464054c1a877 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:14:29 -0800 Subject: [PATCH 0138/1403] Make sure to flush upon upgrade completion --- lib/socket.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/socket.js b/lib/socket.js index 4e7dd0e0..acae2767 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -157,6 +157,7 @@ Socket.prototype.probe = function (name) { self.transport.pause(function () { self.setTransport(self.transport); self.upgrading = false; + self.flush(); self.emit('upgrade', name); }); } else { From e7520a6836bcaaa4eb94745045d8a7d3d8532d81 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:14:43 -0800 Subject: [PATCH 0139/1403] Moved upgrade probes start to onOpen handler --- lib/socket.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/socket.js b/lib/socket.js index acae2767..5d5dc149 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -196,6 +196,13 @@ Socket.prototype.onOpen = function () { this.emit('open'); this.onopen && this.onopen.call(this); this.flush(); + + if (this.upgrade && this.transport.pause) { + // debug: starting upgrade probes + for (var i = 0, l = this.upgrades.length; i < l; i++) { + this.probe(this.upgrades[i]); + } + } }; /** From 2ba029695f57c1183122aab7bf44025214535619 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:15:03 -0800 Subject: [PATCH 0140/1403] Added defense to onPacket handler --- lib/socket.js | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/lib/socket.js b/lib/socket.js index 5d5dc149..980633de 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -212,25 +212,31 @@ Socket.prototype.onOpen = function () { */ Socket.prototype.onPacket = function (packet) { - // debug: socket receive: type "%s" | data "%s", packet.type, packet.data - switch (packet.type) { - case 'noop': - break; + if ('opening' == this.readyState || 'open' == this.readyState) { + // debug: socket receive: type "%s" | data "%s", packet.type, packet.data + switch (packet.type) { + case 'open': + this.onHandshake(util.parseJSON(packet.data)); + break; - case 'ping': - this.sendPacket('pong'); - break; + case 'ping': + this.sendPacket('pong'); + this.setPingTimeout(); + break; - case 'error': - var err = new Error('server error'); - err.code = packet.data; - this.emit('error', err); - break; + case 'error': + var err = new Error('server error'); + err.code = packet.data; + this.emit('error', err); + break; - case 'message': - this.emit('message', packet.data); - this.onmessage && this.onmessage.call(this, packet.data); - break; + case 'message': + this.emit('message', packet.data); + this.onmessage && this.onmessage.call(this, packet.data); + break; + } + } else { + // debug: packet received with socket readyState "%s", this.readyState } }; From 59d33105789f8bd7c87c2ae4fcbfd0e67fbc5755 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:15:39 -0800 Subject: [PATCH 0141/1403] Added onHandshake --- lib/socket.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib/socket.js b/lib/socket.js index 980633de..5e8d7d7b 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -240,6 +240,24 @@ Socket.prototype.onPacket = function (packet) { } }; +/** + * Called upon handshake completion. + * + * @param {Object} handshake obj + * @api private + */ + +Socket.prototype.onHandshake = function (data) { + this.emit('handshake', data); + this.sid = data.sid; + this.transport.query.sid = data.sid; + this.upgrades = data.upgrades; + this.pingTimeout = data.pingTimeout; + this.pingInterval = data.pingInterval; + this.onOpen(); + this.setPingTimeout(); +}; + /** * Flush write buffers. * From 82742c3cb3f0d879009a228fe815c7bbb9edecf5 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:15:51 -0800 Subject: [PATCH 0142/1403] Added method to set ping timeout --- lib/socket.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/socket.js b/lib/socket.js index 5e8d7d7b..0ec08805 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -258,6 +258,20 @@ Socket.prototype.onHandshake = function (data) { this.setPingTimeout(); }; +/** + * Clears and sets a ping timeout based on the expected ping interval. + * + * @api private + */ + +Socket.prototype.setPingTimeout = function () { + clearTimeout(this.pingTimeoutTimer); + var self = this; + this.pingTimeoutTimer = setTimeout(function () { + self.onClose('ping timeout'); + }, this.pingInterval); +}; + /** * Flush write buffers. * From 3fed6df2afb17efd9f7b028e204795924da93406 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:16:07 -0800 Subject: [PATCH 0143/1403] Fixed Socket#close and added close reason --- lib/socket.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/socket.js b/lib/socket.js index 0ec08805..751dab46 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -334,12 +334,9 @@ Socket.prototype.sendPacket = function (type, data) { Socket.prototype.close = function () { if ('opening' == this.readyState || 'open' == this.readyState) { - if (this.transport) { - // debug: socket closing - telling transport to close - this.transport.close(); - } - - this.onClose(); + this.onClose('forced close'); + // debug: socket closing - telling transport to close + this.transport.close(); } return this; From d86a7661040e7fcdf77dbcd2d6492bcf55fc2ed2 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:16:25 -0800 Subject: [PATCH 0144/1403] Added Socket#onError --- lib/socket.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/socket.js b/lib/socket.js index 751dab46..66d0b358 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -342,6 +342,17 @@ Socket.prototype.close = function () { return this; }; +/** + * Called upon transport error + * + * @api private + */ + +Socket.prototype.onError = function (err) { + this.emit('error', err); + this.onClose('transport error', err); +}; + /** * Called upon transport close. * From 642b965d7b592af567d340c3412a01c26cda90f8 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:16:33 -0800 Subject: [PATCH 0145/1403] Added defense to Socket#onClose --- lib/socket.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/socket.js b/lib/socket.js index 66d0b358..ca3060f1 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -359,9 +359,11 @@ Socket.prototype.onError = function (err) { * @api private */ -Socket.prototype.onClose = function () { - // debug: socket close - this.readyState = 'closed'; - this.emit('close'); - this.onclose && this.onclose.call(this); +Socket.prototype.onClose = function (reason, desc) { + if ('closed' != this.readyState) { + // debug: socket close + this.readyState = 'closed'; + this.emit('close', reason, desc); + this.onclose && this.onclose.call(this); + } }; From 1c94ceec6032c4dd1219f7f9e00964f0f679fde1 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:16:59 -0800 Subject: [PATCH 0146/1403] Expose transport options as properties. --- lib/transport.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/transport.js b/lib/transport.js index 1998b2fd..0b64b76b 100644 --- a/lib/transport.js +++ b/lib/transport.js @@ -21,7 +21,11 @@ module.exports = Transport; */ function Transport (opts) { - this.options = opts; + this.path = opts.path; + this.host = opts.host; + this.port = opts.port; + this.secure = opts.secure; + this.query = opts.query; this.readyState = ''; this.writeBuffer = []; }; From 73026b60e248986c8eb612c2598afd5e8c68527d Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:17:14 -0800 Subject: [PATCH 0147/1403] Improved Transport#onError --- lib/transport.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/transport.js b/lib/transport.js index 0b64b76b..a22f06d4 100644 --- a/lib/transport.js +++ b/lib/transport.js @@ -52,10 +52,11 @@ Transport.prototype.buffer = true; * @api public */ -Transport.prototype.error = function (code) { - var err = new Error('transport error'); - err.code = code; - this.emit('error', code); +Transport.prototype.onError = function (msg, desc) { + var err = new Error(msg); + err.type = 'TransportError'; + err.description = desc; + this.emit('error', err); return this; }; From f5e841cbdc3e87f7ad1f1b061cf29a6b569d50c3 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:17:29 -0800 Subject: [PATCH 0148/1403] Fixed style and throw instead of silencing the error in Transport#send --- lib/transport.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/transport.js b/lib/transport.js index a22f06d4..d3cbf414 100644 --- a/lib/transport.js +++ b/lib/transport.js @@ -98,11 +98,9 @@ Transport.prototype.close = function () { */ Transport.prototype.send = function (packet) { - if (this.readyState != 'open') { - this.error('not open'); - } else { + if ('open' == this.readyState) { if (this.buffer) { - this.writeBuffer.push(data); + this.writeBuffer.push(packet); } else { var self = this; this.buffer = true; @@ -110,6 +108,8 @@ Transport.prototype.send = function (packet) { self.flush(); }); } + } else { + throw new Error('Transport not open'); } return this; From f1666cbd99d735ac923fa0e5a65ae39147524838 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:18:00 -0800 Subject: [PATCH 0149/1403] Fixed flashsocket check --- lib/transports/flashsocket.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/transports/flashsocket.js b/lib/transports/flashsocket.js index 591b5bff..7f514825 100644 --- a/lib/transports/flashsocket.js +++ b/lib/transports/flashsocket.js @@ -49,7 +49,7 @@ FlashWS.prototype.name = 'flashsocket'; */ FlashWS.prototype.doOpen = function () { - if (!check) { + if (!check()) { // let the probe timeout return; } From cb05bc80f67eb08646c9f58c7e7676d924250bcf Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:18:13 -0800 Subject: [PATCH 0150/1403] Refactored transports index deps --- lib/transports/index.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/transports/index.js b/lib/transports/index.js index 8de0155b..b09bd3f6 100644 --- a/lib/transports/index.js +++ b/lib/transports/index.js @@ -1,4 +1,14 @@ +/** + * Module dependencies + */ + +var XHR = require('./polling-xhr') + , JSONP = require('./polling-jsonp') + , websocket = require('./websocket') + , flashsocket = require('./flashsocket') + , util = require('../util') + /** * Export transports. */ From 845f71224ef086c29f4172ca3f5741184c031157 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:18:30 -0800 Subject: [PATCH 0151/1403] Moved polling polimorphic constructor to transports index --- lib/transports/index.js | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/lib/transports/index.js b/lib/transports/index.js index b09bd3f6..ec70c0d0 100644 --- a/lib/transports/index.js +++ b/lib/transports/index.js @@ -13,6 +13,28 @@ var XHR = require('./polling-xhr') * Export transports. */ -exports.polling = require('./polling'); -exports.websocket = require('./websocket'); -exports.flashsocket = require('./flashsocket'); +exports.polling = polling; +exports.websocket = websocket; +exports.flashsocket = flashsocket; + +/** + * Polling transport polymorphic constructor. + * Decides on xhr vs jsonp based on feature detection. + * + * @api private + */ + +function polling (opts) { + var xd = false; + + if (global.location) { + xd = opts.host != global.location.hostname + || global.location.port != opts.port; + } + + if (util.request(xd) && !opts.forceJSONP) { + return new XHR(opts); + } else { + return new JSONP(opts); + } +}; From 4d998e0f8b821b366434c86217989ec3e052e98b Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:19:57 -0800 Subject: [PATCH 0152/1403] Simplified JSONP: removed setIndex and leveraged `uri()` --- lib/transports/polling-jsonp.js | 53 ++++++++------------------------- 1 file changed, 13 insertions(+), 40 deletions(-) diff --git a/lib/transports/polling-jsonp.js b/lib/transports/polling-jsonp.js index 5a96aafb..255bd084 100644 --- a/lib/transports/polling-jsonp.js +++ b/lib/transports/polling-jsonp.js @@ -3,8 +3,7 @@ * Module requirements. */ -var Transport = require('../transport') - , Polling = require('./polling') +var Polling = require('./polling') , util = require('../util') /** @@ -34,7 +33,15 @@ function empty () { } function JSONPPolling (opts) { Transport.call(this, opts); - this.setIndex(); + + // add callback to jsonp global + var self = this; + eio.j.push(function (msg) { + self.onData(msg); + }); + + // append to query string + this.query.j = eio.j.length - 1; }; /** @@ -43,33 +50,6 @@ function JSONPPolling (opts) { util.inherits(JSONPPolling, Polling); -/** - * Transport name. - * - * @api public - */ - -JSONPPolling.prototype.name = 'polling-jsonp'; - -/** - * Sets JSONP global callback. - * - * @api private - */ - -JSONPPolling.prototype.setIndex = function () { - // if we have an index already, set it to empy - if (undefined != this.index) { - eio.j[this.index] = empty; - } - - var self = this; - this.index = eio.j.length; - eio.j.push(function (msg) { - self.onData(msg); - }); -}; - /** * Opens the socket. * @@ -90,8 +70,6 @@ JSONPPolling.prototype.doOpen = function () { */ JSONPPolling.prototype.doClose = function () { - this.setIndex(); - if (this.script) { this.script.parentNode.removeChild(this.script); this.script = null; @@ -112,10 +90,7 @@ JSONPPolling.prototype.doClose = function () { */ JSONPPolling.prototype.doPoll = function () { - var self = this - , script = document.createElement('script') - , query = this.options.query + (this.options.query ? '&' : '') - + 't=' + (+new Date) + '&i=' + this.index + var script = document.createElement('script'); if (this.script) { this.script.parentNode.removeChild(this.script); @@ -123,7 +98,7 @@ JSONPPolling.prototype.doPoll = function () { } script.async = true; - script.src = this.prepareUrl() + query; + script.src = this.uri(); var insertAt = document.getElementsByTagName('script')[0] insertAt.parentNode.insertBefore(script, insertAt); @@ -148,8 +123,6 @@ JSONPPolling.prototype.doPoll = function () { JSONPPolling.prototype.write = function (data, fn) { var self = this - , query = this.options.query + (this.options.query ? '&' : '') - + 't=' + (+new Date) + '&i=' + this.index if (!this.form) { var form = document.createElement('form') @@ -172,7 +145,7 @@ JSONPPolling.prototype.write = function (data, fn) { this.area = area; } - this.form.action = this.prepareUrl() + query; + this.form.action = this.uri(); function complete () { initIframe(); From 470a2e50cc101f4ad46e59047349ab9926f9e221 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:20:20 -0800 Subject: [PATCH 0153/1403] Added cache busting for XHR uri and fixed style --- lib/transports/polling-xhr.js | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/lib/transports/polling-xhr.js b/lib/transports/polling-xhr.js index 125186be..02e2f288 100644 --- a/lib/transports/polling-xhr.js +++ b/lib/transports/polling-xhr.js @@ -3,8 +3,7 @@ * Module requirements. */ -var Transport = require('../transport') - , Polling = require('./polling') +var Polling = require('./polling') , EventEmitter = require('../event-emitter') , util = require('../util') , global = this @@ -13,7 +12,7 @@ var Transport = require('../transport') * Module exports. */ -module.exports = XHRPolling; +module.exports = XHR; module.exports.Request = Request; /** @@ -25,32 +24,29 @@ function empty () { } /** * XHR Polling constructor. * - * @param {Object} opts. + * @param {Object} opts * @api public */ -function XHRPolling (opts) { - Transport.call(this, opts); +function XHR (opts) { + Polling.call(this, opts); if (global.location) { this.xd = opts.host != global.location.hostname || global.location.port != opts.port; } + + // cache busting for IE + if (global.ActiveXObject) { + this.query.t = +new Date; + } }; /** * Inherits from Polling. */ -util.inherits(XHRPolling, Polling); - -/** - * Transport name. - * - * @api public - */ - -XHRPolling.prototype.name = 'polling-xhr'; +util.inherits(XHR, Polling); /** * Opens the socket From 0f47a813ad5cb66be8dba2f6be645b79039e68fb Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:20:40 -0800 Subject: [PATCH 0154/1403] Fixed XHR#doOpen --- lib/transports/polling-xhr.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/transports/polling-xhr.js b/lib/transports/polling-xhr.js index 02e2f288..f453d953 100644 --- a/lib/transports/polling-xhr.js +++ b/lib/transports/polling-xhr.js @@ -54,10 +54,10 @@ util.inherits(XHR, Polling); * @api private */ -XHRPolling.prototype.doOpen = function () { +XHR.prototype.doOpen = function () { var self = this; util.defer(function () { - Polling.prototype.open.call(self); + Polling.prototype.doOpen.call(self); }); }; From 1c93b30de9016678452471b9f268ccb18bd6aec6 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:20:54 -0800 Subject: [PATCH 0155/1403] Removed error handling from XHR#request to personalize the error --- lib/transports/polling-xhr.js | 27 +++------------------------ 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/lib/transports/polling-xhr.js b/lib/transports/polling-xhr.js index f453d953..fb9f4106 100644 --- a/lib/transports/polling-xhr.js +++ b/lib/transports/polling-xhr.js @@ -61,24 +61,6 @@ XHR.prototype.doOpen = function () { }); }; -/** - * Closes the socket. - * - * @api private - */ - -XHRPolling.prototype.doClose = function () { - if (this.pollXhr) { - this.pollXhr.abort(); - } - - if (this.sendXhr) { - this.sendXhr.abort(); - } - - Polling.prototype.doClose.call(this); -}; - /** * Creates a request. * @@ -86,14 +68,11 @@ XHRPolling.prototype.doClose = function () { * @api private */ -XHRPolling.prototype.request = function (opts) { +XHR.prototype.request = function (opts) { + opts = opts || {}; opts.uri = this.uri(); opts.xd = this.xd; - var req = new Request(opts); - req.on('error', function () { - self.error('connection error'); - }); - return req; + return new Request(opts); }; /** From c175b39d8d5478a85a0e32c27112dd845d5037b9 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:21:18 -0800 Subject: [PATCH 0156/1403] Added separate error handling to #write and #doPoll --- lib/transports/polling-xhr.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/transports/polling-xhr.js b/lib/transports/polling-xhr.js index fb9f4106..de68fd8b 100644 --- a/lib/transports/polling-xhr.js +++ b/lib/transports/polling-xhr.js @@ -83,9 +83,13 @@ XHR.prototype.request = function (opts) { * @api private */ -XHRPolling.prototype.write = function (data, fn) { +XHR.prototype.write = function (data, fn) { var req = this.request({ method: 'POST', data: data }) + , self = this req.on('success', fn); + req.on('error', function (err) { + self.onError('xhr post error', err); + }); this.sendXhr = req; }; @@ -95,8 +99,17 @@ XHRPolling.prototype.write = function (data, fn) { * @api private */ -XHRPolling.prototype.doPoll = function () { - this.pollXhr = this.request(); +XHR.prototype.doPoll = function () { + // debug: xhr poll + var req = this.request() + , self = this + req.on('data', function (data) { + self.onData(data); + }); + req.on('error', function (err) { + self.onError('xhr poll error', err); + }); + this.pollXhr = req; }; /** From 122d323634307a591fa41e278331076e53b35da4 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:21:58 -0800 Subject: [PATCH 0157/1403] Fixed style --- lib/transports/polling-xhr.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/transports/polling-xhr.js b/lib/transports/polling-xhr.js index de68fd8b..7a8a34bf 100644 --- a/lib/transports/polling-xhr.js +++ b/lib/transports/polling-xhr.js @@ -141,8 +141,10 @@ util.inherits(Request, EventEmitter); */ Request.prototype.create = function () { - var xhr = this.xhr = util.request(this.xd); - this.xhr.open(this.method, this.uri, this.async); + var xhr = this.xhr = util.request(this.xd) + , self = this + + xhr.open(this.method, this.uri, this.async); if ('POST' == this.method) { try { @@ -156,9 +158,9 @@ Request.prototype.create = function () { } catch (e) {} } - if (this.xd && global.XDomainRequest && this.xhr instanceof XDomainRequest) { - this.xhr.onerror = function () { - self.onError(); + if (this.xd && global.XDomainRequest && xhr instanceof XDomainRequest) { + xhr.onerror = function (e) { + self.onError(e); }; this.xhr.onload = function () { self.onData(xhr.responseText); From 4b3d525e6086ce7771aeaa4fb670fde887112de5 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:22:15 -0800 Subject: [PATCH 0158/1403] Fixed style --- lib/transports/polling-xhr.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/transports/polling-xhr.js b/lib/transports/polling-xhr.js index 7a8a34bf..8568faab 100644 --- a/lib/transports/polling-xhr.js +++ b/lib/transports/polling-xhr.js @@ -162,10 +162,10 @@ Request.prototype.create = function () { xhr.onerror = function (e) { self.onError(e); }; - this.xhr.onload = function () { + xhr.onload = function () { self.onData(xhr.responseText); }; - this.xhr.onprogress = empty; + xhr.onprogress = empty; } else { this.xhr.withCredentials = true; this.xhr.onreadystatechange = function () { From fd6f99d9cc18f68c7e0ff9273252bf96241dd47b Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:22:26 -0800 Subject: [PATCH 0159/1403] Added `withCredentials` for CORS cookie support, improved error hadnling and ensured that we call onData outside of the try/catch --- lib/transports/polling-xhr.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/transports/polling-xhr.js b/lib/transports/polling-xhr.js index 8568faab..0deaa1bc 100644 --- a/lib/transports/polling-xhr.js +++ b/lib/transports/polling-xhr.js @@ -167,18 +167,26 @@ Request.prototype.create = function () { }; xhr.onprogress = empty; } else { - this.xhr.withCredentials = true; - this.xhr.onreadystatechange = function () { + xhr.withCredentials = true; + xhr.onreadystatechange = function () { + var data; + try { if (xhr.readyState != 4) return; - if (200 == xhr.status) { - self.onData(xhr.responseText); + data = xhr.responseText; } else { - self.onError(); + var err = new Error; + err.code = xhr.status; + err.type = 'StatusError'; + self.onError(err); } } catch (e) { - self.onError(); + self.onError(e); + } + + if (undefined != data) { + self.onData(data); } }; } From 72236e944d3acd40c712bae7505da8d5e5bca1cc Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:23:04 -0800 Subject: [PATCH 0160/1403] Added debugging to Request --- lib/transports/polling-xhr.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/transports/polling-xhr.js b/lib/transports/polling-xhr.js index 0deaa1bc..25b79a5a 100644 --- a/lib/transports/polling-xhr.js +++ b/lib/transports/polling-xhr.js @@ -191,7 +191,8 @@ Request.prototype.create = function () { }; } - this.xhr.send(this.data); + // debug: sending xhr with url %s | data %s, this.uri, this.data + xhr.send(this.data); if (global.ActiveXObject) { this.index = Request.requestsCount++; From dab713aafe15f35456713bde346646629ef2c846 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:23:15 -0800 Subject: [PATCH 0161/1403] Make sure to conserve the error on Request#onError --- lib/transports/polling-xhr.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/transports/polling-xhr.js b/lib/transports/polling-xhr.js index 25b79a5a..c80beec2 100644 --- a/lib/transports/polling-xhr.js +++ b/lib/transports/polling-xhr.js @@ -228,8 +228,8 @@ Request.prototype.onData = function (data) { * @api private */ -Request.prototype.onError = function () { - this.emit('error'); +Request.prototype.onError = function (err) { + this.emit('error', err); this.cleanup(); } From 857e58a953b12196e6baef195f2ad7953d7d4a61 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:23:36 -0800 Subject: [PATCH 0162/1403] Clean up polling transport --- lib/transports/polling.js | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/lib/transports/polling.js b/lib/transports/polling.js index 2b636325..bb07fa2e 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -17,28 +17,15 @@ var Transport = require('../transport') module.exports = Polling; /** - * Polling transport polymorphic constructor. - * Decides on xhr vs jsonp based on feature detection. + * Polling interface. * - * @api public + * @param {Object} opts + * @api private */ function Polling (opts) { - var xd; - - if (global.location) { - xd = opts.host != global.location.hostname - || global.location.port != opts.port; - } - - var xhr = request(xd); - - if (xhr && !opts.forceJSONP) { - return new XHR; - } else { - return new JSONP; - } -}; + Transport.call(this, opts); +} /** * Inherits from Transport. @@ -46,6 +33,12 @@ function Polling (opts) { util.inherits(Polling, Transport); +/** + * Transport name. + */ + +Polling.prototype.name = 'polling'; + /** * Opens the socket (triggers polling). We write a PING message to determine * when the transport is open. @@ -55,7 +48,6 @@ util.inherits(Polling, Transport); Polling.prototype.doOpen = function () { this.poll(); - this.write({ type: 'ping' }); }; /** From 21a552e7b67d22ecca825ab203fdb07a6ccd7efa Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:24:55 -0800 Subject: [PATCH 0163/1403] Fixed Polling data handler --- lib/transports/polling.js | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/lib/transports/polling.js b/lib/transports/polling.js index bb07fa2e..cdbabb93 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -98,25 +98,20 @@ Polling.prototype.poll = function () { */ Polling.prototype.onData = function (data) { + // if we got data we're not polling + this.polling = false; + + // decode payload var packets = parser.decodePayload(data); for (var i = 0, l = packets.length; i < l; i++) { - // if its a pong to our initial ping we consider the connection open + // if its the first message we consider the trnasport open if ('opening' == this.readyState) { - if ('pong' == packets[i].type) { - this.onOpen(); - continue; - } else { - this.error('protocol violation'); - return; - } + this.onOpen(); } // if its a close packet, we close the ongoing requests if ('close' == packets[i].type) { - // doClose will cancel existing requests, and then fire a close packet - // to signal the server we're closed, since we can't kill the socket/s - this.doClose(); this.onClose(); return; } @@ -125,11 +120,10 @@ Polling.prototype.onData = function (data) { this.onPacket(packets[i]); } - // if we got data we're not polling - this.polling = false; - - // trigger next poll - this.poll(); + if ('open' == this.readyState) { + // trigger next poll + this.poll(); + } }; /** From 7b49cdbedbf1acfe9caf42ab5c31e00a43b15d1f Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:25:07 -0800 Subject: [PATCH 0164/1403] Fixed doClose --- lib/transports/polling.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/transports/polling.js b/lib/transports/polling.js index cdbabb93..9bd74fbd 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -133,8 +133,7 @@ Polling.prototype.onData = function (data) { */ Polling.prototype.doClose = function () { - // we mute existing buffers and bypass buffers - this.write({ type: 'close' }); + this.send({ type: 'close' }); }; /** From 1884aa04bbfda37f3957c22e5ed3e276793d9ce8 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:25:16 -0800 Subject: [PATCH 0165/1403] Fixed polling uri builder --- lib/transports/polling.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/transports/polling.js b/lib/transports/polling.js index 9bd74fbd..e4ce324d 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -165,15 +165,14 @@ Polling.prototype.write = function (packet) { */ Polling.prototype.uri = function () { - var opts = this.options - , query = util.qs(opts.query) - , schema = opts.secure ? 'https' : 'http' + var query = util.qs(this.query) + , schema = this.secure ? 'https' : 'http' , port = '' // avoid port if default for schema - if (('https' == schema && opts.port != 443) - || ('http' == schema && opts.port != 80)) { - port = ':' + opts.port; + if (this.port && (('https' == schema && this.port != 443) + || ('http' == schema && this.port != 80))) { + port = ':' + this.port; } // prepend ? to query @@ -181,5 +180,5 @@ Polling.prototype.uri = function () { query = '?' + query; } - return schema + opts.host + port + opts.path + query; + return schema + '://' + this.host + port + this.path + query; }; From 32fdf4949853ebe78c63eabfcf1f7f4f261d661f Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:25:29 -0800 Subject: [PATCH 0166/1403] FIxed websocket constructor. --- lib/transports/websocket.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/transports/websocket.js b/lib/transports/websocket.js index 54362005..d99780f2 100644 --- a/lib/transports/websocket.js +++ b/lib/transports/websocket.js @@ -50,16 +50,21 @@ WS.prototype.doOpen = function () { return; } - this.socket = new ws()(this.uri); + var self = this; + + this.socket = new (ws())(this.uri()); this.socket.onopen = function () { self.onOpen(); }; this.socket.onclose = function () { self.onClose(); }; - this.socket.ondata = function (ev) { + this.socket.onmessage = function (ev) { self.onData(ev.data); }; + this.socket.onerror = function (e) { + self.onError('websocket error', e); + }; }; /** From 7641beecb533e9c8d808fc0d55ac51ef5ef5bc08 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:25:42 -0800 Subject: [PATCH 0167/1403] Fixed websocket uri --- lib/transports/websocket.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/transports/websocket.js b/lib/transports/websocket.js index d99780f2..8e3fcd9d 100644 --- a/lib/transports/websocket.js +++ b/lib/transports/websocket.js @@ -110,15 +110,14 @@ WS.prototype.doClose = function () { */ WS.prototype.uri = function () { - var opts = this.options - , query = util.qs(opts.query) - , schema = opts.secure ? 'wss' : 'ws' + var query = util.qs(this.query) + , schema = this.secure ? 'wss' : 'ws' , port = '' // avoid port if default for schema - if (('wss' == schema && opts.port != 443) - || ('ws' == schema && opts.port != 80)) { - port = ':' + opts.port; + if (this.port && (('wss' == schema && this.port != 443) + || ('ws' == schema && this.port != 80))) { + port = ':' + this.port; } // prepend ? to query @@ -126,7 +125,7 @@ WS.prototype.uri = function () { query = '?' + query; } - return schema + opts.host + port + opts.path + query; + return schema + '://' + this.host + port + this.path + query; }; /** From 2ef709b28492dfe8ec7bc5d774ff89b491104e42 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:25:56 -0800 Subject: [PATCH 0168/1403] Added back load/defer utils from socket.io Added JSON parse from jquery --- lib/util.js | 112 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 103 insertions(+), 9 deletions(-) diff --git a/lib/util.js b/lib/util.js index c83b009b..0d7c093f 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1,24 +1,118 @@ /** - * engine.io-client - * Copyright(c) 2011 LearnBoost - * MIT Licensed + * Reference to global object. */ +var global = this; + +/** + * Status of page load. + */ + +var pageLoaded = false; + /** * Inheritance. * * @param {Function} ctor a * @param {Function} ctor b - * @api public + * @api private */ exports.inherits = function inherits (a, b) { function c () { } c.prototype = b.prototype; a.prototype = new c; +}; + +/** + * Load utility. + * + * @api private + */ + +exports.load = function (fn) { + if (global.document && document.readyState === 'complete' || pageLoaded) { + return fn(); + } + + exports.on(global, 'load', fn, false); +}; + +/** + * Change the internal pageLoaded value. + */ + +if ('undefined' != typeof window) { + exports.load(function () { + pageLoaded = true; + }); } +/** + * Adds an event. + * + * @api private + */ + +exports.on = function (element, event, fn, capture) { + if (element.attachEvent) { + element.attachEvent('on' + event, fn); + } else if (element.addEventListener) { + element.addEventListener(event, fn, capture); + } +}; + +/** + * Defers a function to ensure a spinner is not displayed by the browser. + * + * @param {Function} fn + * @api private + */ + +exports.defer = function (fn) { + if (!exports.ua.webkit || 'undefined' != typeof importScripts) { + return fn(); + } + + exports.load(function () { + setTimeout(fn, 100); + }); +}; + +/** + * JSON parse. + * + * @see Based on jQuery#parseJSON (MIT) and JSON2 + * @api private + */ + +var rvalidchars = /^[\],:{}\s]*$/ + , rvalidescape = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g + , rvalidtokens = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g + , rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g + , rtrimLeft = /^\s+/ + , rtrimRight = /\s+$/ + +exports.parseJSON = function (data) { + if ('string' != typeof data || !data) { + return null; + } + + data = data.replace(rtrimLeft, '').replace(rtrimRight, ''); + + // Attempt to parse using the native JSON parser first + if (global.JSON && JSON.parse) { + return JSON.parse(data); + } + + if (rvalidchars.test(data.replace(rvalidescape, '@') + .replace(rvalidtokens, ']') + .replace(rvalidbraces, ''))) { + return (new Function('return ' + data))(); + } +}; + /** * UA / engines detection namespace. * @@ -30,7 +124,7 @@ exports.ua = {}; /** * Whether the UA supports CORS for XHR. * - * @api public + * @api private */ exports.ua.hasCORS = 'undefined' != typeof XMLHttpRequest && (function () { @@ -46,7 +140,7 @@ exports.ua.hasCORS = 'undefined' != typeof XMLHttpRequest && (function () { /** * Detect webkit. * - * @api public + * @api private */ exports.ua.webkit = 'undefined' != typeof navigator && @@ -55,7 +149,7 @@ exports.ua.webkit = 'undefined' != typeof navigator && /** * Detect gecko. * - * @api public + * @api private */ exports.ua.gecko = 'undefined' != typeof navigator && @@ -96,7 +190,7 @@ exports.request = function request (xdomain) { * Parses an URI * * @author Steven Levithan (MIT license) - * @api public + * @api private */ var re = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/; @@ -122,7 +216,7 @@ exports.parseUri = function (str) { * Compiles a querystring * * @param {Object} - * @api public + * @api private */ exports.qs = function (obj) { From ea69aee16abe46a66de874749dfea30d321f510b Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:26:12 -0800 Subject: [PATCH 0169/1403] Bumped ws --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fe7ee727..94bc85c5 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ , "main": "./lib/engine.io-client" , "version": "0.1.0" , "dependencies": { - "ws": "0.3.8" + "ws": "0.4.0" , "xmlhttprequest": "1.3.0" } , "devDependencies": { From 249fb89c492273d601505287cfa3c035a35d6186 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jan 2012 13:26:29 -0800 Subject: [PATCH 0170/1403] Added transport unit tests --- test/transport.js | 82 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 test/transport.js diff --git a/test/transport.js b/test/transport.js new file mode 100644 index 00000000..e38395b4 --- /dev/null +++ b/test/transport.js @@ -0,0 +1,82 @@ + +describe('Transport', function () { + + describe('public constructors', function () { + it('should include Transport', function () { + expect(eio.Transport).to.be.a('function'); + }); + + it('should include Polling, WebSocket and FlashSocket', function () { + expect(eio.transports).to.be.an('object'); + expect(eio.transports.polling).to.be.a('function'); + expect(eio.transports.websocket).to.be.a('function'); + expect(eio.transports.flashsocket).to.be.a('function'); + }); + }); + + describe('transport uris', function () { + it('should generate an http uri', function () { + var polling = new eio.transports.polling({ + path: '/engine.io' + , host: 'localhost' + , secure: false + , query: { sid: 'test' } + }); + expect(polling.uri()).to.be('http://localhost/engine.io?sid=test'); + }); + + it('should generate an http uri w/o a port', function () { + var polling = new eio.transports.polling({ + path: '/engine.io' + , host: 'localhost' + , secure: false + , query: { sid: 'test' } + , port: 80 + }); + expect(polling.uri()).to.be('http://localhost/engine.io?sid=test'); + }); + + it('should generate an http uri with a port', function () { + var polling = new eio.transports.polling({ + path: '/engine.io' + , host: 'localhost' + , secure: false + , query: { sid: 'test' } + , port: 3000 + }); + expect(polling.uri()).to.be('http://localhost:3000/engine.io?sid=test'); + }); + + it('should generate an https uri w/o a port', function () { + var polling = new eio.transports.polling({ + path: '/engine.io' + , host: 'localhost' + , secure: true + , query: { sid: 'test' } + , port: 443 + }); + expect(polling.uri()).to.be('https://localhost/engine.io?sid=test'); + }); + + it('should generate a ws uri', function () { + var ws = new eio.transports.websocket({ + path: '/engine.io' + , host: 'test' + , secure: false + , query: { transport: 'websocket' } + }); + expect(ws.uri()).to.be('ws://test/engine.io?transport=websocket'); + }); + + it('should generate a wss uri', function () { + var ws = new eio.transports.websocket({ + path: '/engine.io' + , host: 'test' + , secure: true + , query: {} + }); + expect(ws.uri()).to.be('wss://test/engine.io'); + }); + }); + +}); From d212ab33366d3b0c737f6a66d218117946b8051a Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Wed, 4 Jan 2012 13:07:52 -0800 Subject: [PATCH 0171/1403] Added instrumentation --- lib/transport.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/transport.js b/lib/transport.js index d3cbf414..1d0ba136 100644 --- a/lib/transport.js +++ b/lib/transport.js @@ -100,6 +100,7 @@ Transport.prototype.close = function () { Transport.prototype.send = function (packet) { if ('open' == this.readyState) { if (this.buffer) { + // debug: buffering packet this.writeBuffer.push(packet); } else { var self = this; From 6766dc09aaa92a61a924302e83e54238b9225e19 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Wed, 4 Jan 2012 13:07:59 -0800 Subject: [PATCH 0172/1403] Pass packet directly - give more flexibility to transport. --- lib/transport.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/transport.js b/lib/transport.js index 1d0ba136..17feed31 100644 --- a/lib/transport.js +++ b/lib/transport.js @@ -105,7 +105,7 @@ Transport.prototype.send = function (packet) { } else { var self = this; this.buffer = true; - this.write(parser.encodePacket(data), function () { + this.write(packet, function () { self.flush(); }); } From 417b2135763954963f5d2b36e61bfbe6e708c3e2 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Wed, 4 Jan 2012 13:08:11 -0800 Subject: [PATCH 0173/1403] Reworked flush logic. --- lib/transport.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/transport.js b/lib/transport.js index 17feed31..b2ecd0f9 100644 --- a/lib/transport.js +++ b/lib/transport.js @@ -123,12 +123,19 @@ Transport.prototype.send = function (packet) { */ Transport.prototype.flush = function () { - this.buffer = false; - this.emit('flush'); + var self = this; + + function onFlush () { + // debug: transport flushed + self.buffer = false; + self.emit('flush'); + }; if (this.writeBuffer.length) { - this.writeMany(self.writeBuffer); + this.writeMany(self.writeBuffer, onFlush); this.writeBuffer = []; + } else { + onFlush(); } return this; From 187df309b8acf05a0918e702a09d906d26f063cc Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Wed, 4 Jan 2012 13:08:24 -0800 Subject: [PATCH 0174/1403] Flush buffer upon open. --- lib/transport.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/transport.js b/lib/transport.js index b2ecd0f9..25b107f0 100644 --- a/lib/transport.js +++ b/lib/transport.js @@ -149,6 +149,7 @@ Transport.prototype.flush = function () { Transport.prototype.onOpen = function () { this.readyState = 'open'; + this.flush(); this.emit('open'); }; From 70a663ae9f21ffc144d3a26bf9ac8b2c974c5c7c Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Wed, 4 Jan 2012 13:08:46 -0800 Subject: [PATCH 0175/1403] Fixed Transport#doWrite implementations --- lib/transports/polling-jsonp.js | 2 +- lib/transports/polling-xhr.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/transports/polling-jsonp.js b/lib/transports/polling-jsonp.js index 255bd084..30ae7a28 100644 --- a/lib/transports/polling-jsonp.js +++ b/lib/transports/polling-jsonp.js @@ -121,7 +121,7 @@ JSONPPolling.prototype.doPoll = function () { * @api private */ -JSONPPolling.prototype.write = function (data, fn) { +JSONPPolling.prototype.doWrite = function (data, fn) { var self = this if (!this.form) { diff --git a/lib/transports/polling-xhr.js b/lib/transports/polling-xhr.js index c80beec2..7ecb318c 100644 --- a/lib/transports/polling-xhr.js +++ b/lib/transports/polling-xhr.js @@ -83,7 +83,7 @@ XHR.prototype.request = function (opts) { * @api private */ -XHR.prototype.write = function (data, fn) { +XHR.prototype.doWrite = function (data, fn) { var req = this.request({ method: 'POST', data: data }) , self = this req.on('success', fn); From a22006fc558924aeab315141fa993a2deb5ad04c Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Wed, 4 Jan 2012 13:09:04 -0800 Subject: [PATCH 0176/1403] Made the parser tests a little more realistic. --- test/parser.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/parser.js b/test/parser.js index 2dd32d44..6df3e65a 100644 --- a/test/parser.js +++ b/test/parser.js @@ -38,13 +38,13 @@ describe('parser', function () { }); it('should encode an open packet', function () { - expect(decode(encode({ type: 'open', data: '1' }))) - .to.eql({ type: 'open', data: '1' }); + expect(decode(encode({ type: 'open', data: '{"some":"json"}' }))) + .to.eql({ type: 'open', data: '{"some":"json"}' }); }); it('should encode a close packet', function () { - expect(decode(encode({ type: 'close', data: '1' }))) - .to.eql({ type: 'close', data: '1' }); + expect(decode(encode({ type: 'close' }))) + .to.eql({ type: 'close' }); }); it('should encode a ping packet', function () { From bbf2819b0e038cd24f391d977337781d0590ba13 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Wed, 4 Jan 2012 13:15:02 -0800 Subject: [PATCH 0177/1403] Added more instrumentation --- lib/transports/polling.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/transports/polling.js b/lib/transports/polling.js index e4ce324d..c211af18 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -133,6 +133,7 @@ Polling.prototype.onData = function (data) { */ Polling.prototype.doClose = function () { + // debug: sending close packet this.send({ type: 'close' }); }; From 421e152b6b1039ea76ccdfddf806573c9d2d2fce Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Wed, 4 Jan 2012 13:17:41 -0800 Subject: [PATCH 0178/1403] Fixed polling write and writeMany --- lib/transports/polling.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/transports/polling.js b/lib/transports/polling.js index c211af18..0f33bcbf 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -141,22 +141,24 @@ Polling.prototype.doClose = function () { * Writes a packets payload. * * @param {Array} data packets + * @param {Function} drain callback * @api private */ -Polling.prototype.writeMany = function (packets) { - this.doWrite(parser.encodePayload(packets)); +Polling.prototype.writeMany = function (packets, fn) { + this.doWrite(parser.encodePayload(packets), fn); }; /** * Writes a single-packet payload. * - * @parma {Object} data packet + * @param {Object} data packet + * @param {Function} drain callback * @api private */ -Polling.prototype.write = function (packet) { - this.writeMany([packet]); +Polling.prototype.write = function (packet, fn) { + this.writeMany([packet], fn); }; /** From 4c7d127557779709a37dc5184f864b62bb7ec948 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Wed, 4 Jan 2012 13:17:58 -0800 Subject: [PATCH 0179/1403] Fixed WS write and writeMany --- lib/transports/websocket.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/transports/websocket.js b/lib/transports/websocket.js index 8e3fcd9d..3ec3a20e 100644 --- a/lib/transports/websocket.js +++ b/lib/transports/websocket.js @@ -4,6 +4,7 @@ */ var Transport = require('../transport') + , parser = require('../parser') , util = require('../util') , global = this @@ -71,12 +72,12 @@ WS.prototype.doOpen = function () { * Writes data to socket. * * @param {String} data. - * @param {Function} flush callback. + * @param {Function} drain callback. * @api private */ -WS.prototype.write = function (data, fn) { - this.socket.send(data); +WS.prototype.write = function (packet, fn) { + this.socket.send(parser.encodePacket(packet)); fn(); }; @@ -87,10 +88,11 @@ WS.prototype.write = function (data, fn) { * @api private */ -WS.prototype.writeMany = function (packets) { +WS.prototype.writeMany = function (packets, fn) { for (var i = 0, l = packets.length; i < l; i++) { this.write(packets[0]); } + fn(); }; /** From 364408d0eef8271e1a35b9dd6f7402bce8bfcff4 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Wed, 4 Jan 2012 16:06:21 -0800 Subject: [PATCH 0180/1403] Implemented a public sendMany. I hate public props. --- lib/socket.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/socket.js b/lib/socket.js index ca3060f1..2617ae18 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -281,16 +281,8 @@ Socket.prototype.setPingTimeout = function () { Socket.prototype.flush = function () { if (this.writeBuffer.length) { // debug: flushing %d packets in socket, this.writeBuffer.length - - // make sure to transfer the buffer to the transport - this.transport.buffer = true; - - for (var i = 0, l = this.writeBuffer.length; i < l; i++) { - this.transport.send(this.writeBuffer[i]); - } - - // force transport flush - this.transport.flush(); + this.transport.sendMany(this.writeBuffer); + this.writeBuffer = []; } }; From b4166271ddedd02717fa8eb351752f073b2b8a22 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Wed, 4 Jan 2012 16:06:45 -0800 Subject: [PATCH 0181/1403] Refactored this again, this time I like it. --- lib/transport.js | 72 +++++++++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/lib/transport.js b/lib/transport.js index 25b107f0..c7d0e5ea 100644 --- a/lib/transport.js +++ b/lib/transport.js @@ -36,14 +36,6 @@ function Transport (opts) { util.inherits(Transport, EventEmitter); -/** - * Whether to buffer outgoing data. - * - * @api public - */ - -Transport.prototype.buffer = true; - /** * Emits an error. * @@ -94,51 +86,61 @@ Transport.prototype.close = function () { * Sends a packet. * * @param {Object} packet - * @api public + * @api private */ Transport.prototype.send = function (packet) { if ('open' == this.readyState) { - if (this.buffer) { - // debug: buffering packet - this.writeBuffer.push(packet); - } else { - var self = this; - this.buffer = true; - this.write(packet, function () { - self.flush(); - }); - } + this.writeBuffer.push(packet); + this.flush(); } else { throw new Error('Transport not open'); } - - return this; }; /** - * Flushes the buffer. + * Sends multiple packets. + * + * @param {Array} packets + * @api private + */ + +Transport.prototype.sendMany = function (packets) { + if ('open' == this.readyState) { + this.writeBuffer.push.apply(this.writeBuffer, packets); + this.flush(); + } else { + throw new Error('Transport not open'); + } +} + +/** + * Flushes the send buffer. * * @api private */ Transport.prototype.flush = function () { - var self = this; + if (this.flushing || !this.writeBuffer.length) return; - function onFlush () { - // debug: transport flushed - self.buffer = false; + var offset = this.writeBuffer.length + , self = this + + this.flushing = true; + // debug: flushing transport buffer with %d items, this.writeBuffer.length + this.write(this.writeBuffer, function () { + self.writeBuffer.splice(0, offset); + self.flushing = false; self.emit('flush'); - }; - if (this.writeBuffer.length) { - this.writeMany(self.writeBuffer, onFlush); - this.writeBuffer = []; - } else { - onFlush(); - } - - return this; + if (self.writeBuffer.length) { + // debug: flushing again + self.flush(); + } else { + // debug: flush drained + self.emit('drain'); + } + }); }; /** From fed78d360749776e9c092389ec18f8fd3296b7c7 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Wed, 4 Jan 2012 16:07:30 -0800 Subject: [PATCH 0182/1403] Started finishing Polling#pause. --- lib/transports/polling.js | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/lib/transports/polling.js b/lib/transports/polling.js index 0f33bcbf..cfd2168c 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -53,26 +53,34 @@ Polling.prototype.doOpen = function () { /** * Pauses polling. * - * @param {Function} callback upon flush + * @param {Function} callback upon buffers are flushed and transport is paused * @api private */ -Polling.prototype.pause = function (onFlush) { - this.paused = true; +Polling.prototype.pause = function (onPause) { + var pending = 0 + , self = this - var pending = 0; + this.readyState = 'pausing'; + + function pause () { + if (!--pending) { + self.readyState = 'paused'; + onPause(); + } + } if (this.polling) { pending++; this.once('data', function () { - --pending || onFlush(); + --pending || pause(); }); } - if (this.buffer) { + if (this.writeBuffer.length) { pending++; - this.once('flush', function () { - --pending || onFlush(); + this.once('drain', function () { + --pending || pause(); }); } }; @@ -84,11 +92,8 @@ Polling.prototype.pause = function (onFlush) { */ Polling.prototype.poll = function () { - if (!this.paused) { - this.polling = true; - this.doPoll(); - this.emit('poll'); - } + this.polling = true; + this.doPoll(); }; /** @@ -100,6 +105,7 @@ Polling.prototype.poll = function () { Polling.prototype.onData = function (data) { // if we got data we're not polling this.polling = false; + this.emit('poll'); // decode payload var packets = parser.decodePayload(data); @@ -123,6 +129,8 @@ Polling.prototype.onData = function (data) { if ('open' == this.readyState) { // trigger next poll this.poll(); + } else { + // debug: ignoring poll - transport state "%s", this.readyState } }; From d7b07367edc9ccac2fe50fa2a70e7059a482285b Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Wed, 4 Jan 2012 16:07:55 -0800 Subject: [PATCH 0183/1403] writeMany is gone, write now handles arrays directly. --- lib/transports/polling.js | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/lib/transports/polling.js b/lib/transports/polling.js index cfd2168c..1090c1dc 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -153,22 +153,10 @@ Polling.prototype.doClose = function () { * @api private */ -Polling.prototype.writeMany = function (packets, fn) { +Polling.prototype.write = function (packets, fn) { this.doWrite(parser.encodePayload(packets), fn); }; -/** - * Writes a single-packet payload. - * - * @param {Object} data packet - * @param {Function} drain callback - * @api private - */ - -Polling.prototype.write = function (packet, fn) { - this.writeMany([packet], fn); -}; - /** * Generates uri for connection. * From 9c70561e00facf2c243139388779c9b7ee6da784 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Wed, 4 Jan 2012 16:08:17 -0800 Subject: [PATCH 0184/1403] Refactored WS#write --- lib/transports/websocket.js | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/lib/transports/websocket.js b/lib/transports/websocket.js index 3ec3a20e..22c46e22 100644 --- a/lib/transports/websocket.js +++ b/lib/transports/websocket.js @@ -71,26 +71,14 @@ WS.prototype.doOpen = function () { /** * Writes data to socket. * - * @param {String} data. + * @param {Array} array of packets. * @param {Function} drain callback. * @api private */ -WS.prototype.write = function (packet, fn) { - this.socket.send(parser.encodePacket(packet)); - fn(); -}; - -/** - * Writes a packets payload. - * - * @param {Array} data packets - * @api private - */ - -WS.prototype.writeMany = function (packets, fn) { +WS.prototype.write = function (packets, fn) { for (var i = 0, l = packets.length; i < l; i++) { - this.write(packets[0]); + this.socket.send(parser.encodePacket(packet)); } fn(); }; From ca56e618471c3c52f2221c2c6f5d3a42fdc7de08 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Wed, 4 Jan 2012 22:48:54 -0800 Subject: [PATCH 0185/1403] Fixed WS#write --- lib/transports/websocket.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/transports/websocket.js b/lib/transports/websocket.js index 22c46e22..25e94daf 100644 --- a/lib/transports/websocket.js +++ b/lib/transports/websocket.js @@ -78,7 +78,7 @@ WS.prototype.doOpen = function () { WS.prototype.write = function (packets, fn) { for (var i = 0, l = packets.length; i < l; i++) { - this.socket.send(parser.encodePacket(packet)); + this.socket.send(parser.encodePacket(packets[i])); } fn(); }; From 63e711885f38f2372a5e08fc3ca59640d49d9a8e Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 9 Jan 2012 08:43:06 -0800 Subject: [PATCH 0186/1403] Simplified Polling#pause. --- lib/transports/polling.js | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/lib/transports/polling.js b/lib/transports/polling.js index 1090c1dc..32931802 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -64,24 +64,14 @@ Polling.prototype.pause = function (onPause) { this.readyState = 'pausing'; function pause () { - if (!--pending) { - self.readyState = 'paused'; - onPause(); - } + self.readyState = 'paused'; + onPause(); } if (this.polling) { - pending++; - this.once('data', function () { - --pending || pause(); - }); - } - - if (this.writeBuffer.length) { - pending++; - this.once('drain', function () { - --pending || pause(); - }); + this.once('poll', pause); + } else { + pause(); } }; @@ -92,6 +82,7 @@ Polling.prototype.pause = function (onPause) { */ Polling.prototype.poll = function () { + // debug: polling this.polling = true; this.doPoll(); }; @@ -127,7 +118,6 @@ Polling.prototype.onData = function (data) { } if ('open' == this.readyState) { - // trigger next poll this.poll(); } else { // debug: ignoring poll - transport state "%s", this.readyState From 4c9960ae2723e58173342cc733a1e86a90f0be13 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 9 Jan 2012 08:50:12 -0800 Subject: [PATCH 0187/1403] Claned up Transport concept of buffering. --- lib/transport.js | 53 +++--------------------------------------------- 1 file changed, 3 insertions(+), 50 deletions(-) diff --git a/lib/transport.js b/lib/transport.js index c7d0e5ea..54c4797c 100644 --- a/lib/transport.js +++ b/lib/transport.js @@ -27,7 +27,7 @@ function Transport (opts) { this.secure = opts.secure; this.query = opts.query; this.readyState = ''; - this.writeBuffer = []; + this.writable = true; }; /** @@ -82,22 +82,6 @@ Transport.prototype.close = function () { return this; }; -/** - * Sends a packet. - * - * @param {Object} packet - * @api private - */ - -Transport.prototype.send = function (packet) { - if ('open' == this.readyState) { - this.writeBuffer.push(packet); - this.flush(); - } else { - throw new Error('Transport not open'); - } -}; - /** * Sends multiple packets. * @@ -105,44 +89,14 @@ Transport.prototype.send = function (packet) { * @api private */ -Transport.prototype.sendMany = function (packets) { +Transport.prototype.send = function (packets, fn) { if ('open' == this.readyState) { - this.writeBuffer.push.apply(this.writeBuffer, packets); - this.flush(); + this.write(packets, fn); } else { throw new Error('Transport not open'); } } -/** - * Flushes the send buffer. - * - * @api private - */ - -Transport.prototype.flush = function () { - if (this.flushing || !this.writeBuffer.length) return; - - var offset = this.writeBuffer.length - , self = this - - this.flushing = true; - // debug: flushing transport buffer with %d items, this.writeBuffer.length - this.write(this.writeBuffer, function () { - self.writeBuffer.splice(0, offset); - self.flushing = false; - self.emit('flush'); - - if (self.writeBuffer.length) { - // debug: flushing again - self.flush(); - } else { - // debug: flush drained - self.emit('drain'); - } - }); -}; - /** * Called upon open * @@ -151,7 +105,6 @@ Transport.prototype.flush = function () { Transport.prototype.onOpen = function () { this.readyState = 'open'; - this.flush(); this.emit('open'); }; From 81eb2c37f7c6d12ca4efda111f48fc5e60c63e42 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 9 Jan 2012 08:53:58 -0800 Subject: [PATCH 0188/1403] Make sure Polling#write updates the `Transport#writable` property. --- lib/transports/polling.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/transports/polling.js b/lib/transports/polling.js index 32931802..b6db24c7 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -144,7 +144,12 @@ Polling.prototype.doClose = function () { */ Polling.prototype.write = function (packets, fn) { - this.doWrite(parser.encodePayload(packets), fn); + var self = this; + this.writable = false; + this.doWrite(parser.encodePayload(packets), function () { + self.writable = true; + fn(); + }); }; /** From a2a8ec126f452b3ef23337ea70364d66e9246af7 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 9 Jan 2012 08:54:50 -0800 Subject: [PATCH 0189/1403] Keep reference of flush bound to the instance around. --- lib/socket.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/socket.js b/lib/socket.js index 2617ae18..9e30837a 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -40,7 +40,7 @@ function Socket (opts) { this.transports = opts.transports || ['polling', 'websocket', 'flashsocket']; this.readyState = ''; this.writeBuffer = []; - + this.flush = this.flush.bind(this); this.open(); }; From b417e8df6eb139e4c9d3c4552f651d06f37743a0 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 9 Jan 2012 08:55:13 -0800 Subject: [PATCH 0190/1403] Avoid collisions between upgrading transports --- lib/socket.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/socket.js b/lib/socket.js index 9e30837a..18f1a0ba 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -155,7 +155,9 @@ Socket.prototype.probe = function (name) { // debug: pausing current transport "%s", self.transport.name self.transport.pause(function () { - self.setTransport(self.transport); + self.setTransport(transport); + transport.send({ type: 'upgrade' }); + transport = null; self.upgrading = false; self.flush(); self.emit('upgrade', name); @@ -172,12 +174,14 @@ Socket.prototype.probe = function (name) { transport.open(); this.once('close', function () { - // debug: socket closed prematurely - aborting probe - transport.close(); + if (transport) { + // debug: socket closed prematurely - aborting probe + transport.close(); + } }); this.once('upgrading', function (to) { - if (to != name) { + if (transport) { // debug: probe for "%s" succeeded - aborting "%s", to, name transport.close(); } From 5c3d3a5739df3c559a50d4813538a2e14321489a Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 9 Jan 2012 08:55:40 -0800 Subject: [PATCH 0191/1403] Improved #flush, #sendPacket. --- lib/socket.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/lib/socket.js b/lib/socket.js index 18f1a0ba..31b3c285 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -283,10 +283,13 @@ Socket.prototype.setPingTimeout = function () { */ Socket.prototype.flush = function () { - if (this.writeBuffer.length) { + if ('open' == this.readyState == this.transport.readyState + && this.transport.writable && !this.upgrading) { // debug: flushing %d packets in socket, this.writeBuffer.length - this.transport.sendMany(this.writeBuffer); - this.writeBuffer = []; + if (this.writeBuffer.length) { + this.transport.send(this.writeBuffer, this.flush); + this.writeBuffer = []; + } } }; @@ -313,13 +316,8 @@ Socket.prototype.send = function (msg) { Socket.prototype.sendPacket = function (type, data) { var packet = { type: type, data: data }; - if ('open' != this.readyState || this.upgrading) { - // debug: socket send - buffering packet: type "%s" | data "%s", type, data - this.writeBuffer.push(packet); - } else { - // debug: socket send - transporting: type "%s" | data "%s", type, data - this.transport.send(packet); - } + this.writeBuffer.push(packet); + this.flush(); }; /** From 3b558630d9b895d55da2b1fd15878f634c66dc63 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 9 Jan 2012 09:00:40 -0800 Subject: [PATCH 0192/1403] Added upgrade packet. This packet gets sent by the upgrading transport that completed the probe (ping/pong), once the client confirms that it's now considered the main transport. --- lib/parser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/parser.js b/lib/parser.js index ed386db6..2f4d5619 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -10,7 +10,7 @@ var packets = exports.packets = { , pong: 3 , message: 4 , error: 5 - , noop: 6 + , upgrade: 6 }; var packetslist = Object.keys(packets); From 2a521830d173f6a23905c8373853f639b7d01376 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Wed, 11 Jan 2012 11:22:57 -0800 Subject: [PATCH 0193/1403] Pre-emptively assign an UID for every request. --- lib/socket.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/socket.js b/lib/socket.js index 2617ae18..d591b16d 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -34,6 +34,7 @@ function Socket (opts) { this.host = opts.host || opts.hostname || 'localhost'; this.port = opts.port || 80; this.query = opts.query || {}; + this.query.uid = rnd(); this.upgrade = false !== opts.upgrade; this.path = opts.path || '/engine.io' this.forceJSONP = !!opts.forceJSONP; @@ -359,3 +360,13 @@ Socket.prototype.onClose = function (reason, desc) { this.onclose && this.onclose.call(this); } }; + +/** + * Generates a random uid. + * + * @api private + */ + +function rnd () { + return String(Math.random()).substr(5) + String(Math.random()).substr(5); +} From a23405bfea548a486399975e219cbd4ffcd85f62 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sat, 14 Jan 2012 10:36:44 -0800 Subject: [PATCH 0194/1403] Fixed close packet sending. --- lib/transports/polling.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/transports/polling.js b/lib/transports/polling.js index b6db24c7..6f7d7883 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -132,7 +132,7 @@ Polling.prototype.onData = function (data) { Polling.prototype.doClose = function () { // debug: sending close packet - this.send({ type: 'close' }); + this.send([{ type: 'close' }]); }; /** From 5f0d7fb7e93aad45288513423e238c82ae140d0a Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sat, 14 Jan 2012 10:40:23 -0800 Subject: [PATCH 0195/1403] Removed noop packet test. --- test/parser.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/parser.js b/test/parser.js index 6df3e65a..5a6aa601 100644 --- a/test/parser.js +++ b/test/parser.js @@ -67,9 +67,9 @@ describe('parser', function () { .to.eql({ type: 'error', data: 'aaa' }); }); - it('should encode a noop packet', function () { - expect(decode(encode({ type: 'noop', data: 'aaa' }))) - .to.eql({ type: 'noop', data: 'aaa' }); + it('should encode an upgrade packet', function () { + expect(decode(encode({ type: 'upgrade' }))) + .to.eql({ type: 'upgrade' }); }); it('should distinguish between empty data and no data', function () { From 5bc9c7e1328862037d58cf00b54acc497c9a5ca3 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sat, 14 Jan 2012 13:52:24 -0800 Subject: [PATCH 0196/1403] Made client buffer/flush work more like server. --- lib/socket.js | 17 +++++++++-------- lib/transport.js | 6 +++--- lib/transports/polling.js | 4 ++-- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/lib/socket.js b/lib/socket.js index ae7d83f1..e1db4a7f 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -122,6 +122,9 @@ Socket.prototype.setTransport = function (transport) { // set up transport listeners transport + .on('drain', function () { + self.flush(); + }) .on('packet', function (packet) { self.onPacket(packet); }) @@ -147,7 +150,7 @@ Socket.prototype.probe = function (name) { transport.once('open', function () { // debug: probe transport "%s" opened - pinging, name - transport.send({ type: 'ping', data: 'probe' }); + transport.send([{ type: 'ping', data: 'probe' }]); transport.once('message', function (msg) { if ('pong' == msg.type && 'probe' == msg.data) { // debug: probe transport "%s" pong - upgrading, name @@ -157,7 +160,7 @@ Socket.prototype.probe = function (name) { // debug: pausing current transport "%s", self.transport.name self.transport.pause(function () { self.setTransport(transport); - transport.send({ type: 'upgrade' }); + transport.send([{ type: 'upgrade' }]); transport = null; self.upgrading = false; self.flush(); @@ -284,13 +287,11 @@ Socket.prototype.setPingTimeout = function () { */ Socket.prototype.flush = function () { - if ('open' == this.readyState == this.transport.readyState - && this.transport.writable && !this.upgrading) { + if ('closed' != this.readyState && this.transport.writable + && !this.upgrading && this.writeBuffer.length) { // debug: flushing %d packets in socket, this.writeBuffer.length - if (this.writeBuffer.length) { - this.transport.send(this.writeBuffer, this.flush); - this.writeBuffer = []; - } + this.transport.send(this.writeBuffer); + this.writeBuffer = []; } }; diff --git a/lib/transport.js b/lib/transport.js index 54c4797c..6a9b82dc 100644 --- a/lib/transport.js +++ b/lib/transport.js @@ -27,7 +27,6 @@ function Transport (opts) { this.secure = opts.secure; this.query = opts.query; this.readyState = ''; - this.writable = true; }; /** @@ -89,9 +88,9 @@ Transport.prototype.close = function () { * @api private */ -Transport.prototype.send = function (packets, fn) { +Transport.prototype.send = function (packets) { if ('open' == this.readyState) { - this.write(packets, fn); + this.write(packets); } else { throw new Error('Transport not open'); } @@ -105,6 +104,7 @@ Transport.prototype.send = function (packets, fn) { Transport.prototype.onOpen = function () { this.readyState = 'open'; + this.writable = true; this.emit('open'); }; diff --git a/lib/transports/polling.js b/lib/transports/polling.js index 6f7d7883..f2235289 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -143,12 +143,12 @@ Polling.prototype.doClose = function () { * @api private */ -Polling.prototype.write = function (packets, fn) { +Polling.prototype.write = function (packets) { var self = this; this.writable = false; this.doWrite(parser.encodePayload(packets), function () { self.writable = true; - fn(); + self.emit('drain'); }); }; From b834862454cd6f96d2fb3ccff6a69ca06db12b96 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sat, 14 Jan 2012 15:07:06 -0800 Subject: [PATCH 0197/1403] Removed no-longer needed callback. --- lib/transports/websocket.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/transports/websocket.js b/lib/transports/websocket.js index 25e94daf..79e0678c 100644 --- a/lib/transports/websocket.js +++ b/lib/transports/websocket.js @@ -76,11 +76,10 @@ WS.prototype.doOpen = function () { * @api private */ -WS.prototype.write = function (packets, fn) { +WS.prototype.write = function (packets) { for (var i = 0, l = packets.length; i < l; i++) { this.socket.send(parser.encodePacket(packets[i])); } - fn(); }; /** From 49b1fad7361ef53c1a518df42ce68bff1e80ac42 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 15 Jan 2012 22:24:04 -0800 Subject: [PATCH 0198/1403] Fixed Socket#probe --- lib/socket.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/socket.js b/lib/socket.js index e1db4a7f..1b42691e 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -151,20 +151,22 @@ Socket.prototype.probe = function (name) { transport.once('open', function () { // debug: probe transport "%s" opened - pinging, name transport.send([{ type: 'ping', data: 'probe' }]); - transport.once('message', function (msg) { + transport.once('packet', function (msg) { if ('pong' == msg.type && 'probe' == msg.data) { // debug: probe transport "%s" pong - upgrading, name self.upgrading = true; - self.emit('upgrading', name); + self.emit('upgrading', transport); // debug: pausing current transport "%s", self.transport.name self.transport.pause(function () { + if ('closed' == self.readyState || 'closing' == self.readyState) return; + // debug: changing transport and sending upgrade packet + self.emit('upgrade', transport); self.setTransport(transport); transport.send([{ type: 'upgrade' }]); transport = null; self.upgrading = false; self.flush(); - self.emit('upgrade', name); }); } else { // debug: probe transport "%s" failed, name @@ -181,13 +183,15 @@ Socket.prototype.probe = function (name) { if (transport) { // debug: socket closed prematurely - aborting probe transport.close(); + transport = null; } }); this.once('upgrading', function (to) { - if (transport) { - // debug: probe for "%s" succeeded - aborting "%s", to, name + if (transport && to.name != transport.name) { + // debug: "%s" works - aborting "%s", to.name, transport.name transport.close(); + transport = null; } }); }; From c55bde3651ad89b987f6f1889668d0a72d25426d Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 15 Jan 2012 22:24:21 -0800 Subject: [PATCH 0199/1403] Improved socket close instrumentation. --- lib/socket.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/socket.js b/lib/socket.js index 1b42691e..95a67dbd 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -361,7 +361,7 @@ Socket.prototype.onError = function (err) { Socket.prototype.onClose = function (reason, desc) { if ('closed' != this.readyState) { - // debug: socket close + // debug: socket close: "%s", reason this.readyState = 'closed'; this.emit('close', reason, desc); this.onclose && this.onclose.call(this); From aff04b7f0209b50d5912b748d9005a84779e10cb Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 15 Jan 2012 22:25:14 -0800 Subject: [PATCH 0200/1403] Fixed FlashWS reference --- lib/transports/flashsocket.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/transports/flashsocket.js b/lib/transports/flashsocket.js index 7f514825..876e813c 100644 --- a/lib/transports/flashsocket.js +++ b/lib/transports/flashsocket.js @@ -69,7 +69,7 @@ FlashWS.prototype.doOpen = function () { WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR = true; load(base + 'swfobject.js', base + 'web_socket.js', function () { - FlashWs.prototype.doOpen.call(self); + FlashWS.prototype.doOpen.call(self); }); }; From 18cbd9d3a384ad4f0c7e4e1b97e787e75ea2c537 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 15 Jan 2012 22:25:23 -0800 Subject: [PATCH 0201/1403] Added guard for uninitialized flashsocket close attempt. --- lib/transports/flashsocket.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/transports/flashsocket.js b/lib/transports/flashsocket.js index 876e813c..d5c26f9d 100644 --- a/lib/transports/flashsocket.js +++ b/lib/transports/flashsocket.js @@ -73,6 +73,17 @@ FlashWS.prototype.doOpen = function () { }); }; +/** + * Override to prevent closing uninitialized flashsocket. + * + * @api private + */ + +FlashWS.prototype.doClose = function () { + if (!this.socket) return; + FlashWS.prototype.doClose.call(this); +}; + /** * Feature detection for FlashSocket. * From 53c3e90fd7b059cf9747f587e5e35cb693f7c79a Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 15 Jan 2012 22:25:40 -0800 Subject: [PATCH 0202/1403] Added fix for considering 204 a success. In addition, avoid recursion error with status errors. --- lib/transports/polling-xhr.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/transports/polling-xhr.js b/lib/transports/polling-xhr.js index 7ecb318c..431629ed 100644 --- a/lib/transports/polling-xhr.js +++ b/lib/transports/polling-xhr.js @@ -173,13 +173,10 @@ Request.prototype.create = function () { try { if (xhr.readyState != 4) return; - if (200 == xhr.status) { + if (200 == xhr.status || 204 == xhr.status) { data = xhr.responseText; } else { - var err = new Error; - err.code = xhr.status; - err.type = 'StatusError'; - self.onError(err); + self.onError(xhr.status); } } catch (e) { self.onError(e); From 2fe5b2682f7cfe7d6be849e4cc6cd67095aa8a55 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 15 Jan 2012 22:29:51 -0800 Subject: [PATCH 0203/1403] Restored correct pausing logic for polling. --- lib/transports/polling.js | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/transports/polling.js b/lib/transports/polling.js index f2235289..910bf06b 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -64,12 +64,31 @@ Polling.prototype.pause = function (onPause) { this.readyState = 'pausing'; function pause () { + // debug: paused self.readyState = 'paused'; onPause(); } - if (this.polling) { - this.once('poll', pause); + if (this.polling || !this.writable) { + var total = 0; + + if (this.polling) { + // debug: we are currently polling - waiting to pause + total++; + this.once('poll', function () { + // debug: pre-pause polling complete + --total || pause(); + }); + } + + if (!this.writable) { + // debug: we are currently writing - waiting to pause + total++; + this.once('drain', function () { + // debug: pre-pause writing complete + --total || pause(); + }); + } } else { pause(); } From ce02ec2aaa64083e908a3b00ae2063d1f1eef451 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 15 Jan 2012 22:30:13 -0800 Subject: [PATCH 0204/1403] Added polling data instrumentation. --- lib/transports/polling.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/transports/polling.js b/lib/transports/polling.js index 910bf06b..cd1440a7 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -113,10 +113,7 @@ Polling.prototype.poll = function () { */ Polling.prototype.onData = function (data) { - // if we got data we're not polling - this.polling = false; - this.emit('poll'); - + // debug: polling got, data // decode payload var packets = parser.decodePayload(data); From 5f0d2455d3da2310b74fff2975f96fff0c4c2877 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 15 Jan 2012 22:30:28 -0800 Subject: [PATCH 0205/1403] Ensured we emit the event _after_ we we call `onPacket`. This prevents a race condition where upon `poll`, the transport gets paused, then upon getting closed upgraded (therefore replaced) and packets are lost! --- lib/transports/polling.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/transports/polling.js b/lib/transports/polling.js index cd1440a7..03ac03a8 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -133,6 +133,10 @@ Polling.prototype.onData = function (data) { this.onPacket(packets[i]); } + // if we got data we're not polling + this.polling = false; + this.emit('poll'); + if ('open' == this.readyState) { this.poll(); } else { From b253c8fed5a39d5e0ac3af091f1476ade3432b25 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 15 Jan 2012 22:50:06 -0800 Subject: [PATCH 0206/1403] Fixed title --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b92aa0b7..bc69ed05 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# Engine.IO clien +# Engine.IO client This is the client for [Engine](http://github.com/learnboost/engine.io), the implementation of transport-based cross-browser/cross-device bi-directional From b6daf3212e3b67e5aae6ba670b17836ee2b0e65c Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 16 Jan 2012 08:05:08 -0800 Subject: [PATCH 0207/1403] Fixed order of `on` and `load` in util. --- lib/util.js | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/util.js b/lib/util.js index 0d7c093f..288178f3 100644 --- a/lib/util.js +++ b/lib/util.js @@ -25,6 +25,20 @@ exports.inherits = function inherits (a, b) { a.prototype = new c; }; +/** + * Adds an event. + * + * @api private + */ + +exports.on = function (element, event, fn, capture) { + if (element.attachEvent) { + element.attachEvent('on' + event, fn); + } else if (element.addEventListener) { + element.addEventListener(event, fn, capture); + } +}; + /** * Load utility. * @@ -49,20 +63,6 @@ if ('undefined' != typeof window) { }); } -/** - * Adds an event. - * - * @api private - */ - -exports.on = function (element, event, fn, capture) { - if (element.attachEvent) { - element.attachEvent('on' + event, fn); - } else if (element.addEventListener) { - element.addEventListener(event, fn, capture); - } -}; - /** * Defers a function to ensure a spinner is not displayed by the browser. * From 37ad9521684e87170e9b3194a3a1c1edbcc5daf1 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 16 Jan 2012 08:14:00 -0800 Subject: [PATCH 0208/1403] Fixed 204 status on IE. --- lib/transports/polling-xhr.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/transports/polling-xhr.js b/lib/transports/polling-xhr.js index 431629ed..abdcac22 100644 --- a/lib/transports/polling-xhr.js +++ b/lib/transports/polling-xhr.js @@ -173,7 +173,7 @@ Request.prototype.create = function () { try { if (xhr.readyState != 4) return; - if (200 == xhr.status || 204 == xhr.status) { + if (200 == xhr.status || 1223 == xhr.status || 204 == xhr.status) { data = xhr.responseText; } else { self.onError(xhr.status); From 516525afd4ebd4bbb021de9870df69e7a983d89a Mon Sep 17 00:00:00 2001 From: Vladimir Dronnikov Date: Mon, 16 Jan 2012 11:34:01 -0500 Subject: [PATCH 0209/1403] parser simplified --- lib/parser.js | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/lib/parser.js b/lib/parser.js index 2f4d5619..f915bf4c 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -54,26 +54,13 @@ exports.encodePacket = function (packet) { */ exports.decodePacket = function (data) { - if (':' === data[1]) { - var pieces = data.match(/([0-9]+):(.*)?/) - - if (!pieces) { - // parser error - ignoring packet - return err; - } - - var type = pieces[1] - , data = pieces[2] - - if (!packetslist[type]) { - // parser error - ignoring packet - return err; - } - + var type = data.charAt(0) + if (Number(type) != type || !packetslist[type]) return err; + if (data.charAt(1) === ':') { + data = data.substring(2) return { type: packetslist[type], data: undefined === data ? '' : data }; } else { - if (Number(data) != data || !packetslist[data]) return err; - return { type: packetslist[data] }; + return { type: packetslist[type] }; } }; From 706f1504f474564082f06e263e8e72538f2b1222 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 16 Jan 2012 09:12:56 -0800 Subject: [PATCH 0210/1403] Updated encoding format checks for new upcoming format. --- test/parser.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/parser.js b/test/parser.js index 5a6aa601..c8d56d44 100644 --- a/test/parser.js +++ b/test/parser.js @@ -81,8 +81,8 @@ describe('parser', function () { }); it('should match the encoding format', function () { - expect(encode({ type: 'message', data: 'test' })).to.match(/[0-9]+(:.*)?/); - expect(encode({ type: 'message' })).to.match(/[0-9]+(:.*)?/); + expect(encode({ type: 'message', data: 'test' })).to.match(/[0-9].*/); + expect(encode({ type: 'message' })).to.match(/[0-9].*/); }); }); From 85bf4c05beb16db1903385970873710665e2e706 Mon Sep 17 00:00:00 2001 From: Vladimir Dronnikov Date: Mon, 16 Jan 2012 12:14:15 -0500 Subject: [PATCH 0211/1403] drop excess : in packets --- .gitignore | 1 + lib/parser.js | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index fd4f2b06..b1866059 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ +dist node_modules .DS_Store diff --git a/lib/parser.js b/lib/parser.js index f915bf4c..13bbdf20 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -56,9 +56,8 @@ exports.encodePacket = function (packet) { exports.decodePacket = function (data) { var type = data.charAt(0) if (Number(type) != type || !packetslist[type]) return err; - if (data.charAt(1) === ':') { - data = data.substring(2) - return { type: packetslist[type], data: undefined === data ? '' : data }; + if (data.length > 1) { + return { type: packetslist[type], data: data.substring(1) }; } else { return { type: packetslist[type] }; } From 01260064cfbd85aa1d047b52eeb99de56a6960bd Mon Sep 17 00:00:00 2001 From: Vladimir Dronnikov Date: Mon, 16 Jan 2012 12:21:41 -0500 Subject: [PATCH 0212/1403] fixed encoder --- lib/parser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/parser.js b/lib/parser.js index 13bbdf20..4d4e40bd 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -40,7 +40,7 @@ exports.encodePacket = function (packet) { // data fragment is optional if (undefined !== packet.data) { - encoded += ':' + packet.data; + encoded += packet.data; } return '' + encoded; From 01b4802d1199e762cad6e9a07c0ff11d11468c37 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 16 Jan 2012 09:25:19 -0800 Subject: [PATCH 0213/1403] Removed no longer relevant test. --- test/parser.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test/parser.js b/test/parser.js index c8d56d44..af53b925 100644 --- a/test/parser.js +++ b/test/parser.js @@ -72,14 +72,6 @@ describe('parser', function () { .to.eql({ type: 'upgrade' }); }); - it('should distinguish between empty data and no data', function () { - var packet = decode(encode({ type: 'message', data: '' })); - expect(packet.data).to.equal(''); - - packet = decode(encode({ type: 'message' })); - expect(packet.data).to.be(undefined); - }); - it('should match the encoding format', function () { expect(encode({ type: 'message', data: 'test' })).to.match(/[0-9].*/); expect(encode({ type: 'message' })).to.match(/[0-9].*/); From cc14deb5ab0524c48a511ac30dd435cf56e93856 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 16 Jan 2012 09:41:51 -0800 Subject: [PATCH 0214/1403] Removed error packet type. --- lib/parser.js | 3 +-- test/parser.js | 5 ----- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/parser.js b/lib/parser.js index 4d4e40bd..be39d9c4 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -9,8 +9,7 @@ var packets = exports.packets = { , ping: 2 , pong: 3 , message: 4 - , error: 5 - , upgrade: 6 + , upgrade: 5 }; var packetslist = Object.keys(packets); diff --git a/test/parser.js b/test/parser.js index af53b925..f98bc1bb 100644 --- a/test/parser.js +++ b/test/parser.js @@ -62,11 +62,6 @@ describe('parser', function () { .to.eql({ type: 'message', data: 'aaa' }); }); - it('should encode an error packet', function () { - expect(decode(encode({ type: 'error', data: 'aaa' }))) - .to.eql({ type: 'error', data: 'aaa' }); - }); - it('should encode an upgrade packet', function () { expect(decode(encode({ type: 'upgrade' }))) .to.eql({ type: 'upgrade' }); From 141eaef7d35612d8aaabd494bd2c5fdc8ff870b5 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 16 Jan 2012 09:49:45 -0800 Subject: [PATCH 0215/1403] Failing test. --- test/parser.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/parser.js b/test/parser.js index f98bc1bb..e4f857f2 100644 --- a/test/parser.js +++ b/test/parser.js @@ -62,6 +62,11 @@ describe('parser', function () { .to.eql({ type: 'message', data: 'aaa' }); }); + it('should encode a message packet coercing to string', function () { + expect(decode(encode({ type: 'message', data: 1 }))) + .to.eql({ type: 'message', data: '1' }); + }); + it('should encode an upgrade packet', function () { expect(decode(encode({ type: 'upgrade' }))) .to.eql({ type: 'upgrade' }); From 4b7d7e9787e8f239d6439fd3d542a660ca498135 Mon Sep 17 00:00:00 2001 From: Vladimir Dronnikov Date: Mon, 16 Jan 2012 12:52:23 -0500 Subject: [PATCH 0216/1403] we coerce data to string --- lib/parser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/parser.js b/lib/parser.js index be39d9c4..c486219f 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -39,7 +39,7 @@ exports.encodePacket = function (packet) { // data fragment is optional if (undefined !== packet.data) { - encoded += packet.data; + encoded += String(packet.data); } return '' + encoded; From 38d6b75d0ebed0b44ad80d06cfd3fe4592f3300d Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 16 Jan 2012 09:59:48 -0800 Subject: [PATCH 0217/1403] Fixed missing global reference. --- lib/transports/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/transports/index.js b/lib/transports/index.js index ec70c0d0..97d2a9d2 100644 --- a/lib/transports/index.js +++ b/lib/transports/index.js @@ -8,6 +8,7 @@ var XHR = require('./polling-xhr') , websocket = require('./websocket') , flashsocket = require('./flashsocket') , util = require('../util') + , global = this /** * Export transports. From 9d0cf675d48900bb8e468a81b536150cbc318545 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 16 Jan 2012 10:03:34 -0800 Subject: [PATCH 0218/1403] Thanks Vladimir --- package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package.json b/package.json index 94bc85c5..21854f69 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,10 @@ , "description": "Client for the realtime Engine" , "main": "./lib/engine.io-client" , "version": "0.1.0" + , "contributors": [ + { "name": "Guillermo Rauch", "email": "rauchg@gmail.com" } + , { "name": "Vladimir Dronnikov", "email": "dronnikov@gmail.com" } + ] , "dependencies": { "ws": "0.4.0" , "xmlhttprequest": "1.3.0" From 44eaeeaa608d5bb597561df7f10e52288e84070c Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 16 Jan 2012 14:55:29 -0800 Subject: [PATCH 0219/1403] Added `flashPath` option. --- README.md | 9 +++++++++ lib/socket.js | 2 ++ 2 files changed, 11 insertions(+) diff --git a/README.md b/README.md index bc69ed05..d3ffa1c9 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ The client class. _Inherits from EventEmitter_. - `upgrade` (`Boolean`): defaults to true, whether the client should try to upgrade the transport from long-polling to something better. - `forceJSONP` (`Boolean`): forces JSONP for polling transport. + - `flashPath` (`String`): path to flash client files - `transports` (`Array`): a list of transports to try (in order). Defaults to `['polling', 'websocket', 'flashsocket']`. `Engine` always attempts to connect directly with the first one, provided the @@ -99,6 +100,14 @@ The client class. _Inherits from EventEmitter_. - `close` - Disconnects the client. +## Flash transport + +In order for the Flash transport to work correctly, set the following: + +```js +WEB_SOCKET_SWF_LOCATION = '/path/to/WebSocketMainInsecure.swf'; +``` + ## Tests `engine.io-client` is used to test diff --git a/lib/socket.js b/lib/socket.js index 95a67dbd..05ad208e 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -38,6 +38,7 @@ function Socket (opts) { this.upgrade = false !== opts.upgrade; this.path = opts.path || '/engine.io' this.forceJSONP = !!opts.forceJSONP; + this.flashPath = opts.flashPath || ''; this.transports = opts.transports || ['polling', 'websocket', 'flashsocket']; this.readyState = ''; this.writeBuffer = []; @@ -75,6 +76,7 @@ Socket.prototype.createTransport = function (name) { , path: this.path , query: query , forceJSONP: this.forceJSONP + , flashPath: this.flashPath }); return transport; From b7b9ee1b2e3f999f886ce7ea1327a886f6a13c9c Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 16 Jan 2012 14:55:43 -0800 Subject: [PATCH 0220/1403] Leveraged flashPath from flashsocket transport. --- lib/transports/flashsocket.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/transports/flashsocket.js b/lib/transports/flashsocket.js index d5c26f9d..10b81851 100644 --- a/lib/transports/flashsocket.js +++ b/lib/transports/flashsocket.js @@ -12,12 +12,6 @@ var WebSocket = require('./websocket') module.exports = FlashWS; -/** - * Noop. - */ - -function empty () { } - /** * FlashWS constructor. * @@ -26,6 +20,7 @@ function empty () { } function FlashWS (options) { WebSocket.call(this, options); + this.flashPath = options.flashPath; }; /** From 2e52e6eb600bd34c02312c3736d7c4bfc4f55941 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 16 Jan 2012 14:56:47 -0800 Subject: [PATCH 0221/1403] Transfer web-socket-js logging to instrumentation. --- lib/transports/flashsocket.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/transports/flashsocket.js b/lib/transports/flashsocket.js index 10b81851..8f5ac8d9 100644 --- a/lib/transports/flashsocket.js +++ b/lib/transports/flashsocket.js @@ -49,12 +49,11 @@ FlashWS.prototype.doOpen = function () { return; } - var base = io.enginePath + '/support/web-socket-js/' - , self = this - + // instrument websocketjs logging function log (type) { - return function (msg) { - return self.log[type](msg); + return function () { + var str = Array.prototype.join.call(arguments, ' '); + // debug: [websocketjs %s] %s, type, str } }; From b8d126c2c2b8c6b1307a8c70853fab4aff700e85 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 16 Jan 2012 15:38:56 -0800 Subject: [PATCH 0222/1403] Improved dependency loader. --- lib/transports/flashsocket.js | 41 ++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/lib/transports/flashsocket.js b/lib/transports/flashsocket.js index 8f5ac8d9..7bc78c1e 100644 --- a/lib/transports/flashsocket.js +++ b/lib/transports/flashsocket.js @@ -57,12 +57,17 @@ FlashWS.prototype.doOpen = function () { } }; - // TODO: proxy logging to client logger WEB_SOCKET_LOGGER = { log: log('debug'), error: log('error') }; - WEB_SOCKET_SWF_LOCATION = base + '/WebSocketMainInsecure.swf'; WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR = true; - load(base + 'swfobject.js', base + 'web_socket.js', function () { + // dependencies + var deps = [path + 'web_socket.js']; + + if ('undefined' == typeof swfobject) { + deps.unshift(path + 'swfobject.js'); + } + + load(deps, function () { FlashWS.prototype.doOpen.call(self); }); }; @@ -117,16 +122,17 @@ var scripts = {}; function create (path, fn) { if (scripts[path]) return fn(); - var el = doc.createElement('script') + var el = document.createElement('script') , loaded = false + // debug: loading "%s", path el.onload = el.onreadystatechange = function () { + if (loaded || scripts[path]) return; var rs = el.readyState; - - if ((!rs || rs == 'loaded' || rs == 'complete') && !loaded) { + if (!rs || 'loaded' == rs || 'complete' == rs) { + // debug: loaded "%s", path el.onload = el.onreadystatechange = null; - loaded = 1; - // prevent double execution across multiple instances + loaded = true; scripts[path] = true; fn(); } @@ -135,23 +141,24 @@ function create (path, fn) { el.async = 1; el.src = path; + var head = document.getElementsByTagName('head')[0]; head.insertBefore(el, head.firstChild); }; /** * Loads scripts and fires a callback. * - * @param {String} path (can be multiple parameters) + * @param {Array} paths * @param {Function} callback */ -function load () { - var total = arguments.length - 1 - , fn = arguments[total] - - for (var i = 0, l = total; i < l; i++) { - create(arguments[i], function () { - --total || fn(); +function load (arr, fn) { + function process (i) { + if (!arr[i]) return fn(); + create(arr[i], function () { + process(arr[++i]); }); - } + }; + + process(0); }; From 3012a7922baa0c855e8deb37f037b1c1ec245c4b Mon Sep 17 00:00:00 2001 From: Vladimir Dronnikov Date: Tue, 17 Jan 2012 09:31:12 +0000 Subject: [PATCH 0223/1403] fixed wrong assumption on ping timeout --- lib/socket.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/socket.js b/lib/socket.js index 95a67dbd..c6e28079 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -1,4 +1,3 @@ - /** * Module dependencies. */ @@ -281,7 +280,7 @@ Socket.prototype.setPingTimeout = function () { var self = this; this.pingTimeoutTimer = setTimeout(function () { self.onClose('ping timeout'); - }, this.pingInterval); + }, this.pingTimeout); }; /** From 3877183268ac74fcbf15204181f7225fb2c36556 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 17 Jan 2012 08:12:08 -0800 Subject: [PATCH 0224/1403] Removed unneeded .bind --- lib/socket.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/socket.js b/lib/socket.js index 05ad208e..fd505268 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -42,7 +42,6 @@ function Socket (opts) { this.transports = opts.transports || ['polling', 'websocket', 'flashsocket']; this.readyState = ''; this.writeBuffer = []; - this.flush = this.flush.bind(this); this.open(); }; From ed59483bb977cf5ef53f9746f49bca2c3c763608 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 17 Jan 2012 08:14:06 -0800 Subject: [PATCH 0225/1403] Added util#keys --- lib/util.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/util.js b/lib/util.js index 288178f3..d0bcdc44 100644 --- a/lib/util.js +++ b/lib/util.js @@ -25,6 +25,23 @@ exports.inherits = function inherits (a, b) { a.prototype = new c; }; +/** + * Object.keys + */ + +exports.keys = Object.keys || function (obj) { + var ret = [] + , has = Object.prototype.hasOwnProperty + + for (var i in obj) { + if (has.call(obj, i)) { + ret.push(i); + } + } + + return ret; +}; + /** * Adds an event. * From 3fc505d57343ead997fb0fb6e011eb1acd338e9b Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 17 Jan 2012 08:14:44 -0800 Subject: [PATCH 0226/1403] Leveraged util.keys for browser compliance. --- lib/parser.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/parser.js b/lib/parser.js index c486219f..f93f0619 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -1,4 +1,10 @@ +/** + * Module dependencies. + */ + +var util = require('./util') + /** * Packet types. */ @@ -12,7 +18,7 @@ var packets = exports.packets = { , upgrade: 5 }; -var packetslist = Object.keys(packets); +var packetslist = util.keys(packets); /** * Premade error packet. From 35d35e3980976849904e90173e4140f03db9a8cf Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 17 Jan 2012 09:32:34 -0800 Subject: [PATCH 0227/1403] Consistency with server --- lib/socket.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/socket.js b/lib/socket.js index b478d086..bf8e25db 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -63,8 +63,8 @@ Socket.prototype.createTransport = function (name) { var query = clone(this.query) query.transport = name; - if (this.sid) { - query.sid = this.sid; + if (this.id) { + query.sid = this.id; } var transport = new transports[name]({ @@ -261,7 +261,7 @@ Socket.prototype.onPacket = function (packet) { Socket.prototype.onHandshake = function (data) { this.emit('handshake', data); - this.sid = data.sid; + this.id = data.sid; this.transport.query.sid = data.sid; this.upgrades = data.upgrades; this.pingTimeout = data.pingTimeout; From f3375a86daf62cca88f0aaca7def2a06c3f5daee Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 17 Jan 2012 09:32:42 -0800 Subject: [PATCH 0228/1403] Revert "fixed wrong assumption on ping timeout" This reverts commit 3012a7922baa0c855e8deb37f037b1c1ec245c4b. --- lib/socket.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/socket.js b/lib/socket.js index bf8e25db..e3b75186 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -1,3 +1,4 @@ + /** * Module dependencies. */ @@ -281,7 +282,7 @@ Socket.prototype.setPingTimeout = function () { var self = this; this.pingTimeoutTimer = setTimeout(function () { self.onClose('ping timeout'); - }, this.pingTimeout); + }, this.pingInterval); }; /** From 9800e29f72884b46d1f49ee83f460128120976a1 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 17 Jan 2012 09:50:10 -0800 Subject: [PATCH 0229/1403] Removed notion of pingInterval --- lib/socket.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/socket.js b/lib/socket.js index e3b75186..7193ad0f 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -266,7 +266,6 @@ Socket.prototype.onHandshake = function (data) { this.transport.query.sid = data.sid; this.upgrades = data.upgrades; this.pingTimeout = data.pingTimeout; - this.pingInterval = data.pingInterval; this.onOpen(); this.setPingTimeout(); }; @@ -282,7 +281,7 @@ Socket.prototype.setPingTimeout = function () { var self = this; this.pingTimeoutTimer = setTimeout(function () { self.onClose('ping timeout'); - }, this.pingInterval); + }, this.pingTimeout); }; /** From b8e76bcb51376fa1f7d753019a99ac5ad3492196 Mon Sep 17 00:00:00 2001 From: Vladimir Dronnikov Date: Wed, 18 Jan 2012 10:26:45 -0500 Subject: [PATCH 0230/1403] dangerous typo: JSON --- lib/transports/polling.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/transports/polling.js b/lib/transports/polling.js index 03ac03a8..3adcd6a9 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -1,11 +1,10 @@ - /** * Module dependencies. */ var Transport = require('../transport') , XHR = require('./polling-xhr') - , JSON = require('./polling-jsonp') + , JSONP = require('./polling-jsonp') , util = require('../util') , parser = require('../parser') , global = this From 5aeff8618dc2edc8bfcc5781eeecf2c83b2c0199 Mon Sep 17 00:00:00 2001 From: Vladimir Dronnikov Date: Wed, 18 Jan 2012 10:28:12 -0500 Subject: [PATCH 0231/1403] stopper typo fixed --- lib/transports/polling-xhr.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/transports/polling-xhr.js b/lib/transports/polling-xhr.js index abdcac22..9e544dac 100644 --- a/lib/transports/polling-xhr.js +++ b/lib/transports/polling-xhr.js @@ -1,4 +1,3 @@ - /** * Module requirements. */ @@ -248,7 +247,7 @@ Request.prototype.cleanup = function () { } catch(e) {} if (global.ActiveXObject) { - delete Browser.requests[this.index]; + delete Request.requests[this.index]; } this.xhr = null; From a1720e470ba6361b1fb36bb072403dbd3e498af9 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Wed, 18 Jan 2012 08:31:57 -0800 Subject: [PATCH 0232/1403] Fixed super call from JSONP --- lib/transports/polling-jsonp.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/transports/polling-jsonp.js b/lib/transports/polling-jsonp.js index 30ae7a28..dc20cbfb 100644 --- a/lib/transports/polling-jsonp.js +++ b/lib/transports/polling-jsonp.js @@ -32,7 +32,7 @@ function empty () { } */ function JSONPPolling (opts) { - Transport.call(this, opts); + Polling.call(this, opts); // add callback to jsonp global var self = this; From bbc1fc1b7b271f189af2288ba88fd8571a672502 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Wed, 18 Jan 2012 08:32:07 -0800 Subject: [PATCH 0233/1403] Removed unneeded, cyclic requires. --- lib/transports/polling.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/transports/polling.js b/lib/transports/polling.js index 03ac03a8..fe46caac 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -4,8 +4,6 @@ */ var Transport = require('../transport') - , XHR = require('./polling-xhr') - , JSON = require('./polling-jsonp') , util = require('../util') , parser = require('../parser') , global = this From c2f2b41e174d3b63b5e16bbc4ee009635301d77d Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Wed, 18 Jan 2012 08:36:47 -0800 Subject: [PATCH 0234/1403] JSONP callback improvements. --- lib/engine.io-client.js | 6 ------ lib/transports/polling-jsonp.js | 11 +++++++++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/engine.io-client.js b/lib/engine.io-client.js index 6ed2b069..5d9070e3 100644 --- a/lib/engine.io-client.js +++ b/lib/engine.io-client.js @@ -15,12 +15,6 @@ exports.version = '0.1.0'; exports.protocol = 1; -/** - * JSONP callbacks. - */ - -exports.j = []; - /** * Utils. * diff --git a/lib/transports/polling-jsonp.js b/lib/transports/polling-jsonp.js index dc20cbfb..45025ea2 100644 --- a/lib/transports/polling-jsonp.js +++ b/lib/transports/polling-jsonp.js @@ -5,6 +5,7 @@ var Polling = require('./polling') , util = require('../util') + , global = this /** * Module exports. @@ -18,6 +19,12 @@ module.exports = JSONPPolling; var rNewline = /\n/g +/** + * Global JSONP callbacks. + */ + +var callbacks = global.___eio = {}; + /** * Noop. */ @@ -36,12 +43,12 @@ function JSONPPolling (opts) { // add callback to jsonp global var self = this; - eio.j.push(function (msg) { + callbacks.push(function (msg) { self.onData(msg); }); // append to query string - this.query.j = eio.j.length - 1; + this.query.j = callbacks.length - 1; }; /** From 94bd8effd253705dc1fdc613c8a41130d494ad2e Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Wed, 18 Jan 2012 08:39:58 -0800 Subject: [PATCH 0235/1403] Fixed callbacks data type. --- lib/transports/polling-jsonp.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/transports/polling-jsonp.js b/lib/transports/polling-jsonp.js index 45025ea2..c99cc20c 100644 --- a/lib/transports/polling-jsonp.js +++ b/lib/transports/polling-jsonp.js @@ -23,7 +23,7 @@ var rNewline = /\n/g * Global JSONP callbacks. */ -var callbacks = global.___eio = {}; +var callbacks = global.___eio = []; /** * Noop. From 865ad73dddae89c87818a162e71a25a359f621b0 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Wed, 18 Jan 2012 08:48:16 -0800 Subject: [PATCH 0236/1403] No longer needed with new browserbuild. --- lib/transports/index.js | 1 - lib/transports/polling-jsonp.js | 1 - lib/transports/polling-xhr.js | 1 - lib/transports/polling.js | 1 - lib/transports/websocket.js | 1 - lib/util.js | 6 ------ 6 files changed, 11 deletions(-) diff --git a/lib/transports/index.js b/lib/transports/index.js index 97d2a9d2..ec70c0d0 100644 --- a/lib/transports/index.js +++ b/lib/transports/index.js @@ -8,7 +8,6 @@ var XHR = require('./polling-xhr') , websocket = require('./websocket') , flashsocket = require('./flashsocket') , util = require('../util') - , global = this /** * Export transports. diff --git a/lib/transports/polling-jsonp.js b/lib/transports/polling-jsonp.js index c99cc20c..b1907cbf 100644 --- a/lib/transports/polling-jsonp.js +++ b/lib/transports/polling-jsonp.js @@ -5,7 +5,6 @@ var Polling = require('./polling') , util = require('../util') - , global = this /** * Module exports. diff --git a/lib/transports/polling-xhr.js b/lib/transports/polling-xhr.js index 9e544dac..4c0dbdb0 100644 --- a/lib/transports/polling-xhr.js +++ b/lib/transports/polling-xhr.js @@ -5,7 +5,6 @@ var Polling = require('./polling') , EventEmitter = require('../event-emitter') , util = require('../util') - , global = this /** * Module exports. diff --git a/lib/transports/polling.js b/lib/transports/polling.js index 98df2629..025a2c10 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -5,7 +5,6 @@ var Transport = require('../transport') , util = require('../util') , parser = require('../parser') - , global = this /** * Module exports. diff --git a/lib/transports/websocket.js b/lib/transports/websocket.js index 79e0678c..c767f16d 100644 --- a/lib/transports/websocket.js +++ b/lib/transports/websocket.js @@ -6,7 +6,6 @@ var Transport = require('../transport') , parser = require('../parser') , util = require('../util') - , global = this /** * Module exports. diff --git a/lib/util.js b/lib/util.js index d0bcdc44..9589b094 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1,10 +1,4 @@ -/** - * Reference to global object. - */ - -var global = this; - /** * Status of page load. */ From 9890c5eabb22bdc042d6a510ee7968932ca7954c Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Wed, 18 Jan 2012 10:41:06 -0800 Subject: [PATCH 0237/1403] Fixed style --- lib/parser.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/parser.js b/lib/parser.js index f93f0619..05317615 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -59,8 +59,12 @@ exports.encodePacket = function (packet) { */ exports.decodePacket = function (data) { - var type = data.charAt(0) - if (Number(type) != type || !packetslist[type]) return err; + var type = data.charAt(0); + + if (Number(type) != type || !packetslist[type]) { + return err; + } + if (data.length > 1) { return { type: packetslist[type], data: data.substring(1) }; } else { From aee3fd6fa0f35cdb24845d40f188586a156b04b2 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Thu, 19 Jan 2012 09:46:28 -0800 Subject: [PATCH 0238/1403] Fixed ws compliance in `onmessage`. --- lib/socket.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/socket.js b/lib/socket.js index 7193ad0f..9a86e3e8 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -245,7 +245,7 @@ Socket.prototype.onPacket = function (packet) { case 'message': this.emit('message', packet.data); - this.onmessage && this.onmessage.call(this, packet.data); + this.onmessage && this.onmessage.call(this, { data: packet.data }); break; } } else { From 3290e8bbb40f27e08221d7d36426b445dd8787cd Mon Sep 17 00:00:00 2001 From: Tj Holowaychuk Date: Thu, 2 Feb 2012 16:59:48 -0800 Subject: [PATCH 0239/1403] suppress `make test-browser` cmd` --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 6d5ae9c7..b8ca74c1 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,6 @@ test: $(TESTS) test-browser: - ./node_modules/.bin/serve test/ + @./node_modules/.bin/serve test/ .PHONY: test From 567663fb64c17d4108c3f07cf13d7065f9c5ea61 Mon Sep 17 00:00:00 2001 From: Tj Holowaychuk Date: Thu, 2 Feb 2012 17:01:11 -0800 Subject: [PATCH 0240/1403] make build targets easier to read --- Makefile | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index b8ca74c1..93aa1f5b 100644 --- a/Makefile +++ b/Makefile @@ -3,10 +3,18 @@ TESTS = $(shell find test/*.js -depth 1 -type f ! -name 'common.js') REPORTER = dot build: - @./node_modules/.bin/browserbuild -g eio -f engine.io.js -m engine.io-client lib/ + @./node_modules/.bin/browserbuild \ + -g eio \ + -f engine.io.js \ + -m engine.io-client \ + lib/ build-dev: - @./node_modules/.bin/browserbuild -g eio -f engine.io-dev.js -i -m engine.io-client lib/ + @./node_modules/.bin/browserbuild \ + -g eio \ + -f engine.io-dev.js \ + -i -m engine.io-client \ + lib/ test: @./node_modules/.bin/mocha \ From 9d8f75ff384a9c5461e5e8cae5f39a5a99eaf945 Mon Sep 17 00:00:00 2001 From: Andor Goetzendorff Date: Wed, 15 Feb 2012 16:27:35 +0100 Subject: [PATCH 0241/1403] loop over navigator.plugins instead of using .indexOf, fixes #16 --- lib/transports/flashsocket.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/transports/flashsocket.js b/lib/transports/flashsocket.js index 7bc78c1e..5166ec68 100644 --- a/lib/transports/flashsocket.js +++ b/lib/transports/flashsocket.js @@ -96,8 +96,8 @@ function check () { // end for (var i = 0, l = navigator.plugins.length; i < l; i++) { - if (navigator.plugins[i].indexOf('Shockwave Flash')) { - return true; + for (var j = 0, m = navigator.plugins[i].length; j < m; j++) { + if (navigator.plugins[i][j] == 'Shockwave Flash') return true; } } From d5df4c57dd71148965dca83833b79f74a1b7aa73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lal=20J=C3=A9r=C3=A9my?= Date: Wed, 22 Feb 2012 11:05:47 +0100 Subject: [PATCH 0242/1403] Options should take opts.secure. --- lib/socket.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/socket.js b/lib/socket.js index 9a86e3e8..234bd335 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -1,4 +1,3 @@ - /** * Module dependencies. */ @@ -30,7 +29,7 @@ function Socket (opts) { } opts = opts || {}; - + this.secure = opts.secure || false; this.host = opts.host || opts.hostname || 'localhost'; this.port = opts.port || 80; this.query = opts.query || {}; From 96ac0957f621a2c87f2f5f9ad40100a946908394 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 27 Feb 2012 08:46:45 -0300 Subject: [PATCH 0243/1403] Fixed parser test. --- test/parser.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/parser.js b/test/parser.js index e4f857f2..b7b27897 100644 --- a/test/parser.js +++ b/test/parser.js @@ -73,8 +73,8 @@ describe('parser', function () { }); it('should match the encoding format', function () { - expect(encode({ type: 'message', data: 'test' })).to.match(/[0-9].*/); - expect(encode({ type: 'message' })).to.match(/[0-9].*/); + expect(encode({ type: 'message', data: 'test' })).to.match(/^[0-9]/); + expect(encode({ type: 'message' })).to.match(/^[0-9]$/); }); }); From f651cb45f0e8453a613b6d99449339e03e016ae5 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 27 Feb 2012 08:53:11 -0300 Subject: [PATCH 0244/1403] Added `basePath` option. --- lib/socket.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/socket.js b/lib/socket.js index 234bd335..7c471eef 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -35,7 +35,8 @@ function Socket (opts) { this.query = opts.query || {}; this.query.uid = rnd(); this.upgrade = false !== opts.upgrade; - this.path = opts.path || '/engine.io' + this.basePath = opts.basePath || '/engine.io'; + this.path = opts.path || ''; this.forceJSONP = !!opts.forceJSONP; this.flashPath = opts.flashPath || ''; this.transports = opts.transports || ['polling', 'websocket', 'flashsocket']; @@ -71,7 +72,7 @@ Socket.prototype.createTransport = function (name) { host: this.host , port: this.port , secure: this.secure - , path: this.path + , path: this.basePath + this.path , query: query , forceJSONP: this.forceJSONP , flashPath: this.flashPath From 61c9c4cf0358034a553d6cd55a7abd5070807cfa Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 27 Feb 2012 08:54:04 -0300 Subject: [PATCH 0245/1403] Updated README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d3ffa1c9..06a3619a 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ The client class. _Inherits from EventEmitter_. - `path` (`String`): path name - `query` (`Object`): optional query string addition (eg: `{ a: 'b' }`) - `secure` (`Boolean): whether the connection is secure + - `basePath` (`String`) default prefix path (`/engine.io`) - `upgrade` (`Boolean`): defaults to true, whether the client should try to upgrade the transport from long-polling to something better. - `forceJSONP` (`Boolean`): forces JSONP for polling transport. From f8c56b98f55b92944aac717298953017f4a96160 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 27 Feb 2012 08:57:05 -0300 Subject: [PATCH 0246/1403] Added .travis.yml --- .travis.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..d1e53b1c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: node_js +node_js: + - 0.4 + - 0.6 + +notifications: + irc: "irc.freenode.org#socket.io" From fd0102e4c2664acba1ad40c3e823613e8fb105df Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 27 Feb 2012 08:59:36 -0300 Subject: [PATCH 0247/1403] Added build status image. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 06a3619a..2f99837c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Engine.IO client +[![Build Status](https://secure.travis-ci.org/LearnBoost/engine.io-client.png)](http://travis-ci.org/LearnBoost/engine.io-client) + This is the client for [Engine](http://github.com/learnboost/engine.io), the implementation of transport-based cross-browser/cross-device bi-directional communication layer for [Socket.IO](http://github.com/learnboost/socket.io). From 99306d3fcf699d08ef11092f625687c7bfebb48b Mon Sep 17 00:00:00 2001 From: Andor Goetzendorff Date: Wed, 29 Feb 2012 15:50:04 +0100 Subject: [PATCH 0248/1403] xhr polling does not call onError when xhr.status == 0, refs #21 --- lib/transports/polling-xhr.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/transports/polling-xhr.js b/lib/transports/polling-xhr.js index 4c0dbdb0..6f05111b 100644 --- a/lib/transports/polling-xhr.js +++ b/lib/transports/polling-xhr.js @@ -170,7 +170,7 @@ Request.prototype.create = function () { var data; try { - if (xhr.readyState != 4) return; + if (4 != xhr.readyState || 0 == xhr.status) return; if (200 == xhr.status || 1223 == xhr.status || 204 == xhr.status) { data = xhr.responseText; } else { @@ -180,7 +180,7 @@ Request.prototype.create = function () { self.onError(e); } - if (undefined != data) { + if (undefined !== data) { self.onData(data); } }; From adc67e8da3dc18304251dd4acf33a6863bc2702e Mon Sep 17 00:00:00 2001 From: Jakob Stoeck Date: Thu, 1 Mar 2012 18:45:10 +0100 Subject: [PATCH 0249/1403] replaced control character with space --- lib/socket.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/socket.js b/lib/socket.js index 7c471eef..a30f3fbf 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -29,7 +29,7 @@ function Socket (opts) { } opts = opts || {}; - this.secure = opts.secure || false; + this.secure = opts.secure || false; this.host = opts.host || opts.hostname || 'localhost'; this.port = opts.port || 80; this.query = opts.query || {}; From bcc833275a9f59a8a69de629cc19126ce2409ffe Mon Sep 17 00:00:00 2001 From: Jakob Stoeck Date: Thu, 1 Mar 2012 18:50:01 +0100 Subject: [PATCH 0250/1403] browser compatibility ie7 does not support data[i] but data.charAt(i) --- lib/parser.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/parser.js b/lib/parser.js index 05317615..2b2ae1a1 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -1,4 +1,3 @@ - /** * Module dependencies. */ @@ -120,7 +119,7 @@ exports.decodePayload = function (data) { , n, msg, packet for (var i = 0, l = data.length; i < l; i++) { - var chr = data[i] + var chr = data.charAt(i) if (':' != chr) { length += chr; From 5cf4006321238c0a662ee1721f19944c01650eb3 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Fri, 2 Mar 2012 15:40:47 -0300 Subject: [PATCH 0251/1403] Defined `npm test`. --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 21854f69..27610014 100644 --- a/package.json +++ b/package.json @@ -17,4 +17,5 @@ , "expect.js": "*" , "browserbuild": "*" } + , "scripts" : { "test" : "make test" } } From 94813010c5752ad6b3b2ce4e53d45732577ea340 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Fri, 2 Mar 2012 15:49:28 -0300 Subject: [PATCH 0252/1403] Unbroke make test --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 93aa1f5b..0b656bad 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ build-dev: test: @./node_modules/.bin/mocha \ - --require $(shell pwd)/test/common \ + --require ./test/common \ --reporter $(REPORTER) \ --growl \ $(TESTS) From ef5a514c102a62072ecf1faddc2e7cd2a946d525 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Thu, 8 Mar 2012 10:27:23 -0300 Subject: [PATCH 0253/1403] Fixed JSONP --- lib/transports/polling-jsonp.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/transports/polling-jsonp.js b/lib/transports/polling-jsonp.js index b1907cbf..3e6362ba 100644 --- a/lib/transports/polling-jsonp.js +++ b/lib/transports/polling-jsonp.js @@ -24,6 +24,12 @@ var rNewline = /\n/g var callbacks = global.___eio = []; +/** + * Callbacks count. + */ + +var index = 0; + /** * Noop. */ @@ -40,6 +46,9 @@ function empty () { } function JSONPPolling (opts) { Polling.call(this, opts); + // callback identifier + this.index = index++; + // add callback to jsonp global var self = this; callbacks.push(function (msg) { @@ -133,7 +142,7 @@ JSONPPolling.prototype.doWrite = function (data, fn) { if (!this.form) { var form = document.createElement('form') , area = document.createElement('textarea') - , id = this.iframeId = 'socketio_iframe_' + this.index + , id = this.iframeId = 'eio_iframe_' + this.index , iframe; form.className = 'socketio'; From 722fe8ee0d56c58b209344e4adf9f56345905efe Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Thu, 8 Mar 2012 10:27:23 -0300 Subject: [PATCH 0254/1403] Fixed JSONP (fixes #24) --- lib/transports/polling-jsonp.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/transports/polling-jsonp.js b/lib/transports/polling-jsonp.js index b1907cbf..3e6362ba 100644 --- a/lib/transports/polling-jsonp.js +++ b/lib/transports/polling-jsonp.js @@ -24,6 +24,12 @@ var rNewline = /\n/g var callbacks = global.___eio = []; +/** + * Callbacks count. + */ + +var index = 0; + /** * Noop. */ @@ -40,6 +46,9 @@ function empty () { } function JSONPPolling (opts) { Polling.call(this, opts); + // callback identifier + this.index = index++; + // add callback to jsonp global var self = this; callbacks.push(function (msg) { @@ -133,7 +142,7 @@ JSONPPolling.prototype.doWrite = function (data, fn) { if (!this.form) { var form = document.createElement('form') , area = document.createElement('textarea') - , id = this.iframeId = 'socketio_iframe_' + this.index + , id = this.iframeId = 'eio_iframe_' + this.index , iframe; form.className = 'socketio'; From 046644bd37d88744381474fe53d6f8e51cf7d8ba Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Thu, 8 Mar 2012 12:07:50 -0300 Subject: [PATCH 0255/1403] Fix and simplify readystatechange --- lib/transports/polling-xhr.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/transports/polling-xhr.js b/lib/transports/polling-xhr.js index 6f05111b..875c7a35 100644 --- a/lib/transports/polling-xhr.js +++ b/lib/transports/polling-xhr.js @@ -170,8 +170,8 @@ Request.prototype.create = function () { var data; try { - if (4 != xhr.readyState || 0 == xhr.status) return; - if (200 == xhr.status || 1223 == xhr.status || 204 == xhr.status) { + if (4 != xhr.readyState) return; + if (200 == xhr.status || 1223 == xhr.status) { data = xhr.responseText; } else { self.onError(xhr.status); From 2f7812ece510a2d2d8e51284f702410d94cbe70f Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Thu, 8 Mar 2012 12:28:26 -0300 Subject: [PATCH 0256/1403] Revision 1 of SPEC --- SPEC | 153 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 SPEC diff --git a/SPEC b/SPEC new file mode 100644 index 00000000..50620350 --- /dev/null +++ b/SPEC @@ -0,0 +1,153 @@ + +# Engine.IO SPEC + +## Revision + +This is revision **1** of the Engine.IO protocol. + +## Anatomy of an Engine.IO session + +1. Transport establishes a connection to the Engine.IO URL . +2. Server responds with an `open` packet with JSON-encoded handshake data: + - `sid` session id (`String`) + - `upgrades` possible transport upgrades (`Array` of `String`) + - `pingTimeout` server configured ping timeout, used for the client + to detect that the server is unresponsive (`Number`) +3. Client must respond to periodic `ping` packets sent by the server +with `pong` packets. +4. Client and server can exchange `message` packets at will. +5. Polling transports can send a `close` packet to close the socket, since +they're expected to be "opening" and "closing" all the time. + +## URLs + +An Engine.IO url is composed as follows: + +`/engine.io` ? + +The query string has three reserved keys: + +- `transport`: indicates the transport name. Supported ones by default are + `polling`, `flashsocket`, `websocket`. +- `j`: if the transport is `polling` but a JSONP response is required, `j` + must be set with the JSONP response index. +- `sid`: if the client has been given a session id, it must be included + in the querystring. + +## Encoding + +There's two distinct types of encodings + +- packet +- payload + +### Packet + +The packet encoding format is as follows + +``` + [ `:` ] +``` + +The packet type id is an integer. The following are the accepted packet +types. + +``` +open 0 +close 1 +ping 2 +pong 3 +message 4 +upgrade 5 +``` + +An encoded packet is a UTF-8 string. + +### Payload + +A payload is a series of encoded packets tied together. + +## Transports + +An engine.io server must support three transports: + +- websocket +- flashsocket +- polling + - jsonp + - xhr + +### Polling + +The polling transport consists of recurring GET requests by the client +to the server to get data, and POST requests with payloads from the +client to the server to send data. + +#### XHR + +The server must support CORS responses. + +#### JSONP + +The server implementation must respond with valid JavaScript. The URL +contains a query string parameter `j` that must be used in the response. +`j` is an integer. + +The format of a JSONP packet. + +``` +`___eio[` `]("` `");` +``` + +To ensure that the payload gets processed correctly, it must be escaped +in such a way that the response is still valid JavaScript. Passing the +encoded payload through a JSON encoder is a good way to escape it. + +Example JSONP frame returned by the server: + +``` +___eio[4]("packet data"); +``` + +##### Posting data + +The client posts data through a hidden iframe. The data gets to the server +in the URI encoded format as follows: + +``` +d= +``` + +In addition to the regular qs escaping, in order to prevent +inconsistencies with `\n` handling by browsers, `\n` gets escaped as `\\n` +prior to being POSTd. + +### WebSocket + +Encoding payloads _should not_ be used for WebSocket, as the protocol +already has a lightweight framing mechanism. + +In order to send a payload of messages, encode packets individually +and `send()` them in succession. + +## Transport upgrading + +A connection always starts with polling (either XHR or JSONP). WebSocket +gets tested on the side by sending a probe. If the probe is responded +from the server, an upgrade packet is sent. + +To ensure no messages are lost, the upgrade packet will only be sent +once all the buffers of the existing transport are flushed and the +transport is considered _paused_. + +When the server receives the upgrade packet, it must assume this is the +new transport channel and send all existing buffers (if any) to it. + +The probe sent by the client is a `ping` packet with `probe` sent as data. +The probe sent by the server is a `pong` packet with `probe` sent as data. + +Moving forward, upgrades other than just `polling -> x` are being considered. + +## Timeouts + +The client must use the `pingTimeout` sent as part of the From 7d3b70aaa9885ad89fcd7da41b444b2454b6e1c3 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Thu, 8 Mar 2012 12:29:23 -0300 Subject: [PATCH 0257/1403] Added .md --- SPEC => SPEC.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename SPEC => SPEC.md (100%) diff --git a/SPEC b/SPEC.md similarity index 100% rename from SPEC rename to SPEC.md From dd445fc1bb138d066bcadd4437df0cc61b8ed6c8 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sat, 10 Mar 2012 11:57:26 -0300 Subject: [PATCH 0258/1403] Fixed timeouts section. --- SPEC.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/SPEC.md b/SPEC.md index 50620350..6d110936 100644 --- a/SPEC.md +++ b/SPEC.md @@ -150,4 +150,8 @@ Moving forward, upgrades other than just `polling -> x` are being considered. ## Timeouts -The client must use the `pingTimeout` sent as part of the +The client must use the `pingTimeout` sent as part of the handshake (with +the `open` packet) to determine whether the server is unresponsive. + +If no packet type is received withing `pingTimeout`, the client considers +the socket disconnected. From 8b5147479866c7cc761dbe8fb9f61baeb4158ca9 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Wed, 14 Mar 2012 12:56:32 -0300 Subject: [PATCH 0259/1403] Removed growl --- Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile b/Makefile index 0b656bad..24569d76 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,6 @@ test: @./node_modules/.bin/mocha \ --require ./test/common \ --reporter $(REPORTER) \ - --growl \ $(TESTS) test-browser: From d71d9117ec804b03b81f0a1873e64fcbf07db173 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 25 Mar 2012 10:48:46 -0300 Subject: [PATCH 0260/1403] Fixed Makefile for latest browserbuild. --- Makefile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 24569d76..6f35cbab 100644 --- a/Makefile +++ b/Makefile @@ -2,19 +2,19 @@ TESTS = $(shell find test/*.js -depth 1 -type f ! -name 'common.js') REPORTER = dot +all: build build-dev + build: @./node_modules/.bin/browserbuild \ -g eio \ - -f engine.io.js \ -m engine.io-client \ - lib/ + lib/ > dist/engine.io.js build-dev: @./node_modules/.bin/browserbuild \ -g eio \ - -f engine.io-dev.js \ - -i -m engine.io-client \ - lib/ + -d -m engine.io-client \ + lib/ > dist/engine.io-dev.js test: @./node_modules/.bin/mocha \ From 0162484b16ee1b42bb7d4d43e3042de253767604 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 25 Mar 2012 11:10:46 -0300 Subject: [PATCH 0261/1403] Added basedir to browserbuild commands (fixes #26) --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 6f35cbab..f1544545 100644 --- a/Makefile +++ b/Makefile @@ -7,13 +7,13 @@ all: build build-dev build: @./node_modules/.bin/browserbuild \ -g eio \ - -m engine.io-client \ + -m engine.io-client -b lib/ \ lib/ > dist/engine.io.js build-dev: @./node_modules/.bin/browserbuild \ -g eio \ - -d -m engine.io-client \ + -d -m engine.io-client -b lib/ \ lib/ > dist/engine.io-dev.js test: From 41ea3b728cc3232d791e9e8fc9527a685764f50f Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 25 Mar 2012 11:20:22 -0300 Subject: [PATCH 0262/1403] Fixes double / --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index f1544545..65b6d4ea 100644 --- a/Makefile +++ b/Makefile @@ -8,13 +8,13 @@ build: @./node_modules/.bin/browserbuild \ -g eio \ -m engine.io-client -b lib/ \ - lib/ > dist/engine.io.js + lib > dist/engine.io.js build-dev: @./node_modules/.bin/browserbuild \ -g eio \ -d -m engine.io-client -b lib/ \ - lib/ > dist/engine.io-dev.js + lib > dist/engine.io-dev.js test: @./node_modules/.bin/mocha \ From aa3bfeabf99100358ed3e679d041c97aeaf8f672 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Wed, 28 Mar 2012 11:50:13 -0300 Subject: [PATCH 0263/1403] Stop ignoring dist. --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index b1866059..fd4f2b06 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ -dist node_modules .DS_Store From 2c0c4547d7c5e44c8aaecbe0281f5a891b4e78ea Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Wed, 28 Mar 2012 11:50:25 -0300 Subject: [PATCH 0264/1403] Added `dist/README` (fixes #16). --- dist/README | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 dist/README diff --git a/dist/README b/dist/README new file mode 100644 index 00000000..cdc538a7 --- /dev/null +++ b/dist/README @@ -0,0 +1,5 @@ + +Engine.IO client builds (engine.io.js and engine.io-dev.js) are committed +to GitHub alongside each tagged release. + +In regular branches, expect them to be out of date. From 93605fd3e950d68130900e37ce0beeceef8eea5c Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 2 Apr 2012 12:16:18 -0300 Subject: [PATCH 0265/1403] Implemented resource alongside with path. --- lib/socket.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/socket.js b/lib/socket.js index a30f3fbf..8c70c56a 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -35,8 +35,9 @@ function Socket (opts) { this.query = opts.query || {}; this.query.uid = rnd(); this.upgrade = false !== opts.upgrade; - this.basePath = opts.basePath || '/engine.io'; - this.path = opts.path || ''; + this.resource = opts.resource || 'default'; + this.path = (opts.path || '/engine.io').replace(/\/$/, ''); + this.path += '/' + this.resource + '/'; this.forceJSONP = !!opts.forceJSONP; this.flashPath = opts.flashPath || ''; this.transports = opts.transports || ['polling', 'websocket', 'flashsocket']; @@ -72,7 +73,7 @@ Socket.prototype.createTransport = function (name) { host: this.host , port: this.port , secure: this.secure - , path: this.basePath + this.path + , path: this.path , query: query , forceJSONP: this.forceJSONP , flashPath: this.flashPath From 6eeadf3b20230909f1988b67f8bc1a41b3e79a82 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 2 Apr 2012 12:16:28 -0300 Subject: [PATCH 0266/1403] Updated docs. --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2f99837c..4f382b1e 100644 --- a/README.md +++ b/README.md @@ -84,10 +84,13 @@ The client class. _Inherits from EventEmitter_. - **Options** - `host` (`String`): host name (`localhost`) - `port` (`Number`): port name (`80`) - - `path` (`String`): path name + - `path` (`String`): path to intercept requests to (`/engine.io`) + - `resource` (`String`): name of resource for this server (`default`). + Setting a resource allows you to initialize multiple engine.io + endpoints on the same host without them interfering, and without + changing the `path` directly. - `query` (`Object`): optional query string addition (eg: `{ a: 'b' }`) - `secure` (`Boolean): whether the connection is secure - - `basePath` (`String`) default prefix path (`/engine.io`) - `upgrade` (`Boolean`): defaults to true, whether the client should try to upgrade the transport from long-polling to something better. - `forceJSONP` (`Boolean`): forces JSONP for polling transport. From 244dba26bdd2453e7b874494f6bc78705b52ab49 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 2 Apr 2012 12:55:53 -0300 Subject: [PATCH 0267/1403] Adapted code to leverage debug() --- lib/socket.js | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/lib/socket.js b/lib/socket.js index 8c70c56a..a2c9eaf3 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -4,6 +4,7 @@ var util = require('./util') , transports = require('./transports') + , debug = require('debug')('engine-client:socket') , EventEmitter = require('./event-emitter') /** @@ -61,7 +62,7 @@ util.inherits(Socket, EventEmitter); */ Socket.prototype.createTransport = function (name) { - // debug: creating transport "%s", name + debug('creating transport "%s"', name); var query = clone(this.query) query.transport = name; @@ -115,7 +116,7 @@ Socket.prototype.setTransport = function (transport) { var self = this; if (this.transport) { - // debug: clearing existing transport + debug('clearing existing transport'); this.transport.removeAllListeners(); } @@ -146,23 +147,23 @@ Socket.prototype.setTransport = function (transport) { */ Socket.prototype.probe = function (name) { - // debug: probing transport "%s", name + debug('probing transport "%s"', name); var transport = this.createTransport(name, { probe: 1 }) , self = this transport.once('open', function () { - // debug: probe transport "%s" opened - pinging, name + debug('probe transport "%s" opened', name); transport.send([{ type: 'ping', data: 'probe' }]); transport.once('packet', function (msg) { if ('pong' == msg.type && 'probe' == msg.data) { - // debug: probe transport "%s" pong - upgrading, name + debug('probe transport "%s" pong', name); self.upgrading = true; self.emit('upgrading', transport); - // debug: pausing current transport "%s", self.transport.name + debug('pausing current transport "%s"', self.transport.name); self.transport.pause(function () { if ('closed' == self.readyState || 'closing' == self.readyState) return; - // debug: changing transport and sending upgrade packet + debug('changing transport and sending upgrade packet'); self.emit('upgrade', transport); self.setTransport(transport); transport.send([{ type: 'upgrade' }]); @@ -171,7 +172,7 @@ Socket.prototype.probe = function (name) { self.flush(); }); } else { - // debug: probe transport "%s" failed, name + debug('probe transport "%s" failed', name); var err = new Error('probe error'); err.transport = transport.name; self.emit('error', err); @@ -183,7 +184,7 @@ Socket.prototype.probe = function (name) { this.once('close', function () { if (transport) { - // debug: socket closed prematurely - aborting probe + debug('socket closed prematurely - aborting probe'); transport.close(); transport = null; } @@ -191,7 +192,7 @@ Socket.prototype.probe = function (name) { this.once('upgrading', function (to) { if (transport && to.name != transport.name) { - // debug: "%s" works - aborting "%s", to.name, transport.name + debug('"%s" works - aborting "%s"', to.name, transport.name); transport.close(); transport = null; } @@ -205,14 +206,14 @@ Socket.prototype.probe = function (name) { */ Socket.prototype.onOpen = function () { - // debug: socket open + debug('socket open'); this.readyState = 'open'; this.emit('open'); this.onopen && this.onopen.call(this); this.flush(); if (this.upgrade && this.transport.pause) { - // debug: starting upgrade probes + debug('starting upgrade probes'); for (var i = 0, l = this.upgrades.length; i < l; i++) { this.probe(this.upgrades[i]); } @@ -227,7 +228,7 @@ Socket.prototype.onOpen = function () { Socket.prototype.onPacket = function (packet) { if ('opening' == this.readyState || 'open' == this.readyState) { - // debug: socket receive: type "%s" | data "%s", packet.type, packet.data + debug('socket receive: type "%s", data "%s"', packet.type, packet.data); switch (packet.type) { case 'open': this.onHandshake(util.parseJSON(packet.data)); @@ -250,7 +251,7 @@ Socket.prototype.onPacket = function (packet) { break; } } else { - // debug: packet received with socket readyState "%s", this.readyState + debug('packet received with socket readyState "%s"', this.readyState); } }; @@ -294,7 +295,7 @@ Socket.prototype.setPingTimeout = function () { Socket.prototype.flush = function () { if ('closed' != this.readyState && this.transport.writable && !this.upgrading && this.writeBuffer.length) { - // debug: flushing %d packets in socket, this.writeBuffer.length + debug('flushing %d packets in socket', this.writeBuffer.length); this.transport.send(this.writeBuffer); this.writeBuffer = []; } @@ -336,7 +337,7 @@ Socket.prototype.sendPacket = function (type, data) { Socket.prototype.close = function () { if ('opening' == this.readyState || 'open' == this.readyState) { this.onClose('forced close'); - // debug: socket closing - telling transport to close + debug('socket closing - telling transport to close'); this.transport.close(); } @@ -362,7 +363,7 @@ Socket.prototype.onError = function (err) { Socket.prototype.onClose = function (reason, desc) { if ('closed' != this.readyState) { - // debug: socket close: "%s", reason + debug('socket close with reason: "%s"', reason); this.readyState = 'closed'; this.emit('close', reason, desc); this.onclose && this.onclose.call(this); From e23fe8101055bb326ba221f2899c48f677c35a9f Mon Sep 17 00:00:00 2001 From: Enno Boland Date: Tue, 3 Apr 2012 11:31:14 +0300 Subject: [PATCH 0268/1403] Update spec. --- SPEC.md | 64 ++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/SPEC.md b/SPEC.md index 6d110936..4e91dc19 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,4 +1,3 @@ - # Engine.IO SPEC ## Revision @@ -43,29 +42,66 @@ There's two distinct types of encodings ### Packet -The packet encoding format is as follows +An encoded packet is a UTF-8 string. The packet encoding format is as follows ``` - [ `:` ] +[] +``` +example: +``` +2probe ``` - The packet type id is an integer. The following are the accepted packet types. -``` -open 0 -close 1 -ping 2 -pong 3 -message 4 -upgrade 5 -``` +#### 0 open -An encoded packet is a UTF-8 string. +Sent from the server when a new transport is opened (recheck) +#### 1 close + +Request the close of this transport but does not shutdown the connection itself. + +#### 2 ping + +send by the server. Client should answer with a pong package, containing the same data + +example +1. server sends: ```2probe``` +2. client sends: ```3probe``` + +#### 3 pong + +send by the client to respond to ping packages. + +#### 4 message + +actual message, client and server should call their callbacks with the data. + +##### example 1 + +1. server sends: ```4HelloWorld``` +2. client receives and calls callback ```socket.on('message', function (data) { console.log(data); });``` + +##### example 2 + +1. client sends: ```4HelloWorld``` +2. server receives and calls callback ```socket.on('message', function (data) { console.log(data); });``` + +#### 5 upgrade + +Requests client to upgrade. ### Payload -A payload is a series of encoded packets tied together. +A payload is a series of encoded packets tied together. The payload encoding format is as follows: + +``` +[[...]] +``` +* length: length of the packet in __characters__ +* packet: actual package as descriped above + +The payload is used for transports which do not support framing, as the polling protocol for example. ## Transports From 0c1e1ace7017eb5b3e064791970b9d2af0dda3ab Mon Sep 17 00:00:00 2001 From: Enno Boland Date: Tue, 3 Apr 2012 11:49:19 +0300 Subject: [PATCH 0269/1403] Update SPEC.md, add documentation to 5 upgrade. May have some redundancy. --- SPEC.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/SPEC.md b/SPEC.md index 4e91dc19..7c1e1823 100644 --- a/SPEC.md +++ b/SPEC.md @@ -90,7 +90,17 @@ actual message, client and server should call their callbacks with the data. #### 5 upgrade -Requests client to upgrade. +Before engine.io switches a transport, it tests, if server and client can communicate over this transport. +If this test succeed, the client sends an upgrade package which requests the server to flush its cache on +the old transport and switch to the new transport. + +##### example +1. client connects through new transport +2. client sends ```2probe``` +3. server receives and sends ```3probe``` +4. client receives and sends ```5``` +5. server flushes and closes old transport and switches to new. + ### Payload A payload is a series of encoded packets tied together. The payload encoding format is as follows: From 2c8ed0d458a710f97a6dfb0325dd594261736d41 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Apr 2012 08:08:05 -0300 Subject: [PATCH 0270/1403] Added debug dependency. --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 27610014..b1d25aa1 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ , "dependencies": { "ws": "0.4.0" , "xmlhttprequest": "1.3.0" + , "debug": "0.6.0" } , "devDependencies": { "mocha": "*" From 78f2471e29b8e00637f9fa25dfbdd6bb263b5da5 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Apr 2012 08:25:57 -0300 Subject: [PATCH 0271/1403] Improved and updated URL section. --- SPEC.md | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/SPEC.md b/SPEC.md index 7c1e1823..61de3da5 100644 --- a/SPEC.md +++ b/SPEC.md @@ -22,16 +22,34 @@ they're expected to be "opening" and "closing" all the time. An Engine.IO url is composed as follows: -`/engine.io` ? +`/engine.io/` `/` [ ? ] -The query string has three reserved keys: +- The resource allows to connect several independent sockets to the same + server listening on a given port/host. The default is `default`. It's + always required. -- `transport`: indicates the transport name. Supported ones by default are - `polling`, `flashsocket`, `websocket`. -- `j`: if the transport is `polling` but a JSONP response is required, `j` - must be set with the JSONP response index. -- `sid`: if the client has been given a session id, it must be included - in the querystring. +- The query string is optional and has three reserved keys: + + - `transport`: indicates the transport name. Supported ones by default are + `polling`, `flashsocket`, `websocket`. + - `j`: if the transport is `polling` but a JSONP response is required, `j` + must be set with the JSONP response index. + - `sid`: if the client has been given a session id, it must be included + in the querystring. + +*FAQ:* Is the `/engine.io` portion modifiable? + +Provided the server is customized to intercept requests under a different +path segment, yes. + +*FAQ:* What determines whether an option is going to be part of the path +versus being encoded as part of the query string? In other words, why +is the `transport` not part of the URL? + +It's convention that the path segments remain *only* that which allows to +disambiguate whether a request should be handled by a given Engine.IO +server instance or not. As it stands, it's only the Engine.IO prefix +(`/engine.io`) and the resource (`default` by default). ## Encoding From 3a152c7997fefd3efeeb0a3204773f9ee5eb5600 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 17 Apr 2012 16:56:00 -0300 Subject: [PATCH 0272/1403] Removed XHR specific cache busting. --- lib/transports/polling-xhr.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/transports/polling-xhr.js b/lib/transports/polling-xhr.js index 875c7a35..31eb1acf 100644 --- a/lib/transports/polling-xhr.js +++ b/lib/transports/polling-xhr.js @@ -33,11 +33,6 @@ function XHR (opts) { this.xd = opts.host != global.location.hostname || global.location.port != opts.port; } - - // cache busting for IE - if (global.ActiveXObject) { - this.query.t = +new Date; - } }; /** From 8d98a3359e9f74760ec12dd4f088065eb9e032fb Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 17 Apr 2012 16:56:14 -0300 Subject: [PATCH 0273/1403] Added cache busting to all polling transports Also added forceBust option --- lib/transports/polling.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/transports/polling.js b/lib/transports/polling.js index 025a2c10..4b6ca989 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -21,6 +21,7 @@ module.exports = Polling; function Polling (opts) { Transport.call(this, opts); + this.forceBust = !!opts.forceBust; } /** @@ -175,10 +176,17 @@ Polling.prototype.write = function (packets) { */ Polling.prototype.uri = function () { - var query = util.qs(this.query) + var query = this.query || {} , schema = this.secure ? 'https' : 'http' , port = '' + // cache busting for IE + if (global.ActiveXObject || this.forceBust) { + query.t = +new Date; + } + + query = util.qs(query); + // avoid port if default for schema if (this.port && (('https' == schema && this.port != 443) || ('http' == schema && this.port != 80))) { From 995d5e040030cccbff9b17e7bbe8484e8ec601e6 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 17 Apr 2012 16:56:44 -0300 Subject: [PATCH 0274/1403] Added cache busting test. --- test/transport.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/transport.js b/test/transport.js index e38395b4..10d5ba6c 100644 --- a/test/transport.js +++ b/test/transport.js @@ -58,6 +58,15 @@ describe('Transport', function () { expect(polling.uri()).to.be('https://localhost/engine.io?sid=test'); }); + it('should generate an uri with a t attribute', function () { + var polling = new eio.transports.polling({ + path: '/engine.io' + , host: 'localhost' + , forceBust: true + }); + expect(polling.uri()).to.match(/http:\/\/localhost\/engine\.io\?t=[0-9]+/); + }); + it('should generate a ws uri', function () { var ws = new eio.transports.websocket({ path: '/engine.io' From dbc65f3b5ac1edf25d629636b773856eb28c974a Mon Sep 17 00:00:00 2001 From: <> Date: Fri, 18 May 2012 13:20:53 +0300 Subject: [PATCH 0275/1403] added flashsocket transport logic --- lib/socket.js | 2 + lib/transports/flashsocket.js | 94 +++++++++++++++++++++++++++++------ lib/transports/websocket.js | 29 ++++++----- 3 files changed, 98 insertions(+), 27 deletions(-) diff --git a/lib/socket.js b/lib/socket.js index a2c9eaf3..d0ddb363 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -44,6 +44,7 @@ function Socket (opts) { this.transports = opts.transports || ['polling', 'websocket', 'flashsocket']; this.readyState = ''; this.writeBuffer = []; + this.policyPort = opts.policyPort; this.open(); }; @@ -78,6 +79,7 @@ Socket.prototype.createTransport = function (name) { , query: query , forceJSONP: this.forceJSONP , flashPath: this.flashPath + , policyPort: this.policyPort }); return transport; diff --git a/lib/transports/flashsocket.js b/lib/transports/flashsocket.js index 5166ec68..38fc7ed7 100644 --- a/lib/transports/flashsocket.js +++ b/lib/transports/flashsocket.js @@ -3,7 +3,7 @@ * Module dependencies. */ -var WebSocket = require('./websocket') +var WS = require('./websocket') , util = require('../util') /** @@ -19,15 +19,16 @@ module.exports = FlashWS; */ function FlashWS (options) { - WebSocket.call(this, options); + WS.call(this, options); this.flashPath = options.flashPath; + this.policyPort = options.policyPort; }; /** * Inherits from WebSocket. */ -util.inherits(FlashWS, WebSocket); +util.inherits(FlashWS, WS); /** * Transport name. @@ -44,7 +45,7 @@ FlashWS.prototype.name = 'flashsocket'; */ FlashWS.prototype.doOpen = function () { - if (!check()) { + if (!this.check()) { // let the probe timeout return; } @@ -59,16 +60,23 @@ FlashWS.prototype.doOpen = function () { WEB_SOCKET_LOGGER = { log: log('debug'), error: log('error') }; WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR = true; + WEB_SOCKET_DISABLE_AUTO_INITIALIZATION = true; // dependencies - var deps = [path + 'web_socket.js']; + var deps = [this.flashPath + 'web_socket.js']; if ('undefined' == typeof swfobject) { - deps.unshift(path + 'swfobject.js'); + deps.unshift(this.flashPath + 'swfobject.js'); } + var self = this; + load(deps, function () { - FlashWS.prototype.doOpen.call(self); + self.ready(function () { + WebSocket.__addTask(function () { + WS.prototype.doOpen.call(self); + }); + }); }); }; @@ -80,24 +88,82 @@ FlashWS.prototype.doOpen = function () { FlashWS.prototype.doClose = function () { if (!this.socket) return; - FlashWS.prototype.doClose.call(this); + var self = this; + WebSocket.__addTask(function() { + WS.prototype.doClose.call(self); + }); +}; + +FlashWS.prototype.write = function() { + var self = this, args = arguments; + WebSocket.__addTask(function () { + WS.prototype.write.apply(self, args); + }); +}; + +FlashWS.prototype.ready = function (fn) { + if (typeof WebSocket == 'undefined' + || !('__initialize' in WebSocket) || !swfobject + ) { + return; + } + + if (swfobject.getFlashPlayerVersion().major < 10) { + return; + } + + function init () { + + // Only start downloading the swf file when the checked that this browser + // actually supports it + if (!FlashWS.loaded) { + + if (self.policyPort !== 843) { + WebSocket.loadFlashPolicyFile('xmlsocket://' + self.host + ':' + self.policyPort); + } + + WebSocket.__initialize(); + FlashWS.loaded = true; + } + + fn.call(self); + } + + var self = this; + if (document.body) return init(); + + util.load(init); }; /** - * Feature detection for FlashSocket. + * Feature detection for flashsocket. * * @return {Boolean} whether this transport is available. * @api public */ -function check () { +FlashWS.prototype.check = function () { // if node return false; // end - for (var i = 0, l = navigator.plugins.length; i < l; i++) { - for (var j = 0, m = navigator.plugins[i].length; j < m; j++) { - if (navigator.plugins[i][j] == 'Shockwave Flash') return true; + if (typeof WebSocket != 'undefined' && !('__initialize' in WebSocket)) + return false; + + if (window.ActiveXObject) { + var control = null; + try { + control = new ActiveXObject('ShockwaveFlash.ShockwaveFlash'); + } catch (e) { } + if (control) { + return true; + } + } + else { + for (var i = 0, l = navigator.plugins.length; i < l; i++) { + for (var j = 0, m = navigator.plugins[i].length; j < m; j++) { + if (navigator.plugins[i][j].description == 'Shockwave Flash') return true; + } } } @@ -156,7 +222,7 @@ function load (arr, fn) { function process (i) { if (!arr[i]) return fn(); create(arr[i], function () { - process(arr[++i]); + process(++i); }); }; diff --git a/lib/transports/websocket.js b/lib/transports/websocket.js index c767f16d..e1b64d9c 100644 --- a/lib/transports/websocket.js +++ b/lib/transports/websocket.js @@ -45,7 +45,7 @@ WS.prototype.name = 'websocket'; */ WS.prototype.doOpen = function () { - if (!check()) { + if (!this.check()) { // let probe timeout return; } @@ -88,7 +88,9 @@ WS.prototype.write = function (packets) { */ WS.prototype.doClose = function () { - this.socket.close(); + if (typeof this.socket !== 'undefined') { + this.socket.close(); + } }; /** @@ -116,6 +118,18 @@ WS.prototype.uri = function () { return schema + '://' + this.host + port + this.path + query; }; +/** + * Feature detection for WebSocket. + * + * @return {Boolean} whether this transport is available. + * @api public + */ + +WS.prototype.check = function () { + var websocket = ws(); + return !!websocket && !('__initialize' in websocket && this.name === WS.prototype.name); +} + /** * Getter for WS constructor. * @@ -129,14 +143,3 @@ function ws () { return global.WebSocket || global.MozWebSocket; } - -/** - * Feature detection for WebSocket. - * - * @return {Boolean} whether this transport is available. - * @api public - */ - -function check () { - return !!ws(); -} From fddcec0ea081b97e690958d9eb56e2e1b1705019 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 5 Jun 2012 22:55:22 +0200 Subject: [PATCH 0276/1403] Fixed xhr polling in IE6. --- lib/transports/polling-xhr.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/transports/polling-xhr.js b/lib/transports/polling-xhr.js index 31eb1acf..e4ff4b9d 100644 --- a/lib/transports/polling-xhr.js +++ b/lib/transports/polling-xhr.js @@ -160,7 +160,11 @@ Request.prototype.create = function () { }; xhr.onprogress = empty; } else { - xhr.withCredentials = true; + // ie6 check + if ('withCredentials' in xhr) { + xhr.withCredentials = true; + } + xhr.onreadystatechange = function () { var data; From 85e86ba9c4b63275e7d1cf85e7c94e06a9fcaa79 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Wed, 27 Jun 2012 22:09:37 -0700 Subject: [PATCH 0277/1403] Bumped ws --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b1d25aa1..f871d794 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ , { "name": "Vladimir Dronnikov", "email": "dronnikov@gmail.com" } ] , "dependencies": { - "ws": "0.4.0" + "ws": "0.4.18" , "xmlhttprequest": "1.3.0" , "debug": "0.6.0" } From 75a1304f8bbfd824752f5dbf6041928aa8c8f878 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 1 Jul 2012 18:14:41 -0700 Subject: [PATCH 0278/1403] Bumped `xmlhttprequest` dependency version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f871d794..018d9450 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ ] , "dependencies": { "ws": "0.4.18" - , "xmlhttprequest": "1.3.0" + , "xmlhttprequest": "1.4.2" , "debug": "0.6.0" } , "devDependencies": { From 2aed814cf74994498790fbf5e5fb4c9b4966b138 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Sun, 1 Jul 2012 19:06:34 -0700 Subject: [PATCH 0279/1403] parser: added `noop` packet --- SPEC.md | 4 ++++ lib/parser.js | 1 + 2 files changed, 5 insertions(+) diff --git a/SPEC.md b/SPEC.md index 61de3da5..c48eb1fb 100644 --- a/SPEC.md +++ b/SPEC.md @@ -112,6 +112,10 @@ Before engine.io switches a transport, it tests, if server and client can commun If this test succeed, the client sends an upgrade package which requests the server to flush its cache on the old transport and switch to the new transport. +#### 6 noop + +A noop packet. Used primarily to force a poll cycle when an incoming websocket connection is received. + ##### example 1. client connects through new transport 2. client sends ```2probe``` diff --git a/lib/parser.js b/lib/parser.js index 2b2ae1a1..2c2928e8 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -15,6 +15,7 @@ var packets = exports.packets = { , pong: 3 , message: 4 , upgrade: 5 + , noop: 6 }; var packetslist = util.keys(packets); From 89a1e010c7934cb6ae72825196d32efae0f76974 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 2 Jul 2012 07:43:35 -0700 Subject: [PATCH 0280/1403] socket: added `toString` to fake event sent to `onmessage` --- lib/socket.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/socket.js b/lib/socket.js index d0ddb363..c106fde9 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -249,7 +249,11 @@ Socket.prototype.onPacket = function (packet) { case 'message': this.emit('message', packet.data); - this.onmessage && this.onmessage.call(this, { data: packet.data }); + var event = { data: packet.data }; + event.toString = function () { + return packet.data; + } + this.onmessage && this.onmessage.call(this, event); break; } } else { From 7d514dabfc078a7251078885476e1cc71af1c46a Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 2 Jul 2012 08:00:04 -0700 Subject: [PATCH 0281/1403] docs: improved flash-related docs --- README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4f382b1e..4bc80e80 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,8 @@ The client class. _Inherits from EventEmitter_. - `upgrade` (`Boolean`): defaults to true, whether the client should try to upgrade the transport from long-polling to something better. - `forceJSONP` (`Boolean`): forces JSONP for polling transport. - - `flashPath` (`String`): path to flash client files + - `flashPath` (`String`): path to flash client files with trailing slash + - `policyPort` (`Number`): port the policy server listens on (`843`) - `transports` (`Array`): a list of transports to try (in order). Defaults to `['polling', 'websocket', 'flashsocket']`. `Engine` always attempts to connect directly with the first one, provided the @@ -108,11 +109,9 @@ The client class. _Inherits from EventEmitter_. ## Flash transport -In order for the Flash transport to work correctly, set the following: - -```js -WEB_SOCKET_SWF_LOCATION = '/path/to/WebSocketMainInsecure.swf'; -``` +In order for the Flash transport to work correctly, ensure the `flashPath` +property points to the location where the files `web_socket.js`, +`swfobject.js` and `WebSocketMainInsecure.swf` are located. ## Tests From ece335ea17490e8967b70374ee3548eedf0bf696 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 2 Jul 2012 08:00:17 -0700 Subject: [PATCH 0282/1403] socket: default policyPort to 843 --- lib/socket.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/socket.js b/lib/socket.js index c106fde9..34f0a650 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -44,7 +44,7 @@ function Socket (opts) { this.transports = opts.transports || ['polling', 'websocket', 'flashsocket']; this.readyState = ''; this.writeBuffer = []; - this.policyPort = opts.policyPort; + this.policyPort = opts.policyPort || 843; this.open(); }; From a23387ef8778a4d072098d47b5511211d11efadc Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 2 Jul 2012 08:01:58 -0700 Subject: [PATCH 0283/1403] flashsocket: auto-populate SWF location based on flashPath --- lib/transports/flashsocket.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/transports/flashsocket.js b/lib/transports/flashsocket.js index 38fc7ed7..9f365b97 100644 --- a/lib/transports/flashsocket.js +++ b/lib/transports/flashsocket.js @@ -62,6 +62,10 @@ FlashWS.prototype.doOpen = function () { WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR = true; WEB_SOCKET_DISABLE_AUTO_INITIALIZATION = true; + if ('undefined' == typeof WEB_SOCKET_SWF_LOCATION) { + WEB_SOCKET_SWF_LOCATION = this.flashPath + 'WebSocketMainInsecure.swf'; + } + // dependencies var deps = [this.flashPath + 'web_socket.js']; From eeb2b160589e1b12c5c96329a4f34858e3453a67 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 2 Jul 2012 08:02:23 -0700 Subject: [PATCH 0284/1403] flashsocket: fixed docs & style --- lib/transports/flashsocket.js | 36 ++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/lib/transports/flashsocket.js b/lib/transports/flashsocket.js index 9f365b97..ce71c1d5 100644 --- a/lib/transports/flashsocket.js +++ b/lib/transports/flashsocket.js @@ -77,9 +77,9 @@ FlashWS.prototype.doOpen = function () { load(deps, function () { self.ready(function () { - WebSocket.__addTask(function () { - WS.prototype.doOpen.call(self); - }); + WebSocket.__addTask(function () { + WS.prototype.doOpen.call(self); + }); }); }); }; @@ -98,6 +98,12 @@ FlashWS.prototype.doClose = function () { }); }; +/** + * Writes to the Flash socket. + * + * @api private + */ + FlashWS.prototype.write = function() { var self = this, args = arguments; WebSocket.__addTask(function () { @@ -105,6 +111,12 @@ FlashWS.prototype.write = function() { }); }; +/** + * Called upon dependencies are loaded. + * + * @api private + */ + FlashWS.prototype.ready = function (fn) { if (typeof WebSocket == 'undefined' || !('__initialize' in WebSocket) || !swfobject @@ -117,12 +129,10 @@ FlashWS.prototype.ready = function (fn) { } function init () { - // Only start downloading the swf file when the checked that this browser // actually supports it if (!FlashWS.loaded) { - - if (self.policyPort !== 843) { + if (843 != self.policyPort) { WebSocket.loadFlashPolicyFile('xmlsocket://' + self.host + ':' + self.policyPort); } @@ -134,7 +144,9 @@ FlashWS.prototype.ready = function (fn) { } var self = this; - if (document.body) return init(); + if (document.body) { + return init(); + } util.load(init); }; @@ -151,8 +163,9 @@ FlashWS.prototype.check = function () { return false; // end - if (typeof WebSocket != 'undefined' && !('__initialize' in WebSocket)) + if (typeof WebSocket != 'undefined' && !('__initialize' in WebSocket)) { return false; + } if (window.ActiveXObject) { var control = null; @@ -162,11 +175,12 @@ FlashWS.prototype.check = function () { if (control) { return true; } - } - else { + } else { for (var i = 0, l = navigator.plugins.length; i < l; i++) { for (var j = 0, m = navigator.plugins[i].length; j < m; j++) { - if (navigator.plugins[i][j].description == 'Shockwave Flash') return true; + if (navigator.plugins[i][j].description == 'Shockwave Flash') { + return true; + } } } } From b1b650432d7f5e57718a49b32a6ae137aa110ecd Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 2 Jul 2012 08:04:02 -0700 Subject: [PATCH 0285/1403] meta: removed 0.4 testing from travis CI --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d1e53b1c..56eca033 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: node_js node_js: - - 0.4 - 0.6 notifications: From da071a1327c1adafc2230ebe6385bd8828a2cc72 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Mon, 2 Jul 2012 09:06:30 -0700 Subject: [PATCH 0286/1403] test: added node 0.8 to travis testing targets --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 56eca033..615d3e60 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: node_js node_js: - 0.6 + - 0.8 notifications: irc: "irc.freenode.org#socket.io" From bfbc49ae359aba033a67db6cd326cbdf1bb691c1 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Tue, 3 Jul 2012 15:36:46 -0700 Subject: [PATCH 0287/1403] Release 0.1.0 --- History.md | 5 + dist/engine.io-dev.js | 2444 +++++++++++++++++++++++++++++++++++++++++ dist/engine.io.js | 2323 +++++++++++++++++++++++++++++++++++++++ package.json | 2 +- 4 files changed, 4773 insertions(+), 1 deletion(-) create mode 100644 History.md create mode 100644 dist/engine.io-dev.js create mode 100644 dist/engine.io.js diff --git a/History.md b/History.md new file mode 100644 index 00000000..7fdc96d1 --- /dev/null +++ b/History.md @@ -0,0 +1,5 @@ + +0.1.0 / 2012-07-03 +================== + + * Initial release. diff --git a/dist/engine.io-dev.js b/dist/engine.io-dev.js new file mode 100644 index 00000000..53c74d28 --- /dev/null +++ b/dist/engine.io-dev.js @@ -0,0 +1,2444 @@ +(function(){var global = this; +/*! + * debug + * Copyright(c) 2012 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Create a debugger with the given `name`. + * + * @param {String} name + * @return {Type} + * @api public + */ + +function debug(name) { + if (!debug.enabled(name)) return function(){}; + + return function(fmt){ + var curr = new Date; + var ms = curr - (debug[name] || curr); + debug[name] = curr; + + fmt = name + + ' ' + + fmt + + ' +' + debug.humanize(ms); + + // This hackery is required for IE8 + // where `console.log` doesn't have 'apply' + window.console + && console.log + && Function.prototype.apply.call(console.log, console, arguments); + } +} + +/** + * The currently active debug mode names. + */ + +debug.names = []; +debug.skips = []; + +/** + * Enables a debug mode by name. This can include modes + * separated by a colon and wildcards. + * + * @param {String} name + * @api public + */ + +debug.enable = function(name) { + localStorage.debug = name; + + var split = (name || '').split(/[\s,]+/) + , len = split.length; + + for (var i = 0; i < len; i++) { + name = split[i].replace('*', '.*?'); + if (name[0] === '-') { + debug.skips.push(new RegExp('^' + name.substr(1) + '$')); + } + else { + debug.names.push(new RegExp('^' + name + '$')); + } + } +}; + +/** + * Disable debug output. + * + * @api public + */ + +debug.disable = function(){ + debug.enable(''); +}; + +/** + * Humanize the given `ms`. + * + * @param {Number} m + * @return {String} + * @api private + */ + +debug.humanize = function(ms) { + var sec = 1000 + , min = 60 * 1000 + , hour = 60 * min; + + if (ms >= hour) return (ms / hour).toFixed(1) + 'h'; + if (ms >= min) return (ms / min).toFixed(1) + 'm'; + if (ms >= sec) return (ms / sec | 0) + 's'; + return ms + 'ms'; +}; + +/** + * Returns true if the given mode name is enabled, false otherwise. + * + * @param {String} name + * @return {Boolean} + * @api public + */ + +debug.enabled = function(name) { + for (var i = 0, len = debug.skips.length; i < len; i++) { + if (debug.skips[i].test(name)) { + return false; + } + } + for (var i = 0, len = debug.names.length; i < len; i++) { + if (debug.names[i].test(name)) { + return true; + } + } + return false; +}; + +// persist + +if (window.localStorage) debug.enable(localStorage.debug);function require(p, parent){ var path = require.resolve(p) , mod = require.modules[path]; if (!mod) throw new Error('failed to require "' + p + '" from ' + parent); if (!mod.exports) { mod.exports = {}; mod.call(mod.exports, mod, mod.exports, require.relative(path), global); } return mod.exports;}require.modules = {};require.resolve = function(path){ var orig = path , reg = path + '.js' , index = path + '/index.js'; return require.modules[reg] && reg || require.modules[index] && index || orig;};require.register = function(path, fn){ require.modules[path] = fn;};require.relative = function(parent) { return function(p){ if ('debug' == p) return debug; if ('.' != p.charAt(0)) return require(p); var path = parent.split('/') , segs = p.split('/'); path.pop(); for (var i = 0; i < segs.length; i++) { var seg = segs[i]; if ('..' == seg) path.pop(); else if ('.' != seg) path.push(seg); } return require(path.join('/'), parent); };};require.register("engine.io-client.js", function(module, exports, require, global){ + +/** + * Client version. + * + * @api public. + */ + +exports.version = '0.1.0'; + +/** + * Protocol version. + * + * @api public. + */ + +exports.protocol = 1; + +/** + * Utils. + * + * @api public + */ + +exports.util = require('./util'); + +/** + * Parser. + * + * @api public + */ + +exports.parser = require('./parser'); + +/** + * Socket constructor. + * + * @api public. + */ + +exports.Socket = require('./socket'); + +/** + * Export EventEmitter. + */ + +exports.EventEmitter = require('./event-emitter') + +/** + * Export Transport. + */ + +exports.Transport = require('./transport'); + +/** + * Export transports + */ + +exports.transports = require('./transports'); + +});require.register("event-emitter.js", function(module, exports, require, global){ + +/** + * Module exports. + */ + +module.exports = EventEmitter; + +/** + * Event emitter constructor. + * + * @api public. + */ + +function EventEmitter () {}; + +/** + * Adds a listener + * + * @api public + */ + +EventEmitter.prototype.on = function (name, fn) { + if (!this.$events) { + this.$events = {}; + } + + if (!this.$events[name]) { + this.$events[name] = fn; + } else if (isArray(this.$events[name])) { + this.$events[name].push(fn); + } else { + this.$events[name] = [this.$events[name], fn]; + } + + return this; +}; + +EventEmitter.prototype.addListener = EventEmitter.prototype.on; + +/** + * Adds a volatile listener. + * + * @api public + */ + +EventEmitter.prototype.once = function (name, fn) { + var self = this; + + function on () { + self.removeListener(name, on); + fn.apply(this, arguments); + }; + + on.listener = fn; + this.on(name, on); + + return this; +}; + +/** + * Removes a listener. + * + * @api public + */ + +EventEmitter.prototype.removeListener = function (name, fn) { + if (this.$events && this.$events[name]) { + var list = this.$events[name]; + + if (isArray(list)) { + var pos = -1; + + for (var i = 0, l = list.length; i < l; i++) { + if (list[i] === fn || (list[i].listener && list[i].listener === fn)) { + pos = i; + break; + } + } + + if (pos < 0) { + return this; + } + + list.splice(pos, 1); + + if (!list.length) { + delete this.$events[name]; + } + } else if (list === fn || (list.listener && list.listener === fn)) { + delete this.$events[name]; + } + } + + return this; +}; + +/** + * Removes all listeners for an event. + * + * @api public + */ + +EventEmitter.prototype.removeAllListeners = function (name) { + if (name === undefined) { + this.$events = {}; + return this; + } + + if (this.$events && this.$events[name]) { + this.$events[name] = null; + } + + return this; +}; + +/** + * Gets all listeners for a certain event. + * + * @api publci + */ + +EventEmitter.prototype.listeners = function (name) { + if (!this.$events) { + this.$events = {}; + } + + if (!this.$events[name]) { + this.$events[name] = []; + } + + if (!isArray(this.$events[name])) { + this.$events[name] = [this.$events[name]]; + } + + return this.$events[name]; +}; + +/** + * Emits an event. + * + * @api public + */ + +EventEmitter.prototype.emit = function (name) { + if (!this.$events) { + return false; + } + + var handler = this.$events[name]; + + if (!handler) { + return false; + } + + var args = Array.prototype.slice.call(arguments, 1); + + if ('function' == typeof handler) { + handler.apply(this, args); + } else if (isArray(handler)) { + var listeners = handler.slice(); + + for (var i = 0, l = listeners.length; i < l; i++) { + listeners[i].apply(this, args); + } + } else { + return false; + } + + return true; +}; + +/** + * Checks for Array type. + * + * @param {Object} object + * @api private + */ + +function isArray (obj) { + return '[object Array]' == Object.prototype.toString.call(obj); +}; + +/** + * Compatibility with WebSocket + */ + +EventEmitter.prototype.addEventListener = EventEmitter.prototype.on; +EventEmitter.prototype.removeEventListener = EventEmitter.prototype.removeListener; +EventEmitter.prototype.dispatchEvent = EventEmitter.prototype.emit; + +});require.register("parser.js", function(module, exports, require, global){ +/** + * Module dependencies. + */ + +var util = require('./util') + +/** + * Packet types. + */ + +var packets = exports.packets = { + open: 0 // non-ws + , close: 1 // non-ws + , ping: 2 + , pong: 3 + , message: 4 + , upgrade: 5 + , noop: 6 +}; + +var packetslist = util.keys(packets); + +/** + * Premade error packet. + */ + +var err = { type: 'error', data: 'parser error' } + +/** + * Encodes a packet. + * + * [ `:` ] + * + * Example: + * + * 5:hello world + * 3 + * 4 + * + * @api private + */ + +exports.encodePacket = function (packet) { + var encoded = packets[packet.type] + + // data fragment is optional + if (undefined !== packet.data) { + encoded += String(packet.data); + } + + return '' + encoded; +}; + +/** + * Decodes a packet. + * + * @return {Object} with `type` and `data` (if any) + * @api private + */ + +exports.decodePacket = function (data) { + var type = data.charAt(0); + + if (Number(type) != type || !packetslist[type]) { + return err; + } + + if (data.length > 1) { + return { type: packetslist[type], data: data.substring(1) }; + } else { + return { type: packetslist[type] }; + } +}; + +/** + * Encodes multiple messages (payload). + * + * :data + * + * Example: + * + * 11:hello world2:hi + * + * @param {Array} packets + * @api private + */ + +exports.encodePayload = function (packets) { + if (!packets.length) { + return '0:'; + } + + var encoded = '' + , message + + for (var i = 0, l = packets.length; i < l; i++) { + message = exports.encodePacket(packets[i]); + encoded += message.length + ':' + message; + } + + return encoded; +}; + +/* + * Decodes data when a payload is maybe expected. + * + * @param {String} data + * @return {Array} packets + * @api public + */ + +exports.decodePayload = function (data) { + if (data == '') { + // parser error - ignoring payload + return [err]; + } + + var packets = [] + , length = '' + , n, msg, packet + + for (var i = 0, l = data.length; i < l; i++) { + var chr = data.charAt(i) + + if (':' != chr) { + length += chr; + } else { + if ('' == length || (length != (n = Number(length)))) { + // parser error - ignoring payload + return [err]; + } + + msg = data.substr(i + 1, n); + + if (length != msg.length) { + // parser error - ignoring payload + return [err]; + } + + if (msg.length) { + packet = exports.decodePacket(msg); + + if (err.type == packet.type && err.data == packet.data) { + // parser error in individual packet - ignoring payload + return [err]; + } + + packets.push(packet); + } + + // advance cursor + i += n; + length = '' + } + } + + if (length != '') { + // parser error - ignoring payload + return [err]; + } + + return packets; +}; + +});require.register("socket.js", function(module, exports, require, global){ +/** + * Module dependencies. + */ + +var util = require('./util') + , transports = require('./transports') + , debug = require('debug')('engine-client:socket') + , EventEmitter = require('./event-emitter') + +/** + * Module exports. + */ + +module.exports = Socket; + +/** + * Socket constructor. + * + * @param {Object} options + * @api public + */ + +function Socket (opts) { + if ('string' == typeof opts) { + var uri = util.parseUri(opts); + opts = arguments[1] || {}; + opts.host = uri.host; + opts.secure = uri.scheme == 'https' || uri.scheme == 'wss'; + opts.port = uri.port || (opts.secure ? 443 : 80); + } + + opts = opts || {}; + this.secure = opts.secure || false; + this.host = opts.host || opts.hostname || 'localhost'; + this.port = opts.port || 80; + this.query = opts.query || {}; + this.query.uid = rnd(); + this.upgrade = false !== opts.upgrade; + this.resource = opts.resource || 'default'; + this.path = (opts.path || '/engine.io').replace(/\/$/, ''); + this.path += '/' + this.resource + '/'; + this.forceJSONP = !!opts.forceJSONP; + this.flashPath = opts.flashPath || ''; + this.transports = opts.transports || ['polling', 'websocket', 'flashsocket']; + this.readyState = ''; + this.writeBuffer = []; + this.policyPort = opts.policyPort || 843; + this.open(); +}; + +/** + * Inherits from EventEmitter. + */ + +util.inherits(Socket, EventEmitter); + +/** + * Creates transport of the given type. + * + * @param {String} transport name + * @return {Transport} + * @api private + */ + +Socket.prototype.createTransport = function (name) { + debug('creating transport "%s"', name); + var query = clone(this.query) + query.transport = name; + + if (this.id) { + query.sid = this.id; + } + + var transport = new transports[name]({ + host: this.host + , port: this.port + , secure: this.secure + , path: this.path + , query: query + , forceJSONP: this.forceJSONP + , flashPath: this.flashPath + , policyPort: this.policyPort + }); + + return transport; +}; + +function clone (obj) { + var o = {}; + for (var i in obj) { + if (obj.hasOwnProperty(i)) { + o[i] = obj[i]; + } + } + return o; +} + +/** + * Initializes transport to use and starts probe. + * + * @api private + */ + +Socket.prototype.open = function () { + this.readyState = 'opening'; + var transport = this.createTransport(this.transports[0]); + transport.open(); + this.setTransport(transport); +}; + +/** + * Sets the current transport. Disables the existing one (if any). + * + * @api private + */ + +Socket.prototype.setTransport = function (transport) { + var self = this; + + if (this.transport) { + debug('clearing existing transport'); + this.transport.removeAllListeners(); + } + + // set up transport + this.transport = transport; + + // set up transport listeners + transport + .on('drain', function () { + self.flush(); + }) + .on('packet', function (packet) { + self.onPacket(packet); + }) + .on('error', function (e) { + self.onError(e); + }) + .on('close', function () { + self.onClose('transport close'); + }) +}; + +/** + * Probes a transport. + * + * @param {String} transport name + * @api private + */ + +Socket.prototype.probe = function (name) { + debug('probing transport "%s"', name); + var transport = this.createTransport(name, { probe: 1 }) + , self = this + + transport.once('open', function () { + debug('probe transport "%s" opened', name); + transport.send([{ type: 'ping', data: 'probe' }]); + transport.once('packet', function (msg) { + if ('pong' == msg.type && 'probe' == msg.data) { + debug('probe transport "%s" pong', name); + self.upgrading = true; + self.emit('upgrading', transport); + + debug('pausing current transport "%s"', self.transport.name); + self.transport.pause(function () { + if ('closed' == self.readyState || 'closing' == self.readyState) return; + debug('changing transport and sending upgrade packet'); + self.emit('upgrade', transport); + self.setTransport(transport); + transport.send([{ type: 'upgrade' }]); + transport = null; + self.upgrading = false; + self.flush(); + }); + } else { + debug('probe transport "%s" failed', name); + var err = new Error('probe error'); + err.transport = transport.name; + self.emit('error', err); + } + }); + }); + + transport.open(); + + this.once('close', function () { + if (transport) { + debug('socket closed prematurely - aborting probe'); + transport.close(); + transport = null; + } + }); + + this.once('upgrading', function (to) { + if (transport && to.name != transport.name) { + debug('"%s" works - aborting "%s"', to.name, transport.name); + transport.close(); + transport = null; + } + }); +}; + +/** + * Called when connection is deemed open. + * + * @api public + */ + +Socket.prototype.onOpen = function () { + debug('socket open'); + this.readyState = 'open'; + this.emit('open'); + this.onopen && this.onopen.call(this); + this.flush(); + + if (this.upgrade && this.transport.pause) { + debug('starting upgrade probes'); + for (var i = 0, l = this.upgrades.length; i < l; i++) { + this.probe(this.upgrades[i]); + } + } +}; + +/** + * Handles a packet. + * + * @api private + */ + +Socket.prototype.onPacket = function (packet) { + if ('opening' == this.readyState || 'open' == this.readyState) { + debug('socket receive: type "%s", data "%s"', packet.type, packet.data); + switch (packet.type) { + case 'open': + this.onHandshake(util.parseJSON(packet.data)); + break; + + case 'ping': + this.sendPacket('pong'); + this.setPingTimeout(); + break; + + case 'error': + var err = new Error('server error'); + err.code = packet.data; + this.emit('error', err); + break; + + case 'message': + this.emit('message', packet.data); + var event = { data: packet.data }; + event.toString = function () { + return packet.data; + } + this.onmessage && this.onmessage.call(this, event); + break; + } + } else { + debug('packet received with socket readyState "%s"', this.readyState); + } +}; + +/** + * Called upon handshake completion. + * + * @param {Object} handshake obj + * @api private + */ + +Socket.prototype.onHandshake = function (data) { + this.emit('handshake', data); + this.id = data.sid; + this.transport.query.sid = data.sid; + this.upgrades = data.upgrades; + this.pingTimeout = data.pingTimeout; + this.onOpen(); + this.setPingTimeout(); +}; + +/** + * Clears and sets a ping timeout based on the expected ping interval. + * + * @api private + */ + +Socket.prototype.setPingTimeout = function () { + clearTimeout(this.pingTimeoutTimer); + var self = this; + this.pingTimeoutTimer = setTimeout(function () { + self.onClose('ping timeout'); + }, this.pingTimeout); +}; + +/** + * Flush write buffers. + * + * @api private + */ + +Socket.prototype.flush = function () { + if ('closed' != this.readyState && this.transport.writable + && !this.upgrading && this.writeBuffer.length) { + debug('flushing %d packets in socket', this.writeBuffer.length); + this.transport.send(this.writeBuffer); + this.writeBuffer = []; + } +}; + +/** + * Sends a message. + * + * @param {String} message. + * @return {Socket} for chaining. + * @api public + */ + +Socket.prototype.send = function (msg) { + this.sendPacket('message', msg); + return this; +}; + +/** + * Sends a packet. + * + * @param {String} packet type. + * @param {String} data. + * @api private + */ + +Socket.prototype.sendPacket = function (type, data) { + var packet = { type: type, data: data }; + this.writeBuffer.push(packet); + this.flush(); +}; + +/** + * Closes the connection. + * + * @api private + */ + +Socket.prototype.close = function () { + if ('opening' == this.readyState || 'open' == this.readyState) { + this.onClose('forced close'); + debug('socket closing - telling transport to close'); + this.transport.close(); + } + + return this; +}; + +/** + * Called upon transport error + * + * @api private + */ + +Socket.prototype.onError = function (err) { + this.emit('error', err); + this.onClose('transport error', err); +}; + +/** + * Called upon transport close. + * + * @api private + */ + +Socket.prototype.onClose = function (reason, desc) { + if ('closed' != this.readyState) { + debug('socket close with reason: "%s"', reason); + this.readyState = 'closed'; + this.emit('close', reason, desc); + this.onclose && this.onclose.call(this); + } +}; + +/** + * Generates a random uid. + * + * @api private + */ + +function rnd () { + return String(Math.random()).substr(5) + String(Math.random()).substr(5); +} + +});require.register("transport.js", function(module, exports, require, global){ + +/** + * Module dependencies. + */ + +var util = require('./util') + , parser = require('./parser') + , EventEmitter = require('./event-emitter') + +/** + * Module exports. + */ + +module.exports = Transport; + +/** + * Transport abstract constructor. + * + * @param {Object} options. + * @api private + */ + +function Transport (opts) { + this.path = opts.path; + this.host = opts.host; + this.port = opts.port; + this.secure = opts.secure; + this.query = opts.query; + this.readyState = ''; +}; + +/** + * Inherits from EventEmitter. + */ + +util.inherits(Transport, EventEmitter); + +/** + * Emits an error. + * + * @param {String} str + * @return {Transport} for chaining + * @api public + */ + +Transport.prototype.onError = function (msg, desc) { + var err = new Error(msg); + err.type = 'TransportError'; + err.description = desc; + this.emit('error', err); + return this; +}; + +/** + * Opens the transport. + * + * @api public + */ + +Transport.prototype.open = function () { + if ('closed' == this.readyState || '' == this.readyState) { + this.readyState = 'opening'; + this.doOpen(); + } + + return this; +}; + +/** + * Closes the transport. + * + * @api private + */ + +Transport.prototype.close = function () { + if ('opening' == this.readyState || 'open' == this.readyState) { + this.doClose(); + this.onClose(); + } + + return this; +}; + +/** + * Sends multiple packets. + * + * @param {Array} packets + * @api private + */ + +Transport.prototype.send = function (packets) { + if ('open' == this.readyState) { + this.write(packets); + } else { + throw new Error('Transport not open'); + } +} + +/** + * Called upon open + * + * @api private + */ + +Transport.prototype.onOpen = function () { + this.readyState = 'open'; + this.writable = true; + this.emit('open'); +}; + +/** + * Called with data. + * + * @param {String} data + * @api private + */ + +Transport.prototype.onData = function (data) { + this.onPacket(parser.decodePacket(data)); +}; + +/** + * Called with a decoded packet. + */ + +Transport.prototype.onPacket = function (packet) { + this.emit('packet', packet); +}; + +/** + * Called upon close. + * + * @api private + */ + +Transport.prototype.onClose = function () { + this.readyState = 'closed'; + this.emit('close'); +}; + +});require.register("transports/flashsocket.js", function(module, exports, require, global){ + +/** + * Module dependencies. + */ + +var WS = require('./websocket') + , util = require('../util') + +/** + * Module exports. + */ + +module.exports = FlashWS; + +/** + * FlashWS constructor. + * + * @api public + */ + +function FlashWS (options) { + WS.call(this, options); + this.flashPath = options.flashPath; + this.policyPort = options.policyPort; +}; + +/** + * Inherits from WebSocket. + */ + +util.inherits(FlashWS, WS); + +/** + * Transport name. + * + * @api public + */ + +FlashWS.prototype.name = 'flashsocket'; + +/** + * Opens the transport. + * + * @api public + */ + +FlashWS.prototype.doOpen = function () { + if (!this.check()) { + // let the probe timeout + return; + } + + // instrument websocketjs logging + function log (type) { + return function () { + var str = Array.prototype.join.call(arguments, ' '); + // debug: [websocketjs %s] %s, type, str + } + }; + + WEB_SOCKET_LOGGER = { log: log('debug'), error: log('error') }; + WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR = true; + WEB_SOCKET_DISABLE_AUTO_INITIALIZATION = true; + + if ('undefined' == typeof WEB_SOCKET_SWF_LOCATION) { + WEB_SOCKET_SWF_LOCATION = this.flashPath + 'WebSocketMainInsecure.swf'; + } + + // dependencies + var deps = [this.flashPath + 'web_socket.js']; + + if ('undefined' == typeof swfobject) { + deps.unshift(this.flashPath + 'swfobject.js'); + } + + var self = this; + + load(deps, function () { + self.ready(function () { + WebSocket.__addTask(function () { + WS.prototype.doOpen.call(self); + }); + }); + }); +}; + +/** + * Override to prevent closing uninitialized flashsocket. + * + * @api private + */ + +FlashWS.prototype.doClose = function () { + if (!this.socket) return; + var self = this; + WebSocket.__addTask(function() { + WS.prototype.doClose.call(self); + }); +}; + +/** + * Writes to the Flash socket. + * + * @api private + */ + +FlashWS.prototype.write = function() { + var self = this, args = arguments; + WebSocket.__addTask(function () { + WS.prototype.write.apply(self, args); + }); +}; + +/** + * Called upon dependencies are loaded. + * + * @api private + */ + +FlashWS.prototype.ready = function (fn) { + if (typeof WebSocket == 'undefined' + || !('__initialize' in WebSocket) || !swfobject + ) { + return; + } + + if (swfobject.getFlashPlayerVersion().major < 10) { + return; + } + + function init () { + // Only start downloading the swf file when the checked that this browser + // actually supports it + if (!FlashWS.loaded) { + if (843 != self.policyPort) { + WebSocket.loadFlashPolicyFile('xmlsocket://' + self.host + ':' + self.policyPort); + } + + WebSocket.__initialize(); + FlashWS.loaded = true; + } + + fn.call(self); + } + + var self = this; + if (document.body) { + return init(); + } + + util.load(init); +}; + +/** + * Feature detection for flashsocket. + * + * @return {Boolean} whether this transport is available. + * @api public + */ + +FlashWS.prototype.check = function () { + + + + + if (typeof WebSocket != 'undefined' && !('__initialize' in WebSocket)) { + return false; + } + + if (window.ActiveXObject) { + var control = null; + try { + control = new ActiveXObject('ShockwaveFlash.ShockwaveFlash'); + } catch (e) { } + if (control) { + return true; + } + } else { + for (var i = 0, l = navigator.plugins.length; i < l; i++) { + for (var j = 0, m = navigator.plugins[i].length; j < m; j++) { + if (navigator.plugins[i][j].description == 'Shockwave Flash') { + return true; + } + } + } + } + + return false; +}; + +/** + * Lazy loading of scripts. + * Based on $script by Dustin Diaz - MIT + */ + +var scripts = {}; + +/** + * Injects a script. Keeps tracked of injected ones. + * + * @param {String} path + * @param {Function} callback + * @api private + */ + +function create (path, fn) { + if (scripts[path]) return fn(); + + var el = document.createElement('script') + , loaded = false + + // debug: loading "%s", path + el.onload = el.onreadystatechange = function () { + if (loaded || scripts[path]) return; + var rs = el.readyState; + if (!rs || 'loaded' == rs || 'complete' == rs) { + // debug: loaded "%s", path + el.onload = el.onreadystatechange = null; + loaded = true; + scripts[path] = true; + fn(); + } + }; + + el.async = 1; + el.src = path; + + var head = document.getElementsByTagName('head')[0]; + head.insertBefore(el, head.firstChild); +}; + +/** + * Loads scripts and fires a callback. + * + * @param {Array} paths + * @param {Function} callback + */ + +function load (arr, fn) { + function process (i) { + if (!arr[i]) return fn(); + create(arr[i], function () { + process(++i); + }); + }; + + process(0); +}; + +});require.register("transports/index.js", function(module, exports, require, global){ + +/** + * Module dependencies + */ + +var XHR = require('./polling-xhr') + , JSONP = require('./polling-jsonp') + , websocket = require('./websocket') + , flashsocket = require('./flashsocket') + , util = require('../util') + +/** + * Export transports. + */ + +exports.polling = polling; +exports.websocket = websocket; +exports.flashsocket = flashsocket; + +/** + * Polling transport polymorphic constructor. + * Decides on xhr vs jsonp based on feature detection. + * + * @api private + */ + +function polling (opts) { + var xd = false; + + if (global.location) { + xd = opts.host != global.location.hostname + || global.location.port != opts.port; + } + + if (util.request(xd) && !opts.forceJSONP) { + return new XHR(opts); + } else { + return new JSONP(opts); + } +}; + +});require.register("transports/polling-jsonp.js", function(module, exports, require, global){ + +/** + * Module requirements. + */ + +var Polling = require('./polling') + , util = require('../util') + +/** + * Module exports. + */ + +module.exports = JSONPPolling; + +/** + * Cached regular expressions. + */ + +var rNewline = /\n/g + +/** + * Global JSONP callbacks. + */ + +var callbacks = global.___eio = []; + +/** + * Callbacks count. + */ + +var index = 0; + +/** + * Noop. + */ + +function empty () { } + +/** + * JSONP Polling constructor. + * + * @param {Object} opts. + * @api public + */ + +function JSONPPolling (opts) { + Polling.call(this, opts); + + // callback identifier + this.index = index++; + + // add callback to jsonp global + var self = this; + callbacks.push(function (msg) { + self.onData(msg); + }); + + // append to query string + this.query.j = callbacks.length - 1; +}; + +/** + * Inherits from Polling. + */ + +util.inherits(JSONPPolling, Polling); + +/** + * Opens the socket. + * + * @api private + */ + +JSONPPolling.prototype.doOpen = function () { + var self = this; + util.defer(function () { + Polling.prototype.doOpen.call(self); + }); +}; + +/** + * Closes the socket + * + * @api private + */ + +JSONPPolling.prototype.doClose = function () { + if (this.script) { + this.script.parentNode.removeChild(this.script); + this.script = null; + } + + if (this.form) { + this.form.parentNode.removeChild(this.form); + this.form = null; + } + + Polling.prototype.doClose.call(this); +}; + +/** + * Starts a poll cycle. + * + * @api private + */ + +JSONPPolling.prototype.doPoll = function () { + var script = document.createElement('script'); + + if (this.script) { + this.script.parentNode.removeChild(this.script); + this.script = null; + } + + script.async = true; + script.src = this.uri(); + + var insertAt = document.getElementsByTagName('script')[0] + insertAt.parentNode.insertBefore(script, insertAt); + this.script = script; + + if (util.ua.gecko) { + setTimeout(function () { + var iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + document.body.removeChild(iframe); + }, 100); + } +}; + +/** + * Writes with a hidden iframe. + * + * @param {String} data to send + * @param {Function} called upon flush. + * @api private + */ + +JSONPPolling.prototype.doWrite = function (data, fn) { + var self = this + + if (!this.form) { + var form = document.createElement('form') + , area = document.createElement('textarea') + , id = this.iframeId = 'eio_iframe_' + this.index + , iframe; + + form.className = 'socketio'; + form.style.position = 'absolute'; + form.style.top = '-1000px'; + form.style.left = '-1000px'; + form.target = id; + form.method = 'POST'; + form.setAttribute('accept-charset', 'utf-8'); + area.name = 'd'; + form.appendChild(area); + document.body.appendChild(form); + + this.form = form; + this.area = area; + } + + this.form.action = this.uri(); + + function complete () { + initIframe(); + fn(); + }; + + function initIframe () { + if (self.iframe) { + self.form.removeChild(self.iframe); + } + + try { + // ie6 dynamic iframes with target="" support (thanks Chris Lambacher) + iframe = document.createElement('