├── .github
└── workflows
│ ├── build.yml
│ └── todo.yml
├── .gitignore
├── .travis.yml
├── CHANGES.md
├── Dockerfile
├── LICENSE
├── README.md
├── coverage
└── empty.txt
├── docker-compose.yml
├── docs
├── create-a-release.md
└── examples
│ └── simple-http
│ ├── http-client.js
│ └── http-listen.js
├── lib
├── http.js
├── tcp.js
└── transport-utils.js
├── package.json
├── test
├── basic.skip.js
├── bench
│ ├── bench-external.js
│ ├── bench-internal.js
│ └── bench-service.js
├── entity.test.js
├── http.test.js
├── integration
│ ├── client.js
│ └── server.js
├── misc.test.js
├── reconnect
│ ├── client.js
│ └── server.js
├── stubs
│ ├── README.txt
│ ├── client-foo-pin.js
│ ├── client-foo.js
│ ├── fault.js
│ ├── foo.js
│ ├── memtest-transport.js
│ ├── readme-color-client.js
│ ├── readme-color-service.js
│ ├── readme-color-tcp.js
│ ├── readme-color-web-https.js
│ ├── readme-color.js
│ ├── readme-many-colors-client.js
│ ├── readme-many-colors-server.js
│ ├── readme-many-colors.sh
│ └── service-foo.js
├── tcp.test.js
└── utils
│ ├── createClient.js
│ └── createInstance.js
└── transport.js
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: build
5 |
6 | on:
7 | push:
8 | branches: [ master ]
9 | pull_request:
10 | branches: [ master ]
11 |
12 | jobs:
13 | build:
14 | timeout-minutes: 12
15 |
16 | strategy:
17 | fail-fast: false
18 | matrix:
19 | os: [ubuntu-latest, windows-latest, macos-latest]
20 | node-version: [lts/*, 17.x, 16.x, 14.x, 12.x, 10.x]
21 |
22 | runs-on: ${{ matrix.os }}
23 |
24 | steps:
25 | - uses: actions/checkout@v2
26 | - name: Use Node.js ${{ matrix.node-version }}
27 | uses: actions/setup-node@v2
28 | with:
29 | node-version: ${{ matrix.node-version }}
30 | - run: npm install
31 | - run: npm run build --if-present
32 | - run: npm run coveralls
33 |
34 | - name: Coveralls
35 | uses: coverallsapp/github-action@master
36 | with:
37 | github-token: ${{ secrets.GITHUB_TOKEN }}
38 | path-to-lcov: ./coverage/lcov.info
39 |
--------------------------------------------------------------------------------
/.github/workflows/todo.yml:
--------------------------------------------------------------------------------
1 | name: "TODO"
2 | on: ["push"]
3 | jobs:
4 | build:
5 | runs-on: "ubuntu-latest"
6 | steps:
7 | - uses: "actions/checkout@master"
8 | - name: "todo-to-issue"
9 | uses: "senecajs/todo-to-issue-action@master"
10 | with:
11 | REPO: ${{ github.repository }}
12 | BEFORE: ${{ github.event.before }}
13 | SHA: ${{ github.sha }}
14 | TOKEN: ${{ secrets.GITHUB_TOKEN }}
15 | LABEL: "TODO:"
16 | COMMENT_MARKER: "//"
17 | INCLUDE_EXT: ".js,.md"
18 | id: "todo"
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.log
3 | test/report.html
4 |
5 | docs/annotated
6 | node_modules
7 |
8 | docs/annotated
9 | docs/coverage.html
10 |
11 |
12 | package-lock.json
13 |
14 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false,
2 | language: node_js
3 |
4 | env:
5 | - SENECA_VER=seneca@3.x.x
6 | - SENECA_VER=seneca@plugin
7 | - SENECA_VER=senecajs/seneca
8 |
9 | node_js:
10 | - '11'
11 | - '10'
12 | - '8'
13 |
14 |
15 | install:
16 | - NODE_VERSION=$(node -v); if [ ${NODE_VERSION:1:2} -ge 10 ]; then npm i -g npm@6; npm ci; else npm install; fi
17 |
18 |
19 | before_script:
20 | - npm uninstall seneca
21 | - npm install $SENECA_VER
22 |
23 |
24 | script:
25 | - npm test
26 | - if [ ${NODE_VERSION:1:2} -ge 10 ]; then npm audit; fi
27 |
28 |
29 | after_script:
30 | - npm run coveralls
31 |
--------------------------------------------------------------------------------
/CHANGES.md:
--------------------------------------------------------------------------------
1 | ## 2.1.0 2016-08-25
2 |
3 | * Removed seneca-chain dependency PR#115
4 | * Updated dependencies
5 | * Added Seneca 3 and Node 6 support
6 | * Dropped Node 0.10, 0.12, 5 support
7 |
8 | ## 2.0.0 2016-08-12
9 |
10 | * Fix tcp transport breaks seneca mesh
11 | * Dropped support for Node 0.10, 0.12
12 | * Dependencies update
13 |
14 | ## 1.3.0
15 |
16 | * Documentation update
17 | * Dependencies update
18 | * Tests fixes
19 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node
2 |
3 | RUN mkdir -p /seneca-transport
4 | WORKDIR /seneca-transport
5 | COPY package.json package.json
6 | COPY transport.js transport.js
7 | COPY lib/ lib/
8 | COPY test/integration/server.js server.js
9 | COPY test/integration/client.js client.js
10 |
11 | RUN npm install
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2013 Richard Rodger
4 | Copyright (c) 2014 Richard Rodger
5 | Copyright (c) 2015-2016 Richard Rodger and Seneca.js contributors
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy of
8 | this software and associated documentation files (the "Software"), to deal in
9 | the Software without restriction, including without limitation the rights to
10 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
11 | the Software, and to permit persons to whom the Software is furnished to do so,
12 | subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
19 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
20 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
21 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
22 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | > A [Seneca.js][] transport plugin
3 |
4 | |  | This open source module is sponsored and supported by [Voxgig](https://www.voxgig.com). |
5 | |---|---|
6 |
7 | # seneca-transport
8 | [![npm version][npm-badge]][npm-url]
9 | [![Build Status][travis-badge]][travis-url]
10 | [![Dependency Status][david-badge]][david-url]
11 | [![Gitter][gitter-badge]][gitter-url]
12 |
13 | ## Description
14 |
15 | This plugin provides the HTTP/HTTPS and TCP transport channels for
16 | micro-service messages. It's a built-in dependency of the Seneca
17 | module, so you don't need to include it manually. You use this plugin
18 | to wire up your micro-services so that they can talk to each other.
19 |
20 | seneca-transport's source can be read in an annotated fashion by:
21 | - running `npm run annotate`
22 | - viewing ./docs/annotated/transport.html locally
23 |
24 | If you're using this module, and need help, you can:
25 |
26 | - Post a [github issue][],
27 | - Tweet to [@senecajs][],
28 | - Ask on the [Gitter][gitter-url].
29 |
30 | If you are new to Seneca in general, please take a look at [senecajs.org][]. We have everything from
31 | tutorials to sample apps to help get you up and running quickly.
32 |
33 | ### Seneca compatibility
34 | Supports Seneca versions **3.x** and above.
35 |
36 | ## Install
37 |
38 | This plugin module is included in the main Seneca module:
39 |
40 | ```sh
41 | npm install seneca
42 | ```
43 |
44 | To install separately, use:
45 |
46 | ```sh
47 | npm install seneca-transport
48 | ```
49 |
50 |
51 | ## Quick Example
52 |
53 | Let's do everything in one script to begin with. You'll define a
54 | simple Seneca plugin that returns the hex value of color words. In
55 | fact, all it can handle is the color red!
56 |
57 | You define the action pattern _color:red_, which always returns the
58 | result {hex:'#FF0000'}
. You're also using the name of the
59 | function _color_ to define the name of the plugin (see [How to write a
60 | Seneca plugin](http://senecajs.org/docs/tutorials/how-to-write-a-plugin.html)).
61 |
62 | ```js
63 | function color() {
64 | this.add( 'color:red', function(args,done){
65 | done(null, {hex:'#FF0000'});
66 | })
67 | }
68 | ```
69 |
70 | Now, let's create a server and client. The server Seneca instance will
71 | load the _color_ plugin and start a web server to listen for inbound
72 | messages. The client Seneca instance will submit a _color:red_ message
73 | to the server.
74 |
75 |
76 | ```js
77 | var seneca = require('seneca')
78 |
79 | seneca()
80 | .use(color)
81 | .listen()
82 |
83 | seneca()
84 | .client()
85 | .act('color:red')
86 | ```
87 |
88 | Example with HTTPS:
89 |
90 | To enable HTTPS, pass an options object to the `listen` function setting the `protocol` option to 'https' and provide a `serverOptions` object with `key` and `cert` properties.
91 |
92 | ```js
93 | var seneca = require('seneca')
94 | var Fs = require('fs')
95 |
96 |
97 | seneca()
98 | .use(color)
99 | .listen({
100 | type: 'http',
101 | port: '8000',
102 | host: 'localhost',
103 | protocol: 'https',
104 | serverOptions : {
105 | key : Fs.readFileSync('path/to/key.pem', 'utf8'),
106 | cert : Fs.readFileSync('path/to/cert.pem', 'utf8')
107 | }
108 | })
109 |
110 | seneca()
111 | .client({
112 | type: 'http',
113 | port: '8000',
114 | host: 'localhost',
115 | protocol: 'https'
116 | })
117 | .act('color:red')
118 | ```
119 |
120 | You can create multiple instances of Seneca inside the same Node.js
121 | process. They won't interfere with each other, but they will share
122 | external options from configuration files or the command line.
123 |
124 | If you run the full script (full source is in
125 | [readme-color.js](https://github.com/senecajs/seneca-transport/blob/master/test/stubs/readme-color.js)),
126 | you'll see the standard Seneca startup log messages, but you won't see
127 | anything that tells you what the _color_ plugin is doing since this
128 | code doesn't bother printing the result of the action. Let's use a
129 | filtered log to output the inbound and outbound action messages from
130 | each Seneca instance so we can see what's going on. Run the script with:
131 |
132 | ```sh
133 | node readme-color.js --seneca.log=type:act,regex:color:red
134 | ```
135 |
136 | _NOTE: when running the examples in this documentation, you'll find
137 | that most of the Node.js processes do not exit. This because they
138 | running in server mode. You'll need to kill all the Node.js processes
139 | between execution runs. The quickest way to do this is:_
140 |
141 | ```sh
142 | $ killall node
143 | ```
144 |
145 |
146 | This log filter restricts printed log entries to those that report
147 | inbound and outbound actions, and further, to those log lines that
148 | match the regular expression /color:red/
. Here's what
149 | you'll see:
150 |
151 | ```sh
152 | [TIME] vy../..15/- DEBUG act - - IN 485n.. color:red {color=red} CLIENT
153 | [TIME] ly../..80/- DEBUG act color - IN 485n.. color:red {color=red} f2rv..
154 | [TIME] ly../..80/- DEBUG act color - OUT 485n.. color:red {hex=#FF0000} f2rv..
155 | [TIME] vy../..15/- DEBUG act - - OUT 485n.. color:red {hex=#FF0000} CLIENT
156 | ```
157 |
158 | The second field is the identifier of the Seneca instance. You can see
159 | that first the client (with an identifier of _vy../..15/-_) sends the
160 | message {color=red}
. The message is sent over HTTP to the
161 | server (which has an identifier of _ly../..80/-_). The server performs the
162 | action, generating the result {hex=#FF0000}
, and sends
163 | it back.
164 |
165 | The third field, DEBUG
, indicates the log level. The next
166 | field, act
indicates the type of the log entry. Since
167 | you specified type:act
in the log filter, you've got a
168 | match!
169 |
170 | The next two fields indicate the plugin name and tag, in this case color
171 | -
. The plugin is only known on the server side, so the client
172 | just indicates a blank entry with -
. For more details on
173 | plugin names and tags, see [How to write a Seneca
174 | plugin](http://senecajs.org/tutorials/how-to-write-a-plugin.html).
175 |
176 | The next field (also known as the _case_) is either IN
or
177 | OUT
, and indicates the direction of the message. If you
178 | follow the flow, you can see that the message is first inbound to the
179 | client, and then inbound to the server (the client sends it
180 | onwards). The response is outbound from the server, and then outbound
181 | from the client (back to your own code). The field after that,
182 | 485n..
, is the message identifier. You can see that it
183 | remains the same over multiple Seneca instances. This helps you to
184 | debug message flow.
185 |
186 | The next two fields show the action pattern of the message,
187 | color:red
, followed by the actual data of the request
188 | message (when inbound), or the response message (when outbound).
189 |
190 | The last field f2rv..
is the internal identifier of the
191 | action function that acts on the message. On the client side, there is
192 | no action function, and this is indicated by the CLIENT
193 | marker. If you'd like to match up the action function identifier to
194 | message executions, add a log filter to see them:
195 |
196 | ```
197 | node readme-color.js --seneca.log=type:act,regex:color:red \
198 | --seneca.log=plugin:color,case:ADD
199 | [TIME] ly../..80/- DEBUG plugin color - ADD f2rv.. color:red
200 | [TIME] vy../..15/- DEBUG act - - IN 485n.. color:red {color=red} CLIENT
201 | [TIME] ly../..80/- DEBUG act color - IN 485n.. color:red {color=red} f2rv..
202 | [TIME] ly../..80/- DEBUG act color - OUT 485n.. color:red {hex=#FF0000} f2rv..
203 | [TIME] vy../..15/- DEBUG act - - OUT 485n.. color:red {hex=#FF0000} CLIENT
204 | ```
205 |
206 | The filter plugin:color,case:ADD
picks out log entries of
207 | type _plugin_, where the plugin has the name _color_, and where the
208 | _case_ is ADD. These entries indicate the action patterns that a
209 | plugin has registered. In this case, there's only one, _color:red_.
210 |
211 | You've run this example in a single Node.js process up to now. Of
212 | course, the whole point is to run it in separate processes! Let's do
213 | that. First, here's the server:
214 |
215 | ```js
216 | function color() {
217 | this.add( 'color:red', function(args,done){
218 | done(null, {hex:'#FF0000'});
219 | })
220 | }
221 |
222 | var seneca = require('seneca')
223 |
224 | seneca()
225 | .use(color)
226 | .listen()
227 | ```
228 |
229 | Run this in one terminal window with:
230 |
231 | ```sh
232 | $ node readme-color-service.js --seneca.log=type:act,regex:color:red
233 | ```
234 |
235 | And on the client side:
236 |
237 | ```js
238 | var seneca = require('seneca')
239 |
240 | seneca()
241 | .client()
242 | .act('color:red')
243 | ```
244 |
245 | And run with:
246 |
247 | ```sh
248 | $ node readme-color-client.js --seneca.log=type:act,regex:color:red
249 | ```
250 |
251 | You'll see the same log lines as before, just split over the two processes. The full source code is the [test folder](https://github.com/senecajs/seneca-transport/tree/master/test).
252 |
253 |
254 | ## Non-Seneca Clients
255 |
256 | The default transport mechanism for messages is HTTP. This means you can communicate easily with a Seneca micro-service from other platforms. By default, the listen
method starts a web server on port 10101, listening on all interfaces. If you run the _readme-color-service.js_ script again (as above), you can talk to it by _POSTing_ JSON data to the /act
path. Here's an example using the command line _curl_ utility.
257 |
258 | ```sh
259 | $ curl -d '{"color":"red"}' http://localhost:10101/act
260 | {"hex":"#FF0000"}
261 | ```
262 |
263 | If you dump the response headers, you'll see some additional headers that give you contextual information. Let's use the -v
option of _curl_ to see them:
264 |
265 | ```sh
266 | $ curl -d '{"color":"red"}' -v http://localhost:10101/act
267 | ...
268 | * Connected to localhost (127.0.0.1) port 10101 (#0)
269 | > POST /act HTTP/1.1
270 | > User-Agent: curl/7.30.0
271 | > Host: localhost:10101
272 | > Accept: */*
273 | > Content-Length: 15
274 | > Content-Type: application/x-www-form-urlencoded
275 | >
276 | * upload completely sent off: 15 out of 15 bytes
277 | < HTTP/1.1 200 OK
278 | < Content-Type: application/json
279 | < Cache-Control: private, max-age=0, no-cache, no-store
280 | < Content-Length: 17
281 | < seneca-id: 9wu80xdsn1nu
282 | < seneca-kind: res
283 | < seneca-origin: curl/7.30.0
284 | < seneca-accept: sk5mjwcxxpvh/1409222334824/-
285 | < seneca-time-client-sent: 1409222493910
286 | < seneca-time-listen-recv: 1409222493910
287 | < seneca-time-listen-sent: 1409222493910
288 | < Date: Thu, 28 Aug 2014 10:41:33 GMT
289 | < Connection: keep-alive
290 | <
291 | * Connection #0 to host localhost left intact
292 | {"hex":"#FF0000"}
293 | ```
294 |
295 | You can get the message identifier from the _seneca-id_ header, and
296 | the identifier of the Seneca instance from _seneca-accept_.
297 |
298 | There are two structures that the submitted JSON document can take:
299 |
300 | * Vanilla JSON containing your request message, plain and simple, as per the example above,
301 | * OR: A JSON wrapper containing the client details along with the message data.
302 |
303 | The JSON wrapper follows the standard form of Seneca messages used in
304 | other contexts, such as message queue transports. However, the simple
305 | vanilla format is perfectly valid and provided explicitly for
306 | integration. The wrapper format is described below.
307 |
308 | If you need Seneca to listen on a particular port or host, you can
309 | specify these as options to the listen
method. Both are
310 | optional.
311 |
312 | ```js
313 | seneca()
314 | .listen( { host:'192.168.1.2', port:80 } )
315 | ```
316 |
317 | On the client side, either with your own code, or the Seneca client,
318 | you'll need to use matching host and port options.
319 |
320 | ```bash
321 | $ curl -d '{"color":"red"}' http://192.168.1.2:80/act
322 | ```
323 |
324 | ```js
325 | seneca()
326 | .client( { host:'192.168.1.2', port:80 } )
327 | ```
328 |
329 | You can also set the host and port via the Seneca options facility. When
330 | using the options facility, you are setting the default options for
331 | all message transports. These can be overridden by arguments to individual
332 | listen
and client
calls.
333 |
334 | Let's run the color example again, but with a different port. On the server-side:
335 |
336 | ```sh
337 | $ node readme-color-service.js --seneca.log=type:act,regex:color:red \
338 | --seneca.options.transport.port=8888
339 | ```
340 |
341 | And the client-side:
342 |
343 | ```sh
344 | curl -d '{"color":"red"}' -v http://localhost:8888/act
345 | ```
346 | OR
347 |
348 | ```sh
349 | $ node readme-color-client.js --seneca.log=type:act,regex:color:red \
350 | --seneca.options.transport.port=8888
351 | ```
352 |
353 | ## Using the TCP Channel
354 |
355 | Also included in this plugin is a TCP transport mechanism. The HTTP
356 | mechanism offers easy integration, but it is necessarily slower. The
357 | TCP transport opens a direct TCP connection to the server. The
358 | connection remains open, avoiding connection overhead for each
359 | message. The client side of the TCP transport will also attempt to
360 | reconnect if the connection breaks, providing fault tolerance for
361 | server restarts.
362 |
363 | To use the TCP transport, specify a _type_ property to the
364 | listen
and client
methods, and give it the
365 | value _tcp_. Here's the single script example again:
366 |
367 |
368 | ```js
369 | seneca()
370 | .use(color)
371 | .listen({type:'tcp'})
372 |
373 | seneca()
374 | .client({type:'tcp'})
375 | .act('color:red')
376 | ```
377 |
378 | The full source code is in the
379 | [readme-color-tcp.js](https://github.com/senecajs/seneca-transport/blob/master/test/stubs/readme-color-tcp.js)
380 | file. When you run this script it would be great to verify that the
381 | right transport channels are being created. You'd like to see the
382 | configuration, and any connections that occur. By default, this
383 | information is printed with a log level of _INFO_, so you will see it
384 | if you don't use any log filters.
385 |
386 | Of course, we are using a log filter. So let's add another one to
387 | print the connection details so we can sanity check the system. We want
388 | to print any log entries with a log level of _INFO_. Here's the
389 | command:
390 |
391 | ```sh
392 | $ node readme-color-tcp.js --seneca.log=level:INFO \
393 | --seneca.log=type:act,regex:color:red
394 | ```
395 |
396 | This produces the log output:
397 |
398 | ```sh
399 | [TIME] 6g../..49/- INFO hello Seneca/0.5.20/6g../..49/-
400 | [TIME] f1../..79/- INFO hello Seneca/0.5.20/f1../..79/-
401 | [TIME] f1../..79/- DEBUG act - - IN wdfw.. color:red {color=red} CLIENT
402 | [TIME] 6g../..49/- INFO plugin transport - ACT b01d.. listen open {type=tcp,host=0.0.0.0,port=10201,...}
403 | [TIME] f1../..79/- INFO plugin transport - ACT nid1.. client {type=tcp,host=0.0.0.0,port=10201,...} any
404 | [TIME] 6g../..49/- INFO plugin transport - ACT b01d.. listen connection {type=tcp,host=0.0.0.0,port=10201,...} remote 127.0.0.1 52938
405 | [TIME] 6g../..49/- DEBUG act color - IN bpwi.. color:red {color=red} mcx8i4slu68z UNGATE
406 | [TIME] 6g../..49/- DEBUG act color - OUT bpwi.. color:red {hex=#FF0000} mcx8i4slu68z
407 | [TIME] f1../..79/- DEBUG act - - OUT wdfw.. color:red {hex=#FF0000} CLIENT
408 | ```
409 |
410 | The inbound and outbound log entries are as before. In addition, you
411 | can see the _INFO_ level entries. At startup, Seneca logs a "hello"
412 | entry with the identifier of the current instance execution. This
413 | identifier has the form:
414 | Seneca/[version]/[12-random-chars]/[timestamp]/[tag]
. This
415 | identifier can be used for debugging multi-process message flows. The
416 | second part is a local timestamp. The third is an optional tag, which
417 | you could provide with seneca({tag:'foo'})
, although we
418 | don't use tags in this example.
419 |
420 | There are three _INFO_ level entries of interest. On the server-side,
421 | the listen facility logs the fact that it has opened a TCP port, and
422 | is now listening for connections. Then the client-side logs that it
423 | has opened a connection to the server. And finally the server logs the
424 | same thing.
425 |
426 | As with the HTTP transport example above, you can split this code into
427 | two processes by separating the client and server code. Here's the server:
428 |
429 | ```js
430 | function color() {
431 | this.add( 'color:red', function(args,done){
432 | done(null, {hex:'#FF0000'});
433 | })
434 | }
435 |
436 | var seneca = require('seneca')
437 |
438 | seneca()
439 | .use(color)
440 | .listen({type:'tcp'})
441 | ```
442 |
443 | And here's the client:
444 |
445 | ```js
446 | seneca()
447 | .client({type:'tcp'})
448 | .act('color:red')
449 | ```
450 |
451 | You can cheat by running the HTTP examples with the additional command
452 | line option: --seneca.options.transport.type=tcp
.
453 |
454 | To communicate with a Seneca instance over TCP, you can send a message from the command line that Seneca understands:
455 |
456 | ```sh
457 | # call the color:red action pattern
458 | echo '{"id":"w91/enj","kind":"act","origin":"h5x/146/..77/-","act":{"color":"red"},"sync":true}' | nc 127.0.0.1 10201
459 |
460 | ```
461 |
462 | Seneca answers with a message like:
463 |
464 | ```sh
465 | {"id":"w91/enj","kind":"res","origin":"h5x/146/..77/-","accept":"bj../14../..47/-","time":{"client_sent":..,"listen_recv":..,"listen_sent":..},"sync":true,"res":{"hex":"#FF0000"}}
466 | # the produced result is in the "res" field
467 | ```
468 |
469 | HTTP and TCP are not the only transport mechanisms available. Of
470 | course, in true Seneca-style, the other mechanisms are available as
471 | plugins. Here's the list.
472 |
473 | * [redis-transport](https://github.com/senecajs/seneca-redis-transport): uses redis for a pub-sub message distribution model
474 | * [beanstalk-transport](https://github.com/senecajs/seneca-beanstalk-transport): uses beanstalkd for a message queue
475 | * [balance-client](https://github.com/rjrodger/seneca-balance-client): a load-balancing client transport over multiple Seneca listeners
476 |
477 | If you're written your own transport plugin (see below for
478 | instructions), and want to have it listed here, please submit a pull
479 | request.
480 |
481 |
482 | ## Multiple Channels
483 |
484 | You can use multiple listen
and client
485 | definitions on the same Seneca instance, in any order. By default, a
486 | single client
definition will send all unrecognized
487 | action patterns over the network. When you have multiple client
488 | definitions, it's becuase you want to send some action patterns to one
489 | micro-service, and other patterns to other micro-services. To do this,
490 | you need to specify the patterns you are interested in. In Seneca,
491 | this is done with a `pin`.
492 |
493 | A Seneca `pin` is a pattern for action patterns. You provide a list of
494 | property names and values that must match. Unlike ordinary action
495 | patterns, where the values are fixed, with a `pin`, you can use globs
496 | to match more than one value. For example, let's say you have the patterns:
497 |
498 | * foo:1,bar:zed-aaa
499 | * foo:1,bar:zed-bbb
500 | * foo:1,bar:zed-ccc
501 |
502 | Then you can use these `pins` to pick out the patterns you want:
503 |
504 | * The pin foo:1
matches the patterns foo:1,bar:zed-aaa
and foo:1,bar:zed-bbb
and foo:1,bar:zed-ccc
505 | * The pin foo:1, bar:*
also matches the patterns foo:1,bar:zed-aaa
and foo:1,bar:zed-bbb
and foo:1,bar:zed-ccc
506 | * The pin foo:1, bar:*-aaa
matches only the pattern foo:1,bar:zed-aaa
507 |
508 | Let's extend the color service example. You'll have three separate
509 | services, all running in separate processes. They will listen on ports
510 | 8081, 8082, and 8083 respectively. You'll use command line arguments
511 | for settings. Here's the service code (see
512 | [readme-many-colors-server.js](https://github.com/senecajs/seneca-transport/blob/master/test/stubs/readme-many-colors-server.js)):
513 |
514 | ```js
515 | var color = process.argv[2]
516 | var hexval = process.argv[3]
517 | var port = process.argv[4]
518 |
519 | var seneca = require('seneca')
520 |
521 | seneca()
522 |
523 | .add( 'color:'+color, function(args,done){
524 | done(null, {hex:'#'+hexval});
525 | })
526 |
527 | .listen( port )
528 |
529 | .log.info('color',color,hexval,port)
530 | ```
531 |
532 | This service takes in a color name, a color hexadecimal value, and a
533 | port number from the command line. You can also see how the listen
534 | method can take a single argument, the port number. To offer the
535 | _color:red_ service, run this script with:
536 |
537 | ```sh
538 | $ node readme-many-colors-server.js red FF0000 8081
539 | ```
540 |
541 | And you can test with:
542 |
543 | ```sh
544 | $ curl -d '{"color":"red"}' http://localhost:8081/act
545 | ```
546 |
547 | Of course, you need to use some log filters to pick out the activity
548 | you're interested in. In this case, you've used a
549 | log.info
call to dump out settings. You'll also want to
550 | see the actions as the occur. Try this:
551 |
552 | ```sh
553 | node readme-many-colors-server.js red FF0000 8081 --seneca.log=level:info \
554 | --seneca.log=type:act,regex:color
555 | ```
556 |
557 | And you'll get:
558 |
559 | ```sh
560 | [TIME] mi../..66/- INFO hello Seneca/0.5.20/mi../..66/-
561 | [TIME] mi../..66/- INFO color red FF0000 8081
562 | [TIME] mi../..66/- INFO plugin transport - ACT 7j.. listen {type=web,port=8081,host=0.0.0.0,path=/act,protocol=http,timeout=32778,msgprefix=seneca_,callmax=111111,msgidlen=12,role=transport,hook=listen}
563 | [TIME] mi../..66/- DEBUG act - - IN ux.. color:red {color=red} 9l..
564 | [TIME] mi../..66/- DEBUG act - - OUT ux.. color:red {hex=#FF0000} 9l..
565 | ```
566 |
567 | You can see the custom _INFO_ log entry at the top, and also the transport
568 | settings after that.
569 |
570 | Let's run three of these servers, one each for red, green and
571 | blue. Let's also run a client to connect to them.
572 |
573 | Let's make it interesting. The client will listen
so that it can
574 | handle incoming actions, and pass them on to the appropriate server by
575 | using a pin
. The client will also define a new action that can
576 | aggregate color lookups.
577 |
578 | ```js
579 | var seneca = require('seneca')
580 |
581 | seneca()
582 |
583 | // send matching actions out over the network
584 | .client({ port:8081, pin:'color:red' })
585 | .client({ port:8082, pin:'color:green' })
586 | .client({ port:8083, pin:'color:blue' })
587 |
588 | // an aggregration action that calls other actions
589 | .add( 'list:colors', function( args, done ){
590 | var seneca = this
591 | var colors = {}
592 |
593 | args.names.forEach(function( name ){
594 | seneca.act({color:name}, function(err, result){
595 | if( err ) return done(err);
596 |
597 | colors[name] = result.hex
598 | if( Object.keys(colors).length == args.names.length ) {
599 | return done(null,colors)
600 | }
601 | })
602 | })
603 |
604 | })
605 |
606 | .listen()
607 |
608 | // this is a sanity check
609 | .act({list:'colors',names:['blue','green','red']},console.log)
610 | ```
611 |
612 | This code calls the client
method three times. Each time,
613 | it specifies an action pattern pin
, and a destination port. And
614 | action submitted to this Seneca instance via the act
615 | method will be matched against these pin
patterns. If there is a
616 | match, they will not be processed locally. Instead they will be sent
617 | out over the network to the micro-service that deals with them.
618 |
619 | In this code, you are using the default HTTP transport, and just
620 | changing the port number to connect to. This reflects the fact that
621 | each color micro-service runs on a separate port.
622 |
623 | The `listen` call at the bottom makes this "client" also
624 | listen for inbound messages. So if you run, say the _color:red_
625 | service, and also run the client, then you can send color:red messages
626 | to the client.
627 |
628 | You need to run four processes:
629 |
630 | ```sh
631 | node readme-many-colors-server.js red FF0000 8081 --seneca.log=level:info --seneca.log=type:act,regex:color &
632 | node readme-many-colors-server.js green 00FF00 8082 --seneca.log=level:info --seneca.log=type:act,regex:color &
633 | node readme-many-colors-server.js blue 0000FF 8083 --seneca.log=level:info --seneca.log=type:act,regex:color &
634 | node readme-many-colors-client.js --seneca.log=type:act,regex:CLIENT &
635 |
636 | ```
637 |
638 | And then you can test with:
639 |
640 | ```sh
641 | $ curl -d '{"color":"red"}' http://localhost:10101/act
642 | $ curl -d '{"color":"green"}' http://localhost:10101/act
643 | $ curl -d '{"color":"blue"}' http://localhost:10101/act
644 | ```
645 |
646 | These commands are all going via the client, which is listening on port 10101.
647 |
648 | The client code also includes an aggregation action,
649 | _list:colors_. This lets you call multiple color actions and return
650 | one result. This is a common micro-service pattern.
651 |
652 | The script
653 | [readme-many-colors.sh](https://github.com/senecajs/seneca-transport/blob/master/test/stubs/readme-many-colors.sh)
654 | wraps all this up into one place for you so that it is easy to run.
655 |
656 | Seneca does not require you to use message transports. You can run
657 | everything in one process. But when the time comes, and you need to
658 | scale, or you need to break out micro-services, you have the option to
659 | do so.
660 |
661 |
662 | ## Message Protocols
663 |
664 | There is no message protocol as such, as the data representation of
665 | the underlying message transport is used. However, the plain text
666 | message representation is JSON in all known transports.
667 |
668 | For the HTTP transport, message data is encoded as per the HTTP
669 | protocol. For the TCP transport, UTF8 JSON is used, with one
670 | well-formed JSON object per line (with a single "\n" as line
671 | terminator).
672 |
673 | For other transports, please see the documentation for the underlying
674 | protocol. In general the transport plugins, such as
675 | _seneca-redis-transport_ will handle this for you so that you only
676 | have to think in terms of JavaScript objects.
677 |
678 | The JSON object is a wrapper for the message data. The wrapper contains
679 | some tracking fields to make debugging easier. These are:
680 |
681 | * _id_: action identifier (appears in Seneca logs after IN/OUT)
682 | * _kind_: 'act' for inbound actions, 'res' for outbound responses
683 | * _origin_: identifier of orginating Seneca instance, where action is submitted
684 | * _accept_: identifier of accepting Seneca instance, where action is performed
685 | * _time_:
686 | * _client_sent_: client timestamp when message sent
687 | * _listen_recv_: server timestamp when message received
688 | * _listen_sent_: server timestamp when response sent
689 | * _client_recv_: client timestamp when response received
690 | * _act_: action message data, as submitted to Seneca
691 | * _res_: response message data, as provided by Seneca
692 | * _error_: error message, if any
693 | * _input_: input generating error, if any
694 |
695 |
696 | ## Writing Your Own Transport
697 |
698 | To write your own transport, the best approach is to copy one of the existing ones:
699 |
700 | * [transport.js](https://github.com/senecajs/seneca-transport/blob/master/transport.js): disconnected or point-to-point
701 | * [redis-transport.js](https://github.com/rjrodger/seneca-redis-transport/blob/master/lib/index.js): publish/subscribe
702 | * [beanstalk-transport.js](https://github.com/rjrodger/seneca-beanstalk-transport/blob/master/lib/index.js): message queue
703 |
704 | Choose a _type_ for your transport, say "foo". You will need to
705 | implement two patterns:
706 |
707 | * role:transport, hook:listen, type:foo
708 | * role:transport, hook:client, type:foo
709 |
710 | Rather than writing all of the code yourself, and dealing with all the
711 | messy details, you can take advantage of the built-in message
712 | serialization and error handling by using the utility functions that
713 | the _transport_ plugin exports. These utility functions can be called
714 | in a specific sequence, providing a template for the implementation of
715 | a message transport:
716 |
717 | The transport utility functions provide the concept of topics. Each
718 | message pattern is encoded as a topic string (alphanumeric) that could
719 | be used with a message queue server. You do not need to use topics,
720 | but they can be convenient to separate message flows.
721 |
722 | To implement the client, use the template:
723 |
724 | ```js
725 | var transport_utils = seneca.export('transport/utils')
726 |
727 | function hook_client_redis( args, clientdone ) {
728 | var seneca = this
729 | var type = args.type
730 |
731 | // get your transport type default options
732 | var client_options = seneca.util.clean(_.extend({},options[type],args))
733 |
734 | transport_utils.make_client( make_send, client_options, clientdone )
735 |
736 | // implement your transport here
737 | // see an existing transport for full example
738 | // make_send is called per topic
739 | function make_send( spec, topic, send_done ) {
740 |
741 | // setup topic in transport mechanism
742 |
743 | // send the args over the transport
744 | send_done( null, function( args, done ) {
745 |
746 | // create message JSON
747 | var outbound_message = transport_utils.prepare_request( seneca, args, done )
748 |
749 | // send JSON using your transport API
750 |
751 | // don't call done - that only happens if there's a response!
752 | // this will be done for you
753 | })
754 | }
755 | }
756 | ```
757 |
758 | To implement the server, use the template:
759 |
760 | ```js
761 | var transport_utils = seneca.export('transport/utils')
762 |
763 | function hook_listen_redis( args, done ) {
764 | var seneca = this
765 | var type = args.type
766 |
767 | // get your transport type default options
768 | var listen_options = seneca.util.clean(_.extend({},options[type],args))
769 |
770 | // get the list of topics
771 | var topics = tu.listen_topics( seneca, args, listen_options )
772 |
773 | topics.forEach( function(topic) {
774 |
775 | // "listen" on the topic - implementation dependent!
776 |
777 | // handle inbound messages
778 | transport_utils.handle_request( seneca, data, listen_options, function(out){
779 |
780 | // there may be no result!
781 | if( null == out ) return ...;
782 |
783 | // otherwise, send the result back
784 | // don't forget to stringifyJSON(out) if necessary
785 | })
786 | })
787 | }
788 | ```
789 |
790 | If you do not wish to use a template, you can implement transports
791 | using entirely custom code. In this case, you need to need to provide
792 | results from the _hook_ actions. For the _role:transport,hook:listen_
793 | action, this is easy, as no result is required. For
794 | _role:transport,hook:client_, you need to provide an object with
795 | properties:
796 |
797 | * `id`: an identifier for the client
798 | * `toString`: a string description for debug logs
799 | * `match( args )`: return _true_ if the client can transport the given args (i.e. they match the client action pattern)
800 | * `send( args, done )`: a function that performs the transport, and calls `done` with the result when received
801 |
802 | See the `make_anyclient` and `make_pinclient` functions in
803 | [transport.js](transport.js) for implementation examples.
804 |
805 | Message transport code should be written very carefully as it will be
806 | subject to high load and many error conditions.
807 |
808 |
809 | ## Plugin Options
810 |
811 | The transport plugin family uses an extension to the normal Seneca
812 | options facility. As well as supporting the standard method for
813 | defining options (see [How to Write a
814 | Plugin](http://senecajs.org/tutorials/how-to-write-a-plugin.html#wp-options)), you can
815 | also supply options via arguments to the client
or
816 | listen
methods, and via the type name of the transport
817 | under the top-level _transport_ property.
818 |
819 | The primary options are:
820 |
821 | * _msgprefix_: a string to prefix to topic names so that they are namespaced
822 | * _callmax_: the maximum number of in-flight request/response messages to cache
823 | * _msgidlen_: length of the message indentifier string
824 |
825 | These can be set within the top-level _transport_ property of the main
826 | Seneca options tree:
827 |
828 | ```js
829 | var seneca = require('seneca')
830 | seneca({
831 | transport:{
832 | msgprefix:'foo'
833 | }
834 | })
835 | ```
836 |
837 | Each transport type forms a sub-level within the _transport_
838 | option. The recognized types depend on the transport plugins you have
839 | loaded. By default, _web_ and _tcp_ are available. To use _redis_, for example, you
840 | need to do this:
841 |
842 | ```js
843 | var seneca = require('seneca')
844 | seneca({
845 | transport:{
846 | redis:{
847 | timeout:500
848 | }
849 | }
850 | })
851 |
852 | // assumes npm install seneca-redis-transport
853 | .use('redis-transport')
854 |
855 | .listen({type:'redis'})
856 | ```
857 |
858 | You can set transport-level options inside the type property:
859 |
860 | ```js
861 | var seneca = require('seneca')
862 | seneca({
863 | transport:{
864 | tcp:{
865 | timeout:1000
866 | }
867 | }
868 | })
869 | ```
870 |
871 | The transport-level options vary by transport. Here are the default ones for HTTP:
872 |
873 | * _type_: type name; constant: 'web'
874 | * _port_: port number; default: 10101
875 | * _host_: hostname; default: '0.0.0.0' (all interfaces)
876 | * _path_: URL path to submit messages; default: '/act'
877 | * _protocol_: HTTP protocol; default 'http'
878 | * _timeout_: timeout in milliseconds; default: 5555
879 | * _headers_: extra headers to include in requests the transport makes; default {}
880 |
881 | And for TCP:
882 |
883 | * _type_: type name; constant: 'tcp'
884 | * _port_: port number; default: 10201
885 | * _host_: hostname; default: '0.0.0.0' (all interfaces)
886 | * _timeout_: timeout in milliseconds; default: 5555
887 |
888 | The client
and listen
methods accept an
889 | options object as the primary way to specify options:
890 |
891 | ```js
892 | var seneca = require('seneca')
893 | seneca()
894 | .client({timeout:1000})
895 | .listen({timeout:2000})
896 | ```
897 |
898 | As a convenience, you can specify the port and host as optional arguments:
899 |
900 | ```js
901 | var seneca = require('seneca')
902 | seneca()
903 | .client( 8080 )
904 | .listen( 9090, 'localhost')
905 | ```
906 |
907 | To see the options actually in use at any time, you can call the
908 | seneca.options()
method. Or try
909 |
910 | ```sh
911 | $ node seneca-script.js --seneca.log=type:options
912 | ```
913 |
914 | ## Releases
915 |
916 | * 0.9.0: Fixes from @technicallyjosh; proper glob matching with patrun 5.x
917 | * 0.7.1: fixed log levels
918 | * 0.7.0: all logs now debug level
919 | * 0.2.6: fixed error transmit bug https://github.com/senecajs/seneca/issues/63
920 |
921 | ## Testing with Docker Compose
922 |
923 | With docker-machine and docker-compose installed run the following commands:
924 |
925 | ```
926 | docker-compose build
927 | docker-compose up
928 | ```
929 |
930 | The output will be the stdout from the server and client logs. You should also
931 | see the client instance outputting the result from the server: `{ hex: '#FF0000' }`
932 |
933 | ## Contributing
934 |
935 | The [Senecajs org][] encourage open participation. If you feel you can help in any way, be it with
936 | documentation, examples, extra testing, or new features please get in touch.
937 |
938 | ## Test
939 |
940 | To run tests, simply use npm:
941 |
942 | ```sh
943 | npm run test
944 | ```
945 |
946 | ## License
947 |
948 | Copyright (c) 2013-2016, Richard Rodger and other contributors.
949 | Licensed under [MIT][].
950 |
951 | [npm-badge]: https://img.shields.io/npm/v/seneca-transport.svg
952 | [npm-url]: https://npmjs.com/package/seneca-transport
953 | [travis-badge]: https://travis-ci.org/senecajs/seneca-transport.svg
954 | [travis-url]: https://travis-ci.org/senecajs/seneca-transport
955 | [gitter-badge]: https://badges.gitter.im/Join%20Chat.svg
956 | [gitter-url]: https://gitter.im/senecajs/seneca
957 | [david-badge]: https://david-dm.org/senecajs/seneca-transport.svg
958 | [david-url]: https://david-dm.org/senecajs/seneca-transport
959 | [MIT]: ./LICENSE
960 | [Senecajs org]: https://github.com/senecajs/
961 | [Seneca.js]: https://www.npmjs.com/package/seneca
962 | [senecajs.org]: http://senecajs.org/
963 | [leveldb]: http://leveldb.org/
964 | [github issue]: https://github.com/senecajs/seneca-transport/issues
965 | [@senecajs]: http://twitter.com/senecajs
966 |
--------------------------------------------------------------------------------
/coverage/empty.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/senecajs/seneca-transport/f03ac3a634e1da21553b07ef74eae2938eaf4f9c/coverage/empty.txt
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | base:
2 | build: ./
3 | server:
4 | extends:
5 | service: base
6 | expose:
7 | - "8000"
8 | entrypoint: node /seneca-transport/server
9 | client:
10 | extends:
11 | service: base
12 | links:
13 | - server
14 | entrypoint: node /seneca-transport/client
15 |
--------------------------------------------------------------------------------
/docs/create-a-release.md:
--------------------------------------------------------------------------------
1 | # Creating a release
2 |
3 | 1. Review github issues, triage, close and merge issues related to the release.
4 | 2. Update CHANGES.md, with date release, notes, and version.
5 | 3. Pull down the repository locally on the master branch.
6 | 4. Ensure there are no outstanding commits and the branch is clean.
7 | 5. Run `npm install` and ensure all dependencies correctly install.
8 | 6. Run `npm run test` and ensure testing and linting passes.
9 | 7. Run `npm version vx.x.x -m "version x.x.x"` where `x.x.x` is the version.
10 | 8. Run `git push upstream master --tags`
11 | 9. Run `npm publish`
12 | 10. Go to the [Github release page][Releases] and hit 'Draft a new release'.
13 | 11. Paste the Changelog content for this release and add additional release notes.
14 | 12. Choose the tag version and a title matching the release and publish.
15 | 13. Notify core maintainers of the release via email.
16 |
17 | [Releases]: https://github.com/senecajs/seneca-transport/releases
18 |
--------------------------------------------------------------------------------
/docs/examples/simple-http/http-client.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | let seneca = require('seneca')()
4 | seneca.use('../../../transport').ready(function () {
5 | this.act({foo: 'one', bar: 'aloha'}, function (err, response) {
6 | if (err) {
7 | return console.log(err)
8 | }
9 | console.log(response)
10 | })
11 | }).client({type: 'http', pin: 'foo:one'})
12 |
--------------------------------------------------------------------------------
/docs/examples/simple-http/http-listen.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | let seneca = require('seneca')()
4 | seneca.use('../../../transport').ready(function () {
5 | this.add({foo: 'one'}, function (args, done) {
6 | done(null, {bar: args.bar})
7 | })
8 | })
9 |
10 | seneca.listen({type: 'http', pin: 'foo:one'})
11 |
--------------------------------------------------------------------------------
/lib/http.js:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2013-2015 Richard Rodger, MIT License */
2 | /* jshint node:true, asi:true, eqnull:true */
3 | 'use strict'
4 |
5 | // Load modules
6 | var Buffer = require('buffer')
7 | var Http = require('http')
8 | var Https = require('https')
9 | var Qs = require('qs')
10 | var Url = require('url')
11 | // var Jsonic = require('jsonic')
12 | var Wreck = require('@hapi/wreck')
13 | var Omit = require('lodash.omit')
14 |
15 | // Declare internals
16 | var internals = {}
17 |
18 | exports.listen = function (options, transportUtil) {
19 | return function (msg, callback) {
20 | var seneca = this.root.delegate()
21 |
22 | var listenOptions = seneca.util.deepextend(options[msg.type], msg)
23 |
24 | var server =
25 | listenOptions.protocol === 'https'
26 | ? Https.createServer(listenOptions.serverOptions)
27 | : Http.createServer()
28 |
29 | var listener
30 | var listenAttempts = 0
31 | var listen_details = seneca.util.deep(msg)
32 |
33 | server.on('request', function (req, res) {
34 | internals.timeout(listenOptions, req, res)
35 | req.query = Qs.parse(Url.parse(req.url).query)
36 | internals.setBody(seneca, transportUtil, req, res, function (err) {
37 | if (err) {
38 | return res.end()
39 | }
40 |
41 | internals.trackHeaders(listenOptions, seneca, transportUtil, req, res)
42 | })
43 | })
44 |
45 | server.on('error', function (err) {
46 | if (
47 | 'EADDRINUSE' === err.code &&
48 | listenAttempts < listenOptions.max_listen_attempts
49 | ) {
50 | listenAttempts++
51 | seneca.log.warn(
52 | 'listen',
53 | 'attempt',
54 | listenAttempts,
55 | err.code,
56 | listenOptions,
57 | )
58 | setTimeout(
59 | listen,
60 | 100 + Math.floor(Math.random() * listenOptions.attempt_delay),
61 | )
62 | return
63 | }
64 | callback(err)
65 | })
66 |
67 | server.on('listening', function () {
68 | listen_details.port = server.address().port
69 | seneca.log.debug('listen', listen_details)
70 | callback(null, listen_details)
71 | })
72 |
73 | function listen() {
74 | listener = server.listen(
75 | (listen_details.port = transportUtil.resolveDynamicValue(
76 | listenOptions.port,
77 | listenOptions,
78 | )),
79 | (listen_details.host = transportUtil.resolveDynamicValue(
80 | listenOptions.host,
81 | listenOptions,
82 | )),
83 | )
84 | }
85 |
86 | transportUtil.close(seneca, function (done) {
87 | // node 0.10 workaround, otherwise it throws
88 | if (listener && listener._handle) {
89 | listener.close()
90 | }
91 | done()
92 | })
93 |
94 | listen()
95 | }
96 | }
97 |
98 | exports.client = function (options, transportUtil) {
99 | return function (msg, callback) {
100 | var seneca = this.root.delegate()
101 |
102 | var clientOptions = seneca.util.deepextend(options[msg.type], msg)
103 | var defaultHeaders = null
104 |
105 | // these are seneca internal, users are not allowed to change them
106 | if (options[msg.type].headers) {
107 | defaultHeaders = Omit(options[msg.type].headers, [
108 | 'Accept',
109 | 'Content-Type',
110 | 'Content-Length',
111 | 'Cache-Control',
112 | 'seneca-id',
113 | 'seneca-kind',
114 | 'seneca-origin',
115 | 'seneca-track',
116 | 'seneca-time-client-sent',
117 | 'seneca-accept',
118 | 'seneca-time-listen-recv',
119 | 'seneca-time-listen-sent',
120 | ])
121 | }
122 |
123 | var send = function (spec, topic, send_done) {
124 | var host = transportUtil.resolveDynamicValue(
125 | clientOptions.host,
126 | clientOptions,
127 | )
128 | var port = transportUtil.resolveDynamicValue(
129 | clientOptions.port,
130 | clientOptions,
131 | )
132 | var path = transportUtil.resolveDynamicValue(
133 | clientOptions.path,
134 | clientOptions,
135 | )
136 |
137 | // never use a 0.0.0.0 as targeted host, because Windows can't handle it
138 | host = host === '0.0.0.0' ? '127.0.0.1' : host
139 |
140 | var url = clientOptions.protocol + '://' + host + ':' + port + path
141 | seneca.log.debug('client', 'web', 'send', spec, topic, clientOptions, url)
142 |
143 | function action(msg, done, meta) {
144 | var data = transportUtil.prepare_request(this, msg, done, meta)
145 |
146 | var headers = {
147 | Accept: 'application/json',
148 | 'Content-Type': 'application/json',
149 | 'seneca-id': data.id,
150 | 'seneca-kind': 'req',
151 | 'seneca-origin': seneca.id,
152 | 'seneca-track': transportUtil.stringifyJSON(
153 | seneca,
154 | 'send-track',
155 | data.track || [],
156 | ),
157 | 'seneca-time-client-sent': data.time.client_sent,
158 | }
159 |
160 | if (defaultHeaders) {
161 | headers = Object.assign(headers, defaultHeaders)
162 | }
163 |
164 | var requestOptions = {
165 | json: true,
166 | headers: headers,
167 | timeout: clientOptions.timeout,
168 | payload: JSON.stringify(data.act),
169 | }
170 |
171 | var postP = Wreck.post(url, requestOptions)
172 |
173 | postP
174 | .then(function (out) {
175 | handle_post(null, out.res, out.payload)
176 | })
177 | .catch(function (err) {
178 | handle_post(err)
179 | })
180 |
181 | function handle_post(err, res, payload) {
182 | var response = {
183 | kind: 'res',
184 | res: payload && 'object' === typeof payload ? payload : null,
185 | error: err,
186 | sync: (msg.meta$ || meta).sync,
187 | }
188 |
189 | if (res) {
190 | response.id = res.headers['seneca-id']
191 | response.origin = res.headers['seneca-origin']
192 | response.accept = res.headers['seneca-accept']
193 | response.time = {
194 | client_sent: res.headers['seneca-time-client-sent'],
195 | listen_recv: res.headers['seneca-time-listen-recv'],
196 | listen_sent: res.headers['seneca-time-listen-sent'],
197 | }
198 |
199 | if (res.statusCode !== 200) {
200 | response.error = payload
201 | }
202 | } else {
203 | response.id = data.id
204 | response.origin = seneca.id
205 | }
206 |
207 | transportUtil.handle_response(seneca, response, clientOptions)
208 | }
209 | }
210 |
211 | send_done(null, action)
212 |
213 | transportUtil.close(seneca, function (done) {
214 | done()
215 | })
216 | }
217 | transportUtil.make_client(seneca, send, clientOptions, callback)
218 | }
219 | }
220 |
221 | internals.setBody = function (seneca, transportUtil, req, res, next) {
222 | var buf = []
223 | req.setEncoding('utf8')
224 | req.on('data', function (chunk) {
225 | buf.push(chunk)
226 | })
227 | req.on('end', function () {
228 | try {
229 | var bufstr = buf.join('')
230 |
231 | var bodydata = bufstr.length
232 | ? transportUtil.parseJSON(seneca, 'req-body', bufstr)
233 | : {}
234 |
235 | if (bodydata instanceof Error) {
236 | var out = transportUtil.prepareResponse(seneca, {})
237 | out.input = bufstr
238 | out.error = transportUtil.error('invalid_json', { input: bufstr })
239 | internals.sendResponse(seneca, transportUtil, res, out, {})
240 | return
241 | }
242 |
243 | req.body = Object.assign(
244 | {},
245 | bodydata,
246 |
247 | // deprecated
248 | req.query && req.query.args$ ? seneca.util.Jsonic(req.query.args$) : {},
249 |
250 | req.query && req.query.msg$ ? seneca.util.Jsonic(req.query.msg$) : {},
251 | req.query || {},
252 | )
253 |
254 | next()
255 | } catch (err) {
256 | res.write(err.message + ': ' + bufstr)
257 | res.statusCode = 400
258 | next(err)
259 | }
260 | })
261 | }
262 |
263 | internals.trackHeaders = function (
264 | listenOptions,
265 | seneca,
266 | transportUtil,
267 | req,
268 | res,
269 | ) {
270 | if (Url.parse(req.url).pathname !== listenOptions.path) {
271 | res.statusCode = 404
272 | return res.end()
273 | }
274 | var data
275 | if (req.headers['seneca-id']) {
276 | data = {
277 | id: req.headers['seneca-id'],
278 | kind: 'act',
279 | origin: req.headers['seneca-origin'],
280 | track:
281 | transportUtil.parseJSON(
282 | seneca,
283 | 'track-receive',
284 | req.headers['seneca-track'],
285 | ) || [],
286 | time: {
287 | client_sent: req.headers['seneca-time-client-sent'],
288 | },
289 | act: req.body,
290 | }
291 | }
292 |
293 | // convenience for non-seneca clients
294 | if (!req.headers['seneca-id']) {
295 | data = {
296 | id: seneca.idgen(),
297 | kind: 'act',
298 | origin: req.headers['user-agent'] || 'UNKNOWN',
299 | track: [],
300 | time: {
301 | client_sent: Date.now(),
302 | },
303 | act: req.body,
304 | }
305 | }
306 |
307 | transportUtil.handle_request(seneca, data, listenOptions, function (out) {
308 | internals.sendResponse(seneca, transportUtil, res, out, data)
309 | })
310 | }
311 |
312 | internals.sendResponse = function (seneca, transportUtil, res, out, data) {
313 | var outJson = 'null'
314 | var httpcode = 200
315 |
316 | if (out && out.res) {
317 | httpcode = out.res.statusCode || httpcode
318 | outJson = transportUtil.stringifyJSON(seneca, 'listen-web', out.res)
319 | } else if (out && out.error) {
320 | httpcode = out.error.statusCode || 500
321 | outJson = transportUtil.stringifyJSON(seneca, 'listen-web', out.error)
322 | }
323 |
324 | var headers = {
325 | 'Content-Type': 'application/json',
326 | 'Cache-Control': 'private, max-age=0, no-cache, no-store',
327 | 'Content-Length': Buffer.Buffer.byteLength(outJson),
328 | }
329 |
330 | headers['seneca-id'] = out && out.id ? out.id : seneca.id
331 | headers['seneca-kind'] = 'res'
332 | headers['seneca-origin'] = out && out.origin ? out.origin : 'UNKNOWN'
333 | headers['seneca-accept'] = seneca.id
334 | headers['seneca-track'] = '' + (data.track ? data.track : [])
335 | headers['seneca-time-client-sent'] =
336 | out && out.item ? out.time.client_sent : '0'
337 | headers['seneca-time-listen-recv'] =
338 | out && out.item ? out.time.listen_recv : '0'
339 | headers['seneca-time-listen-sent'] =
340 | out && out.item ? out.time.listen_sent : '0'
341 |
342 | res.writeHead(httpcode, headers)
343 | res.end(outJson)
344 | }
345 |
346 | internals.timeout = function (listenOptions, req, res) {
347 | var id = setTimeout(function () {
348 | res.statusCode = 503
349 | res.statusMessage = 'Response timeout'
350 | res.end('{ "code": "ETIMEDOUT" }')
351 | }, listenOptions.timeout || 5000)
352 |
353 | var clearTimeoutId = function () {
354 | clearTimeout(id)
355 | }
356 |
357 | req.once('close', clearTimeoutId)
358 | req.once('error', clearTimeoutId)
359 | res.once('error', clearTimeoutId)
360 | if (res.socket) {
361 | res.socket.once('data', clearTimeoutId)
362 | } else {
363 | clearTimeoutId()
364 | }
365 | }
366 |
--------------------------------------------------------------------------------
/lib/tcp.js:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2013-2015 Richard Rodger, MIT License */
2 | /* jshint node:true, asi:true, eqnull:true */
3 | 'use strict'
4 |
5 | // Load modules
6 | var Net = require('net')
7 | var Stream = require('stream')
8 | var Ndjson = require('ndjson')
9 | var Reconnect = require('reconnect-core')
10 |
11 | // Declare internals
12 | var internals = {}
13 |
14 | exports.listen = function (options, transportUtil) {
15 | return function (args, callback) {
16 | var seneca = this.root.delegate()
17 |
18 | var listenOptions = seneca.util.deepextend(options[args.type], args)
19 |
20 | var connections = []
21 | var listenAttempts = 0
22 |
23 | var listener = Net.createServer(function (connection) {
24 | seneca.log.debug(
25 | 'listen',
26 | 'connection',
27 | listenOptions,
28 | 'remote',
29 | connection.remoteAddress,
30 | connection.remotePort,
31 | )
32 |
33 | var parser = Ndjson.parse()
34 | var stringifier = Ndjson.stringify()
35 | parser.on('error', function (error) {
36 | console.error(error)
37 | connection.end()
38 | })
39 | parser.on('data', function (data) {
40 | if (data instanceof Error) {
41 | var out = transportUtil.prepareResponse(seneca, {})
42 | out.input = data.input
43 | out.error = transportUtil.error('invalid_json', { input: data.input })
44 |
45 | stringifier.write(out)
46 | return
47 | }
48 |
49 | transportUtil.handle_request(seneca, data, options, function (out) {
50 | if (out === null || !out.sync) {
51 | return
52 | }
53 |
54 | stringifier.write(out)
55 | })
56 | })
57 |
58 | connection.pipe(parser)
59 | stringifier.pipe(connection)
60 |
61 | connection.on('error', function (err) {
62 | seneca.log.error(
63 | 'listen',
64 | 'pipe-error',
65 | listenOptions,
66 | err && err.stack,
67 | )
68 | })
69 |
70 | connections.push(connection)
71 | })
72 |
73 | listener.once('listening', function () {
74 | listenOptions.port = listener.address().port
75 | seneca.log.debug('listen', 'open', listenOptions)
76 | return callback(null, listenOptions)
77 | })
78 |
79 | listener.on('error', function (err) {
80 | seneca.log.error('listen', 'net-error', listenOptions, err && err.stack)
81 |
82 | if (
83 | 'EADDRINUSE' === err.code &&
84 | listenAttempts < listenOptions.max_listen_attempts
85 | ) {
86 | listenAttempts++
87 | seneca.log.warn(
88 | 'listen',
89 | 'attempt',
90 | listenAttempts,
91 | err.code,
92 | listenOptions,
93 | )
94 | setTimeout(
95 | listen,
96 | 100 + Math.floor(Math.random() * listenOptions.attempt_delay),
97 | )
98 | return
99 | }
100 | })
101 |
102 | listener.on('close', function () {
103 | seneca.log.debug('listen', 'close', listenOptions)
104 | })
105 |
106 | function listen() {
107 | if (listenOptions.path) {
108 | listener.listen(listenOptions.path)
109 | } else {
110 | listener.listen(listenOptions.port, listenOptions.host)
111 | }
112 | }
113 | listen()
114 |
115 | transportUtil.close(seneca, function (next) {
116 | // node 0.10 workaround, otherwise it throws
117 | if (listener._handle) {
118 | listener.close()
119 | }
120 | internals.closeConnections(connections, seneca)
121 | next()
122 | })
123 | }
124 | }
125 |
126 | exports.client = function (options, transportUtil) {
127 | return function (args, callback) {
128 | var seneca = this.root.delegate()
129 | var conStream
130 | var connection
131 | var established = false
132 | var stringifier
133 |
134 | var type = args.type
135 | if (args.host) {
136 | // under Windows host, 0.0.0.0 host will always fail
137 | args.host = args.host === '0.0.0.0' ? '127.0.0.1' : args.host
138 | }
139 | var clientOptions = seneca.util.deepextend(options[args.type], args)
140 | clientOptions.host =
141 | !args.host && clientOptions.host === '0.0.0.0'
142 | ? '127.0.0.1'
143 | : clientOptions.host
144 |
145 | var connect = function () {
146 | seneca.log.debug('client', type, 'send-init', '', '', clientOptions)
147 |
148 | var reconnect = internals.reconnect(function (stream) {
149 | conStream = stream
150 | var msger = internals.clientMessager(
151 | seneca,
152 | clientOptions,
153 | transportUtil,
154 | )
155 | var parser = Ndjson.parse()
156 | stringifier = Ndjson.stringify()
157 |
158 | stream.pipe(parser).pipe(msger).pipe(stringifier).pipe(stream)
159 |
160 | if (!established) reconnect.emit('s_connected', stringifier)
161 | established = true
162 | })
163 |
164 | reconnect.on('connect', function (connection) {
165 | seneca.log.debug('client', type, 'connect', '', '', clientOptions)
166 | // connection.clientOptions = clientOptions // unique per connection
167 | // connections.push(connection)
168 | // established = true
169 | })
170 |
171 | reconnect.on('reconnect', function () {
172 | seneca.log.debug('client', type, 'reconnect', '', '', clientOptions)
173 | })
174 | reconnect.on('disconnect', function (err) {
175 | seneca.log.debug(
176 | 'client',
177 | type,
178 | 'disconnect',
179 | '',
180 | '',
181 | clientOptions,
182 | (err && err.stack) || err,
183 | )
184 |
185 | established = false
186 | })
187 | reconnect.on('error', function (err) {
188 | seneca.log.debug(
189 | 'client',
190 | type,
191 | 'error',
192 | '',
193 | '',
194 | clientOptions,
195 | err.stack,
196 | )
197 | })
198 |
199 | reconnect.connect({
200 | port: clientOptions.port,
201 | host: clientOptions.host,
202 | })
203 |
204 | transportUtil.close(seneca, function (done) {
205 | reconnect.disconnect()
206 | internals.closeConnections([conStream], seneca)
207 | done()
208 | })
209 |
210 | return reconnect
211 | }
212 |
213 | function getClient(cb) {
214 | if (!connection) connection = connect()
215 | if (established) {
216 | cb(stringifier)
217 | } else {
218 | connection.once('s_connected', cb)
219 | }
220 | }
221 |
222 | var send = function (spec, topic, send_done) {
223 | send_done(null, function (args, done, meta) {
224 | var self = this
225 | getClient(function (stringifier) {
226 | var outmsg = transportUtil.prepare_request(self, args, done, meta)
227 | if (!outmsg.replied) stringifier.write(outmsg)
228 | })
229 | })
230 | }
231 |
232 | transportUtil.make_client(seneca, send, clientOptions, callback)
233 | }
234 | }
235 |
236 | internals.clientMessager = function (seneca, options, transportUtil) {
237 | var messager = new Stream.Duplex({ objectMode: true })
238 | messager._read = function () {}
239 | messager._write = function (data, enc, callback) {
240 | transportUtil.handle_response(seneca, data, options)
241 | return callback()
242 | }
243 | return messager
244 | }
245 |
246 | internals.closeConnections = function (connections, seneca) {
247 | for (var i = 0, il = connections.length; i < il; ++i) {
248 | internals.destroyConnection(connections[i], seneca)
249 | }
250 | }
251 |
252 | internals.destroyConnection = function (connection, seneca) {
253 | try {
254 | connection.destroy()
255 | } catch (e) {
256 | seneca.log.error(e)
257 | }
258 | }
259 |
260 | internals.reconnect = Reconnect(function () {
261 | var args = [].slice.call(arguments)
262 | return Net.connect.apply(null, args)
263 | })
264 |
--------------------------------------------------------------------------------
/lib/transport-utils.js:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2015-2017 Richard Rodger, MIT License */
2 | 'use strict'
3 |
4 | var Util = require('util')
5 |
6 | // var Nid = require('nid')
7 | // var Patrun = require('patrun')
8 | // var Jsonic = require('jsonic')
9 | var Eraro = require('eraro')
10 | var Each = require('lodash.foreach')
11 |
12 | // Declare internals
13 | var internals = {
14 | error: Eraro({
15 | package: 'seneca',
16 | msgmap: {
17 | no_data: 'The message has no data.',
18 | invalid_kind_act:
19 | 'Inbound messages should have kind "act", kind was: <%=kind%>.',
20 | no_message_id: 'The message has no identifier.',
21 | invalid_origin:
22 | 'The message response is not for this instance, origin was <%=origin%>.',
23 | unknown_message_id: 'The message has an unknown identifier',
24 | own_message: 'Inbound message rejected as originated from this server.',
25 | message_loop: 'Inbound message rejected as looping back to this server.',
26 | data_error: 'Inbound message included an error description.',
27 | invalid_json: 'Invalid JSON: <%=input%>.',
28 | unexcepted_async_error:
29 | 'Unexcepted error response to asynchronous message.',
30 | },
31 | override: true,
32 | }),
33 | }
34 |
35 | module.exports = internals.Utils = function (context) {
36 | this._msgprefix = !context.options.msgprefix ? '' : context.options.msgprefix
37 | this._context = context
38 | }
39 |
40 | internals.Utils.prototype.error = internals.error // fixes #63
41 |
42 | internals.Utils.prototype.handle_response = function (
43 | seneca,
44 | data,
45 | client_options,
46 | ) {
47 | data.time = data.time || {}
48 | data.time.client_recv = Date.now()
49 | data.sync = void 0 === data.sync ? true : data.sync
50 |
51 | if (data.kind !== 'res') {
52 | if (this._context.options.warn.invalid_kind) {
53 | seneca.log.warn('client', 'invalid_kind_res', client_options, data)
54 | }
55 | return false
56 | }
57 |
58 | if (data.id === null) {
59 | if (this._context.options.warn.no_message_id) {
60 | seneca.log.warn('client', 'no_message_id', client_options, data)
61 | }
62 | return false
63 | }
64 |
65 | if (seneca.id !== data.origin) {
66 | if (this._context.options.warn.invalid_origin) {
67 | seneca.log.warn('client', 'invalid_origin', client_options, data)
68 | }
69 | return false
70 | }
71 |
72 | var err = null
73 | var result = null
74 |
75 | if (data.error) {
76 | err = new Error(data.error.message)
77 |
78 | Each(data.error, function (value, key) {
79 | err[key] = value
80 | })
81 |
82 | if (!data.sync) {
83 | seneca.log.warn(
84 | 'client',
85 | 'unexcepted_async_error',
86 | client_options,
87 | data,
88 | err,
89 | )
90 | return true
91 | }
92 | } else {
93 | result = this.handle_entity(seneca, data.res)
94 | }
95 |
96 | if (!data.sync) {
97 | return true
98 | }
99 |
100 | var callmeta = this._context.callmap.get(data.id)
101 |
102 | if (callmeta) {
103 | this._context.callmap.delete(data.id)
104 | } else {
105 | if (this._context.options.warn.unknown_message_id) {
106 | seneca.log.warn('client', 'unknown_message_id', client_options, data)
107 | }
108 | return false
109 | }
110 |
111 | var actinfo = {
112 | id: data.id,
113 | accept: data.accept,
114 | track: data.track,
115 | time: data.time,
116 | }
117 |
118 | this.callmeta({
119 | callmeta: callmeta,
120 | err: err,
121 | result: result,
122 | actinfo: actinfo,
123 | seneca: seneca,
124 | client_options: client_options,
125 | data: data,
126 | })
127 |
128 | return true
129 | }
130 |
131 | internals.Utils.prototype.callmeta = function (options) {
132 | try {
133 | options.callmeta.done(options.err, options.result, options.actinfo)
134 | } catch (e) {
135 | options.seneca.log.error(
136 | 'client',
137 | 'callback_error',
138 | options.client_options,
139 | options.data,
140 | e.stack || e,
141 | )
142 | }
143 | }
144 |
145 | internals.Utils.prototype.prepare_request = function (
146 | seneca,
147 | args,
148 | done,
149 | meta,
150 | ) {
151 | var meta$ = args.meta$ || meta || {}
152 |
153 | // FIX: this is mutating args.meta$ - sync should be inited elsewhere
154 | meta$.sync = void 0 === meta$.sync ? true : meta$.sync
155 |
156 | var callmeta = {
157 | args: args,
158 | //done: _.bind(done, seneca),
159 | done: done.bind(seneca),
160 | when: Date.now(),
161 | }
162 |
163 | // store callback only if sync is response expected
164 | if (meta$.sync) {
165 | this._context.callmap.set(meta$.id, callmeta)
166 | } else {
167 | this.callmeta({
168 | callmeta: callmeta,
169 | err: null,
170 | result: null,
171 | actinfo: null,
172 | seneca: seneca,
173 | client_options: null,
174 | data: null,
175 | })
176 | }
177 |
178 | var track = []
179 | if (args.transport$) {
180 | track = seneca.util.deep(args.transport$.track || [])
181 | }
182 | track.push(seneca.id)
183 |
184 | var output = {
185 | id: meta$.id,
186 | kind: 'act',
187 | origin: seneca.id,
188 | track: track,
189 | time: { client_sent: Date.now() },
190 | act: seneca.util.clean(args),
191 | sync: meta$.sync,
192 | }
193 |
194 | // workaround to send meta.custom object go along with transport
195 | if (meta && meta.custom) {
196 | output.act.custom$ = (meta && meta.custom) || undefined
197 | }
198 |
199 | output.msg$ = {
200 | vin: 1,
201 | sid: seneca.id,
202 | out: true,
203 | mid: meta$.mi,
204 | cid: meta$.tx,
205 | snc: meta$.sync,
206 | pat: meta$.pattern,
207 | }
208 |
209 | return output
210 | }
211 |
212 | internals.Utils.prototype.handle_request = function (
213 | seneca,
214 | data,
215 | listen_options,
216 | respond,
217 | ) {
218 | if (!data) {
219 | return respond({ input: data, error: internals.error('no_data') })
220 | }
221 |
222 | // retain transaction information from incoming request
223 | var ids = data.id && data.id.split('/')
224 | var tx = ids && ids[1]
225 | seneca.fixedargs.tx$ = tx || seneca.fixedargs.tx$
226 |
227 | if (data.kind !== 'act') {
228 | if (this._context.options.warn.invalid_kind) {
229 | seneca.log.warn('listen', 'invalid_kind_act', listen_options, data)
230 | }
231 | return respond({
232 | input: data,
233 | error: internals.error('invalid_kind_act', { kind: data.kind }),
234 | })
235 | }
236 |
237 | if (data.id === null) {
238 | if (this._context.options.warn.no_message_id) {
239 | seneca.log.warn('listen', 'no_message_id', listen_options, data)
240 | }
241 | return respond({ input: data, error: internals.error('no_message_id') })
242 | }
243 |
244 | if (
245 | this._context.options.check.own_message &&
246 | this._context.callmap.has(data.id)
247 | ) {
248 | if (this._context.options.warn.own_message) {
249 | seneca.log.warn('listen', 'own_message', listen_options, data)
250 | }
251 | return respond({ input: data, error: internals.error('own_message') })
252 | }
253 |
254 | if (this._context.options.check.message_loop && Array.isArray(data.track)) {
255 | for (var i = 0; i < data.track.length; i++) {
256 | if (seneca.id === data.track[i]) {
257 | if (this._context.options.warn.message_loop) {
258 | seneca.log.warn('listen', 'message_loop', listen_options, data)
259 | }
260 | return respond({ input: data, error: internals.error('message_loop') })
261 | }
262 | }
263 | }
264 |
265 | if (data.error) {
266 | seneca.log.error('listen', 'data_error', listen_options, data)
267 | return respond({ input: data, error: internals.error('data_error') })
268 | }
269 |
270 | var output = this.prepareResponse(seneca, data)
271 | var input = this.handle_entity(seneca, data.act)
272 |
273 | input.transport$ = {
274 | track: data.track || [],
275 | origin: data.origin,
276 | time: data.time,
277 | }
278 |
279 | input.id$ = data.id
280 |
281 | this.requestAct(seneca, input, output, respond)
282 | }
283 |
284 | internals.Utils.prototype.requestAct = function (
285 | seneca,
286 | input,
287 | output,
288 | respond,
289 | ) {
290 | var self = this
291 |
292 | try {
293 | seneca.act(input, function (err, out) {
294 | self.update_output(input, output, err, out)
295 | respond(output)
296 | })
297 | } catch (e) {
298 | self.catch_act_error(seneca, e, input, {}, output)
299 | respond(output)
300 | }
301 | }
302 |
303 | internals.Utils.prototype.make_client = function (
304 | context_seneca,
305 | make_send,
306 | client_options,
307 | client_done,
308 | ) {
309 | var instance = this._context.seneca
310 |
311 | // legacy api
312 | if (!context_seneca.seneca) {
313 | client_done = client_options
314 | client_options = make_send
315 | make_send = context_seneca
316 | } else {
317 | instance = context_seneca
318 | }
319 |
320 | var pins = this.resolve_pins(client_options)
321 | instance.log.debug('client', client_options, pins || 'any')
322 |
323 | var finish = function (err, send) {
324 | if (err) {
325 | return client_done(err)
326 | }
327 | client_done(null, send)
328 | }
329 |
330 | if (pins) {
331 | var argspatrun = this.make_argspatrun(pins)
332 | var resolvesend = this.make_resolvesend(client_options, {}, make_send)
333 |
334 | return this.make_pinclient(client_options, resolvesend, argspatrun, finish)
335 | }
336 |
337 | this.make_anyclient(client_options, make_send, finish)
338 | }
339 |
340 | internals.Utils.prototype.make_anyclient = function (opts, make_send, done) {
341 | var self = this
342 | make_send({}, this._msgprefix + 'any', function (err, send) {
343 | if (err) {
344 | return done(err)
345 | }
346 | if (typeof send !== 'function') {
347 | return done(self._context.seneca.fail('null-client', { opts: opts }))
348 | }
349 |
350 | var client = {
351 | // id: opts.id || Nid(),
352 | id: opts.id || self._context.seneca.util.Nid(),
353 | toString: function () {
354 | return 'any-' + this.id
355 | },
356 |
357 | send: function (args, done, meta) {
358 | send.call(this, args, done, meta)
359 | },
360 | }
361 |
362 | done(null, client)
363 | })
364 | }
365 |
366 | internals.Utils.prototype.make_pinclient = function (
367 | opts,
368 | resolvesend,
369 | argspatrun,
370 | done,
371 | ) {
372 | var client = {
373 | // id: opts.id || Nid(),
374 | id: opts.id || self._context.seneca.util.Nid(),
375 | toString: function () {
376 | return 'pin-' + argspatrun.mark + '-' + this.id
377 | },
378 |
379 | /*
380 | // TODO: is this used?
381 | match: function (args) {
382 | var match = !!argspatrun.find(args)
383 | return match
384 | },
385 | */
386 |
387 | send: function (args, done, meta) {
388 | var seneca = this
389 | var spec = argspatrun.find(args)
390 |
391 | resolvesend(spec, args, function (err, send) {
392 | if (err) {
393 | return done(err)
394 | }
395 | send.call(seneca, args, done, meta)
396 | })
397 | },
398 | }
399 |
400 | done(null, client)
401 | }
402 |
403 | internals.Utils.prototype.resolve_pins = function (opts) {
404 | const self = this
405 | var pins = opts.pin || opts.pins
406 | if (pins) {
407 | pins = Array.isArray(pins) ? pins : [pins]
408 | }
409 |
410 | if (pins) {
411 | pins = pins.map(function (pin) {
412 | return typeof pin === 'string'
413 | ? self._context.seneca.util.Jsonic(pin)
414 | : pin
415 | })
416 | }
417 |
418 | return pins
419 | }
420 |
421 | internals.Utils.prototype.make_argspatrun = function (pins) {
422 | var argspatrun = this._context.seneca.util.Patrun({ gex: true })
423 |
424 | Each(pins, function (pin) {
425 | var spec = { pin: pin }
426 | argspatrun.add(pin, spec)
427 | })
428 |
429 | argspatrun.mark = Util.inspect(pins).replace(/\s+/g, '').replace(/\n/g, '')
430 |
431 | return argspatrun
432 | }
433 |
434 | internals.Utils.prototype.make_resolvesend = function (
435 | opts,
436 | sendmap,
437 | make_send,
438 | ) {
439 | var self = this
440 | return function (spec, args, done) {
441 | var topic = self.resolve_topic(opts, spec, args)
442 | var send = sendmap[topic]
443 | if (send) {
444 | return done(null, send)
445 | }
446 |
447 | make_send(spec, topic, function (err, send) {
448 | if (err) {
449 | return done(err)
450 | }
451 | sendmap[topic] = send
452 | done(null, send)
453 | })
454 | }
455 | }
456 |
457 | internals.Utils.prototype.resolve_topic = function (opts, spec, args) {
458 | var self = this
459 | if (!spec.pin) {
460 | return function () {
461 | return self._msgprefix + 'any'
462 | }
463 | }
464 |
465 | var topicpin = Object.assign({}, spec.pin)
466 |
467 | var topicargs = {}
468 | Each(topicpin, function (v, k) {
469 | topicargs[k] = args[k]
470 | })
471 |
472 | var sb = []
473 | Each(Object.keys(topicargs).sort(), function (k) {
474 | sb.push(k)
475 | sb.push('=')
476 | sb.push(topicargs[k])
477 | sb.push(',')
478 | })
479 |
480 | var topic = this._msgprefix + sb.join('').replace(/[^\w\d]+/g, '_')
481 | return topic
482 | }
483 |
484 | internals.Utils.prototype.listen_topics = function (
485 | seneca,
486 | args,
487 | listen_options,
488 | do_topic,
489 | ) {
490 | var self = this
491 | var topics = []
492 |
493 | var pins = this.resolve_pins(args)
494 |
495 | if (pins) {
496 | Each(this._context.seneca.findpins(pins), function (pin) {
497 | var sb = []
498 | Each(Object.keys(pin).sort(), function (k) {
499 | sb.push(k)
500 | sb.push('=')
501 | sb.push(pin[k])
502 | sb.push(',')
503 | })
504 |
505 | var topic = self._msgprefix + sb.join('').replace(/[^\w\d]+/g, '_')
506 |
507 | topics.push(topic)
508 | })
509 |
510 | // TODO: die if no pins!!!
511 | // otherwise no listener established and seneca ends without msg
512 | } else {
513 | topics.push(this._msgprefix + 'any')
514 | }
515 |
516 | if (typeof do_topic === 'function') {
517 | topics.forEach(function (topic) {
518 | do_topic(topic)
519 | })
520 | }
521 |
522 | return topics
523 | }
524 |
525 | internals.Utils.prototype.update_output = function (input, output, err, out) {
526 | output.res = out
527 |
528 | if (err) {
529 | var errobj = Object.assign({}, err)
530 | errobj.message = err.message
531 | errobj.name = err.name || 'Error'
532 |
533 | output.error = errobj
534 | output.input = input
535 | }
536 |
537 | output.time.listen_sent = Date.now()
538 | }
539 |
540 | internals.Utils.prototype.catch_act_error = function (
541 | seneca,
542 | e,
543 | listen_options,
544 | input,
545 | output,
546 | ) {
547 | seneca.log.error('listen', 'act-error', listen_options, e.stack || e)
548 | output.error = e
549 | output.input = input
550 | }
551 |
552 | // legacy names
553 | internals.Utils.prototype.resolvetopic = internals.Utils.prototype.resolve_topic
554 |
555 | internals.Utils.prototype.prepareResponse = function (seneca, input) {
556 | return {
557 | id: input.id,
558 | kind: 'res',
559 | origin: input.origin,
560 | accept: seneca.id,
561 | track: input.track,
562 | time: {
563 | client_sent: (input.time && input.time.client_sent) || 0,
564 | listen_recv: Date.now(),
565 | },
566 | sync: input.sync,
567 | }
568 | }
569 |
570 | // Utilities
571 |
572 | // only support first level
573 | // interim measure - deal with this in core seneca act api
574 | // allow user to specify operations on result
575 | internals.Utils.prototype.handle_entity = function (seneca, raw) {
576 | if (!raw) {
577 | return raw
578 | }
579 |
580 | raw = 'object' === typeof raw && null != raw ? raw : {}
581 |
582 | if (raw.entity$) {
583 | return seneca.make$(raw)
584 | }
585 |
586 | Each(raw, function (value, key) {
587 | if ('object' === typeof value && null != value && value.entity$) {
588 | raw[key] = seneca.make$(value)
589 | }
590 | })
591 |
592 | return raw
593 | }
594 |
595 | internals.Utils.prototype.close = function (seneca, closer) {
596 | seneca.add('role:seneca,cmd:close', function (close_args, done) {
597 | var seneca = this
598 |
599 | closer.call(seneca, function (err) {
600 | if (err) {
601 | seneca.log.error(err)
602 | }
603 |
604 | seneca.prior(close_args, done)
605 | })
606 | })
607 | }
608 |
609 | internals.Utils.prototype.stringifyJSON = function (seneca, note, obj) {
610 | if (!obj) {
611 | return
612 | }
613 |
614 | try {
615 | return JSON.stringify(obj)
616 | } catch (e) {
617 | seneca.log.warn('json-stringify', note, obj, e.message)
618 | }
619 | }
620 |
621 | internals.Utils.prototype.parseJSON = function (seneca, note, str) {
622 | if (!str) {
623 | return
624 | }
625 |
626 | try {
627 | return JSON.parse(str)
628 | } catch (e) {
629 | seneca.log.warn(
630 | 'json-parse',
631 | note,
632 | str.replace(/[\r\n\t]+/g, ''),
633 | e.message,
634 | )
635 | e.input = str
636 | return e
637 | }
638 | }
639 |
640 | internals.Utils.prototype.resolveDynamicValue = function (value, options) {
641 | if ('function' == typeof value) {
642 | return value(options)
643 | }
644 | return value
645 | }
646 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "seneca-transport",
3 | "version": "8.3.0",
4 | "description": "Seneca transport",
5 | "main": "transport.js",
6 | "license": "MIT",
7 | "author": "Richard Rodger (http://richardrodger.com)",
8 | "precommit": "test",
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/senecajs/seneca-transport.git"
12 | },
13 | "keywords": [
14 | "seneca",
15 | "transport",
16 | "plugin"
17 | ],
18 | "scripts": {
19 | "test": "lab -v -P test -t 70 -I AggregateError,atob,btoa,DOMException,AbortController,AbortSignal,EventTarget,Event,MessageChannel,MessagePort,MessageEvent,performance,structuredClone",
20 | "test-some": "lab -v -P test -g",
21 | "coveralls": "lab -s -P test -I AggregateError,atob,btoa,DOMException,AbortController,AbortSignal,EventTarget,Event,MessageChannel,MessagePort,MessageEvent,performance,structuredClone -r lcov > ./coverage/lcov.info",
22 | "coverage": "lab -v -P test -t 70 -r html -I URL,URLSearchParams > coverage.html",
23 | "prettier": "prettier --write --no-semi --single-quote *.js lib/*.js test/*.js",
24 | "reset": "npm run clean && npm i && npm test",
25 | "clean": "rm -rf node_modules package-lock.json yarn.lock",
26 | "repo-tag": "REPO_VERSION=`node -e \"console.log(require('./package').version)\"` && echo TAG: v$REPO_VERSION && git commit -a -m v$REPO_VERSION && git push && git tag v$REPO_VERSION && git push --tags;",
27 | "repo-publish": "npm run reset && npm run repo-publish-quick",
28 | "repo-publish-quick": "npm run prettier && npm test && npm run repo-tag --registry https://registry.npmjs.org && npm publish --access public --registry https://registry.npmjs.org"
29 | },
30 | "contributors": [
31 | "Richard Rodger (https://github.com/rjrodger)",
32 | "Wyatt Preul (https://github.com/geek)",
33 | "Dean McDonnell (https://github.com/mcdonnelldean)",
34 | "Mihai Dima (https://github.com/mihaidma)",
35 | "David Gonzalez (https://github.com/dgonzalez)",
36 | "Glen Keane (https://github.com/thekemkid)",
37 | "Marco Piraccini (https://github.com/marcopiraccini)",
38 | "Shane Lacey (https://github.com/shanel262)",
39 | "Cristian Kiss (https://github.com/ckiss)",
40 | "jaamison (https://github.com/jaamison)",
41 | "peterli888 (https://github.com/peterli888)",
42 | "Emer Rutherford (https://github.com/eeswr)",
43 | "Greg Kubisa (https://github.com/gkubisa)",
44 | "Geoffrey Clements (https://github.com/baldmountain)",
45 | "Rumkin (https://github.com/rumkin)",
46 | "Boris Jonica (https://github.com/bjonica)",
47 | "Damien Simonin Feugas (https://github.com/feugy)",
48 | "Tyler Waters (https://github.com/tswaters)",
49 | "Christian Gaggero (https://github.com/chirigg)"
50 | ],
51 | "dependencies": {
52 | "@hapi/wreck": "^18.1.0",
53 | "eraro": "^3.0.1",
54 | "lodash.foreach": "^4.5.0",
55 | "lodash.omit": "^4.5.0",
56 | "lru-cache": "8.x",
57 | "ndjson": "^2.0.0",
58 | "qs": "^6.13.0",
59 | "reconnect-core": "^1.3.0"
60 | },
61 | "devDependencies": {
62 | "@hapi/code": "9",
63 | "@hapi/joi": "17",
64 | "@hapi/lab": "25",
65 | "async": "^3.2.6",
66 | "bench": "^0.3.6",
67 | "coveralls": "^3.1.1",
68 | "prettier": "^3.3.3",
69 | "seneca-transport-test": "^1.0.0",
70 | "sinon": "^19.0.2"
71 | },
72 | "peerDependencies": {
73 | "seneca": ">=3",
74 | "seneca-entity": ">=28"
75 | },
76 | "files": [
77 | "transport.js",
78 | "README.md",
79 | "LICENSE",
80 | "lib"
81 | ]
82 | }
83 |
--------------------------------------------------------------------------------
/test/basic.skip.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var Lab = require('@hapi/lab')
4 | var Shared = require('seneca-transport-test')
5 | var CreateInstance = require('./utils/createInstance')
6 |
7 | var lab = (exports.lab = Lab.script())
8 |
9 | // These tests are currently skipped until the source of a
10 | // timeout on 0.10, 0.12, and intermittently on 4 is found
11 |
12 | Shared.basictest({
13 | seneca: CreateInstance(),
14 | script: lab,
15 | type: 'tcp',
16 | })
17 |
18 | Shared.basicpintest({
19 | seneca: CreateInstance(),
20 | script: lab,
21 | type: 'tcp',
22 | })
23 |
24 | Shared.basictest({
25 | seneca: CreateInstance(),
26 | script: lab,
27 | type: 'http',
28 | })
29 |
30 | Shared.basicpintest({
31 | seneca: CreateInstance(),
32 | script: lab,
33 | type: 'http',
34 | })
35 |
--------------------------------------------------------------------------------
/test/bench/bench-external.js:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2014 Richard Rodger, MIT License */
2 | 'use strict'
3 |
4 |
5 | var Makeseneca = require('seneca')
6 |
7 | var fr = Math.floor
8 | function start_printer (ctxt) {
9 | console.log('rate', 'allrate', 'total', 'realrate', 'memusedpc', 'memtotal')
10 | setInterval(function () {
11 | ctxt.count++
12 | ctxt.seneca.act('role:seneca,stats:true', function (err, out) {
13 | console.assert(!err)
14 | var stats = out.actmap['{a=1}']
15 | var mem = process.memoryUsage()
16 | console.log(stats.time.rate, fr(stats.time.allrate), ctxt.total, fr(ctxt.total / ctxt.count), (fr(100 * mem.heapUsed / mem.heapTotal)) / 100, fr(mem.heapTotal / (1024 * 1024)))
17 | })
18 | }, ctxt.interval)
19 | }
20 |
21 |
22 | var typemap = {}
23 |
24 |
25 | typemap.tcp = function () {
26 | Makeseneca({log: 'silent', stats: {duration: 1000, size: 99998}})
27 | .client({type: 'tcp'})
28 | .ready(function () {
29 | var ctxt = {
30 | count: 0,
31 | total: 0,
32 | interval: 1000
33 | }
34 | ctxt.seneca = this
35 |
36 | start_printer(ctxt)
37 |
38 | function call () {
39 | ctxt.seneca.act('a:1')
40 | ctxt.total++
41 | setImmediate(call)
42 | }
43 |
44 | call()
45 | })
46 | }
47 |
48 |
49 | typemap.web = function () {
50 | Makeseneca({log: 'silent', stats: {duration: 1000, size: 99998}})
51 | .client({type: 'web'})
52 | .ready(function () {
53 | var ctxt = {
54 | count: 0,
55 | total: 0,
56 | interval: 1000
57 | }
58 | ctxt.seneca = this
59 |
60 | start_printer(ctxt)
61 |
62 | function call () {
63 | ctxt.seneca.act('a:1')
64 | ctxt.total++
65 | setImmediate(call)
66 | }
67 |
68 | call()
69 | })
70 | }
71 |
72 | typemap[process.argv[2]]()
73 |
--------------------------------------------------------------------------------
/test/bench/bench-internal.js:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2014 Richard Rodger, MIT License */
2 | 'use strict'
3 |
4 |
5 | var Makeseneca = require('seneca')
6 | var aa = function (args, done) { done(null, {aa: args.a}) }
7 |
8 |
9 | var fr = Math.floor
10 | function start_printer (ctxt) {
11 | console.log('rate', 'allrate', 'total', 'realrate', 'memusedpc', 'memtotal')
12 | setInterval(function () {
13 | ctxt.count++
14 | ctxt.seneca.act('role:seneca,stats:true', function (err, out) {
15 | console.assert(!err)
16 | var stats = out.actmap['{a=1}']
17 | var mem = process.memoryUsage()
18 | console.log(stats.time.rate, fr(stats.time.allrate), ctxt.total, fr(ctxt.total / ctxt.count), (fr(100 * mem.heapUsed / mem.heapTotal)) / 100, fr(mem.heapTotal / (1024 * 1024)))
19 | })
20 | }, ctxt.interval)
21 | }
22 |
23 |
24 | var typemap = {}
25 |
26 | typemap.internal = function () {
27 | Makeseneca({log: 'silent', stats: {duration: 1000, size: 99998}})
28 | .add('a:1', aa)
29 | .ready(function () {
30 | var ctxt = {
31 | count: 0,
32 | total: 0,
33 | interval: 1000
34 | }
35 | ctxt.seneca = this
36 |
37 | start_printer(ctxt)
38 |
39 | function call () {
40 | ctxt.seneca.act('a:1')
41 | ctxt.total++
42 | setImmediate(call)
43 | }
44 |
45 | call()
46 | })
47 | }
48 |
49 |
50 | typemap.tcp = function () {
51 | Makeseneca({log: 'silent', stats: {duration: 1000, size: 99998}})
52 | .add('a:1', aa)
53 | .listen({type: 'tcp'})
54 | .ready(function () {
55 | Makeseneca({log: 'silent', stats: {duration: 1000, size: 99998}})
56 | .client({type: 'tcp'})
57 | .ready(function () {
58 | var ctxt = {
59 | count: 0,
60 | total: 0,
61 | interval: 1000
62 | }
63 | ctxt.seneca = this
64 |
65 | start_printer(ctxt)
66 |
67 | function call () {
68 | ctxt.seneca.act('a:1')
69 | ctxt.total++
70 | 0 === ctxt.total % 100 ? setImmediate(call) : call()
71 | }
72 |
73 | call()
74 | })
75 | })
76 | }
77 |
78 |
79 | typemap.web = function () {
80 | Makeseneca({log: 'silent', stats: {duration: 1000, size: 99998}})
81 | .add('a:1', aa)
82 | .listen({type: 'web'})
83 | .ready(function () {
84 | Makeseneca({log: 'silent', stats: {duration: 1000, size: 99998}})
85 | .client({type: 'web'})
86 | .ready(function () {
87 | var ctxt = {
88 | count: 0,
89 | total: 0,
90 | interval: 1000
91 | }
92 | ctxt.seneca = this
93 |
94 | start_printer(ctxt)
95 |
96 | function call () {
97 | ctxt.seneca.act('a:1')
98 | ctxt.total++
99 | setImmediate(call)
100 | }
101 |
102 | call()
103 | })
104 | })
105 | }
106 |
107 | typemap[process.argv[2]]()
108 |
--------------------------------------------------------------------------------
/test/bench/bench-service.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var type = process.argv[2]
4 | console.log('TYPE:' + type)
5 |
6 | var Makeseneca = require('seneca')
7 | var aa = function (args, done) { done(null, {aa: args.a}) }
8 |
9 | Makeseneca({log: 'silent', stats: {duration: 1000, size: 99998}})
10 | .add('a:1', aa)
11 | .listen({type: type})
12 |
--------------------------------------------------------------------------------
/test/entity.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var Util = require('util')
4 | var Assert = require('assert')
5 | var Lab = require('@hapi/lab')
6 | var Entity = require('seneca-entity')
7 | var CreateInstance = require('./utils/createInstance')
8 |
9 | var lab = (exports.lab = Lab.script())
10 | var describe = lab.describe
11 | var it = make_it(lab)
12 |
13 | describe('Transporting Entities', function () {
14 | it('uses correct tx$ properties on entity actions for "transported" entities', function (done) {
15 | var seneca1 = CreateInstance()
16 |
17 | if (seneca1.version >= '2.0.0') {
18 | seneca1.use(Entity)
19 | }
20 |
21 | seneca1.ready(function () {
22 | seneca1
23 | .add({ cmd: 'test' }, function (args, cb) {
24 | args.entity.save$(function (err, entitySaveResponse) {
25 | if (err) return cb(err)
26 |
27 | this.act({ cmd: 'test2' }, function (err, test2Result) {
28 | if (err) {
29 | return cb(err)
30 | }
31 |
32 | cb(null, {
33 | entity: entitySaveResponse.entity,
34 | txBeforeEntityAction: args.tx$,
35 | txInsideEntityAction: entitySaveResponse.tx,
36 | txAfterEntityAction: test2Result.tx,
37 | })
38 | })
39 | })
40 | })
41 | .add({ role: 'entity', cmd: 'save' }, function (args, cb) {
42 | cb(null, { entity: args.ent, tx: args.tx$ })
43 | })
44 | .add({ cmd: 'test2' }, function (args, cb) {
45 | cb(null, { tx: args.tx$ })
46 | })
47 | .listen({ type: 'tcp', port: 20103 })
48 |
49 | var seneca2 = CreateInstance()
50 |
51 | if (seneca2.version >= '2.0.0') {
52 | seneca2.use(Entity)
53 | }
54 |
55 | seneca2.ready(function () {
56 | seneca2.client({ type: 'tcp', port: 20103 })
57 | this.act(
58 | { cmd: 'test', entity: this.make$('test').data$({ name: 'bar' }) },
59 | function (err, res) {
60 | Assert(!err)
61 |
62 | Assert(res.entity.name === 'bar')
63 | Assert(res.txBeforeEntityAction === res.txInsideEntityAction)
64 | Assert(res.txBeforeEntityAction === res.txAfterEntityAction)
65 | done()
66 | },
67 | )
68 | })
69 | })
70 | })
71 |
72 | it('uses correct tx$ properties on entity actions for "non-transported" requests', function (done) {
73 | CreateInstance()
74 | .add({ cmd: 'test' }, function (args, cb) {
75 | this.act({ cmd: 'test2' }, function (err, test2Result) {
76 | if (err) {
77 | return cb(err)
78 | }
79 |
80 | cb(null, {
81 | txBeforeEntityAction: args.tx$,
82 | txAfterEntityAction: test2Result.tx,
83 | })
84 | })
85 | })
86 | .add({ cmd: 'test2' }, function (args, cb) {
87 | cb(null, { tx: args.tx$ })
88 | })
89 | .listen({ type: 'tcp', port: 20104 })
90 | .ready(function () {
91 | CreateInstance()
92 | .client({ type: 'tcp', port: 20104 })
93 | .ready(function () {
94 | this.act({ cmd: 'test' }, function (err, res) {
95 | Assert(!err)
96 | Assert(res.txBeforeEntityAction === res.txAfterEntityAction)
97 | done()
98 | })
99 | })
100 | })
101 | })
102 | })
103 |
104 | function make_it(lab) {
105 | return function it(name, opts, func) {
106 | if ('function' === typeof opts) {
107 | func = opts
108 | opts = {}
109 | }
110 |
111 | lab.it(
112 | name,
113 | opts,
114 | Util.promisify(function (x, fin) {
115 | func(fin)
116 | }),
117 | )
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/test/http.test.js:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2019 Richard Rodger and other contributors, MIT License */
2 | 'use strict'
3 |
4 | var Assert = require('assert')
5 | var Util = require('util')
6 |
7 | var Code = require('@hapi/code')
8 | var Lab = require('@hapi/lab')
9 | var Sinon = require('sinon')
10 | var PassThrough = require('stream').PassThrough
11 | var NodeHttp = require('http')
12 | var Http = require('../lib/http')
13 | var TransportUtil = require('../lib/transport-utils')
14 | var Wreck = require('@hapi/wreck')
15 |
16 | var CreateInstance = require('./utils/createInstance')
17 | var CreateClient = require('./utils/createClient')
18 |
19 | var lab = (exports.lab = Lab.script())
20 | var describe = lab.describe
21 | var expect = Code.expect
22 | var beforeEach = lab.beforeEach
23 | var afterEach = lab.afterEach
24 |
25 | var it = make_it(lab)
26 |
27 | describe('http errors', function () {
28 | let request = null
29 |
30 | beforeEach(function () {
31 | request = Sinon.stub(NodeHttp, 'request')
32 | })
33 |
34 | afterEach(function () {
35 | request.restore()
36 | })
37 |
38 | it("doesn't hang the process", function (fin) {
39 | // wreck is expecting a http.ClientRequest, but we are stubbing request
40 | // to return a PassThrough, for a simple stream that can emit an error.
41 | // wreck does however call abort which is not on Passthrough;
42 | // so we need to set up a dummy function so nothing blows up.
43 | var req = new PassThrough()
44 | req.abort = () => {}
45 | request.returns(req)
46 |
47 | CreateInstance()
48 | .add('a:1', function (args, done) {
49 | done(null, this.util.clean(args))
50 | })
51 | .listen(30304)
52 |
53 | CreateInstance()
54 | .client(30304)
55 | .act('a:1', function (err, out) {
56 | Assert.equal(
57 | err.msg,
58 | 'seneca: Action failed: Client request error: aw snap.',
59 | )
60 | fin()
61 | })
62 | // need to wait until after wreck sets up request before emitting
63 | // otherwise domain catches the emitted error and the tests blow up
64 | // 200ms should be plenty of time for this.
65 | setTimeout(() => req.emit('error', new Error('aw snap')), 1000)
66 | })
67 | })
68 |
69 | describe('Specific http', function () {
70 | it('web-basic', { timeout: 8888 }, function (done) {
71 | CreateInstance()
72 | .add('c:1', function (args, cb) {
73 | cb(null, { s: '1-' + args.d })
74 | })
75 | .listen({ type: 'web', port: 20202 })
76 | .ready(function () {
77 | var count = 0
78 | function check() {
79 | count++
80 | if (count === 4) {
81 | done()
82 | }
83 | }
84 |
85 | CreateClient('http', 20202, check)
86 | CreateClient('http', 20202, check)
87 | CreateClient('http', 20202, check)
88 |
89 | var requestOptions = {
90 | payload: JSON.stringify({ c: 1, d: 'A' }),
91 | json: true,
92 | }
93 |
94 | // special case for non-seneca clients
95 | var post = Wreck.post('http://127.0.0.1:20202/act', requestOptions)
96 | post
97 | .then((out) => {
98 | handle_post(null, out.res, out.payload)
99 | })
100 | .catch((err) => {
101 | handle_post(err)
102 | })
103 |
104 | function handle_post(err, res, body) {
105 | // console.log(err, res, body)
106 | if (err) {
107 | return done(err)
108 | }
109 | Assert.equal('{"s":"1-A"}', JSON.stringify(body))
110 | check()
111 | }
112 | })
113 | })
114 |
115 | it('error-passing-http', function (fin) {
116 | CreateInstance()
117 | .add('a:1', function (args, done) {
118 | done(new Error('bad-wire'))
119 | })
120 | .listen(30303)
121 |
122 | CreateInstance()
123 | .client(30303)
124 | .act('a:1', function (err, out) {
125 | Assert(!!err)
126 | fin()
127 | })
128 | })
129 |
130 | it('not-found', function (fin) {
131 | CreateInstance()
132 | .add('c:1', function (args, cb) {
133 | cb(null, { s: '1-' + args.d })
134 | })
135 | .listen({ type: 'web', port: 20207 })
136 | .ready(function () {
137 | var post = Wreck.post('http://127.0.0.1:20207/act-foo', {
138 | payload: JSON.stringify({ c: 1, d: 'A' }),
139 | json: true,
140 | })
141 |
142 | post
143 | .then((out) => {
144 | handle_post(null, out.res, out.payload)
145 | })
146 | .catch((err) => {
147 | handle_post(err)
148 | })
149 |
150 | function handle_post(err, res, body) {
151 | Assert.equal(err.output.statusCode, 404)
152 | fin()
153 | }
154 | })
155 | })
156 |
157 | it('http-query', function (fin) {
158 | CreateInstance({ errhandler: fin })
159 | .add('a:1', function (args, done) {
160 | done(null, this.util.clean(args))
161 | })
162 | .listen({ type: 'web', port: 20302 })
163 | .ready(function () {
164 | var get = Wreck.get('http://127.0.0.1:20302/act?a=1&b=2', {
165 | json: true,
166 | })
167 |
168 | get
169 | .then((out) => {
170 | handle_get(null, out.res, out.payload)
171 | })
172 | .catch((err) => {
173 | handle_get(err)
174 | })
175 |
176 | function handle_get(err, res, body) {
177 | if (err) {
178 | return fin(err)
179 | }
180 | Assert.equal(1, body.a)
181 | Assert.equal(2, body.b)
182 |
183 | get = Wreck.get(
184 | 'http://127.0.0.1:20302/act?args$=a:1, b:2, c:{d:3}',
185 | { json: true },
186 | )
187 |
188 | get
189 | .then((out) => {
190 | handle_get2(null, out.res, out.payload)
191 | })
192 | .catch((err) => {
193 | handle_get2(err)
194 | })
195 |
196 | function handle_get2(err, res, body) {
197 | if (err) {
198 | return fin(err)
199 | }
200 | Assert.equal(1, body.a)
201 | Assert.equal(2, body.b)
202 | Assert.equal(3, body.c.d)
203 |
204 | fin()
205 | }
206 | }
207 | })
208 | })
209 |
210 | it('web-add-headers', function (fin) {
211 | CreateInstance({ errhandler: fin })
212 | .add('c:1', function (args, done) {
213 | done(null, { s: '1-' + args.d })
214 | })
215 | .listen({ type: 'web', port: 20205 })
216 | .ready(function () {
217 | CreateInstance(
218 | { errhandler: fin },
219 | { web: { headers: { 'client-id': 'test-client' } } },
220 | )
221 | .client({ type: 'web', port: 20205 })
222 | .ready(function () {
223 | this.act('c:1,d:A', function (err, out) {
224 | if (err) {
225 | return fin(err)
226 | }
227 |
228 | Assert.equal('{"s":"1-A"}', JSON.stringify(out))
229 |
230 | this.act('c:1,d:AA', function (err, out) {
231 | if (err) {
232 | return fin(err)
233 | }
234 |
235 | Assert.equal('{"s":"1-AA"}', JSON.stringify(out))
236 |
237 | this.close(fin)
238 | })
239 | })
240 | })
241 | })
242 | })
243 |
244 | it('can listen on ephemeral port', function (done) {
245 | var seneca = CreateInstance()
246 | var settings = {
247 | web: {
248 | port: 0,
249 | },
250 | }
251 |
252 | var callmap = {}
253 |
254 | var transportUtil = new TransportUtil({
255 | callmap: callmap,
256 | seneca: seneca,
257 | options: settings,
258 | })
259 |
260 | var http = Http.listen(settings, transportUtil)
261 | expect(typeof http).to.equal('function')
262 |
263 | http.call(seneca, { type: 'web' }, function (err) {
264 | expect(err).to.not.exist()
265 | done()
266 | })
267 | })
268 |
269 | it('defaults to 127.0.0.1 for connections', function (done) {
270 | var seneca = CreateInstance()
271 |
272 | var settings = {
273 | web: {
274 | port: 0,
275 | },
276 | }
277 |
278 | var callmap = {}
279 |
280 | var transportUtil = new TransportUtil({
281 | callmap: callmap,
282 | seneca: seneca,
283 | options: settings,
284 | })
285 |
286 | var server = Http.listen(settings, transportUtil)
287 | expect(typeof server).to.equal('function')
288 |
289 | server.call(seneca, { type: 'web' }, function (err, address) {
290 | expect(err).to.not.exist()
291 |
292 | expect(address.type).to.equal('web')
293 | settings.web.port = address.port
294 | var client = Http.client(settings, transportUtil)
295 | expect(typeof client).to.equal('function')
296 | client.call(seneca, { type: 'web' }, function (err) {
297 | expect(err).to.not.exist()
298 | done()
299 | })
300 | })
301 | })
302 | })
303 |
304 | describe('Specific https', function () {
305 | it('Creates a seneca server running on port 8000 https and expects hex to be equal to #FF0000', function (done) {
306 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
307 | function color() {
308 | this.add('color:red', function (args, done) {
309 | done(null, { hex: '#FF0000' })
310 | })
311 | }
312 |
313 | CreateInstance()
314 | .use(color)
315 | .listen({
316 | type: 'web',
317 | port: 8000,
318 | host: '127.0.0.1',
319 | protocol: 'https',
320 | serverOptions: {
321 | key: '-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAuE1A7DmpJyffmXx1men/L3NJXIt/zXR4CoF0hZYloLBblwyV\nebQLfHq+Pn3E/xvFDIBVm6xDQhl9T+z/kLvCw2NSxkN5aSTjtwFA7iNPx9TqeUV/\n48ijQIR8gfrT7QV2Nl3pGk5RZfYzKEObddJeh7oSCAI9dLaBObcX5FfYrlsMg9S6\nHC3XI9HBlFtaMCpWmjY24xQlQ/yC98V6zkLcEQgqDo4TiOx6qNh0KDzoqpcV9HWm\n4E+m8K9JO5c1IR0y8Gv8aBnz/py6Pyw16pPm5MoxoMcWxSfdvx4TwhgALWvafVwO\nCSGMOphzAAid3QA6n1lc6J+asei5dk0cvxng3QIDAQABAoIBAEnFmpw0BHKI8mbk\nu8otMRlUQ2RI7pJV8Yr7AKJMVKl6jl7rCZYarJJaK3amL0mSWxDC+gGDNbTqsQ9i\nJXZQwggl5Mc50Qp2WrQxS0VHWzL5FhYO7L9H25kCrzf0KApzKjte4eTGvqxanWWb\nkknaOD6KC5erFeB3AUkR8f1T8IbxewCG/79RF3/DO2Obi0R9vOfoTNzuc6BKqIJ+\nvcS+z6+YEFSzbuDA4QuJiD3Uv/HlGzJf9HF2KJ6tjalo2nwmsWV1jXMX7dn13Rxa\ny12otOdqN7lVF1ulsoHBwbsX0PfDj5Kxa+i8fsry/4herYVwogEhOpFs0D552r9D\nKnUXIh0CgYEA42YXh5UDRIkJvhVcTdRpmUwv3C861Uk2Om3ibT7mREc32GanSMtU\n/JVBCCYUXhnSpHpazKL8iPEoHpX5HqxBhXEjwh054nKYrik69cn4ARxkXbiZsX3G\nTjNMB/NVVepu0xA1tA+viMNf11uI6peJa8F8Ldl1xI5DgJGMV/c/k7MCgYEAz3uA\nKe1wZeHEyrO2o9KnIPbPLkxV0/fxFkKi3g9F6NSUfTYUDpJN9m+wQA08DRTyzlOJ\nepmn12fCTQ51wYvFEjwajtDoRrGjbPVM6qz/N1XH18GaXUJ9z4eQKQ5SwHACh5W8\nfjJ4pPBHpDUF7CnV8PnDCJCFYtZdg1xvP0n1sS8CgYBTBXf7uSy7Pej/rB7KD44K\nOOWUVu385sDUrj+nsPoy3WmHKVtT2WCK4xceGYEAJh9gi4dRBQR8HseN+yU7zJoT\nVQ5AFZmHkl0p4MW07OsNxMbj7Ly4L3pSHKpakL2MI44YoudoeP2WSfZY0wN22qKC\nY96pgqZbf7EnZHw/tXZRvwKBgQCtYfkSEHcyzF3VPiTL9cbwBw/PEr9OaQ2wmnLb\nukuja7HCiKRuINjBrUfN3sFl9TGKNcjXCPx3Rx/ZoNHKsXA38r4GxpC0MtHsxXhH\nS9Xiee6MYB8M+/mCqThQ9sU0RuX2Q6zGkIq82oYjtKOEXNmJjE3tJEgy9gwjL+VP\nMBD+xQKBgGtfS/7BIznrLq2/29nWIUo9vNyXPNnobHi7doCdYoaBaadCCCK5Vn+K\nGiE8ZNneYZGsvfblggFUwdTrm/rRpiztRbtno/M+ikCn3GnKr0TBFj0u3DpCHHUR\nHk9Ukixv0t0zW6o3DhYS5WD12q6NwNNxkEMMF2/hIKsgCknPg9MG\n-----END RSA PRIVATE KEY-----\n',
322 | cert: '-----BEGIN CERTIFICATE-----\nMIIDtTCCAp2gAwIBAgIJAL6i6NpdpvunMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV\nBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX\naWRnaXRzIFB0eSBMdGQwHhcNMTYwMzE1MTY1MjQwWhcNMTcwMzE1MTY1MjQwWjBF\nMQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50\nZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\nCgKCAQEAuE1A7DmpJyffmXx1men/L3NJXIt/zXR4CoF0hZYloLBblwyVebQLfHq+\nPn3E/xvFDIBVm6xDQhl9T+z/kLvCw2NSxkN5aSTjtwFA7iNPx9TqeUV/48ijQIR8\ngfrT7QV2Nl3pGk5RZfYzKEObddJeh7oSCAI9dLaBObcX5FfYrlsMg9S6HC3XI9HB\nlFtaMCpWmjY24xQlQ/yC98V6zkLcEQgqDo4TiOx6qNh0KDzoqpcV9HWm4E+m8K9J\nO5c1IR0y8Gv8aBnz/py6Pyw16pPm5MoxoMcWxSfdvx4TwhgALWvafVwOCSGMOphz\nAAid3QA6n1lc6J+asei5dk0cvxng3QIDAQABo4GnMIGkMB0GA1UdDgQWBBT171ri\nK/l2kGOpMv2SrMC1X4Kw6zB1BgNVHSMEbjBsgBT171riK/l2kGOpMv2SrMC1X4Kw\n66FJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNV\nBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAL6i6NpdpvunMAwGA1UdEwQF\nMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAGf8ymbAUUOvoPpAKXzZ7oIWRomiATSq\nDveCiuxCiIb71wKtb+kffXxQNiNnslqooJiKMiof8HUxnH8NOJL+0Rss4V0golQH\n/YzoogVvcKQUnyMFHMRX9pklN8v8Wt9xIjqDbu3ltMu2VQ+ahepuZCuY+4YQgusf\nKCOYs2ycJzMJYbe0i80tlGqqhcoGuEuW70963126WUOhUQq5xaecJ9cwoVee2xEb\nXW9yt53KCyhpF/ALb8Orv66CCSV3rvbNgOdeNCnKNnr83VpCNCNRvmw1bYzK7LCW\nhTRQZonHX/PcdhW4i0Lqr2GPvA287eZK/riMcLP96mQIpX3A9NapwIk=\n-----END CERTIFICATE-----\n',
323 | // key: key,
324 | // cert: cert
325 | },
326 | })
327 | .ready(function () {
328 | CreateInstance()
329 | .client({
330 | type: 'http',
331 | port: 8000,
332 | host: '127.0.0.1',
333 | protocol: 'https',
334 | })
335 | .act('color:red', function (error, res) {
336 | if (error) {
337 | console.log(error)
338 | }
339 | expect(res.hex).to.be.equal('#FF0000')
340 | done()
341 | })
342 | })
343 | })
344 |
345 | /*
346 | it('Creates a seneca server running on port 8000 https and expects hex to be equal to #FF0000 (wreck client)', function(done) {
347 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
348 | var StringDecoder = require('string_decoder').StringDecoder
349 | var Decoder = new StringDecoder('utf8')
350 | var get = Wreck.get(
351 | 'https://127.0.0.1:8000/act?color=red',
352 | { rejectUnauthorized: false }
353 | )
354 |
355 | get
356 | .then((out)=>{handle_get(null,out.res,out.payload)})
357 | .catch((err)=>{handle_get(err)})
358 |
359 | function handle_get(err, res, body) {
360 | expect(err).to.not.exist()
361 | expect(body.toString()).to.be.equal('{"hex":"#FF0000"}')
362 | }
363 | })
364 | */
365 | })
366 |
367 | function make_it(lab) {
368 | return function it(name, opts, func) {
369 | if ('function' === typeof opts) {
370 | func = opts
371 | opts = {}
372 | }
373 |
374 | lab.it(
375 | name,
376 | opts,
377 | Util.promisify(function (x, fin) {
378 | func(fin)
379 | }),
380 | )
381 | }
382 | }
383 |
--------------------------------------------------------------------------------
/test/integration/client.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var Transport = require('./transport')
4 | var Seneca = require('seneca')
5 |
6 | Seneca({ default_plugins: { transport: false } })
7 | .use(Transport)
8 | .client({ host: 'server', port: 8000 })
9 | .act('color:red', console.log)
10 |
--------------------------------------------------------------------------------
/test/integration/server.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var Transport = require('./transport')
4 | var Seneca = require('seneca')
5 |
6 | var color = function () {
7 | this.add('color:red', function (args, callback) {
8 | callback(null, { hex: '#000000' })
9 | })
10 | }
11 |
12 | Seneca({ default_plugins: { transport: false } })
13 | .use(Transport)
14 | .use(color)
15 | .listen({ port: 8000 })
16 |
--------------------------------------------------------------------------------
/test/misc.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | process.setMaxListeners(999)
4 |
5 | var Util = require('util')
6 | var Assert = require('assert')
7 | var Lab = require('@hapi/lab')
8 | var CreateInstance = require('./utils/createInstance')
9 |
10 | var lab = (exports.lab = Lab.script())
11 | var describe = lab.describe
12 | var it = make_it(lab)
13 |
14 | describe('Miscellaneous', function () {
15 | // NOTE: SENECA_LOG=all will break this test as it counts log entries
16 | /*
17 | it.skip('own-message', function(fin) {
18 | // a -> b -> a
19 |
20 | do_type('tcp', function(err) {
21 | if (err) {
22 | return fin(err)
23 | }
24 | do_type('http', fin)
25 | })
26 |
27 | function do_type(type, fin) {
28 | function counter_a(args, done) {
29 | counters.a++
30 | done(null, { aa: args.a })
31 | }
32 | function counter_b(args, done) {
33 | counters.b++
34 | done(null, { bb: args.b })
35 | }
36 |
37 | var counters = { log_a: 0, log_b: 0, own: 0, a: 0, b: 0, c: 0 }
38 |
39 | var log_a = function() {
40 | counters.log_a++
41 | }
42 | var log_b = function() {
43 | counters.log_b++
44 | }
45 | var own_a = function() {
46 | counters.own++
47 | }
48 |
49 | var a = CreateInstance(
50 | {
51 | log: {
52 | map: [
53 | { level: 'debug', regex: /\{a:1\}/, handler: log_a },
54 | { level: 'warn', regex: /own_message/, handler: own_a }
55 | ]
56 | },
57 | timeout: 111
58 | },
59 | { check: { message_loop: false }, warn: { own_message: true } }
60 | )
61 | .add('a:1', counter_a)
62 | .listen({ type: type, port: 40405 })
63 | .client({ type: type, port: 40406 })
64 |
65 | var b = CreateInstance({
66 | log: { map: [{ level: 'debug', regex: /\{b:1\}/, handler: log_b }] },
67 | timeout: 111
68 | })
69 | .add('b:1', counter_b)
70 | .listen({ type: type, port: 40406 })
71 | .client({ type: type, port: 40405 })
72 |
73 | a.ready(function() {
74 | b.ready(function() {
75 | a.act('a:1', function(err, out) {
76 | if (err) {
77 | return fin(err)
78 | }
79 | Assert.equal(1, out.aa)
80 | })
81 |
82 | a.act('b:1', function(err, out) {
83 | if (err) {
84 | return fin(err)
85 | }
86 | Assert.equal(1, out.bb)
87 | })
88 |
89 | a.act('c:1', function(err, out) {
90 | if (!err) {
91 | Assert.fail()
92 | }
93 | Assert.ok(err.timeout)
94 | })
95 | })
96 | })
97 |
98 | setTimeout(function() {
99 | a.close(function(err) {
100 | if (err) {
101 | return fin(err)
102 | }
103 |
104 | b.close(function(err) {
105 | if (err) {
106 | return fin(err)
107 | }
108 |
109 | try {
110 | Assert.equal(1, counters.a)
111 | Assert.equal(1, counters.b)
112 | Assert.equal(1, counters.log_a)
113 | Assert.equal(1, counters.log_b)
114 | Assert.equal(1, counters.own)
115 | } catch (e) {
116 | return fin(e)
117 | }
118 |
119 | fin()
120 | })
121 | })
122 | }, 222)
123 | }
124 | })
125 |
126 | // NOTE: SENECA_LOG=all will break this test as it counts log entries
127 | it.skip('message-loop', function(fin) {
128 | // a -> b -> c -> a
129 |
130 | do_type('tcp', function(err) {
131 | if (err) {
132 | return fin(err)
133 | }
134 | do_type('http', fin)
135 | })
136 |
137 | function do_type(type, fin) {
138 | function counter_a(args, done) {
139 | counters.a++
140 | done(null, { aa: args.a })
141 | }
142 | function counter_b(args, done) {
143 | counters.b++
144 | done(null, { bb: args.b })
145 | }
146 | function counter_c(args, done) {
147 | counters.c++
148 | done(null, { cc: args.c })
149 | }
150 |
151 | var counters = {
152 | log_a: 0,
153 | log_b: 0,
154 | log_c: 0,
155 | loop: 0,
156 | a: 0,
157 | b: 0,
158 | c: 0,
159 | d: 0
160 | }
161 |
162 | var log_a = function() {
163 | counters.log_a++
164 | }
165 | var log_b = function() {
166 | counters.log_b++
167 | }
168 | var log_c = function() {
169 | counters.log_c++
170 | }
171 | var loop_a = function() {
172 | counters.loop++
173 | }
174 |
175 | var a = CreateInstance(
176 | {
177 | log: {
178 | map: [
179 | { level: 'debug', regex: /\{a:1\}/, handler: log_a },
180 | { level: 'warn', regex: /message_loop/, handler: loop_a }
181 | ]
182 | },
183 | timeout: 111
184 | },
185 | { check: { own_message: false }, warn: { message_loop: true } }
186 | )
187 | .add('a:1', counter_a)
188 | .listen({ type: type, port: 40405 })
189 | .client({ type: type, port: 40406 })
190 |
191 | var b = CreateInstance({
192 | log: { map: [{ level: 'debug', regex: /\{b:1\}/, handler: log_b }] },
193 | timeout: 111
194 | })
195 | .add('b:1', counter_b)
196 | .listen({ type: type, port: 40406 })
197 | .client({ type: type, port: 40407 })
198 |
199 | var c = CreateInstance({
200 | log: { map: [{ level: 'debug', regex: /\{c:1\}/, handler: log_c }] },
201 | timeout: 111,
202 | default_plugins: { transport: false }
203 | })
204 | .add('c:1', counter_c)
205 | .listen({ type: type, port: 40407 })
206 | .client({ type: type, port: 40405 })
207 |
208 | a.ready(function() {
209 | b.ready(function() {
210 | c.ready(function() {
211 | a.act('a:1', function(err, out) {
212 | if (err) {
213 | return fin(err)
214 | }
215 | Assert.equal(1, out.aa)
216 | })
217 |
218 | a.act('b:1', function(err, out) {
219 | if (err) {
220 | return fin(err)
221 | }
222 | Assert.equal(1, out.bb)
223 | })
224 |
225 | a.act('c:1', function(err, out) {
226 | if (err) {
227 | return fin(err)
228 | }
229 | Assert.equal(1, out.cc)
230 | })
231 |
232 | a.act('d:1', function(err) {
233 | if (!err) {
234 | Assert.fail()
235 | }
236 | Assert.ok(err.timeout)
237 | })
238 | })
239 | })
240 | })
241 |
242 | setTimeout(function() {
243 | a.close(function(err) {
244 | if (err) {
245 | return fin(err)
246 | }
247 |
248 | b.close(function(err) {
249 | if (err) {
250 | return fin(err)
251 | }
252 |
253 | c.close(function(err) {
254 | if (err) {
255 | return fin(err)
256 | }
257 |
258 | try {
259 | Assert.equal(1, counters.a)
260 | Assert.equal(1, counters.b)
261 | Assert.equal(1, counters.c)
262 | Assert.equal(1, counters.log_a)
263 | Assert.equal(1, counters.log_b)
264 | Assert.equal(1, counters.log_c)
265 | Assert.equal(1, counters.loop)
266 | } catch (e) {
267 | return fin(e)
268 | }
269 | fin()
270 | })
271 | })
272 | })
273 | }, 222)
274 | }
275 | })
276 | */
277 |
278 | it('testmem-topic-star', function (fin) {
279 | CreateInstance()
280 | .use('./stubs/memtest-transport.js')
281 | .add('foo:1', function (args, done, meta) {
282 | Assert.equal('aaa/AAA', args.meta$ ? args.meta$.id : meta.id)
283 | done(null, { bar: 1 })
284 | })
285 | .add('foo:2', function (args, done, meta) {
286 | Assert.equal('bbb/BBB', args.meta$ ? args.meta$.id : meta.id)
287 | done(null, { bar: 2 })
288 | })
289 | .listen({ type: 'memtest', pin: 'foo:*' })
290 | .ready(function () {
291 | var siClient = CreateInstance()
292 | .use('./stubs/memtest-transport.js')
293 | .client({ type: 'memtest', pin: 'foo:*' })
294 |
295 | siClient.act('foo:1,id$:aaa/AAA', function (err, out) {
296 | Assert.equal(err, null)
297 | Assert.equal(1, out.bar)
298 | siClient.act('foo:2,id$:bbb/BBB', function (err, out) {
299 | Assert.equal(err, null)
300 | Assert.equal(2, out.bar)
301 |
302 | fin()
303 | })
304 | })
305 | })
306 | })
307 |
308 | it('catchall-ordering', function (fin) {
309 | CreateInstance()
310 | .use('./stubs/memtest-transport.js')
311 | .add('foo:1', function (args, done) {
312 | done(null, { FOO: 1 })
313 | })
314 | .add('bar:1', function (args, done) {
315 | done(null, { BAR: 1 })
316 | })
317 | .listen({ type: 'memtest', dest: 'D0', pin: 'foo:*' })
318 | .listen({ type: 'memtest', dest: 'D1' })
319 |
320 | .ready(function () {
321 | do_catchall_first()
322 |
323 | function do_catchall_first() {
324 | var siClient = CreateInstance()
325 | .use('./stubs/memtest-transport.js')
326 | .client({ type: 'memtest', dest: 'D1' })
327 | .client({ type: 'memtest', dest: 'D0', pin: 'foo:*' })
328 |
329 | siClient.act('foo:1', function (err, out) {
330 | Assert.equal(err, null)
331 | Assert.equal(1, out.FOO)
332 |
333 | siClient.act('bar:1', function (err, out) {
334 | Assert.equal(err, null)
335 | Assert.equal(1, out.BAR)
336 |
337 | do_catchall_last()
338 | })
339 | })
340 | }
341 |
342 | function do_catchall_last() {
343 | var siClient = CreateInstance()
344 | .use('./stubs/memtest-transport.js')
345 | .client({ type: 'memtest', dest: 'D0', pin: 'foo:*' })
346 | .client({ type: 'memtest', dest: 'D1' })
347 |
348 | siClient.act('foo:1', function (err, out) {
349 | Assert.equal(err, null)
350 | Assert.equal(1, out.FOO)
351 |
352 | siClient.act('bar:1', function (err, out) {
353 | Assert.equal(err, null)
354 | Assert.equal(1, out.BAR)
355 |
356 | fin()
357 | })
358 | })
359 | }
360 | })
361 | })
362 | })
363 |
364 | function make_it(lab) {
365 | return function it(name, opts, func) {
366 | if ('function' === typeof opts) {
367 | func = opts
368 | opts = {}
369 | }
370 |
371 | lab.it(
372 | name,
373 | opts,
374 | Util.promisify(function (x, fin) {
375 | func(fin)
376 | }),
377 | )
378 | }
379 | }
380 |
--------------------------------------------------------------------------------
/test/reconnect/client.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var Code = require('code')
4 | var Seneca = require('seneca')
5 | var Transport = require('../../')
6 |
7 | var expect = Code.expect
8 | var client = Seneca({ log: 'silent', default_plugins: { transport: false } })
9 | client.use(Transport)
10 |
11 | process.on('message', function (address) {
12 | if (!address.port) {
13 | return
14 | }
15 |
16 | client.ready(function () {
17 | client.client({type: 'tcp', port: address.port})
18 | client.act({ foo: 'bar' }, function (err, message) {
19 | expect(err).to.not.exist()
20 | expect(message.result).to.equal('bar')
21 | process.send({ acted: true })
22 | })
23 | })
24 | })
25 |
--------------------------------------------------------------------------------
/test/reconnect/server.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var CreateInstance = require('../utils/createInstance')
4 | var server = CreateInstance()
5 |
6 | server.add({foo: 'bar'}, function (message, cb) {
7 | cb(null, {result: 'bar'})
8 | })
9 |
10 | server.ready(function () {
11 | server.listen({type: 'tcp', port: 3507}, function (err, address) {
12 | if (err) {
13 | throw err
14 | }
15 |
16 | process.send({port: address.port})
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/test/stubs/README.txt:
--------------------------------------------------------------------------------
1 |
2 | tcp single run:
3 |
4 | $ node service-foo.js tcp --seneca.log.all
5 | $ node client-foo.js tcp --seneca.log.all
6 |
7 | web single run:
8 |
9 | $ node service-foo.js web --seneca.log.all
10 | $ node client-foo.js web --seneca.log.all
11 |
12 | benchmarking:
13 |
14 | inside one process:
15 |
16 | $ node bench-internal.js tcp
17 | $ node bench-internal.js web
18 |
19 | separate client and server:
20 |
21 | $ node bench-server.js tcp
22 | $ node bench-external.js tcp
23 |
24 | $ node bench-server.js web
25 | $ node bench-external.js web
26 |
27 | server & client running on https://127.0.0.1:8000 (https)
28 |
29 | Create a folder 'ssl' within 'test' folder (ie ./test/ssl)
30 | Create a self-signed certificate with OpenSSL by running within the ./ssl folder:
31 |
32 | $ openssl genrsa -out key.pem 2048
33 | $ openssl req -new -key key.pem -out csr.pem
34 | $ openssl req -x509 -days 365 -key key.pem -in csr.pem -out cert.pem
35 |
36 | Then from within ./test folder run:
37 |
38 | $ node readme-color-web-https.js
39 |
--------------------------------------------------------------------------------
/test/stubs/client-foo-pin.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | require('seneca')()
4 |
5 | .add('foo:1,bar:A', function (args, done) {
6 | done(null, 'localA-' + args.bar)
7 | })
8 |
9 | .client({pin: {foo: 2, bar: '*'}})
10 |
11 | .add('foo:3,bar:C', function (args, done) {
12 | done(null, 'localC-' + args.bar)
13 | })
14 |
15 | .client({pin: {foo: 4, bar: '*'}})
16 |
17 | .add('foo:5,bar:E', function (args, done) {
18 | done(null, 'localE-' + args.bar)
19 | })
20 |
21 | .ready(function () {
22 | this.act('foo:1,bar:A', function (err, out) {
23 | console.assert(!err)
24 | console.log(out)
25 | })
26 | this.act('foo:2,bar:B', function (err, out) {
27 | console.assert(!err)
28 | console.log(out)
29 | })
30 | this.act('foo:3,bar:C', function (err, out) {
31 | console.assert(!err)
32 | console.log(out)
33 | })
34 | this.act('foo:4,bar:D', function (err, out) {
35 | console.assert(!err)
36 | console.log(out)
37 | })
38 | this.act('foo:5,bar:E', function (err, out) {
39 | console.assert(!err)
40 | console.log(out)
41 | })
42 |
43 | this.act('foo:1,bar:A', function (err, out) {
44 | console.assert(!err)
45 | console.log(out)
46 | })
47 | this.act('foo:2,bar:B', function (err, out) {
48 | console.assert(!err)
49 | console.log(out)
50 | })
51 | this.act('foo:3,bar:C', function (err, out) {
52 | console.assert(!err)
53 | console.log(out)
54 | })
55 | this.act('foo:4,bar:D', function (err, out) {
56 | console.assert(!err)
57 | console.log(out)
58 | })
59 | this.act('foo:5,bar:E', function (err, out) {
60 | console.assert(!err)
61 | console.log(out)
62 | })
63 |
64 | this.act('foo:1,bar:A', function (err, out) {
65 | console.assert(!err)
66 | console.log(out)
67 | })
68 | this.act('foo:2,bar:B', function (err, out) {
69 | console.assert(!err)
70 | console.log(out)
71 | })
72 | this.act('foo:3,bar:C', function (err, out) {
73 | console.assert(!err)
74 | console.log(out)
75 | })
76 | this.act('foo:4,bar:D', function (err, out) {
77 | console.assert(!err)
78 | console.log(out)
79 | })
80 | this.act('foo:5,bar:E', function (err, out) {
81 | console.assert(!err)
82 | console.log(out)
83 | })
84 | })
85 |
--------------------------------------------------------------------------------
/test/stubs/client-foo.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var type = process.argv[2]
4 | console.log('TYPE:' + type)
5 |
6 | require('seneca')()
7 | .use('../transport.js')
8 | .client({type: type})
9 | .ready(function () {
10 | var seneca = this
11 | seneca.act('foo:1,bar:A', function (err, out) {
12 | console.assert(!err)
13 | console.log(out)
14 | })
15 | seneca.act('foo:2,bar:B', function (err, out) {
16 | console.assert(!err)
17 | console.log(out)
18 | })
19 |
20 | setInterval(function () {
21 | seneca.act('foo:3,bar:C', function (err, out) {
22 | console.assert(!err)
23 | console.log(out)
24 | })
25 | }, 1000)
26 | })
27 |
--------------------------------------------------------------------------------
/test/stubs/fault.js:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2014 Richard Rodger */
2 | 'use strict'
3 |
4 |
5 | // node fault.js
6 |
7 | var Test = require('seneca-transport-test')
8 |
9 | Test.foo_fault(require, process.argv[2] || 'tcp')
10 |
--------------------------------------------------------------------------------
/test/stubs/foo.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = function () {
4 | this.add('foo:1', function (args, done) { done(null, {s: '1-' + args.bar}) })
5 | this.add('foo:2', function (args, done) { done(null, {s: '2-' + args.bar}) })
6 | this.add('foo:3', function (args, done) { done(null, {s: '3-' + args.bar}) })
7 | this.add('foo:4', function (args, done) { done(null, {s: '4-' + args.bar}) })
8 | this.add('foo:5', function (args, done) { done(null, {s: '5-' + args.bar}) })
9 |
10 | this.add('bad:1', function (args, done) { done(new Error('ouch')) })
11 | }
12 |
--------------------------------------------------------------------------------
/test/stubs/memtest-transport.js:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2015 Richard Rodger, MIT License */
2 | 'use strict'
3 |
4 |
5 | //var _ = require('lodash')
6 | var Async = require('async')
7 |
8 | var queuemap = {}
9 |
10 |
11 | module.exports = function (options) {
12 | var seneca = this
13 | var so = seneca.options()
14 |
15 | options = seneca.util.deepextend(
16 | {
17 | memtest: {
18 | timeout: so.timeout ? so.timeout - 555 : 22222
19 | }
20 | },
21 | so.transport,
22 | options)
23 |
24 |
25 | var tu = seneca.export('transport/utils')
26 |
27 | seneca.add({role: 'transport', hook: 'listen', type: 'memtest'}, hook_listen_memtest)
28 | seneca.add({role: 'transport', hook: 'client', type: 'memtest'}, hook_client_memtest)
29 |
30 |
31 | function hook_listen_memtest (args, done) {
32 | var seneca = this
33 | var type = args.type
34 | var listen_options = seneca.util.clean(Object.assign({}, options[type], args))
35 |
36 | var dest = listen_options.dest || 'common'
37 | queuemap[dest] = queuemap[dest] || {}
38 |
39 | var topics = tu.listen_topics(seneca, args, listen_options)
40 |
41 | topics.forEach(function (topic) {
42 | seneca.log.debug('listen', 'subscribe', topic + '_act', listen_options, seneca)
43 |
44 | queuemap[dest][topic + '_act'] = Async.queue(function (data, done) {
45 | tu.handle_request(seneca, data, listen_options, function (out) {
46 | if (null == out) {
47 | return done()
48 | }
49 |
50 | queuemap[dest][topic + '_res'].push(out)
51 | return done()
52 | })
53 | })
54 | })
55 |
56 | tu.close(seneca, function (done) {
57 | done()
58 | })
59 |
60 | seneca.log.info('listen', 'open', listen_options, seneca)
61 |
62 | done()
63 | }
64 |
65 |
66 | function hook_client_memtest (args, clientdone) {
67 | var seneca = this
68 | var type = args.type
69 | var client_options = seneca.util.clean(Object.assign({}, options[type], args))
70 |
71 | var dest = client_options.dest || 'common'
72 | queuemap[dest] = queuemap[dest] || {}
73 |
74 | tu.make_client(make_send, client_options, clientdone)
75 |
76 | function make_send (spec, topic, send_done) {
77 | seneca.log.debug('client', 'subscribe', topic + '_res', client_options, seneca)
78 |
79 | queuemap[dest][topic + '_res'] = Async.queue(function (data, done) {
80 | tu.handle_response(seneca, data, client_options)
81 | return done()
82 | })
83 |
84 | send_done(null, function (args, done, meta) {
85 | var outmsg = tu.prepare_request(seneca, args, done, meta)
86 |
87 | queuemap[dest][topic + '_act'].push(outmsg)
88 | })
89 | }
90 |
91 | tu.close(seneca, function (done) {
92 | done()
93 | })
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/test/stubs/readme-color-client.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var Seneca = require('seneca')
4 |
5 | Seneca()
6 | .client()
7 | .act('color:red')
8 |
9 | // node readme-color-client.js --seneca.log=type:act,regex:color:red
10 |
--------------------------------------------------------------------------------
/test/stubs/readme-color-service.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | function color () {
4 | this.add('color:red', function (args, done) {
5 | done(null, {hex: '#FF0000'})
6 | })
7 | }
8 |
9 |
10 | var Seneca = require('seneca')
11 |
12 | Seneca()
13 | .use(color)
14 | .listen()
15 |
16 |
17 | // node readme-color-service.js --seneca.log=type:act,regex:color:red
18 |
19 | // curl -d '{"color":"red"}' http://localhost:10101/act
20 |
--------------------------------------------------------------------------------
/test/stubs/readme-color-tcp.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | function color () {
4 | this.add('color:red', function (args, done) {
5 | done(null, {hex: '#FF0000'})
6 | })
7 | }
8 |
9 |
10 | var Seneca = require('seneca')
11 |
12 | Seneca()
13 | .use(color)
14 | .listen({type: 'tcp'})
15 |
16 | Seneca()
17 | .client({type: 'tcp'})
18 | .act('color:red')
19 |
20 | // node readme-color-tcp.js --seneca.log=plugin:transport,level:INFO --seneca.log=type:act,regex:color:red
21 |
--------------------------------------------------------------------------------
/test/stubs/readme-color-web-https.js:
--------------------------------------------------------------------------------
1 | /* jshint node:true, asi:true, eqnull:true */
2 | 'use strict'
3 | var Seneca = require('seneca')
4 | var Fs = require('fs')
5 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
6 |
7 | function color () {
8 | this.add('color:red', function (args, done) {
9 | console.log('Connected to client! Color returned:', {hex: '#FF0000'})
10 | done(null, {hex: '#FF0000'})
11 | })
12 | }
13 |
14 | Seneca()
15 | .use('../transport')
16 | .use(color)
17 | .listen({
18 | type: 'web',
19 | port: 8000,
20 | host: '127.0.0.1',
21 | protocol: 'https',
22 | serverOptions: {
23 | key: Fs.readFileSync('ssl/key.pem', 'utf8'),
24 | cert: Fs.readFileSync('ssl/cert.pem', 'utf8')
25 | }
26 | })
27 | .ready(function () {
28 | Seneca()
29 | .use('../transport')
30 | .client({
31 | type: 'http',
32 | port: 8000,
33 | host: '127.0.0.1',
34 | protocol: 'https'
35 | })
36 | .act('color:red', function (error, res) {
37 | if (error) {
38 | console.log(error)
39 | }
40 | console.log('Result from service: ', res)
41 | })
42 | })
43 | // node readme-color.js --seneca.log=type:act,regex:color:red
44 |
--------------------------------------------------------------------------------
/test/stubs/readme-color.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | function color () {
4 | this.add('color:red', function (args, done) {
5 | done(null, {hex: '#FF0000'})
6 | })
7 | }
8 |
9 |
10 | var Seneca = require('seneca')
11 |
12 | Seneca()
13 | .use(color)
14 | .listen()
15 |
16 | Seneca()
17 | .client()
18 | .act('color:red')
19 |
20 | // node readme-color.js --seneca.log=type:act,regex:color:red
21 |
--------------------------------------------------------------------------------
/test/stubs/readme-many-colors-client.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var Seneca = require('seneca')
4 |
5 |
6 | Seneca()
7 |
8 | // send matching actions out over the network
9 | .client({ port: 8081, pin: 'color:red' })
10 | .client({ port: 8082, pin: 'color:green' })
11 | .client({ port: 8083, pin: 'color:blue' })
12 |
13 | // an aggregration action that calls other actions
14 | .add('list:colors', function (args, done) {
15 | var seneca = this
16 | var colors = {}
17 |
18 | args.names.forEach(function (name) {
19 | seneca.act({color: name}, function (err, result) {
20 | if (err) {
21 | return done(err)
22 | }
23 |
24 | colors[name] = result.hex
25 | if (Object.keys(colors).length === args.names.length) {
26 | return done(null, colors)
27 | }
28 | })
29 | })
30 | })
31 |
32 | .listen()
33 |
34 | // this is a sanity check
35 | .act({list: 'colors', names: ['blue', 'green', 'red']}, console.log)
36 |
37 | // node readme-many-colors-client.js --seneca.log=type:act,regex:CLIENT
38 |
--------------------------------------------------------------------------------
/test/stubs/readme-many-colors-server.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var color = process.argv[2]
4 | var hexval = process.argv[3]
5 | var port = process.argv[4]
6 |
7 | var Seneca = require('seneca')
8 |
9 | Seneca()
10 |
11 | .add('color:' + color, function (args, done) {
12 | done(null, {hex: '#' + hexval})
13 | })
14 |
15 | .listen(port)
16 |
17 | .log.info('color', color, hexval, port)
18 |
19 | // node readme-many-colors-server.js red FF0000 8081 --seneca.log=level:info --seneca.log=type:act,regex:color
20 |
--------------------------------------------------------------------------------
/test/stubs/readme-many-colors.sh:
--------------------------------------------------------------------------------
1 | node readme-many-colors-server.js red FF0000 8081 --seneca.log=level:info --seneca.log=type:act,regex:color &
2 | node readme-many-colors-server.js green 00FF00 8082 --seneca.log=level:info --seneca.log=type:act,regex:color &
3 | node readme-many-colors-server.js blue 0000FF 8083 --seneca.log=level:info --seneca.log=type:act,regex:color &
4 |
5 | node readme-many-colors-client.js --seneca.log=type:act,regex:CLIENT &
6 | sleep 1
7 | echo
8 | echo
9 |
10 |
11 | curl -d '{"color":"red"}' http://localhost:10101/act
12 | echo
13 | echo
14 | sleep 1
15 |
16 | curl -d '{"color":"green"}' http://localhost:10101/act
17 | echo
18 | echo
19 | sleep 1
20 |
21 | curl -d '{"color":"blue"}' http://localhost:10101/act
22 | echo
23 | echo
24 | sleep 1
25 |
26 | curl -d '{"list":"colors","names":["red","green","blue"]}' http://localhost:10101/act
27 | echo
28 | echo
29 | sleep 1
30 |
31 | killall node
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/test/stubs/service-foo.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var type = process.argv[2]
4 | console.log('TYPE:' + type)
5 |
6 | require('seneca')()
7 | .use('../transport.js')
8 | .use('foo')
9 | .listen({type: type})
10 |
--------------------------------------------------------------------------------
/test/tcp.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var Util = require('util')
4 | var Assert = require('assert')
5 | var Fs = require('fs')
6 | var Code = require('@hapi/code')
7 | var Lab = require('@hapi/lab')
8 | var Tcp = require('../lib/tcp')
9 | var TransportUtil = require('../lib/transport-utils')
10 | var ChildProcess = require('child_process')
11 | var Path = require('path')
12 |
13 | var CreateInstance = require('./utils/createInstance')
14 | var CreateClient = require('./utils/createClient')
15 |
16 | var lab = (exports.lab = Lab.script())
17 | var describe = lab.describe
18 | var it = make_it(lab)
19 | var expect = Code.expect
20 |
21 | describe('Specific tcp', function () {
22 | it('client and listen work as expected', function (fin) {
23 | var instance = CreateInstance()
24 |
25 | instance.add('c:1', function (args, done) {
26 | done(null, { s: '1-' + args.d })
27 | })
28 |
29 | instance.listen({ type: 'tcp', port: 20102 })
30 |
31 | instance.ready(function () {
32 | var seneca = this
33 | var count = 0
34 |
35 | function check() {
36 | count++
37 |
38 | if (count === 3) {
39 | seneca.close(fin)
40 | }
41 | }
42 |
43 | CreateClient('tcp', 20102, check, 'cln0')
44 | CreateClient('tcp', 20102, check, 'cln1')
45 | CreateClient('tcp', 20102, check, 'cln2')
46 | })
47 | })
48 |
49 | it('error-passing-tcp', function (fin) {
50 | CreateInstance()
51 | .add('a:1', function (args, done) {
52 | done(new Error('bad-wire'))
53 | })
54 | .listen({ type: 'tcp', port: 40404 })
55 |
56 | CreateInstance()
57 | .client({ type: 'tcp', port: 40404 })
58 | .act('a:1', function (err, out) {
59 | Assert.equal('seneca: Action a:1 failed: bad-wire.', err.message)
60 | fin()
61 | })
62 | })
63 |
64 | it('can listen on ephemeral port', function (done) {
65 | var seneca = CreateInstance()
66 |
67 | var settings = { tcp: { port: 0, host: '127.0.0.1' } }
68 |
69 | var transportUtil = new TransportUtil({
70 | callmap: {},
71 | seneca: seneca,
72 | options: settings,
73 | })
74 |
75 | var tcp = Tcp.listen(settings, transportUtil)
76 |
77 | expect(typeof tcp).to.equal('function')
78 |
79 | tcp.call(seneca, { type: 'tcp' }, function (err) {
80 | expect(err).to.not.exist()
81 | done()
82 | })
83 | })
84 |
85 | it(
86 | 'can listen on unix path',
87 | { skip: /win/.test(process.platform) },
88 | function (done) {
89 | var sock = '/tmp/seneca.sock'
90 |
91 | if (Fs.existsSync(sock)) {
92 | Fs.unlinkSync(sock)
93 | }
94 |
95 | var seneca = CreateInstance()
96 | var settings = { tcp: { path: sock } }
97 |
98 | var transportUtil = new TransportUtil({
99 | callmap: {},
100 | seneca: seneca,
101 | options: settings,
102 | })
103 |
104 | var tcp = Tcp.listen(settings, transportUtil)
105 | expect(typeof tcp).to.equal('function')
106 |
107 | tcp.call(seneca, { type: 'tcp' }, function (err) {
108 | expect(err).to.not.exist()
109 | done()
110 | })
111 | },
112 | )
113 |
114 | it('will retry listening a specified number of times', function (done) {
115 | var seneca1 = CreateInstance()
116 | var seneca2 = CreateInstance()
117 |
118 | var settings1 = { tcp: { port: 0 } }
119 |
120 | var transportUtil1 = new TransportUtil({
121 | callmap: {},
122 | seneca: seneca1,
123 | options: settings1,
124 | })
125 |
126 | var tcp1 = Tcp.listen(settings1, transportUtil1)
127 | expect(typeof tcp1).to.equal('function')
128 |
129 | tcp1.call(seneca1, { type: 'tcp' }, function (err, address) {
130 | expect(err).to.not.exist()
131 |
132 | var settings2 = {
133 | tcp: {
134 | port: address.port,
135 | max_listen_attempts: 10,
136 | attempt_delay: 10,
137 | },
138 | }
139 |
140 | var transportUtil2 = new TransportUtil({
141 | callmap: {},
142 | seneca: seneca2,
143 | options: settings2,
144 | })
145 | var tcp2 = Tcp.listen(settings2, transportUtil2)
146 | expect(typeof tcp2).to.equal('function')
147 |
148 | setTimeout(function () {
149 | seneca1.close()
150 | }, 20)
151 |
152 | tcp2.call(seneca2, { type: 'tcp' }, function (err, address) {
153 | expect(err).to.not.exist()
154 | done()
155 | })
156 | })
157 | })
158 |
159 | it('defaults to 127.0.0.1 for connections', function (done) {
160 | var seneca = CreateInstance()
161 |
162 | var settings = {
163 | tcp: {
164 | port: 0,
165 | },
166 | }
167 |
168 | var transportUtil = new TransportUtil({
169 | callmap: {},
170 | seneca: seneca,
171 | options: settings,
172 | })
173 |
174 | var server = Tcp.listen(settings, transportUtil)
175 | expect(typeof server).to.equal('function')
176 |
177 | server.call(seneca, { type: 'tcp' }, function (err, address) {
178 | expect(err).to.not.exist()
179 | expect(address.type).to.equal('tcp')
180 | settings.tcp.port = address.port
181 | var client = Tcp.client(settings, transportUtil)
182 | expect(typeof client).to.equal('function')
183 | client.call(seneca, { type: 'tcp' }, function (err) {
184 | expect(err).to.not.exist()
185 | done()
186 | })
187 | })
188 | })
189 |
190 | /*
191 | it.skip('handles reconnects', function(done) {
192 | var serverPath = Path.join(__dirname, 'reconnect', 'server.js')
193 | var clientPath = Path.join(__dirname, 'reconnect', 'client.js')
194 |
195 | var server = ChildProcess.fork(serverPath)
196 | var client = ChildProcess.fork(clientPath)
197 | var actedCount = 0
198 |
199 | server.once('message', function(address) {
200 | client.on('message', function(message) {
201 | if (!message.acted) {
202 | return
203 | }
204 |
205 | actedCount++
206 | server.kill('SIGKILL')
207 | setTimeout(function() {
208 | server = ChildProcess.fork(serverPath, [address.port])
209 | }, 500)
210 | })
211 | client.send({ port: address.port })
212 |
213 | var finish = function() {
214 | expect(actedCount).to.equal(1)
215 | server.kill('SIGKILL')
216 | client.kill('SIGKILL')
217 | done()
218 | finish = function() {}
219 | }
220 |
221 | setTimeout(finish, 2000)
222 | })
223 | })
224 | */
225 | })
226 |
227 | function make_it(lab) {
228 | return function it(name, opts, func) {
229 | if ('function' === typeof opts) {
230 | func = opts
231 | opts = {}
232 | }
233 |
234 | lab.it(
235 | name,
236 | opts,
237 | Util.promisify(function (x, fin) {
238 | func(fin)
239 | }),
240 | )
241 | }
242 | }
243 |
--------------------------------------------------------------------------------
/test/utils/createClient.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var Assert = require('assert')
4 | var CreateInstance = require('./createInstance')
5 |
6 | function createClient (type, port, done, tag) {
7 | CreateInstance()
8 | .client({type: type, port: port})
9 | .ready(function () {
10 | this.act('c:1,d:A', function (err, out) {
11 | if (err) return done(err)
12 |
13 | Assert.equal('{"s":"1-A"}', JSON.stringify(out))
14 |
15 | this.act('c:1,d:AA', function (err, out) {
16 | if (err) return done(err)
17 |
18 | Assert.equal('{"s":"1-AA"}', JSON.stringify(out))
19 |
20 | this.close(done)
21 | })
22 | })
23 | })
24 | }
25 |
26 | module.exports = createClient
27 |
--------------------------------------------------------------------------------
/test/utils/createInstance.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var Seneca = require('seneca')
4 |
5 | var Transport = require('../../')
6 |
7 | var defaults = {
8 | default_plugins: {transport: false},
9 | }
10 |
11 | function createInstance (options, transportOptions) {
12 | options = {...defaults, ...options}
13 |
14 | var instance = Seneca(options).test()
15 | instance.use(Transport, transportOptions || {})
16 |
17 | return instance
18 | }
19 |
20 | module.exports = createInstance
21 |
--------------------------------------------------------------------------------
/transport.js:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2013-2015 Richard Rodger & other contributors, MIT License */
2 | /* jshint node:true, asi:true, eqnull:true */
3 | 'use strict'
4 |
5 | // Load modules
6 | var LruCache = require('lru-cache')
7 | var Tcp = require('./lib/tcp')
8 | var TransportUtil = require('./lib/transport-utils.js')
9 | var Http = require('./lib/http')
10 |
11 | // Declare internals
12 | var internals = {
13 | defaults: {
14 | msgprefix: 'seneca_',
15 | callmax: 1111,
16 | msgidlen: 12,
17 | warn: {
18 | unknown_message_id: true,
19 | invalid_kind: true,
20 | invalid_origin: true,
21 | no_message_id: true,
22 | message_loop: true,
23 | own_message: true,
24 | },
25 | check: {
26 | message_loop: true,
27 | own_message: true,
28 | },
29 | web: {
30 | type: 'web',
31 | port: 10101,
32 | host: '0.0.0.0',
33 | path: '/act',
34 | protocol: 'http',
35 | timeout: 5555,
36 | max_listen_attempts: 11,
37 | attempt_delay: 222,
38 | serverOptions: {},
39 | },
40 | tcp: {
41 | type: 'tcp',
42 | host: '0.0.0.0',
43 | port: 10201,
44 | timeout: 5555,
45 | },
46 | },
47 | plugin: 'transport',
48 | }
49 |
50 | module.exports = function transport(options) {
51 | var seneca = this
52 |
53 | var settings = seneca.util.deepextend(internals.defaults, options)
54 | var callmap = new LruCache({ max: settings.callmax })
55 | var transportUtil = new TransportUtil({
56 | callmap: callmap,
57 | seneca: seneca,
58 | options: settings,
59 | })
60 |
61 | seneca.add(
62 | { role: internals.plugin, cmd: 'inflight' },
63 | internals.inflight(callmap),
64 | )
65 | seneca.add({ role: internals.plugin, cmd: 'listen' }, internals.listen)
66 | seneca.add({ role: internals.plugin, cmd: 'client' }, internals.client)
67 |
68 | seneca.add(
69 | { role: internals.plugin, hook: 'listen', type: 'tcp' },
70 | Tcp.listen(settings, transportUtil),
71 | )
72 | seneca.add(
73 | { role: internals.plugin, hook: 'client', type: 'tcp' },
74 | Tcp.client(settings, transportUtil),
75 | )
76 |
77 | seneca.add(
78 | { role: internals.plugin, hook: 'listen', type: 'web' },
79 | Http.listen(settings, transportUtil),
80 | )
81 | seneca.add(
82 | { role: internals.plugin, hook: 'client', type: 'web' },
83 | Http.client(settings, transportUtil),
84 | )
85 |
86 | // Aliases.
87 | seneca.add(
88 | { role: internals.plugin, hook: 'listen', type: 'http' },
89 | Http.listen(settings, transportUtil),
90 | )
91 | seneca.add(
92 | { role: internals.plugin, hook: 'client', type: 'http' },
93 | Http.client(settings, transportUtil),
94 | )
95 |
96 | // Legacy API.
97 | seneca.add(
98 | { role: internals.plugin, hook: 'listen', type: 'direct' },
99 | Http.listen(settings, transportUtil),
100 | )
101 | seneca.add(
102 | { role: internals.plugin, hook: 'client', type: 'direct' },
103 | Http.client(settings, transportUtil),
104 | )
105 |
106 | return {
107 | name: internals.plugin,
108 | exportmap: { utils: transportUtil },
109 | options: settings,
110 | }
111 | }
112 |
113 | module.exports.preload = function () {
114 | var seneca = this
115 |
116 | var meta = {
117 | name: internals.plugin,
118 | exportmap: {
119 | utils: function () {
120 | var transportUtil = seneca.export(internals.plugin).utils
121 | if (transportUtil !== meta.exportmap.utils) {
122 | transportUtil.apply(this, arguments)
123 | }
124 | },
125 | },
126 | }
127 |
128 | return meta
129 | }
130 |
131 | internals.inflight = function (callmap) {
132 | return function (args, callback) {
133 | var inflight = {}
134 | callmap.forEach(function (val, key) {
135 | inflight[key] = val
136 | })
137 | callback(null, inflight)
138 | }
139 | }
140 |
141 | internals.listen = function (args, callback) {
142 | var seneca = this
143 |
144 | var config = Object.assign({}, args.config, {
145 | role: internals.plugin,
146 | hook: 'listen',
147 | })
148 | //var listen_args = seneca.util.clean(_.omit(config, 'cmd'))
149 | var listen_args = seneca.util.clean(config)
150 | delete config.cmd
151 | var legacyError = internals.legacyError(seneca, listen_args.type)
152 | if (legacyError) {
153 | return callback(legacyError)
154 | }
155 | seneca.act(listen_args, callback)
156 | }
157 |
158 | internals.client = function (args, callback) {
159 | var seneca = this
160 |
161 | var config = Object.assign({}, args.config, {
162 | role: internals.plugin,
163 | hook: 'client',
164 | })
165 | //var client_args = seneca.util.clean(_.omit(config, 'cmd'))
166 | var client_args = seneca.util.clean(config)
167 | delete config.cmd
168 | var legacyError = internals.legacyError(seneca, client_args.type)
169 | if (legacyError) {
170 | return callback(legacyError)
171 | }
172 | seneca.act(client_args, callback)
173 | }
174 |
175 | internals.legacyError = function (seneca, type) {
176 | if (type === 'pubsub') {
177 | return seneca.fail('plugin-needed', { name: 'seneca-redis-transport' })
178 | }
179 | if (type === 'queue') {
180 | return seneca.fail('plugin-needed', { name: 'seneca-beanstalkd-transport' })
181 | }
182 | }
183 |
--------------------------------------------------------------------------------