Source: libs/RPC.js

/**
* @file Handles asynchronous JSON-RPC 2.0 requests to a HTTP / HTTPS,
* WebSocket / Secure WebSocket, or routed endpoint. Requires {@link EventPromise}
* to exist in the current execution context.
*
* @version 0.4.1
*/

/**
* @property {Number} __rpc_request_id unique RPC message id value that can be used
* across the application. Use {@link uniqueRPCID} to get a new unique value.
* @private
*/
var __rpc_request_id = 0;

/**
* Invokes a RPC (API) request through a HTTP, WebSocket, or routed interface.
*
* @param {String} method The remote procedure/method/function to invoke.
* @param {Object} param The parameters to include with the remote method.
* @param {XMLHttpRequest|WebSocket|router} transport The transport object to use for
* for the request. The request is handled automatically based on this object
* type.
* @param {Boolean} [generateOnly=false] Flag denoting whether the
* request should only be generated and returned (true), or processed and a
* Promise object returned (false).
* @param {String|Number} [msgID=null] ID to include with the request.
* In order to differentiate requests/responses, this value should always
* be unique. If not provided, an internal integer value is used instead.
* @param {Boolean} [resolveOnID=true] If true, the returned promise resolves
* <i>only</i> when a response is received matching the message ID of the request,
* otherwise the first server response or notification resolves the returned promise.
*
* @return {Promise|Object} An asynchorous Promise or JSON-RPC 2.0 object if
* (generateOnly=true).<br/><br/>
*
* The returned Promise will resolve with either the immediate response (if <code>resolveOnID=false</code>)
* or when the mathing response ID matches the request ID (if <code>resolveOnID=true</code>).
* It will reject if the request could not be processed. Note that the result data
* for WebSocket objects is returned as the <i>data</i> property, a string, of
* the result while for XHR objects the result is the <i>target.response</i>
* property which is a native (parsed) object.<br/><br/>
*
* The generated JSON-RPC 2.0 object can be stringified and sent using another
* communication channel.
*
* @example
* var xhr = new XMLHttpRequest();
* xhr.open("POST", "http://localhost:8080");
* RPC("Hello", {}, xhr).then((event) => {
*   //event.target.response is a native (parsed) object in XHR replies
*   var dataObj = event.target.response;
*   alert("Hello" + JSON.stringify(dataObj));
* });
*
* @example
* async function callHTTPRPC() {
*   var xhr = new XMLHttpRequest();
*   xhr.open("POST", "http://localhost:8080");
*   var event = await RPC("Hello", {}, xhr);
*   var dataObj = event.target.response;
*   alert("Hello" + JSON.stringify(dataObj));
* }
* callHTTPRPC();
*
* @example
* var ws = new WebSocket("ws://localhost:8090");
* RPC("Hello", {}, ws).then((event) => {
*   //event.data is a string in WebSocket replies
*   var dataObj = JSON.parse(event.data);
*   alert("Hello" + JSON.stringify(dataObj));
* });
*
* @example
* async function callWSRPC() {
*  var ws = new WebSocket("ws://localhost:8090");
*  var event = await RPC("Hello", {}, ws);
*  var dataObj = JSON.parse(event.data);
*  alert("Hello" + JSON.stringify(dataObj));
* }
* callWSRPC();
*
* @example
* //note that this is just a regular function call and returns an object, not a
* //promise!
* let JSONRequest = RPC("Hello", {}, null, true);
*/
function RPC(method, params, transport, generateOnly=false, msgID=null, resolveOnID=true) {
   if ((transport == null) || (transport == undefined)) {
      var transportType = "generate";
  } else {
     if ((transport["response"] != null) && (transport["response"] != undefined)) {
       transportType = "http";
    } else if (transport.toString() == "APIRouter") {
        //non-API messaging is usually handled by the transport directly
        transportType = "router";
     } else {
       transportType = "websocket";
     }
  }
  if (msgID == null) {
      msgID = uniqueRPCID();
  }
  let requestObj = buildJSONRPC("request", {"method":method, "params":params, "id":msgID});
  //https / wss are assumed to have identical interfaces as http / ws (for now)
  switch (transportType) {
    case "http":
      //response ID will always match request ID when using HTTP/S
      transport.overrideMimeType("application/json-rpc");
      transport.responseType = "json";
      var promise = transport.onEventPromise("load");
      transport.send(JSON.stringify(requestObj));
      break;
    case "router":
      if (resolveOnID == true) {
         promise = transport.request(requestObj, msgID);
      } else {
         promise = transport.request(requestObj);
      }
      break;
    case "websocket":
      if (resolveOnID == true) {
         promise = new Promise((resolve, reject) => {
            handleRPCResponses(transport, "websocket", msgID, resolve, reject);
         })
      } else {
         promise = transport.onEventPromise("message");
      }
      if (transport.readyState != transport.OPEN) {
         //socket not yet connected
         transport.onEventPromise("open").then((event) => {
           transport.send(JSON.stringify(requestObj));
         });
      } else {
         //socket already open
         transport.send(JSON.stringify(requestObj));
      }
      break;
   case "generate":
      //generate only - swap generated request for usual promise
      promise = requestObj;
      break;
   default:
      break;
  }
  return (promise);
}

/**
* Handles asynchronous JSON-RPC 2.0 responses where a response ID must match a request ID before
* associated promises can be resolved.
*
* @param {Object} transport A reference to the network transport handling thr response.
* @param {String} type The transport type being handled. Supported types include: "websocket"
* @param {String|Number} expectedResponseID The expected response ID to match from messages
* received by the <code>transport</code> before resolving.
* @param {Function} resolve A promise resolve function to invoke with the response data when
* the response ID matches <code>expectedResponseID</code>
* @param {Function} reject A promise reject function. Not currently used.
*
* @async
*/
async function handleRPCResponses(transport, type, expectedResponseID, resolve, reject) {
   if (type == "websocket") {
      var responseID = null;
      while (responseID != expectedResponseID) {
         var response = await transport.onEventPromise("message");
         var responseObj = JSON.parse(response.data);
         responseID = responseObj.id;
      }
      resolve(response);
   }
}

/**
* Builds a valid JSON-RPC request, result, or notification object.
*
* @param {String} [type="request"] The JSON-RPC message type. Valid types include:<br/>
* <ul>
* <li><code>"request"</code>: A request / method invocation object. </li>
* <li><code>"result"</code>: An invocation result object.</li>
* <li><code>"notification"</code>: A notification object.</li>
* </ul>
* @param {Object} [options=null] An object containing additional options
* for the returned object depending on its type.
* @param {Object} [options.id=null] An id value for the object. If <code>type</code>
* is a "result" or "request" and this value is <code>null</code> or omiited,
* a random value is used. If <code>type</code> is "notification" the <code>id</code>
* is ommitted according to specification.
* @param {String} [options.method=null] A remote RPC method to invoke. If <code>type</code>
* is "request" and this value is <code>null</code> or omiited, an exception is thrown.
* If <code>type</code> is not "request" this option is ignored.
* @param {Object|Array} [options.params=null] Parameters to invoke the remote <code>method</code>
* with. If <code>type</code> is not "request" this option is ignored.
* @param {String} [version="2.0"] The JSON-RPC version identifier.
*
* @return {Object} A JSON-RPC-formatted object of the defined type.
*
* @see https://www.jsonrpc.org/specification
*/
function buildJSONRPC (type="request", options=null, version="2.0") {
   var JSONRPC = new Object();
   JSONRPC.jsonrpc = version;
   if (options == null) {
      options = new Object();
   }
   if ((type == "result") || (type == "request")) {
      if (options["id"] == undefined) {
         JSONRPC.id = String(Math.random()).split(".")[1];
      } else {
         JSONRPC.id = options.id;
      }
   }
   if ((type == "result") || (type == "notification")) {
      JSONRPC.result = new Object();
   } else if (type == "request") {
      if (typeof(options["method"]) != "string") {
         throw (new Error("options.method must be a string."));
      }
      JSONRPC.method = options.method;
      if (options["params"] != undefined) {
         if (typeof(options.params) != "object") {
            throw (new Error("options.params must be an array or object."));
         }
         JSONRPC.params = options.params;
      }
   }
   return (JSONRPC);
}

/**
* Returns a unique RPC <code>id</code> value that can be used to identify
* JSON-RPC 2.0 messages.
*
* @return {String} A unique <code>id</code>.
*/
function uniqueRPCID() {
   __rpc_request_id++;
   return (String(__rpc_request_id));
}