diff --git a/.zuul.yml b/.zuul.yml index 566f6780..88b88d6e 100644 --- a/.zuul.yml +++ b/.zuul.yml @@ -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 diff --git a/Makefile b/Makefile index 7c70beb5..664ca149 100644 --- a/Makefile +++ b/Makefile @@ -18,4 +18,4 @@ test-cov: --reporter $(REPORTER) \ $(TESTS) -.PHONY: test +.PHONY: test build diff --git a/README.md b/README.md index 3b6d2725..af7fe2a0 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,23 @@ browserify app.js > bundle.js ``` +### Sending and receiving binary + +```html + + +``` + ### 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. diff --git a/lib/socket.js b/lib/socket.js index 5b635b5a..4d019f1c 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -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](); diff --git a/lib/transport.js b/lib/transport.js index 9e7ea558..57af7f31 100644 --- a/lib/transport.js +++ b/lib/transport.js @@ -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)); }; /** diff --git a/lib/transports/flashsocket.js b/lib/transports/flashsocket.js index 67735f6a..aa31f9a3 100644 --- a/lib/transports/flashsocket.js +++ b/lib/transports/flashsocket.js @@ -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); diff --git a/lib/transports/polling-jsonp.js b/lib/transports/polling-jsonp.js index 3ad0da74..b89f8c53 100644 --- a/lib/transports/polling-jsonp.js +++ b/lib/transports/polling-jsonp.js @@ -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 * diff --git a/lib/transports/polling-xhr.js b/lib/transports/polling-xhr.js index 058cb6a8..f7953d6f 100755 --- a/lib/transports/polling-xhr.js +++ b/lib/transports/polling-xhr.js @@ -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); } diff --git a/lib/transports/polling.js b/lib/transports/polling.js index 79f4c7c7..0294e8f2 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -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 diff --git a/lib/transports/websocket.js b/lib/transports/websocket.js index a5f0102c..30bf1f2f 100644 --- a/lib/transports/websocket.js +++ b/lib/transports/websocket.js @@ -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 diff --git a/package.json b/package.json index 1e682826..4a10a5ff 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/arraybuffer/index.js b/test/arraybuffer/index.js new file mode 100644 index 00000000..0dabe36e --- /dev/null +++ b/test/arraybuffer/index.js @@ -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'); +} diff --git a/test/arraybuffer/polling.js b/test/arraybuffer/polling.js new file mode 100644 index 00000000..4ed706b3 --- /dev/null +++ b/test/arraybuffer/polling.js @@ -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(); + }); + }); + }); +}); diff --git a/test/arraybuffer/ws.js b/test/arraybuffer/ws.js new file mode 100644 index 00000000..26d03377 --- /dev/null +++ b/test/arraybuffer/ws.js @@ -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(); + }); + }); + }); + }); +}); diff --git a/test/binary-fallback.js b/test/binary-fallback.js new file mode 100644 index 00000000..27de8a4b --- /dev/null +++ b/test/binary-fallback.js @@ -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(); + }); + }); + }); +}); diff --git a/test/blob/index.js b/test/blob/index.js new file mode 100644 index 00000000..f433cd54 --- /dev/null +++ b/test/blob/index.js @@ -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'); +} diff --git a/test/blob/polling.js b/test/blob/polling.js new file mode 100644 index 00000000..8098591b --- /dev/null +++ b/test/blob/polling.js @@ -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(); + }); + }); + }); +}); diff --git a/test/blob/ws.js b/test/blob/ws.js new file mode 100644 index 00000000..e91510c5 --- /dev/null +++ b/test/blob/ws.js @@ -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(); + }); + }); + }); + }); +}); diff --git a/test/browser-only-parser.js b/test/browser-only-parser.js new file mode 100644 index 00000000..37972eb9 --- /dev/null +++ b/test/browser-only-parser.js @@ -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); + }); + }); + } +}); diff --git a/test/connection.js b/test/connection.js index ecc64c4b..5b80b83f 100644 --- a/test/connection.js +++ b/test/connection.js @@ -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(); diff --git a/test/index.js b/test/index.js index 88d7c45a..b6d7bdba 100644 --- a/test/index.js +++ b/test/index.js @@ -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'); + } } diff --git a/test/parser.js b/test/parser.js index 285ce38d..d279d614 100644 --- a/test/parser.js +++ b/test/parser.js @@ -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); + }); }); }); }); - }); diff --git a/test/support/server.js b/test/support/server.js index e2d55249..732e8312 100644 --- a/test/support/server.js +++ b/test/support/server.js @@ -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); + }); });