jaysonic
Version:
A feature rich JSON-RPC 1.0/2.0 compliant client and server library
771 lines (683 loc) • 25.9 kB
JavaScript
"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;