Merge pull request #244 from rase-/add/binary-support

Binary support
This commit is contained in:
Guillermo Rauch
2014-02-19 15:32:07 -08:00
23 changed files with 786 additions and 131 deletions

View File

@@ -3,11 +3,16 @@ server: ./test/support/server.js
browsers:
- name: chrome
version: 29..latest
# Firefox disabled for now because it can cause infinite wait loops when
# running any tests
# - name: firefox
# version: latest
- name: safari
version: latest
- name: ie
version: 6..latest
version: 10
platform: Windows 2012
- name: ie
version: [6..9, latest]
- name: iphone
version: oldest..latest

View File

@@ -18,4 +18,4 @@ test-cov:
--reporter $(REPORTER) \
$(TESTS)
.PHONY: test
.PHONY: test build

View File

@@ -56,6 +56,23 @@ browserify app.js > bundle.js
<script src="/path/to/bundle.js"></script>
```
### Sending and receiving binary
```html
<script src="/path/to/engine.io.js"></script>
<script>
var socket = new eio.Socket('ws://localhost/');
socket.binaryType = 'blob'; // receives Blob instead of ArrayBuffer (default)
socket.on('open', function () {
socket.send(new Int8Array(5));
socket.on('message', function (data) {
// data instanceof Blob => true when receiving binary
});
socket.on('close', function () { });
});
</script>
```
### Node.JS
Add `engine.io-client` to your `package.json` and then:
@@ -78,6 +95,14 @@ socket.onopen = function(){
- Easy to debug
- Easy to unit test
- Runs inside HTML5 WebWorker
- Can send and receive binary data
- Receives as ArrayBuffer or Blob when in browser, and Buffer or ArrayBuffer
in Node
- When XHR2 or WebSockets are used, binary is emitted directly. Otherwise
binary is encoded into base64 strings, and decoded when binary types are
supported.
- With browsers that don't support ArrayBuffer, an object { base64: true,
data: dataAsBase64String } is emitted in onmessage
## API
@@ -95,6 +120,9 @@ Exposed as `eio` in the browser standalone build.
- `message` event handler
- `onclose` (_Function_)
- `message` event handler
- `binaryType` _(String)_ : can be set to 'arraybuffer' or 'blob' in browsers,
and `buffer` or `arraybuffer` in Node. Blob is only used in browser if it's
supported.
#### Events
@@ -103,7 +131,8 @@ Exposed as `eio` in the browser standalone build.
- `message`
- Fired when data is received from the server.
- **Arguments**
- `String`: utf-8 encoded data
- `String` | `ArrayBuffer`: utf-8 encoded data or ArrayBuffer containing
binary data
- `close`
- Fired upon disconnection. In compliance with the WebSocket API spec, this event may be
fired even if the `open` event does not occur (i.e. due to connection error or `close()`).
@@ -130,6 +159,7 @@ Exposed as `eio` in the browser standalone build.
- `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.
- `forceBase64` (`Boolean`): forces base 64 encoding for polling transport even when XHR2 responseType is available and WebSocket even if the used standard supports binary.
- `timestampRequests` (`Boolean`): whether to add the timestamp with
each transport request. Note: this is ignored if the browser is
IE or Android, in which case requests are always stamped (`false`)
@@ -150,7 +180,7 @@ Exposed as `eio` in the browser standalone build.
- `send`
- Sends a message to the server
- **Parameters**
- `String`: data to send
- `String` | `ArrayBuffer` | `ArrayBufferView` | `Blob`: data to send
- `Function`: optional, callback upon `drain`
- `close`
- Disconnects the client.

View File

@@ -75,6 +75,7 @@ function Socket(uri, opts){
this.upgrade = false !== opts.upgrade;
this.path = (opts.path || '/engine.io').replace(/\/$/, '') + '/';
this.forceJSONP = !!opts.forceJSONP;
this.forceBase64 = !!opts.forceBase64;
this.timestampParam = opts.timestampParam || 't';
this.timestampRequests = opts.timestampRequests;
this.flashPath = opts.flashPath || '';
@@ -85,6 +86,8 @@ function Socket(uri, opts){
this.policyPort = opts.policyPort || 843;
this.rememberUpgrade = opts.rememberUpgrade || false;
this.open();
this.binaryType = null;
this.onlyBinaryUpgrades = opts.onlyBinaryUpgrades;
}
Socket.priorWebsocketSuccess = false;
@@ -144,10 +147,12 @@ Socket.prototype.createTransport = function (name) {
path: this.path,
query: query,
forceJSONP: this.forceJSONP,
forceBase64: this.forceBase64,
timestampRequests: this.timestampRequests,
timestampParam: this.timestampParam,
flashPath: this.flashPath,
policyPort: this.policyPort
policyPort: this.policyPort,
socket: this
});
return transport;
@@ -231,6 +236,10 @@ Socket.prototype.probe = function (name) {
Socket.priorWebsocketSuccess = false;
transport.once('open', function () {
if (this.onlyBinaryUpgrades) {
var upgradeLosesBinary = !this.supportsBinary && self.transport.supportsBinary;
failed = failed || upgradeLosesBinary;
}
if (failed) return;
debug('probe transport "%s" opened', name);
@@ -366,6 +375,7 @@ Socket.prototype.onPacket = function (packet) {
event.toString = function () {
return packet.data;
};
this.onmessage && this.onmessage.call(this, event);
break;
}
@@ -444,7 +454,7 @@ Socket.prototype.ping = function () {
* @api private
*/
Socket.prototype.onDrain = function() {
Socket.prototype.onDrain = function() {
for (var i = 0; i < this.prevBufferLen; i++) {
if (this.callbackBuffer[i]) {
this.callbackBuffer[i]();

View File

@@ -1,4 +1,3 @@
/**
* Module dependencies.
*/
@@ -30,6 +29,7 @@ function Transport (opts) {
this.timestampRequests = opts.timestampRequests;
this.readyState = '';
this.agent = opts.agent || false;
this.socket = opts.socket;
}
/**
@@ -119,7 +119,7 @@ Transport.prototype.onOpen = function () {
*/
Transport.prototype.onData = function (data) {
this.onPacket(parser.decodePacket(data));
this.onPacket(parser.decodePacket(data, this.socket.binaryType));
};
/**

View File

@@ -51,6 +51,12 @@ util.inherits(FlashWS, WS);
FlashWS.prototype.name = 'flashsocket';
/*
* FlashSockets only support binary as base64 encoded strings
*/
FlashWS.prototype.supportsBinary = false;
/**
* Opens the transport.
*
@@ -91,7 +97,7 @@ FlashWS.prototype.doOpen = function(){
load(deps, function(){
self.ready(function(){
WebSocket.__addTask(function () {
self.socket = new WebSocket(self.uri());
self.ws = new WebSocket(self.uri());
self.addEventListeners();
});
});
@@ -105,7 +111,7 @@ FlashWS.prototype.doOpen = function(){
*/
FlashWS.prototype.doClose = function(){
if (!this.socket) return;
if (!this.ws) return;
var self = this;
WebSocket.__addTask(function(){
WS.prototype.doClose.call(self);

View File

@@ -79,6 +79,12 @@ function JSONPPolling (opts) {
util.inherits(JSONPPolling, Polling);
/*
* JSONP only supports binary as base64 encoded strings
*/
JSONPPolling.prototype.supportsBinary = false;
/**
* Closes the socket
*

View File

@@ -63,6 +63,12 @@ function XHR(opts){
util.inherits(XHR, Polling);
/**
* XHR supports binary
*/
XHR.prototype.supportsBinary = true;
/**
* Creates a request.
*
@@ -75,6 +81,7 @@ XHR.prototype.request = function(opts){
opts.uri = this.uri();
opts.xd = this.xd;
opts.agent = this.agent || false;
opts.supportsBinary = this.supportsBinary;
return new Request(opts);
};
@@ -87,7 +94,8 @@ XHR.prototype.request = function(opts){
*/
XHR.prototype.doWrite = function(data, fn){
var req = this.request({ method: 'POST', data: data });
var isBinary = typeof data !== 'string' && data !== undefined;
var req = this.request({ method: 'POST', data: data, isBinary: isBinary });
var self = this;
req.on('success', fn);
req.on('error', function(err){
@@ -129,7 +137,7 @@ function Request(opts){
this.async = false !== opts.async;
this.data = undefined != opts.data ? opts.data : null;
this.agent = opts.agent;
this.create();
this.create(opts.isBinary, opts.supportsBinary);
}
/**
@@ -144,17 +152,26 @@ Emitter(Request.prototype);
* @api private
*/
Request.prototype.create = function(){
Request.prototype.create = function(isBinary, supportsBinary){
var xhr = this.xhr = new XMLHttpRequest({ agent: this.agent, xdomain: this.xd });
var self = this;
try {
debug('xhr open %s: %s', this.method, this.uri);
xhr.open(this.method, this.uri, this.async);
if (supportsBinary) {
// This has to be done after open because Firefox is stupid
// http://stackoverflow.com/questions/13216903/get-binary-data-with-xmlhttprequest-in-a-firefox-extension
xhr.responseType = 'arraybuffer';
}
if ('POST' == this.method) {
try {
xhr.setRequestHeader('Content-type', 'text/plain;charset=UTF-8');
if (isBinary) {
xhr.setRequestHeader('Content-type', 'application/octet-stream');
} else {
xhr.setRequestHeader('Content-type', 'text/plain;charset=UTF-8');
}
} catch (e) {}
}
@@ -169,7 +186,16 @@ Request.prototype.create = function(){
try {
if (4 != xhr.readyState) return;
if (200 == xhr.status || 1223 == xhr.status) {
data = xhr.responseText;
var contentType = xhr.getResponseHeader('Content-Type');
if (contentType === 'application/octet-stream') {
data = xhr.response;
} else {
if (!supportsBinary) {
data = xhr.responseText;
} else {
data = 'ok';
}
}
} else {
// make sure the `error` event handler that's user-set
// does not throw in the same tick and gets caught here
@@ -189,7 +215,7 @@ Request.prototype.create = function(){
debug('xhr data %s', this.data);
xhr.send(this.data);
} catch (e) {
// Need to defer since .create() is called directly from the constructor
// Need to defer since .create() is called directly fhrom the constructor
// and thus the 'error' event can only be only bound *after* this exception
// occurs. Therefore, also, we cannot throw here at all.
setTimeout(function() {
@@ -280,11 +306,13 @@ if (hasAttachEvent) {
Request.requestsCount = 0;
Request.requests = {};
global.attachEvent('onunload', function(){
function unloadHandler() {
for (var i in Request.requests) {
if (Request.requests.hasOwnProperty(i)) {
Request.requests[i].abort();
}
}
});
}
global.attachEvent('onunload', unloadHandler);
}

View File

@@ -19,6 +19,16 @@ module.exports = Polling;
var global = require('global');
/**
* Is XHR2 supported?
*/
var hasXHR2 = (function() {
var XMLHttpRequest = require('xmlhttprequest');
var xhr = new XMLHttpRequest({ agent: this.agent, xdomain: false });
return null != xhr.responseType;
})();
/**
* Polling interface.
*
@@ -27,6 +37,10 @@ var global = require('global');
*/
function Polling(opts){
var forceBase64 = (opts && opts.forceBase64);
if (!hasXHR2 || forceBase64) {
this.supportsBinary = false;
}
Transport.call(this, opts);
}
@@ -119,9 +133,7 @@ Polling.prototype.poll = function(){
Polling.prototype.onData = function(data){
var self = this;
debug('polling got data %s', data);
// decode payload
parser.decodePayload(data, function(packet, index, total) {
var callback = function(packet, index, total) {
// if its the first message we consider the transport open
if ('opening' == self.readyState) {
self.onOpen();
@@ -135,7 +147,10 @@ Polling.prototype.onData = function(data){
// otherwise bypass onData and handle the message
self.onPacket(packet);
});
};
// decode payload
parser.decodePayload(data, this.socket.binaryType, callback);
// if an event did not trigger closing
if ('closed' != this.readyState) {
@@ -187,9 +202,14 @@ Polling.prototype.doClose = function(){
Polling.prototype.write = function(packets){
var self = this;
this.writable = false;
this.doWrite(parser.encodePayload(packets), function(){
var callbackfn = function() {
self.writable = true;
self.emit('drain');
};
var self = this;
parser.encodePayload(packets, this.supportsBinary, function(data) {
self.doWrite(data, callbackfn);
});
};
@@ -215,6 +235,10 @@ Polling.prototype.uri = function(){
}
}
if (!this.supportsBinary && !query.sid) {
query.b64 = 1;
}
query = util.qs(query);
// avoid port if default for schema

View File

@@ -35,6 +35,10 @@ var global = require('global');
*/
function WS(opts){
var forceBase64 = (opts && opts.forceBase64);
if (forceBase64) {
this.supportsBinary = false;
}
Transport.call(this, opts);
}
@@ -52,6 +56,12 @@ util.inherits(WS, Transport);
WS.prototype.name = 'websocket';
/*
* WebSockets support binary
*/
WS.prototype.supportsBinary = true;
/**
* Opens socket.
*
@@ -69,7 +79,13 @@ WS.prototype.doOpen = function(){
var protocols = void(0);
var opts = { agent: this.agent };
this.socket = new WebSocket(uri, protocols, opts);
this.ws = new WebSocket(uri, protocols, opts);
if (this.ws.binaryType !== undefined) {
this.supportsBinary = false;
}
this.ws.binaryType = 'arraybuffer';
this.addEventListeners();
};
@@ -82,16 +98,16 @@ WS.prototype.doOpen = function(){
WS.prototype.addEventListeners = function(){
var self = this;
this.socket.onopen = function(){
this.ws.onopen = function(){
self.onOpen();
};
this.socket.onclose = function(){
this.ws.onclose = function(){
self.onClose();
};
this.socket.onmessage = function(ev){
this.ws.onmessage = function(ev){
self.onData(ev.data);
};
this.socket.onerror = function(e){
this.ws.onerror = function(e){
self.onError('websocket error', e);
};
};
@@ -126,8 +142,11 @@ WS.prototype.write = function(packets){
// encodePacket efficient as it uses WS framing
// no need for encodePayload
for (var i = 0, l = packets.length; i < l; i++) {
this.socket.send(parser.encodePacket(packets[i]));
parser.encodePacket(packets[i], this.supportsBinary, function(data) {
self.ws.send(data);
});
}
function ondrain() {
self.writable = true;
self.emit('drain');
@@ -154,8 +173,8 @@ WS.prototype.onClose = function(){
*/
WS.prototype.doClose = function(){
if (typeof this.socket !== 'undefined') {
this.socket.close();
if (typeof this.ws !== 'undefined') {
this.ws.close();
}
};
@@ -181,6 +200,11 @@ WS.prototype.uri = function(){
query[this.timestampParam] = +new Date;
}
// communicate binary support capabilities
if (!this.supportsBinary) {
query.b64 = 1;
}
query = util.qs(query);
// prepend ? to query

View File

@@ -32,7 +32,7 @@
"debug": "0.7.4"
},
"devDependencies": {
"zuul": "1.5.2",
"zuul": "1.5.4",
"mocha": "1.16.2",
"expect.js": "0.2.0",
"istanbul": "0.2.3",

View File

@@ -0,0 +1,8 @@
var wsSupport = require('has-cors');
require('./polling.js');
var uagent = navigator.userAgent;
var isOldSimulator = ~uagent.indexOf('iPhone OS 4') || ~uagent.indexOf('iPhone OS 5');
if (wsSupport && !isOldSimulator) {
require ('./ws.js');
}

View File

@@ -0,0 +1,45 @@
var expect = require('expect.js');
var eio = require('../../');
describe('arraybuffer', function() {
this.timeout(30000);
it('should be able to receive binary data when bouncing it back (polling)', function(done) {
var binaryData = new Int8Array(5);
for (var i = 0; i < 5; i++) {
binaryData[i] = i;
}
var socket = new eio.Socket({ transports: ['polling'] });
socket.on('open', function() {
socket.send(binaryData);
socket.on('message', function (data) {
if (data === 'hi') return;
expect(data).to.be.an(ArrayBuffer);
expect(new Int8Array(data)).to.eql(binaryData);
socket.close();
done();
});
});
});
it('should be able to receive binary data when forcing base64 (polling)', function(done) {
var binaryData = new Int8Array(5);
for (var i = 0; i < 5; i++) {
binaryData[i] = i;
}
var socket = new eio.Socket({ forceBase64: true });
socket.on('open', function() {
socket.send(binaryData);
socket.on('message', function (data) {
if (typeof data === 'string') return;
expect(data).to.be.an(ArrayBuffer);
var ia = new Int8Array(data);
expect(ia).to.eql(binaryData);
socket.close();
done();
});
});
});
});

50
test/arraybuffer/ws.js Normal file
View File

@@ -0,0 +1,50 @@
var expect = require('expect.js');
var eio = require('../../');
describe('arraybuffer', function() {
this.timeout(30000);
it('should be able to receive binary data when bouncing it back (ws)', function(done) {
var binaryData = new Int8Array(5);
for (var i = 0; i < 5; i++) {
binaryData[i] = i;
}
var socket = new eio.Socket();
socket.on('open', function() {
socket.on('upgrade', function() {
socket.send(binaryData);
socket.on('message', function (data) {
if (typeof data === 'string') return;
expect(data).to.be.an(ArrayBuffer);
expect(new Int8Array(data)).to.eql(binaryData);
socket.close();
done();
});
});
});
});
it('should be able to receive binary data when bouncing it back and forcing base64 (ws)', function(done) {
var binaryData = new Int8Array(5);
for (var i = 0; i < 5; i++) {
binaryData[i] = i;
}
var socket = new eio.Socket({ forceBase64: true });
socket.on('open', function() {
socket.on('upgrade', function() {
socket.send(binaryData);
socket.on('message', function (data) {
if (typeof data === 'string') return;
expect(data).to.be.an(ArrayBuffer);
expect(new Int8Array(data)).to.eql(binaryData);
socket.close();
done();
});
});
});
});
});

26
test/binary-fallback.js Normal file
View File

@@ -0,0 +1,26 @@
var expect = require('expect.js');
var eio = require('../');
describe('binary fallback', function() {
this.timeout(10000);
it('should be able to receive binary data when ArrayBuffer not available (polling)', function(done) {
var socket = new eio.Socket({ forceBase64: true });
socket.on('open', function() {
socket.send('give binary');
var firstPacket = true;
socket.on('message', function (data) {
if (firstPacket) {
firstPacket = false;
return;
}
expect(data.base64).to.be(true);
expect(data.data).to.equal('AAECAwQ=');
socket.close();
done();
});
});
});
});

8
test/blob/index.js Normal file
View File

@@ -0,0 +1,8 @@
var wsSupport = require('has-cors');
require('./polling.js');
var uagent = navigator.userAgent;
var isOldSimulator = ~uagent.indexOf('iPhone OS 4') || ~uagent.indexOf('iPhone OS 5');
if (wsSupport && !isOldSimulator) {
require('./ws.js');
}

66
test/blob/polling.js Normal file
View File

@@ -0,0 +1,66 @@
var expect = require('expect.js');
var eio = require('../../');
var blobSupported = (function() {
try {
var b = new Blob(['hi']);
return b.size == 2;
} catch(e) {
return false;
}
})();
describe('blob', function() {
this.timeout(30000);
it('should be able to receive binary data as blob when bouncing it back (polling)', function(done) {
var binaryData = new Int8Array(5);
for (var i = 0; i < 5; i++) {
binaryData[i] = i;
}
var socket = new eio.Socket();
socket.binaryType = 'blob';
socket.on('open', function() {
socket.send(binaryData);
socket.on('message', function (data) {
if (typeof data === 'string') return;
expect(data).to.be.a(Blob);
var fr = new FileReader();
fr.onload = function() {
var ab = this.result;
var ia = new Int8Array(ab);
expect(ia).to.eql(binaryData);
socket.close();
done();
};
fr.readAsArrayBuffer(data);
});
});
});
it('should be able to send data as a blob when bouncing it back (polling)', function(done) {
var binaryData = new Int8Array(5);
for (var i = 0; i < 5; i++) {
binaryData[i] = i;
}
var socket = new eio.Socket();
socket.on('open', function() {
if (blobSupported) {
socket.send(new Blob([binaryData.buffer]));
} else {
var bb = new BlobBuilder();
bb.append(binaryData.buffer);
socket.send(bb.getBlob());
}
socket.on('message', function (data) {
if (typeof data == 'string') { return; }
expect(data).to.be.an(ArrayBuffer);
expect(new Int8Array(data)).to.eql(binaryData);
socket.close();
done();
});
});
});
});

91
test/blob/ws.js Normal file
View File

@@ -0,0 +1,91 @@
var expect = require('expect.js');
var eio = require('../../');
var blobSupported = (function() {
try {
var b = new Blob(['hi']);
return b.size == 2;
} catch(e) {
return false;
}
})();
describe('blob', function() {
this.timeout(30000);
it('should be able to receive binary data as blob when bouncing it back (ws)', function(done) {
var binaryData = new Int8Array(5);
for (var i = 0; i < 5; i++) {
binaryData[i] = i;
}
var socket = new eio.Socket();
socket.binaryType = 'blob';
socket.on('open', function() {
socket.on('upgrade', function() {
socket.send(binaryData);
socket.on('message', function (data) {
expect(data).to.be.a(Blob);
var fr = new FileReader();
fr.onload = function() {
var ab = this.result;
var ia = new Int8Array(ab);
expect(ia).to.eql(binaryData);
socket.close();
done();
};
fr.readAsArrayBuffer(data);
});
});
});
});
it('should be able to send data as a blob when bouncing it back (ws)', function(done) {
var binaryData = new Int8Array(5);
for (var i = 0; i < 5; i++) {
binaryData[i] = i;
}
var socket = new eio.Socket();
socket.on('open', function() {
socket.on('upgrade', function() {
if (blobSupported) {
socket.send(new Blob([binaryData.buffer]));
} else {
var bb = new BlobBuilder();
bb.append(binaryData.buffer);
socket.send(bb.getBlob());
}
socket.on('message', function (data) {
expect(data).to.be.an(ArrayBuffer);
expect(new Int8Array(data)).to.eql(binaryData);
socket.close();
done();
});
});
});
});
it('should be able to send data as a blob encoded into base64 when bouncing it back (ws)', function(done) {
var binaryData = new Int8Array(5);
for (var i = 0; i < 5; i++) {
binaryData[i] = i;
}
var socket = new eio.Socket({ forceBase64: true });
socket.on('open', function() {
socket.on('upgrade', function() {
if (blobSupported) {
socket.send(new Blob([binaryData.buffer]));
} else {
var bb = new BlobBuilder();
bb.append(binaryData.buffer);
socket.send(bb.getBlob());
}
socket.on('message', function (data) {
expect(data).to.be.an(ArrayBuffer);
expect(new Int8Array(data)).to.eql(binaryData);
socket.close();
done();
});
});
});
});
});

152
test/browser-only-parser.js Normal file
View File

@@ -0,0 +1,152 @@
/**
* Test dependencies.
*/
var expect = require('expect.js');
var eio = require('../');
var parser = eio.parser
/**
* Shortcuts
*/
var encode = parser.encodePacket;
var decode = parser.decodePacket;
var encPayload = parser.encodePayload;
var decPayload = parser.decodePayload;
var encPayloadB = parser.encodePayloadAsArrayBuffer;
var encPayloadBB = parser.encodePayloadAsBlob;
var decPayloadB = parser.decodePayloadAsBinary;
var canUseBlobs = (function() {
try {
new Blob(["hi"]);
return true;
} catch(e) {
return !!global.BlobBuilder;
}
})();
/**
* Tests.
*/
describe('browser-only-parser', function () {
it('should encode a binary message', function(done) {
var data = new Int8Array(5);
for (var i = 0; i < data.length; i++) data[i] = i;
encode({ type: 'message', data: data }, function (encoded) {
expect(decode(encoded)).to.eql({ type: 'message', data: data.buffer });
done();
});
});
it('should encode/decode mixed binary and string contents as b64', function(done) {
var data = new Int8Array(5);
for (var i = 0; i < data.length; i++) data[i] = i;
encPayload([{ type: 'message', data: data }, { type: 'message', data: 'hello' }], function(encoded) {
decPayload(encoded, function(packet, index, total) {
var isLast = index + 1 == total;
expect(packet.type).to.eql('message');
if (!isLast) {
expect(new Int8Array(packet.data)).to.eql(data);
} else {
expect(packet.data).to.eql('hello');
done();
}
});
});
});
it('should encode binary contents as binary (ArrayBuffer)', function(done) {
var first = new Int8Array(5);
for (var i = 0; i < first.length; i++) first[i] = i;
var second = new Int8Array(4);
for (var i = 0; i < second.length; i++) second[i] = first.length + i;
encPayloadB([{ type: 'message', data: first }, { type: 'message', data: second }], function(data) {
decPayloadB(data, function(packet, index, total) {
var isLast = index + 1 == total;
expect(packet.type).to.eql('message');
if (!isLast) {
expect(new Int8Array(packet.data)).to.eql(first);
} else {
expect(new Int8Array(packet.data)).to.eql(second);
done();
}
});
});
});
it('should encode mixed binary and string contents as binary (ArrayBuffer)', function(done) {
var first = new Int8Array(15);
for (var i = 0; i < first.length; i++) first[i] = i;
encPayloadB([ { type: 'message', data: first }, { type: 'message', data: 'hello' }, { type: 'close' } ], function(data) {
decPayloadB(data, function(packet, index, total) {
if (index == 0) {
expect(packet.type).to.eql('message');
expect(new Int8Array(packet.data)).to.eql(first);
} else if (index == 1) {
expect(packet.type).to.eql('message');
expect(packet.data).to.eql('hello');
} else {
expect(packet.type).to.eql('close');
done();
}
});
});
});
if (canUseBlobs) {
it('should encode binary contents as binary (Blob)', function(done) {
var first = new Int8Array(5);
for (var i = 0; i < first.length; i++) first[i] = i;
var second = new Int8Array(4);
for (var i = 0; i < second.length; i++) second[i] = first.length + i;
encPayloadBB([{ type: 'message', data: first }, { type: 'message', data: second }], function(data) {
var fr = new FileReader();
fr.onload = function() {
decPayloadB(this.result, function(packet, index, total) {
var isLast = index + 1 == total;
expect(packet.type).to.eql('message');
if (!isLast) {
expect(new Int8Array(packet.data)).to.eql(first);
} else {
expect(new Int8Array(packet.data)).to.eql(second);
done();
}
});
};
fr.readAsArrayBuffer(data);
});
});
it('should encode mixed binary and string contents as binary (Blob)', function(done) {
var first = new Int8Array(5);
for (var i = 0; i < first.length; i++) first[i] = i;
encPayloadBB([ { type: 'message', data: first }, { type: 'message', data: 'hello' }, { type: 'close' } ], function(data) {
var fr = new FileReader();
fr.onload = function() {
decPayloadB(this.result, function(packet, index, total) {
if (index == 0) {
expect(packet.type).to.eql('message');
expect(new Int8Array(packet.data)).to.eql(first);
} else if (index == 1) {
expect(packet.type).to.eql('message');
expect(packet.data).to.eql('hello');
} else {
expect(packet.type).to.eql('close');
done();
}
});
};
fr.readAsArrayBuffer(data);
});
});
}
});

View File

@@ -2,7 +2,7 @@ var expect = require('expect.js');
var eio = require('../');
describe('connection', function() {
this.timeout(10000);
this.timeout(20000);
it('should connect to localhost', function(done){
var socket = new eio.Socket();

View File

@@ -7,6 +7,21 @@ global.WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR = null;
global.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION = null;
global.WEB_SOCKET_SWF_LOCATION = null;
var blobSupported = (function() {
try {
new Blob(['hi']);
return true;
} catch(e) {}
return false;
})();
/**
* Create a blob builder even when vendor prefixes exist
*/
var BlobBuilder = global.BlobBuilder || global.WebKitBlobBuilder || global.MSBlobBuilder || global.MozBlobBuilder;
var blobBuilderSupported = !!BlobBuilder && !!BlobBuilder.prototype.append && !!BlobBuilder.prototype.getBlob;
require('./engine.io-client');
require('./util');
require('./parser');
@@ -16,4 +31,14 @@ require('./transport');
// browser only tests
if (env.browser) {
require('./connection');
if (global.ArrayBuffer) {
require('./browser-only-parser');
require('./arraybuffer');
} else {
require('./binary-fallback');
}
if (blobSupported || blobBuilderSupported) {
require('./blob');
}
}

View File

@@ -15,7 +15,7 @@ var parser = eio.parser
var encode = parser.encodePacket
, decode = parser.decodePacket
, encPayload = parser.encodePayload
, decPayload = parser.decodePayload
, decPayload = parser.decodePayload;
/**
* Tests.
@@ -25,59 +25,85 @@ describe('parser', function () {
describe('packets', function () {
describe('basic functionality', function () {
it('should encode packets as strings', function () {
expect(encode({ type: 'message', data: 'test' })).to.be.a('string');
it('should encode packets as strings', function (done) {
encode({ type: 'message', data: 'test' }, function(data) {
expect(data).to.be.a('string');
done();
});
});
it('should decode packets as objects', function () {
expect(decode(encode({ type: 'message', data: 'test' }))).to.be.an('object');
it('should decode packets as objects', function (done) {
encode({ type: 'message', data: 'test' }, function(data) {
expect(decode(data)).to.be.an('object');
done();
});
});
});
describe('encoding and decoding', function () {
it('should allow no data', function () {
expect(decode(encode({ type: 'message' })))
.to.eql({ type: 'message' });
it('should allow no data', function (done) {
encode({ type: 'message' }, function(data) {
expect(decode(data)).to.eql({ type: 'message' });
done();
});
});
it('should encode an open packet', function () {
expect(decode(encode({ type: 'open', data: '{"some":"json"}' })))
.to.eql({ type: 'open', data: '{"some":"json"}' });
it('should encode an open packet', function (done) {
encode({ type: 'open', data: '{"some":"json"}' }, function(data) {
expect(decode(data)).to.eql({ type: 'open', data: '{"some":"json"}' });
done();
});
});
it('should encode a close packet', function () {
expect(decode(encode({ type: 'close' })))
.to.eql({ type: 'close' });
it('should encode a close packet', function (done) {
encode({ type: 'close' }, function(data) {
expect(decode(data)).to.eql({ type: 'close' });
done();
});
});
it('should encode a ping packet', function () {
expect(decode(encode({ type: 'ping', data: '1' })))
.to.eql({ type: 'ping', data: '1' });
it('should encode a ping packet', function (done) {
encode({ type: 'ping', data: '1' }, function(data) {
expect(decode(data)).to.eql({ type: 'ping', data: '1' });
done();
});
});
it('should encode a pong packet', function () {
expect(decode(encode({ type: 'pong', data: '1' })))
.to.eql({ type: 'pong', data: '1' });
it('should encode a pong packet', function (done) {
encode({ type: 'pong', data: '1' }, function(data) {
expect(decode(data)).to.eql({ type: 'pong', data: '1' });
done();
});
});
it('should encode a message packet', function () {
expect(decode(encode({ type: 'message', data: 'aaa' })))
.to.eql({ type: 'message', data: 'aaa' });
it('should encode a message packet', function (done) {
encode({ type: 'message', data: 'aaa' }, function(data) {
expect(decode(data)).to.eql({ type: 'message', data: 'aaa' });
done();
});
});
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 a message packet coercing to string', function (done) {
encode({ type: 'message', data: 1 }, function(data) {
expect(decode(data)).to.eql({ type: 'message', data: 1 });
done();
});
});
it('should encode an upgrade packet', function () {
expect(decode(encode({ type: 'upgrade' })))
.to.eql({ type: 'upgrade' });
it('should encode an upgrade packet', function (done) {
encode({ type: 'upgrade' }, function(data) {
expect(decode(data)).to.eql({ type: 'upgrade' });
done();
});
});
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]$/);
encode({ type: 'message', data: 'test' }, function(data) {
expect(data).to.match(/^[0-9]/);
});
encode({ type: 'message' }, function(data) {
expect(data).to.match(/^[0-9]$/);
});
});
});
@@ -86,7 +112,7 @@ describe('parser', function () {
it('should disallow bad format', function () {
expect(decode(':::')).to.eql(err);
})
});
it('should disallow inexistent types', function () {
expect(decode('94103')).to.eql(err);
@@ -94,93 +120,104 @@ describe('parser', function () {
});
});
var packets, indices, totals;
initCallback = function() {
packets = []; indices = []; totals = [];
}
callback = function(packet, index, total) {
packets.push(packet);
indices.push(index);
totals.push(total);
}
describe('payloads', function () {
describe('basic functionality', function () {
it('should encode payloads as strings', function () {
expect(encPayload([{ type: 'ping' }, { type: 'post' }])).to.be.a('string');
});
it('should decode payloads as arrays', function () {
initCallback();
decPayload(encPayload(['1:a', '2:b']), callback)
expect(packets).to.be.an('array');
it('should encode payloads as strings', function (done) {
encPayload([{ type: 'ping' }, { type: 'post' }], function(data) {
expect(data).to.be.a('string');
done();
});
});
});
describe('encoding and decoding', function () {
it('should encode/decode packets', function () {
initCallback();
decPayload(encPayload([{ type: 'message', data: 'a' }]), callback);
expect(packets).to.eql([{ type: 'message', data: 'a' }]);
var seen = 0;
it('should encode/decode packets', function (done) {
encPayload([{ type: 'message', data: 'a' }], function(data) {
decPayload(data,
function(packet, index, total) {
var isLast = index + 1 == total;
expect(isLast).to.eql(true);
seen++;
});
});
encPayload([{type: 'message', data: 'a'}, {type: 'ping'}], function(data) {
decPayload(data,
function(packet, index, total) {
var isLast = index + 1 == total;
if (!isLast) {
expect(packet.type).to.eql('message');
} else {
expect(packet.type).to.eql('ping');
if (seen == 2) { done(); }
}
seen++;
});
});
});
it('should encode/decode empty payloads', function () {
initCallback();
decPayload(encPayload([]), callback);
expect(packets).to.have.length(0);
expect(indices).to.have.length(0);
expect(totals).to.have.length(0);
});
it('should encode/decode multiple packets with correct indices/totals', function () {
initCallback();
decPayload(encPayload([{ type: 'message', data: 'a' },
{ type: 'message', data: 'b' }, { type: 'message', data: 'c' }]), callback);
expect(packets).to.eql([{ type: 'message', data: 'a' },
{ type: 'message', data: 'b' }, { type: 'message', data: 'c' }]);
expect(indices).to.eql([ 3, 7, 11 ]);
expect(totals).to.eql([ 12, 12, 12 ]);
encPayload([], function(data) {
decPayload(data,
function (packet, index, total) {
expect(packet.type).to.eql('open');
var isLast = index + 1 == total;
expect(isLast).to.eql(true);
});
});
});
});
describe('decoding error handling', function () {
var err = [{ type: 'error', data: 'parser error' }];
var err = { type: 'error', data: 'parser error' };
it('should err on bad payload format', function () {
initCallback();
decPayload('1!', callback)
expect(packets).to.eql(err);
initCallback();
decPayload('', callback);
expect(packets).to.eql(err);
initCallback();
decPayload('))', callback);
expect(packets).to.eql(err);
decPayload('1!', function (packet, index, total) {
var isLast = index + 1 == total;
expect(packet).to.eql(err);
expect(isLast).to.eql(true);
});
decPayload('', function (packet, index, total) {
var isLast = index + 1 == total;
expect(packet).to.eql(err);
expect(isLast).to.eql(true);
});
decPayload('))', function (packet, index, total) {
var isLast = index + 1 == total;
expect(packet).to.eql(err);
expect(isLast).to.eql(true);
});
});
it('should err on bad payload length', function () {
initCallback();
decPayload('1:aa', callback);
expect(packets).to.eql(err);
initCallback();
decPayload('1:', callback);
expect(packets).to.eql(err);
initCallback();
decPayload('1:a2:b', callback);
expect(packets).to.eql(err);
// line 137
decPayload('1:', function (packet, index, total) {
var isLast = index + 1 == total;
expect(packet).to.eql(err);
expect(isLast).to.eql(true);
});
});
it('should err on bad packet format', function () {
initCallback();
decPayload('3:99:', callback);
expect(packets).to.eql(err);
// line 137
decPayload('3:99:', function (packet, index, total) {
var isLast = index + 1 == total;
expect(packet).to.eql(err);
expect(isLast).to.eql(true);
});
// line 146
decPayload('1:aa', function (packet, index, total) {
var isLast = index + 1 == total;
expect(packet).to.eql(err);
expect(isLast).to.eql(true);
});
// line 137
decPayload('1:a2:b', function (packet, index, total) {
var isLast = index + 1 == total;
expect(packet).to.eql(err);
expect(isLast).to.eql(true);
});
});
});
});
});

View File

@@ -26,4 +26,18 @@ app.get('/test/support/engine.io.js', function(err, res, next) {
server.on('connection', function(socket){
socket.send('hi');
// Bounce any received messages back
socket.on('message', function (data) {
if (data === 'give binary') {
var abv = new Int8Array(5);
for (var i = 0; i < 5; i++) {
abv[i] = i;
}
socket.send(abv);
return;
}
socket.send(data);
});
});