Source: index.js

  1. /**
  2. * @file Main file responsible for starting up the web (client) portion of
  3. * CypherPoker.JS. Also provides functionality for dynamic loading of additional
  4. * scripts and JSON data.
  5. *
  6. * @version 0.5.0
  7. */
  8. /**
  9. * @property {String} appVersion The version of the application. This information
  10. * is appended to the {@link appTitle}.
  11. */
  12. var appVersion = "0.5.0";
  13. /**
  14. * @property {String} appName The name of the application. This information
  15. * is prepended to the {@link appTitle}.
  16. */
  17. var appName = "CypherPoker.JS";
  18. /**
  19. * @property {String} appTitle The title of the application as it should appear in
  20. * the main browser window / tab and any new windows / tabs. If running as a desktop
  21. * (Electron) application, this is the name that appears on all child windows of
  22. * the main process.
  23. */
  24. var appTitle = appName+" v"+appVersion;
  25. /**
  26. * @property {String} _settingsURL="./scripts/settings.json" The URL of the main
  27. * settings JSON file.
  28. * @private
  29. */
  30. const _settingsURL = "./scripts/settings.json";
  31. /**
  32. * @property {Boolean} _useCache=false Will force script-based loads to bypass
  33. * local browser caches if false.
  34. * @private
  35. */
  36. const _useCache = false;
  37. /**
  38. * @property {CypherPoker} cypherpoker=null A reference to the main CypherPoker.JS instance.
  39. * @private
  40. */
  41. var cypherpoker = null;
  42. /**
  43. * @property {CypherPokerUI} ui=null A reference to the CypherPokerUI instance
  44. * @private
  45. */
  46. var ui = null;
  47. /**
  48. * @property {Object} hostEnv=null Contains settings and references supplied by
  49. * a non-browser host environment such as Electron. When running as a standard web
  50. * page in a browser this value should remain null.
  51. */
  52. var hostEnv = null;
  53. /**
  54. * @property {Object} ipcRenderer=null A reference to the <code>ipcRenderer</code>
  55. * object of the host desktop (Electron) environment. If this script is running
  56. * within a standard web browser this reference will remain <code>null</code>.
  57. */
  58. var ipcRenderer = null;
  59. /**
  60. * @property {String} ipcID=null an interprocess communication ID used to
  61. * identify this window (child process) to the main process. If not running
  62. * in a desktop (Electron) environment, this value will remain <code>null</code>.
  63. */
  64. var ipcID = null;
  65. /**
  66. * @property {Array} _require Indexed array of required external scripts,
  67. * in the order that they must be loaded in.
  68. * @property {String} _require.url The URL of the external script to load.
  69. * @property {Function} [_require.onload] A function reference to invoke
  70. * when the script is finished loading.
  71. * @private
  72. */
  73. const _require = [
  74. {"url":"./scripts/libs/Polyfills.js"},
  75. {"url":"./scripts/libs/EventDispatcher.js"},
  76. {"url":"./scripts/libs/EventPromise.js"},
  77. {"url":"./scripts/libs/SDB.js"},
  78. {"url":"./scripts/libs/RPC.js"},
  79. {"url":"./scripts/libs/transports/WSSClient.js"},
  80. {"url":"./scripts/libs/transports/WSSTunnel.js"},
  81. {"url":"./scripts/libs/transports/WebRTCClient.js"},
  82. {"url":"./scripts/libs/APIRouter.js"},
  83. {"url":"./scripts/libs/P2PRouter.js"},
  84. {"url":"./scripts/libs/ConnectivityManager.js"},
  85. {"url":"./scripts/libs/WorkerHost.js"},
  86. {"url":"./scripts/libs/SRACrypto.js"},
  87. {"url":"./scripts/libs/BigInteger.min.js"},
  88. {"url":"./scripts/CypherPokerGame.js"},
  89. {"url":"./scripts/CypherPokerPlayer.js"},
  90. {"url":"./scripts/CypherPokerAccount.js"},
  91. {"url":"./scripts/CypherPokerCard.js"},
  92. {"url":"./scripts/CypherPokerContract.js"},
  93. {"url":"./scripts/CypherPokerAnalyzer.js"},
  94. {"url":"./scripts/CypherPokerUI.js",
  95. "onload": () => {
  96. var promise = new Promise((resolve, reject) => {
  97. //game UI to be contained in the #game element
  98. var gameElement = document.querySelector("#game");
  99. ui = new CypherPokerUI(gameElement);
  100. ui.initialize();
  101. resolve(true);
  102. })
  103. return (promise);
  104. }
  105. },
  106. {"url":"./scripts/CypherPoker.js",
  107. "onload": () => {
  108. var promise = new Promise((resolve, reject) => {
  109. ui.showDialog ("Loading game settings...");
  110. //EventDispatcher and EventPromise must already exist here!
  111. loadJSON(_settingsURL).onEventPromise("load").then(promise => {
  112. if (promise.target.response != null) {
  113. cypherpoker = new CypherPoker(promise.target.response);
  114. ui.cypherpoker = cypherpoker; //attach the cypherpoker instance to the UI
  115. var urlParams = parseURLParameters(document.location);
  116. var startOptions = new Object();
  117. startOptions.urlParams = urlParams;
  118. cypherpoker.start(startOptions).then(result => {
  119. console.log ("CypherPoker.JS instance fully started and connected.");
  120. resolve(true);
  121. }).catch(err => {
  122. ui.showDialog(err.message);
  123. console.error(err.stack);
  124. reject(false);
  125. });
  126. } else {
  127. alert (`Settings data (${_settingsURL}) not loaded or parsed.`);
  128. throw (new Error(`Settings data (${_settingsURL}) not loaded or parsed.`));
  129. reject(false);
  130. }
  131. });
  132. });
  133. }
  134. }
  135. ]
  136. /**
  137. * Parses a supplied URL string that may contain parameters (e.g. document.location),
  138. * and returns an object with the parameters parsed to name-value pairs. Any URL-encoded
  139. * properties are decoded to native representations prior to being parsed.
  140. *
  141. * @param {String} urlString The URL string, either absolute or relative, to parse.
  142. *
  143. * @return {URLSearchParams} A [URLSearchParams]{@link https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams}
  144. * instance containing the parsed name-value pairs found in the <code>urlString</code>.
  145. *
  146. * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams}
  147. */
  148. function parseURLParameters(urlString) {
  149. var decodedURL = decodeURI(urlString);
  150. var urlObj = new URL(decodedURL);
  151. return (urlObj.searchParams);
  152. }
  153. /**
  154. * Loads an external JavaScript file by adding a <script> tag to the
  155. * DOM's <head> tag.
  156. *
  157. * @param {String} scriptURL The URL of the script to load and parse.
  158. * @private
  159. */
  160. function loadJavaScript(scriptURL) {
  161. var script = document.createElement("script");
  162. script.setAttribute("type", "text/javascript");
  163. script.setAttribute("language", "text/JavaScript");
  164. if (_useCache == false) {
  165. //force script load (ignore cache)
  166. scriptURL = scriptURL + "?" + String(Math.random()).split("0.")[1];
  167. }
  168. script.setAttribute("src", scriptURL);
  169. script.addEventListener ("load", onLoadJavaScript);
  170. document.getElementsByTagName("head")[0].appendChild(script);
  171. }
  172. /**
  173. * Event handler invoked when an external JavaScript file has completed loading.
  174. * The next file in the {@link _requires} array is automatically loaded.
  175. *
  176. * @param {Event} event A standard DOM event object.
  177. * @private
  178. * @async
  179. */
  180. async function onLoadJavaScript(event) {
  181. var loadedObj = _require.shift(); //important! -- remove current element from array
  182. var loadedURL = loadedObj.url;
  183. var loadedTimeStamp = new Date(event.timeStamp);
  184. console.log (`"${loadedURL}" loaded at ${loadedTimeStamp.getSeconds()}s ${loadedTimeStamp.getMilliseconds()}ms`);
  185. if (_require.length > 0) {
  186. loadJavaScript (_require[0].url);
  187. } else {
  188. console.log (`All scripts loaded in ${loadedTimeStamp.getSeconds()}s ${loadedTimeStamp.getMilliseconds()}ms`);
  189. }
  190. if (typeof loadedObj["onload"] == "function") {
  191. await loadedObj.onload();
  192. }
  193. }
  194. /**
  195. * Loads an external JSON data file using XMLHTTPRequest.
  196. *
  197. * @param {String} jsonURL The URL of the JSON data file to load and parse.
  198. *
  199. * @return {XMLHTTPRequest} The XHR instance used to load the data.
  200. * @private
  201. */
  202. function loadJSON(jsonURL) {
  203. var xhr = new XMLHttpRequest();
  204. if (_useCache == false) {
  205. //force new data load
  206. jsonURL = jsonURL + "?" + String(Math.random()).split("0.")[1];
  207. }
  208. xhr.open("GET", jsonURL);
  209. xhr.overrideMimeType("application/json");
  210. xhr.responseType = "json";
  211. xhr.send();
  212. return (xhr);
  213. }
  214. /**
  215. * Sends an IPC command to the main Electron process if this script is
  216. * running within a desktop (Electron) environment.
  217. *
  218. * @param {String} command The command to send to the main process via IPC.
  219. * @param {*} [data=null] Any accompanying data to include with the <code>command</code>.
  220. * If omitted or <code>null</code>, an empty object is created.
  221. * @param {Boolean} [async=false] Sends the request asynchronously, immediately
  222. * returning a promise instead of the synchronous response object. Synchronous requests
  223. * <code>async=false</code> will block the main thread.
  224. *
  225. * @return {Object|Promise} A reply object is immediately returned if the desktop IPC
  226. * interface is available otherwise <code>null</code> is returned. If <code>async=true</code>,
  227. * a promise is returned instead that resolves with the reply object or rejects with an error.
  228. * The behaiour of the promise matches the behaviour of the synchronous reply.
  229. */
  230. function IPCSend (command, data=null, async=false) {
  231. if (async == true) {
  232. var promise = new Promise((resolve, reject) => {
  233. if (isDesktop()) {
  234. var request = new Object();
  235. request.command = command;
  236. if (data == null) {
  237. data = new Object();
  238. }
  239. request.async = true;
  240. request.data = data;
  241. request.data.ipcID = ipcID;
  242. var responseID = command + ipcID;
  243. try {
  244. ipcRenderer.once(responseID, (senderObj, replyObj) => {
  245. resolve(replyObj);
  246. });
  247. ipcRenderer.send("ipc-main", request);
  248. } catch (err) {
  249. reject (err);
  250. }
  251. } else {
  252. resolve (null);
  253. }
  254. });
  255. return (promise);
  256. } else {
  257. if (isDesktop()) {
  258. var request = new Object();
  259. request.command = command;
  260. if (data == null) {
  261. data = new Object();
  262. }
  263. request.async = false;
  264. request.data = data;
  265. request.data.ipcID = ipcID;
  266. try {
  267. return (ipcRenderer.sendSync("ipc-main", request));
  268. } catch (err) {
  269. console.error (err.stack);
  270. }
  271. } else {
  272. return (null);
  273. }
  274. }
  275. }
  276. /**
  277. * Invoked when an interprocess message is asynchronously received
  278. * from the main process on the "ipc-main" channel. The synchronous IPC response
  279. * will be an object with at least a response <code>type</code> string and some
  280. * <code>data</code>. If this script is not running in a desktop (Electron)
  281. * host environemnt this handler will never be invoked.
  282. *
  283. * @param {Event} event The event being dispatched.
  284. * @param {Object} request The request object. It must contain at least
  285. * a <code>command</string> to process by the handler.
  286. *
  287. * @private
  288. */
  289. function onIPCMessage(event, request) {
  290. var response = new Object();
  291. if (request.ipcID != ipcID) {
  292. //not for this window / child process
  293. return;
  294. }
  295. //be sure not to include any circular references in the response
  296. //since it will be stringified before being returned...
  297. switch (request.command) {
  298. default:
  299. response.type = "error";
  300. response.data = new Object();
  301. response.data.code = -1;
  302. response.data.message = "Unrecognized IPC request command \""+request.command+"\"";
  303. break;
  304. }
  305. event.returnValue = response; //respond immediately
  306. //...or respond asynchronously:
  307. //event.sender.send(request.ipcID, response);
  308. }
  309. /**
  310. * Invoked when a key, or key combination, is pressed on the keyboard.
  311. *
  312. * @param {Event} event The event being dispatched.
  313. * @param {Object} request The request object. It must contain at least
  314. * a <code>command</string> to process by the handler.
  315. *
  316. * @private
  317. */
  318. function onKeyPress(event) {
  319. const key = event.key;
  320. var alt = event.altKey;
  321. var ctrl = event.ctrlKey;
  322. var shift = event.shiftKey;
  323. if (isDesktop()) {
  324. //matches Dev Tools toogle keyboard shortcut in standard browser
  325. if ((ctrl == true) && (alt == false) && (shift == true) && ((key == "i") || (key == "I"))) {
  326. //toggle Dev Tools on all open windows:
  327. // IPCSend("toggle-devtools", {all:true});
  328. IPCSend("toggle-devtools");
  329. }
  330. }
  331. }
  332. /**
  333. * Tests whether or not the host environment is a desktop (Electron) one.
  334. *
  335. * @return {Boolean} True if the host environment is a desktop (Electron) one
  336. * otherwise it's a standard web (browser) host environment.
  337. */
  338. function isDesktop() {
  339. if ((ipcRenderer != null) && (ipcID != null)) {
  340. return (true);
  341. }
  342. return (false);
  343. }
  344. /**
  345. * Main page load handler; invokes {@link loadJavaScript} with the first
  346. * JavaScript file found in the {@link _requires} array.
  347. * @private
  348. */
  349. onload = function () {
  350. try {
  351. //try initializing through Electron IPC
  352. ipcRenderer = require("electron").ipcRenderer;
  353. ipcRenderer.on("ipc-main", onIPCMessage); //set IPC message handler
  354. ipcID = String(Math.random()).split("0.")[1];
  355. var initData = new Object();
  356. initData.ipcID = ipcID;
  357. hostEnv = IPCSend("init", initData).data;
  358. appVersion = hostEnv.version;
  359. appName = hostEnv.name;
  360. appTitle = hostEnv.title;
  361. console.log ("Desktop (Electron) host environment detected.");
  362. } catch (err) {
  363. //probably running in standard browser
  364. console.log ("Browser (web) host environment detected.");
  365. ipcRenderer = null;
  366. hostEnv = null;
  367. ipcID = null;
  368. } finally {
  369. window.addEventListener('keydown', onKeyPress);
  370. }
  371. document.title = appTitle;
  372. loadJavaScript (_require[0].url);
  373. }