├── .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 [](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 |
--------------------------------------------------------------------------------