├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── benchmarks ├── bench.js └── tentacoli_echo.js ├── echo-server.js ├── example.js ├── examples ├── echo-client.js └── echo.html ├── package.json ├── schema.proto ├── tentacoli.js ├── test-browser.js ├── test.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | profile* 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | - "6" 5 | - "7" 6 | - "8" 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Matteo Collina 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tentacoli   [![Build Status](https://travis-ci.org/mcollina/tentacoli.png)](https://travis-ci.org/mcollina/tentacoli) 2 | 3 | Multiplexing requests and streams over a single connection since 2015. 4 | 5 | * [Install](#install) 6 | * [Example](#example) 7 | * [API](#api) 8 | * [TODO](#todo) 9 | * [Acknowledgements](#acknowledgements) 10 | * [License](#license) 11 | 12 | 13 | ## Install 14 | 15 | ``` 16 | npm i tentacoli --save 17 | ``` 18 | 19 | 20 | ## Example 21 | 22 | ```js 23 | 'use strict' 24 | 25 | var tentacoli = require('./') 26 | var net = require('net') 27 | var from = require('from2') 28 | var through = require('through2') 29 | var pump = require('pump') 30 | 31 | var server = net.createServer(function (original) { 32 | var stream = tentacoli() 33 | pump(stream, original, stream) 34 | 35 | stream.on('request', handle) 36 | }) 37 | 38 | function handle (req, reply) { 39 | console.log('--> request is', req.cmd) 40 | reply(null, { 41 | data: 'some data', 42 | streams: { 43 | echo: req.streams.inStream.pipe(through.obj()) 44 | } 45 | }) 46 | } 47 | 48 | server.listen(4200, function () { 49 | var original = net.connect(4200) 50 | var instance = tentacoli() 51 | pump(original, instance, original) 52 | 53 | instance.request({ 54 | cmd: 'a request', 55 | streams: { 56 | inStream: from.obj(['hello', 'world']) 57 | } 58 | }, function (err, result) { 59 | if (err) { 60 | throw err 61 | } 62 | 63 | console.log('--> result is', result.data) 64 | console.log('--> stream data:') 65 | 66 | result.streams.echo.pipe(through.obj(function (chunk, enc, cb) { 67 | cb(null, chunk + '\n') 68 | })).pipe(process.stdout) 69 | result.streams.echo.on('end', function () { 70 | console.log('--> ended') 71 | instance.destroy() 72 | server.close() 73 | }) 74 | }) 75 | }) 76 | ``` 77 | 78 | Yes, it is long. 79 | 80 | 81 | ## API 82 | 83 | * tentacoli() 84 | * instance.request() 85 | * instance.fire() 86 | * instance.on('request', cb) 87 | 88 | ------------------------------------------------------- 89 | 90 | ### tentacoli([opts]) 91 | 92 | Creates a new instance of Tentacoli, which is a 93 | [Duplex](https://nodejs.org/api/stream.html#stream_class_stream_duplex) 94 | stream and inherits from [multiplex](http://npm.im/multiplex) 95 | 96 | It accepts the following option: 97 | 98 | * `codec`: an object with a `encode` and `decode` method, which will 99 | be used to encode messages. Valid encoding libraries are 100 | [protocol-buffers](http://npm.im/protocol-buffers) and 101 | [msgpack5](http://npm.im/msgpack5). The default one is JSON. 102 | This capability is provided by 103 | [net-object-stream](http://npm.im/net-object-stream). 104 | * `maxInflight`: max number of concurrent requests in 105 | flight at any given moment. 106 | 107 | ------------------------------------------------------- 108 | 109 | ### instance.request(message, callback(err, res)) 110 | 111 | Sends a request to the remote peer. 112 | 113 | * `message` is a standard JS object, but all streams contained in its 114 | `streams` property will be multiplexed and forwarded to the other 115 | peer. 116 | * `callback` will be called if an error occurred or a response is 117 | available. The `res.streams` property will contain all streams 118 | passed by the other peer. 119 | 120 | ------------------------------------------------------- 121 | 122 | ### instance.fire(message, callback(err)) 123 | 124 | Sends a *fire and forget* request to the remote peer. 125 | 126 | * `message` is a standard JS object, but all streams contained in its 127 | `streams` property will be multiplexed and forwarded to the other 128 | peer. 129 | * `callback` will be called if there is an error while sending the message, or after the message has been sent successfully. 130 | 131 | ------------------------------------------------------- 132 | 133 | ### instance.on('request', callback(req, reply)) 134 | 135 | The `'request'` event is emitted when there is an incoming request. 136 | 137 | * `req` is the standard JS object coming from [`request`](#request), 138 | and all the streams contained in its 139 | `streams` property will have been multiplexed and forwarded from 140 | the other peer. 141 | * `reply` is the function to send a reply to the other peer, and it 142 | follows the standard node callback pattern: `reply(err, res).` 143 | The `res.streams` property should contain all the streams 144 | that need to be forwarded to the other peer. 145 | 146 | 147 | ## TODO 148 | 149 | * [ ] battle test it, you can definitely help! I am particularly 150 | concerned about error handling, I do not want tentacoli to crash 151 | your process. 152 | * [ ] figure out how to handle reconnects. 153 | * [x] provide examples, with WebSockets (via 154 | [websocket-stream](http://npm.im/websocket-stream) net, SSL, etc.. 155 | * [ ] provide an example where a request is forwarded sender -> router 156 | -> receiver. With streams! 157 | * [ ] tentacoli needs a microservice framework as its companion, but it 158 | is framework agnostic. We should build a 159 | [seneca](http://npm.im/seneca) transport and probably something more 160 | lean too. 161 | 162 | ## In the Browser 163 | 164 | You will use [websocket-stream](http://npm.im/websocket-stream) to 165 | wire tentacoli to the websocket. 166 | 167 | On the server: 168 | ```js 169 | 'use strict' 170 | 171 | var http = require('http') 172 | var tentacoli = require('./') 173 | var pump = require('pump') 174 | var websocket = require('websocket-stream') 175 | var server = http.createServer(serve) 176 | 177 | websocket.createServer({ 178 | server: server 179 | }, handle) 180 | 181 | function handle (sock) { 182 | var receiver = tentacoli() 183 | pump(sock, receiver, sock) 184 | receiver.on('request', function request (req, reply) { 185 | // just echo 186 | reply(null, req) 187 | }) 188 | } 189 | 190 | server.listen(3000, function (err) { 191 | if (err) throw err 192 | console.error('listening on', server.address().port) 193 | }) 194 | ``` 195 | 196 | On the client: 197 | ```js 198 | 'use strict' 199 | 200 | var tentacoli = require('../') 201 | var ws = require('websocket-stream') 202 | var pump = require('pump') 203 | var from = require('from2') 204 | 205 | var URL = require('url') 206 | var serverOpts = URL.parse(document.URL) 207 | serverOpts.path = undefined 208 | serverOpts.pathname = undefined 209 | serverOpts.protocol = 'ws' 210 | var server = URL.format(serverOpts) 211 | 212 | var stream = ws(server) 213 | var instance = tentacoli() 214 | 215 | pump(stream, instance, stream) 216 | 217 | instance.request({ 218 | streams: { 219 | inStream: from.obj(['hello', 'world']) 220 | } 221 | }, function (err, data) { 222 | if (err) throw err 223 | 224 | var res = data.streams.inStream 225 | res.on('data', function (chunk) { 226 | console.log(chunk) 227 | }) 228 | }) 229 | ``` 230 | 231 | ### with Browserify 232 | 233 | [Browserify](http://npm.im/browserify) offers a way of packaging up this 234 | module for front-end usage. You will just need to install/specify the 235 | [brfs](http://npm.im/brfs) transform. 236 | 237 | As an example: 238 | 239 | ``` 240 | browserify -t brfs tentacoli.js > bundle.js 241 | ``` 242 | 243 | ### with WebPack 244 | 245 | [WebPack](http://npm.im/webpack) offers the more popular way of packaging 246 | up node modules for browser usage. You will just need to install/specify the 247 | [brfs](http://npm.im/brfs) transform. 248 | 249 | You should install webpack, 250 | [transform-loader](http://npm.im/transform-loader) and [brfs](http://npm.im/brfs): 251 | 252 | ``` 253 | npm i webpack transform-loader brfs websocket-stream --save 254 | ``` 255 | 256 | Then, set this as your webpack configuration: 257 | 258 | ``` 259 | 'use strict' 260 | 261 | module.exports = { 262 | module: { 263 | postLoaders: [{ 264 | loader: "transform?brfs" 265 | }] 266 | } 267 | } 268 | ``` 269 | 270 | To build: 271 | ``` 272 | webpack --config webpack.config.js yourfile.js build.js 273 | ``` 274 | 275 | 276 | ## Acknowledgements 277 | 278 | This library would not be possible without the great work of 279 | [@mafintosh](http://gitub.com/mafintosh), 280 | [@substack](http://github.com/substack) and 281 | [@maxodgen](http://github.com/maxodgen). This library is fully based on 282 | their work, look at package.json! 283 | 284 | Another great source of inspriation was [jschan](http://npm.im/jschan) 285 | from which I borrowed a lot of ideas. Thanks [Adrian 286 | Roussow](https://github.com/AdrianRossouw) for all the discussions 287 | around microservices, streams and channels. 288 | 289 | Many thanks to [@mcdonnelldean](http://github.com/mcdonnelldean) for 290 | providing an excuse to write this random idea out. 291 | 292 | This project is kindly sponsored by [nearForm](http://nearform.com). 293 | 294 | 295 | ## License 296 | 297 | MIT 298 | -------------------------------------------------------------------------------- /benchmarks/bench.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var minimist = require('minimist') 4 | var bench = require('fastbench') 5 | var pump = require('pump') 6 | var net = require('net') 7 | var childProcess = require('child_process') 8 | var path = require('path') 9 | var parallel = require('fastparallel')({ 10 | results: false 11 | }) 12 | var tentacoli = require('../') 13 | 14 | var argv = minimist(process.argv.slice(2), { 15 | boolean: 'child', 16 | default: { 17 | child: true, 18 | port: 3000, 19 | host: 'localhost' 20 | } 21 | }) 22 | 23 | function buildPingPong (cb) { 24 | var sender = tentacoli() 25 | var timer = setTimeout(function () { 26 | throw new Error('unable to start child') 27 | }, 1000) 28 | var child 29 | 30 | if (argv.child) { 31 | child = childProcess.fork(path.join(__dirname, 'tentacoli_echo.js'), { 32 | stdio: 'inherit' 33 | }) 34 | 35 | child.on('message', start) 36 | 37 | child.on('error', cb) 38 | 39 | child.on('exit', console.log) 40 | } else { 41 | start(argv) 42 | } 43 | 44 | function start (addr) { 45 | var client = net.connect(addr.port, addr.host) 46 | 47 | client.on('connect', function () { 48 | cb(null, benchPingPong) 49 | clearTimeout(timer) 50 | }) 51 | 52 | pump(client, sender, client) 53 | } 54 | 55 | var max = 1000 56 | var functions = new Array(max) 57 | 58 | for (var i = 0; i < max; i++) { 59 | functions[i] = sendEcho 60 | } 61 | 62 | function benchPingPong (cb) { 63 | parallel(null, functions, null, cb) 64 | } 65 | 66 | function sendEcho (cb) { 67 | sender.request({ 68 | cmd: 'ping' 69 | }, function () { 70 | cb() 71 | }) 72 | } 73 | } 74 | 75 | buildPingPong(function (err, benchPingPong) { 76 | if (err) throw err 77 | 78 | var run = bench([benchPingPong], 100) 79 | 80 | run(function (err) { 81 | if (err) throw err 82 | 83 | run(function (err) { 84 | if (err) throw err 85 | 86 | // close the sockets the bad way 87 | process.exit(0) 88 | }) 89 | }) 90 | }) 91 | -------------------------------------------------------------------------------- /benchmarks/tentacoli_echo.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var minimist = require('minimist') 4 | var net = require('net') 5 | var tentacoli = require('../') 6 | var pump = require('pump') 7 | var server = net.createServer(handle) 8 | var count = 0 9 | var port 10 | 11 | var argv = minimist(process.argv.slice(2), { 12 | boolean: 'child', 13 | default: { 14 | child: true, 15 | port: 3000, 16 | host: 'localhost' 17 | } 18 | }) 19 | 20 | if (argv.child) { 21 | port = 0 22 | } else { 23 | port = 3000 24 | } 25 | 26 | function handle (sock) { 27 | var receiver = tentacoli() 28 | pump(sock, receiver, sock) 29 | receiver.on('request', function request (req, reply) { 30 | count++ 31 | reply(null, req) 32 | }) 33 | } 34 | 35 | server.listen(port, function (err) { 36 | if (err) throw err 37 | 38 | if (argv.child) { 39 | process.send(server.address()) 40 | } else { 41 | console.error('listening on', server.address().port) 42 | } 43 | }) 44 | 45 | process.on('disconnect', function () { 46 | process.exit(0) 47 | }) 48 | 49 | var signal = 'SIGINT' 50 | 51 | // Cleanly shut down process on SIGTERM to ensure that perf-.map gets flushed 52 | process.on(signal, onSignal) 53 | 54 | function onSignal () { 55 | console.error('count', count) 56 | // IMPORTANT to log on stderr, to not clutter stdout which is purely for data, i.e. dtrace stacks 57 | console.error('Caught', signal, ', shutting down.') 58 | server.close() 59 | process.exit(0) 60 | } 61 | -------------------------------------------------------------------------------- /echo-server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var minimist = require('minimist') 4 | var http = require('http') 5 | var tentacoli = require('./') 6 | var pump = require('pump') 7 | var fs = require('fs') 8 | var p = require('path') 9 | var websocket = require('websocket-stream') 10 | var server = http.createServer(serve) 11 | 12 | websocket.createServer({ 13 | server: server 14 | }, handle) 15 | 16 | var argv = minimist(process.argv.slice(2), { 17 | default: { 18 | port: process.env.ZUUL_PORT || 3000, 19 | host: 'localhost' 20 | } 21 | }) 22 | 23 | function handle (sock) { 24 | var receiver = tentacoli() 25 | pump(sock, receiver, sock) 26 | receiver.on('request', function request (req, reply) { 27 | reply(null, req) 28 | }) 29 | } 30 | 31 | function serve (req, res) { 32 | if (req.url === '/') { 33 | req.url = '/echo.html' 34 | } 35 | var path = p.join(__dirname, 'examples', req.url.replace('/', '')) 36 | pump( 37 | fs.createReadStream(path), 38 | res 39 | ) 40 | } 41 | 42 | server.listen(argv.port, function (err) { 43 | if (err) throw err 44 | console.error('listening on', server.address().port) 45 | }) 46 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var tentacoli = require('./') 4 | var net = require('net') 5 | var from = require('from2') 6 | var through = require('through2') 7 | var pump = require('pump') 8 | 9 | var server = net.createServer(function (original) { 10 | var stream = tentacoli() 11 | pump(stream, original, stream) 12 | 13 | stream.on('request', handle) 14 | }) 15 | 16 | function handle (req, reply) { 17 | console.log('--> request is', req.cmd) 18 | reply(null, { 19 | data: 'some data', 20 | streams: { 21 | echo: req.streams.inStream.pipe(through.obj()) 22 | } 23 | }) 24 | } 25 | 26 | server.listen(4200, function () { 27 | var original = net.connect(4200) 28 | var instance = tentacoli() 29 | pump(original, instance, original) 30 | 31 | instance.request({ 32 | cmd: 'a request', 33 | streams: { 34 | inStream: from.obj(['hello', 'world']) 35 | } 36 | }, function (err, result) { 37 | if (err) { 38 | throw err 39 | } 40 | 41 | console.log('--> result is', result.data) 42 | console.log('--> stream data:') 43 | 44 | result.streams.echo.pipe(through.obj(function (chunk, enc, cb) { 45 | cb(null, chunk + '\n') 46 | })).pipe(process.stdout) 47 | result.streams.echo.on('end', function () { 48 | console.log('--> ended') 49 | instance.destroy() 50 | server.close() 51 | }) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /examples/echo-client.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var tentacoli = require('../') 4 | var ws = require('websocket-stream') 5 | var pump = require('pump') 6 | var from = require('from2') 7 | 8 | var URL = require('url') 9 | var serverOpts = URL.parse(document.URL) 10 | serverOpts.path = undefined 11 | serverOpts.pathname = undefined 12 | serverOpts.protocol = 'ws' 13 | var server = URL.format(serverOpts) 14 | 15 | var stream = ws(server) 16 | var instance = tentacoli() 17 | 18 | pump(stream, instance, stream) 19 | 20 | instance.request({ 21 | streams$: { 22 | inStream: from.obj(['hello', 'world']) 23 | } 24 | }, function (err, data) { 25 | if (err) throw err 26 | 27 | var res = data.streams$.inStream 28 | res.on('data', function (chunk) { 29 | console.log(chunk) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /examples/echo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tentacoli", 3 | "version": "1.0.0", 4 | "description": "All the ways for doing requests/streams multiplexing over a single stream", 5 | "main": "tentacoli.js", 6 | "scripts": { 7 | "browser": "webpack --config webpack.config.js examples/echo-client.js examples/build.js && node echo-server.js", 8 | "browser-test": "zuul --server echo-server.js --local 8080 --ui tape -- test.js test-browser.js", 9 | "test": "standard && tape test.js | faucet", 10 | "clean": "rm examples/build.js &> /dev/null || echo nothing to clean" 11 | }, 12 | "precommit": [ 13 | "clean", 14 | "test" 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/mcollina/tentacoli.git" 19 | }, 20 | "keywords": [ 21 | "multiplexing", 22 | "multiplex", 23 | "multi", 24 | "request", 25 | "stream" 26 | ], 27 | "author": "Matteo Collina ", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/mcollina/tentacoli/issues" 31 | }, 32 | "homepage": "https://github.com/mcollina/tentacoli#readme", 33 | "devDependencies": { 34 | "brfs": "^1.4.3", 35 | "browserify": "^14.4.0", 36 | "callback-stream": "^1.1.0", 37 | "fastbench": "^1.0.1", 38 | "fastparallel": "^2.3.0", 39 | "faucet": "0.0.1", 40 | "from2": "^2.3.0", 41 | "minimist": "^1.2.0", 42 | "msgpack5": "^3.5.0", 43 | "pre-commit": "^1.2.2", 44 | "safe-buffer": "^5.1.1", 45 | "standard": "^10.0.3", 46 | "tape": "^4.8.0", 47 | "through2": "^2.0.3", 48 | "transform-loader": "^0.2.4", 49 | "webpack": "^3.5.5", 50 | "websocket-stream": "^5.0.1", 51 | "zuul": "^3.11.1" 52 | }, 53 | "dependencies": { 54 | "fastq": "^1.5.0", 55 | "inherits": "^2.0.3", 56 | "multiplex": "^6.7.0", 57 | "net-object-stream": "^2.1.0", 58 | "protocol-buffers": "^3.2.1", 59 | "pump": "^1.0.2", 60 | "readable-stream": "^2.3.3", 61 | "reusify": "^1.0.2", 62 | "shallow-copy": "0.0.1", 63 | "uuid": "^3.1.0" 64 | }, 65 | "browserify": { 66 | "transform": [ 67 | "brfs" 68 | ] 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /schema.proto: -------------------------------------------------------------------------------- 1 | 2 | message Message { 3 | optional string id = 1; 4 | optional string error = 2; 5 | optional bytes data = 3; 6 | repeated Stream streams = 4; 7 | optional bool fire = 5; 8 | 9 | message Stream { 10 | optional string id = 1; 11 | optional string name = 2; 12 | optional bool objectMode = 3; 13 | optional StreamType type = 4; 14 | } 15 | } 16 | 17 | enum StreamType { 18 | Readable = 1; 19 | Writable = 2; 20 | Duplex = 3; 21 | } 22 | -------------------------------------------------------------------------------- /tentacoli.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var inherits = require('inherits') 4 | var protobuf = require('protocol-buffers') 5 | var fs = require('fs') 6 | var path = require('path') 7 | var schema = fs.readFileSync(path.join(__dirname, 'schema.proto'), 'utf8') 8 | var messages = protobuf(schema) 9 | var Multiplex = require('multiplex') 10 | var nos = require('net-object-stream') 11 | var copy = require('shallow-copy') 12 | var pump = require('pump') 13 | var reusify = require('reusify') 14 | var fastq = require('fastq') 15 | var streamRegexp = /stream-[\d]+/ 16 | var messageCodec = { 17 | codec: messages.Message 18 | } 19 | 20 | function Tentacoli (opts) { 21 | if (!(this instanceof Tentacoli)) { 22 | return new Tentacoli(opts) 23 | } 24 | 25 | this._requests = {} 26 | this._opts = opts || {} 27 | this._waiting = {} 28 | this._replyPool = reusify(Reply) 29 | this._nextId = 0 30 | 31 | this._opts.codec = this._opts.codec || { 32 | encode: JSON.stringify, 33 | decode: JSON.parse 34 | } 35 | // TODO clean up waiting streams that are left there 36 | 37 | var that = this 38 | 39 | var qIn = this._qIn = fastq(workIn, that._opts.maxInflight || 100) 40 | 41 | function workIn (msg, cb) { 42 | msg.callback = cb 43 | that.emit('request', msg.toCall, msg.func) 44 | } 45 | 46 | Multiplex.call(this, function newStream (stream, id) { 47 | if (id.match(streamRegexp)) { 48 | this._waiting[id] = stream 49 | return 50 | } 51 | 52 | var parser = nos.parser(messageCodec) 53 | 54 | parser.on('message', function (decoded) { 55 | var response = new Response(decoded.id) 56 | var toCall = that._opts.codec.decode(decoded.data) 57 | unwrapStreams(that, toCall, decoded) 58 | 59 | var reply = that._replyPool.get() 60 | 61 | reply.toCall = toCall 62 | reply.stream = stream 63 | reply.response = response 64 | reply.isFire = !!decoded.fire 65 | qIn.push(reply, noop) 66 | }) 67 | 68 | stream.on('readable', parseInBatch) 69 | 70 | function parseInBatch () { 71 | var data = stream.read(null) 72 | parser.parse(data) 73 | } 74 | }) 75 | 76 | var main = this._main = this.createStream(null) 77 | 78 | var parser = this._parser = nos.parser(messageCodec) 79 | 80 | this._parser.on('message', function (msg) { 81 | var req = that._requests[msg.id] 82 | var err = null 83 | var data = null 84 | 85 | delete that._requests[msg.id] 86 | 87 | if (msg.error) { 88 | err = new Error(msg.error) 89 | } else if (msg.data) { 90 | data = that._opts.codec.decode(msg.data) 91 | unwrapStreams(that, data, msg) 92 | } 93 | 94 | req.callback(err, data) 95 | }) 96 | 97 | this._main.on('readable', parseBatch) 98 | 99 | function parseBatch (err) { 100 | if (err) { 101 | that.emit('error', err) 102 | return 103 | } 104 | parser.parse(main.read(null)) 105 | } 106 | 107 | this._main.on('error', this.emit.bind(this, 'error')) 108 | this._parser.on('error', this.emit.bind(this, 'error')) 109 | 110 | this.on('close', closer) 111 | this.on('finish', closer) 112 | 113 | function closer () { 114 | Object.keys(this._requests).forEach(function (reqId) { 115 | this._requests[reqId].callback(new Error('connection closed')) 116 | delete this._requests[reqId] 117 | }, this) 118 | } 119 | 120 | var self = this 121 | function Reply () { 122 | this.response = null 123 | this.stream = null 124 | this.callback = noop 125 | this.toCall = null 126 | this.isFire = null 127 | 128 | var that = this 129 | 130 | this.func = function reply (err, result) { 131 | if (that.isFire) { 132 | if (result && result.streams) { 133 | if (result.streams.destroy) result.streams.destroy() 134 | if (result.streams.end) result.streams.end() 135 | } 136 | } else { 137 | if (err) { 138 | self.emit('responseError', err) 139 | that.response.error = err.message 140 | } else { 141 | wrapStreams(self, result, that.response, false) 142 | } 143 | nos.writeToStream(that.response, messageCodec, that.stream) 144 | } 145 | var cb = that.callback 146 | that.response = null 147 | that.stream = null 148 | that.callback = noop 149 | that.toCall = null 150 | that.isFire = null 151 | self._replyPool.release(that) 152 | cb() 153 | } 154 | } 155 | } 156 | 157 | function Response (id) { 158 | this.id = id 159 | this.error = null 160 | } 161 | 162 | function wrapStreams (that, data, msg, isFire) { 163 | if (data && data.streams) { 164 | msg.streams = Object.keys(data.streams) 165 | .map(mapStream, data.streams) 166 | .map(pipeStream, that) 167 | 168 | data = copy(data) 169 | delete data.streams 170 | } 171 | 172 | msg.data = that._opts.codec.encode(data) 173 | msg.fire = isFire 174 | 175 | return msg 176 | } 177 | 178 | function mapStream (key) { 179 | var stream = this[key] 180 | var objectMode = false 181 | var type 182 | 183 | if (!stream._transform && stream._readableState && stream._writableState) { 184 | type = messages.StreamType.Duplex 185 | objectMode = stream._readableState.objectMode || stream._writableState.objectMode 186 | } else if ((!stream._writableState || stream._readableState) && stream._readableState.pipesCount === 0) { 187 | type = messages.StreamType.Readable 188 | objectMode = stream._readableState.objectMode 189 | } else { 190 | type = messages.StreamType.Writable 191 | objectMode = stream._writableState.objectMode 192 | } 193 | 194 | // this is the streams object 195 | return { 196 | id: null, 197 | name: key, 198 | objectMode: objectMode, 199 | stream: stream, 200 | type: type 201 | } 202 | } 203 | 204 | function pipeStream (container) { 205 | // this is the tentacoli instance 206 | container.id = 'stream-' + this._nextId++ 207 | var dest = this.createStream(container.id) 208 | 209 | if (container.type === messages.StreamType.Readable || 210 | container.type === messages.StreamType.Duplex) { 211 | if (container.objectMode) { 212 | pump( 213 | container.stream, 214 | nos.encoder(this._opts), 215 | dest) 216 | } else { 217 | pump( 218 | container.stream, 219 | dest) 220 | } 221 | } 222 | 223 | if (container.type === messages.StreamType.Writable || 224 | container.type === messages.StreamType.Duplex) { 225 | if (container.objectMode) { 226 | pump( 227 | dest, 228 | nos.decoder(this._opts), 229 | container.stream) 230 | } else { 231 | pump( 232 | dest, 233 | container.stream) 234 | } 235 | } 236 | 237 | return container 238 | } 239 | 240 | function waitingOrReceived (that, id) { 241 | var stream 242 | 243 | if (that._waiting[id]) { 244 | stream = that._waiting[id] 245 | delete that._waiting[id] 246 | } else { 247 | stream = that.receiveStream(id, { halfOpen: true }) 248 | } 249 | 250 | stream.halfOpen = true 251 | 252 | return stream 253 | } 254 | 255 | function unwrapStreams (that, data, decoded) { 256 | if (decoded.streams.length > 0) { 257 | data.streams = decoded.streams.reduce(function (acc, container) { 258 | var stream = waitingOrReceived(that, container.id) 259 | var writable 260 | if (container.objectMode) { 261 | if (container.type === messages.StreamType.Duplex) { 262 | stream = nos(stream) 263 | } else if (container.type === messages.StreamType.Readable) { 264 | // if it is a readble, we close this side 265 | stream.end() 266 | stream = pump(stream, nos.decoder(that._opts)) 267 | } else if (container.type === messages.StreamType.Writable) { 268 | writable = nos.encoder(that._opts) 269 | pump(writable, stream) 270 | stream = writable 271 | } 272 | } 273 | acc[container.name] = stream 274 | return acc 275 | }, {}) 276 | } 277 | } 278 | 279 | inherits(Tentacoli, Multiplex) 280 | 281 | function Request (parent, callback) { 282 | this.id = parent ? 'req-' + parent._nextId++ : null 283 | this.callback = callback 284 | this.data = null 285 | } 286 | 287 | Tentacoli.prototype.request = function (data, callback) { 288 | var that = this 289 | var req = new Request(this, callback) 290 | 291 | try { 292 | wrapStreams(that, data, req, false) 293 | } catch (err) { 294 | callback(err) 295 | return this 296 | } 297 | 298 | this._requests[req.id] = req 299 | 300 | nos.writeToStream(req, messageCodec, this._main) 301 | 302 | return this 303 | } 304 | 305 | Tentacoli.prototype.fire = function (data, callback) { 306 | callback = callback || noop 307 | var that = this 308 | var req = new Request(null) 309 | 310 | try { 311 | wrapStreams(that, data, req, true) 312 | } catch (err) { 313 | callback(err) 314 | return this 315 | } 316 | 317 | nos.writeToStream(req, messageCodec, this._main, callback) 318 | 319 | return this 320 | } 321 | 322 | function noop () {} 323 | 324 | module.exports = Tentacoli 325 | -------------------------------------------------------------------------------- /test-browser.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var test = require('tape') 4 | var tentacoli = require('./') 5 | var ws = require('websocket-stream') 6 | var pump = require('pump') 7 | var from = require('from2') 8 | var URL = require('url') 9 | var serverOpts = URL.parse(document.URL) 10 | serverOpts.path = undefined 11 | serverOpts.pathname = undefined 12 | serverOpts.protocol = 'ws' 13 | var server = URL.format(serverOpts) 14 | 15 | test('browser req/res', function (t) { 16 | var stream = ws(server) 17 | var instance = tentacoli() 18 | var msg = { hello: 'world' } 19 | 20 | pump(stream, instance, stream) 21 | 22 | instance.request(msg, function (err, data) { 23 | t.error(err) 24 | t.deepEqual(data, msg, 'echo the message') 25 | t.end() 26 | stream.destroy() 27 | }) 28 | }) 29 | 30 | test('browser streams', function (t) { 31 | var stream = ws(server) 32 | var instance = tentacoli() 33 | 34 | pump(stream, instance, stream) 35 | 36 | instance.request({ 37 | streams: { 38 | inStream: from.obj(['hello', 'world']) 39 | } 40 | }, function (err, data) { 41 | t.error(err) 42 | 43 | var res = data.streams.inStream 44 | res.once('data', function (chunk) { 45 | t.deepEqual(chunk, 'hello') 46 | res.once('data', function (chunk) { 47 | t.deepEqual(chunk, 'world') 48 | t.end() 49 | stream.destroy() 50 | }) 51 | }) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var Buffer = require('safe-buffer').Buffer 4 | var test = require('tape') 5 | var tentacoli = require('./') 6 | var from = require('from2') 7 | var callback = require('callback-stream') 8 | var Writable = require('stream').Writable 9 | var through = require('through2') 10 | var msgpack = require('msgpack5') 11 | 12 | function setup (opts) { 13 | var sender = tentacoli(opts) 14 | var receiver = tentacoli(opts) 15 | 16 | sender.pipe(receiver).pipe(sender) 17 | 18 | return { 19 | sender: sender, 20 | receiver: receiver 21 | } 22 | } 23 | 24 | test('can issue a request', function (t) { 25 | t.plan(3) 26 | 27 | var s = setup() 28 | var msg = 'the answer to life, the universe and everything' 29 | var expected = '42' 30 | 31 | s.sender.request(msg, function (err, res) { 32 | t.error(err, 'no error') 33 | t.deepEqual(res, expected, 'response matches') 34 | }) 35 | 36 | s.receiver.on('request', function (req, reply) { 37 | t.deepEqual(req, msg, 'request matches') 38 | reply(null, expected) 39 | }) 40 | }) 41 | 42 | test('can pass through an object', function (t) { 43 | t.plan(3) 44 | 45 | var s = setup() 46 | var msg = { cmd: 'the answer to life, the universe and everything' } 47 | var expected = { res: '42' } 48 | 49 | s.sender.request(msg, function (err, res) { 50 | t.error(err, 'no error') 51 | t.deepEqual(res, expected, 'response matches') 52 | }) 53 | 54 | s.receiver.on('request', function (req, reply) { 55 | t.deepEqual(req, msg, 'request matches') 56 | reply(null, expected) 57 | }) 58 | }) 59 | 60 | test('can handle an error response from a request', function (t) { 61 | t.plan(4) 62 | 63 | var s = setup() 64 | var msg = 'the answer to life, the universe and everything' 65 | 66 | s.sender.request(msg, function (err, res) { 67 | t.ok(err instanceof Error, 'there is an error') 68 | t.equal(err.message, 'something went wrong') 69 | }) 70 | 71 | s.receiver.on('request', function (req, reply) { 72 | t.deepEqual(req, msg, 'request matches') 73 | reply(new Error('something went wrong')) 74 | }) 75 | 76 | s.receiver.on('responseError', function (err) { 77 | t.ok(err, 'error exists') 78 | }) 79 | }) 80 | 81 | test('can pass from receiver to sender an object readable stream', function (t) { 82 | t.plan(3) 83 | 84 | var s = setup() 85 | var msg = { cmd: 'subscribe' } 86 | 87 | s.sender.request(msg, function (err, res) { 88 | t.error(err, 'no error') 89 | res.streams.result.pipe(callback.obj(function (err, list) { 90 | t.error(err, 'no error') 91 | t.deepEqual(list, ['hello', 'streams'], 'is passed through correctly') 92 | })) 93 | }) 94 | 95 | s.receiver.on('request', function (req, reply) { 96 | reply(null, { 97 | streams: { 98 | result: from.obj(['hello', 'streams']) 99 | } 100 | }) 101 | }) 102 | }) 103 | 104 | test('can pass from receiver to sender a writable stream', function (t) { 105 | t.plan(2) 106 | 107 | var s = setup() 108 | var msg = { cmd: 'publish' } 109 | 110 | s.sender.request(null, function (err, res) { 111 | t.error(err, 'no error') 112 | res.streams.writable.end(msg) 113 | }) 114 | 115 | s.receiver.on('request', function (req, reply) { 116 | var writable = new Writable({ objectMode: true }) 117 | writable._write = function (chunk, enc, cb) { 118 | t.deepEqual(chunk, msg, 'msg match') 119 | cb() 120 | } 121 | reply(null, { 122 | streams: { 123 | writable: writable 124 | } 125 | }) 126 | }) 127 | }) 128 | 129 | test('can pass from receiver to sender a transform stream as a writable', function (t) { 130 | t.plan(2) 131 | 132 | var s = setup() 133 | var msg = { cmd: 'publish' } 134 | 135 | s.sender.request(null, function (err, res) { 136 | t.error(err, 'no error') 137 | res.streams.writable.end(msg) 138 | }) 139 | 140 | s.receiver.on('request', function (req, reply) { 141 | var writable = new Writable({ objectMode: true }) 142 | writable._write = function (chunk, enc, cb) { 143 | t.deepEqual(chunk, msg, 'msg match') 144 | } 145 | var transform = through.obj() 146 | 147 | transform.pipe(writable) 148 | 149 | reply(null, { 150 | streams: { 151 | writable: transform 152 | } 153 | }) 154 | }) 155 | }) 156 | 157 | test('can pass from receiver to sender a transform stream as a readable streams', function (t) { 158 | t.plan(3) 159 | 160 | var s = setup() 161 | var msg = { cmd: 'subscribe' } 162 | 163 | s.sender.request(msg, function (err, res) { 164 | t.error(err, 'no error') 165 | res.streams.result.pipe(callback.obj(function (err, list) { 166 | t.error(err, 'no error') 167 | t.deepEqual(list, ['hello', 'streams'], 'is passed through correctly') 168 | })) 169 | }) 170 | 171 | s.receiver.on('request', function (req, reply) { 172 | reply(null, { 173 | streams: { 174 | result: from.obj(['hello', 'streams']).pipe(through.obj(function (chunk, enc, cb) { 175 | cb(null, chunk) 176 | })) 177 | } 178 | }) 179 | }) 180 | }) 181 | 182 | test('can pass from sender to receiver an object readable stream', function (t) { 183 | t.plan(3) 184 | 185 | var s = setup() 186 | var msg = { 187 | cmd: 'publish', 188 | streams: { 189 | events: from.obj(['hello', 'streams']) 190 | } 191 | } 192 | 193 | s.sender.request(msg, function (err, res) { 194 | t.error(err, 'no error') 195 | }) 196 | 197 | s.receiver.on('request', function (req, reply) { 198 | req.streams.events.pipe(callback.obj(function (err, list) { 199 | t.error(err, 'no error') 200 | t.deepEqual(list, ['hello', 'streams'], 'is passed through correctly') 201 | reply() 202 | })) 203 | }) 204 | }) 205 | 206 | test('can pass from sender to receiver an object writable stream', function (t) { 207 | t.plan(2) 208 | 209 | var s = setup() 210 | var writable = new Writable({ objectMode: true }) 211 | 212 | writable._write = function (chunk, enc, cb) { 213 | t.deepEqual(chunk, 'hello', 'chunk match') 214 | cb() 215 | } 216 | 217 | var msg = { 218 | cmd: 'subscribe', 219 | streams: { 220 | events: writable 221 | } 222 | } 223 | 224 | s.sender.request(msg, function (err, res) { 225 | t.error(err, 'no error') 226 | }) 227 | 228 | s.receiver.on('request', function (req, reply) { 229 | req.streams.events.end('hello') 230 | reply() 231 | }) 232 | }) 233 | 234 | test('supports custom encodings', function (t) { 235 | t.plan(3) 236 | 237 | var s = setup({ codec: msgpack() }) 238 | var msg = { cmd: 'subscribe' } 239 | var expected = [ 240 | Buffer.from('hello'), 241 | Buffer.from('streams') 242 | ] 243 | 244 | s.sender.request(msg, function (err, res) { 245 | t.error(err, 'no error') 246 | res.streams.result.pipe(callback.obj(function (err, list) { 247 | t.error(err, 'no error') 248 | t.deepEqual(list, expected, 'is passed through correctly') 249 | })) 250 | }) 251 | 252 | s.receiver.on('request', function (req, reply) { 253 | reply(null, { 254 | streams: { 255 | result: from.obj(expected).pipe(through.obj(function (chunk, enc, cb) { 256 | cb(null, chunk) 257 | })) 258 | } 259 | }) 260 | }) 261 | }) 262 | 263 | test('can reply with null', function (t) { 264 | t.plan(3) 265 | 266 | var s = setup() 267 | var msg = 'the answer to life, the universe and everything' 268 | 269 | s.sender.request(msg, function (err, res) { 270 | t.error(err, 'no error') 271 | t.notOk(res, 'empty response') 272 | }) 273 | 274 | s.receiver.on('request', function (req, reply) { 275 | t.deepEqual(req, msg, 'request matches') 276 | reply() 277 | }) 278 | }) 279 | 280 | test('errors if piping something errors', function (t) { 281 | t.plan(1) 282 | 283 | var s = setup() 284 | var writable = new Writable({ objectMode: true }) 285 | var throwErr 286 | 287 | writable.on('pipe', function () { 288 | throwErr = new Error('something goes wrong') 289 | throw throwErr 290 | }) 291 | 292 | var msg = { 293 | cmd: 'subscribe', 294 | streams: { 295 | events: writable 296 | } 297 | } 298 | 299 | s.sender.request(msg, function (err, res) { 300 | t.equal(err, throwErr, 'an error happens') 301 | }) 302 | 303 | s.receiver.on('request', function (req, reply) { 304 | t.fail('it never happens') 305 | }) 306 | }) 307 | 308 | test('errors if the connection end', function (t) { 309 | t.plan(2) 310 | 311 | var s = setup() 312 | var msg = 'the answer to life, the universe and everything' 313 | 314 | s.sender.request(msg, function (err) { 315 | t.ok(err, 'should error') 316 | }) 317 | 318 | s.receiver.on('request', function (req, reply) { 319 | t.deepEqual(req, msg, 'request matches') 320 | s.receiver.end() 321 | }) 322 | }) 323 | 324 | test('errors if the receiver is destroyed', function (t) { 325 | t.plan(3) 326 | 327 | var s = setup() 328 | var msg = 'the answer to life, the universe and everything' 329 | 330 | s.sender.request(msg, function (err) { 331 | t.ok(err, 'should error') 332 | }) 333 | 334 | s.receiver.on('error', function (err) { 335 | t.ok(err, 'should error') 336 | }) 337 | 338 | s.receiver.on('request', function (req, reply) { 339 | t.deepEqual(req, msg, 'request matches') 340 | s.receiver.destroy(new Error('kaboom')) 341 | }) 342 | }) 343 | 344 | test('errors if the sender is destroyed with error', function (t) { 345 | t.plan(3) 346 | 347 | var s = setup() 348 | var msg = 'the answer to life, the universe and everything' 349 | 350 | s.sender.request(msg, function (err) { 351 | t.ok(err, 'should error') 352 | }) 353 | 354 | s.sender.on('error', function (err) { 355 | t.ok(err, 'should error') 356 | }) 357 | 358 | s.receiver.on('request', function (req, reply) { 359 | t.deepEqual(req, msg, 'request matches') 360 | s.sender.destroy(new Error('kaboom')) 361 | }) 362 | }) 363 | 364 | test('errors if the sender is destroyed', function (t) { 365 | t.plan(2) 366 | 367 | var s = setup() 368 | var msg = 'the answer to life, the universe and everything' 369 | 370 | s.sender.request(msg, function (err) { 371 | t.ok(err, 'should error') 372 | }) 373 | 374 | s.receiver.on('request', function (req, reply) { 375 | t.deepEqual(req, msg, 'request matches') 376 | s.sender.destroy() 377 | }) 378 | }) 379 | 380 | test('fire and forget - send string', function (t) { 381 | t.plan(1) 382 | 383 | var s = setup() 384 | var msg = 'the answer to life, the universe and everything' 385 | 386 | s.sender.fire(msg) 387 | 388 | s.receiver.on('request', function (req) { 389 | t.deepEqual(req, msg, 'request matches') 390 | }) 391 | }) 392 | 393 | test('fire and forget - the error callback should be called on send', function (t) { 394 | t.plan(2) 395 | 396 | var s = setup() 397 | var msg = 'the answer to life, the universe and everything' 398 | 399 | s.sender.fire(msg, function (err) { 400 | t.error(err) 401 | }) 402 | 403 | s.receiver.on('request', function (req) { 404 | t.deepEqual(req, msg, 'request matches') 405 | }) 406 | }) 407 | 408 | test('fire and forget - the error callback should be called in case of error while sending the message', function (t) { 409 | t.plan(1) 410 | 411 | var s = setup() 412 | var msg = 'the answer to life, the universe and everything' 413 | 414 | s.sender.fire({ 415 | streams: 'kaboom!' 416 | }, function (err) { 417 | t.ok(err) 418 | }) 419 | 420 | s.receiver.on('request', function (req) { 421 | t.deepEqual(req, msg, 'request matches') 422 | }) 423 | }) 424 | 425 | test('fire and forget - if reply is called, nothing should happen in the sender', function (t) { 426 | t.plan(2) 427 | 428 | var s = setup() 429 | var msg = 'the answer to life, the universe and everything' 430 | 431 | s.sender.fire(msg, function (err) { 432 | t.error(err) 433 | }) 434 | 435 | s.receiver.on('request', function (req, reply) { 436 | t.deepEqual(req, msg, 'request matches') 437 | reply(new Error('kaboom!')) 438 | }) 439 | }) 440 | 441 | test('fire and forget - send object', function (t) { 442 | t.plan(1) 443 | 444 | var s = setup() 445 | var msg = { cmd: 'the answer to life, the universe and everything' } 446 | 447 | s.sender.fire(msg) 448 | 449 | s.receiver.on('request', function (req) { 450 | t.deepEqual(req, msg, 'request matches') 451 | }) 452 | }) 453 | 454 | test('fire and forget - can pass from sender to receiver an object readable stream', function (t) { 455 | t.plan(2) 456 | 457 | var s = setup() 458 | var msg = { 459 | cmd: 'publish', 460 | streams: { 461 | events: from.obj(['hello', 'streams']) 462 | } 463 | } 464 | 465 | s.sender.fire(msg) 466 | 467 | s.receiver.on('request', function (req, reply) { 468 | req.streams.events.pipe(callback.obj(function (err, list) { 469 | t.error(err, 'no error') 470 | t.deepEqual(list, ['hello', 'streams'], 'is passed through correctly') 471 | })) 472 | }) 473 | }) 474 | 475 | test('fire and forget - can pass from sender to receiver an object writable stream', function (t) { 476 | t.plan(1) 477 | 478 | var s = setup() 479 | var writable = new Writable({ objectMode: true }) 480 | 481 | writable._write = function (chunk, enc, cb) { 482 | t.deepEqual(chunk, 'hello', 'chunk match') 483 | cb() 484 | } 485 | 486 | var msg = { 487 | cmd: 'subscribe', 488 | streams: { 489 | events: writable 490 | } 491 | } 492 | 493 | s.sender.fire(msg) 494 | 495 | s.receiver.on('request', function (req, reply) { 496 | req.streams.events.end('hello') 497 | }) 498 | }) 499 | 500 | test('fire and forget - should not care if the connection end', function (t) { 501 | t.plan(1) 502 | 503 | var s = setup() 504 | var msg = 'the answer to life, the universe and everything' 505 | 506 | s.sender.fire(msg) 507 | 508 | s.receiver.on('request', function (req, reply) { 509 | t.deepEqual(req, msg, 'request matches') 510 | s.receiver.end() 511 | }) 512 | }) 513 | 514 | test('fire and forget - should not care if the receiver is destroyed', function (t) { 515 | t.plan(2) 516 | 517 | var s = setup() 518 | var msg = 'the answer to life, the universe and everything' 519 | 520 | s.sender.fire(msg) 521 | 522 | s.receiver.on('error', function (err) { 523 | t.ok(err, 'should error') 524 | }) 525 | 526 | s.receiver.on('request', function (req, reply) { 527 | t.deepEqual(req, msg, 'request matches') 528 | s.receiver.destroy(new Error('kaboom')) 529 | }) 530 | }) 531 | 532 | test('fire and forget - should not care about errors', function (t) { 533 | t.plan(1) 534 | 535 | var s = setup() 536 | var msg = { cmd: 'the answer to life, the universe and everything' } 537 | 538 | s.sender.fire(msg) 539 | 540 | s.receiver.on('request', function (req) { 541 | t.deepEqual(req, msg, 'request matches') 542 | s.sender.destroy() 543 | }) 544 | }) 545 | 546 | test('fire and forget - if a writable stream is passed to reply it should be destroyed', function (t) { 547 | t.plan(1) 548 | 549 | var s = setup() 550 | var msg = { cmd: 'subscribe' } 551 | var writable = new Writable({ objectMode: true }) 552 | 553 | s.sender.fire(msg) 554 | 555 | writable.on('error', function (err) { 556 | t.ok(err) 557 | }) 558 | 559 | writable.on('finish', function () { 560 | t.pass('stream closed') 561 | }) 562 | 563 | s.receiver.on('request', function (req, reply) { 564 | reply(null, { 565 | streams: writable 566 | }) 567 | }) 568 | }) 569 | 570 | test('fire and forget - if a writable stream is passed to reply it should be destroyed', function (t) { 571 | t.plan(1) 572 | 573 | var s = setup() 574 | var msg = { cmd: 'subscribe' } 575 | var readable = from.obj(['hello', 'streams']) 576 | 577 | s.sender.fire(msg) 578 | 579 | readable.on('close', function () { 580 | t.pass('stream closed') 581 | }) 582 | 583 | s.receiver.on('request', function (req, reply) { 584 | reply(null, { 585 | streams: readable 586 | }) 587 | }) 588 | }) 589 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | module: { 5 | postLoaders: [{ 6 | loader: 'transform?brfs' 7 | }] 8 | } 9 | } 10 | --------------------------------------------------------------------------------