From dc4cab56198b2f895e00f165671c4e9ed041edd5 Mon Sep 17 00:00:00 2001 From: Tony Kovanen Date: Fri, 24 Jan 2014 09:56:25 +0200 Subject: [PATCH 01/39] Binary support --- README.md | 13 +- engine.io.js | 458 +++++++++++++++++++++++++++----- lib/socket.js | 9 +- lib/transport.js | 4 +- lib/transports/flashsocket.js | 10 +- lib/transports/polling-jsonp.js | 6 + lib/transports/polling-xhr.js | 28 +- lib/transports/polling.js | 29 +- lib/transports/websocket.js | 23 +- lib/util.js | 26 ++ test/browser-only-parser.js | 84 ++++++ test/connection.js | 123 +++++++++ test/index.js | 1 + test/parser.js | 3 +- test/support/server.js | 5 + 15 files changed, 724 insertions(+), 98 deletions(-) create mode 100644 test/browser-only-parser.js diff --git a/README.md b/README.md index 0c0da8d7..600f3cb2 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,11 @@ socket.onopen = function(){ - Easy to debug - Easy to unit test - Runs inside HTML5 WebWorker +- Can send and receive binary data + - Receives in ArrayBuffer or Blob when in browser, and Buffer or ArrayBuffer + in Node + - With browsers that don't support ArrayBuffer, an object { base64: true, + data: dataAsBase64String } is emitted in onmessage ## API @@ -95,6 +100,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 +111,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. - `error` @@ -141,7 +150,7 @@ Exposed as `eio` in the browser standalone build. - `send` - Sends a message to the server - **Parameters** - - `String`: data to send + - `String` | `ArrayBuffer` | `ArrayBufferView`: data to send - `Function`: optional, callback upon `drain` - `close` - Disconnects the client. diff --git a/engine.io.js b/engine.io.js index e4d6a120..9e2a193e 100644 --- a/engine.io.js +++ b/engine.io.js @@ -131,6 +131,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 || ''; @@ -140,6 +141,7 @@ function Socket(uri, opts){ this.callbackBuffer = []; this.policyPort = opts.policyPort || 843; this.open(); + this.binaryType = null; } /** @@ -197,10 +199,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; @@ -411,6 +415,7 @@ Socket.prototype.onPacket = function (packet) { event.toString = function () { return packet.data; }; + this.onmessage && this.onmessage.call(this, event); break; } @@ -489,7 +494,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](); @@ -647,8 +652,7 @@ Socket.prototype.filterUpgrades = function (upgrades) { return filteredUpgrades; }; -},{"./emitter":2,"./transport":5,"./transports":7,"./util":12,"debug":14,"engine.io-parser":16,"global":19,"indexof":21}],5:[function(require,module,exports){ - +},{"./emitter":2,"./transport":5,"./transports":7,"./util":12,"debug":14,"engine.io-parser":16,"global":18,"indexof":20}],5:[function(require,module,exports){ /** * Module dependencies. */ @@ -680,6 +684,7 @@ function Transport (opts) { this.timestampRequests = opts.timestampRequests; this.readyState = ''; this.agent = opts.agent || false; + this.socket = opts.socket; } /** @@ -769,7 +774,7 @@ Transport.prototype.onOpen = function () { */ Transport.prototype.onData = function (data) { - this.onPacket(parser.decodePacket(data)); + this.onPacket(parser.decodePacket(data, this.socket.binaryType)); }; /** @@ -845,6 +850,12 @@ util.inherits(FlashWS, WS); FlashWS.prototype.name = 'flashsocket'; +/* + * FlashSockets only support binary as base64 encoded strings + */ + +FlashWS.prototype.supportsBinary = false; + /** * Opens the transport. * @@ -885,7 +896,7 @@ FlashWS.prototype.doOpen = function(){ load(deps, function(){ self.ready(function(){ WebSocket.__addTask(function () { - self.socket = new WebSocket(self.uri()); + self.webSocket = new WebSocket(self.uri()); self.addEventListeners(); }); }); @@ -899,7 +910,7 @@ FlashWS.prototype.doOpen = function(){ */ FlashWS.prototype.doClose = function(){ - if (!this.socket) return; + if (!this.webSocket) return; var self = this; WebSocket.__addTask(function(){ WS.prototype.doClose.call(self); @@ -1055,7 +1066,7 @@ function load(arr, fn){ process(0); } -},{"../util":12,"./websocket":11,"debug":14,"global":19}],7:[function(require,module,exports){ +},{"../util":12,"./websocket":11,"debug":14,"global":18}],7:[function(require,module,exports){ /** * Module dependencies @@ -1114,7 +1125,7 @@ function polling (opts) { } }; -},{"./flashsocket":6,"./polling-jsonp":8,"./polling-xhr":9,"./websocket":11,"global":19,"xmlhttprequest":13}],8:[function(require,module,exports){ +},{"./flashsocket":6,"./polling-jsonp":8,"./polling-xhr":9,"./websocket":11,"global":18,"xmlhttprequest":13}],8:[function(require,module,exports){ /** * Module requirements. @@ -1196,6 +1207,12 @@ function JSONPPolling (opts) { util.inherits(JSONPPolling, Polling); +/* + * JSONP only supports binary as base64 encoded strings + */ + +JSONPPolling.prototype.supportsBinary = false; + /** * Closes the socket * @@ -1335,7 +1352,7 @@ JSONPPolling.prototype.doWrite = function (data, fn) { } }; -},{"../util":12,"./polling":10,"global":19}],9:[function(require,module,exports){ +},{"../util":12,"./polling":10,"global":18}],9:[function(require,module,exports){ /** * Module requirements. */ @@ -1401,6 +1418,12 @@ function XHR(opts){ util.inherits(XHR, Polling); +/** + * XHR supports binary + */ + +XHR.prototype.supportsBinary = true; + /** * Creates a request. * @@ -1413,6 +1436,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); }; @@ -1425,7 +1449,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){ @@ -1467,7 +1492,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); } /** @@ -1482,8 +1507,9 @@ 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 }); + if (supportsBinary) xhr.responseType = 'arraybuffer'; var self = this; try { @@ -1492,7 +1518,8 @@ Request.prototype.create = function(){ 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) {} } @@ -1507,7 +1534,13 @@ 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 @@ -1527,7 +1560,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() { @@ -1622,7 +1655,7 @@ if (xobject) { }); } -},{"../emitter":2,"../util":12,"./polling":10,"debug":14,"global":19,"xmlhttprequest":13}],10:[function(require,module,exports){ +},{"../emitter":2,"../util":12,"./polling":10,"debug":14,"global":18,"xmlhttprequest":13}],10:[function(require,module,exports){ /** * Module dependencies. */ @@ -1652,6 +1685,8 @@ var global = require('global'); */ function Polling(opts){ + var forceBase64 = (opts && opts.forceBase64); + if (!util.bs.xhr2 || forceBase64) this.supportsBinary = false; Transport.call(this, opts); } @@ -1744,10 +1779,8 @@ 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) { - // if its the first message we consider the transport open + var callback = function(packet, index, total) { + // if its the first message we consider the transport open if ('opening' == self.readyState) { self.onOpen(); } @@ -1760,7 +1793,14 @@ Polling.prototype.onData = function(data){ // otherwise bypass onData and handle the message self.onPacket(packet); - }); + }; + + // decode payload + if (typeof data === 'string') { + parser.decodePayload(data, this.socket.binaryType, callback); + } else { + parser.decodePayloadAsBinary(data, this.socket.binaryType, callback); + } // if an event did not trigger closing if ('closed' != this.readyState) { @@ -1812,10 +1852,16 @@ 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'); - }); + }; + + if (!this.supportsBinary) { + this.doWrite(parser.encodePayload(packets), callbackfn); + } else { + this.doWrite(parser.encodePayloadAsBinary(packets), callbackfn); + } }; /** @@ -1840,6 +1886,8 @@ Polling.prototype.uri = function(){ } } + if (!this.supportsBinary && !query.sid) query.b64 = 1; + query = util.qs(query); // avoid port if default for schema @@ -1856,7 +1904,7 @@ Polling.prototype.uri = function(){ return schema + '://' + this.hostname + port + this.path + query; }; -},{"../transport":5,"../util":12,"debug":14,"engine.io-parser":16,"global":19}],11:[function(require,module,exports){ +},{"../transport":5,"../util":12,"debug":14,"engine.io-parser":16,"global":18}],11:[function(require,module,exports){ /** * Module dependencies. */ @@ -1904,6 +1952,12 @@ util.inherits(WS, Transport); WS.prototype.name = 'websocket'; +/* + * WebSockets support binary + */ + +WS.prototype.supportsBinary = true; + /** * Opens socket. * @@ -1921,7 +1975,8 @@ WS.prototype.doOpen = function(){ var protocols = void(0); var opts = { agent: this.agent }; - this.socket = new WebSocket(uri, protocols, opts); + this.webSocket = new WebSocket(uri, protocols, opts); + this.webSocket.binaryType = 'arraybuffer'; this.addEventListeners(); }; @@ -1934,16 +1989,16 @@ WS.prototype.doOpen = function(){ WS.prototype.addEventListeners = function(){ var self = this; - this.socket.onopen = function(){ + this.webSocket.onopen = function(){ self.onOpen(); }; - this.socket.onclose = function(){ + this.webSocket.onclose = function(){ self.onClose(); }; - this.socket.onmessage = function(ev){ + this.webSocket.onmessage = function(ev){ self.onData(ev.data); }; - this.socket.onerror = function(e){ + this.webSocket.onerror = function(e){ self.onError('websocket error', e); }; }; @@ -1978,7 +2033,7 @@ 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])); + this.webSocket.send(parser.encodePacket(packets[i])); } function ondrain() { self.writable = true; @@ -2006,8 +2061,8 @@ WS.prototype.onClose = function(){ */ WS.prototype.doClose = function(){ - if (typeof this.socket !== 'undefined') { - this.socket.close(); + if (typeof this.webSocket !== 'undefined') { + this.webSocket.close(); } }; @@ -2054,7 +2109,7 @@ WS.prototype.check = function(){ return !!WebSocket && !('__initialize' in WebSocket && this.name === WS.prototype.name); }; -},{"../transport":5,"../util":12,"debug":14,"engine.io-parser":16,"global":19,"ws":22}],12:[function(require,module,exports){ +},{"../transport":5,"../util":12,"debug":14,"engine.io-parser":16,"global":18,"ws":21}],12:[function(require,module,exports){ var global = require('global'); @@ -2213,6 +2268,32 @@ exports.ua.ios6 = exports.ua.ios && /OS 6_/.test(navigator.userAgent); exports.ua.chromeframe = Boolean(global.externalHost); +/** + * Binary support detection namespace. + * + * @namespace + */ + +exports.bs = {}; + +/** + * Detect ArrayBuffer + */ + +exports.bs.arraybuffer = global.ArrayBuffer !== undefined; + +/** + * Detect Blob + */ + +exports.bs.blob = global.Blob !== undefined; + +/** + * Detect XHR 2 + */ + +exports.bs.xhr2 = global.XMLHttpRequest !== undefined && (new XMLHttpRequest()).responseType !== undefined; + /** * Parses an URI * @@ -2276,7 +2357,7 @@ exports.qsParse = function(qs){ return qry; }; -},{"global":19}],13:[function(require,module,exports){ +},{"global":18}],13:[function(require,module,exports){ // browser shim for xmlhttprequest module var hasCORS = require('has-cors'); @@ -2297,7 +2378,7 @@ module.exports = function(opts) { } } -},{"has-cors":20}],14:[function(require,module,exports){ +},{"has-cors":19}],14:[function(require,module,exports){ /** * Expose `debug()` as the module. @@ -2600,17 +2681,31 @@ Emitter.prototype.hasListeners = function(event){ return !! this.listeners(event).length; }; -},{"indexof":21}],16:[function(require,module,exports){ - -module.exports = require('./lib/'); - -},{"./lib/":17}],17:[function(require,module,exports){ -/** +},{"indexof":20}],16:[function(require,module,exports){ +var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {};/** * Module dependencies. */ var keys = require('./keys'); +/** + * A utility for doing slicing, even when ArrayBuffer.prototype.slice doesn't + * exist + * + * @api private + */ + +function sliceBuffer(arraybuffer, start, end) { + if (arraybuffer.slice !== undefined) return arraybuffer.slice(start, end); + if (end === undefined) end = this.arraybuffer.byteLength; + if (start === undefined) start = 0; + + var abView = new Uin8Array(ab); + var newView = new Uint8Array(end - start); + for (var i = start, ii = 0; i < end; i++, ii++) newView[ii] = abView[i]; + return newView.buffer; +}; + /** * Current protocol version. */ @@ -2638,21 +2733,92 @@ var packetslist = keys(packets); var err = { type: 'error', data: 'parser error' }; +/** + * Converting base64 to arraybuffer helper + * + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding + * + * @api private + */ + +function b64ToUint6 (nChr) { + + return nChr > 64 && nChr < 91 ? + nChr - 65 + : nChr > 96 && nChr < 123 ? + nChr - 71 + : nChr > 47 && nChr < 58 ? + nChr + 4 + : nChr === 43 ? + 62 + : nChr === 47 ? + 63 + : + 0; + +} + +/** + * Converting base64 to arraybuffer + * + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding + * + * @api private + */ +function base64DecToArr (sBase64, nBlocksSize) { + + var + sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), nInLen = sB64Enc.length, + nOutLen = nBlocksSize + ? Math.ceil((nInLen * 3 + 1 >> 2) / nBlocksSize) * nBlocksSize + : nInLen * 3 + 1 >> 2, taBytes = new Uint8Array(nOutLen); + + for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) { + nMod4 = nInIdx & 3; + nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4; + if (nMod4 === 3 || nInLen - nInIdx === 1) { + for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) { + taBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255; + } + nUint24 = 0; + + } + } + + return taBytes; +} + /** * Encodes a packet. * - * [ `:` ] + * [ ] * * Example: * - * 5:hello world + * 5hello world * 3 * 4 * + * Binary is encoded in an identical principle + * * @api private */ exports.encodePacket = function (packet) { + var data = (packet.data === undefined) + ? undefined + : packet.data.buffer || packet.data; + + if (global.ArrayBuffer && data instanceof ArrayBuffer) { + var contentArray = new Uint8Array(data); + var resultBuffer = new Uint8Array(1 + data.byteLength); + + resultBuffer[0] = packets[packet.type]; + for (var i = 0; i < contentArray.length; i++) resultBuffer[i+1] = contentArray[i]; + return resultBuffer.buffer; + } + + // Sending data as a utf-8 string var encoded = packets[packet.type]; // data fragment is optional @@ -2661,27 +2827,70 @@ exports.encodePacket = function (packet) { } return '' + encoded; + }; /** - * Decodes a packet. + * Encodes a packet with binary data in a base64 string + * + * @param {Object} packet, has `type` and `data` + * @return {String} base64 encoded message + */ + +exports.encodeBase64Packet = function(packet) { + var message = '' + exports.packets[packet.type]; + var b64data = String.fromCharCode.apply(null, new Uint8Array(packet.data)); + message += global.btoa(b64data); + return message; +}; + +/** + * Decodes a packet. Changes format to Blob if requested. * * @return {Object} with `type` and `data` (if any) * @api private */ -exports.decodePacket = function (data) { - var type = data.charAt(0); +exports.decodePacket = function (data, binaryType) { + // String data + if (typeof data == 'string' || data === undefined) { + var type = data.charAt(0); - if (Number(type) != type || !packetslist[type]) { - return err; - } + if (Number(type) != type || !packetslist[type]) { + return err; + } - if (data.length > 1) { - return { type: packetslist[type], data: data.substring(1) }; - } else { - return { type: packetslist[type] }; + if (data.length > 1) { + return { type: packetslist[type], data: data.substring(1) }; + } else { + return { type: packetslist[type] }; + } } + + var asArray = new Uint8Array(data); + var type = asArray[0]; + var rest = sliceBuffer(data, 1); + if (global.Blob && Blob.prototype.slice && binaryType === 'blob') { + rest = new Blob([rest]); + } + return { type: packetslist[type], data: rest }; +}; + +/** + * Decodes a packet encoded in a base64 string + * + * @param {String} base64 encoded message + * @return {Object} with `type` and `data` (if any) + */ + +exports.decodeBase64Packet = function(msg, binaryType) { + var type = packetslist[msg.charAt(0)]; + var data = (!global.ArrayBuffer) + ? { base64: true, data: msg.substr(1) } + : base64DecToArr(msg.substr(1)).buffer; + + if (binaryType === 'blob') data = new Blob([data]); + return { type: type, data: data }; }; /** @@ -2693,6 +2902,9 @@ exports.decodePacket = function (data) { * * 11:hello world2:hi * + * If any contents are binary, they will be encoded as base64 strings. Base64 + * encoded strings are marked with a b before the length specifier + * * @param {Array} packets * @api private */ @@ -2706,22 +2918,34 @@ exports.encodePayload = function (packets) { var message; for (var i = 0, l = packets.length; i < l; i++) { - message = exports.encodePacket(packets[i]); - encoded += message.length + ':' + message; + if (!packets[i].data || typeof packets[i].data === 'string') { + var message = exports.encodePacket(packets[i]); + encoded += message.length + ':' + message; + } else { + var message = exports.encodeBase64Packet(packets[i]); + encoded += 'b' + message.length + ':' + message; + } } - + return encoded; }; /* - * Decodes data when a payload is maybe expected. + * Decodes data when a payload is maybe expected. Possible binary contents are + * decoded from their base64 representation * * @param {String} data, callback method * @api public */ -exports.decodePayload = function (data, callback) { +exports.decodePayload = function (data, binaryType, callback) { + if (typeof binaryType === 'function') { + callback = binaryType; + binaryType = null; + } + var packet; + var base64 = false; if (data == '') { // parser error - ignoring payload return callback(err, 0, 1); @@ -2732,8 +2956,9 @@ exports.decodePayload = function (data, callback) { for (var i = 0, l = data.length; i < l; i++) { var chr = data.charAt(i); - - if (':' != chr) { + + if (chr == 'b') base64 = true; + else if (':' != chr) { length += chr; } else { if ('' == length || (length != (n = Number(length)))) { @@ -2749,7 +2974,11 @@ exports.decodePayload = function (data, callback) { } if (msg.length) { - packet = exports.decodePacket(msg); + if (base64) { + packet = exports.decodeBase64Packet(msg, binaryType); + } else { + packet = exports.decodePacket(msg, binaryType); + } if (err.type == packet.type && err.data == packet.data) { // parser error in individual packet - ignoring payload @@ -2763,6 +2992,7 @@ exports.decodePayload = function (data, callback) { // advance cursor i += n; length = ''; + base64 = false; } } @@ -2773,7 +3003,101 @@ exports.decodePayload = function (data, callback) { }; -},{"./keys":18}],18:[function(require,module,exports){ +/** + * Encodes multiple messages (payload) as binary. + * + * <1 = binary, 0 = string>[...] + * + * Example: + * 1 3 255 1 2 3, if the binary contents are interpreted as 8 bit integers + * + * @param {Array} packets + * @return {ArrayBuffer} encoded payload + * @api private + */ + +exports.encodePayloadAsBinary = function (packets) { + if (!packets.length) { + return new ArrayBuffer(0); + } + + var encodedPackets = packets.map(exports.encodePacket); + var totalLength = encodedPackets.reduce(function(acc, p) { + var len; + if (typeof p === 'string') len = p.length; + else len = p.byteLength; + return acc + (new String(len)).length + len + 2; // string/binary identifier + separator = 2 + }, 0); + + var resultArray = new Uint8Array(totalLength); + + var bufferIndex = 0; + encodedPackets.forEach(function(p) { + var isString = typeof p === 'string'; + var ab = p; + if (isString) { + var view = new Uint8Array(p.length); + for (var i = 0; i < p.length; i++) view[i] = p.charCodeAt(i); + ab = view.buffer; + } + + if (isString) resultArray[bufferIndex++] = 0; // not true binary + else resultArray[bufferIndex++] = 1; // true binary + + var lenStr = new String(ab.byteLength); + for (var i = 0; i < lenStr.length; i++) resultArray[bufferIndex++] = parseInt(lenStr[i]); + resultArray[bufferIndex++] = 255; + + var view = new Uint8Array(ab); + for (var i = 0; i < view.length; i++) resultArray[bufferIndex++] = view[i]; + }); + + return resultArray.buffer; +}; + +/* + * Decodes data when a payload is maybe expected. Strings are decoded by + * interpreting each byte as a key code for entries marked to start with 0. See + * description of encodePayloadAsBinary + * + * @param {ArrayBuffer} data, callback method + * @api public + */ + +exports.decodePayloadAsBinary = function (data, binaryType, callback) { + if (typeof binaryType === 'function') { + callback = binaryType; + binaryType = null; + } + + var bufferTail = data; + var buffers = []; + + while (bufferTail.byteLength > 0) { + var tailArray = new Uint8Array(bufferTail); + var isString = tailArray[0] == 0; + var msgLength = ''; + for (var i = 1; ; i++) { + if (tailArray[i] == 255) break; + msgLength += tailArray[i]; + } + bufferTail = sliceBuffer(bufferTail, 2 + msgLength.length); + msgLength = parseInt(msgLength); + + var msg = sliceBuffer(bufferTail, 0, msgLength); + if (isString) msg = String.fromCharCode.apply(null, new Uint8Array(msg)); + buffers.push(msg); + bufferTail = sliceBuffer(bufferTail, msgLength); + } + + var total = buffers.length; + buffers.forEach(function(buffer, i) { + callback(exports.decodePacket(buffer, binaryType), i, total); + }); +}; + +},{"./keys":17}],17:[function(require,module,exports){ /** * Gets the keys for an object. @@ -2794,7 +3118,7 @@ module.exports = Object.keys || function keys (obj){ return arr; }; -},{}],19:[function(require,module,exports){ +},{}],18:[function(require,module,exports){ /** * Returns `this`. Execute this without a "context" (i.e. without it being @@ -2804,7 +3128,7 @@ module.exports = Object.keys || function keys (obj){ module.exports = (function () { return this; })(); -},{}],20:[function(require,module,exports){ +},{}],19:[function(require,module,exports){ /** * Module dependencies. @@ -2823,7 +3147,7 @@ var global = require('global'); module.exports = 'XMLHttpRequest' in global && 'withCredentials' in new global.XMLHttpRequest(); -},{"global":19}],21:[function(require,module,exports){ +},{"global":18}],20:[function(require,module,exports){ var indexOf = [].indexOf; @@ -2834,7 +3158,7 @@ module.exports = function(arr, obj){ } return -1; }; -},{}],22:[function(require,module,exports){ +},{}],21:[function(require,module,exports){ /** * Module dependencies. diff --git a/lib/socket.js b/lib/socket.js index 6c1b378e..eb29d274 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 || ''; @@ -84,6 +85,7 @@ function Socket(uri, opts){ this.callbackBuffer = []; this.policyPort = opts.policyPort || 843; this.open(); + this.binaryType = null; } /** @@ -141,10 +143,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; @@ -355,6 +359,7 @@ Socket.prototype.onPacket = function (packet) { event.toString = function () { return packet.data; }; + this.onmessage && this.onmessage.call(this, event); break; } @@ -433,7 +438,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..617a6107 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.webSocket = new WebSocket(self.uri()); self.addEventListeners(); }); }); @@ -105,7 +111,7 @@ FlashWS.prototype.doOpen = function(){ */ FlashWS.prototype.doClose = function(){ - if (!this.socket) return; + if (!this.webSocket) 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 4645d00c..c1f9b892 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,8 +152,9 @@ 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 }); + if (supportsBinary) xhr.responseType = 'arraybuffer'; var self = this; try { @@ -154,7 +163,8 @@ Request.prototype.create = function(){ 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 +179,13 @@ 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 +205,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() { diff --git a/lib/transports/polling.js b/lib/transports/polling.js index 79f4c7c7..d1788145 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -27,6 +27,8 @@ var global = require('global'); */ function Polling(opts){ + var forceBase64 = (opts && opts.forceBase64); + if (!util.bs.xhr2 || forceBase64) this.supportsBinary = false; Transport.call(this, opts); } @@ -119,10 +121,8 @@ 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) { - // if its the first message we consider the transport open + var callback = function(packet, index, total) { + // if its the first message we consider the transport open if ('opening' == self.readyState) { self.onOpen(); } @@ -135,7 +135,14 @@ Polling.prototype.onData = function(data){ // otherwise bypass onData and handle the message self.onPacket(packet); - }); + }; + + // decode payload + if (typeof data === 'string') { + parser.decodePayload(data, this.socket.binaryType, callback); + } else { + parser.decodePayloadAsBinary(data, this.socket.binaryType, callback); + } // if an event did not trigger closing if ('closed' != this.readyState) { @@ -187,10 +194,16 @@ 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'); - }); + }; + + if (!this.supportsBinary) { + this.doWrite(parser.encodePayload(packets), callbackfn); + } else { + this.doWrite(parser.encodePayloadAsBinary(packets), callbackfn); + } }; /** @@ -215,6 +228,8 @@ 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 324d55f9..483cccb3 100644 --- a/lib/transports/websocket.js +++ b/lib/transports/websocket.js @@ -45,6 +45,12 @@ util.inherits(WS, Transport); WS.prototype.name = 'websocket'; +/* + * WebSockets support binary + */ + +WS.prototype.supportsBinary = true; + /** * Opens socket. * @@ -62,7 +68,8 @@ WS.prototype.doOpen = function(){ var protocols = void(0); var opts = { agent: this.agent }; - this.socket = new WebSocket(uri, protocols, opts); + this.webSocket = new WebSocket(uri, protocols, opts); + this.webSocket.binaryType = 'arraybuffer'; this.addEventListeners(); }; @@ -75,16 +82,16 @@ WS.prototype.doOpen = function(){ WS.prototype.addEventListeners = function(){ var self = this; - this.socket.onopen = function(){ + this.webSocket.onopen = function(){ self.onOpen(); }; - this.socket.onclose = function(){ + this.webSocket.onclose = function(){ self.onClose(); }; - this.socket.onmessage = function(ev){ + this.webSocket.onmessage = function(ev){ self.onData(ev.data); }; - this.socket.onerror = function(e){ + this.webSocket.onerror = function(e){ self.onError('websocket error', e); }; }; @@ -119,7 +126,7 @@ 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])); + this.webSocket.send(parser.encodePacket(packets[i])); } function ondrain() { self.writable = true; @@ -147,8 +154,8 @@ WS.prototype.onClose = function(){ */ WS.prototype.doClose = function(){ - if (typeof this.socket !== 'undefined') { - this.socket.close(); + if (typeof this.webSocket !== 'undefined') { + this.webSocket.close(); } }; diff --git a/lib/util.js b/lib/util.js index d1011946..d6da5d01 100644 --- a/lib/util.js +++ b/lib/util.js @@ -156,6 +156,32 @@ exports.ua.ios6 = exports.ua.ios && /OS 6_/.test(navigator.userAgent); exports.ua.chromeframe = Boolean(global.externalHost); +/** + * Binary support detection namespace. + * + * @namespace + */ + +exports.bs = {}; + +/** + * Detect ArrayBuffer + */ + +exports.bs.arraybuffer = global.ArrayBuffer !== undefined; + +/** + * Detect Blob + */ + +exports.bs.blob = global.Blob !== undefined; + +/** + * Detect XHR 2 + */ + +exports.bs.xhr2 = global.XMLHttpRequest !== undefined && (new XMLHttpRequest()).responseType !== undefined; + /** * Parses an URI * diff --git a/test/browser-only-parser.js b/test/browser-only-parser.js new file mode 100644 index 00000000..ec850c7b --- /dev/null +++ b/test/browser-only-parser.js @@ -0,0 +1,84 @@ + +/** + * Test dependencies. + */ + +var expect = require('expect.js'); +var eio = require('../'); + +var parser = eio.parser + +/** + * Shortcuts + */ + +var encode = parser.encodePacket + , decode = parser.decodePacket + , encPayload = parser.encodePayload + , decPayload = parser.decodePayload + , encPayloadB = parser.encodePayloadAsBinary + , decPayloadB = parser.decodePayloadAsBinary + +/** + * Tests. + */ + +describe('browser-only-parser', function () { + it('should encode a binary message', function() { + var data = new Int8Array(5); + for (var i = 0; i < data.length; i++) data[i] = i; + expect(decode(encode({ type: 'message', data: data }))) + .to.eql({ type: 'message', data: data.buffer }); + }); + + it('should encode/decode mixed binary and string contents as b64', function() { + var data = new Int8Array(5); + for (var i = 0; i < data.length; i++) data[i] = i; + decPayload(encPayload([{ type: 'message', data: data }, { type: 'message', data: 'hello' }]), + 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'); + } + }); + }); + + it('should encode binary contents as binary', function() { + 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; + + decPayloadB(encPayloadB([{ type: 'message', data: first }, { type: 'message', data: second }]), + 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); + } + }); + }); + + it('should encode mixed binary and string contents as binary', function() { + var first = new Int8Array(15); + for (var i = 0; i < first.length; i++) first[i] = i; + + decPayloadB(encPayloadB([ { type: 'message', data: first }, { type: 'message', data: 'hello' }, { type: 'close' } ]), + 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'); + } + }); + }); +}); diff --git a/test/connection.js b/test/connection.js index ecc64c4b..be5eef8c 100644 --- a/test/connection.js +++ b/test/connection.js @@ -15,6 +15,129 @@ describe('connection', function() { }); }); + 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('ws://localhost', { transports: ['polling'] }); + socket.on('open', function() { + socket.send(binaryData); + socket.on('message', function (data) { + if (data === 'hi') return; + + expect(data instanceof ArrayBuffer).to.be(true); + expect(new Int8Array(data)).to.eql(binaryData); + socket.close(); + done(); + }); + }); + }); + + 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 instanceof Blob).to.be(true); + var fr = new FileReader(); + var ab = fr.readAsArrayBuffer(data); + var ia = new Int8Array(ab); + for (var i = 0; i < ia.length; i++) expect(ia[i]).to.equal(i); + 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 instanceof ArrayBuffer).to.be(true); + var ia = new Int8Array(data); + expect(ia).to.eql(binaryData); + socket.close(); + done(); + }); + }); + }); + + it('should be able to receive binary data when forcing base64 and not decode it when overriding ArrayBuffer (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); + var ab = global.ArrayBuffer; + global.ArrayBuffer = undefined; + var firstPacket = true; + socket.on('message', function (data) { + if (firstPacket) { + firstPacket = false; + return; + } + expect(data.base64).to.be(true); + expect(data.data).to.eql('AAECAwQ='); + + global.ArrayBuffer = ab; + socket.close(); + done(); + }); + }); + }); + + 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 instanceof ArrayBuffer).to.be(true); + expect(new Int8Array(data)).to.eql(binaryData); + + socket.close(); + done(); + }); + }); + }); + }); + + 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) { + if (typeof data === 'string') return; + + expect(data instanceof Blob).to.be(true); + var fr = new FileReader(); + var ab = fr.readAsArrayBuffer(data); + var ia = new Int8Array(ab); + for (var i = 0; i < ia.length; i++) expect(ia[i]).to.equal(i); + socket.close(); + done(); + }); + }); + }); + }); + // no `Worker` on old IE if (global.Worker) { it('should work in a worker', function(done){ diff --git a/test/index.js b/test/index.js index 88d7c45a..98439192 100644 --- a/test/index.js +++ b/test/index.js @@ -16,4 +16,5 @@ require('./transport'); // browser only tests if (env.browser) { require('./connection'); + require('./browser-only-parser'); } diff --git a/test/parser.js b/test/parser.js index 285ce38d..ea544f41 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. @@ -182,5 +182,4 @@ describe('parser', function () { }); }); }); - }); diff --git a/test/support/server.js b/test/support/server.js index ce9195c4..1087f382 100644 --- a/test/support/server.js +++ b/test/support/server.js @@ -12,4 +12,9 @@ app.use('/test/support', express.static(join(__dirname, 'public'))); server.on('connection', function(socket){ socket.send('hi'); + + // Bounce any received messages back + socket.on('message', function (data) { + socket.send(data); + }); }); From 552f871b063eb59b0cb8cdb4a206d3d2b0c852e7 Mon Sep 17 00:00:00 2001 From: Tony Kovanen Date: Sat, 1 Feb 2014 14:54:23 +0200 Subject: [PATCH 02/39] Parser now has to figure out whether binary or string data is sent --- README.md | 17 +++++++++++++++++ engine.io.js | 20 +++++++++----------- lib/transports/polling.js | 12 ++---------- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 600f3cb2..0f7dfa73 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,23 @@ this repository, which is a standalone build you can use as follows: ``` +Sending and receiving binary + +```html + + +``` + ### Node.JS Add `engine.io-client` to your `package.json` and then: diff --git a/engine.io.js b/engine.io.js index 9e2a193e..12dafd48 100644 --- a/engine.io.js +++ b/engine.io.js @@ -1796,11 +1796,7 @@ Polling.prototype.onData = function(data){ }; // decode payload - if (typeof data === 'string') { - parser.decodePayload(data, this.socket.binaryType, callback); - } else { - parser.decodePayloadAsBinary(data, this.socket.binaryType, callback); - } + parser.decodePayload(data, this.socket.binaryType, callback); // if an event did not trigger closing if ('closed' != this.readyState) { @@ -1857,11 +1853,7 @@ Polling.prototype.write = function(packets){ self.emit('drain'); }; - if (!this.supportsBinary) { - this.doWrite(parser.encodePayload(packets), callbackfn); - } else { - this.doWrite(parser.encodePayloadAsBinary(packets), callbackfn); - } + this.doWrite(parser.encodePayload(packets, this.supportsBinary), callbackfn); }; /** @@ -2909,7 +2901,9 @@ exports.decodeBase64Packet = function(msg, binaryType) { * @api private */ -exports.encodePayload = function (packets) { +exports.encodePayload = function (packets, supportsBinary) { + if (supportsBinary) { return exports.encodePayloadAsBinary(packets); } + if (!packets.length) { return '0:'; } @@ -2939,6 +2933,10 @@ exports.encodePayload = function (packets) { */ exports.decodePayload = function (data, binaryType, callback) { + if (!(typeof data == 'string')) { + return exports.decodePayloadAsBinary(data, binaryType, callback); + } + if (typeof binaryType === 'function') { callback = binaryType; binaryType = null; diff --git a/lib/transports/polling.js b/lib/transports/polling.js index d1788145..386c761e 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -138,11 +138,7 @@ Polling.prototype.onData = function(data){ }; // decode payload - if (typeof data === 'string') { - parser.decodePayload(data, this.socket.binaryType, callback); - } else { - parser.decodePayloadAsBinary(data, this.socket.binaryType, callback); - } + parser.decodePayload(data, this.socket.binaryType, callback); // if an event did not trigger closing if ('closed' != this.readyState) { @@ -199,11 +195,7 @@ Polling.prototype.write = function(packets){ self.emit('drain'); }; - if (!this.supportsBinary) { - this.doWrite(parser.encodePayload(packets), callbackfn); - } else { - this.doWrite(parser.encodePayloadAsBinary(packets), callbackfn); - } + this.doWrite(parser.encodePayload(packets, this.supportsBinary), callbackfn); }; /** From e419a13d040fcd7df5b2f3c77158de55b23fd47b Mon Sep 17 00:00:00 2001 From: Tony Kovanen Date: Sat, 1 Feb 2014 19:01:46 +0200 Subject: [PATCH 03/39] style --- engine.io.js | 59 ++++++++++++----------------------- lib/transports/polling-xhr.js | 6 ++-- lib/transports/polling.js | 9 +++++- lib/transports/websocket.js | 18 +++++------ lib/util.js | 26 --------------- test/browser-only-parser.js | 12 +++---- 6 files changed, 46 insertions(+), 84 deletions(-) diff --git a/engine.io.js b/engine.io.js index 12dafd48..f21b6049 100644 --- a/engine.io.js +++ b/engine.io.js @@ -1509,7 +1509,7 @@ Emitter(Request.prototype); Request.prototype.create = function(isBinary, supportsBinary){ var xhr = this.xhr = new XMLHttpRequest({ agent: this.agent, xdomain: this.xd }); - if (supportsBinary) xhr.responseType = 'arraybuffer'; + if (supportsBinary) { xhr.responseType = 'arraybuffer'; } var self = this; try { @@ -1538,8 +1538,8 @@ Request.prototype.create = function(isBinary, supportsBinary){ if (contentType === 'application/octet-stream') { data = xhr.response; } else { - if (!supportsBinary) data = xhr.responseText; - else data = 'ok'; + if (!supportsBinary) { data = xhr.responseText; } + else { data = 'ok'; } } } else { // make sure the `error` event handler that's user-set @@ -1677,6 +1677,13 @@ module.exports = Polling; var global = require('global'); +/** + * Is XHR2 supported? + */ + +var xhr2 = global.XMLHttpRequest !== undefined + && (new XMLHttpRequest()).responseType !== undefined; + /** * Polling interface. * @@ -1686,7 +1693,7 @@ var global = require('global'); function Polling(opts){ var forceBase64 = (opts && opts.forceBase64); - if (!util.bs.xhr2 || forceBase64) this.supportsBinary = false; + if (!xhr2 || forceBase64) this.supportsBinary = false; Transport.call(this, opts); } @@ -1967,8 +1974,8 @@ WS.prototype.doOpen = function(){ var protocols = void(0); var opts = { agent: this.agent }; - this.webSocket = new WebSocket(uri, protocols, opts); - this.webSocket.binaryType = 'arraybuffer'; + this.ws = new WebSocket(uri, protocols, opts); + this.ws.binaryType = 'arraybuffer'; this.addEventListeners(); }; @@ -1981,16 +1988,16 @@ WS.prototype.doOpen = function(){ WS.prototype.addEventListeners = function(){ var self = this; - this.webSocket.onopen = function(){ + this.ws.onopen = function(){ self.onOpen(); }; - this.webSocket.onclose = function(){ + this.ws.onclose = function(){ self.onClose(); }; - this.webSocket.onmessage = function(ev){ + this.ws.onmessage = function(ev){ self.onData(ev.data); }; - this.webSocket.onerror = function(e){ + this.ws.onerror = function(e){ self.onError('websocket error', e); }; }; @@ -2025,7 +2032,7 @@ 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.webSocket.send(parser.encodePacket(packets[i])); + this.ws.send(parser.encodePacket(packets[i])); } function ondrain() { self.writable = true; @@ -2053,8 +2060,8 @@ WS.prototype.onClose = function(){ */ WS.prototype.doClose = function(){ - if (typeof this.webSocket !== 'undefined') { - this.webSocket.close(); + if (typeof this.ws !== 'undefined') { + this.ws.close(); } }; @@ -2260,32 +2267,6 @@ exports.ua.ios6 = exports.ua.ios && /OS 6_/.test(navigator.userAgent); exports.ua.chromeframe = Boolean(global.externalHost); -/** - * Binary support detection namespace. - * - * @namespace - */ - -exports.bs = {}; - -/** - * Detect ArrayBuffer - */ - -exports.bs.arraybuffer = global.ArrayBuffer !== undefined; - -/** - * Detect Blob - */ - -exports.bs.blob = global.Blob !== undefined; - -/** - * Detect XHR 2 - */ - -exports.bs.xhr2 = global.XMLHttpRequest !== undefined && (new XMLHttpRequest()).responseType !== undefined; - /** * Parses an URI * diff --git a/lib/transports/polling-xhr.js b/lib/transports/polling-xhr.js index c1f9b892..85ec6692 100755 --- a/lib/transports/polling-xhr.js +++ b/lib/transports/polling-xhr.js @@ -154,7 +154,7 @@ Emitter(Request.prototype); Request.prototype.create = function(isBinary, supportsBinary){ var xhr = this.xhr = new XMLHttpRequest({ agent: this.agent, xdomain: this.xd }); - if (supportsBinary) xhr.responseType = 'arraybuffer'; + if (supportsBinary) { xhr.responseType = 'arraybuffer'; } var self = this; try { @@ -183,8 +183,8 @@ Request.prototype.create = function(isBinary, supportsBinary){ if (contentType === 'application/octet-stream') { data = xhr.response; } else { - if (!supportsBinary) data = xhr.responseText; - else data = 'ok'; + if (!supportsBinary) { data = xhr.responseText; } + else { data = 'ok'; } } } else { // make sure the `error` event handler that's user-set diff --git a/lib/transports/polling.js b/lib/transports/polling.js index 386c761e..51c96b89 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -19,6 +19,13 @@ module.exports = Polling; var global = require('global'); +/** + * Is XHR2 supported? + */ + +var xhr2 = global.XMLHttpRequest !== undefined + && (new XMLHttpRequest()).responseType !== undefined; + /** * Polling interface. * @@ -28,7 +35,7 @@ var global = require('global'); function Polling(opts){ var forceBase64 = (opts && opts.forceBase64); - if (!util.bs.xhr2 || forceBase64) this.supportsBinary = false; + if (!xhr2 || forceBase64) this.supportsBinary = false; Transport.call(this, opts); } diff --git a/lib/transports/websocket.js b/lib/transports/websocket.js index 483cccb3..21375fa3 100644 --- a/lib/transports/websocket.js +++ b/lib/transports/websocket.js @@ -68,8 +68,8 @@ WS.prototype.doOpen = function(){ var protocols = void(0); var opts = { agent: this.agent }; - this.webSocket = new WebSocket(uri, protocols, opts); - this.webSocket.binaryType = 'arraybuffer'; + this.ws = new WebSocket(uri, protocols, opts); + this.ws.binaryType = 'arraybuffer'; this.addEventListeners(); }; @@ -82,16 +82,16 @@ WS.prototype.doOpen = function(){ WS.prototype.addEventListeners = function(){ var self = this; - this.webSocket.onopen = function(){ + this.ws.onopen = function(){ self.onOpen(); }; - this.webSocket.onclose = function(){ + this.ws.onclose = function(){ self.onClose(); }; - this.webSocket.onmessage = function(ev){ + this.ws.onmessage = function(ev){ self.onData(ev.data); }; - this.webSocket.onerror = function(e){ + this.ws.onerror = function(e){ self.onError('websocket error', e); }; }; @@ -126,7 +126,7 @@ 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.webSocket.send(parser.encodePacket(packets[i])); + this.ws.send(parser.encodePacket(packets[i])); } function ondrain() { self.writable = true; @@ -154,8 +154,8 @@ WS.prototype.onClose = function(){ */ WS.prototype.doClose = function(){ - if (typeof this.webSocket !== 'undefined') { - this.webSocket.close(); + if (typeof this.ws !== 'undefined') { + this.ws.close(); } }; diff --git a/lib/util.js b/lib/util.js index d6da5d01..d1011946 100644 --- a/lib/util.js +++ b/lib/util.js @@ -156,32 +156,6 @@ exports.ua.ios6 = exports.ua.ios && /OS 6_/.test(navigator.userAgent); exports.ua.chromeframe = Boolean(global.externalHost); -/** - * Binary support detection namespace. - * - * @namespace - */ - -exports.bs = {}; - -/** - * Detect ArrayBuffer - */ - -exports.bs.arraybuffer = global.ArrayBuffer !== undefined; - -/** - * Detect Blob - */ - -exports.bs.blob = global.Blob !== undefined; - -/** - * Detect XHR 2 - */ - -exports.bs.xhr2 = global.XMLHttpRequest !== undefined && (new XMLHttpRequest()).responseType !== undefined; - /** * Parses an URI * diff --git a/test/browser-only-parser.js b/test/browser-only-parser.js index ec850c7b..18fa80a7 100644 --- a/test/browser-only-parser.js +++ b/test/browser-only-parser.js @@ -12,12 +12,12 @@ var parser = eio.parser * Shortcuts */ -var encode = parser.encodePacket - , decode = parser.decodePacket - , encPayload = parser.encodePayload - , decPayload = parser.decodePayload - , encPayloadB = parser.encodePayloadAsBinary - , decPayloadB = parser.decodePayloadAsBinary +var encode = parser.encodePacket; +var decode = parser.decodePacket; +var encPayload = parser.encodePayload; +var decPayload = parser.decodePayload; +var encPayloadB = parser.encodePayloadAsBinary; +var decPayloadB = parser.decodePayloadAsBinary; /** * Tests. From e88937036cfe68b35592fe37cdad59c14e762f72 Mon Sep 17 00:00:00 2001 From: Tony Kovanen Date: Sat, 1 Feb 2014 23:59:24 +0200 Subject: [PATCH 04/39] Some README updates --- README.md | 6 +- engine.io.js | 204 ++++++++++++++++++++++++++++++--------------------- 2 files changed, 125 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index 0f7dfa73..6a349c77 100644 --- a/README.md +++ b/README.md @@ -96,8 +96,11 @@ socket.onopen = function(){ - Easy to unit test - Runs inside HTML5 WebWorker - Can send and receive binary data - - Receives in ArrayBuffer or Blob when in browser, and Buffer or ArrayBuffer + - 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 @@ -153,6 +156,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. - `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`) diff --git a/engine.io.js b/engine.io.js index f21b6049..b7163095 100644 --- a/engine.io.js +++ b/engine.io.js @@ -652,7 +652,7 @@ Socket.prototype.filterUpgrades = function (upgrades) { return filteredUpgrades; }; -},{"./emitter":2,"./transport":5,"./transports":7,"./util":12,"debug":14,"engine.io-parser":16,"global":18,"indexof":20}],5:[function(require,module,exports){ +},{"./emitter":2,"./transport":5,"./transports":7,"./util":12,"debug":14,"engine.io-parser":16,"global":20,"indexof":22}],5:[function(require,module,exports){ /** * Module dependencies. */ @@ -1066,7 +1066,7 @@ function load(arr, fn){ process(0); } -},{"../util":12,"./websocket":11,"debug":14,"global":18}],7:[function(require,module,exports){ +},{"../util":12,"./websocket":11,"debug":14,"global":20}],7:[function(require,module,exports){ /** * Module dependencies @@ -1125,7 +1125,7 @@ function polling (opts) { } }; -},{"./flashsocket":6,"./polling-jsonp":8,"./polling-xhr":9,"./websocket":11,"global":18,"xmlhttprequest":13}],8:[function(require,module,exports){ +},{"./flashsocket":6,"./polling-jsonp":8,"./polling-xhr":9,"./websocket":11,"global":20,"xmlhttprequest":13}],8:[function(require,module,exports){ /** * Module requirements. @@ -1352,7 +1352,7 @@ JSONPPolling.prototype.doWrite = function (data, fn) { } }; -},{"../util":12,"./polling":10,"global":18}],9:[function(require,module,exports){ +},{"../util":12,"./polling":10,"global":20}],9:[function(require,module,exports){ /** * Module requirements. */ @@ -1655,7 +1655,7 @@ if (xobject) { }); } -},{"../emitter":2,"../util":12,"./polling":10,"debug":14,"global":18,"xmlhttprequest":13}],10:[function(require,module,exports){ +},{"../emitter":2,"../util":12,"./polling":10,"debug":14,"global":20,"xmlhttprequest":13}],10:[function(require,module,exports){ /** * Module dependencies. */ @@ -1903,7 +1903,7 @@ Polling.prototype.uri = function(){ return schema + '://' + this.hostname + port + this.path + query; }; -},{"../transport":5,"../util":12,"debug":14,"engine.io-parser":16,"global":18}],11:[function(require,module,exports){ +},{"../transport":5,"../util":12,"debug":14,"engine.io-parser":16,"global":20}],11:[function(require,module,exports){ /** * Module dependencies. */ @@ -2108,7 +2108,7 @@ WS.prototype.check = function(){ return !!WebSocket && !('__initialize' in WebSocket && this.name === WS.prototype.name); }; -},{"../transport":5,"../util":12,"debug":14,"engine.io-parser":16,"global":18,"ws":21}],12:[function(require,module,exports){ +},{"../transport":5,"../util":12,"debug":14,"engine.io-parser":16,"global":20,"ws":23}],12:[function(require,module,exports){ var global = require('global'); @@ -2330,7 +2330,7 @@ exports.qsParse = function(qs){ return qry; }; -},{"global":18}],13:[function(require,module,exports){ +},{"global":20}],13:[function(require,module,exports){ // browser shim for xmlhttprequest module var hasCORS = require('has-cors'); @@ -2351,7 +2351,7 @@ module.exports = function(opts) { } } -},{"has-cors":19}],14:[function(require,module,exports){ +},{"has-cors":21}],14:[function(require,module,exports){ /** * Expose `debug()` as the module. @@ -2654,12 +2654,14 @@ Emitter.prototype.hasListeners = function(event){ return !! this.listeners(event).length; }; -},{"indexof":20}],16:[function(require,module,exports){ +},{"indexof":22}],16:[function(require,module,exports){ var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {};/** * Module dependencies. */ var keys = require('./keys'); +var base64encoder = require('base64-arraybuffer'); +require('arraybuffer-slice'); /** * A utility for doing slicing, even when ArrayBuffer.prototype.slice doesn't @@ -2668,16 +2670,16 @@ var keys = require('./keys'); * @api private */ -function sliceBuffer(arraybuffer, start, end) { - if (arraybuffer.slice !== undefined) return arraybuffer.slice(start, end); - if (end === undefined) end = this.arraybuffer.byteLength; - if (start === undefined) start = 0; - - var abView = new Uin8Array(ab); - var newView = new Uint8Array(end - start); - for (var i = start, ii = 0; i < end; i++, ii++) newView[ii] = abView[i]; - return newView.buffer; -}; +//function sliceBuffer(arraybuffer, start, end) { +// if (arraybuffer.slice !== undefined) return arraybuffer.slice(start, end); +// if (end === undefined) end = this.arraybuffer.byteLength; +// if (start === undefined) start = 0; +// +// var abView = new Uin8Array(ab); +// var newView = new Uint8Array(end - start); +// for (var i = start, ii = 0; i < end; i++, ii++) newView[ii] = abView[i]; +// return newView.buffer; +//}; /** * Current protocol version. @@ -2706,61 +2708,6 @@ var packetslist = keys(packets); var err = { type: 'error', data: 'parser error' }; -/** - * Converting base64 to arraybuffer helper - * - * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding - * - * @api private - */ - -function b64ToUint6 (nChr) { - - return nChr > 64 && nChr < 91 ? - nChr - 65 - : nChr > 96 && nChr < 123 ? - nChr - 71 - : nChr > 47 && nChr < 58 ? - nChr + 4 - : nChr === 43 ? - 62 - : nChr === 47 ? - 63 - : - 0; - -} - -/** - * Converting base64 to arraybuffer - * - * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding - * - * @api private - */ -function base64DecToArr (sBase64, nBlocksSize) { - - var - sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), nInLen = sB64Enc.length, - nOutLen = nBlocksSize - ? Math.ceil((nInLen * 3 + 1 >> 2) / nBlocksSize) * nBlocksSize - : nInLen * 3 + 1 >> 2, taBytes = new Uint8Array(nOutLen); - - for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) { - nMod4 = nInIdx & 3; - nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4; - if (nMod4 === 3 || nInLen - nInIdx === 1) { - for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) { - taBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255; - } - nUint24 = 0; - - } - } - - return taBytes; -} - /** * Encodes a packet. * @@ -2842,7 +2789,7 @@ exports.decodePacket = function (data, binaryType) { var asArray = new Uint8Array(data); var type = asArray[0]; - var rest = sliceBuffer(data, 1); + var rest = data.slice(1); if (global.Blob && Blob.prototype.slice && binaryType === 'blob') { rest = new Blob([rest]); } @@ -2860,7 +2807,7 @@ exports.decodeBase64Packet = function(msg, binaryType) { var type = packetslist[msg.charAt(0)]; var data = (!global.ArrayBuffer) ? { base64: true, data: msg.substr(1) } - : base64DecToArr(msg.substr(1)).buffer; + : base64encoder.decode(msg.substr(1)); if (binaryType === 'blob') data = new Blob([data]); return { type: type, data: data }; @@ -3061,13 +3008,13 @@ exports.decodePayloadAsBinary = function (data, binaryType, callback) { if (tailArray[i] == 255) break; msgLength += tailArray[i]; } - bufferTail = sliceBuffer(bufferTail, 2 + msgLength.length); + bufferTail = bufferTail.slice(2 + msgLength.length); msgLength = parseInt(msgLength); - var msg = sliceBuffer(bufferTail, 0, msgLength); + var msg = bufferTail.slice(0, msgLength); if (isString) msg = String.fromCharCode.apply(null, new Uint8Array(msg)); buffers.push(msg); - bufferTail = sliceBuffer(bufferTail, msgLength); + bufferTail = bufferTail.slice(msgLength); } var total = buffers.length; @@ -3076,7 +3023,7 @@ exports.decodePayloadAsBinary = function (data, binaryType, callback) { }); }; -},{"./keys":17}],17:[function(require,module,exports){ +},{"./keys":17,"arraybuffer-slice":18,"base64-arraybuffer":19}],17:[function(require,module,exports){ /** * Gets the keys for an object. @@ -3098,6 +3045,95 @@ module.exports = Object.keys || function keys (obj){ }; },{}],18:[function(require,module,exports){ +// https://github.com/ttaubert/node-arraybuffer-slice +// (c) 2013 Tim Taubert +// arraybuffer-slice may be freely distributed under the MIT license. + +"use strict"; + +if (!ArrayBuffer.prototype.slice) { + ArrayBuffer.prototype.slice = function (begin, end) { + begin = (begin|0) || 0; + var num = this.byteLength; + end = end === (void 0) ? num : (end|0); + + // Handle negative values. + if (begin < 0) begin += num; + if (end < 0) end += num; + + if (num === 0 || begin >= num || begin >= end) { + return new ArrayBuffer(0); + } + + var length = Math.min(num - begin, end - begin); + var target = new ArrayBuffer(length); + var targetArray = new Uint8Array(target); + targetArray.set(new Uint8Array(this, begin, length)); + return target; + }; +} + +},{}],19:[function(require,module,exports){ +/* + * base64-arraybuffer + * https://github.com/niklasvh/base64-arraybuffer + * + * Copyright (c) 2012 Niklas von Hertzen + * Licensed under the MIT license. + */ +(function(chars){ + "use strict"; + + exports.encode = function(arraybuffer) { + var bytes = new Uint8Array(arraybuffer), + i, len = bytes.buffer.byteLength, base64 = ""; + + for (i = 0; i < len; i+=3) { + base64 += chars[bytes.buffer[i] >> 2]; + base64 += chars[((bytes.buffer[i] & 3) << 4) | (bytes.buffer[i + 1] >> 4)]; + base64 += chars[((bytes.buffer[i + 1] & 15) << 2) | (bytes.buffer[i + 2] >> 6)]; + base64 += chars[bytes.buffer[i + 2] & 63]; + } + + if ((len % 3) === 2) { + base64 = base64.substring(0, base64.length - 1) + "="; + } else if (len % 3 === 1) { + base64 = base64.substring(0, base64.length - 2) + "=="; + } + + return base64; + }; + + exports.decode = function(base64) { + var bufferLength = base64.length * 0.75, + len = base64.length, i, p = 0, + encoded1, encoded2, encoded3, encoded4; + + if (base64[base64.length - 1] === "=") { + bufferLength--; + if (base64[base64.length - 2] === "=") { + bufferLength--; + } + } + + var arraybuffer = new ArrayBuffer(bufferLength), + bytes = new Uint8Array(arraybuffer); + + for (i = 0; i < len; i+=4) { + encoded1 = chars.indexOf(base64[i]); + encoded2 = chars.indexOf(base64[i+1]); + encoded3 = chars.indexOf(base64[i+2]); + encoded4 = chars.indexOf(base64[i+3]); + + bytes[p++] = (encoded1 << 2) | (encoded2 >> 4); + bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2); + bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63); + } + + return arraybuffer; + }; +})("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"); +},{}],20:[function(require,module,exports){ /** * Returns `this`. Execute this without a "context" (i.e. without it being @@ -3107,7 +3143,7 @@ module.exports = Object.keys || function keys (obj){ module.exports = (function () { return this; })(); -},{}],19:[function(require,module,exports){ +},{}],21:[function(require,module,exports){ /** * Module dependencies. @@ -3126,7 +3162,7 @@ var global = require('global'); module.exports = 'XMLHttpRequest' in global && 'withCredentials' in new global.XMLHttpRequest(); -},{"global":18}],20:[function(require,module,exports){ +},{"global":20}],22:[function(require,module,exports){ var indexOf = [].indexOf; @@ -3137,7 +3173,7 @@ module.exports = function(arr, obj){ } return -1; }; -},{}],21:[function(require,module,exports){ +},{}],23:[function(require,module,exports){ /** * Module dependencies. From 1067ae80d346052676ddaa54c7a9f4850882c6e9 Mon Sep 17 00:00:00 2001 From: Tony Kovanen Date: Sun, 2 Feb 2014 03:01:07 +0200 Subject: [PATCH 05/39] WebSocket's support for binary is now checked, and base64 is used when there is no binaryType. If WebSocket doesn't support binary, this is communicated to the server through the upgrade packet. --- README.md | 2 +- engine.io.js | 38 ++++++++++++++++++++++--------------- lib/socket.js | 4 +++- lib/transports/polling.js | 2 +- lib/transports/websocket.js | 10 +++++++++- test/connection.js | 34 ++++++++++++++++++++++++++++----- 6 files changed, 66 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 6a349c77..74243263 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,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. + - `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`) diff --git a/engine.io.js b/engine.io.js index b7163095..7283cf89 100644 --- a/engine.io.js +++ b/engine.io.js @@ -303,7 +303,9 @@ Socket.prototype.probe = function (name) { transport.removeListener('error', onerror); self.emit('upgrade', transport); self.setTransport(transport); - transport.send([{ type: 'upgrade' }]); + var upgradePacket = { type: 'upgrade' }; + if (!transport.supportsBinary) { upgradePacket.data = 'b64' }; + transport.send([upgradePacket]); transport = null; self.upgrading = false; self.flush(); @@ -1693,7 +1695,7 @@ var xhr2 = global.XMLHttpRequest !== undefined function Polling(opts){ var forceBase64 = (opts && opts.forceBase64); - if (!xhr2 || forceBase64) this.supportsBinary = false; + if (!xhr2 || forceBase64) { this.supportsBinary = false; } Transport.call(this, opts); } @@ -1926,6 +1928,12 @@ module.exports = WS; var global = require('global'); +/** + * Does the supported WebSocket standard support binary data if on browser? + */ + +var standardBinarySupport = (!global.document || 'binaryType' in (new WebSocket('ws://0.0.0.0'))); + /** * WebSocket transport constructor. * @@ -1934,6 +1942,8 @@ var global = require('global'); */ function WS(opts){ + var forceBase64 = (opts && opts.forceBase64); + if (forceBase64 || !standardBinarySupport) { this.supportsBinary = false; } Transport.call(this, opts); } @@ -2032,7 +2042,7 @@ 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.ws.send(parser.encodePacket(packets[i])); + this.ws.send(parser.encodePacket(packets[i], this.supportsBinary)); } function ondrain() { self.writable = true; @@ -2670,20 +2680,10 @@ require('arraybuffer-slice'); * @api private */ -//function sliceBuffer(arraybuffer, start, end) { -// if (arraybuffer.slice !== undefined) return arraybuffer.slice(start, end); -// if (end === undefined) end = this.arraybuffer.byteLength; -// if (start === undefined) start = 0; -// -// var abView = new Uin8Array(ab); -// var newView = new Uint8Array(end - start); -// for (var i = start, ii = 0; i < end; i++, ii++) newView[ii] = abView[i]; -// return newView.buffer; -//}; - /** * Current protocol version. */ + exports.protocol = 2; /** @@ -2724,12 +2724,16 @@ var err = { type: 'error', data: 'parser error' }; * @api private */ -exports.encodePacket = function (packet) { +exports.encodePacket = function (packet, supportsBinary) { + if (supportsBinary === undefined) { supportsBinary = true; } + var data = (packet.data === undefined) ? undefined : packet.data.buffer || packet.data; if (global.ArrayBuffer && data instanceof ArrayBuffer) { + if (!supportsBinary) { return 'b' + exports.encodeBase64Packet(packet); } + var contentArray = new Uint8Array(data); var resultBuffer = new Uint8Array(1 + data.byteLength); @@ -2774,6 +2778,10 @@ exports.encodeBase64Packet = function(packet) { exports.decodePacket = function (data, binaryType) { // String data if (typeof data == 'string' || data === undefined) { + if (data.charAt(0) == 'b') { + return exports.decodeBase64Packet(data.substr(1)); + } + var type = data.charAt(0); if (Number(type) != type || !packetslist[type]) { diff --git a/lib/socket.js b/lib/socket.js index eb29d274..31fe40de 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -247,7 +247,9 @@ Socket.prototype.probe = function (name) { transport.removeListener('error', onerror); self.emit('upgrade', transport); self.setTransport(transport); - transport.send([{ type: 'upgrade' }]); + var upgradePacket = { type: 'upgrade' }; + if (!transport.supportsBinary) { upgradePacket.data = 'b64' }; + transport.send([upgradePacket]); transport = null; self.upgrading = false; self.flush(); diff --git a/lib/transports/polling.js b/lib/transports/polling.js index 51c96b89..9c6f0c67 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -35,7 +35,7 @@ var xhr2 = global.XMLHttpRequest !== undefined function Polling(opts){ var forceBase64 = (opts && opts.forceBase64); - if (!xhr2 || forceBase64) this.supportsBinary = false; + if (!xhr2 || forceBase64) { this.supportsBinary = false; } Transport.call(this, opts); } diff --git a/lib/transports/websocket.js b/lib/transports/websocket.js index 21375fa3..57ef84c4 100644 --- a/lib/transports/websocket.js +++ b/lib/transports/websocket.js @@ -20,6 +20,12 @@ module.exports = WS; var global = require('global'); +/** + * Does the supported WebSocket standard support binary data if on browser? + */ + +var standardBinarySupport = (!global.document || 'binaryType' in (new WebSocket('ws://0.0.0.0'))); + /** * WebSocket transport constructor. * @@ -28,6 +34,8 @@ var global = require('global'); */ function WS(opts){ + var forceBase64 = (opts && opts.forceBase64); + if (forceBase64 || !standardBinarySupport) { this.supportsBinary = false; } Transport.call(this, opts); } @@ -126,7 +134,7 @@ 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.ws.send(parser.encodePacket(packets[i])); + this.ws.send(parser.encodePacket(packets[i], this.supportsBinary)); } function ondrain() { self.writable = true; diff --git a/test/connection.js b/test/connection.js index be5eef8c..f1e812c7 100644 --- a/test/connection.js +++ b/test/connection.js @@ -44,11 +44,14 @@ describe('connection', function() { expect(data instanceof Blob).to.be(true); var fr = new FileReader(); - var ab = fr.readAsArrayBuffer(data); - var ia = new Int8Array(ab); - for (var i = 0; i < ia.length; i++) expect(ia[i]).to.equal(i); - socket.close(); - done(); + fr.onload = function() { + var ab = this.result; + var ia = new Int8Array(ab); + expect(ia).to.eql(binaryData); + socket.close(); + done(); + }; + fr.readAsArrayBuffer(data); }); }); }); @@ -138,6 +141,27 @@ describe('connection', function() { }); }); + 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 instanceof ArrayBuffer).to.be(true); + expect(new Int8Array(data)).to.eql(binaryData); + + socket.close(); + done(); + }); + }); + }); + }); + + // no `Worker` on old IE if (global.Worker) { it('should work in a worker', function(done){ From 948c811d9235989fab696c191d083d1450d735c8 Mon Sep 17 00:00:00 2001 From: Tony Kovanen Date: Sun, 2 Feb 2014 17:10:23 +0200 Subject: [PATCH 06/39] Cleaning up connection.js tests --- engine.io.js | 60 ++++++++++++++++++++++++---------------------- test/connection.js | 14 +++++------ 2 files changed, 38 insertions(+), 36 deletions(-) diff --git a/engine.io.js b/engine.io.js index 7283cf89..e4e0d83a 100644 --- a/engine.io.js +++ b/engine.io.js @@ -2671,7 +2671,7 @@ var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ? var keys = require('./keys'); var base64encoder = require('base64-arraybuffer'); -require('arraybuffer-slice'); +var sliceBuffer = require('./slice-buffer'); /** * A utility for doing slicing, even when ArrayBuffer.prototype.slice doesn't @@ -2797,7 +2797,7 @@ exports.decodePacket = function (data, binaryType) { var asArray = new Uint8Array(data); var type = asArray[0]; - var rest = data.slice(1); + var rest = sliceBuffer(data, 1); if (global.Blob && Blob.prototype.slice && binaryType === 'blob') { rest = new Blob([rest]); } @@ -3016,13 +3016,13 @@ exports.decodePayloadAsBinary = function (data, binaryType, callback) { if (tailArray[i] == 255) break; msgLength += tailArray[i]; } - bufferTail = bufferTail.slice(2 + msgLength.length); + bufferTail = sliceBuffer(bufferTail, 2 + msgLength.length); msgLength = parseInt(msgLength); - var msg = bufferTail.slice(0, msgLength); + var msg = sliceBuffer(bufferTail, 0, msgLength); if (isString) msg = String.fromCharCode.apply(null, new Uint8Array(msg)); buffers.push(msg); - bufferTail = bufferTail.slice(msgLength); + bufferTail = sliceBuffer(bufferTail, msgLength); } var total = buffers.length; @@ -3031,7 +3031,7 @@ exports.decodePayloadAsBinary = function (data, binaryType, callback) { }); }; -},{"./keys":17,"arraybuffer-slice":18,"base64-arraybuffer":19}],17:[function(require,module,exports){ +},{"./keys":17,"./slice-buffer":18,"base64-arraybuffer":19}],17:[function(require,module,exports){ /** * Gets the keys for an object. @@ -3053,33 +3053,35 @@ module.exports = Object.keys || function keys (obj){ }; },{}],18:[function(require,module,exports){ -// https://github.com/ttaubert/node-arraybuffer-slice -// (c) 2013 Tim Taubert -// arraybuffer-slice may be freely distributed under the MIT license. +/** + * An abstraction for slicing an arraybuffer even when + * ArrayBuffer.prototype.slice is not supported + * + * @api private + */ -"use strict"; +module.exports = function(arraybuffer, start, end) { + var bytes = arraybuffer.byteLength; + start = start || 0; + end = end || bytes; -if (!ArrayBuffer.prototype.slice) { - ArrayBuffer.prototype.slice = function (begin, end) { - begin = (begin|0) || 0; - var num = this.byteLength; - end = end === (void 0) ? num : (end|0); + if (arraybuffer.slice) { return arraybuffer.slice(start, end); } + + if (start < 0) { start += bytes; } + if (end < 0) { end += bytes; } + if (end > bytes) { end = bytes; } - // Handle negative values. - if (begin < 0) begin += num; - if (end < 0) end += num; + if (start >= bytes || start >= end || bytes == 0) { + return new ArrayBuffer(0); + } - if (num === 0 || begin >= num || begin >= end) { - return new ArrayBuffer(0); - } - - var length = Math.min(num - begin, end - begin); - var target = new ArrayBuffer(length); - var targetArray = new Uint8Array(target); - targetArray.set(new Uint8Array(this, begin, length)); - return target; - }; -} + var abv = new Uint8Array(arraybuffer); + var result = Uint8Array(end - start); + for (var i = start, ii = 0; i < end; i++, ii++) { + result[ii] = abv[i]; + } + return result.buffer; +}; },{}],19:[function(require,module,exports){ /* diff --git a/test/connection.js b/test/connection.js index f1e812c7..04443cb9 100644 --- a/test/connection.js +++ b/test/connection.js @@ -24,7 +24,7 @@ describe('connection', function() { socket.on('message', function (data) { if (data === 'hi') return; - expect(data instanceof ArrayBuffer).to.be(true); + expect(data).to.be.an(ArrayBuffer); expect(new Int8Array(data)).to.eql(binaryData); socket.close(); done(); @@ -42,7 +42,7 @@ describe('connection', function() { socket.on('message', function (data) { if (typeof data === 'string') return; - expect(data instanceof Blob).to.be(true); + expect(data).to.be.a(Blob); var fr = new FileReader(); fr.onload = function() { var ab = this.result; @@ -65,7 +65,7 @@ describe('connection', function() { socket.on('message', function (data) { if (typeof data === 'string') return; - expect(data instanceof ArrayBuffer).to.be(true); + expect(data).to.be.an(ArrayBuffer); var ia = new Int8Array(data); expect(ia).to.eql(binaryData); socket.close(); @@ -74,7 +74,7 @@ describe('connection', function() { }); }); - it('should be able to receive binary data when forcing base64 and not decode it when overriding ArrayBuffer (polling)', function(done) { + it('should be able to receive binary data when forcing base64 and not decode it when overriding ArrayBuffer (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 }); @@ -108,7 +108,7 @@ describe('connection', function() { socket.on('message', function (data) { if (typeof data === 'string') return; - expect(data instanceof ArrayBuffer).to.be(true); + expect(data).to.be.an(ArrayBuffer); expect(new Int8Array(data)).to.eql(binaryData); socket.close(); @@ -129,7 +129,7 @@ describe('connection', function() { socket.on('message', function (data) { if (typeof data === 'string') return; - expect(data instanceof Blob).to.be(true); + expect(data).to.be.a(Blob); var fr = new FileReader(); var ab = fr.readAsArrayBuffer(data); var ia = new Int8Array(ab); @@ -151,7 +151,7 @@ describe('connection', function() { socket.on('message', function (data) { if (typeof data === 'string') return; - expect(data instanceof ArrayBuffer).to.be(true); + expect(data).to.be.an(ArrayBuffer); expect(new Int8Array(data)).to.eql(binaryData); socket.close(); From 944e3953ab7ca81b92ff4de4689855266a61cf8a Mon Sep 17 00:00:00 2001 From: Tony Kovanen Date: Sun, 2 Feb 2014 19:28:56 +0200 Subject: [PATCH 07/39] Nicer feature detection --- engine.io.js | 15 ++++++++++++--- lib/transports/polling.js | 4 ++-- lib/transports/websocket.js | 11 ++++++++++- test/connection.js | 3 ++- 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/engine.io.js b/engine.io.js index e4e0d83a..4ee0c18e 100644 --- a/engine.io.js +++ b/engine.io.js @@ -1683,8 +1683,8 @@ var global = require('global'); * Is XHR2 supported? */ -var xhr2 = global.XMLHttpRequest !== undefined - && (new XMLHttpRequest()).responseType !== undefined; +var xhr2 = !!(global.XMLHttpRequest + && (new XMLHttpRequest()).responseType !== undefined); /** * Polling interface. @@ -1932,7 +1932,16 @@ var global = require('global'); * Does the supported WebSocket standard support binary data if on browser? */ -var standardBinarySupport = (!global.document || 'binaryType' in (new WebSocket('ws://0.0.0.0'))); +var standardBinarySupport = (function() { + (!global.document || 'binaryType' in (new WebSocket('ws://0.0.0.0'))); + if (global.document) { + try { + return 'binaryType' in (new WebSocket('ws://0.0.0.0')); + } catch(e) { return false; } + } + + return true; +})(); /** * WebSocket transport constructor. diff --git a/lib/transports/polling.js b/lib/transports/polling.js index 9c6f0c67..9abf3790 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -23,8 +23,8 @@ var global = require('global'); * Is XHR2 supported? */ -var xhr2 = global.XMLHttpRequest !== undefined - && (new XMLHttpRequest()).responseType !== undefined; +var xhr2 = !!(global.XMLHttpRequest + && (new XMLHttpRequest()).responseType !== undefined); /** * Polling interface. diff --git a/lib/transports/websocket.js b/lib/transports/websocket.js index 57ef84c4..84d96970 100644 --- a/lib/transports/websocket.js +++ b/lib/transports/websocket.js @@ -24,7 +24,16 @@ var global = require('global'); * Does the supported WebSocket standard support binary data if on browser? */ -var standardBinarySupport = (!global.document || 'binaryType' in (new WebSocket('ws://0.0.0.0'))); +var standardBinarySupport = (function() { + (!global.document || 'binaryType' in (new WebSocket('ws://0.0.0.0'))); + if (global.document) { + try { + return 'binaryType' in (new WebSocket('ws://0.0.0.0')); + } catch(e) { return false; } + } + + return true; +})(); /** * WebSocket transport constructor. diff --git a/test/connection.js b/test/connection.js index 04443cb9..50327c28 100644 --- a/test/connection.js +++ b/test/connection.js @@ -88,8 +88,9 @@ describe('connection', function() { firstPacket = false; return; } + expect(data.base64).to.be(true); - expect(data.data).to.eql('AAECAwQ='); + expect(data.data).to.equal('AAECAwQ='); global.ArrayBuffer = ab; socket.close(); From 8d6e42f5f373d266e60fe4d57d833295e0b01678 Mon Sep 17 00:00:00 2001 From: Tony Kovanen Date: Mon, 3 Feb 2014 01:25:32 +0200 Subject: [PATCH 08/39] Failing test for sending blobs --- test/connection.js | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/test/connection.js b/test/connection.js index 50327c28..de9fb1af 100644 --- a/test/connection.js +++ b/test/connection.js @@ -128,15 +128,40 @@ describe('connection', function() { socket.on('upgrade', function() { socket.send(binaryData); socket.on('message', function (data) { - if (typeof data === 'string') return; - expect(data).to.be.a(Blob); var fr = new FileReader(); - var ab = fr.readAsArrayBuffer(data); - var ia = new Int8Array(ab); - for (var i = 0; i < ia.length; i++) expect(ia[i]).to.equal(i); - socket.close(); - done(); + 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 and receive 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.binaryType = 'blob'; + socket.on('open', function() { + socket.on('upgrade', function() { + socket.send(new Blob([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); }); }); }); From 1c1cfc1f9cd083f736ae4176af9aa47bceee08bc Mon Sep 17 00:00:00 2001 From: Tony Kovanen Date: Mon, 3 Feb 2014 02:33:14 +0200 Subject: [PATCH 09/39] Using url parameter for communicating the need for base64 instead of upgrade packet for WebSocket --- engine.io.js | 7 ++++--- lib/socket.js | 4 +--- lib/transports/websocket.js | 3 +++ 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/engine.io.js b/engine.io.js index 4ee0c18e..ace69e9e 100644 --- a/engine.io.js +++ b/engine.io.js @@ -303,9 +303,7 @@ Socket.prototype.probe = function (name) { transport.removeListener('error', onerror); self.emit('upgrade', transport); self.setTransport(transport); - var upgradePacket = { type: 'upgrade' }; - if (!transport.supportsBinary) { upgradePacket.data = 'b64' }; - transport.send([upgradePacket]); + transport.send([{ type: 'upgrade' }]); transport = null; self.upgrading = false; self.flush(); @@ -2106,6 +2104,9 @@ 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/lib/socket.js b/lib/socket.js index 31fe40de..eb29d274 100644 --- a/lib/socket.js +++ b/lib/socket.js @@ -247,9 +247,7 @@ Socket.prototype.probe = function (name) { transport.removeListener('error', onerror); self.emit('upgrade', transport); self.setTransport(transport); - var upgradePacket = { type: 'upgrade' }; - if (!transport.supportsBinary) { upgradePacket.data = 'b64' }; - transport.send([upgradePacket]); + transport.send([{ type: 'upgrade' }]); transport = null; self.upgrading = false; self.flush(); diff --git a/lib/transports/websocket.js b/lib/transports/websocket.js index 84d96970..bf62af08 100644 --- a/lib/transports/websocket.js +++ b/lib/transports/websocket.js @@ -198,6 +198,9 @@ 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 From 13ff11977294943e70626a34dc640eba442498fc Mon Sep 17 00:00:00 2001 From: Tony Kovanen Date: Tue, 4 Feb 2014 00:04:30 +0200 Subject: [PATCH 10/39] Making encoding work with callbacks. Not completely there yet. --- engine.io.js | 97 +++++++++------ lib/transports/polling.js | 5 +- lib/transports/websocket.js | 5 +- test/browser-only-parser.js | 32 +++-- test/parser.js | 230 +++++++++++++++++++++--------------- 5 files changed, 223 insertions(+), 146 deletions(-) diff --git a/engine.io.js b/engine.io.js index ace69e9e..b3caa0c6 100644 --- a/engine.io.js +++ b/engine.io.js @@ -1860,7 +1860,10 @@ Polling.prototype.write = function(packets){ self.emit('drain'); }; - this.doWrite(parser.encodePayload(packets, this.supportsBinary), callbackfn); + var self = this; + parser.encodePayload(packets, this.supportsBinary, function(data) { + self.doWrite(data, callbackfn); + }); }; /** @@ -2049,8 +2052,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.ws.send(parser.encodePacket(packets[i], this.supportsBinary)); + parser.encodePacket(packets[i], this.supportsBinary, function(data) { + self.ws.send(data); + }); } + function ondrain() { self.writable = true; self.emit('drain'); @@ -2718,6 +2724,18 @@ var packetslist = keys(packets); var err = { type: 'error', data: 'parser error' }; +/** + * Check if Blob constructor is supported + */ + +var blobSupported = (function() { + try { + new Blob([1, 2, 3]); + return true; + } catch(e) {} + return false; +})(); + /** * Encodes a packet. * @@ -2734,22 +2752,27 @@ var err = { type: 'error', data: 'parser error' }; * @api private */ -exports.encodePacket = function (packet, supportsBinary) { - if (supportsBinary === undefined) { supportsBinary = true; } +exports.encodePacket = function (packet, supportsBinary, callback) { + if (typeof supportsBinary == 'function') { + callback = supportsBinary; + supportsBinary = false; + } var data = (packet.data === undefined) ? undefined : packet.data.buffer || packet.data; if (global.ArrayBuffer && data instanceof ArrayBuffer) { - if (!supportsBinary) { return 'b' + exports.encodeBase64Packet(packet); } + if (!supportsBinary) { + return callback('b' + exports.encodeBase64Packet(packet)); + } var contentArray = new Uint8Array(data); var resultBuffer = new Uint8Array(1 + data.byteLength); resultBuffer[0] = packets[packet.type]; for (var i = 0; i < contentArray.length; i++) resultBuffer[i+1] = contentArray[i]; - return resultBuffer.buffer; + return callback(resultBuffer.buffer); } // Sending data as a utf-8 string @@ -2760,7 +2783,7 @@ exports.encodePacket = function (packet, supportsBinary) { encoded += String(packet.data); } - return '' + encoded; + return callback('' + encoded); }; @@ -2775,7 +2798,7 @@ exports.encodeBase64Packet = function(packet) { var message = '' + exports.packets[packet.type]; var b64data = String.fromCharCode.apply(null, new Uint8Array(packet.data)); message += global.btoa(b64data); - return message; + return callback(message); }; /** @@ -2789,7 +2812,7 @@ exports.decodePacket = function (data, binaryType) { // String data if (typeof data == 'string' || data === undefined) { if (data.charAt(0) == 'b') { - return exports.decodeBase64Packet(data.substr(1)); + return exports.decodeBase64Packet(data.substr(1), binaryType); } var type = data.charAt(0); @@ -2808,7 +2831,7 @@ exports.decodePacket = function (data, binaryType) { var asArray = new Uint8Array(data); var type = asArray[0]; var rest = sliceBuffer(data, 1); - if (global.Blob && Blob.prototype.slice && binaryType === 'blob') { + if (blobSupported && binaryType === 'blob') { rest = new Blob([rest]); } return { type: packetslist[type], data: rest }; @@ -2847,27 +2870,30 @@ exports.decodeBase64Packet = function(msg, binaryType) { * @api private */ -exports.encodePayload = function (packets, supportsBinary) { - if (supportsBinary) { return exports.encodePayloadAsBinary(packets); } +exports.encodePayload = function (packets, supportsBinary, callback) { + if (typeof supportsBinary == 'function') { + callback = supportsBinary; + supportsBinary = null; + } + + if (supportsBinary) { + return exports.encodePayloadAsBinary(packets, callback); + } if (!packets.length) { - return '0:'; + return callback('0:'); } var encoded = ''; var message; for (var i = 0, l = packets.length; i < l; i++) { - if (!packets[i].data || typeof packets[i].data === 'string') { - var message = exports.encodePacket(packets[i]); + exports.encodePacket(packets[i], supportsBinary, function(message) { encoded += message.length + ':' + message; - } else { - var message = exports.encodeBase64Packet(packets[i]); - encoded += 'b' + message.length + ':' + message; - } + }); } - return encoded; + return callback(encoded); }; /* @@ -2889,7 +2915,6 @@ exports.decodePayload = function (data, binaryType, callback) { } var packet; - var base64 = false; if (data == '') { // parser error - ignoring payload return callback(err, 0, 1); @@ -2901,8 +2926,7 @@ exports.decodePayload = function (data, binaryType, callback) { for (var i = 0, l = data.length; i < l; i++) { var chr = data.charAt(i); - if (chr == 'b') base64 = true; - else if (':' != chr) { + if (':' != chr) { length += chr; } else { if ('' == length || (length != (n = Number(length)))) { @@ -2918,11 +2942,7 @@ exports.decodePayload = function (data, binaryType, callback) { } if (msg.length) { - if (base64) { - packet = exports.decodeBase64Packet(msg, binaryType); - } else { - packet = exports.decodePacket(msg, binaryType); - } + packet = exports.decodePacket(msg, binaryType); if (err.type == packet.type && err.data == packet.data) { // parser error in individual packet - ignoring payload @@ -2936,7 +2956,6 @@ exports.decodePayload = function (data, binaryType, callback) { // advance cursor i += n; length = ''; - base64 = false; } } @@ -2961,19 +2980,25 @@ exports.decodePayload = function (data, binaryType, callback) { * @api private */ -exports.encodePayloadAsBinary = function (packets) { +exports.encodePayloadAsBinary = function (packets, callback) { if (!packets.length) { - return new ArrayBuffer(0); + return callback(new ArrayBuffer(0)); } - var encodedPackets = packets.map(exports.encodePacket); + var encodedPackets = []; + packets.forEach(function(p) { + exports.encodePacket(p, function(data) { + encodedPackets.push(data); + }); + }); + var totalLength = encodedPackets.reduce(function(acc, p) { var len; if (typeof p === 'string') len = p.length; else len = p.byteLength; return acc + (new String(len)).length + len + 2; // string/binary identifier + separator = 2 }, 0); - + var resultArray = new Uint8Array(totalLength); var bufferIndex = 0; @@ -2986,8 +3011,8 @@ exports.encodePayloadAsBinary = function (packets) { ab = view.buffer; } - if (isString) resultArray[bufferIndex++] = 0; // not true binary - else resultArray[bufferIndex++] = 1; // true binary + if (isString) { resultArray[bufferIndex++] = 0; } // not true binary + else { resultArray[bufferIndex++] = 1; } // true binary var lenStr = new String(ab.byteLength); for (var i = 0; i < lenStr.length; i++) resultArray[bufferIndex++] = parseInt(lenStr[i]); @@ -2997,7 +3022,7 @@ exports.encodePayloadAsBinary = function (packets) { for (var i = 0; i < view.length; i++) resultArray[bufferIndex++] = view[i]; }); - return resultArray.buffer; + return callback(resultArray.buffer); }; /* diff --git a/lib/transports/polling.js b/lib/transports/polling.js index 9abf3790..a538bd74 100644 --- a/lib/transports/polling.js +++ b/lib/transports/polling.js @@ -202,7 +202,10 @@ Polling.prototype.write = function(packets){ self.emit('drain'); }; - this.doWrite(parser.encodePayload(packets, this.supportsBinary), callbackfn); + var self = this; + parser.encodePayload(packets, this.supportsBinary, function(data) { + self.doWrite(data, callbackfn); + }); }; /** diff --git a/lib/transports/websocket.js b/lib/transports/websocket.js index bf62af08..c3882ccf 100644 --- a/lib/transports/websocket.js +++ b/lib/transports/websocket.js @@ -143,8 +143,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.ws.send(parser.encodePacket(packets[i], this.supportsBinary)); + parser.encodePacket(packets[i], this.supportsBinary, function(data) { + self.ws.send(data); + }); } + function ondrain() { self.writable = true; self.emit('drain'); diff --git a/test/browser-only-parser.js b/test/browser-only-parser.js index 18fa80a7..d21cd43a 100644 --- a/test/browser-only-parser.js +++ b/test/browser-only-parser.js @@ -24,52 +24,58 @@ var decPayloadB = parser.decodePayloadAsBinary; */ describe('browser-only-parser', function () { - it('should encode a binary message', 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; - expect(decode(encode({ type: 'message', data: data }))) - .to.eql({ type: 'message', data: data.buffer }); + 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() { + 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; - decPayload(encPayload([{ type: 'message', data: data }, { type: 'message', data: 'hello' }]), - function(packet, index, total) { + 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', function() { + it('should encode binary contents as binary', 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; - decPayloadB(encPayloadB([{ type: 'message', data: first }, { type: 'message', data: second }]), - function(packet, index, total) { + 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', function() { + it('should encode mixed binary and string contents as binary', function(done) { var first = new Int8Array(15); for (var i = 0; i < first.length; i++) first[i] = i; - decPayloadB(encPayloadB([ { type: 'message', data: first }, { type: 'message', data: 'hello' }, { type: 'close' } ]), - function(packet, index, total) { + 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); @@ -78,7 +84,9 @@ describe('browser-only-parser', function () { expect(packet.data).to.eql('hello'); } else { expect(packet.type).to.eql('close'); + done(); } + }); }); }); }); diff --git a/test/parser.js b/test/parser.js index ea544f41..d279d614 100644 --- a/test/parser.js +++ b/test/parser.js @@ -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,91 +120,103 @@ 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); + }); }); }); }); From f59451ee4c3a77646eee40474863a0f0241c5447 Mon Sep 17 00:00:00 2001 From: Tony Kovanen Date: Tue, 4 Feb 2014 09:46:36 +0200 Subject: [PATCH 11/39] Dummy support for sending blobs --- engine.io.js | 1179 +++++++++++++++++++++++++++++++++++++++++--- test/connection.js | 34 +- 2 files changed, 1132 insertions(+), 81 deletions(-) diff --git a/engine.io.js b/engine.io.js index b3caa0c6..0140e0a0 100644 --- a/engine.io.js +++ b/engine.io.js @@ -41,7 +41,7 @@ Emitter.prototype.removeEventListener = Emitter.prototype.off; Emitter.prototype.removeListener = Emitter.prototype.off; -},{"emitter":15}],3:[function(require,module,exports){ +},{"emitter":16}],3:[function(require,module,exports){ module.exports = require('./socket'); @@ -53,7 +53,7 @@ module.exports = require('./socket'); */ module.exports.parser = require('engine.io-parser'); -},{"./socket":4,"engine.io-parser":16}],4:[function(require,module,exports){ +},{"./socket":4,"engine.io-parser":17}],4:[function(require,module,exports){ /** * Module dependencies. */ @@ -652,7 +652,7 @@ Socket.prototype.filterUpgrades = function (upgrades) { return filteredUpgrades; }; -},{"./emitter":2,"./transport":5,"./transports":7,"./util":12,"debug":14,"engine.io-parser":16,"global":20,"indexof":22}],5:[function(require,module,exports){ +},{"./emitter":2,"./transport":5,"./transports":7,"./util":12,"debug":15,"engine.io-parser":17,"global":22,"indexof":24}],5:[function(require,module,exports){ /** * Module dependencies. */ @@ -796,7 +796,7 @@ Transport.prototype.onClose = function () { this.emit('close'); }; -},{"./emitter":2,"./util":12,"engine.io-parser":16}],6:[function(require,module,exports){ +},{"./emitter":2,"./util":12,"engine.io-parser":17}],6:[function(require,module,exports){ /** * Module dependencies. @@ -1066,7 +1066,7 @@ function load(arr, fn){ process(0); } -},{"../util":12,"./websocket":11,"debug":14,"global":20}],7:[function(require,module,exports){ +},{"../util":12,"./websocket":11,"debug":15,"global":22}],7:[function(require,module,exports){ /** * Module dependencies @@ -1125,7 +1125,7 @@ function polling (opts) { } }; -},{"./flashsocket":6,"./polling-jsonp":8,"./polling-xhr":9,"./websocket":11,"global":20,"xmlhttprequest":13}],8:[function(require,module,exports){ +},{"./flashsocket":6,"./polling-jsonp":8,"./polling-xhr":9,"./websocket":11,"global":22,"xmlhttprequest":13}],8:[function(require,module,exports){ /** * Module requirements. @@ -1352,7 +1352,7 @@ JSONPPolling.prototype.doWrite = function (data, fn) { } }; -},{"../util":12,"./polling":10,"global":20}],9:[function(require,module,exports){ +},{"../util":12,"./polling":10,"global":22}],9:[function(require,module,exports){ /** * Module requirements. */ @@ -1655,7 +1655,7 @@ if (xobject) { }); } -},{"../emitter":2,"../util":12,"./polling":10,"debug":14,"global":20,"xmlhttprequest":13}],10:[function(require,module,exports){ +},{"../emitter":2,"../util":12,"./polling":10,"debug":15,"global":22,"xmlhttprequest":13}],10:[function(require,module,exports){ /** * Module dependencies. */ @@ -1906,7 +1906,7 @@ Polling.prototype.uri = function(){ return schema + '://' + this.hostname + port + this.path + query; }; -},{"../transport":5,"../util":12,"debug":14,"engine.io-parser":16,"global":20}],11:[function(require,module,exports){ +},{"../transport":5,"../util":12,"debug":15,"engine.io-parser":17,"global":22}],11:[function(require,module,exports){ /** * Module dependencies. */ @@ -2134,7 +2134,7 @@ WS.prototype.check = function(){ return !!WebSocket && !('__initialize' in WebSocket && this.name === WS.prototype.name); }; -},{"../transport":5,"../util":12,"debug":14,"engine.io-parser":16,"global":20,"ws":23}],12:[function(require,module,exports){ +},{"../transport":5,"../util":12,"debug":15,"engine.io-parser":17,"global":22,"ws":25}],12:[function(require,module,exports){ var global = require('global'); @@ -2356,7 +2356,7 @@ exports.qsParse = function(qs){ return qry; }; -},{"global":20}],13:[function(require,module,exports){ +},{"global":22}],13:[function(require,module,exports){ // browser shim for xmlhttprequest module var hasCORS = require('has-cors'); @@ -2377,7 +2377,62 @@ module.exports = function(opts) { } } -},{"has-cors":21}],14:[function(require,module,exports){ +},{"has-cors":23}],14:[function(require,module,exports){ +// shim for using process in browser + +var process = module.exports = {}; + +process.nextTick = (function () { + var canSetImmediate = typeof window !== 'undefined' + && window.setImmediate; + var canPost = typeof window !== 'undefined' + && window.postMessage && window.addEventListener + ; + + if (canSetImmediate) { + return function (f) { return window.setImmediate(f) }; + } + + if (canPost) { + var queue = []; + window.addEventListener('message', function (ev) { + var source = ev.source; + if ((source === window || source === null) && ev.data === 'process-tick') { + ev.stopPropagation(); + if (queue.length > 0) { + var fn = queue.shift(); + fn(); + } + } + }, true); + + return function nextTick(fn) { + queue.push(fn); + window.postMessage('process-tick', '*'); + }; + } + + return function nextTick(fn) { + setTimeout(fn, 0); + }; +})(); + +process.title = 'browser'; +process.browser = true; +process.env = {}; +process.argv = []; + +process.binding = function (name) { + throw new Error('process.binding is not supported'); +} + +// TODO(shtylman) +process.cwd = function () { return '/' }; +process.chdir = function (dir) { + throw new Error('process.chdir is not supported'); +}; + +},{}],15:[function(require,module,exports){ /** * Expose `debug()` as the module. @@ -2516,7 +2571,7 @@ try { if (window.localStorage) debug.enable(localStorage.debug); } catch(e){} -},{}],15:[function(require,module,exports){ +},{}],16:[function(require,module,exports){ /** * Module dependencies. @@ -2680,7 +2735,7 @@ Emitter.prototype.hasListeners = function(event){ return !! this.listeners(event).length; }; -},{"indexof":22}],16:[function(require,module,exports){ +},{"indexof":24}],17:[function(require,module,exports){ var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {};/** * Module dependencies. */ @@ -2688,6 +2743,7 @@ var global=typeof self !== "undefined" ? self : typeof window !== "undefined" ? var keys = require('./keys'); var base64encoder = require('base64-arraybuffer'); var sliceBuffer = require('./slice-buffer'); +var async = require('async'); /** * A utility for doing slicing, even when ArrayBuffer.prototype.slice doesn't @@ -2764,7 +2820,7 @@ exports.encodePacket = function (packet, supportsBinary, callback) { if (global.ArrayBuffer && data instanceof ArrayBuffer) { if (!supportsBinary) { - return callback('b' + exports.encodeBase64Packet(packet)); + return exports.encodeBase64Packet(packet, callback); } var contentArray = new Uint8Array(data); @@ -2773,6 +2829,17 @@ exports.encodePacket = function (packet, supportsBinary, callback) { resultBuffer[0] = packets[packet.type]; for (var i = 0; i < contentArray.length; i++) resultBuffer[i+1] = contentArray[i]; return callback(resultBuffer.buffer); + } else if (blobSupported && data instanceof Blob) { + if (!supportsBinary) { + return exports.encodeBase64Packet(packet, callback); + } + + var fr = new FileReader(); + fr.onload = function() { + packet.data = fr.result; + exports.encodePacket(packet, supportsBinary, callback); + }; + return fr.readAsArrayBuffer(packet.data); } // Sending data as a utf-8 string @@ -2794,8 +2861,18 @@ exports.encodePacket = function (packet, supportsBinary, callback) { * @return {String} base64 encoded message */ -exports.encodeBase64Packet = function(packet) { - var message = '' + exports.packets[packet.type]; +exports.encodeBase64Packet = function(packet, callback) { + var message = 'b' + exports.packets[packet.type]; + if (blobSupported && packet.data instanceof Blob) { + console.log('yolo'); + var fr = new FileReader(); + fr.onload = function() { + var b64 = fr.result.split(',')[1]; + callback(message + b64); + }; + return fr.readAsDataURL(packet.data); + } + var b64data = String.fromCharCode.apply(null, new Uint8Array(packet.data)); message += global.btoa(b64data); return callback(message); @@ -2884,16 +2961,19 @@ exports.encodePayload = function (packets, supportsBinary, callback) { return callback('0:'); } - var encoded = ''; - var message; - - for (var i = 0, l = packets.length; i < l; i++) { - exports.encodePacket(packets[i], supportsBinary, function(message) { - encoded += message.length + ':' + message; - }); - } + function setLengthHeader(message) { + return message.length + ':' + message; + }; - return callback(encoded); + function encodeOne(packet, doneCallback) { + exports.encodePacket(packet, supportsBinary, function(message) { + doneCallback(null, setLengthHeader(message)); + }); + }; + + async.map(packets, encodeOne, function(err, results) { + return callback(results.join('')); + }); }; /* @@ -2985,44 +3065,45 @@ exports.encodePayloadAsBinary = function (packets, callback) { return callback(new ArrayBuffer(0)); } - var encodedPackets = []; - packets.forEach(function(p) { - exports.encodePacket(p, function(data) { - encodedPackets.push(data); + function encodeOne(packet, doneCallback) { + exports.encodePacket(packet, function(data) { + return doneCallback(null, data); }); + } + + async.map(packets, encodeOne, function(err, encodedPackets) { + var totalLength = encodedPackets.reduce(function(acc, p) { + var len; + if (typeof p === 'string') len = p.length; + else len = p.byteLength; + return acc + (new String(len)).length + len + 2; // string/binary identifier + separator = 2 + }, 0); + + var resultArray = new Uint8Array(totalLength); + + var bufferIndex = 0; + encodedPackets.forEach(function(p) { + var isString = typeof p === 'string'; + var ab = p; + if (isString) { + var view = new Uint8Array(p.length); + for (var i = 0; i < p.length; i++) view[i] = p.charCodeAt(i); + ab = view.buffer; + } + + if (isString) { resultArray[bufferIndex++] = 0; } // not true binary + else { resultArray[bufferIndex++] = 1; } // true binary + + var lenStr = new String(ab.byteLength); + for (var i = 0; i < lenStr.length; i++) resultArray[bufferIndex++] = parseInt(lenStr[i]); + resultArray[bufferIndex++] = 255; + + var view = new Uint8Array(ab); + for (var i = 0; i < view.length; i++) resultArray[bufferIndex++] = view[i]; + }); + + return callback(resultArray.buffer); }); - - var totalLength = encodedPackets.reduce(function(acc, p) { - var len; - if (typeof p === 'string') len = p.length; - else len = p.byteLength; - return acc + (new String(len)).length + len + 2; // string/binary identifier + separator = 2 - }, 0); - - var resultArray = new Uint8Array(totalLength); - - var bufferIndex = 0; - encodedPackets.forEach(function(p) { - var isString = typeof p === 'string'; - var ab = p; - if (isString) { - var view = new Uint8Array(p.length); - for (var i = 0; i < p.length; i++) view[i] = p.charCodeAt(i); - ab = view.buffer; - } - - if (isString) { resultArray[bufferIndex++] = 0; } // not true binary - else { resultArray[bufferIndex++] = 1; } // true binary - - var lenStr = new String(ab.byteLength); - for (var i = 0; i < lenStr.length; i++) resultArray[bufferIndex++] = parseInt(lenStr[i]); - resultArray[bufferIndex++] = 255; - - var view = new Uint8Array(ab); - for (var i = 0; i < view.length; i++) resultArray[bufferIndex++] = view[i]; - }); - - return callback(resultArray.buffer); }; /* @@ -3066,7 +3147,7 @@ exports.decodePayloadAsBinary = function (data, binaryType, callback) { }); }; -},{"./keys":17,"./slice-buffer":18,"base64-arraybuffer":19}],17:[function(require,module,exports){ +},{"./keys":18,"./slice-buffer":19,"async":20,"base64-arraybuffer":21}],18:[function(require,module,exports){ /** * Gets the keys for an object. @@ -3087,7 +3168,7 @@ module.exports = Object.keys || function keys (obj){ return arr; }; -},{}],18:[function(require,module,exports){ +},{}],19:[function(require,module,exports){ /** * An abstraction for slicing an arraybuffer even when * ArrayBuffer.prototype.slice is not supported @@ -3118,7 +3199,967 @@ module.exports = function(arraybuffer, start, end) { return result.buffer; }; -},{}],19:[function(require,module,exports){ +},{}],20:[function(require,module,exports){ +var process=require("__browserify_process");/*global setImmediate: false, setTimeout: false, console: false */ +(function () { + + var async = {}; + + // global on the server, window in the browser + var root, previous_async; + + root = this; + if (root != null) { + previous_async = root.async; + } + + async.noConflict = function () { + root.async = previous_async; + return async; + }; + + function only_once(fn) { + var called = false; + return function() { + if (called) throw new Error("Callback was already called."); + called = true; + fn.apply(root, arguments); + } + } + + //// cross-browser compatiblity functions //// + + var _each = function (arr, iterator) { + if (arr.forEach) { + return arr.forEach(iterator); + } + for (var i = 0; i < arr.length; i += 1) { + iterator(arr[i], i, arr); + } + }; + + var _map = function (arr, iterator) { + if (arr.map) { + return arr.map(iterator); + } + var results = []; + _each(arr, function (x, i, a) { + results.push(iterator(x, i, a)); + }); + return results; + }; + + var _reduce = function (arr, iterator, memo) { + if (arr.reduce) { + return arr.reduce(iterator, memo); + } + _each(arr, function (x, i, a) { + memo = iterator(memo, x, i, a); + }); + return memo; + }; + + var _keys = function (obj) { + if (Object.keys) { + return Object.keys(obj); + } + var keys = []; + for (var k in obj) { + if (obj.hasOwnProperty(k)) { + keys.push(k); + } + } + return keys; + }; + + //// exported async module functions //// + + //// nextTick implementation with browser-compatible fallback //// + if (typeof process === 'undefined' || !(process.nextTick)) { + if (typeof setImmediate === 'function') { + async.nextTick = function (fn) { + // not a direct alias for IE10 compatibility + setImmediate(fn); + }; + async.setImmediate = async.nextTick; + } + else { + async.nextTick = function (fn) { + setTimeout(fn, 0); + }; + async.setImmediate = async.nextTick; + } + } + else { + async.nextTick = process.nextTick; + if (typeof setImmediate !== 'undefined') { + async.setImmediate = function (fn) { + // not a direct alias for IE10 compatibility + setImmediate(fn); + }; + } + else { + async.setImmediate = async.nextTick; + } + } + + async.each = function (arr, iterator, callback) { + callback = callback || function () {}; + if (!arr.length) { + return callback(); + } + var completed = 0; + _each(arr, function (x) { + iterator(x, only_once(function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + completed += 1; + if (completed >= arr.length) { + callback(null); + } + } + })); + }); + }; + async.forEach = async.each; + + async.eachSeries = function (arr, iterator, callback) { + callback = callback || function () {}; + if (!arr.length) { + return callback(); + } + var completed = 0; + var iterate = function () { + iterator(arr[completed], function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + completed += 1; + if (completed >= arr.length) { + callback(null); + } + else { + iterate(); + } + } + }); + }; + iterate(); + }; + async.forEachSeries = async.eachSeries; + + async.eachLimit = function (arr, limit, iterator, callback) { + var fn = _eachLimit(limit); + fn.apply(null, [arr, iterator, callback]); + }; + async.forEachLimit = async.eachLimit; + + var _eachLimit = function (limit) { + + return function (arr, iterator, callback) { + callback = callback || function () {}; + if (!arr.length || limit <= 0) { + return callback(); + } + var completed = 0; + var started = 0; + var running = 0; + + (function replenish () { + if (completed >= arr.length) { + return callback(); + } + + while (running < limit && started < arr.length) { + started += 1; + running += 1; + iterator(arr[started - 1], function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + completed += 1; + running -= 1; + if (completed >= arr.length) { + callback(); + } + else { + replenish(); + } + } + }); + } + })(); + }; + }; + + + var doParallel = function (fn) { + return function () { + var args = Array.prototype.slice.call(arguments); + return fn.apply(null, [async.each].concat(args)); + }; + }; + var doParallelLimit = function(limit, fn) { + return function () { + var args = Array.prototype.slice.call(arguments); + return fn.apply(null, [_eachLimit(limit)].concat(args)); + }; + }; + var doSeries = function (fn) { + return function () { + var args = Array.prototype.slice.call(arguments); + return fn.apply(null, [async.eachSeries].concat(args)); + }; + }; + + + var _asyncMap = function (eachfn, arr, iterator, callback) { + var results = []; + arr = _map(arr, function (x, i) { + return {index: i, value: x}; + }); + eachfn(arr, function (x, callback) { + iterator(x.value, function (err, v) { + results[x.index] = v; + callback(err); + }); + }, function (err) { + callback(err, results); + }); + }; + async.map = doParallel(_asyncMap); + async.mapSeries = doSeries(_asyncMap); + async.mapLimit = function (arr, limit, iterator, callback) { + return _mapLimit(limit)(arr, iterator, callback); + }; + + var _mapLimit = function(limit) { + return doParallelLimit(limit, _asyncMap); + }; + + // reduce only has a series version, as doing reduce in parallel won't + // work in many situations. + async.reduce = function (arr, memo, iterator, callback) { + async.eachSeries(arr, function (x, callback) { + iterator(memo, x, function (err, v) { + memo = v; + callback(err); + }); + }, function (err) { + callback(err, memo); + }); + }; + // inject alias + async.inject = async.reduce; + // foldl alias + async.foldl = async.reduce; + + async.reduceRight = function (arr, memo, iterator, callback) { + var reversed = _map(arr, function (x) { + return x; + }).reverse(); + async.reduce(reversed, memo, iterator, callback); + }; + // foldr alias + async.foldr = async.reduceRight; + + var _filter = function (eachfn, arr, iterator, callback) { + var results = []; + arr = _map(arr, function (x, i) { + return {index: i, value: x}; + }); + eachfn(arr, function (x, callback) { + iterator(x.value, function (v) { + if (v) { + results.push(x); + } + callback(); + }); + }, function (err) { + callback(_map(results.sort(function (a, b) { + return a.index - b.index; + }), function (x) { + return x.value; + })); + }); + }; + async.filter = doParallel(_filter); + async.filterSeries = doSeries(_filter); + // select alias + async.select = async.filter; + async.selectSeries = async.filterSeries; + + var _reject = function (eachfn, arr, iterator, callback) { + var results = []; + arr = _map(arr, function (x, i) { + return {index: i, value: x}; + }); + eachfn(arr, function (x, callback) { + iterator(x.value, function (v) { + if (!v) { + results.push(x); + } + callback(); + }); + }, function (err) { + callback(_map(results.sort(function (a, b) { + return a.index - b.index; + }), function (x) { + return x.value; + })); + }); + }; + async.reject = doParallel(_reject); + async.rejectSeries = doSeries(_reject); + + var _detect = function (eachfn, arr, iterator, main_callback) { + eachfn(arr, function (x, callback) { + iterator(x, function (result) { + if (result) { + main_callback(x); + main_callback = function () {}; + } + else { + callback(); + } + }); + }, function (err) { + main_callback(); + }); + }; + async.detect = doParallel(_detect); + async.detectSeries = doSeries(_detect); + + async.some = function (arr, iterator, main_callback) { + async.each(arr, function (x, callback) { + iterator(x, function (v) { + if (v) { + main_callback(true); + main_callback = function () {}; + } + callback(); + }); + }, function (err) { + main_callback(false); + }); + }; + // any alias + async.any = async.some; + + async.every = function (arr, iterator, main_callback) { + async.each(arr, function (x, callback) { + iterator(x, function (v) { + if (!v) { + main_callback(false); + main_callback = function () {}; + } + callback(); + }); + }, function (err) { + main_callback(true); + }); + }; + // all alias + async.all = async.every; + + async.sortBy = function (arr, iterator, callback) { + async.map(arr, function (x, callback) { + iterator(x, function (err, criteria) { + if (err) { + callback(err); + } + else { + callback(null, {value: x, criteria: criteria}); + } + }); + }, function (err, results) { + if (err) { + return callback(err); + } + else { + var fn = function (left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }; + callback(null, _map(results.sort(fn), function (x) { + return x.value; + })); + } + }); + }; + + async.auto = function (tasks, callback) { + callback = callback || function () {}; + var keys = _keys(tasks); + if (!keys.length) { + return callback(null); + } + + var results = {}; + + var listeners = []; + var addListener = function (fn) { + listeners.unshift(fn); + }; + var removeListener = function (fn) { + for (var i = 0; i < listeners.length; i += 1) { + if (listeners[i] === fn) { + listeners.splice(i, 1); + return; + } + } + }; + var taskComplete = function () { + _each(listeners.slice(0), function (fn) { + fn(); + }); + }; + + addListener(function () { + if (_keys(results).length === keys.length) { + callback(null, results); + callback = function () {}; + } + }); + + _each(keys, function (k) { + var task = (tasks[k] instanceof Function) ? [tasks[k]]: tasks[k]; + var taskCallback = function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + if (err) { + var safeResults = {}; + _each(_keys(results), function(rkey) { + safeResults[rkey] = results[rkey]; + }); + safeResults[k] = args; + callback(err, safeResults); + // stop subsequent errors hitting callback multiple times + callback = function () {}; + } + else { + results[k] = args; + async.setImmediate(taskComplete); + } + }; + var requires = task.slice(0, Math.abs(task.length - 1)) || []; + var ready = function () { + return _reduce(requires, function (a, x) { + return (a && results.hasOwnProperty(x)); + }, true) && !results.hasOwnProperty(k); + }; + if (ready()) { + task[task.length - 1](taskCallback, results); + } + else { + var listener = function () { + if (ready()) { + removeListener(listener); + task[task.length - 1](taskCallback, results); + } + }; + addListener(listener); + } + }); + }; + + async.waterfall = function (tasks, callback) { + callback = callback || function () {}; + if (tasks.constructor !== Array) { + var err = new Error('First argument to waterfall must be an array of functions'); + return callback(err); + } + if (!tasks.length) { + return callback(); + } + var wrapIterator = function (iterator) { + return function (err) { + if (err) { + callback.apply(null, arguments); + callback = function () {}; + } + else { + var args = Array.prototype.slice.call(arguments, 1); + var next = iterator.next(); + if (next) { + args.push(wrapIterator(next)); + } + else { + args.push(callback); + } + async.setImmediate(function () { + iterator.apply(null, args); + }); + } + }; + }; + wrapIterator(async.iterator(tasks))(); + }; + + var _parallel = function(eachfn, tasks, callback) { + callback = callback || function () {}; + if (tasks.constructor === Array) { + eachfn.map(tasks, function (fn, callback) { + if (fn) { + fn(function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + callback.call(null, err, args); + }); + } + }, callback); + } + else { + var results = {}; + eachfn.each(_keys(tasks), function (k, callback) { + tasks[k](function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + results[k] = args; + callback(err); + }); + }, function (err) { + callback(err, results); + }); + } + }; + + async.parallel = function (tasks, callback) { + _parallel({ map: async.map, each: async.each }, tasks, callback); + }; + + async.parallelLimit = function(tasks, limit, callback) { + _parallel({ map: _mapLimit(limit), each: _eachLimit(limit) }, tasks, callback); + }; + + async.series = function (tasks, callback) { + callback = callback || function () {}; + if (tasks.constructor === Array) { + async.mapSeries(tasks, function (fn, callback) { + if (fn) { + fn(function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + callback.call(null, err, args); + }); + } + }, callback); + } + else { + var results = {}; + async.eachSeries(_keys(tasks), function (k, callback) { + tasks[k](function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + results[k] = args; + callback(err); + }); + }, function (err) { + callback(err, results); + }); + } + }; + + async.iterator = function (tasks) { + var makeCallback = function (index) { + var fn = function () { + if (tasks.length) { + tasks[index].apply(null, arguments); + } + return fn.next(); + }; + fn.next = function () { + return (index < tasks.length - 1) ? makeCallback(index + 1): null; + }; + return fn; + }; + return makeCallback(0); + }; + + async.apply = function (fn) { + var args = Array.prototype.slice.call(arguments, 1); + return function () { + return fn.apply( + null, args.concat(Array.prototype.slice.call(arguments)) + ); + }; + }; + + var _concat = function (eachfn, arr, fn, callback) { + var r = []; + eachfn(arr, function (x, cb) { + fn(x, function (err, y) { + r = r.concat(y || []); + cb(err); + }); + }, function (err) { + callback(err, r); + }); + }; + async.concat = doParallel(_concat); + async.concatSeries = doSeries(_concat); + + async.whilst = function (test, iterator, callback) { + if (test()) { + iterator(function (err) { + if (err) { + return callback(err); + } + async.whilst(test, iterator, callback); + }); + } + else { + callback(); + } + }; + + async.doWhilst = function (iterator, test, callback) { + iterator(function (err) { + if (err) { + return callback(err); + } + if (test()) { + async.doWhilst(iterator, test, callback); + } + else { + callback(); + } + }); + }; + + async.until = function (test, iterator, callback) { + if (!test()) { + iterator(function (err) { + if (err) { + return callback(err); + } + async.until(test, iterator, callback); + }); + } + else { + callback(); + } + }; + + async.doUntil = function (iterator, test, callback) { + iterator(function (err) { + if (err) { + return callback(err); + } + if (!test()) { + async.doUntil(iterator, test, callback); + } + else { + callback(); + } + }); + }; + + async.queue = function (worker, concurrency) { + if (concurrency === undefined) { + concurrency = 1; + } + function _insert(q, data, pos, callback) { + if(data.constructor !== Array) { + data = [data]; + } + _each(data, function(task) { + var item = { + data: task, + callback: typeof callback === 'function' ? callback : null + }; + + if (pos) { + q.tasks.unshift(item); + } else { + q.tasks.push(item); + } + + if (q.saturated && q.tasks.length === concurrency) { + q.saturated(); + } + async.setImmediate(q.process); + }); + } + + var workers = 0; + var q = { + tasks: [], + concurrency: concurrency, + saturated: null, + empty: null, + drain: null, + push: function (data, callback) { + _insert(q, data, false, callback); + }, + unshift: function (data, callback) { + _insert(q, data, true, callback); + }, + process: function () { + if (workers < q.concurrency && q.tasks.length) { + var task = q.tasks.shift(); + if (q.empty && q.tasks.length === 0) { + q.empty(); + } + workers += 1; + var next = function () { + workers -= 1; + if (task.callback) { + task.callback.apply(task, arguments); + } + if (q.drain && q.tasks.length + workers === 0) { + q.drain(); + } + q.process(); + }; + var cb = only_once(next); + worker(task.data, cb); + } + }, + length: function () { + return q.tasks.length; + }, + running: function () { + return workers; + } + }; + return q; + }; + + async.cargo = function (worker, payload) { + var working = false, + tasks = []; + + var cargo = { + tasks: tasks, + payload: payload, + saturated: null, + empty: null, + drain: null, + push: function (data, callback) { + if(data.constructor !== Array) { + data = [data]; + } + _each(data, function(task) { + tasks.push({ + data: task, + callback: typeof callback === 'function' ? callback : null + }); + if (cargo.saturated && tasks.length === payload) { + cargo.saturated(); + } + }); + async.setImmediate(cargo.process); + }, + process: function process() { + if (working) return; + if (tasks.length === 0) { + if(cargo.drain) cargo.drain(); + return; + } + + var ts = typeof payload === 'number' + ? tasks.splice(0, payload) + : tasks.splice(0); + + var ds = _map(ts, function (task) { + return task.data; + }); + + if(cargo.empty) cargo.empty(); + working = true; + worker(ds, function () { + working = false; + + var args = arguments; + _each(ts, function (data) { + if (data.callback) { + data.callback.apply(null, args); + } + }); + + process(); + }); + }, + length: function () { + return tasks.length; + }, + running: function () { + return working; + } + }; + return cargo; + }; + + var _console_fn = function (name) { + return function (fn) { + var args = Array.prototype.slice.call(arguments, 1); + fn.apply(null, args.concat([function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (typeof console !== 'undefined') { + if (err) { + if (console.error) { + console.error(err); + } + } + else if (console[name]) { + _each(args, function (x) { + console[name](x); + }); + } + } + }])); + }; + }; + async.log = _console_fn('log'); + async.dir = _console_fn('dir'); + /*async.info = _console_fn('info'); + async.warn = _console_fn('warn'); + async.error = _console_fn('error');*/ + + async.memoize = function (fn, hasher) { + var memo = {}; + var queues = {}; + hasher = hasher || function (x) { + return x; + }; + var memoized = function () { + var args = Array.prototype.slice.call(arguments); + var callback = args.pop(); + var key = hasher.apply(null, args); + if (key in memo) { + callback.apply(null, memo[key]); + } + else if (key in queues) { + queues[key].push(callback); + } + else { + queues[key] = [callback]; + fn.apply(null, args.concat([function () { + memo[key] = arguments; + var q = queues[key]; + delete queues[key]; + for (var i = 0, l = q.length; i < l; i++) { + q[i].apply(null, arguments); + } + }])); + } + }; + memoized.memo = memo; + memoized.unmemoized = fn; + return memoized; + }; + + async.unmemoize = function (fn) { + return function () { + return (fn.unmemoized || fn).apply(null, arguments); + }; + }; + + async.times = function (count, iterator, callback) { + var counter = []; + for (var i = 0; i < count; i++) { + counter.push(i); + } + return async.map(counter, iterator, callback); + }; + + async.timesSeries = function (count, iterator, callback) { + var counter = []; + for (var i = 0; i < count; i++) { + counter.push(i); + } + return async.mapSeries(counter, iterator, callback); + }; + + async.compose = function (/* functions... */) { + var fns = Array.prototype.reverse.call(arguments); + return function () { + var that = this; + var args = Array.prototype.slice.call(arguments); + var callback = args.pop(); + async.reduce(fns, args, function (newargs, fn, cb) { + fn.apply(that, newargs.concat([function () { + var err = arguments[0]; + var nextargs = Array.prototype.slice.call(arguments, 1); + cb(err, nextargs); + }])) + }, + function (err, results) { + callback.apply(that, [err].concat(results)); + }); + }; + }; + + var _applyEach = function (eachfn, fns /*args...*/) { + var go = function () { + var that = this; + var args = Array.prototype.slice.call(arguments); + var callback = args.pop(); + return eachfn(fns, function (fn, cb) { + fn.apply(that, args.concat([cb])); + }, + callback); + }; + if (arguments.length > 2) { + var args = Array.prototype.slice.call(arguments, 2); + return go.apply(this, args); + } + else { + return go; + } + }; + async.applyEach = doParallel(_applyEach); + async.applyEachSeries = doSeries(_applyEach); + + async.forever = function (fn, callback) { + function next(err) { + if (err) { + if (callback) { + return callback(err); + } + throw err; + } + fn(next); + } + next(); + }; + + // AMD / RequireJS + if (typeof define !== 'undefined' && define.amd) { + define([], function () { + return async; + }); + } + // Node.js + else if (typeof module !== 'undefined' && module.exports) { + module.exports = async; + } + // included directly via