├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── client.js ├── examples ├── cli-extended.js ├── cli-single-function.js ├── client.js ├── custom-serializer.js ├── example.js ├── reconnect.js ├── server.js ├── use.js └── validation.js ├── lib ├── bin.js ├── client.js └── server.js ├── package.json ├── server.js └── test ├── async-await.js ├── basic.test.js ├── cli.test.js ├── custom-parser-serializer.test.js ├── errors.test.js ├── get-port.js ├── ipc.test.js ├── reconnect.test.js ├── use.test.js └── validation.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # mac files 61 | .DS_Store 62 | 63 | # vim swap files 64 | *.swp 65 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "8" 5 | - "7" 6 | - "6" 7 | - "5" 8 | - "4" 9 | 10 | after_script: 11 | - npm run coveralls 12 | 13 | notifications: 14 | email: 15 | on_success: never 16 | on_failure: always 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Tomas Della Vedova 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rinvoke 2 | 3 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/) 4 | [![Build Status](https://travis-ci.org/delvedor/rinvoke.svg?branch=master)](https://travis-ci.org/delvedor/rinvoke) [![Coverage Status](https://coveralls.io/repos/github/delvedor/rinvoke/badge.svg?branch=master)](https://coveralls.io/github/delvedor/rinvoke?branch=master) 5 | 6 | Build your distributed functions system with **Rinvoke**! 7 | **Rinvoke** is a RPC library based on net sockets, can work both with **tcp sockets** and **ipc**. 8 | It has built in reconnect logic and supports multiple parser/serializers, such as [msgpack](http://msgpack.org/) or [protbuf](https://developers.google.com/protocol-buffers/). 9 | Internally uses [tentacoli](https://github.com/mcollina/tentacoli) to multiplex the requests and [avvio](https://github.com/mcollina/avvio) to guarantee the asynchronous bootstrap of the application, it also provide an handy request validation out of the box with [*JSON schema*](http://json-schema.org/). 10 | 11 | 12 | ## Install 13 | ``` 14 | npm i rinvoke --save 15 | ``` 16 | 17 | 18 | ## Usage 19 | Rinvoke could be used as a server or client, so when your require it you must specify it. 20 | Let's see an example for the server: 21 | ```js 22 | const rinvoke = require('rinvoke/server')() 23 | 24 | rinvoke.register('concat', (req, reply) => { 25 | reply(null, req.a + req.b) 26 | }) 27 | 28 | rinvoke.listen(3000, err => { 29 | if (err) throw err 30 | }) 31 | ``` 32 | And now for the client: 33 | ```js 34 | const rinvoke = require('rinvoke/client')({ 35 | port: 3000 36 | }) 37 | 38 | rinvoke.invoke({ 39 | procedure: 'concat', 40 | a: 'hello ', 41 | b: 'world' 42 | }, (err, result) => { 43 | if (err) { 44 | console.log(err) 45 | return 46 | } 47 | console.log(result) 48 | }) 49 | ``` 50 | The client could seem synchronous but internally everything is handled asynchronously with events. 51 | Checkout the [examples folder](https://github.com/delvedor/rinvoke/tree/master/examples) if you want to see more examples! 52 | 53 | 54 | ## API 55 | 56 | 57 | ### Server 58 | #### `server([opts])` 59 | Instance a new server, the options object can accept a custom parser/serializer via the `codec` field. 60 | ```js 61 | const rinvoke = require('rinvoke/server')({ 62 | codec: { 63 | encode: JSON.stringify, 64 | decode: JSON.parse 65 | } 66 | }) 67 | ``` 68 | The default codec is *JSON*. 69 | **Events**: 70 | - `'connection'` 71 | - `'error'` 72 | - `'request'` 73 | - `'listening'` 74 | 75 | #### `register(procedureName, [schema,] procedureFunction)` 76 | Registers a new procedure, the name of the procedure must be a string, the function has the following signature: `(request, reply)` where `request` is the request object and `reply` a function t send the response back to the client. 77 | ```js 78 | rinvoke.register('concat', (req, reply) => { 79 | reply(null, req.a + req.b) 80 | }) 81 | ``` 82 | *Promises* and *async/await* are supported as well! 83 | ```js 84 | rinvoke.register('concat', async req => { 85 | return req.a + req.b 86 | }) 87 | ``` 88 | 89 | #### Validation 90 | Rinvoke offers you out of the box a nice and standard way to validate your requests, [*JSON schema*](http://json-schema.org/)! 91 | Internally uses [ajv](https://github.com/epoberezkin/ajv/blob/master/README.md) to achieve the maximum speed and correctness. 92 | ```js 93 | rinvoke.register('concat', { 94 | type: 'object', 95 | properties: { 96 | a: { type: 'string' }, 97 | b: { type: 'string' } 98 | }, 99 | required: ['a', 'b'] 100 | }, (req, reply) => { 101 | reply(null, req.a + req.b) 102 | }) 103 | ``` 104 | 105 | #### `listen(portOrPath, [address], callback)` 106 | Run the server over the specified `port` (and `address`, default to `127.0.0.1`), if you specify a path (as a string) 107 | it will use the system socket to perform ipc. 108 | ```js 109 | rinvoke.listen(3000, err => { 110 | if (err) throw err 111 | }) 112 | 113 | rinvoke.listen(3000, '127.0.0.1', err => { 114 | if (err) throw err 115 | }) 116 | 117 | rinvoke.listen('/tmp/socket.sock', err => { 118 | if (err) throw err 119 | }) 120 | ``` 121 | 122 | 123 | ### Client 124 | 125 | #### `client(options)` 126 | Instance a new client, the options object must contain a `port` or `path` field, furthermore can accept a custom parser/serializer via the `codec` field. If you want to activate the automatic reconnection handling pass `reconnect: true` (3 attempts with 1s timeout), if you want to configure the timeout handling pass an object like the following: 127 | ```js 128 | const rinvoke = require('rinvoke/client')({ 129 | port: 3000, 130 | address: '127.0.0.1' 131 | reconnect: { 132 | attempts: 5, 133 | timeout: 2000 134 | }, 135 | codec: { 136 | encode: JSON.stringify, 137 | decode: JSON.parse 138 | } 139 | }) 140 | ``` 141 | The default codec is *JSON*. 142 | **Events**: 143 | - `'connect'` 144 | - `'error'` 145 | - `'close'` 146 | - `'timeout'` 147 | 148 | #### `invoke(request, callback)` 149 | Invoke a procedure on the server, the request object **must** contain the key `procedure` with the name of the function to call. 150 | The callback is a function with the following signature: `(error, response)`. 151 | ```js 152 | rinvoke.invoke({ 153 | procedure: 'concat', 154 | a: 'hello ', 155 | b: 'world' 156 | }, (err, result) => { 157 | if (err) { 158 | console.log(err) 159 | return 160 | } 161 | console.log(result) 162 | }) 163 | ``` 164 | *Promises* are supported as well! 165 | ```js 166 | rinvoke 167 | .invoke({ 168 | procedure: 'concat', 169 | a: 'a', 170 | b: 'b' 171 | }) 172 | .then(console.log) 173 | .catch(console.log) 174 | ``` 175 | 176 | #### `fire(request [, callback])` 177 | Fire (and forget) a procedure on the server, the request object **must** contain the key `procedure` with the name of the function to call. 178 | The optional callback will be called if there is an error while sending the message, or after the message has been sent successfully. 179 | 180 | ```js 181 | rinvoke.fire({ 182 | procedure: 'concat', 183 | a: 'hello ', 184 | b: 'world' 185 | }) 186 | ``` 187 | 188 | #### `timeout(time)` 189 | Sets the timeout of the socket. 190 | #### `keepAlive(bool)` 191 | Sets the `keep-alive` property. 192 | 193 | 194 | ### Method for both client and server 195 | #### `use(callback)` 196 | The callback is a function witb the following signature: `instance, options, next`. 197 | Where `instance` is the client instance, options, is an options object and `next` a function you must call when your code is ready. 198 | This api is useful if you need to load an utility, a database connection for example. `use` will guarantee the load order an that your client/server will boot up once every `use` has completed. 199 | ```js 200 | rinvoke.use((instance, opts, next) => { 201 | dbClient.connect(opts.url, (err, conn) => { 202 | instance.db = conn // now you can access in your function the database connection with `this.db` 203 | next() 204 | }) 205 | }) 206 | ``` 207 | #### `onClose(callback)` 208 | Hook that will be called once you fire the `close` callback. 209 | ```js 210 | rinvoke.onClose((instance, done) => { 211 | // do something 212 | done() 213 | }) 214 | ``` 215 | 216 | #### `close(callback)` 217 | Once you call this function the socket server and client will close and all the registered functions with `onClose` will be called. 218 | ```js 219 | rinvoke.close((err, instance, done) => { 220 | // do something 221 | done() 222 | }) 223 | ``` 224 | 225 | ### CLI 226 | You can even run the server with the integrated cli! 227 | In your `package.json` add: 228 | ```json 229 | { 230 | "scripts": { 231 | "start": "rinvoke server.js" 232 | } 233 | } 234 | ``` 235 | And then create your server file: 236 | ```js 237 | module.exports = async req => `Hello ${req.name}!` 238 | ``` 239 | You can also use an extended version of the above example: 240 | ```js 241 | function sayHello (rinvoke, opts, next) { 242 | rinvoke.register('hello', (req, reply) => { 243 | reply(null, { hello: 'world' }) 244 | }) 245 | 246 | next() 247 | } 248 | 249 | module.exports = sayHello 250 | ``` 251 | The options of the cli are: 252 | ``` 253 | --port -p # default 3000 254 | --address -a # default 127.0.0.1 255 | --path -P # path of the ipc web socket 256 | --name -n # name of your exported function 257 | ``` 258 | 259 | 260 | ## Acknowledgements 261 | 262 | This project is kindly sponsored by [LetzDoIt](http://www.letzdoitapp.com/). 263 | 264 | 265 | ## License 266 | 267 | [MIT](./LICENSE) 268 | 269 | Copyright © 2017 Tomas Della Vedova 270 | -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/client') 2 | -------------------------------------------------------------------------------- /examples/cli-extended.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function sayHello (rinvoke, opts, next) { 4 | rinvoke.register('hello', (req, reply) => { 5 | reply(null, { hello: 'world' }) 6 | }) 7 | 8 | next() 9 | } 10 | 11 | module.exports = sayHello 12 | -------------------------------------------------------------------------------- /examples/cli-single-function.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function sayHello (req, reply) { 4 | reply(null, 'hello!') 5 | } 6 | 7 | module.exports = sayHello 8 | -------------------------------------------------------------------------------- /examples/client.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rinvoke = require('../client')({ 4 | port: 3030, 5 | host: '127.0.0.1' 6 | }) 7 | 8 | rinvoke.invoke({ 9 | procedure: 'concat', 10 | a: 'a', 11 | b: 'b' 12 | }, (err, result) => { 13 | if (err) console.log(err) 14 | console.log(result) 15 | }) 16 | 17 | rinvoke 18 | .invoke({ 19 | procedure: 'concat', 20 | a: 'a', 21 | b: 'b' 22 | }) 23 | .then(console.log) 24 | .catch(console.log) 25 | -------------------------------------------------------------------------------- /examples/custom-serializer.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const msgpack = require('msgpack5')() 4 | const server = require('../lib/server')({ 5 | codec: msgpack 6 | }) 7 | const Client = require('../lib/client') 8 | 9 | // register a new function 10 | server.register('concat', (req, reply) => reply(null, req.a + req.b)) 11 | 12 | // run the listener 13 | server.listen(3030, err => { 14 | if (err) throw err 15 | 16 | // connect to the listener 17 | const client = Client({ 18 | port: 3030, 19 | codec: msgpack 20 | }) 21 | // invoke the remote function 22 | client.invoke({ 23 | procedure: 'concat', 24 | a: 'a', 25 | b: 'b' 26 | }, (err, res) => { 27 | if (err) console.log(err) 28 | console.log('concat:', res) 29 | // close the connections 30 | server.close(err => { if (err) throw err }) 31 | client.close(err => { if (err) throw err }) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /examples/example.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const server = require('../lib/server')() 4 | const Client = require('../lib/client') 5 | 6 | // register a new function 7 | server.register('concat', (req, reply) => reply(null, req.a + req.b)) 8 | 9 | // run the listener 10 | server.listen(3030, err => { 11 | if (err) throw err 12 | 13 | // connect to the listener 14 | const client = Client({ port: 3030 }) 15 | // invoke the remote function 16 | client.invoke({ 17 | procedure: 'concat', 18 | a: 'a', 19 | b: 'b' 20 | }, (err, res) => { 21 | if (err) console.log(err) 22 | console.log('concat:', res) 23 | // close the connections 24 | server.close(err => { if (err) throw err }) 25 | client.close(err => { if (err) throw err }) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /examples/reconnect.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const server = require('../lib/server')() 4 | const Client = require('../lib/client') 5 | 6 | // register a new function 7 | server.register('concat', (req, reply) => reply(null, req.a + req.b)) 8 | 9 | // run the listener 10 | server.listen(3030, err => { 11 | if (err) throw err 12 | 13 | // connect to the listener 14 | const client = Client({ 15 | port: 3030, 16 | reconnect: { // could be also true 17 | attempts: 3, 18 | timeout: 1000 19 | } 20 | }) 21 | // invoke the remote function 22 | client.invoke({ 23 | procedure: 'concat', 24 | a: 'a', 25 | b: 'b' 26 | }, (err, res) => { 27 | if (err) console.log(err) 28 | console.log('concat:', res) 29 | // close the connections 30 | server.close(err => { if (err) throw err }) 31 | client.close(err => { if (err) throw err }) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /examples/server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const rinvoke = require('../server')() 4 | 5 | rinvoke.register('concat', (req, reply) => reply(null, req.a + req.b)) 6 | 7 | rinvoke.listen(3030, err => { 8 | if (err) throw err 9 | }) 10 | -------------------------------------------------------------------------------- /examples/use.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const server = require('../lib/server')() 4 | const Client = require('../lib/client') 5 | 6 | server.use((instance, opts, next) => { 7 | instance.concat = (a, b) => a + b 8 | next() 9 | }) 10 | // register a new function 11 | server.register('concat', (req, reply) => { 12 | reply(null, server.concat(req.a, req.b)) 13 | }) 14 | 15 | // run the listener 16 | server.listen(3030, err => { 17 | if (err) throw err 18 | 19 | // connect to the listener 20 | const client = Client({ port: 3030 }) 21 | // invoke the remote function 22 | client.invoke({ 23 | procedure: 'concat', 24 | a: 'a', 25 | b: 'b' 26 | }, (err, res) => { 27 | if (err) console.log(err) 28 | console.log('concat:', res) 29 | // close the connections 30 | server.close(err => { if (err) throw err }) 31 | client.close(err => { if (err) throw err }) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /examples/validation.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const server = require('../lib/server')() 4 | const Client = require('../lib/client') 5 | 6 | // register a new function 7 | server.register('concat', { 8 | type: 'object', 9 | properties: { 10 | a: { type: 'string' }, 11 | b: { type: 'string' } 12 | }, 13 | required: ['a', 'b'] 14 | }, (req, reply) => { 15 | reply(null, req.a + req.b) 16 | }) 17 | 18 | // run the listener 19 | server.listen(3030, err => { 20 | if (err) throw err 21 | 22 | // connect to the listener 23 | const client = Client({ port: 3030 }) 24 | // invoke the remote function 25 | client.invoke({ 26 | procedure: 'concat', 27 | a: 'a', 28 | b: 'b' 29 | }, (err, res) => { 30 | if (err) console.log(err) 31 | console.log('concat:', res) 32 | // close the connections 33 | server.close(err => { if (err) throw err }) 34 | client.close(err => { if (err) throw err }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /lib/bin.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | 'use strict' 4 | 5 | const path = require('path') 6 | const minimist = require('minimist') 7 | const Server = require('./server') 8 | const version = require('../package.json').version 9 | 10 | function start (opts, callback) { 11 | /* istanbul ignore if */ 12 | if (opts.help) { 13 | console.log(`# Rinvoke v${version} # 14 | 15 | --port -p # default 3000 16 | --address -a # default 127.0.0.1 17 | --path -P # path of the ipc web socket 18 | --name -n # name of your exported function 19 | `) 20 | return 21 | } 22 | 23 | var fn = null 24 | try { 25 | fn = require(path.resolve(process.cwd(), opts._[0])) 26 | } catch (e) { 27 | /* istanbul ignore next */ 28 | console.log(`Cannot find the specified file: '${opts._[0]}'`) 29 | /* istanbul ignore next */ 30 | process.exit(1) 31 | } 32 | 33 | const server = Server(opts) 34 | if (opts.name) { 35 | server.register(opts.name, fn) 36 | } else { 37 | server.use(fn) 38 | } 39 | 40 | server.listen(opts.port, err => { 41 | /* istanbul ignore if */ 42 | if (err) throw err 43 | /* istanbul ignore else */ 44 | if (callback) { 45 | callback(server) 46 | } else { 47 | console.log(`Function listening at ${opts.port}`) 48 | } 49 | }) 50 | } 51 | 52 | /* istanbul ignore next */ 53 | function cli () { 54 | start(minimist(process.argv.slice(2), { 55 | integer: ['port'], 56 | alias: { 57 | port: 'p', 58 | help: 'h', 59 | address: 'a', 60 | path: 'P', 61 | name: 'n' 62 | }, 63 | default: { 64 | port: 3000, 65 | host: '127.0.0.1' 66 | } 67 | })) 68 | } 69 | 70 | /* istanbul ignore if */ 71 | if (require.main === module) { 72 | cli() 73 | } 74 | 75 | module.exports = { start } 76 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const debug = require('debug')('rinvoke-client') 4 | const assert = require('assert') 5 | const EE = require('events').EventEmitter 6 | const inherits = require('util').inherits 7 | const net = require('net') 8 | const pump = require('pump') 9 | const tentacoli = require('tentacoli') 10 | const avvio = require('avvio') 11 | const promisify = require('util.promisify') 12 | 13 | function Client (opts) { 14 | if (!(this instanceof Client)) { 15 | return new Client(opts) 16 | } 17 | 18 | avvio(this) 19 | 20 | assert(opts, 'Missing client options') 21 | this._opts = opts 22 | this._opts.host = this._opts.host || '127.0.0.1' 23 | this._opts.codec = this._opts.codec || { 24 | encode: JSON.stringify, 25 | decode: JSON.parse 26 | } 27 | 28 | this._reconnect = !!this._opts.reconnect 29 | if (typeof this._opts.reconnect !== 'object') { 30 | this._opts.reconnect = {} 31 | } 32 | this._reconnectAttempts = this._opts.reconnect.attempts || 3 33 | this._reconnectTimeout = this._opts.reconnect.timeout || 1000 34 | this._reconnectAttemptsCount = 0 35 | 36 | this._socket = null 37 | this._client = null 38 | 39 | this.ready(runClient) 40 | this.isReady = false 41 | this.isClosing = false 42 | 43 | this.onClose(instance => { 44 | debug('on close') 45 | instance.isClosing = true 46 | instance._socket.end() 47 | instance._client.destroy() 48 | }) 49 | } 50 | 51 | function runClient (err, context, done) { 52 | /* istanbul ignore if */ 53 | if (err) throw err 54 | if (context._opts.path) { 55 | debug('run the ipc client') 56 | context._socket = net.connect(context._opts.path) 57 | } else { 58 | debug('run the tcp client') 59 | context._socket = net.connect(context._opts.port, context._opts.host) 60 | } 61 | context._client = tentacoli(context._opts) 62 | pump(context._socket, context._client, context._socket) 63 | 64 | context._socket.on('connect', handleConnect) 65 | function handleConnect () { 66 | debug('connected to the server') 67 | context.isReady = true 68 | context._reconnectAttemptsCount = 0 69 | context.emit('connect') 70 | done() 71 | } 72 | 73 | context._socket.on('timeout', handleTimeout) 74 | /* istanbul ignore next */ 75 | function handleTimeout () { 76 | debug('socket timeout event') 77 | handleReconnect(() => { 78 | context.close() 79 | context.emit('timeout') 80 | }) 81 | } 82 | 83 | context._socket.on('error', handleError) 84 | /* istanbul ignore next */ 85 | function handleError (err) { 86 | debug('socket error event', err) 87 | handleReconnect(() => { 88 | context.close() 89 | context.emit('error', err) 90 | }) 91 | } 92 | 93 | context._socket.on('close', handleClose) 94 | /* istanbul ignore next */ 95 | function handleClose () { 96 | debug('socket close event') 97 | if (context.isClosing) return 98 | handleReconnect(() => { 99 | context.close() 100 | context.emit('close') 101 | }) 102 | } 103 | 104 | context._client.on('error', handleTError) 105 | /* istanbul ignore next */ 106 | function handleTError (err) { 107 | debug('tentacoli error event', err) 108 | handleReconnect(() => { 109 | context.close() 110 | context.emit('error', err) 111 | }) 112 | } 113 | 114 | function handleReconnect (cb) { 115 | if (!context._reconnect) { 116 | cb() 117 | } else if (context._reconnectAttemptsCount++ >= context._reconnectAttempts) { 118 | cb() 119 | } else { 120 | debug('remove listeners before try reconnect') 121 | context._socket.removeListener('connect', handleConnect) 122 | context._socket.removeListener('timeout', handleTimeout) 123 | context._socket.removeListener('error', handleError) 124 | context._socket.removeListener('close', handleClose) 125 | context._client.removeListener('error', handleTError) 126 | 127 | context._socket.destroy() 128 | context._client.destroy() 129 | context._socket = null 130 | context._client = null 131 | context.isReady = false 132 | 133 | setTimeout(() => { 134 | debug('try reconnect') 135 | runClient(null, context, () => {}) 136 | }, context._reconnectTimeout) 137 | } 138 | } 139 | } 140 | 141 | inherits(Client, EE) 142 | 143 | Client.prototype.invoke = promisify(function invoke (opts, cb) { 144 | assert(typeof opts.procedure === 'string', 'Procedure must be a string') 145 | 146 | if (!this.isReady) { 147 | debug('client not ready yet, wait for connect') 148 | this.once('connect', () => { 149 | debug('invoke', opts) 150 | this._client.request(opts, cb) 151 | }) 152 | return 153 | } 154 | 155 | debug('invoke', opts) 156 | this._client.request(opts, cb) 157 | }) 158 | 159 | Client.prototype.fire = promisify(function fire (opts, cb) { 160 | assert(typeof opts.procedure === 'string', 'Procedure must be a string') 161 | cb = cb || noop 162 | 163 | if (!this.isReady) { 164 | debug('client not ready yet, wait for connect') 165 | this.once('connect', () => { 166 | debug('fire', opts) 167 | this._client.fire(opts, cb) 168 | }) 169 | return 170 | } 171 | 172 | debug('fire', opts) 173 | this._client.fire(opts, cb) 174 | }) 175 | 176 | Client.prototype.timeout = function timeout (t) { 177 | assert(typeof t === 'number', 'timeout must be a number') 178 | debug('set timeout', t) 179 | 180 | if (!this.isReady) { 181 | this.once('connect', () => { 182 | this._socket.setTimeout(t) 183 | }) 184 | return 185 | } 186 | 187 | this._socket.setTimeout(t) 188 | } 189 | 190 | Client.prototype.keepAlive = function timeout (bool) { 191 | assert(typeof bool === 'boolean', 'keepAlive must be a boolean') 192 | debug('set keep alive', bool) 193 | 194 | if (!this.isReady) { 195 | this.once('connect', () => { 196 | this._socket.setKeepAlive(bool) 197 | }) 198 | return 199 | } 200 | 201 | this._socket.setKeepAlive(bool) 202 | } 203 | 204 | function noop () {} 205 | 206 | module.exports = Client 207 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const debug = require('debug')('rinvoke-server') 4 | const assert = require('assert') 5 | const net = require('net') 6 | const pump = require('pump') 7 | const tentacoli = require('tentacoli') 8 | const avvio = require('avvio') 9 | const EE = require('events').EventEmitter 10 | const inherits = require('util').inherits 11 | const Ajv = require('ajv') 12 | const ajv = new Ajv({ coerceTypes: true }) 13 | 14 | /* istanbul ignore next */ 15 | const noop = err => { if (err) throw err } 16 | 17 | function Server (opts) { 18 | if (!(this instanceof Server)) { 19 | return new Server(opts) 20 | } 21 | 22 | avvio(this) 23 | 24 | this._functions = Object.create(null) 25 | this._validations = Object.create(null) 26 | this._server = net.createServer(create.bind(this)) 27 | 28 | /* istanbul ignore next */ 29 | this._server.on('error', err => { 30 | debug('error event', err) 31 | this.close() 32 | this.emit('error', err) 33 | }) 34 | 35 | this._server.on('connection', () => { 36 | debug('new connection') 37 | this.emit('connection') 38 | }) 39 | 40 | this._opts = opts || {} 41 | this._opts.codec = this._opts.codec || { 42 | encode: JSON.stringify, 43 | decode: JSON.parse 44 | } 45 | 46 | this.onClose((instance, done) => { 47 | debug('on close') 48 | instance._server.close(done) 49 | }) 50 | } 51 | 52 | inherits(Server, EE) 53 | 54 | function create (original) { 55 | const stream = tentacoli(this._opts) 56 | pump(stream, original, stream) 57 | stream.on('request', handle.bind(this)) 58 | 59 | /* istanbul ignore next */ 60 | stream.on('error', err => { 61 | debug('tentacoli error', err) 62 | this.close() 63 | this.emit('error', err) 64 | }) 65 | } 66 | 67 | function handle (request, reply) { 68 | debug('incoming request', request) 69 | this.emit('request', request) 70 | var fn = this._functions[request.procedure] 71 | var validate = this._validations[request.procedure] 72 | 73 | if (fn) { 74 | debug('found procedure') 75 | 76 | if (validate) { 77 | debug('validate request') 78 | var valid = validate(request) 79 | if (valid !== true) { 80 | debug('request not valid') 81 | reply(new Error('400'), valid) 82 | return 83 | } 84 | debug('request valid') 85 | } 86 | 87 | var result = fn.call(this, request, reply) 88 | } else { 89 | debug('procedure not found') 90 | reply(new Error('404'), null) 91 | return 92 | } 93 | 94 | if (result && result.then) { 95 | result 96 | .then(res => { 97 | reply.call(this, null, res) 98 | }) 99 | .catch(err => { 100 | reply.call(this, err, null) 101 | }) 102 | } 103 | } 104 | 105 | Server.prototype.register = function (procedure, schema, fn) { 106 | if (!fn) { 107 | fn = schema 108 | schema = null 109 | } 110 | assert(typeof procedure === 'string', 'procedure must be a string') 111 | assert(typeof schema === 'object', 'schema must be an object') 112 | assert(typeof fn === 'function', 'fn must be a function') 113 | assert(!this._functions[procedure], `You have already registered procedure '${procedure}'`) 114 | debug('register new procedure:', procedure) 115 | 116 | if (schema) { 117 | schema.properties.procedure = { type: 'string' } 118 | schema.required = schema.required || [] 119 | schema.required.push('procedure') 120 | this._validations[procedure] = ajv.compile(schema) 121 | } 122 | 123 | this._functions[procedure] = fn 124 | return this 125 | } 126 | 127 | Server.prototype.listen = function (portOrPath, address, cb) { 128 | assert(typeof portOrPath === 'number' || typeof portOrPath === 'string', 'portOrPath should be a number or a string') 129 | if (typeof address === 'function') { 130 | cb = address 131 | address = null 132 | } 133 | cb = cb || noop 134 | 135 | this.ready(err => { 136 | debug(`Run the ${typeof portOrPath === 'number' ? 'tcp' : 'ipc'} server`) 137 | /* istanbul ignore if */ 138 | if (err) return cb(err) 139 | if (address) { 140 | this._server.listen(portOrPath, address, err => { 141 | cb(err) 142 | this.emit('listening') 143 | }) 144 | } else { 145 | this._server.listen(portOrPath, err => { 146 | cb(err) 147 | this.emit('listening') 148 | }) 149 | } 150 | }) 151 | } 152 | 153 | module.exports = Server 154 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rinvoke", 3 | "version": "1.1.0", 4 | "description": "RPC library based on net sockets, can work both with tcp sockets and ipc", 5 | "main": "./server.js", 6 | "bin": { 7 | "rinvoke": "./lib/bin.js" 8 | }, 9 | "scripts": { 10 | "test": "standard && tap test/*.test.js", 11 | "coveralls": "tap test/*.test.js --cov --coverage-report=text-lcov | coveralls" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/delvedor/rinvoke.git" 16 | }, 17 | "keywords": [ 18 | "rpc", 19 | "tcp", 20 | "ipc", 21 | "function", 22 | "remote", 23 | "multiplex", 24 | "fire", 25 | "forget", 26 | "fire and forget" 27 | ], 28 | "author": "Tomas Della Vedova - @delvedor (http://delved.org)", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/delvedor/rinvoke/issues" 32 | }, 33 | "homepage": "https://github.com/delvedor/rinvoke#readme", 34 | "dependencies": { 35 | "ajv": "^5.2.2", 36 | "avvio": "^2.0.3", 37 | "debug": "^3.0.0", 38 | "minimist": "^1.2.0", 39 | "pump": "^1.0.2", 40 | "tentacoli": "^1.0.0", 41 | "util.promisify": "^1.0.0" 42 | }, 43 | "devDependencies": { 44 | "coveralls": "^2.13.1", 45 | "msgpack5": "^3.5.0", 46 | "standard": "^10.0.3", 47 | "tap": "^10.7.2", 48 | "then-sleep": "^1.0.1" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/server') 2 | -------------------------------------------------------------------------------- /test/async-await.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Server = require('../lib/server') 4 | const Client = require('../lib/client') 5 | const sleep = require('then-sleep') 6 | 7 | function asyncTest (test, port) { 8 | test('should support async await', t => { 9 | t.plan(7) 10 | const server = Server() 11 | 12 | server.register('concat', async req => { 13 | t.equal(req.a, 'a') 14 | t.equal(req.b, 'b') 15 | await sleep(200) 16 | return req.a + req.b 17 | }) 18 | 19 | server.listen(port, err => { 20 | t.error(err) 21 | 22 | const client = Client({ port }) 23 | 24 | client.invoke({ 25 | procedure: 'concat', 26 | a: 'a', 27 | b: 'b' 28 | }, (err, res) => { 29 | t.error(err) 30 | t.equal(res, 'ab') 31 | client.close(t.error) 32 | server.close(t.error) 33 | }) 34 | }) 35 | }) 36 | } 37 | 38 | module.exports = asyncTest 39 | -------------------------------------------------------------------------------- /test/basic.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('tap') 4 | const beforeEach = t.beforeEach 5 | const test = t.test 6 | const Server = require('../lib/server') 7 | const Client = require('../lib/client') 8 | const getPort = require('./get-port') 9 | 10 | var port = 0 11 | 12 | beforeEach(done => { 13 | getPort((err, p) => { 14 | if (err) throw err 15 | port = p 16 | done() 17 | }) 18 | }) 19 | 20 | test('should register a function and reply to the request', t => { 21 | t.plan(8) 22 | const server = Server() 23 | 24 | server.register('concat', (req, reply) => { 25 | t.equal(req.a, 'a') 26 | t.equal(req.b, 'b') 27 | t.is(typeof reply, 'function') 28 | reply(null, req.a + req.b) 29 | }) 30 | 31 | server.listen(port, err => { 32 | t.error(err) 33 | 34 | const client = Client({ port }) 35 | 36 | client.invoke({ 37 | procedure: 'concat', 38 | a: 'a', 39 | b: 'b' 40 | }, (err, res) => { 41 | t.error(err) 42 | t.equal(res, 'ab') 43 | client.close(t.error) 44 | server.close(t.error) 45 | }) 46 | }) 47 | }) 48 | 49 | test('should register a function and reply to the request (multiple)', t => { 50 | t.plan(13) 51 | const server = Server() 52 | 53 | server.register('concat', (req, reply) => { 54 | t.equal(req.a, 'a') 55 | t.equal(req.b, 'b') 56 | t.is(typeof reply, 'function') 57 | reply(null, req.a + req.b) 58 | }) 59 | 60 | server.listen(port, err => { 61 | t.error(err) 62 | 63 | const client = Client({ port }) 64 | 65 | client.invoke({ 66 | procedure: 'concat', 67 | a: 'a', 68 | b: 'b' 69 | }, (err, res) => { 70 | t.error(err) 71 | t.equal(res, 'ab') 72 | 73 | client.invoke({ 74 | procedure: 'concat', 75 | a: 'a', 76 | b: 'b' 77 | }, (err, res) => { 78 | t.error(err) 79 | t.equal(res, 'ab') 80 | client.close(t.error) 81 | server.close(t.error) 82 | }) 83 | }) 84 | }) 85 | }) 86 | 87 | test('should register a function and reply to the request - error', t => { 88 | t.plan(5) 89 | const server = Server() 90 | 91 | server.register('concat', (req, reply) => { 92 | reply(new Error('some error'), null) 93 | }) 94 | 95 | server.listen(port, err => { 96 | t.error(err) 97 | 98 | const client = Client({ port }) 99 | 100 | client.invoke({ 101 | procedure: 'concat', 102 | a: 'a', 103 | b: 'b' 104 | }, (err, res) => { 105 | t.equal(err.message, 'some error') 106 | t.equal(res, null) 107 | client.close(t.error) 108 | server.close(t.error) 109 | }) 110 | }) 111 | }) 112 | 113 | test('function not found', t => { 114 | t.plan(5) 115 | const server = Server() 116 | 117 | server.listen(port, err => { 118 | t.error(err) 119 | 120 | const client = Client({ port }) 121 | 122 | client.invoke({ 123 | procedure: 'concat', 124 | a: 'a', 125 | b: 'b' 126 | }, (err, res) => { 127 | t.equal(err.message, '404') 128 | t.equal(res, null) 129 | client.close(t.error) 130 | server.close(t.error) 131 | }) 132 | }) 133 | }) 134 | 135 | test('should register a function and reply to the request (promises - resolve)', t => { 136 | t.plan(5) 137 | const server = Server() 138 | 139 | server.register('concat', (req, reply) => { 140 | const p = new Promise((resolve, reject) => { 141 | resolve(req.a + req.b) 142 | }) 143 | return p 144 | }) 145 | 146 | server.listen(port, err => { 147 | t.error(err) 148 | 149 | const client = Client({ port }) 150 | 151 | client.invoke({ 152 | procedure: 'concat', 153 | a: 'a', 154 | b: 'b' 155 | }, (err, res) => { 156 | t.error(err) 157 | t.equal(res, 'ab') 158 | client.close(t.error) 159 | server.close(t.error) 160 | }) 161 | }) 162 | }) 163 | 164 | test('should register a function and reply to the request (promises - reject)', t => { 165 | t.plan(5) 166 | const server = Server() 167 | 168 | server.register('concat', (req, reply) => { 169 | const p = new Promise((resolve, reject) => { 170 | reject(new Error('some error')) 171 | }) 172 | return p 173 | }) 174 | 175 | server.listen(port, err => { 176 | t.error(err) 177 | 178 | const client = Client({ port }) 179 | 180 | client.invoke({ 181 | procedure: 'concat', 182 | a: 'a', 183 | b: 'b' 184 | }, (err, res) => { 185 | t.equal(err.message, 'some error') 186 | t.equal(res, null) 187 | client.close(t.error) 188 | server.close(t.error) 189 | }) 190 | }) 191 | }) 192 | 193 | test('client should set timeout and keep alive', t => { 194 | t.plan(8) 195 | const server = Server() 196 | 197 | server.register('concat', (req, reply) => { 198 | t.equal(req.a, 'a') 199 | t.equal(req.b, 'b') 200 | t.is(typeof reply, 'function') 201 | reply(null, req.a + req.b) 202 | }) 203 | 204 | server.listen(port, err => { 205 | t.error(err) 206 | 207 | const client = Client({ port }) 208 | 209 | client.timeout(500) 210 | client.keepAlive(true) 211 | 212 | client.invoke({ 213 | procedure: 'concat', 214 | a: 'a', 215 | b: 'b' 216 | }, (err, res) => { 217 | t.error(err) 218 | t.equal(res, 'ab') 219 | client.close(t.error) 220 | server.close(t.error) 221 | }) 222 | }) 223 | }) 224 | 225 | test('client should set timeout and keep alive / 2', t => { 226 | t.plan(8) 227 | const server = Server() 228 | 229 | server.register('concat', (req, reply) => { 230 | t.equal(req.a, 'a') 231 | t.equal(req.b, 'b') 232 | t.is(typeof reply, 'function') 233 | reply(null, req.a + req.b) 234 | }) 235 | 236 | server.listen(port, err => { 237 | t.error(err) 238 | 239 | const client = Client({ port }) 240 | 241 | client.invoke({ 242 | procedure: 'concat', 243 | a: 'a', 244 | b: 'b' 245 | }, (err, res) => { 246 | t.error(err) 247 | t.equal(res, 'ab') 248 | 249 | client.timeout(500) 250 | client.keepAlive(true) 251 | 252 | client.close(t.error) 253 | server.close(t.error) 254 | }) 255 | }) 256 | }) 257 | 258 | test('async await support', t => { 259 | if (Number(process.versions.node[0]) >= 8) { 260 | require('./async-await')(t.test, port) 261 | } else { 262 | t.pass('Skip because Node version < 8') 263 | } 264 | t.end() 265 | }) 266 | 267 | test('listen should work even without a callback', t => { 268 | t.plan(7) 269 | const server = Server() 270 | 271 | server.register('concat', (req, reply) => { 272 | t.equal(req.a, 'a') 273 | t.equal(req.b, 'b') 274 | t.is(typeof reply, 'function') 275 | reply(null, req.a + req.b) 276 | }) 277 | 278 | server.listen(port) 279 | 280 | server.on('listening', () => { 281 | const client = Client({ port }) 282 | 283 | client.invoke({ 284 | procedure: 'concat', 285 | a: 'a', 286 | b: 'b' 287 | }, (err, res) => { 288 | t.error(err) 289 | t.equal(res, 'ab') 290 | client.close(t.error) 291 | server.close(t.error) 292 | }) 293 | }) 294 | }) 295 | 296 | test('should accept a port and an address', t => { 297 | t.plan(7) 298 | const server = Server() 299 | 300 | server.register('concat', (req, reply) => { 301 | t.equal(req.a, 'a') 302 | t.equal(req.b, 'b') 303 | t.is(typeof reply, 'function') 304 | reply(null, req.a + req.b) 305 | }) 306 | 307 | server.listen(port, '127.0.0.1') 308 | 309 | server.on('listening', () => { 310 | const client = Client({ port }) 311 | 312 | client.invoke({ 313 | procedure: 'concat', 314 | a: 'a', 315 | b: 'b' 316 | }, (err, res) => { 317 | t.error(err) 318 | t.equal(res, 'ab') 319 | client.close(t.error) 320 | server.close(t.error) 321 | }) 322 | }) 323 | }) 324 | 325 | test('fire and forget', t => { 326 | t.plan(6) 327 | const server = Server() 328 | var client = null 329 | 330 | server.register('concat', (req, reply) => { 331 | t.equal(req.a, 'a') 332 | t.equal(req.b, 'b') 333 | t.is(typeof reply, 'function') 334 | client.close(t.error) 335 | server.close(t.error) 336 | }) 337 | 338 | server.listen(port, err => { 339 | t.error(err) 340 | 341 | client = Client({ port }) 342 | 343 | client.fire({ 344 | procedure: 'concat', 345 | a: 'a', 346 | b: 'b' 347 | }) 348 | }) 349 | }) 350 | 351 | test('fire and forget (multiple)', t => { 352 | t.plan(9) 353 | const server = Server() 354 | var client = null 355 | var check = false 356 | 357 | server.register('concat', (req, reply) => { 358 | t.equal(req.a, 'a') 359 | t.equal(req.b, 'b') 360 | t.is(typeof reply, 'function') 361 | if (check) { 362 | client.close(t.error) 363 | server.close(t.error) 364 | } else { 365 | check = true 366 | } 367 | }) 368 | 369 | server.listen(port, err => { 370 | t.error(err) 371 | 372 | client = Client({ port }) 373 | 374 | client.fire({ 375 | procedure: 'concat', 376 | a: 'a', 377 | b: 'b' 378 | }) 379 | 380 | setTimeout(() => { 381 | client.fire({ 382 | procedure: 'concat', 383 | a: 'a', 384 | b: 'b' 385 | }) 386 | }, 500) 387 | }) 388 | }) 389 | 390 | test('fire and forget with cb', t => { 391 | t.plan(7) 392 | const server = Server() 393 | var client = null 394 | 395 | server.register('concat', (req, reply) => { 396 | t.equal(req.a, 'a') 397 | t.equal(req.b, 'b') 398 | t.is(typeof reply, 'function') 399 | client.close(t.error) 400 | server.close(t.error) 401 | }) 402 | 403 | server.listen(port, err => { 404 | t.error(err) 405 | 406 | client = Client({ port }) 407 | 408 | client.fire({ 409 | procedure: 'concat', 410 | a: 'a', 411 | b: 'b' 412 | }, t.error) 413 | }) 414 | }) 415 | 416 | test('fire and forget with cb, should not care abotu reply', t => { 417 | t.plan(7) 418 | const server = Server() 419 | var client = null 420 | 421 | server.register('concat', (req, reply) => { 422 | t.equal(req.a, 'a') 423 | t.equal(req.b, 'b') 424 | t.is(typeof reply, 'function') 425 | reply(new Error('kaboom')) 426 | client.close(t.error) 427 | server.close(t.error) 428 | }) 429 | 430 | server.listen(port, err => { 431 | t.error(err) 432 | 433 | client = Client({ port }) 434 | 435 | client.fire({ 436 | procedure: 'concat', 437 | a: 'a', 438 | b: 'b' 439 | }, t.error) 440 | }) 441 | }) 442 | -------------------------------------------------------------------------------- /test/cli.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('tap') 4 | const beforeEach = t.beforeEach 5 | const test = t.test 6 | const Client = require('../lib/client') 7 | const cli = require('../lib/bin') 8 | const getPort = require('./get-port') 9 | 10 | var port = 0 11 | 12 | beforeEach(done => { 13 | getPort((err, p) => { 14 | if (err) throw err 15 | port = p 16 | done() 17 | }) 18 | }) 19 | 20 | test('cli single function', t => { 21 | t.plan(4) 22 | 23 | cli.start({ 24 | host: '127.0.0.1', 25 | port: port, 26 | name: 'hello', 27 | _: ['./examples/cli-single-function.js'] 28 | }, server => { 29 | const client = Client({ port }) 30 | 31 | client.invoke({ procedure: 'hello' }, (err, res) => { 32 | t.error(err) 33 | t.equal(res, 'hello!') 34 | client.close(t.error) 35 | server.close(t.error) 36 | }) 37 | }) 38 | }) 39 | 40 | test('cli extended', t => { 41 | t.plan(4) 42 | 43 | cli.start({ 44 | host: '127.0.0.1', 45 | port: port, 46 | _: ['./examples/cli-extended.js'] 47 | }, server => { 48 | const client = Client({ port }) 49 | 50 | client.invoke({ procedure: 'hello' }, (err, res) => { 51 | t.error(err) 52 | t.deepEqual(res, { hello: 'world' }) 53 | client.close(t.error) 54 | server.close(t.error) 55 | }) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /test/custom-parser-serializer.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('tap') 4 | const beforeEach = t.beforeEach 5 | const test = t.test 6 | const Server = require('../lib/server') 7 | const Client = require('../lib/client') 8 | const msgpack = require('msgpack5')() 9 | const getPort = require('./get-port') 10 | 11 | var port = 0 12 | 13 | beforeEach(done => { 14 | getPort((err, p) => { 15 | if (err) throw err 16 | port = p 17 | done() 18 | }) 19 | }) 20 | 21 | test('use msgpack5 as custom serializer/parser', t => { 22 | t.plan(6) 23 | const payload = { procedure: 'hello', hello: 'world' } 24 | const server = Server({ 25 | codec: msgpack 26 | }) 27 | 28 | server.register('hello', function (req, reply) { 29 | t.deepEqual(req, payload) 30 | reply(null, req) 31 | }) 32 | 33 | server.listen(port, err => { 34 | t.error(err) 35 | 36 | const client = Client({ 37 | port: port, 38 | codec: msgpack 39 | }) 40 | 41 | client.invoke(payload, (err, res) => { 42 | t.error(err) 43 | t.deepEqual(res, payload) 44 | client.close(t.error) 45 | server.close(t.error) 46 | }) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /test/errors.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('tap') 4 | const test = t.test 5 | const Server = require('../lib/server') 6 | 7 | test('server assertions', t => { 8 | t.plan(3) 9 | const server = Server() 10 | 11 | try { 12 | server.register(null, () => {}) 13 | t.fail() 14 | } catch (e) { 15 | t.is(e.message, 'procedure must be a string') 16 | } 17 | 18 | try { 19 | server.register('', null) 20 | t.fail() 21 | } catch (e) { 22 | t.is(e.message, 'fn must be a function') 23 | } 24 | 25 | try { 26 | server.listen(null) 27 | t.fail() 28 | } catch (e) { 29 | t.is(e.message, 'portOrPath should be a number or a string') 30 | } 31 | }) 32 | -------------------------------------------------------------------------------- /test/get-port.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const net = require('net') 4 | 5 | function getPort (cb) { 6 | const server = net.createServer() 7 | 8 | server.unref() 9 | server.on('error', err => { 10 | cb(err, null) 11 | }) 12 | 13 | server.listen(0, () => { 14 | const port = server.address().port 15 | server.close(() => { 16 | cb(null, port) 17 | }) 18 | }) 19 | } 20 | 21 | module.exports = getPort 22 | -------------------------------------------------------------------------------- /test/ipc.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const t = require('tap') 5 | const beforeEach = t.beforeEach 6 | const test = t.test 7 | const Server = require('../lib/server') 8 | const Client = require('../lib/client') 9 | 10 | const socket = '/tmp/ipc-test.sock' 11 | 12 | beforeEach(done => { 13 | fs.unlink(socket, () => done()) 14 | }) 15 | 16 | test('Should communicate via ipc', t => { 17 | t.plan(8) 18 | const server = Server() 19 | 20 | server.register('concat', (req, reply) => { 21 | t.equal(req.a, 'a') 22 | t.equal(req.b, 'b') 23 | t.is(typeof reply, 'function') 24 | reply(null, req.a + req.b) 25 | }) 26 | 27 | server.listen(socket, err => { 28 | t.error(err) 29 | 30 | const client = Client({ path: socket }) 31 | 32 | client.invoke({ 33 | procedure: 'concat', 34 | a: 'a', 35 | b: 'b' 36 | }, (err, res) => { 37 | t.error(err) 38 | t.equal(res, 'ab') 39 | client.close(t.error) 40 | server.close(t.error) 41 | }) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /test/reconnect.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('tap') 4 | const beforeEach = t.beforeEach 5 | const test = t.test 6 | const Server = require('../lib/server') 7 | const Client = require('../lib/client') 8 | const getPort = require('./get-port') 9 | 10 | var port = 0 11 | 12 | beforeEach(done => { 13 | getPort((err, p) => { 14 | if (err) throw err 15 | port = p 16 | done() 17 | }) 18 | }) 19 | 20 | test('the client should try to reconnect before emit an error', t => { 21 | t.plan(7) 22 | 23 | setTimeout(() => { 24 | const server = Server() 25 | server.register('concat', (req, reply) => { 26 | t.equal(req.a, 'a') 27 | t.equal(req.b, 'b') 28 | t.is(typeof reply, 'function') 29 | reply(null, req.a + req.b) 30 | }) 31 | 32 | server.listen(port, t.error) 33 | server.on('connection', server.close) 34 | }, 1000) 35 | 36 | const client = Client({ 37 | port: port, 38 | reconnect: { 39 | timeout: 1500 40 | } 41 | }) 42 | 43 | client.on('error', () => { 44 | t.fail('should not error') 45 | }) 46 | 47 | client.invoke({ 48 | procedure: 'concat', 49 | a: 'a', 50 | b: 'b' 51 | }, (err, res) => { 52 | t.error(err) 53 | t.equal(res, 'ab') 54 | client.close(t.error) 55 | }) 56 | }) 57 | 58 | test('client reconnect fail', t => { 59 | t.plan(1) 60 | 61 | const client = Client({ 62 | port: port, 63 | reconnect: { 64 | timeout: 100 65 | } 66 | }) 67 | 68 | client.on('error', () => { 69 | t.pass('should error') 70 | }) 71 | }) 72 | 73 | test('client without reconnect', t => { 74 | t.plan(1) 75 | 76 | const client = Client({ 77 | port: port 78 | }) 79 | 80 | client.on('error', () => { 81 | t.pass('should error') 82 | }) 83 | }) 84 | -------------------------------------------------------------------------------- /test/use.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('tap') 4 | const beforeEach = t.beforeEach 5 | const test = t.test 6 | const Server = require('../lib/server') 7 | const Client = require('../lib/client') 8 | const getPort = require('./get-port') 9 | 10 | var port = 0 11 | 12 | beforeEach(done => { 13 | getPort((err, p) => { 14 | if (err) throw err 15 | port = p 16 | done() 17 | }) 18 | }) 19 | 20 | test('use can add methods to the instance', t => { 21 | t.plan(7) 22 | const server = Server() 23 | 24 | server.use((instance, opts, next) => { 25 | t.ok(instance instanceof Server) 26 | instance.concat = (a, b) => a + b 27 | next() 28 | }) 29 | 30 | server.register('concat', function (req, reply) { 31 | t.ok(this.concat) 32 | reply(null, this.concat(req.a, req.b)) 33 | }) 34 | 35 | server.listen(port, err => { 36 | t.error(err) 37 | 38 | const client = Client({ port }) 39 | 40 | client.invoke({ 41 | procedure: 'concat', 42 | a: 'a', 43 | b: 'b' 44 | }, (err, res) => { 45 | t.error(err) 46 | t.equal(res, 'ab') 47 | client.close(t.error) 48 | server.close(t.error) 49 | }) 50 | }) 51 | }) 52 | 53 | test('close event', t => { 54 | t.plan(1) 55 | const server = Server() 56 | 57 | server 58 | .use((instance, opts, next) => { 59 | instance.onClose(i => { 60 | t.ok(i instanceof Server) 61 | }) 62 | next() 63 | }) 64 | .after(() => { 65 | server.close() 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /test/validation.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const t = require('tap') 4 | const beforeEach = t.beforeEach 5 | const test = t.test 6 | const Server = require('../lib/server') 7 | const Client = require('../lib/client') 8 | const getPort = require('./get-port') 9 | 10 | var port = 0 11 | 12 | beforeEach(done => { 13 | getPort((err, p) => { 14 | if (err) throw err 15 | port = p 16 | done() 17 | }) 18 | }) 19 | 20 | test('should validate the request object - valid', t => { 21 | t.plan(8) 22 | const server = Server() 23 | 24 | server.register('concat', { 25 | type: 'object', 26 | properties: { 27 | a: { type: 'string' }, 28 | b: { type: 'string' } 29 | }, 30 | required: ['a', 'b'] 31 | }, (req, reply) => { 32 | t.equal(req.a, 'a') 33 | t.equal(req.b, 'b') 34 | t.is(typeof reply, 'function') 35 | reply(null, req.a + req.b) 36 | }) 37 | 38 | server.listen(port, err => { 39 | t.error(err) 40 | 41 | const client = Client({ port }) 42 | 43 | client.invoke({ 44 | procedure: 'concat', 45 | a: 'a', 46 | b: 'b' 47 | }, (err, res) => { 48 | t.error(err) 49 | t.equal(res, 'ab') 50 | client.close(t.error) 51 | server.close(t.error) 52 | }) 53 | }) 54 | }) 55 | 56 | test('should validate the request object (without required) - valid', t => { 57 | t.plan(8) 58 | const server = Server() 59 | 60 | server.register('concat', { 61 | type: 'object', 62 | properties: { 63 | a: { type: 'string' }, 64 | b: { type: 'string' } 65 | } 66 | }, (req, reply) => { 67 | t.equal(req.a, 'a') 68 | t.equal(req.b, 'b') 69 | t.is(typeof reply, 'function') 70 | reply(null, req.a + req.b) 71 | }) 72 | 73 | server.listen(port, err => { 74 | t.error(err) 75 | 76 | const client = Client({ port }) 77 | 78 | client.invoke({ 79 | procedure: 'concat', 80 | a: 'a', 81 | b: 'b' 82 | }, (err, res) => { 83 | t.error(err) 84 | t.equal(res, 'ab') 85 | client.close(t.error) 86 | server.close(t.error) 87 | }) 88 | }) 89 | }) 90 | 91 | test('should validate the request object - not valid', t => { 92 | t.plan(5) 93 | const server = Server() 94 | 95 | server.register('concat', { 96 | type: 'object', 97 | properties: { 98 | a: { type: 'string' }, 99 | b: { type: 'string' } 100 | }, 101 | required: ['a', 'b'] 102 | }, (req, reply) => { 103 | t.fail('this should not be called') 104 | }) 105 | 106 | server.listen(port, err => { 107 | t.error(err) 108 | 109 | const client = Client({ port }) 110 | 111 | client.invoke({ 112 | procedure: 'concat', 113 | a: 'a' 114 | }, (err, res) => { 115 | t.equal(err.message, '400') 116 | t.is(typeof res, 'object') 117 | client.close(t.error) 118 | server.close(t.error) 119 | }) 120 | }) 121 | }) 122 | --------------------------------------------------------------------------------