├── .gitignore ├── .npmignore ├── .travis.yml ├── .zuul.yml ├── LICENSE ├── Makefile ├── README.md ├── index.js ├── lib ├── blob-read-stream.js ├── index.js ├── iostream.js ├── parser.js ├── socket.js └── uuid.js ├── package.json └── test ├── connection.js ├── index.js ├── parser.js ├── socket.io-stream.js └── support ├── checksum.js ├── frog.jpg ├── index.js └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.tmp 2 | npm-debug.log 3 | node_modules 4 | .DS_Store 5 | socket.io-stream.js 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | Makefile 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | install: 3 | - npm install 4 | - make install 5 | node_js: 6 | - 0.10 7 | - 0.12 8 | - 4.2 9 | sudo: false 10 | env: 11 | global: 12 | - secure: BOJOcmv3/8m1e2W7c4MStir5VraXxCeXlslW1xg926FUFry4kayWJhG9FD9GzWopVpeERAHPvr63pou7ylp7m2lkt1zUnacicKdTtXrdSlq2518LasnGJSxfFB59JOTdmdHn/gBAZKrhLTtsYw+mU6AanctXE/NyCVsSZXAig1Y= 13 | - secure: JoPDfwbrYEKY96XjVc59SSZ/cJ7ll2Vj2vmXqVk0yIwQiJdoXEHg3vwgZRPyCztwXlkjyQsLSLuJN1LSA/b04V8UdreAIILIbSazpQFDjH2ize18v6EVtyvP1PbQoicOsbLON8GdZGfP4kCJ4VNdr8JWLlpHS2Ef/Y0eCIQOxN0= 14 | matrix: 15 | - SOCKETIO_VERSION= 16 | - SOCKETIO_VERSION=0.9 17 | matrix: 18 | include: 19 | - node_js: 0.12 20 | env: BROWSER_NAME=chrome BROWSER_VERSION=latest 21 | - node_js: 0.12 22 | env: BROWSER_NAME=safari BROWSER_VERSION=latest 23 | - node_js: 0.12 24 | env: BROWSER_NAME=ie BROWSER_VERSION=latest 25 | - node_js: 0.12 26 | env: BROWSER_NAME=iphone BROWSER_VERSION=9.0 27 | - node_js: 0.12 28 | env: BROWSER_NAME=android BROWSER_VERSION=5.1 29 | -------------------------------------------------------------------------------- /.zuul.yml: -------------------------------------------------------------------------------- 1 | ui: mocha-bdd 2 | server: ./test/support/server.js 3 | tunnel: 4 | type: ngrok 5 | authtoken: XP5uKk4Sm6C5XfdTZozb 6 | proto: tcp 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Naoyuki Kanezawa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | REPORTER = dot 3 | 4 | build: 5 | @./node_modules/.bin/browserify index.js -s ss > socket.io-stream.js 6 | 7 | install: 8 | ifeq ($(SOCKETIO_VERSION),) 9 | @npm install 10 | else 11 | @npm install --cache-min 999999 socket.io@$(SOCKETIO_VERSION) 12 | @npm install --cache-min 999999 socket.io-client@$(SOCKETIO_VERSION) 13 | endif 14 | 15 | test: 16 | ifeq ($(BROWSER_NAME),) 17 | @./node_modules/.bin/mocha --reporter $(REPORTER) --require test/support/server.js 18 | else 19 | @./node_modules/.bin/zuul \ 20 | --browser-name $(BROWSER_NAME) \ 21 | --browser-version $(BROWSER_VERSION) \ 22 | -- test/index.js 23 | endif 24 | 25 | test-local: 26 | @./node_modules/.bin/zuul --local 8888 --disable-tunnel -- test/index.js 27 | 28 | .PHONY: build install test 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Socket.IO stream 2 | 3 | [![Build Status](https://travis-ci.org/nkzawa/socket.io-stream.png?branch=master)](https://travis-ci.org/nkzawa/socket.io-stream) 4 | [![NPM version](https://badge.fury.io/js/socket.io-stream.png)](http://badge.fury.io/js/socket.io-stream) 5 | 6 | This is the module for bidirectional binary data transfer with Stream API through [Socket.IO](https://github.com/socketio/socket.io). 7 | 8 | ## Installation 9 | 10 | npm install socket.io-stream 11 | 12 | ## Usage 13 | 14 | If you are not familiar with Stream API, be sure to check out [the docs](http://nodejs.org/api/stream.html). 15 | I also recommend checking out the awesome [Stream Handbook](https://github.com/substack/stream-handbook). 16 | 17 | For streaming between server and client, you will send stream instances first. 18 | To receive streams, you just wrap `socket` with `socket.io-stream`, then listen any events as usual. 19 | 20 | Server: 21 | 22 | ```js 23 | var io = require('socket.io').listen(80); 24 | var ss = require('socket.io-stream'); 25 | var path = require('path'); 26 | 27 | io.of('/user').on('connection', function(socket) { 28 | ss(socket).on('profile-image', function(stream, data) { 29 | var filename = path.basename(data.name); 30 | stream.pipe(fs.createWriteStream(filename)); 31 | }); 32 | }); 33 | ``` 34 | 35 | `createStream()` returns a new stream which can be sent by `emit()`. 36 | 37 | Client: 38 | 39 | ```js 40 | var io = require('socket.io-client'); 41 | var ss = require('socket.io-stream'); 42 | 43 | var socket = io.connect('http://example.com/user'); 44 | var stream = ss.createStream(); 45 | var filename = 'profile.jpg'; 46 | 47 | ss(socket).emit('profile-image', stream, {name: filename}); 48 | fs.createReadStream(filename).pipe(stream); 49 | ``` 50 | 51 | You can stream data from a client to server, and vice versa. 52 | 53 | ```js 54 | // send data 55 | ss(socket).on('file', function(stream) { 56 | fs.createReadStream('/path/to/file').pipe(stream); 57 | }); 58 | 59 | // receive data 60 | ss(socket).emit('file', stream); 61 | stream.pipe(fs.createWriteStream('file.txt')); 62 | ``` 63 | 64 | ### Browser 65 | 66 | This module can be used on the browser. To do so, just copy a file to a public directory. 67 | 68 | $ cp node_modules/socket.io-stream/socket.io-stream.js somewhere/public/ 69 | 70 | You can also use [browserify](http://github.com/substack/node-browserify) to create your own bundle. 71 | 72 | $ npm install browserify -g 73 | $ cd node_modules/socket.io-stream 74 | $ browserify index.js -s ss > socket.io-stream.js 75 | 76 | ```html 77 | 78 | 79 | 80 | 81 | 82 | 96 | ``` 97 | 98 | #### Upload progress 99 | 100 | You can track upload progress like the following: 101 | 102 | ```js 103 | var blobStream = ss.createBlobReadStream(file); 104 | var size = 0; 105 | 106 | blobStream.on('data', function(chunk) { 107 | size += chunk.length; 108 | console.log(Math.floor(size / file.size * 100) + '%'); 109 | // -> e.g. '42%' 110 | }); 111 | 112 | blobStream.pipe(stream); 113 | ``` 114 | 115 | ### Socket.IO v0.9 support 116 | 117 | You have to set `forceBase64` option `true` when using the library with socket.io v0.9.x. 118 | 119 | ```js 120 | ss.forceBase64 = true; 121 | ``` 122 | 123 | 124 | ## Documentation 125 | 126 | ### ss(sio) 127 | 128 | - sio `socket.io Socket` A socket of Socket.IO, both for client and server 129 | - return `Socket` 130 | 131 | Look up an existing `Socket` instance based on `sio` (a socket of Socket.IO), or create one if it doesn't exist. 132 | 133 | ### socket.emit(event, [arg1], [arg2], [...]) 134 | 135 | - event `String` The event name 136 | 137 | Emit an `event` with variable number of arguments including at least a stream. 138 | 139 | ```js 140 | ss(socket).emit('myevent', stream, {name: 'thefilename'}, function() { ... }); 141 | 142 | // send some streams at a time. 143 | ss(socket).emit('multiple-streams', stream1, stream2); 144 | 145 | // as members of array or object. 146 | ss(socket).emit('flexible', [stream1, { foo: stream2 }]); 147 | 148 | // get streams through the ack callback 149 | ss(socket).emit('ack', function(stream1, stream2) { ... }); 150 | ``` 151 | 152 | ### socket.on(event, listener) 153 | 154 | - event `String` The event name 155 | - listener `Function` The event handler function 156 | 157 | Add a `listener` for `event`. `listener` will take stream(s) with any data as arguments. 158 | 159 | ```js 160 | ss(socket).on('myevent', function(stream, data, callback) { ... }); 161 | 162 | // access stream options 163 | ss(socket).on('foo', function(stream) { 164 | if (stream.options && stream.options.highWaterMark > 1024) { 165 | console.error('Too big highWaterMark.'); 166 | return; 167 | } 168 | }); 169 | ``` 170 | 171 | ### ss.createStream([options]) 172 | 173 | - options `Object` 174 | - highWaterMark `Number` 175 | - encoding `String` 176 | - decodeStrings `Boolean` 177 | - objectMode `Boolean` 178 | - allowHalfOpen `Boolean` if `true`, then the stream won't automatically close when the other endpoint ends. Default to `false`. 179 | - return `Duplex Stream` 180 | 181 | Create a new duplex stream. See [the docs](http://nodejs.org/api/stream.html) for the details of stream and `options`. 182 | 183 | ```js 184 | var stream = ss.createStream(); 185 | 186 | // with options 187 | var stream = ss.createStream({ 188 | highWaterMark: 1024, 189 | objectMode: true, 190 | allowHalfOpen: true 191 | }); 192 | ``` 193 | 194 | ### ss.createBlobReadStream(blob, [options]) 195 | 196 | - options `Object` 197 | - highWaterMark `Number` 198 | - encoding `String` 199 | - objectMode `Boolean` 200 | - return `Readable Stream` 201 | 202 | Create a new readable stream for [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) and [File](https://developer.mozilla.org/en-US/docs/Web/API/File) on browser. See [the docs](http://nodejs.org/api/stream.html) for the details of stream and `options`. 203 | 204 | ```js 205 | var stream = ss.createBlobReadStream(new Blob([1, 2, 3])); 206 | ``` 207 | 208 | ### ss.Buffer 209 | 210 | [Node Buffer](https://nodejs.org/api/buffer.html) class to use on browser, which is exposed for convenience. On Node environment, you should just use normal `Buffer`. 211 | 212 | ```js 213 | var stream = ss.createStream(); 214 | stream.write(new ss.Buffer([0, 1, 2])); 215 | ``` 216 | 217 | ## License 218 | 219 | MIT 220 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = require('./lib'); 3 | 4 | -------------------------------------------------------------------------------- /lib/blob-read-stream.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var Readable = require('stream').Readable; 3 | var bind = require('component-bind'); 4 | 5 | 6 | module.exports = BlobReadStream; 7 | 8 | util.inherits(BlobReadStream, Readable); 9 | 10 | /** 11 | * Readable stream for Blob and File on browser. 12 | * 13 | * @param {Object} options 14 | * @api private 15 | */ 16 | function BlobReadStream(blob, options) { 17 | if (!(this instanceof BlobReadStream)) { 18 | return new BlobReadStream(blob, options); 19 | } 20 | 21 | Readable.call(this, options); 22 | 23 | options = options || {}; 24 | this.blob = blob; 25 | this.slice = blob.slice || blob.webkitSlice || blob.mozSlice; 26 | this.start = 0; 27 | this.sync = options.synchronous || false; 28 | 29 | var fileReader; 30 | 31 | if (options.synchronous) { 32 | fileReader = this.fileReader = new FileReaderSync(); 33 | } else { 34 | fileReader = this.fileReader = new FileReader(); 35 | } 36 | 37 | fileReader.onload = bind(this, '_onload'); 38 | fileReader.onerror = bind(this, '_onerror'); 39 | } 40 | 41 | BlobReadStream.prototype._read = function(size) { 42 | var start = this.start; 43 | var end = this.start = this.start + size; 44 | var chunk = this.slice.call(this.blob, start, end); 45 | 46 | if (chunk.size) { 47 | if (this.sync) { 48 | var bufferChunk = new Buffer(new Uint8Array(this.fileReader.readAsArrayBuffer(chunk))); 49 | this.push(bufferChunk); 50 | } else { 51 | this.fileReader.readAsArrayBuffer(chunk); 52 | } 53 | } else { 54 | this.push(null); 55 | } 56 | } 57 | 58 | BlobReadStream.prototype._onload = function(e) { 59 | var chunk = new Buffer(new Uint8Array(e.target.result)); 60 | this.push(chunk); 61 | }; 62 | 63 | BlobReadStream.prototype._onerror = function(e) { 64 | var err = e.target.error; 65 | this.emit('error', err); 66 | }; 67 | 68 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var Socket = require('./socket'); 2 | var IOStream = require('./iostream'); 3 | var BlobReadStream = require('./blob-read-stream'); 4 | 5 | 6 | exports = module.exports = lookup; 7 | 8 | /** 9 | * Expose Node Buffer for browser. 10 | * 11 | * @api public 12 | */ 13 | exports.Buffer = Buffer; 14 | 15 | /** 16 | * Expose Socket constructor. 17 | * 18 | * @api public 19 | */ 20 | exports.Socket = Socket; 21 | 22 | /** 23 | * Expose IOStream constructor. 24 | * 25 | * @api public 26 | */ 27 | exports.IOStream = IOStream; 28 | 29 | /** 30 | * Forces base 64 encoding when emitting. Must be set to true for Socket.IO v0.9 or lower. 31 | * 32 | * @api public 33 | */ 34 | exports.forceBase64 = false; 35 | 36 | /** 37 | * Look up an existing Socket. 38 | * 39 | * @param {socket.io#Socket} socket.io 40 | * @param {Object} options 41 | * @return {Socket} Socket instance 42 | * @api public 43 | */ 44 | function lookup(sio, options) { 45 | options = options || {}; 46 | if (null == options.forceBase64) { 47 | options.forceBase64 = exports.forceBase64; 48 | } 49 | 50 | if (!sio._streamSocket) { 51 | sio._streamSocket = new Socket(sio, options); 52 | } 53 | return sio._streamSocket; 54 | } 55 | 56 | /** 57 | * Creates a new duplex stream. 58 | * 59 | * @param {Object} options 60 | * @return {IOStream} duplex stream 61 | * @api public 62 | */ 63 | exports.createStream = function(options) { 64 | return new IOStream(options); 65 | }; 66 | 67 | /** 68 | * Creates a new readable stream for Blob/File on browser. 69 | * 70 | * @param {Blob} blob 71 | * @param {Object} options 72 | * @return {BlobReadStream} stream 73 | * @api public 74 | */ 75 | exports.createBlobReadStream = function(blob, options) { 76 | return new BlobReadStream(blob, options); 77 | }; 78 | -------------------------------------------------------------------------------- /lib/iostream.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var Duplex = require('stream').Duplex; 3 | var bind = require('component-bind'); 4 | var uuid = require('./uuid'); 5 | var debug = require('debug')('socket.io-stream:iostream'); 6 | 7 | 8 | module.exports = IOStream; 9 | 10 | util.inherits(IOStream, Duplex); 11 | 12 | /** 13 | * Duplex 14 | * 15 | * @param {Object} options 16 | * @api private 17 | */ 18 | function IOStream(options) { 19 | if (!(this instanceof IOStream)) { 20 | return new IOStream(options); 21 | } 22 | 23 | IOStream.super_.call(this, options); 24 | 25 | this.options = options; 26 | this.id = uuid(); 27 | this.socket = null; 28 | 29 | // Buffers 30 | this.pushBuffer = []; 31 | this.writeBuffer = []; 32 | 33 | // Op states 34 | this._readable = false; 35 | this._writable = false; 36 | this.destroyed = false; 37 | 38 | // default to *not* allowing half open sockets 39 | this.allowHalfOpen = options && options.allowHalfOpen || false; 40 | 41 | this.on('finish', this._onfinish); 42 | this.on('end', this._onend); 43 | this.on('error', this._onerror); 44 | } 45 | 46 | /** 47 | * Ensures that no more I/O activity happens on this stream. 48 | * Not necessary in the usual case. 49 | * 50 | * @api public 51 | */ 52 | IOStream.prototype.destroy = function() { 53 | debug('destroy'); 54 | 55 | if (this.destroyed) { 56 | debug('already destroyed'); 57 | return; 58 | } 59 | 60 | this.readable = this.writable = false; 61 | 62 | if (this.socket) { 63 | debug('clean up'); 64 | this.socket.cleanup(this.id); 65 | this.socket = null; 66 | } 67 | 68 | this.destroyed = true; 69 | }; 70 | 71 | /** 72 | * Local read 73 | * 74 | * @api private 75 | */ 76 | IOStream.prototype._read = function(size) { 77 | var push; 78 | 79 | // We can not read from the socket if it's destroyed obviously ... 80 | if (this.destroyed) return; 81 | 82 | if (this.pushBuffer.length) { 83 | // flush buffer and end if it exists. 84 | while (push = this.pushBuffer.shift()) { 85 | if (!push()) break; 86 | } 87 | return; 88 | } 89 | 90 | this._readable = true; 91 | 92 | // Go get data from remote stream 93 | // Calls 94 | // ._onread remotely 95 | // then 96 | // ._onwrite locally 97 | this.socket._read(this.id, size); 98 | }; 99 | 100 | 101 | /** 102 | * Read from remote stream 103 | * 104 | * @api private 105 | */ 106 | IOStream.prototype._onread = function(size) { 107 | var write = this.writeBuffer.shift(); 108 | if (write) return write(); 109 | 110 | this._writable = true; 111 | }; 112 | 113 | /** 114 | * Write local data to remote stream 115 | * Calls 116 | * remtote ._onwrite 117 | * 118 | * @api private 119 | */ 120 | IOStream.prototype._write = function(chunk, encoding, callback) { 121 | var self = this; 122 | 123 | function write() { 124 | // We can not write to the socket if it's destroyed obviously ... 125 | if (self.destroyed) return; 126 | 127 | self._writable = false; 128 | self.socket._write(self.id, chunk, encoding, callback); 129 | } 130 | 131 | if (this._writable) { 132 | write(); 133 | } else { 134 | this.writeBuffer.push(write); 135 | } 136 | }; 137 | 138 | /** 139 | * Write the data fetched remotely 140 | * so that we can now read locally 141 | * 142 | * @api private 143 | */ 144 | IOStream.prototype._onwrite = function(chunk, encoding, callback) { 145 | var self = this; 146 | 147 | function push() { 148 | self._readable = false; 149 | var ret = self.push(chunk || '', encoding); 150 | callback(); 151 | return ret; 152 | } 153 | 154 | if (this._readable) { 155 | push(); 156 | } else { 157 | this.pushBuffer.push(push); 158 | } 159 | }; 160 | 161 | /** 162 | * When ending send 'end' event to remote stream 163 | * 164 | * @api private 165 | */ 166 | IOStream.prototype._end = function() { 167 | if (this.pushBuffer.length) { 168 | // end after flushing buffer. 169 | this.pushBuffer.push(bind(this, '_done')); 170 | } else { 171 | this._done(); 172 | } 173 | }; 174 | 175 | /** 176 | * Remote stream just ended 177 | * 178 | * @api private 179 | */ 180 | IOStream.prototype._done = function() { 181 | this._readable = false; 182 | 183 | // signal the end of the data. 184 | return this.push(null); 185 | }; 186 | 187 | /** 188 | * the user has called .end(), and all the bytes have been 189 | * sent out to the other side. 190 | * If allowHalfOpen is false, or if the readable side has 191 | * ended already, then destroy. 192 | * If allowHalfOpen is true, then we need to set writable false, 193 | * so that only the writable side will be cleaned up. 194 | * 195 | * @api private 196 | */ 197 | IOStream.prototype._onfinish = function() { 198 | debug('_onfinish'); 199 | // Local socket just finished 200 | // send 'end' event to remote 201 | if (this.socket) { 202 | this.socket._end(this.id); 203 | } 204 | 205 | this.writable = false; 206 | this._writableState.ended = true; 207 | 208 | if (!this.readable || this._readableState.ended) { 209 | debug('_onfinish: ended, destroy %s', this._readableState); 210 | return this.destroy(); 211 | } 212 | 213 | debug('_onfinish: not ended'); 214 | 215 | if (!this.allowHalfOpen) { 216 | this.push(null); 217 | 218 | // just in case we're waiting for an EOF. 219 | if (this.readable && !this._readableState.endEmitted) { 220 | this.read(0); 221 | } 222 | } 223 | }; 224 | 225 | /** 226 | * the EOF has been received, and no more bytes are coming. 227 | * if the writable side has ended already, then clean everything 228 | * up. 229 | * 230 | * @api private 231 | */ 232 | IOStream.prototype._onend = function() { 233 | debug('_onend'); 234 | this.readable = false; 235 | this._readableState.ended = true; 236 | 237 | if (!this.writable || this._writableState.finished) { 238 | debug('_onend: %s', this._writableState); 239 | return this.destroy(); 240 | } 241 | 242 | debug('_onend: not finished'); 243 | 244 | if (!this.allowHalfOpen) { 245 | this.end(); 246 | } 247 | }; 248 | 249 | /** 250 | * When error in local stream 251 | * notyify remote 252 | * if err.remote = true 253 | * then error happened on remote stream 254 | * 255 | * @api private 256 | */ 257 | IOStream.prototype._onerror = function(err) { 258 | // check if the error came from remote stream. 259 | if (!err.remote && this.socket) { 260 | // notify the error to the corresponding remote stream. 261 | this.socket._error(this.id, err); 262 | } 263 | 264 | this.destroy(); 265 | }; 266 | -------------------------------------------------------------------------------- /lib/parser.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var EventEmitter = require('events').EventEmitter; 3 | var IOStream = require('./iostream'); 4 | var slice = Array.prototype.slice; 5 | 6 | exports.Encoder = Encoder; 7 | exports.Decoder = Decoder; 8 | 9 | util.inherits(Encoder, EventEmitter); 10 | 11 | function Encoder() { 12 | EventEmitter.call(this); 13 | } 14 | 15 | /** 16 | * Encode streams to placeholder objects. 17 | * 18 | * @api public 19 | */ 20 | Encoder.prototype.encode = function(v) { 21 | if (v instanceof IOStream) { 22 | return this.encodeStream(v); 23 | } else if (util.isArray(v)) { 24 | return this.encodeArray(v); 25 | } else if (v && 'object' == typeof v) { 26 | return this.encodeObject(v); 27 | } 28 | return v; 29 | } 30 | 31 | Encoder.prototype.encodeStream = function(stream) { 32 | this.emit('stream', stream); 33 | 34 | // represent a stream in an object. 35 | var v = { $stream: stream.id }; 36 | if (stream.options) { 37 | v.options = stream.options; 38 | } 39 | return v; 40 | } 41 | 42 | Encoder.prototype.encodeArray = function(arr) { 43 | var v = []; 44 | for (var i = 0, len = arr.length; i < len; i++) { 45 | v.push(this.encode(arr[i])); 46 | } 47 | return v; 48 | } 49 | 50 | Encoder.prototype.encodeObject = function(obj) { 51 | var v = {}; 52 | for (var k in obj) { 53 | if (obj.hasOwnProperty(k)) { 54 | v[k] = this.encode(obj[k]); 55 | } 56 | } 57 | return v; 58 | } 59 | 60 | util.inherits(Decoder, EventEmitter); 61 | 62 | function Decoder() { 63 | EventEmitter.call(this); 64 | } 65 | 66 | /** 67 | * Decode placeholder objects to streams. 68 | * 69 | * @api public 70 | */ 71 | Decoder.prototype.decode = function(v) { 72 | if (v && v.$stream) { 73 | return this.decodeStream(v); 74 | } else if (util.isArray(v)) { 75 | return this.decodeArray(v); 76 | } else if (v && 'object' == typeof v) { 77 | return this.decodeObject(v); 78 | } 79 | return v; 80 | } 81 | 82 | Decoder.prototype.decodeStream = function(obj) { 83 | var stream = new IOStream(obj.options); 84 | stream.id = obj.$stream; 85 | this.emit('stream', stream); 86 | return stream; 87 | } 88 | 89 | Decoder.prototype.decodeArray = function(arr) { 90 | var v = []; 91 | for (var i = 0, len = arr.length; i < len; i++) { 92 | v.push(this.decode(arr[i])); 93 | } 94 | return v; 95 | } 96 | 97 | Decoder.prototype.decodeObject = function(obj) { 98 | var v = {}; 99 | for (var k in obj) { 100 | if (obj.hasOwnProperty(k)) { 101 | v[k] = this.decode(obj[k]); 102 | } 103 | } 104 | return v; 105 | } 106 | -------------------------------------------------------------------------------- /lib/socket.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var EventEmitter = require('events').EventEmitter; 3 | var bind = require('component-bind'); 4 | var IOStream = require('./iostream'); 5 | var parser = require('./parser'); 6 | var debug = require('debug')('socket.io-stream:socket'); 7 | var emit = EventEmitter.prototype.emit; 8 | var on = EventEmitter.prototype.on; 9 | var slice = Array.prototype.slice; 10 | 11 | 12 | exports = module.exports = Socket; 13 | 14 | /** 15 | * Base event name for messaging. 16 | * 17 | * @api public 18 | */ 19 | exports.event = '$stream'; 20 | 21 | exports.events = [ 22 | 'error', 23 | 'newListener', 24 | 'removeListener' 25 | ]; 26 | 27 | util.inherits(Socket, EventEmitter); 28 | 29 | /** 30 | * Bidirectional stream socket which wraps Socket.IO. 31 | * 32 | * @param {socket.io#Socket} socket.io 33 | * @api public 34 | */ 35 | function Socket(sio, options) { 36 | if (!(this instanceof Socket)) { 37 | return new Socket(sio, options); 38 | } 39 | 40 | EventEmitter.call(this); 41 | 42 | options = options || {}; 43 | 44 | this.sio = sio; 45 | this.forceBase64 = !!options.forceBase64; 46 | this.streams = {}; 47 | this.encoder = new parser.Encoder(); 48 | this.decoder = new parser.Decoder(); 49 | 50 | var eventName = exports.event; 51 | sio.on(eventName, bind(this, emit)); 52 | sio.on(eventName + '-read', bind(this, '_onread')); 53 | sio.on(eventName + '-write', bind(this, '_onwrite')); 54 | sio.on(eventName + '-end', bind(this, '_onend')); 55 | sio.on(eventName + '-error', bind(this, '_onerror')); 56 | sio.on('error', bind(this, emit, 'error')); 57 | sio.on('disconnect', bind(this, '_ondisconnect')); 58 | 59 | this.encoder.on('stream', bind(this, '_onencode')); 60 | this.decoder.on('stream', bind(this, '_ondecode')); 61 | } 62 | 63 | /** 64 | * Original emit function. 65 | * 66 | * @api private 67 | */ 68 | Socket.prototype.$emit = emit; 69 | 70 | /** 71 | * Emits streams to this corresponding server/client. 72 | * 73 | * @return {Socket} self 74 | * @api public 75 | */ 76 | Socket.prototype.emit = function(type) { 77 | if (~exports.events.indexOf(type)) { 78 | return emit.apply(this, arguments); 79 | } 80 | this._stream.apply(this, arguments); 81 | return this; 82 | }; 83 | 84 | Socket.prototype.on = function(type, listener) { 85 | if (~exports.events.indexOf(type)) { 86 | return on.apply(this, arguments); 87 | } 88 | 89 | this._onstream(type, listener); 90 | return this; 91 | }; 92 | 93 | /** 94 | * Sends a new stream request. 95 | * 96 | * @param {String} event type 97 | * @api private 98 | */ 99 | Socket.prototype._stream = function(type) { 100 | debug('sending new streams'); 101 | 102 | var self = this; 103 | var args = slice.call(arguments, 1); 104 | var ack = args[args.length - 1]; 105 | if ('function' == typeof ack) { 106 | args[args.length - 1] = function() { 107 | var args = slice.call(arguments); 108 | args = self.decoder.decode(args); 109 | ack.apply(this, args); 110 | }; 111 | } 112 | 113 | args = this.encoder.encode(args); 114 | var sio = this.sio; 115 | sio.emit.apply(sio, [exports.event, type].concat(args)); 116 | }; 117 | 118 | /** 119 | * Notifies the read event. 120 | * 121 | * @api private 122 | */ 123 | Socket.prototype._read = function(id, size) { 124 | this.sio.emit(exports.event + '-read', id, size); 125 | }; 126 | 127 | /** 128 | * Requests to write a chunk. 129 | * 130 | * @api private 131 | */ 132 | Socket.prototype._write = function(id, chunk, encoding, callback) { 133 | if (Buffer.isBuffer(chunk)) { 134 | if (this.forceBase64) { 135 | encoding = 'base64'; 136 | chunk = chunk.toString(encoding); 137 | } else if (!global.Buffer) { 138 | // socket.io can't handle Buffer when using browserify. 139 | if (chunk.toArrayBuffer) { 140 | chunk = chunk.toArrayBuffer(); 141 | } else { 142 | chunk = chunk.buffer; 143 | } 144 | } 145 | } 146 | this.sio.emit(exports.event + '-write', id, chunk, encoding, callback); 147 | }; 148 | 149 | Socket.prototype._end = function(id) { 150 | this.sio.emit(exports.event + '-end', id); 151 | }; 152 | 153 | Socket.prototype._error = function(id, err) { 154 | this.sio.emit(exports.event + '-error', id, err.message || err); 155 | }; 156 | 157 | /** 158 | * Handles a new stream request. 159 | * 160 | * @param {String} event type 161 | * @param {Function} listener 162 | * 163 | * @api private 164 | */ 165 | Socket.prototype._onstream = function(type, listener) { 166 | if ('function' != typeof listener) { 167 | throw TypeError('listener must be a function'); 168 | } 169 | 170 | function onstream() { 171 | debug('new streams'); 172 | var self = this; 173 | var args = slice.call(arguments); 174 | var ack = args[args.length - 1]; 175 | if ('function' == typeof ack) { 176 | args[args.length - 1] = function() { 177 | var args = slice.call(arguments); 178 | args = self.encoder.encode(args); 179 | ack.apply(this, args); 180 | }; 181 | } 182 | 183 | args = this.decoder.decode(args); 184 | listener.apply(this, args); 185 | } 186 | 187 | // for removeListener 188 | onstream.listener = listener; 189 | 190 | on.call(this, type, onstream); 191 | }; 192 | 193 | Socket.prototype._onread = function(id, size) { 194 | debug('read: "%s"', id); 195 | 196 | var stream = this.streams[id]; 197 | if (stream) { 198 | stream._onread(size); 199 | } else { 200 | debug('ignore invalid stream id'); 201 | } 202 | }; 203 | 204 | Socket.prototype._onwrite = function(id, chunk, encoding, callback) { 205 | debug('write: "%s"', id); 206 | 207 | var stream = this.streams[id]; 208 | if (!stream) { 209 | callback('invalid stream id: ' + id); 210 | return; 211 | } 212 | 213 | if (global.ArrayBuffer && chunk instanceof ArrayBuffer) { 214 | // make sure that chunk is a buffer for stream 215 | chunk = new Buffer(new Uint8Array(chunk)); 216 | } 217 | stream._onwrite(chunk, encoding, callback); 218 | }; 219 | 220 | Socket.prototype._onend = function(id) { 221 | debug('end: "%s"', id); 222 | 223 | var stream = this.streams[id]; 224 | if (!stream) { 225 | debug('ignore non-existent stream id: "%s"', id); 226 | return; 227 | } 228 | 229 | stream._end(); 230 | }; 231 | 232 | Socket.prototype._onerror = function(id, message) { 233 | debug('error: "%s", "%s"', id, message); 234 | 235 | var stream = this.streams[id]; 236 | if (!stream) { 237 | debug('invalid stream id: "%s"', id); 238 | return; 239 | } 240 | 241 | var err = new Error(message); 242 | err.remote = true; 243 | stream.emit('error', err); 244 | }; 245 | 246 | Socket.prototype._ondisconnect = function() { 247 | var stream; 248 | for (var id in this.streams) { 249 | stream = this.streams[id]; 250 | stream.destroy(); 251 | 252 | // Close streams when the underlaying 253 | // socket.io connection is closed (regardless why) 254 | stream.emit('close'); 255 | stream.emit('error', new Error('Connection aborted')); 256 | } 257 | }; 258 | 259 | Socket.prototype._onencode = function(stream) { 260 | if (stream.socket || stream.destroyed) { 261 | throw new Error('stream has already been sent.'); 262 | } 263 | 264 | var id = stream.id; 265 | if (this.streams[id]) { 266 | throw new Error('Encoded stream already exists: ' + id); 267 | } 268 | 269 | this.streams[id] = stream; 270 | stream.socket = this; 271 | }; 272 | 273 | Socket.prototype._ondecode = function(stream) { 274 | var id = stream.id; 275 | if (this.streams[id]) { 276 | this._error(id, new Error('Decoded stream already exists: ' + id)); 277 | return; 278 | } 279 | 280 | this.streams[id] = stream; 281 | stream.socket = this; 282 | }; 283 | 284 | Socket.prototype.cleanup = function(id) { 285 | delete this.streams[id]; 286 | }; 287 | 288 | -------------------------------------------------------------------------------- /lib/uuid.js: -------------------------------------------------------------------------------- 1 | // UUID function from https://gist.github.com/jed/982883 2 | // More lightweight than node-uuid 3 | function b( 4 | a // placeholder 5 | ){ 6 | return a // if the placeholder was passed, return 7 | ? ( // a random number from 0 to 15 8 | a ^ // unless b is 8, 9 | Math.random() // in which case 10 | * 16 // a random number from 11 | >> a/4 // 8 to 11 12 | ).toString(16) // in hexadecimal 13 | : ( // or otherwise a concatenated string: 14 | [1e7] + // 10000000 + 15 | -1e3 + // -1000 + 16 | -4e3 + // -4000 + 17 | -8e3 + // -80000000 + 18 | -1e11 // -100000000000, 19 | ).replace( // replacing 20 | /[018]/g, // zeroes, ones, and eights with 21 | b // random hex digits 22 | ) 23 | } 24 | 25 | module.exports = b; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "socket.io-stream", 3 | "version": "0.9.1", 4 | "description": "stream for socket.io", 5 | "author": "Naoyuki Kanezawa ", 6 | "contributors": [ 7 | { 8 | "name": "Naoyuki Kanezawa", 9 | "email": "naoyuki.kanezawa@gmail.com" 10 | }, 11 | { 12 | "name": "Aaron O'Mullan", 13 | "email": "aaron.omullan@friendco.de" 14 | } 15 | ], 16 | "keywords": [ 17 | "stream", 18 | "socket.io", 19 | "binary", 20 | "file", 21 | "upload", 22 | "download" 23 | ], 24 | "repository": { 25 | "type": "git", 26 | "url": "git://github.com/nkzawa/socket.io-stream.git" 27 | }, 28 | "scripts": { 29 | "prepublish": "make build", 30 | "test": "make test" 31 | }, 32 | "dependencies": { 33 | "component-bind": "~1.0.0", 34 | "debug": "~2.2.0" 35 | }, 36 | "devDependencies": { 37 | "blob": "0.0.4", 38 | "browserify": "~13.1.0", 39 | "expect.js": "~0.3.1", 40 | "mocha": "~3.0.2", 41 | "socket.io": "~1.4.8", 42 | "socket.io-client": "~1.4.8", 43 | "zuul": "~3.11.1", 44 | "zuul-ngrok": "~4.0.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/connection.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect.js'); 2 | var Blob = require('blob'); 3 | var ss = require('../'); 4 | var support = require('./support'); 5 | var client = support.client; 6 | 7 | describe('socket.io-stream', function() { 8 | this.timeout(70000); 9 | 10 | it('should send/receive a file', function(done) { 11 | var sums = []; 12 | var socket = client(); 13 | socket.on('connect', function() { 14 | var file = ss.createStream(); 15 | ss(socket).emit('read', file, 'test/support/frog.jpg', function(sum) { 16 | check(sum); 17 | }); 18 | 19 | var checksum = ss.createStream(); 20 | ss(socket).emit('checksum', checksum, function(sum) { 21 | check(sum); 22 | }); 23 | 24 | file.pipe(checksum); 25 | 26 | function check(sum) { 27 | sums.push(sum); 28 | if (sums.length < 2) return; 29 | expect(sums[0]).to.equal(sums[1]); 30 | socket.disconnect(); 31 | done(); 32 | } 33 | }); 34 | }); 35 | 36 | it('should send/receive data in flowing mode', function(done) { 37 | var socket = client(); 38 | socket.on('connect', function() { 39 | var stream = ss.createStream(); 40 | ss(socket) 41 | .emit('echo', stream, { hi: 1 }) 42 | .on('echo', function(stream, obj) { 43 | expect(obj).to.eql({ hi: 1 }); 44 | 45 | var data = ''; 46 | stream.on('data', function(chunk) { 47 | data += chunk; 48 | }).on('end', function() { 49 | expect(data).to.equal('foobar'); 50 | socket.disconnect(); 51 | done(); 52 | }); 53 | }); 54 | 55 | stream.write('foo'); 56 | stream.write('bar'); 57 | stream.end(); 58 | }); 59 | }); 60 | 61 | it('should send/receive data in paused mode', function(done) { 62 | var socket = client(); 63 | socket.on('connect', function() { 64 | var stream = ss.createStream(); 65 | ss(socket) 66 | .emit('echo', stream, { hi: 1 }) 67 | .on('echo', function(stream, obj) { 68 | expect(obj).to.eql({ hi: 1 }); 69 | 70 | var data = ''; 71 | stream.on('readable', function() { 72 | var chunk; 73 | while (null !== (chunk = stream.read())) { 74 | data += chunk; 75 | } 76 | }).on('end', function() { 77 | expect(data).to.equal('foobar'); 78 | socket.disconnect(); 79 | done(); 80 | }); 81 | }); 82 | 83 | stream.write('foo'); 84 | stream.write('bar'); 85 | stream.end(); 86 | }); 87 | }); 88 | 89 | it('should send/receive Buffer', function(done) { 90 | var socket = client(); 91 | socket.on('connect', function() { 92 | var stream = ss.createStream(); 93 | ss(socket) 94 | .emit('echo', stream) 95 | .on('echo', function(stream) { 96 | var buffers = []; 97 | stream.on('data', function(chunk) { 98 | buffers.push(chunk); 99 | }).on('end', function() { 100 | var buffer = Buffer.concat(buffers); 101 | expect(buffer.length).to.be(4); 102 | for (var i = 0; i < 4; i++) { 103 | expect(buffer[i]).to.be(i); 104 | } 105 | socket.disconnect(); 106 | done(); 107 | }); 108 | }); 109 | 110 | stream.write(new Buffer([0, 1])); 111 | stream.write(new Buffer([2, 3])); 112 | stream.end(); 113 | }); 114 | }); 115 | 116 | it('should send/receive an object in object mode', function(done) { 117 | var socket = client(); 118 | socket.on('connect', function() { 119 | var stream = ss.createStream({ objectMode: true }); 120 | ss(socket) 121 | .emit('echo', stream) 122 | .on('echo', function(stream) { 123 | var data = []; 124 | stream.on('data', function(chunk) { 125 | data.push(chunk); 126 | }).on('end', function() { 127 | expect(data.length).to.be(2);; 128 | expect(data[0]).to.eql({ foo: 0 }); 129 | expect(data[1]).to.eql({ bar: 1 }); 130 | socket.disconnect(); 131 | done(); 132 | }); 133 | }); 134 | 135 | stream.write({ foo: 0 }); 136 | stream.write({ bar: 1 }); 137 | stream.end(); 138 | }); 139 | }); 140 | 141 | it('should send/receive streams in an array', function(done) { 142 | var socket = client(); 143 | socket.on('connect', function() { 144 | ss(socket) 145 | .emit('echo', [ss.createStream(), ss.createStream()]) 146 | .on('echo', function(data) { 147 | expect(data[0]).to.be.a(ss.IOStream); 148 | expect(data[1]).to.be.a(ss.IOStream); 149 | socket.disconnect(); 150 | done(); 151 | }); 152 | }); 153 | }); 154 | 155 | it('should send/receive streams in an object', function(done) { 156 | var socket = client(); 157 | socket.on('connect', function() { 158 | ss(socket) 159 | .emit('echo', { 160 | foo: ss.createStream(), 161 | bar: ss.createStream() 162 | }) 163 | .on('echo', function(data) { 164 | expect(data.foo).to.be.a(ss.IOStream); 165 | expect(data.bar).to.be.a(ss.IOStream); 166 | socket.disconnect(); 167 | done(); 168 | }); 169 | }); 170 | }); 171 | 172 | it('should send/receive data through a same stream', function(done) { 173 | var socket = client(); 174 | socket.on('connect', function() { 175 | var stream = ss.createStream({ allowHalfOpen: true }); 176 | ss(socket).emit('sendBack', stream); 177 | stream.write('foo'); 178 | stream.write('bar'); 179 | stream.end(); 180 | 181 | var data = ''; 182 | stream.on('data', function(chunk) { 183 | data += chunk; 184 | }).on('end', function() { 185 | expect(data).to.equal('foobar'); 186 | socket.disconnect(); 187 | done(); 188 | }); 189 | }); 190 | }); 191 | 192 | it('should handle multiple streams', function(done) { 193 | var socket = client(); 194 | socket.on('connect', function() { 195 | var stream1 = ss.createStream(); 196 | var stream2 = ss.createStream(); 197 | ss(socket).emit('multi', stream1, stream2); 198 | stream1.write('foo'); 199 | stream1.write('bar'); 200 | stream1.end(); 201 | 202 | var data = ''; 203 | stream2.on('data', function(chunk) { 204 | data += chunk; 205 | }).on('end', function() { 206 | expect(data).to.equal('foobar'); 207 | socket.disconnect(); 208 | done(); 209 | }); 210 | }); 211 | }); 212 | 213 | it('should get a stream through ack', function(done) { 214 | var socket = client(); 215 | socket.on('connect', function() { 216 | var stream = ss.createStream(); 217 | ss(socket).emit('ack', stream, function(stream) { 218 | var data = ''; 219 | stream.on('data', function(chunk) { 220 | data += chunk; 221 | }).on('end', function() { 222 | expect(data).to.equal('foobar'); 223 | socket.disconnect(); 224 | done(); 225 | }); 226 | }); 227 | 228 | stream.write('foo'); 229 | stream.write('bar'); 230 | stream.end(); 231 | }); 232 | }); 233 | 234 | it('should get streams through ack as object and array', function(done) { 235 | var socket = client(); 236 | socket.on('connect', function() { 237 | ss(socket).emit('ack', [ss.createStream(), { foo: ss.createStream() }], function(data) { 238 | expect(data[0]).to.be.a(ss.IOStream); 239 | expect(data[1].foo).to.be.a(ss.IOStream); 240 | socket.disconnect(); 241 | done(); 242 | }); 243 | }); 244 | }); 245 | 246 | it('should send an error happened on the client', function(done) { 247 | var socket = client(); 248 | socket.on('connect', function() { 249 | var stream = ss.createStream(); 250 | ss(socket).emit('clientError', stream, function(msg) { 251 | expect(msg).to.equal('error on the client'); 252 | done() 253 | }); 254 | stream.emit('error', new Error('error on the client')); 255 | }); 256 | }); 257 | 258 | it('should receive an error happened on the server', function(done) { 259 | var socket = client(); 260 | socket.on('connect', function() { 261 | var stream = ss.createStream(); 262 | ss(socket).emit('serverError', stream, 'error on the server'); 263 | stream.on('error', function(err) { 264 | expect(err.message).to.equal('error on the server'); 265 | done() 266 | }); 267 | }); 268 | }); 269 | 270 | if (Blob) { 271 | describe('BlobReadStream', function() { 272 | it('should read blob', function(done) { 273 | var socket = client(); 274 | socket.on('connect', function() { 275 | var stream = ss.createStream(); 276 | ss(socket) 277 | .emit('echo', stream) 278 | .on('echo', function(stream) { 279 | var data = ''; 280 | stream.on('data', function(chunk) { 281 | data += chunk; 282 | }).on('end', function() { 283 | expect(data).to.equal('foobar'); 284 | socket.disconnect(); 285 | done(); 286 | }); 287 | }); 288 | ss.createBlobReadStream(new Blob(['foo', 'bar'])).pipe(stream); 289 | }); 290 | }); 291 | }); 292 | } 293 | }); 294 | 295 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | require('./socket.io-stream'); 2 | require('./connection'); 3 | require('./parser'); 4 | -------------------------------------------------------------------------------- /test/parser.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect.js'); 2 | var ss = require('..'); 3 | var parser = require('../lib/parser'); 4 | 5 | describe('parser', function() { 6 | it('should encode/decode a stream', function() { 7 | var encoder = new parser.Encoder(); 8 | var decoder = new parser.Decoder(); 9 | var stream = ss.createStream(); 10 | var result = decoder.decode(encoder.encode(stream)); 11 | expect(result).to.be.a(ss.IOStream); 12 | expect(result).not.to.be(stream); 13 | }); 14 | 15 | it('should keep stream options', function() { 16 | var encoder = new parser.Encoder(); 17 | var decoder = new parser.Decoder(); 18 | var stream = ss.createStream({ highWaterMark: 10, objectMode: true, allowHalfOpen: true }) 19 | var result = decoder.decode(encoder.encode(stream)); 20 | expect(result.options).to.eql({ highWaterMark: 10, objectMode: true, allowHalfOpen: true }); 21 | }); 22 | 23 | it('should encode/decode every streams', function() { 24 | var encoder = new parser.Encoder(); 25 | var decoder = new parser.Decoder(); 26 | var result = decoder.decode(encoder.encode([ 27 | ss.createStream(), 28 | { foo: ss.createStream() } 29 | ])); 30 | expect(result[0]).to.be.a(ss.IOStream); 31 | expect(result[1].foo).to.be.a(ss.IOStream); 32 | }); 33 | 34 | it('should keep non-stream values', function() { 35 | var encoder = new parser.Encoder(); 36 | var decoder = new parser.Decoder(); 37 | var result = decoder.decode(encoder.encode([1, 'foo', { foo: 'bar' }, null, undefined])); 38 | expect(result).to.be.eql([1, 'foo', { foo: 'bar' }, null, undefined]); 39 | }); 40 | 41 | describe('Encoder', function() { 42 | it('should fire stream event', function(done) { 43 | var encoder = new parser.Encoder(); 44 | var stream = ss.createStream(); 45 | encoder.on('stream', function(s) { 46 | expect(s).to.be(stream); 47 | done(); 48 | }); 49 | encoder.encode(stream); 50 | }); 51 | }); 52 | 53 | describe('Decoder', function() { 54 | it('should fire stream event', function() { 55 | var encoder = new parser.Encoder(); 56 | var decoder = new parser.Decoder(); 57 | var stream; 58 | decoder.on('stream', function(s) { 59 | stream = s; 60 | }); 61 | var decoded = decoder.decode(encoder.encode(ss.createStream())); 62 | expect(stream).to.be(decoded); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/socket.io-stream.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect.js'); 2 | var io = require('socket.io-client'); 3 | var ss = require('../'); 4 | var parser = require('../lib/parser'); 5 | var client = require('./support').client; 6 | 7 | describe('socket.io-stream', function() { 8 | 9 | it('should expose values', function() { 10 | expect(ss.Buffer).to.be(Buffer); 11 | expect(ss.Socket).to.be.a('function'); 12 | expect(ss.IOStream).to.be.a('function'); 13 | expect(ss.forceBase64).to.be.a('boolean'); 14 | }); 15 | 16 | it('should always return a same instance for a socket', function() { 17 | var socket = client({ autoConnect: false }); 18 | expect(ss(socket)).to.be(ss(socket)); 19 | }); 20 | 21 | it('should throw an error when resending a stream', function() { 22 | var socket = ss(client({ autoConnect: false })); 23 | var stream = ss.createStream(); 24 | 25 | socket.emit('foo', stream); 26 | expect(function() { 27 | socket.emit('bar', stream); 28 | }).to.throwError(); 29 | }); 30 | 31 | it('should throw an error when sending destroyed streams', function() { 32 | var socket = ss(client({ autoConnect: false })); 33 | var stream = ss.createStream(); 34 | 35 | stream.destroy(); 36 | expect(function() { 37 | socket.emit('foo', stream); 38 | }).to.throwError(); 39 | }); 40 | 41 | describe('clean up', function() { 42 | beforeEach(function() { 43 | this.socket = ss(client({ autoConnect: false })); 44 | this.streams = function() { 45 | return Object.keys(this.socket.streams); 46 | }; 47 | }); 48 | 49 | describe('local streams', function() { 50 | beforeEach(function() { 51 | this.stream = ss.createStream(); 52 | this.socket.emit('foo', this.stream); 53 | expect(this.streams()).to.have.length(1); 54 | }); 55 | 56 | it('should be cleaned up on error', function() { 57 | this.stream.emit('error', new Error()); 58 | expect(this.streams()).to.have.length(0); 59 | }); 60 | 61 | it('should be cleaned up on finish', function(done) { 62 | var self = this; 63 | this.stream.on('end', function() { 64 | expect(self.streams()).to.have.length(0); 65 | done(); 66 | }); 67 | this.stream.emit('finish'); 68 | }); 69 | 70 | it('should be cleaned up on end', function() { 71 | this.stream.emit('end'); 72 | expect(this.streams()).to.have.length(0); 73 | }); 74 | }); 75 | 76 | describe('remote streams', function() { 77 | beforeEach(function(done) { 78 | var self = this; 79 | this.socket.on('foo', function(stream) { 80 | expect(self.streams()).to.have.length(1); 81 | self.stream = stream; 82 | done(); 83 | }); 84 | // emit a new stream event manually. 85 | var encoder = new parser.Encoder(); 86 | this.socket.$emit('foo', encoder.encode(ss.createStream())); 87 | }); 88 | 89 | it('should be cleaned up on error', function() { 90 | this.stream.emit('error', new Error()); 91 | expect(this.streams()).to.have.length(0); 92 | }); 93 | 94 | it('should be cleaned up on finish', function(done) { 95 | var self = this; 96 | this.stream.on('end', function() { 97 | expect(self.streams()).to.have.length(0); 98 | done(); 99 | }); 100 | this.stream.emit('finish'); 101 | }); 102 | 103 | it('should be cleaned up on end', function() { 104 | this.stream.emit('end'); 105 | expect(this.streams()).to.have.length(0); 106 | }); 107 | }); 108 | 109 | describe('when allowHalfOpen is enabled', function() { 110 | it('should clean up local streams only after both "finish" and "end" were called', function() { 111 | var stream = ss.createStream({ allowHalfOpen: true }); 112 | this.socket.emit('foo', stream); 113 | expect(this.streams()).to.have.length(1); 114 | 115 | stream.emit('end'); 116 | expect(this.streams()).to.have.length(1); 117 | 118 | stream.emit('finish'); 119 | expect(this.streams()).to.have.length(0); 120 | }); 121 | 122 | it('should clean up remote streams only after both "finish" and "end" were called', function(done) { 123 | var self = this; 124 | this.socket.on('foo', function(stream) { 125 | expect(self.streams()).to.have.length(1); 126 | 127 | stream.emit('end'); 128 | expect(self.streams()).to.have.length(1); 129 | 130 | stream.emit('finish'); 131 | expect(self.streams()).to.have.length(0); 132 | done(); 133 | }); 134 | // emit a new stream event manually. 135 | var encoder = new parser.Encoder(); 136 | this.socket.$emit('foo', encoder.encode(ss.createStream({ allowHalfOpen: true }))); 137 | }); 138 | }); 139 | }); 140 | 141 | describe('when socket.io has an error', function() { 142 | it('should propagate the error', function(done) { 143 | var sio = client({ autoConnect: false }); 144 | var socket = ss(sio); 145 | socket.on('error', function(err) { 146 | expect(err).to.be.an(Error); 147 | done(); 148 | }); 149 | (sio.$emit || sio.emit).call(sio, 'error', new Error()); 150 | }); 151 | }); 152 | 153 | describe('when socket.io is disconnected', function() { 154 | beforeEach(function() { 155 | var sio = client({ autoConnect: false }); 156 | var socket = ss(sio); 157 | this.stream = ss.createStream(); 158 | socket.emit('foo', this.stream); 159 | 160 | var emit = sio.$emit || sio.emit; 161 | this.disconnect = function() { 162 | emit.call(sio, 'disconnect'); 163 | }; 164 | }); 165 | 166 | it('should destroy streams', function() { 167 | this.disconnect(); 168 | expect(this.stream.destroyed).to.be.ok(); 169 | }); 170 | 171 | it('should trigger close event', function(done) { 172 | this.stream.on('close', done); 173 | this.disconnect(); 174 | }); 175 | 176 | it('should trigger error event', function(done) { 177 | this.stream.on('error', function(err) { 178 | expect(err).to.be.an(Error); 179 | done(); 180 | }); 181 | this.disconnect(); 182 | }); 183 | }); 184 | }); 185 | 186 | -------------------------------------------------------------------------------- /test/support/checksum.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var crypto = require('crypto'); 3 | var PassThrough = require('stream').PassThrough; 4 | 5 | module.exports = Checksum; 6 | 7 | util.inherits(Checksum, PassThrough); 8 | 9 | function Checksum(options) { 10 | PassThrough.call(this, options); 11 | this.hash = crypto.createHash('sha1'); 12 | this.resume(); 13 | } 14 | 15 | Checksum.prototype._write = function(chunk, encoding, callback) { 16 | this.hash.update(chunk, encoding); 17 | PassThrough.prototype._write.call(this, chunk, encoding, callback); 18 | }; 19 | 20 | Checksum.prototype.digest = function() { 21 | return this.hash.digest('hex'); 22 | }; 23 | -------------------------------------------------------------------------------- /test/support/frog.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nkzawa/socket.io-stream/8feef9e9ce296ec87e471731735abc6982e4158d/test/support/frog.jpg -------------------------------------------------------------------------------- /test/support/index.js: -------------------------------------------------------------------------------- 1 | var io = require('socket.io-client'); 2 | var ss = require('../../'); 3 | 4 | exports.port = process.env.ZUUL_PORT || 4000; 5 | 6 | var isBrowser = !!global.window; 7 | var defaultURI = isBrowser ? '' : 'http://localhost:' + exports.port; 8 | 9 | if (io.version) { 10 | ss.forceBase64 = true; 11 | 12 | var optionMap = { 13 | autoConnect: 'auto connect', 14 | forceNew: 'force new connection', 15 | reconnection: 'reconnect' 16 | }; 17 | 18 | // 0.9.x 19 | exports.client = function(uri, options) { 20 | if ('object' === typeof uri) { 21 | options = uri; 22 | uri = null; 23 | } 24 | uri = uri || defaultURI; 25 | options = options || {}; 26 | 27 | var _options = { 28 | 'force new connection': true 29 | }; 30 | 31 | for (var key in options) { 32 | _options[optionMap[key] || key] = options[key]; 33 | } 34 | 35 | return io.connect(uri, _options); 36 | }; 37 | 38 | } else { 39 | // 1.x.x 40 | 41 | exports.client = function(uri, options) { 42 | if ('object' === typeof uri) { 43 | options = uri; 44 | uri = null; 45 | } 46 | uri = uri || defaultURI; 47 | options = options || {}; 48 | 49 | var _options = { 50 | forceNew: true 51 | }; 52 | for (var key in options) { 53 | _options[key] = options[key]; 54 | } 55 | 56 | return io(uri, _options); 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /test/support/server.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var util = require('util'); 3 | var io = require('socket.io'); 4 | var Checksum = require('./checksum'); 5 | var crypto = require('crypto'); 6 | var ss = require('../../'); 7 | var support = require('./'); 8 | 9 | var server; 10 | if (io.version) { 11 | // 0.9.x 12 | ss.forceBase64 = true; 13 | server = io.listen(support.port, { 'log level': 1 }); 14 | } else { 15 | // 1.x.x 16 | server = io(support.port); 17 | } 18 | 19 | server.on('connection', function(socket) { 20 | 21 | ss(socket).on('read', function(stream, path, callback) { 22 | var file = fs.createReadStream(__dirname + '/../../' + path); 23 | var checksum = new Checksum(); 24 | file.pipe(checksum).pipe(stream).on('finish', function() { 25 | callback(checksum.digest()); 26 | }); 27 | }); 28 | 29 | ss(socket).on('checksum', function(stream, callback) { 30 | var checksum = new Checksum(); 31 | stream.pipe(checksum).on('finish', function() { 32 | callback(checksum.digest()); 33 | }).resume(); 34 | }); 35 | 36 | ss(socket).on('echo', function() { 37 | var args = Array.prototype.slice.call(arguments); 38 | var s = ss(socket); 39 | s.emit.apply(s, ['echo'].concat(echo(args))); 40 | }); 41 | 42 | ss(socket).on('sendBack', function() { 43 | var args = Array.prototype.slice.call(arguments); 44 | sendBack(args); 45 | }); 46 | 47 | ss(socket).on('multi', function(stream1, stream2) { 48 | stream1.pipe(stream2); 49 | }); 50 | 51 | ss(socket).on('ack', function() { 52 | var args = Array.prototype.slice.call(arguments); 53 | var callback = args.pop(); 54 | callback.apply(this, echo(args)); 55 | }); 56 | 57 | ss(socket).on('clientError', function(stream, callback) { 58 | stream.on('error', function(err) { 59 | callback(err.message); 60 | }); 61 | }); 62 | 63 | ss(socket).on('serverError', function(stream, msg) { 64 | stream.emit('error', new Error(msg)); 65 | }); 66 | }); 67 | 68 | function echo(v) { 69 | if (v instanceof ss.IOStream) { 70 | return v.pipe(ss.createStream(v.options)); 71 | } 72 | 73 | if (util.isArray(v)) { 74 | v = v.map(function(v) { 75 | return echo(v); 76 | }); 77 | } else if (v && 'object' == typeof v) { 78 | for (var k in v) { 79 | if (v.hasOwnProperty(k)) { 80 | v[k] = echo(v[k]); 81 | } 82 | } 83 | } 84 | return v; 85 | } 86 | 87 | function sendBack(v) { 88 | if (v instanceof ss.IOStream) { 89 | return v.pipe(v); 90 | } 91 | 92 | if (util.isArray(v)) { 93 | v.forEach(sendBack); 94 | } else if (v && 'object' == typeof v) { 95 | for (var k in v) { 96 | if (v.hasOwnProperty(k)) { 97 | sendBack(v[k]); 98 | } 99 | } 100 | } 101 | } 102 | --------------------------------------------------------------------------------