├── .travis.yml ├── examples ├── echo.html └── echo-client.js ├── webpack.config.js ├── schema.proto ├── .gitignore ├── echo-server.js ├── LICENSE ├── example.js ├── test-browser.js ├── benchmarks ├── tentacoli_echo.js └── bench.js ├── package.json ├── tentacoli.js ├── README.md └── test.js /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | - "6" 5 | - "7" 6 | - "8" 7 | -------------------------------------------------------------------------------- /examples/echo.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | module: { 5 | postLoaders: [{ 6 | loader: 'transform?brfs' 7 | }] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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-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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------