├── .gitignore ├── .npmignore ├── .travis.yml ├── History.md ├── Makefile ├── Readme.md ├── example.js ├── index.js ├── lib ├── client.js └── server.js ├── package.json └── test ├── index.js └── unix.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | support 2 | test 3 | examples 4 | *.sock 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "15" 4 | - "14" 5 | - "12" 6 | - "10" 7 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | ### 0.0.x 3 | 4 | --- 5 | 6 | #### 0.0.3 — 2014-05-07 7 | 8 | * Update to make axon-rpc compatible with axon 2.x. 9 | * Added the original error stack to the error received by the client. 10 | * Fixed tests on Windows. 11 | * Fixed crash on empty error message. 12 | 13 | #### 0.0.2 — 2013-03-25 14 | 15 | * bump axon version so we include the req/rep fixes 16 | 17 | #### 0.0.1 — 2010-01-03 18 | 19 | * Initial release 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | test: 3 | @./node_modules/.bin/mocha \ 4 | --reporter spec 5 | 6 | .PHONY: test 7 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | # axon-rpc 3 | 4 | [![Build Status](https://travis-ci.org/Unitech/pm2-axon-rpc.png)](https://travis-ci.org/unitech/pm2-axon-rpc) 5 | 6 | RPC client / server for [axon](https://github.com/visionmedia/axon). 7 | 8 | ## arpc(1) 9 | 10 | The `arpc(1)` executable allows you to expose entire 11 | node modules with a single command, or inspect 12 | methods exposed by a given node. 13 | 14 | ``` 15 | 16 | Usage: arpc [options] 17 | 18 | Options: 19 | 20 | -h, --help output usage information 21 | -V, --version output the version number 22 | -a, --addr bind to the given 23 | -m, --methods inspect methods exposed by 24 | 25 | ``` 26 | 27 | ## Server 28 | 29 | ```js 30 | var rpc = require('axon-rpc') 31 | , axon = require('axon') 32 | , rep = axon.socket('rep'); 33 | 34 | var server = new rpc.Server(rep); 35 | rep.bind(4000); 36 | ``` 37 | 38 | ### Server#expose(name, fn) 39 | 40 | Expose a single method `name` mapped to `fn` callback. 41 | 42 | ```js 43 | server.expose('add', function(a, b, fn){ 44 | fn(null, a + b); 45 | }); 46 | ``` 47 | 48 | ### Server#expose(object) 49 | 50 | Expose several methods: 51 | 52 | ```js 53 | server.expose({ 54 | add: function(){ ... }, 55 | sub: function(){ ... } 56 | }); 57 | ``` 58 | 59 | This may also be used to expose 60 | an entire node module with exports: 61 | 62 | ```js 63 | server.expose(require('./api')); 64 | ``` 65 | 66 | ## Client 67 | 68 | ```js 69 | var rpc = require('axon-rpc') 70 | , axon = require('axon') 71 | , req = axon.socket('req'); 72 | 73 | var client = new rpc.Client(req); 74 | req.connect(4000); 75 | ``` 76 | 77 | ### Client#call(name, ..., fn) 78 | 79 | Invoke method `name` with some arguments and invoke `fn(err, ...)`: 80 | 81 | ```js 82 | client.call('add', 1, 2, function(err, n){ 83 | console.log(n); 84 | // => 3 85 | }) 86 | ``` 87 | 88 | ### Client#methods(fn) 89 | 90 | Request available methods: 91 | 92 | ```js 93 | client.methods(function(err, methods){ 94 | console.log(methods); 95 | }) 96 | ``` 97 | 98 | Responds with objects such as: 99 | 100 | ```js 101 | { 102 | add: { 103 | name: 'add', 104 | params: ['a', 'b', 'fn'] 105 | } 106 | } 107 | ``` 108 | 109 | ## License 110 | 111 | (The MIT License) 112 | 113 | Copyright (c) 2014 TJ Holowaychuk <tj@learnboost.com> 114 | 115 | Permission is hereby granted, free of charge, to any person obtaining 116 | a copy of this software and associated documentation files (the 117 | 'Software'), to deal in the Software without restriction, including 118 | without limitation the rights to use, copy, modify, merge, publish, 119 | distribute, sublicense, and/or sell copies of the Software, and to 120 | permit persons to whom the Software is furnished to do so, subject to 121 | the following conditions: 122 | 123 | The above copyright notice and this permission notice shall be 124 | included in all copies or substantial portions of the Software. 125 | 126 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 127 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 128 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 129 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 130 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 131 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 132 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 133 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | 2 | exports.add = function(a, b, fn){ 3 | fn(null, a + b); 4 | }; 5 | 6 | exports.sub = function(a, b, fn){ 7 | fn(null, a - b); 8 | }; 9 | 10 | exports.uppercase = function(str, fn){ 11 | fn(null, str.toUpperCase()); 12 | }; 13 | 14 | exports.lowercase = function(str, fn){ 15 | fn(null, str.toLowerCase()); 16 | }; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | exports.Server = require('./lib/server'); 3 | exports.Client = require('./lib/client'); -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Expose `Client`. 4 | */ 5 | 6 | module.exports = Client; 7 | 8 | /** 9 | * Initialize an rpc client with `sock`. 10 | * 11 | * @param {Socket} sock 12 | * @api public 13 | */ 14 | 15 | function Client(sock) { 16 | if (typeof sock.format === 'function') sock.format('json'); 17 | this.sock = sock; 18 | } 19 | 20 | /** 21 | * Invoke method `name` with args and invoke the 22 | * tailing callback function. 23 | * 24 | * @param {String} name 25 | * @param {Mixed} ... 26 | * @param {Function} fn 27 | * @api public 28 | */ 29 | 30 | Client.prototype.call = function(name){ 31 | var args = [].slice.call(arguments, 1, -1); 32 | var fn = arguments[arguments.length - 1]; 33 | 34 | this.sock.send({ 35 | type: 'call', 36 | method: name, 37 | args: args 38 | }, function(msg){ 39 | if ('error' in msg) { 40 | var err = new Error(msg.error); 41 | err.stack = msg.stack || err.stack; 42 | fn(err); 43 | } else { 44 | msg.args.unshift(null); 45 | fn.apply(null, msg.args); 46 | } 47 | }); 48 | }; 49 | 50 | /** 51 | * Fetch the methods exposed and invoke `fn(err, methods)`. 52 | * 53 | * @param {Function} fn 54 | * @api public 55 | */ 56 | 57 | Client.prototype.methods = function(fn){ 58 | this.sock.send({ 59 | type: 'methods' 60 | }, function(msg){ 61 | fn(null, msg.methods); 62 | }); 63 | }; 64 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var debug = require('debug'); 7 | 8 | /** 9 | * Expose `Server`. 10 | */ 11 | 12 | module.exports = Server; 13 | 14 | /** 15 | * Initialize a server with the given `sock`. 16 | * 17 | * @param {Socket} sock 18 | * @api public 19 | */ 20 | 21 | function Server(sock) { 22 | if (typeof sock.format === 'function') sock.format('json'); 23 | this.sock = sock; 24 | this.methods = {}; 25 | this.sock.on('message', this.onmessage.bind(this)); 26 | } 27 | 28 | /** 29 | * Return method descriptions with: 30 | * 31 | * `.name` string 32 | * `.params` array 33 | * 34 | * @return {Object} 35 | * @api private 36 | */ 37 | 38 | Server.prototype.methodDescriptions = function(){ 39 | var obj = {}; 40 | var fn; 41 | 42 | for (var name in this.methods) { 43 | fn = this.methods[name]; 44 | obj[name] = { 45 | name: name, 46 | params: params(fn) 47 | }; 48 | } 49 | 50 | return obj; 51 | }; 52 | 53 | /** 54 | * Response with the method descriptions. 55 | * 56 | * @param {Function} fn 57 | * @api private 58 | */ 59 | 60 | Server.prototype.respondWithMethods = function(reply){ 61 | reply({ methods: this.methodDescriptions() }); 62 | }; 63 | 64 | /** 65 | * Handle `msg`. 66 | * 67 | * @param {Object} msg 68 | * @param {Object} fn 69 | * @api private 70 | */ 71 | 72 | Server.prototype.onmessage = function(msg, reply){ 73 | if ('methods' == msg.type) return this.respondWithMethods(reply); 74 | 75 | if (!reply) { 76 | console.error('reply false'); 77 | return false; 78 | } 79 | 80 | // .method 81 | var meth = msg.method; 82 | if (!meth) return reply({ error: '.method required' }); 83 | 84 | // ensure .method is exposed 85 | var fn = this.methods[meth]; 86 | if (!fn) return reply({ error: 'method "' + meth + '" does not exist' }); 87 | 88 | // .args 89 | var args = msg.args; 90 | if (!args) return reply({ error: '.args required' }); 91 | 92 | // invoke 93 | args.push(function(err){ 94 | if (err) { 95 | if (err instanceof Error) 96 | return reply({ error: err.message, stack: err.stack }); 97 | else 98 | return reply({error : err}); 99 | } 100 | var args = [].slice.call(arguments, 1); 101 | reply({ args: args }); 102 | }); 103 | 104 | fn.apply(null, args); 105 | }; 106 | 107 | /** 108 | * Expose many or a single method. 109 | * 110 | * @param {String|Object} name 111 | * @param {String|Object} fn 112 | * @api public 113 | */ 114 | 115 | Server.prototype.expose = function(name, fn){ 116 | if (1 == arguments.length) { 117 | for (var key in name) { 118 | this.expose(key, name[key]); 119 | } 120 | } else { 121 | debug('expose "%s"', name); 122 | this.methods[name] = fn; 123 | } 124 | }; 125 | 126 | /** 127 | * Parse params. 128 | * 129 | * @param {Function} fn 130 | * @return {Array} 131 | * @api private 132 | */ 133 | 134 | function params(fn) { 135 | // remove space to make it work on node 10.x.x too 136 | var ret = fn.toString().replace(/\s/g, '').match(/^function *(\w*)\((.*?)\)/)[2]; 137 | if (ret) return ret.split(/ *, */); 138 | return []; 139 | } 140 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pm2-axon-rpc", 3 | "version": "0.7.1", 4 | "description": "Remote procedure calls built on top of axon.", 5 | "keywords": [ 6 | "axon", 7 | "rpc", 8 | "cloud" 9 | ], 10 | "author": "TJ Holowaychuk ", 11 | "engines": { 12 | "node": ">=5" 13 | }, 14 | "contributors": [ 15 | { 16 | "name": "Bret Copeland", 17 | "email": "bret@atlantisflight.org", 18 | "url": "https://github.com/bretcope" 19 | } 20 | ], 21 | "dependencies": { 22 | "debug": "^4.3.1" 23 | }, 24 | "devDependencies": { 25 | "better-assert": "*", 26 | "mocha": "^8.1", 27 | "pm2-axon": "^4.0.0" 28 | }, 29 | "main": "index", 30 | "scripts": { 31 | "test": "mocha --reporter spec" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "https://github.com/unitech/pm2-axon-rpc.git" 36 | }, 37 | "license": "MIT" 38 | } 39 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var rpc = require('..') 7 | , axon = require('pm2-axon') 8 | , assert = require('better-assert'); 9 | 10 | 11 | 12 | describe('SOCKET', function() { 13 | var req, rep, server, client; 14 | 15 | before(function() { 16 | rep = axon.socket('rep'); 17 | req = axon.socket('req'); 18 | 19 | rep.bind(4000); 20 | req.connect(4000, 'localhost'); 21 | 22 | server = new rpc.Server(rep); 23 | client = new rpc.Client(req); 24 | }); 25 | 26 | after(function() { 27 | req.close(); 28 | rep.close(); 29 | }); 30 | 31 | describe('Server#expose(name, fn)', function(){ 32 | it('should expose a single function', function(done){ 33 | server.expose('add', function(a, b, fn){ 34 | fn(null, a + b); 35 | }); 36 | 37 | client.call('add', 1, 2, function(err, n){ 38 | assert(!err); 39 | assert(3 === n); 40 | done(); 41 | }); 42 | }); 43 | }); 44 | 45 | describe('Server#expose(obj)', function(){ 46 | it('should expose multiple', function(done){ 47 | server.expose({ 48 | uppercase: function(str, fn){ 49 | fn(null, str.toUpperCase()); 50 | } 51 | }); 52 | 53 | client.call('uppercase', 'hello', function(err, str){ 54 | assert(!err); 55 | assert('HELLO' == str); 56 | done(); 57 | }); 58 | }); 59 | }); 60 | 61 | describe('Client#methods(fn)', function(){ 62 | it('should respond with available methods', function(done){ 63 | client.methods(function(err, methods){ 64 | assert(!err); 65 | assert('add' == methods.add.name); 66 | assert('a' == methods.add.params[0]); 67 | assert('b' == methods.add.params[1]); 68 | assert('fn' == methods.add.params[2]); 69 | assert(methods.uppercase); 70 | done(); 71 | }); 72 | }); 73 | }); 74 | 75 | describe('Client#call(name, ..., fn)', function(){ 76 | describe('when method is not exposed', function(){ 77 | it('should error', function(done){ 78 | client.call('something', function(err){ 79 | assert('method "something" does not exist' == err.message); 80 | done(); 81 | }); 82 | }) 83 | }); 84 | 85 | describe('with an error response', function(){ 86 | it('should provide an Error', function(done){ 87 | var svrErr; 88 | server.expose('error', function(fn){ 89 | svrErr = new Error('boom'); 90 | fn(svrErr); 91 | }); 92 | 93 | client.call('error', function(err){ 94 | assert(err instanceof Error); 95 | assert('boom' == err.message); 96 | assert(err.stack === svrErr.stack, 'Original error stack should have been passed to the client'); 97 | done(); 98 | }); 99 | }); 100 | 101 | it('should works with something different than a error', function(done){ 102 | var svrErr; 103 | server.expose('errorNoStack', function(fn){ 104 | fn('shit'); 105 | }); 106 | 107 | client.call('errorNoStack', function(err){ 108 | assert('shit' == err.message); 109 | done(); 110 | }); 111 | }); 112 | 113 | it('empty string edge case should still work', function(done){ 114 | var svrErr; 115 | server.expose('error', function(fn){ 116 | svrErr = new Error(''); 117 | fn(svrErr); 118 | }); 119 | 120 | client.call('error', function(err){ 121 | assert(err instanceof Error); 122 | assert(svrErr.message == err.message); 123 | assert(err.stack === svrErr.stack, 'Original error stack should have been passed to the client'); 124 | done(); 125 | }); 126 | }); 127 | }); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /test/unix.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var rpc = require('..') 8 | , axon = require('pm2-axon') 9 | , assert = require('better-assert'); 10 | 11 | describe('UNIX SOCKET', function() { 12 | var rep; 13 | var req; 14 | var server; 15 | var client; 16 | 17 | before(function() { 18 | rep = axon.socket('rep'); 19 | req = axon.socket('req'); 20 | 21 | var unix_socket = 'hey.sock'; 22 | 23 | rep.bind(unix_socket); 24 | req.connect(unix_socket); 25 | 26 | server = new rpc.Server(rep); 27 | client = new rpc.Client(req); 28 | }); 29 | 30 | after(function() { 31 | req.close(); 32 | rep.close(); 33 | }); 34 | 35 | describe('Server#expose(name, fn)', function(){ 36 | it('should expose a single function', function(done){ 37 | server.expose('add', function(a, b, fn){ 38 | fn(null, a + b); 39 | }); 40 | 41 | client.call('add', 1, 2, function(err, n){ 42 | assert(!err); 43 | assert(3 === n); 44 | done(); 45 | }); 46 | }); 47 | }); 48 | 49 | describe('Server#expose(obj)', function(){ 50 | it('should expose multiple', function(done){ 51 | server.expose({ 52 | uppercase: function(str, fn){ 53 | fn(null, str.toUpperCase()); 54 | } 55 | }); 56 | 57 | client.call('uppercase', 'hello', function(err, str){ 58 | assert(!err); 59 | assert('HELLO' == str); 60 | done(); 61 | }); 62 | }); 63 | }); 64 | 65 | describe('Client#methods(fn)', function(){ 66 | it('should respond with available methods', function(done){ 67 | client.methods(function(err, methods){ 68 | assert(!err); 69 | assert('add' == methods.add.name); 70 | assert('a' == methods.add.params[0]); 71 | assert('b' == methods.add.params[1]); 72 | assert('fn' == methods.add.params[2]); 73 | assert(methods.uppercase); 74 | done(); 75 | }); 76 | }); 77 | }); 78 | 79 | describe('Client#call(name, ..., fn)', function(){ 80 | describe('when method is not exposed', function(){ 81 | it('should error', function(done){ 82 | client.call('something', function(err){ 83 | assert('method "something" does not exist' == err.message); 84 | done(); 85 | }); 86 | }) 87 | }); 88 | 89 | describe('with an error response', function(){ 90 | it('should provide an Error', function(done){ 91 | var svrErr; 92 | server.expose('error', function(fn){ 93 | svrErr = new Error('boom'); 94 | fn(svrErr); 95 | }); 96 | 97 | client.call('error', function(err){ 98 | assert(err instanceof Error); 99 | assert('boom' == err.message); 100 | assert(err.stack === svrErr.stack, 'Original error stack should have been passed to the client'); 101 | done(); 102 | }); 103 | }); 104 | 105 | it('empty string edge case should still work', function(done){ 106 | var svrErr; 107 | server.expose('error', function(fn){ 108 | svrErr = new Error(''); 109 | fn(svrErr); 110 | }); 111 | 112 | client.call('error', function(err){ 113 | assert(err instanceof Error); 114 | assert(svrErr.message == err.message); 115 | assert(err.stack === svrErr.stack, 'Original error stack should have been passed to the client'); 116 | done(); 117 | }); 118 | }); 119 | }); 120 | }); 121 | 122 | }); 123 | --------------------------------------------------------------------------------