├── .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 | ![Seneca](http://senecajs.org/files/assets/seneca-logo.png) 2 | > A [Seneca.js][] transport plugin 3 | 4 | | ![Voxgig](https://www.voxgig.com/res/img/vgt01r.png) | 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 | --------------------------------------------------------------------------------