node-jest

0.2.0 • Public • Published

Jest: A JSON-RPC compatible client/server library for Node.js

Jest is a JSON-RPC compatible client/server library that allows a Node.js app execute methods on one or more server.

It provides these features:

  • Client authentication
  • Automatic load balancing and failover management
  • Service discovery and introspection
  • Compatibility with both promises and callback

(Note: client authentication and service discovery/introspections are not compatible with plain-vanilla JSON-RPC.)

Installation

node install node-jest

Instantiating a server

A server allows you to give an external client access to one or more methods of your choosing:

var jest = require('node-jest');
 
var server = jest.server();
 
// Configure here
 
server.listen(8000, function() {
    console.log('Listening on port ' + server.address().port);
});

Authenticating clients

Jest supports extending the base JSON-RPC model with a special auth payload that makes it possible for the server to authenticate clients prior to giving them access to the server's functionality.

By default, Jest turns this feature on, and prevents any client from actually logging in. This is a simple security measure whose goal is to ensure that you don't accidentally forget about authentication.

You can turn on the authentication extension by setting server.requireAuth to false. If you want to keep it on, you must provide an authentication handler that validates your clients:

server.auth = function authenticateClient(authData, cb) {
    // The `authData` parameter is arbitrary data sent by the client
    // during the authentication process
 
    process.nextTick(function() {
        if (authData == 'securekey') {
            cb(
                null, 
                { 
                    clientId : 'service-1', 
                    permissions: ['read', 'write'] 
                }
            );
        } else {
            cb(new Error('Forbidden!'));
        }
    });
}

When executing the callback from your authentication handler, you can attach arbitrary data to the connection that you can later retrieve when the client attempts to execute a method. You can use this data any way you want (or not at all), for example to manage access control.

Registering routes

A routes represents a method that your server makes available to its clients. Routes can have completely arbitrary names, although Jest works best with methods that are namespaced using a dot notation.

Route handlers are asynchronous, and can either use a traditional callback or a promise:

 
// Register a route using a callback
 
server.route('math.add', function(a, b, cb) {
    // Do something in here, and call `cb` when you're done.
 
    process.nextTick(function() {
        // The context returned by the authentication handler
        // can be retrieved from the server:
 
        if (server.context(arguments).indexOf('read') == -1) {
            cb(new Error('Forbidden'));
            return;
        }
 
        cb(null, a + b);
    });
});
 
// Register a route using promises
 
server.proute('math.mult', function(a, b) {
    // Here, the context is passed as the last parameter to the function.
 
    var deferred = Q.defer();
 
    process.nextTick(function() {
        if (server.context(arguments).permissions.indexOf('read') == -1) {
            deferred.reject(new Error('Forbidden'));
            return;
        }
 
        deferred.resolve(* b);
    });
 
    return deferred.promise;
});

Note that the result that the authentication handler returned on success is passed in to the individual route handlers as an extra parameter. Jest will pad your function calls so that it will fall outside of the declared arguments of your route handlers (for example, if your function declares two arguments, the context is passed as the third, even if the caller only supplies a single argument). This is something you will probably want to keep in mind if you write methods that accept a variable number of arguments.

Documenting methods

Jest allows you to document the methods exposed by a server. You can do so by calling the describe method:

server.route('a.testAdd', function(a, b, cb) {
  process.nextTick(function() {
    cb(null, a + b);
  });
});
 
server.describe(
  'a.testAdd',
 
  'Adds two numbers together',
 
  [
    {
      name: 'a',
      type: 'number',
      description: 'The first operand',
      required: true
    },
 
    {
      name: 'b',
      type: 'number',
      description: 'The second operand',
      required: true
    }
  ],
 
  {
    type: 'number',
    description: 'The resulting value.'
  }
);

Closing a server

You can completely close a server by calling its destroy method. This will close all sockets down and disconnect all clients immediately.

Writing a client

Clients are meant to consume the methods provided by a server. Jest's client allows to connect to more than one server and automatically load balance method calls between them using a simple round-robin algorithm.

Note that Jest's client is not compatible with generic JSON-RPC servers. It only works with Jest servers.

Client basics

The typical client instantiation looks like this:

var client = jest.client(
    { host : 'service-1.internal' , port : 1234 },
    { host : 'service-2.internal' , port : 3212 }
);
 
client.auth = function(socket, cb) {
    process.nextTick(function() {
        cb(null, { key : 'sekret-key' });
    });
};
 
client.on('authError', function(err, socket) {
    // An authentication error has occurred
});
 
client.once('ready', function() {
    // At least one socket is ready to accept requests
});

Jest assumes that authentication is on (this is required because, at least currently, Jest server provide service discovery in response to the authentication handshake). The auth method allows you to provide arbitrary data that the server can use to identify your client. In the event of an authentication failure, the client emits an authError event.

As soon as the client has connected to at least one server, the client emits the ready event, at which point you can start issuing method calls to the server.

Note that ready is called whenever the client transitions from a state in which no connections are available to a state in which there is at least one. Therefore, it's possible (and likely) that the event will be fired multiple times throughout the life of your app—and you should plan accordingly.

Handling connectivity failures

Jest automatically handles connection failures by quarantining the offending socket and attempting to reconnect to the corresponding server. Because it is designed to work in environments that put a premium on availability, the reconnection retries are very aggressive: the first one occurs immediately upon disconnection; if that is unsuccessful, the next occurs after 100ms, and successive ones at intervals that increase geometrically at the rate of 1.5x.

When no connections are available, the client issues an offline event to advise you that it cannot perform any method calls. Attempts to execute methods will be met with an error that has a property called offline set to true.

When a connectivity issue arises, Jest also terminates all pending method calls, and returns an instance of the Jest.errors.timeout class. To avoid unwanted side effects, the client won't try to automatically fail the request over to the next client.

Handling timeouts

By default, the client times out a method calls after ten seconds. You can change this value by setting the timeout property of the client.

Executing methods

The client leverages Jest's service discovery and introspection features to greatly simplify the process of calling a method.

Upon authentication, the client asks the server to provide a map of the methods it exposes and automatically creates a proxy object that can be used to make calls from your JavaScript code. In the process, it automatically unwinds namespaces if your methods follow dot notation.

For example, suppose that your server supports two methods called a.add and a.multiply. You can use them like this:

 
client.proxy.a.add(1, 2, function(err, result) {
    ...
});
 
client.proxy.a.multiply(1, 2, function(err, result) {
    ...
});

In addition to the traditional callback mechanism, the client also supports promises:

 
client.proxy.a.add(1, 2)
 
.then(function(result) {
    ...
})
 
.fail(function(err) {
    ...
})
 
.done();

Closing a client

You can call the destroy method to completely terminate a client. This will close all extant connections and shut down the load balancing mechanism.

Using the documentation tool

Jest comes with a documentation tool, available in bin/jest, that allows you to quickly introspect a running service from the command line, discovering the methods it exposes and their signatures.

The service takes these parameters:

Usage: jest [options] <hostname> <port> [regex]

Options:

  -h, --help     output usage information
  -V, --version  output the version number
  -a <auth>      Set the authentication JSON payload

For example, this extracts every method available from a service running on server-1.internal at port 1234, with the authentication parameter sekret:

./bin/jest -a sekret server-1.internal 1234

    a.test    Adds one to a value
    a.testAdd Adds two numbers together
    b.test    Returns 10

If you want to learn more about a specific method, you can drill down further:


./bin/jest -a sekret server-1.internal 1234 a.test

Method a.test(s) void:

     s any The value 

    Return value: (any) The resulting value

Server API

jest.server([server])

Returns an instance of net.Server configured for Jest use.

You can optionally pass in a pre-configured net.Server; if you don't, Jest will create one for you.

server.context(args)

Returns the context associated with a running method handler.

The args parameter must be the arguments parameter of a method handler, or context will return an unpredictable value.

server.describe(route, description, args, retval)

Provides a description for route, which must be registered prior to calling this method. The description parameter offers a general description of the route, while args can be one of:

  • null or undefined, to indicate that the route takes no arguments
  • A single argument descriptor, which indicates that the route takes exactly one argument
  • An array of one or more argument descriptors, if the route takes more than one argument.

Argument descriptors are hashes that contain these properties:

  • The name of the argument
  • A description of its purpose
  • An optional type
  • An optional required Boolean if the argument is required

The retval argument describes the return value of the route. It can either be omitted, in which case the route is assumed to be void, or be a hash that contains name, description, and type properties.

server.destroy()

Completely shuts down the server, immediately disconnecting all clients.

server.proute(name, handler)

Adds a promises-based route handler.

Internally, Jest uses Kris Kowal's Q library to handle promise fulfillment.

server.route(name, handler)

Adds a callback-based route handler.

The name must be unique.

Event: jestAudit

function(method, params, result, timeTrace, context, id)

Emitted whenever a route completes successfully for audit purposes. method the name of the method being called, params the arguments that were passed to it, and result the route's return value. timeTrace provides information about execution times, context is the authentication context associated with the callign client, and id is the unique ID of the transaction.

Event: jestError

function(err, method, params, timeTrace, context, id)

Emitted whenever a route completes with an error for audit purposes. err is the error that was emitted, method the name of the method being called, and params the arguments that were passed to it. timeTrace provides information about execution times, context is the authentication context associated with the callign client, and id is the unique ID of the transaction.

Client API

jest.client(options[, options...])

Creates and returns a new client.

options can be any combination of configuration options that can be passed to net.Socket.connect(). An arbitrary number of option sets can be used, each representing a server on which the client will load-balance requests.

client.auth

A callback-based function that provides authentication data when challenged by a server. The function takes a socket and a callback parameter.

client.destroy()

Completely shuts down and destroys a client, immediately terminating all extant connections and method calls.

client.proxy

A proxy that exposes all the methods provided by the server. Any methods name that follow dot-notation will be unwound and represented as if they were properties of the proxy itself.

Methods can transparently be called using either a traditional callback syntax, or by using promises.

client.ready

A Boolean property that indicates whether the client is capable of processing requests (that is, whether at least one server has been successfully contacted)

client.timeout

A property that indicates the number of milliseconds after which a request is considered to have timed out.

Event: authError

function(err, socket)

Emitted when the client encounters err (an instance of Jest.errors.auth) while attempting to authenticate itself to the server at socket. Note that the client will continue to attempt and authenticate with the remote host using a geometric retry interval.

Event: connect

function(socket)

Emitted when socket connects successfully to a server, but before authentication is performed.

Event: disconnect

function(socket)

Emitted when socket is disconnected.

Event: error

function(err)

Emitted when a communication error occurs and the client cannot communicate with the server due to an unknown failure.

Event: ready

function()

Emitted when the client goes from a state in which it cannot process requests to one in which it can. In practice, this event will be emitted any time the client goes from having zero active connection to one—which is likely to happen multiple times throughout the life of your app.

It is, therefore, important to plan accordingly (for example by using EventEmitter.once instead of EventEmitter.on when listening to this event) to prevent handlers being fired more than once.

Event: offline

function()

Emitted when the client goes from a state in which it can process requests to one in which it cannot. In practice, this event will be emitted any time the client goes from having at least on active connection to none—which is likely to happen multiple times throughout the life of your app if temporary network issues arise.

It is, therefore, important to plan accordingly (for example by using EventEmitter.once instead of EventEmitter.on when listening to this event) to prevent handlers being fired more than once.

Limitations

The current version of Jest is experimental, and should not (yet) be used in production.

Contributing

Fixes and contributions are warmly welcome, provided that they are accompanied by the appropriate test cases.

Package Sidebar

Install

npm i node-jest

Weekly Downloads

10

Version

0.2.0

License

BSD

Last publish

Collaborators

  • mtabini