UNPKG

jaysonic

Version:

A feature rich JSON-RPC 1.0/2.0 compliant client and server library

771 lines (683 loc) 25.9 kB
"use strict"; function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } var _require = require("../../util/format"), formatRequest = _require.formatRequest, formatError = _require.formatError; var _require2 = require("../../util/constants"), ERR_CODES = _require2.ERR_CODES, ERR_MSGS = _require2.ERR_MSGS; var MessageBuffer = require("../../util/buffer"); var logging = require("../../util/logger"); /** * Creates an instance of the base client protocol class. * This is the class that all other client protocols inherit from. */ var JsonRpcClientProtocol = /*#__PURE__*/function () { /** * JsonRpcClientProtocol contructor * @param {class} factory Instance of [JsonRpcClientFactory]{@link JsonRpcClientFactory} * @param {(1|2)} version JSON-RPC version to make requests with * @param {string} delimiter Delimiter to use for message buffer * @property {class} factory Instance of [JsonRpcClientFactory]{@link JsonRpcClientFactory} * @property {class} connector The socket instance for the client * @property {(1|2)} version JSON-RPC version to use * @property {string} delimiter Delimiter to use for message buffer * @property {number} message_id Current message ID * @property {number} serving_message_id Current message ID. Used for external functions to hook into * @property {Object} pendingCalls Key value pairs for pending message IDs to promise resolve/reject objects * @property {Object.<string|number, JSON>} responseQueue Key value pairs for outstanding message IDs to response object * @property {Object} server Server host and port object {host: "x.x.x.x", port: xxxx} * @property {class} messageBuffer Instance of [MessageBuffer]{@link MessageBuffer} * */ function JsonRpcClientProtocol(factory, version, delimiter) { _classCallCheck(this, JsonRpcClientProtocol); if (!(this instanceof JsonRpcClientProtocol)) { return new JsonRpcClientProtocol(factory, version, delimiter); } this.factory = factory; this.connector = undefined; this.listener = undefined; this.delimiter = delimiter; this.version = version; this.message_id = 1; this.serving_message_id = 1; this.pendingCalls = {}; this.responseQueue = {}; this.server = this.factory.server; this._connectionTimeout = undefined; this.messageBuffer = new MessageBuffer(this.delimiter); } /** * Set the `connector` attribute for the protocol instance. * The connector is essentially the socket or connection instance for the client. * * @abstract * */ _createClass(JsonRpcClientProtocol, [{ key: "setConnector", value: function setConnector() { throw Error("function must be overwritten in subclass"); } /** * Make the connection to the server. * * Calls [setConnector]{@link JsonRpcClientProtocol#setConnector} to establish the client connection. * * Calls [listen]{@link JsonRpcClientProtocol#listen} if connection was successful, and will resolve the promise. * * Will retry connection on the `connectionTimeout` interval. * Number of connection retries is based on `remainingRetries`. * * If `null` is set for number of retries, then connections will attempt indefinitely. * * Will reject the promise if connect or re-connect attempts fail and there are no remaining retries. * * @returns Promise * */ }, { key: "connect", value: function connect() { var _this = this; return new Promise(function (resolve, reject) { return _this._retryConnection(resolve, reject); }); } /** * * Manage the connection attempts for the client. * * @param {Promise.resolve} resolve `Promise.resolve` passed from [connect]{@link JsonRpcClientProtocol#connect} * @param {Promise.reject} reject `Promise.reject` passed from [connect]{@link JsonRpcClientProtocol#connect} * * @returns Promise * * @private */ }, { key: "_retryConnection", value: function _retryConnection(resolve, reject) { var _this2 = this; this.setConnector(); this.connector.connect(this.server); this.connector.setEncoding("utf8"); this.connector.on("connect", function () { _this2.factory.remainingRetries = _this2.factory.options.retries; _this2.listener = _this2.connector; _this2.listen(); resolve(_this2.server); }); this.connector.on("error", function (error) { return _this2._onConnectionFailed(error, resolve, reject); }); this.connector.on("close", function (hadError) { if (!hadError) { _this2.factory.emit("serverDisconnected"); } }); } /** * * Handle connection attempt errors. Log failures and * retry if required. * * * @param {Error} error `node.net` system error (https://nodejs.org/api/errors.html#errors_common_system_errors) * @param {Promise.resolve} resolve `Promise.resolve` passed from [connect]{@link JsonRpcClientProtocol#connect} * @param {Promise.reject} reject `Promise.reject` passed from [connect]{@link JsonRpcClientProtocol#connect} * * @returns Promise * * @private */ }, { key: "_onConnectionFailed", value: function _onConnectionFailed(error, resolve, reject) { var _this3 = this; if (this.factory.remainingRetries > 0) { this.factory.remainingRetries -= 1; logging.getLogger().error("Failed to connect. Address [".concat(this.server.host, ":").concat(this.server.port, "]. Retrying. ").concat(this.factory.remainingRetries, " attempts left.")); } else if (this.factory.remainingRetries === 0) { this.factory.pcolInstance = undefined; return reject(error); } else { logging.getLogger().error("Failed to connect. Address [".concat(this.server.host, ":").concat(this.server.port, "]. Retrying.")); } this._connectionTimeout = setTimeout(function () { _this3._retryConnection(resolve, reject); }, this.factory.connectionTimeout); } /** * Ends connection to the server. * * Sets `JsonRpcClientFactory.pcolInstance` to `undefined` * * Clears the connection timeout * * @param {function} cb Called when connection is sucessfully closed */ }, { key: "end", value: function end(cb) { clearTimeout(this._connectionTimeout); this.factory.pcolInstance = undefined; this.connector.end(cb); } /** * Setup `this.listner.on("data")` event to listen for data coming into the client. * * Pushes received data into `messageBuffer` and calls * [_waitForData]{@link JsonRpcClientProtocol#_waitForData} */ }, { key: "listen", value: function listen() { var _this4 = this; this.listener.on("data", function (data) { _this4.messageBuffer.push(data); _this4._waitForData(); }); } /** * Accumulate data while [MessageBuffer.isFinished]{@link MessageBuffer#isFinished} is returning false. * * If the buffer returns a message it will be passed to [verifyData]{@link JsonRpcClientProtocol#verifyData} * * @private * */ }, { key: "_waitForData", value: function _waitForData() { while (!this.messageBuffer.isFinished()) { var message = this.messageBuffer.handleData(); try { this.verifyData(message); } catch (e) { this.gotError(e); } } } /** * Verify the incoming data returned from the `messageBuffer` * * Throw an error if its not a valid JSON-RPC object. * * Call [gotNotification]{@link JsonRpcClientProtocol#gotNotification} if the message a notification. * * Call [gotBatch]{@link JsonRpcClientProtocol#gotBatch} if the message is a batch request. * * @param {string} chunk Data to verify */ }, { key: "verifyData", value: function verifyData(chunk) { try { // will throw an error if not valid json var message = JSON.parse(chunk); if (Array.isArray(message)) { this.gotBatch(message); } else if (!(message === Object(message))) { // error out if it cant be parsed throw SyntaxError(); } else if (!("id" in message)) { // no id, so assume notification this.gotNotification(message); } else if (message.error) { // got an error back so reject the message var id = message.id; var _message$error = message.error, code = _message$error.code, data = _message$error.data; var errorMessage = message.error.message; this._raiseError(errorMessage, code, id, data); } else if ("result" in message) { // Got a result, so must be a response this.gotResponse(message); } else { var _code = ERR_CODES.unknown; var _errorMessage = ERR_MSGS.unknown; this._raiseError(_errorMessage, _code, null); } } catch (e) { if (e instanceof SyntaxError) { var _code2 = ERR_CODES.parseError; var _errorMessage2 = "Unable to parse message: '".concat(chunk, "'"); this._raiseError(_errorMessage2, _code2, null); } else { throw e; } } } /** * Called when the received `message` is a notification. * Emits an event using `message.method` as the name. * The data passed to the event is the `message`. * * @param {JSON} message A valid JSON-RPC message object */ }, { key: "gotNotification", value: function gotNotification(message) { this.factory.emit(message.method, message); } /** * Called when the received message is a batch. * * Calls [gotNotification]{@link JsonRpcClientProtocol#gotNotification} for every * notification in the batch. * * Calls [gotBatchResponse]{@link JsonRpcClientProtocol#gotBatchResponse} otherwise. * * @param {JSON[]} message A valid JSON-RPC batch message */ }, { key: "gotBatch", value: function gotBatch(message) { var _this5 = this; // possible batch request message.forEach(function (res) { if (res && res.method && !res.id) { _this5.gotNotification(res); } }); this.gotBatchResponse(message); } /** * Called when the received message is a response object from the server. * * Gets the response using [getResponse]{@link JsonRpcClientProtocol#getResponse}. * * Resolves the message and removes it from the `responseQueue`. Cleans up any * outstanding timers. * * @param {JSON} message A valid JSON-RPC message object */ }, { key: "gotResponse", value: function gotResponse(message) { this.serving_message_id = message.id; this.responseQueue[message.id] = message; try { var response = this.getResponse(message.id); this.pendingCalls[message.id].resolve(response); delete this.responseQueue[message.id]; this.factory.cleanUp(message.id); } catch (e) { if (e instanceof TypeError) { // response id likely not in the queue logging.getLogger().error("Message has no outstanding calls: ".concat(JSON.stringify(message))); } } } /** * Get the outstanding request object for the given ID. * * @param {string|number} id ID of outstanding request */ }, { key: "getResponse", value: function getResponse(id) { return this.responseQueue[id]; } /** * Send a message to the server * * @param {string} request Stringified JSON-RPC message object * @param {function=} cb Callback function to be called when message has been sent */ }, { key: "write", value: function write(request, cb) { this.connector.write(request, cb); } /** * Generate a stringified JSON-RPC message object. * * @param {string} method Name of the method to use in the request * @param {Array|JSON} params Params to send * @param {boolean=} id If true it will use instances `message_id` for the request id, if false will generate a notification request * @example * client.message("hello", ["world"]) // returns {"jsonrpc": "2.0", "method": "hello", "params": ["world"], "id": 1} * client.message("hello", ["world"], false) // returns {"jsonrpc": "2.0", "method": "hello", "params": ["world"]} */ }, { key: "message", value: function message(method, params) { var id = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true; var request = formatRequest({ method: method, params: params, id: id ? this.message_id : undefined, version: this.version, delimiter: this.delimiter }); if (id) { this.message_id += 1; } return request; } /** * Send a notification to the server. * * Promise will resolve if the request was sucessfully sent, and reject if * there was an error sending the request. * * @param {string} method Name of the method to use in the notification * @param {Array|JSON} params Params to send * @return Promise * @example * client.notify("hello", ["world"]) */ }, { key: "notify", value: function notify(method, params) { var _this6 = this; return new Promise(function (resolve, reject) { var request = _this6.message(method, params, false); try { _this6.write(request, function () { resolve(request); }); } catch (e) { // this.connector is probably undefined reject(e); } }); } /** * Send a request to the server * * Promise will resolve when a response has been received for the request. * * Promise will reject if the server responds with an error object, or if * the response is not received within the set `requestTimeout` * * @param {string} method Name of the method to use in the request * @param {Array|JSON} params Params to send * @returns Promise * @example * client.send("hello", {"foo": "bar"}) */ }, { key: "send", value: function send(method, params) { var _this7 = this; return new Promise(function (resolve, reject) { var request = _this7.message(method, params); _this7.pendingCalls[JSON.parse(request).id] = { resolve: resolve, reject: reject }; try { _this7.write(request); } catch (e) { // this.connector is probably undefined reject(e); } _this7._timeoutPendingCalls(JSON.parse(request).id); }); } /** * Method used to call [message]{@link JsonRpcClientProtocol#message}, [notify]{@link JsonRpcClientProtocol#notify} and [send]{@link JsonRpcClientProtocol#send} * * @returns Object * @example * client.request().send("hello", ["world"]) * client.request().notify("foo") * client.request().message("foo", ["bar"]) */ }, { key: "request", value: function request() { var self = this; return { message: this.message.bind(self), send: this.send.bind(self), notify: this.notify.bind(self) }; } /** * Used to send a batch request to the server. * * Recommend using [message]{@link JsonRpcClientProtocol#message} to construct the message objects. * * Will use the IDs for the requests in the batch in an array as the keys for `pendingCalls`. * * How a client should associate batch responses to their requests is not in the spec, so this is the solution. * * @param {Array} requests An array of valid JSON-RPC message objects * @returns Promise * @example * client.batch([client.message("foo", ["bar"]), client.message("hello", [], false)]) */ }, { key: "batch", value: function batch(requests) { var _this8 = this; return new Promise(function (resolve, reject) { var batchIds = []; var batchRequests = []; var _iterator = _createForOfIteratorHelper(requests), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var _request = _step.value; var json = JSON.parse(_request); batchRequests.push(json); if (json.id) { batchIds.push(json.id); } } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } _this8.pendingCalls[String(batchIds)] = { resolve: resolve, reject: reject }; var request = JSON.stringify(batchRequests); try { _this8.write(request + _this8.delimiter); } catch (e) { // this.connector is probably undefined reject(e); } _this8._timeoutPendingCalls(String(batchIds)); }); } /** * Associate the ids in the batch message to their corresponding `pendingCalls`. * * Will call [_resolveOrRejectBatch]{@link JsonRpcClientProtocol#_resolveOrRejectBatch} when object is determined. * * @param {Array} batch Array of valid JSON-RPC message objects */ }, { key: "gotBatchResponse", value: function gotBatchResponse(batch) { var batchResponseIds = []; batch.forEach(function (message) { if ("id" in message) { batchResponseIds.push(message.id); } }); if (batchResponseIds.length === 0) { // dont do anything here since its basically an invalid response return; } // find the resolve and reject objects that match the batch request ids for (var _i = 0, _Object$keys = Object.keys(this.pendingCalls); _i < _Object$keys.length; _i++) { var ids = _Object$keys[_i]; var arrays = [JSON.parse("[".concat(ids, "]")), batchResponseIds]; var difference = arrays.reduce(function (a, b) { return a.filter(function (c) { return !b.includes(c); }); }); if (difference.length === 0) { this.factory.cleanUp(ids); this._resolveOrRejectBatch(batch, batchResponseIds); } } } /** * Returns the batch response. * * Overwrite if class needs to reformat response in anyway (i.e. in [HttpClientProtocol]{@link HttpClientProtocol}) * * @param {Array} batch Array of valid JSON-RPC message objects */ }, { key: "getBatchResponse", value: function getBatchResponse(batch) { return batch; } /** * Will reject the request associated with the given ID with a JSON-RPC formated error object. * * Removes the id from `pendingCalls` and deletes outstanding timeouts. * * @param {string|number} id ID of the request to timeout * @private */ }, { key: "_timeoutPendingCalls", value: function _timeoutPendingCalls(id) { var _this9 = this; this.factory.timeouts[id] = setTimeout(function () { _this9.factory.cleanUp(id); try { var error = JSON.parse(formatError({ jsonrpc: _this9.version, delimiter: _this9.delimiter, id: typeof id === "string" ? null : id, code: ERR_CODES.timeout, message: ERR_MSGS.timeout })); _this9.pendingCalls[id].reject(error); delete _this9.pendingCalls[id]; } catch (e) { if (e instanceof TypeError) { logging.getLogger().error("Message has no outstanding calls. ID [".concat(id, "]")); } } }, this.factory.requestTimeout); } /** * Resolve or reject the given batch request based on the given batch IDs. * * @param {Array} batch Valid JSON-RPC batch request * @param {string} batchIds Stringified list of batch IDs associated with the given batch * @private */ }, { key: "_resolveOrRejectBatch", value: function _resolveOrRejectBatch(batch, batchIds) { var _this10 = this; var batchResponse = this.getBatchResponse(batch); try { var invalidBatches = []; batch.forEach(function (message) { if (message.error) { // reject the whole message if there are any errors _this10.pendingCalls[batchIds].reject(batchResponse); invalidBatches.push(batchIds); } }); if (invalidBatches.length !== 0) { invalidBatches.forEach(function (id) { delete _this10.pendingCalls[id]; }); } else { this.pendingCalls[batchIds].resolve(batchResponse); delete this.pendingCalls[batchIds]; } } catch (e) { if (e instanceof TypeError) { // no outstanding calls logging.getLogger().log("Batch response has no outstanding calls. Response IDs [".concat(batchIds, "]")); } } } /** * Throws an Error whos message is a JSON-RPC error object * * @param {string} message Error message * @param {number} code Error code * @param {string|number=} id ID for error message object * @param {*=} data Optional data to include about the error * @throws Error * @private */ }, { key: "_raiseError", value: function _raiseError(message, code, id, data) { var error = formatError({ jsonrpc: this.version, delimiter: this.delimiter, id: id, code: code, message: message, data: data }); throw new Error(error); } /** * Calls [rejectPendingCalls]{@link JsonRpcClientProtocol#rejectPendingCalls} with error object. * * If the object cannot be parsed, then an unkown error code is sent with a `null` id. * * @param {string} error Stringified JSON-RPC error object */ }, { key: "gotError", value: function gotError(error) { var err; try { err = JSON.parse(error.message); } catch (e) { err = JSON.parse(formatError({ jsonrpc: this.version, delimiter: this.delimiter, id: null, code: ERR_CODES.unknown, message: JSON.stringify(error, Object.getOwnPropertyNames(error)) })); } this.rejectPendingCalls(err); } /** * Reject the pending call for the given ID is in the error object. * * If the error object has a null id, then log the message to the logging .getLogger(). * * @param {string} error Stringified JSON-RPC error object * */ }, { key: "rejectPendingCalls", value: function rejectPendingCalls(error) { try { this.pendingCalls[error.id].reject(error); this.factory.cleanUp(error.id); } catch (e) { if (e instanceof TypeError) { // error object id probably not a pending response logging.getLogger().error("Message has no outstanding calls: ".concat(JSON.stringify(error))); } } } }]); return JsonRpcClientProtocol; }(); module.exports = JsonRpcClientProtocol;