UNPKG

@adt/json-rpc-client

Version:

Json-rpc client with pluggable transport layers

1,116 lines (918 loc) 32.2 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.JsonRpcClient = factory()); }(this, (function () { 'use strict'; function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function (obj) { return typeof obj; }; } else { _typeof = function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } /** * JSON-RPC Element * Base class for all JSON-RPC messages * This is abstract class, do not try to create instance of this class! * @class * @abstract */ function JsonRpcElement() { throw new Error("JsonRpcElement is an abstract class. Can't instantiate or run"); } JsonRpcElement.prototype = Object.freeze(Object.create(null, /** @lends JsonRpcElement.prototype */{ jsonrpc: { value: "2.0" }, serialize: { value: function value() { return JSON.stringify(this); } } })); var _typeof$1 = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; var validateId = function validateId(o) { if ("id" in o === false) { throw new Error("Required 'id' param is not present"); } }; var validateMethod = function validateMethod(method) { var methodType = typeof method === "undefined" ? "undefined" : _typeof$1(method); if (methodType !== "string") { throw new Error("Method must be a string, but '" + methodType + "' given"); } }; var validateParams = function validateParams(params) { var paramsType = typeof params === "undefined" ? "undefined" : _typeof$1(params); var paramsIsArray = Array.isArray(params); if (paramsType === "undefined") { return; } if (paramsIsArray === false && paramsType !== "object" && paramsType !== "undefined") { throw new Error("Params must be an array, object or undefined, but '" + paramsType + "' given"); } }; var validateArguments = function validateArguments(args) { if (typeof args === "undefined") { throw new Error("Required parameters object not present"); } }; /** * JSON-RPC Error object * @class * @param {Object} o Parameters object * @param {Number} o.code Error code * @param {String} [o.message=""] Error message * @param {?Object} [o.data=undefined] Additional data associated with error */ function JsonRpcError(o) { if (this instanceof JsonRpcError === false) { return new JsonRpcError(o); } validateArguments(o); if (typeof o.code !== "number") { throw "Error code must be integer"; } this.code = o.code; this.message = "message" in o ? o.message : ""; this.data = o.data; return Object.freeze(this); } JsonRpcError.prototype = Object.create(null, /** @lends JsonRpcError.prototype */{ code: { value: 0, writable: true }, message: { value: "", writable: true }, data: { value: undefined, writable: true } }); /** * Standard JSON-RPC errors * @type {Object} */ JsonRpcError.ERRORS = Object.freeze(Object.create(null, { PARSE_ERROR: { value: Object.freeze(Object.create(JsonRpcError.prototype, { code: { value: -32700, enumerable: true }, message: { value: "Parse error", enumerable: true } })), enumerable: true }, INVALID_REQUEST: { value: Object.freeze(Object.create(JsonRpcError.prototype, { code: { value: -32600, enumerable: true }, message: { value: "Invalid Request", enumerable: true } })), enumerable: true }, METHOD_NOT_FOUND: { value: Object.freeze(Object.create(JsonRpcError.prototype, { code: { value: -32601, enumerable: true }, message: { value: "Method not found", enumerable: true } })), enumerable: true }, INVALID_PARAMS: { value: Object.freeze(Object.create(JsonRpcError.prototype, { code: { value: -32602, enumerable: true }, message: { value: "Invalid params", enumerable: true } })), enumerable: true }, INTERNAL_ERROR: { value: Object.freeze(Object.create(JsonRpcError.prototype, { code: { value: -32603, enumerable: true }, message: { value: "Internal error", enumerable: true } })), enumerable: true } })); // TODO: For now this is only one level shallow copy, but should be real deep copy with omitting functions var copyParams = function copyParams(params) { if (typeof params === "undefined") { return undefined; } else if (Array.isArray(params)) { return Object.freeze(params.splice(0)); } else { var ret = Object.create(null); for (var a in params) { ret[a] = params[a]; Object.defineProperty(ret, a, { value: params[a] }); } return Object.freeze(ret); } }; /** * JSON-RPC Event * TODO: Maybe length of 'method' parameter should be checked? Specification does not say anything about empty string as method name * @class * @param {Object} o Parameters object * @param {String} o.method Method name * @param {?Array} [o.params=undefined] Event params */ function JsonRpcEvent(o) { validateArguments(o); validateMethod(o.method); var method = o.method; validateParams(o.params); var params = copyParams(o.params); var instance = Object.create(JsonRpcEvent.prototype); instance.method = method; instance.params = params; return Object.freeze(instance); } JsonRpcEvent.prototype = Object.create(JsonRpcElement.prototype, /** @lends JsonRpcElement.prototype */{ method: { writable: true }, params: { writable: true }, serialize: { value: function value() { return JSON.stringify({ jsonrpc: this.jsonrpc, method: this.method, params: this.params }); } }, toJSON: { value: function value() { return this.serialize(); } } }); /** * UUID * @constructor */ function UUID(uuid) { return Object.create(UUID.prototype, /** @lends UUID.prototype */{ value: { value: uuid, writable: true }, serialize: { value: function value() { var val = this.value; return [val[0] + val[1], val[2], val[3], val[4], val[5] + val[6] + val[7]].join("-"); } } }); } UUID.randomUUID = function () { return UUID(guid()); }; UUID.fromString = function (str) { var replaced = str.replace(/-/g, ""); var a = []; for (var i = 0; i < 32; i += 4) { a.push(replaced.slice(i, i + 4)); } return UUID(a); }; /** * Helper function * @returns {String} */ function s4() { return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); } /** * Helper function * @return {Array} */ function guid() { var ar = []; for (var i = 0; i < 8; i++) { ar.push(s4()); } return ar; } /** * JSON-RPC Request * TODO: Maybe length of 'method' parameter should be checked? Specification does not say anything about empty string as method name * @class * @param {Object} o Parameters object * @param {String} o.method * @param {Array|Object|undefined} o.params Params must be an Array, Object or undefined * @param {?String} [o.id=UUID] Request id if empty it will be set to randomly generated UUID TODO: For now id is set always to 1!!! */ function JsonRpcRequest(o) { validateArguments(o); validateMethod(o.method); var method = o.method; validateParams(o.params); var params = copyParams(o.params); var id = "id" in o ? o.id : UUID.randomUUID().serialize(); //return Object.freeze(Object.create(JsonRpcRequest.prototype,)); var instance = Object.create(JsonRpcRequest.prototype); instance.id = id; instance.method = method; instance.params = params; return Object.freeze(instance); } JsonRpcRequest.prototype = Object.create(JsonRpcElement.prototype, /** @lends JsonRpcRequest.prototype */{ method: { writable: true }, params: { writable: true }, id: { writable: true }, /** * Serialize request object to string * @return {String} */ serialize: { value: function value() { var ret = { jsonrpc: this.jsonrpc, id: this.id, method: this.method }; if (typeof this.params !== "undefined") { ret.params = this.params; } return JSON.stringify(ret); } }, toJSON: { value: function value() { return this.serialize(); } } }); /** * JSON-RPC Response * This is abstract class, do not try to create instance of this class! * Base class for JSON-RPC responses * @class * @abstract */ function JsonRpcResponse() { throw new Error("JsonRpcResponse is an abstract class. Can't instantiate or run"); } JsonRpcResponse.prototype = Object.freeze(Object.create(JsonRpcElement.prototype, { id: { value: null, writable: true } })); /** * JSON-RPC Error Response * @class * @param {Object} o Parameters object * @param {String} o.id * @param {JsonRpcError} o.error */ function JsonRpcResponseError(o) { validateArguments(o); validateId(o); if (!(o.error instanceof JsonRpcError)) { throw Error("error property is not an instance of JsonRpcError!"); } var id = o.id; var error = o.error; return Object.freeze(Object.create(JsonRpcResponseError.prototype, { id: { value: id }, error: { value: error }, /** * Serialize request object to string * @return {String} */ serialize: { value: function value() { return JSON.stringify({ jsonrpc: this.jsonrpc, id: this.id, error: this.error }); } }, toJSON: { value: function value() { return this.serialize(); } } })); } JsonRpcResponseError.prototype = Object.freeze(Object.create(JsonRpcResponse.prototype)); /** * JSON-RPC Response result * @class * @param {Object} o Parameters object * @param {String} o.id * @param {Object} o.result */ function JsonRpcResponseResult(o) { validateArguments(o); validateId(o); if ("result" in o === false) { throw new Error("Required 'result' param is not present"); } var id = o.id; var result = o.result; return Object.freeze(Object.create(JsonRpcResponseResult.prototype, /** @lends JsonRpcResponseResult.prototype */{ id: { value: id }, result: { value: result }, /** * Serialize request object to string * @return {String} */ serialize: { value: function value() { return JSON.stringify({ jsonrpc: this.jsonrpc, id: this.id, result: this.result }); } }, toJSON: { value: function value() { return this.serialize(); } } })); } JsonRpcResponseResult.prototype = Object.freeze(Object.create(JsonRpcResponse.prototype)); //import JsonRpcElement from "./JsonRpcElement"; /** * Parses Json to JsonRpc object * @param {String} str String to parse * @param {Boolean} [weak=false] Don't check for existence of "jsonrpc" property in object * @return JsonRpcElement */ var parse = function parse(str, weak) { var u = "undefined"; var obj = void 0; try { obj = JSON.parse(str); } catch (e) { throw new Error("String " + str + " is not a valid JSON"); } if (weak === true && "jsonrpc" in obj === false) { // TODO: Dedicated exception throw new Error("Missing 'jsonrpc' property in unserialized. You can omit this check by using the `weak` param"); } var _obj = obj, id = _obj.id, method = _obj.method, result = _obj.result, error = _obj.error, params = _obj.params; if ((typeof id === "undefined" ? "undefined" : _typeof$1(id)) !== u) { // Request or response if ((typeof method === "undefined" ? "undefined" : _typeof$1(method)) !== u) { if ((typeof params === "undefined" ? "undefined" : _typeof$1(params)) === u) { throw new Error("Missing 'method' property in unserialized object"); } return JsonRpcRequest({ id: id, method: method, params: params }); } else if ((typeof result === "undefined" ? "undefined" : _typeof$1(result)) !== u) { return JsonRpcResponseResult({ id: id, result: result }); } else if ((typeof error === "undefined" ? "undefined" : _typeof$1(error)) !== u) { var code = error.code, message = error.message, data = error.data; return JsonRpcResponseError({ id: id, error: JsonRpcError({ code: code, message: message, data: data }) }); } else { // TODO: Dedicated Exception throw new Error("Unserialized object is not a valid JSON-RPC element"); } } else { // Event return JsonRpcEvent({ method: method, params: params }); } }; JsonRpcElement.parse = parse; JsonRpcElement.fromJSON = parse; /** * Callback * @class * @private * @param {String} evtName Event name * @param {Function} callback Callback function * @param {Object} context Callback execution context * @param {?Object} args Additional arguments */ function Callback(evtName, callback, context, args) { return Object.freeze(Object.create(null, { evtName: { value: evtName }, callback: { value: callback }, context: { value: context }, args: { value: args } })); } /** * EventEmitter */ function EventEmitter() { var _this = this; var callbacks = new Map(); var ret = Object.create(EventEmitter.prototype, { /** * Register callback function.<br /> * Event object will be passed to registered callback function as first argument. * @name EventEmitter#on * @memberof EventEmitter * @function * @param {String} evtName Event name * @param {Function} cbf Callback function * @param {Object} [context=EventEmitter] Callback execution context (this) */ on: { value: function value(evtName, cbf, context) { var ctx = context || _this; var cb = Callback(evtName, cbf, ctx); if (callbacks.has(evtName)) { callbacks.get(evtName).push(cb); } else { callbacks.set(evtName, [cb]); } } }, /** * Execute callbacks on given event if callbacks exist for this event (emit events). * @protected * @param {String} evtName Event name * @param {Object} evt Associated source event */ emit: { value: function value(evtName, evt) { if (callbacks.has(evtName)) { callbacks.get(evtName).forEach(function (cb) { return cb.callback.call(cb.context, evt); }); } } } }); return ret; } /** * Tracker constructor. * You can use this function with or without "new" operator * * @constructor * @param {Object} o * @param {Number} o.timeout Message timeout in milliseconds * @param {Number} [o.checkInterval=1000] Check interval in milliseconds */ function MessageTracker(o) { if ( typeof o === "undefined" || o === null ) { throw Error("Missing configuration object"); } if ( ("timeout" in o ) === false ) { throw Error("Required param 'timeout' is not defined"); } var promiseRegister = [], globalTimeout = o.timeout, checkInterval = o.checkInterval ? o.checkInterval : 1000, timer = null; function checkOverdue() { var now = new Date() / 1000 | 0; // Get seconds and cut milliseconds promiseRegister = promiseRegister.filter( function(val) { if ( now - val.regTime > val.timeout ) { val.reject(val.timeoutRejectWith); return false; } else { return true; } }); // Timer is set again only when we have any message if ( promiseRegister.length > 0 ) { timer = setTimeout(checkOverdue, checkInterval); } else { clearTimeout(timer); timer = null; } } return Object.create(MessageTracker.prototype, { globalTimeout : { set : function (val) { globalTimeout = val; }, get : function(){ return globalTimeout; }}, register : { value : /** * Registers message in tracker. * Filter function is a function witch should accept one argument as object with following properties<br> * - message - message passed to matchMessage method<br> * - current - iterated message from internal message register)<br> * - resolve - resolving function<br> * - reject - rejecting function)<br> * - params - additional params * Filter function is executed on each registered message when you call MessageTracker#matchMessage method * If o.context will be passed then filter function will be executed within passed context * * @param {Object} o * @param {Object} o.message * @param {Function} o.filter Filter function * @param {Number} [o.timeout=this.globalTimeout] Individual timeout in seconds for registered message, if not given, global timeout from constructor will be used * @param {Object} o.timeoutRejectWith Overdue messages will be rejected with this object * @param {Object} [o.params={}] Additional params which will be passed to filter function * @param {Object} [o.context] Context (this) to execute filter function * @return Promise */ function (o) { var that = this; var ret = new Promise(function(resolve, reject) { var now = new Date() / 1000 | 0, // Get seconds and cut milliseconds params = Object.create(null); if ("params" in o && typeof params !== "undefined" ) { params = o.params; } promiseRegister.push( { message : o.message, filter : o.filter, resolve : resolve, reject : reject, regTime : now, timeout : o.timeout ? o.timeout : that.globalTimeout, timeoutRejectWith : o.timeoutRejectWith, params : params, context : o.context } ); }); // Start timer if it's not started if ( timer === null ) { timer = setTimeout(checkOverdue, checkInterval); } return ret; }}, /** * Match message passed as argument with registered messages * Matching is done by function passed to register method in filter property * Function returns true if message was matched * @see MessageTracker#register * @param {Object} m Message to check * @function * @return Boolean */ matchMessage : { value : function(m) { var l = promiseRegister.length, br = false, rejectForFilter, resolveForFilter; function resolve(resolveFnc) { return function(res) { resolveFnc(res); br = true; }; } function reject(rejectFnc) { return function(err) { rejectFnc(err); br = true; }; } for(var i=0;i<l;i++) { var val = promiseRegister[i]; resolveForFilter = resolve(val.resolve); rejectForFilter = reject(val.reject); val.filter.call(val.context, { message : m, current : val.message, resolve : resolveForFilter, reject : rejectForFilter, params : val.params }); if ( br === true ) { promiseRegister.splice(i,1); break; } } return br; } } }); } var version = "#version#"; /** * @constructor * @param {Object} c Configuration * @param {JsonRpcTransportProvider} c.transportProvider * @param {Number} [c.messageCheckInterval=1000] c.messageCheckInterval Interval (in ms) of checking overdue requests with no response * @param {Number} [c.messageTimeout=5000] Message timeout in milliseconds * @param {Boolean} [c.reconnect=false] Reconnect flag * @param {Number} [c.reconnectAfter=5000] Reconnect timeout in milliseconds * @fires connected * @fires connecting * @fires disconnected * @fires connectionerror * @fires message * @fires error */ function JsonRpcClient(c) { if (typeof c === "undefined" || c === null) { throw new Error("Missing configuration object"); } if (typeof c.transportProvider === "undefined") { throw new Error("Required param 'transportProvider' is not defined"); } var that = this; var emitter = EventEmitter(); var config = { messageCheckInterval: "messageCheckInterval" in c ? c.messageCheckInterval : 1000, messageTimeout: "messageTimeout" in c ? c.messageTimeout : 5000 }; var transportProvider = c.transportProvider; transportProvider.onMessage(onMessage.bind(that)); var o = onDisconnect.bind(that); transportProvider.onDisconnect(o); var messageTracker = MessageTracker({ timeout: config.messageTimeout, checkInterval: config.messageCheckInterval }); var ret = Object.create(JsonRpcClient.prototype, /** @lends JsonRpcClient.prototype */ { version: { get: function get() { return version; } }, // TODO: Getter connected: { value: false, writable: true }, /** * @type {WebSocket} * @default null */ socket: { value: null, writable: true }, /** * @type {MessageTracker} * @default null */ messageTracker: { value: null, writable: true }, connect: { value: connect }, sendRequest: { value: sendRequest }, sendEvent: { value: sendEvent }, disconnect: { value: disconnect }, on: { value: function value(evtName, cbf, context) { return emitter.on(evtName, cbf, context); } } }); /** * Connects to remote endpoint by underlying transport channel * @name JsonRpcClient#connect * @function * @this JsonRpcClient * @return {Promise} */ function connect() { return new Promise(function (resolve, reject) { emitter.emit("connecting"); transportProvider.connect().then(function () { emitter.emit("connected"); resolve(); })["catch"](function (e) { reject(e); }); }); } /** * Send request to the remote side * If websocket is not connected then message will not be send and function returns rejected promise with JsonRpcClient.ERRORS.INVALID_STATE_ERR * If request will be timeouted during waiting from response then returned promise will be rejected with JsonRpcError.ERRORS.TIMEOUT_EXCEEDED with request attached in data property * @function * @name JsonRpcClient#sendRequest * @this JsonRpcClient * @param {JsonRpcClient.JsonRpcRequest} req * @param {Boolean} [enableCallbacks=false] If true, then callbacks assigned to message event will be executed before resolving/rejecting promise on response. * @return Promise */ function sendRequest(req, enableCallbacks) { var retPromise, rejectWith = JsonRpcClient.ERRORS.TIMEOUT_EXCEEDED; rejectWith.data = req; if (transportProvider.isConnected() === true) { retPromise = messageTracker.register({ message: req, filter: filterMessage, timeoutRejectWith: rejectWith, params: { enableCallbacks: enableCallbacks }, context: this }); // TODO: Czy powinienem tutaj pchać zaserializowane, czy może jednak JsonRpcRequest??? Chyba lepiej mieć obiekt... transportProvider.send(req); } else { retPromise = Promise.reject(new JsonRpcResponseError({ id: req.id, error: JsonRpcClient.ERRORS.INVALID_STATE_ERR })); } return retPromise; } /** * Sends JSON-RPC event to the remote side. * On success method returns JsonRpcClient.ERRORS.NO_ERROR * If websocket is not connected then message will not be send and function returns error JsonRpcClient.ERRORS.INVALID_STATE_ERR * @function * @name JsonRpcClient#sendEvent * @param {JsonRpcClient.JsonRpcEvent} * @this JsonRpcClient * @return JsonRpcError */ function sendEvent(evt) { if (transportProvider.isConnected() === true) { transportProvider.send(evt); } else { return JsonRpcClient.ERRORS.INVALID_STATE_ERR; } return JsonRpcClient.ERRORS.NO_ERROR; } /** * @this JsonRpcClient * @param {JsonRpcElement} */ function onMessage(m) { var msg, rObj = {}; // If message tracker matched message, then eventually emit message is realized by filterFunction // otherwise process message as event or request here if (!messageTracker.matchMessage(m)) { if (_typeof(m) === "object") { if ("id" in m) { rObj.id = m.id; if ("result" in m) { rObj.result = m.result; msg = JsonRpcResponseResult(rObj); } if ("error" in m) { msg = JsonRpcResponseError({ id: m.id, error: JsonRpcError(m.error) }); } if ("method" in m) { rObj.method = m.method; rObj.params = m.params; msg = JsonRpcRequest(rObj); } } else { rObj.method = m.method; rObj.params = m.params; msg = JsonRpcEvent(rObj); } } else { msg = m; } emitter.emit("message", msg); } } function onDisconnect(evt) { emitter.emit("disconnected", evt); } /** * Disconnects underlying transport channel. * @function * @name JsonRpcClient#disconnect * @this JsonRpcClient */ function disconnect() { transportProvider.disconnect(); } /** * Function for filter messages in MessageTracker. * This function is executed by message tracker in context of JsonRpcClient. * * @param {Object} o Parameters object * @param {JsonRpcElement} o.message Message passed to matchMessage method * @param {JsonRpcElement} o.current Iterated message from internal message register) * @param {Function} o.resolve Resolving function * @param {Function} o.reject Rejecting function * @param {Object} o.params Additional params * @this JsonRpcClient */ function filterMessage(o) { var m = o === null || o === void 0 ? void 0 : o.message; if (typeof m === "undefined" || m.id !== o.current.id) { return; } var msg; var err = false; if ("result" in m) { msg = JsonRpcResponseResult({ id: m.id, result: m.result }); } if ("error" in m) { err = true; msg = JsonRpcResponseError({ id: m.id, error: JsonRpcError(m.error) }); } if (msg !== undefined) { if (o.params.enableCallbacks === true) { emitter.emit("message", m); } if (err === true) { o.reject(msg); } else { o.resolve(msg); } } } return ret; } JsonRpcClient.ERRORS = Object.create(null, { NO_ERROR: { value: Object.create(JsonRpcError.prototype, { code: { value: -32000, enumerable: true }, message: { value: "No error", enumerable: true }, data: { value: undefined, enumerable: true, writable: true } }), enumerable: true }, TIMEOUT_EXCEEDED: { value: Object.create(JsonRpcError.prototype, { code: { value: -32001, enumerable: true }, message: { value: "Waiting for response timeout exceeded", enumerable: true }, data: { value: undefined, enumerable: true, writable: true } }), enumerable: true }, INVALID_STATE_ERR: { value: Object.create(JsonRpcError.prototype, { code: { value: -32002, enumerable: true }, message: { value: "WebSocket is already in CLOSING or CLOSED state", enumerable: true }, data: { value: undefined, enumerable: true, writable: true } }), enumerable: true } }); return JsonRpcClient; })));