├── .gitignore ├── .jshintrc ├── .travis.yml ├── .zuul.yml ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── examples ├── memory.js └── rexec │ ├── client.js │ └── server.js ├── lib ├── channels.js ├── encoder.js ├── jschan.js ├── jschan_browser.js ├── memory │ ├── channel.js │ └── session.js └── stream │ ├── channels.js │ └── session.js ├── package.json └── test ├── abstract_session.js ├── memory.js └── stream.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | node_modules 14 | npm-debug.log 15 | mochahelper.js 16 | 17 | .idea/ 18 | .settings/ 19 | 20 | dist 21 | .tmp 22 | .sass-cache 23 | app/bower_components 24 | options.mine.js 25 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "white": false, 3 | "node": true, 4 | "browser": true, 5 | "esnext": true, 6 | "bitwise": true, 7 | "camelcase": true, 8 | "curly": true, 9 | "eqeqeq": true, 10 | "immed": true, 11 | "indent": 2, 12 | "latedef": true, 13 | "newcap": true, 14 | "noarg": true, 15 | "quotmark": "single", 16 | "regexp": true, 17 | "undef": true, 18 | "unused": true, 19 | "strict": true, 20 | "trailing": true, 21 | "smarttabs": true, 22 | "globals": { 23 | "angular": true, 24 | "document": true, 25 | "nf": true, 26 | "$":true, 27 | "_":true, 28 | "Highcharts": true, 29 | "timeseries": true, 30 | "describe": true, 31 | "beforeEach": true, 32 | "afterEach": true, 33 | "it": true, 34 | "before": true, 35 | "after": true 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - "0.10" 5 | - "0.12" 6 | - "iojs-v3.0" 7 | -------------------------------------------------------------------------------- /.zuul.yml: -------------------------------------------------------------------------------- 1 | ui: mocha-bdd 2 | server: ./test/support/browser_server.js 3 | browsers: 4 | - name: chrome 5 | version: latest 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # JsChan is an OPEN Open Source Project 2 | 3 | ----------------------------------------- 4 | 5 | ## What? 6 | 7 | Individuals making significant and valuable contributions are given commit-access to the project to contribute as they see fit. This project is more like an open wiki than a standard guarded open source project. 8 | 9 | ## Rules 10 | 11 | There are a few basic ground-rules for contributors: 12 | 13 | 1. **No `--force` pushes** or modifying the Git history in any way. 14 | 1. **Non-master branches** ought to be used for ongoing work. 15 | 1. **External API changes and significant modifications** ought to be subject to an **internal pull-request** to solicit feedback from other contributors. 16 | 1. Internal pull-requests to solicit feedback are *encouraged* for any other non-trivial contribution but left to the discretion of the contributor. 17 | 1. Contributors should attempt to adhere to the prevailing code-style. 18 | 19 | ## Releases 20 | 21 | Declaring formal releases remains the prerogative of the project maintainers. 22 | 23 | ## Changes to this arrangement 24 | 25 | This is an experiment and feedback is welcome! This document may also be subject to pull-requests or changes by contributors where you believe you have something valuable to add or change. 26 | 27 | ----------------------------------------- 28 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2014 jsChan Contributors, as listed in the README 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jschan  [![Build Status](https://travis-ci.org/GraftJS/jschan.png)](https://travis-ci.org/GraftJS/jschan) 2 | 3 | __jschan__ is a JavaScript port of [libchan](https://github.com/docker/libchan) based around node streams 4 | 5 | ## Status 6 | 7 | The jschan API should be stable at this point, but libchan is a developing standard and there may be breaking changes in future. 8 | 9 | When used over the standard SPDY transport, jschan is compatible with the current Go reference implementation. 10 | 11 | We also have a websocket transport to allow us to run jschan on the browser. This feature is not covered by the spec, and is not compatible with other implementations. 12 | 13 | ## Transports 14 | 15 | The jschan API has support for swappable transports, some of which are included and others which need to be installed. 16 | 17 | * memory (built-in) 18 | * binary streams (built-in) 19 | * [WebSockets](https://github.com/GraftJS/jschan-ws) 20 | * [SPDY](https://github.com/GraftJS/jschan-spdy) 21 | 22 | 23 | ## Install 24 | 25 | ```bash 26 | npm install jschan --save 27 | ``` 28 | 29 | ## Example 30 | 31 | This example exposes a service over SPDY. 32 | It is built to be interoperable with the original libchan version 33 | [rexec](https://github.com/dmcgowan/libchan/tree/rexec_tls_support/examples/rexec). 34 | 35 | ### Server 36 | 37 | The server opens up a jschan server to accept new sessions, and then 38 | execute the requests that comes through the channel. 39 | 40 | ```js 41 | 'use strict'; 42 | 43 | var spdy = require('jschan-spdy'); 44 | var childProcess = require('child_process'); 45 | var server = spdy.server(); 46 | server.listen(9323); 47 | 48 | function handleReq(req) { 49 | var child = childProcess.spawn( 50 | req.Cmd, 51 | req.Args, 52 | { 53 | stdio: [ 54 | 'pipe', 55 | 'pipe', 56 | 'pipe' 57 | ] 58 | } 59 | ); 60 | 61 | req.Stdin.pipe(child.stdin); 62 | child.stdout.pipe(req.Stdout); 63 | child.stderr.pipe(req.Stderr); 64 | 65 | child.on('exit', function(status) { 66 | req.StatusChan.write({ Status: status }); 67 | }); 68 | } 69 | 70 | function handleChannel(channel) { 71 | channel.on('data', handleReq); 72 | } 73 | 74 | function handleSession(session) { 75 | session.on('channel', handleChannel); 76 | } 77 | 78 | server.on('session', handleSession); 79 | ``` 80 | 81 | ### Client 82 | 83 | ```js 84 | 'use strict'; 85 | 86 | var usage = process.argv[0] + ' ' + process.argv[1] + ' command '; 87 | 88 | if (!process.argv[2]) { 89 | console.log(usage) 90 | process.exit(1) 91 | } 92 | 93 | var spdy = require('jschan-spdy'); 94 | var session = spdy.clientSession({ port: 9323 }); 95 | var sender = session.WriteChannel(); 96 | 97 | var cmd = { 98 | Args: process.argv.slice(3), 99 | Cmd: process.argv[2], 100 | StatusChan: sender.ReadChannel(), 101 | Stderr: process.stderr, 102 | Stdout: process.stdout, 103 | Stdin: process.stdin 104 | }; 105 | 106 | sender.write(cmd); 107 | 108 | cmd.StatusChan.on('data', function(data) { 109 | sender.end(); 110 | setTimeout(function() { 111 | console.log('ended with status', data.Status); 112 | process.exit(data.Status); 113 | }, 500); 114 | }) 115 | ``` 116 | 117 | ## What can we write as a message? 118 | 119 | You can write: 120 | 121 | * Any plain JS object, string, number, ecc that can be serialized by 122 | [msgpack5](http://npm.im/msgpack5) 123 | * Any channels, created from the [Channel interface](#channel) 124 | * Any _binary_ node streams, these will automatically be piped to jschan 125 | bytestreams, e.g. you can send a `fs.createReadStream()` as it is. 126 | Duplex works too, so you can send a TCP connection, too. 127 | * `objectMode: true` streams. If it's a Transform (see 128 | [through2](https://github.com/rvagg/through2)) then it __must__ be 129 | already piped with their source/destination. 130 | 131 | _What is left out?_ 132 | 133 | * Your custom objects, we do not want you to go through that route, we 134 | are already doing too much on that side with jsChan. 135 | 136 | 137 | ## API 138 | 139 | * Session Interface 140 | * session.WriteChannel() 141 | * session.close() 142 | * session.destroy() 143 | * Channel Interface 144 | * channel.ReadChannel() 145 | * channel.WriteChannel() 146 | * channel.BinaryStream() 147 | * channel.destroy() 148 | * jschan.memorySession() 149 | * jschan.streamSession() 150 | 151 | ------------------------------------------------------- 152 | 153 | ### Session Interface 154 | 155 | A session identifies an exchange of channels between two parties: an 156 | initiator and a recipient. Top-level channels can only be created by the 157 | initiator in 'write' mode, with 158 | WriteChannel(). 159 | 160 | 161 | Channels are unidirectional, but they can be nested (more on that 162 | later). 163 | 164 | 165 | #### session.WriteChannel() 166 | 167 | Creates a Channel in 'write mode', e.g. a `streams.Writable`. 168 | The channel follows the interface defined in 169 | Channel Interface. The stream is in `objectMode` 170 | with an `highWaterMark` of 16. 171 | 172 | 173 | #### session.close([callback]) 174 | 175 | Close the current session, but let any Channel to finish cleanly. 176 | Callback is called once all channels have been closed. 177 | 178 | 179 | #### session.destroy([callback]) 180 | 181 | Terminate the current session, forcing to close all the involved 182 | channels. 183 | Callback is called once all channels have been closed. 184 | 185 | #### Event: 'channel' 186 | 187 | `function (channel) { }` 188 | 189 | Emitted each time there is a new Channel. The channel will __always__ be 190 | a Readable stream. 191 | 192 | ------------------------------------------------------- 193 | 194 | ### Channel Interface 195 | 196 | A Channel is a Stream and can be a `Readable` or `Writable` depending on 197 | which side of the communication you are. A Channel is __never__ a 198 | duplex. 199 | 200 | In order to send messages through a Channel, you can use standards 201 | streams methods. Moreover, you can nest channels by including them 202 | in a message, like so: 203 | 204 | ```js 205 | var chan = session.WriteChannel(); 206 | var ret = chan.ReadChannel(); 207 | 208 | ret.on('data', function(res) { 209 | console.log('response', res); 210 | }); 211 | 212 | chan.write({ returnChannel: ret }); 213 | ``` 214 | 215 | Each channel has two properties to indicate its direction: 216 | 217 | * `isReadChannel`, is `true` when you can read from the channel, e.g. 218 | `chan.pipe(something)` 219 | * `isWriteChannel`, is `true` when you can write to the channel, e.g. 220 | `something.pipe(chan)` 221 | 222 | 223 | #### channel.ReadChannel() 224 | 225 | Returns a nested read channel, this channel will wait for data from the 226 | other party. 227 | 228 | 229 | #### channel.WriteChannel() 230 | 231 | Returns a nested write channel, this channel will buffer data up until 232 | is received by the other party. It fully respect backpressure. 233 | 234 | 235 | ### channel.BinaryStream() 236 | 237 | Returns a nested duplex binary stream. It fully respect backpressure. 238 | 239 | 240 | #### channel.destroy([callback]) 241 | 242 | Close the channel now. 243 | 244 | ------------------------------------------------------- 245 | 246 | ### jschan.memorySession() 247 | 248 | Returns a session that works only through the current node process 249 | memory. 250 | 251 | This is an examples that uses the in memory session: 252 | 253 | ```js 254 | 'use strict'; 255 | 256 | var jschan = require('jschan'); 257 | var session = jschan.memorySession(); 258 | var assert = require('assert'); 259 | 260 | session.on('channel', function server(chan) { 261 | // chan is a Readable stream 262 | chan.on('data', function(msg) { 263 | var returnChannel = msg.returnChannel; 264 | 265 | returnChannel.write({ hello: 'world' }); 266 | }); 267 | }); 268 | 269 | function client() { 270 | // chan is a Writable stream 271 | var chan = session.WriteChannel(); 272 | var ret = chan.ReadChannel(); 273 | var called = false; 274 | 275 | ret.on('data', function(res) { 276 | called = true; 277 | console.log('response', res); 278 | }); 279 | 280 | chan.write({ returnChannel: ret }); 281 | 282 | setTimeout(function() { 283 | assert(called, 'no response'); 284 | }, 200); 285 | } 286 | 287 | client(); 288 | ``` 289 | 290 | ------------------------------------------------------- 291 | 292 | ### jschan.streamSession(readable, writable, opts) 293 | 294 | Returns a session that works over any pair of readable and 295 | writable streams. This session encodes all messages in msgpack, 296 | and sends them over. It can work on top of TCP, websocket or 297 | other transports. 298 | 299 | _`streamSession` is not compatible with libchan._ 300 | 301 | Supported options: 302 | 303 | - `header`: `true` or `false` (default true), specifies if 304 | we want to prefix every msgPack message with its length. 305 | This is not needed if the underlining streams have their own 306 | framing. 307 | - `server`: `true` or `false` (default false), specifies if 308 | this is the server component or the client component. 309 | 310 | 311 | ## About LibChan 312 | 313 | It's most unique characteristic is that it replicates the semantics of go channels across network connections, while allowing for nested channels to be transferred in messages. This would let you to do things like attach a reference to a remote file on an HTTP response, that could be opened on the client side for reading or writing. 314 | 315 | The protocol uses SPDY as it's default transport with MSGPACK as it's default serialization format. Both are able to be switched out, with http1+websockets and protobuf fallbacks planned. 316 | SPDY is encrypted over TLS by default. 317 | 318 | While the RequestResponse pattern is the primary focus, Asynchronous Message Passing is still possible, due to the low level nature of the protocol. 319 | 320 | ![Graft](https://rawgit.com/GraftJS/graft.io/master/static/images/graft_logo.svg) 321 | 322 | The Graft project is formed to explore the possibilities of a web where servers and clients are able to communicate freely through a microservices architecture. 323 | 324 | > "instead of pretending everything is a local function even over the network (which turned out to be a bad idea), what if we did it the other way around? Pretend your components are communicating over a network even when they aren't." 325 | > [Solomon Hykes](http://github.com/shykes) (of Docker fame) on LibChan - [[link]](https://news.ycombinator.com/item?id=7874317) 326 | 327 | [Find out more about Graft](https://github.com/GraftJS/graft) 328 | 329 | ## Contributors 330 | 331 | * [Adrian Rossouw](http://github.com/Vertice) 332 | * [Peter Elger](https://github.com/pelger) 333 | * [Matteo Collina](https://github.com/mcollina) 334 | 335 | ## License 336 | 337 | MIT 338 | -------------------------------------------------------------------------------- /examples/memory.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | var jschan = require('./'); 5 | var session = jschan.memorySession(); 6 | var assert = require('assert'); 7 | 8 | session.on('channel', function server(chan) { 9 | chan.on('data', function(msg) { 10 | var returnChannel = msg.returnChannel; 11 | 12 | returnChannel.write({ hello: 'world' }); 13 | }); 14 | }); 15 | 16 | function client() { 17 | var chan = session.WriteChannel(); 18 | var ret = chan.ReadChannel(); 19 | var called = false; 20 | 21 | ret.on('data', function(res) { 22 | called = true; 23 | console.log('response', res); 24 | }); 25 | 26 | chan.write({ returnChannel: ret }); 27 | 28 | setTimeout(function() { 29 | assert(called, 'no response'); 30 | }, 200); 31 | } 32 | 33 | client(); 34 | -------------------------------------------------------------------------------- /examples/rexec/client.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | var usage = process.argv[0] + ' ' + process.argv[1] + ' command '; 6 | 7 | if (!process.argv[2]) { 8 | console.log(usage); 9 | process.exit(1); 10 | } 11 | 12 | var spdy = require('jschan-spdy'); 13 | var session = spdy.clientSession({ port: 9323 }); 14 | 15 | var sender = session.WriteChannel(); 16 | 17 | var cmd = { 18 | Args: process.argv.slice(3), 19 | Cmd: process.argv[2], 20 | StatusChan: sender.ReadChannel(), 21 | Stderr: process.stderr, 22 | Stdout: process.stdout, 23 | Stdin: process.stdin 24 | }; 25 | 26 | sender.write(cmd); 27 | 28 | cmd.StatusChan.on('data', function(data) { 29 | sender.end(); 30 | setTimeout(function() { 31 | console.log('ended with status', data.Status); 32 | process.exit(data.Status); 33 | }, 500); 34 | }); 35 | -------------------------------------------------------------------------------- /examples/rexec/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var spdy = require('jschan-spdy'); 4 | var childProcess = require('child_process'); 5 | var server = spdy.server(); 6 | server.listen(9323); 7 | 8 | function handleReq(req) { 9 | var child = childProcess.spawn( 10 | req.Cmd, 11 | req.Args, 12 | { 13 | stdio: [ 14 | 'pipe', 15 | 'pipe', 16 | 'pipe' 17 | ] 18 | } 19 | ); 20 | 21 | req.Stdin.pipe(child.stdin); 22 | child.stdout.pipe(req.Stdout); 23 | child.stderr.pipe(req.Stderr); 24 | 25 | child.on('exit', function(status) { 26 | req.StatusChan.write({ Status: status }); 27 | }); 28 | } 29 | 30 | function handleChannel(channel) { 31 | channel.on('data', handleReq); 32 | } 33 | 34 | function handleSession(session) { 35 | session.on('channel', handleChannel); 36 | } 37 | 38 | server.on('session', handleSession); 39 | -------------------------------------------------------------------------------- /lib/channels.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | var inherits = require('inherits'); 5 | var Transform = require('readable-stream').Transform; 6 | var Duplexer = require('reduplexer'); 7 | 8 | function Channel(session, id) { 9 | Transform.call(this, { objectMode: true, highWaterMark: 16 }); 10 | 11 | this._session = session; 12 | this.id = id; 13 | } 14 | 15 | inherits(Channel, Transform); 16 | 17 | Channel.prototype.WriteChannel = function() { 18 | return this._session._createWriteChannel(this); 19 | }; 20 | 21 | Channel.prototype.ReadChannel = function() { 22 | return this._session._createReadChannel(this); 23 | }; 24 | 25 | Channel.prototype.ByteStream = function() { 26 | return this._session._createByteStream(this); 27 | }; 28 | 29 | function ReadChannel(session, id) { 30 | Channel.call(this, session, id); 31 | 32 | this._finished = false; 33 | 34 | this.on('end', function() { 35 | this._finished = true; 36 | this.emit('close'); 37 | }); 38 | } 39 | 40 | inherits(ReadChannel, Channel); 41 | 42 | ReadChannel.prototype._transform = function transform(buf, enc, done) { 43 | this.push(buf); // we are just passing through 44 | done(); 45 | }; 46 | 47 | ReadChannel.prototype.destroy = function destroy(cb) { 48 | if (cb && !this._finished) { 49 | this.on('end', cb); 50 | } else if (cb) { 51 | cb(); 52 | } 53 | 54 | this.end(); 55 | }; 56 | 57 | ReadChannel.prototype.isReadChannel = true; 58 | ReadChannel.prototype.isWriteChannel = false; 59 | 60 | function WriteChannel(session, id) { 61 | Channel.call(this, session, id); 62 | 63 | this._finished = false; 64 | 65 | this.on('finish', function() { 66 | this._finished = true; 67 | this.emit('close'); 68 | }); 69 | } 70 | 71 | inherits(WriteChannel, Channel); 72 | 73 | WriteChannel.prototype._transform = function transform(buf, enc, done) { 74 | this.push(buf); 75 | done(); 76 | }; 77 | 78 | WriteChannel.prototype.destroy = function destroy(cb) { 79 | if (cb && !this._finished) { 80 | this.on('finish', cb); 81 | } else if (cb) { 82 | cb(); 83 | } 84 | this.end(); 85 | }; 86 | 87 | WriteChannel.prototype.isWriteChannel = true; 88 | WriteChannel.prototype.isReadChannel = false; 89 | 90 | function handleFinish() { 91 | /*jshint validthis:true */ 92 | this._finished = true; 93 | this.removeAllListeners('error'); 94 | this.on('error', function() {}); 95 | this.emit('close'); 96 | } 97 | 98 | function ByteStream(session, id) { 99 | if (!(this instanceof ByteStream)) { 100 | return new ByteStream(session, id); 101 | } 102 | 103 | this._session = session; 104 | this.id = id; 105 | this._finished = false; 106 | 107 | Duplexer.call(this); 108 | 109 | this.on('finish', handleFinish); 110 | } 111 | 112 | inherits(ByteStream, Duplexer); 113 | 114 | ByteStream.prototype.destroy = function destroy(cb) { 115 | if (this._finished) { 116 | return cb && cb(); 117 | } 118 | 119 | if (cb) { 120 | this.on('finish', cb); 121 | } 122 | 123 | this.end(); 124 | }; 125 | 126 | module.exports.Channel = Channel; 127 | module.exports.WriteChannel = WriteChannel; 128 | module.exports.ReadChannel = ReadChannel; 129 | module.exports.ByteStream = ByteStream; 130 | -------------------------------------------------------------------------------- /lib/encoder.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | var EventEmitter = require('events').EventEmitter; 5 | var inherits = require('inherits'); 6 | var msgpack = require('msgpack5'); 7 | 8 | function encodeAsChannel(obj) { 9 | return obj && 10 | ( obj.isReadChannel || 11 | obj.isWriteChannel || 12 | ( obj._readableState && 13 | obj._readableState.objectMode 14 | ) || 15 | ( obj._writableState && 16 | obj._writableState.objectMode 17 | ) 18 | ); 19 | } 20 | 21 | function encodeAsByteStream(obj) { 22 | return obj && 23 | ( 24 | (obj._readableState && !obj._readableState.objectMode) || 25 | (obj._writableState && !obj._writableState.objectMode) 26 | ) && 27 | !(obj.isReadChannel || obj.isWriteChannel); 28 | } 29 | 30 | function Encoder(session, channels) { 31 | if (!(this instanceof Encoder)) { 32 | return new Encoder(session, channels); 33 | } 34 | 35 | this._msgpack = msgpack(); 36 | this._session = session; 37 | 38 | var that = this; 39 | var ReadChannel = channels.ReadChannel; 40 | var WriteChannel = channels.WriteChannel; 41 | var ByteStream = channels.ByteStream; 42 | 43 | function encodeChannel(chan) { 44 | chan = that._rewriteChannel(chan); 45 | 46 | // hack, let's just use 4 bytes 47 | var buf = new Buffer(6); 48 | var pos = 0; 49 | 50 | // the channel type is 0x1 51 | buf.writeUInt8(0x1, pos++); 52 | 53 | if (chan.isReadChannel) { 54 | // let's encode is as outbound 55 | buf.writeUInt8(0x2, pos++); 56 | } else if (chan.isWriteChannel) { 57 | // let's encode is as inbound 58 | buf.writeUInt8(0x1, pos++); 59 | } 60 | 61 | buf.writeUInt32BE(chan.id, pos++); 62 | 63 | return buf; 64 | } 65 | 66 | function decodeChannel(buf) { 67 | var id = buf.readUInt32BE(1); 68 | var chan; 69 | 70 | switch (buf.readUInt8(0)) { 71 | case 0x01: 72 | chan = new ReadChannel(session, id); 73 | break; 74 | case 0x02: 75 | chan = new WriteChannel(session, id); 76 | break; 77 | default: 78 | return that.emit('error', new Error('unkown direction')); 79 | } 80 | 81 | that.emit('channel', chan); 82 | 83 | return chan; 84 | } 85 | 86 | function encodeByteStream(stream) { 87 | stream = that._rewriteStream(stream); 88 | 89 | // hack, let's just use 4 bytes 90 | var buf = new Buffer(5); 91 | buf.writeUInt8(0x2, 0); 92 | buf.writeUInt32BE(stream.id, 1); 93 | 94 | return buf; 95 | } 96 | 97 | function decodeByteStream(buf) { 98 | var id = buf.readUInt32BE(0); 99 | var stream = new ByteStream(session, id); 100 | that.emit('channel', stream); 101 | 102 | return stream; 103 | } 104 | 105 | this._msgpack.registerEncoder(encodeAsChannel, encodeChannel); 106 | this._msgpack.registerDecoder(0x1, decodeChannel); 107 | 108 | this._msgpack.registerEncoder(encodeAsByteStream, encodeByteStream); 109 | this._msgpack.registerDecoder(0x2, decodeByteStream); 110 | } 111 | 112 | inherits(Encoder, EventEmitter); 113 | 114 | Encoder.prototype.encode = function encode(obj, channel) { 115 | this.encodingChannel = channel; 116 | return this._msgpack.encode(obj); 117 | }; 118 | 119 | Encoder.prototype.decode = function decode(obj) { 120 | return this._msgpack.decode(obj); 121 | }; 122 | 123 | Encoder.prototype.decoder = function (opts) { 124 | return this._msgpack.decoder(opts); 125 | }; 126 | 127 | var transformErrorString = 'unable to auto-serialize a Transform stream not in object mode'; 128 | 129 | Encoder.prototype._rewriteChannel = function rewriteChannel(chan) { 130 | var newChan; 131 | 132 | if (chan._session === this._session) { 133 | return chan; 134 | } 135 | 136 | // auto fix transform streams into channels 137 | if (!chan.isReadChannel && !chan.isWriteChannel && chan._transform) { 138 | if (chan._readableState.objectMode && chan._readableState.pipesCount > 0) { 139 | // so this should be mapped to a WriteChannel 140 | chan.isWriteChannel = true; 141 | } else if (chan._writableState.objectMode) { 142 | // so this should be mapped to a ReadChannel 143 | chan.isReadChannel = true; 144 | } else { 145 | throw new Error(transformErrorString); 146 | } 147 | } 148 | 149 | if (!chan.isReadChannel && !chan.isWriteChannel) { 150 | if (chan._readableState && chan._readableState.objectMode) { 151 | // so this should be mapped to a ReadChannel 152 | chan.isReadChannel = true; 153 | } else if (chan._writableState.objectMode) { 154 | // so this should be mapped to a WriteChannel 155 | chan.isWriteChannel = true; 156 | } else { 157 | throw new Error('this should not happen'); 158 | } 159 | } 160 | 161 | if (chan.isReadChannel) { 162 | newChan = this.encodingChannel.WriteChannel(); 163 | chan.pipe(newChan); 164 | } else if (chan.isWriteChannel) { 165 | newChan = this.encodingChannel.ReadChannel(); 166 | newChan.pipe(chan); 167 | } else { 168 | throw new Error('specify readChannel or writeChannel property for streams not in the current channels'); 169 | } 170 | 171 | return newChan; 172 | }; 173 | 174 | Encoder.prototype._rewriteStream = function rewriteBinaryStream(stream) { 175 | if (stream._session === this._session) { 176 | return stream; 177 | } 178 | 179 | var byteStream = this._session._createByteStream(this.encodingChannel); 180 | 181 | if (stream._transform) { 182 | throw new Error(transformErrorString); 183 | } 184 | 185 | if (stream.readable) { 186 | stream.pipe(byteStream); 187 | } 188 | 189 | if (stream.writable) { 190 | byteStream.pipe(stream); 191 | } 192 | 193 | return byteStream; 194 | }; 195 | 196 | module.exports = Encoder; 197 | -------------------------------------------------------------------------------- /lib/jschan.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | var jschan = {}; 5 | module.exports = jschan; 6 | 7 | jschan.memorySession = require('./memory/session'); 8 | jschan.streamSession = require('./stream/session'); 9 | -------------------------------------------------------------------------------- /lib/jschan_browser.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | // weird hack to work on browserify 5 | // here we have a strange dependency loop 6 | require('stream'); 7 | require('readable-stream'); 8 | 9 | var jschan = {}; 10 | module.exports = jschan; 11 | 12 | jschan.memorySession = require('./memory/session'); 13 | jschan.streamSession = require('./stream/session'); 14 | -------------------------------------------------------------------------------- /lib/memory/channel.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | var Channel = require('../channels').Channel; 5 | var WriteChannel = require('../channels').WriteChannel; 6 | var ReadChannel = require('../channels').ReadChannel; 7 | var inherits = require('inherits'); 8 | var EventEmitter = require('events').EventEmitter; 9 | 10 | function MemoryReadChannel(session, id) { 11 | ReadChannel.call(this, session, id); 12 | } 13 | 14 | inherits(MemoryReadChannel, ReadChannel); 15 | 16 | function MemoryWriteChannel(session, id) { 17 | WriteChannel.call(this, session, id); 18 | 19 | this.pair = new MemoryReadChannel(session, id); 20 | this.pair.pair = this; 21 | this.pipe(this.pair); 22 | } 23 | 24 | inherits(MemoryWriteChannel, WriteChannel); 25 | 26 | function traverse(session, obj) { 27 | if (typeof obj !== 'object') { 28 | return; 29 | } 30 | 31 | var re = null; 32 | 33 | for (var key in obj) { 34 | if (obj[key] && obj.hasOwnProperty(key)) { 35 | if (obj[key]._libchanRef) { 36 | obj[key] = session._streams[obj[key]._libchanRef]; 37 | } else if (obj[key] instanceof MemoryWriteChannel && obj[key]._session === session) { 38 | obj[key] = obj[key].pair; 39 | } else if (obj[key] instanceof MemoryReadChannel && obj[key]._session === session) { 40 | obj[key] = obj[key].pair; 41 | } else if (!(obj[key] instanceof Channel) && !(obj[key] instanceof EventEmitter)) { 42 | // no recursion for channels and streams 43 | traverse(session, obj[key]); 44 | } 45 | 46 | if (!(obj[key] instanceof Channel) && obj[key]._transform) { 47 | if (obj[key]._readableState.objectMode && obj[key]._readableState.pipesCount > 0) { 48 | // so this is ReadChannel 49 | re = session._createWriteChannel(); 50 | re.pair.pipe(obj[key]); 51 | obj[key] = re; 52 | } else if (obj[key]._writableState.objectMode) { 53 | // so this is a WriteChannel 54 | re = session._createWriteChannel(); 55 | obj[key].pipe(re); 56 | obj[key] = re.pair; 57 | } else { 58 | throw new Error('unable to auto-serialize a Transform stream not in object mode'); 59 | } 60 | } 61 | } 62 | } 63 | } 64 | 65 | MemoryWriteChannel.prototype._transform = function(obj, enc, done) { 66 | try { 67 | traverse(this._session, obj); 68 | this.push(obj); 69 | } catch(err) { 70 | this.emit('error', err); 71 | } 72 | done(); 73 | }; 74 | 75 | module.exports = MemoryWriteChannel; 76 | -------------------------------------------------------------------------------- /lib/memory/session.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | var EventEmitter = require('events').EventEmitter; 5 | var inherits = require('inherits'); 6 | var duplexer = require('reduplexer'); 7 | var MemoryWriteChannel = require('./channel'); 8 | var PassThrough = require('readable-stream').PassThrough; 9 | 10 | function MemorySession(server) { 11 | if (!(this instanceof MemorySession)) { 12 | return new MemorySession(server); 13 | } 14 | 15 | this._delayedChannels = []; 16 | this._streams = {}; 17 | this._nextId = 0; 18 | 19 | this.on('newListener', function(event, listener) { 20 | var chan; 21 | if (event === 'channel') { 22 | while ((chan = this._delayedChannels.pop())) { 23 | listener(chan); 24 | } 25 | } 26 | }); 27 | 28 | if (server) { 29 | this.on('channel', server); 30 | } 31 | } 32 | 33 | inherits(MemorySession, EventEmitter); 34 | 35 | MemorySession.prototype._createWriteChannel = function() { 36 | var chan = new MemoryWriteChannel(this, this._nextId++); 37 | this._streams[chan.id] = chan; 38 | chan.on('error', this.emit.bind(this, 'error')); 39 | return chan; 40 | }; 41 | 42 | MemorySession.prototype._createReadChannel = function() { 43 | return this._createWriteChannel().pair; 44 | }; 45 | 46 | MemorySession.prototype._createByteStream = function() { 47 | var inStream = new PassThrough(); 48 | var outStream = new PassThrough(); 49 | var id = ++this._nextId; 50 | var result = duplexer(inStream, outStream); 51 | var other = duplexer(outStream, inStream); 52 | 53 | result._libchanRef = id; 54 | this._streams[id] = other; 55 | 56 | return result; 57 | }; 58 | 59 | MemorySession.prototype.WriteChannel = function WriteChannel() { 60 | var count = EventEmitter.listenerCount(this, 'channel'); 61 | var chan = this._createWriteChannel(); 62 | 63 | if (count > 0) { 64 | this.emit('channel', chan.pair); 65 | } else { 66 | this._delayedChannels.push(chan.pair); 67 | } 68 | 69 | return chan; 70 | }; 71 | 72 | MemorySession.prototype.close = function close(cb) { 73 | if (cb) { 74 | this.once('close', cb); 75 | } 76 | this.emit('close'); 77 | return this; 78 | }; 79 | 80 | MemorySession.prototype.destroy = MemorySession.prototype.close; 81 | 82 | module.exports = MemorySession; 83 | -------------------------------------------------------------------------------- /lib/stream/channels.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | var channels = require('../channels'); 5 | var inherits = require('inherits'); 6 | var Duplex = require('readable-stream').Duplex; 7 | 8 | module.exports = Object.create(channels); 9 | 10 | function StreamWriteChannel(session, id) { 11 | if (!(this instanceof StreamWriteChannel)) { 12 | return new StreamWriteChannel(session, id); 13 | } 14 | channels.WriteChannel.call(this, session, id); 15 | 16 | this._firstChunk = true; 17 | } 18 | 19 | inherits(StreamWriteChannel, channels.WriteChannel); 20 | 21 | function dispatchData(obj, enc, done) { 22 | /*jshint validthis:true */ 23 | var msg = { 24 | id: this.id, 25 | data: obj 26 | }; 27 | 28 | if (this._firstChunk) { 29 | msg.parent = this.parentId || null; 30 | this._firstChunk = false; 31 | } 32 | 33 | this._session._dispatch(msg, this, done); 34 | } 35 | 36 | StreamWriteChannel.prototype._transform = dispatchData; 37 | 38 | StreamWriteChannel.prototype._flush = function(done) { 39 | this._session._dispatch({ 40 | id: this.id 41 | }, this, done); 42 | }; 43 | 44 | module.exports.WriteChannel = StreamWriteChannel; 45 | 46 | function ByteStream(session, id) { 47 | if (!(this instanceof ByteStream)) { 48 | return new ByteStream(session, id); 49 | } 50 | 51 | this._session = session; 52 | this.id = id; 53 | 54 | this._lastDone = null; 55 | 56 | Duplex.call(this); 57 | 58 | this._finished = false; 59 | 60 | this.on('finish', function() { 61 | var that = this; 62 | this._session._dispatch({ 63 | id: id 64 | }, this, function() { 65 | that._finished = true; 66 | that.emit('finishDispatched'); 67 | }); 68 | }); 69 | } 70 | 71 | inherits(ByteStream, Duplex); 72 | 73 | ByteStream.prototype._read = function() { 74 | var done = this._lastDone; 75 | if (done) { 76 | this._lastDone = null; 77 | done(); 78 | } 79 | return null; 80 | }; 81 | 82 | ByteStream.prototype._write = dispatchData; 83 | 84 | ByteStream.prototype.dispatch = function(chunk, done) { 85 | var keepOn = this.push(chunk || null); 86 | 87 | if (keepOn) { 88 | done(); 89 | } else { 90 | this._lastDone = done; 91 | } 92 | }; 93 | 94 | ByteStream.prototype.destroy = function destroy(cb) { 95 | if (cb && !this._finished) { 96 | this.on('finishDispatched', cb); 97 | } else if (cb) { 98 | cb(); 99 | } 100 | 101 | this.end(); 102 | }; 103 | 104 | module.exports.ByteStream = ByteStream; 105 | -------------------------------------------------------------------------------- /lib/stream/session.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | var EventEmitter = require('events').EventEmitter; 5 | var channels = require('./channels'); 6 | var inherits = require('inherits'); 7 | var through = require('through2'); 8 | var ReadChannel = channels.ReadChannel; 9 | var WriteChannel = channels.WriteChannel; 10 | var ByteStream = channels.ByteStream; 11 | var encoder = require('../encoder'); 12 | var PassThrough = require('readable-stream').PassThrough; 13 | var async = require('async'); 14 | 15 | function StreamSession(inStream, outStream, opts, server) { 16 | if (!(this instanceof StreamSession)) { 17 | return new StreamSession(inStream, outStream, opts, server); 18 | } 19 | 20 | opts = opts || {}; 21 | 22 | this._isServer = opts.server || false; 23 | 24 | if (opts.header === false) { 25 | this._haveHeaders = false; 26 | } else { 27 | this._haveHeaders = true; 28 | } 29 | 30 | this._inStream = inStream; 31 | this._outStream = outStream; 32 | this._delayedChannels = []; 33 | this._streams = {}; 34 | this._nextId = this._isServer ? 0 : 1; 35 | this._toBeWritten = []; 36 | this._encoder = encoder(this, channels); 37 | 38 | var that = this; 39 | 40 | this._readPipe = inStream 41 | .pipe(this._encoder.decoder(opts)) 42 | .pipe(through.obj(function(chunk, enc, done) { 43 | if (chunk.id === undefined) { 44 | that.emit('error', new Error('wrong message format, missing id')); 45 | return; 46 | } 47 | 48 | var count = EventEmitter.listenerCount(that, 'channel'); 49 | var stream = that._streams[chunk.id]; 50 | 51 | if (!stream && !chunk.parent) { 52 | stream = new ReadChannel(that, chunk.id); 53 | that._streams[stream.id] = stream; 54 | 55 | stream.on('close', function() { 56 | delete that._streams[stream.id]; 57 | }); 58 | 59 | if (count > 0 ) { 60 | that.emit('channel', stream); 61 | } else { 62 | that._delayedChannels.push(stream); 63 | } 64 | } 65 | 66 | if (!stream) { 67 | // we need to queue data 68 | stream = new PassThrough({ 69 | objectMode: true, 70 | highWaterMark: 16 71 | }); 72 | stream.id = chunk.id; 73 | that._streams[stream.id] = stream; 74 | } 75 | 76 | if (stream.dispatch) { 77 | stream.dispatch(chunk.data, done); 78 | } else if (chunk.data) { 79 | stream.write(chunk.data, null, done); 80 | } else { 81 | stream.end(); 82 | done(); 83 | } 84 | })); 85 | 86 | this.on('newListener', function(event, listener) { 87 | var chan; 88 | if (event === 'channel') { 89 | while ((chan = this._delayedChannels.pop())) { 90 | listener(chan); 91 | } 92 | } 93 | }); 94 | 95 | this._encoder.on('channel', function(chan) { 96 | if (that._streams[chan.id]) { 97 | that._streams[chan.id].pipe(chan, { end: false }); 98 | } 99 | that._streams[chan.id] = chan; 100 | }); 101 | 102 | if (server) { 103 | this.on('channel', server); 104 | } 105 | 106 | this._inStream.on('error', this.emit.bind(this, 'error')); 107 | if (this._inStream !== this._outStream) { 108 | this._outStream.on('error', this.emit.bind(this, 'error')); 109 | } 110 | 111 | var count = 2; 112 | function complete() { 113 | /*jshint validthis:true */ 114 | this.removeListener('finish', complete); 115 | this.removeListener('end', complete); 116 | this.removeListener('close', complete); 117 | if (--count === 0) { 118 | that._closing = false; 119 | that._closed = true; 120 | that.emit('close'); 121 | } 122 | } 123 | 124 | that._inStream.on('end', complete); 125 | that._outStream.on('finish', complete); 126 | that._inStream.on('close', complete); 127 | that._outStream.on('close', complete); 128 | } 129 | 130 | inherits(StreamSession, EventEmitter); 131 | 132 | function createChannel(session, Type, parent) { 133 | var chan = new Type(session, session._nextId); 134 | 135 | if (parent) { 136 | chan.parentId = parent.id; 137 | } 138 | 139 | session._nextId += 2; 140 | session._streams[chan.id] = chan; 141 | return chan; 142 | } 143 | 144 | StreamSession.prototype._createWriteChannel = function(parent) { 145 | var chan = createChannel(this, WriteChannel, parent); 146 | 147 | chan.on('finish', function() { 148 | delete this._session._streams[chan.id]; 149 | }); 150 | 151 | return chan; 152 | }; 153 | 154 | StreamSession.prototype._createReadChannel = function(parent) { 155 | var chan = createChannel(this, ReadChannel, parent); 156 | var that = this; 157 | 158 | chan.on('close', function() { 159 | delete that._streams[chan.id]; 160 | }); 161 | 162 | return chan; 163 | }; 164 | 165 | StreamSession.prototype._createByteStream = function(parent) { 166 | return createChannel(this, ByteStream, parent); 167 | }; 168 | 169 | StreamSession.prototype.WriteChannel = function WriteChannel() { 170 | return this._createWriteChannel(); 171 | }; 172 | 173 | StreamSession.prototype._dispatch = function dispatch(obj, chan, done) { 174 | if (this._closing) { 175 | if (done) { 176 | // we are closing everything anyway 177 | done(); 178 | } 179 | return this; 180 | } 181 | 182 | try { 183 | var encoded = this._encoder.encode(obj, chan).slice(0); 184 | 185 | // header logic copied from msgpack5.encoder 186 | if (this._haveHeaders) { 187 | var header = new Buffer(4); 188 | header.writeUInt32BE(encoded.length, 0); 189 | this._outStream.write(header); 190 | } 191 | 192 | if (this._outStream.write.length >= 2 ) { 193 | this._outStream.write(encoded, done); 194 | } else { 195 | this._outStream.write(encoded); 196 | done(); 197 | } 198 | } catch(err) { 199 | done(); 200 | // swallow any closing error 201 | this.emit('error', err); 202 | } 203 | 204 | return this; 205 | }; 206 | 207 | StreamSession.prototype.destroy = function close(wait, done) { 208 | if (typeof wait === 'function') { 209 | done = wait; 210 | wait = false; 211 | } 212 | 213 | if (this._closing) { 214 | return done && this.on('close', done) || this; 215 | } else if (this._closed) { 216 | return done && done() || this; 217 | } 218 | 219 | var that = this; 220 | 221 | if (done) { 222 | this.on('close', done); 223 | } 224 | 225 | that._closing = true; 226 | 227 | async.forEach(Object.keys(this._streams), function(id, cb) { 228 | if (wait) { 229 | that._streams[id].on('close', cb); 230 | } else { 231 | that._streams[id].destroy(cb); 232 | } 233 | }, function() { 234 | that._inStream.end(); 235 | if (that._inStream.destroy) { 236 | that._inStream.destroy(); 237 | } 238 | 239 | that._outStream.end(); 240 | if (that._outStream.destroy) { 241 | that._outStream.destroy(); 242 | } 243 | 244 | // consume all awaiting messages 245 | try { 246 | that._inStream.resume(); 247 | } catch(err) {} 248 | 249 | try { 250 | that._outStream.resume(); 251 | } catch(err) {} 252 | }); 253 | 254 | 255 | return this; 256 | }; 257 | 258 | StreamSession.prototype.close = function(cb) { 259 | this.destroy(true, cb); 260 | }; 261 | 262 | module.exports = StreamSession; 263 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jschan", 3 | "description": "node.js port of docker/libchan", 4 | "keywords": [ 5 | "docker", 6 | "libchan", 7 | "streams", 8 | "stream", 9 | "channel", 10 | "channels" 11 | ], 12 | "version": "0.3.0", 13 | "license": "MIT", 14 | "contributors": [ 15 | "Matteo Collina (http://matteocollina.com)", 16 | "Peter Elger (http://peterelger.com/)", 17 | "Adrian Rossouw (http://daemon.co.za)" 18 | ], 19 | "engines": { 20 | "node": "v0.10.x" 21 | }, 22 | "dependencies": { 23 | "async": "^0.9.0", 24 | "inherits": "^2.0.1", 25 | "msgpack5": "^1.2.0", 26 | "readable-stream": "^1.0.27-1", 27 | "reduplexer": "1.0.0", 28 | "through2": "^0.6.1" 29 | }, 30 | "main": "lib/jschan.js", 31 | "scripts": { 32 | "test": "_mocha --recursive test", 33 | "jshint": "jshint --exclude-path .gitignore .", 34 | "test-browser": "zuul --local 8080 -- test/websocket_client.js" 35 | }, 36 | "pre-commit": [ 37 | "jshint", 38 | "test" 39 | ], 40 | "repository": { 41 | "type": "git", 42 | "url": "https://github.com/GraftJS/jschan.git" 43 | }, 44 | "browser": { 45 | "./lib/jschan.js": "./lib/jschan_browser.js" 46 | }, 47 | "devDependencies": { 48 | "concat-stream": "^1.4.6", 49 | "jshint": "^2.5.2", 50 | "mocha": "^1.20.1", 51 | "must": "^0.12.0", 52 | "pre-commit": "0.0.9", 53 | "zuul": "^1.10.1" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/abstract_session.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | var expect = require('must'); 5 | var concat = require('concat-stream'); 6 | var fs = require('fs'); 7 | var Readable = require('readable-stream').Readable; 8 | var Writable = require('readable-stream').Writable; 9 | var Duplex = require('readable-stream').Duplex; 10 | var WritableN = require('stream').Writable; 11 | var DuplexN = require('stream').Duplex; 12 | var Transform = require('readable-stream').Transform; 13 | var through = require('through2'); 14 | 15 | module.exports = function abstractSession(builder) { 16 | 17 | var inSession; 18 | var outSession; 19 | 20 | beforeEach(function buildSessions(done) { 21 | builder(function(err, inS, out) { 22 | if (err) { 23 | return done(err); 24 | } 25 | 26 | inSession = inS; 27 | 28 | outSession = out; 29 | 30 | done(); 31 | }); 32 | }); 33 | 34 | afterEach(function closeOutSession(done) { 35 | outSession.destroy(function() { 36 | // avoid errors 37 | done(); 38 | }); 39 | }); 40 | 41 | afterEach(function closeInSession(done) { 42 | inSession.destroy(function() { 43 | // avoid errors 44 | done(); 45 | }); 46 | }); 47 | 48 | describe('one-direction', function() { 49 | 50 | function client(data) { 51 | var chan = outSession.WriteChannel(); 52 | 53 | data = data || { 54 | hello: 'world' 55 | }; 56 | 57 | chan.write(data); 58 | } 59 | 60 | function reply(done, msg) { 61 | expect(msg).to.eql({ hello: 'world' }); 62 | done(); 63 | } 64 | 65 | it('should receive a message', function(done) { 66 | inSession.on('channel', function server(chan) { 67 | chan.on('data', reply.bind(null, done)); 68 | }); 69 | 70 | client(); 71 | }); 72 | 73 | it('should receive a message if the session closes', function(done) { 74 | var chan = outSession.WriteChannel(); 75 | 76 | inSession.on('channel', function server(chan) { 77 | chan.on('data', reply.bind(null, done)); 78 | }); 79 | 80 | chan.end({ 81 | hello: 'world' 82 | }); 83 | 84 | outSession.close(); 85 | }); 86 | 87 | it('should receive 50 messages', function(done) { 88 | 89 | var chan = outSession.WriteChannel(); 90 | var count = 0; 91 | var max = 50; 92 | var i; 93 | 94 | inSession.on('channel', function server(chan) { 95 | chan.on('data', function() { 96 | count++; 97 | if (count === max) { 98 | done(); 99 | } 100 | }); 101 | }); 102 | 103 | for (i = 0; i < max; i++) { 104 | chan.write({ hello: 'world' }); 105 | } 106 | }); 107 | 108 | it('should receive 50 channels', function(done) { 109 | 110 | var chan; 111 | var count = 0; 112 | var max = 50; 113 | var i; 114 | 115 | inSession.on('channel', function server(chan) { 116 | chan.resume(); 117 | 118 | count++; 119 | if (count === max) { 120 | done(); 121 | } 122 | }); 123 | 124 | for (i = 0; i < max; i++) { 125 | chan = outSession.WriteChannel(); 126 | chan.end({ hello: 'world' }); 127 | } 128 | }); 129 | 130 | it('should be a readChannel server side', function(done) { 131 | inSession.on('channel', function server(chan) { 132 | chan.on('data', function() {}); 133 | expect(chan.isReadChannel).to.be.true(); 134 | expect(chan.isWriteChannel).to.be.false(); 135 | done(); 136 | }); 137 | 138 | client(); 139 | }); 140 | 141 | it('should receive a big message', function(done) { 142 | var i; 143 | var data = []; 144 | 145 | for (i = 0; i < 3000; i++) { 146 | data.push(i); 147 | } 148 | 149 | inSession.on('channel', function server(chan) { 150 | chan.on('data', function(msg) { 151 | expect(msg).to.eql(data); 152 | done(); 153 | }); 154 | }); 155 | 156 | client(data); 157 | }); 158 | 159 | it('should support late channel rande-vouz', function(done) { 160 | client(); 161 | 162 | inSession.on('channel', function server(chan) { 163 | chan.on('data', reply.bind(null, done)); 164 | }); 165 | }); 166 | }); 167 | 168 | describe('basic reply subChannel', function() { 169 | 170 | function client(done) { 171 | var chan = outSession.WriteChannel(); 172 | var ret = chan.ReadChannel(); 173 | 174 | ret.on('data', function(res) { 175 | expect(res).to.eql({ hello: 'world' }); 176 | done(); 177 | }); 178 | 179 | chan.write({ 180 | hello:'world', 181 | returnChannel: ret 182 | }); 183 | } 184 | 185 | function reply(msg) { 186 | var stream = msg.returnChannel; 187 | delete msg.returnChannel; 188 | stream.end(msg); 189 | } 190 | 191 | it('should send and reply', function(done) { 192 | inSession.on('channel', function server(chan) { 193 | chan.on('data', reply); 194 | }); 195 | 196 | client(done); 197 | }); 198 | 199 | it('should support late channel rande-vouz', function(done) { 200 | client(done); 201 | 202 | inSession.on('channel', function server(chan) { 203 | chan.on('data', reply); 204 | }); 205 | }); 206 | 207 | it('should reply with a big chunk', function(done) { 208 | var i; 209 | var data = []; 210 | var chan = outSession.WriteChannel(); 211 | var ret = chan.ReadChannel(); 212 | 213 | for (i = 0; i < 3000; i++) { 214 | data.push(i); 215 | } 216 | 217 | inSession.on('channel', function server(chan) { 218 | chan.on('data', function(msg) { 219 | msg.returnChannel.end(data); 220 | }); 221 | }); 222 | 223 | ret.on('data', function(res) { 224 | expect(res).to.eql(data); 225 | done(); 226 | }); 227 | 228 | chan.write({ 229 | returnChannel: ret 230 | }); 231 | }); 232 | }); 233 | 234 | describe('write subChannel', function() { 235 | 236 | function client() { 237 | var chan = outSession.WriteChannel(); 238 | var more = chan.WriteChannel(); 239 | 240 | chan.write({ 241 | hello: 'world', 242 | more: more 243 | }); 244 | 245 | more.write(1); 246 | more.write(2); 247 | more.end(3); 248 | } 249 | 250 | function reply(done, msg) { 251 | var more = msg.more; 252 | var count = 0; 253 | 254 | delete msg.more; 255 | expect(msg).to.eql({ hello: 'world' }); 256 | 257 | more.on('data', function(msg) { 258 | expect(msg).to.eql(++count); 259 | }); 260 | 261 | more.on('end', done); 262 | } 263 | 264 | it('should receive some more update through the substream', function(done) { 265 | inSession.on('channel', function server(chan) { 266 | chan.on('data', reply.bind(null, done)); 267 | }); 268 | 269 | client(); 270 | }); 271 | 272 | it('should support late channel rande-vouz', function(done) { 273 | client(); 274 | 275 | inSession.on('channel', function server(chan) { 276 | chan.on('data', reply.bind(null, done)); 277 | }); 278 | }); 279 | }); 280 | 281 | describe('binaryStream', function() { 282 | 283 | function client(done) { 284 | var chan = outSession.WriteChannel(); 285 | var bin = chan.ByteStream(); 286 | 287 | chan.write({ 288 | hello: 'world', 289 | bin: bin 290 | }); 291 | 292 | bin.write(new Buffer([1])); 293 | bin.write(new Buffer([2])); 294 | bin.end(new Buffer([3])); 295 | 296 | bin.pipe(concat(function(buf) { 297 | expect(buf.length).to.eql(3); 298 | expect(buf[0]).to.eql(1); 299 | expect(buf[1]).to.eql(2); 300 | expect(buf[2]).to.eql(3); 301 | done(); 302 | })); 303 | } 304 | 305 | function reply(msg) { 306 | var bin = msg.bin; 307 | 308 | delete msg.bin; 309 | expect(msg).to.eql({ hello: 'world' }); 310 | 311 | // echo mode 312 | bin.pipe(bin); 313 | } 314 | 315 | it('should receive some more update through the substream', function(done) { 316 | inSession.on('channel', function server(chan) { 317 | chan.on('data', reply); 318 | }); 319 | 320 | client(done); 321 | }); 322 | 323 | it('should support late channel rande-vouz', function(done) { 324 | client(done); 325 | 326 | inSession.on('channel', function server(chan) { 327 | chan.on('data', reply); 328 | }); 329 | }); 330 | 331 | it('should auto-pipe a node Readable', function(done) { 332 | var chan = outSession.WriteChannel(); 333 | var file = __dirname + '/../package.json'; 334 | var bin = fs.createReadStream(file); 335 | 336 | chan.write({ 337 | bin: bin 338 | }); 339 | 340 | inSession.on('channel', function server(chan) { 341 | chan.on('data', function(msg) { 342 | msg.bin.pipe(concat(function(buf) { 343 | expect(buf.toString()).to.eql(fs.readFileSync(file).toString()); 344 | done(); 345 | })); 346 | }); 347 | }); 348 | }); 349 | 350 | it('should auto-pipe a Readable from readable-stream', function(done) { 351 | var chan = outSession.WriteChannel(); 352 | var bin = new Readable(); 353 | 354 | bin._read = function() { 355 | this.push('hello world'); 356 | this.push(null); 357 | }; 358 | 359 | chan.write({ 360 | bin: bin 361 | }); 362 | 363 | inSession.on('channel', function server(chan) { 364 | chan.on('data', function(msg) { 365 | msg.bin.pipe(concat(function(buf) { 366 | expect(buf.toString()).to.eql('hello world'); 367 | done(); 368 | })); 369 | }); 370 | }); 371 | }); 372 | 373 | it('should auto-pipe a Writable from readable-stream', function(done) { 374 | var chan = outSession.WriteChannel(); 375 | var bin = new Writable(); 376 | 377 | bin._write = function(chunk, enc, done) { 378 | expect(chunk.toString()).to.eql('hello world'); 379 | done(); 380 | }; 381 | 382 | bin.on('finish', done); 383 | 384 | chan.write({ 385 | bin: bin 386 | }); 387 | 388 | inSession.on('channel', function server(chan) { 389 | chan.on('data', function(msg) { 390 | msg.bin.end('hello world'); 391 | }); 392 | }); 393 | }); 394 | 395 | it('should auto-pipe a Duplex from readable-stream', function(done) { 396 | var chan = outSession.WriteChannel(); 397 | var bin = new Duplex(); 398 | 399 | bin._read = function() { 400 | this.push('hello world'); 401 | this.push(null); 402 | }; 403 | 404 | bin._write = function(chunk, enc, done) { 405 | expect(chunk.toString()).to.eql('hello world'); 406 | done(); 407 | }; 408 | 409 | bin.on('finish', done); 410 | 411 | chan.write({ 412 | bin: bin 413 | }); 414 | 415 | inSession.on('channel', function server(chan) { 416 | chan.on('data', function(msg) { 417 | msg.bin.pipe(msg.bin); 418 | }); 419 | }); 420 | }); 421 | 422 | it('should auto-pipe a node Writable', function(done) { 423 | var chan = outSession.WriteChannel(); 424 | var bin = new WritableN(); 425 | 426 | bin._write = function(chunk, enc, done) { 427 | expect(chunk.toString()).to.eql('hello world'); 428 | done(); 429 | }; 430 | 431 | bin.on('finish', done); 432 | 433 | chan.write({ 434 | bin: bin 435 | }); 436 | 437 | inSession.on('channel', function server(chan) { 438 | chan.on('data', function(msg) { 439 | msg.bin.end('hello world'); 440 | }); 441 | }); 442 | }); 443 | 444 | it('should auto-pipe a node core Duplex', function(done) { 445 | var chan = outSession.WriteChannel(); 446 | var bin = new DuplexN(); 447 | 448 | bin._read = function() { 449 | this.push('hello world'); 450 | this.push(null); 451 | }; 452 | 453 | bin._write = function(chunk, enc, done) { 454 | expect(chunk.toString()).to.eql('hello world'); 455 | done(); 456 | }; 457 | 458 | bin.on('finish', done); 459 | 460 | chan.write({ 461 | bin: bin 462 | }); 463 | 464 | inSession.on('channel', function server(chan) { 465 | chan.on('data', function(msg) { 466 | msg.bin.pipe(msg.bin); 467 | }); 468 | }); 469 | }); 470 | 471 | it('should error if the stream is a Transform', function(done) { 472 | var chan = outSession.WriteChannel(); 473 | var bin = new Transform(); 474 | 475 | outSession.once('error', function(err) { 476 | expect(err.message).to.eql('unable to auto-serialize a Transform stream not in object mode'); 477 | done(); 478 | 479 | outSession.on('error', function() {}); 480 | }); 481 | 482 | chan.end({ 483 | bin: bin 484 | }); 485 | 486 | inSession.on('channel', function server(chan) { 487 | chan.resume(); // skip all of it 488 | }); 489 | }); 490 | }); 491 | 492 | describe('double nested channels', function() { 493 | 494 | it('should support receiving a ReadChannel through a ReadChannel', function(done) { 495 | 496 | var chan = outSession.WriteChannel(); 497 | var ret = chan.ReadChannel(); 498 | 499 | ret.on('data', function(res) { 500 | res.nested.on('data', function(msg) { 501 | expect(msg).to.eql({ some: 'stuff' }); 502 | done(); 503 | }); 504 | }); 505 | 506 | chan.write({ 507 | hello:'world', 508 | returnChannel: ret 509 | }); 510 | 511 | inSession.on('channel', function server(chan) { 512 | chan.on('data', function(msg) { 513 | var ret = msg.returnChannel; 514 | var nested = chan.WriteChannel(); 515 | 516 | ret.write({ nested: nested }); 517 | 518 | nested.write({ some: 'stuff' }); 519 | }); 520 | }); 521 | }); 522 | 523 | it('should auto-convert already piped transform streams in object mode into channels', function(done) { 524 | 525 | var chan = outSession.WriteChannel(); 526 | var ret = through.obj(); 527 | 528 | ret.pipe(through.obj()).on('data', function(res) { 529 | res.nested.on('data', function(msg) { 530 | expect(msg).to.eql({ some: 'stuff' }); 531 | done(); 532 | }); 533 | }); 534 | 535 | chan.write({ 536 | hello:'world', 537 | returnChannel: ret 538 | }); 539 | 540 | inSession.on('channel', function server(chan) { 541 | chan.on('data', function(msg) { 542 | var ret = msg.returnChannel; 543 | var nested = through.obj(); 544 | var driver = through.obj(); 545 | 546 | driver.pipe(nested); 547 | 548 | ret.write({ nested: nested }); 549 | 550 | driver.write({ some: 'stuff' }); 551 | }); 552 | }); 553 | }); 554 | 555 | it('should auto-convert a Readable stream', function(done) { 556 | var chan = outSession.WriteChannel(); 557 | var more = new Readable({ objectMode: true }); 558 | 559 | more._read = function() { 560 | this.push({ 'hello': 'world' }); 561 | this.push(null); 562 | }; 563 | 564 | chan.write({ 565 | hello:'world', 566 | more: more 567 | }); 568 | 569 | inSession.on('channel', function server(chan) { 570 | chan.on('data', function(msg) { 571 | msg.more.on('data', function(data) { 572 | expect(data).to.eql({ 'hello': 'world' }); 573 | done(); 574 | }); 575 | }); 576 | }); 577 | }); 578 | 579 | it('should auto-convert a Writable stream', function(done) { 580 | var chan = outSession.WriteChannel(); 581 | var more = new Writable({ objectMode: true }); 582 | 583 | more._write = function(data, enc, cb) { 584 | expect(data).to.eql({ 'hello': 'world' }); 585 | cb(); 586 | done(); 587 | }; 588 | 589 | 590 | chan.write({ 591 | hello:'world', 592 | more: more 593 | }); 594 | 595 | inSession.on('channel', function server(chan) { 596 | chan.on('data', function(msg) { 597 | msg.more.end({ 'hello': 'world' }); 598 | }); 599 | }); 600 | }); 601 | 602 | it('should support receiving a WriteChannel through a ReadChannel', function(done) { 603 | 604 | var chan = outSession.WriteChannel(); 605 | var ret = chan.ReadChannel(); 606 | 607 | ret.on('data', function(res) { 608 | res.nested.end({ some: 'stuff' }); 609 | }); 610 | 611 | chan.write({ 612 | hello:'world', 613 | returnChannel: ret 614 | }); 615 | 616 | inSession.on('channel', function server(chan) { 617 | chan.on('data', function(msg) { 618 | var ret = msg.returnChannel; 619 | var nested = chan.ReadChannel(); 620 | 621 | ret.end({ nested: nested }); 622 | 623 | nested.on('data', function(data) { 624 | expect(data).to.eql({ some: 'stuff' }); 625 | done(); 626 | }); 627 | }); 628 | }); 629 | }); 630 | 631 | it('should support receiving a byte stream through a ReadChannel', function(done) { 632 | 633 | var chan = outSession.WriteChannel(); 634 | var ret = chan.ReadChannel(); 635 | 636 | ret.on('data', function(res) { 637 | res.bin.pipe(concat(function(buf) { 638 | expect(buf.length).to.eql(3); 639 | expect(buf[0]).to.eql(1); 640 | expect(buf[1]).to.eql(2); 641 | expect(buf[2]).to.eql(3); 642 | done(); 643 | })); 644 | }); 645 | 646 | chan.write({ 647 | hello: 'world', 648 | returnChannel: ret 649 | }); 650 | 651 | inSession.on('channel', function server(chan) { 652 | chan.on('data', function(msg) { 653 | var ret = msg.returnChannel; 654 | var bin = chan.ByteStream(); 655 | 656 | ret.write({ bin: bin }); 657 | 658 | bin.write(new Buffer([1])); 659 | bin.write(new Buffer([2])); 660 | bin.write(new Buffer([3])); 661 | bin.end(); 662 | }); 663 | }); 664 | }); 665 | }); 666 | 667 | describe('close event', function() { 668 | it('must be emitted by the server session', function(done) { 669 | inSession.once('close', done); 670 | inSession.close(); 671 | }); 672 | 673 | it('must be emitted by the client session', function(done) { 674 | outSession.once('close', done); 675 | outSession.close(); 676 | }); 677 | }); 678 | 679 | describe('orchestration', function() { 680 | 681 | var inSession2; 682 | var outSession2; 683 | 684 | beforeEach(function buildSessions2(done) { 685 | builder(function(err, inS, out) { 686 | if (err) { 687 | return done(err); 688 | } 689 | 690 | inSession2 = inS; 691 | 692 | outSession2 = out; 693 | 694 | done(); 695 | }); 696 | }); 697 | 698 | afterEach(function closeOutSession2(done) { 699 | outSession2.destroy(function() { 700 | // avoid errors 701 | done(); 702 | }); 703 | }); 704 | 705 | afterEach(function closeInSession2(done) { 706 | inSession2.destroy(function() { 707 | // avoid errors 708 | done(); 709 | }); 710 | }); 711 | 712 | it('should pass ReadChannel between sessions', function(done) { 713 | (function client1() { 714 | var chan = outSession.WriteChannel(); 715 | var ret = chan.ReadChannel(); 716 | 717 | ret.on('data', function(data) { 718 | data.chan.end({ hello: 'world' }); 719 | }); 720 | 721 | chan.write({ ret: ret }); 722 | })(); 723 | 724 | function client2() { 725 | var chan = outSession2.WriteChannel(); 726 | var ret = chan.ReadChannel(); 727 | 728 | ret.on('data', function(data) { 729 | expect(data).to.eql({ hello: 'world' }); 730 | done(); 731 | }); 732 | 733 | chan.write({ chan: ret }); 734 | } 735 | 736 | (function server() { 737 | inSession.once('channel', function(channel1) { 738 | client2(); 739 | channel1.on('data', function(msg) { 740 | inSession2.once('channel', function(channel2) { 741 | channel2.pipe(msg.ret); 742 | }); 743 | }); 744 | }); 745 | })(); 746 | }); 747 | 748 | it('should pass WriteChannel between sessions', function(done) { 749 | (function client1() { 750 | var chan = outSession.WriteChannel(); 751 | var more = chan.WriteChannel(); 752 | 753 | chan.write({ more: more }); 754 | more.write({ hello: 'world' }); 755 | })(); 756 | 757 | function client2() { 758 | var chan = outSession2.WriteChannel(); 759 | var ret = chan.ReadChannel(); 760 | 761 | ret.on('data', function(msg) { 762 | msg.more.on('data', function(data) { 763 | expect(data).to.eql({ hello: 'world' }); 764 | done(); 765 | }); 766 | }); 767 | 768 | chan.write({ ret: ret }); 769 | } 770 | 771 | (function server() { 772 | inSession.once('channel', function(channel1) { 773 | client2(); 774 | inSession2.once('channel', function(channel2) { 775 | channel2.on('data', function(msg) { 776 | channel1.pipe(msg.ret); 777 | }); 778 | }); 779 | }); 780 | })(); 781 | }); 782 | 783 | it('should pass BinaryStream between sessions', function(done) { 784 | var file = __dirname + '/../package.json'; 785 | 786 | (function client1() { 787 | var chan = outSession.WriteChannel(); 788 | var bin = fs.createReadStream(file); 789 | 790 | chan.write({ 791 | bin: bin 792 | }); 793 | })(); 794 | 795 | function client2() { 796 | var chan = outSession2.WriteChannel(); 797 | var ret = chan.ReadChannel(); 798 | 799 | ret.on('data', function(msg) { 800 | msg.bin.pipe(concat(function(buf) { 801 | expect(buf.toString()).to.eql(fs.readFileSync(file).toString()); 802 | done(); 803 | })); 804 | }); 805 | 806 | chan.write({ ret: ret }); 807 | } 808 | 809 | (function server() { 810 | inSession.once('channel', function(channel1) { 811 | client2(); 812 | inSession2.once('channel', function(channel2) { 813 | channel2.on('data', function(msg) { 814 | channel1.pipe(msg.ret); 815 | }); 816 | }); 817 | }); 818 | })(); 819 | }); 820 | }); 821 | }; 822 | -------------------------------------------------------------------------------- /test/memory.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | var jschan = require('../lib/jschan'); 5 | var abstractSession = require('./abstract_session'); 6 | 7 | describe('memory session', function() { 8 | abstractSession(function(cb) { 9 | var session = jschan.memorySession(); 10 | cb(null, session, session); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /test/stream.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | var jschan = require('../lib/jschan'); 5 | var abstractSession = require('./abstract_session'); 6 | var PassThrough = require('readable-stream').PassThrough; 7 | 8 | describe('stream session with standard mode', function() { 9 | abstractSession(function(cb) { 10 | var inStream = new PassThrough(); 11 | var outStream = new PassThrough(); 12 | var inSession = jschan.streamSession(inStream, outStream, { server: true }); 13 | var outSession = jschan.streamSession(outStream, inStream); 14 | cb(null, inSession, outSession); 15 | }); 16 | }); 17 | 18 | describe('stream session with object mode and no headers', function() { 19 | abstractSession(function(cb) { 20 | var inStream = new PassThrough({ objectMode: true }); 21 | var outStream = new PassThrough({ objectMode: true }); 22 | var inSession = jschan.streamSession(inStream, outStream, { server: true, header: false }); 23 | var outSession = jschan.streamSession(outStream, inStream, { header: false }); 24 | cb(null, inSession, outSession); 25 | }); 26 | }); 27 | --------------------------------------------------------------------------------