etk-pl/jsonrpc

View on GitHub
src/jsonrpc.js

Summary

Maintainability
A
1 hr
Test Coverage
/**
 * @author Michał Żaloudik <ponury.kostek@gmail.com>
 */
"use strict";
const __version = "1.2.0";
const JSONLess = require("json-less");
const ExtError = require("exterror");
const error_codes = require("./codes");

/**
 *
 * @param {Object} [options]
 * @param {boolean} [options.autoFireCallbacks]
 * @param {number} [options.callbacksTimeout]
 * @param {Object} [options.encoder]
 * @param {Function} options.encoder.parse
 * @param {Function} options.encoder.stringify
 * @constructor
 */
function JsonRpc(options) {
    this.__id = 0;
    this.options = Object.assign({
        autoFireCallbacks: true,
        callbacksTimeout: 86400000,
        encoder: JSONLess
    }, options || {});
    this.callbacks = {};
}

/**
 *
 * @returns {{autoFireCallbacks: boolean, encoder, callbacksTimeout: number}}
 */
JsonRpc.prototype.getOptions = function () {
    return this.options;
};
/**
 *
 * @param {Object} options
 */
JsonRpc.prototype.setOptions = function (options) {
    this.options = Object.assign(this.options, options);
};
/**
 * Gets message type
 * @param {Object} message
 * @returns {string} Possible values: request, response, notification
 * @throws {Error}
 */
JsonRpc.getType = function (message) {
    if (typeof message !== "object" || message === null) {
        throw new ExtError("ERR_MESSAGE_PARAM_MUST_BE_OBJECT", "Message parameter must be object");
    }
    switch (true) {
        case JsonRpc.isValidRequest(message):
            return "Request";
        case JsonRpc.isValidResponse(message):
            return "Response";
        case JsonRpc.isValidNotification(message):
            return "Notification";
        default:
            break;
    }
};
/**
 * Parse message
 * @param {Object|String} message
 * @returns {Request|Response|Notification}
 * @throws {Error|TypeError}
 */
JsonRpc.prototype.parse = function (message) {
    if (typeof message !== "object") {
        if (typeof message === "string") {
            try {
                message = this.options.encoder.parse(message);
            } catch (e) {
                const {code, message} = error_codes.E_PARSE;
                throw new ExtError(code, message);
            }
        } else {
            throw new TypeError("(JsonRpc) -> parse(): Message must be string or object type");
        }
    }
    const type = JsonRpc.getType(message);
    switch (type) {
        case "Request":
            return this.Request(message);
        case "Response":
            const response = this.Response(message);
            this.fireCallback(response);
            return response;
        case "Notification":
            return this.Notification(message);
        default:
            throw new ExtError("ERR_TYPE", "Type " + type);
    }
};
/**
 *
 * @returns {number}
 */
JsonRpc.prototype.getNextId = function () {
    return ++this.__id;
};
/**
 * Checks that message is valid request
 * @param {Object} message
 * @returns {Boolean}
 */
JsonRpc.isValidRequest = function isValidRequest(message) {
    if (typeof message !== "object" || message === null) {
        return false;
    }
    if (message.error !== undefined || message.result !== undefined) {
        return false;
    }
    return message.version === __version && typeof message.id === "number" && message.id > 0 && typeof message.resource === "string" && !!message.resource.length && typeof message.method === "string" && !!message.method.length && typeof message.params === "object" && message.params !== null;
};
/**
 * Checks that message is valid response
 * @param {Object} message
 * @returns {Boolean}
 */
JsonRpc.isValidResponse = function isValidResponse(message) {
    if (typeof message !== "object" || message === null) {
        return false;
    }
    if (message.method !== undefined || message.resource !== undefined || message.params !== undefined) {
        return false;
    }
    if (message.id !== undefined) {
        if (typeof message.id !== "number" || message.id <= 0) {
            return false;
        }
    }
    return message.version === __version && (message.result !== undefined || ((typeof message.error === "object" && message.error !== null && Object.keys(message.error).sort().join(" ") === [
        "code",
        "message"
    ].join(" ") && typeof message.error.code === "string" && typeof message.error.message === "string") || message.error instanceof ExtError));
};
/**
 * Checks that message is valid notification
 * @param {Object} message
 * @returns {Boolean}
 */
JsonRpc.isValidNotification = function isValidNotification(message) {
    if (typeof message !== "object" || message === null) {
        return false;
    }
    if (message.error !== undefined || message.result !== undefined || message.id !== undefined) {
        return false;
    }
    return !!(message.version === __version && typeof message.resource === "string" && message.resource.length && typeof message.method === "string" && message.method.length && (typeof message.params === "object" && message.params !== null));
};
/**
 * Checks that message has correct syntax
 * @param {Object} message
 * @returns {Boolean}
 */
JsonRpc.hasValidSyntax = function (message) {
    return JsonRpc.isValidRequest(message) || JsonRpc.isValidResponse(message) || JsonRpc.isValidNotification(message);
};
/**
 * Returns id for new request
 * @returns {number}
 */
JsonRpc.prototype.getNextId = function () {
    return ++this.__id;
};
/**
 * Fires callback for response if any, if callback not found do nothing
 * @param {Response} response
 */
JsonRpc.prototype.fireCallback = function (response) {
    if (response instanceof Response) {
        const callback = this.callbacks[response.getId()];
        if (callback instanceof Object && callback.cb instanceof Function) {
            clearTimeout(callback.timeout);
            callback.cb(response.getError(), response.getResult());
            this.removeCallback(response.getId());
        }
    } else {
        throw new Error("(JsonRpc) -> fireCallback(): Response must be instance of Response");
    }
};
/**
 * Removes registered callback if exists
 * @param {Number} id
 */
JsonRpc.prototype.removeCallback = function (id) {
    const callback = this.callbacks[id];
    if (typeof callback === "object" && callback !== null) {
        delete this.callbacks[id];
    }
};
/**
 *
 * @param {Number} id
 */
JsonRpc.prototype.timeoutCallback = function (id) {
    const callback = this.callbacks[id];
    if (typeof callback === "object" && callback !== null) {
        callback.cb(new ExtError("ERR_RPC_REQUEST_TIMEOUT", "RPC request timeout after " + callback.tls + "ms"));
        delete this.callbacks[id];
    }
};
/**
 *
 * @type {JsonRpc}
 */
module.exports = JsonRpc;
const Request = require("./request.js");
const Response = require("./response.js");
const Notification = require("./notification.js");
module.exports.Request = Request;
module.exports.Response = Response;
module.exports.Notification = Notification;
module.exports.version = __version;
module.exports.addHandler = JSONLess.addHandler;
/**
 *
 * @param message
 * @returns {Request}
 * @constructor
 */
JsonRpc.prototype.Request = function (message) {
    return new Request(this, message);
};
/**
 *
 * @param message
 * @returns {Response}
 * @constructor
 */
JsonRpc.prototype.Response = function (message) {
    return new Response(this, message);
};
/**
 *
 * @param message
 * @returns {Notification}
 * @constructor
 */
JsonRpc.prototype.Notification = function (message) {
    return new Notification(this, message);
};