UNPKG

jayson

Version:

JSON-RPC 2.0 compliant server and client

545 lines (389 loc) 18.3 kB
# Jayson Jayson is a [JSON-RPC 2.0][jsonrpc-spec] compliant server and client written in JavaScript for [node.js][node.js] that wants to be as simple as possible to use. [jsonrpc-spec]: http://jsonrpc.org/spec.html [node.js]: http://nodejs.org/ ## Features * Servers that listen to many interfaces at once * Supports HTTP client and server connections * jQuery AJAX client * Automatic request relaying to other servers * Simple process forking for expensive computations * JSON Reviving and Replacing for advanced (de)serialization of objects * CLI client * Extensively tested to comply with the [official specification][jsonrpc-spec] ## Example A basic JSON-RPC 2.0 server via HTTP: Server in `examples/simple_example/server.js`: ```javascript var jayson = require(__dirname + '/../..'); // create a server var server = jayson.server({ add: function(a, b, callback) { callback(null, a + b); } }); // Bind a http interface to the server and let it listen to localhost:3000 server.http().listen(3000); ``` Client in `examples/simple_example/client.js` invoking `add` on the above server: ```javascript var jayson = require(__dirname + '/../..'); // create a client var client = jayson.client.http({ port: 3000, hostname: 'localhost' }); // invoke "add" client.request('add', [1, 1], function(err, error, response) { if(err) throw err; console.log(response); // 2! }); ``` ## Installation Install the latest version of _jayson_ from [npm](https://github.com/isaacs/npm) by executing `npm install jayson` in your shell. Do a global install with `npm install --global jayson` if you want the `jayson` client CLI in your PATH. ### CLI client There is a CLI client in `bin/jayson.js` and it should be available as `jayson` if you installed the package with the `--global` switch. Run `jayson --help` to see how it works. ## Requirements Jayson does not have any special dependencies that cannot be resolved with a simple `npm install`. It has been tested with the following node.js versions: - node.js v0.6.x - node.js v0.8.x ## Class documentation In addition to this document, a comprehensive class documentation is available at [jayson.tedeh.net](http://jayson.tedeh.net). ## Running tests - Change directory to the repository root - Install the testing framework ([mocha](https://github.com/visionmedia/mocha) together with [should](https://github.com/visionmedia/should.js)) by executing `npm install --dev` - Run the tests with `make test` or `npm test` ## Usage ### Client The client is available as the `Client` or `client` property of `require('jayson')`. #### Interfaces * `Client` Base class for interfacing with a server. * `Client.http` HTTP interface. * `Client.fork` Node.js child_process/fork interface. * `Client.jquery` Wrapper around `jQuery.ajax`. #### Notifications Notification requests are for cases where the reply from the server is not important and should be ignored. This is accomplished by setting the `id` property of a request object to `null`. Client in `examples/notifications/client.js` doing a notification request: ```javascript var jayson = require(__dirname + '/../..'); var client = jayson.client.http({ host: 'localhost', port: 3000 }); // the third parameter is set to "null" to indicate a notification client.request('ping', [], null, function(err) { if(err) throw err; // request was received successfully }); ``` A server in `examples/notifications/server.js`: ```javascript var jayson = require(__dirname + '/../..'); var server = jayson.server({ ping: function(callback) { // do something callback(); } }); server.http().listen(3000); ``` ##### Notes * Any value that the server returns will be discarded when doing a notification request. * Omitting the third argument `null` to `Client.prototype.request` does not generate a notification request. This argument has to be set explicitly to `null` for this to happen. * Network errors and the like will still reach the callback. When the callback is invoked (with or without error) one can be certain that the server has received the request. * See the [Official JSON-RPC 2.0 Specification][jsonrpc-spec] for additional information on how Jayson handles notifications that are erroneous. #### Batches A batch request is an array of individual requests that are sent to the server as one. Doing a batch request is very simple in Jayson and consists of constructing an `Array` of individual requests (created by not passing a callback to `Client.prototype.request`) that is then itself passed to `Client.prototype.request`. Client example in `examples/batch_request/client.js`: ```javascript var jayson = require(__dirname + '/../..'); var client = jayson.client.http({ host: 'localhost', port: 3000 }); var batch = [ client.request('does_not_exist', [10, 5]), client.request('add', [1, 1]), client.request('add', [0, 0], null) // a notification ]; // callback takes two arguments (first type of callback) client.request(batch, function(err, responses) { if(err) throw err; // responses is an array of errors and successes together console.log('responses', responses); }); // callback takes three arguments (second type of callback) client.request(batch, function(err, errors, successes) { if(err) throw err; // errors is an array of the requests that errored console.log('errors', errors); // successes is an array of requests that succeded console.log('successes', successes); }); ``` Server example in `examples/batch_request/server.js`: ```javascript var jayson = require(__dirname + '/../..'); var server = jayson.server({ add: function(a, b, callback) { callback(null, a + b); } }); server.http().listen(3000); ``` ##### Notes * See the [Official JSON-RPC 2.0 Specification][jsonrpc-spec] for additional information on how Jayson handles different types of batches, mainly with regards to notifications, request errors and so forth. * There is no guarantee that the results will be in the same order as request Array `request`. To find the right result, compare the ID from the request with the ID in the result yourself. #### Callback syntactic sugar When the length (number of arguments) of a client callback function is either 2 or 3 it receives slightly different values when invoked. * 2 arguments: first argument is an error or `null`, second argument is the response object as returned (containing _either_ a `result` or a `error` property) or `null` for notifications. * 3 arguments: first argument is an error or null, second argument is a JSON-RPC `error` property or `null` (if success), third argument is a JSON-RPC `result` property or `null` (if error). When doing a batch request with a 3-length callback, the second argument will be an array of requests with a `error` property and the third argument will be an array of requests with a `result` property. #### Client interfaces and options Every client supports these options: * `reviver` -> Function to use as a JSON reviver * `replacer` -> Function to use as a JSON replacer * `generator` -> Function to generate request ids with. If omitted, Jayson will just generate a "random" number like this: `Math.round(Math.random() * Math.pow(2, 24))` #### Client events A client will emit the following events (in addition to any special ones emitted by a specific interface): * `request` Emitted when a client is just about to dispatch a request. First argument is the request object. * `response` Emitted when a client has just received a reponse. First argument is the request object, second argument is the response as received. ##### Client.http Uses the same options as [http.request][nodejs_docs_http_request] (which also enables the use of https) in addition to these options: * `encoding` -> String that determines the encoding to use and defaults to utf8 [nodejs_docs_http_request]: http://nodejs.org/docs/latest/api/http.html#http_http_request_options_callback ##### Client.fork Uses the same options as the base class. ##### Client.jquery The jQuery Client is stand-alone from the other classes and should preferably be compiled with `make compile` which outputs different flavors into the `build` directory. Supports inclusion via AMD. Uses the same options as [jQuery.ajax][jquery_docs_ajax] and exposes itself as $.jayson with the same arguments as `Client.prototype.request`. [jquery_docs_ajax]: http://api.jquery.com/jQuery.ajax/ ### Server The server classes are available as the `Server` or `server` property of `require('jayson')`. The server also sports several interfaces that can be accessed as properties of an instance of `Server`. #### Server interfaces and options * `Server` - Base interface for a server that supports receiving JSON-RPC 2.0 requests. * `Server.http` - HTTP server that inherits from [http.Server][nodejs_doc_http_server]. * `Server.https` - HTTPS server that inherits from [https.Server][nodejs_doc_http_server]. * `Server.middleware` - Method that returns a [Connect][connect]/[Express][express] compatible middleware function. * `Server.fork` Creates a child process that can take requests via `client.fork` [nodejs_doc_http_server]: http://nodejs.org/docs/latest/api/http.html#http_class_http_server [nodejs_doc_https_server]: http://nodejs.org/docs/latest/api/https.html#https_class_https_server [connect]: http://www.senchalabs.org/connect/ [express]: http://expressjs.com/ #### Using many server interfaces at the same time A Jayson server can use many interfaces at the same time. Server in `examples/many_interfaces/server.js` that listens to both `http` and a `https` requests: ```javascript var jayson = require(__dirname + '/../..'); var server = jayson.server({ add: function(a, b, callback) { return callback(null, a + b); } }); // "http" will be an instance of require('http').Server var http = server.http(); // "https" will be an instance of require('https').Server var https = server.https({ //cert: require('fs').readFileSync('cert.pem'), //key require('fs').readFileSync('key.pem') }); http.listen(80, function() { console.log('Listening on *:80') }); https.listen(443, function() { console.log('Listening on *:443') }); ``` #### Using the server as a relay Passing an instance of a client as a method to the server makes the server relay incoming requests to wherever the client is pointing to. This might be used to delegate computationally expensive functions into a separate fork/server or to abstract a cluster of servers behind a common interface. Public server in `examples/relay/server_public.js` listening on `*:3000`: ```javascript var jayson = require(__dirname + '/../..'); // create a server where "add" will relay a localhost-only server var server = jayson.server({ add: jayson.client.http({ hostname: 'localhost', port: 3001 }) }); // let the server listen to *:3000 server.http().listen(3000, function() { console.log('Listening on *:3000'); }); ``` Private server in `examples/relay/server_private.js` listening on localhost:3001: ```javascript var jayson = require(__dirname + '/../..'); var server = jayson.server({ add: function(a, b, callback) { callback(null, a + b); } }); // let the private server listen to localhost:3001 server.http().listen(3001, '127.0.0.1', function() { console.log('Listening on 127.0.0.1:3001'); }); ``` Every request to `add` on the public server will now relay the request to the private server. See the client example in `examples/relay/client.js`. #### Server events In addition to events that are specific to a certain interface, all servers will emit the following events: * `request` Emitted when the server receives an interpretable request. First argument is the request object. * `response` Emitted when the server is returning a response. First argument is the request object, the second is the response object. #### Errors If you should like to return an error from an method request to indicate a failure, remember that the [JSON-RPC 2.0][jsonrpc-spec] specification requires the error to be an `Object` with a `code (Integer/Number)` to be regarded as valid. You can also provide a `message (String)` and a `data (Object)` with additional information. Example: ```javascript var jayson = require(__dirname + '/../..'); var server = jayson.server({ i_cant_find_anything: function(id, callback) { var error = {code: 404, message: 'Cannot find ' + id}; callback(error); // will return the error object as given }, i_cant_return_a_valid_error: function(callback) { callback({message: 'I forgot to enter a code'}); // will return an "Internal Error" } }); ``` ##### Predefined Errors It is also possible to cause a method to return one of the predefined [JSON-RPC 2.0 error codes][jsonrpc-spec#error_object] using the server helper function `Server.prototype.error` inside of a server method. Example: [jsonrpc-spec#error_object]: http://jsonrpc.org/spec.html#error_object ```javascript var jayson = require(__dirname + '/../..'); var server = jayson.server({ invalid_params: function(id, callback) { var error = this.error(-32602); // returns an error with the default properties set callback(error); } }); ``` You can even override the default messages: ```javascript var jayson = require(__dirname + '/../..'); var server = jayson.server({ error_giver_of_doom: function(callback) { callback(true) // invalid error format, which causes an Internal Error to be returned instead } }); // Override the default message server.errorMessages[Server.errors.INTERNAL_ERROR] = 'I has a sad. I cant do anything right'; ``` ### Revivers and Replacers JSON is a great data format, but it lacks support for representing types other than the simple ones defined in the [JSON specification][jsonrpc-spec]. Fortunately the JSON methods in JavaScript (`JSON.parse` and `JSON.stringify`) provides options for custom serialization/deserialization routines. Jayson allows you to pass your own routines as options to both clients and servers. Simple example transferring the state of an object between a client and a server: Shared code between the server and the client in `examples/reviving_and_replacing/shared.js`: ```javascript var Counter = exports.Counter = function(value) { this.count = value || 0; }; Counter.prototype.increment = function() { this.count += 1; }; exports.replacer = function(key, value) { if(value instanceof Counter) { return {$class: 'counter', $props: {count: value.count}}; } return value; }; exports.reviver = function(key, value) { if(value && value.$class === 'counter') { var obj = new Counter; for(var prop in value.$props) obj[prop] = value.$props[prop]; return obj; } return value; }; ``` Server in `examples/reviving_and_replacing/server.js`: ```javascript var jayson = require(__dirname + '/../..'); var shared = require('./shared'); // Set the reviver/replacer options var options = { reviver: shared.reviver, replacer: shared.replacer }; // create a server var server = jayson.server({ increment: function(counter, callback) { counter.increment(); callback(null, counter); } }, options); // let the server listen to for http connections on localhost:3000 server.http().listen(3000); ``` A client in `examples/reviving_and_replacing/client.js` invoking "increment" on the server: ```javascript var jayson = require(__dirname + '/../..'); var shared = require('./shared'); // create a client with the shared reviver and replacer var client = jayson.client.http({ port: 3000, hostname: 'localhost', reviver: shared.reviver, replacer: shared.replacer }); // create the object var instance = new shared.Counter(2); // invoke "increment" client.request('increment', [instance], function(err, error, result) { if(err) throw err; console.log(result instanceof shared.Counter); // true console.log(result.count); // 3! console.log(instance === result); // false - it won't be the same object, naturally }); ``` ##### Notes * Instead of using a replacer, it is possible to define a `toJSON` method for any JavaScript object. Unfortunately there is no corresponding method for reviving objects (that would not work, obviously), so the _reviver_ always has to be set up manually. ### Forking It is possible (and _simple_) to create automatic forks with jayson using the node.js `child_process` core library. This might be used for expensive or blocking calculations and to provide some separation from the main server thread. The forking server class is available as `jayson.Server.Fork` and takes a file as the first option. This file will be require'd by jayson and should export any methods that are to be made available to clients. The main server in `examples/forking/server.js` ```javascript var jayson = require(__dirname + '/../..'); // creates a fork var fork = jayson.server.fork(__dirname + '/fork'); var front = jayson.server({ fib: jayson.client.fork(fork) // connects "fib" to the fork }); // let the front server listen to localhost:3000 front.http().listen(3000); ``` The forked server in `examples/forking/fork.js` ```javascript // export "fib" for forking exports.fib = function(n, callback) { function fib(n) { if(n < 2) return n; return fib(n - 1) + fib(n - 2); }; var result = fib(n); callback(null, fib(n)); }; ``` A client doing a fibonacci request in `examples/forking/client.js` ```javascript var jayson = require(__dirname + '/../..'); var client = jayson.client.http({ port: 3000, hostname: 'localhost' }); // request "fib" on the server client.request('fib', [15], function(err, response) { console.log(response); }); ``` #### Notes * A child_process is spawned immediately * To specify options (such as a reviver and a replacer) for the forked server, `module.exports` an instance of `jayson.Server` instead of exporting plain methods. ### Contributing Highlighting [issues](https://github.com/tedeh/jayson/issues) or submitting pull requests on [Github](https://github.com/tedeh/jayson) is most welcome. ### TODO * `jayson.Server.fork.deferred` - Deferred forking that only spawns on demand * Streaming * Middleware-like support for defining server methods * Integration with the Cluster API * Benchmarks