Tiny transport agnostic JSON-RPC 2.0 client and server which can work both in NodeJs, Browser, Electron etc
- Transport agnostic (works with HTTP, MQTT, Websocket, Browser post message etc). Here is the list of supported transports - https://www.npmjs.com/search?q=keywords:mole-transport
- Zero dependencies. Mole-RPC itself has zero dependencies.
- Works both in NodeJs and in a browser (both client anf server). For example, you can use it to send request to webworker in your browser.
- Bidirectional websocket connections support via WS tranport. For example, you want a JSON RPC server which handles remote calls but the same time you want to send commands in opposite direction using the same connection.So, you can use connection initiated by any of the sides for the server and the client the same time.
- Server can use several transports the same time. For example, you want an RPC server that accepts connections from your local workers by TCP and from Web browser by websocket. You can pass as many transports as you wish.
- Transport independent ping/pong mechanism. You can check server availability and connection stability even if your transport doesn't support native ping/pong API.
- Lightweight
- Modern API. Totally based on Promises and supports Proxified interface
- Supports all features of JSON-RPC 2.0 (batches, notifications etc)
- Easy to create own transport. Transports have simple API as possible, so it is very easy to add a new transport. See "How to create own transport?" section.
Yet another JSON-RPC library? Why do we need it? The reality, that there is no transport agnostic JSON-RPC library for nodejs. One of our projects required JSON RPC over MQTT and unfortunetely we were not able to find a good solution.
Key issues with existing libraries:
- No multiple transports support.
- Not possible to add a new transport.
- For some possible, but you need to duplicate almost all protocol logic in every transport.
- Bidirectional websocket connections are not supported.
- Reverse connection (when server connect to client via websocket and after that the client send commands to the server)
- Leaking transport abtractions (like topic name in MQTT needs to be the same as method name).
- Huge codebase with tons of classes (JSON RPC should not look like this)
- No promise based API.
- Everything is bundled to one large package with high level of coupling.
- Bugs, not tests.
- etc
Mole-RPC solves all of the issues described above.
This module is transport agnostics. So, you can choose any transport you need
In this example, we use WebSocketServer for RPC server but you you can use simple WS server transport as well. This can be useful for the case when server connects to client (you can bypass firefall in this way).
Server
const MoleServer = require('mole-rpc/MoleServer');
const TransportServerWSS = require('mole-rpc-transport-ws/TransportServerWSS');
const WebSocket = require('ws');
const WSS_PORT = 12345;
function sum(a, b) { return a + b }
function multiply(a, b) { return a * b }
async function main() {
const server = new MoleServer({
transports: prepareTransports()
});
server.expose({ sum, multiply });
await server.run();
}
function prepareTransports() {
return [
new TransportServerWSS({
wss: new WebSocket.Server({
port: WSS_PORT
})
})
];
}
main().catch(console.error);
Client (with Proxy support)
const MoleClient = require('mole-rpc/MoleClientProxified');
const X = require('mole-rpc/X');
const TransportClientWS = require('mole-rpc-transport-ws/TransportClientWS');
const WebSocket = require('ws');
const WSS_PORT = 12345;
async function main() {
const client = new MoleClient({
requestTimeout: 1000,
transport: prepareTransport()
});
try {
console.log( await client.sum(2, 3) );
} catch (error) {
if (error instanceof X.ExecutionError) {
console.log('ERROR', error.data);
} else {
throw error;
}
}
}
function prepareTransport() {
return new TransportClientWS({
wsBuilder: () => new WebSocket(`ws://localhost:${WSS_PORT}`)
});
}
main().then(console.log, console.error);
If you use modern JavaScript you can use proxified client. It allows you to do remote calls very similar to local calls
import MoleClientProxified from 'mole-rpc/MoleClientProxified';
// choose any transports here
// https://www.npmjs.com/search?q=keywords:mole-transport
const transport = new TransportClient();
const calculator = new MoleClientProxified({ transport });
const result1 = await calculator.sum(1, 3);
const result2 = await calculator.asyncSum(2, 3);
// Send JSON RPC notification (fire and forget)
// server will send no response
await calculator.notify.sum(3, 2);
import MoleClient from 'mole-rpc/MoleClient';
// choose any transports here
// https://www.npmjs.com/search?q=keywords:mole-transport
const transport = new TransportClient();
const client = new MoleClient({ transport });
const result1 = await client.callMethod('sum', [1, 3]);
const result2 = await client.callMethod('sum', [2, 3]);
// Send JSON RPC notification (fire and forget)
// server will send no response
await client.notify('sum', [2, 3]);
const MoleClient = require('mole-rpc/MoleClient');
const X = require('mole-rpc/X');
// choose any transports here
// https://www.npmjs.com/search?q=keywords:mole-transport
const transport = new TransportClient();
const client = new MoleClient({ transport });
try {
await client.ping();
} catch (error) {
if (error instanceof X.RequestTimeout) {
console.log('Ping failed. Server is unavailable');
} else if (error instanceof X.MethodNotFound) {
console.log('Ping method not found. Update your mole-rpc server');
} else {
throw error;
}
}
You can expose instance directly. Methods which start with underscore will not be exposed. Built-in methods of Object base class will not be exposed.
import MoleServer from 'mole-rpc/MoleServer';
class Calculator {
sum(a, b) {
return a + b;
}
asyncSum(a, b) {
return new Promise((resolve, reject) => {
resolve(this.sum(a, b));
});
}
_privateMethod() {
// will not be exposed
}
}
const calculator = new Calculator();
// choose any transports here
// https://www.npmjs.com/search?q=keywords:mole-transport
const transports = [new TransportServer()];
const server = new MoleServer({ transports: [] });
server.expose(calculator);
await server.run();
You can expose functions directly
import MoleServer from "mole-rpc/MoleServer";
function sum(a, b) {
return a+b;
}
function asyncSum(a, b) {
return new Promise((resolve, reject) {
resolve( sum(a, b) );
});
}
// choose any transports here
// https://www.npmjs.com/search?q=keywords:mole-transport
const transports = [ new TransportServer() ];
const server = new MoleServer({ transports });
server.expose({
sum,
asyncSum
});
await server.run();
When an rpc call encounters an error, the server will return an object with an error code. See JSON RPC 2.0 Specification for details.
Getting an error Mole RPC Client will throw (reject promise) a corresponding exception.
List of available exception classes:
- Base
- MethodNotFound
- InvalidRequest
- InvalidParams
- InternalError
- ParseError
- ServerError - custom server errors
- RequestTimeout - Request exceeded maximum execution time
- ExecutionError - Method has returned an error.
Every exception object has following properties:
- "code" - numeric code from the spec
- "message" - human readable message.
- "data" - additional data. Used only by ExecutionError, contains error returned by method
How to return an error from method?
Nothing special required. Just reject promise or throw an exception.
function divide(a, b) {
if (b == 0) throw "devision by zero";
// throw 'new Error("devision by zero")' will behave the same
return a / b;
}
function loadUser(userId) {
...
// you can throw an object
return Promise.reject({ error: 'NOT_EXISTING_USER'})
}
server.expose({ divide, loadUser });
How to handle the error?
Nothing special. Just catch the exception.
const X = require('mole-rpc/X');
async function main() {
...
try {
await client.divide(2, 3);
} catch (error) {
if (error instanceof X.ExecutionError) {
console.log('METHOD RETURNED ERROR', error.data);
} else if (error instanceof X.RequestTimeout) {
console.log('METHOD EXCEEDED ALLOWED EXECUTION TIME');
} else {
throw error;
}
}
}
// Proxified client: explicit call
await calculator.callMethod.sum(1, 2); // the same as "calculator.sum(1, 2)"
// Can be usefull if your server method is a reserverd name.
// For example to make a call to remote "notify" method.
await proxifiedClient.callMethod.notify("Hello");
// Proxified client: run request in parallel
const promises = [
calculator.sum(1, 2);
calculator.notify.sum(1, 2);
];
const results = await Promise.all(promises);
// Simple client: run in parallel
const promises = [
client.callMethod('sum', [1, 2]);
client.notify('sum', [1, 2]);
];
const results = await Promise.all(promises);
// Simple client: run batch
const results = await client.runBatch([
// [methodName, params, mode]
['sum', [1, 3]],
['sum', [2, 5], 'notify'],
['sum', [7, 9], 'callMethod'], // "callMethod" is optional
]);
// Result structure
[
{success: true, result: 123},
null, // no response for notification
{success: false, error: errorObject}
];
To communicate with web worker, in most cases, you will try to simulate JSON RPC having "id", "method", "params" in each request and "id", "result" in each response.
With Mole RCP there is no need to use custom hacks. Just use mole-rpc-transport-webworker.
You have a device (or service) in local network and want to manage it. You cannot get to it from Internet, as the device is hidden behind NAT. But your device can connect to your internet server. So, with Mole RPC your device (RPC Server) can connect to the your internet server (RPC Client) and after that the internet server will be able to call methods on the devices hidden behind NAT.
Here is an example - https://github.com/koorchik/node-mole-rpc-transport-ws/tree/master/examples/server-connects-to-client
This case is rather hard to implement with other JSON RPC modules but with Mole RPC it works by design.
You have a lot microservices and you want allow them to communicate with each other. The best solutions here is to have a message broker. Mole RPC has MQTT transport which will allow to setup the communication easily.
Websocket is a good options for it. With websocker transport you can connect browser to server and the same time it suitable for connecting to server processes. It is not only option, you not limited to use any transport you wish.
You can pass multiple transports to MoleServer. This transport can be of different types. For example, you can expose the same methods via MQTT and WebSockets the same time.
Transports have simple API as possible, so it is very easy to add a new transport. MoleRPC has strong separation between protocol handler and transports. Transports know nothing about what is inside payload. Therefore, they are very simple. Usually, is is just two classes with 1-2 methods.
The best way to start is just to look at source code of existing implemenations - https://www.npmjs.com/search?q=keywords:mole-transport
Moreover, we have created an AutoTester for transports. Use AutoTester to cover 95% of cases and just add several tests to cover transport specific logic like reconnections etc.