@yoursunny/mole-rpc
Version:
Transport agnostic spec compliant JSON RPC client and server
218 lines (169 loc) • 6.28 kB
JavaScript
const X = require('./X');
const errorCodes = require('./errorCodes');
class MoleClient {
constructor({ transport, requestTimeout = 20000 }) {
if (!transport) throw new Error('TRANSPORT_REQUIRED');
this.transport = transport;
this.requestTimeout = requestTimeout;
this.pendingRequest = {};
this.initialized = false;
}
async callMethod(method, params) {
await this._init();
const request = this._makeRequestObject({ method, params });
return this._sendRequest({ object: request, id: request.id });
}
async notify(method, params) {
await this._init();
const request = this._makeRequestObject({ method, params, mode: 'notify' });
await this.transport.sendData(JSON.stringify(request));
return true;
}
async runBatch(calls) {
const batchId = this._generateId();
let onlyNotifications = true;
const batchRequest = [];
for (const [method, params, mode] of calls) {
const request = this._makeRequestObject({ method, params, mode, batchId });
if (request.id) {
onlyNotifications = false;
}
batchRequest.push(request);
}
if (onlyNotifications) {
return this.transport.sendData(JSON.stringify(batchRequest));
} else {
return this._sendRequest({ object: batchRequest, id: batchId });
}
}
async _init() {
if (this.initialized) return;
await this.transport.onData(this._processResponse.bind(this));
this.initialized = true;
}
_sendRequest({ object, id }) {
const data = JSON.stringify(object);
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
if (this.pendingRequest[id]) {
delete this.pendingRequest[id];
reject(new X.RequestTimout());
}
}, this.requestTimeout);
this.pendingRequest[id] = { resolve, reject, sentObject: object, timer };
return this.transport.sendData(data).catch(error => {
delete this.pendingRequest[id];
reject(error); // TODO new X.InternalError()
});
});
}
_processResponse(data) {
const response = JSON.parse(data);
if (Array.isArray(response)) {
this._processBatchResponse(response);
} else {
this._processSingleCallResponse(response);
}
}
_processSingleCallResponse(response) {
const isSuccessfulResponse = response.hasOwnProperty('result') || false;
const isErrorResponse = response.hasOwnProperty('error');
if (!isSuccessfulResponse && !isErrorResponse) return;
const resolvers = this.pendingRequest[response.id];
delete this.pendingRequest[response.id];
if (!resolvers) return;
clearTimeout(resolvers.timer);
if (isSuccessfulResponse) {
resolvers.resolve(response.result);
} else if (isErrorResponse) {
const errorObject = this._makeErrorObject(response.error);
resolvers.reject(errorObject);
}
}
_processBatchResponse(responses) {
let batchId;
const responseById = {};
const errorsWithoutId = [];
for (const response of responses) {
if (response.id) {
if (!batchId) {
batchId = response.id.split('|')[0];
}
responseById[response.id] = response;
} else if (response.error) {
errorsWithoutId.push(response.error);
}
}
if (!this.pendingRequest[batchId]) return;
const { sentObject, resolve, timer } = this.pendingRequest[batchId];
delete this.pendingRequest[batchId];
clearTimeout(timer);
const batchResults = [];
let errorIdx = 0;
for (const request of sentObject) {
if (!request.id) {
// Skip notifications
batchResults.push(null);
continue;
}
const response = responseById[request.id];
if (response) {
const isSuccessfulResponse = response.hasOwnProperty('result') || false;
if (isSuccessfulResponse) {
batchResults.push({
success: true,
result: response.result
});
} else {
batchResults.push({
success: false,
result: this._makeErrorObject(response.error)
});
}
} else {
batchResults.push({
success: false,
error: this._makeErrorObject(errorsWithoutId[errorIdx])
});
errorIdx++;
}
}
resolve(batchResults);
}
_makeRequestObject({ method, params, mode, batchId }) {
const request = {
jsonrpc: '2.0',
method
};
if (typeof params === "object") {
request.params = params;
}
if (mode !== 'notify') {
request.id = batchId ? `${batchId}|${this._generateId()}` : this._generateId();
}
return request;
}
_makeErrorObject(errorData) {
const errorBuilder = {
[errorCodes.METHOD_NOT_FOUND]: () => {
return new X.MethodNotFound();
},
[errorCodes.EXECUTION_ERROR]: ({ data }) => {
return new X.ExecutionError({ data });
}
}[errorData.code];
return errorBuilder ? errorBuilder(errorData) :
new Error(`${errorData.code} ${errorData.message}`);
}
_generateId() {
// from "nanoid" package
const alphabet = 'bjectSymhasOwnProp-0123456789ABCDEFGHIJKLMNQRTUVWXYZ_dfgiklquvxz';
let size = 10;
let id = '';
while (0 < size--) {
id += alphabet[(Math.random() * 64) | 0];
}
return id;
}
}
module.exports = MoleClient;