passage-rpc

Client and server side JSON-RPC websockets

Usage no npm install needed!

<script type="module">
  import passageRpc from 'https://cdn.skypack.dev/passage-rpc';
</script>

README

Passage RPC

Client and server side JSON-RPC 2.0 websockets

This is a websocket subprotocol implementation designed for remote procedure calls and server responses.

http://www.jsonrpc.org/specification

Installation from NPM

Install the package.

npm i passage-rpc --save

Import it into your server side script.

const Passage = require('passage-rpc');

Import it into your client side script.

import Passage from 'passage-rpc';

Installation using IIFE

Download and include the library onto your page, a minified version can be found in the /dist directory.

<script src="/javascripts/passage-rpc.min.js"></script>

Setup

Create a new instance of Passage providing a uri and set of options.

const options = {
    reconnect: true,
};

const passage = new Passage('wss://example.com', options);

passage.on('rpc.open', () => {
    console.log('connected!');
});

Options

requestTimeout <default: 6000>

The amount of time the server can take responding to requests before a timeout.

reconnect <default: false>

Whether the client should attempt to reconnect when disconnected.

reconnectTimeout <default: 2000>

The amount of time to wait between reconnection attempts.

reconnectTries <default: 60>

Maximum number of reconnection attempts.

persistent <default: false>

Whether the library should keep trying to reconnect forever. The library will first use the reconnect feature, and if it runs out of tries will use this feature.

persistentTimeout <default: 10 * 60 * 1000>

The amount of time to wait between each persistent reconnection attempt.

Events

When the server sends a notification to your application, it triggers an event. This library uses node's EventEmitter therefore events can be listened for in a common way.

| method | description | params | | - | - | - | | rpc.message | Message was received. | data | | rpc.open | Connection established. | | | rpc.close | Connection closed. | reconnecting | | rpc.error | Error has occurred. | Error |

passage.on('myapp.welcome', (params) => {
    console.log(params);
});

On rpc.close a parameter is passed representing whether the module intends to reconnect. This will only be true if the reconnect option has been set to true, and the maximum number of reconnectTries has not been met. The persistent option will not affect this.

Instance

close (code?: number, reason?: string) => void

Closes the connection using the optional code and reason given.

connect () => void

This will close the connection, then reconnect.

readyState

The ready state of the connection, useful to compare against named ready states available on the constructor.

| name | value | location | | - | - | - | | CONNECTING | 0 | Passage.CONNECTING | | OPEN | 1 | Passage.OPEN | | CLOSING | 2 | Passage.CLOSING | | CLOSED | 3 | Passage.CLOSED |

if (passage.readyState !== Passage.OPEN) {
    console.log('Not connected');
}

send (method: string, [params: any], callback?: (error: Error, result?: any) => void, timeout?: number) => void

If a callback is provided, then the server will respond once it has finished processing. It may return an error or a result once completed but not both. Params will be available for consumption on the server. If a timeout is provided it will override the default requestTimeout from options.

passage.send('myapp.hello');

passage.send('myapp.hello', (error, response) => {
    if (error) throw error;
    console.log(response);
});

passage.send('myapp.hello', { my: 'params' });

passage.send('myapp.hello', { my: 'params' }, (error, response) => {
    if (error) throw error;
    console.log(response);
});

Note: If a callback is not provided and the connection is not available this method will throw an error.

Sending more than one request at the same time

JSON-RPC supports sending an array of messages. To do this the library exposes helper methods for you to use. A full example sending multiple messages can be seen below.

const callback = (error, response) => {
    console.log(response);
};

const messages = [
    passage.buildMessage('myapp.request', callback),
    passage.buildMessage('myapp.request', { code: 'the stork swims at midnight' }),
    passage.buildMessage('myapp.alert', 'important message')
];

const payload = JSON.stringify(messages);
passage.connection.send(payload);

buildMessage (method: string, [params: any], callback?: (error: Error, result?: any) => void, timeout?: number) => Object

This creates a simple object for consumption by the server. It takes the same values as the send method, however does not stringify or send the message. If a callback is provided it will timeout, if you use this function you should send your payload soon.

expectResponse (callback: (error: Error, result?: any) => void, timeout?: number) => number

Returns a number representing a message id. The callback will timeout if a response containing the message id is not received in time.

Server setup

The server implementation is built on the npm ws library and shares several similarities with it. There are a few additional options and events utilised for JSON-RPC.

const Passage = require('passage-rpc');

const options = {
    port: 8000,
    heartrate: 30000,
    methods: {
        'myapp.hello': () => 'hi';
    }
};

const server = new Passage.Server(options);

server.on('rpc.listening', () => {
    console.log('Listening on port: ' + port);
});

heartrate <default: 30000>

Periodically each of the connected clients will be pinged terminating ones for which there is no response. Checking every 30 seconds is a good default but you might wish to adjust this.

methods <default: {}>

The methods parameter is a dictionary of procedures your server listens to from the client. In this case if a client sends myapp.hello the server will run the associated function and respond with "hi". You may return an Error instead, or even nothing at all.

Whether the server responds is dependent on the client. If the client is not waiting for a response the server will not send one. Every method is expected to be in the following format.

(params: any, client: ConnectedClient) => any

Must return a Promise if you are doing something which is asyncronous.

For example:

const methods = {
    'myapp.findUser': async (userId) => {
        const user = await findUser(userId);
        return { user };
    }
};

Errors

When being returned from the server, it should be a Error instance. If your method is a Promise it is acceptable to reject. The following attributes are transmitted across the network, message name code and data. The data attribute contains any additional information you would like to include but must be stringifiable into JSON.

There are some errors the library may return itself in callbacks.

| name | code | message | | - | - | - | | Timeout | 408 | Timeout | | ServiceUnavailable | 503 | Service unavailable | | ParseError | -32700 | Parse error | | InvalidRequest | -32600 | Invalid request | | MethodNotFound | -32601 | Method not found |

Server events

Events on the server are handled differently than on the client in most cases, but there are important ones.

| method | description | params | | - | - | - | | rpc.listening | Server is listening. | | | rpc.connection | Connection established. | ConnectedClient, req object | | rpc.error | Error has occurred. | Error |

Server instance

close (callback?: () => void) => void

Closes the server then runs the callback.

clients

Set of connected clients.

ConnectedClient instance

The rpc.connection event offers a connected client instance, and a req object. The connected client has its own events separate from the server.

| method | description | params | | - | - | - | | rpc.message | Message was received. | data | | rpc.close | Connection closed. | | | rpc.error | Error has occurred. | Error |

close (code?: number, reason?: string) => void

Closes the connection using the optional code and reason given.

readyState

The ready state of the connection.

send (method: string, [params: any], callback?: (error: Error) => void) => void

Send a notification to the connected client. The server cannot expect a response from the client with the current implementation. The callback is only to let you know that the message was sent successfully.

client.send('myapp.welcome');

client.send('myapp.welcome', { my: 'params' });

client.send('myapp.welcome', (error) => {
    if (!error) console.log('Notification sent');
});

client.send('myapp.welcome', { my: 'params' }, (error) => {
    if (!error) console.log('Notification sent');
});

Note: If a callback is not provided and the connection is not available this method will throw an error.

Sending more than one notification at the same time

A full example from the server can be seen below.

const messages = [
    client.buildMessage('myapp.notify'),
    client.buildMessage('myapp.notify', { friends: 'forevah' }),
    client.buildMessage('myapp.alert'),
];

const payload = JSON.stringify(messages);
client.connection.send(payload, (error) => {
    if (!error) console.log('Notifications sent');
});

buildMessage (method: string, params?: any) => Object

This creates a simple object for consumption by the client. It takes nearly the same values as the send method, however does not stringify or send the message and does not accept a callback.

Example

Server

const Passage = require('passage-rpc');

const port = 8080;
const methods = {
    'myapp.cats.list': async () => {
        const cats = await getCats();
        return { cats };
    }
};

const server = new Passage.Server({ port, methods });

server.on('rpc.listening', () => {
    console.log('Server listening on port: ' + port);
});

server.on('rpc.connection', (client) => {
    setTimeout(() => {
        client.send('myapp.hi', { message: 'Connected 10 seconds ago.' });
    }, 10000);
});

Client

const Passage = require('passage-rpc');

const passage = new Passage('ws://localhost:8080');

function processResponse (error, response) {
    if (error) throw error;
    console.log(`Returned ${response.cats.length} cat(s).`);
}

passage.on('myapp.hi', (params) => {
    console.log(params.message);
    passage.send('myapp.cats.list', processResponse);
});