UNPKG

@yoursunny/mole-rpc

Version:

Transport agnostic spec compliant JSON RPC client and server

218 lines (169 loc) 6.28 kB
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;