Initial import

This commit is contained in:
Guillermo Rauch
2011-11-18 10:08:39 -08:00
commit 21b75fa15e
19 changed files with 3150 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules
.DS_Store

1
.npmignore Normal file
View File

@@ -0,0 +1 @@
support/

153
README.md Normal file
View File

@@ -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
<script src="/path/to/engine.js"></script>
<script>
var socket = new io.Engine({ host: 'localhost', port: 80 });
socket.onopen = function () {
socket.onmessage = function (data) { });
socket.onclose = function () { });
};
</script>
```
## 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
<hr><br>
### 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 &lt;guillermo@learnboost.com&gt;
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.

24
lib/engine-client.js Normal file
View File

@@ -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');

312
lib/engine.js Normal file
View File

@@ -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');
};

170
lib/event-emitter.js Normal file
View File

@@ -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;
};

102
lib/parser.js Normal file
View File

@@ -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];
}
}

163
lib/transport.js Normal file
View File

@@ -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');
};

View File

@@ -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
});

8
lib/transports/index.js Normal file
View File

@@ -0,0 +1,8 @@
/**
* Export transports.
*/
exports.polling = require('./polling');
exports.websocket = require('./websocket');
exports.flashsocket = require('./flashsocket');

View File

@@ -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);
}
};

View File

@@ -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();
}
}
});
}

158
lib/transports/polling.js Normal file
View File

@@ -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));
};

133
lib/transports/websocket.js Normal file
View File

@@ -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();
}

89
lib/util.js Normal file
View File

@@ -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) { }
}
};

5
package.json Normal file
View File

@@ -0,0 +1,5 @@
{
"name": "engine-client"
, "description": "Client for the realtime Engine"
, "version": "0.0.1"
}

1238
support/should.js Normal file

File diff suppressed because it is too large Load Diff

1
support/web-socket-js Submodule

Submodule support/web-socket-js added at 14023075bf

12
test/engine-client.js Normal file
View File

@@ -0,0 +1,12 @@
describe('engine-client', function () {
it('should expose version number', function () {
io.engineVersion.should().match(/);
});
it('should expose protocol number', function () {
});
});