├── .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 | [](https://travis-ci.org/nkzawa/socket.io-stream)
4 | [](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 |
--------------------------------------------------------------------------------