Source: index.js

/**
* @file Main file responsible for starting up the web (client) portion of
* CypherPoker.JS. Also provides functionality for dynamic loading of additional
* scripts and JSON data.
*
* @version 0.5.0
*/

/**
* @property {String} appVersion The version of the application. This information
* is appended to the {@link appTitle}.
*/
var appVersion = "0.5.0";
/**
* @property {String} appName The name of the application. This information
* is prepended to the {@link appTitle}.
*/
var appName = "CypherPoker.JS";
/**
* @property {String} appTitle The title of the application as it should appear in
* the main browser window / tab and any new windows / tabs. If running as a desktop
* (Electron) application, this is the name that appears on all child windows of
* the main process.
*/
var appTitle = appName+" v"+appVersion;
/**
* @property {String} _settingsURL="./scripts/settings.json" The URL of the main
* settings JSON file.
* @private
*/
const _settingsURL = "./scripts/settings.json";
/**
* @property {Boolean} _useCache=false Will force script-based loads to bypass
* local browser caches if false.
* @private
*/
const _useCache = false;
/**
* @property {CypherPoker} cypherpoker=null A reference to the main CypherPoker.JS instance.
* @private
*/
var cypherpoker = null;
/**
* @property {CypherPokerUI} ui=null A reference to the CypherPokerUI instance
* @private
*/
var ui = null;
/**
* @property {Object} hostEnv=null Contains settings and references supplied by
* a non-browser host environment such as Electron. When running as a standard web
* page in a browser this value should remain null.
*/
var hostEnv = null;
/**
* @property {Object} ipcRenderer=null A reference to the <code>ipcRenderer</code>
* object of the host desktop (Electron) environment. If this script is running
* within a standard web browser this reference will remain <code>null</code>.
*/
var ipcRenderer = null;
/**
* @property {String} ipcID=null an interprocess communication ID used to
* identify this window (child process) to the main process. If not running
* in a desktop (Electron) environment, this value will remain <code>null</code>.
*/
var ipcID = null;
/**
* @property {Array} _require Indexed array of required external scripts,
* in the order that they must be loaded in.
* @property {String} _require.url The URL of the external script to load.
* @property {Function} [_require.onload] A function reference to invoke
* when the script is finished loading.
* @private
*/
const _require = [
   {"url":"./scripts/libs/Polyfills.js"},
   {"url":"./scripts/libs/EventDispatcher.js"},
   {"url":"./scripts/libs/EventPromise.js"},
   {"url":"./scripts/libs/SDB.js"},
   {"url":"./scripts/libs/RPC.js"},
   {"url":"./scripts/libs/transports/WSSClient.js"},
   {"url":"./scripts/libs/transports/WSSTunnel.js"},
   {"url":"./scripts/libs/transports/WebRTCClient.js"},
   {"url":"./scripts/libs/APIRouter.js"},
   {"url":"./scripts/libs/P2PRouter.js"},
   {"url":"./scripts/libs/ConnectivityManager.js"},
   {"url":"./scripts/libs/WorkerHost.js"},
   {"url":"./scripts/libs/SRACrypto.js"},
   {"url":"./scripts/libs/BigInteger.min.js"},
   {"url":"./scripts/CypherPokerGame.js"},
   {"url":"./scripts/CypherPokerPlayer.js"},
   {"url":"./scripts/CypherPokerAccount.js"},
   {"url":"./scripts/CypherPokerCard.js"},
   {"url":"./scripts/CypherPokerContract.js"},
   {"url":"./scripts/CypherPokerAnalyzer.js"},
   {"url":"./scripts/CypherPokerUI.js",
      "onload": () => {
         var promise = new Promise((resolve, reject) => {
            //game UI to be contained in the #game element
            var gameElement = document.querySelector("#game");
            ui = new CypherPokerUI(gameElement);
            ui.initialize();
            resolve(true);
         })
         return (promise);
      }
   },
   {"url":"./scripts/CypherPoker.js",
      "onload": () => {
         var promise = new Promise((resolve, reject) => {
            ui.showDialog ("Loading game settings...");
            //EventDispatcher and EventPromise must already exist here!
            loadJSON(_settingsURL).onEventPromise("load").then(promise => {
               if (promise.target.response != null) {
                  cypherpoker = new CypherPoker(promise.target.response);
                  ui.cypherpoker = cypherpoker; //attach the cypherpoker instance to the UI
                  var urlParams = parseURLParameters(document.location);
                  var startOptions = new Object();
                  startOptions.urlParams = urlParams;
                  cypherpoker.start(startOptions).then(result => {
                     console.log ("CypherPoker.JS instance fully started and connected.");
                     resolve(true);
                  }).catch(err => {
                     ui.showDialog(err.message);
                     console.error(err.stack);
                     reject(false);
                  });
               } else {
                  alert (`Settings data (${_settingsURL}) not loaded or parsed.`);
                  throw (new Error(`Settings data (${_settingsURL}) not loaded or parsed.`));
                  reject(false);
               }
            });
         });
      }
   }
]

/**
* Parses a supplied URL string that may contain parameters (e.g. document.location),
* and returns an object with the parameters parsed to name-value pairs. Any URL-encoded
* properties are decoded to native representations prior to being parsed.
*
* @param {String} urlString The URL string, either absolute or relative, to parse.
*
* @return {URLSearchParams} A [URLSearchParams]{@link https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams}
* instance containing the parsed name-value pairs found in the <code>urlString</code>.
*
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams}
*/
function parseURLParameters(urlString) {
   var decodedURL = decodeURI(urlString);
   var urlObj = new URL(decodedURL);
   return (urlObj.searchParams);
}

/**
* Loads an external JavaScript file by adding a <script> tag to the
* DOM's <head> tag.
*
* @param {String} scriptURL The URL of the script to load and parse.
* @private
*/
function loadJavaScript(scriptURL) {
   var script = document.createElement("script");
   script.setAttribute("type", "text/javascript");
   script.setAttribute("language", "text/JavaScript");
   if (_useCache == false) {
      //force script load (ignore cache)
      scriptURL = scriptURL + "?" + String(Math.random()).split("0.")[1];
   }
   script.setAttribute("src", scriptURL);
   script.addEventListener ("load", onLoadJavaScript);
   document.getElementsByTagName("head")[0].appendChild(script);
}

/**
* Event handler invoked when an external JavaScript file has completed loading.
* The next file in the {@link _requires} array is automatically loaded.
*
* @param {Event} event A standard DOM event object.
* @private
* @async
*/
async function onLoadJavaScript(event) {
   var loadedObj = _require.shift(); //important! -- remove current element from array
   var loadedURL = loadedObj.url;
   var loadedTimeStamp = new Date(event.timeStamp);
   console.log (`"${loadedURL}" loaded at ${loadedTimeStamp.getSeconds()}s ${loadedTimeStamp.getMilliseconds()}ms`);
   if (_require.length > 0) {
      loadJavaScript (_require[0].url);
   } else {
      console.log (`All scripts loaded in ${loadedTimeStamp.getSeconds()}s ${loadedTimeStamp.getMilliseconds()}ms`);
   }
   if (typeof loadedObj["onload"] == "function") {
      await loadedObj.onload();
   }
}

/**
* Loads an external JSON data file using XMLHTTPRequest.
*
* @param {String} jsonURL The URL of the JSON data file to load and parse.
*
* @return {XMLHTTPRequest} The XHR instance used to load the data.
* @private
*/
function loadJSON(jsonURL) {
   var xhr = new XMLHttpRequest();
   if (_useCache == false) {
      //force new data load
      jsonURL = jsonURL + "?" + String(Math.random()).split("0.")[1];
   }
   xhr.open("GET", jsonURL);
   xhr.overrideMimeType("application/json");
   xhr.responseType = "json";
   xhr.send();
   return (xhr);
}

/**
* Sends an IPC command to the main Electron process if this script is
* running within a desktop (Electron) environment.
*
* @param {String} command The command to send to the main process via IPC.
* @param {*} [data=null] Any accompanying data to include with the <code>command</code>.
* If omitted or <code>null</code>, an empty object is created.
* @param {Boolean} [async=false] Sends the request asynchronously, immediately
* returning a promise instead of the synchronous response object. Synchronous requests
* <code>async=false</code> will block the main thread.
*
* @return {Object|Promise} A reply object is immediately returned if the desktop IPC
* interface is available otherwise <code>null</code> is returned. If <code>async=true</code>,
* a promise is returned instead that resolves with the reply object or rejects with an error.
* The behaiour of the promise matches the behaviour of the synchronous reply.
*/
function IPCSend (command, data=null, async=false) {
   if (async == true) {
      var promise = new Promise((resolve, reject) => {
         if (isDesktop()) {
            var request = new Object();
            request.command = command;
            if (data == null) {
               data = new Object();
            }
            request.async = true;
            request.data = data;
            request.data.ipcID = ipcID;
            var responseID = command + ipcID;
            try {
               ipcRenderer.once(responseID, (senderObj, replyObj) => {
                  resolve(replyObj);
               });
               ipcRenderer.send("ipc-main", request);
            } catch (err) {
               reject (err);
            }
         } else {
            resolve (null);
         }
      });
      return (promise);
   } else {
      if (isDesktop()) {
         var request = new Object();
         request.command = command;
         if (data == null) {
            data = new Object();
         }
         request.async = false;
         request.data = data;
         request.data.ipcID = ipcID;
         try {
            return (ipcRenderer.sendSync("ipc-main", request));
         } catch (err) {
            console.error (err.stack);
         }
      } else {
         return (null);
      }
   }
}


/**
* Invoked when an interprocess message is asynchronously received
* from the main process on the "ipc-main" channel. The synchronous IPC response
* will be an object with at least a response <code>type</code> string and some
* <code>data</code>. If this script is not running in a desktop (Electron)
* host environemnt this handler will never be invoked.
*
* @param {Event} event The event being dispatched.
* @param {Object} request The request object. It must contain at least
* a <code>command</string> to process by the handler.
*
* @private
*/
function onIPCMessage(event, request) {
   var response = new Object();
   if (request.ipcID != ipcID) {
      //not for this window / child process
      return;
   }
   //be sure not to include any circular references in the response
   //since it will be stringified before being returned...
   switch (request.command) {
      default:
         response.type = "error";
         response.data = new Object();
         response.data.code = -1;
         response.data.message = "Unrecognized IPC request command \""+request.command+"\"";
         break;
   }
   event.returnValue = response; //respond immediately
   //...or respond asynchronously:
   //event.sender.send(request.ipcID, response);
}

/**
* Invoked when a key, or key combination, is pressed on the keyboard.
*
* @param {Event} event The event being dispatched.
* @param {Object} request The request object. It must contain at least
* a <code>command</string> to process by the handler.
*
* @private
*/
function onKeyPress(event) {
   const key = event.key;
   var alt = event.altKey;
   var ctrl = event.ctrlKey;
   var shift = event.shiftKey;
   if (isDesktop()) {
      //matches Dev Tools toogle keyboard shortcut in standard browser
      if ((ctrl == true) && (alt == false) && (shift == true) && ((key == "i") || (key == "I"))) {
         //toggle Dev Tools on all open windows:
         // IPCSend("toggle-devtools", {all:true});
         IPCSend("toggle-devtools");
      }
   }
}

/**
* Tests whether or not the host environment is a desktop (Electron) one.
*
* @return {Boolean} True if the host environment is a desktop (Electron) one
* otherwise it's a standard web (browser) host environment.
*/
function isDesktop() {
   if ((ipcRenderer != null) && (ipcID != null)) {
      return (true);
   }
   return (false);
}

/**
* Main page load handler; invokes {@link loadJavaScript} with the first
* JavaScript file found in the {@link _requires} array.
* @private
*/
onload = function () {
   try {
      //try initializing through Electron IPC
      ipcRenderer = require("electron").ipcRenderer;
      ipcRenderer.on("ipc-main", onIPCMessage); //set IPC message handler
      ipcID = String(Math.random()).split("0.")[1];
      var initData = new Object();
      initData.ipcID = ipcID;
      hostEnv = IPCSend("init", initData).data;
      appVersion = hostEnv.version;
      appName = hostEnv.name;
      appTitle = hostEnv.title;
      console.log ("Desktop (Electron) host environment detected.");
   } catch (err) {
      //probably running in standard browser
      console.log ("Browser (web) host environment detected.");
      ipcRenderer = null;
      hostEnv = null;
      ipcID = null;
   } finally {
      window.addEventListener('keydown', onKeyPress);
   }
   document.title = appTitle;
   loadJavaScript (_require[0].url);
}