Source: server/protocol/base.js

const MessageBuffer = require("../../util/buffer");
const { formatResponse, formatError } = require("../../util/format");
const { ERR_CODES, ERR_MSGS } = require("../../util/constants");

/**
 * Creates an instance of JsonRpcServerProtocol. This is the
 * base protocol from which all others inherit.
 *
 */
class JsonRpcServerProtocol {
  /**
   * @param {class} factory Instance of [JsonRpcServerFactory]{@link JsonRpcServerFactory}
   * @param {class} client Instance of `net.Socket`
   * @param {(1|2)} version JSON-RPC version to use
   * @param {string} delimiter Delimiter to use for `messageBuffer`
   * @property {class} messageBuffer Instance of [MessageBuffer]{@link MessageBuffer}
   * @property {string} event="data" The event name to listen for incoming data
   */
  constructor(factory, client, version, delimiter) {
    this.client = client;
    this.factory = factory;
    this.delimiter = delimiter;
    this.version = version;
    this.messageBuffer = new MessageBuffer(delimiter);
    this.event = "data";
  }

  /**
   * Registers the `event` data listener when client connects.
   *
   * Pushes received data into `messageBuffer` and calls
   * [_waitForData]{@link JsonRpcServerProtocol#_waitForData}.
   *
   */
  clientConnected() {
    this.client.on(this.event, (data) => {
      this.messageBuffer.push(data);
      this._waitForData();
    });
  }

  /**
   * Accumulate data while [MessageBuffer.isFinished]{@link MessageBuffer#isFinished} is returning false.
   *
   * If the buffer returns a message it will be passed to [validateRequest]{@link JsonRpcServerProtocol#validateRequest}.
   * If [validateRequest]{@link JsonRpcServerProtocol#validateRequest} returns a parsed result, then the result
   * is passed to [_maybeHandleRequest]{@link JsonRpcServerProtocol#_maybeHandleRequest}.
   * If [_maybeHandleRequest]{@link JsonRpcServerProtocol#_maybeHandleRequest} returns true, then
   * [gotRequest]{@link JsonRpcServerProtocol#gotRequest} is called.<br/><br/>
   *
   * If any of the above throws an error, [gotError]{@link JsonRpcServerProtocol#gotError} is called.
   *
   * @private
   *
   */
  _waitForData() {
    while (!this.messageBuffer.isFinished()) {
      const chunk = this.messageBuffer.handleData();
      this._validateData(chunk);
    }
  }

  /**
   * Validates data returned from `messageBuffer`.
   *
   * Will call [gotError]{@link JsonRpcClientProtocol#gotError} if error thrown
   * during validation.
   *
   * @param {string} chunk Data to validate
   * @private
   *
   */
  _validateData(chunk) {
    try {
      const result = this.validateRequest(chunk);
      const isMessage = this._maybeHandleRequest(result);
      if (isMessage) {
        this.gotRequest(result);
      }
    } catch (e) {
      this.gotError(e);
    }
  }

  /**
   * Validate the request message
   *
   * @param {string} chunk
   * @returns {JSON}
   * @throws Will throw an error with a JSON-RPC error object if chunk cannot be parsed
   */
  validateRequest(chunk) {
    try {
      return JSON.parse(chunk);
    } catch (e) {
      throw new Error(
        formatError({
          jsonrpc: this.version,
          id: null,
          code: ERR_CODES.parseError,
          message: ERR_MSGS.parseError,
          delimiter: this.delimiter
        })
      );
    }
  }

  /**
   * Determines the type of request being made (batch, notification, request) and
   * calls the corresponding function.
   *
   * @param {JSON} result Valid JSON-RPC request object
   * @private
   */
  _maybeHandleRequest(result) {
    if (Array.isArray(result)) {
      if (result.length === 0) {
        return this.writeToClient(
          formatError({
            code: ERR_CODES.invalidRequest,
            message: ERR_MSGS.invalidRequest,
            delimiter: this.delimiter,
            jsonrpc: this.version,
            id: null
          })
        );
      }
      // possible batch request
      this.gotBatchRequest(result).then((res) => {
        if (res.length !== 0) {
          // if all the messages in the batch were notifications,
          // then we wouldnt want to return anything
          this.writeToClient(JSON.stringify(res) + this.delimiter);
        }
      });
    } else if (result === Object(result) && !("id" in result)) {
      // no id, so assume notification
      this.gotNotification(result);
      return false;
    } else {
      this.validateMessage(result);
      return true;
    }
  }

  /**
   * Validates if there are any issues with the incoming request<br/>
   *
   *
   * @param {JSON} message Valid JSON-RPC request object
   * @throws Will throw an error for any of the below reasons
   *
   * Reason|Type
   * ---|---
   * message is not an object| Invalid Request
   * the server does not have the required method| Method Not Found
   * the params are not an array or object| Invalid Params
   * the "jsonrpc" property was passed for a v1 server| Invalid Request
   */
  validateMessage(message) {
    if (!(message === Object(message))) {
      this._raiseError(
        ERR_MSGS.invalidRequest,
        ERR_CODES.invalidRequest,
        null,
        this.version
      );
    } else if (!(typeof message.method === "string")) {
      this._raiseError(
        ERR_MSGS.invalidRequest,
        ERR_CODES.invalidRequest,
        message.id,
        message.jsonrpc
      );
    } else if (!(message.method in this.factory.methods)) {
      this._raiseError(
        ERR_MSGS.methodNotFound,
        ERR_CODES.methodNotFound,
        message.id,
        message.jsonrpc
      );
    } else if (
      message.params
      && !Array.isArray(message.params)
      && !(message.params === Object(message.params))
    ) {
      this._raiseError(
        ERR_MSGS.invalidParams,
        ERR_CODES.invalidParams,
        message.id,
        message.jsonrpc
      );
    } else if (message.jsonrpc && this.version !== 2) {
      this._raiseError(
        ERR_MSGS.invalidRequest,
        ERR_CODES.invalidRequest,
        message.id,
        this.version
      );
    }
  }

  /**
   * Send message to the client
   *
   * @param {string} message Stringified JSON-RPC message object
   */
  writeToClient(message) {
    this.client.write(message);
  }

  /**
   * Calls `emit` on factory with the event name being `message.method` and
   * the date being `message`.
   *
   * @param {string} message JSON-RPC message object
   */
  gotNotification(message) {
    this.factory.emit(message.method, message);
  }

  /**
   * Attempts to get the result for the request object. Will
   * send result to client if successful and will send an error
   * otherwise.
   *
   * @param {JSON} message JSON-RPC message object
   * @returns {Promise}
   */
  gotRequest(message) {
    return this.getResult(message)
      .then((result) => {
        this.writeToClient(result);
      })
      .catch((error) => {
        this.gotError(Error(error));
      });
  }

  /**
   * Attempts to get the result for all requests in the batch.
   * Will send result to client if successful and error otherwise.
   *
   * @param {JSON[]} requests Valid JSON-RPC batch request
   * @returns {Promise[]}
   */
  gotBatchRequest(requests) {
    const batchResponses = requests
      .map((request) => {
        try {
          const isMessage = this._maybeHandleRequest(request);
          if (isMessage) {
            // if its a notification we dont want to return anything
            return this.getResult(request)
              .then(result => JSON.parse(result))
              .catch(error => JSON.parse(error));
          }
          return null;
        } catch (e) {
          // basically reject the whole batch if any one thing fails
          return JSON.parse(e.message);
        }
      })
      .filter(el => el != null);
    return Promise.all(batchResponses);
  }

  /**
   * Get the result for the request. Calls the function associated
   * with the method and returns the result.
   *
   * @param {JSON} message Valid JSON-RPC message object
   * @returns {Promise}
   */
  getResult(message) {
    // function needs to be async since the method can be a promise
    return new Promise((resolve, reject) => {
      const { params } = message;
      const response = {
        jsonrpc: message.jsonrpc,
        id: message.id,
        delimiter: this.delimiter
      };
      const error = {
        jsonrpc: message.jsonrpc,
        id: message.id,
        delimiter: this.delimiter
      };
      try {
        const methodResult = params
          ? this.factory.methods[message.method](params)
          : this.factory.methods[message.method]();
        if (
          methodResult instanceof Promise
          || typeof methodResult.then === "function"
        ) {
          methodResult
            .then((results) => {
              response.result = results;
              resolve(formatResponse(response));
            })
            .catch((resError) => {
              error.code = ERR_CODES.internal;
              error.message = `${JSON.stringify(resError.message || resError)}`;
              error.data = resError.data;
              reject(formatError(error));
            });
        } else {
          response.result = methodResult;
          resolve(formatResponse(response));
        }
      } catch (e) {
        if (e instanceof TypeError) {
          error.code = ERR_CODES.invalidParams;
          error.message = ERR_MSGS.invalidParams;
          // error.data = e.message;
        } else {
          error.code = ERR_CODES.unknown;
          error.message = ERR_MSGS.unknown;
          // error.data = e.message;
        }
        error.data = e.data;
        reject(formatError(error));
      }
    });
  }

  /**
   *
   * @param {string} message Error message
   * @param {number} code Error code
   * @param {string|number} id Error message ID
   * @throws Throws a JSON-RPC error object
   * @private
   */
  _raiseError(message, code, id, version) {
    const error = formatError({
      jsonrpc: version,
      delimiter: this.delimiter,
      id,
      code,
      message
    });
    throw new Error(error);
  }

  /**
   * Writes error to the client. Will send a JSON-RPC error object if the
   * passed error cannot be parsed.
   *
   * @param {Error} error `Error` object instance where the message should be a JSON-RPC message object
   */
  gotError(error) {
    let err;
    try {
      err = JSON.stringify(JSON.parse(error.message)) + this.delimiter;
    } catch (e) {
      err = formatError({
        jsonrpc: this.version,
        delimiter: this.delimiter,
        id: null,
        code: ERR_CODES.unknown,
        message: JSON.stringify(error, Object.getOwnPropertyNames(error))
      });
    }
    this.writeToClient(err);
  }
}

module.exports = JsonRpcServerProtocol;