├── .gitignore ├── lib ├── errorcode.js ├── index.js ├── transports │ ├── shared │ │ ├── loopback.js │ │ └── tcp.js │ ├── server │ │ ├── childProcess.js │ │ ├── middleware.js │ │ ├── http.js │ │ └── tcp.js │ └── client │ │ ├── http.js │ │ ├── childProcess.js │ │ └── tcp.js ├── client.js └── server.js ├── .travis.yml ├── test ├── child │ ├── child.js │ └── child-compressed.js ├── jshint.json ├── jshint.sh ├── client-childProccess.js ├── client-http.js ├── client-childProccess-compressed.js ├── server-http.js ├── server-tcp.js ├── client.js ├── server.js ├── client-tcp.js └── full-stack.js ├── prepublish.sh ├── bin └── jsonrpc-repl ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ -------------------------------------------------------------------------------- /lib/errorcode.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | paserError: -32700, 3 | invalidRequest: -32600, 4 | methodNotFound: -32601, 5 | invalidParams: -32602, 6 | internalError: -32603, 7 | serverErrorStart: -32000, 8 | serverErrorLast: -32099 9 | }; 10 | 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.8 4 | - 0.10 5 | - 0.12 6 | before_install: 7 | npm install -g npm@1.4.x 8 | 9 | after_script: 10 | npm run coveralls 11 | 12 | notifications: 13 | email: 14 | recipients: 15 | - dispatch@uber.com 16 | on_success: change 17 | on_failure: change 18 | -------------------------------------------------------------------------------- /test/child/child.js: -------------------------------------------------------------------------------- 1 | var jsonrpc = require('../../lib/index'); 2 | var JsonRpcServer = jsonrpc.server; 3 | var JsonRpcChildProcTransport = jsonrpc.transports.server.childProcess; 4 | 5 | var server = new JsonRpcServer(new JsonRpcChildProcTransport(), { 6 | loopback: function(obj, callback) { 7 | callback(null, obj); 8 | }, 9 | failure: function(obj, callback) { 10 | var error = new Error("Whatchoo talkin' 'bout, Willis?"); 11 | error.prop = 1; 12 | callback(error); 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /test/jshint.json: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": false, 3 | "camelcase": true, 4 | "curly": false, 5 | "eqeqeq": true, 6 | "forin": true, 7 | "immed": true, 8 | "indent": 4, 9 | "latedef": true, 10 | "newcap": true, 11 | "noarg": true, 12 | "nonew": true, 13 | "plusplus": false, 14 | "quotmark": false, 15 | "regexp": false, 16 | "undef": true, 17 | "unused": true, 18 | "strict": false, 19 | "trailing": true, 20 | "node": true, 21 | "noempty": true, 22 | "maxdepth": 4, 23 | "maxparams": 4 24 | } 25 | -------------------------------------------------------------------------------- /test/child/child-compressed.js: -------------------------------------------------------------------------------- 1 | var jsonrpc = require('../../lib/index'); 2 | var JsonRpcServer = jsonrpc.server; 3 | var JsonRpcChildProcTransport = jsonrpc.transports.server.childProcess; 4 | 5 | var server = new JsonRpcServer(new JsonRpcChildProcTransport({ compressed: true, compressLength: 1000 }), { 6 | loopback: function(obj, callback) { 7 | callback(null, obj); 8 | }, 9 | failure: function(obj, callback) { 10 | var error = new Error("Whatchoo talkin' 'bout, Willis?"); 11 | error.prop = 1; 12 | callback(error); 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /test/jshint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Unenforced jshint files 4 | 5 | #jshint --config scripts/jshint.json test/*.js 6 | 7 | # Enforced jshint files 8 | 9 | ## Full hinting rules 10 | 11 | jshint --config test/jshint.json lib/*.js lib/transports/client/*.js lib/transports/server/*.js lib/transports/shared/*.js test/*.js 12 | 13 | HINT_RESULT=$? 14 | 15 | # Determine exit code and quit 16 | 17 | if [ $HINT_RESULT -eq 0 ]; then 18 | echo Success: No enforced files failed the style test. 19 | else 20 | echo Failure: One or more enforced files failed the style test. 21 | fi 22 | 23 | exit $HINT_RESULT 24 | -------------------------------------------------------------------------------- /prepublish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Make sure tests pass before publishing 4 | npm test 5 | 6 | # Build documentation and commit to the gh-pages branch, then go back to master branch 7 | docco-husky lib/* test/* 8 | git stash 9 | mv docs docs-new 10 | git checkout gh-pages 11 | rm -rf docs 12 | mv docs-new docs 13 | git commit -am "Automatic documentation for version $npm_package_version" 14 | git checkout master 15 | git stash pop 16 | 17 | # Create the commit, tag the commit with the proper version, and push to GitHub 18 | git commit -am "Automatic commit of version $npm_package_version" 19 | git tag $npm_package_version 20 | git push 21 | git push --tags 22 | 23 | # Publish to NPM 24 | npm publish 25 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | server: require('./server'), 3 | client: require('./client'), 4 | transports: { 5 | client: { 6 | http: require('./transports/client/http'), 7 | tcp: require('./transports/client/tcp'), 8 | childProcess: require('./transports/client/childProcess') 9 | }, 10 | server: { 11 | http: require('./transports/server/http'), 12 | tcp: require('./transports/server/tcp'), 13 | middleware: require('./transports/server/middleware'), 14 | childProcess: require('./transports/server/childProcess') 15 | }, 16 | shared: { 17 | loopback: require('./transports/shared/loopback') 18 | } 19 | }, 20 | errorcode: require('./errorcode') 21 | }; 22 | -------------------------------------------------------------------------------- /test/client-childProccess.js: -------------------------------------------------------------------------------- 1 | var jsonrpc = require('../lib/index'); 2 | var ChildProcessTransport = jsonrpc.transports.client.childProcess; 3 | var JsonRpcClient = jsonrpc.client; 4 | var childProcess = require('child_process'); 5 | 6 | var child = childProcess.fork(__dirname + '/child/child.js'); 7 | var jsonRpcClient = new JsonRpcClient(new ChildProcessTransport(child)); 8 | jsonRpcClient.register(['loopback', 'failure']); 9 | 10 | exports.loopback = function(test) { 11 | test.expect(2); 12 | jsonRpcClient.loopback({foo: 'bar'}, function(err, result) { 13 | test.ok(!!result, 'result exists'); 14 | test.equal(result.foo, 'bar', 'Looped back correctly'); 15 | test.done(); 16 | }); 17 | }; 18 | 19 | exports.failureTcp = function(test) { 20 | test.expect(3); 21 | jsonRpcClient.failure({foo: 'bar'}, function(err) { 22 | test.ok(!!err, 'error exists'); 23 | test.equal("Whatchoo talkin' 'bout, Willis?", err.message, 'The error message was received correctly'); 24 | test.equal(1, err.prop, 'The error message was received correctly'); 25 | child.kill(); 26 | test.done(); 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /lib/transports/shared/loopback.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var EventEmitter = require('events').EventEmitter; 3 | 4 | // The loopback transport allows you to mock a JSON-RPC interface where the client 5 | // and server are on the same process. 6 | function LoopbackTransport() { 7 | // Set up the event emitter and create the property the server's message handler will bind to 8 | EventEmitter.call(this); 9 | this.handler = function fakeHandler() {}; 10 | return this; 11 | } 12 | 13 | // Inherit the event emitter methods 14 | util.inherits(LoopbackTransport, EventEmitter); 15 | 16 | // Create a fake shutdown method for the sake of API compatibility 17 | LoopbackTransport.prototype.shutdown = function shutdown(done) { 18 | this.emit('shutdown'); 19 | if(done instanceof Function) done(); 20 | }; 21 | 22 | // Pass the client requests to the server handler, and the response handling is taken care of 23 | // by the client's response handler. 24 | LoopbackTransport.prototype.request = function request(body, callback) { 25 | this.emit('message', body, JSON.stringify(body).length); 26 | this.handler(body, callback); 27 | }; 28 | 29 | // Export the Loopback object 30 | module.exports = LoopbackTransport; 31 | -------------------------------------------------------------------------------- /test/client-http.js: -------------------------------------------------------------------------------- 1 | var jsonrpc = require('../lib/index'); 2 | var HttpTransport = jsonrpc.transports.client.http; 3 | var http = require('http'); 4 | 5 | exports.loopback = function(test) { 6 | test.expect(3); 7 | var server = http.createServer(function(req, res) { 8 | test.equal('authToken', req.headers.authorization, 'authorization header received'); 9 | test.equal('thing', req.headers.other, 'other header received'); 10 | 11 | var buffer = ''; 12 | req.setEncoding('utf8'); 13 | req.on('data', function(data) { 14 | buffer += data; 15 | }); 16 | req.on('end', function() { 17 | res.write(buffer); 18 | res.end(); 19 | }); 20 | }); 21 | server.listen(12345, 'localhost', function() { 22 | var options = { 23 | headers: { 24 | authorization: 'authToken', 25 | other: 'thing' 26 | } 27 | }; 28 | 29 | var httpTransport = new HttpTransport('localhost', 12345, options); 30 | httpTransport.request('foo', function(result) { 31 | test.equal('foo', result, 'loopback works correctly'); 32 | server.close(); 33 | test.done(); 34 | }); 35 | }); 36 | }; -------------------------------------------------------------------------------- /bin/jsonrpc-repl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var repl = require('repl'); 4 | var commander = require('commander'); 5 | var l = require('lambda-js'); 6 | var jsonrpc = require('../lib/index'); 7 | var Client = jsonrpc.client; 8 | var TcpTransport = jsonrpc.transports.client.tcp; 9 | var HttpTransport = jsonrpc.transports.client.http; 10 | 11 | commander 12 | .option('-s, --server ', 'The hostname the server is located on. (Default: "localhost")', 'localhost') 13 | .option('-p, --port ', 'The port the server is bound to. (Default: 80)', 80) 14 | .option('-t, --tcp', 'Connects to the server via TCP instead of HTTP (Default: false)', false) 15 | .option('--path ', 'The path part of the URL, e.g. "/rpc" (Default: "/")', '/') 16 | .parse(process.argv); 17 | 18 | var Transport = commander.tcp ? TcpTransport : HttpTransport; 19 | 20 | new Client(new Transport(commander.server, commander.port, { path: commander.path }), {}, function(client) { 21 | console.log('Connected to ' + commander.server + ':' + commander.port + commander.path + ' over ' + (commander.tcp ? 'TCP' : 'HTTP')); 22 | console.log('The server reported the following methods:'); 23 | console.log(Object.getOwnPropertyNames(client).filter(l('name', 'name !== "transport"')).join(', ')); 24 | console.log('Access them with `rpc.foo(arg1, arg2, callbackFunction)`'); 25 | var r = repl.start({ prompt: 'jsonrpc> ', useGlobal: true }); 26 | r.context.rpc = client; 27 | r.on('exit', process.exit.bind(process)); 28 | }); 29 | -------------------------------------------------------------------------------- /test/client-childProccess-compressed.js: -------------------------------------------------------------------------------- 1 | var jsonrpc = require('../lib/index'); 2 | var ChildProcessTransport = jsonrpc.transports.client.childProcess; 3 | var JsonRpcClient = jsonrpc.client; 4 | var childProcess = require('child_process'); 5 | 6 | var child = childProcess.fork(__dirname + '/child/child-compressed.js'); 7 | var jsonRpcClient = new JsonRpcClient(new ChildProcessTransport(child, { compressed: true, compressLength: 1000 })); 8 | jsonRpcClient.register(['loopback', 'failure']); 9 | 10 | exports.loopback = function(test) { 11 | test.expect(2); 12 | jsonRpcClient.loopback({foo: 'bar'}, function(err, result) { 13 | test.ok(!!result, 'result exists'); 14 | test.equal(result.foo, 'bar', 'Looped back correctly'); 15 | test.done(); 16 | }); 17 | }; 18 | 19 | String.prototype.repeat = function(num) { 20 | return new Array(num + 1).join(this); 21 | }; 22 | 23 | exports.loopbackCompressed = function(test) { 24 | test.expect(2); 25 | jsonRpcClient.loopback('a'.repeat(1001), function(err, result) { 26 | test.ok(!!result, 'result exists'); 27 | test.equal(result, 'a'.repeat(1001), 'Looped back correctly'); 28 | test.done(); 29 | }); 30 | }; 31 | 32 | exports.failureTcp = function(test) { 33 | test.expect(3); 34 | jsonRpcClient.failure({foo: 'bar'}, function(err) { 35 | test.ok(!!err, 'error exists'); 36 | test.equal("Whatchoo talkin' 'bout, Willis?", err.message, 'The error message was received correctly'); 37 | test.equal(1, err.prop, 'The error message was received correctly'); 38 | child.kill(); 39 | test.done(); 40 | }); 41 | }; 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "contributors": [ 3 | "Aiden Scandella ", 4 | "Alain Rodriguez ", 5 | "Amos Barreto ", 6 | "David Ellis ", 7 | "Hector Lugo ", 8 | "Jeff Wolski ", 9 | "Jeremy Suurkivi ", 10 | "Hiroki Horiuchi " 11 | ], 12 | "name": "multitransport-jsonrpc", 13 | "version": "0.9.4", 14 | "description": "JSON-RPC where performance matters", 15 | "keywords": [ 16 | "json-rpc", 17 | "http", 18 | "tcp", 19 | "multitransport" 20 | ], 21 | "homepage": "http://uber.github.com/multitransport-jsonrpc/", 22 | "bugs": "https://github.com/uber/multitransport-jsonrpc/issues", 23 | "repository": { 24 | "type": "git", 25 | "url": "git://git@github.com:uber/multitransport-jsonrpc.git" 26 | }, 27 | "main": "lib/index.js", 28 | "bin": { 29 | "jsonrpc-repl": "bin/jsonrpc-repl" 30 | }, 31 | "dependencies": { 32 | "queue-flow": "<0.7.x", 33 | "lambda-js": "*", 34 | "commander": "*" 35 | }, 36 | "devDependencies": { 37 | "async": "*", 38 | "nodeunit": "*", 39 | "docco-husky": "*", 40 | "express": "3.x", 41 | "jshint": "*", 42 | "istanbul": "^0.2.16", 43 | "coveralls": "^2.11.0" 44 | }, 45 | "scripts": { 46 | "realpublish": "./prepublish.sh", 47 | "jshint": "test/jshint.sh", 48 | "test": "npm run jshint && istanbul --print=none cover nodeunit test/*.js && istanbul report text", 49 | "cover": "istanbul cover --report none --print detail nodeunit test/*.js", 50 | "view-cover": "istanbul report html && open ./coverage/index.html", 51 | "coveralls": "cat coverage/lcov.info | coveralls" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/server-http.js: -------------------------------------------------------------------------------- 1 | var jsonrpc = require('../lib/index'); 2 | var HttpTransport = jsonrpc.transports.server.http; 3 | var http = require('http'); 4 | 5 | exports.loopback = function(test) { 6 | test.expect(2); 7 | var httpTransport = new HttpTransport(11235); 8 | httpTransport.handler = function(jsonObj, callback) { 9 | callback(jsonObj); 10 | }; 11 | var testJSON = JSON.stringify({ hello: 'world' }); 12 | var req = http.request({ 13 | hostname: 'localhost', 14 | port: 11235, 15 | path: '/', 16 | method: 'POST' 17 | }, function(res) { 18 | res.setEncoding('utf8'); 19 | var resultString = ''; 20 | res.on('data', function(data) { 21 | resultString += data; 22 | }); 23 | res.on('end', function() { 24 | test.equal(res.statusCode, 200, 'The http transport provided an OK status code'); 25 | test.equal(resultString, testJSON, 'The http transport successfully sent the same JSON data back to the client.'); 26 | httpTransport.server.close(); 27 | test.done(); 28 | }); 29 | }); 30 | req.write(testJSON); 31 | req.end(); 32 | }; 33 | 34 | exports.failure = function(test) { 35 | test.expect(1); 36 | var httpTransport = new HttpTransport(12345); 37 | httpTransport.handler = function(jsonObj, callback) { 38 | callback({ error: "I have no idea what I'm doing." }); 39 | }; 40 | var testJSON = JSON.stringify({ hello: 'world' }); 41 | var req = http.request({ 42 | hostname: 'localhost', 43 | port: 12345, 44 | path: '/', 45 | method: 'POST' 46 | }, function(res) { 47 | res.setEncoding('utf8'); 48 | var resultString = ''; 49 | res.on('data', function(data) { 50 | resultString += data; 51 | }); 52 | res.on('end', function() { 53 | test.equal(res.statusCode, 500, 'The http transport provided a server error status code'); 54 | httpTransport.server.close(); 55 | test.done(); 56 | }); 57 | }); 58 | req.write(testJSON); 59 | req.end(); 60 | }; 61 | -------------------------------------------------------------------------------- /lib/transports/server/childProcess.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var EventEmitter = require('events').EventEmitter; 3 | var zlib = require('zlib'); 4 | 5 | // The Server ChildProcessTransport constructor function 6 | function ChildProcessTransport(config) { 7 | // Initialize the EventEmitter for this object 8 | EventEmitter.call(this); 9 | 10 | // Make sure the config is addressable and add config settings 11 | // and a dummy handler function to the object 12 | config = config || {}; 13 | this.handler = function fakeHandler(json, next) { next({}); }; 14 | 15 | function uncompressedMessageHandler(json) { 16 | this.emit('message', json, -1); // Message len unsupported by the child process message event 17 | this.handler(json, process.send.bind(process)); 18 | } 19 | 20 | function compressedMessageResponseHandler(jsonrpcObj) { 21 | var jsonrpcStr = JSON.stringify(jsonrpcObj); 22 | if (!config.compressLength || jsonrpcStr.length > config.compressLength) { 23 | zlib.gzip(new Buffer(JSON.stringify(jsonrpcObj)), function(err, compressedJSON) { 24 | if (err) return this.emit('error', err.message); 25 | process.send('z' + compressedJSON.toString('base64')); 26 | }.bind(this)); 27 | } else { 28 | process.send(jsonrpcStr); 29 | } 30 | } 31 | 32 | function compressedMessageHandler(json) { 33 | if (json.charAt(0) === 'z') { 34 | var buf = new Buffer(json.substring(1), 'base64'); 35 | zlib.gunzip(buf, function(err, uncompressedJSON) { 36 | if (err) return this.emit('error', err.message); 37 | var obj = JSON.parse(uncompressedJSON.toString('utf8')); 38 | this.handler(obj, compressedMessageResponseHandler.bind(this)); 39 | }.bind(this)); 40 | } else { 41 | var obj = JSON.parse(json); 42 | this.handler(obj, compressedMessageResponseHandler.bind(this)); 43 | } 44 | } 45 | 46 | this.messageHandler = config.compressed ? compressedMessageHandler.bind(this) : uncompressedMessageHandler.bind(this); 47 | process.on('message', this.messageHandler); 48 | 49 | return this; 50 | } 51 | 52 | // Attach the EventEmitter prototype to the prototype chain 53 | util.inherits(ChildProcessTransport, EventEmitter); 54 | 55 | // A simple wrapper for closing the HTTP server (so the TCP 56 | // and HTTP transports have a more uniform API) 57 | ChildProcessTransport.prototype.shutdown = function shutdown(done) { 58 | this.emit('shutdown'); 59 | process.removeListener('message', this.messageHandler); 60 | if(done instanceof Function) done(); 61 | }; 62 | 63 | // Export the Server ChildProcess transport 64 | module.exports = ChildProcessTransport; 65 | -------------------------------------------------------------------------------- /lib/transports/client/http.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var q = require('queue-flow'); 3 | var l = require('lambda-js'); 4 | var util = require('util'); 5 | var EventEmitter = require('events').EventEmitter; 6 | 7 | // The Client HTTP Transport constructor function 8 | function HttpTransport(server, port, config) { 9 | // Initialize the EventEmitter 10 | EventEmitter.call(this); 11 | // Make sure the config is a valid object 12 | // and set the necessary elements 13 | config = config || {}; 14 | this.path = config.path || '/'; 15 | this.headers = config.headers || {}; 16 | this.server = server; 17 | this.port = port; 18 | 19 | return this; 20 | } 21 | 22 | // Attach the EventEmitter prototype into the prototype chain 23 | util.inherits(HttpTransport, EventEmitter); 24 | 25 | // For the HTTP client, the meat of the transport lives in its request 26 | // method, since HTTP requests are separate connections 27 | HttpTransport.prototype.request = function request(body, callback) { 28 | // Create a request object for the server, using the POST method 29 | var req = http.request({ 30 | hostname: this.server, 31 | port: this.port, 32 | path: this.path, 33 | headers: this.headers, 34 | method: 'POST' 35 | }, function(res) { 36 | // This one liner creates an anonymous queue assigned to `r` 37 | // that concatenates all input together until the queue is 38 | // closed and then returns the result to the callback 39 | var r = q.ns()().reduce(l('cum, cur', 'cum + cur'), function(result) { 40 | // The callback assumes the input is JSON and parses it 41 | // and sends it to the request's callback function. If 42 | // its not valid JSON, it'll simply send it `undefined`. 43 | var json; 44 | try { 45 | json = JSON.parse(result); 46 | } catch(e) { 47 | } 48 | this.emit('message', json, result.length); 49 | callback(json); 50 | }.bind(this), ''); 51 | // The queue's push and close methods are attached to the 52 | // `data` and `end` events of the request 53 | res.on('data', r.push.bind(r)); 54 | res.on('end', r.close.bind(r)); 55 | }.bind(this)); 56 | 57 | // Handle dead connections gracefully without crashing node 58 | req.once('error', callback); 59 | 60 | // The request body is sent to the server as JSON 61 | req.setHeader('Content-Type', 'application/json'); 62 | req.write(JSON.stringify(body)); 63 | req.end(); 64 | }; 65 | 66 | // Literally nothing needed for the HTTP client. Just call 67 | // the callback for API consistency 68 | HttpTransport.prototype.shutdown = function shutdown(done) { 69 | this.emit('shutdown'); 70 | if(done instanceof Function) done(); 71 | }; 72 | 73 | // Export the Client HTTP Transport 74 | module.exports = HttpTransport; 75 | -------------------------------------------------------------------------------- /lib/transports/server/middleware.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var EventEmitter = require('events').EventEmitter; 3 | 4 | // Connect/Express middleware style JSON-RPC server transport 5 | // Let's you have a hybrid Connect/Express server that also performs JSON-RPC 6 | // on a particular path. Still done as an instance so you can conceivably have 7 | // multiple JSON-RPC servers on a single Connect/Express server. 8 | function MiddlewareTransport(config) { 9 | // Initialize the EventEmitter for this object 10 | EventEmitter.call(this); 11 | 12 | // Make sure the config object exists, the handler function exists, 13 | // and the Access-Control-Allow-Origin header is properly set. Also 14 | // allow the user to provide a reference to the underlying HTTP server 15 | // so the ``shutdown`` method can work as expected, if desired. 16 | config = config || {}; 17 | this.handler = function fakeHandler(json, next) { next({}); }; 18 | this.acao = config.acao ? config.acao : "*"; 19 | this.server = config.server || null; 20 | this.middleware = this.requestHandler.bind(this); 21 | 22 | return this; 23 | } 24 | 25 | // Attach the EventEmitter prototype to the prototype chain 26 | util.inherits(MiddlewareTransport, EventEmitter); 27 | 28 | // The ``requestHandler`` method gets the request and response objects, and passes 29 | // the request body and the bound responseHandler to the JSON-RPC hander function 30 | MiddlewareTransport.prototype.requestHandler = function requestHandler(req, res) { 31 | // All requests are assumed to be "Express-like" and have the bodyParser run 32 | // before it. Express doesn't have a good way (that I'm aware of) to specify 33 | // "run this middleware if it hasn't already been run". 34 | this.emit('message', req.body); 35 | this.handler(req.body, this.responseHandler.bind(this, res)); 36 | }; 37 | 38 | // The ``responseHandler`` method takes the output object and sends it to the client 39 | MiddlewareTransport.prototype.responseHandler = function responseHandler(res, retObj) { 40 | var outString = JSON.stringify(retObj); 41 | res.writeHead(retObj.error? 500:200, { 42 | "Access-Control-Allow-Origin": this.acao, 43 | "Content-Length": Buffer.byteLength(outString, 'utf8'), 44 | "Content-Type": "application/json;charset=utf-8" 45 | }); 46 | res.end(outString); 47 | }; 48 | 49 | // If the user defined the server in the config, the ``shutdown`` method will 50 | // tell the server to shut down. Likely, when a JSON-RPC server is used as a 51 | // middleware, this will not be done, but for API consistency's sake, it could. 52 | MiddlewareTransport.prototype.shutdown = function shutdown(done) { 53 | if(this.server) { 54 | this.emit('shutdown'); 55 | this.server.close(done); 56 | } else { 57 | if(done instanceof Function) done(); 58 | } 59 | }; 60 | 61 | // Export the Server Middleware Transport 62 | module.exports = MiddlewareTransport; 63 | -------------------------------------------------------------------------------- /lib/transports/server/http.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var q = require('queue-flow'); 3 | var l = require('lambda-js'); 4 | var util = require('util'); 5 | var EventEmitter = require('events').EventEmitter; 6 | 7 | // The Server HttpTransport constructor function 8 | function HttpTransport(port, config) { 9 | // Initialize the EventEmitter for this object 10 | EventEmitter.call(this); 11 | 12 | // Make sure the config is addressable and add config settings 13 | // and a dummy handler function to the object 14 | config = config || {}; 15 | this.handler = function fakeHandler(json, next) { next({}); }; 16 | this.acao = config.acao ? config.acao : "*"; 17 | this.port = port; 18 | 19 | // Construct the http server and listen on the desired port 20 | this.server = http.createServer(function(req, res) { 21 | // All requests are assumed to be POST-like and have a body 22 | // This first line creates an anonymous queue, and appends 23 | // all inputs together until the queue is closed, then returns the 24 | // result to the callback function 25 | var r = q.ns()().reduce(l('cum, cur', 'cum + cur'), function(result) { 26 | // The result is assumed to be JSON and is parsed and 27 | // passed along to the request handler, whose results are passed 28 | // to the responseHandler 29 | var json; 30 | try { 31 | json = JSON.parse(result); 32 | } catch(e) { 33 | // Literally don't need to do anything at the moment here. 34 | } 35 | this.emit('message', json, result.length); 36 | this.handler(json, this.responseHandler.bind(this, res)); 37 | }.bind(this), ''); 38 | // The queue defined above has its push and close methods bound to the 39 | // `data` and `end` events 40 | req.on('data', r.push.bind(r)); 41 | req.on('end', r.close.bind(r)); 42 | }.bind(this)); 43 | this.server.on('listening', function() { 44 | this.emit('listening'); 45 | }.bind(this)); 46 | this.server.listen(this.port); 47 | 48 | return this; 49 | } 50 | 51 | // Attach the EventEmitter prototype to the prototype chain 52 | util.inherits(HttpTransport, EventEmitter); 53 | 54 | // The responseHandler gets a response object and the return object, stringifies 55 | // the return object and sends it down to the client with the appropriate HTTP 56 | // headers 57 | HttpTransport.prototype.responseHandler = function responseHandler(res, retObj) { 58 | var outString = JSON.stringify(retObj); 59 | res.writeHead(retObj.error?500:200, { 60 | "Access-Control-Allow-Origin": this.acao, 61 | "Content-Length": Buffer.byteLength(outString, 'utf8'), 62 | "Content-Type": "application/json;charset=utf-8" 63 | }); 64 | res.end(outString); 65 | }; 66 | 67 | // A simple wrapper for closing the HTTP server (so the TCP 68 | // and HTTP transports have a more uniform API) 69 | HttpTransport.prototype.shutdown = function shutdown(done) { 70 | this.emit('shutdown'); 71 | this.server.close(done); 72 | }; 73 | 74 | // Export the Server HTTP transport 75 | module.exports = HttpTransport; 76 | -------------------------------------------------------------------------------- /test/server-tcp.js: -------------------------------------------------------------------------------- 1 | var jsonrpc = require('../lib/index'); 2 | var TcpTransport = jsonrpc.transports.server.tcp; 3 | var shared = require('../lib/transports/shared/tcp'); 4 | var net = require('net'); 5 | 6 | exports.loopback = function(test) { 7 | test.expect(1); 8 | var tcpTransport = new TcpTransport(11235); 9 | tcpTransport.handler = function(jsonObj, callback) { 10 | callback(jsonObj); 11 | }; 12 | var testJSON = JSON.stringify({ hello: 'world' }); 13 | var con = net.connect({ 14 | port: 11235, 15 | host: 'localhost' 16 | }, function() { 17 | con.write(shared.formatMessage(testJSON)); 18 | }); 19 | var buffers = [], bufferLen = 0, messageLen = 0; 20 | con.on('data', function(data) { 21 | buffers.push(data); 22 | bufferLen += data.length; 23 | if(messageLen === 0) messageLen = shared.getMessageLen(buffers); 24 | if(bufferLen === messageLen + 4) con.end(); 25 | }); 26 | con.on('end', function() { 27 | var result = buffers.reduce(function(outBuffer, currBuffer) { 28 | return Buffer.concat([outBuffer, currBuffer]); 29 | }, new Buffer('')); 30 | test.equal(result.toString(), shared.formatMessage(testJSON).toString(), 'Loopback functioned correctly'); 31 | tcpTransport.shutdown(); 32 | test.done(); 33 | }); 34 | }; 35 | 36 | exports.failure = function(test) { 37 | test.expect(1); 38 | var tcpTransport = new TcpTransport(12345); 39 | tcpTransport.handler = function(jsonObj, callback) { 40 | callback({ error: "I have no idea what I'm doing." }); 41 | }; 42 | var testJSON = JSON.stringify({ hello: 'world' }); 43 | var con = net.connect({ 44 | port: 12345, 45 | host: 'localhost' 46 | }, function() { 47 | con.write(shared.formatMessage(testJSON)); 48 | }); 49 | var buffers = [], bufferLen = 0, messageLen = 0; 50 | con.on('data', function(data) { 51 | buffers.push(data); 52 | bufferLen += data.length; 53 | if(messageLen === 0) messageLen = shared.getMessageLen(buffers); 54 | if(bufferLen === messageLen + 4) con.end(); 55 | }); 56 | con.on('end', function() { 57 | var result = buffers.reduce(function(outBuffer, currBuffer) { 58 | return Buffer.concat([outBuffer, currBuffer]); 59 | }, new Buffer('')); 60 | try { 61 | var obj = JSON.parse(result.toString('utf8', 4)); 62 | test.equal(obj.error, "I have no idea what I'm doing.", 'error returned correctly'); 63 | } catch(e) { 64 | // Nothing 65 | } 66 | tcpTransport.shutdown(); 67 | test.done(); 68 | }); 69 | }; 70 | 71 | exports.listening = function(test) { 72 | test.expect(1); 73 | var tcpTransport = new TcpTransport(12346); 74 | tcpTransport.on('listening', function() { 75 | test.ok(true, 'listening callback fired'); 76 | tcpTransport.server.close(); 77 | test.done(); 78 | }); 79 | }; 80 | 81 | exports.retry = function(test) { 82 | test.expect(1); 83 | var tcpTransport1 = new TcpTransport(2468); 84 | tcpTransport1.on('listening', function() { 85 | var tcpTransport2 = new TcpTransport(2468, { retries: 1 }); 86 | tcpTransport2.on('listening', function() { 87 | test.ok(true, 'second tcpTransport eventually succeeded to start'); 88 | tcpTransport2.server.close(); 89 | test.done(); 90 | }); 91 | setTimeout(function() { 92 | tcpTransport1.shutdown(); 93 | }, 50); 94 | }); 95 | }; 96 | 97 | exports.dontSendAfterClose = function(test) { 98 | test.expect(1); 99 | var tcpTransport = new TcpTransport(2222); 100 | tcpTransport.handler = function(jsonObj, callback) { 101 | // The timeout should cause it to try to send the message after the client disconnected 102 | // The server should not throw an error in this condition 103 | setTimeout(callback.bind(this, jsonObj), 3000); 104 | }; 105 | tcpTransport.on('listening', function() { 106 | var con = net.connect({ 107 | port: 2222, 108 | host: 'localhost' 109 | }, function() { 110 | con.write(shared.formatMessage({hello: 'world'})); 111 | test.ok(true, 'wrote the message to the server and killed the connection'); 112 | con.destroy(); 113 | }); 114 | }); 115 | setTimeout(function() { 116 | tcpTransport.shutdown(test.done.bind(test)); 117 | }, 4000); 118 | }; 119 | -------------------------------------------------------------------------------- /lib/transports/server/tcp.js: -------------------------------------------------------------------------------- 1 | var net = require('net'); 2 | var util = require('util'); 3 | var EventEmitter = require('events').EventEmitter; 4 | var shared = require('../shared/tcp'); 5 | 6 | // The Server TCP Transport constructor function 7 | function TcpTransport(port, config) { 8 | // Initialize the EventEmitter for this object 9 | EventEmitter.call(this); 10 | 11 | // Fix config property references if no config provided 12 | config = config || {}; 13 | 14 | // If the server retries count is a number, establish the number of times it has currently retried to zero 15 | // and make sure there is a retry interval 16 | if(config.retries/1 === config.retries) { 17 | config.retry = 0; 18 | config.retryInterval = config.retryInterval || 250; 19 | } 20 | 21 | // The fake handler guarantees that V8 doesn't subclass the transport when the user's handler is attached 22 | this.handler = function fakeHandler(json, next) { next({}); }; 23 | this.port = port; 24 | 25 | this.logger = config.logger || function() {}; 26 | this.connections = {}; 27 | this.server = net.createServer(function(con) { 28 | this.connections[JSON.stringify(con.address())] = con; 29 | this.emit('connection', con); 30 | con.on('data', shared.createDataHandler(this, function(message) { 31 | this.handler(message, this.handlerCallback.bind(this, con)); 32 | }.bind(this))); 33 | var onEndOrError = function() { 34 | delete this.connections[JSON.stringify(con.address())]; 35 | if(!con.isClosed) { 36 | this.emit('closedConnection', con); 37 | // When the connection for a client dies, make sure the handlerCallbacks don't try to use it 38 | con.isClosed = true; 39 | } 40 | }.bind(this); 41 | con.on('end', onEndOrError); 42 | con.on('error', onEndOrError); 43 | }.bind(this)); 44 | 45 | // Shorthand for registering a listening callback handler 46 | this.server.on('listening', function() { 47 | // Reset the retry counter on a successful connection 48 | config.retry = 0; 49 | this.emit('listening'); 50 | }.bind(this)); 51 | this.server.listen(port); 52 | 53 | // Any time the server encounters an error, check it here. 54 | // Right now it only handles errors when trying to start the server 55 | this.server.on('error', function(e) { 56 | if(e.code === 'EADDRINUSE') { 57 | // If something else has the desired port 58 | if(config.retries && config.retry < config.retries) { 59 | this.emit('retry', e); 60 | // And we're allowed to retry 61 | config.retry++; 62 | // Wait a bit and retry 63 | setTimeout(function() { 64 | this.server.listen(port); 65 | }.bind(this), config.retryInterval); 66 | } else { 67 | // Or bitch about it 68 | this.emit('error', e); 69 | } 70 | } else { 71 | // Some unhandled error 72 | this.emit('error', e); 73 | } 74 | }.bind(this)); 75 | 76 | // A simple flag to make sure calling ``shutdown`` after the server has already been shutdown doesn't crash Node 77 | this.server.on('close', function() { 78 | this.logger('closing'); 79 | this.emit('shutdown'); 80 | this.notClosed = false; 81 | }.bind(this)); 82 | this.notClosed = true; 83 | 84 | return this; 85 | } 86 | 87 | // Attach the EventEmitter prototype into the prototype chain 88 | util.inherits(TcpTransport, EventEmitter); 89 | 90 | // An almost ridiculously simple callback handler, whenever the return object comes in, stringify it and send it down the line (along with a message length prefix) 91 | TcpTransport.prototype.handlerCallback = function handlerCallback(con, retObj) { 92 | if(!con.isClosed) con.write(shared.formatMessage(retObj, this)); 93 | }; 94 | 95 | // When asked to shutdown the server, shut it down 96 | TcpTransport.prototype.shutdown = function shutdown(done) { 97 | this.logger('shutdown transport'); 98 | if(this.server && this.notClosed) { 99 | this.logger('shutdown transport 2'); 100 | this.server.close(done); 101 | Object.keys(this.connections).forEach(function (key) { 102 | var con = this.connections[key]; 103 | con.destroy(); 104 | }.bind(this)); 105 | } 106 | }; 107 | 108 | // Export the Server TCP Transport 109 | module.exports = TcpTransport; 110 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | // ## The JSONRPC constructor 2 | // Each JSON-RPC object created is tied to a particular JSON-RPC server URL. 3 | // This may be inconvenient for server architectures that have many URLs for 4 | // each JSON-RPC server, but this is an odd use case we aren't implementing. 5 | // 6 | // The constructed JSON-RPC objects consist of three built-in methods: 7 | // 8 | // * request 9 | // * register 10 | // 11 | // The *request* and *requestBlock* functions are the ones actually used to 12 | // call the JSON-RPC server, and the *register* function constructs the expected 13 | // function names to be used by the developer using this JSON-RPC client. 14 | 15 | // The JSONRPC constructor *must* receive a server URL on initialization 16 | function JSONRPC(transport, options, done) { 17 | this.transport = transport; 18 | // Parse any *options* provided to the client 19 | // If no *options* object provided, create an empty one 20 | if(typeof(options) !== "object" || options === null) { 21 | options = {}; 22 | } 23 | //add custom request id generator if provided in options 24 | if(options.hasOwnProperty("idGenerator") && typeof(options.idGenerator) === "function") { 25 | this.idGenerator = options.idGenerator; 26 | } 27 | //default 28 | else this.idGenerator = Math.random; 29 | 30 | // *autoRegister* methods from the server unless explicitly told otherwise 31 | if(!options.hasOwnProperty("autoRegister") || options.autoRegister) { 32 | this.request('rpc.methodList', [], function(err, result) { 33 | if(!err) this.register(result); 34 | if(done) done(this); 35 | }.bind(this)); 36 | } 37 | // Once the JSONRPC object has been properly initialized, return the object 38 | // to the developer 39 | return this; 40 | } 41 | 42 | // ### The *request* function 43 | // is a non-blocking function that takes an arbitrary number of arguments, 44 | // where the first argument is the remote method name to execute, the last 45 | // argument is the callback function to execute when the server returns its 46 | // results, and all of the arguments in between are the values passed to the 47 | // remote method. 48 | JSONRPC.prototype.request = function(method, args, callback) { 49 | // The *contents* variable contains the JSON-RPC 1.0 POST string. 50 | var requestId = this.idGenerator(); 51 | if(!requestId || requestId === null) { 52 | if(callback instanceof Function) { 53 | callback(new Error('Request id generator function should return an id')); 54 | return; 55 | } 56 | } 57 | 58 | var contents = { 59 | method: method, 60 | params: args, 61 | id: requestId 62 | }; 63 | this.transport.request(contents, function(response) { 64 | if(!response && callback instanceof Function) { 65 | callback(new Error("Server did not return valid JSON-RPC response")); 66 | return; 67 | } 68 | if(callback instanceof Function) { 69 | if (response instanceof Error){ 70 | callback(response); 71 | } else if(response.error) { 72 | if(response.error.message) { 73 | var err = new Error(response.error.message); 74 | Object.keys(response.error).forEach(function(key) { 75 | if(key !== 'message') err[key] = response.error[key]; 76 | }); 77 | callback(err); 78 | } else if (typeof response.error === 'string') { 79 | callback(new Error(response.error)); 80 | } else { 81 | callback(response.error); 82 | } 83 | } else { 84 | callback(undefined, response.result); 85 | } 86 | } 87 | }); 88 | }; 89 | 90 | // ### The *register* function 91 | // is a simple blocking function that takes a method name or array of 92 | // method names and directly modifies the 93 | JSONRPC.prototype.register = function(methods) { 94 | if(!(methods instanceof Array)) { 95 | methods = [methods]; 96 | } 97 | methods.forEach(function(method) { 98 | if(method !== "transport" && method !== "request" && method !== "register" && method !== "shutdown") { 99 | this[method] = function() { 100 | var theArgs = []; 101 | for(var i = 0; i < arguments.length-1; i++) { 102 | theArgs[i] = arguments[i]; 103 | } 104 | var callback = arguments[arguments.length-1]; 105 | this.request(method, theArgs, callback); 106 | }; 107 | } 108 | }.bind(this)); 109 | }; 110 | 111 | // Cleanly shutdown the JSONRPC client 112 | JSONRPC.prototype.shutdown = function(done) { 113 | this.transport.shutdown(done); 114 | }; 115 | 116 | module.exports = JSONRPC; 117 | -------------------------------------------------------------------------------- /lib/transports/client/childProcess.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var EventEmitter = require('events').EventEmitter; 3 | var zlib = require('zlib'); 4 | 5 | // The Client ChildProcessTransport constructor function 6 | function ChildProcessTransport(child, config) { 7 | // Initialize the Node EventEmitter on this 8 | EventEmitter.call(this); 9 | 10 | config = config || {}; 11 | this.requests = {}; 12 | this.killChildOnShutdown = typeof(config.killChildOnShutdown) === 'boolean' ? config.killChildOnShutdown : true; 13 | this.timeout = config.timeout || 30*1000; 14 | this.sweepTime = config.sweepTime || 1*1000; 15 | this.sweepInterval = setInterval(this.sweep.bind(this), this.sweepTime); 16 | this.compressed = config.compressed || false; 17 | this.compressLength = config.compressLength || 0; 18 | this.child = child; 19 | 20 | function uncompressedMessageHandler(message) { 21 | if (message && this.requests[message.id]) { 22 | this.requests[message.id].callback(message); 23 | delete this.requests[message.id]; 24 | } 25 | } 26 | 27 | function compressedMessageHandler(message) { 28 | if (message && message.charAt(0) === 'z') { 29 | var buf = new Buffer(message.substring(1), 'base64'); 30 | zlib.gunzip(buf, function(err, uncompressedJSON) { 31 | if (err) return this.emit('error', err.message); 32 | var obj = JSON.parse(uncompressedJSON.toString('utf8')); 33 | if (obj && this.requests[obj.id]) { 34 | this.requests[obj.id].callback(obj); 35 | delete this.requests[obj.id]; 36 | } 37 | }.bind(this)); 38 | } else { 39 | var json = JSON.parse(message); 40 | if (this.requests[json.id]) { 41 | this.requests[json.id].callback(json); 42 | delete this.requests[json.id]; 43 | } 44 | } 45 | } 46 | this.child.on('message', this.compressed ? compressedMessageHandler.bind(this) : uncompressedMessageHandler.bind(this)); 47 | this.child.on('exit', function(code, signal) { 48 | this.emit('exit', code, signal); 49 | this.shutdown(); 50 | }.bind(this)); 51 | this.child.on('error', function(e) { 52 | this.emit('error', e); 53 | this.shutdown(); 54 | }.bind(this)); 55 | 56 | return this; 57 | } 58 | 59 | // Attach the EventEmitter prototype as the ChildProcessTransport's prototype's prototype 60 | util.inherits(ChildProcessTransport, EventEmitter); 61 | 62 | // The request logic is relatively straightforward, given the request 63 | // body and callback function, register the request with the requests 64 | // object, then if there is a valid connection at the moment, send the 65 | // request to the server with a null terminator attached. This ordering 66 | // guarantees that requests called during a connection issue won't be 67 | // lost while a connection is re-established. 68 | ChildProcessTransport.prototype.request = function request(body, callback) { 69 | this.requests[body.id] = { 70 | callback: callback, 71 | body: body, 72 | timestamp: Date.now() 73 | }; 74 | if (this.child) { 75 | if (this.compressed) { 76 | var jsonStr = JSON.stringify(body); 77 | if (!this.compressLength || jsonStr.length > this.compressLength) { 78 | zlib.gzip(new Buffer(JSON.stringify(body)), function(err, compressedJSON) { 79 | if (err) return this.emit('error', err.message); 80 | this.child.send('z' + compressedJSON.toString('base64')); 81 | }.bind(this)); 82 | } else { 83 | this.child.send(jsonStr); 84 | } 85 | } else { 86 | this.child.send(body); 87 | } 88 | } 89 | }; 90 | 91 | // The sweep function looks at the timestamps for each request, and any 92 | // request that is longer lived than the timeout (default 2 min) will be 93 | // culled and assumed lost. 94 | ChildProcessTransport.prototype.sweep = function sweep() { 95 | var now = Date.now(); 96 | var cannedRequests = {}; 97 | for(var key in this.requests) { 98 | if(this.requests[key].timestamp && this.requests[key].timestamp + this.timeout < now) { 99 | this.requests[key].callback({ error: 'Request Timed Out' }); 100 | cannedRequests[key] = this.requests[key]; 101 | delete this.requests[key]; 102 | } 103 | } 104 | this.emit('sweep', cannedRequests); 105 | }; 106 | 107 | // When shutting down the client connection, the sweep is turned off, the 108 | // requests are removed, the number of allowed retries is set to zero, the 109 | // connection is ended, and a callback, if any, is called. 110 | ChildProcessTransport.prototype.shutdown = function shutdown(done) { 111 | clearInterval(this.sweepInterval); 112 | this.requests = {}; 113 | if(this.killChildOnShutdown) { 114 | if(this.child) this.child.kill(); 115 | delete this.child; 116 | } else { 117 | this.child.disconnect(); 118 | } 119 | this.emit('shutdown'); 120 | if(done instanceof Function) done(); 121 | }; 122 | 123 | // Export the client ChildProcessTransport 124 | module.exports = ChildProcessTransport; 125 | -------------------------------------------------------------------------------- /lib/transports/shared/tcp.js: -------------------------------------------------------------------------------- 1 | // Take a JSON object and transform it into a [Pascal string](http://en.wikipedia.org/wiki/String_%28computer_science%29#Length-prefixed) stored in a buffer. 2 | // The length prefix is big-endian because DEATH TO THE LITTLE ENDIAN LILLIPUTIANS! 3 | function formatMessage(obj, eventEmitter) { 4 | var str = JSON.stringify(obj); 5 | var strlen = Buffer.byteLength(str); 6 | if(eventEmitter) eventEmitter.emit('outMessage', obj, strlen); 7 | var buf = new Buffer(4 + strlen); 8 | buf.writeUInt32BE(strlen, 0); 9 | buf.write(str, 4, strlen, 'utf8'); 10 | return buf; 11 | } 12 | 13 | // Since all messages start with a length prefix and the "current" message is the first in the buffers array, 14 | // we can determine the message length just by the first buffer in the array. This technically assumes that 15 | // a buffer is at least 4 bytes large, but that should be a safe assumption. 16 | function getMessageLen(buffers) { 17 | if(buffers[0] && buffers[0].length >= 4) { 18 | return buffers[0].readUInt32BE(0); 19 | } else { 20 | return 0; 21 | } 22 | } 23 | 24 | // Simple helper function that returns the minimum value from all values passed into it 25 | function min() { 26 | return Array.prototype.reduce.call(arguments, function(curr, val) { 27 | return (val < curr) ? val : curr; 28 | }, Infinity); 29 | } 30 | 31 | // Given an array of buffers, the message length, and the eventEmitter object (in case of error) 32 | // try to parse the message and return the object it contains 33 | function parseBuffer(buffers, messageLen, eventEmitter) { 34 | 35 | // Allocate a new buffer the size of the message to copy the buffers into 36 | // and keep track of how many bytes have been copied and what buffer we're currently on 37 | var buf = new Buffer(messageLen); 38 | var bytesCopied = 0; 39 | var currBuffer = 0; 40 | 41 | // Continue copying until we've hit the message size 42 | while (bytesCopied < messageLen) { 43 | 44 | // bytesToCopy contains how much of the buffer we'll copy, either the 45 | // "whole thing" or "the rest of the message". 46 | var bytesToCopy = 0; 47 | 48 | // Since the first buffer contains the message length itself, it's special-cased 49 | // to skip those 4 bytes 50 | if (currBuffer === 0) { 51 | bytesToCopy = min(messageLen, buffers[0].length-4); 52 | buffers[0].copy(buf, bytesCopied, 4, bytesToCopy+4); 53 | } else { 54 | bytesToCopy = min(messageLen-bytesCopied, buffers[currBuffer].length); 55 | buffers[currBuffer].copy(buf, bytesCopied, 0, bytesToCopy); 56 | } 57 | 58 | // Increment the number of bytes copied by how many were copied 59 | bytesCopied += bytesToCopy; 60 | 61 | // If we're done, we have some cleanup to do; either appending the final chunk of the buffer 62 | // to the next buffer, or making sure that the array slice after the while loop is done 63 | // appropriately 64 | if (bytesCopied === messageLen) { 65 | if(currBuffer === 0) bytesToCopy += 4; 66 | if(buffers[currBuffer].length !== bytesToCopy) { 67 | buffers[currBuffer] = buffers[currBuffer].slice(bytesToCopy); 68 | if (buffers[currBuffer].length < 4 && buffers[currBuffer+1]) { 69 | buffers[currBuffer+1] = Buffer.concat([buffers[currBuffer], buffers[currBuffer+1]]); 70 | } else { 71 | currBuffer--; // Counter the increment below 72 | } 73 | } 74 | } 75 | 76 | // Move on to the next buffer in the array 77 | currBuffer++; 78 | } 79 | 80 | // Trim the buffers array to the next message 81 | buffers = buffers.slice(currBuffer); 82 | 83 | // Parse the buffer we created into a string and then a JSON object, or emit the parsing error 84 | var obj; 85 | try { 86 | obj = JSON.parse(buf.toString()); 87 | } catch (e) { 88 | eventEmitter.emit('babel', buf.toString()); 89 | eventEmitter.emit('error', e); 90 | } 91 | return [buffers, obj]; 92 | } 93 | 94 | 95 | function createDataHandler(self, callback) { 96 | var buffers = [], bufferLen = 0, messageLen = 0; 97 | return function dataHandler(data) { 98 | if(!data) { return; } // Should we emit some sort of error here? 99 | if(buffers[buffers.length-1] && buffers[buffers.length-1].length < 4) { 100 | buffers[buffers.length-1] = Buffer.concat([buffers[buffers.length-1], data], buffers[buffers.length-1].length + data.length); 101 | } else { 102 | buffers.push(data); 103 | } 104 | bufferLen += data.length; 105 | if(!messageLen) messageLen = getMessageLen(buffers); 106 | if(bufferLen - 4 >= messageLen) { 107 | var result, obj; 108 | while (messageLen && bufferLen - 4 >= messageLen && (result = parseBuffer(buffers, messageLen, self))) { 109 | buffers = result[0]; 110 | obj = result[1]; 111 | self.emit('message', obj, messageLen); 112 | try { 113 | callback(obj); 114 | } catch(e) { 115 | /* jshint loopfunc: true */ 116 | process.nextTick(function() { throw e; }); 117 | } 118 | bufferLen = bufferLen - (messageLen + 4); 119 | messageLen = getMessageLen(buffers); 120 | } 121 | } 122 | }; 123 | } 124 | 125 | // Export the public methods 126 | module.exports.formatMessage = formatMessage; 127 | module.exports.getMessageLen = getMessageLen; 128 | module.exports.parseBuffer = parseBuffer; 129 | module.exports.createDataHandler = createDataHandler; 130 | -------------------------------------------------------------------------------- /test/client.js: -------------------------------------------------------------------------------- 1 | var jsonrpc = require('../lib/index'); 2 | var HttpTransport = jsonrpc.transports.client.http; 3 | var TcpTransport = jsonrpc.transports.client.tcp; 4 | var JSONRPCclient = jsonrpc.client; 5 | var shared = require('../lib/transports/shared/tcp'); 6 | var l = require('lambda-js'); 7 | var http = require('http'); 8 | var net = require('net'); 9 | 10 | exports.loopbackHttp = function(test) { 11 | test.expect(1); 12 | var server = http.createServer(function(req, res) { 13 | var buffer = ''; 14 | req.setEncoding('utf8'); 15 | req.on('data', function(data) { 16 | buffer += data; 17 | }); 18 | req.on('end', function() { 19 | var json; 20 | try { 21 | json = JSON.parse(buffer); 22 | } catch(e) { 23 | } 24 | res.write(JSON.stringify({ 25 | id: json && json.id, 26 | result: json && json.params 27 | })); 28 | res.end(); 29 | }); 30 | }); 31 | server.listen(22222); 32 | var jsonRpcClient = new JSONRPCclient(new HttpTransport('localhost', 22222)); 33 | jsonRpcClient.register('foo'); 34 | jsonRpcClient.foo('bar', function(err, result) { 35 | test.equal('bar', result, 'Looped-back correctly'); 36 | server.close(function() { 37 | test.done(); 38 | }); 39 | }); 40 | }; 41 | 42 | exports.loopbackHttpWithCustomIdGenerator = function(test) { 43 | test.expect(2); 44 | var id = 2; 45 | var generator = function() { 46 | return id; 47 | }; 48 | var server = http.createServer(function(req, res) { 49 | var buffer = ''; 50 | req.setEncoding('utf8'); 51 | req.on('data', function(data) { 52 | buffer += data; 53 | }); 54 | 55 | req.on('end', function() { 56 | var json; 57 | try { 58 | json = JSON.parse(buffer); 59 | } 60 | catch(e) { 61 | } 62 | test.equal(json.id, id); 63 | res.write(JSON.stringify({ 64 | id : json && json.id, 65 | result: json && json.params 66 | })); 67 | res.end(); 68 | }); 69 | }); 70 | server.listen(22722); 71 | var options = { 72 | autoRegister: false, 73 | idGenerator: generator 74 | }; 75 | var jsonRpcClient = new JSONRPCclient(new HttpTransport('localhost', 22722), options); 76 | jsonRpcClient.register('foo'); 77 | jsonRpcClient.foo('bar', function(err, result) { 78 | test.equal('bar', result, 'Looped-back correctly'); 79 | server.close(function() { 80 | test.done(); 81 | }); 82 | }); 83 | }; 84 | 85 | exports.loopbackHttpWithInvalidIdGenerator = function(test) { 86 | test.expect(2); 87 | var server = http.createServer(function(req, res) { 88 | req.on('end', function() { 89 | res.end(); 90 | }); 91 | }); 92 | server.listen(22223); 93 | var generator = function() {}; 94 | var options = { 95 | idGenerator: generator 96 | }; 97 | var jsonRpcClient = new JSONRPCclient(new HttpTransport('localhost', 22223), options); 98 | jsonRpcClient.register('foo'); 99 | jsonRpcClient.foo('bar', function(err) { 100 | test.ok(!!err, 'error is thrown'); 101 | test.equals('Request id generator function should return an id', err.message); 102 | server.close(function() { 103 | test.done(); 104 | }); 105 | }); 106 | }; 107 | 108 | exports.failureTcp = function(test) { 109 | test.expect(2); 110 | var server = net.createServer(function(con) { 111 | var buffers = []; 112 | var bufferLen = 0; 113 | var messageLen = 0; 114 | con.on('data', function(data) { 115 | buffers.push(data); 116 | bufferLen += data.length; 117 | if(messageLen === 0) messageLen = shared.getMessageLen(buffers); 118 | var res, obj; 119 | if(bufferLen - 4 >= messageLen) { 120 | while (messageLen && bufferLen - 4 >= messageLen && (res = shared.parseBuffer(buffers, messageLen))) { 121 | buffers = res[0]; 122 | obj = res[1]; 123 | con.write(shared.formatMessage({ 124 | id: obj && obj.id, 125 | error: "I have no idea what I'm doing." 126 | })); 127 | bufferLen = buffers.map(l('buffer', 'buffer.length')).reduce(l('fullLen, currLen', 'fullLen + currLen'), 0); 128 | messageLen = shared.getMessageLen(buffers); 129 | } 130 | } 131 | }); 132 | }); 133 | server.listen(11111); 134 | var jsonRpcClient = new JSONRPCclient(new TcpTransport({ host: 'localhost', port: 11111 })); 135 | jsonRpcClient.register('foo'); 136 | jsonRpcClient.foo('bar', function(err) { 137 | test.ok(!!err, 'error exists'); 138 | test.equal("I have no idea what I'm doing.", err.message, 'The error message was received correctly'); 139 | jsonRpcClient.transport.con.end(); 140 | jsonRpcClient.shutdown(function() { 141 | server.close(test.done.bind(test)); 142 | }); 143 | }); 144 | }; 145 | 146 | exports.invalidHttp = function(test) { 147 | test.expect(1); 148 | var server = http.createServer(function(req, res) { 149 | res.end('Hahahaha'); 150 | }); 151 | server.listen(23232); 152 | var jsonRpcClient = new JSONRPCclient(new HttpTransport('localhost', 23232)); 153 | jsonRpcClient.register('foo'); 154 | jsonRpcClient.foo('bar', function(err) { 155 | test.ok(err instanceof Error, 'received the error response from the client library'); 156 | server.close(test.done.bind(test)); 157 | }); 158 | }; 159 | 160 | exports.serverDownHttp = function(test) { 161 | test.expect(1); 162 | var jsonRpcClient = new JSONRPCclient(new HttpTransport('localhost', 23232)); 163 | jsonRpcClient.register('foo'); 164 | jsonRpcClient.foo('bar', function(err) { 165 | test.ok(err instanceof Error, 'received the error response from the client library'); 166 | test.done(); 167 | }); 168 | }; 169 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | var errorCode = require('./errorcode'); 2 | 3 | // ## The JSONRPC constructor 4 | // Each JSON-RPC object is tied to a *scope*, an object containing functions to 5 | // call. If not passed an explicit scope, *Node.js*' *root* scope will be used. 6 | // Also, unlike the Javascript running in web browsers, functions not explicitly 7 | // assigned to a scope are attached to the anonymous scope block only and cannot 8 | // be accessed even from the *root* scope. 9 | function JSONRPC(transports, scope) { 10 | this.transports = Array.isArray(transports) ? transports : [transports]; 11 | this.transport = this.transports[0]; // For compatibility with existing code 12 | this.scope = scope; 13 | 14 | // The actual object initialization occurs here. If the *scope* is not 15 | // defined, the *root* scope is used, and then the object is returned to 16 | // the developer. 17 | if(!scope || typeof(scope) !== "object") { 18 | /* global root: false */ 19 | scope = root; 20 | } 21 | // ### The *rpc.methodList* method 22 | // is a JSON-RPC extension that returns a list of all methods in the scope 23 | scope['rpc.methodList'] = function(callback) { 24 | callback(null, Object.keys(scope)); 25 | }; 26 | 27 | for(var i = 0; i < this.transports.length; i++) { 28 | this.transports[i].handler = this.handleJSON.bind(this); 29 | } 30 | 31 | return this; 32 | } 33 | 34 | // ### The *handleJSON* function 35 | // makes up the majority of the JSON-RPC server logic, handling the requests 36 | // from clients, passing the call to the correct function, catching any 37 | // errors the function may throw, and calling the function to return the 38 | // results back to the client. 39 | JSONRPC.prototype.handleJSON = function handleJSON(data, callback) { 40 | function setDefaultProperty(data) { 41 | var outObj = {}; 42 | if (data.hasOwnProperty('jsonrpc')) { 43 | outObj.jsonrpc = data.jsonrpc; 44 | } 45 | outObj.id = data.hasOwnProperty('id') ? data.id : undefined; 46 | return outObj; 47 | } 48 | function batchCallback(response, size) { 49 | return function cb(obj) { 50 | response.push(obj); 51 | if (response.length === size) { 52 | callback(response); 53 | } 54 | }; 55 | } 56 | 57 | if(Array.isArray(data)) { 58 | var response = []; 59 | var len = data.length; 60 | for (var i = 0; i < len; ++i) { 61 | var x = data[i]; 62 | this.handleJSON(x, batchCallback(response, len)); 63 | } 64 | } else if(data instanceof Object) { 65 | if(data.method) { 66 | // If the method is defined in the scope and is not marked as a 67 | // blocking function, then a callback must be defined for 68 | // the function. The callback takes two parameters: the 69 | // *result* of the function, and an *error* message. 70 | var arglen = data.params && data.params instanceof Array ? data.params.length : data.params ? 1 : 0; 71 | if(this.scope[data.method] && !(this.scope[data.method].length === arglen || this.scope[data.method].blocking)) { 72 | var next = function(error, result) { 73 | var outObj = setDefaultProperty(data); 74 | if(error) { 75 | if(error instanceof Error) { 76 | outObj.error = {}; 77 | var keys = Object.keys(error); 78 | for (var i = 0; i < keys.length; ++i) { 79 | var key = keys[i]; 80 | outObj.error[key] = error[key]; 81 | } 82 | outObj.error.code = errorCode.internalError; 83 | outObj.error.message = error.message; 84 | } else { 85 | outObj.error = error; 86 | } 87 | } else { 88 | outObj.result = result; 89 | } 90 | callback(outObj); 91 | }; 92 | 93 | if (!(data.params instanceof Array)) { 94 | data.params = data.params ? [data.params] : []; 95 | } 96 | 97 | var paramsMissing = this.scope[data.method].length - (arglen + 1); 98 | 99 | for (var j = 0; j < paramsMissing; j++) { 100 | data.params.push(undefined); 101 | } 102 | 103 | data.params.push(next); 104 | 105 | // This *try-catch* block is for catching errors in an asynchronous server method. 106 | // Since the async methods are supposed to return an error in the callback, this 107 | // is assumed to be an unintended mistake, so we catch the error, send a JSON-RPC 108 | // error response, and then re-throw the error so the server code gets the error 109 | // and can deal with it appropriately (which could be "crash because this isn't 110 | // expected to happen"). 111 | try { 112 | this.scope[data.method].apply(this.scope, data.params); 113 | } catch(e) { 114 | var outErr = {}; 115 | outErr.code = errorCode.internalError; 116 | outErr.message = e.message ? e.message : ""; 117 | outErr.stack = e.stack ? e.stack : ""; 118 | var outObj = setDefaultProperty(data); 119 | outObj.error = outErr; 120 | callback(outObj); 121 | throw e; 122 | } 123 | } else { 124 | var errObj1 = setDefaultProperty(data); 125 | errObj1.error = { 126 | code: errorCode.methodNotFound, 127 | message: "Requested method does not exist." 128 | }; 129 | callback(errObj1); 130 | } 131 | } else { 132 | var errObj2 = setDefaultProperty(data); 133 | errObj2.error = { 134 | code: errorCode.invalidRequest, 135 | message: "Did not receive valid JSON-RPC data." 136 | }; 137 | callback(errObj2); 138 | } 139 | } else { 140 | var errObj3 = setDefaultProperty(data); 141 | errObj3.error = { 142 | code: errorCode.parseError, 143 | message: "Did not receive valid JSON-RPC data." 144 | }; 145 | callback(errObj3); 146 | } 147 | }; 148 | 149 | // ### The *register* function 150 | // allows one to attach a function to the current scope after the scope has 151 | // been attached to the JSON-RPC server, for similar possible shenanigans as 152 | // described above. This method in particular, though, by attaching new 153 | // functions to the current scope, could be used for caching purposes or 154 | // self-modifying code that rewrites its own definition. 155 | JSONRPC.prototype.register = function(methodName, method) { 156 | if(!this.scope || typeof(this.scope) !== "object") { 157 | this.scope = {}; 158 | } 159 | this.scope[methodName] = method; 160 | }; 161 | 162 | // Make a ``blocking`` helper method to async-ify them 163 | JSONRPC.prototype.blocking = function blocking(func) { 164 | return function blocked() { 165 | var args = Array.prototype.slice.call(arguments, 0, arguments.length-1); 166 | var callback = arguments[arguments.length-1]; 167 | var err, res; 168 | try { 169 | res = func.apply(this, args); 170 | } catch(e) { 171 | err = e; // Doesn't throw because it's the only way to return an error with sync methods 172 | } 173 | callback(err, res); 174 | }; 175 | }; 176 | 177 | // Cleanly shut down the JSONRPC server 178 | JSONRPC.prototype.shutdown = function shutdown(done) { 179 | var closed = 0; 180 | var transports = this.transports; 181 | transports.forEach(function(transport) { 182 | transport.shutdown(function() { 183 | closed++; 184 | if(closed === transports.length && typeof done === 'function') done(); 185 | }); 186 | }); 187 | }; 188 | 189 | // Export the server constructor 190 | module.exports = JSONRPC; 191 | -------------------------------------------------------------------------------- /lib/transports/client/tcp.js: -------------------------------------------------------------------------------- 1 | var net = require('net'); 2 | var util = require('util'); 3 | var EventEmitter = require('events').EventEmitter; 4 | var shared = require('../shared/tcp'); 5 | 6 | // Client Transport's data handling function, bound to the TcpTransport 7 | // instance when attached to the data event handler 8 | function onDataCallback(message) { 9 | if(message && this.requests[message.id]) { 10 | var request = this.requests[message.id]; 11 | delete this.requests[message.id]; 12 | request.callback(message); 13 | } 14 | } 15 | 16 | var onClose; 17 | 18 | // At the interval specified by the user, attempt to reestablish the 19 | // connection 20 | var connect = function connect(toReconnect) { 21 | this.logger('onClose.reconnect - old con is: ' + (this.con && this.con.random)); 22 | var oldPort = this.con && this.con.random; 23 | // Set the connection reference to the new connection 24 | if (this.con) { 25 | this.logger('ERRORRORO connection should not be set'); 26 | this.con.destroy(); 27 | this.con = null; 28 | } 29 | this.con = net.connect(this.tcpConfig, function() { 30 | this.logger('net.connect.callback - new con: ' + (this.con && this.con.random) + '. old con: ' + oldPort); 31 | // Clear the reconnect interval if successfully reconnected 32 | if(this.reconnectInterval) { 33 | clearInterval(this.reconnectInterval); 34 | delete this.reconnectInterval; 35 | } 36 | if(this.stopBufferingAfter) { 37 | clearTimeout(this.stopBufferingTimeout); 38 | delete this.stopBufferingTimeout; 39 | } 40 | if (this._request) { 41 | this.request = this._request; 42 | delete this._request; 43 | } 44 | this.retry = 0; 45 | this.reconnect++; 46 | if(toReconnect) { 47 | // Get the list of all pending requests, place them in a private 48 | // variable, and reset the requests object 49 | var oldReqs = this.requests; 50 | this.requests = {}; 51 | // Then requeue the old requests, but only after a run through the 52 | // implicit event loop. Why? Because ``this.con`` won't be the 53 | // correct connection object until *after* this callback function 54 | // is called. 55 | process.nextTick(function() { 56 | Object.keys(oldReqs).forEach(function(key) { 57 | this.request(oldReqs[key].body, oldReqs[key].callback); 58 | }.bind(this)); 59 | }.bind(this)); 60 | } 61 | }.bind(this)); 62 | this.con.random = Math.random(); 63 | this.logger('new.con.created - con: ' + (this.con && this.con.random)); 64 | // Reconnect the data and end event handlers to the new connection object 65 | this.con.on('data', shared.createDataHandler(this, onDataCallback.bind(this))); 66 | this.con.on('end', function() { 67 | this.logger('con.end - ' + (this.con && this.con.random)); 68 | this.con.destroy(); 69 | }.bind(this)); 70 | this.con.on('error', function() { 71 | this.logger('con.error - ' + (this.con && this.con.random)); 72 | this.con.destroy(); 73 | }.bind(this)); 74 | this.con.on('close', function () { 75 | this.logger('con.close - ' + (this.con && this.con.random)); 76 | if(this.con) { 77 | this.con.destroy(); 78 | this.con = null; 79 | onClose.call(this); 80 | } 81 | }.bind(this)); 82 | }; 83 | 84 | // The handler for a connection close. Will try to reconnect if configured 85 | // to do so and it hasn't tried "too much," otherwise mark the connection 86 | // dead. 87 | onClose = function onClose() { 88 | this.logger('onClose ' + (this.con && this.con.random)); 89 | // Attempting to reconnect 90 | if(this.retries && this.retry < this.retries && this.reconnect < this.reconnects) { 91 | this.logger('onClose if (retries) - old con is: ' + (this.con && this.con.random)); 92 | this.emit('retry'); 93 | // When reconnecting, all previous buffered data is invalid, so wipe 94 | // it out, and then increment the retry flag 95 | this.retry++; 96 | // If this is the first try, attempt to reconnect immediately 97 | if(this.retry === 1) { 98 | this.logger('call onClose.reconnect for retry === 1 - old con: ' + (this.con && this.con.random)); 99 | connect.call(this, true); 100 | } 101 | if(typeof(this.stopBufferingAfter) === 'number' && this.stopBufferingAfter !== 0 && !this.stopBufferingTimeout) { 102 | this.stopBufferingTimeout = setTimeout(this.stopBuffering.bind(this), this.stopBufferingAfter); 103 | } 104 | if(!this.reconnectInterval) { 105 | this.reconnectInterval = setInterval(function() { 106 | this.logger('call onClose.reconnect from reconnectInterval - old con: ' + (this.con && this.con.random)); 107 | connect.call(this, true); 108 | }.bind(this), this.retryInterval); 109 | } 110 | } else { 111 | // Too many tries, or not allowed to retry, mark the connection as dead 112 | this.emit('end'); 113 | this.con = undefined; 114 | } 115 | }; 116 | 117 | // The Client TcpTransport constructor function 118 | function TcpTransport(tcpConfig, config) { 119 | // Shim to support old-style call 120 | if (typeof tcpConfig === 'string') { 121 | tcpConfig = { 122 | host: arguments[0], 123 | port: arguments[1] 124 | }; 125 | config = arguments[2]; 126 | } 127 | // Initialize the Node EventEmitter on this 128 | EventEmitter.call(this); 129 | // Attach the config object (or an empty object if not defined, as well 130 | // as the server and port 131 | config = config || {}; 132 | this.retries = config.retries || Infinity; 133 | this.reconnects = config.reconnects || Infinity; 134 | this.reconnectClearInterval = config.reconnectClearInterval || 0; 135 | this.retry = 0; 136 | this.reconnect = -1; 137 | this.retryInterval = config.retryInterval || 250; 138 | this.stopBufferingAfter = config.stopBufferingAfter || 0; 139 | this.stopBufferingTimeout = null; 140 | this.reconnectInterval = null; 141 | this.logger = config.logger || function() {}; 142 | 143 | // Set up the server connection and request-handling properties 144 | this.tcpConfig = tcpConfig; 145 | this.requests = {}; 146 | 147 | // Set up the garbage collector for requests that never receive a response 148 | // and build the buffer 149 | this.timeout = config.timeout || 30*1000; 150 | this.sweepIntervalMs = config.sweepIntervalMs || 1*1000; 151 | this.sweepInterval = setInterval(this.sweep.bind(this), this.sweepIntervalMs); 152 | 153 | if (this.reconnectClearInterval > 0 && this.reconnectClearInterval !== Infinity) { 154 | this.reconnectClearTimer = setInterval(this.clearReconnects.bind(this), 155 | this.reconnectClearInterval); 156 | } 157 | 158 | connect.call(this, false); 159 | 160 | return this; 161 | } 162 | 163 | // Attach the EventEmitter prototype as the TcpTransport's prototype's prototype 164 | util.inherits(TcpTransport, EventEmitter); 165 | 166 | TcpTransport.prototype.stopBuffering = function stopBuffering() { 167 | this.logger('Stopping the buffering of requests on ' + (this.con && this.con.random)); 168 | this._request = this.request; 169 | this.request = function fakeRequest(body, callback) { 170 | callback({ error: 'Connection Unavailable' }); 171 | }; 172 | }; 173 | 174 | // The request logic is relatively straightforward, given the request 175 | // body and callback function, register the request with the requests 176 | // object, then if there is a valid connection at the moment, send the 177 | // request to the server with a null terminator attached. This ordering 178 | // guarantees that requests called during a connection issue won't be 179 | // lost while a connection is re-established. 180 | TcpTransport.prototype.request = function request(body, callback) { 181 | this.requests[body.id] = { 182 | callback: callback, 183 | body: body, 184 | timestamp: Date.now() 185 | }; 186 | if(this.con) this.con.write(shared.formatMessage(body, this)); 187 | }; 188 | 189 | // The sweep function looks at the timestamps for each request, and any 190 | // request that is longer lived than the timeout (default 2 min) will be 191 | // culled and assumed lost. 192 | TcpTransport.prototype.sweep = function sweep() { 193 | var now = new Date().getTime(); 194 | var cannedRequests = {}; 195 | for(var key in this.requests) { 196 | if(this.requests[key].timestamp && this.requests[key].timestamp + this.timeout < now) { 197 | this.requests[key].callback({ error: 'Request Timed Out' }); 198 | cannedRequests[key] = this.requests[key]; 199 | delete this.requests[key]; 200 | } 201 | } 202 | this.emit('sweep', cannedRequests); 203 | }; 204 | 205 | // The clearReconnects function periodically resets the internal counter 206 | // of how many times we have re-established a connection to the server. 207 | // If the connection is currently dead (undefined), it attempts a reconnect. 208 | TcpTransport.prototype.clearReconnects = function clearReconnects() { 209 | this.reconnect = -1; 210 | if (this.con === undefined) { 211 | connect.call(this, true); 212 | } 213 | }; 214 | 215 | // When shutting down the client connection, the sweep is turned off, the 216 | // requests are removed, the number of allowed retries is set to zero, the 217 | // connection is ended, and a callback, if any, is called. 218 | TcpTransport.prototype.shutdown = function shutdown(done) { 219 | clearInterval(this.sweepInterval); 220 | if(this.reconnectInterval) clearInterval(this.reconnectInterval); 221 | if(this.reconnectClearTimer) clearInterval(this.reconnectClearTimer); 222 | this.requests = {}; 223 | this.retries = 0; 224 | if(this.con) this.con.destroy(); 225 | this.emit('shutdown'); 226 | if(done instanceof Function) done(); 227 | }; 228 | 229 | // Export the client TcpTransport 230 | module.exports = TcpTransport; 231 | -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | var jsonrpc = require('../lib/index'); 2 | var HttpTransport = jsonrpc.transports.server.http; 3 | var TcpTransport = jsonrpc.transports.server.tcp; 4 | var shared = require('../lib/transports/shared/tcp'); 5 | var JSONRPCserver = jsonrpc.server; 6 | var ErrorCode = jsonrpc.errorcode; 7 | var http = require('http'); 8 | var net = require('net'); 9 | 10 | exports.loopbackHttp = function(test) { 11 | test.expect(4); 12 | var jsonRpcServer = new JSONRPCserver(new HttpTransport(65432), { 13 | loopback: function(arg1, callback) { 14 | callback(null, arg1); 15 | } 16 | }); 17 | var testJSON = JSON.stringify({ 18 | id: 1, 19 | method: 'loopback', 20 | params: [{ hello: 'world' }] 21 | }); 22 | var req = http.request({ 23 | hostname: 'localhost', 24 | port: 65432, 25 | path: '/', 26 | method: 'POST' 27 | }, function(res) { 28 | res.setEncoding('utf8'); 29 | var resultString = ''; 30 | res.on('data', function(data) { 31 | resultString += data; 32 | }); 33 | res.on('end', function() { 34 | test.equal(200, res.statusCode, 'The http transport provided an OK status code'); 35 | var resultObj; 36 | try { 37 | resultObj = JSON.parse(resultString); 38 | } catch(e) { 39 | // Do nothing, test will fail 40 | } 41 | test.equal(resultObj.id, 1, 'The JSON-RPC server sent back the same ID'); 42 | test.equal(resultObj.result.hello, 'world', 'The loopback method worked as expected'); 43 | test.ok(resultObj.error === undefined, 'The error property is not defined on success'); 44 | test.done(); 45 | jsonRpcServer.transport.server.close(); 46 | }); 47 | }); 48 | req.write(testJSON); 49 | req.end(); 50 | }; 51 | 52 | exports.loopbackHttp = function(test) { 53 | test.expect(5); 54 | var jsonRpcServer = new JSONRPCserver(new HttpTransport(65432), { 55 | loopback: function(arg1, callback) { 56 | callback(null, arg1); 57 | } 58 | }); 59 | var testJSON = JSON.stringify({ 60 | jsonrpc: "2.0", 61 | id: 1, 62 | method: 'loopback', 63 | params: [{ hello: 'world' }] 64 | }); 65 | var req = http.request({ 66 | hostname: 'localhost', 67 | port: 65432, 68 | path: '/', 69 | method: 'POST' 70 | }, function(res) { 71 | res.setEncoding('utf8'); 72 | var resultString = ''; 73 | res.on('data', function(data) { 74 | resultString += data; 75 | }); 76 | res.on('end', function() { 77 | test.equal(200, res.statusCode, 'The http transport provided an OK status code'); 78 | var resultObj; 79 | try { 80 | resultObj = JSON.parse(resultString); 81 | } catch(e) { 82 | // Do nothing, test will fail 83 | } 84 | test.equal(resultObj.jsonrpc, "2.0", 'The JSON-RPC server sent back the same jsonrpc version'); 85 | test.equal(resultObj.id, 1, 'The JSON-RPC server sent back the same ID'); 86 | test.equal(resultObj.result.hello, 'world', 'The loopback method worked as expected'); 87 | test.ok(resultObj.error === undefined, 'The error property is not defined on success'); 88 | test.done(); 89 | jsonRpcServer.transport.server.close(); 90 | }); 91 | }); 92 | req.write(testJSON); 93 | req.end(); 94 | }; 95 | 96 | exports.loopbackHttpBatch = function(test) { 97 | test.expect(11); 98 | var jsonRpcServer = new JSONRPCserver(new HttpTransport(65123), { 99 | loopback: function(arg1, callback) { 100 | callback(null, arg1); 101 | } 102 | }); 103 | var testJSON = JSON.stringify([ 104 | { 105 | id: 1, 106 | method: 'loopback', 107 | params: [{ hello: 'world' }] 108 | }, 109 | { 110 | id: 2, 111 | method: 'noexists', 112 | params: [{ hello: 'world' }] 113 | }, 114 | { 115 | id: 3, 116 | method: 'loopback', 117 | params: [{ hello: 'batch world' }] 118 | } 119 | ]); 120 | var req = http.request({ 121 | hostname: 'localhost', 122 | port: 65123, 123 | path: '/', 124 | method: 'POST' 125 | }, function(res) { 126 | res.setEncoding('utf8'); 127 | var resultString = ''; 128 | res.on('data', function(data) { 129 | resultString += data; 130 | }); 131 | res.on('end', function() { 132 | test.equal(200, res.statusCode, 'The http transport provided an OK status code'); 133 | var resultObj; 134 | try { 135 | resultObj = JSON.parse(resultString); 136 | } catch(e) { 137 | // Do nothing, test will fail 138 | } 139 | test.equal(Array.isArray(resultObj), true, 'The batch response is array'); 140 | var obj; 141 | { 142 | obj = resultObj[0]; 143 | test.equal(obj.id, 1, 'The JSON-RPC server sent back the same ID'); 144 | test.equal(obj.result.hello, 'world', 'The loopback method worked as expected'); 145 | test.ok(resultObj.error === undefined, 'The error property is not defined on success'); 146 | } 147 | { 148 | obj = resultObj[1]; 149 | test.equal(obj.id, 2, 'The JSON-RPC server sent back the same ID'); 150 | test.equal(obj.error.code, -32601, 'The method is not found'); 151 | test.ok(obj.result === undefined, 'The result property is not defined on error response'); 152 | } 153 | { 154 | obj = resultObj[2]; 155 | test.equal(obj.id, 3, 'The JSON-RPC server sent back the same ID'); 156 | test.equal(obj.result.hello, 'batch world', 'The loopback method worked as expected'); 157 | test.ok(resultObj.error === undefined, 'The error property is not defined on success'); 158 | } 159 | test.done(); 160 | jsonRpcServer.transport.server.close(); 161 | }); 162 | }); 163 | req.write(testJSON); 164 | req.end(); 165 | }; 166 | 167 | exports.failureTcp = function(test) { 168 | test.expect(4); 169 | var jsonRpcServer = new JSONRPCserver(new TcpTransport(64863), { 170 | failure: function(arg1, callback) { 171 | callback(new Error("I have no idea what I'm doing")); 172 | } 173 | }); 174 | var con = net.connect({ 175 | port: 64863, 176 | host: 'localhost' 177 | }, function() { 178 | con.write(shared.formatMessage({ 179 | id: 1, 180 | method: 'failure', 181 | params: [{ hello: 'world' }] 182 | })); 183 | }); 184 | var buffers = [], bufferLen = 0, messageLen = 0; 185 | con.on('data', function(data) { 186 | buffers.push(data); 187 | bufferLen += data.length; 188 | if(messageLen === 0) messageLen = shared.getMessageLen(buffers); 189 | if(bufferLen === messageLen + 4) con.end(); 190 | }); 191 | con.on('end', function() { 192 | try { 193 | var res = shared.parseBuffer(buffers, messageLen); 194 | test.equal(res[1].id, 1, 'The JSON-RPC server sent back the same ID'); 195 | test.equal(res[1].error.code, ErrorCode.internalError); 196 | test.equal(res[1].error.message, "I have no idea what I'm doing", 'Returns the error as an error'); 197 | test.ok(res[1].result === undefined, 'The result property is not defined on error response'); 198 | } catch(e) { 199 | // Do nothing 200 | } 201 | jsonRpcServer.transport.server.close(); 202 | test.done(); 203 | }); 204 | }; 205 | 206 | exports.nonexistentMethod = function(test) { 207 | test.expect(4); 208 | var jsonRpcServer = new JSONRPCserver(new HttpTransport(65111), {}); 209 | var testJSON = JSON.stringify({ 210 | id: 25, 211 | method: 'nonexistent', 212 | params: [] 213 | }); 214 | var req = http.request({ 215 | hostname: 'localhost', 216 | port: 65111, 217 | path: '/', 218 | method: 'POST' 219 | }, function(res) { 220 | res.setEncoding('utf8'); 221 | var resultString = ''; 222 | res.on('data', function(data) { 223 | resultString += data; 224 | }); 225 | res.on('end', function() { 226 | var resultObj; 227 | try { 228 | resultObj = JSON.parse(resultString); 229 | } catch(e) { 230 | // Do nothing, test will fail 231 | } 232 | test.equal(resultObj.id, 25, 'The JSON-RPC server sent back the correct ID'); 233 | test.equal(resultObj.error.code, ErrorCode.methodNotFound); 234 | test.equal(resultObj.error.message, 'Requested method does not exist.', 'The JSON-RPC server returned the expected error message.'); 235 | test.ok(resultObj.result === undefined, 'The result property is not defined on error response'); 236 | jsonRpcServer.shutdown(test.done.bind(test)); 237 | }); 238 | }); 239 | req.write(testJSON); 240 | req.end(); 241 | }; 242 | 243 | exports.noncompliantJSON = function(test) { 244 | test.expect(4); 245 | var jsonRpcServer = new JSONRPCserver(new HttpTransport(64123), {}); 246 | var testJSON = JSON.stringify({ hello: 'world' }); 247 | var req = http.request({ 248 | hostname: 'localhost', 249 | port: 64123, 250 | path: '/', 251 | method: 'POST' 252 | }, function(res) { 253 | res.setEncoding('utf8'); 254 | var resultString = ''; 255 | res.on('data', function(data) { 256 | resultString += data; 257 | }); 258 | res.on('end', function() { 259 | var resultObj; 260 | try { 261 | resultObj = JSON.parse(resultString); 262 | } catch(e) { 263 | // Do nothing, test will fail 264 | } 265 | test.equal(resultObj.id, null, 'The JSON-RPC server sent back the correct ID'); 266 | test.equal(resultObj.error.code, ErrorCode.invalidRequest); 267 | test.equal(resultObj.error.message, 'Did not receive valid JSON-RPC data.', 'The JSON-RPC server returned the expected error message.'); 268 | test.ok(resultObj.result === undefined, 'The result property is not defined on error response'); 269 | jsonRpcServer.shutdown(test.done.bind(test)); 270 | }); 271 | }); 272 | req.write(testJSON); 273 | req.end(); 274 | }; 275 | -------------------------------------------------------------------------------- /test/client-tcp.js: -------------------------------------------------------------------------------- 1 | var jsonrpc = require('../lib/index'); 2 | var TcpTransport = jsonrpc.transports.client.tcp; 3 | var shared = require('../lib/transports/shared/tcp'); 4 | var net = require('net'); 5 | var async = require('async'); 6 | 7 | var Server = jsonrpc.server; 8 | var ServerTcp = jsonrpc.transports.server.tcp; 9 | var Client = jsonrpc.client; 10 | var ClientTcp = jsonrpc.transports.client.tcp; 11 | 12 | exports.loopback = function(test) { 13 | test.expect(1); 14 | var server = net.createServer(function(con) { 15 | var buffer = new Buffer(''); 16 | var messageLen = 0; 17 | con.on('data', function(data) { 18 | buffer = Buffer.concat([buffer, data]); 19 | if(messageLen === 0) messageLen = shared.getMessageLen([data]); 20 | if(buffer.length === messageLen + 4) { 21 | con.write(buffer); 22 | con.end(); 23 | } 24 | }); 25 | }); 26 | server.listen(23456); 27 | var tcpTransport = new TcpTransport({ host: 'localhost', port: 23456 }, { 28 | logger: console.log 29 | }); 30 | tcpTransport.request('foo', function(result) { 31 | test.equal('foo', result, 'loopback worked correctly'); 32 | tcpTransport.shutdown(function() { 33 | server.close(test.done.bind(test)); 34 | }); 35 | }); 36 | }; 37 | 38 | exports.sweep = function(test) { 39 | test.expect(2); 40 | var server = net.createServer(function(con) { 41 | var buffer = new Buffer(''); 42 | var messageLen = 0; 43 | con.on('data', function(data) { 44 | buffer = Buffer.concat([buffer, data]); 45 | if(messageLen === 0) messageLen = shared.getMessageLen([data]); 46 | if(buffer.length === messageLen + 4) { 47 | setTimeout(function() { 48 | con.write(buffer); 49 | con.end(); 50 | }, 400); 51 | } 52 | }); 53 | }); 54 | server.listen(23457); 55 | var tcpTransport = new TcpTransport({ host: 'localhost', port: 23457 }, { timeout: 100 }); 56 | tcpTransport.request('foo', function(err, result) { 57 | test.ok(!!err, 'should receive a timeout error'); 58 | if(result) test.ok(false, 'this should never run'); 59 | }); 60 | setTimeout(function() { 61 | test.ok(true, 'this should always run'); 62 | tcpTransport.shutdown(function() { 63 | server.close(test.done.bind(test)); 64 | }); 65 | }, 1000); 66 | }; 67 | 68 | exports.glitchedConnection = function(test) { 69 | test.expect(3); 70 | var con; 71 | var serverFunc = function(c) { 72 | con = c; 73 | var buffer = new Buffer(''); 74 | var messageLen = 0; 75 | c.on('data', function(data) { 76 | buffer = Buffer.concat([buffer, data]); 77 | if(messageLen === 0) messageLen = shared.getMessageLen([data]); 78 | if(buffer.length === messageLen + 4) { 79 | setTimeout(function() { 80 | if(con) { 81 | con.write(buffer); 82 | con.end(); 83 | } 84 | }, 400); 85 | } 86 | }); 87 | c.on('end', function() { 88 | con = undefined; 89 | }); 90 | }; 91 | var server = net.createServer(serverFunc); 92 | server.listen(23458); 93 | var tcpTransport = new TcpTransport({ host: 'localhost', port: 23458 }); 94 | tcpTransport.request({'id': 'foo'}, function(result) { 95 | test.equal(JSON.stringify({'id': 'foo'}), JSON.stringify(result), 'eventually received the response'); 96 | tcpTransport.shutdown(function() { 97 | server.close(test.done.bind(test)); 98 | }); 99 | }); 100 | 101 | // Kill the original server to simulate an error 102 | setTimeout(function() { 103 | test.ok(true, 'server was killed'); 104 | con.destroy(); 105 | con = undefined; 106 | server.close(); 107 | }, 50); 108 | 109 | // Start a new server to reconnect to 110 | setTimeout(function() { 111 | test.ok(true, 'new server created to actually handle the request'); 112 | server = net.createServer(serverFunc); 113 | server.listen(23458); 114 | }, 100); 115 | }; 116 | 117 | exports.stopBuffering = function(test) { 118 | test.expect(6); 119 | var con, server; 120 | // Create a client pointed to nowhere, telling it to stop trying requests after a while 121 | // (but continue attempting to connect to the server) 122 | var tcpTransport = new TcpTransport({ host: 'localhost', port: 23459 }, { 123 | timeout: 2*1000, 124 | stopBufferingAfter: 5*1000 125 | }); 126 | // Early messages will be attempted and eventually time out 127 | tcpTransport.request({id: 'foo'}, function(result) { 128 | test.ok(!!result.error, "Couldn't connect to the (nonexistent) server"); 129 | test.equal(result.error, 'Request Timed Out', 'time out error message received'); 130 | }); 131 | // Later messages will be immediately killed 132 | setTimeout(function() { 133 | tcpTransport.request({id: 'foo'}, function(result) { 134 | test.ok(!!result.error, "Still can't connect to the nonexistent server"); 135 | test.equal(result.error, 'Connection Unavailable', 'immediately blocked by the maximum timeout time for the server'); 136 | var serverFunc = function(c) { 137 | con = c; 138 | var buffer = new Buffer(''); 139 | var messageLen = 0; 140 | c.on('data', function(data) { 141 | buffer = Buffer.concat([buffer, data]); 142 | if(messageLen === 0) messageLen = shared.getMessageLen([data]); 143 | if(buffer.length === messageLen + 4) { 144 | if(con) { 145 | con.write(buffer); 146 | con.end(); 147 | } 148 | } 149 | }); 150 | c.on('end', function() { 151 | con = undefined; 152 | }); 153 | }; 154 | server = net.createServer(serverFunc); 155 | server.listen(23459); 156 | }); 157 | }, 6*1000); 158 | // After the server is started, messages will go through as expected 159 | setTimeout(function() { 160 | tcpTransport.request({id: 'foo'}, function(result) { 161 | test.ok(result instanceof Object, 'got a result'); 162 | test.equal(result.id, 'foo', 'got the expected result'); 163 | tcpTransport.shutdown(function() { 164 | server.close(test.done.bind(test)); 165 | }); 166 | }); 167 | }, 8*1000); 168 | }; 169 | 170 | exports.dontStopBuffering = function(test) { 171 | test.expect(6); 172 | // This test tests a modification of the above test, 173 | // if its told to stop buffering after a period of time of 174 | // being disconnected, but then reconnects *before* that period 175 | // the stopBuffering code shouldn't interfere with regular requests 176 | var server; 177 | var tcpTransport = new TcpTransport({ host: 'localhost', port: 23460 }, { 178 | timeout: 2*1000, 179 | stopBufferingAfter: 8*1000 180 | }); 181 | tcpTransport.request({id: 'foo'}, function(result) { 182 | test.ok(!!result.error); 183 | test.equal(result.error, 'Request Timed Out'); 184 | }); 185 | setTimeout(function() { 186 | tcpTransport.request({id: 'foo'}, function(result) { 187 | test.ok(result instanceof Object); 188 | test.equal(result.id, 'foo'); 189 | }); 190 | var serverFunc = function(c) { 191 | var buffer = new Buffer(''); 192 | var messageLen = 0; 193 | c.on('data', function(data) { 194 | buffer = Buffer.concat([buffer, data]); 195 | if(messageLen === 0) messageLen = shared.getMessageLen([data]); 196 | if(buffer.length === messageLen + 4) { 197 | c.write(buffer); 198 | c.end(); 199 | } 200 | }); 201 | }; 202 | server = net.createServer(serverFunc); 203 | server.listen(23460); 204 | }, 6*1000); 205 | setTimeout(function() { 206 | tcpTransport.request({id: 'foo'}, function(result) { 207 | test.ok(result instanceof Object); 208 | test.equal(result.id, 'foo'); 209 | tcpTransport.shutdown(function() { 210 | server.close(test.done.bind(test)); 211 | }); 212 | }); 213 | }, 10*1000); 214 | }; 215 | 216 | exports.reconnect = function(test) { 217 | test.expect(4); 218 | var tcpServer; 219 | 220 | var tcpClient; 221 | 222 | var sendRequest = function (done) { 223 | if (!tcpClient) { 224 | tcpClient = new Client(new ClientTcp({ host: 'localhost', port: 23458 }, { 225 | stopBufferingAfter: 30*1000, 226 | logger: console.log.bind(console) 227 | })); 228 | tcpClient.register('loopback'); 229 | } 230 | tcpClient.loopback({'id': 'foo'}, function(err, result) { 231 | console.log('got response'); 232 | console.dir(arguments); 233 | test.equal(JSON.stringify({'id': 'foo'}), JSON.stringify(result), 'received the response'); 234 | done(); 235 | }); 236 | }; 237 | 238 | var createServer = function (done) { 239 | console.log('create server'); 240 | tcpServer = new Server(new ServerTcp(23458), { 241 | loopback: function(arg, callback) { callback(null, arg); } 242 | }, done); 243 | tcpServer.transport.on('listening', done); 244 | }; 245 | var killServer = function (done) { 246 | console.log('kill server'); 247 | tcpServer.shutdown(done); 248 | }; 249 | 250 | async.series([ 251 | //sendRequest, 252 | function(done) { setTimeout(done, Math.random() * 5000); }, 253 | createServer, 254 | sendRequest, 255 | killServer, 256 | function(done) { setTimeout(done, Math.random() * 5000); }, 257 | createServer, 258 | sendRequest, 259 | killServer, 260 | function(done) { setTimeout(done, Math.random() * 5000); }, 261 | createServer, 262 | sendRequest, 263 | killServer, 264 | function(done) { setTimeout(done, Math.random() * 5000); }, 265 | createServer, 266 | sendRequest, 267 | killServer 268 | ], function (err) { 269 | console.dir(err); 270 | tcpClient.shutdown(); 271 | test.done(); 272 | }); 273 | 274 | }; 275 | 276 | exports.nullresponse = function(test) { 277 | test.expect(1); 278 | var server = net.createServer(function(con) { 279 | con.end(); 280 | }); 281 | server.listen(23456); 282 | var tcpTransport = new TcpTransport({ host: 'localhost', port: 23456 }, { 283 | logger: console.log, 284 | reconnects: 1 285 | }); 286 | setTimeout(function() { 287 | test.equal(tcpTransport.con, undefined, 'should not have a connection'); 288 | tcpTransport.shutdown(function() { 289 | server.close(test.done.bind(test)); 290 | }); 291 | }, 100); 292 | }; 293 | 294 | exports.reconnectclearing = function(test) { 295 | test.expect(2); 296 | var server = net.createServer(function(con) { 297 | con.end(); 298 | }); 299 | server.listen(23456); 300 | 301 | var tcpTransport = new TcpTransport({ host: 'localhost', port: 23456 }, { 302 | logger: console.log, 303 | reconnects: 1, 304 | reconnectClearInterval: 110 305 | }); 306 | 307 | setTimeout(function() { 308 | test.equal(tcpTransport.con, undefined, 'should not have a connection'); 309 | 310 | // Pretend the service came back to life 311 | server.close(function() { 312 | server = net.createServer(); 313 | server.listen(23456); 314 | 315 | setTimeout(function() { 316 | test.ok(tcpTransport.con, 'should have a connection'); 317 | 318 | tcpTransport.shutdown(function() { 319 | server.close(test.done.bind(test)); 320 | }); 321 | }, 100); 322 | }); 323 | }, 100); 324 | }; 325 | -------------------------------------------------------------------------------- /test/full-stack.js: -------------------------------------------------------------------------------- 1 | var jsonrpc = require('../lib/index'); 2 | var Client = jsonrpc.client; 3 | var Server = jsonrpc.server; 4 | var ClientHttp = jsonrpc.transports.client.http; 5 | var ClientTcp = jsonrpc.transports.client.tcp; 6 | var ClientChildProc = jsonrpc.transports.client.childProcess; 7 | var ServerHttp = jsonrpc.transports.server.http; 8 | var ServerTcp = jsonrpc.transports.server.tcp; 9 | var ServerMiddleware = jsonrpc.transports.server.middleware; 10 | var Loopback = jsonrpc.transports.shared.loopback; 11 | var express = require('express'); 12 | var http = require('http'); 13 | var net = require('net'); 14 | var childProcess = require('child_process'); 15 | var child = childProcess.fork(__dirname + '/child/child.js'); 16 | var childProcClient = new Client(new ClientChildProc(child)); 17 | childProcClient.register('loopback'); 18 | 19 | exports.loopbackHttp = function(test) { 20 | test.expect(1); 21 | var server = new Server(new ServerHttp(33333), { 22 | loopback: function(arg, callback) { callback(null, arg); } 23 | }); 24 | var client = new Client(new ClientHttp('localhost', 33333), {}, function(c) { 25 | c.loopback('foo', function(err, result) { 26 | test.equal('foo', result, 'loopback works as expected'); 27 | server.transport.server.close(function() { 28 | client.shutdown(); 29 | test.done(); 30 | }); 31 | }); 32 | }); 33 | }; 34 | 35 | exports.failureTcp = function(test) { 36 | test.expect(4); 37 | var server = new Server(new ServerTcp(44444), { 38 | failure: function(arg, callback) { callback(new Error("I have no idea what I'm doing.")); } 39 | }); 40 | var client = new Client(new ClientTcp('localhost', 44444), {}, function(c) { 41 | c.failure('foo', function(err) { 42 | test.ok(!!err, 'error exists'); 43 | test.equal(err.message, "I have no idea what I'm doing.", 'error message transmitted successfully.'); 44 | c.shutdown(function() { 45 | server.shutdown(test.done.bind(test)); 46 | }); 47 | }); 48 | }); 49 | client.transport.on('message', function() { 50 | test.ok('received a message'); // should happen twice 51 | }); 52 | }; 53 | 54 | exports.objectFailureTcp = function(test) { 55 | test.expect(4); 56 | var server = new Server(new ServerTcp(44444), { 57 | failure: function(arg, callback) { callback({ foo: "I have no idea what I'm doing." }); } 58 | }); 59 | var client = new Client(new ClientTcp('localhost', 44444), {}, function(c) { 60 | c.failure('foo', function(err) { 61 | test.ok(!!err, 'error exists'); 62 | test.equal(err.foo, "I have no idea what I'm doing.", 'error message transmitted successfully.'); 63 | c.shutdown(function() { 64 | server.shutdown(test.done.bind(test)); 65 | }); 66 | }); 67 | }); 68 | client.transport.on('message', function() { 69 | test.ok('received a message'); // should happen twice 70 | }); 71 | }; 72 | 73 | exports.sweepedRequest = function(test) { 74 | test.expect(2); 75 | var client = new Client(new ClientTcp('localhost', 44444)); 76 | client.register(['willNeverReachAServer']); 77 | client.willNeverReachAServer(function(err) { 78 | test.ok(err instanceof Error, 'received an error message'); 79 | test.equal(err.message, 'Request Timed Out', 'received the "sweep" error message'); 80 | client.shutdown(); 81 | test.done(); 82 | }); 83 | }; 84 | 85 | exports.loopbackLoopback = function(test) { 86 | test.expect(3); 87 | var loopback = new Loopback(); 88 | var server = new Server(loopback, { 89 | loopback: function(arg, callback) { callback(null, arg); }, 90 | failure: function(arg, callback) { callback(new Error("I have no idea what I'm doing.")); } 91 | }); 92 | var client = new Client(loopback); 93 | client.register(['loopback', 'failure']); 94 | client.loopback('foo', function(err, result) { 95 | test.equal('foo', result, 'loopback works as expected'); 96 | client.failure('foo', function(err) { 97 | test.ok(!!err, 'error exists'); 98 | test.equal(err.message, "I have no idea what I'm doing.", 'error message transmitted successfully.'); 99 | server.shutdown(); 100 | test.done(); 101 | }); 102 | }); 103 | }; 104 | 105 | exports.loopbackExpress = function(test) { 106 | test.expect(2); 107 | 108 | var app = express(); 109 | app.use(express.bodyParser()); 110 | app.get('/foo', function(req, res) { 111 | res.end('bar'); 112 | }); 113 | 114 | var server = new Server(new ServerMiddleware(), { 115 | loopback: function(arg, callback) { callback(null, arg); } 116 | }); 117 | app.use('/rpc', server.transport.middleware); 118 | 119 | //app.listen(55555); // Express 3.0 removed the ability to cleanly shutdown an express server 120 | // The following is copied from the definition of app.listen() 121 | var httpServer = http.createServer(app); 122 | httpServer.listen(55555); 123 | 124 | var client = new Client(new ClientHttp('localhost', 55555, { path: '/rpc' })); 125 | client.register('loopback'); 126 | 127 | http.get({ 128 | port: 55555, 129 | path: '/foo' 130 | }, function(res) { 131 | res.setEncoding('utf8'); 132 | var data = ''; 133 | res.on('data', function(chunk) { data += chunk; }); 134 | res.on('end', function() { 135 | test.equal(data, 'bar', 'regular http requests work'); 136 | client.loopback('bar', function(err, result) { 137 | test.equal(result, 'bar', 'JSON-RPC as a middleware works'); 138 | httpServer.close(test.done.bind(test)); 139 | }); 140 | }); 141 | }); 142 | }; 143 | 144 | exports.tcpServerEvents1 = function(test) { 145 | test.expect(10); 146 | var server = new Server(new ServerTcp(11111), { 147 | loopback: function(arg, callback) { callback(null, arg); } 148 | }); 149 | server.transport.on('connection', function(con) { 150 | test.ok(con instanceof net.Socket, 'incoming connection is a socket'); 151 | }); 152 | server.transport.on('closedConnection', function(con) { 153 | test.ok(con instanceof net.Socket, 'closing connection is a socket'); 154 | }); 155 | server.transport.on('listening', function() { 156 | test.ok(true, 'server started correctly'); 157 | }); 158 | server.transport.on('shutdown', function() { 159 | test.ok(true, 'the server was shutdown correctly'); 160 | test.done(); 161 | }); 162 | server.transport.on('message', function(obj, len) { 163 | test.ok(obj instanceof Object, 'object received'); 164 | test.ok(len > 0, 'message length provided'); 165 | }); 166 | server.transport.on('outMessage', function(obj, len) { 167 | test.ok(obj instanceof Object, 'object ready'); 168 | test.ok(len > 0, 'message length calcuated'); 169 | }); 170 | server.transport.on('retry', function() { 171 | // Not implemented yet 172 | }); 173 | server.transport.on('error', function() { 174 | // Not implemented yet 175 | }); 176 | var client = new Client(new ClientTcp('localhost', 11111), { autoRegister: false }); 177 | client.register('loopback'); 178 | client.loopback('foo', function(err, result) { 179 | test.ok(!err, 'no error'); 180 | test.equal(result, 'foo', 'loopback worked'); 181 | client.shutdown(function() { 182 | setTimeout(server.shutdown.bind(server), 500); 183 | }); 184 | }); 185 | }; 186 | 187 | exports.tcpServerEvents2 = function(test) { 188 | test.expect(2); 189 | var server1 = new Server(new ServerTcp(11112), { 190 | loopback: function(arg, callback) { callback(null, arg); } 191 | }); 192 | server1.transport.on('listening', function() { 193 | var server2 = new Server(new ServerTcp(11112, { retries: 1 }), {}); 194 | server2.transport.on('retry', function() { 195 | test.ok(true, 'retried to connect to the specified port'); 196 | }); 197 | server2.transport.on('error', function(e) { 198 | test.ok(e instanceof Error, 'received the error object after second retry was denied'); 199 | server1.shutdown(test.done.bind(test)); 200 | }); 201 | }); 202 | }; 203 | 204 | exports.multitransport = function(test) { 205 | test.expect(2); 206 | var server = new Server([new ServerTcp(9999), new ServerHttp(9998)], { 207 | loopback: function(arg, callback) { callback(null, arg); } 208 | }); 209 | var client1 = new Client(new ClientTcp('localhost', 9999)); 210 | var client2 = new Client(new ClientHttp('localhost', 9998)); 211 | client1.register('loopback'); 212 | client2.register('loopback'); 213 | client1.loopback('foo', function(err, result) { 214 | test.equal('foo', result, 'got the result over TCP'); 215 | client2.loopback('bar', function(err, result) { 216 | test.equal('bar', result, 'got the result of HTTP'); 217 | client1.shutdown(function() { 218 | client2.shutdown(function() { 219 | server.shutdown(test.done.bind(test)); 220 | }); 221 | }); 222 | }); 223 | }); 224 | }; 225 | 226 | 227 | String.prototype.repeat = function(num) { 228 | return new Array(num + 1).join(this); 229 | }; 230 | 231 | function perf(testString, test) { 232 | test.expect(4); 233 | var numMessages = 250; 234 | var tcpServer = new Server(new ServerTcp(9001), { 235 | loopback: function(arg, callback) { callback(null, arg); } 236 | }); 237 | var httpServer = new Server(new ServerHttp(9002), { 238 | loopback: function(arg, callback) { callback(null, arg); } 239 | }); 240 | var loopback = new Loopback(); 241 | var loopbackServer = new Server(loopback, { 242 | loopback: function(arg, callback) { callback(null, arg); } 243 | }); 244 | var tcpClient = new Client(new ClientTcp('localhost', 9001)); 245 | 246 | function last() { 247 | var loopbackClient = new Client(loopback); 248 | loopbackClient.register('loopback'); 249 | var loopbackCount = 0, loopbackStart = Date.now(), loopbackEnd; 250 | for(var i = 0; i < numMessages; i++) { 251 | /* jshint loopfunc: true */ 252 | loopbackClient.loopback(i, function() { 253 | loopbackCount++; 254 | if(loopbackCount === numMessages) { 255 | test.ok(true, 'loopback finished'); 256 | loopbackEnd = Date.now(); 257 | var loopbackTime = loopbackEnd - loopbackStart; 258 | var loopbackRate = numMessages * 1000 / loopbackTime; 259 | console.log("Loopback took " + loopbackTime + "ms, " + loopbackRate + " reqs/sec"); 260 | loopbackClient.shutdown(); 261 | loopbackServer.shutdown(); 262 | test.done(); 263 | } 264 | }); 265 | } 266 | } 267 | 268 | function more() { 269 | var childProcCount = 0, childProcStart = Date.now(), childProcEnd; 270 | for(var i = 0; i < numMessages; i++) { 271 | /* jshint loopfunc: true */ 272 | childProcClient.loopback(i, function() { 273 | childProcCount++; 274 | if(childProcCount === numMessages) { 275 | test.ok(true, 'childProc finished'); 276 | childProcEnd = Date.now(); 277 | var childProcTime = childProcEnd - childProcStart; 278 | var childProcRate = numMessages * 1000 / childProcTime; 279 | console.log("Child Proc IPC took " + childProcTime + "ms, " + childProcRate + " reqs/sec"); 280 | last(); 281 | } 282 | }); 283 | } 284 | } 285 | 286 | function next() { 287 | var httpClient = new Client(new ClientHttp('localhost', 9002)); 288 | httpClient.register('loopback'); 289 | var httpCount = 0, httpStart = new Date().getTime(), httpEnd; 290 | for(var i = 0; i < numMessages; i++) { 291 | /* jshint loopfunc: true */ 292 | httpClient.loopback(i, function() { 293 | httpCount++; 294 | if(httpCount === numMessages) { 295 | test.ok(true, 'http finished'); 296 | httpEnd = new Date().getTime(); 297 | var httpTime = httpEnd - httpStart; 298 | var httpRate = numMessages * 1000 / httpTime; 299 | console.log("HTTP took " + httpTime + "ms, " + httpRate + " reqs/sec"); 300 | httpClient.shutdown(); 301 | httpServer.shutdown(); 302 | more(); 303 | } 304 | }); 305 | } 306 | } 307 | 308 | tcpClient.register('loopback'); 309 | var tcpCount = 0, tcpStart = new Date().getTime(), tcpEnd; 310 | for(var i = 0; i < numMessages; i++) { 311 | /* jshint loopfunc: true */ 312 | tcpClient.loopback(testString || i, function() { 313 | tcpCount++; 314 | if(tcpCount === numMessages) { 315 | test.ok(true, 'tcp finished'); 316 | tcpEnd = new Date().getTime(); 317 | var tcpTime = tcpEnd - tcpStart; 318 | var tcpRate = numMessages * 1000 / tcpTime; 319 | console.log("TCP took " + tcpTime + "ms, " + tcpRate + " reqs/sec"); 320 | tcpClient.shutdown(); 321 | tcpServer.shutdown(); 322 | next(); 323 | } 324 | }); 325 | } 326 | } 327 | 328 | exports.perfSimple = perf.bind(null, null); 329 | exports.perf100 = perf.bind(null, 'a'.repeat(100)); 330 | exports.perf1000 = perf.bind(null, 'a'.repeat(1000)); 331 | exports.perf10000 = perf.bind(null, 'a'.repeat(10000)); 332 | exports.perf100000 = perf.bind(null, 'a'.repeat(100000)); 333 | 334 | exports.closeChild = function(test) { 335 | child.kill(); 336 | test.done(); 337 | }; 338 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Multitransport JSON-RPC Client and Server 2 | 3 | [![NPM version](https://badge.fury.io/js/multitransport-jsonrpc.png)](http://badge.fury.io/js/multitransport-jsonrpc) [![Dependency Status](https://gemnasium.com/uber/multitransport-jsonrpc.png)](https://gemnasium.com/uber/multitransport-jsonrpc) [![Build Status](https://travis-ci.org/uber/multitransport-jsonrpc.png?branch=master)](https://travis-ci.org/uber/multitransport-jsonrpc) [![Coverage Status](https://coveralls.io/repos/uber/multitransport-jsonrpc/badge.png?branch=master)](https://coveralls.io/r/uber/multitransport-jsonrpc?branch=master) 4 | 5 | *multitransport-jsonrpc* provides a JSON-RPC solution for both the traditional HTTP scenario as well as for persistent, raw TCP connections. It's designed as a collection of constructor functions where both the client and server are split into two components: a single outer object in charge of the JSON-RPC protocol and providing the API for your code to interact with, and multiple sets of inner transport objects that deal with the particular data transport layer you want to use and how precisely to configure it. 6 | 7 | This pluggable architecture means you can continue to use an RPC-type pattern even in use-cases where JSON-RPC has not traditionally been a great fit. The HTTP transport provides compatibility with traditional JSON-RPC clients and servers, while the TCP transport trims the fat of the HTTP header and amortizes the TCP handshake overhead, improving transport performance for large numbers of small messages. A theoretical ZeroMQ or SMTP transport could allow totally asynchronous clients and servers, where neither the client nor server need to be running all the time for communication to still successfully take place. 8 | 9 | ## Why nonstandard transports (such as TCP)? 10 | 11 | It's not an official JSON-RPC standard, so why not just use HTTP for everything? The answer is simple: ridiculous performance gains when you don't need to do a TCP handshake or account for the HTTP header overhead on each request and response. Here's the results of a perf test on Travis CI: 12 | 13 | ``` 14 | Loopback took 7ms, 142857.14285714287 reqs/sec 15 | ChildProc IPC took 30ms, 33333.333333333336 reqs/sec 16 | TCP took 74ms, 13513.513513513513 reqs/sec 17 | HTTP took 758ms, 1319.2612137203166 reqs/sec 18 | ``` 19 | 20 | The Loopback transport (all in-process, useful for testing and gauging the fundamental limit of JSON-RPC) comes in at over 100x faster than HTTP, over Node's IPC mechanism to child processes it's over 25x faster, and the TCP transport is over 10x faster. 21 | 22 | ## Install 23 | 24 | npm install multitransport-jsonrpc 25 | 26 | If you want to use the ``jsonrpc-repl`` binary, also 27 | 28 | npm install -g multitransport-jsonrpc 29 | 30 | ## Library Usage 31 | 32 | ```js 33 | var jsonrpc = require('multitransport-jsonrpc'); // Get the multitransport JSON-RPC suite 34 | 35 | var Server = jsonrpc.server; // The server constructor function 36 | var Client = jsonrpc.client; // The client constructor function 37 | 38 | var ServerHttp = jsonrpc.transports.server.http; // The server HTTP transport constructor function 39 | var ServerTcp = jsonrpc.transports.server.tcp; // The server TCP transport constructor function 40 | var ServerMiddleware = jsonrpc.transports.server.middleware; // The server Middleware transport constructor function (for Express/Connect) 41 | var Loopback = jsonrpc.transports.shared.loopback; // The Loopback transport for mocking clients/servers in tests 42 | 43 | var ClientHttp = jsonrpc.transports.client.http; 44 | var ClientTcp = jsonrpc.transports.client.tcp; 45 | 46 | // Setting up servers 47 | var jsonRpcHttpServer = new Server(new ServerHttp(8000), { 48 | loopback: function(obj, callback) { callback(undefined, obj); } 49 | }); 50 | 51 | var jsonRpcTcpServer = new Server(new ServerTcp(8001), { 52 | loopback: function(obj, callback) { callback(undefined, obj); } 53 | }); 54 | 55 | var express = require('express'); 56 | var app = express(); 57 | app.use(express.bodyParser()); 58 | var jsonRpcMiddlewareServer = new Server(new ServerMiddleware(), { 59 | loopback: function(obj, callback) { callback(undefined, obj); } 60 | }); 61 | app.use('/rpc', jsonRpcMiddlewareServer.transport.middleware); 62 | app.listen(8002); 63 | 64 | var loopback = new Loopback(); 65 | var jsonRpcLoopbackServer = new Server(loopback, { 66 | loopback: function(obj, callback) { callback(undefined, obj); } 67 | }); 68 | 69 | // Setting up and using the clients 70 | 71 | // Either explicitly register the remote methods 72 | var jsonRpcHttpClient = new Client(new ClientHttp('localhost', 8000)); 73 | jsonRpcHttpClient.register('loopback'); 74 | jsonRpcHttpClient.loopback('foo', function(err, val) { 75 | console.log(val); // Prints 'foo' 76 | }); 77 | 78 | // Or wait for the "auto-register" functionality do that for you 79 | new Client(new ClientTcp('localhost', 8001), {}, function(jsonRpcTcpClient) { 80 | jsonRpcTcpClient.loopback('foo', function(err, val) { 81 | console.log(val); // Prints 'foo' 82 | }); 83 | }); 84 | 85 | var jsonRpcExpressClient = new Client(new ClientHttp('localhost', 8002, { path: '/rpc' })); 86 | jsonRpcExpressClient.register('loopback'); 87 | jsonRpcExpressClient.loopback('foo', function(err, val) { 88 | console.log(val); // Prints 'foo' 89 | }); 90 | 91 | new Client(loopback, {}, function(jsonRpcLoopbackClient) { 92 | jsonRpcLoopbackClient.loopback('foo', function(err, val) { 93 | console.log(val); // Prints 'foo' 94 | }); 95 | }); 96 | 97 | // The server can run multiple transports simultaneously, too 98 | var jsonRpcMultitransportServer = new Server([new ServerTcp(8000), new ServerHttp(8080)], { 99 | loopback: function(obj, callback) { callback(undefined, obj); } 100 | }); 101 | var client1 = new Client(new ClientTcp('localhost', 8000)); 102 | var client2 = new Client(new ClientHttp('localhost', 8080)); 103 | ``` 104 | 105 | ### Constructor Function Parameters 106 | 107 | #### jsonrpc.client 108 | 109 | ``new jsonrpc.client(transport, options, done)`` 110 | 111 | ``transport`` - A client transport object (pre-constructed, so you don't need to write a Javascript constructor function if you don't want to). 112 | 113 | ``options`` - An object containing configuration options. The only configuration option for the client is ``autoRegister`` at the moment, a flag (default: true) that tells the client to attempt to get the listing of valid remote methods from the server. 114 | 115 | ``done`` - An optional callback function that is passed a reference to the client object after the ``autoRegister`` remote call has completed. 116 | 117 | #### jsonrpc.server 118 | 119 | ``new jsonrpc.server(transport, scope)`` 120 | 121 | ``transport`` - A server transport object (pre-constructed). 122 | 123 | ``scope`` - An object containing a set of functions that will be accessible by the connecting clients. 124 | 125 | #### jsonrpc.transports.client.http 126 | 127 | ``new jsonrpc.transports.client.http(server, port, config)`` 128 | 129 | ``server`` - The address of the server you're connecting to. 130 | 131 | ``port`` - The port of the server you're connecting to. 132 | 133 | ``config`` - The configuration settings for the client HTTP transport, which at the moment is only the ``path``, which defaults to ``/``. 134 | 135 | The various transports also provide events you can listen on, using the [Node.js EventEmitter](http://nodejs.org/api/events.html) so the semantics should be familiar. The Client HTTP Transport provides: 136 | 137 | ``message`` - This event is fired any time a message (response) is returned, and provides the registered callback with the JSON-RPC object received. 138 | 139 | ``shutdown`` - This event is fired when the transport is shut down, and provides no arguments to the callback handlers. 140 | 141 | #### jsonrpc.transports.client.tcp 142 | 143 | ``new jsonrpc.transports.client.tcp(server, port, config)`` 144 | 145 | ``server`` - The address of the server. 146 | 147 | ``port`` - The port of the server. 148 | 149 | ``config`` - The configuration settings. For the client TCP transport, these are: 150 | 151 | ``timeout`` - The time, in ms, that the transport will wait for a response (default: 30 seconds) 152 | 153 | ``retries`` - The number of times the client will attempt to reconnect to the server when a connection is dropped (default: Infinity) 154 | 155 | ``retryInterval`` - The time, in ms, that the client will wait before reconnect attempts (default: 250ms) 156 | 157 | ``reconnects`` - The number of times the client will reconnect after a connection is closed (default: Infinity) 158 | 159 | ``reconnectClearInterval`` - The time, in ms, after which the reconnect counter is reset. Set to 0 to disable (default: 1 hour) 160 | 161 | ``stopBufferingAfter`` - The time, in ms, that the client will return errors immediately to the caller *while still attempting to reconnect to the server*. If 0, it will never immediately return errors (default: 0) 162 | 163 | The Client TCP Transport events are: 164 | 165 | ``message`` - This event is fired whenever a complete message is received, and the registered callbacks receive the JSON-RPC object as their only argument. 166 | 167 | ``retry`` - This event is fired whenever the transport attempts to reconnect to the server. There are no arguments provided to the callback. 168 | 169 | ``end`` - This event is fired when the TCP connection is ended. If reconnection retries are enabled, it is only fired when the transport fails to reconnect. 170 | 171 | ``sweep`` - This event is fired when the transport clears out old requests that went past the expiration time. The callbacks receive an array of failed requests (if any) as the only argument. 172 | 173 | ``shutdown`` - This event is fired when the transport is shutdown. 174 | 175 | #### jsonrpc.transports.client.childProcess 176 | 177 | ``new jsonrpc.transports.client.childProcess(child, config)`` 178 | 179 | ``child`` - The Node.js child process object created by ``child_process.fork(sourceFile)`` 180 | 181 | ``config`` - The configuration settings. For the client Child Process transport, these are: 182 | 183 | ``timeout`` - The time, in ms, that the transport will wait for a response (default: 30 seconds) 184 | 185 | ``sweepTime`` - The time, in ms, that the transport will run a sweep mechanism to throw away old requests that never returned (default: 1 second) 186 | 187 | ``killChildOnShutdown`` - A flag that specifies whether or not shutting down the client kills the child process (true) or merely disconnects the IPC from it (false). (default: true) 188 | 189 | The Client Child Process Transport events are: 190 | 191 | ``exit`` - This event is fired whenever the child process exits. The client object is automatically shut down at this time. 192 | 193 | ``error`` - This event is fired whenever the child process returns an error. The client object is automatically shut down at this time. 194 | 195 | ``sweep`` - This event is fired when the transport clears out old requests that went past the expiration time. The callbacks receive an array of failed requests (if any) as the only argument. 196 | 197 | ``shutdown`` - This event is fired when the transport is shutdown. 198 | 199 | #### jsonrpc.transports.server.http 200 | 201 | ``new jsonrpc.transports.server.http(port, config)`` 202 | 203 | ``port`` - The port the server should use. 204 | 205 | ``config`` - The configuration settings. For the server HTTP transport, only ``acao`` exists. It is the value that should be returned to clients in the ``Access-Control-Allow-Origin`` header, and defaults to ``*``. 206 | 207 | The Server HTTP Transport events are: 208 | 209 | ``message`` - This event is fired whenever a complete message is received, and the registered callbacks receive the JSON-RPC object as their only argument. 210 | 211 | ``listening`` - This event is fired whenever the HTTP server is open and listening for connections. 212 | 213 | ``shutdown`` - This event is fired when the transport is shutdown. 214 | 215 | #### jsonrpc.transports.server.tcp 216 | 217 | ``new jsonrpc.transports.server.tcp(port, config)`` 218 | 219 | ``port`` - The port the server should use. 220 | 221 | ``config`` - The configuration settings. For the server TCP transport, these are: 222 | 223 | ``retries`` - The number of times the server will attempt to listen to the TCP port specified. (Useful during fast restarts where the new node app is starting while the old node app is being shut down.) 224 | 225 | ``retryInterval`` - The time, in ms, that the server will wait between attempts to grab the TCP port. 226 | 227 | The Server TCP Transport events are: 228 | 229 | ``connection`` - This event is fired whenever a new connection is made to the TCP server. The callbacks receive a reference to the connection object as their only argument. 230 | 231 | ``message`` - This event is fired whenever a JSON-RPC message is received. The callbacks receive the JSON-RPC object as their only argument. 232 | 233 | ``closedConnection`` - This event is fired whenever an open connection to a client is closed. The callbacks receive a reference to the connection object as their only argument. 234 | 235 | ``listening`` - This event is fired whenever the TCP server is open and listening for connections. 236 | 237 | ``retry`` - This event is fired whenever the TCP server cannot open the port to listen for connections and is retrying to connect. 238 | 239 | ``error`` - This event is fired whenever an unhandled error in the TCP server occurs. If configured, the server will attempt to solve listen errors. The callbacks receive the error object as their only argument. 240 | 241 | ``shutdown`` - This event is fired when the server is shutdown. 242 | 243 | #### jsonrpc.transports.server.middleware 244 | 245 | ``new jsonrpc.transports.server.middleware(config)`` 246 | 247 | ``config`` - The configuration settings. For the Connect/Express middleware transport, these are: 248 | 249 | ``acao`` - The ``Access-Control-Allow-Origin`` header value, which defaults to ``*``. 250 | 251 | ``server`` - A reference to the underlying server the middleware relies on. Used only for ``shutdown`` compatibility, if desired. 252 | 253 | The Server Middleware Transport events are: 254 | 255 | ``message`` - This event is fired whenever a complete message is received, and the registered callbacks receive the JSON-RPC object as their only argument. 256 | 257 | ``shutdown`` - This event is fired when the transport is shutdown. 258 | 259 | #### jsonrpc.transports.server.childProcess 260 | 261 | ``new jsonrpc.transports.server.childProcess(config)`` 262 | 263 | ``config`` - The configuration settings. For the Child Process transport, there are no configuration options at this time! 264 | 265 | The Child Process Transport events are: 266 | 267 | ``message`` - This event is fired whenever a complete message is received, and the registered callbacks receive the JSON-RPC object as their only argument. 268 | 269 | ``shutdown`` - This event is fired when the transport is shutdown. 270 | 271 | #### jsonrpc.transports.shared.loopback 272 | 273 | ``new jsonrpc.transports.shared.loopback()`` 274 | 275 | No configuration used by the loopback object. It's still a constructor solely so you can have mutliple loopbacks in a single test suite, if needed. 276 | 277 | The Loopback Transport events are: 278 | 279 | ``message`` - This event is fired whenever a message is passed into the client on its way to the server. 280 | 281 | ``shutdown`` - This event is fired when the transport is "shutdown." (The method doesn't actually do anything, this just helps if your tests depend on this event.) 282 | 283 | ## Defining JSON-RPC Server Methods 284 | 285 | By default, JSON-RPC server methods are asynchronous, taking a callback function as the last argument. The callback function assumes the first argument it receives is an error and the second argument is a result, in the Node.js style. 286 | 287 | ```js 288 | function foo(bar, baz, callback) { 289 | if(!baz) { 290 | callback(new Error('no baz!')); 291 | } else { 292 | callback(null, bar + baz); 293 | } 294 | } 295 | ``` 296 | 297 | Alternately, the JSON-RPC server provides a ``blocking`` method that can be used to mark a function as a blocking function that takes no callback. Then the result is returned and errors are thrown. 298 | 299 | ```js 300 | var blocking = jsonrpc.server.blocking; 301 | var blockingFoo = blocking(function(bar, baz) { 302 | if(!baz) { 303 | throw new Error('no baz!'); 304 | } else { 305 | return bar + baz; 306 | } 307 | }); 308 | ``` 309 | 310 | ## Using JSON-RPC Client Methods 311 | 312 | On the client side, you can only use the methods in an asynchronous way. All assume the last argument is a callback method where the first argument is an error and the second is a result. The JSON-RPC client highly recommends your server doesn't provide methods named ``transport``, ``request``, ``register``, or ``shutdown``, since the remote methods are in the same namespace as these helper methods of the JSON-RPC client, but the ``request`` method can still be used in this way to manually call any of these "blacklisted" methods: 313 | 314 | ```js 315 | jsonRpcClient.request("shutdown", ["arg1", "arg2"], callbackFunc); 316 | ``` 317 | 318 | # Using the jsonrpc-repl binary 319 | 320 | Usage: jsonrpc-repl [options] 321 | 322 | Options: 323 | 324 | -h, --help output usage information 325 | -s, --server The hostname the server is located on. (Default: "localhost") 326 | -p, --port The port the server is bound to. (Default: 80) 327 | -t, --tcp Connects to the server via TCP instead of HTTP (Default: false) 328 | 329 | The ``jsonrpc-repl`` dumps you into a [Node.js repl](http://nodejs.org/api/repl.html) with some bootstrapping done on connecting you to the RPC server and getting a list of valid server methods. You can access them with the ``rpc`` object in the exact same way as described above in the "Using JSON-RPC Client Methods" section. 330 | 331 | 332 | ## Creating A New Transport 333 | 334 | If you want to write your own transport constructor functions for multitransport-jsonrpc, here's what the client and server objects expect from their transport: 335 | 336 | ### Client 337 | 338 | The transport is expected to have two methods: ``request`` and ``shutdown``. 339 | 340 | ``request`` is expected to be given a JSON-RPC object (not a string) as its first argument and a callback function as its second argument. The callback function expects its one and only argument to be a JSON-RPC object (not a string) that the error or result can be pulled from. 341 | 342 | ``shutdown`` is expected to take one argument, an **optional** callback function to let it know when the shutdown has completed. 343 | 344 | ### Server 345 | 346 | The transport is expected to have a ``shutdown`` method that behaves exactly the same as the method described above. 347 | 348 | It is also expected to make use of a ``handler`` method that the server attaches to it. This method expects two arguments, the first is a JSON-RPC object (not a string), but if the input is not valid JSON will handle the unparsed data just fine. The second argument is a callback that it provides with the response JSON-RPC object (not a string). 349 | 350 | ## License (MIT) 351 | 352 | Portions Copyright (C) 2013 by Uber Technologies, Inc, David Ellis 353 | 354 | Portions Copyright (C) 2011 by Agrosica, Inc, David Ellis, Alain Rodriguez, Hector Lugo 355 | 356 | Permission is hereby granted, free of charge, to any person obtaining a copy 357 | of this software and associated documentation files (the "Software"), to deal 358 | in the Software without restriction, including without limitation the rights 359 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 360 | copies of the Software, and to permit persons to whom the Software is 361 | furnished to do so, subject to the following conditions: 362 | 363 | The above copyright notice and this permission notice shall be included in 364 | all copies or substantial portions of the Software. 365 | 366 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 367 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 368 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 369 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 370 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 371 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 372 | THE SOFTWARE. 373 | --------------------------------------------------------------------------------