Home Identifier Source Test Repository

src/isoproxy.js

import _ from "lodash";
import jsonrpc from "./jsonrpc";
import fetch from "./fetch";

/**
 * Isomorphic API Proxy
 */
export default class IsoProxy {

  /**
   * @param {Object} opts
   * @param {string} opts.root Root url path, that is beginning with "/" and NOT end with "/".
   * @param {boolean} opts.isServer Server mode or not.
   */
  constructor(opts = {}) {
    const {root = "", isServer = true} = opts;
    /** @private */
    this.root = root;
    /** @private */
    this.isServer = isServer;
    /** @private */
    this.interfaces = {};
    /** @private */
    this.implementations = {};
    /**
     * @public
     * @type {Object}
     * @example
     * {math: {add: function() { isomorphic_blackbox(); }}}
     */
    this.api = {};
    /**
     * @public
     * @type {Object}
     * @example
     * {"/api/math": function processJsonrpcRequest(jsonrpcRequest) { ... }}
     */
    this.routes = {};
  }

  /**
   * @public
   * @param {Object} interfaces
   */
  setInterfaces(interfaces) {
    this.interfaces = interfaces;
    this.updateApi();
    this.updateRoutes();
  }

  /**
   * @public
   * @param {Object} implementations
   */
  setImplementations(implementations) {
    if (!this.isServer) {
      // Implementations are required only in server mode.
      return;
    }
    this.implementations = implementations;
    this.updateApi();
    this.updateRoutes();
  }

  /**
   * @access private
   */
  updateApi() {
    return this.isServer ? this.updateServerApi() : this.updateClientApi();
  }

  /**
   * @private
   */
  preprocessMethodDefinitions(methodDefinitions) {
    if (_.isArray(methodDefinitions)) {
      // ["add", "sub"] => {add: {}, sub: {}}
      methodDefinitions = _.reduce(methodDefinitions, (r, methodName) => {
        r[methodName] = {}; // no options
        return r;
      }, {});
    }
    return methodDefinitions;
  }

  /**
   * @private
   */
  updateServerApi() {
    this.api = {};
    _.forEach(this.interfaces, (methodDefinitions, ns) => {
      methodDefinitions = this.preprocessMethodDefinitions(methodDefinitions);
      this.api[ns] = {};
      _.forEach(methodDefinitions, (methodOpts, methodName) => {
        const impl = _.get(this.implementations, [ns, methodName]);
        if (impl) {
          // wrap with Promise.
          this.api[ns][methodName] = (...args) => {
            return Promise.resolve(impl(...args));
          };
        } else {
          // mock method.
          this.api[ns][methodName] = (...args) => {
            const argsStr = _.map(args, (p) => p.toString()).join(", ");
            throw new Error(`${ns}.${methodName}(${argsStr}) is called but is not implemented.`);
          };
        }
      });
    });
  }

  /**
   * @private
   */
  updateClientApi() {
    this.api = {};
    _.forEach(this.interfaces, (methodDefinitions, ns) => {
      methodDefinitions = this.preprocessMethodDefinitions(methodDefinitions);
      this.api[ns] = {};
      _.forEach(methodDefinitions, (methodOpts, methodName) => {
        this.api[ns][methodName] = (...params) => {
          return new Promise((resolve, reject) => {
            // RPC
            fetch(this.createPath(ns), {
              method: "post",
              headers: {
                "Accept": "application/json",
                "Content-Type": "application/json"
              },
              body: JSON.stringify(jsonrpc.createRequest(methodName, params))
            })
            .then((response) => {
              if (!response.ok) {
                reject(response.error());
                return null;
              }
              return response.json();
            })
            .then((jsonrpcResponse) => {
              if (jsonrpcResponse.error) {
                return reject(jsonrpcResponse.error);
              }
              resolve(jsonrpcResponse.result);
            })
            .catch((error) => {
              reject(error);
            });
          });
        };
      });
    });
  }

  /**
   * @private
   */
  updateRoutes() {
    _.forEach(this.implementations, (methods, ns) => {
      this.routes[this.createPath(ns)] = (jsonrpcRequest) => {
        return new Promise((resolve) => {
          Promise
            .resolve(methods[jsonrpcRequest.method](...jsonrpcRequest.params))
            .then((result) => {
              resolve(jsonrpc.createResponse(null, result));
            })
            .catch((error) => {
              resolve(jsonrpc.createResponse(error));
            });
        });
      };
    });
  }

  /**
   * @private
   */
  createPath(ns) {
    return `${this.root}/${ns}`;
  }

}