dcp-client 4.1.11 → 4.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -4988,7 +4988,7 @@ eval("/**\n * @file bank.js\n * @author Ryan Rossiter, ryan@kingsds.
|
|
|
4988
4988
|
/*! no static exports found */
|
|
4989
4989
|
/***/ (function(module, exports, __webpack_require__) {
|
|
4990
4990
|
|
|
4991
|
-
eval("/**\n * @file alert.js\n * Replacement for native `window.alert` to fit in with the rest of our modal system.\n * \n * @author Nazila Kharazian - nazila@kingsds.network\n * @date Apr 2020\n */\nconst utils = __webpack_require__(/*! ./utils */ \"./src/dcp-client/client-modal/utils.js\");\n\nexports.config = {\n id: 'dcp-modal-alert',\n required: ['dcp-modal-alert-form', 'dcp-modal-alert-message', 'dcp-modal-alert-submit-button'],\n optional: ['dcp-modal-alert-title'],\n path: \"./templates/alert-modal.html\",\n};\n\n/**\n * @param {string} message The text which will be alerted.\n * @param {Object} message An instanceof Error whose message will be alerted, and stack trace made available.\n * @param {Object} options\n * @param {string} [options.submit=\"OK\"] Text on the submit button.\n * @param {string} [options.title=\"\"] Text in the title area.\n * @param {function} [options.onClose] Override default onClose behavior. An instance of `modal` is handed to the function.\n * @throws If modal is closed without the form being submitted (submitting the form closes the modal).\n * @memberof module:dcp/client-modal\n * @alias alert\n * @async\n * @access public\n */\nexports.alert = async function (message, {submit = \"OK\", title = \"\", onClose = null} ={}) {\n const [elements, formPromise, optionalElements] = await utils.initModal(exports.config, onClose);\n const [modal, form, formMessage, submitButton] = elements;\n const [modalTitle] = optionalElements;\n \n if(message instanceof Object)\n message =JSON.stringify(message, null, 2);\n\n formMessage.innerText = message;\n submitButton.textContent = submit;\n if (modalTitle) modalTitle.innerText = title;\n\n if (message instanceof Error) {\n let bug = modal.querySelector('.dcp-bug-icon');\n let stack = modal.querySelector('.dcp-stack-trace');\n \n modal.classList.add('exception');\n if (bug)\n bug.addEventListener('click', () => modal.classList.toggle('dcp-show-stack'));\n if (stack)\n stack.textContent = message.stack.replace(new RegExp('@webpack:/*', 'g'), '@');\n } else {\n modal.classList.remove('exception');\n }\n \n await formPromise;\n utils.MicroModal.close(modal.id);\n}\n\n\n
|
|
4991
|
+
eval("/**\n * @file alert.js\n * Replacement for native `window.alert` to fit in with the rest of our modal system.\n * \n * @author Nazila Kharazian - nazila@kingsds.network\n * @date Apr 2020\n */\nconst utils = __webpack_require__(/*! ./utils */ \"./src/dcp-client/client-modal/utils.js\");\n\nexports.config = {\n id: 'dcp-modal-alert',\n required: ['dcp-modal-alert-form', 'dcp-modal-alert-message', 'dcp-modal-alert-submit-button'],\n optional: ['dcp-modal-alert-title'],\n path: \"./templates/alert-modal.html\",\n};\n\n/**\n * @param {string} message The text which will be alerted.\n * @param {Object} message An instanceof Error whose message will be alerted, and stack trace made available.\n * @param {Object} options\n * @param {string} [options.submit=\"OK\"] Text on the submit button.\n * @param {string} [options.title=\"\"] Text in the title area.\n * @param {function} [options.onClose] Override default onClose behavior. An instance of `modal` is handed to the function.\n * @throws If modal is closed without the form being submitted (submitting the form closes the modal).\n * @memberof module:dcp/client-modal\n * @alias alert\n * @async\n * @access public\n */\nexports.alert = async function (message, {submit = \"OK\", title = \"\", onClose = null} ={}) {\n const [elements, formPromise, optionalElements] = await utils.initModal(exports.config, onClose);\n const [modal, form, formMessage, submitButton] = elements;\n const [modalTitle] = optionalElements;\n \n if(message instanceof Object && !(message instanceof Error))\n message =JSON.stringify(message, null, 2);\n\n formMessage.innerText = message;\n submitButton.textContent = submit;\n if (modalTitle) modalTitle.innerText = title;\n\n if (message instanceof Error) {\n let bug = modal.querySelector('.dcp-bug-icon');\n let stack = modal.querySelector('.dcp-stack-trace');\n \n modal.classList.add('exception');\n if (bug)\n bug.addEventListener('click', () => modal.classList.toggle('dcp-show-stack'));\n if (stack)\n stack.textContent = message.stack.replace(new RegExp('@webpack:/*', 'g'), '@');\n } else {\n modal.classList.remove('exception');\n }\n \n await formPromise;\n utils.MicroModal.close(modal.id);\n}\n\n\n//# sourceURL=webpack:///./src/dcp-client/client-modal/alert.js?");
|
|
4992
4992
|
|
|
4993
4993
|
/***/ }),
|
|
4994
4994
|
|
|
@@ -5065,7 +5065,7 @@ eval("/**\n * @file password.js\n * Modal providing a way to
|
|
|
5065
5065
|
/*! no static exports found */
|
|
5066
5066
|
/***/ (function(module, exports, __webpack_require__) {
|
|
5067
5067
|
|
|
5068
|
-
eval("/**\n * @file client-modal/utils.js\n * @author KC Erb\n * @date Mar 2020\n * \n * All shared functions among the modals.\n */\nconst { fetchRelative } = __webpack_require__(/*! ./fetch-relative */ \"./src/dcp-client/client-modal/fetch-relative.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nexports.OnCloseErrorCode = 'DCP_CM:CANCELX';\n\nif (DCP_ENV.isBrowserPlatform) {\n // Provide as export for the convenience of `utils.MicroModal` instead of a separate require.\n exports.MicroModal = __webpack_require__(/*! micromodal */ \"./node_modules/micromodal/dist/micromodal.es.js\").default;\n}\n\n/**\n * Return a unique string, formatted as a GET parameter, that changes often enough to\n * always force the browser to fetch the latest version of our resource.\n *\n * @note Currently always returns the Date-based poison due to webpack. \n */\nfunction cachePoison() {\n if (true)\n return '?ucp=
|
|
5068
|
+
eval("/**\n * @file client-modal/utils.js\n * @author KC Erb\n * @date Mar 2020\n * \n * All shared functions among the modals.\n */\nconst { fetchRelative } = __webpack_require__(/*! ./fetch-relative */ \"./src/dcp-client/client-modal/fetch-relative.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nexports.OnCloseErrorCode = 'DCP_CM:CANCELX';\n\nif (DCP_ENV.isBrowserPlatform) {\n // Provide as export for the convenience of `utils.MicroModal` instead of a separate require.\n exports.MicroModal = __webpack_require__(/*! micromodal */ \"./node_modules/micromodal/dist/micromodal.es.js\").default;\n}\n\n/**\n * Return a unique string, formatted as a GET parameter, that changes often enough to\n * always force the browser to fetch the latest version of our resource.\n *\n * @note Currently always returns the Date-based poison due to webpack. \n */\nfunction cachePoison() {\n if (true)\n return '?ucp=dd98e423ca01eaeeba22e0bd0d948e358ffc7b43'; /* installer token */\n return '?ucp=' + Date.now();\n}\n \n/* Detect load type - on webpack, load dynamic content relative to webpack bundle;\n * otherwise load relative to the current scheduler's configured portal.\n */\nexports.myScript = (typeof document !== 'undefined') && document.currentScript;\nexports.corsProxyHref = undefined;\nif (exports.myScript && exports.myScript === __webpack_require__(/*! ./fetch-relative */ \"./src/dcp-client/client-modal/fetch-relative.js\").myScript) {\n let url = new (__webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\").DcpURL)(exports.myScript.src);\n exports.corsProxyHref = url.resolve('../cors-proxy.html');\n}\n\n/**\n * Look for modal id and required ids on page based on config, if not found, provide from dcp-client.\n * The first id in the required array must be the id of the modal's form element.\n * @param {Object} modalConfig Modal configuration object\n * @param {string} modalConfig.id Id of parent modal element\n * @param {string[]} modalConfig.required Array of required ids in parent modal element\n * @param {string[]} [modalConfig.optional] Array of optional ids in parent modal element\n * @param {string} modalConfig.path Relative path to modal html in dcp-client\n * @returns {DOMElement[]} Array of modal elements on page [config.id, ...config.required]\n */\nexports.initModal = async function (modalConfig, onClose) {\n exports.corsProxyHref = exports.corsProxyHref || dcpConfig.portal.location.resolve('dcp-client/cors-proxy.html');\n\n // Call ensure modal on any eager-loaded modals.\n if (modalConfig.eagerLoad) {\n Promise.all(\n modalConfig.eagerLoad.map(config => ensureModal(config))\n )\n };\n\n const [elements, optionalElements] = await ensureModal(modalConfig);\n\n // Wire up form to prevent default, resolve on submission, reject+reset when closed (or call onClose when closed)\n const [modal, form] = elements;\n form.reset(); // ensure that form is fresh\n let formResolve, formReject;\n let formPromise = new Promise( function(res, rej) {\n formResolve = res;\n formReject = rej;\n });\n form.onsubmit = function (submitEvent) {\n submitEvent.preventDefault();\n modal.setAttribute(\"data-state\", \"submitted\");\n formResolve(submitEvent);\n }\n\n exports.MicroModal.show(modalConfig.id, { \n disableFocus: true, \n onClose: onClose || getDefaultOnClose(formReject)\n });\n return [elements, formPromise, optionalElements];\n};\n\n// Ensure all required modal elements are on page according to modalConfig\nasync function ensureModal(modalConfig) {\n let allRequiredIds = [modalConfig.id, ...modalConfig.required];\n let missing = allRequiredIds.filter( id => !document.getElementById(id) );\n if (missing.length > 0) {\n if (missing.length !== allRequiredIds.length)\n console.warn(`Some of the ids needed to replace the default DCP-modal were found, but not all. So the default DCP-Modal will be used. Missing ids are: [${missing}].`);\n let contents = await fetchRelative(exports.corsProxyHref, modalConfig.path + cachePoison());\n const container = document.createElement('div');\n container.innerHTML = contents;\n document.body.appendChild(container);\n }\n\n const elements = allRequiredIds.map(id => document.getElementById(id));\n const optionalElements = (modalConfig.optional || []).map(id => document.getElementById(id));\n return [elements, optionalElements];\n};\n\n// This onClose is called by MicroModal and thus has the modal passed to it.\nfunction getDefaultOnClose (formReject) {\n return (modal) => {\n modal.offsetLeft; // forces style recalc\n const origState = modal.dataset.state;\n // reset form including data-state\n modal.setAttribute(\"data-state\", \"new\");\n // reject if closed without submitting form.\n if (origState !== \"submitted\") {\n const err = new DCPError(\"Modal was closed but modal's form was not submitted.\", exports.OnCloseErrorCode);\n formReject(err);\n }\n }\n}\n\n\n//# sourceURL=webpack:///./src/dcp-client/client-modal/utils.js?");
|
|
5069
5069
|
|
|
5070
5070
|
/***/ }),
|
|
5071
5071
|
|
|
@@ -5076,7 +5076,7 @@ eval("/**\n * @file client-modal/utils.js\n * @author KC Erb\n * @date Mar 2020\
|
|
|
5076
5076
|
/*! no static exports found */
|
|
5077
5077
|
/***/ (function(module, exports, __webpack_require__) {
|
|
5078
5078
|
|
|
5079
|
-
eval("/**\n * @file Client facing module that implements Compute Groups API\n * @module dcp/compute-groups\n * @access public\n * @author Kayra E-A <kayra@kingsds.network>\n * @date Sept 2020\n */\n\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst hash = __webpack_require__(/*! ../../common/hash */ \"./src/common/hash.js\");\n\nconst { DCPError } = __webpack_require__(/*! ../../common/dcp-error */ \"./src/common/dcp-error.js\");\n\n/**\n * Establishes the client connection to the computeGroups microservice if it does not exist already from the default config.\n * \n * @returns {protocolV4.Connection}\n * @access public\n * @example\n * const result = await exports.serviceConnection.send('createGroup', {\n name: name,\n description: description,\n });\n */\n\n\nexports.serviceConnection = null;\n\nconst openServiceConn = function openServiceConn()\n{\n exports.serviceConnection = new protocolV4.Connection(dcpConfig.scheduler.services.computeGroups);\n exports.serviceConnection.on('close', openServiceConn)\n}\n\n\n/**\n * Resets the client connection to the computeGroups microservice.\n */\nexports.closeServiceConnection = async function closeServiceConnection() {\n if (exports.serviceConnection)\n {\n exports.serviceConnection.off('close', openServiceConn);\n exports.serviceConnection.close();\n }\n \n exports.serviceConnection = null;\n};\n\n/**\n * KeepAlive for the service connection to compute groups\n */\nexports.keepAlive = async function keepAlive() {\n if (!exports.serviceConnection)\n openServiceConn();\n \n exports.serviceConnection.keepalive().catch(err => console.error('Warning: keepalive failed for compute groups service', err));\n}\n\n /**\n * Fetches the public compute group ID from the scheduler constants.\n */\nexports.getPublicComputeGroupId = function getPublicComputeGroupId() {\n return __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups.public.id;\n};\n\n/**\n * Creates a Compute Group, optionally specifying certain aspects of it.\n *\n * @param {string} name The name of the compute group\n * @param {object} [options] Additional properties to specify\n * @param {string} [options.description] Description of the compute group. Is\n * auto-populated based on name and creation date if not specified.\n * @param {string} [options.joinKey] A \"username\" for the compute group. Must be\n * unique and can be specified without a joinSecret.\n * @param {string} [options.joinSecret] A \"password\" for the compute group. Must\n * be specified alongside a joinKey.\n * @returns {Promise<number>} The id of the compute group that was just created\n * @access public\n * @example\n * const groupId = await computeGroup.create('myGroup', {\n * description: 'This compute group is computing for...!'\n * joinKey: '...',\n * joinSecret: '...',\n * });\n */\nexports.create = async function create(name, options) {\n let description, joinKey, joinHash;\n\n if(options) {\n description = options.description;\n joinKey = options.joinKey;\n if(options.joinSecret)\n joinHash = exports.calculateJoinHash({joinKey, joinSecret: options.joinSecret});\n }\n\n if (typeof name !== 'string' || name === '') {\n throw new TypeError(\n `A compute group's name must be a non-empty string, not ${name}`,\n );\n }\n\n if (options && typeof options.joinSecret !== 'undefined' && typeof joinKey === 'undefined') {\n throw new Error(\n \"A compute group's join secret cannot be specified without a join key\",\n );\n }\n \n if (!exports.serviceConnection)\n openServiceConn();\n\n const { success, payload } = await exports.serviceConnection.send(\n 'createGroup',\n {\n name,\n description,\n joinKey,\n joinHash,\n },\n );\n\n if (!success) {\n switch (payload.code) {\n case 'ER_DUP_ENTRY':\n /**\n * If it's a mysql duplicate entry error, it's most likely referring to\n * the unique joinKey column.\n */\n throw new DCPError(\n `Cannot create another compute group with the joinKey '${joinKey}'. It is already in use.`,\n );\n default:\n throw new DCPError(\n `Unable to create Compute Group called ${name}`,\n payload,\n );\n }\n }\n\n return payload;\n};\n\n /**\n * Function that changes the properties of an existing Compute Group. The id (computeGroupId, this will be changed later) must be\n * specified as a number. Must be the owner of the Compute Group to change its properties. The joinSecret is entered as a string and stored as\n * an ethutil hash with 'eh1-' appended to the beginning, indicating an ethereum hash.\n * \n * Changing joinKey and joinSecret will always happen in tandem, since joinKey is salt for joinSecret Ethereum hash. \n * The joinSecret hash is calculated here on the client side - microservice assumes data is good.\n *\n *\n * @param {string} id - the of of the Compute Group.\n * @param {object} prototype - the object that contains the list of properties to change. This is name, description, joinKey, joinSecret, joinAddress\n * @param {string} prototype.name - the name of the Compute Group.\n * @param {string} prototype.description - the description of the Compute Group.\n * @param {string} prototype.joinKey - the joinKey for joining the Compute Group. Analogous to username.\n * @param {string} prototype.joinSecret - the joinSecret for joining the Compute Group. Analogous to password\n * @param {string} prototype.joinAddress - the joinAddress of the Compute Group.\n * @access public\n * @example\n * await computeGroup.changeGroup( id = 234, {name = 'new name', description = 'new description', joinKey = 'string', joinSecret = 'another string', joinAddress = 'ethAddress'} );\n */\nexports.changeGroup = async function changeGroup( id, prototype ) {\n\n if ( +(prototype.joinKey !== undefined) ^ +(prototype.joinSecret !== undefined) ){\n throw new DCPError(`Changes to joinKey or joinSecret must happen together!`);\n }\n\n if (!exports.serviceConnection)\n openServiceConn();\n\n const joinHash = exports.calculateJoinHash(prototype); /* warning, dcp 1905 */\n\n prototype.joinSecret = joinHash; /* change with dcp 1905 */\n\n const { success, payload } = await exports.serviceConnection.send('changeGroup', {\n id: id,\n prototype: prototype,\n });\n\n if (!success) {\n throw new DCPError(`Cannot change the compute group ${id}: ${payload}`);\n }\n};\n\n /**\n * Async function that deletes a Compute Group from the database. The Compute Group ID must be specified as a number.\n * Must be the owner of the Compute Group to delete it.\n * \n * @param {number} computeGroupId - the ID of the Compute Group that is to be deleted. Analogous to row ID in table.\n * @access public\n * @example\n * await computeGroup.deleteGroup(456);\n */\nexports.deleteGroup = async function deleteGroup(computeGroupId) {\n if (!exports.serviceConnection)\n openServiceConn();\n \n const { success, payload } = await exports.serviceConnection.send('deleteGroup', {\n id: computeGroupId,\n });\n\n if (!success) {\n throw new DCPError(`Cannot delete the compute group ${computeGroupId}: ${payload}`);\n }\n};\n\n/**\n * Async function that adds a job to a specified compute group. Both Compute\n * Group ID and Job ID must be specified as number Must be the owner of the\n * Compute Group to add a Job to it. The joinKey and the joinSecret\n * (user/password) must also be provided.\n *\n * @param {Address} job the opaque id of the Job that will be added to the Compute Group.\n * @param {object} descriptor the descriptor object for the compute group. This descriptor\n * needs to contain enough information to authorize access to the\n * compute group. Properties may include:\n * - id\n * - joinKey\n * - joinSecret\n * - joinAddress\n * \n * Additional, either the joinKey or the compute group id MUST be specified so\n * that we can identify the compute group in question.\n *\n * All compute groups can have jobs submitted to them, provided either the joinKey\n * or the id (maybe deprecated in the future?) are specified, and the message is\n * signed by the compute group owner.\n *\n * FUTURE - after DCP-1910\n * keystore A keystore used to grant access to job deployment within this compute group.\n * This can be either the ownerKeystore or the joinAddress keystore when the\n * compute group is in deployAccessType='join' mode.\n *\n * @access public\n * @example\n * await computeGroup.addJob(computeGroupId = 456, job = 'P+Y4IApeFQLrYS2W7MkVg7');\n */\nexports.addJob = async function addJob(job, descriptor)\n{\n var result;\n var message = Object.assign({}, {job: job});\n message.computeGroupId = descriptor.id;\n message.joinKey = descriptor.joinKey;\n if(descriptor.joinSecret)\n message.joinHashHash = hash.calculate(hash.eh1, exports.calculateJoinHash(descriptor), exports.serviceConnection.dcpsid);\n \n if (!message.job)\n throw new Error('job not specified');\n if (!message.computeGroupId && !message.joinKey)\n throw new Error('compute group not identified');\n \n if (!exports.serviceConnection)\n openServiceConn();\n\n result = await exports.serviceConnection.send('addJob', message);\n\n if (!result.success)\n throw new DCPError(result.payload.message);\n};\n\n /**\n * Async function that lists all the current Jobs in a Compute Group.\n * The Compute Group ID must be specified. Must be of type number.\n * Must be the owner of the Compute Group to list its current Jobs.\n *\n * @param {number} computeGroupId - the ID of the Compute Group from which we will list its jobs.\n * @returns {Array} - the array of Jobs belonging to the computeGroup.\n * @access public\n * @example\n * let listOfJobs = await computeGroup.listJobs(computeGroupId = 456);\n */\nexports.listJobs = async function listJobs(computeGroupId) {\n if (!exports.serviceConnection)\n openServiceConn();\n\n const { success, payload } = await exports.serviceConnection.send('listJobs', {\n computeGroupId: computeGroupId,\n });\n\n if (!success) {\n throw new DCPError(`Cannot list jobs for compute group ${computeGroupId}: ${payload}`);\n }\n\n return payload;\n};\n\n /**\n * Async function that removes a Compute Group from the the computeGroups table in the scheduler_dcp mysql database.\n * Both Compute Group ID and Job ID must be specified to remove Job from the group. Both of type number.\n * Must be the owner of the Compute Group to remove a Job from it.\n *\n * @param {number} computeGroupId - the ID of the Compute Group where the job will be removed. Analogous to row ID in table.\n * @param {address} job - the address of the Job that will be removed from the Compute Group.\n * @access public\n * @example\n * await computeGroup.removeJob(computeGroupId = 123456, job = 'P+Y4IApeFQLrYS2W7MkVg7');\n */\nexports.removeJob = async function removeJob(computeGroupId, job, joinKey, joinSecret) {\n var joinHashHash;\n if (!exports.serviceConnection)\n openServiceConn();\n\n if(joinSecret)\n joinHashHash = hash.calculate(hash.eh1, exports.calculateJoinHash({joinKey, joinSecret}, joinSecret), exports.serviceConnection.dcpsid);\n const { success, payload } = await exports.serviceConnection.send('removeJob', {\n computeGroupId: parseInt(computeGroupId),\n job: job,\n joinKey: joinKey,\n joinHashHash: joinHashHash,\n });\n\n if (!success) {\n throw new DCPError(`Cannot remove job ${job} from compute group ${computeGroupId}: ${payload}`);\n }\n};\n\n/**\n * Async function that cancel the specified job.\n *\n * @param {number} computeGroupId - the ID of the Compute Group.\n * @param {address} job - the address of the job that will be cancelled.\n * @access public\n * @example\n * await computeGroup.cancelJob(computeGroupId = 123456, job = 'P+Y4IApeFQLrYS2W7MkVg7');\n */\nexports.cancelJob = async function cancelJob(computeGroupId, job) {\n if (!exports.serviceConnection)\n openServiceConn();\n\n const { success, payload } = await exports.serviceConnection.send('cancelJob', {\n computeGroupId: parseInt(computeGroupId),\n job: job\n });\n\n if (!success) {\n throw new DCPError(`Cannot cancel job ${job} for compute group ${computeGroupId}: ${payload}`);\n }\n};\n\n/**\n * Async function that cancel all jobs.\n *\n * @param {number} computeGroupId - the ID of the Compute Group.\n * @access public\n * @example\n * await computeGroup.cancelAllJobs(computeGroupId = 123456);\n */\nexports.cancelAllJobs = async function cancelAllJobs(computeGroupId) {\n if (!exports.serviceConnection)\n openServiceConn();\n\n const { success, payload } = await exports.serviceConnection.send('cancelAllJobs', {\n computeGroupId: parseInt(computeGroupId)\n });\n\n if (!success) {\n throw new DCPError(`Cannot cancel all jobs for compute group ${computeGroupId}: ${payload}`);\n }\n};\n\n/**\n * Calculate a joinHash for a compute group. This is an eh1- hash of the cg salt and \n * joinSecret components of a compute group description.\n *\n * @param {object} details an object containing the cg salt, which is\n * the joinKey if the compute group uses one;\n * otherwise it is the joinAddress. This object\n * may also contain the joinSecret.\n * @param {string} joinSecret the join secret -- plain text -- that is\n * the \"password\" for the compute group. If not\n * specified, we use details.joinSecret.\n */\nexports.calculateJoinHash = function computeGroups$calculateJoinHash(details, joinSecret)\n{\n if (typeof joinSecret === 'undefined')\n joinSecret = details.joinSecret;\n\n return hash.calculate(hash.eh1, `${details.joinKey || details.joinAddress} ${details.joinSecret}`);\n}\n\n\n//# sourceURL=webpack:///./src/dcp-client/compute-groups/index.js?");
|
|
5079
|
+
eval("/**\n * @file Client facing module that implements Compute Groups API\n * @module dcp/compute-groups\n * @access public\n * @author Kayra E-A <kayra@kingsds.network>\n * @date Sept 2020\n */\n\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst hash = __webpack_require__(/*! ../../common/hash */ \"./src/common/hash.js\");\n\nconst { DCPError } = __webpack_require__(/*! ../../common/dcp-error */ \"./src/common/dcp-error.js\");\n\n/**\n * Establishes the client connection to the computeGroups microservice if it does not exist already from the default config.\n * \n * @returns {protocolV4.Connection}\n * @access public\n * @example\n * const result = await exports.serviceConnection.send('createGroup', {\n name: name,\n description: description,\n });\n */\n\n\nexports.serviceConnection = null;\n\nconst openServiceConn = function openServiceConn()\n{\n exports.serviceConnection = new protocolV4.Connection(dcpConfig.scheduler.services.computeGroups);\n exports.serviceConnection.on('close', openServiceConn)\n}\n\n\n/**\n * Resets the client connection to the computeGroups microservice.\n */\nexports.closeServiceConnection = async function closeServiceConnection() {\n if (exports.serviceConnection)\n {\n exports.serviceConnection.off('close', openServiceConn);\n exports.serviceConnection.close();\n }\n \n exports.serviceConnection = null;\n};\n\n/**\n * KeepAlive for the service connection to compute groups\n */\nexports.keepAlive = async function keepAlive() {\n if (!exports.serviceConnection)\n openServiceConn();\n \n exports.serviceConnection.keepalive().catch(err => console.error('Warning: keepalive failed for compute groups service', err));\n}\n\n /**\n * Fetches the public compute group ID from the scheduler constants.\n */\nexports.getPublicComputeGroupId = function getPublicComputeGroupId() {\n return __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups.public.id;\n};\n\n/**\n * Creates a Compute Group, optionally specifying certain aspects of it.\n *\n * @param {string} name The name of the compute group\n * @param {object} [options] Additional properties to specify\n * @param {string} [options.description] Description of the compute group. Is\n * auto-populated based on name and creation date if not specified.\n * @param {string} [options.joinKey] A \"username\" for the compute group. Must be\n * unique and can be specified without a joinSecret.\n * @param {string} [options.joinSecret] A \"password\" for the compute group. Must\n * be specified alongside a joinKey.\n * @returns {Promise<number>} The id of the compute group that was just created\n * @access public\n * @example\n * const groupId = await computeGroup.create('myGroup', {\n * description: 'This compute group is computing for...!'\n * joinKey: '...',\n * joinSecret: '...',\n * });\n */\nexports.create = async function create(name, options) {\n let description, joinKey, joinHash;\n\n if(options) {\n description = options.description;\n joinKey = options.joinKey;\n if(options.joinSecret)\n joinHash = exports.calculateJoinHash({joinKey, joinSecret: options.joinSecret});\n }\n\n if (typeof name !== 'string' || name === '') {\n throw new TypeError(\n `A compute group's name must be a non-empty string, not ${name}`,\n );\n }\n\n if (options && typeof options.joinSecret !== 'undefined' && typeof joinKey === 'undefined') {\n throw new Error(\n \"A compute group's join secret cannot be specified without a join key\",\n );\n }\n \n if (!exports.serviceConnection)\n openServiceConn();\n\n const { success, payload } = await exports.serviceConnection.send(\n 'createGroup',\n {\n name,\n description,\n joinKey,\n joinHash,\n },\n );\n\n if (!success) {\n switch (payload.code) {\n case 'ER_DUP_ENTRY':\n /**\n * If it's a mysql duplicate entry error, it's most likely referring to\n * the unique joinKey column.\n */\n throw new DCPError(\n `Cannot create another compute group with the joinKey '${joinKey}'. It is already in use.`,\n );\n default:\n throw new DCPError(\n `Unable to create Compute Group called ${name}`,\n payload,\n );\n }\n }\n\n return payload;\n};\n\n /**\n * Function that changes the properties of an existing Compute Group. The id (computeGroupId, this will be changed later) must be\n * specified as a number. Must be the owner of the Compute Group to change its properties. The joinSecret is entered as a string and stored as\n * an ethutil hash with 'eh1-' appended to the beginning, indicating an ethereum hash.\n * \n * Changing joinKey and joinSecret will always happen in tandem, since joinKey is salt for joinSecret Ethereum hash. \n * The joinSecret hash is calculated here on the client side - microservice assumes data is good.\n *\n *\n * @param {string} id - the of of the Compute Group.\n * @param {object} prototype - the object that contains the list of properties to change. This is name, description, joinKey, joinSecret, joinAddress\n * @param {string} prototype.name - the name of the Compute Group.\n * @param {string} prototype.description - the description of the Compute Group.\n * @param {string} prototype.joinKey - the joinKey for joining the Compute Group. Analogous to username.\n * @param {string} prototype.joinSecret - the joinSecret for joining the Compute Group. Analogous to password\n * @param {string} prototype.joinAddress - the joinAddress of the Compute Group.\n * @access public\n * @example\n * await computeGroup.changeGroup( id = 234, {name = 'new name', description = 'new description', joinKey = 'string', joinSecret = 'another string', joinAddress = 'ethAddress'} );\n */\nexports.changeGroup = async function changeGroup( id, prototype ) {\n\n if ( +(prototype.joinKey !== undefined) ^ +(prototype.joinSecret !== undefined) ){\n throw new DCPError(`Changes to joinKey or joinSecret must happen together!`);\n }\n\n if (!exports.serviceConnection)\n openServiceConn();\n\n const joinHash = exports.calculateJoinHash(prototype); /* warning, dcp 1905 */\n\n prototype.joinSecret = joinHash; /* change with dcp 1905 */\n\n const { success, payload } = await exports.serviceConnection.send('changeGroup', {\n id: id,\n prototype: prototype,\n });\n\n if (!success) {\n throw new DCPError(`Cannot change the compute group ${id}: ${payload}`);\n }\n};\n\n /**\n * Async function that deletes a Compute Group from the database. The Compute Group ID must be specified as a number.\n * Must be the owner of the Compute Group to delete it.\n * \n * @param {number} computeGroupId - the ID of the Compute Group that is to be deleted. Analogous to row ID in table.\n * @access public\n * @example\n * await computeGroup.deleteGroup(456);\n */\nexports.deleteGroup = async function deleteGroup(computeGroupId) {\n if (!exports.serviceConnection)\n openServiceConn();\n \n const { success, payload } = await exports.serviceConnection.send('deleteGroup', {\n id: computeGroupId,\n });\n\n if (!success) {\n throw new DCPError(`Cannot delete the compute group ${computeGroupId}: ${payload}`);\n }\n};\n\n/**\n * Async function that adds a job to a specified compute group. Both Compute\n * Group ID and Job ID must be specified as number Must be the owner of the\n * Compute Group to add a Job to it. The joinKey and the joinSecret/joinHash\n * (user/password) must also be provided.\n *\n * @param {Address} job the opaque id of the Job that will be added to the Compute Group.\n * @param {object} descriptor the descriptor object for the compute group. This descriptor\n * needs to contain enough information to authorize access to the\n * compute group. Properties may include:\n * - id\n * - joinKey\n * - joinSecret\n * - joinHash\n * - joinAddress\n * \n * Additional, either the joinKey or the compute group id MUST be specified so\n * that we can identify the compute group in question.\n *\n * All compute groups can have jobs submitted to them, provided either the joinKey\n * or the id (maybe deprecated in the future?) are specified, and the message is\n * signed by the compute group owner.\n *\n * FUTURE - after DCP-1910\n * keystore A keystore used to grant access to job deployment within this compute group.\n * This can be either the ownerKeystore or the joinAddress keystore when the\n * compute group is in deployAccessType='join' mode.\n *\n * @access public\n * @example\n * await computeGroup.addJob(computeGroupId = 456, job = 'P+Y4IApeFQLrYS2W7MkVg7');\n */\nexports.addJob = async function addJob(job, descriptor)\n{\n var result;\n var message = Object.assign({}, {job: job});\n message.computeGroupId = descriptor.id;\n message.joinKey = descriptor.joinKey;\n \n if(descriptor.joinHash) {\n message.joinHashHash = hash.calculate(hash.eh1, descriptor.joinHash, exports.serviceConnection.dcpsid);\n } else if(descriptor.joinSecret) {\n message.joinHashHash = hash.calculate(hash.eh1, exports.calculateJoinHash(descriptor), exports.serviceConnection.dcpsid);\n }\n \n if (!message.job)\n throw new Error('job not specified');\n if (!message.computeGroupId && !message.joinKey)\n throw new Error('compute group not identified');\n \n if (!exports.serviceConnection)\n openServiceConn();\n\n result = await exports.serviceConnection.send('addJob', message);\n\n if (!result.success)\n throw new DCPError(result.payload.message);\n};\n\n /**\n * Async function that lists all the current Jobs in a Compute Group.\n * The Compute Group ID must be specified. Must be of type number.\n * Must be the owner of the Compute Group to list its current Jobs.\n *\n * @param {number} computeGroupId - the ID of the Compute Group from which we will list its jobs.\n * @returns {Array} - the array of Jobs belonging to the computeGroup.\n * @access public\n * @example\n * let listOfJobs = await computeGroup.listJobs(computeGroupId = 456);\n */\nexports.listJobs = async function listJobs(computeGroupId) {\n if (!exports.serviceConnection)\n openServiceConn();\n\n const { success, payload } = await exports.serviceConnection.send('listJobs', {\n computeGroupId: computeGroupId,\n });\n\n if (!success) {\n throw new DCPError(`Cannot list jobs for compute group ${computeGroupId}: ${payload}`);\n }\n\n return payload;\n};\n\n /**\n * Async function that removes a Compute Group from the the computeGroups table in the scheduler_dcp mysql database.\n * Both Compute Group ID and Job ID must be specified to remove Job from the group. Both of type number.\n * Must be the owner of the Compute Group to remove a Job from it.\n *\n * @param {number} computeGroupId - the ID of the Compute Group where the job will be removed. Analogous to row ID in table.\n * @param {address} job - the address of the Job that will be removed from the Compute Group.\n * @access public\n * @example\n * await computeGroup.removeJob(computeGroupId = 123456, job = 'P+Y4IApeFQLrYS2W7MkVg7');\n */\nexports.removeJob = async function removeJob(computeGroupId, job, joinKey, joinSecret) {\n var joinHashHash;\n if (!exports.serviceConnection)\n openServiceConn();\n\n if(joinSecret)\n joinHashHash = hash.calculate(hash.eh1, exports.calculateJoinHash({joinKey, joinSecret}, joinSecret), exports.serviceConnection.dcpsid);\n const { success, payload } = await exports.serviceConnection.send('removeJob', {\n computeGroupId: parseInt(computeGroupId),\n job: job,\n joinKey: joinKey,\n joinHashHash: joinHashHash,\n });\n\n if (!success) {\n throw new DCPError(`Cannot remove job ${job} from compute group ${computeGroupId}: ${payload}`);\n }\n};\n\n/**\n * Async function that cancel the specified job.\n *\n * @param {number} computeGroupId - the ID of the Compute Group.\n * @param {address} job - the address of the job that will be cancelled.\n * @access public\n * @example\n * await computeGroup.cancelJob(computeGroupId = 123456, job = 'P+Y4IApeFQLrYS2W7MkVg7');\n */\nexports.cancelJob = async function cancelJob(computeGroupId, job) {\n if (!exports.serviceConnection)\n openServiceConn();\n\n const { success, payload } = await exports.serviceConnection.send('cancelJob', {\n computeGroupId: parseInt(computeGroupId),\n job: job\n });\n\n if (!success) {\n throw new DCPError(`Cannot cancel job ${job} for compute group ${computeGroupId}: ${payload}`);\n }\n};\n\n/**\n * Async function that cancel all jobs.\n *\n * @param {number} computeGroupId - the ID of the Compute Group.\n * @access public\n * @example\n * await computeGroup.cancelAllJobs(computeGroupId = 123456);\n */\nexports.cancelAllJobs = async function cancelAllJobs(computeGroupId) {\n if (!exports.serviceConnection)\n openServiceConn();\n\n const { success, payload } = await exports.serviceConnection.send('cancelAllJobs', {\n computeGroupId: parseInt(computeGroupId)\n });\n\n if (!success) {\n throw new DCPError(`Cannot cancel all jobs for compute group ${computeGroupId}: ${payload}`);\n }\n};\n\n/**\n * Calculate a joinHash for a compute group. This is an eh1- hash of the cg salt and \n * joinSecret components of a compute group description.\n *\n * @param {object} details an object containing the cg salt, which is\n * the joinKey if the compute group uses one;\n * otherwise it is the joinAddress. This object\n * may also contain the joinSecret.\n * @param {string} joinSecret the join secret -- plain text -- that is\n * the \"password\" for the compute group. If not\n * specified, we use details.joinSecret.\n */\nexports.calculateJoinHash = function computeGroups$calculateJoinHash(details, joinSecret)\n{\n if (typeof joinSecret === 'undefined')\n joinSecret = details.joinSecret;\n\n return hash.calculate(hash.eh1, `${details.joinKey || details.joinAddress} ${details.joinSecret}`);\n}\n\n\n//# sourceURL=webpack:///./src/dcp-client/compute-groups/index.js?");
|
|
5080
5080
|
|
|
5081
5081
|
/***/ }),
|
|
5082
5082
|
|
|
@@ -5098,7 +5098,7 @@ eval("/**\n * @file Module that implements Compute API\n * @module dcp/comput
|
|
|
5098
5098
|
/*! no static exports found */
|
|
5099
5099
|
/***/ (function(module, exports, __webpack_require__) {
|
|
5100
5100
|
|
|
5101
|
-
eval("/* WEBPACK VAR INJECTION */(function(module) {/**\n * @file dcp-client-bundle-src.js\n * Top-level file which gets webpacked into the bundle consumed by dcp-client 2.5\n * @author Wes Garland, wes@kingsds.network\n * @date July 2019\n */\n\n{\n let thisScript = typeof document !== 'undefined' ? (typeof document.currentScript !== 'undefined' && document.currentScript) || document.getElementById('_dcp_client_bundle') : {}\n let realModuleDeclare\n\n if ( false || typeof module.declare === 'undefined') {\n realModuleDeclare = ( true) ? module.declare : undefined\n if (false) {}\n module.declare = function moduleUnWrapper (deps, factory) {\n factory(null, module.exports, module)\n return module.exports\n }\n }\n\n let _debugging = () => false\n dcpConfig.future = __webpack_require__(/*! ../common/config-future.js */ \"./src/common/config-future.js\").futureFactory(_debugging, dcpConfig);\n\n /* These modules are official API and must be part of DCP Client */\n let officialApi = {\n 'protocol': __webpack_require__(/*! ../protocol-v4 */ \"./src/protocol-v4/index.js\"),\n 'compute': __webpack_require__(/*! ./compute */ \"./src/dcp-client/compute.js\").compute,\n 'worker': __webpack_require__(/*! ./worker */ \"./src/dcp-client/worker/index.js\"),\n 'wallet': __webpack_require__(/*! ./wallet */ \"./src/dcp-client/wallet/index.js\"),\n };\n\n /* Allow client programs to use modules which happen to be in the bundle anyhow */\n let conveniencePeers = {\n 'ethereumjs-tx': __webpack_require__(/*! ./wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.tx,\n 'ethereumjs-wallet': __webpack_require__(/*! ./wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.wallet,\n 'ethereumjs-util': __webpack_require__(/*! ./wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.util,\n 'socket.io-client': __webpack_require__(/*! socket.io-client */ \"./node_modules/socket.io-client/build/index.js\"),\n 'bignumber.js': __webpack_require__(/*! bignumber.js */ \"./node_modules/bignumber.js/bignumber.js\"),\n 'semver': __webpack_require__(/*! semver */ \"./node_modules/semver/semver.js\"),\n };\n\n /* Some of these modules are API-track. Some of them need to be published to be\n * available for top-level resolution by DCP internals. Those (mostly) should have\n * been written using relative module paths.....\n */\n let modules = Object.assign({\n 'protocol-v3': __webpack_require__(/*! ../protocol-v3 */ \"./src/protocol-v3/index.js\").protocol,\n 'dcp-build': {\"version\":\"
|
|
5101
|
+
eval("/* WEBPACK VAR INJECTION */(function(module) {/**\n * @file dcp-client-bundle-src.js\n * Top-level file which gets webpacked into the bundle consumed by dcp-client 2.5\n * @author Wes Garland, wes@kingsds.network\n * @date July 2019\n */\n\n{\n let thisScript = typeof document !== 'undefined' ? (typeof document.currentScript !== 'undefined' && document.currentScript) || document.getElementById('_dcp_client_bundle') : {}\n let realModuleDeclare\n\n if ( false || typeof module.declare === 'undefined') {\n realModuleDeclare = ( true) ? module.declare : undefined\n if (false) {}\n module.declare = function moduleUnWrapper (deps, factory) {\n factory(null, module.exports, module)\n return module.exports\n }\n }\n\n let _debugging = () => false\n dcpConfig.future = __webpack_require__(/*! ../common/config-future.js */ \"./src/common/config-future.js\").futureFactory(_debugging, dcpConfig);\n\n /* These modules are official API and must be part of DCP Client */\n let officialApi = {\n 'protocol': __webpack_require__(/*! ../protocol-v4 */ \"./src/protocol-v4/index.js\"),\n 'compute': __webpack_require__(/*! ./compute */ \"./src/dcp-client/compute.js\").compute,\n 'worker': __webpack_require__(/*! ./worker */ \"./src/dcp-client/worker/index.js\"),\n 'wallet': __webpack_require__(/*! ./wallet */ \"./src/dcp-client/wallet/index.js\"),\n };\n\n /* Allow client programs to use modules which happen to be in the bundle anyhow */\n let conveniencePeers = {\n 'ethereumjs-tx': __webpack_require__(/*! ./wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.tx,\n 'ethereumjs-wallet': __webpack_require__(/*! ./wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.wallet,\n 'ethereumjs-util': __webpack_require__(/*! ./wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.util,\n 'socket.io-client': __webpack_require__(/*! socket.io-client */ \"./node_modules/socket.io-client/build/index.js\"),\n 'bignumber.js': __webpack_require__(/*! bignumber.js */ \"./node_modules/bignumber.js/bignumber.js\"),\n 'semver': __webpack_require__(/*! semver */ \"./node_modules/semver/semver.js\"),\n };\n\n /* Some of these modules are API-track. Some of them need to be published to be\n * available for top-level resolution by DCP internals. Those (mostly) should have\n * been written using relative module paths.....\n */\n let modules = Object.assign({\n 'protocol-v3': __webpack_require__(/*! ../protocol-v3 */ \"./src/protocol-v3/index.js\").protocol,\n 'dcp-build': {\"version\":\"dd98e423ca01eaeeba22e0bd0d948e358ffc7b43\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"4.1.11\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#release\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#9275023e3d51fbc2437111118c8d3bbb73bfebc1\"},\"built\":\"Mon Sep 20 2021 11:03:08 GMT-0400 (Eastern Daylight Time)\",\"config\":{\"generated\":\"Mon 20 Sep 2021 11:03:07 AM EDT by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"4.46.0\",\"node\":\"v12.22.6\"},\n 'dcp-xhr': __webpack_require__(/*! ../common/dcp-xhr */ \"./src/common/dcp-xhr.js\"),\n 'dcp-env': __webpack_require__(/*! ../common/dcp-env */ \"./src/common/dcp-env.js\"),\n 'dcp-url': __webpack_require__(/*! ../common/dcp-url */ \"./src/common/dcp-url.js\"),\n 'cli': __webpack_require__(/*! ../common/cli */ \"./src/common/cli.js\"),\n 'dcp-timers': __webpack_require__(/*! ../common/dcp-timers */ \"./src/common/dcp-timers.js\"),\n 'dcp-dot-dir': __webpack_require__(/*! ../common/dcp-dot-dir */ \"./src/common/dcp-dot-dir.js\"),\n 'dcp-assert': __webpack_require__(/*! ../common/dcp-assert */ \"./src/common/dcp-assert.js\"),\n 'dcp-events': __webpack_require__(/*! ../common/dcp-events */ \"./src/common/dcp-events/index.js\"),\n 'utils': __webpack_require__(/*! ../utils */ \"./src/utils/index.js\"),\n 'debugging': __webpack_require__(/*! ../debugging */ \"./src/debugging.js\"),\n 'publish': __webpack_require__(/*! ../common/dcp-publish */ \"./src/common/dcp-publish.js\"),\n 'compute-groups': {\n ...__webpack_require__(/*! ./compute-groups */ \"./src/dcp-client/compute-groups/index.js\"),\n publicGroupOpaqueId: __webpack_require__(/*! ../common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups.public.opaqueId,\n },\n 'bank-util': __webpack_require__(/*! ./bank-util */ \"./src/dcp-client/bank-util.js\"),\n 'protocol-v4': __webpack_require__(/*! ../protocol-v4 */ \"./src/protocol-v4/index.js\"), /* deprecated */\n 'client-modal': __webpack_require__(/*! ./client-modal */ \"./src/dcp-client/client-modal/index.js\"),\n 'legacy-modal': __webpack_require__(/*! ../protocol-v3/modal */ \"./src/protocol-v3/modal.js\").Modal,\n 'eth': __webpack_require__(/*! ./wallet/eth */ \"./src/dcp-client/wallet/eth.js\"),\n 'serialize': __webpack_require__(/*! ../utils/serialize */ \"./src/utils/serialize.js\"),\n 'job': __webpack_require__(/*! ./job */ \"./src/dcp-client/job/index.js\"),\n 'range-object': __webpack_require__(/*! ./range-object */ \"./src/dcp-client/range-object.js\"),\n 'stats-ranges': __webpack_require__(/*! ./stats-ranges */ \"./src/dcp-client/stats-ranges.js\"),\n 'standard-objects': {}\n }, conveniencePeers, officialApi);\n\n /* Export the JS Standard Classes (etc) from the global object of the bundle evaluation context,\n * in case we have code somewhere that needs to use these for instanceof checks.\n */\n ;[ Object, Function, Boolean, Symbol,\n Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError,\n Number, Math, Date,\n String, RegExp,\n Array, Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array,\n Map, Set, WeakMap, WeakSet,\n ArrayBuffer, DataView, JSON,\n Promise, \n Reflect, Proxy, Intl, WebAssembly, __webpack_require__\n ].forEach(function (obj) {\n if (obj.name && (typeof obj === 'function' || typeof obj === 'object'))\n modules['standard-objects'][obj.name] = obj\n })\n\n if (typeof BigInt !== 'undefined')\n modules['standard-objects']['BigInt'] === BigInt;\n if (typeof BigInt64Array !== 'undefined')\n modules['standard-objects']['BigInt64Array'] === BigInt64Array;\n if (typeof BigInt64Array !== 'undefined')\n modules['standard-objects']['BigUint64Array'] === BigUint64Array;\n\n module.declare([], function(require, exports, module) {\n Object.assign(exports, modules)\n exports['dcp-config'] = dcpConfig\n })\n if (realModuleDeclare)\n module.declare = realModuleDeclare\n\n bundleExports = thisScript.exports = exports; /* must be last expression evaluated! */\n}\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../node_modules/webpack/buildin/module.js */ \"./node_modules/webpack/buildin/module.js\")(module)))\n\n//# sourceURL=webpack:///./src/dcp-client/index.js?");
|
|
5102
5102
|
|
|
5103
5103
|
/***/ }),
|
|
5104
5104
|
|
|
@@ -5110,7 +5110,7 @@ eval("/* WEBPACK VAR INJECTION */(function(module) {/**\n * @file dcp-cli
|
|
|
5110
5110
|
/***/ (function(module, exports, __webpack_require__) {
|
|
5111
5111
|
|
|
5112
5112
|
"use strict";
|
|
5113
|
-
eval("/**\n * @file job/index.js\n * @author Eddie Roosenmaallen, eddie@kingsds.network\n * Matthew Palma, mpalma@kingsds.network\n * @date November 2018\n *\n * This module implements the Compute API's Job Handle\n *\n */\n\n/** @typedef {import('dcp/dcp-client/wallet/keystore').Keystore} Keystore */\n\n\nconst BigNumber = __webpack_require__(/*! bignumber.js */ \"./node_modules/bignumber.js/bignumber.js\");\nconst { v4: uuidv4 } = __webpack_require__(/*! uuid */ \"./node_modules/uuid/dist/esm-browser/index.js\");\nconst { EventEmitter, PropagatingEventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { RangeObject, MultiRangeObject, DistributionRange, SuperRangeObject } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst { fetchURI, encodeDataURI } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst serialize = __webpack_require__(/*! dcp/utils/serialize */ \"./src/utils/serialize.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst { EventSubscriber } = __webpack_require__(/*! dcp/events/event-subscriber */ \"./src/events/event-subscriber.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst ClientModal = __webpack_require__(/*! dcp/dcp-client/client-modal */ \"./src/dcp-client/client-modal/index.js\");\nconst { Worker } = __webpack_require__(/*! dcp/dcp-client/worker */ \"./src/dcp-client/worker/index.js\");\nconst RemoteDataSet = __webpack_require__(/*! dcp/dcp-client/remote-data-set */ \"./src/dcp-client/remote-data-set.js\");\nconst { ResultHandle } = __webpack_require__(/*! ./result-handle */ \"./src/dcp-client/job/result-handle.js\");\nconst { SlicePaymentOffer } = __webpack_require__(/*! ./slice-payment-offer */ \"./src/dcp-client/job/slice-payment-offer.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst dcpPublish = __webpack_require__(/*! dcp/common/dcp-publish */ \"./src/common/dcp-publish.js\");\nconst computeGroups = __webpack_require__(/*! dcp/dcp-client/compute-groups */ \"./src/dcp-client/compute-groups/index.js\");\nconst schedulerConstants = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst bankUtil = __webpack_require__(/*! dcp/dcp-client/bank-util */ \"./src/dcp-client/bank-util.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp-client');\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\n\nconst log = (...args) => {\n if (debugging('job')) {\n console.debug('dcp-client:job', ...args);\n }\n};\n\nconst ON_BROWSER = DCP_ENV.isBrowserPlatform;\nconst sideloaderModuleIdentifier = 'sideloader-v1';\n\n// Symbols used to hide private members and functions on the Job instance\nconst debugBuild = (__webpack_require__(/*! dcp/common/dcp-build */ \"./src/common/dcp-build.js\").build === 'debug');\nconst INTERNAL_SYMBOL = debugBuild ? '_' : Symbol('Job Internals');\nconst SNAPSHOT = debugBuild ? '_snapshot' : Symbol('Job.snapshot');\nconst DEPLOY_JOB = debugBuild ? '_deploy' : Symbol('Job.deploy');\n\nconst ADD_LISTENERS = Symbol('Job.addListeners');\nconst LISTEN_TO_EVENTS = Symbol('Job.listenToEvents');\nconst LISTEN_TO_WORK_EVENTS = Symbol('Job.listenToWorkEvents');\nconst ON_RESULT = Symbol('Job.onResult');\nconst ON_STATUS = Symbol('Job.onStatus');\n\nexports.JOB_INTERNAL_SYMBOL = INTERNAL_SYMBOL; /* allow friends to access our guts, eg. job/result-handle */\n\nconst DEFAULT_REQUIREMENTS = {\n engine: {\n es7: null,\n spidermonkey: null\n },\n environment: {\n webgpu: null,\n offscreenCanvas: null,\n fdlibm: null\n },\n browser: {\n chrome: null\n },\n details: {\n offscreenCanvas: {\n bigTexture4096: null,\n bigTexture8192: null,\n bigTexture16384: null,\n bigTexture32768: null,\n }\n },\n discrete: null,\n};\nconst RELIABLE_EVENTS = [\n 'complete', 'cancel', 'ENOFUNDS', 'ETOOMANYERRORS', 'ESLICETOOSLOW',\n 'ENOPROGRESS', 'ETOOMANYTASKS', 'EWORKTOOBIG', 'ETOOBIG',\n];\nconst OPTIONAL_EVENTS = [\n 'console', 'error', 'noProgress', 'noProgressData',\n];\nconst ZERO_COST_HOLD_ADDRESS = '0x' + '0'.repeat(130);\n\n/** @typedef {import('../range-object').RangeLike} RangeLike */\n\n/**\n * Ensure input data is an appropriate format\n * @param {RangeObject | DistributionRange | RemoteDataSet | Array | Iterable}\n * inputData - A URI-shaped string, a [Multi]RangeObject-constructing value, or\n * an array of slice data\n * @return {RangeObject | RangeLike | DistributionRange | RemoteDataSet | Array}\n * The coerced input in an appropriate format ([Multi]RangeObject,\n * DistributionRange, RemoteDataSet, or array)\n */\nconst wrangleData = (inputData) => {\n if (typeof inputData === 'object' && !!inputData.ranges) { return new MultiRangeObject(inputData) }\n\n if (RangeObject.isRangelike(inputData)) { return inputData }\n if (RangeObject.isRangeObject(inputData)) { return inputData }\n if (DistributionRange.isDistribution(inputData)) { return inputData }\n if (RangeObject.isProtoRangelike(inputData)) { return new RangeObject(inputData) }\n if (DistributionRange.isProtoDistribution(inputData)) { return new DistributionRange(inputData) }\n if (RemoteDataSet.isRemoteDataSet(inputData)) {\n return new RemoteDataSet(...inputData);\n }\n\n return Array.isArray(inputData) ? inputData : [inputData];\n};\n\n// Used to validate the requirements object,\n// applies the default requirements schema\nconst applyObjectSchema = (obj, schema, errContext='', scope='') => {\n let checkedObjs = [];\n\n for (let p in schema) {\n let fullPropScope = scope.concat(p);\n if (!(p in obj)) {\n if (typeof schema[p] === 'object' && schema[p] !== null) {\n obj[p] = {};\n checkedObjs.push(fullPropScope);\n applyObjectSchema(obj[p], schema[p], errContext, fullPropScope.concat('.'));\n } else obj[p] = schema[p];\n } else if (typeof schema[p] === 'object' && schema[p] !== null && !checkedObjs.includes(fullPropScope)) {\n if (typeof obj[p] !== 'object') throw new Error(`${errContext}: Schema mismatch, property '${fullPropScope}' should be an object.`);\n else {\n checkedObjs.push(fullPropScope);\n applyObjectSchema(obj[p], schema[p], errContext, fullPropScope.concat('.'));\n }\n } else if ((typeof schema[p] !== 'object' || schema[p] === null)\n && typeof obj[p] !== 'boolean' && obj[p] !== null) {\n throw new Error(`${errContext}: Schema mismatch, object property '${fullPropScope}' should be a boolean.`);\n }\n }\n\n for (let p in obj) {\n let fullPropScope = scope.concat(p);\n if (!(p in schema)) throw new Error(`${errContext}: Schema mismatch, object has extra key '${fullPropScope}'.`);\n else if (typeof obj[p] === 'object' && obj[p] !== null && !checkedObjs.includes(fullPropScope)) {\n checkedObjs.push(fullPropScope);\n applyObjectSchema(obj[p], schema[p], errContext, fullPropScope.concat('.'));\n }\n }\n}\n\n/**\n * @classdesc The Compute API's Job Handle (see {@link https://docs.dcp.dev/specs/compute-api.html#job-handles|Compute API spec})\n * Job handles are objects which correspond to jobs. \n * They are created by some exports of the compute module, such as {@link module:dcp/compute.do|compute.do} and {@link module:dcp/compute.for|compute.for}.\n * @extends module:dcp/dcp-events.PropagatingEventEmitter\n * @hideconstructor\n * @access public\n */\nclass Job extends PropagatingEventEmitter {\n /**\n * This event is emitted when the job is accepted by the scheduler on deploy.\n * \n * @event Job#accepted\n * @access public\n * @type {object}\n * @property {object} job Original object that was delivered to the scheduler for deployment\n *//**\n * Fired when the job is cancelled.\n * \n * @event Job#cancel\n * @access public\n *//**\n * Fired when a result is returned.\n * \n * @event Job#result\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {string} task ID of the task (slice) the result came from\n * @property {number} sort The index of the slice\n * @property {object} result\n * @property {string} result.request\n * @property {*} result.result The value returned from the work function\n *//**\n * Fired when the result handle is modified, either when a new `result` event is fired or when the results are populated with `results.fetch()`\n * \n * @event Job#resultsUpdated\n * @access public\n *//**\n * Fired when the job has been completed.\n * \n * @event Job#complete\n * @access public\n * @type {ResultHandle}\n *//**\n * Fired when the job's status changes.\n * \n * @event Job#status\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} total Total number of slices in the job\n * @property {number} distributed Number of slices that have been distributed\n * @property {number} computed Number of slices that have completed execution (returned a result)\n * @property {string} runStatus Current runStatus of the job\n *//**\n * Fired when a slice throws an error.\n * \n * @event Job#error\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} sliceIndex Index of the slice that threw the error\n * @property {string} message The error message\n * @property {string} stack The error stacktrace\n * @property {string} name The error type name\n *//**\n * Fired when a slice uses one of the console log functions.\n * \n * @event Job#console\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} sliceIndex The index of the slice that produced this event\n * @property {string} level The log level, one of `debug`, `info`, `log`, `warn`, or `error`\n * @property {string} message The console log message\n *//**\n * Fired when a slice is stopped for not calling progress. Contains information about how long the slice ran for, and about the last reported progress calls.\n * \n * @event Job#noProgress\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} sliceIndex The index of the slice that failed due to no progress\n * @property {number} timestamp How long the slice ran before failing\n * @property {object} progressReports\n * @property {object} progressReports.last The last progress report received from the worker\n * @property {number} progressReports.last.timestamp Time since the start of the slice\n * @property {number} progressReports.last.progress Progress value reported\n * @property {*} progressReports.last.value The last value that was passed to the progress function\n * @property {number} progressReports.last.throttledReports Number of calls to progress that were throttled since the last report\n * @property {object} progressReports.lastUpdate The last determinate (update to the progress param) progress report received from the worker\n * @property {number} progressReports.lastUpdate.timestamp\n * @property {number} progressReports.lastUpdate.progress\n * @property {*} progressReports.lastUpdate.value\n * @property {number} progressReports.lastUpdate.throttledReports\n *//**\n * Identical to `noProgress`, except that it also contains the data that the slice was executed with.\n * \n * @event Job#noProgressData\n * @access public\n * @type {object}\n * @property {*} data The data that the slice was executed with\n *//**\n * Fired when the job is paused due to running out of funds. The job can be resumed by escrowing more funds then resuming the job.\n * \n * Event payload is the estimated funds required to complete the job\n * \n * @event Job#ENOFUNDS\n * @access public\n * @type {BigNumber}\n *//**\n * Fired when the job is cancelled due to the work function not calling the `progress` method frequently enough.\n * \n * @event Job#ENOPROGRESS\n * @access public\n *//**\n * The job was cancelled because scheduler has determined that individual tasks in this job exceed the maximum allowable execution time.\n * \n * @event Job#ESLICETOOSLOW\n * @access public\n *//**\n * Fired when the job is cancelled because too many work functions are terminating with uncaught exceptions.\n * \n * @event Job#ETOOMANYERRORS\n * @access public\n */\n\n /**\n * @form1 new Job(job_shaped_object)\n * @form2 new Job('application_worker_address'[, data[, arguments]])\n * @form3b new Job('worker source'[, data[, arguments]])\n * @form3b new Job(worker_function[, data[, arguments]])\n */\n\n constructor() {\n super('Job');\n\n this.readyStateChange = (readyState) => {\n this.readyState = readyState;\n this.emit('readyStateChange', this.readyState);\n };\n this.readyStateChange('new');\n \n /*\n * Private members\n */\n this[INTERNAL_SYMBOL] = {\n events: new EventEmitter('Job Internal'),\n connected: false, // set to true after first call to exec\n /**\n * This object holds details for generating DCPv4 messages about this job.\n * It is updated everytime we call SNAPSHOT.\n */\n payloadDetails: {\n localExec: false,\n },\n\n /**\n * The slicePaymentOffer default value is set to compute.marketValue, in .exec() \n */\n slicePaymentOffer: null,\n paymentAccountKeystore: null,\n\n /**\n * These are private but getters are provided so they can be modified but\n * not replaced.\n */\n /**\n * List of module prefixes using in CommonJS module resolution.\n * @type {string[]}\n */\n requirePath: [],\n\n /**\n * List of modules the job needs.\n * @type {string[]}\n */\n\n dependencies: [],\n\n // This array contains the names of worker events that\n // had listeners registered before exec is called, once\n // the job has been deployed then the proper event handlers\n // will be generated from this list\n subscribedEvents: new Set(),\n subscribedWorkerEvents: new Set(),\n\n results: [],\n resultsAvailable: [],\n resultStorageType: 'values',\n resultStorageDetails: undefined,\n resultStorageParams: undefined, //Holds the POST params and URL for off-prem storage\n\n // Tracks job progress\n status: {\n runStatus: null,\n total: null,\n distributed: null,\n computed: null,\n },\n\n // Cancel is special. We need to fire an `alert` when the job is canceled. \n // If they are listening for the (reliable) event then they need to be able to\n // prevent it. If not, then it'll be handled by the `exec` rejection via the 'stopped'\n // event. The result is that we want only one of two ways the `alert` can be fired\n // to be active based on whether or not the user is listening for cancel. \n // Once DCP-1150 lands, we won't need to listen on stopped since more failures will fire a cancel event.\n listeningForCancel: false,\n // TODO - cancel events should have more info in them. DCP-1150\n cancelAlert: () => ClientModal.alert(\"More details in console...\", {title: 'Job Canceled'}),\n\n listeningForError: false,\n errorAlert: (err) => ClientModal.alert(err, {title: 'Unexpected Error'}),\n\n listeningForNoFunds: false,\n noFundsAlert: (event) =>\n ClientModal.alert(event, { title: 'Job Paused' }),\n };\n\n /*\n * Public members\n */\n // Deep copy the default requirements. I know, I hate it too\n /**\n * An object describing the requirements that workers must have to be eligible for this job. See\n * {@link https://docs.dcp.dev/specs/compute-api.html#requirements-objects|Requirements Objects}.\n *\n * @type {object}\n * @access public\n */\n this.requirements = JSON.parse(JSON.stringify(DEFAULT_REQUIREMENTS));\n this.schedulerURL = undefined;\n this.bankURL = undefined;\n this.deployURL = '';\n /**\n * @see {@link https://kingsds.atlassian.net/browse/DCP-1475?atlOrigin=eyJpIjoiNzg3NmEzOWE0OWI4NGZkNmI5NjU0MWNmZGY2OTYzZDUiLCJwIjoiaiJ9|Jira Issue}\n */\n let uuid = uuidv4();\n\n /**\n * An object describing the cost the user believes each the average slice will incur, in terms of CPU/GPU and I/O.\n * If defined, this object is used to provide initial scheduling hints and to calculate escrow amounts.\n *\n * @type {object}\n * @access public\n */\n this.initialSliceProfile = undefined;\n\n /**\n * A place to store public-facing attributes of the job. Anything stored on this object will be available inside the work \n * function (see {@link module:dcp/compute~sandboxEnv.work}). The properties documented here may be used by workers to display what jobs are currently being \n * worked on.\n * @access public\n * @property {string} name Public-facing name of this job.\n * @property {string} description Public-facing description for this job.\n * @property {string} link Public-facing link to external resource about this job.\n */\n this.public = {\n name: null,\n description: null,\n link: null,\n };\n\n this.contextId = null\n\n // The following public members are only populated after the job has been deployed\n this.address = null;\n this.receipt = null;\n this.meanSliceProfile = null;\n\n /* We avoid using job.id internally because it is easy to confuse with db::jobs.id, but is an API\n * interface that we present to end-user developers so we need to keep it.\n */\n Object.defineProperty(this, 'id', {\n get: () => this.address,\n set: (id) => this.address = id\n });\n \n // \n /**\n * An EventEmitter for custom events dispatched by the work function.\n * @type {module:dcp/dcp-events.EventEmitter}\n * @access public\n * @example\n * // in sandbox\n * work.emit('myEventName', 1, [2], \"three\");\n * // clientside\n * job.work.on('myEventName', (num, arr, string) => { });\n */\n this.work = new EventEmitter('job.work');\n\n //Initialize the eventSubscriber so each job has unique eventSubscriber\n this.eventSubscriber = new EventSubscriber()\n \n this.work.on('newListener', (evt) => {\n if (!this[INTERNAL_SYMBOL].connected && evt !== 'newListener') {\n this[INTERNAL_SYMBOL].subscribedWorkerEvents.add(evt);\n }\n });\n\n this.on('newListener', (evt) => {\n if (!this[INTERNAL_SYMBOL].connected && evt !== 'newListener') {\n this[INTERNAL_SYMBOL].subscribedEvents.add(evt);\n }\n if (evt === 'cancel') {\n this[INTERNAL_SYMBOL].listeningForCancel = true;\n }\n\n if (evt === 'error') {\n this[INTERNAL_SYMBOL].listeningForError = true;\n }\n\n if (evt === 'nofunds') {\n this[INTERNAL_SYMBOL].listeningForNoFunds = true;\n }\n });\n\n this.addDefaultListener('cancel', (msg) => {\n if (ON_BROWSER && this[INTERNAL_SYMBOL].listeningForCancel) \n this[INTERNAL_SYMBOL].cancelAlert(msg);\n });\n\n this.addDefaultListener('error', (event) => {\n if (!this[INTERNAL_SYMBOL].listeningForError) {\n if (ON_BROWSER) {\n this[INTERNAL_SYMBOL].errorAlert(event);\n } else {\n console.error(event);\n }\n }\n });\n\n this.addDefaultListener('nofunds', (event) => {\n if (!this[INTERNAL_SYMBOL].listeningForNoFunds) {\n if (ON_BROWSER) {\n this[INTERNAL_SYMBOL].noFundsAlert(event);\n } else {\n console.warn(\n `Job ${event.job} has been ${event.runStatus} with ${event.remainingSlices} slices remaining due to running out of funds (need ${event.fundsRequired} credits). Re-add the necessary funds and resume the job to continue getting results.`,\n );\n }\n }\n });\n\n // Form1: If arguments[0] is an object that looks like a job, this is a 'copy constructor'\n // where we inherit as much as possible from the original instance.\n if (typeof arguments[0] === 'object' &&\n arguments[0].type &&\n arguments[0].data &&\n arguments[0].public) {\n \n let src = arguments[0];\n\n this[INTERNAL_SYMBOL].payloadDetails = {\n ...src,\n data: wrangleData(src.data), // rehydrate ranges\n };\n\n if (src.feeStructure) {\n this.setSlicePaymentOffer(src.feeStructure);\n }\n \n if (src.address)\n this.address = src.address;\n if (src.payloadData.status)\n this[ON_STATUS](this[INTERNAL_SYMBOL].payload.status, false);\n if (src.public)\n Object.assign(this.public, src.public);\n } else {\n /* Forms 2-4 */ \n if (typeof arguments[0] === 'function')\n arguments[0] = arguments[0].toString();\n\n if (typeof arguments[0] === 'string') {\n this[INTERNAL_SYMBOL].workFunctionURI = encodeDataURI(arguments[0], 'application/javascript');\n } else if (DcpURL.isURL(arguments[0])) {\n this[INTERNAL_SYMBOL].workFunctionURI = arguments[0].href;\n } \n\n const wrangledInputData = wrangleData(arguments[1] || []);\n log('wrangledInputData:', wrangledInputData);\n Object.assign(this[INTERNAL_SYMBOL].payloadDetails, {\n request: 'main',\n data: wrangledInputData,\n arguments: arguments[2] || [],\n });\n }\n\n // This should happen last, it depends on the this.[INTERNAL_SYMBOL].payloadDetails.data array\n /**\n * A Result Handle object used to query and manipulate the output set. \n * Present once job has been deployed.\n * @type {ResultHandle}\n * @access public\n */\n this.results = new ResultHandle(this);\n\n /**\n * Read-only copy of the job's uuid (generated or rehydrated via form1 constructor)\n */\n Object.defineProperty(this, 'uuid', {\n get: () => uuid,\n configurable: false,\n enumerable: true,\n });\n \n // each entry contains the computeGroupID, joinKey, joinSecret, joinKeystore\n // Add to public compute group by default\n this.computeGroups = [ Object.assign({}, schedulerConstants.computeGroups.public) ];\n\n\n const ceci = this;\n\n // Initialize to null so these properties are recognized for the Job class\n this.bankConnection = null;\n this.deployConnection = null;\n this.openBankConn = function openBankConn()\n {\n ceci.bankConnection = new protocolV4.Connection(dcpConfig.bank.services.bankTeller);\n ceci.bankConnection.on('close', ceci.openBankConn);\n }\n\n this.openDeployConn = function openDeployConn()\n {\n ceci.deployConnection = new protocolV4.Connection(dcpConfig.scheduler.services.jobSubmit);\n ceci.deployConnection.on('close', ceci.openDeployConn);\n }\n\n this.openBankConn();\n this.openDeployConn();\n }\n\n /** \n * Cancel the job\n * @access public\n * @param {string} reason If provided, will be sent to client\n */\n async cancel (reason = undefined) {\n const response = await this.deployConnection.send('cancelJob', {\n job: this.address,\n ownerAddress: this.paymentAccountKeystore.address,\n reason,\n }, this.paymentAccountKeystore);\n\n return response.payload;\n }\n\n /** \n * Resume this job\n * @access public\n */\n async resume() {\n const response = await this.schedulerConnection.send('resumeJob', {\n job: this.address,\n owner: this.paymentAccountKeystore.address,\n }, this.paymentAccountKeystore);\n\n return response.payload;\n }\n\n /**\n * Helper function for retrieving info about the job. The job must have already been deployed.\n * An alias for {@link module:dcp/compute.getJobInfo}.\n * @access public\n */\n async getJobInfo(){\n return await __webpack_require__(/*! ../compute */ \"./src/dcp-client/compute.js\").compute.getJobInfo(this.address);\n }\n\n /**\n * Helper function for retrieving info about the job's slices. The job must have already been deployed.\n * An alias for {@link module:dcp/compute.getSliceInfo}.\n * @access public\n */\n async getSliceInfo(){\n return await __webpack_require__(/*! ../compute */ \"./src/dcp-client/compute.js\").compute.getSliceInfo(this.address);\n }\n \n async addSliceData(dataValues) {\n if (!Array.isArray(this[INTERNAL_SYMBOL].payloadDetails.data)) {\n throw new TypeError('Only data-by-value jobs may dynamically add slices');\n }\n\n const { payload } = await this.deployConnection.send('addSliceData', {\n job: this.address,\n dataValues,\n });\n\n if (payload.success && typeof payload.lastSliceNumber !== 'undefined')\n this.status.total = payload.lastSliceNumber;\n\n return payload;\n }\n\n /**\n * job.snapshot(): Private function used to populate the payloadDetails from private data,\n * inferred data, etc. Once this function has run, the payloadDetails are\n * considered authoritatively up to date until the calling function returns\n * or awaits.\n */\n [SNAPSHOT]() {\n const pd = this[INTERNAL_SYMBOL].payloadDetails;\n\n pd.type = 'ad-hoc'; /* @todo implement appliances */\n pd.uuid = this.uuid;\n pd.workFunctionURI = this[INTERNAL_SYMBOL].workFunctionURI;\n pd.dependencies = this[INTERNAL_SYMBOL].dependencies;\n pd.requirePath = this[INTERNAL_SYMBOL].requirePath;\n pd.modulePath = this[INTERNAL_SYMBOL].modulePath;\n pd.resultStorageType = this[INTERNAL_SYMBOL].resultStorageType;\n pd.resultStorageDetails = this[INTERNAL_SYMBOL].resultStorageDetails;\n pd.resultStorageParams = this[INTERNAL_SYMBOL].resultStorageParams;\n\n pd.requirements = this.requirements;\n applyObjectSchema(pd.requirements, DEFAULT_REQUIREMENTS, 'Requirements Object');\n \n // @todo: 'figure this out' - wise words from eddie /mp jan 2019\n if (!pd.options) { pd.options = {}; }\n if (!pd.public) { pd.public = {}; } \n\n for (let p of ['name', 'description', 'link']) {\n if (typeof this.public[p] === 'string') {\n pd.public[p] = this.public[p];\n }\n }\n\n // The max value that the client is willing to spend to deploy\n // (list on the scheduler, doesn't include compute payment)\n /// maxDeployPayment is the max the user is willing to pay to DCP (as a\n /// Hold), in addition to the per-slice offer and associated scrape.\n /// Currently calculated as `deployCost = costPerKB *\n /// (JSON.stringify(job).length / 1024) // 1e-9 per kb`\n // @todo: figure this out / er nov 2018\n pd.maxDeployPayment = 1;\n \n /// payloadDetails.timing can be provided as an initial estimate of slice time, to\n /// give a more useful useful calculated heap value (heap.value is more or less\n /// dcc-per-millisecond)\n pd.timing = pd.timing || 1; \n }\n\n /** Escrow additional funds for this job\n * @access public\n * @param {number|BigNumber} fundsRequired - A number or BigNumber instance representing the funds to escrow for this job\n */\n async escrow (fundsRequired) {\n if ((typeof fundsRequired !== 'number' && !BigNumber.isBigNumber(fundsRequired))\n || fundsRequired <= 0 || !Number.isFinite(fundsRequired) || Number.isNaN(fundsRequired)) {\n throw new Error(`Job.escrow: fundsRequired must be a number greater than zero. (not ${fundsRequired})`);\n }\n\n const response = await this.bankConnection.send('embiggenFeeStructure', {\n feeStructureAddress: this[INTERNAL_SYMBOL].payloadDetails.feeStructureId,\n additionalEscrow: BigNumber(fundsRequired),\n fromAddress: this.paymentAccountKeystore.address,\n }, this.paymentAccountKeystore);\n\n this.receipt = response.payload;\n\n return this.receipt;\n }\n\n async _pack () {\n var retval = __webpack_require__(/*! ./node-modules */ \"./src/dcp-client/job/node-modules.js\").createModuleBundle(this[INTERNAL_SYMBOL].dependencies);\n return retval;\n }\n\n /** \n * Collect all of the dependencies together, throw them into a BravoJS\n * module which sideloads them as a side effect of declaration, and transmit\n * them to the package manager. Then we return the package descriptor object,\n * which is guaranteed to have only one file in it.\n *\n * @returns {object} with properties name and files[0]\n */\n async _publishLocalModules() {\n const { tempFile, hash, unresolved } = await this._pack();\n\n if (!tempFile) {\n return { unresolved };\n }\n\n const sideloaderFilename = tempFile.filename;\n const pkg = {\n name: `dcp-pkg-v1-localhost-${hash.toString('hex')}`,\n version: '1.0.0',\n files: {\n [sideloaderFilename]: `${sideloaderModuleIdentifier}.js`,\n },\n }\n\n await dcpPublish.publish(pkg);\n tempFile.remove();\n\n return { pkg, unresolved };\n }\n\n /**\n * Deploys the job to the scheduler.\n * @param {number | object} [slicePaymentOffer=compute.marketValue] - Amount\n * in DCC that the user is willing to pay per slice.\n * @param {Keystore} [paymentAccountKeystore=wallet.get] - An instance of the\n * Wallet API Keystore that's used as the payment account when executing the\n * job.\n * @param {object} [initialSliceProfile] - An object describing the cost the\n * user believes the average slice will incur.\n * @access public\n * @emits Job#accepted\n */\n async exec(slicePaymentOffer = __webpack_require__(/*! ../compute */ \"./src/dcp-client/compute.js\").compute.marketValue, paymentAccountKeystore, initialSliceProfile) {\n if (this[INTERNAL_SYMBOL].connected) {\n throw new Error('Exec called twice on the same job handle.');\n }\n\n /* eagerly connect to depedent services for better performance */\n this.eventSubscriber.eventRouterConnection.keepalive();\n this.deployConnection.keepalive();\n\n this.readyState = 'exec'\n this.emit('readystatechange', this.readyState)\n\n if (typeof slicePaymentOffer !== 'undefined') this.setSlicePaymentOffer(slicePaymentOffer);\n if (typeof initialSliceProfile !== 'undefined') this.initialSliceProfile = initialSliceProfile;\n if (typeof paymentAccountKeystore !== 'undefined') {\n /** XXX @todo deprecate use of ethereum wallet objects */\n if (typeof paymentAccountKeystore === 'object' && paymentAccountKeystore.hasOwnProperty('_privKey')) {\n console.warn('* deprecated API * - job.exec invoked with ethereum wallet object as paymentAccountKeystore') /* /wg oct 2019 */\n paymentAccountKeystore = paymentAccountKeystore._privKey\n }\n /** XXX @todo deprecate use of private keys */\n if (wallet.isPrivateKey(paymentAccountKeystore)) {\n console.warn('* deprecated API * - job.exec invoked with private key as paymentAccountKeystore') /* /wg dec 2019 */\n paymentAccountKeystore = await new wallet.Keystore(paymentAccountKeystore, '');\n }\n\n this.setPaymentAccountKeystore(paymentAccountKeystore)\n }\n\n // Unlock\n if (this[INTERNAL_SYMBOL].paymentAccountKeystore) {\n // Throws if they fail to unlock, we allow this since the keystore was set programmatically. \n await this[INTERNAL_SYMBOL].paymentAccountKeystore.unlock(undefined, parseFloat(dcpConfig.job.maxDeployTime));\n } else {\n // If not set programmatically, we keep trying to get an unlocked keystore ... forever.\n let locked = true;\n let safety = 0; // no while loop shall go unguarded\n let ks;\n do {\n ks = null;\n // custom message for the browser modal to denote the purpose of keystore submission\n let msg = `This application is requesting a keystore file to execute ${this.public.description || this.public.name || 'this job'}. Please upload the corresponding keystore file. If you upload a keystore file which has been encrypted with a passphrase, the application will not be able to use it until it prompts for a passphrase and you enter it.`;\n try {\n ks = await wallet.get({ contextId: this.contextId, jobName: this.public.name, msg});\n } catch (e) {\n if (e.code !== ClientModal.CancelErrorCode) throw e;\n };\n if (ks) {\n try {\n await ks.unlock(undefined, parseFloat(dcpConfig.job.maxDeployTime));\n locked = false;\n } catch (e) {\n const expectedCodes = [wallet.unlockFailErrorCode, ClientModal.CancelErrorCode];\n if (!expectedCodes.includes(e.code)) throw e;\n }\n }\n if (safety++ > 1000) throw new Error('EINFINITY: job.exec tried wallet.get more than 1000 times.')\n } while (locked);\n this.setPaymentAccountKeystore(ks)\n }\n\n // We either have a valid keystore + password or we have rejected by this point.\n if (!this.slicePaymentOffer) {\n throw new Error('A payment profile must be assigned before executing the job');\n } else {\n let pd = this[INTERNAL_SYMBOL].payloadDetails;\n pd.feeStructure = this[INTERNAL_SYMBOL].slicePaymentOffer.toFeeStructure(pd.data.length);\n }\n\n if (!this.address) {\n try {\n this.readyStateChange('init');\n await this[DEPLOY_JOB]();\n // localExec jobs are not entered in any compute group.\n if (!this[INTERNAL_SYMBOL].payloadDetails.localExec) {\n // Add this job to its currently-defined compute groups (if any)\n let failedComputeGroups = 0;\n for (const group of this.computeGroups) {\n try\n {\n await computeGroups.addJob(this.address, group);\n this.emit('addComputeGroup', group);\n } catch (error)\n {\n failedComputeGroups++;\n error.message = `Error from compute group with joinKey '${group.joinKey}': ` + error.message;\n this.emit('error', error)\n }\n }\n if (this.computeGroups.length > 0 && failedComputeGroups === this.computeGroups.length)\n {\n this.emit('error', 'Failed to join all specified compute groups, cancelling the job')\n this.cancel('No valid compute groups.');\n }\n\n computeGroups\n .closeServiceConnection()\n .catch((err) =>\n console.error(\n 'Warning: could not close compute groups service connection',\n err,\n ),\n );\n }\n\n this.readyState = 'deployed';\n this.emit('readystatechange', this.readyState);\n this.emit('accepted');\n } catch (error) {\n if (ON_BROWSER) {\n await ClientModal.alert(error, { title: 'Failed to deploy job!' });\n }\n\n throw error;\n }\n } else {\n await this[ADD_LISTENERS]();\n\n this.readyState = 'reconnected';\n this.emit('readystatechange', this.readyState);\n }\n\n this[ON_STATUS](this[INTERNAL_SYMBOL].payloadDetails.status);\n this[INTERNAL_SYMBOL].connected = true;\n\n return new Promise((resolve, reject) => {\n const onComplete = () => resolve(this.results);\n const onCancel = (event) => {\n const reason = `Job was cancelled by ${\n event.reason ? event.reason : 'Cancelled for unknown reason'\n }.`;\n\n /**\n * FIXME(DCP-1150): Remove this since normal cancel event is noisy\n * enough to not need stopped event too.\n */\n if (ON_BROWSER && !this[INTERNAL_SYMBOL].listeningForCancel)\n this[INTERNAL_SYMBOL].cancelAlert(event.reason);\n this.emit('cancel', event);\n reject(new DCPError(event.reason, event.code));\n };\n\n this[INTERNAL_SYMBOL].events.once('stopped', (stopEvent) => {\n switch (stopEvent.runStatus) {\n case 'finished':\n onComplete();\n break;\n case 'cancelled':\n onCancel(stopEvent);\n break;\n default:\n /**\n * Asserting that we should never be able to reach here. The only\n * scheduler events that should trigger the Job's 'stopped' event\n * are 'cancelled', 'finished', and 'paused'.\n */\n reject(\n new Error(\n `Unknown event \"${stopEvent.runStatus}\" caused the job to be stopped.`,\n ),\n );\n break;\n }\n });\n\n if (!this[INTERNAL_SYMBOL].payloadDetails.running) {\n const runStatus = this[INTERNAL_SYMBOL].payloadDetails.runStatus;\n this[INTERNAL_SYMBOL].events.emit('stopped', { runStatus });\n }\n })\n .finally(() => {\n const handleErr = (e) => {\n console.error('Error while closing job connection:');\n console.error(e);\n }\n\n // Create an async IIFE to not block the promise chain\n (async () => {\n //delay to let last few events to be received\n await new Promise((resolve) => setTimeout(resolve, 1000));\n \n // close all of the connections so that we don't cause node processes to hang.\n await this.eventSubscriber.close().catch(handleErr);\n this.deployConnection.off('close', this.openDeployConn)\n await this.deployConnection.close().catch(handleErr);\n })();\n });\n }\n\n /**\n * job.addListeners(): Private function used to set up event listeners to the scheduler\n * before deploying the job.\n */\n async [ADD_LISTENERS] () {\n // This is important: We need to flush the task queue before adding listeners\n // because we queue pending listeners by listening to the newListener event (in the constructor).\n // If we don't flush here, then the newListener events may fire after this function has run,\n // and the events won't be properly set up.\n await new Promise(resolve => setTimeout(resolve, 0));\n\n // @todo: Listen for an estimated cost, probably emit an \"estimated\" event when it comes in?\n // also @todo: Do the estimation task(s) on the scheduler and send an \"estimated\" event\n\n let listeningForResults = false;\n let listenForResults = async () => {\n listeningForResults = true;\n // Initialize the local results arr, don't replace! Modify in-place only because the result handle needs it\n this[INTERNAL_SYMBOL].results.length = this[INTERNAL_SYMBOL].resultsAvailable.length = this[INTERNAL_SYMBOL].payloadDetails.data.length;\n\n // Listen for result events\n await this.eventSubscriber.subscribe('job::result', (ev) => this[ON_RESULT](ev), {\n filter: { job: this.address, }\n });\n }\n\n if (this.listenerCount('result') > 0) {\n await listenForResults();\n } else {\n this.on('newListener', (evt) => {\n if (evt === 'result' && !listeningForResults) {\n listenForResults();\n }\n });\n }\n\n // Listen for status updates\n // e.g. when a slice gets distributed or finishes being computed\n await this.eventSubscriber.subscribe('job::status', (ev) => this[ON_STATUS](ev), {\n filter: { job: this.address, }\n });\n\n // Listen for the job stopping (complete) - used to reject the exec promise\n await this.eventSubscriber.subscribe('job::stop', (ev) => this[INTERNAL_SYMBOL].events.emit('stopped', ev), {\n filter: { job: this.address, },\n });\n // Listen for the job being cancelled - used to reject the exec promise\n await this.eventSubscriber.subscribe('job::cancel', (ev) => this[INTERNAL_SYMBOL].events.emit('stopped', ev), {\n filter: { job: this.address, },\n });\n\n /**\n * Listen for the job ENOFUNDS (the payment account has run out) - used to\n * pause the exec promise.\n */\n await this.eventSubscriber.subscribe(\n 'job::ENOFUNDS',\n (event) => this.emit('nofunds', event),\n {\n filter: { job: this.address },\n },\n );\n\n // Connect listeners that were set up before exec\n const evts = Array.from(this[INTERNAL_SYMBOL].subscribedEvents);\n this[INTERNAL_SYMBOL].subscribedEvents.clear();\n for (let evt of evts) {\n await this[LISTEN_TO_EVENTS](evt);\n }\n\n if (!this[INTERNAL_SYMBOL].listeningForError) {\n await this[LISTEN_TO_EVENTS]('error');\n }\n\n // Connect listeners that are set up after exec\n this.on('newListener', (evt) => {\n if (evt === 'newListener') return;\n this[LISTEN_TO_EVENTS](evt);\n });\n\n // Connect work event listeners that were set up before exec\n const workEvts = Array.from(this[INTERNAL_SYMBOL].subscribedWorkerEvents);\n this[INTERNAL_SYMBOL].subscribedWorkerEvents.clear();\n for (let evt of workEvts) {\n await this[LISTEN_TO_WORK_EVENTS](evt);\n }\n\n // Connect work event listeners that are set up after exec\n this.work.on('newListener', (evt) => {\n if (evt === 'newListener') return;\n this[LISTEN_TO_WORK_EVENTS](evt);\n });\n }\n\n /**\n * Subscribes to either reliable events or optional events,\n * depending on the eventName being in RELIABLE_EVENTS or OPTIONAL_EVENTS\n * @param {string} eventName \n */\n async [LISTEN_TO_EVENTS](eventName) {\n // Listen for Reliable Events (ENOFUNDS, etc.)\n // The client app will add a listener for the eventName,\n // eg. job.on('ENOFUNDS', handleNoFunds)\n const listenForReliableEvent = async (eventName) => {\n await this.eventSubscriber.subscribe('job::reliableEvent', async (ev) => {\n let { /* eventName, - use original event name */ eventData, serializer } = ev;\n switch (serializer) {\n case 'json':\n eventData = JSON.parse(eventData);\n break;\n case 'serialize':\n eventData = serialize.deserialize(eventData);\n break;\n default:\n throw new RangeError(`Unsupported serializer: ${serializer}`)\n }\n\n if (eventName === 'complete') {\n // Complete is a special case: it should emit the ResultHandle\n this.emit(eventName, this.results);\n } else {\n this.emit(eventName, eventData);\n }\n }, {\n filter: {\n job: this.address,\n eventName: eventName.toUpperCase(),\n },\n });\n }\n\n // Establishes listeners for optional events when requested by the client\n const listenForOptionalEvent = async (eventName) => {\n let subscribedEventName = eventName;\n let callback = (ev) => this.emit(eventName, ev);\n let omit = [];\n switch (eventName) {\n case 'noProgress':\n case 'noprogress':\n omit.push('data');\n subscribedEventName = 'noProgress';\n break;\n case 'noProgressData':\n case 'noprogressdata':\n subscribedEventName = 'noProgress';\n break;\n case 'console':\n callback = (ev) => {\n ev.message = kvin.deserialize(ev.message);\n this.emit(eventName, ev);\n };\n break;\n }\n\n log('listening to optional event:', { eventName, subscribedEventName });\n await this.eventSubscriber.subscribe(`job::${subscribedEventName}`, callback, {\n filter: { job: this.address, },\n omit,\n });\n }\n\n if (this[INTERNAL_SYMBOL].subscribedEvents.has(eventName)) {\n // already subscribed to this event\n return;\n } else {\n this[INTERNAL_SYMBOL].subscribedEvents.add(eventName);\n\n if (RELIABLE_EVENTS.map(ev => ev.toLowerCase()).includes(eventName)) {\n // console.debug('599: listening for reliable event:', eventName);\n await listenForReliableEvent(eventName);\n } else if (OPTIONAL_EVENTS.map(ev => ev.toLowerCase()).includes(eventName)) {\n // console.debug('602: listening for optional event:', eventName);\n await listenForOptionalEvent(eventName);\n }\n else {\n // console.debug('606: listening for unexpected/unsupported event:', eventName);\n }\n }\n }\n\n /**\n * Establishes listeners for worker events when requested by the client\n * @param {string} eventName \n */\n async [LISTEN_TO_WORK_EVENTS](eventName) {\n if (this[INTERNAL_SYMBOL].subscribedWorkerEvents.has(eventName)) {\n // already subscribed to this event\n return;\n } else {\n this[INTERNAL_SYMBOL].subscribedWorkerEvents.add(eventName);\n\n await this.eventSubscriber.subscribe('job::custom', (ev) => this.work.emit(eventName, ev), {\n filter: {\n job: this.address,\n eventName,\n }\n });\n }\n }\n\n /**\n * Takes result events as input, stores the result and fires off\n * events on the job handle as required. (result, duplicate-result)\n *\n * @param {object} ev - the event recieved from protocol.listen('/results/0xThisGenAdr')\n */\n async [ON_RESULT] (ev) {\n if (this[INTERNAL_SYMBOL].results === null) {\n // This should never happen - the onResult event should only be established/called\n // in addListeners which should also initialize the internal results array\n throw new Error('Job.onResult was invoked before initializing internal results');\n }\n \n const { result: _result, time } = ev.result;\n let result = await fetchURI(_result);\n try {\n result = kvin.unmarshal(result)\n } catch (error) {\n if (error.message === \"Invalid serialization format\"){\n //Means the slice was returned from a web-worker, which doesn't double-serialize the results like the sa-worker does\n }\n else {\n throw error\n }\n }\n\n if (this[INTERNAL_SYMBOL].results[ev.sliceNumber]) {\n const changed = JSON.stringify(this[INTERNAL_SYMBOL].results[ev.sliceNumber]) !== JSON.stringify(result);\n this.emit('duplicate-result', { sliceNumber: ev.sliceNumber, changed });\n }\n\n this[INTERNAL_SYMBOL].results[ev.sliceNumber] = result;\n this[INTERNAL_SYMBOL].resultsAvailable[ev.sliceNumber] = true;\n this.emit('result', { sliceNumber: ev.sliceNumber, result });\n this.emit('resultsUpdated');\n }\n\n /**\n * Receives status events from the scheduler, updates the local status object\n * and emits a 'status' event\n *\n * @param {object} ev - the status event received from\n * protocol.listen('/status/0xThisGenAdr')\n * @param {boolean} emitStatus - value indicating whether or not the status\n * event should be emitted\n */\n [ON_STATUS]({ runStatus, total, distributed, computed }, emitStatus = true) {\n Object.assign(this[INTERNAL_SYMBOL].status, {\n runStatus,\n total,\n distributed,\n computed,\n });\n\n if (emitStatus) {\n this.emit('status', this.status);\n }\n }\n\n /**\n * Sends a request to the scheduler to deploy the job.\n */\n async [DEPLOY_JOB] () {\n const { payloadDetails } = this[INTERNAL_SYMBOL];\n \n this[SNAPSHOT](); /* .payloadDetails now up to date */\n \n /* Send sideloader bundle to the package server */\n if (DCP_ENV.platform === 'nodejs' && this[INTERNAL_SYMBOL].dependencies.length) {\n let {pkg, unresolved} = await this._publishLocalModules();\n\n payloadDetails.dependencies = unresolved;\n if (pkg)\n payloadDetails.dependencies.push(pkg.name + '/' + sideloaderModuleIdentifier);\n }\n \n this.readyState = 'preauth';\n this.emit('readystatechange', this.readyState);\n\n /* eagerly connect to dependent services for better performance */\n computeGroups.keepAlive()\n \n if (!this.deployConnection || !this.deployConnection.state.is('established')) {\n try {\n await this.deployConnection.connect();\n }\n catch (error) {\n console.error('1046: connection failed', error);\n throw error;\n }\n }\n \n const adhocId = payloadDetails.uuid.slice(payloadDetails.uuid.length - 6, payloadDetails.uuid.length);\n const schedId = this.deployConnection.peerAddress;\n const myId = this.deployConnection.identity;\n const preauthToken = await bankUtil.preAuthorizePayment(schedId, payloadDetails.maxDeployPayment, this.paymentAccountKeystore);\n const { dataRange, dataValues, dataPattern } = marshalInputData(payloadDetails.data);\n\n this.readyState = 'deploying';\n this.emit('readystatechange', this.readyState);\n \n /* Payload format is documented in scheduler-v4/libexec/job-submit/operations/submit.js */\n const submitPayload = {\n owner: myId.address,\n paymentAccount: this.paymentAccountKeystore.address,\n priority: 0, // @nyi\n\n workFunctionURI: payloadDetails.workFunctionURI,\n uuid: payloadDetails.uuid,\n mvScalarSlicePayment: +payloadDetails.feeStructure.marketValue || 0, // @todo: improve feeStructure internals to better reflect v4\n absoluteSlicePayment: +payloadDetails.feeStructure.maxPerRequest || 0,\n requirePath: payloadDetails.requirePath,\n modulePath: payloadDetails.modulePath,\n dependencies: payloadDetails.dependencies,\n requirements: payloadDetails.requirements, /* capex */\n localExec: payloadDetails.localExec,\n\n description: payloadDetails.public.description || 'Discreetly making the world smarter',\n name: payloadDetails.public.name || 'Ad-Hoc Job' + adhocId,\n \n preauthToken, // XXXwg/er @todo: validate this after fleshing out the stub(s)\n\n resultStorageType: payloadDetails.resultStorageType, // @todo: implement other result types\n resultStorageDetails: payloadDetails.resultStorageDetails, // Content depends on resultStorageType\n resultStorageParams: payloadDetails.resultStorageParams, // Post params for off-prem storage\n dataRange,\n marshalizedDataValues: kvin.marshal(dataValues), // serialize input data to avoid passing them as json\n dataPattern,\n };\n\n /* Determine thee type of the arguments option and set the submit message payload accordingly. */\n if (payloadDetails.arguments instanceof URL || payloadDetails.arguments instanceof DcpURL) {\n submitPayload.argumentsURI = payloadDetails.arguments.href;\n } else if (payloadDetails.arguments) {\n try {\n submitPayload.marshalizedArguments = kvin.marshal(Array.from(payloadDetails.arguments));\n } catch(e) {\n throw new Error(`Could not convert job arguments to Array (${e.message})`);\n }\n }\n\n // Deploy the job!\n const deployed = await this.deployConnection.send('submit', submitPayload, myId);\n\n if (!deployed.success) {\n if(deployed.payload.code === 'ENOTFOUND') {\n throw new DCPError(`Failed to submit job to scheduler. Account: ${submitPayload.paymentAccount} was not found or does not have sufficient balance (${deployed.payload.info.deployCost} DCCs needed to deploy this job)`, deployed.payload);\n } else {\n throw new DCPError('Failed to submit job to scheduler', deployed.payload);\n }\n }\n\n this.address = payloadDetails.address = deployed.payload.job;\n this[INTERNAL_SYMBOL].deployCost = deployed.payload.deployCost;\n \n if (!payloadDetails.status)\n payloadDetails.status = {\n runStatus: null,\n total: 0,\n computed: 0,\n distributed: 0,\n };\n \n payloadDetails.runStatus = payloadDetails.status.runStatus = deployed.payload.status;\n payloadDetails.status.total = deployed.payload.lastSliceNumber;\n payloadDetails.running = true;\n \n this.readyState = 'listeners';\n\n this.emit('readystatechange', this.readyState);\n\n const listenersP = this[ADD_LISTENERS]();\n\n // @todo: NYI, but we'll need it _soon_:\n // const { payload: fetched } = await this.schedulerConnection.send('fetchJob', {\n // job: this.address,\n // owner: this.paymentAccountKeystore.address,\n // includeData: false, // we already have the data, don't bother sending it back\n // }, this.paymentAccountKeystore);\n\n this[INTERNAL_SYMBOL].payloadDetails = {\n ...this[INTERNAL_SYMBOL].payloadDetails,\n ...payloadDetails,\n };\n \n return listenersP;\n }\n\n /**\n * This function is identical to exec, except that the job is executed locally\n * in the client.\n * @async\n * @param {number} cores - the number of local cores in which to execute the job.\n * @param {...any} args - The remaining arguments are identical to the arguments of exec\n * @return {Promise<ResultHandle>} - resolves with the results of the job, rejects on an error\n * @access public\n */\n localExec (cores = 1, ...args) {\n this[INTERNAL_SYMBOL].payloadDetails.localExec = true;\n\n let worker;\n this.on('accepted', () => {\n // Start a worker for this job\n worker = new Worker({\n localExec: true,\n jobAddresses: [this.address],\n paymentAddress: this[INTERNAL_SYMBOL].paymentAccountKeystore.address,\n maxWorkingSandboxes: cores,\n sandboxOptions: {\n ignoreNoProgress: true,\n SandboxConstructor: (DCP_ENV.platform === 'nodejs' &&\n __webpack_require__(/*! ../worker/evaluators */ \"./src/dcp-client/worker/evaluators/index.js\").nodeEvaluatorFactory())\n },\n });\n\n worker.start().catch((e) => {\n console.error(\"Failed to start worker for localExec:\");\n console.error(e);\n });\n });\n\n return this.exec(...args).finally(() => {\n if (worker) {\n setTimeout(() => {\n // stop the worker\n worker.stop(true);\n }, 3000);\n }\n });\n }\n\n /**\n * The current job status. Will have undefined values when the handle hasn't had exec called on it yet.\n * @access public\n * @readonly\n * @type {object}\n * @property {number} total Total number of slices in the job\n * @property {number} distributed Number of slices that have been distributed\n * @property {number} computed Number of slices that have completed execution (returned a result)\n * @property {string} runStatus Current runStatus of the job\n */\n get status () {\n // Shallow-copy to prevent modification\n return { ...this[INTERNAL_SYMBOL].status };\n }\n\n get requirePath() {\n return this[INTERNAL_SYMBOL].requirePath;\n }\n\n /**\n * This function specifies a module dependency (when the argument is a string)\n * or a list of dependencies (when the argument is an array) of the work\n * function. This function can be invoked multiple times before deployment.\n * @param {string | string[]} modulePaths - A string or array describing one\n * or more dependencies of the job.\n * @access public\n */\n requires(modulePaths) {\n if (\n typeof modulePaths !== 'string' &&\n (!Array.isArray(modulePaths) ||\n modulePaths.some((modulePath) => typeof modulePath !== 'string'))\n ) {\n throw new TypeError(\n 'The argument to dependencies is not a string or an array of strings',\n );\n } else if (modulePaths.length === 0) {\n throw new RangeError(\n 'The argument to dependencies cannot be an empty string or array',\n );\n } else if (\n Array.isArray(modulePaths) &&\n modulePaths.some((modulePath) => modulePath.length === 0)\n ) {\n throw new RangeError(\n 'The argument to dependencies cannot be an array containing an empty string',\n );\n }\n\n if (!Array.isArray(modulePaths)) {\n modulePaths = [modulePaths];\n }\n\n for (const modulePath of modulePaths) {\n if (modulePath[0] !== '.' && modulePath.indexOf('/') !== -1) {\n const modulePrefixRegEx = /^(.*)\\/.*?$/;\n const [, modulePrefix] = modulePath.match(modulePrefixRegEx);\n if (\n modulePrefix &&\n this[INTERNAL_SYMBOL].requirePath.indexOf(modulePrefix) === -1\n ) {\n this[INTERNAL_SYMBOL].requirePath.push(modulePrefix);\n }\n }\n\n this[INTERNAL_SYMBOL].dependencies.push(modulePath);\n }\n }\n\n get slicePaymentOffer () {\n return this[INTERNAL_SYMBOL].slicePaymentOffer;\n }\n\n /**\n * The keystore that will be used to pay for the job. Can be set with {@link Job.setPaymentAccountKeystore} or by providing a keystore to {@link Job.exec}.\n * @readonly\n * @access public\n * @type {module:dcp/wallet.AuthKeystore}\n */\n get paymentAccountKeystore () {\n return this[INTERNAL_SYMBOL].paymentAccountKeystore;\n }\n\n /** Set the account upon which funds will be drawn to pay for the job.\n * @param {module:dcp/wallet.AuthKeystore} keystore A keystore that representa a bank account.\n * @access public\n */\n setPaymentAccountKeystore (keystore) {\n if (this.address) {\n if (!keystore.address.eq(this[INTERNAL_SYMBOL].payloadDetails.owner)) {\n let message = \"Cannot change payment account after job has been deployed\";\n this.emit('EPERM', message);\n throw new Error(`EPERM: ${message}`);\n }\n }\n \n if (!(keystore instanceof wallet.Keystore)) {\n let e = new Error('Not an instance of Keystore: ' + keystore.toString())\n console.log(`Not an instance of Keystore: ${keystore}`)\n throw e\n }\n this[INTERNAL_SYMBOL].paymentAccountKeystore = keystore;\n }\n\n /** Set the slice payment offer. This is equivalent to the first argument to exec.\n * @param {number} slicePaymentOffer - The number of DCC the user is willing to pay to compute one slice of this job\n */\n setSlicePaymentOffer (slicePaymentOffer) {\n this[INTERNAL_SYMBOL].slicePaymentOffer = new SlicePaymentOffer(slicePaymentOffer);\n }\n\n /**\n * @param {URL} locationUrl - A URL object \n * @param {object} postParams - An object with any parameters that a user would like to be passed to a \n * remote result location. This object is capable of carry API keys for S3, \n * DropBox, etc. These parameters are passed as parameters in an \n * application/x-www-form-urlencoded request.\n */\n setResultStorage(locationUrl, postParams) {\n if (locationUrl instanceof URL || locationUrl instanceof DcpURL) {\n this[INTERNAL_SYMBOL].resultStorageDetails = locationUrl;\n } else {\n const e = new Error('Not an instance of a DCP URL: ' + locationUrl);\n console.log('Not an instance of a DCP URL ' + locationUrl);\n throw e;\n }\n\n // resultStorageParams contains any post params required for off-prem storage\n if (typeof postParams !== 'undefined' && typeof postParams === 'object' ) {\n this[INTERNAL_SYMBOL].resultStorageParams = postParams;\n } else {\n const e = new Error('Not an instance of a object: ' + postParams);\n console.log('Not an instance of an object ' + postParams);\n throw e;\n } \n\n // Some type of object here\n this[INTERNAL_SYMBOL].resultStorageType = 'pattern';\n } \n}\n\n/**\n * @typedef {object} MarshaledInputData\n * @property {any[]} [dataValues]\n * @property {string} [dataPattern]\n * @property {RangeObject} [dataRange]\n */\n\n/**\n * Depending on the shape of the job's data, resolve it into a RangeObject, a\n * Pattern, or a values array, and return it in the appropriate property.\n *\n * @param {any} data Job's input data\n * @return {MarshaledInputData} An object with one of the following properties set:\n * - dataValues: job input is an array of arbitrary values \n * - dataPattern: job input is a URI pattern \n * - dataRange: job input is a RangeObject (and/or friends)\n */\nfunction marshalInputData(data) {\n if (!(data instanceof Object\n || data instanceof SuperRangeObject)) {\n throw new TypeError(`Invalid job data type: ${typeof data}`);\n }\n\n /**\n * @type MarshaledInputData\n */\n const marshalledInputData = {};\n\n // TODO(wesgarland): Make this more robust.\n if (data instanceof SuperRangeObject ||\n (data.hasOwnProperty('ranges') && data.ranges instanceof MultiRangeObject) ||\n (data.hasOwnProperty('start') && data.hasOwnProperty('end'))) {\n marshalledInputData.dataRange = data;\n } else if (Array.isArray(data)) {\n marshalledInputData.dataValues = data;\n } else if (data instanceof URL || data instanceof DcpURL) {\n marshalledInputData.dataPattern = String(data);\n }\n\n log('marshalledInputData:', marshalledInputData);\n return marshalledInputData;\n}\n\nObject.assign(exports, {\n Job,\n SlicePaymentOffer,\n ResultHandle,\n});\n\n\n//# sourceURL=webpack:///./src/dcp-client/job/index.js?");
|
|
5113
|
+
eval("/**\n * @file job/index.js\n * @author Eddie Roosenmaallen, eddie@kingsds.network\n * Matthew Palma, mpalma@kingsds.network\n * @date November 2018\n *\n * This module implements the Compute API's Job Handle\n *\n */\n\n/** @typedef {import('dcp/dcp-client/wallet/keystore').Keystore} Keystore */\n\n\nconst BigNumber = __webpack_require__(/*! bignumber.js */ \"./node_modules/bignumber.js/bignumber.js\");\nconst { v4: uuidv4 } = __webpack_require__(/*! uuid */ \"./node_modules/uuid/dist/esm-browser/index.js\");\nconst { EventEmitter, PropagatingEventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { RangeObject, MultiRangeObject, DistributionRange, SuperRangeObject } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst { fetchURI, encodeDataURI, dumpObject } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst serialize = __webpack_require__(/*! dcp/utils/serialize */ \"./src/utils/serialize.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst { EventSubscriber } = __webpack_require__(/*! dcp/events/event-subscriber */ \"./src/events/event-subscriber.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst ClientModal = __webpack_require__(/*! dcp/dcp-client/client-modal */ \"./src/dcp-client/client-modal/index.js\");\nconst { Worker } = __webpack_require__(/*! dcp/dcp-client/worker */ \"./src/dcp-client/worker/index.js\");\nconst RemoteDataSet = __webpack_require__(/*! dcp/dcp-client/remote-data-set */ \"./src/dcp-client/remote-data-set.js\");\nconst { ResultHandle } = __webpack_require__(/*! ./result-handle */ \"./src/dcp-client/job/result-handle.js\");\nconst { SlicePaymentOffer } = __webpack_require__(/*! ./slice-payment-offer */ \"./src/dcp-client/job/slice-payment-offer.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst dcpPublish = __webpack_require__(/*! dcp/common/dcp-publish */ \"./src/common/dcp-publish.js\");\nconst computeGroups = __webpack_require__(/*! dcp/dcp-client/compute-groups */ \"./src/dcp-client/compute-groups/index.js\");\nconst schedulerConstants = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst bankUtil = __webpack_require__(/*! dcp/dcp-client/bank-util */ \"./src/dcp-client/bank-util.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp-client');\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\n\nconst log = (...args) => {\n if (debugging('job')) {\n console.debug('dcp-client:job', ...args);\n }\n};\n\nconst ON_BROWSER = DCP_ENV.isBrowserPlatform;\nconst sideloaderModuleIdentifier = 'sideloader-v1';\n\n// Symbols used to hide private members and functions on the Job instance\nconst debugBuild = (__webpack_require__(/*! dcp/common/dcp-build */ \"./src/common/dcp-build.js\").build === 'debug');\nconst INTERNAL_SYMBOL = debugBuild ? '_' : Symbol('Job Internals');\nconst SNAPSHOT = debugBuild ? '_snapshot' : Symbol('Job.snapshot');\nconst DEPLOY_JOB = debugBuild ? '_deploy' : Symbol('Job.deploy');\n\nconst ADD_LISTENERS = Symbol('Job.addListeners');\nconst LISTEN_TO_EVENTS = Symbol('Job.listenToEvents');\nconst LISTEN_TO_WORK_EVENTS = Symbol('Job.listenToWorkEvents');\nconst ON_RESULT = Symbol('Job.onResult');\nconst ON_STATUS = Symbol('Job.onStatus');\n\nexports.JOB_INTERNAL_SYMBOL = INTERNAL_SYMBOL; /* allow friends to access our guts, eg. job/result-handle */\n\nconst DEFAULT_REQUIREMENTS = {\n engine: {\n es7: null,\n spidermonkey: null\n },\n environment: {\n webgpu: null,\n offscreenCanvas: null,\n fdlibm: null\n },\n browser: {\n chrome: null\n },\n details: {\n offscreenCanvas: {\n bigTexture4096: null,\n bigTexture8192: null,\n bigTexture16384: null,\n bigTexture32768: null,\n }\n },\n discrete: null,\n};\nconst RELIABLE_EVENTS = [\n 'complete', 'cancel', 'ENOFUNDS', 'ETOOMANYERRORS', 'ESLICETOOSLOW',\n 'ENOPROGRESS', 'ETOOMANYTASKS', 'EWORKTOOBIG', 'ETOOBIG',\n];\nconst OPTIONAL_EVENTS = [\n 'console', 'error', 'noProgress', 'noProgressData',\n];\nconst ZERO_COST_HOLD_ADDRESS = '0x' + '0'.repeat(130);\n\n/** @typedef {import('../range-object').RangeLike} RangeLike */\n\n/**\n * Ensure input data is an appropriate format\n * @param {RangeObject | DistributionRange | RemoteDataSet | Array | Iterable}\n * inputData - A URI-shaped string, a [Multi]RangeObject-constructing value, or\n * an array of slice data\n * @return {RangeObject | RangeLike | DistributionRange | RemoteDataSet | Array}\n * The coerced input in an appropriate format ([Multi]RangeObject,\n * DistributionRange, RemoteDataSet, or array)\n */\nconst wrangleData = (inputData) => {\n if (typeof inputData === 'object' && !!inputData.ranges) { return new MultiRangeObject(inputData) }\n\n if (RangeObject.isRangelike(inputData)) { return inputData }\n if (RangeObject.isRangeObject(inputData)) { return inputData }\n if (DistributionRange.isDistribution(inputData)) { return inputData }\n if (RangeObject.isProtoRangelike(inputData)) { return new RangeObject(inputData) }\n if (DistributionRange.isProtoDistribution(inputData)) { return new DistributionRange(inputData) }\n if (RemoteDataSet.isRemoteDataSet(inputData)) {\n return new RemoteDataSet(...inputData);\n }\n\n return Array.isArray(inputData) ? inputData : [inputData];\n};\n\n// Used to validate the requirements object,\n// applies the default requirements schema\nconst applyObjectSchema = (obj, schema, errContext='', scope='') => {\n let checkedObjs = [];\n\n for (let p in schema) {\n let fullPropScope = scope.concat(p);\n if (!(p in obj)) {\n if (typeof schema[p] === 'object' && schema[p] !== null) {\n obj[p] = {};\n checkedObjs.push(fullPropScope);\n applyObjectSchema(obj[p], schema[p], errContext, fullPropScope.concat('.'));\n } else obj[p] = schema[p];\n } else if (typeof schema[p] === 'object' && schema[p] !== null && !checkedObjs.includes(fullPropScope)) {\n if (typeof obj[p] !== 'object') throw new Error(`${errContext}: Schema mismatch, property '${fullPropScope}' should be an object.`);\n else {\n checkedObjs.push(fullPropScope);\n applyObjectSchema(obj[p], schema[p], errContext, fullPropScope.concat('.'));\n }\n } else if ((typeof schema[p] !== 'object' || schema[p] === null)\n && typeof obj[p] !== 'boolean' && obj[p] !== null) {\n throw new Error(`${errContext}: Schema mismatch, object property '${fullPropScope}' should be a boolean.`);\n }\n }\n\n for (let p in obj) {\n let fullPropScope = scope.concat(p);\n if (!(p in schema)) throw new Error(`${errContext}: Schema mismatch, object has extra key '${fullPropScope}'.`);\n else if (typeof obj[p] === 'object' && obj[p] !== null && !checkedObjs.includes(fullPropScope)) {\n checkedObjs.push(fullPropScope);\n applyObjectSchema(obj[p], schema[p], errContext, fullPropScope.concat('.'));\n }\n }\n}\n\n/**\n * @classdesc The Compute API's Job Handle (see {@link https://docs.dcp.dev/specs/compute-api.html#job-handles|Compute API spec})\n * Job handles are objects which correspond to jobs. \n * They are created by some exports of the compute module, such as {@link module:dcp/compute.do|compute.do} and {@link module:dcp/compute.for|compute.for}.\n * @extends module:dcp/dcp-events.PropagatingEventEmitter\n * @hideconstructor\n * @access public\n */\nclass Job extends PropagatingEventEmitter {\n /**\n * This event is emitted when the job is accepted by the scheduler on deploy.\n * \n * @event Job#accepted\n * @access public\n * @type {object}\n * @property {object} job Original object that was delivered to the scheduler for deployment\n *//**\n * Fired when the job is cancelled.\n * \n * @event Job#cancel\n * @access public\n *//**\n * Fired when a result is returned.\n * \n * @event Job#result\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {string} task ID of the task (slice) the result came from\n * @property {number} sort The index of the slice\n * @property {object} result\n * @property {string} result.request\n * @property {*} result.result The value returned from the work function\n *//**\n * Fired when the result handle is modified, either when a new `result` event is fired or when the results are populated with `results.fetch()`\n * \n * @event Job#resultsUpdated\n * @access public\n *//**\n * Fired when the job has been completed.\n * \n * @event Job#complete\n * @access public\n * @type {ResultHandle}\n *//**\n * Fired when the job's status changes.\n * \n * @event Job#status\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} total Total number of slices in the job\n * @property {number} distributed Number of slices that have been distributed\n * @property {number} computed Number of slices that have completed execution (returned a result)\n * @property {string} runStatus Current runStatus of the job\n *//**\n * Fired when a slice throws an error.\n * \n * @event Job#error\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} sliceIndex Index of the slice that threw the error\n * @property {string} message The error message\n * @property {string} stack The error stacktrace\n * @property {string} name The error type name\n *//**\n * Fired when a slice uses one of the console log functions.\n * \n * @event Job#console\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} sliceIndex The index of the slice that produced this event\n * @property {string} level The log level, one of `debug`, `info`, `log`, `warn`, or `error`\n * @property {string} message The console log message\n *//**\n * Fired when a slice is stopped for not calling progress. Contains information about how long the slice ran for, and about the last reported progress calls.\n * \n * @event Job#noProgress\n * @access public\n * @type {object}\n * @property {string} jobAddress Address of the job\n * @property {number} sliceIndex The index of the slice that failed due to no progress\n * @property {number} timestamp How long the slice ran before failing\n * @property {object} progressReports\n * @property {object} progressReports.last The last progress report received from the worker\n * @property {number} progressReports.last.timestamp Time since the start of the slice\n * @property {number} progressReports.last.progress Progress value reported\n * @property {*} progressReports.last.value The last value that was passed to the progress function\n * @property {number} progressReports.last.throttledReports Number of calls to progress that were throttled since the last report\n * @property {object} progressReports.lastUpdate The last determinate (update to the progress param) progress report received from the worker\n * @property {number} progressReports.lastUpdate.timestamp\n * @property {number} progressReports.lastUpdate.progress\n * @property {*} progressReports.lastUpdate.value\n * @property {number} progressReports.lastUpdate.throttledReports\n *//**\n * Identical to `noProgress`, except that it also contains the data that the slice was executed with.\n * \n * @event Job#noProgressData\n * @access public\n * @type {object}\n * @property {*} data The data that the slice was executed with\n *//**\n * Fired when the job is paused due to running out of funds. The job can be resumed by escrowing more funds then resuming the job.\n * \n * Event payload is the estimated funds required to complete the job\n * \n * @event Job#ENOFUNDS\n * @access public\n * @type {BigNumber}\n *//**\n * Fired when the job is cancelled due to the work function not calling the `progress` method frequently enough.\n * \n * @event Job#ENOPROGRESS\n * @access public\n *//**\n * The job was cancelled because scheduler has determined that individual tasks in this job exceed the maximum allowable execution time.\n * \n * @event Job#ESLICETOOSLOW\n * @access public\n *//**\n * Fired when the job is cancelled because too many work functions are terminating with uncaught exceptions.\n * \n * @event Job#ETOOMANYERRORS\n * @access public\n */\n\n /**\n * @form1 new Job(job_shaped_object)\n * @form2 new Job('application_worker_address'[, data[, arguments]])\n * @form3b new Job('worker source'[, data[, arguments]])\n * @form3b new Job(worker_function[, data[, arguments]])\n */\n\n constructor() {\n super('Job');\n\n this.readyStateChange = (readyState) => {\n this.readyState = readyState;\n this.emit('readyStateChange', this.readyState);\n };\n this.readyStateChange('new');\n \n /*\n * Private members\n */\n this[INTERNAL_SYMBOL] = {\n events: new EventEmitter('Job Internal'),\n connected: false, // set to true after first call to exec\n /**\n * This object holds details for generating DCPv4 messages about this job.\n * It is updated everytime we call SNAPSHOT.\n */\n payloadDetails: {\n localExec: false,\n },\n\n /**\n * The slicePaymentOffer default value is set to compute.marketValue, in .exec() \n */\n slicePaymentOffer: null,\n paymentAccountKeystore: null,\n\n /**\n * These are private but getters are provided so they can be modified but\n * not replaced.\n */\n /**\n * List of module prefixes using in CommonJS module resolution.\n * @type {string[]}\n */\n requirePath: [],\n\n /**\n * List of modules the job needs.\n * @type {string[]}\n */\n\n dependencies: [],\n\n // This array contains the names of worker events that\n // had listeners registered before exec is called, once\n // the job has been deployed then the proper event handlers\n // will be generated from this list\n subscribedEvents: new Set(),\n subscribedWorkerEvents: new Set(),\n\n results: [],\n resultsAvailable: [],\n resultStorageType: 'values',\n resultStorageDetails: undefined,\n resultStorageParams: undefined, //Holds the POST params and URL for off-prem storage\n\n // Tracks job progress\n status: {\n runStatus: null,\n total: null,\n distributed: null,\n computed: null,\n },\n\n // Cancel is special. We need to fire an `alert` when the job is canceled. \n // If they are listening for the (reliable) event then they need to be able to\n // prevent it. If not, then it'll be handled by the `exec` rejection via the 'stopped'\n // event. The result is that we want only one of two ways the `alert` can be fired\n // to be active based on whether or not the user is listening for cancel. \n // Once DCP-1150 lands, we won't need to listen on stopped since more failures will fire a cancel event.\n listeningForCancel: false,\n // TODO - cancel events should have more info in them. DCP-1150\n cancelAlert: () => ClientModal.alert(\"More details in console...\", {title: 'Job Canceled'}),\n\n listeningForError: false,\n errorAlert: (err) => ClientModal.alert(err, {title: 'Unexpected Error'}),\n\n listeningForNoFunds: false,\n noFundsAlert: (event) => {\n let msg = `Job \"${event.name}\" is paused due to insufficient funds. ${event.fundsRequired} DCC is required to compute remaining ${event.remainingSlices} slices.\\njobId: ${event.job}\\nbankAccount: ${event.bankAccount}`; \n ClientModal.alert(msg, { title: 'Job paused' })\n },\n };\n\n /*\n * Public members\n */\n // Deep copy the default requirements. I know, I hate it too\n /**\n * An object describing the requirements that workers must have to be eligible for this job. See\n * {@link https://docs.dcp.dev/specs/compute-api.html#requirements-objects|Requirements Objects}.\n *\n * @type {object}\n * @access public\n */\n this.requirements = JSON.parse(JSON.stringify(DEFAULT_REQUIREMENTS));\n this.schedulerURL = undefined;\n this.bankURL = undefined;\n this.deployURL = '';\n /**\n * @see {@link https://kingsds.atlassian.net/browse/DCP-1475?atlOrigin=eyJpIjoiNzg3NmEzOWE0OWI4NGZkNmI5NjU0MWNmZGY2OTYzZDUiLCJwIjoiaiJ9|Jira Issue}\n */\n let uuid = uuidv4();\n\n /**\n * An object describing the cost the user believes each the average slice will incur, in terms of CPU/GPU and I/O.\n * If defined, this object is used to provide initial scheduling hints and to calculate escrow amounts.\n *\n * @type {object}\n * @access public\n */\n this.initialSliceProfile = undefined;\n\n /**\n * A place to store public-facing attributes of the job. Anything stored on this object will be available inside the work \n * function (see {@link module:dcp/compute~sandboxEnv.work}). The properties documented here may be used by workers to display what jobs are currently being \n * worked on.\n * @access public\n * @property {string} name Public-facing name of this job.\n * @property {string} description Public-facing description for this job.\n * @property {string} link Public-facing link to external resource about this job.\n */\n this.public = {\n name: null,\n description: null,\n link: null,\n };\n\n this.contextId = null\n\n // The following public members are only populated after the job has been deployed\n this.address = null;\n this.receipt = null;\n this.meanSliceProfile = null;\n\n /* We avoid using job.id internally because it is easy to confuse with db::jobs.id, but is an API\n * interface that we present to end-user developers so we need to keep it.\n */\n Object.defineProperty(this, 'id', {\n get: () => this.address,\n set: (id) => this.address = id\n });\n \n // \n /**\n * An EventEmitter for custom events dispatched by the work function.\n * @type {module:dcp/dcp-events.EventEmitter}\n * @access public\n * @example\n * // in sandbox\n * work.emit('myEventName', 1, [2], \"three\");\n * // clientside\n * job.work.on('myEventName', (num, arr, string) => { });\n */\n this.work = new EventEmitter('job.work');\n\n //Initialize the eventSubscriber so each job has unique eventSubscriber\n this.eventSubscriber = new EventSubscriber()\n \n this.work.on('newListener', (evt) => {\n if (!this[INTERNAL_SYMBOL].connected && evt !== 'newListener') {\n this[INTERNAL_SYMBOL].subscribedWorkerEvents.add(evt);\n }\n });\n\n this.on('newListener', (evt) => {\n if (!this[INTERNAL_SYMBOL].connected && evt !== 'newListener') {\n this[INTERNAL_SYMBOL].subscribedEvents.add(evt);\n }\n if (evt === 'cancel') {\n this[INTERNAL_SYMBOL].listeningForCancel = true;\n }\n\n if (evt === 'error') {\n this[INTERNAL_SYMBOL].listeningForError = true;\n }\n\n if (evt === 'nofunds') {\n this[INTERNAL_SYMBOL].listeningForNoFunds = true;\n }\n });\n\n this.addDefaultListener('cancel', (msg) => {\n if (ON_BROWSER && this[INTERNAL_SYMBOL].listeningForCancel) \n this[INTERNAL_SYMBOL].cancelAlert(msg);\n });\n\n this.addDefaultListener('error', (event) => {\n if (!this[INTERNAL_SYMBOL].listeningForError) {\n if (ON_BROWSER) {\n this[INTERNAL_SYMBOL].errorAlert(event);\n } else {\n console.error(event);\n }\n }\n });\n\n this.addDefaultListener('nofunds', (event) => {\n if (!this[INTERNAL_SYMBOL].listeningForNoFunds) {\n if (ON_BROWSER) {\n this[INTERNAL_SYMBOL].noFundsAlert(event);\n } else {\n console.warn(\n `Job ${event.job} has been ${event.runStatus} with ${event.remainingSlices} slices remaining due to running out of funds (need ${event.fundsRequired} credits). Re-add the necessary funds and resume the job to continue getting results.`,\n );\n }\n }\n });\n\n // Form1: If arguments[0] is an object that looks like a job, this is a 'copy constructor'\n // where we inherit as much as possible from the original instance.\n if (typeof arguments[0] === 'object' &&\n arguments[0].type &&\n arguments[0].data &&\n arguments[0].public) {\n \n let src = arguments[0];\n\n this[INTERNAL_SYMBOL].payloadDetails = {\n ...src,\n data: wrangleData(src.data), // rehydrate ranges\n };\n\n if (src.feeStructure) {\n this.setSlicePaymentOffer(src.feeStructure);\n }\n \n if (src.address)\n this.address = src.address;\n if (src.payloadData.status)\n this[ON_STATUS](this[INTERNAL_SYMBOL].payload.status, false);\n if (src.public)\n Object.assign(this.public, src.public);\n } else {\n /* Forms 2-4 */ \n if (typeof arguments[0] === 'function')\n arguments[0] = arguments[0].toString();\n\n if (typeof arguments[0] === 'string') {\n this[INTERNAL_SYMBOL].workFunctionURI = encodeDataURI(arguments[0], 'application/javascript');\n } else if (DcpURL.isURL(arguments[0])) {\n this[INTERNAL_SYMBOL].workFunctionURI = arguments[0].href;\n } \n\n const wrangledInputData = wrangleData(arguments[1] || []);\n log('wrangledInputData:', wrangledInputData);\n Object.assign(this[INTERNAL_SYMBOL].payloadDetails, {\n request: 'main',\n data: wrangledInputData,\n arguments: arguments[2] || [],\n });\n }\n\n // This should happen last, it depends on the this.[INTERNAL_SYMBOL].payloadDetails.data array\n /**\n * A Result Handle object used to query and manipulate the output set. \n * Present once job has been deployed.\n * @type {ResultHandle}\n * @access public\n */\n this.results = new ResultHandle(this);\n\n /**\n * Read-only copy of the job's uuid (generated or rehydrated via form1 constructor)\n */\n Object.defineProperty(this, 'uuid', {\n get: () => uuid,\n configurable: false,\n enumerable: true,\n });\n \n // each entry contains the computeGroupID, joinKey, joinSecret, joinKeystore\n // Add to public compute group by default\n this.computeGroups = [ Object.assign({}, schedulerConstants.computeGroups.public) ];\n\n\n const ceci = this;\n\n // Initialize to null so these properties are recognized for the Job class\n this.bankConnection = null;\n this.deployConnection = null;\n this.openBankConn = function openBankConn()\n {\n ceci.bankConnection = new protocolV4.Connection(dcpConfig.bank.services.bankTeller);\n ceci.bankConnection.on('close', ceci.openBankConn);\n }\n\n this.openDeployConn = function openDeployConn()\n {\n ceci.deployConnection = new protocolV4.Connection(dcpConfig.scheduler.services.jobSubmit);\n ceci.deployConnection.on('close', ceci.openDeployConn);\n }\n\n this.openBankConn();\n this.openDeployConn();\n }\n\n /** \n * Cancel the job\n * @access public\n * @param {string} reason If provided, will be sent to client\n */\n async cancel (reason = undefined) {\n const response = await this.deployConnection.send('cancelJob', {\n job: this.address,\n ownerAddress: this.paymentAccountKeystore.address,\n reason,\n }, this.paymentAccountKeystore);\n\n return response.payload;\n }\n\n /** \n * Resume this job\n * @access public\n */\n async resume() {\n const response = await this.schedulerConnection.send('resumeJob', {\n job: this.address,\n owner: this.paymentAccountKeystore.address,\n }, this.paymentAccountKeystore);\n\n return response.payload;\n }\n\n /**\n * Helper function for retrieving info about the job. The job must have already been deployed.\n * An alias for {@link module:dcp/compute.getJobInfo}.\n * @access public\n */\n async getJobInfo(){\n return await __webpack_require__(/*! ../compute */ \"./src/dcp-client/compute.js\").compute.getJobInfo(this.address);\n }\n\n /**\n * Helper function for retrieving info about the job's slices. The job must have already been deployed.\n * An alias for {@link module:dcp/compute.getSliceInfo}.\n * @access public\n */\n async getSliceInfo(){\n return await __webpack_require__(/*! ../compute */ \"./src/dcp-client/compute.js\").compute.getSliceInfo(this.address);\n }\n \n async addSliceData(dataValues) {\n if (!Array.isArray(this[INTERNAL_SYMBOL].payloadDetails.data)) {\n throw new TypeError('Only data-by-value jobs may dynamically add slices');\n }\n\n const { payload } = await this.deployConnection.send('addSliceData', {\n job: this.address,\n dataValues,\n });\n\n if (payload.success && typeof payload.lastSliceNumber !== 'undefined')\n this.status.total = payload.lastSliceNumber;\n\n return payload;\n }\n\n /**\n * job.snapshot(): Private function used to populate the payloadDetails from private data,\n * inferred data, etc. Once this function has run, the payloadDetails are\n * considered authoritatively up to date until the calling function returns\n * or awaits.\n */\n [SNAPSHOT]() {\n const pd = this[INTERNAL_SYMBOL].payloadDetails;\n\n pd.type = 'ad-hoc'; /* @todo implement appliances */\n pd.uuid = this.uuid;\n pd.workFunctionURI = this[INTERNAL_SYMBOL].workFunctionURI;\n pd.dependencies = this[INTERNAL_SYMBOL].dependencies;\n pd.requirePath = this[INTERNAL_SYMBOL].requirePath;\n pd.modulePath = this[INTERNAL_SYMBOL].modulePath;\n pd.resultStorageType = this[INTERNAL_SYMBOL].resultStorageType;\n pd.resultStorageDetails = this[INTERNAL_SYMBOL].resultStorageDetails;\n pd.resultStorageParams = this[INTERNAL_SYMBOL].resultStorageParams;\n\n pd.requirements = this.requirements;\n applyObjectSchema(pd.requirements, DEFAULT_REQUIREMENTS, 'Requirements Object');\n \n // @todo: 'figure this out' - wise words from eddie /mp jan 2019\n if (!pd.options) { pd.options = {}; }\n if (!pd.public) { pd.public = {}; } \n\n for (let p of ['name', 'description', 'link']) {\n if (typeof this.public[p] === 'string') {\n pd.public[p] = this.public[p];\n }\n }\n\n // The max value that the client is willing to spend to deploy\n // (list on the scheduler, doesn't include compute payment)\n /// maxDeployPayment is the max the user is willing to pay to DCP (as a\n /// Hold), in addition to the per-slice offer and associated scrape.\n /// Currently calculated as `deployCost = costPerKB *\n /// (JSON.stringify(job).length / 1024) // 1e-9 per kb`\n // @todo: figure this out / er nov 2018\n pd.maxDeployPayment = 1;\n \n /// payloadDetails.timing can be provided as an initial estimate of slice time, to\n /// give a more useful useful calculated heap value (heap.value is more or less\n /// dcc-per-millisecond)\n pd.timing = pd.timing || 1; \n }\n\n /** Escrow additional funds for this job\n * @access public\n * @param {number|BigNumber} fundsRequired - A number or BigNumber instance representing the funds to escrow for this job\n */\n async escrow (fundsRequired) {\n if ((typeof fundsRequired !== 'number' && !BigNumber.isBigNumber(fundsRequired))\n || fundsRequired <= 0 || !Number.isFinite(fundsRequired) || Number.isNaN(fundsRequired)) {\n throw new Error(`Job.escrow: fundsRequired must be a number greater than zero. (not ${fundsRequired})`);\n }\n\n const response = await this.bankConnection.send('embiggenFeeStructure', {\n feeStructureAddress: this[INTERNAL_SYMBOL].payloadDetails.feeStructureId,\n additionalEscrow: BigNumber(fundsRequired),\n fromAddress: this.paymentAccountKeystore.address,\n }, this.paymentAccountKeystore);\n\n this.receipt = response.payload;\n\n return this.receipt;\n }\n\n async _pack () {\n var retval = __webpack_require__(/*! ./node-modules */ \"./src/dcp-client/job/node-modules.js\").createModuleBundle(this[INTERNAL_SYMBOL].dependencies);\n return retval;\n }\n\n /** \n * Collect all of the dependencies together, throw them into a BravoJS\n * module which sideloads them as a side effect of declaration, and transmit\n * them to the package manager. Then we return the package descriptor object,\n * which is guaranteed to have only one file in it.\n *\n * @returns {object} with properties name and files[0]\n */\n async _publishLocalModules() {\n const { tempFile, hash, unresolved } = await this._pack();\n\n if (!tempFile) {\n return { unresolved };\n }\n\n const sideloaderFilename = tempFile.filename;\n const pkg = {\n name: `dcp-pkg-v1-localhost-${hash.toString('hex')}`,\n version: '1.0.0',\n files: {\n [sideloaderFilename]: `${sideloaderModuleIdentifier}.js`,\n },\n }\n\n await dcpPublish.publish(pkg);\n tempFile.remove();\n\n return { pkg, unresolved };\n }\n\n /**\n * Deploys the job to the scheduler.\n * @param {number | object} [slicePaymentOffer=compute.marketValue] - Amount\n * in DCC that the user is willing to pay per slice.\n * @param {Keystore} [paymentAccountKeystore=wallet.get] - An instance of the\n * Wallet API Keystore that's used as the payment account when executing the\n * job.\n * @param {object} [initialSliceProfile] - An object describing the cost the\n * user believes the average slice will incur.\n * @access public\n * @emits Job#accepted\n */\n async exec(slicePaymentOffer = __webpack_require__(/*! ../compute */ \"./src/dcp-client/compute.js\").compute.marketValue, paymentAccountKeystore, initialSliceProfile) {\n if (this[INTERNAL_SYMBOL].connected) {\n throw new Error('Exec called twice on the same job handle.');\n }\n\n /* eagerly connect to depedent services for better performance */\n this.eventSubscriber.eventRouterConnection.keepalive();\n this.deployConnection.keepalive();\n\n this.readyState = 'exec'\n this.emit('readystatechange', this.readyState)\n\n if (typeof slicePaymentOffer !== 'undefined') this.setSlicePaymentOffer(slicePaymentOffer);\n if (typeof initialSliceProfile !== 'undefined') this.initialSliceProfile = initialSliceProfile;\n if (typeof paymentAccountKeystore !== 'undefined') {\n /** XXX @todo deprecate use of ethereum wallet objects */\n if (typeof paymentAccountKeystore === 'object' && paymentAccountKeystore.hasOwnProperty('_privKey')) {\n console.warn('* deprecated API * - job.exec invoked with ethereum wallet object as paymentAccountKeystore') /* /wg oct 2019 */\n paymentAccountKeystore = paymentAccountKeystore._privKey\n }\n /** XXX @todo deprecate use of private keys */\n if (wallet.isPrivateKey(paymentAccountKeystore)) {\n console.warn('* deprecated API * - job.exec invoked with private key as paymentAccountKeystore') /* /wg dec 2019 */\n paymentAccountKeystore = await new wallet.Keystore(paymentAccountKeystore, '');\n }\n\n this.setPaymentAccountKeystore(paymentAccountKeystore)\n }\n\n // Unlock\n if (this[INTERNAL_SYMBOL].paymentAccountKeystore) {\n // Throws if they fail to unlock, we allow this since the keystore was set programmatically. \n await this[INTERNAL_SYMBOL].paymentAccountKeystore.unlock(undefined, parseFloat(dcpConfig.job.maxDeployTime));\n } else {\n // If not set programmatically, we keep trying to get an unlocked keystore ... forever.\n let locked = true;\n let safety = 0; // no while loop shall go unguarded\n let ks;\n do {\n ks = null;\n // custom message for the browser modal to denote the purpose of keystore submission\n let msg = `This application is requesting a keystore file to execute ${this.public.description || this.public.name || 'this job'}. Please upload the corresponding keystore file. If you upload a keystore file which has been encrypted with a passphrase, the application will not be able to use it until it prompts for a passphrase and you enter it.`;\n try {\n ks = await wallet.get({ contextId: this.contextId, jobName: this.public.name, msg});\n } catch (e) {\n if (e.code !== ClientModal.CancelErrorCode) throw e;\n };\n if (ks) {\n try {\n await ks.unlock(undefined, parseFloat(dcpConfig.job.maxDeployTime));\n locked = false;\n } catch (e) {\n const expectedCodes = [wallet.unlockFailErrorCode, ClientModal.CancelErrorCode];\n if (!expectedCodes.includes(e.code)) throw e;\n }\n }\n if (safety++ > 1000) throw new Error('EINFINITY: job.exec tried wallet.get more than 1000 times.')\n } while (locked);\n this.setPaymentAccountKeystore(ks)\n }\n\n // We either have a valid keystore + password or we have rejected by this point.\n if (!this.slicePaymentOffer) {\n throw new Error('A payment profile must be assigned before executing the job');\n } else {\n let pd = this[INTERNAL_SYMBOL].payloadDetails;\n pd.feeStructure = this[INTERNAL_SYMBOL].slicePaymentOffer.toFeeStructure(pd.data.length);\n }\n\n if (!this.address) {\n try {\n this.readyStateChange('init');\n await this[DEPLOY_JOB]();\n // localExec jobs are not entered in any compute group.\n if (!this[INTERNAL_SYMBOL].payloadDetails.localExec) {\n // Add this job to its currently-defined compute groups (if any)\n let failedComputeGroups = 0;\n for (const group of this.computeGroups) {\n try\n {\n await computeGroups.addJob(this.address, group);\n this.emit('addComputeGroup', group);\n } catch (error)\n {\n failedComputeGroups++;\n error.message = `Error from compute group with joinKey '${group.joinKey}': ` + error.message;\n this.emit('error', error)\n }\n }\n if (this.computeGroups.length > 0 && failedComputeGroups === this.computeGroups.length)\n {\n this.emit('error', 'Failed to join all specified compute groups, cancelling the job')\n this.cancel('No valid compute groups.');\n }\n\n computeGroups\n .closeServiceConnection()\n .catch((err) =>\n console.error(\n 'Warning: could not close compute groups service connection',\n err,\n ),\n );\n }\n\n this.readyState = 'deployed';\n this.emit('readystatechange', this.readyState);\n this.emit('accepted');\n } catch (error) {\n if (ON_BROWSER) {\n await ClientModal.alert(error, { title: 'Failed to deploy job!' });\n }\n\n throw error;\n }\n } else {\n await this[ADD_LISTENERS]();\n\n this.readyState = 'reconnected';\n this.emit('readystatechange', this.readyState);\n }\n\n this[ON_STATUS](this[INTERNAL_SYMBOL].payloadDetails.status);\n this[INTERNAL_SYMBOL].connected = true;\n\n return new Promise((resolve, reject) => {\n const onComplete = () => resolve(this.results);\n const onCancel = (event) => {\n const reason = `Job was cancelled by ${\n event.reason ? event.reason : 'Cancelled for unknown reason'\n }.`;\n\n /**\n * FIXME(DCP-1150): Remove this since normal cancel event is noisy\n * enough to not need stopped event too.\n */\n if (ON_BROWSER && !this[INTERNAL_SYMBOL].listeningForCancel)\n this[INTERNAL_SYMBOL].cancelAlert(event.reason);\n this.emit('cancel', event);\n reject(new DCPError(event.reason, event.code));\n };\n\n this[INTERNAL_SYMBOL].events.once('stopped', (stopEvent) => {\n switch (stopEvent.runStatus) {\n case 'finished':\n onComplete();\n break;\n case 'cancelled':\n onCancel(stopEvent);\n break;\n default:\n /**\n * Asserting that we should never be able to reach here. The only\n * scheduler events that should trigger the Job's 'stopped' event\n * are 'cancelled', 'finished', and 'paused'.\n */\n reject(\n new Error(\n `Unknown event \"${stopEvent.runStatus}\" caused the job to be stopped.`,\n ),\n );\n break;\n }\n });\n\n if (!this[INTERNAL_SYMBOL].payloadDetails.running) {\n const runStatus = this[INTERNAL_SYMBOL].payloadDetails.runStatus;\n this[INTERNAL_SYMBOL].events.emit('stopped', { runStatus });\n }\n })\n .finally(() => {\n const handleErr = (e) => {\n console.error('Error while closing job connection:');\n console.error(e);\n }\n\n // Create an async IIFE to not block the promise chain\n (async () => {\n //delay to let last few events to be received\n await new Promise((resolve) => setTimeout(resolve, 1000));\n \n // close all of the connections so that we don't cause node processes to hang.\n await this.eventSubscriber.close().catch(handleErr);\n this.deployConnection.off('close', this.openDeployConn)\n await this.deployConnection.close().catch(handleErr);\n })();\n });\n }\n\n /**\n * job.addListeners(): Private function used to set up event listeners to the scheduler\n * before deploying the job.\n */\n async [ADD_LISTENERS] () {\n // This is important: We need to flush the task queue before adding listeners\n // because we queue pending listeners by listening to the newListener event (in the constructor).\n // If we don't flush here, then the newListener events may fire after this function has run,\n // and the events won't be properly set up.\n await new Promise(resolve => setTimeout(resolve, 0));\n\n // @todo: Listen for an estimated cost, probably emit an \"estimated\" event when it comes in?\n // also @todo: Do the estimation task(s) on the scheduler and send an \"estimated\" event\n\n let listeningForResults = false;\n let listenForResults = async () => {\n listeningForResults = true;\n // Initialize the local results arr, don't replace! Modify in-place only because the result handle needs it\n this[INTERNAL_SYMBOL].results.length = this[INTERNAL_SYMBOL].resultsAvailable.length = this[INTERNAL_SYMBOL].payloadDetails.data.length;\n\n // Listen for result events\n await this.eventSubscriber.subscribe('job::result', (ev) => this[ON_RESULT](ev), {\n filter: { job: this.address, }\n });\n }\n\n if (this.listenerCount('result') > 0) {\n await listenForResults();\n } else {\n this.on('newListener', (evt) => {\n if (evt === 'result' && !listeningForResults) {\n listenForResults();\n }\n });\n }\n\n // Listen for status updates\n // e.g. when a slice gets distributed or finishes being computed\n await this.eventSubscriber.subscribe('job::status', (ev) => this[ON_STATUS](ev), {\n filter: { job: this.address, }\n });\n\n // Listen for the job stopping (complete) - used to reject the exec promise\n await this.eventSubscriber.subscribe('job::stop', (ev) => this[INTERNAL_SYMBOL].events.emit('stopped', ev), {\n filter: { job: this.address, },\n });\n // Listen for the job being cancelled - used to reject the exec promise\n await this.eventSubscriber.subscribe('job::cancel', (ev) => this[INTERNAL_SYMBOL].events.emit('stopped', ev), {\n filter: { job: this.address, },\n });\n\n /**\n * Listen for the job ENOFUNDS (the payment account has run out) - used to\n * pause the exec promise.\n */\n await this.eventSubscriber.subscribe(\n 'job::ENOFUNDS',\n (event) => this.emit('nofunds', event),\n {\n filter: { job: this.address },\n },\n );\n\n // Connect listeners that were set up before exec\n const evts = Array.from(this[INTERNAL_SYMBOL].subscribedEvents);\n this[INTERNAL_SYMBOL].subscribedEvents.clear();\n for (let evt of evts) {\n await this[LISTEN_TO_EVENTS](evt);\n }\n\n if (!this[INTERNAL_SYMBOL].listeningForError) {\n await this[LISTEN_TO_EVENTS]('error');\n }\n\n // Connect listeners that are set up after exec\n this.on('newListener', (evt) => {\n if (evt === 'newListener') return;\n this[LISTEN_TO_EVENTS](evt);\n });\n\n // Connect work event listeners that were set up before exec\n const workEvts = Array.from(this[INTERNAL_SYMBOL].subscribedWorkerEvents);\n this[INTERNAL_SYMBOL].subscribedWorkerEvents.clear();\n for (let evt of workEvts) {\n await this[LISTEN_TO_WORK_EVENTS](evt);\n }\n\n // Connect work event listeners that are set up after exec\n this.work.on('newListener', (evt) => {\n if (evt === 'newListener') return;\n this[LISTEN_TO_WORK_EVENTS](evt);\n });\n }\n\n /**\n * Subscribes to either reliable events or optional events,\n * depending on the eventName being in RELIABLE_EVENTS or OPTIONAL_EVENTS\n * @param {string} eventName \n */\n async [LISTEN_TO_EVENTS](eventName) {\n // Listen for Reliable Events (ENOFUNDS, etc.)\n // The client app will add a listener for the eventName,\n // eg. job.on('ENOFUNDS', handleNoFunds)\n const listenForReliableEvent = async (eventName) => {\n await this.eventSubscriber.subscribe('job::reliableEvent', async (ev) => {\n let { /* eventName, - use original event name */ eventData, serializer } = ev;\n switch (serializer) {\n case 'json':\n eventData = JSON.parse(eventData);\n break;\n case 'serialize':\n eventData = serialize.deserialize(eventData);\n break;\n default:\n throw new RangeError(`Unsupported serializer: ${serializer}`)\n }\n\n if (eventName === 'complete') {\n // Complete is a special case: it should emit the ResultHandle\n this.emit(eventName, this.results);\n } else {\n this.emit(eventName, eventData);\n }\n }, {\n filter: {\n job: this.address,\n eventName: eventName.toUpperCase(),\n },\n });\n }\n\n // Establishes listeners for optional events when requested by the client\n const listenForOptionalEvent = async (eventName) => {\n let subscribedEventName = eventName;\n let callback = (ev) => this.emit(eventName, ev);\n let omit = [];\n switch (eventName) {\n case 'noProgress':\n case 'noprogress':\n omit.push('data');\n subscribedEventName = 'noProgress';\n break;\n case 'noProgressData':\n case 'noprogressdata':\n subscribedEventName = 'noProgress';\n break;\n case 'console':\n callback = (ev) => {\n ev.message = kvin.deserialize(ev.message);\n this.emit(eventName, ev);\n };\n break;\n }\n\n log('listening to optional event:', { eventName, subscribedEventName });\n await this.eventSubscriber.subscribe(`job::${subscribedEventName}`, callback, {\n filter: { job: this.address, },\n omit,\n });\n }\n\n if (this[INTERNAL_SYMBOL].subscribedEvents.has(eventName)) {\n // already subscribed to this event\n return;\n } else {\n this[INTERNAL_SYMBOL].subscribedEvents.add(eventName);\n\n if (RELIABLE_EVENTS.map(ev => ev.toLowerCase()).includes(eventName)) {\n // console.debug('599: listening for reliable event:', eventName);\n await listenForReliableEvent(eventName);\n } else if (OPTIONAL_EVENTS.map(ev => ev.toLowerCase()).includes(eventName)) {\n // console.debug('602: listening for optional event:', eventName);\n await listenForOptionalEvent(eventName);\n }\n else {\n // console.debug('606: listening for unexpected/unsupported event:', eventName);\n }\n }\n }\n\n /**\n * Establishes listeners for worker events when requested by the client\n * @param {string} eventName \n */\n async [LISTEN_TO_WORK_EVENTS](eventName) {\n if (this[INTERNAL_SYMBOL].subscribedWorkerEvents.has(eventName)) {\n // already subscribed to this event\n return;\n } else {\n this[INTERNAL_SYMBOL].subscribedWorkerEvents.add(eventName);\n\n await this.eventSubscriber.subscribe('job::custom', (ev) => this.work.emit(eventName, ev), {\n filter: {\n job: this.address,\n eventName,\n }\n });\n }\n }\n\n /**\n * Takes result events as input, stores the result and fires off\n * events on the job handle as required. (result, duplicate-result)\n *\n * @param {object} ev - the event recieved from protocol.listen('/results/0xThisGenAdr')\n */\n async [ON_RESULT] (ev) {\n if (this[INTERNAL_SYMBOL].results === null) {\n // This should never happen - the onResult event should only be established/called\n // in addListeners which should also initialize the internal results array\n throw new Error('Job.onResult was invoked before initializing internal results');\n }\n \n const { result: _result, time } = ev.result;\n let result = await fetchURI(_result);\n try {\n result = kvin.unmarshal(result)\n } catch (error) {\n if (error.message === \"Invalid serialization format\"){\n //Means the slice was returned from a web-worker, which doesn't double-serialize the results like the sa-worker does\n }\n else {\n throw error\n }\n }\n\n if (this[INTERNAL_SYMBOL].results[ev.sliceNumber]) {\n const changed = JSON.stringify(this[INTERNAL_SYMBOL].results[ev.sliceNumber]) !== JSON.stringify(result);\n this.emit('duplicate-result', { sliceNumber: ev.sliceNumber, changed });\n }\n\n this[INTERNAL_SYMBOL].results[ev.sliceNumber] = result;\n this[INTERNAL_SYMBOL].resultsAvailable[ev.sliceNumber] = true;\n this.emit('result', { sliceNumber: ev.sliceNumber, result });\n this.emit('resultsUpdated');\n }\n\n /**\n * Receives status events from the scheduler, updates the local status object\n * and emits a 'status' event\n *\n * @param {object} ev - the status event received from\n * protocol.listen('/status/0xThisGenAdr')\n * @param {boolean} emitStatus - value indicating whether or not the status\n * event should be emitted\n */\n [ON_STATUS]({ runStatus, total, distributed, computed }, emitStatus = true) {\n Object.assign(this[INTERNAL_SYMBOL].status, {\n runStatus,\n total,\n distributed,\n computed,\n });\n\n if (emitStatus) {\n this.emit('status', this.status);\n }\n }\n\n /**\n * Sends a request to the scheduler to deploy the job.\n */\n async [DEPLOY_JOB] () {\n const { payloadDetails } = this[INTERNAL_SYMBOL];\n \n this[SNAPSHOT](); /* .payloadDetails now up to date */\n \n /* Send sideloader bundle to the package server */\n if (DCP_ENV.platform === 'nodejs' && this[INTERNAL_SYMBOL].dependencies.length) {\n let {pkg, unresolved} = await this._publishLocalModules();\n\n payloadDetails.dependencies = unresolved;\n if (pkg)\n payloadDetails.dependencies.push(pkg.name + '/' + sideloaderModuleIdentifier);\n }\n \n this.readyState = 'preauth';\n this.emit('readystatechange', this.readyState);\n\n /* eagerly connect to dependent services for better performance */\n computeGroups.keepAlive()\n \n if (!this.deployConnection || !this.deployConnection.state.is('established')) {\n try {\n await this.deployConnection.connect();\n }\n catch (error) {\n console.error('1046: connection failed', error);\n throw error;\n }\n }\n \n const adhocId = payloadDetails.uuid.slice(payloadDetails.uuid.length - 6, payloadDetails.uuid.length);\n const schedId = this.deployConnection.peerAddress;\n const myId = this.deployConnection.identity;\n const preauthToken = await bankUtil.preAuthorizePayment(schedId, payloadDetails.maxDeployPayment, this.paymentAccountKeystore);\n const { dataRange, dataValues, dataPattern } = marshalInputData(payloadDetails.data);\n\n this.readyState = 'deploying';\n this.emit('readystatechange', this.readyState);\n \n /* Payload format is documented in scheduler-v4/libexec/job-submit/operations/submit.js */\n const submitPayload = {\n owner: myId.address,\n paymentAccount: this.paymentAccountKeystore.address,\n priority: 0, // @nyi\n\n workFunctionURI: payloadDetails.workFunctionURI,\n uuid: payloadDetails.uuid,\n mvScalarSlicePayment: +payloadDetails.feeStructure.marketValue || 0, // @todo: improve feeStructure internals to better reflect v4\n absoluteSlicePayment: +payloadDetails.feeStructure.maxPerRequest || 0,\n requirePath: payloadDetails.requirePath,\n modulePath: payloadDetails.modulePath,\n dependencies: payloadDetails.dependencies,\n requirements: payloadDetails.requirements, /* capex */\n localExec: payloadDetails.localExec,\n\n description: payloadDetails.public.description || 'Discreetly making the world smarter',\n name: payloadDetails.public.name || 'Ad-Hoc Job' + adhocId,\n \n preauthToken, // XXXwg/er @todo: validate this after fleshing out the stub(s)\n\n resultStorageType: payloadDetails.resultStorageType, // @todo: implement other result types\n resultStorageDetails: payloadDetails.resultStorageDetails, // Content depends on resultStorageType\n resultStorageParams: payloadDetails.resultStorageParams, // Post params for off-prem storage\n dataRange,\n marshalizedDataValues: kvin.marshal(dataValues), // serialize input data to avoid passing them as json\n dataPattern,\n };\n\n /* Determine thee type of the arguments option and set the submit message payload accordingly. */\n if (payloadDetails.arguments instanceof URL || payloadDetails.arguments instanceof DcpURL) {\n submitPayload.argumentsURI = payloadDetails.arguments.href;\n } else if (payloadDetails.arguments) {\n try {\n submitPayload.marshalizedArguments = kvin.marshal(Array.from(payloadDetails.arguments));\n } catch(e) {\n throw new Error(`Could not convert job arguments to Array (${e.message})`);\n }\n }\n\n // XXXpfr Excellent tracing.\n if (debugging('dcp-client')) {\n dumpObject(submitPayload, 'Submit: Job Index: Examine submitPayload', 128);\n }\n\n // Deploy the job!\n const deployed = await this.deployConnection.send('submit', submitPayload, myId);\n\n if (!deployed.success) {\n if(deployed.payload.code === 'ENOTFOUND') {\n throw new DCPError(`Failed to submit job to scheduler. Account: ${submitPayload.paymentAccount} was not found or does not have sufficient balance (${deployed.payload.info.deployCost} DCCs needed to deploy this job)`, deployed.payload);\n } else {\n throw new DCPError('Failed to submit job to scheduler', deployed.payload);\n }\n }\n\n this.address = payloadDetails.address = deployed.payload.job;\n this[INTERNAL_SYMBOL].deployCost = deployed.payload.deployCost;\n \n if (!payloadDetails.status)\n payloadDetails.status = {\n runStatus: null,\n total: 0,\n computed: 0,\n distributed: 0,\n };\n \n payloadDetails.runStatus = payloadDetails.status.runStatus = deployed.payload.status;\n payloadDetails.status.total = deployed.payload.lastSliceNumber;\n payloadDetails.running = true;\n \n this.readyState = 'listeners';\n\n this.emit('readystatechange', this.readyState);\n\n const listenersP = this[ADD_LISTENERS]();\n\n // @todo: NYI, but we'll need it _soon_:\n // const { payload: fetched } = await this.schedulerConnection.send('fetchJob', {\n // job: this.address,\n // owner: this.paymentAccountKeystore.address,\n // includeData: false, // we already have the data, don't bother sending it back\n // }, this.paymentAccountKeystore);\n\n this[INTERNAL_SYMBOL].payloadDetails = {\n ...this[INTERNAL_SYMBOL].payloadDetails,\n ...payloadDetails,\n };\n \n return listenersP;\n }\n\n /**\n * This function is identical to exec, except that the job is executed locally\n * in the client.\n * @async\n * @param {number} cores - the number of local cores in which to execute the job.\n * @param {...any} args - The remaining arguments are identical to the arguments of exec\n * @return {Promise<ResultHandle>} - resolves with the results of the job, rejects on an error\n * @access public\n */\n localExec (cores = 1, ...args) {\n this[INTERNAL_SYMBOL].payloadDetails.localExec = true;\n\n let worker;\n this.on('accepted', () => {\n // Start a worker for this job\n worker = new Worker({\n localExec: true,\n jobAddresses: [this.address],\n paymentAddress: this[INTERNAL_SYMBOL].paymentAccountKeystore.address,\n maxWorkingSandboxes: cores,\n sandboxOptions: {\n ignoreNoProgress: true,\n SandboxConstructor: (DCP_ENV.platform === 'nodejs' &&\n __webpack_require__(/*! ../worker/evaluators */ \"./src/dcp-client/worker/evaluators/index.js\").nodeEvaluatorFactory())\n },\n });\n\n worker.start().catch((e) => {\n console.error(\"Failed to start worker for localExec:\");\n console.error(e.message);\n });\n });\n\n return this.exec(...args).finally(() => {\n if (worker) {\n setTimeout(() => {\n // stop the worker\n worker.stop(true);\n }, 3000);\n }\n });\n }\n\n /**\n * The current job status. Will have undefined values when the handle hasn't had exec called on it yet.\n * @access public\n * @readonly\n * @type {object}\n * @property {number} total Total number of slices in the job\n * @property {number} distributed Number of slices that have been distributed\n * @property {number} computed Number of slices that have completed execution (returned a result)\n * @property {string} runStatus Current runStatus of the job\n */\n get status () {\n // Shallow-copy to prevent modification\n return { ...this[INTERNAL_SYMBOL].status };\n }\n\n get requirePath() {\n return this[INTERNAL_SYMBOL].requirePath;\n }\n\n /**\n * This function specifies a module dependency (when the argument is a string)\n * or a list of dependencies (when the argument is an array) of the work\n * function. This function can be invoked multiple times before deployment.\n * @param {string | string[]} modulePaths - A string or array describing one\n * or more dependencies of the job.\n * @access public\n */\n requires(modulePaths) {\n if (\n typeof modulePaths !== 'string' &&\n (!Array.isArray(modulePaths) ||\n modulePaths.some((modulePath) => typeof modulePath !== 'string'))\n ) {\n throw new TypeError(\n 'The argument to dependencies is not a string or an array of strings',\n );\n } else if (modulePaths.length === 0) {\n throw new RangeError(\n 'The argument to dependencies cannot be an empty string or array',\n );\n } else if (\n Array.isArray(modulePaths) &&\n modulePaths.some((modulePath) => modulePath.length === 0)\n ) {\n throw new RangeError(\n 'The argument to dependencies cannot be an array containing an empty string',\n );\n }\n\n if (!Array.isArray(modulePaths)) {\n modulePaths = [modulePaths];\n }\n\n for (const modulePath of modulePaths) {\n if (modulePath[0] !== '.' && modulePath.indexOf('/') !== -1) {\n const modulePrefixRegEx = /^(.*)\\/.*?$/;\n const [, modulePrefix] = modulePath.match(modulePrefixRegEx);\n if (\n modulePrefix &&\n this[INTERNAL_SYMBOL].requirePath.indexOf(modulePrefix) === -1\n ) {\n this[INTERNAL_SYMBOL].requirePath.push(modulePrefix);\n }\n }\n\n this[INTERNAL_SYMBOL].dependencies.push(modulePath);\n }\n }\n\n get slicePaymentOffer () {\n return this[INTERNAL_SYMBOL].slicePaymentOffer;\n }\n\n /**\n * The keystore that will be used to pay for the job. Can be set with {@link Job.setPaymentAccountKeystore} or by providing a keystore to {@link Job.exec}.\n * @readonly\n * @access public\n * @type {module:dcp/wallet.AuthKeystore}\n */\n get paymentAccountKeystore () {\n return this[INTERNAL_SYMBOL].paymentAccountKeystore;\n }\n\n /** Set the account upon which funds will be drawn to pay for the job.\n * @param {module:dcp/wallet.AuthKeystore} keystore A keystore that representa a bank account.\n * @access public\n */\n setPaymentAccountKeystore (keystore) {\n if (this.address) {\n if (!keystore.address.eq(this[INTERNAL_SYMBOL].payloadDetails.owner)) {\n let message = \"Cannot change payment account after job has been deployed\";\n this.emit('EPERM', message);\n throw new Error(`EPERM: ${message}`);\n }\n }\n \n if (!(keystore instanceof wallet.Keystore)) {\n let e = new Error('Not an instance of Keystore: ' + keystore.toString())\n console.log(`Not an instance of Keystore: ${keystore}`)\n throw e\n }\n this[INTERNAL_SYMBOL].paymentAccountKeystore = keystore;\n }\n\n /** Set the slice payment offer. This is equivalent to the first argument to exec.\n * @param {number} slicePaymentOffer - The number of DCC the user is willing to pay to compute one slice of this job\n */\n setSlicePaymentOffer (slicePaymentOffer) {\n this[INTERNAL_SYMBOL].slicePaymentOffer = new SlicePaymentOffer(slicePaymentOffer);\n }\n\n /**\n * @param {URL} locationUrl - A URL object \n * @param {object} postParams - An object with any parameters that a user would like to be passed to a \n * remote result location. This object is capable of carry API keys for S3, \n * DropBox, etc. These parameters are passed as parameters in an \n * application/x-www-form-urlencoded request.\n */\n setResultStorage(locationUrl, postParams) {\n if (locationUrl instanceof URL || locationUrl instanceof DcpURL) {\n this[INTERNAL_SYMBOL].resultStorageDetails = locationUrl;\n } else {\n const e = new Error('Not an instance of a DCP URL: ' + locationUrl);\n console.log('Not an instance of a DCP URL ' + locationUrl);\n throw e;\n }\n\n // resultStorageParams contains any post params required for off-prem storage\n if (typeof postParams !== 'undefined' && typeof postParams === 'object' ) {\n this[INTERNAL_SYMBOL].resultStorageParams = postParams;\n } else {\n const e = new Error('Not an instance of a object: ' + postParams);\n console.log('Not an instance of an object ' + postParams);\n throw e;\n } \n\n // Some type of object here\n this[INTERNAL_SYMBOL].resultStorageType = 'pattern';\n } \n}\n\n/**\n * @typedef {object} MarshaledInputData\n * @property {any[]} [dataValues]\n * @property {string} [dataPattern]\n * @property {RangeObject} [dataRange]\n */\n\n/**\n * Depending on the shape of the job's data, resolve it into a RangeObject, a\n * Pattern, or a values array, and return it in the appropriate property.\n *\n * @param {any} data Job's input data\n * @return {MarshaledInputData} An object with one of the following properties set:\n * - dataValues: job input is an array of arbitrary values \n * - dataPattern: job input is a URI pattern \n * - dataRange: job input is a RangeObject (and/or friends)\n */\nfunction marshalInputData(data) {\n if (!(data instanceof Object\n || data instanceof SuperRangeObject)) {\n throw new TypeError(`Invalid job data type: ${typeof data}`);\n }\n\n /**\n * @type MarshaledInputData\n */\n const marshalledInputData = {};\n\n // TODO(wesgarland): Make this more robust.\n if (data instanceof SuperRangeObject ||\n (data.hasOwnProperty('ranges') && data.ranges instanceof MultiRangeObject) ||\n (data.hasOwnProperty('start') && data.hasOwnProperty('end'))) {\n marshalledInputData.dataRange = data;\n } else if (Array.isArray(data)) {\n marshalledInputData.dataValues = data;\n } else if (data instanceof URL || data instanceof DcpURL) {\n marshalledInputData.dataPattern = String(data);\n }\n\n log('marshalledInputData:', marshalledInputData);\n return marshalledInputData;\n}\n\nObject.assign(exports, {\n Job,\n SlicePaymentOffer,\n ResultHandle,\n});\n\n\n//# sourceURL=webpack:///./src/dcp-client/job/index.js?");
|
|
5114
5114
|
|
|
5115
5115
|
/***/ }),
|
|
5116
5116
|
|
|
@@ -5199,7 +5199,7 @@ eval("/* WEBPACK VAR INJECTION */(function(process) {/**\n * @file /src/sch
|
|
|
5199
5199
|
/*! no static exports found */
|
|
5200
5200
|
/***/ (function(module, exports, __webpack_require__) {
|
|
5201
5201
|
|
|
5202
|
-
eval("/**\n * @file /src/schedmsg/schedmsg-web.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date March 2020\n *\n * This is the SchedMsg implementation for commands that are browser-specific\n * or have browser-specific behaviour.\n */\n\nconst { SchedMsg } = __webpack_require__(/*! ./schedmsg */ \"./src/dcp-client/schedmsg/schedmsg.js\");\n\nclass SchedMsgWeb extends SchedMsg {\n constructor(worker) {\n super(worker);\n this.modal = null;\n\n this.registerHandler('announce', this.onAnnouncement.bind(this));\n this.registerHandler('openPopup', this.onOpenPopup.bind(this));\n this.registerHandler('reload', this.onReload.bind(this));\n }\n\n onAnnouncement({ message }) {\n if (this.modal) {\n this.modal.close();\n }\n\n this.modal = window.userInterface.alert('Announcement', '' /* subtitle */, message,\n /* onClose */ () => this.modal = null);\n }\n\n onOpenPopup({ href }) {\n window.open(href);\n }\n\n onReload() {\n const hash = window.location.hash;\n\n let newUrl = window.location.href.replace(/#.*/, '');\n newUrl += (newUrl.indexOf('?') === -1 ? '?' : '&');\n newUrl += 'dcp=
|
|
5202
|
+
eval("/**\n * @file /src/schedmsg/schedmsg-web.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date March 2020\n *\n * This is the SchedMsg implementation for commands that are browser-specific\n * or have browser-specific behaviour.\n */\n\nconst { SchedMsg } = __webpack_require__(/*! ./schedmsg */ \"./src/dcp-client/schedmsg/schedmsg.js\");\n\nclass SchedMsgWeb extends SchedMsg {\n constructor(worker) {\n super(worker);\n this.modal = null;\n\n this.registerHandler('announce', this.onAnnouncement.bind(this));\n this.registerHandler('openPopup', this.onOpenPopup.bind(this));\n this.registerHandler('reload', this.onReload.bind(this));\n }\n\n onAnnouncement({ message }) {\n if (this.modal) {\n this.modal.close();\n }\n\n this.modal = window.userInterface.alert('Announcement', '' /* subtitle */, message,\n /* onClose */ () => this.modal = null);\n }\n\n onOpenPopup({ href }) {\n window.open(href);\n }\n\n onReload() {\n const hash = window.location.hash;\n\n let newUrl = window.location.href.replace(/#.*/, '');\n newUrl += (newUrl.indexOf('?') === -1 ? '?' : '&');\n newUrl += 'dcp=dd98e423ca01eaeeba22e0bd0d948e358ffc7b43,' + Date.now() + hash;\n\n window.location.replace(newUrl);\n }\n}\n\nObject.assign(module.exports, {\n SchedMsgWeb\n});\n\n\n//# sourceURL=webpack:///./src/dcp-client/schedmsg/schedmsg-web.js?");
|
|
5203
5203
|
|
|
5204
5204
|
/***/ }),
|
|
5205
5205
|
|
|
@@ -5345,7 +5345,7 @@ eval("exports.BrowserEvaluator = __webpack_require__(/*! ./browser */ \"./src/dc
|
|
|
5345
5345
|
/*! no static exports found */
|
|
5346
5346
|
/***/ (function(module, exports, __webpack_require__) {
|
|
5347
5347
|
|
|
5348
|
-
eval("/**\n * @file node-localExec.js Node-specific support for creating/running localExec\n * jobs with the Compute API.\n * @author Wes Garland, wes@kingsds.network\n * @date May 2020\n */\n\n/* global dcpConfig */\n// @ts-nocheck\n\nconst utils = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst isDebugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp-client')('localExec');\n\nconst debug = (...args) => {\n if (isDebugging) {\n console.debug('dcp-client:localExec', ...args);\n }\n};\n\n/**\n * Factory function which makes NodeEvaluator constructors based on the current configuration.\n */\nexports.nodeEvaluatorFactory = function evaluators$$nodeEvaluatorFactory(inTesting = false)\n{\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n const path = __webpack_require__(/*! path */ \"./node_modules/path-browserify/index.js\");\n var controlCodeFiles; /* Evaluator's control code establishes the sandbox environment and postMessage/onmessage implementation */\n\n if (DCP_ENV.platform !== 'nodejs') {\n throw new Error(`Can't create NodeEvaluator constructor: platform is '${DCP_ENV.platform}'; must be 'nodejs'.`);\n }\n\n /**\n * The `standaloneWorker.js` and `evaluator-node.js` currently resides in\n * `dcp-client` and that this code can be executed from source or from the\n * dcp-client bundle, we need to detect whether we're running in either mode.\n *\n * If we're running from source, `dcp-rtlink` has most likely been initialized\n * so we can resolve `dcp-client` in the `node_modules` folder of the `dcp`\n * repo.\n *\n * Else if we're running from the bundle, `dcp-rtlink` has not been\n * initialized so this code is in the bundle being evaluated in the context of\n * `dcp-client/index.js`. In that case, we need to switch to relative paths to\n * resolve the modules.\n *\n * @todo TODO(bryan-hoang): Improve detection of whether the dcp-rtlink module has\n * been initialized. e.g. Exposed an API through the dcp-rtlink module\n */\n const hasRtlinkBeenInitialized =\n Object.keys(requireNative('module')._cache)\n .map((key) => path.basename(key))\n .indexOf('rtLink.js') !== -1;\n const dcpClientModulePrefix = hasRtlinkBeenInitialized ? 'dcp-client' : '.';\n const { workerFactory } = requireNative(\n `${dcpClientModulePrefix}/lib/standaloneWorker`,\n );\n const { Evaluator } = requireNative(`${dcpClientModulePrefix}/libexec/evaluator-node`);\n \n /**\n * @param socket \"server\" socket for the node evaluator\n */\n function localExecConnectHandler(socket)\n {\n new Evaluator(socket, socket, controlCodeFiles);\n }\n\n /**\n * Constructor for a NodeEvaluator. An instance of this class becomes the SandboxConstructor \n * in the implementation of job::localExec() when running under NodeJS.\n *\n * @todo Eliminate the extra wrapper here. It was previously necessary when we had to\n * capture the factory argument vector in a closure. It should be possible now to\n * implement this as a single function.\n */\n function NodeEvaluator()\n {\n const child_process = requireNative('child_process');\n const process = requireNative('process');\n const env = Object.assign({}, process.env, {\n /**\n * Assume the user is okay with the security implications of executing\n * their work locally. Needed to start the script without a warning\n * and delay. All other environment properties are inherited from the\n * calling process.\n */\n I_WANT_AN_INSECURE_DCP_WORKER: 'badly',\n DCP_SCHEDULER_LOCATION: `${dcpConfig.scheduler.location.href}`,\n });\n var command;\n var pipe = __webpack_require__(/*! dcp/utils/tmpfiles */ \"./src/utils/tmpfiles.js\").createTempSocket(undefined, 'localExec-pipe', localExecConnectHandler);\n var options = {\n readStream: pipe,\n writeStream: pipe\n };\n\n /**\n * First, the set up files for a node evaluator are needed, so the output of\n * the `dcp-evaluator-start` script from the `dcp-worker` package will be\n * used to be the authoritative source of the locations of the files.\n */\n try {\n command = requireNative.resolve('dcp-worker/bin/dcp-evaluator-start');\n } catch (e) {\n /* Sometimes require can't resolve this module through `npm link`, even though it is\n * the \"right way\" to resolve a peer dependency, so we try to resolve from the main\n * module's POV also. This makes use of the 'main' section in dcp-worker/package.json.\n */\n const mainModuleRequire = requireNative('module').createRequireFromPath(\n `${requireNative.main.path}/`,\n );\n command = mainModuleRequire.resolve('dcp-worker/bin/dcp-evaluator-start');\n }\n\n if (!command)\n throw new Error('Could not locate dcp-worker/bin/dcp-evaluator-start');\n\n /* Ask dcp-evaluator-start for the sandbox setup files for a node evaluator */\n let sandboxType = 'node';\n if (inTesting)\n sandboxType = 'nodeTesting';\n delete env.DCP_DEBUG; /* don't polute stdio pipeline with debug status messages */\n const child = child_process.spawnSync(command, [ `--sandbox-type=${sandboxType}`, '-FJ' ],\n {\n encoding: 'utf8',\n env\n },\n );\n\n debug('Raw localExec evaluator output:', child.stdout);\n if (child.error) {\n console.log(\"ERROR: \",child.error);\n }\n\n if (child.status !== 0) {\n console.error('\\n',child.stderr, child.stdout);\n process.exit(child.status)\n }\n\n controlCodeFiles = JSON.parse(child.stdout);\n debug('Read controlCodeFiles:', controlCodeFiles);\n\n return new (workerFactory(options))();\n }\n\n NodeEvaluator.prototype = new EventEmitter('nodeLocalExec');\n return NodeEvaluator;\n}\n\n\n//# sourceURL=webpack:///./src/dcp-client/worker/evaluators/node-localExec.js?");
|
|
5348
|
+
eval("/**\n * @file node-localExec.js Node-specific support for creating/running localExec\n * jobs with the Compute API.\n * @author Wes Garland, wes@kingsds.network\n * @date May 2020\n */\n\n/* global dcpConfig */\n// @ts-nocheck\n\nconst utils = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst isDebugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp-client')('localExec');\n\nconst debug = (...args) => {\n if (isDebugging) {\n console.debug('dcp-client:localExec', ...args);\n }\n};\n\n/**\n * Factory function which makes NodeEvaluator constructors based on the current configuration.\n */\nexports.nodeEvaluatorFactory = function evaluators$$nodeEvaluatorFactory(inTesting = false)\n{\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n const path = __webpack_require__(/*! path */ \"./node_modules/path-browserify/index.js\");\n var controlCodeFiles; /* Evaluator's control code establishes the sandbox environment and postMessage/onmessage implementation */\n\n if (DCP_ENV.platform !== 'nodejs') {\n throw new Error(`Can't create NodeEvaluator constructor: platform is '${DCP_ENV.platform}'; must be 'nodejs'.`);\n }\n\n /**\n * The `standaloneWorker.js` and `evaluator-node.js` currently resides in\n * `dcp-client` and that this code can be executed from source or from the\n * dcp-client bundle, we need to detect whether we're running in either mode.\n *\n * If we're running from source, `dcp-rtlink` has most likely been initialized\n * so we can resolve `dcp-client` in the `node_modules` folder of the `dcp`\n * repo.\n *\n * Else if we're running from the bundle, `dcp-rtlink` has not been\n * initialized so this code is in the bundle being evaluated in the context of\n * `dcp-client/index.js`. In that case, we need to switch to relative paths to\n * resolve the modules.\n *\n * @todo TODO(bryan-hoang): Improve detection of whether the dcp-rtlink module has\n * been initialized. e.g. Exposed an API through the dcp-rtlink module\n */\n const hasRtlinkBeenInitialized =\n Object.keys(requireNative('module')._cache)\n .map((key) => path.basename(key))\n .indexOf('rtLink.js') !== -1;\n const dcpClientModulePrefix = hasRtlinkBeenInitialized ? 'dcp-client' : '.';\n const { workerFactory } = requireNative(\n `${dcpClientModulePrefix}/lib/standaloneWorker`,\n );\n const { Evaluator } = requireNative(`${dcpClientModulePrefix}/libexec/evaluator-node`);\n \n /**\n * @param socket \"server\" socket for the node evaluator\n */\n function localExecConnectHandler(socket)\n {\n new Evaluator(socket, socket, controlCodeFiles);\n }\n\n /**\n * Constructor for a NodeEvaluator. An instance of this class becomes the SandboxConstructor \n * in the implementation of job::localExec() when running under NodeJS.\n *\n * @todo Eliminate the extra wrapper here. It was previously necessary when we had to\n * capture the factory argument vector in a closure. It should be possible now to\n * implement this as a single function.\n */\n function NodeEvaluator()\n {\n const child_process = requireNative('child_process');\n const process = requireNative('process');\n const env = Object.assign({}, process.env, {\n /**\n * Assume the user is okay with the security implications of executing\n * their work locally. Needed to start the script without a warning\n * and delay. All other environment properties are inherited from the\n * calling process.\n */\n I_WANT_AN_INSECURE_DCP_WORKER: 'badly',\n DCP_SCHEDULER_LOCATION: `${dcpConfig.scheduler.location.href}`,\n });\n var command;\n var pipe = __webpack_require__(/*! dcp/utils/tmpfiles */ \"./src/utils/tmpfiles.js\").createTempSocket(undefined, 'localExec-pipe', localExecConnectHandler);\n var options = {\n readStream: pipe,\n writeStream: pipe\n };\n\n /**\n * First, the set up files for a node evaluator are needed, so the output of\n * the `dcp-evaluator-start` script from the `dcp-worker` package will be\n * used to be the authoritative source of the locations of the files.\n */\n try {\n command = requireNative.resolve('dcp-worker/bin/dcp-evaluator-start');\n } catch (e) {\n /* Sometimes require can't resolve this module through `npm link`, even though it is\n * the \"right way\" to resolve a peer dependency, so we try to resolve from the main\n * module's POV also. This makes use of the 'main' section in dcp-worker/package.json.\n */\n const mainModuleRequire = requireNative('module').createRequireFromPath(\n `${requireNative.main.path}/`,\n );\n try{\n command = mainModuleRequire.resolve('dcp-worker/bin/dcp-evaluator-start');\n } catch (err) {\n throw new DCPError('Could not load dcp-worker', 'ENOWORKER');\n }\n }\n\n if (!command)\n throw new Error('Could not locate dcp-worker/bin/dcp-evaluator-start');\n\n /* Ask dcp-evaluator-start for the sandbox setup files for a node evaluator */\n let sandboxType = 'node';\n if (inTesting)\n sandboxType = 'nodeTesting';\n delete env.DCP_DEBUG; /* don't polute stdio pipeline with debug status messages */\n const child = child_process.spawnSync(command, [ `--sandbox-type=${sandboxType}`, '-FJ' ],\n {\n encoding: 'utf8',\n env\n },\n );\n\n debug('Raw localExec evaluator output:', child.stdout);\n if (child.error) {\n console.log(\"ERROR: \",child.error);\n }\n\n if (child.status !== 0) {\n console.error('\\n',child.stderr, child.stdout);\n process.exit(child.status)\n }\n\n controlCodeFiles = JSON.parse(child.stdout);\n debug('Read controlCodeFiles:', controlCodeFiles);\n\n return new (workerFactory(options))();\n }\n\n NodeEvaluator.prototype = new EventEmitter('nodeLocalExec');\n return NodeEvaluator;\n}\n\n\n//# sourceURL=webpack:///./src/dcp-client/worker/evaluators/node-localExec.js?");
|
|
5349
5349
|
|
|
5350
5350
|
/***/ }),
|
|
5351
5351
|
|
|
@@ -5368,7 +5368,7 @@ eval("/**\n * @file This module implements the Worker API, used to create worker
|
|
|
5368
5368
|
/***/ (function(module, exports, __webpack_require__) {
|
|
5369
5369
|
|
|
5370
5370
|
"use strict";
|
|
5371
|
-
eval("// NOTE - need timeout/postmessage function\n/**\n * @file dcp-client/worker/sandbox.js\n *\n * A sandbox that when constructed and assigned can do work for\n * a distributed slice. A sandbox runs for a single slice at a time.\n *\n * Usage:\n * let sandbox = new Sandbox()\n * await sandbox.start()\n * let results = await sandbox.run(slice)\n *\n * Debug flags:\n * Sandbox.debugWork = true // - turns off 30 second timeout to let user debug sandbox innards more easily\n * Sandbox.debugState = true // - logs all state transitions for this sandbox\n * Sandbox.debugEvents = true // - logs all events received from the sandbox\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * Ryan Rossiter, ryan@kingsds.network\n * @date May 2019\n * @module sandbox\n */\n/* global dcpConfig */\n// @ts-check\n\n\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('worker');\nconst { assert, assertEq3 } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\nconst scopedKvin = new kvin.KVIN({Object: ({}).constructor,\n Array: ([]).constructor, \n Function: (()=>{}).constructor});\n\nlet timeDilation = 1;\nif (DCP_ENV.platform === 'nodejs') {\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n /** Make timers 10x slower when running in niim */\n timeDilation = (requireNative('module')._cache.niim instanceof requireNative('module').Module) ? 10 : 1;\n}\n\n/**\n * Wraps console.debug to emulate debug module prefixing messages on npm.\n * @param {...any} args\n */\nconst debug = (...args) => {\n if (debugging()) {\n console.debug('dcp-client:worker:sandbox', ...args);\n }\n};\n\nconst nanoid = __webpack_require__(/*! nanoid/non-secure */ \"./node_modules/nanoid/non-secure/index.js\");\n\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { fetchURI, encodeDataURI } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n\n// Sandbox states\nconst UNREADY = 'UNREADY' // No Sandbox (web worker, saworker, etc) has been constructed yet\nconst READYING = 'READYING' // Sandbox is being constructed and environment (bravojs, env) is being set up\nconst READY_FOR_ASSIGN = 'READY_FOR_ASSIGN' // Sandbox is ready to be assigned\nconst ASSIGNING = 'ASSIGNING' // Sandbox is running through assigning steps\nconst ASSIGNED = 'ASSIGNED' // Sandbox is assigned but not working\nconst WORKING = 'WORKING' // Sandbox is working\nconst TERMINATED = 'TERMINATED'\nconst EVAL_RESULT_PREFIX = 'evalResult::';\n\nclass SandboxError extends Error {}\nclass NoProgressError extends SandboxError { constructor(...args) { super(...args); this.errorCode = 'ENOPROGRESS'; } }\nclass SliceTooSlowError extends SandboxError { constructor(...args) { super(...args); this.errorCode = 'ESLICETOOSLOW'; } }\nclass UncaughtExceptionError extends SandboxError { constructor(...args) { super(...args); this.errorCode = 'EUNCAUGHTERROR'; } }\n\n/** @typedef {import('dcp/common/dcp-events').EventEmitter} EventEmitter */\n/** @typedef {import('./slice').Slice} Slice */\n/** @typedef {import('./supervisor-cache').SupervisorCache} SupervisorCache */\n/** @typedef {*} opaqueId */\n\n/**\n * @access public\n * @typedef {object} SandboxOptions\n * @constructor {function} [SandboxConstructor]\n * @property {boolean} [ignoreNoProgress] - When true, the sandbox will not be stopped for not calling progress\n */\n\nclass Sandbox extends EventEmitter {\n /**\n * A Sandbox (i.e. a worker sandbox) which executes distributed slices.\n *\n * @constructor\n * @param {SupervisorCache} cache\n * @param {SandboxOptions} options\n */\n constructor (cache, options, origins) {\n super('Sandbox');\n /** @type {SupervisorCache} */\n this.supervisorCache = cache;\n /** @type {SandboxOptions} */\n this.options = {\n ignoreNoProgress: false,\n ...options,\n SandboxConstructor: options.SandboxConstructor ||\n __webpack_require__(/*! ./evaluators */ \"./src/dcp-client/worker/evaluators/index.js\").BrowserEvaluator,\n }\n this.allowedOrigins = origins;\n\n /** @type {opaqueId} */\n this.jobAddress = null;\n /** @type {object} */\n this.evaluatorHandle = null;\n /** @type {object} */\n this.capabilities = null;\n /** @type {EventEmitter} */\n this.ee = new EventEmitter('SandboxInternal')\n\n /** @type {string} */\n this._state = UNREADY;\n /** @type {boolean} */\n this.allocated = false;\n /** @type {number?} */\n this.progress = 100;\n /** @type {object} */\n this.progressReports = null;\n /** @type {object} */\n this.progressTimeout = null;\n /** @type {object} */\n this.sliceTimeout = null;\n\n /** @type {Slice} */\n this.slice = null;\n\n /** @type {number?} */\n this.started = null;\n /** @type {number?} */\n this.sliceStartTime = null;\n /** @type {boolean} */\n this.requiresGPU = false;\n /** @type {string|URL} */\n this.packageURL = dcpConfig.packageManager.location\n /** @type {number?} */\n this.id = Sandbox.getNewId();\n\n this.ringMessageHandlers = [\n this.handleRing0Message,\n this.handleRing1Message,\n this.handleRing2Message,\n this.handleRing3Message,\n ];\n\n this.resetSliceTimeReport();\n }\n\n static getNewId() {\n return Sandbox.idCounter++;\n }\n\n get state () {\n return this._state\n }\n\n set state (value) {\n if (Sandbox.debugState) {\n console.debug(`sandbox - changing state of ${this.id}... ${this._state} -> ${value}`)\n }\n\n if (this.state === TERMINATED && value !== TERMINATED) {\n // For safety!\n throw new Error(`Sandbox set state violation, attepted to change state from ${this.state} to ${value}`);\n }\n\n this._state = value;\n }\n\n get isReadyForAssign () {\n return this.state === READY_FOR_ASSIGN;\n }\n\n get isAssigned () {\n return this.state === ASSIGNED;\n }\n\n get isWorking () {\n return this.state === WORKING;\n }\n\n get isTerminated () {\n return this.state === TERMINATED;\n }\n\n changeWorkingToAssigned () {\n if (this.isWorking)\n this.state = ASSIGNED;\n }\n\n setIsAssigning () {\n this.state = ASSIGNING;\n }\n\n /**\n * Readies the sandbox. This will result in the sandbox being ready and not assigned,\n * it will need to be assigned with a job before it is able to do work.\n *\n * @todo maybe preload specific modules or let the cache pass in what modules to load?\n * @throws on failure to ready\n */\n async start(delay = 0) {\n this.started = Date.now();\n this.state = READYING;\n\n if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay * timeDilation));\n\n try {\n // RING 0\n this.evaluatorHandle = new this.options.SandboxConstructor({\n name: `DCP Sandbox #${this.id}`,\n });\n this.evaluatorHandle.onmessage = this.onmessage.bind(this);\n this.evaluatorHandle.onerror = this.onerror.bind(this);\n\n // Now in RING 1\n\n // Now in RING 2\n await this.describe();\n this.state = READY_FOR_ASSIGN;\n this.emit('ready', this);\n } catch (error) {\n console.warn('Failed to start the sandbox -', error);\n this.terminate(false);\n throw error;\n }\n }\n\n /**\n * This will assign the sandbox with a job, loading its sandbox code\n * into the sandbox.\n *\n * @param {string} jobAddress The address of the job to assign to\n * @throws on initialization failure\n */\n async assign(jobAddress) {\n this.jobAddress = jobAddress;\n this.job = await this.supervisorCache.fetchJob(jobAddress, this.allowedOrigins);\n\n assertEq3(this.job.address, jobAddress);\n assert(typeof this.job === 'object');\n assert(typeof this.job.requirements === 'object');\n assert(Array.isArray(this.job.dependencies));\n assert(Array.isArray(this.job.requirePath));\n\n // Extract public data from job, with defaults\n this.public = Object.assign({\n name: `Anonymous Job ${this.job.address.slice(0, 6)}`,\n description: 'Discreetly helping make the world smarter.',\n link: 'https://distributed.computer/about',\n }, this.job.public);\n\n // Future: We may want other filename tags for appliances // RR Nov 2019\n\n // Important: The order of applying requirements before loading the sandbox code\n // is important for modules and sandbox code to set globals over the whitelist.\n await this.applySandboxRequirements(this.job.requirements);\n await this.assignEvaluator();\n this.state = ASSIGNED;\n }\n\n async assignEvaluator() {\n debug('Begin assigning job to evaluator');\n const ceci = this;\n\n return new Promise(function sandbox$$assignEvaluatorPromise(resolve, reject) {\n if(!DCP_ENV.isBrowserPlatform) {\n if (!ceci.job.arguments._serializeVerId) {\n ceci.job.arguments = scopedKvin.marshal(ceci.job.arguments);\n }\n }\n \n const message = {\n request: 'assign',\n job: ceci.job,\n sandboxConfig: {\n worker: dcpConfig.worker,\n },\n };\n\n const onSuccess = (event) => {\n // eslint-disable-next-line no-use-before-define\n ceci.ee.removeListener('reject', onFailListener);\n ceci.emit('assigned', event.jobAddress);\n debug('Job assigned to evaluator');\n resolve();\n };\n\n const onFail = (error) => {\n // eslint-disable-next-line no-use-before-define\n ceci.ee.removeListener('assigned', onSuccessListener);\n reject(error);\n };\n\n const onSuccessListener = ceci.ee.once('assigned', onSuccess);\n const onFailListener = ceci.ee.once('reject', onFail);\n ceci.evaluatorHandle.postMessage(message);\n });\n }\n\n /**\n * Evaluates a string inside the sandbox.\n *\n * @param {string} code - the code to evaluate in the sandbox\n * @param {string} filename - the name of the 'file' to help with debugging,\n * no longer working though?\n * @returns {Promise} - resolves with eval result on success, rejects\n * otherwise\n */\n eval(code, filename) {\n var ceci = this;\n \n return new Promise(function sandbox$$eval$Promise(resolve, reject) {\n let msgId = nanoid();\n let msg = {\n request: 'eval',\n data: code,\n filename,\n msgId, \n }\n\n const eventId = EVAL_RESULT_PREFIX + msgId;\n\n let onSuccess = (event) => {\n ceci.ee.removeListener('reject', onFailListener)\n resolve(event)\n }\n\n let onFail = (event) => {\n ceci.ee.removeListener(eventId, onSuccessListener)\n reject(event)\n }\n\n let onSuccessListener = ceci.ee.once(eventId, onSuccess);\n let onFailListener = ceci.ee.once('reject', onFail)\n\n ceci.evaluatorHandle.postMessage(msg)\n })\n }\n\n /**\n * Resets the state of the bootstrap, without resetting the sandbox function if assigned.\n * Mostly used to reset the progress status before reusing a sandbox on another slice.\n * Must be called after @start.\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n resetSandboxState () {\n var ceci = this;\n\n return new Promise(function sandbox$resetSandboxStatePromise(resolve, reject) {\n let successCb, failTimeout;\n let msg = {\n request: 'resetState',\n };\n\n successCb = ceci.ee.once('resetStateDone', function sandbox$resetSandboxState$success () {\n if (failTimeout === false)\n return; /* already rejected */\n clearTimeout(failTimeout);\n failTimeout = false;\n resolve();\n });\n\n failTimeout = setTimeout(function sandbox$resetSandboxState$fail() {\n if (failTimeout === false)\n return; /* already resolved */\n \n ceci.ee.removeListener('resetStateDone', successCb);\n ceci.terminate(false);\n failTimeout = false;\n\n reject(new Error('resetState never received resetStateDone event from sandbox'));\n }, 3000 * timeDilation); /* XXXwg need tuneable */\n\n assert(ceci.evaluatorHandle); // It is possible that ceci.terminate nulls out evaluatorHandle before getting here.\n ceci.evaluatorHandle.postMessage(msg);\n });\n }\n\n /**\n * Clear all timers that are set inside the sandbox (evaluator) environment.\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n clearSandboxTimers() {\n var ceci = this;\n \n return new Promise(function sandbox$clearSandboxTimersPromise(resolve, reject) {\n let successCb, failTimeout;\n let msg = {\n request: 'clearTimers',\n };\n\n successCb = ceci.ee.once('clearTimersDone', function sandbox$clearSandboxTimers$success() {\n if (failTimeout === false)\n return; /* already rejected */\n clearTimeout(failTimeout);\n failTimeout = false;\n resolve();\n });\n\n failTimeout = setTimeout(function sanbox$clearSandboxTimers$fail() {\n if (failTimeout === false)\n return; /* already resolved */\n \n ceci.ee.removeListener('clearTimersDone', successCb);\n ceci.terminate(false);\n failTimeout = false;\n \n reject(new Error('clearTimers never received clearTimersDone event from sandbox'));\n }, 3000 * timeDilation); /* XXXwg need tuneable */\n\n if (ceci.evaluatorHandle) // Sometimes ceci.terminate nulls out evaluatorHandle before getting here.\n ceci.evaluatorHandle.postMessage(msg);\n });\n }\n\n /**\n * Sends a post message to describe its capabilities.\n *\n * Side effect: Sets the capabilities property of the current sandbox.\n *\n * @returns {Promise} Resolves with the sandbox's capabilities. Rejects with\n * an error saying a response was not received.\n * @memberof Sandbox\n */\n describe() {\n debug('Beginning to describe evaluator');\n var ceci = this;\n \n return new Promise(function sandbox$describePromise(resolve, reject) {\n if (ceci.evaluatorHandle === null) {\n return reject(new Error('Evaluator has not been initialized.'));\n }\n\n /**\n * Opted to create a flag for the describe response being received so that\n * we don't have to *hoist* the timeout's id to clear it in the response\n * handler.\n */\n let didReceiveDescribeResponse = false;\n const describeResponseHandler = ceci.ee.once('describe', (data) => {\n didReceiveDescribeResponse = true;\n const { capabilities } = data;\n if (typeof capabilities === 'undefined') {\n reject(\n new Error('Did not receive capabilities from describe response.'),\n );\n }\n ceci.capabilities = capabilities;\n\n // Currently only used in tests. May use the event in the future.\n ceci.emit('described', capabilities);\n debug('Evaluator has been described');\n resolve(capabilities);\n });\n const describeResponseFailedHandler = () => {\n if (!didReceiveDescribeResponse) {\n ceci.ee.removeListener('describe', describeResponseHandler);\n ceci.terminate(false);\n reject(\n new Error(\n 'Describe message timed-out. No describe response was received from the describe command.',\n ),\n );\n }\n };\n\n const message = {\n request: 'describe',\n };\n\n // Arbitrarily set the waiting time.\n setTimeout(describeResponseFailedHandler, 6000 * timeDilation); /* XXXwg need tuneable */\n assert(ceci.evaluatorHandle); // It is possible that ceci.terminate nulls out evaluatorHandle before getting here.\n ceci.evaluatorHandle.postMessage(message);\n });\n }\n\n /**\n * Passes the job's requirements object into the sandbox so that the global\n * access lists can be updated accordingly.\n *\n * e.g. disallow access to OffscreenCanvas without\n * environment.offscreenCanvas=true present.\n *\n * Must be called after @start.\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n applySandboxRequirements(requirements) {\n var ceci = this;\n \n return new Promise(function sandbox$applySandboxRequirementsPromise(resolve, reject) {\n const message = {\n requirements,\n request: 'applyRequirements',\n };\n let wereRequirementsApplied = false;\n\n const successCb = ceci.ee.once(\n 'applyRequirementsDone',\n function sandbox$applyRequirements$success() {\n wereRequirementsApplied = true;\n resolve();\n },\n );\n\n assert(typeof message.requirements === 'object');\n ceci.evaluatorHandle.postMessage(message);\n\n setTimeout(function sandbox$finishApplySandboxRequirements() {\n if (!wereRequirementsApplied) {\n ceci.ee.removeListener('applyRequirementsDone', successCb);\n ceci.terminate(false);\n reject(\n new Error(\n 'applyRequirements never received applyRequirementsDone response from sandbox',\n ),\n );\n }\n }, 3000 * timeDilation); /* XXXwg needs tunable */\n });\n }\n\n /**\n * Executes a slice received from the supervisor.\n * Must be called after @start.\n *\n * @param {Slice} slice - bare minimum data required for the job/job code to be executed on\n * @param {number} [delay = 0] the delay that this method should wait before beginning work, used to avoid starting all sandboxes at once\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n\n async work (slice, delay = 0) {\n var ceci = this;\n\n if (!ceci.isAssigned) {\n throw new Error(\"Sandbox.run: Sandbox is not ready to work, state=\" + ceci.state);\n }\n\n ceci.state = WORKING;\n ceci.slice = slice;\n assert(slice);\n\n // cf. DCP-1720\n this.resetSliceTimeReport();\n \n // Now wait for the delay if provided, prevents many sandboxes starting at once from crashing the supervisor\n if (delay > 0) await new Promise(resolve => setTimeout(resolve, (delay + 1) * timeDilation));\n if (!ceci.isWorking) return; // sandbox.terminate could have been called during the delay timeout\n\n // Prepare the sandbox to begin work\n // will be replaced by `assign` message that should be called before emitting a `work` message\n if (ceci.jobAddress !== slice.jobAddress) {\n throw new Error(`Sandbox.run: Sandbox is already assigned and jobAddress doesn't match previous (${ceci.jobAddress} !== ${slice.jobAddress})`);\n }\n\n let sliceHnd = { job: ceci.public, sandbox: ceci };\n await ceci.resetSandboxState();\n if (!ceci.slice) {\n console.error(`Slice for job ${ceci.jobAddress} vanished during work initialization - aborting`);\n return;\n }\n\n let inputDatum;\n let dataError = false;\n try {\n inputDatum = await fetchURI(\n ceci.slice.datumUri,\n this.allowedOrigins,\n dcpConfig.worker.allowOrigins.fetchData,\n );\n } catch (err) {\n dataError = err;\n dataError.errorCode = 'EUNCAUGHTERROR'\n ceci.emit('workEmit', {\n eventName: 'error',\n payload: {\n message: dataError.message,\n stack:dataError.stack,\n name: ceci.public.name\n }\n });\n }\n\n debugging('sandbox') && debug(`Fetched datum: ${inputDatum}`);\n\n if (!ceci.slice) {\n console.error(`Slice for job ${ceci.jobAddress} vanished after data fetch - aborting`);\n return;\n }\n\n ceci.resetProgressTimeout();\n ceci.resetSliceTimeout();\n\n return new Promise(function sandbox$$workPromise(resolve, reject) {\n let onSuccess, onFail\n\n onSuccess = ceci.ee.once('resolve', function sandbox$$work$success (event) {\n ceci.ee.removeListener('reject', onFail)\n resolve(event)\n }.bind(ceci))\n\n onFail = ceci.ee.once('reject', function sandbox$$work$fail (err) {\n ceci.ee.removeListener('resolve', onSuccess)\n reject(err)\n }.bind(ceci))\n\n ceci.sliceStartTime = Date.now();\n ceci.progress = null;\n ceci.progressReports = {\n last: undefined,\n lastDeterministic: undefined,\n };\n\n ceci.resetProgressTimeout();\n ceci.resetSliceTimeout();\n ceci.emit('sliceStart', sliceHnd); /** @todo: remove */\n ceci.emit('start', sliceHnd);\n \n if(dataError){\n ceci.ee.removeListener('resolve', onSuccess);\n ceci.ee.removeListener('reject', onFail);\n setTimeout(() => reject(dataError), 0)\n\n } else {\n // Try to only pass the data required - keeps the message size low\n // and prevents sandbox code from snooping on internal data \n if(!DCP_ENV.isBrowserPlatform) {\n inputDatum = scopedKvin.marshal(inputDatum);\n }\n ceci.evaluatorHandle.postMessage({\n request: 'main',\n data: inputDatum,\n })\n }\n })\n .then(async function sandbox$$work$then(event) {\n // Ceci is the complete callback when the slice completes\n // prevent any hanging timers from being fired\n await ceci.clearSandboxTimers();\n\n // TODO: Should sliceHnd just be replaced with ceci.public?\n ceci.emit('slice', sliceHnd); /** @todo: decide which event is right */\n ceci.emit('sliceFinish', event);\n ceci.emit('complete', event);\n\n ceci.changeWorkingToAssigned();\n ceci.slice = false;\n return event;\n })\n .catch((err) => {\n // Ceci is the reject callback for when the slice throws an error\n ceci.terminate(false);\n\n ceci.emit('error', err, 'slice');\n ceci.emit('sliceError', sliceHnd, err); /** @todo: remove */\n\n if (err instanceof NoProgressError) {\n ceci.emit('workEmit', {\n eventName: 'noProgress',\n payload: {\n timestamp: Date.now() - ceci.sliceStartTime,\n data: ceci.slice.data, /** XXXpfr @todo slice.data is legacy from v3; figure out what to put here. */\n progressReports: ceci.progressReports,\n }\n });\n }\n throw err;\n })\n .finally(function sandbox$$work$finally() {\n ceci.emit('sliceEnd', sliceHnd); /** @todo: remove */\n ceci.emit('end', sliceHnd);\n });\n }\n\n resetProgressTimeout() {\n if (this.progressTimeout) {\n clearTimeout(this.progressTimeout);\n this.progressTimeout = null;\n }\n\n this.progressTimeout = setTimeout(() => {\n if (this.options.ignoreNoProgress) {\n return console.warn(\"ENOPROGRESS silenced by localExec: In a remote worker, this slice would be stopped for not calling progress frequently enough.\");\n }\n\n this.ee.emit('reject', new NoProgressError(`No progress event was received in the last ${dcpConfig.worker.sandbox.progressTimeout / 1000} seconds.`));\n }, +dcpConfig.worker.sandbox.progressTimeout * timeDilation);\n }\n\n resetSliceTimeout() {\n if (this.sliceTimeout) clearTimeout(this.sliceTimeout);\n\n this.sliceTimeout = setTimeout(() => {\n if (Sandbox.debugWork) return console.warn(\"Sandbox.debugWork: Ignoring slice timeout\");\n\n this.ee.emit('reject', new SliceTooSlowError(`Slice took longer than ${dcpConfig.worker.sandbox.sliceTimeout / 1000} seconds.`));\n }, +dcpConfig.worker.sandbox.sliceTimeout * timeDilation);\n }\n \n async handleRing0Message(data) {\n debugging('event:ring-0') && debug('event:ring-0', data);\n //handling a true ring 0 message\n switch (data.request) {\n case 'sandboxLoaded':\n // emit externally\n this.emit('sandboxLoaded', this)\n this.emit('workerLoaded', this) /** @todo: remove */\n break;\n\n case 'scriptLoaded':\n // emit externally\n this.emit('scriptLoaded', data);\n \n if(data.result !== \"success\") {\n this.onerror(data);\n }\n break;\n \n case 'clearTimersDone':\n this.ee.emit(data.request, data);\n break;\n case 'measurement':\n this.updateTime(data);\n break;\n case 'error':\n // Warning: rejecting here with just event.data.error causes issues\n // where the reject handlers modify the object so it interferes with the\n // workEmit event payload, wrapping in an Error instance copies the values\n let e = new Error(\n data.error.message,\n data.error.fileName,\n data.error.lineNumber);\n e.stack = data.error.stack;\n e.name = data.error.name;\n \n if (this.ee.listenerCount('reject') > 0) {\n this.ee.emit('reject', e);\n } else {\n // This will happen if the error is thrown during initialization\n throw e;\n }\n\n break;\n default:\n let error = new Error('Received unhandled request from sandbox: ' + data.request + '\\n\\t' + JSON.stringify(data));\n console.error(error);\n break; \n }\n }\n\n async handleRing1Message(data) {\n switch (data.request) {\n case 'applyRequirementsDone':\n // emit internally\n this.ee.emit(data.request, data)\n break;\n default:\n let error = new Error('Received unhandled request from sandbox ring 1: ' + data.request + '\\n\\t' + JSON.stringify(data));\n console.error(error)\n break; \n }\n }\n\n async handleRing2Message(data) {\n debugging('event:ring-2') && debug('event:ring-2', data);\n switch (data.request) {\n case 'dependency': {\n let moduleData;\n try {\n moduleData = await this.supervisorCache.fetchModule(data.data);\n } catch (error) {\n /*\n * In the event of an error here, we want to let the client know there was a problem in\n * loading their module. However, there hasn't yet been an actual slice assigned to the sandbox.\n * Therefore, we assign 'slice 0' to the sandbox, a slice that will never exist, and is used\n * purely for this purpose. \n */\n this.slice = {\n jobAddress: this.jobAddress,\n sliceNumber: 0,\n };\n\n const payload = {\n name: error.name,\n timestamp: error.timestamp,\n message: error.message,\n };\n\n this.emit('workEmit', {\n eventName: 'error',\n payload,\n });\n this.ee.emit('reject', error);\n break;\n }\n this.evaluatorHandle.postMessage({\n request: 'moduleGroup',\n data: moduleData,\n id: data.id,\n });\n break;\n }\n case 'error':\n /*\n * Ring 2 error messages will only fire for problems inside of the worker that are separate from\n * the work function. In most cases there are other handlers for situations where 'error' may be emitted\n * such as timeouts if the expected message isn't recieved. Thus, we will output the error, but nothing else.\n */\n console.error(data.error);\n break;\n case 'describe':\n case 'evalResult':\n case 'resetStateDone':\n case 'assigned':\n // emit internally\n this.ee.emit(data.request, data);\n break;\n case 'reject':\n // emit internally\n this.ee.emit(data.request, data.error);\n break;\n default: {\n const error = new Error(\n `Received unhandled request from sandbox ring 2. Data: ${JSON.stringify(\n data,\n null,\n 2,\n )}`,\n );\n console.error(error);\n break;\n }\n }\n }\n\n async handleRing3Message(data) {\n switch (data.request) {\n case 'complete':\n clearTimeout(this.progressTimeout);\n clearTimeout(this.sliceTimeout);\n this.progressTimeout = this.sliceTimeout = null;\n\n if (this.progress === null) {\n if (this.options.ignoreNoProgress) {\n console.warn(\"ENOPROGRESS silenced by localExec: Progress was not called during this slice's execution, in a remote sandbox this would cause the slice to be failed\");\n } else {\n // If a progress update was never received (progress === null) then reject\n this.ee.emit('reject', new NoProgressError('Sandbox never emitted a progress event.'));\n break;\n }\n }\n this.progress = 100;\n data.timeReport = this.sliceTimeReport;\n this.ee.emit('resolve', data);\n break;\n case 'progress':\n let { progress, indeterminate, throttledReports, value } = data;\n this.progress = progress;\n const progressReport = {\n timestamp: Date.now() - this.sliceStartTime,\n progress,\n value,\n throttledReports,\n }\n this.progressReports.last = progressReport;\n if (!indeterminate) {\n this.progressReports.lastDeterministic = progressReport;\n }\n\n this.resetProgressTimeout();\n\n this.emit('sliceProgress', data);\n break;\n\n case 'noProgress':\n let { message } = data;\n\n this.ee.emit('reject', new NoProgressError(message));\n break;\n case 'console':\n if (DCP_ENV.isBrowserPlatform){\n data.payload.message = kvin.serialize(data.payload.message)\n }\n this.emit('workEmit', {\n eventName: 'console',\n payload: data.payload\n });\n break;\n\n case 'emitEvent':/* ad-hoc event from the sandbox (work.emit) */\n this.emit('workEmit', {\n eventName: 'custom',\n payload: data.payload\n })\n break;\n case 'measurement':\n this.updateTime(data);\n break;\n case 'workError': {\n this.emit('workEmit', {\n eventName: 'error',\n payload: data.error,\n });\n\n // Warning: rejecting here with just .data.error causes issues\n // where the reject handlers modify the object so it interferes with the\n // workEmit payload, wrapping in an Error instance copies the values\n const wrappedError = new UncaughtExceptionError(\n data.error.message,\n data.error.fileName,\n data.error.lineNumber,\n );\n wrappedError.stack = data.error.stack;\n wrappedError.name = data.error.name;\n\n if (this.ee.listenerCount('reject') > 0) {\n this.ee.emit('reject', wrappedError);\n } else {\n // This will happen if the error is thrown during initialization\n throw wrappedError;\n }\n break;\n }\n default:\n let error = new Error('Received unhandled request from sandbox ring 3: ' + data.request + '\\n\\t' + JSON.stringify(data));\n console.error(error)\n break; \n }\n }\n\n /**\n * Handles progress and completion events from sandbox.\n * Unless explicitly returned out of this function will re-emit the event\n * on @this.ee where the name of the event is event.data.request.\n *\n * @param {object} event - event received from the sandbox\n */\n async onmessage(event) {\n debugging('event') && debug('event', event);\n if (Sandbox.debugEvents) {\n console.debug('sandbox - eventDebug:', {\n id: this.id,\n state: this.state,\n event: JSON.stringify(event)\n })\n }\n\n const { data } = event;\n const ringLevel = data.ringSource\n\n // Give the data to a handler depending on ring level\n if (ringLevel === -1) {\n console.error('Message sent directly from raw postMessage. Terminating worker...');\n console.debug(event);\n return this.terminate(true);\n } else {\n const handler = this.ringMessageHandlers[ringLevel];\n if (handler) {\n handler.call(this, data.value);\n } else {\n console.warn(`No handler defined for message from ring ${ringLevel}`);\n console.debug(event);\n }\n }\n }\n\n /**\n * Error handler for the internal sandbox.\n * Currently just logs the errors that the sandbox spits out.\n */\n onerror(event) {\n console.error('Sandbox emitted an error:', event);\n this.terminate(true, true);\n }\n\n /**\n * Clears the timeout and terminates the sandbox.\n *\n * @param {boolean} [reject = true] - if true call reject\n * @param {boolean} [immediate = false] - passed to terminate, used by standaloneWorker to immediately close the connection\n */\n terminate (reject = true, immediate = false) {\n this.state = TERMINATED\n\n clearTimeout(this.progressTimeout);\n clearTimeout(this.sliceTimeout);\n this.progressTimeout = this.sliceTimeout = null;\n \n if (this.evaluatorHandle && typeof this.evaluatorHandle.terminate === 'function') {\n try {\n this.evaluatorHandle.terminate(immediate);\n this.evaluatorHandle = null;\n } catch (e) {\n console.error(\"Sandbox.terminate: Encountered an error while terminating the sandbox:\", e);\n } finally {\n this.emit('terminate', this);\n this.emit('workerStop', this); /** @todo: remove */\n }\n }\n\n if (reject) {\n this.ee.emit('reject', new Error(`Sandbox was terminated or timed out.`));\n }\n }\n\n /**\n * Attempts to stop the sandbox from doing completing its current\n * set of work without terminating the working.\n */\n stop () {\n throw new Error('Sandbox.stop is not yet implemented.')\n }\n\n /**\n * ringNPostMessage can send a `measurement` request and update these\n * totals.\n */\n updateTime (measurementEvent) {\n ['total', 'idle', 'webGL'].forEach((key) => {\n if (measurementEvent[key]) this.sliceTimeReport[key] += measurementEvent[key];\n })\n }\n\n resetSliceTimeReport () {\n this.sliceTimeReport = {\n total: 0,\n idle: 0,\n webGL: 0,\n }\n }\n}\n\nSandbox.idCounter = 1;\nSandbox.debugWork = false;\nSandbox.debugState = false;\nSandbox.debugEvents = false;\n\nexports.Sandbox = Sandbox;\nexports.SandboxError = SandboxError;\nexports.NoProgressError = NoProgressError;\nexports.SliceTooSlowError = SliceTooSlowError;\nexports.UncaughtExceptionError = UncaughtExceptionError;\n\n\n//# sourceURL=webpack:///./src/dcp-client/worker/sandbox.js?");
|
|
5371
|
+
eval("// NOTE - need timeout/postmessage function\n/**\n * @file dcp-client/worker/sandbox.js\n *\n * A sandbox that when constructed and assigned can do work for\n * a distributed slice. A sandbox runs for a single slice at a time.\n *\n * Usage:\n * let sandbox = new Sandbox()\n * await sandbox.start()\n * let results = await sandbox.run(slice)\n *\n * Debug flags:\n * Sandbox.debugWork = true // - turns off 30 second timeout to let user debug sandbox innards more easily\n * Sandbox.debugState = true // - logs all state transitions for this sandbox\n * Sandbox.debugEvents = true // - logs all events received from the sandbox\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * Ryan Rossiter, ryan@kingsds.network\n * @date May 2019\n * @module sandbox\n */\n/* global dcpConfig */\n// @ts-check\n\n\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('worker');\nconst { assert, assertEq3 } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\nconst scopedKvin = new kvin.KVIN({Object: ({}).constructor,\n Array: ([]).constructor, \n Function: (()=>{}).constructor});\n\nlet timeDilation = 1;\nif (DCP_ENV.platform === 'nodejs') {\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n /** Make timers 10x slower when running in niim */\n timeDilation = (requireNative('module')._cache.niim instanceof requireNative('module').Module) ? 10 : 1;\n}\n\n/**\n * Wraps console.debug to emulate debug module prefixing messages on npm.\n * @param {...any} args\n */\nconst debug = (...args) => {\n if (debugging()) {\n console.debug('dcp-client:worker:sandbox', ...args);\n }\n};\n\nconst nanoid = __webpack_require__(/*! nanoid/non-secure */ \"./node_modules/nanoid/non-secure/index.js\");\n\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { fetchURI, encodeDataURI } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n\n// Sandbox states\nconst UNREADY = 'UNREADY' // No Sandbox (web worker, saworker, etc) has been constructed yet\nconst READYING = 'READYING' // Sandbox is being constructed and environment (bravojs, env) is being set up\nconst READY_FOR_ASSIGN = 'READY_FOR_ASSIGN' // Sandbox is ready to be assigned\nconst ASSIGNING = 'ASSIGNING' // Sandbox is running through assigning steps\nconst ASSIGNED = 'ASSIGNED' // Sandbox is assigned but not working\nconst WORKING = 'WORKING' // Sandbox is working\nconst TERMINATED = 'TERMINATED'\nconst EVAL_RESULT_PREFIX = 'evalResult::';\n\nclass SandboxError extends Error {}\nclass NoProgressError extends SandboxError { constructor(...args) { super(...args); this.errorCode = 'ENOPROGRESS'; } }\nclass SliceTooSlowError extends SandboxError { constructor(...args) { super(...args); this.errorCode = 'ESLICETOOSLOW'; } }\nclass UncaughtExceptionError extends SandboxError { constructor(...args) { super(...args); this.errorCode = 'EUNCAUGHTERROR'; } }\n\n/** @typedef {import('dcp/common/dcp-events').EventEmitter} EventEmitter */\n/** @typedef {import('./slice').Slice} Slice */\n/** @typedef {import('./supervisor-cache').SupervisorCache} SupervisorCache */\n/** @typedef {*} opaqueId */\n\n/**\n * @access public\n * @typedef {object} SandboxOptions\n * @constructor {function} [SandboxConstructor]\n * @property {boolean} [ignoreNoProgress] - When true, the sandbox will not be stopped for not calling progress\n */\n\nclass Sandbox extends EventEmitter {\n /**\n * A Sandbox (i.e. a worker sandbox) which executes distributed slices.\n *\n * @constructor\n * @param {SupervisorCache} cache\n * @param {SandboxOptions} options\n */\n constructor (cache, options, origins) {\n super('Sandbox');\n /** @type {SupervisorCache} */\n this.supervisorCache = cache;\n /** @type {SandboxOptions} */\n this.options = {\n ignoreNoProgress: false,\n ...options,\n SandboxConstructor: options.SandboxConstructor ||\n __webpack_require__(/*! ./evaluators */ \"./src/dcp-client/worker/evaluators/index.js\").BrowserEvaluator,\n }\n this.allowedOrigins = origins;\n\n /** @type {opaqueId} */\n this.jobAddress = null;\n /** @type {object} */\n this.evaluatorHandle = null;\n /** @type {object} */\n this.capabilities = null;\n /** @type {EventEmitter} */\n this.ee = new EventEmitter('SandboxInternal')\n\n /** @type {string} */\n this._state = UNREADY;\n /** @type {boolean} */\n this.allocated = false;\n /** @type {number?} */\n this.progress = 100;\n /** @type {object} */\n this.progressReports = null;\n /** @type {object} */\n this.progressTimeout = null;\n /** @type {object} */\n this.sliceTimeout = null;\n\n /** @type {Slice} */\n this.slice = null;\n\n /** @type {number?} */\n this.started = null;\n /** @type {number?} */\n this.sliceStartTime = null;\n /** @type {boolean} */\n this.requiresGPU = false;\n /** @type {string|URL} */\n this.packageURL = dcpConfig.packageManager.location\n /** @type {number?} */\n this.id = Sandbox.getNewId();\n\n this.ringMessageHandlers = [\n this.handleRing0Message,\n this.handleRing1Message,\n this.handleRing2Message,\n this.handleRing3Message,\n ];\n\n this.resetSliceTimeReport();\n }\n\n static getNewId() {\n return Sandbox.idCounter++;\n }\n\n get state () {\n return this._state\n }\n\n set state (value) {\n if (Sandbox.debugState) {\n console.debug(`sandbox - changing state of ${this.id}... ${this._state} -> ${value}`)\n }\n\n if (this.state === TERMINATED && value !== TERMINATED) {\n // For safety!\n throw new Error(`Sandbox set state violation, attepted to change state from ${this.state} to ${value}`);\n }\n\n this._state = value;\n }\n\n get isReadyForAssign () {\n return this.state === READY_FOR_ASSIGN;\n }\n\n get isAssigned () {\n return this.state === ASSIGNED;\n }\n\n get isWorking () {\n return this.state === WORKING;\n }\n\n get isTerminated () {\n return this.state === TERMINATED;\n }\n\n changeWorkingToAssigned () {\n if (this.isWorking)\n this.state = ASSIGNED;\n }\n\n setIsAssigning () {\n this.state = ASSIGNING;\n }\n\n /**\n * Readies the sandbox. This will result in the sandbox being ready and not assigned,\n * it will need to be assigned with a job before it is able to do work.\n *\n * @todo maybe preload specific modules or let the cache pass in what modules to load?\n * @throws on failure to ready\n */\n async start(delay = 0) {\n this.started = Date.now();\n this.state = READYING;\n\n if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay * timeDilation));\n\n try {\n // RING 0\n this.evaluatorHandle = new this.options.SandboxConstructor({\n name: `DCP Sandbox #${this.id}`,\n });\n this.evaluatorHandle.onmessage = this.onmessage.bind(this);\n this.evaluatorHandle.onerror = this.onerror.bind(this);\n\n // Now in RING 1\n\n // Now in RING 2\n await this.describe();\n this.state = READY_FOR_ASSIGN;\n this.emit('ready', this);\n } catch (error) {\n console.warn('Failed to start the sandbox -', error.message);\n this.terminate(false);\n throw error;\n }\n }\n\n /**\n * This will assign the sandbox with a job, loading its sandbox code\n * into the sandbox.\n *\n * @param {string} jobAddress The address of the job to assign to\n * @throws on initialization failure\n */\n async assign(jobAddress) {\n this.jobAddress = jobAddress;\n this.job = await this.supervisorCache.fetchJob(jobAddress, this.allowedOrigins);\n\n assertEq3(this.job.address, jobAddress);\n assert(typeof this.job === 'object');\n assert(typeof this.job.requirements === 'object');\n assert(Array.isArray(this.job.dependencies));\n assert(Array.isArray(this.job.requirePath));\n\n // Extract public data from job, with defaults\n this.public = Object.assign({\n name: `Anonymous Job ${this.job.address.slice(0, 6)}`,\n description: 'Discreetly helping make the world smarter.',\n link: 'https://distributed.computer/about',\n }, this.job.public);\n\n // Future: We may want other filename tags for appliances // RR Nov 2019\n\n // Important: The order of applying requirements before loading the sandbox code\n // is important for modules and sandbox code to set globals over the whitelist.\n await this.applySandboxRequirements(this.job.requirements);\n await this.assignEvaluator();\n this.state = ASSIGNED;\n }\n\n async assignEvaluator() {\n debug('Begin assigning job to evaluator');\n const ceci = this;\n\n return new Promise(function sandbox$$assignEvaluatorPromise(resolve, reject) {\n if(!DCP_ENV.isBrowserPlatform) {\n if (!ceci.job.arguments._serializeVerId) {\n ceci.job.arguments = scopedKvin.marshal(ceci.job.arguments);\n }\n }\n \n const message = {\n request: 'assign',\n job: ceci.job,\n sandboxConfig: {\n worker: dcpConfig.worker,\n },\n };\n\n const onSuccess = (event) => {\n // eslint-disable-next-line no-use-before-define\n ceci.ee.removeListener('reject', onFailListener);\n ceci.emit('assigned', event.jobAddress);\n debug('Job assigned to evaluator');\n resolve();\n };\n\n const onFail = (error) => {\n // eslint-disable-next-line no-use-before-define\n ceci.ee.removeListener('assigned', onSuccessListener);\n reject(error);\n };\n\n const onSuccessListener = ceci.ee.once('assigned', onSuccess);\n const onFailListener = ceci.ee.once('reject', onFail);\n ceci.evaluatorHandle.postMessage(message);\n });\n }\n\n /**\n * Evaluates a string inside the sandbox.\n *\n * @param {string} code - the code to evaluate in the sandbox\n * @param {string} filename - the name of the 'file' to help with debugging,\n * no longer working though?\n * @returns {Promise} - resolves with eval result on success, rejects\n * otherwise\n */\n eval(code, filename) {\n var ceci = this;\n \n return new Promise(function sandbox$$eval$Promise(resolve, reject) {\n let msgId = nanoid();\n let msg = {\n request: 'eval',\n data: code,\n filename,\n msgId, \n }\n\n const eventId = EVAL_RESULT_PREFIX + msgId;\n\n let onSuccess = (event) => {\n ceci.ee.removeListener('reject', onFailListener)\n resolve(event)\n }\n\n let onFail = (event) => {\n ceci.ee.removeListener(eventId, onSuccessListener)\n reject(event)\n }\n\n let onSuccessListener = ceci.ee.once(eventId, onSuccess);\n let onFailListener = ceci.ee.once('reject', onFail)\n\n ceci.evaluatorHandle.postMessage(msg)\n })\n }\n\n /**\n * Resets the state of the bootstrap, without resetting the sandbox function if assigned.\n * Mostly used to reset the progress status before reusing a sandbox on another slice.\n * Must be called after @start.\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n resetSandboxState () {\n var ceci = this;\n\n return new Promise(function sandbox$resetSandboxStatePromise(resolve, reject) {\n let successCb, failTimeout;\n let msg = {\n request: 'resetState',\n };\n\n successCb = ceci.ee.once('resetStateDone', function sandbox$resetSandboxState$success () {\n if (failTimeout === false)\n return; /* already rejected */\n clearTimeout(failTimeout);\n failTimeout = false;\n resolve();\n });\n\n failTimeout = setTimeout(function sandbox$resetSandboxState$fail() {\n if (failTimeout === false)\n return; /* already resolved */\n \n ceci.ee.removeListener('resetStateDone', successCb);\n ceci.terminate(false);\n failTimeout = false;\n\n reject(new Error('resetState never received resetStateDone event from sandbox'));\n }, 3000 * timeDilation); /* XXXwg need tuneable */\n\n assert(ceci.evaluatorHandle); // It is possible that ceci.terminate nulls out evaluatorHandle before getting here.\n ceci.evaluatorHandle.postMessage(msg);\n });\n }\n\n /**\n * Clear all timers that are set inside the sandbox (evaluator) environment.\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n clearSandboxTimers() {\n var ceci = this;\n \n return new Promise(function sandbox$clearSandboxTimersPromise(resolve, reject) {\n let successCb, failTimeout;\n let msg = {\n request: 'clearTimers',\n };\n\n successCb = ceci.ee.once('clearTimersDone', function sandbox$clearSandboxTimers$success() {\n if (failTimeout === false)\n return; /* already rejected */\n clearTimeout(failTimeout);\n failTimeout = false;\n resolve();\n });\n\n failTimeout = setTimeout(function sanbox$clearSandboxTimers$fail() {\n if (failTimeout === false)\n return; /* already resolved */\n \n ceci.ee.removeListener('clearTimersDone', successCb);\n ceci.terminate(false);\n failTimeout = false;\n \n reject(new Error('clearTimers never received clearTimersDone event from sandbox'));\n }, 3000 * timeDilation); /* XXXwg need tuneable */\n\n if (ceci.evaluatorHandle) // Sometimes ceci.terminate nulls out evaluatorHandle before getting here.\n ceci.evaluatorHandle.postMessage(msg);\n });\n }\n\n /**\n * Sends a post message to describe its capabilities.\n *\n * Side effect: Sets the capabilities property of the current sandbox.\n *\n * @returns {Promise} Resolves with the sandbox's capabilities. Rejects with\n * an error saying a response was not received.\n * @memberof Sandbox\n */\n describe() {\n debug('Beginning to describe evaluator');\n var ceci = this;\n \n return new Promise(function sandbox$describePromise(resolve, reject) {\n if (ceci.evaluatorHandle === null) {\n return reject(new Error('Evaluator has not been initialized.'));\n }\n\n /**\n * Opted to create a flag for the describe response being received so that\n * we don't have to *hoist* the timeout's id to clear it in the response\n * handler.\n */\n let didReceiveDescribeResponse = false;\n const describeResponseHandler = ceci.ee.once('describe', (data) => {\n didReceiveDescribeResponse = true;\n const { capabilities } = data;\n if (typeof capabilities === 'undefined') {\n reject(\n new Error('Did not receive capabilities from describe response.'),\n );\n }\n ceci.capabilities = capabilities;\n\n // Currently only used in tests. May use the event in the future.\n ceci.emit('described', capabilities);\n debug('Evaluator has been described');\n resolve(capabilities);\n });\n const describeResponseFailedHandler = () => {\n if (!didReceiveDescribeResponse) {\n ceci.ee.removeListener('describe', describeResponseHandler);\n ceci.terminate(false);\n reject(\n new Error(\n 'Describe message timed-out. No describe response was received from the describe command.',\n ),\n );\n }\n };\n\n const message = {\n request: 'describe',\n };\n\n // Arbitrarily set the waiting time.\n setTimeout(describeResponseFailedHandler, 6000 * timeDilation); /* XXXwg need tuneable */\n assert(ceci.evaluatorHandle); // It is possible that ceci.terminate nulls out evaluatorHandle before getting here.\n ceci.evaluatorHandle.postMessage(message);\n });\n }\n\n /**\n * Passes the job's requirements object into the sandbox so that the global\n * access lists can be updated accordingly.\n *\n * e.g. disallow access to OffscreenCanvas without\n * environment.offscreenCanvas=true present.\n *\n * Must be called after @start.\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n applySandboxRequirements(requirements) {\n var ceci = this;\n \n return new Promise(function sandbox$applySandboxRequirementsPromise(resolve, reject) {\n const message = {\n requirements,\n request: 'applyRequirements',\n };\n let wereRequirementsApplied = false;\n\n const successCb = ceci.ee.once(\n 'applyRequirementsDone',\n function sandbox$applyRequirements$success() {\n wereRequirementsApplied = true;\n resolve();\n },\n );\n\n assert(typeof message.requirements === 'object');\n ceci.evaluatorHandle.postMessage(message);\n\n setTimeout(function sandbox$finishApplySandboxRequirements() {\n if (!wereRequirementsApplied) {\n ceci.ee.removeListener('applyRequirementsDone', successCb);\n ceci.terminate(false);\n reject(\n new Error(\n 'applyRequirements never received applyRequirementsDone response from sandbox',\n ),\n );\n }\n }, 3000 * timeDilation); /* XXXwg needs tunable */\n });\n }\n\n /**\n * Executes a slice received from the supervisor.\n * Must be called after @start.\n *\n * @param {Slice} slice - bare minimum data required for the job/job code to be executed on\n * @param {number} [delay = 0] the delay that this method should wait before beginning work, used to avoid starting all sandboxes at once\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n\n async work (slice, delay = 0) {\n var ceci = this;\n\n if (!ceci.isAssigned) {\n throw new Error(\"Sandbox.run: Sandbox is not ready to work, state=\" + ceci.state);\n }\n\n ceci.state = WORKING;\n ceci.slice = slice;\n assert(slice);\n\n // cf. DCP-1720\n this.resetSliceTimeReport();\n \n // Now wait for the delay if provided, prevents many sandboxes starting at once from crashing the supervisor\n if (delay > 0) await new Promise(resolve => setTimeout(resolve, (delay + 1) * timeDilation));\n if (!ceci.isWorking) return; // sandbox.terminate could have been called during the delay timeout\n\n // Prepare the sandbox to begin work\n // will be replaced by `assign` message that should be called before emitting a `work` message\n if (ceci.jobAddress !== slice.jobAddress) {\n throw new Error(`Sandbox.run: Sandbox is already assigned and jobAddress doesn't match previous (${ceci.jobAddress} !== ${slice.jobAddress})`);\n }\n\n let sliceHnd = { job: ceci.public, sandbox: ceci };\n await ceci.resetSandboxState();\n if (!ceci.slice) {\n console.error(`Slice for job ${ceci.jobAddress} vanished during work initialization - aborting`);\n return;\n }\n\n let inputDatum;\n let dataError = false;\n try {\n inputDatum = await fetchURI(\n ceci.slice.datumUri,\n this.allowedOrigins,\n dcpConfig.worker.allowOrigins.fetchData,\n );\n } catch (err) {\n dataError = err;\n dataError.errorCode = 'EUNCAUGHTERROR'\n ceci.emit('workEmit', {\n eventName: 'error',\n payload: {\n message: dataError.message,\n stack:dataError.stack,\n name: ceci.public.name\n }\n });\n }\n\n debugging('sandbox') && debug(`Fetched datum: ${inputDatum}`);\n\n if (!ceci.slice) {\n console.error(`Slice for job ${ceci.jobAddress} vanished after data fetch - aborting`);\n return;\n }\n\n ceci.resetProgressTimeout();\n ceci.resetSliceTimeout();\n\n return new Promise(function sandbox$$workPromise(resolve, reject) {\n let onSuccess, onFail\n\n onSuccess = ceci.ee.once('resolve', function sandbox$$work$success (event) {\n ceci.ee.removeListener('reject', onFail)\n resolve(event)\n }.bind(ceci))\n\n onFail = ceci.ee.once('reject', function sandbox$$work$fail (err) {\n ceci.ee.removeListener('resolve', onSuccess)\n reject(err)\n }.bind(ceci))\n\n ceci.sliceStartTime = Date.now();\n ceci.progress = null;\n ceci.progressReports = {\n last: undefined,\n lastDeterministic: undefined,\n };\n\n ceci.resetProgressTimeout();\n ceci.resetSliceTimeout();\n ceci.emit('sliceStart', sliceHnd); /** @todo: remove */\n ceci.emit('start', sliceHnd);\n \n if(dataError){\n ceci.ee.removeListener('resolve', onSuccess);\n ceci.ee.removeListener('reject', onFail);\n setTimeout(() => reject(dataError), 0)\n\n } else {\n // Try to only pass the data required - keeps the message size low\n // and prevents sandbox code from snooping on internal data \n if(!DCP_ENV.isBrowserPlatform) {\n inputDatum = scopedKvin.marshal(inputDatum);\n }\n ceci.evaluatorHandle.postMessage({\n request: 'main',\n data: inputDatum,\n })\n }\n })\n .then(async function sandbox$$work$then(event) {\n // Ceci is the complete callback when the slice completes\n // prevent any hanging timers from being fired\n await ceci.clearSandboxTimers();\n\n // TODO: Should sliceHnd just be replaced with ceci.public?\n ceci.emit('slice', sliceHnd); /** @todo: decide which event is right */\n ceci.emit('sliceFinish', event);\n ceci.emit('complete', event);\n\n ceci.changeWorkingToAssigned();\n ceci.slice = false;\n return event;\n })\n .catch((err) => {\n // Ceci is the reject callback for when the slice throws an error\n ceci.terminate(false);\n\n ceci.emit('error', err, 'slice');\n ceci.emit('sliceError', sliceHnd, err); /** @todo: remove */\n\n if (err instanceof NoProgressError) {\n ceci.emit('workEmit', {\n eventName: 'noProgress',\n payload: {\n timestamp: Date.now() - ceci.sliceStartTime,\n data: ceci.slice.data, /** XXXpfr @todo slice.data is legacy from v3; figure out what to put here. */\n progressReports: ceci.progressReports,\n }\n });\n }\n throw err;\n })\n .finally(function sandbox$$work$finally() {\n ceci.emit('sliceEnd', sliceHnd); /** @todo: remove */\n ceci.emit('end', sliceHnd);\n });\n }\n\n resetProgressTimeout() {\n if (this.progressTimeout) {\n clearTimeout(this.progressTimeout);\n this.progressTimeout = null;\n }\n\n this.progressTimeout = setTimeout(() => {\n if (this.options.ignoreNoProgress) {\n return console.warn(\"ENOPROGRESS silenced by localExec: In a remote worker, this slice would be stopped for not calling progress frequently enough.\");\n }\n\n this.ee.emit('reject', new NoProgressError(`No progress event was received in the last ${dcpConfig.worker.sandbox.progressTimeout / 1000} seconds.`));\n }, +dcpConfig.worker.sandbox.progressTimeout * timeDilation);\n }\n\n resetSliceTimeout() {\n if (this.sliceTimeout) clearTimeout(this.sliceTimeout);\n\n this.sliceTimeout = setTimeout(() => {\n if (Sandbox.debugWork) return console.warn(\"Sandbox.debugWork: Ignoring slice timeout\");\n\n this.ee.emit('reject', new SliceTooSlowError(`Slice took longer than ${dcpConfig.worker.sandbox.sliceTimeout / 1000} seconds.`));\n }, +dcpConfig.worker.sandbox.sliceTimeout * timeDilation);\n }\n \n async handleRing0Message(data) {\n debugging('event:ring-0') && debug('event:ring-0', data);\n //handling a true ring 0 message\n switch (data.request) {\n case 'sandboxLoaded':\n // emit externally\n this.emit('sandboxLoaded', this)\n this.emit('workerLoaded', this) /** @todo: remove */\n break;\n\n case 'scriptLoaded':\n // emit externally\n this.emit('scriptLoaded', data);\n \n if(data.result !== \"success\") {\n this.onerror(data);\n }\n break;\n \n case 'clearTimersDone':\n this.ee.emit(data.request, data);\n break;\n case 'measurement':\n this.updateTime(data);\n break;\n case 'error':\n // Warning: rejecting here with just event.data.error causes issues\n // where the reject handlers modify the object so it interferes with the\n // workEmit event payload, wrapping in an Error instance copies the values\n let e = new Error(\n data.error.message,\n data.error.fileName,\n data.error.lineNumber);\n e.stack = data.error.stack;\n e.name = data.error.name;\n \n if (this.ee.listenerCount('reject') > 0) {\n this.ee.emit('reject', e);\n } else {\n // This will happen if the error is thrown during initialization\n throw e;\n }\n\n break;\n default:\n let error = new Error('Received unhandled request from sandbox: ' + data.request + '\\n\\t' + JSON.stringify(data));\n console.error(error);\n break; \n }\n }\n\n async handleRing1Message(data) {\n switch (data.request) {\n case 'applyRequirementsDone':\n // emit internally\n this.ee.emit(data.request, data)\n break;\n default:\n let error = new Error('Received unhandled request from sandbox ring 1: ' + data.request + '\\n\\t' + JSON.stringify(data));\n console.error(error)\n break; \n }\n }\n\n async handleRing2Message(data) {\n debugging('event:ring-2') && debug('event:ring-2', data);\n switch (data.request) {\n case 'dependency': {\n let moduleData;\n try {\n moduleData = await this.supervisorCache.fetchModule(data.data);\n } catch (error) {\n /*\n * In the event of an error here, we want to let the client know there was a problem in\n * loading their module. However, there hasn't yet been an actual slice assigned to the sandbox.\n * Therefore, we assign 'slice 0' to the sandbox, a slice that will never exist, and is used\n * purely for this purpose. \n */\n this.slice = {\n jobAddress: this.jobAddress,\n sliceNumber: 0,\n };\n\n const payload = {\n name: error.name,\n timestamp: error.timestamp,\n message: error.message,\n };\n\n this.emit('workEmit', {\n eventName: 'error',\n payload,\n });\n this.ee.emit('reject', error);\n break;\n }\n this.evaluatorHandle.postMessage({\n request: 'moduleGroup',\n data: moduleData,\n id: data.id,\n });\n break;\n }\n case 'error':\n /*\n * Ring 2 error messages will only fire for problems inside of the worker that are separate from\n * the work function. In most cases there are other handlers for situations where 'error' may be emitted\n * such as timeouts if the expected message isn't recieved. Thus, we will output the error, but nothing else.\n */\n console.error(data.error);\n break;\n case 'describe':\n case 'evalResult':\n case 'resetStateDone':\n case 'assigned':\n // emit internally\n this.ee.emit(data.request, data);\n break;\n case 'reject':\n // emit internally\n this.ee.emit(data.request, data.error);\n break;\n default: {\n const error = new Error(\n `Received unhandled request from sandbox ring 2. Data: ${JSON.stringify(\n data,\n null,\n 2,\n )}`,\n );\n console.error(error);\n break;\n }\n }\n }\n\n async handleRing3Message(data) {\n switch (data.request) {\n case 'complete':\n clearTimeout(this.progressTimeout);\n clearTimeout(this.sliceTimeout);\n this.progressTimeout = this.sliceTimeout = null;\n\n if (this.progress === null) {\n if (this.options.ignoreNoProgress) {\n console.warn(\"ENOPROGRESS silenced by localExec: Progress was not called during this slice's execution, in a remote sandbox this would cause the slice to be failed\");\n } else {\n // If a progress update was never received (progress === null) then reject\n this.ee.emit('reject', new NoProgressError('Sandbox never emitted a progress event.'));\n break;\n }\n }\n this.progress = 100;\n data.timeReport = this.sliceTimeReport;\n this.ee.emit('resolve', data);\n break;\n case 'progress':\n let { progress, indeterminate, throttledReports, value } = data;\n this.progress = progress;\n const progressReport = {\n timestamp: Date.now() - this.sliceStartTime,\n progress,\n value,\n throttledReports,\n }\n this.progressReports.last = progressReport;\n if (!indeterminate) {\n this.progressReports.lastDeterministic = progressReport;\n }\n\n this.resetProgressTimeout();\n\n this.emit('sliceProgress', data);\n break;\n\n case 'noProgress':\n let { message } = data;\n\n this.ee.emit('reject', new NoProgressError(message));\n break;\n case 'console':\n if (DCP_ENV.isBrowserPlatform){\n data.payload.message = kvin.serialize(data.payload.message)\n }\n this.emit('workEmit', {\n eventName: 'console',\n payload: data.payload\n });\n break;\n\n case 'emitEvent':/* ad-hoc event from the sandbox (work.emit) */\n this.emit('workEmit', {\n eventName: 'custom',\n payload: data.payload\n })\n break;\n case 'measurement':\n this.updateTime(data);\n break;\n case 'workError': {\n this.emit('workEmit', {\n eventName: 'error',\n payload: data.error,\n });\n\n // Warning: rejecting here with just .data.error causes issues\n // where the reject handlers modify the object so it interferes with the\n // workEmit payload, wrapping in an Error instance copies the values\n const wrappedError = new UncaughtExceptionError(\n data.error.message,\n data.error.fileName,\n data.error.lineNumber,\n );\n wrappedError.stack = data.error.stack;\n wrappedError.name = data.error.name;\n\n if (this.ee.listenerCount('reject') > 0) {\n this.ee.emit('reject', wrappedError);\n } else {\n // This will happen if the error is thrown during initialization\n throw wrappedError;\n }\n break;\n }\n default:\n let error = new Error('Received unhandled request from sandbox ring 3: ' + data.request + '\\n\\t' + JSON.stringify(data));\n console.error(error)\n break; \n }\n }\n\n /**\n * Handles progress and completion events from sandbox.\n * Unless explicitly returned out of this function will re-emit the event\n * on @this.ee where the name of the event is event.data.request.\n *\n * @param {object} event - event received from the sandbox\n */\n async onmessage(event) {\n debugging('event') && debug('event', event);\n if (Sandbox.debugEvents) {\n console.debug('sandbox - eventDebug:', {\n id: this.id,\n state: this.state,\n event: JSON.stringify(event)\n })\n }\n\n const { data } = event;\n const ringLevel = data.ringSource\n\n // Give the data to a handler depending on ring level\n if (ringLevel === -1) {\n console.error('Message sent directly from raw postMessage. Terminating worker...');\n console.debug(event);\n return this.terminate(true);\n } else {\n const handler = this.ringMessageHandlers[ringLevel];\n if (handler) {\n handler.call(this, data.value);\n } else {\n console.warn(`No handler defined for message from ring ${ringLevel}`);\n console.debug(event);\n }\n }\n }\n\n /**\n * Error handler for the internal sandbox.\n * Currently just logs the errors that the sandbox spits out.\n */\n onerror(event) {\n console.error('Sandbox emitted an error:', event);\n this.terminate(true, true);\n }\n\n /**\n * Clears the timeout and terminates the sandbox.\n *\n * @param {boolean} [reject = true] - if true call reject\n * @param {boolean} [immediate = false] - passed to terminate, used by standaloneWorker to immediately close the connection\n */\n terminate (reject = true, immediate = false) {\n this.state = TERMINATED\n\n clearTimeout(this.progressTimeout);\n clearTimeout(this.sliceTimeout);\n this.progressTimeout = this.sliceTimeout = null;\n \n if (this.evaluatorHandle && typeof this.evaluatorHandle.terminate === 'function') {\n try {\n this.evaluatorHandle.terminate(immediate);\n this.evaluatorHandle = null;\n } catch (e) {\n console.error(\"Sandbox.terminate: Encountered an error while terminating the sandbox:\", e);\n } finally {\n this.emit('terminate', this);\n this.emit('workerStop', this); /** @todo: remove */\n }\n }\n\n if (reject) {\n this.ee.emit('reject', new Error(`Sandbox was terminated or timed out.`));\n }\n }\n\n /**\n * Attempts to stop the sandbox from doing completing its current\n * set of work without terminating the working.\n */\n stop () {\n throw new Error('Sandbox.stop is not yet implemented.')\n }\n\n /**\n * ringNPostMessage can send a `measurement` request and update these\n * totals.\n */\n updateTime (measurementEvent) {\n ['total', 'idle', 'webGL'].forEach((key) => {\n if (measurementEvent[key]) this.sliceTimeReport[key] += measurementEvent[key];\n })\n }\n\n resetSliceTimeReport () {\n this.sliceTimeReport = {\n total: 0,\n idle: 0,\n webGL: 0,\n }\n }\n}\n\nSandbox.idCounter = 1;\nSandbox.debugWork = false;\nSandbox.debugState = false;\nSandbox.debugEvents = false;\n\nexports.Sandbox = Sandbox;\nexports.SandboxError = SandboxError;\nexports.NoProgressError = NoProgressError;\nexports.SliceTooSlowError = SliceTooSlowError;\nexports.UncaughtExceptionError = UncaughtExceptionError;\n\n\n//# sourceURL=webpack:///./src/dcp-client/worker/sandbox.js?");
|
|
5372
5372
|
|
|
5373
5373
|
/***/ }),
|
|
5374
5374
|
|
|
@@ -5391,7 +5391,7 @@ eval("/**\n * @file worker/slice.js\n *\n * A wrapper for the slice object retur
|
|
|
5391
5391
|
/*! no static exports found */
|
|
5392
5392
|
/***/ (function(module, exports, __webpack_require__) {
|
|
5393
5393
|
|
|
5394
|
-
eval("/**\n * @file worker/supervisor-cache.js\n *\n * A cache for the supervisor, anything the supervisor\n * may request that is cacheable (the same every time its requested)\n * can be cached in this class.\n *\n * Currently only jobs and modules are cached.\n *\n * If a cached job is not accessed in tuning.jobCacheTTL ms\n * it will be invalidated.\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * @date May 2019\n */\n\n/* global dcpConfig */\n\nconst { justFetch, fetchURI } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\n\nconst tuning = dcpConfig.future('supervisor.tuning', {\n jobCacheTTL: 300, /* seconds */\n jobCacheCleanupInterval: 60, /* seconds */\n});\n\nclass SupervisorCache extends EventEmitter {\n constructor (supervisor) {\n var timer;\n \n super('SupervisorCache')\n this.supervisor = supervisor;\n this.cache = {\n job: {},\n module: {},\n dependency: {},\n }\n \n this.promises = {\n job: {},\n module: {},\n dependency: {},\n }\n\n this.lastAccess = {}\n for (let key in this.cache) {\n this.lastAccess[key] = {}\n }\n\n timer = setInterval(this.checkTTL, tuning.jobCacheCleanupInterval);\n if (timer.unref)\n timer.unref();\n }\n\n /**\n * Returns an object listing what jobs and modules are currently cached.\n * @returns {object} - in the form: { job: [0xgen1, 0xgen2,...], modules: [modGroup1, modGroup2,...] }\n */\n get cacheDescription () {\n let description = {}\n for (let key in this.cache) {\n description[key] = Object.keys(this.cache[key])\n }\n return description\n }\n\n /**\n * Returns an array of all jobId currently cached\n * @returns {Array} all the jobId's in the cache\n */\n get jobs () {\n return Object.keys(this.cache.job);\n }\n\n /**\n * Runs on an interval and checks to see if a cached object \n * should be invalidated because it hasn't been accessed recently.\n */\n checkTTL () {\n for (let key in this.lastAccess) {\n for (let address in this.lastAccess[key]) {\n let lastAccess = this.lastAccess[key][address]\n let now = Date.now()\n if (lastAccess + (1000 * tuning.jobCacheTTL) < now) { /* dcpConfig.supervisor.cacheTTL */\n delete this.cache[key][address]\n }\n }\n }\n }\n\n /**\n * Attempts to look up an item from the cache.\n * If item is found its last access time is updated.\n *\n * @param {string} key - the cache to look in (job or module)\n * @param {string} id - the items identifier (jobAddress or module group name)\n *\n * @returns {any} The value of the stored item or null if nothing is found\n */\n fetch(key, id) {\n if (typeof this.cache[key] === 'undefined') {\n throw new Error(`${key} does not relate to any cache.`);\n }\n if (this.cache[key][id]) {\n this.lastAccess[key][id] = Date.now();\n return this.cache[key][id];\n }\n return null;\n }\n\n /**\n * Stores a fetched value for one of the caches.\n *\n * @param {string} key - the cache to store the item in\n * @param {string} id - the items identifier (job Address or module group name)\n * @param {any} value - the item to store\n *\n * @returns {any} - @value passed in\n */\n store (key, id, value) {\n if (typeof this.cache[key] === 'undefined') {\n throw new Error(`${key} does not relate to any cache.`)\n }\n this.cache[key][id] = value\n this.lastAccess[key][id] = Date.now()\n return value\n }\n\n /** \n * Fetch a job from the job cache. If the job has components which are \n * which need to be fetched over the network, they are fetched before the\n * returned promise is resolved.\n *\n * The job cache is initially populated during fetchTask.\n */\n async fetchJob(address, allowedOrigins) {\n let job = this.fetch('job', address);\n\n if (!job) {\n let e = new Error(`No job in supervisor cache with address ${address}`);\n e.code = 'ENOJOB';\n throw e;\n }\n\n if (!job.workFunction) {\n job.workFunction = await fetchURI(job.codeLocation, allowedOrigins, dcpConfig.worker.allowOrigins.fetchWorkFunctions);\n delete job.codeLocation;\n }\n if (!job.arguments) {\n job.arguments = await fetchURI(job.argumentsLocation, dcpConfig.worker.allowOrigins.any, dcpConfig.worker.allowOrigins.fetchArguments);\n delete job.argumentsLocation;\n }\n \n return job;\n }\n\n /**\n * Attempts to fetch a module group from the cache and\n * if it's not found it attempts to fetch then store\n * the module group from the package manager.\n *\n * @param {array} modulesArray - the array of modules requested \n * - (when stringified it's the identifier of the module group)\n *\n * @returns {Promise<object>} - the module group\n * @throws when the module group can not be fetched\n */\n async fetchModule(modulesArray) {\n const cacheKey = JSON.stringify(modulesArray);\n let modules = this.fetch('module', cacheKey);\n if (modules !== null) {\n return modules;\n }\n\n if (this.promises.module[cacheKey]) {\n return this.promises.module[cacheKey];\n }\n\n const {\n success,\n payload: responsePayload,\n } = await this.supervisor.packageManagerConnection.send('fetchModule', {\n modules: modulesArray,\n });\n\n if (!success) {\n /**\n * Preserving the error message by not rewrapping it with DCPError incase\n * we want to let clients know which module couldn't be fetched.\n */\n throw responsePayload;\n }\n\n this.promises.module[cacheKey] = responsePayload;\n modules = await this.promises.module[cacheKey];\n delete this.promises.module[cacheKey];\n return this.store('module', cacheKey, modules);\n }\n\n /**\n * Attempts to fetch a dependency from the cache and\n * if it's not found it attempts to fetch then store\n * the dependency from the package manager.\n *\n * @param {string} dependencyUri - The URI of the dependency\n *\n * @returns {string} file contents\n * @throws when the dependency can not be fetched\n */\n async fetchDependency(dependencyUri) {\n let dependency = this.fetch('dependency', dependencyUri);\n if (dependency !== null) {\n return dependency;\n }\n\n if (this.promises.dependency[dependencyUri]) {\n return this.promises.dependency[dependencyUri];\n }\n\n const url = dcpConfig.packageManager.location.resolve(dependencyUri);\n this.promises.dependency[dependencyUri] = justFetch(url, 'string', 'GET', true);\n\n dependency = await this.promises.dependency[dependencyUri];\n\n delete this.promises.dependency[dependencyUri];\n\n return this.store('dependency', dependencyUri, dependency);\n }\n}\n\nexports.SupervisorCache = SupervisorCache;\n\n\n//# sourceURL=webpack:///./src/dcp-client/worker/supervisor-cache.js?");
|
|
5394
|
+
eval("/**\n * @file worker/supervisor-cache.js\n *\n * A cache for the supervisor, anything the supervisor\n * may request that is cacheable (the same every time its requested)\n * can be cached in this class.\n *\n * Currently only jobs and modules are cached.\n *\n * If a cached job is not accessed in tuning.jobCacheTTL ms\n * it will be invalidated.\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * @date May 2019\n */\n\n/* global dcpConfig */\n\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('worker');\nconst { justFetch, fetchURI, dumpObject } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\n\nconst tuning = dcpConfig.future('supervisor.tuning', {\n jobCacheTTL: 300, /* seconds */\n jobCacheCleanupInterval: 60, /* seconds */\n});\n\nclass SupervisorCache extends EventEmitter {\n constructor (supervisor) {\n var timer;\n \n super('SupervisorCache')\n this.supervisor = supervisor;\n this.cache = {\n job: {},\n module: {},\n dependency: {},\n }\n \n this.promises = {\n job: {},\n module: {},\n dependency: {},\n }\n\n this.lastAccess = {}\n for (let key in this.cache) {\n this.lastAccess[key] = {}\n }\n\n timer = setInterval(this.checkTTL, tuning.jobCacheCleanupInterval);\n if (timer.unref)\n timer.unref();\n }\n\n /**\n * Returns an object listing what jobs and modules are currently cached.\n * @returns {object} - in the form: { job: [0xgen1, 0xgen2,...], modules: [modGroup1, modGroup2,...] }\n */\n get cacheDescription () {\n let description = {}\n for (let key in this.cache) {\n description[key] = Object.keys(this.cache[key])\n }\n return description\n }\n\n /**\n * Returns an array of all jobId currently cached\n * @returns {Array} all the jobId's in the cache\n */\n get jobs () {\n return Object.keys(this.cache.job);\n }\n\n /**\n * Runs on an interval and checks to see if a cached object \n * should be invalidated because it hasn't been accessed recently.\n */\n checkTTL () {\n for (let key in this.lastAccess) {\n for (let address in this.lastAccess[key]) {\n let lastAccess = this.lastAccess[key][address]\n let now = Date.now()\n if (lastAccess + (1000 * tuning.jobCacheTTL) < now) { /* dcpConfig.supervisor.cacheTTL */\n delete this.cache[key][address]\n }\n }\n }\n }\n\n /**\n * Attempts to look up an item from the cache.\n * If item is found its last access time is updated.\n *\n * @param {string} key - the cache to look in (job or module)\n * @param {string} id - the items identifier (jobAddress or module group name)\n *\n * @returns {any} The value of the stored item or null if nothing is found\n */\n fetch(key, id) {\n if (typeof this.cache[key] === 'undefined') {\n throw new Error(`${key} does not relate to any cache.`);\n }\n if (this.cache[key][id]) {\n this.lastAccess[key][id] = Date.now();\n return this.cache[key][id];\n }\n return null;\n }\n\n /**\n * Stores a fetched value for one of the caches.\n *\n * @param {string} key - the cache to store the item in\n * @param {string} id - the items identifier (job Address or module group name)\n * @param {any} value - the item to store\n *\n * @returns {any} - @value passed in\n */\n store (key, id, value) {\n if (typeof this.cache[key] === 'undefined') {\n throw new Error(`${key} does not relate to any cache.`)\n }\n this.cache[key][id] = value\n this.lastAccess[key][id] = Date.now()\n return value\n }\n\n /** \n * Fetch a job from the job cache. If the job has components which are \n * which need to be fetched over the network, they are fetched before the\n * returned promise is resolved.\n *\n * The job cache is initially populated during fetchTask.\n */\n async fetchJob(address, allowedOrigins) {\n let job = this.fetch('job', address);\n\n if (!job) {\n let e = new Error(`No job in supervisor cache with address ${address}`);\n e.code = 'ENOJOB';\n throw e;\n }\n\n // XXXpfr Excellent tracing.\n if (debugging('worker')) {\n dumpObject(job, 'SupervisorCache.fetchJob: job', 128);\n }\n\n if (!job.workFunction) {\n job.workFunction = await fetchURI(job.codeLocation, allowedOrigins, dcpConfig.worker.allowOrigins.fetchWorkFunctions);\n delete job.codeLocation;\n }\n if (!job.arguments) {\n job.arguments = await fetchURI(job.argumentsLocation, dcpConfig.worker.allowOrigins.any, dcpConfig.worker.allowOrigins.fetchArguments);\n delete job.argumentsLocation;\n }\n \n return job;\n }\n\n /**\n * Attempts to fetch a module group from the cache and\n * if it's not found it attempts to fetch then store\n * the module group from the package manager.\n *\n * @param {array} modulesArray - the array of modules requested \n * - (when stringified it's the identifier of the module group)\n *\n * @returns {Promise<object>} - the module group\n * @throws when the module group can not be fetched\n */\n async fetchModule(modulesArray) {\n const cacheKey = JSON.stringify(modulesArray);\n let modules = this.fetch('module', cacheKey);\n if (modules !== null) {\n return modules;\n }\n\n if (this.promises.module[cacheKey]) {\n return this.promises.module[cacheKey];\n }\n\n const {\n success,\n payload: responsePayload,\n } = await this.supervisor.packageManagerConnection.send('fetchModule', {\n modules: modulesArray,\n });\n\n if (!success) {\n /**\n * Preserving the error message by not rewrapping it with DCPError incase\n * we want to let clients know which module couldn't be fetched.\n */\n throw responsePayload;\n }\n\n this.promises.module[cacheKey] = responsePayload;\n modules = await this.promises.module[cacheKey];\n delete this.promises.module[cacheKey];\n return this.store('module', cacheKey, modules);\n }\n\n /**\n * Attempts to fetch a dependency from the cache and\n * if it's not found it attempts to fetch then store\n * the dependency from the package manager.\n *\n * @param {string} dependencyUri - The URI of the dependency\n *\n * @returns {Promise<string>} file contents\n * @throws when the dependency can not be fetched\n */\n async fetchDependency(dependencyUri) {\n let dependency = this.fetch('dependency', dependencyUri);\n if (dependency !== null) {\n return dependency;\n }\n\n if (this.promises.dependency[dependencyUri]) {\n return this.promises.dependency[dependencyUri];\n }\n\n const url = dcpConfig.packageManager.location.resolve(dependencyUri);\n this.promises.dependency[dependencyUri] = justFetch(url, 'string', 'GET', true);\n\n dependency = await this.promises.dependency[dependencyUri];\n\n delete this.promises.dependency[dependencyUri];\n\n return this.store('dependency', dependencyUri, dependency);\n }\n}\n\nexports.SupervisorCache = SupervisorCache;\n\n\n//# sourceURL=webpack:///./src/dcp-client/worker/supervisor-cache.js?");
|
|
5395
5395
|
|
|
5396
5396
|
/***/ }),
|
|
5397
5397
|
|
|
@@ -5403,7 +5403,7 @@ eval("/**\n * @file worker/supervisor-cache.js\n *\n * A cache for the superviso
|
|
|
5403
5403
|
/***/ (function(module, exports, __webpack_require__) {
|
|
5404
5404
|
|
|
5405
5405
|
"use strict";
|
|
5406
|
-
eval("/**\n * @file worker/supervisor.js\n *\n * The component that controls each of the sandboxes\n * and distributes work to them. Also communicates with the\n * scheduler to fetch said work.\n *\n * The supervisor readies sandboxes before/while fetching slices.\n * This means sometimes there are extra instantiated WebWorkers\n * that are idle (in this.readiedSandboxes). Readied sandboxes can\n * be used for any slice. After a readied sandbox is given a slice\n * it becomes assigned to slice's job and can only do work\n * for that job.\n *\n * After a sandbox completes its work, the sandbox becomes cached\n * and can be reused if another slice with a matching job is fetched.\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * Ryan Rossiter, ryan@kingsds.network\n * @date May 2019\n */\n\n/* global dcpConfig */\n// @ts-check\n\n\nconst nanoid = __webpack_require__(/*! nanoid/non-secure */ \"./node_modules/nanoid/non-secure/index.js\");\nconst constants = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst hash = __webpack_require__(/*! dcp/common/hash */ \"./src/common/hash.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\n\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('worker');\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { Sandbox, SandboxError } = __webpack_require__(/*! ./sandbox */ \"./src/dcp-client/worker/sandbox.js\");\nconst { Slice } = __webpack_require__(/*! ./slice */ \"./src/dcp-client/worker/slice.js\");\nconst { SupervisorCache } = __webpack_require__(/*! ./supervisor-cache */ \"./src/dcp-client/worker/supervisor-cache.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { localStorage } = __webpack_require__(/*! dcp/common/dcp-localstorage */ \"./src/common/dcp-localstorage.js\");\nconst { booley, encodeDataURI, makeSliceURI, leafMerge, asleepMs, shortTime,\n dumpSandboxes, dumpSlices, dumpSandboxesIfNotUnique, dumpSlicesIfNotUnique } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst { calculateJoinHash } = __webpack_require__(/*! dcp/dcp-client/compute-groups */ \"./src/dcp-client/compute-groups/index.js\");\n\nconst debug = (...args) => {\n if (debugging('supervisor')) {\n console.debug('dcp-client:worker:supervisor', ...args);\n }\n};\n\nconst supervisorTuning = dcpConfig.future('supervisor.tuning');\nconst tuning = {\n watchdogInterval: 7, /**< seconds - time between fetches when ENOTASK(? /wg nov 2019) */\n minSandboxStartDelay: 0.1, /**< seconds - minimum time between WebWorker starts */\n maxSandboxStartDelay: 0.7, /**< seconds - maximum delay time between WebWorker starts */\n ...supervisorTuning\n};\n\n/** Make timers 10x slower when running in niim */\nlet timeDilation = 1;\nif (DCP_ENV.platform === 'nodejs') {\n /** Make timers 10x slower when running in niim */\n timeDilation = (requireNative('module')._cache.niim instanceof requireNative('module').Module) ? 10 : 1;\n}\n\ndcpConfig.future('worker.sandbox', { progressReportInterval: (5 * 60 * 1000) });\nconst sandboxTuning = dcpConfig.worker.sandbox;\n\n/**\n * @typedef {*} opaqueId\n */\n\n/**\n * @typedef {object} SandboxSlice\n * @property {Sandbox} sandbox\n * @property {Slice} slice\n */\n\n/**\n * @typedef {object} Signature\n * @property {Uint8Array} r\n * @property {Uint8Array} s\n * @property {Uint8Array} v\n */\n\n/**\n * @typedef {object} SignedAuthorizationMessageObject\n * @property {object} auth\n * @property {Signature} signature\n * @property {module:dcp/wallet.Address} owner\n */\n\n/** @typedef {import('.').Worker} Worker */\n/** @typedef {import('.').SupervisorOptions} SupervisorOptions */\n\nclass Supervisor extends EventEmitter {\n /**\n * @constructor\n * @param {Worker} worker\n * @param {SupervisorOptions} options\n */\n constructor (worker, options={}) {\n super('Supervisor');\n\n /** @type {Worker} */\n this.worker = worker;\n\n /** @type {Sandbox[]} */\n this.sandboxes = [];\n\n /**\n * XXXpfr @todo We may still needs this.slices to communicate state to result-submitter status endpoint, but it isn't used otherwise.\n * @deprecated\n * @type {Slice[]}\n */\n this.slices = [];\n\n /**\n * The true type is associative array of associative array of SignedAuthorizationMessageObject.\n * How can that be specified in jsdoc?\n * @type {object}\n */\n this.authorizationMessages = {};\n\n /** @type {Slice[]} */\n this.queuedSlices = [];\n\n /** @type {Sandbox[]} */\n this.readiedSandboxes = [];\n\n /** @type {Sandbox[]} */\n this.assignedSandboxes = [];\n\n /** @type {boolean} */\n this.matching = false;\n\n if (!options) {\n console.error('Supervisor Options', options, new Error().stack);\n options = {};\n }\n \n /** @type {object} */\n this.options = {\n jobAddresses: options.jobAddresses || [/* all jobs unless priorityOnly */],\n ...options,\n };\n\n const { paymentAddress, identityKeystore } = options;\n if (paymentAddress) {\n if (paymentAddress instanceof wallet.Keystore) {\n this.paymentAddress = paymentAddress.address;\n } else {\n this.paymentAddress = new wallet.Address(paymentAddress);\n }\n } else {\n this.paymentAddress = null;\n }\n\n this._identityKeystore = identityKeystore;\n\n this.allowedOrigins = dcpConfig.worker.allowOrigins.any;\n if(options.allowedOrigins && options.allowedOrigins.length > 0)\n this.allowedOrigins = options.allowedOrigins.concat(this.allowedOrigins);\n\n /**\n * Maximum sandboxes allowed to work at a given time.\n * @type {number}\n */\n this.maxWorkingSandboxes = options.maxWorkingSandboxes || 1;\n\n /** @type {number} */\n this.defaultMaxGPUs = 1;\n // this.GPUsAssigned = 0;\n \n // Object.defineProperty(this, 'GPUsAssigned', {\n // get: () => this.workingSandboxes.filter(sb => !!sb.requiresGPU).length,\n // enumerable: true,\n // configurable: false,\n // });\n\n /** @type {boolean} */\n this.isFetchingNewWork = false;\n /**\n * TODO: Remove this when the supervisor sends all of the sandbox\n * capabilities to the scheduler when fetching work.\n * @type {object}\n */\n this.capabilities = null;\n\n // This sets an offset into the watchdog bin at which to fire the sweeper\n /** @deprecated - UNUSED */\n this.watchdogSlotTime = options.watchdogInterval || tuning.watchdogInterval * 1000;\n /** @deprecated - UNUSED */\n this.watchdogOffset = Math.random();\n /** @deprecated - UNUSED */\n this.watchdogTimeout = null;\n\n /** @type {number} */\n this.lastProgressReport = 0;\n\n // @hack - dcp-env.isBrowserPlatform is not set unless the platform is _explicitly_ set,\n // using the default detected platform doesn't set it.\n // Fixing that causes an error in the wallet module's startup on web platform, which I\n // probably can't fix in a reasonable time this morning.\n // ~ER2020-02-20\n\n if (!options.maxWorkingSandboxes\n && DCP_ENV.browserPlatformList.includes(DCP_ENV.platform)\n && navigator.hardwareConcurrency > 1) {\n this.maxWorkingSandboxes = navigator.hardwareConcurrency - 1;\n if (typeof navigator.userAgent === 'string') {\n if (/(Android).*(Chrome|Chromium)/.exec(navigator.userAgent)) {\n this.maxWorkingSandboxes = 1;\n console.log('Doing work with Chromimum browsers on Android is currently limited to one sandbox');\n }\n }\n }\n\n /** @type {SupervisorCache} */\n this.cache = new SupervisorCache(this);\n /** @type {object} */\n this._connections = {}; /* active DCPv4 connections */\n // Call the watchdog every 7 seconds.\n this.watchdogInterval = setInterval(() => this.watchdog(), 7000);\n\n const ceci = this;\n\n // Initialize to null so these properties are recognized for the Supervisor class\n this.taskDistributorConnection = null;\n this.eventRouterConnection = null;\n this.resultSubmitterConnection = null;\n this.packageManagerConnection = null;\n this.openTaskDistributorConn = function openTaskDistributorConn()\n {\n let config = dcpConfig.scheduler.services.taskDistributor;\n ceci.taskDistributorConnection = new protocolV4.Connection(config, ceci.identityKeystore, connectionOptions(config.location, 'taskDistributor'));\n ceci.taskDistributorConnection.on('close', ceci.openTaskDistributorConn);\n }\n\n this.openEventRouterConn = function openEventRouterConn()\n {\n let config = dcpConfig.scheduler.services.eventRouter;\n ceci.eventRouterConnection = new protocolV4.Connection(config, ceci.identityKeystore, connectionOptions(config.location, 'eventRouter'));\n ceci.eventRouterConnection.on('close', ceci.openEventRouterConn);\n }\n \n this.openResultSubmitterConn = function openResultSubmitterConn()\n {\n let config = dcpConfig.scheduler.services.resultSubmitter;\n ceci.resultSubmitterConnection = new protocolV4.Connection(config, ceci.identityKeystore, connectionOptions(config.location, 'resultSubmitter'));\n ceci.resultSubmitterConnection.on('close', ceci.openResultSubmitterConn);\n }\n\n this.openPackageManagerConn = function openPackageManagerConn()\n {\n let config = dcpConfig.packageManager;\n ceci.packageManagerConnection = new protocolV4.Connection(config, ceci.identityKeystore, connectionOptions(config.location, 'packageManager'));\n ceci.packageManagerConnection.on('close', ceci.openPackageManagerConn);\n }\n }\n\n /**\n * Return worker opaqueId.\n * @type {opaqueId}\n */\n get workerOpaqueId() {\n if (!this._workerOpaqueId)\n this._workerOpaqueId = localStorage.getItem('workerOpaqueId');\n\n if (!this._workerOpaqueId || this._workerOpaqueId.length !== 22) { /** @todo XXXwg fix magic number problem */\n this._workerOpaqueId = nanoid(22);\n localStorage.setItem('workerOpaqueId', this._workerOpaqueId);\n }\n\n return this._workerOpaqueId;\n }\n\n /**\n * This getter is the absolute source-of-truth for what the\n * identity keystore is for this instance of the Supervisor.\n */\n get identityKeystore() {\n assert(this.defaultIdentityKeystore);\n\n return this._identityKeystore || this.defaultIdentityKeystore;\n }\n\n /**\n * Open all connections. Used when supervisor is instantiated or stopped/started\n * to initially open connections.\n */\n instantiateAllConnections() {\n this.openTaskDistributorConn();\n this.openEventRouterConn();\n this.openResultSubmitterConn();\n this.openPackageManagerConn();\n }\n\n /** Set the default identity keystore -- needs to happen before anything that talks\n * to the scheduler for work gets called. This is a wart and should be removed by\n * refactoring.\n *\n * The default identity keystore will be used if the Supervisor was not provided\n * with an alternate. This keystore will be located via the Wallet API, and \n * if not found, a randomized default identity will be generated. \n *\n * @param {object} ks An instance of wallet::Keystore -- if undefined, we pick the best default we can.\n * @returns {Promise<void>}\n */\n async setDefaultIdentityKeystore(ks) {\n try {\n if (ks) {\n this.defaultIdentityKeystore = ks;\n return;\n }\n\n if (this.defaultIdentityKeystore)\n return;\n\n try {\n this.defaultIdentityKeystore = await wallet.getId();\n } catch(e) {\n debugging('supervisor') && console.debug('supervisor: generating default identity', this.defaultIdentityKeystore.address);\n this.defaultIdentityKeystore = await new wallet.IdKeystore(null, '');\n }\n } finally {\n debugging('supervisor') && console.debug('supervisor: set default identity =', this.defaultIdentityKeystore.address);\n }\n }\n \n //\n // What follows is a bunch of utility functions for creating filtered views\n // of the slices and sandboxes array.\n //\n\n /**\n * Sandboxes that are in WORKING state.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Sandbox[]}\n */\n get workingSandboxes() {\n return this.sandboxes.filter(sandbox => sandbox.isWorking);\n }\n\n /**\n * This property is used as the target number of sandboxes to be associated with slices and transition sandbox-state from ASSIGNED -> WORKING.\n *\n * It is used in this.watchdog as to prevent a call to this.work when unallocatedSpace <= 0.\n * It is also used in this.distributeQueuedSlices where it is passed as an argument to this.matchSlicesWithSandboxes to indicate how many sandboxes\n * to associate with slices and transition sandbox-state from ASSIGNED -> WORKING.\n *\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {number}\n */\n get unallocatedSpace() {\n return this.maxWorkingSandboxes - this.sandboxes.filter(sandbox => sandbox.allocated).length;\n }\n\n /**\n * Slices that have SLICE_STATUS_UNASSIGNED status.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Slice[]}\n */\n get snapshotOfQueuedSlices() {\n return this.slices.filter(slice => slice.isUnassigned);\n }\n\n /**\n * Slices that have SLICE_STATUS_WORKING status.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Slice[]}\n */\n get snapshotOfWorkingSlices() {\n return this.slices.filter(slice => slice.isWorking)\n }\n\n /**\n * Sandboxes that are in READY_FOR_ASSIGN state.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Sandbox[]}\n */\n get snapshotOfReadiedSandboxes() {\n return this.sandboxes.filter(sandbox => sandbox.isReadyForAssign);\n }\n\n /**\n * Sandboxes that are in ASSIGNED state.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Sandbox[]}\n */\n get snapshotOfAssignedSandboxes() {\n return this.sandboxes.filter(sandbox => sandbox.isAssigned);\n }\n\n /**\n * Sandboxes that are in TERMINATED state.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Sandbox[]}\n */\n get snapshotOfTerminatedSandboxes() {\n return this.sandboxes.filter(sandbox => sandbox.isTerminated);\n }\n\n /**\n * Remove element from theArray.\n * @parameter {Array<*>} theArray\n * @parameter {object|number} element\n * @parameter {boolean} [assertExists = true]\n */\n removeElement(theArray, element, assertExists = false) {\n let index = theArray.indexOf(element);\n assert(index !== -1 || !assertExists);\n if (index !== -1) theArray.splice(index, 1);\n }\n\n /**\n * Check authorization message wrt jobAddress and sliceNumber.\n * @parameter {string} text\n * @parameter {opaqueId} jobAddress\n * @parameter {number} sliceNumber\n * @returns {SignedAuthorizationMessageObject}\n */\n checkAuthorization(text, jobAddress, sliceNumber) {\n const authorizationMessage = this.authorizationMessages[jobAddress][sliceNumber];\n if (!authorizationMessage)\n console.log(`\\t${text} DANGER: Undefined authorization. ${jobAddress}, ${sliceNumber}`);\n return authorizationMessage;\n }\n\n /**\n * If the elements of sandboxSliceArray are not unique, log the duplicates and dump the array.\n * @parameter {SandboxSlice[]} sandboxSliceArray\n * @parameter {string} header\n */\n dumpSandboxSlicesIfNotUnique(sandboxSliceArray, header) {\n if (!this.isUniqueSandboxSlices(sandboxSliceArray, header))\n this.dumpSandboxSlices(sandboxSliceArray);\n }\n\n /**\n * Log sandboxSlice.\n * @parameter {SandboxSlice} sandboxSlice\n * @returns {string}\n */\n dumpSandboxSlice(sandboxSlice) {\n return `${sandboxSlice.sandbox.id}.${sandboxSlice.slice.sliceNumber}.${sandboxSlice.slice.jobAddress}`;\n }\n\n /**\n * Log { sandbox, slice }.\n * @parameter {Sandbox} sandbox\n * @parameter {Slice} slice\n * @returns {string}\n */\n dumpSandboxAndSlice(sandbox, slice) {\n return this.dumpSandboxSlice({ sandbox, slice });\n }\n\n /**\n * Dump sandboxSliceArray.\n * @parameter {SandboxSlice[]} sandboxSliceArray\n * @parameter {string} header\n */\n dumpSandboxSlices(sandboxSliceArray, header) {\n if (header) console.log(`\\n${header}`);\n sandboxSliceArray.forEach(x => console.log(`\\t{ ${x.sandbox.id}.${x.sandbox.jobAddress}.${x.sandbox.state}, ${x.slice.sliceNumber}.${x.slice.jobAddress}.${x.slice.status} }`));\n }\n\n /**\n * Check sandboxSliceArray for duplicates.\n * @parameter {SandboxSlice[]} sandboxSliceArray\n * @parameter {string} header\n * @parameter {function} log\n * @returns {boolean}\n */\n isUniqueSandboxSlices(sandboxSliceArray, header, log) {\n return sandboxSliceArray.length === this.makeUniqueSandboxSlices(sandboxSliceArray, header, log).length;\n }\n\n /**\n * Returns a copy of sandboxSliceArray with all duplicates removed.\n * @parameter {SandboxSlice[]} sandboxSliceArray\n * @parameter {string} header\n * @parameter {function} log\n * @returns {SandboxSlice[]}\n */\n makeUniqueSandboxSlices(sandboxSliceArray, header, log) {\n const result = [], slices = [], sandboxes = [];\n let once = true;\n sandboxSliceArray.forEach(x => {\n const sliceIndex = slices.indexOf(x.slice);\n const sandboxIndex = sandboxes.indexOf(x.sandbox);\n\n if (sandboxIndex >= 0) {\n if (once && header) console.log(`\\n${header}`); once = false;\n log ? log(x.sandbox) : console.log(`\\tDANGER: Found duplicate sandbox ${x.sandbox.id}.${x.sandbox.jobAddress}.${x.sandbox.state}.`);\n } else sandboxes.push(x.sandbox);\n\n if (sliceIndex >= 0) {\n if (once && header) console.log(`\\n${header}`); once = false;\n log ? log(x.slice) : console.log(`\\tDANGER: Found duplicate slice ${x.slice.sliceNumber}.${x.slice.jobAddress}.${x.slice.status}.`);\n } else {\n slices.push(x.slice);\n if (sandboxIndex < 0) result.push(x);\n }\n });\n return result;\n }\n\n /** NOT used ATM, it's faster to just filter the assigned sandboxes for a jobAddress on-demand\n * \n * Bins the assigned sandboxes in an object, keyed by their jobAddress.\n * { 0xgenAddress1: sandboxes[], 0xgenAddress2: sandboxes[], ... }\n * @type {object}\n */\n get assignedSandboxesSorted () {\n return this.assignedSandboxes.reduce((o, w) => {\n if (!w.jobAddress) throw new Error(\"Assigned sandbox doesn't have a job opaque id\", w);\n\n if (w.jobAddress in o) {\n o[w.jobAddress].push(w);\n } else o[w.jobAddress] = [w];\n\n return o;\n }, {});\n }\n\n /**\n * Attempts to create and start a given number of sandboxes.\n * The sandboxes that are created can then be assigned for a\n * specific job at a later time. All created sandboxes\n * get put into the @this.readiedSandboxes array.\n *\n * @param {number} numSandboxes - the number of sandboxes to create\n * @returns {Promise<Sandbox[]>} - resolves with array of created sandboxes, rejects otherwise\n * @throws when given a numSandboxes is not a number > 0 or if numSandboxes is Infinity\n */\n async readySandboxes (numSandboxes) {\n debug('Readying sandboxes');\n if (typeof numSandboxes !== 'number' || Number.isNaN(numSandboxes) || numSandboxes === Infinity) {\n throw new Error(`${numSandboxes} is not a number of sandboxes that can be readied.`);\n }\n if (numSandboxes <= 0) {\n return [];\n }\n\n const sandboxStartPromises = [];\n const sandboxes = [];\n const errors = [];\n for (let i = 0; i < numSandboxes; i++) {\n const sandbox = new Sandbox(this.cache, {\n ...this.options.sandboxOptions,\n }, this.allowedOrigins);\n sandbox.addListener('ready', () => this.emit('sandboxReady', sandbox));\n sandbox.addListener('start', () => {\n this.emit('sandboxStart', sandbox);\n\n // When sliceNumber == 0, result-submitter status skips the slice,\n // so don't send it in the first place.\n // The 'start' event is fired when a worker starts up, hence there's no way\n // to determine whether sandbox has a valid slice without checking.\n if (sandbox.slice) {\n const jobAddress = sandbox.jobAddress;\n const sliceNumber = sandbox.slice.sliceNumber;\n const authorizationMessage = this.checkAuthorization('Supervisor.readySandboxes sandboxStart:', jobAddress, sliceNumber);\n if (!authorizationMessage) return;\n\n this.resultSubmitterConnection.send('status', {\n worker: this.workerOpaqueId,\n slices: [{\n job: jobAddress,\n sliceNumber: sliceNumber,\n status: 'begin',\n authorizationMessage,\n }],\n });\n }\n });\n sandbox.addListener('workEmit', ({ eventName, payload }) => {\n // Need to check if the sandbox hasn't been assigned a slice yet.\n if (!sandbox.slice) {\n if (__webpack_require__(/*! dcp/common/dcp-build */ \"./src/common/dcp-build.js\").build === \"debug\") {\n console.error(\n `Sandbox not assigned a slice before sending workEmit message to scheduler. 'workEmit' event originates from \"${eventName}\" event`, \n payload,\n );\n }\n }\n\n const jobAddress = sandbox.slice.jobAddress;\n const sliceNumber = sandbox.slice.sliceNumber;\n // sliceNumber can be zero if it came from a problem with loading modules.\n assert(jobAddress && (sliceNumber || sliceNumber === 0));\n try {\n /* Make sure we actually have an authorization message at jobAddress and sliceNumber. */\n const authorizationMessage = this.checkAuthorization('Supervisor.readySandboxes workEmit:', jobAddress, sliceNumber);\n if (!authorizationMessage) return;\n\n /* DCP-1698 Send auth msg with tasks to worker, then validate authority of worker to send slice info back to scheduler. */\n this.eventRouterConnection.send('workEmit', {\n eventName,\n payload,\n job: jobAddress,\n slice: sliceNumber,\n worker: this.workerOpaqueId,\n authorizationMessage,\n });\n } catch (error) {\n debugging('supervisor') &&\n console.debug(\n `Authorizing failed: jobAddress: ${jobAddress}, slice: ${sliceNumber}, error: ${error}`,\n );\n }\n });\n\n // When any sbx completes, \n sandbox.addListener('complete', () => {\n // this.distributeQueuedSlices();\n this.watchdog();\n //this.work();\n });\n\n const delayMs =\n 1000 *\n (tuning.minSandboxStartDelay +\n Math.random() *\n (tuning.maxSandboxStartDelay - tuning.minSandboxStartDelay));\n \n sandboxStartPromises.push(\n sandbox\n .start(delayMs)\n .then(() => {\n this.sandboxes.push(sandbox);\n })\n .catch((err) => errors.push(err)),\n );\n sandboxes.push(sandbox);\n }\n\n await Promise.all(sandboxStartPromises);\n\n if (errors.length) {\n console.warn(`Failed to ready ${errors.length} of ${numSandboxes} sandboxes.`, errors);\n throw new Error('Failed to ready sandboxes.');\n }\n\n if (this.readiedSandboxes.length > 0)\n sandboxes.push(...this.readiedSandboxes);\n this.readiedSandboxes = sandboxes;\n\n debugging() && console.log(`readySandboxes: all readied sandboxes ${JSON.stringify(this.readiedSandboxes.map(sandbox => sandbox.id))}`);\n \n return sandboxes;\n }\n\n /**\n * Accepts a sandbox after it has finished working or encounters an error.\n * If the sandbox was terminated or if \"!slice || slice.failed\" then\n * the sandbox will be removed from the sandboxes array and terminated if necessary.\n * Otherwise it will try to distribute a slice to the sandbox immediately.\n *\n * @param {Sandbox} sandbox - the sandbox to return\n * @param {Slice} [slice] - the slice just worked on; !slice => terminate\n * @returns {Promise<void>}\n */\n async returnSandbox (sandbox, slice) {\n if (!slice || slice.failed || sandbox.isTerminated) {\n this.removeElement(this.sandboxes, sandbox);\n if (!sandbox.isTerminated) {\n sandbox.terminate(false);\n }\n }\n\n if (slice) {\n\n const sliceNumber = slice.sliceNumber;\n const jobAddress = slice.jobAddress;\n\n //\n // An upstream failure like certain connection failures, can cause an error cascade,\n // so we need to gaurd against indeterminant state with this conditional.\n // An example of such an error cascade is calling Connection.fetchTransport\n // when this.connectionOptions.transports is empty.\n //\n if (this.authorizationMessages[jobAddress][sliceNumber]) {\n debugging() && console.log(`Supervisor.returnSandbox: ${jobAddress}, ${sliceNumber}, kill corresponding auth message.`);\n /* Done with slice. Cleanup. */\n delete this.authorizationMessages[jobAddress][sliceNumber];\n if (Object.keys(this.authorizationMessages[jobAddress]).length === 0) {\n delete this.authorizationMessages[jobAddress];\n }\n }\n }\n }\n\n /**\n * Terminates sandboxes, in order of creation, when the total started sandboxes exceeds the total allowed sandboxes.\n *\n * @todo Check through the currently started sandboxes and attempt to terminate\n * ones that aren't frequently used.\n * ^^^ This todo may be partially done due to terminating older sandboxes - RR Nov 2019\n * @returns {Promise<void>}\n */\n async pruneSandboxes () {\n let numOver = this.sandboxes.length - (dcpConfig.worker.maxAllowedSandboxes + this.maxWorkingSandboxes);\n if (numOver <= 0 || this.matching) return; // Don't kill sandboxes while matching with slices.\n \n let index = 0;\n while (index < this.readiedSandboxes.length) {\n const sandbox = this.readiedSandboxes[index];\n // if this sbx is allocated, skip it and look at the next one\n if (sandbox.allocated) {\n index++;\n continue;\n }\n // otherwise, remove this one and look at the same array element next loop\n debugging() && console.log(`pruneSandboxes: Killing readied sandbox ${sandbox.id}`);\n this.readiedSandboxes.splice(index, 1);\n await this.returnSandbox(sandbox);\n\n if (--numOver <= 0) break;\n }\n\n\n if (numOver <= 0) return;\n index = 0;\n while (index < this.assignedSandboxes.length) {\n const sandbox = this.assignedSandboxes[index];\n \n // if the sandbox is allocated, advance to the next one in the list\n if (sandbox.allocated) {\n index++;\n continue;\n }\n \n // otherwise, remove this sandbox but look at the same array index in the next loop\n debugging() && console.log(`pruneSandboxes: Killing assigned sandbox ${sandbox.id}`);\n this.assignedSandboxes.splice(index, 1);\n await this.returnSandbox(sandbox);\n\n if (--numOver <= 0) break;\n }\n }\n \n /**\n * Basic watch dog to check if there are idle sandboxes and\n * attempts to nudge the supervisor to feed them work.\n *\n * Run in an interval created in @constructor .\n * @returns {Promise<void>}\n */\n async watchdog () {\n if (!this.watchdogState)\n this.watchdogState = {};\n \n // Every 5 minutes, report progress of all working slices to the scheduler\n if (Date.now() > ((this.lastProgressReport || 0) + sandboxTuning.progressReportInterval)) {\n // console.log('454: Assembling progress update...');\n this.lastProgressReport = Date.now();\n \n const slices = [];\n \n this.queuedSlices.forEach(slice => {\n const jobAddress = slice.jobAddress;\n const sliceNumber = slice.sliceNumber;\n const authorizationMessage = this.checkAuthorization('Supervisor.watchdog:', jobAddress, sliceNumber);\n\n if (authorizationMessage) {\n addToSlicePayload(slices, jobAddress, sliceNumber, 'scheduled', authorizationMessage);\n }\n })\n \n this.workingSandboxes.forEach(sb => {\n // The lifetime of sandbox.isWorking is (or should be) precisely defined,\n // so we can assume sb.slice is always valid here.\n assert(sb.slice);\n\n const jobAddress = sb.jobAddress;\n const sliceNumber = sb.slice.sliceNumber;\n const authorizationMessage = this.checkAuthorization('Supervisor.watchdog:', jobAddress, sliceNumber);\n\n if (authorizationMessage) {\n addToSlicePayload(slices, jobAddress, sliceNumber, 'progress', authorizationMessage);\n }\n });\n\n if (slices.length) {\n // console.log('471: sending progress update...');\n const progressReportPayload = {\n worker: this.workerOpaqueId,\n slices,\n };\n\n this.resultSubmitterConnection.send('status', progressReportPayload)\n .catch(error => {\n console.error('479: Failed to send status update:', error/*.message*/);\n });\n }\n }\n\n if (this.worker.working) {\n if (this.unallocatedSpace > 0) {\n await this.work().catch(err => {\n if (!this.watchdogState[err.code || '0'])\n this.watchdogState[err.code || '0'] = 0;\n if (Date.now() - this.watchdogState[err.code || '0'] > ((dcpConfig.worker.watchdogLogInterval * timeDilation || 120) * 1000))\n console.error('301: Failed to start work:', err);\n this.watchdogState[err.code || '0'] = Date.now();\n });\n }\n\n this.pruneSandboxes();\n }\n }\n\n /**\n * Gets the logical and physical number of cores and also\n * the total number of sandboxes the worker is allowed to run\n *\n */\n getStatisticsCPU() {\n if (DCP_ENV.isBrowserPlatform) {\n return {\n worker: this.workerOpaqueId,\n lCores: window.navigator.hardwareConcurrency,\n pCores: dcpConfig.worker.pCores || window.navigator.hardwareConcurrency,\n sandbox: this.maxWorkingSandboxes\n }\n }\n\n return {\n worker: this.workerOpaqueId,\n lCores: requireNative('os').cpus().length,\n pCores: requireNative('physical-cpu-count'),\n sandbox: this.maxWorkingSandboxes\n }\n }\n\n /**\n * Call to start doing work on the network.\n * This is the one place where requests to fetch new slices are made.\n * After the initial slice and job are fetched it calls @this.distributeQueuedSlices.\n *\n * @returns {Promise<void>}\n */\n async work()\n {\n\n await this.setDefaultIdentityKeystore();\n\n // if connections don't exist, instantiate them. Needed every time worker is started/stopped\n if (!this.taskDistributorConnection)\n this.instantiateAllConnections();\n\n let slicesToFetch;\n if (this.options.priorityOnly && this.options.jobAddresses.length === 0) {\n slicesToFetch = 0;\n } else if (this.queuedSlices.length > 1) {\n // We have slices queued, no need to fetch\n slicesToFetch = 0;\n } else {\n // The queue is almost empty (there may be 0 or 1 element), fetch a full task.\n // The task is full, in the sense that it will contain slices whose\n // aggregate execution time is this.maxWorkingSandboxes * 5-minutes.\n // However, there can only be this.unallocatedSpace # of long slices.\n // Thus we need to know whether a slice in this.queuedSlices is long or not.\n // (A long slice has estimated execution time > 5-minutes.)\n const longSliceCount = (this.queuedSlices.length > 0 && this.queuedSlices[0].isLongSlice) ? 1 : 0;\n slicesToFetch = this.unallocatedSpace - longSliceCount;\n }\n\n debugging() && console.log(`Supervisor.work: Try to get ${slicesToFetch} slices in working sandboxes, unallocatedSpace ${this.unallocatedSpace}, queued slices ${this.queuedSlices.length}`);\n \n // Fetch a new task if we have no more slices queued, then start workers\n try {\n if (slicesToFetch > 0 && !this.isFetchingNewWork) {\n this.isFetchingNewWork = true;\n\n /**\n * This will only ready sandboxes up to a total count of\n * maxWorkingSandboxes (in any state). It is not possible to know the\n * actual number of sandboxes required until we have the slices because we\n * may have sandboxes assigned for the slice's job already.\n */\n if (this.maxWorkingSandboxes > this.sandboxes.length) {\n await this.readySandboxes(\n this.maxWorkingSandboxes - this.sandboxes.length,\n );\n }\n\n /**\n * Temporary change: Assign the capabilities of one of readied sandboxes\n * before fetching slices from the scheduler.\n *\n * TODO: Remove this once fetchTask uses the capabilities of every\n * sandbox to fetch slices.\n */\n if (!this.capabilities) {\n this.capabilities = this.sandboxes[0].capabilities;\n this.emit('capabilitiesCalculated', this.capabilities);\n }\n\n if(DCP_ENV.isBrowserPlatform && this.capabilities.browser)\n this.capabilities.browser.chrome = DCP_ENV.isBrowserChrome;\n\n const fetchTimeout = setTimeout(() => {\n console.warn(`679: Fetch exceeded timeout, will retry at next watchdog interval`);\n this.isFetchingNewWork = false;\n }, 3 * 60 * 1000); // max out at 3 minutes to fetch\n\n // ensure result submitter connection before fetching tasks\n try\n {\n await this.resultSubmitterConnection.keepalive();\n }\n catch (error)\n {\n console.error('Failed to connect to result submitter, refusing to fetch slices. Will try again at next fetch cycle.')\n debugging('supervisor') && console.log(`Error: ${error}`);\n this.isFetchingNewWork = false;\n return;\n }\n await this.fetchTask(slicesToFetch);\n clearTimeout(fetchTimeout);\n }\n \n this.distributeQueuedSlices().then(() => debugging('supervisor') && 'supervisor: finished distributeQueuedSlices()').catch((e)=>console.error(e)) ;\n// await this.distributeQueuedSlices();\n // No catch(), because it will bubble outward to the caller\n } finally {\n this.isFetchingNewWork = false;\n }\n }\n\n /**\n * Generate the workerComputeGroups property of the requestTask message. \n * \n * Concatenate the compute groups object from dcpConfig with the list of compute groups\n * from the supervisor, and remove the public group if accidentally present. Finally,\n * we transform joinSecrets into joinHashHash for secure transmission.\n *\n * @note computeGroup objects with joinSecrets are mutated to record their hashes. This\n * affects the supervisor options and dcpConfig. Re-adding a joinSecret property\n * to one of these will cause the hash to be recomputed.\n */\n generateWorkerComputeGroups()\n {\n var computeGroups = Object.values(dcpConfig.worker.computeGroups || {});\n if (this.options.computeGroups)\n computeGroups = computeGroups.concat(this.options.computeGroups);\n computeGroups = computeGroups.filter(group => group.id !== constants.computeGroups.public.id);\n const hashedComputeGroups = [];\n for (const group of computeGroups)\n {\n const groupCopy = Object.assign({}, group);\n if (group.joinSecret && (!group.joinHashHash || this.lastDcpsid !== this.taskDistributorConnection.dcpsid))\n {\n const joinHash = calculateJoinHash(groupCopy);\n groupCopy.joinHashHash = hash.calculate(hash.eh1, joinHash, this.taskDistributorConnection.dcpsid);\n delete groupCopy.joinSecret;\n debugging('computeGroups') && console.debug(`Calculated joinHash=${joinHash} for`, group);\n }\n hashedComputeGroups.push(groupCopy);\n }\n this.lastDcpsid = this.taskDistributorConnection.dcpsid;\n debugging('computeGroups') && console.debug('Requesting ', computeGroups.length, 'non-public groups for session', this.lastDcpsid);\n return hashedComputeGroups;\n }\n\n /**\n * Fetches a task, which contains job information and slices for sandboxes and\n * manages events related to fetching tasks so the UI can more clearly display\n * to user what is actually happening.\n * @param {number} numCores\n * @returns {Promise<void>} The requestTask request, resolve on success, rejects otherwise.\n * @emits Supervisor#fetchingTask\n * @emits Supervisor#fetchedTask\n */\n async fetchTask(numCores)\n {\n this.emit('fetchingTask');\n debugging('supervisor') && console.debug('supervisor: fetching task');\n const requestPayload = {\n numCores,\n coreStats: this.getStatisticsCPU(),\n numGPUs: this.defaultMaxGPUs,\n capabilities: this.capabilities,\n paymentAddress: this.paymentAddress,\n jobAddresses: this.options.jobAddresses, // when set, only fetches slices for these jobs\n localExec: this.options.localExec,\n workerComputeGroups: this.generateWorkerComputeGroups(),\n minimumWage: dcpConfig.worker.minimumWage || this.options.minimumWage,\n readyJobs: [ /* list of jobs addresses XXXwg */ ],\n previouslyWorkedJobs: this.cache.jobs,\n };\n\n // workers should be part of the public compute group by default\n if (!booley(dcpConfig.worker.leavePublicGroup) && !booley(this.options.leavePublicGroup))\n requestPayload.workerComputeGroups.push(constants.computeGroups.public);\n\n debugging('computeGroups') && console.log(`Fetching work for ${requestPayload.workerComputeGroups.length} ComputeGroups: `, requestPayload.workerComputeGroups);\n \n debugging() && console.log(`${shortTime()} fetchTask wants ${numCores} slices, unallocatedSpace ${this.unallocatedSpace}, queuedSlices ${this.queuedSlices.length}`);\n try {\n debugging('requestTask') && debug('requestPayload', requestPayload);\n\n let result = await this.taskDistributorConnection.send('requestTask', requestPayload);\n let responsePayload = result.payload; \n \n if (!result.success) { \n debugging() && console.log('Task fetch failure; request=', requestPayload);\n debugging() && console.log('Task fetch failure; response=', result.payload);\n throw new DCPError('Unable to fetch task for worker', responsePayload);\n }\n\n const sliceCount = responsePayload.body.task.length || 0;\n\n /**\n * The fetchedTask event fires when the supervisor has finished trying to\n * fetch work from the scheduler (task-manager). The data emitted is the\n * number of new slices to work on in the fetched task.\n *\n * @event Supervisor#fetchedTask\n * @type {number}\n */\n this.emit('fetchedTask', sliceCount);\n\n if (sliceCount < 1) {\n return;\n }\n\n /*\n * DCP-1698 Send auth msg with tasks to worker, then validate authority of worker to send slice info back to scheduler.\n * payload structure: { owner: this.address, signature: signature, auth: messageLightWeight, body: messageBody };\n * XXXpfr: @todo Do not think we need 'owner', think about it.\n */\n const { body, ...authorizationMessage } = responsePayload;\n const { newJobs, task } = body;\n assert(newJobs); // It should not be possible to have !newJobs -- we throw on !success.\n for (const jobAddress of Object.keys(newJobs)) {\n this.cache.store('job', jobAddress, newJobs[jobAddress]);\n }\n debugging('supervisor') && console.log(`${shortTime()} fetchTask: Task has ${task.length} slices from ${Object.keys(newJobs).length} new jobs.`);\n\n const tmpQueuedSlices = [];\n for (const taskElement of task) {\n\n if (!this.authorizationMessages[taskElement.jobAddress]) {\n this.authorizationMessages[taskElement.jobAddress] = {};\n // All workers are authorized to send messages to the '0' slice if it is assigned the job.\n this.authorizationMessages[taskElement.jobAddress][0] = authorizationMessage;\n }\n this.authorizationMessages[taskElement.jobAddress][taskElement.sliceNumber] = authorizationMessage;\n\n tmpQueuedSlices.push(new Slice(taskElement));\n debugging('supervisor') && console.log(`/t${shortTime()} fetchTask: slice ${taskElement.sliceNumber}, jobAddress ${taskElement.jobAddress}`);\n }\n\n /** XXXpfr @todo Try to get rid of this.slices. */\n this.slices.push(...tmpQueuedSlices);\n\n this.queuedSlices = [...tmpQueuedSlices, ...this.queuedSlices]; /* Make sure the old ones are up front. */\n\n } catch (error) {\n this.emit('fetchTaskFailed', error);\n debugging('supervisor') && console.debug(`Supervisor.fetchTask failed!: error: ${error}`);\n }\n }\n\n /**\n * For each slice in this.queuedSlices, match with a sandbox in the following order:\n * 1. Try to find an already assigned sandbox in this.assignedSandboxes for the slice's job.\n * 2. Find a ready sandbox in this.readiedSandboxes that is unassigned.\n * 3. Ready a new sandbox and use that.\n *\n * Take great care in assuring sandboxes and slices are uniquely associated, viz.,\n * a given slice cannot be associated with multiple sandboxes and a given sandbox cannot be associated with multiple slices.\n * The lack of such uniqueness has been the root cause of several difficult bugs.\n *\n * @param {number} numSlices The number of slices to match with sandboxes.\n * @returns {Promise<SandboxSlice[]>} Returns SandboxSlice[], may have length zero.\n */\n async matchSlicesWithSandboxes (numSlices) {\n const sandboxSlices = [];\n if (numSlices <= 0 || this.queuedSlices.length == 0 || this.matching)\n return sandboxSlices;\n\n try\n {\n this.matching = true;\n const jobSliceMap = {};\n const jobs = [];\n\n // Until this is rock-solid-stable I want to check for uniquenees.\n if (true) {\n dumpSlicesIfNotUnique(this.queuedSlices, `DANGER: this.queuedSlices slices are not unique -- this is ok when slice is rescheduled.`);\n dumpSandboxesIfNotUnique(this.readiedSandboxes, `DANGER: this.readiedSandboxes sandboxes are not unique!`);\n dumpSandboxesIfNotUnique(this.assignedSandboxes, `DANGER: this.assignedSandboxes sandboxes are not unique!`);\n }\n\n // 'false' is so it is easy to turn back to 'true'\n // Since we don't know yet whether these changes are rock-solid-stable I want to keep easy-to-debug stuff like this.\n if ( false || debugging()) {\n console.log(`${shortTime()} matchSlicesWithSandboxes: numSlices ${numSlices}, numQueuedSlices ${this.queuedSlices.length}: numAssignedSandboxes ${this.assignedSandboxes.length}, numReadySandboxes ${this.readiedSandboxes.length}, total sandbox count: ${this.sandboxes.length}`);\n dumpSlices(this.queuedSlices, 'matchSlicesWithSandboxes(top): this.queuedSlices');\n dumpSandboxes(this.assignedSandboxes, 'matchSlicesWithSandboxes(top): this.assignedSandboxes');\n dumpSandboxes(this.readiedSandboxes, 'matchSlicesWithSandboxes(top): this.readiedSandboxes');\n }\n\n // Partition slices to be parameterized by slice.jobAddress.\n let counter = 0, readyCounter = 0, newCounter = 0;\n while (this.queuedSlices.length > 0) {\n const slice = this.queuedSlices.pop();\n if (!slice.isUnassigned) continue;\n if (!jobSliceMap[slice.jobAddress]) jobSliceMap[slice.jobAddress] = [];\n jobSliceMap[slice.jobAddress].push(slice);\n if (++counter >= numSlices) break;\n }\n counter = 0;\n\n for (const [jobAddress, slicesForJob] of Object.entries(jobSliceMap)) {\n jobs.push(jobAddress);\n\n // First get all assigned sandboxes.\n const assignedSandboxesForJob = this.assignedSandboxes.filter(sandbox => sandbox.jobAddress === jobAddress);\n\n while (assignedSandboxesForJob.length > 0 && slicesForJob.length > 0) {\n counter++;\n const sandbox = assignedSandboxesForJob.pop();\n this.removeElement(this.assignedSandboxes, sandbox);\n const slice = slicesForJob.pop();\n assert(slice.jobAddress === sandbox.jobAddress);\n debugging() && console.log(`matchSlicesWithSandboxes: assigned sb matched ${this.dumpSandboxAndSlice(sandbox, slice)}`);\n sandboxSlices.push({ sandbox, slice });\n }\n\n // Next get existing and possibly new ready-for-assign sandboxes.\n while (slicesForJob.length > 0) {\n while (this.readiedSandboxes.length > 0 && slicesForJob.length > 0) {\n readyCounter++;\n const slice = slicesForJob.pop();\n const sandbox = this.readiedSandboxes.pop();\n debugging() && console.log(`matchSlicesWithSandboxes: readied sb matched ${this.dumpSandboxAndSlice(sandbox, slice)}`);\n sandboxSlices.push({ sandbox, slice });\n }\n\n if (slicesForJob.length > 0) {\n newCounter++;\n await this.readySandboxes(slicesForJob.length);\n } else {\n readyCounter -= newCounter;\n break;\n }\n }\n }\n\n debugging() && console.log(`matchSlicesWithSandboxes: matches: ${ JSON.stringify(sandboxSlices.map(ss => this.dumpSandboxSlice(ss))) }`);\n\n // Until this is rock-solid-stable I want to check for uniquenees.\n if (true) {\n this.dumpSandboxSlicesIfNotUnique(sandboxSlices, `DANGER: sandboxSlices; { sandbox, slice } pairs are not unique!`);\n }\n\n // 'false' is so it is easy to turn back to 'true'\n // Since we don't know yet whether these changes are rock-solid-stable I want to keep easy-to-debug stuff like this.\n if ( false || debugging()) {\n console.log(`matchSlicesWithSandboxes: found ${sandboxSlices.length} sandboxes for jobs ${JSON.stringify(jobs)}: assigned ${counter}, ready ${readyCounter}, new ${newCounter}`);\n this.dumpSandboxSlices(sandboxSlices, 'matchSlicesWithSandboxes: sandboxSlices');\n dumpSandboxes(this.assignedSandboxes, 'matchSlicesWithSandboxes: this.assignedSandboxes');\n dumpSandboxes(this.readiedSandboxes, 'matchSlicesWithSandboxes: this.readiedSandboxes');\n console.log(`matchSlicesWithSandboxes: remaining readied: ${JSON.stringify(this.readiedSandboxes.map(s => s.id))}`);\n }\n\n sandboxSlices.forEach(sandboxSlice => {\n assert(!sandboxSlice.sandbox.allocated);\n sandboxSlice.sandbox.allocated = true;\n });\n\n } catch (e) {\n console.error(`DANGER matchSlicesWithSandboxes threw exception`, e);\n //throw e; // Should we rethrow?\n } finally {\n this.matching = false;\n }\n\n debugging() && console.log(`${shortTime()} matchSlicesWithSandboxes allocated ${sandboxSlices.length} sandboxes, queuedSlices ${this.queuedSlices.length}, unallocatedSpace ${this.unallocatedSpace}.`);\n\n return sandboxSlices;\n }\n\n /**\n * Searches for a sandbox that is eligible to work on the given slice.\n * It will search for a sandbox in the following order:\n * 1. Try to find an already assigned sandbox for the slice's job\n * 2. Find a ready sandbox that is unassigned\n * 3. Ready a new sandbox and use that\n * @deprecated This is a high-powered foot-gun.\n * @param {Slice} slice The slice for which to find a sandbox.\n * @returns {Promise<Sandbox|null>} Returns the sandbox instance if matched, otherwise returns null.\n */\n async findSandboxForSlice (slice) {\n let sandbox;\n let assignedSandboxesForJob = this.snapshotOfAssignedSandboxes.filter(w => w.jobAddress === slice.jobAddress);\n\n switch (true) {\n case !!(sandbox = assignedSandboxesForJob.pop()):\n case !!(sandbox = this.snapshotOfReadiedSandboxes.pop()):\n case !!(sandbox = (await this.readySandboxes(1)).pop()):\n return sandbox;\n default:\n return null;\n }\n }\n\n /**\n * Given an assigned sandbox, it tries to find a slice that the sandbox can work on.\n * Unused.\n * @deprecated Foot-gun.\n * @param {Sandbox} sandbox The assigned sandbox\n * @returns {Slice|undefined} Returns a slice in case of a match, undefined otherwise\n */\n findSliceForSandbox (sandbox) {\n return this.queuedSlices.find((slice) => slice.jobAddress === sandbox.jobAddress);\n }\n\n /**\n * This method will call @this.startSandboxWork(sandbox, slice) for { sandbox, slice }\n * in the array returned by @this.matchSlicesWithSandboxes(availableSandboxes) until all sandboxes are working.\n * It is possible for a sandbox to finish simultaneously and leave a sandbox that is not working.\n * Note: @this.queuedSlices may be exhausted before all sandboxes are working.\n * @returns {Promise<void>}\n */\n async distributeQueuedSlices () {\n //\n // All matching of sandbox with slice is taken care of by @this.matchSlicesWithSandboxes().\n // We do not use @this.snapshotOfQueuedSlices becuase it is in a fuzzy state; when matched with a sandbox,\n // a slice transitions to working at an unknown point in the future.\n // Instead we use @this.queuedSlices which really is a queue, whose elements are\n // dequeued in @this.matchSlicesWithSandboxes() and enqueued in fetchTask.\n // We should try to get rid of @this.slices, because it has low utility.\n // However, to minimize churn, this can be done later.\n //\n\n const availableSandboxes = this.unallocatedSpace;\n if (availableSandboxes <= 0) return;\n\n debugging() && console.log(`distributeQueuedSlices: unallocatedSpace ${availableSandboxes}, readied ${JSON.stringify(this.readiedSandboxes.map(s => s.id))}, queuedSlices ${this.queuedSlices.length}`);\n\n const sandboxSlices = await this.matchSlicesWithSandboxes(availableSandboxes);\n\n for (let sandboxSlice of sandboxSlices) {\n\n const { sandbox, slice } = sandboxSlice;\n if (sandbox) {\n debugging() && console.log(`${shortTime()}: distributeQueuedSlices: matched slice ${slice.sliceNumber} with sandbox ${sandbox.id} for job ${slice.jobAddress}, total sandbox count: ${this.sandboxes.length}`);\n if (sandbox.isReadyForAssign) {\n try {\n let timeoutMs = Math.floor(Math.min(+Supervisor.lastAssignFailTimerMs || 0, 10 * 60 * 1000 /* 10m */));\n await asleepMs(timeoutMs);\n if (sandbox.isReadyForAssign) { // Don't need this double-check anymore because of @this.matchSlicesWithSandboxes().\n sandbox.setIsAssigning(); // Don't need this either --> Set the state to assigning before the await to circumvent the event loop problem from await.\n await this.assignJobToSandbox(sandbox, slice.jobAddress);\n } else {\n return // Don't need this either --> The sandbox was picked a second time while it was being prepared, return without an error\n }\n } catch (e) {\n console.error(`Could not assign slice ${slice.sliceNumber} for job ${slice.jobAddress} to sandbox ${sandbox.id}`);\n Supervisor.lastAssignFailTimerMs = Supervisor.lastAssignFailTimerMs ? +Supervisor.lastAssignFailTimerMs * 1.25 : Math.random() * 200;\n this.returnSlice(slice);\n continue;\n }\n }\n\n if (!Supervisor.lastAssignFailTimerMs)\n Supervisor.lastAssignFailTimerMs = Math.random() * 200;\n this.startSandboxWork(sandbox, slice);\n Supervisor.lastAssignFailTimerMs = false;\n\n } else {\n // We should never get here.\n console.error(\"Supervisor.distributeQueuedSlices: Failed to find sandbox for slice\", {\n jobAddress: slice.jobAddress,\n sliceNumber: slice.sliceNumber\n });\n }\n }\n }\n\n /**\n *\n * @param {Sandbox} sandbox\n * @param {opaqueId} jobAddress\n * @returns {Promise<void>}\n */\n async assignJobToSandbox(sandbox, jobAddress) {\n var ceci = this;\n\n try {\n return sandbox.assign(jobAddress)\n } catch(error) {\n // return slice to scheduler, log error\n console.error('Supervisor.assignJobToSandbox: Failed to assign job to sandbox.', {\n jobAddress: jobAddress.substr(0,10),\n error,\n });\n\n await ceci.returnSandbox(sandbox);\n\n throw error;\n }\n }\n \n /**\n * Gives a slice to a sandbox which begins working. Handles collecting\n * the slice result (complete/fail) from the sandbox and submitting the result to the scheduler.\n * It will also return the sandbox to @this.returnSandbox when completed so the sandbox can be re-assigned.\n *\n * @param {Sandbox} sandbox - the sandbox to give the slice\n * @param {Slice} slice - the slice to distribute\n * @returns {Promise<void>} Promise returned from sandbox.run\n */\n async startSandboxWork (sandbox, slice) {\n var startDelayMs;\n\n try {\n slice.markAsWorking();\n } catch (e) {\n // This will occur when the same slice is distributed twice.\n // It is normal because two sandboxes could finish at the same time and be assigned the\n // same slice before the slice is marked as working.\n debugging() && console.debug(e);\n return;\n }\n\n // sandbox.requiresGPU = slice.requiresGPU;\n // if (sandbox.requiresGPU) {\n // this.GPUsAssigned++;\n // }\n\n if (Supervisor.startSandboxWork_beenCalled)\n startDelayMs = 1000 * (tuning.minSandboxStartDelay + (Math.random() * (tuning.maxSandboxStartDelay - tuning.minSandboxStartDelay)));\n else {\n startDelayMs = 1000 * tuning.minSandboxStartDelay;\n Supervisor.startSandboxWork_beenCalled = true;\n }\n \n try {\n debugging() && console.log(`${shortTime()} startSandboxWork: started sandbox.id.sliceNumber ${sandbox.id}.${slice.sliceNumber}.${slice.jobAddress}, total sandbox count: ${this.sandboxes.length}`);\n let result = await sandbox.work(slice, startDelayMs);\n slice.collectResult(result, true);\n // In watchdog, all sandboxes in 'working' state, have their status sent to result submitter.\n // However, this can happen after the sandbox/slice has already sent results\n // to result submitter, in which case, the activeSlices table has already removed the row\n // corresponding to slice and hence is incapable of updating status.\n sandbox.changeWorkingToAssigned();\n sandbox.allocated = false; // This is a little redundant: don't await any promises between here and the finally clause.\n this.assignedSandboxes.push(sandbox);\n debugging() && console.log(`${shortTime()} startSandboxWork: completed sandbox.id.sliceNumber ${sandbox.id}.${slice.sliceNumber}.${slice.jobAddress}, total sandbox count: ${this.sandboxes.length}`);\n } catch(error) {\n let logLevel;\n if (error instanceof SandboxError) {\n logLevel = 'warn';\n // The message and stack properties of error objects are not enumerable,\n // so they have to be copied into a plain object this way\n const errorResult = Object.getOwnPropertyNames(error).reduce((o, p) => {\n o[p] = error[p]; return o;\n }, { message: 'Unexpected worker error' });\n slice.collectResult(errorResult, false);\n } else {\n logLevel = 'error';\n // This error was unrelated to the work being done,\n // so just return the slice in the finally block.\n }\n\n if (error.errorCode === 'EUNCAUGHTERROR') {\n console[logLevel](`Supervisor.startSandboxWork: Uncaught error in sandbox, could not compute.`, {\n jobAddress: slice.jobAddress.substr(0,10),\n sliceNumber: slice.sliceNumber,\n sandbox: sandbox.id,\n jobName: sandbox.public ? sandbox.public.name : 'unnamed',\n });\n } else {\n // sandbox.public.name is defined in Sandbox.assign.\n // It is possible to get here with !sandbox.public.\n console[logLevel](`Supervisor.startSandboxWork: Sandbox failed (${error.message})`, {\n jobAddress: slice.jobAddress.substr(0,10),\n sliceNumber: slice.sliceNumber,\n sandbox: sandbox.id,\n jobName: sandbox.public ? sandbox.public.name : 'unnamed',\n });\n }\n } finally {\n sandbox.allocated = false;\n if (slice.result) {\n await this.recordResult(slice);\n } else if (slice.error) {\n this.returnSlice(slice);\n } else {\n this.returnSlice(slice);\n }\n await this.returnSandbox(sandbox, slice);\n }\n }\n\n /**\n * Terminates sandboxes and returns slices.\n * Sets the working flag to false, call @this.work to start working again.\n * \n * If forceTerminate is true: Terminates all sandboxes and returns all slices.\n * If forceTerminate is false: Terminates non-working sandboxes and returns queued slices.\n *\n * @param {boolean} [forceTerminate = true] - true if you want to stop the sandboxes from completing their current slice.\n * @returns {Promise<void>}\n */\n async stopWork (forceTerminate = true) {\n if (forceTerminate) {\n let sandbox;\n while (sandbox = this.sandboxes.pop()) {\n sandbox.terminate(false);\n }\n\n await this.returnSlices(this.slices).then(() => {\n this.slices.length = 0;\n this.queuedSlices.length = 0;\n });\n\n this.resultSubmitterConnection.off('close', this.openResultSubmitterConn);\n this.resultSubmitterConnection.close();\n this.resultSubmitterConnection = null;\n } else {\n // Only terminate idle sandboxes and return only queued slices\n let idleSandboxes = this.sandboxes.filter(w => !w.isWorking);\n for (let sandbox of idleSandboxes) {\n sandbox.terminate(false);\n }\n\n let queuedSlices = this.queuedSlices;\n await this.returnSlices(queuedSlices).then(() => {\n this.queuedSlices.length = 0;\n });\n await new Promise((resolve, reject) => {\n let sandboxesRemaining = this.workingSandboxes.length;\n if (sandboxesRemaining === 0)\n {\n resolve();\n }\n // Resolve and finish work once all sandboxes have finished submitting their results.\n this.on('submitFinished', () => {\n sandboxesRemaining--;\n if (sandboxesRemaining === 0)\n {\n console.log('All sandboxes empty, stopping worker and closing all connections');\n resolve();\n }\n });\n })\n this.resultSubmitterConnection.off('close', this.openResultSubmitterConn);\n this.resultSubmitterConnection.close();\n this.resultSubmitterConnection = null;\n }\n\n this.taskDistributorConnection.off('close', this.openTaskDistributorConn);\n this.taskDistributorConnection.close();\n this.taskDistributorConnection = null;\n\n this.packageManagerConnection.off('close', this.openPackageManagerConn);\n this.packageManagerConnection.close();\n this.packageManagerConnection = null;\n\n this.eventRouterConnection.off('close', this.openEventRouterConn);\n this.eventRouterConnection.close();\n this.eventRouterConnection = null;\n\n this.emit('stop');\n }\n\n /**\n * Takes a slice and returns it to the scheduler to be redistributed.\n * Usually called when the supervisor told it to forcibly stop working.\n *\n * @param {Slice} slice - the slice to return to the scheduler\n * @returns {Promise<*>} - response from the scheduler the slice was returned to\n */\n returnSlice (slice) {\n // Remove the slice from the slices array.\n this.removeElement(this.slices, slice);\n\n const jobAddress = slice.jobAddress;\n const sliceNumber = slice.sliceNumber;\n const authorizationMessage = this.checkAuthorization('Supervisor.returnSlice:', jobAddress, sliceNumber);\n if (!authorizationMessage) return undefined;\n\n let payload = {};\n if (slice.error) {\n payload = slice.getReturnMessagePayload(this.workerOpaqueId, authorizationMessage, 'uncaught');\n } else {\n payload = slice.getReturnMessagePayload(this.workerOpaqueId, authorizationMessage);\n }\n\n return this.resultSubmitterConnection.send('status', payload)\n .then(response => {\n return response;\n })\n .catch(error => {\n console.error('Failed to return slice', {\n sliceIdentifier: slice.identifier,\n sliceNumber: sliceNumber,\n jobAddress: jobAddress,\n error,\n });\n })\n }\n\n /** Bulk-return multiple slices, possibly for assorted jobs\n */\n returnSlices(slices) {\n const slicePayload = [];\n\n slices.forEach(slice => {\n addToSlicePayload(slicePayload, slice.jobAddress, slice.sliceNumber, 'return', this.checkAuthorization('Supervisor.returnSlices:', slice.jobAddress, slice.sliceNumber));\n });\n\n return this.resultSubmitterConnection.send('status', {\n worker: this.workerOpaqueId,\n slices: slicePayload,\n });\n }\n\n /**\n * Submits the slice results to the scheduler, either to the\n * work submit or fail endpoints based on the slice status.\n * Then remove the slice from the @this.slices cache.\n *\n * @param {Slice} slice - the slice to submit\n * @returns {Promise<void>}\n */\n async recordResult (slice) {\n const metrics = { GPUTime: 0, CPUTime: 0, CPUDensity: 0, GPUDensity: 0 }; \n \n debugging('supervisor') && console.log('supervisor: recording result');\n assert(slice && slice.hasOwnProperty('result'), slice.sliceNumber, slice.jobAddress);\n\n const jobAddress = slice.jobAddress;\n const sliceNumber = slice.sliceNumber;\n const authorizationMessage = this.checkAuthorization('Supervisor.recordResult:', jobAddress, sliceNumber);\n if (!authorizationMessage) return; // Question: Is it better to proceed and have a cascade of errors?\n\n /* @see result-submitter::result for full message details */\n var payloadData = {\n slice: sliceNumber,\n job: jobAddress,\n worker: this.workerOpaqueId,\n paymentAddress: this.paymentAddress,\n metrics,\n authorizationMessage,\n }\n const isValidTimeReport = slice.result && slice.result.timeReport && slice.result.timeReport.total > 0;\n if (isValidTimeReport) {\n const timeReport = slice.result.timeReport;\n assert(timeReport.total);\n metrics.GPUTime = timeReport.webGL;\n metrics.CPUTime = timeReport.total - (metrics.GPUTime + timeReport.idle);\n metrics.CPUDensity = metrics.CPUTime / timeReport.total;\n metrics.GPUDensity = metrics.GPUTime / timeReport.total;\n }\n \n this.emit('submittingResult');\n\n if (!slice.isFinished)\n throw new Error('Cannot record result for slice that is not finished');\n\n if (slice.resultStorageDetails === 'pattern') /* this is a remote-storage slice */\n payloadData.result = await sendResultToRemote(slice);\n else {\n // It is possible for slice.result to be undefined when there are upstream errors.\n if (!slice.result)\n throw new Error('slice.result is undefined. This is ok when there are upstream errors.');\n payloadData.result = encodeDataURI(slice.result.result); /* XXXwg - result.result is awful */\n }\n\n try {\n if (slice.completed) {\n\n /* work function returned a result */\n const { success, payload } = await this.resultSubmitterConnection.send(\n 'result',\n payloadData,\n );\n\n if (!success) {\n throw new DCPError('Unable to submit result for work done', payload);\n }\n\n const receipt = {\n accepted: true,\n payment: payload.slicePaymentAmount,\n };\n this.emit('submittedResult', payload);\n this.emit('dccCredit', receipt);\n } else {\n\n /* slice did not complete for some reason */\n let statusPayloadData = {\n worker: this.workerOpaqueId,\n slices: [\n {\n sliceNumber: sliceNumber,\n job: jobAddress,\n status: 'return', // special state looked for in status.js / result-submitter\n reason: 'uncaught', // special state looked for in status.js / result-submitter\n /** @todo XXXpfr: Is there error info we can use here? */ \n // error: slice.error, \n authorizationMessage,\n }\n ], \n };\n \n await this.resultSubmitterConnection.send('status', statusPayloadData);\n }\n } catch(error) {\n console.info(`1014: Failed to submit results for slice ${payloadData.slice} of job ${payloadData.job}`, error);\n this.emit('submitSliceFailed', error);\n }\n finally\n {\n this.emit('submitFinished');\n // Remove the slice from the slices array\n this.removeElement(this.slices, slice);\n }\n }\n}\n\n/** \n * Send a work function's result to a server that speaks our DCP Storage Server protocol.\n * A sample storage server has been implemented in https://gitlab.com/Distributed-Compute-Protocol/dcp-storage-server\n * as part of DCP-1479.\n *\n * @param {Slice} slice slice object whose result we are sending\n * @returns {Promise<object>} object with properties contentType and uri\n * @throws if the remote server returned a non-ok status object, or an HTTP status not in the 2xx range.\n */\nasync function sendResultToRemote(slice) {\n const postParams = {\n ...JSON.parse(slice.resultStorageParams), // These come in as a String from the db currently, @todo - output as an object\n };\n\n const sliceResultUri = makeSliceURI('pattern', slice.resultStorageDetails, {\n slice: slice.sliceNumber,\n job: slice.jobAddress,\n //juuid: slice.job.uuid -- what should be here?\n });\n\n const url = new DcpURL(sliceResultUri);\n\n if (dcpConfig.worker.allowOrigins.any.indexOf(url.origin) === -1 &&\n dcpConfig.worker.allowOriginResultStorage.indexOf(url.origin) === -1) {\n throw new Error(`Invalid origin for remote result storage: '${url.origin}'`);\n }\n \n postParams.slice = slice.sliceNumber;\n postParams.job = slice.jobAddress;\n postParams.contentType = 'application/json'; // Currently data will be outputed as a JSON object, @todo: Support file upload\n\n if (slice.result.result)\n postParams.result = slice.result.result;\n else\n postParams.error = new Error('no result'); /** @todo XXXwg return the error object from the sandbox */\n\n const formBodyArray = [];\n for (const property in postParams) {\n const encodedKey = encodeURIComponent(property);\n const encodedValue = encodeURIComponent(postParams[property]);\n formBodyArray.push(`${encodedKey}=${encodedValue}`);\n }\n const formBody = formBodyArray.join('&');\n\n let postPromise = new Promise(function supervisorPostResult(resolve, reject) {\n let deeperErrorStack = new Error().stack;\n deeperErrorStack = deeperErrorStack.substring(deeperErrorStack.indexOf('\\n') + 1);\n const xhr = new XMLHttpRequest();\n xhr.onloadend = function supervisor$$recordResult$onloadend() {\n try {\n let o;\n \n delete xhr.onloadend;\n if (xhr.status >= 200 && xhr.status < 300) {\n /* Successful post: record the resultant URL; needed for dcp-client application to be able to fetch the result */\n switch(xhr.getResponseHeader('content-type'))\n {\n default: /* support lousy web servers */\n case 'application/json': {\n o = JSON.parse(xhr.responseText);\n break;\n }\n case 'application/x-kvin': {\n o = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\").deserialize(xhr.responseText);\n break;\n }\n }\n\n if (o.status === 'ok')\n return resolve(o);\n \n return reject(new Error(`Remote server ${url} did not set status=ok (o.error ? ${JSON.stringify(o)} : error)`));\n } else {\n const error = new Error(`HTTP Error ${xhr.status} sending slice results to ${url}`);\n //\n // Note: This impl is mostly grafted from justFetch in dcp/src/protocol-v3/index.\n // It is a safe assumption that this function has never been tested.\n // Refine the following error info when we support remote-storage.\n //\n error.request = xhr;\n error.request.location = url;\n error.request.method = 'POST';\n error.request.status = xhr.status;\n error.stack += '\\n----------\\n' + deeperErrorStack;\n throw error;\n }\n } catch (e) {\n return reject(e);\n }\n\n return reject(new Error('impossible'));\n }\n \n xhr.onreadystatechange = function() {\n if (this.readyState == 4 && this.status == 200) {\n debugging('supervisor') && console.log('Response text', this.responseText);\n }\n }\n \n xhr.open('POST', url.href, true);\n xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');\n xhr.send(formBody);\n });\n\n return postPromise;\n}\n\n/**\n * Return DCPv4-specific connection options, composed of type-specific, URL-specific, \n * and worker-specific options, any/all of which can override the dcpConfig.dcp.connectOptions.\n * The order of precedence is the order of specificity.\n */\nfunction connectionOptions(url, label) {\n return leafMerge(/* ordered from most to least specific */\n dcpConfig.worker.dcp.connectionOptions.default,\n dcpConfig.worker.dcp.connectionOptions[label],\n dcpConfig.worker.dcp.connectionOptions[url.href]);\n}\n\n\n/**\n * Add a slice to the slice payload being built. If a sliceList already exists for the \n * job-status-authMessage tuple, then the slice will be added to that, otherwise a new \n * sliceList will be added to the payload.\n *\n * @param {Object[]} slicePayload Slice payload being built. Will be mutated in place.\n * @param {Address} job Address of job the slice belongs to\n * @param {Number} sliceNumber Slice number\n * @param {String} status Status update, eg. progress or scheduled\n * @param {Object} authorizationMessage authorizationMessage for the slice\n *\n * @returns {Object[]} mutated slicePayload array\n */\nfunction addToSlicePayload(slicePayload, job, sliceNumber, status, authorizationMessage) {\n // Try to find a sliceList in the payload which matches the job, status, and auth message\n let sliceList = slicePayload.find(desc => {\n return desc.job === job && desc.status === status && desc.authorizationMessage === authorizationMessage;\n });\n\n // If we didn't find a sliceList, start a new one and add it to the payload\n if (!sliceList) {\n sliceList = {\n job,\n sliceNumbers: [],\n status,\n authorizationMessage: authorizationMessage,\n };\n slicePayload.push(sliceList);\n }\n\n sliceList.sliceNumbers.push(sliceNumber);\n\n return slicePayload;\n}\n\n\n/** @type {number | boolean} */\nSupervisor.lastAssignFailTimerMs = false;\n/** @type {boolean} */\nSupervisor.startSandboxWork_beenCalled = false;\n\nexports.Supervisor = Supervisor;\n\n\n//# sourceURL=webpack:///./src/dcp-client/worker/supervisor.js?");
|
|
5406
|
+
eval("/**\n * @file worker/supervisor.js\n *\n * The component that controls each of the sandboxes\n * and distributes work to them. Also communicates with the\n * scheduler to fetch said work.\n *\n * The supervisor readies sandboxes before/while fetching slices.\n * This means sometimes there are extra instantiated WebWorkers\n * that are idle (in this.readiedSandboxes). Readied sandboxes can\n * be used for any slice. After a readied sandbox is given a slice\n * it becomes assigned to slice's job and can only do work\n * for that job.\n *\n * After a sandbox completes its work, the sandbox becomes cached\n * and can be reused if another slice with a matching job is fetched.\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * Ryan Rossiter, ryan@kingsds.network\n * @date May 2019\n */\n\n/* global dcpConfig */\n// @ts-check\n\n\nconst nanoid = __webpack_require__(/*! nanoid/non-secure */ \"./node_modules/nanoid/non-secure/index.js\");\nconst constants = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst hash = __webpack_require__(/*! dcp/common/hash */ \"./src/common/hash.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\n\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('worker');\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { Sandbox, SandboxError } = __webpack_require__(/*! ./sandbox */ \"./src/dcp-client/worker/sandbox.js\");\nconst { Slice } = __webpack_require__(/*! ./slice */ \"./src/dcp-client/worker/slice.js\");\nconst { SupervisorCache } = __webpack_require__(/*! ./supervisor-cache */ \"./src/dcp-client/worker/supervisor-cache.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { localStorage } = __webpack_require__(/*! dcp/common/dcp-localstorage */ \"./src/common/dcp-localstorage.js\");\nconst { booley, encodeDataURI, makeSliceURI, leafMerge, asleepMs, shortTime,\n dumpSandboxes, dumpSlices, dumpSandboxesIfNotUnique, dumpSlicesIfNotUnique } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst { calculateJoinHash } = __webpack_require__(/*! dcp/dcp-client/compute-groups */ \"./src/dcp-client/compute-groups/index.js\");\nconst debug = (...args) => {\n if (debugging('supervisor')) {\n console.debug('dcp-client:worker:supervisor', ...args);\n }\n};\n\nconst supervisorTuning = dcpConfig.future('supervisor.tuning');\nconst tuning = {\n watchdogInterval: 7, /**< seconds - time between fetches when ENOTASK(? /wg nov 2019) */\n minSandboxStartDelay: 0.1, /**< seconds - minimum time between WebWorker starts */\n maxSandboxStartDelay: 0.7, /**< seconds - maximum delay time between WebWorker starts */\n ...supervisorTuning\n};\n\n/** Make timers 10x slower when running in niim */\nlet timeDilation = 1;\nif (DCP_ENV.platform === 'nodejs') {\n /** Make timers 10x slower when running in niim */\n timeDilation = (requireNative('module')._cache.niim instanceof requireNative('module').Module) ? 10 : 1;\n}\n\ndcpConfig.future('worker.sandbox', { progressReportInterval: (5 * 60 * 1000) });\nconst sandboxTuning = dcpConfig.worker.sandbox;\n\n/**\n * @typedef {*} opaqueId\n */\n\n/**\n * @typedef {object} SandboxSlice\n * @property {Sandbox} sandbox\n * @property {Slice} slice\n */\n\n/**\n * @typedef {object} Signature\n * @property {Uint8Array} r\n * @property {Uint8Array} s\n * @property {Uint8Array} v\n */\n\n/**\n * @typedef {object} SignedAuthorizationMessageObject\n * @property {object} auth\n * @property {Signature} signature\n * @property {module:dcp/wallet.Address} owner\n */\n\n/** @typedef {import('.').Worker} Worker */\n/** @typedef {import('.').SupervisorOptions} SupervisorOptions */\n\nclass Supervisor extends EventEmitter {\n /**\n * @constructor\n * @param {Worker} worker\n * @param {SupervisorOptions} options\n */\n constructor (worker, options={}) {\n super('Supervisor');\n\n /** @type {Worker} */\n this.worker = worker;\n\n /** @type {Sandbox[]} */\n this.sandboxes = [];\n\n /**\n * XXXpfr @todo We may still needs this.slices to communicate state to result-submitter status endpoint, but it isn't used otherwise.\n * @deprecated\n * @type {Slice[]}\n */\n this.slices = [];\n\n /**\n * The true type is associative array of associative array of SignedAuthorizationMessageObject.\n * How can that be specified in jsdoc?\n * @type {object}\n */\n this.authorizationMessages = {};\n\n /** @type {Slice[]} */\n this.queuedSlices = [];\n\n /** @type {Sandbox[]} */\n this.readiedSandboxes = [];\n\n /** @type {Sandbox[]} */\n this.assignedSandboxes = [];\n\n /** @type {boolean} */\n this.matching = false;\n\n if (!options) {\n console.error('Supervisor Options', options, new Error().stack);\n options = {};\n }\n \n /** @type {object} */\n this.options = {\n jobAddresses: options.jobAddresses || [/* all jobs unless priorityOnly */],\n ...options,\n };\n\n const { paymentAddress, identityKeystore } = options;\n if (paymentAddress) {\n if (paymentAddress instanceof wallet.Keystore) {\n this.paymentAddress = paymentAddress.address;\n } else {\n this.paymentAddress = new wallet.Address(paymentAddress);\n }\n } else {\n this.paymentAddress = null;\n }\n\n this._identityKeystore = identityKeystore;\n\n this.allowedOrigins = dcpConfig.worker.allowOrigins.any;\n if(options.allowedOrigins && options.allowedOrigins.length > 0)\n this.allowedOrigins = options.allowedOrigins.concat(this.allowedOrigins);\n\n /**\n * Maximum sandboxes allowed to work at a given time.\n * @type {number}\n */\n this.maxWorkingSandboxes = options.maxWorkingSandboxes || 1;\n\n /** @type {number} */\n this.defaultMaxGPUs = 1;\n // this.GPUsAssigned = 0;\n \n // Object.defineProperty(this, 'GPUsAssigned', {\n // get: () => this.workingSandboxes.filter(sb => !!sb.requiresGPU).length,\n // enumerable: true,\n // configurable: false,\n // });\n\n /** @type {boolean} */\n this.isFetchingNewWork = false;\n /**\n * TODO: Remove this when the supervisor sends all of the sandbox\n * capabilities to the scheduler when fetching work.\n * @type {object}\n */\n this.capabilities = null;\n\n // This sets an offset into the watchdog bin at which to fire the sweeper\n /** @deprecated - UNUSED */\n this.watchdogSlotTime = options.watchdogInterval || tuning.watchdogInterval * 1000;\n /** @deprecated - UNUSED */\n this.watchdogOffset = Math.random();\n /** @deprecated - UNUSED */\n this.watchdogTimeout = null;\n\n /** @type {number} */\n this.lastProgressReport = 0;\n\n // @hack - dcp-env.isBrowserPlatform is not set unless the platform is _explicitly_ set,\n // using the default detected platform doesn't set it.\n // Fixing that causes an error in the wallet module's startup on web platform, which I\n // probably can't fix in a reasonable time this morning.\n // ~ER2020-02-20\n\n if (!options.maxWorkingSandboxes\n && DCP_ENV.browserPlatformList.includes(DCP_ENV.platform)\n && navigator.hardwareConcurrency > 1) {\n this.maxWorkingSandboxes = navigator.hardwareConcurrency - 1;\n if (typeof navigator.userAgent === 'string') {\n if (/(Android).*(Chrome|Chromium)/.exec(navigator.userAgent)) {\n this.maxWorkingSandboxes = 1;\n console.log('Doing work with Chromimum browsers on Android is currently limited to one sandbox');\n }\n }\n }\n\n /** @type {SupervisorCache} */\n this.cache = new SupervisorCache(this);\n /** @type {object} */\n this._connections = {}; /* active DCPv4 connections */\n // Call the watchdog every 7 seconds.\n this.watchdogInterval = setInterval(() => this.watchdog(), 7000);\n\n const ceci = this;\n\n // Initialize to null so these properties are recognized for the Supervisor class\n this.taskDistributorConnection = null;\n this.eventRouterConnection = null;\n this.resultSubmitterConnection = null;\n this.packageManagerConnection = null;\n this.openTaskDistributorConn = function openTaskDistributorConn()\n {\n let config = dcpConfig.scheduler.services.taskDistributor;\n ceci.taskDistributorConnection = new protocolV4.Connection(config, ceci.identityKeystore, connectionOptions(config.location, 'taskDistributor'));\n ceci.taskDistributorConnection.on('close', ceci.openTaskDistributorConn);\n }\n\n this.openEventRouterConn = function openEventRouterConn()\n {\n let config = dcpConfig.scheduler.services.eventRouter;\n ceci.eventRouterConnection = new protocolV4.Connection(config, ceci.identityKeystore, connectionOptions(config.location, 'eventRouter'));\n ceci.eventRouterConnection.on('close', ceci.openEventRouterConn);\n }\n \n this.openResultSubmitterConn = function openResultSubmitterConn()\n {\n let config = dcpConfig.scheduler.services.resultSubmitter;\n ceci.resultSubmitterConnection = new protocolV4.Connection(config, ceci.identityKeystore, connectionOptions(config.location, 'resultSubmitter'));\n ceci.resultSubmitterConnection.on('close', ceci.openResultSubmitterConn);\n }\n\n this.openPackageManagerConn = function openPackageManagerConn()\n {\n let config = dcpConfig.packageManager;\n ceci.packageManagerConnection = new protocolV4.Connection(config, ceci.identityKeystore, connectionOptions(config.location, 'packageManager'));\n ceci.packageManagerConnection.on('close', ceci.openPackageManagerConn);\n }\n }\n\n /**\n * Return worker opaqueId.\n * @type {opaqueId}\n */\n get workerOpaqueId() {\n if (!this._workerOpaqueId)\n this._workerOpaqueId = localStorage.getItem('workerOpaqueId');\n\n if (!this._workerOpaqueId || this._workerOpaqueId.length !== 22) { /** @todo XXXwg fix magic number problem */\n this._workerOpaqueId = nanoid(22);\n localStorage.setItem('workerOpaqueId', this._workerOpaqueId);\n }\n\n return this._workerOpaqueId;\n }\n\n /**\n * This getter is the absolute source-of-truth for what the\n * identity keystore is for this instance of the Supervisor.\n */\n get identityKeystore() {\n assert(this.defaultIdentityKeystore);\n\n return this._identityKeystore || this.defaultIdentityKeystore;\n }\n\n /**\n * Open all connections. Used when supervisor is instantiated or stopped/started\n * to initially open connections.\n */\n instantiateAllConnections() {\n this.openTaskDistributorConn();\n this.openEventRouterConn();\n this.openResultSubmitterConn();\n this.openPackageManagerConn();\n }\n\n /** Set the default identity keystore -- needs to happen before anything that talks\n * to the scheduler for work gets called. This is a wart and should be removed by\n * refactoring.\n *\n * The default identity keystore will be used if the Supervisor was not provided\n * with an alternate. This keystore will be located via the Wallet API, and \n * if not found, a randomized default identity will be generated. \n *\n * @param {object} ks An instance of wallet::Keystore -- if undefined, we pick the best default we can.\n * @returns {Promise<void>}\n */\n async setDefaultIdentityKeystore(ks) {\n try {\n if (ks) {\n this.defaultIdentityKeystore = ks;\n return;\n }\n\n if (this.defaultIdentityKeystore)\n return;\n\n try {\n this.defaultIdentityKeystore = await wallet.getId();\n } catch(e) {\n debugging('supervisor') && console.debug('supervisor: generating default identity', this.defaultIdentityKeystore.address);\n this.defaultIdentityKeystore = await new wallet.IdKeystore(null, '');\n }\n } finally {\n debugging('supervisor') && console.debug('supervisor: set default identity =', this.defaultIdentityKeystore.address);\n }\n }\n \n //\n // What follows is a bunch of utility functions for creating filtered views\n // of the slices and sandboxes array.\n //\n\n /**\n * Sandboxes that are in WORKING state.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Sandbox[]}\n */\n get workingSandboxes() {\n return this.sandboxes.filter(sandbox => sandbox.isWorking);\n }\n\n /**\n * This property is used as the target number of sandboxes to be associated with slices and transition sandbox-state from ASSIGNED -> WORKING.\n *\n * It is used in this.watchdog as to prevent a call to this.work when unallocatedSpace <= 0.\n * It is also used in this.distributeQueuedSlices where it is passed as an argument to this.matchSlicesWithSandboxes to indicate how many sandboxes\n * to associate with slices and transition sandbox-state from ASSIGNED -> WORKING.\n *\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {number}\n */\n get unallocatedSpace() {\n return this.maxWorkingSandboxes - this.sandboxes.filter(sandbox => sandbox.allocated).length;\n }\n\n /**\n * Slices that have SLICE_STATUS_UNASSIGNED status.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Slice[]}\n */\n get snapshotOfQueuedSlices() {\n return this.slices.filter(slice => slice.isUnassigned);\n }\n\n /**\n * Slices that have SLICE_STATUS_WORKING status.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Slice[]}\n */\n get snapshotOfWorkingSlices() {\n return this.slices.filter(slice => slice.isWorking)\n }\n\n /**\n * Sandboxes that are in READY_FOR_ASSIGN state.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Sandbox[]}\n */\n get snapshotOfReadiedSandboxes() {\n return this.sandboxes.filter(sandbox => sandbox.isReadyForAssign);\n }\n\n /**\n * Sandboxes that are in ASSIGNED state.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Sandbox[]}\n */\n get snapshotOfAssignedSandboxes() {\n return this.sandboxes.filter(sandbox => sandbox.isAssigned);\n }\n\n /**\n * Sandboxes that are in TERMINATED state.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Sandbox[]}\n */\n get snapshotOfTerminatedSandboxes() {\n return this.sandboxes.filter(sandbox => sandbox.isTerminated);\n }\n\n /**\n * Remove element from theArray.\n * @parameter {Array<*>} theArray\n * @parameter {object|number} element\n * @parameter {boolean} [assertExists = true]\n */\n removeElement(theArray, element, assertExists = false) {\n let index = theArray.indexOf(element);\n assert(index !== -1 || !assertExists);\n if (index !== -1) theArray.splice(index, 1);\n }\n\n /**\n * Check authorization message wrt jobAddress and sliceNumber.\n * @parameter {string} text\n * @parameter {opaqueId} jobAddress\n * @parameter {number} sliceNumber\n * @returns {SignedAuthorizationMessageObject}\n */\n checkAuthorization(text, jobAddress, sliceNumber) {\n const authorizationMessage = this.authorizationMessages[jobAddress][sliceNumber];\n if (!authorizationMessage)\n console.log(`\\t${text} DANGER: Undefined authorization. ${jobAddress}, ${sliceNumber}`);\n return authorizationMessage;\n }\n\n /**\n * If the elements of sandboxSliceArray are not unique, log the duplicates and dump the array.\n * @parameter {SandboxSlice[]} sandboxSliceArray\n * @parameter {string} header\n */\n dumpSandboxSlicesIfNotUnique(sandboxSliceArray, header) {\n if (!this.isUniqueSandboxSlices(sandboxSliceArray, header))\n this.dumpSandboxSlices(sandboxSliceArray);\n }\n\n /**\n * Log sandboxSlice.\n * @parameter {SandboxSlice} sandboxSlice\n * @returns {string}\n */\n dumpSandboxSlice(sandboxSlice) {\n return `${sandboxSlice.sandbox.id}.${sandboxSlice.slice.sliceNumber}.${sandboxSlice.slice.jobAddress}`;\n }\n\n /**\n * Log { sandbox, slice }.\n * @parameter {Sandbox} sandbox\n * @parameter {Slice} slice\n * @returns {string}\n */\n dumpSandboxAndSlice(sandbox, slice) {\n return this.dumpSandboxSlice({ sandbox, slice });\n }\n\n /**\n * Dump sandboxSliceArray.\n * @parameter {SandboxSlice[]} sandboxSliceArray\n * @parameter {string} header\n */\n dumpSandboxSlices(sandboxSliceArray, header) {\n if (header) console.log(`\\n${header}`);\n sandboxSliceArray.forEach(x => console.log(`\\t{ ${x.sandbox.id}.${x.sandbox.jobAddress}.${x.sandbox.state}, ${x.slice.sliceNumber}.${x.slice.jobAddress}.${x.slice.status} }`));\n }\n\n /**\n * Check sandboxSliceArray for duplicates.\n * @parameter {SandboxSlice[]} sandboxSliceArray\n * @parameter {string} header\n * @parameter {function} log\n * @returns {boolean}\n */\n isUniqueSandboxSlices(sandboxSliceArray, header, log) {\n return sandboxSliceArray.length === this.makeUniqueSandboxSlices(sandboxSliceArray, header, log).length;\n }\n\n /**\n * Returns a copy of sandboxSliceArray with all duplicates removed.\n * @parameter {SandboxSlice[]} sandboxSliceArray\n * @parameter {string} header\n * @parameter {function} log\n * @returns {SandboxSlice[]}\n */\n makeUniqueSandboxSlices(sandboxSliceArray, header, log) {\n const result = [], slices = [], sandboxes = [];\n let once = true;\n sandboxSliceArray.forEach(x => {\n const sliceIndex = slices.indexOf(x.slice);\n const sandboxIndex = sandboxes.indexOf(x.sandbox);\n\n if (sandboxIndex >= 0) {\n if (once && header) console.log(`\\n${header}`); once = false;\n log ? log(x.sandbox) : console.log(`\\tDANGER: Found duplicate sandbox ${x.sandbox.id}.${x.sandbox.jobAddress}.${x.sandbox.state}.`);\n } else sandboxes.push(x.sandbox);\n\n if (sliceIndex >= 0) {\n if (once && header) console.log(`\\n${header}`); once = false;\n log ? log(x.slice) : console.log(`\\tDANGER: Found duplicate slice ${x.slice.sliceNumber}.${x.slice.jobAddress}.${x.slice.status}.`);\n } else {\n slices.push(x.slice);\n if (sandboxIndex < 0) result.push(x);\n }\n });\n return result;\n }\n\n /** NOT used ATM, it's faster to just filter the assigned sandboxes for a jobAddress on-demand\n * \n * Bins the assigned sandboxes in an object, keyed by their jobAddress.\n * { 0xgenAddress1: sandboxes[], 0xgenAddress2: sandboxes[], ... }\n * @type {object}\n */\n get assignedSandboxesSorted () {\n return this.assignedSandboxes.reduce((o, w) => {\n if (!w.jobAddress) throw new Error(\"Assigned sandbox doesn't have a job opaque id\", w);\n\n if (w.jobAddress in o) {\n o[w.jobAddress].push(w);\n } else o[w.jobAddress] = [w];\n\n return o;\n }, {});\n }\n\n /**\n * Attempts to create and start a given number of sandboxes.\n * The sandboxes that are created can then be assigned for a\n * specific job at a later time. All created sandboxes\n * get put into the @this.readiedSandboxes array.\n *\n * @param {number} numSandboxes - the number of sandboxes to create\n * @returns {Promise<Sandbox[]>} - resolves with array of created sandboxes, rejects otherwise\n * @throws when given a numSandboxes is not a number > 0 or if numSandboxes is Infinity\n */\n async readySandboxes (numSandboxes) {\n debug('Readying sandboxes');\n if (typeof numSandboxes !== 'number' || Number.isNaN(numSandboxes) || numSandboxes === Infinity) {\n throw new Error(`${numSandboxes} is not a number of sandboxes that can be readied.`);\n }\n if (numSandboxes <= 0) {\n return [];\n }\n\n const sandboxStartPromises = [];\n const sandboxes = [];\n const errors = [];\n for (let i = 0; i < numSandboxes; i++) {\n const sandbox = new Sandbox(this.cache, {\n ...this.options.sandboxOptions,\n }, this.allowedOrigins);\n sandbox.addListener('ready', () => this.emit('sandboxReady', sandbox));\n sandbox.addListener('start', () => {\n this.emit('sandboxStart', sandbox);\n\n // When sliceNumber == 0, result-submitter status skips the slice,\n // so don't send it in the first place.\n // The 'start' event is fired when a worker starts up, hence there's no way\n // to determine whether sandbox has a valid slice without checking.\n if (sandbox.slice) {\n const jobAddress = sandbox.jobAddress;\n const sliceNumber = sandbox.slice.sliceNumber;\n const authorizationMessage = this.checkAuthorization('Supervisor.readySandboxes sandboxStart:', jobAddress, sliceNumber);\n if (!authorizationMessage) return;\n\n this.resultSubmitterConnection.send('status', {\n worker: this.workerOpaqueId,\n slices: [{\n job: jobAddress,\n sliceNumber: sliceNumber,\n status: 'begin',\n authorizationMessage,\n }],\n });\n }\n });\n sandbox.addListener('workEmit', ({ eventName, payload }) => {\n // Need to check if the sandbox hasn't been assigned a slice yet.\n if (!sandbox.slice) {\n if (__webpack_require__(/*! dcp/common/dcp-build */ \"./src/common/dcp-build.js\").build === \"debug\") {\n console.error(\n `Sandbox not assigned a slice before sending workEmit message to scheduler. 'workEmit' event originates from \"${eventName}\" event`, \n payload,\n );\n }\n }\n\n const jobAddress = sandbox.slice.jobAddress;\n const sliceNumber = sandbox.slice.sliceNumber;\n // sliceNumber can be zero if it came from a problem with loading modules.\n assert(jobAddress && (sliceNumber || sliceNumber === 0));\n try {\n /* Make sure we actually have an authorization message at jobAddress and sliceNumber. */\n const authorizationMessage = this.checkAuthorization('Supervisor.readySandboxes workEmit:', jobAddress, sliceNumber);\n if (!authorizationMessage) return;\n\n /* DCP-1698 Send auth msg with tasks to worker, then validate authority of worker to send slice info back to scheduler. */\n this.eventRouterConnection.send('workEmit', {\n eventName,\n payload,\n job: jobAddress,\n slice: sliceNumber,\n worker: this.workerOpaqueId,\n authorizationMessage,\n });\n } catch (error) {\n debugging('supervisor') &&\n console.debug(\n `Authorizing failed: jobAddress: ${jobAddress}, slice: ${sliceNumber}, error: ${error}`,\n );\n }\n });\n\n // When any sbx completes, \n sandbox.addListener('complete', () => {\n // this.distributeQueuedSlices();\n this.watchdog();\n //this.work();\n });\n\n const delayMs =\n 1000 *\n (tuning.minSandboxStartDelay +\n Math.random() *\n (tuning.maxSandboxStartDelay - tuning.minSandboxStartDelay));\n \n sandboxStartPromises.push(\n sandbox\n .start(delayMs)\n .then(() => {\n this.sandboxes.push(sandbox);\n })\n .catch((err) => {\n errors.push(err);\n if(err.code === 'ENOWORKER'){\n throw new DCPError(\"Cannot use localExec without dcp-worker installed. Use the command 'npm install dcp-worker' to install the neccessary modules.\", 'ENOWORKER');\n }\n }));\n sandboxes.push(sandbox);\n }\n\n await Promise.all(sandboxStartPromises);\n\n if (errors.length) {\n console.warn(`Failed to ready ${errors.length} of ${numSandboxes} sandboxes.`, errors);\n throw new Error('Failed to ready sandboxes.');\n }\n\n if (this.readiedSandboxes.length > 0)\n sandboxes.push(...this.readiedSandboxes);\n this.readiedSandboxes = sandboxes;\n\n debugging() && console.log(`readySandboxes: all readied sandboxes ${JSON.stringify(this.readiedSandboxes.map(sandbox => sandbox.id))}`);\n \n return sandboxes;\n }\n\n /**\n * Accepts a sandbox after it has finished working or encounters an error.\n * If the sandbox was terminated or if \"!slice || slice.failed\" then\n * the sandbox will be removed from the sandboxes array and terminated if necessary.\n * Otherwise it will try to distribute a slice to the sandbox immediately.\n *\n * @param {Sandbox} sandbox - the sandbox to return\n * @param {Slice} [slice] - the slice just worked on; !slice => terminate\n * @returns {Promise<void>}\n */\n async returnSandbox (sandbox, slice) {\n if (!slice || slice.failed || sandbox.isTerminated) {\n this.removeElement(this.sandboxes, sandbox);\n if (!sandbox.isTerminated) {\n sandbox.terminate(false);\n }\n }\n\n if (slice) {\n\n const sliceNumber = slice.sliceNumber;\n const jobAddress = slice.jobAddress;\n\n //\n // An upstream failure like certain connection failures, can cause an error cascade,\n // so we need to gaurd against indeterminant state with this conditional.\n // An example of such an error cascade is calling Connection.fetchTransport\n // when this.connectionOptions.transports is empty.\n //\n if (this.authorizationMessages[jobAddress][sliceNumber]) {\n debugging() && console.log(`Supervisor.returnSandbox: ${jobAddress}, ${sliceNumber}, kill corresponding auth message.`);\n /* Done with slice. Cleanup. */\n delete this.authorizationMessages[jobAddress][sliceNumber];\n if (Object.keys(this.authorizationMessages[jobAddress]).length === 0) {\n delete this.authorizationMessages[jobAddress];\n }\n }\n }\n }\n\n /**\n * Terminates sandboxes, in order of creation, when the total started sandboxes exceeds the total allowed sandboxes.\n *\n * @todo Check through the currently started sandboxes and attempt to terminate\n * ones that aren't frequently used.\n * ^^^ This todo may be partially done due to terminating older sandboxes - RR Nov 2019\n * @returns {Promise<void>}\n */\n async pruneSandboxes () {\n let numOver = this.sandboxes.length - (dcpConfig.worker.maxAllowedSandboxes + this.maxWorkingSandboxes);\n if (numOver <= 0 || this.matching) return; // Don't kill sandboxes while matching with slices.\n \n let index = 0;\n while (index < this.readiedSandboxes.length) {\n const sandbox = this.readiedSandboxes[index];\n // if this sbx is allocated, skip it and look at the next one\n if (sandbox.allocated) {\n index++;\n continue;\n }\n // otherwise, remove this one and look at the same array element next loop\n debugging() && console.log(`pruneSandboxes: Killing readied sandbox ${sandbox.id}`);\n this.readiedSandboxes.splice(index, 1);\n await this.returnSandbox(sandbox);\n\n if (--numOver <= 0) break;\n }\n\n\n if (numOver <= 0) return;\n index = 0;\n while (index < this.assignedSandboxes.length) {\n const sandbox = this.assignedSandboxes[index];\n \n // if the sandbox is allocated, advance to the next one in the list\n if (sandbox.allocated) {\n index++;\n continue;\n }\n \n // otherwise, remove this sandbox but look at the same array index in the next loop\n debugging() && console.log(`pruneSandboxes: Killing assigned sandbox ${sandbox.id}`);\n this.assignedSandboxes.splice(index, 1);\n await this.returnSandbox(sandbox);\n\n if (--numOver <= 0) break;\n }\n }\n \n /**\n * Basic watch dog to check if there are idle sandboxes and\n * attempts to nudge the supervisor to feed them work.\n *\n * Run in an interval created in @constructor .\n * @returns {Promise<void>}\n */\n async watchdog () {\n if (!this.watchdogState)\n this.watchdogState = {};\n \n // Every 5 minutes, report progress of all working slices to the scheduler\n if (Date.now() > ((this.lastProgressReport || 0) + sandboxTuning.progressReportInterval)) {\n // console.log('454: Assembling progress update...');\n this.lastProgressReport = Date.now();\n \n const slices = [];\n \n this.queuedSlices.forEach(slice => {\n const jobAddress = slice.jobAddress;\n const sliceNumber = slice.sliceNumber;\n const authorizationMessage = this.checkAuthorization('Supervisor.watchdog:', jobAddress, sliceNumber);\n\n if (authorizationMessage) {\n addToSlicePayload(slices, jobAddress, sliceNumber, 'scheduled', authorizationMessage);\n }\n })\n \n this.workingSandboxes.forEach(sb => {\n // The lifetime of sandbox.isWorking is (or should be) precisely defined,\n // so we can assume sb.slice is always valid here.\n assert(sb.slice);\n\n const jobAddress = sb.jobAddress;\n const sliceNumber = sb.slice.sliceNumber;\n const authorizationMessage = this.checkAuthorization('Supervisor.watchdog:', jobAddress, sliceNumber);\n\n if (authorizationMessage) {\n addToSlicePayload(slices, jobAddress, sliceNumber, 'progress', authorizationMessage);\n }\n });\n\n if (slices.length) {\n // console.log('471: sending progress update...');\n const progressReportPayload = {\n worker: this.workerOpaqueId,\n slices,\n };\n\n this.resultSubmitterConnection.send('status', progressReportPayload)\n .catch(error => {\n console.error('479: Failed to send status update:', error/*.message*/);\n });\n }\n }\n\n if (this.worker.working) {\n if (this.unallocatedSpace > 0) {\n await this.work().catch(err => {\n if (!this.watchdogState[err.code || '0'])\n this.watchdogState[err.code || '0'] = 0;\n if (Date.now() - this.watchdogState[err.code || '0'] > ((dcpConfig.worker.watchdogLogInterval * timeDilation || 120) * 1000))\n console.error('301: Failed to start work:', err);\n this.watchdogState[err.code || '0'] = Date.now();\n });\n }\n\n this.pruneSandboxes();\n }\n }\n\n /**\n * Gets the logical and physical number of cores and also\n * the total number of sandboxes the worker is allowed to run\n *\n */\n getStatisticsCPU() {\n if (DCP_ENV.isBrowserPlatform) {\n return {\n worker: this.workerOpaqueId,\n lCores: window.navigator.hardwareConcurrency,\n pCores: dcpConfig.worker.pCores || window.navigator.hardwareConcurrency,\n sandbox: this.maxWorkingSandboxes\n }\n }\n\n return {\n worker: this.workerOpaqueId,\n lCores: requireNative('os').cpus().length,\n pCores: requireNative('physical-cpu-count'),\n sandbox: this.maxWorkingSandboxes\n }\n }\n\n /**\n * Call to start doing work on the network.\n * This is the one place where requests to fetch new slices are made.\n * After the initial slice and job are fetched it calls @this.distributeQueuedSlices.\n *\n * @returns {Promise<void>}\n */\n async work()\n {\n\n await this.setDefaultIdentityKeystore();\n\n // if connections don't exist, instantiate them. Needed every time worker is started/stopped\n if (!this.taskDistributorConnection)\n this.instantiateAllConnections();\n\n let slicesToFetch;\n if (this.options.priorityOnly && this.options.jobAddresses.length === 0) {\n slicesToFetch = 0;\n } else if (this.queuedSlices.length > 1) {\n // We have slices queued, no need to fetch\n slicesToFetch = 0;\n } else {\n // The queue is almost empty (there may be 0 or 1 element), fetch a full task.\n // The task is full, in the sense that it will contain slices whose\n // aggregate execution time is this.maxWorkingSandboxes * 5-minutes.\n // However, there can only be this.unallocatedSpace # of long slices.\n // Thus we need to know whether a slice in this.queuedSlices is long or not.\n // (A long slice has estimated execution time > 5-minutes.)\n const longSliceCount = (this.queuedSlices.length > 0 && this.queuedSlices[0].isLongSlice) ? 1 : 0;\n slicesToFetch = this.unallocatedSpace - longSliceCount;\n }\n\n debugging() && console.log(`Supervisor.work: Try to get ${slicesToFetch} slices in working sandboxes, unallocatedSpace ${this.unallocatedSpace}, queued slices ${this.queuedSlices.length}`);\n \n // Fetch a new task if we have no more slices queued, then start workers\n try {\n if (slicesToFetch > 0 && !this.isFetchingNewWork) {\n this.isFetchingNewWork = true;\n\n /**\n * This will only ready sandboxes up to a total count of\n * maxWorkingSandboxes (in any state). It is not possible to know the\n * actual number of sandboxes required until we have the slices because we\n * may have sandboxes assigned for the slice's job already.\n */\n if (this.maxWorkingSandboxes > this.sandboxes.length) {\n await this.readySandboxes(\n this.maxWorkingSandboxes - this.sandboxes.length,\n );\n }\n\n /**\n * Temporary change: Assign the capabilities of one of readied sandboxes\n * before fetching slices from the scheduler.\n *\n * TODO: Remove this once fetchTask uses the capabilities of every\n * sandbox to fetch slices.\n */\n if (!this.capabilities) {\n this.capabilities = this.sandboxes[0].capabilities;\n this.emit('capabilitiesCalculated', this.capabilities);\n }\n\n if(DCP_ENV.isBrowserPlatform && this.capabilities.browser)\n this.capabilities.browser.chrome = DCP_ENV.isBrowserChrome;\n\n const fetchTimeout = setTimeout(() => {\n console.warn(`679: Fetch exceeded timeout, will retry at next watchdog interval`);\n this.isFetchingNewWork = false;\n }, 3 * 60 * 1000); // max out at 3 minutes to fetch\n\n // ensure result submitter connection before fetching tasks\n try\n {\n await this.resultSubmitterConnection.keepalive();\n }\n catch (error)\n {\n console.error('Failed to connect to result submitter, refusing to fetch slices. Will try again at next fetch cycle.')\n debugging('supervisor') && console.log(`Error: ${error}`);\n this.isFetchingNewWork = false;\n return;\n }\n await this.fetchTask(slicesToFetch);\n clearTimeout(fetchTimeout);\n }\n \n this.distributeQueuedSlices().then(() => debugging('supervisor') && 'supervisor: finished distributeQueuedSlices()').catch((e)=>console.error(e)) ;\n// await this.distributeQueuedSlices();\n // No catch(), because it will bubble outward to the caller\n } finally {\n this.isFetchingNewWork = false;\n }\n }\n\n /**\n * Generate the workerComputeGroups property of the requestTask message. \n * \n * Concatenate the compute groups object from dcpConfig with the list of compute groups\n * from the supervisor, and remove the public group if accidentally present. Finally,\n * we transform joinSecrets/joinHash into joinHashHash for secure transmission.\n *\n * @note computeGroup objects with joinSecrets are mutated to record their hashes. This\n * affects the supervisor options and dcpConfig. Re-adding a joinSecret property\n * to one of these will cause the hash to be recomputed.\n */\n generateWorkerComputeGroups()\n {\n var computeGroups = Object.values(dcpConfig.worker.computeGroups || {});\n if (this.options.computeGroups)\n computeGroups = computeGroups.concat(this.options.computeGroups);\n computeGroups = computeGroups.filter(group => group.id !== constants.computeGroups.public.id);\n const hashedComputeGroups = [];\n for (const group of computeGroups)\n {\n const groupCopy = Object.assign({}, group);\n if ((group.joinSecret || group.joinHash) && (!group.joinHashHash || this.lastDcpsid !== this.taskDistributorConnection.dcpsid))\n {\n let joinHash;\n if(group.joinHash) {\n joinHash = group.joinHash;\n } else {\n joinHash = calculateJoinHash(groupCopy);\n } \n\n groupCopy.joinHashHash = hash.calculate(hash.eh1, joinHash, this.taskDistributorConnection.dcpsid);\n delete groupCopy.joinSecret;\n delete groupCopy.joinHash;\n debugging('computeGroups') && console.debug(`Calculated joinHash=${joinHash} for`, groupCopy);\n }\n hashedComputeGroups.push(groupCopy);\n }\n this.lastDcpsid = this.taskDistributorConnection.dcpsid;\n debugging('computeGroups') && console.debug('Requesting ', computeGroups.length, 'non-public groups for session', this.lastDcpsid);\n return hashedComputeGroups;\n }\n\n /**\n * Fetches a task, which contains job information and slices for sandboxes and\n * manages events related to fetching tasks so the UI can more clearly display\n * to user what is actually happening.\n * @param {number} numCores\n * @returns {Promise<void>} The requestTask request, resolve on success, rejects otherwise.\n * @emits Supervisor#fetchingTask\n * @emits Supervisor#fetchedTask\n */\n async fetchTask(numCores)\n {\n this.emit('fetchingTask');\n debugging('supervisor') && console.debug('supervisor: fetching task');\n const requestPayload = {\n numCores,\n coreStats: this.getStatisticsCPU(),\n numGPUs: this.defaultMaxGPUs,\n capabilities: this.capabilities,\n paymentAddress: this.paymentAddress,\n jobAddresses: this.options.jobAddresses, // when set, only fetches slices for these jobs\n localExec: this.options.localExec,\n workerComputeGroups: this.generateWorkerComputeGroups(),\n minimumWage: dcpConfig.worker.minimumWage || this.options.minimumWage,\n readyJobs: [ /* list of jobs addresses XXXwg */ ],\n previouslyWorkedJobs: this.cache.jobs,\n };\n\n // workers should be part of the public compute group by default\n if (!booley(dcpConfig.worker.leavePublicGroup) && !booley(this.options.leavePublicGroup))\n requestPayload.workerComputeGroups.push(constants.computeGroups.public);\n\n debugging('computeGroups') && console.log(`Fetching work for ${requestPayload.workerComputeGroups.length} ComputeGroups: `, requestPayload.workerComputeGroups);\n \n debugging() && console.log(`${shortTime()} fetchTask wants ${numCores} slices, unallocatedSpace ${this.unallocatedSpace}, queuedSlices ${this.queuedSlices.length}`);\n try {\n debugging('requestTask') && debug('requestPayload', requestPayload);\n\n let result = await this.taskDistributorConnection.send('requestTask', requestPayload);\n let responsePayload = result.payload; \n \n if (!result.success) { \n debugging() && console.log('Task fetch failure; request=', requestPayload);\n debugging() && console.log('Task fetch failure; response=', result.payload);\n throw new DCPError('Unable to fetch task for worker', responsePayload);\n }\n\n const sliceCount = responsePayload.body.task.length || 0;\n\n /**\n * The fetchedTask event fires when the supervisor has finished trying to\n * fetch work from the scheduler (task-manager). The data emitted is the\n * number of new slices to work on in the fetched task.\n *\n * @event Supervisor#fetchedTask\n * @type {number}\n */\n this.emit('fetchedTask', sliceCount);\n\n if (sliceCount < 1) {\n return;\n }\n\n /*\n * DCP-1698 Send auth msg with tasks to worker, then validate authority of worker to send slice info back to scheduler.\n * payload structure: { owner: this.address, signature: signature, auth: messageLightWeight, body: messageBody };\n * XXXpfr: @todo Do not think we need 'owner', think about it.\n */\n const { body, ...authorizationMessage } = responsePayload;\n const { newJobs, task } = body;\n assert(newJobs); // It should not be possible to have !newJobs -- we throw on !success.\n for (const jobAddress of Object.keys(newJobs)) {\n this.cache.store('job', jobAddress, newJobs[jobAddress]);\n }\n debugging('supervisor') && console.log(`${shortTime()} fetchTask: Task has ${task.length} slices from ${Object.keys(newJobs).length} new jobs.`);\n\n const tmpQueuedSlices = [];\n for (const taskElement of task) {\n\n if (!this.authorizationMessages[taskElement.jobAddress]) {\n this.authorizationMessages[taskElement.jobAddress] = {};\n // All workers are authorized to send messages to the '0' slice if it is assigned the job.\n this.authorizationMessages[taskElement.jobAddress][0] = authorizationMessage;\n }\n this.authorizationMessages[taskElement.jobAddress][taskElement.sliceNumber] = authorizationMessage;\n\n tmpQueuedSlices.push(new Slice(taskElement));\n debugging('supervisor') && console.log(`/t${shortTime()} fetchTask: slice ${taskElement.sliceNumber}, jobAddress ${taskElement.jobAddress}`);\n }\n\n /** XXXpfr @todo Try to get rid of this.slices. */\n this.slices.push(...tmpQueuedSlices);\n\n this.queuedSlices = [...tmpQueuedSlices, ...this.queuedSlices]; /* Make sure the old ones are up front. */\n\n } catch (error) {\n this.emit('fetchTaskFailed', error);\n debugging('supervisor') && console.debug(`Supervisor.fetchTask failed!: error: ${error}`);\n }\n }\n\n /**\n * For each slice in this.queuedSlices, match with a sandbox in the following order:\n * 1. Try to find an already assigned sandbox in this.assignedSandboxes for the slice's job.\n * 2. Find a ready sandbox in this.readiedSandboxes that is unassigned.\n * 3. Ready a new sandbox and use that.\n *\n * Take great care in assuring sandboxes and slices are uniquely associated, viz.,\n * a given slice cannot be associated with multiple sandboxes and a given sandbox cannot be associated with multiple slices.\n * The lack of such uniqueness has been the root cause of several difficult bugs.\n *\n * @param {number} numSlices The number of slices to match with sandboxes.\n * @returns {Promise<SandboxSlice[]>} Returns SandboxSlice[], may have length zero.\n */\n async matchSlicesWithSandboxes (numSlices) {\n const sandboxSlices = [];\n if (numSlices <= 0 || this.queuedSlices.length == 0 || this.matching)\n return sandboxSlices;\n\n try\n {\n this.matching = true;\n const jobSliceMap = {};\n const jobs = [];\n\n // Until this is rock-solid-stable I want to check for uniquenees.\n if (true) {\n dumpSlicesIfNotUnique(this.queuedSlices, `DANGER: this.queuedSlices slices are not unique -- this is ok when slice is rescheduled.`);\n dumpSandboxesIfNotUnique(this.readiedSandboxes, `DANGER: this.readiedSandboxes sandboxes are not unique!`);\n dumpSandboxesIfNotUnique(this.assignedSandboxes, `DANGER: this.assignedSandboxes sandboxes are not unique!`);\n }\n\n // 'false' is so it is easy to turn back to 'true'\n // Since we don't know yet whether these changes are rock-solid-stable I want to keep easy-to-debug stuff like this.\n if ( false || debugging()) {\n console.log(`${shortTime()} matchSlicesWithSandboxes: numSlices ${numSlices}, numQueuedSlices ${this.queuedSlices.length}: numAssignedSandboxes ${this.assignedSandboxes.length}, numReadySandboxes ${this.readiedSandboxes.length}, total sandbox count: ${this.sandboxes.length}`);\n dumpSlices(this.queuedSlices, 'matchSlicesWithSandboxes(top): this.queuedSlices');\n dumpSandboxes(this.assignedSandboxes, 'matchSlicesWithSandboxes(top): this.assignedSandboxes');\n dumpSandboxes(this.readiedSandboxes, 'matchSlicesWithSandboxes(top): this.readiedSandboxes');\n }\n\n // Partition slices to be parameterized by slice.jobAddress.\n let counter = 0, readyCounter = 0, newCounter = 0;\n while (this.queuedSlices.length > 0) {\n const slice = this.queuedSlices.pop();\n if (!slice.isUnassigned) continue;\n if (!jobSliceMap[slice.jobAddress]) jobSliceMap[slice.jobAddress] = [];\n jobSliceMap[slice.jobAddress].push(slice);\n if (++counter >= numSlices) break;\n }\n counter = 0;\n\n for (const [jobAddress, slicesForJob] of Object.entries(jobSliceMap)) {\n jobs.push(jobAddress);\n\n // First get all assigned sandboxes.\n const assignedSandboxesForJob = this.assignedSandboxes.filter(sandbox => sandbox.jobAddress === jobAddress);\n\n while (assignedSandboxesForJob.length > 0 && slicesForJob.length > 0) {\n counter++;\n const sandbox = assignedSandboxesForJob.pop();\n this.removeElement(this.assignedSandboxes, sandbox);\n const slice = slicesForJob.pop();\n assert(slice.jobAddress === sandbox.jobAddress);\n debugging() && console.log(`matchSlicesWithSandboxes: assigned sb matched ${this.dumpSandboxAndSlice(sandbox, slice)}`);\n sandboxSlices.push({ sandbox, slice });\n }\n\n // Next get existing and possibly new ready-for-assign sandboxes.\n while (slicesForJob.length > 0) {\n while (this.readiedSandboxes.length > 0 && slicesForJob.length > 0) {\n readyCounter++;\n const slice = slicesForJob.pop();\n const sandbox = this.readiedSandboxes.pop();\n debugging() && console.log(`matchSlicesWithSandboxes: readied sb matched ${this.dumpSandboxAndSlice(sandbox, slice)}`);\n sandboxSlices.push({ sandbox, slice });\n }\n\n if (slicesForJob.length > 0) {\n newCounter++;\n await this.readySandboxes(slicesForJob.length);\n } else {\n readyCounter -= newCounter;\n break;\n }\n }\n }\n\n debugging() && console.log(`matchSlicesWithSandboxes: matches: ${ JSON.stringify(sandboxSlices.map(ss => this.dumpSandboxSlice(ss))) }`);\n\n // Until this is rock-solid-stable I want to check for uniquenees.\n if (true) {\n this.dumpSandboxSlicesIfNotUnique(sandboxSlices, `DANGER: sandboxSlices; { sandbox, slice } pairs are not unique!`);\n }\n\n // 'false' is so it is easy to turn back to 'true'\n // Since we don't know yet whether these changes are rock-solid-stable I want to keep easy-to-debug stuff like this.\n if ( false || debugging()) {\n console.log(`matchSlicesWithSandboxes: found ${sandboxSlices.length} sandboxes for jobs ${JSON.stringify(jobs)}: assigned ${counter}, ready ${readyCounter}, new ${newCounter}`);\n this.dumpSandboxSlices(sandboxSlices, 'matchSlicesWithSandboxes: sandboxSlices');\n dumpSandboxes(this.assignedSandboxes, 'matchSlicesWithSandboxes: this.assignedSandboxes');\n dumpSandboxes(this.readiedSandboxes, 'matchSlicesWithSandboxes: this.readiedSandboxes');\n console.log(`matchSlicesWithSandboxes: remaining readied: ${JSON.stringify(this.readiedSandboxes.map(s => s.id))}`);\n }\n\n sandboxSlices.forEach(sandboxSlice => {\n assert(!sandboxSlice.sandbox.allocated);\n sandboxSlice.sandbox.allocated = true;\n });\n\n } catch (e) {\n console.error(`DANGER matchSlicesWithSandboxes threw exception`, e);\n //throw e; // Should we rethrow?\n } finally {\n this.matching = false;\n }\n\n debugging() && console.log(`${shortTime()} matchSlicesWithSandboxes allocated ${sandboxSlices.length} sandboxes, queuedSlices ${this.queuedSlices.length}, unallocatedSpace ${this.unallocatedSpace}.`);\n\n return sandboxSlices;\n }\n\n /**\n * Searches for a sandbox that is eligible to work on the given slice.\n * It will search for a sandbox in the following order:\n * 1. Try to find an already assigned sandbox for the slice's job\n * 2. Find a ready sandbox that is unassigned\n * 3. Ready a new sandbox and use that\n * @deprecated This is a high-powered foot-gun.\n * @param {Slice} slice The slice for which to find a sandbox.\n * @returns {Promise<Sandbox|null>} Returns the sandbox instance if matched, otherwise returns null.\n */\n async findSandboxForSlice (slice) {\n let sandbox;\n let assignedSandboxesForJob = this.snapshotOfAssignedSandboxes.filter(w => w.jobAddress === slice.jobAddress);\n\n switch (true) {\n case !!(sandbox = assignedSandboxesForJob.pop()):\n case !!(sandbox = this.snapshotOfReadiedSandboxes.pop()):\n case !!(sandbox = (await this.readySandboxes(1)).pop()):\n return sandbox;\n default:\n return null;\n }\n }\n\n /**\n * Given an assigned sandbox, it tries to find a slice that the sandbox can work on.\n * Unused.\n * @deprecated Foot-gun.\n * @param {Sandbox} sandbox The assigned sandbox\n * @returns {Slice|undefined} Returns a slice in case of a match, undefined otherwise\n */\n findSliceForSandbox (sandbox) {\n return this.queuedSlices.find((slice) => slice.jobAddress === sandbox.jobAddress);\n }\n\n /**\n * This method will call @this.startSandboxWork(sandbox, slice) for { sandbox, slice }\n * in the array returned by @this.matchSlicesWithSandboxes(availableSandboxes) until all sandboxes are working.\n * It is possible for a sandbox to finish simultaneously and leave a sandbox that is not working.\n * Note: @this.queuedSlices may be exhausted before all sandboxes are working.\n * @returns {Promise<void>}\n */\n async distributeQueuedSlices () {\n //\n // All matching of sandbox with slice is taken care of by @this.matchSlicesWithSandboxes().\n // We do not use @this.snapshotOfQueuedSlices becuase it is in a fuzzy state; when matched with a sandbox,\n // a slice transitions to working at an unknown point in the future.\n // Instead we use @this.queuedSlices which really is a queue, whose elements are\n // dequeued in @this.matchSlicesWithSandboxes() and enqueued in fetchTask.\n // We should try to get rid of @this.slices, because it has low utility.\n // However, to minimize churn, this can be done later.\n //\n\n const availableSandboxes = this.unallocatedSpace;\n if (availableSandboxes <= 0) return;\n\n debugging() && console.log(`distributeQueuedSlices: unallocatedSpace ${availableSandboxes}, readied ${JSON.stringify(this.readiedSandboxes.map(s => s.id))}, queuedSlices ${this.queuedSlices.length}`);\n\n const sandboxSlices = await this.matchSlicesWithSandboxes(availableSandboxes);\n\n for (let sandboxSlice of sandboxSlices) {\n\n const { sandbox, slice } = sandboxSlice;\n if (sandbox) {\n debugging() && console.log(`${shortTime()}: distributeQueuedSlices: matched slice ${slice.sliceNumber} with sandbox ${sandbox.id} for job ${slice.jobAddress}, total sandbox count: ${this.sandboxes.length}`);\n if (sandbox.isReadyForAssign) {\n try {\n let timeoutMs = Math.floor(Math.min(+Supervisor.lastAssignFailTimerMs || 0, 10 * 60 * 1000 /* 10m */));\n await asleepMs(timeoutMs);\n if (sandbox.isReadyForAssign) { // Don't need this double-check anymore because of @this.matchSlicesWithSandboxes().\n sandbox.setIsAssigning(); // Don't need this either --> Set the state to assigning before the await to circumvent the event loop problem from await.\n await this.assignJobToSandbox(sandbox, slice.jobAddress);\n } else {\n return // Don't need this either --> The sandbox was picked a second time while it was being prepared, return without an error\n }\n } catch (e) {\n console.error(`Could not assign slice ${slice.sliceNumber} for job ${slice.jobAddress} to sandbox ${sandbox.id}`);\n if (Supervisor.debugBuild) console.error(`...exception`, e);\n Supervisor.lastAssignFailTimerMs = Supervisor.lastAssignFailTimerMs ? +Supervisor.lastAssignFailTimerMs * 1.25 : Math.random() * 200;\n this.returnSlice(slice);\n continue;\n }\n }\n\n if (!Supervisor.lastAssignFailTimerMs)\n Supervisor.lastAssignFailTimerMs = Math.random() * 200;\n this.startSandboxWork(sandbox, slice);\n Supervisor.lastAssignFailTimerMs = false;\n\n } else {\n // We should never get here.\n console.error(\"Supervisor.distributeQueuedSlices: Failed to find sandbox for slice\", {\n jobAddress: slice.jobAddress,\n sliceNumber: slice.sliceNumber\n });\n }\n }\n }\n\n /**\n *\n * @param {Sandbox} sandbox\n * @param {opaqueId} jobAddress\n * @returns {Promise<void>}\n */\n async assignJobToSandbox(sandbox, jobAddress) {\n var ceci = this;\n\n try {\n return sandbox.assign(jobAddress)\n } catch(error) {\n // return slice to scheduler, log error\n console.error('Supervisor.assignJobToSandbox: Failed to assign job to sandbox.', {\n jobAddress: jobAddress.substr(0,10),\n error,\n });\n\n await ceci.returnSandbox(sandbox);\n\n throw error;\n }\n }\n \n /**\n * Gives a slice to a sandbox which begins working. Handles collecting\n * the slice result (complete/fail) from the sandbox and submitting the result to the scheduler.\n * It will also return the sandbox to @this.returnSandbox when completed so the sandbox can be re-assigned.\n *\n * @param {Sandbox} sandbox - the sandbox to give the slice\n * @param {Slice} slice - the slice to distribute\n * @returns {Promise<void>} Promise returned from sandbox.run\n */\n async startSandboxWork (sandbox, slice) {\n var startDelayMs;\n\n try {\n slice.markAsWorking();\n } catch (e) {\n // This will occur when the same slice is distributed twice.\n // It is normal because two sandboxes could finish at the same time and be assigned the\n // same slice before the slice is marked as working.\n debugging() && console.debug(e);\n return;\n }\n\n // sandbox.requiresGPU = slice.requiresGPU;\n // if (sandbox.requiresGPU) {\n // this.GPUsAssigned++;\n // }\n\n if (Supervisor.startSandboxWork_beenCalled)\n startDelayMs = 1000 * (tuning.minSandboxStartDelay + (Math.random() * (tuning.maxSandboxStartDelay - tuning.minSandboxStartDelay)));\n else {\n startDelayMs = 1000 * tuning.minSandboxStartDelay;\n Supervisor.startSandboxWork_beenCalled = true;\n }\n \n try {\n debugging() && console.log(`${shortTime()} startSandboxWork: started sandbox.id.sliceNumber ${sandbox.id}.${slice.sliceNumber}.${slice.jobAddress}, total sandbox count: ${this.sandboxes.length}`);\n let result = await sandbox.work(slice, startDelayMs);\n slice.collectResult(result, true);\n // In watchdog, all sandboxes in 'working' state, have their status sent to result submitter.\n // However, this can happen after the sandbox/slice has already sent results\n // to result submitter, in which case, the activeSlices table has already removed the row\n // corresponding to slice and hence is incapable of updating status.\n sandbox.changeWorkingToAssigned();\n sandbox.allocated = false; // This is a little redundant: don't await any promises between here and the finally clause.\n this.assignedSandboxes.push(sandbox);\n debugging() && console.log(`${shortTime()} startSandboxWork: completed sandbox.id.sliceNumber ${sandbox.id}.${slice.sliceNumber}.${slice.jobAddress}, total sandbox count: ${this.sandboxes.length}`);\n } catch(error) {\n let logLevel;\n if (error instanceof SandboxError) {\n logLevel = 'warn';\n // The message and stack properties of error objects are not enumerable,\n // so they have to be copied into a plain object this way\n const errorResult = Object.getOwnPropertyNames(error).reduce((o, p) => {\n o[p] = error[p]; return o;\n }, { message: 'Unexpected worker error' });\n slice.collectResult(errorResult, false);\n } else {\n logLevel = 'error';\n // This error was unrelated to the work being done,\n // so just return the slice in the finally block.\n }\n\n // sandbox.public.name is defined in Sandbox.assign.\n // It is possible to get here with !sandbox.public.\n const jobName = sandbox.public ? sandbox.public.name : 'unnamed';\n const errorObject = {\n jobAddress: slice.jobAddress.substr(0,10),\n sliceNumber: slice.sliceNumber,\n sandbox: sandbox.id,\n jobName: jobName,\n };\n\n // XXXpfr Enabled Informative sandbox errors when in debug mode.\n if (!Supervisor.debugBuild && error.errorCode === 'EUNCAUGHTERROR') {\n console[logLevel](`Supervisor.startSandboxWork: Uncaught error in sandbox, could not compute.`, errorObject);\n } else {\n errorObject.stack = error.stack;\n console[logLevel](`Supervisor.startSandboxWork: Sandbox failed (${error.message})`, errorObject);\n }\n } finally {\n sandbox.allocated = false;\n if (slice.result) {\n await this.recordResult(slice);\n } else if (slice.error) {\n this.returnSlice(slice);\n } else {\n this.returnSlice(slice);\n }\n await this.returnSandbox(sandbox, slice);\n }\n }\n\n /**\n * Terminates sandboxes and returns slices.\n * Sets the working flag to false, call @this.work to start working again.\n * \n * If forceTerminate is true: Terminates all sandboxes and returns all slices.\n * If forceTerminate is false: Terminates non-working sandboxes and returns queued slices.\n *\n * @param {boolean} [forceTerminate = true] - true if you want to stop the sandboxes from completing their current slice.\n * @returns {Promise<void>}\n */\n async stopWork (forceTerminate = true) {\n if (forceTerminate) {\n let sandbox;\n while (sandbox = this.sandboxes.pop()) {\n sandbox.terminate(false);\n }\n\n await this.returnSlices(this.slices).then(() => {\n this.slices.length = 0;\n this.queuedSlices.length = 0;\n });\n\n this.resultSubmitterConnection.off('close', this.openResultSubmitterConn);\n this.resultSubmitterConnection.close();\n this.resultSubmitterConnection = null;\n } else {\n // Only terminate idle sandboxes and return only queued slices\n let idleSandboxes = this.sandboxes.filter(w => !w.isWorking);\n for (let sandbox of idleSandboxes) {\n sandbox.terminate(false);\n }\n\n let queuedSlices = this.queuedSlices;\n await this.returnSlices(queuedSlices).then(() => {\n // Kill corresponding entries in this.slices .\n this.queuedSlices.forEach(slice => {\n this.removeElement(this.slices, slice);\n });\n this.queuedSlices.length = 0;\n });\n await new Promise((resolve, reject) => {\n let sandboxesRemaining = this.workingSandboxes.length;\n if (sandboxesRemaining === 0)\n {\n resolve();\n }\n // Resolve and finish work once all sandboxes have finished submitting their results.\n this.on('submitFinished', () => {\n sandboxesRemaining--;\n if (sandboxesRemaining === 0)\n {\n console.log('All sandboxes empty, stopping worker and closing all connections');\n resolve();\n }\n });\n })\n this.resultSubmitterConnection.off('close', this.openResultSubmitterConn);\n this.resultSubmitterConnection.close();\n this.resultSubmitterConnection = null;\n }\n\n this.taskDistributorConnection.off('close', this.openTaskDistributorConn);\n this.taskDistributorConnection.close();\n this.taskDistributorConnection = null;\n\n this.packageManagerConnection.off('close', this.openPackageManagerConn);\n this.packageManagerConnection.close();\n this.packageManagerConnection = null;\n\n this.eventRouterConnection.off('close', this.openEventRouterConn);\n this.eventRouterConnection.close();\n this.eventRouterConnection = null;\n\n this.emit('stop');\n }\n\n /**\n * Takes a slice and returns it to the scheduler to be redistributed.\n * Usually called when the supervisor told it to forcibly stop working.\n *\n * @param {Slice} slice - the slice to return to the scheduler\n * @returns {Promise<*>} - response from the scheduler the slice was returned to\n */\n returnSlice (slice) {\n // Remove the slice from the slices array.\n this.removeElement(this.slices, slice);\n\n const jobAddress = slice.jobAddress;\n const sliceNumber = slice.sliceNumber;\n const authorizationMessage = this.checkAuthorization('Supervisor.returnSlice:', jobAddress, sliceNumber);\n if (!authorizationMessage) return undefined;\n\n let payload = {};\n if (slice.error) {\n payload = slice.getReturnMessagePayload(this.workerOpaqueId, authorizationMessage, 'uncaught');\n } else {\n payload = slice.getReturnMessagePayload(this.workerOpaqueId, authorizationMessage);\n }\n\n return this.resultSubmitterConnection.send('status', payload)\n .then(response => {\n return response;\n })\n .catch(error => {\n console.error('Failed to return slice', {\n sliceIdentifier: slice.identifier,\n sliceNumber: sliceNumber,\n jobAddress: jobAddress,\n error,\n });\n })\n }\n\n /** Bulk-return multiple slices, possibly for assorted jobs\n */\n returnSlices(slices) {\n const slicePayload = [];\n\n slices.forEach(slice => {\n addToSlicePayload(slicePayload, slice.jobAddress, slice.sliceNumber, 'return', this.checkAuthorization('Supervisor.returnSlices:', slice.jobAddress, slice.sliceNumber));\n });\n\n return this.resultSubmitterConnection.send('status', {\n worker: this.workerOpaqueId,\n slices: slicePayload,\n });\n }\n\n /**\n * Submits the slice results to the scheduler, either to the\n * work submit or fail endpoints based on the slice status.\n * Then remove the slice from the @this.slices cache.\n *\n * @param {Slice} slice - the slice to submit\n * @returns {Promise<void>}\n */\n async recordResult (slice) {\n const metrics = { GPUTime: 0, CPUTime: 0, CPUDensity: 0, GPUDensity: 0 }; \n \n debugging('supervisor') && console.log('supervisor: recording result');\n assert(slice && slice.hasOwnProperty('result'), slice.sliceNumber, slice.jobAddress);\n\n const jobAddress = slice.jobAddress;\n const sliceNumber = slice.sliceNumber;\n const authorizationMessage = this.checkAuthorization('Supervisor.recordResult:', jobAddress, sliceNumber);\n if (!authorizationMessage) return; // Question: Is it better to proceed and have a cascade of errors?\n\n /* @see result-submitter::result for full message details */\n var payloadData = {\n slice: sliceNumber,\n job: jobAddress,\n worker: this.workerOpaqueId,\n paymentAddress: this.paymentAddress,\n metrics,\n authorizationMessage,\n }\n const isValidTimeReport = slice.result && slice.result.timeReport && slice.result.timeReport.total > 0;\n if (isValidTimeReport) {\n const timeReport = slice.result.timeReport;\n assert(timeReport.total);\n metrics.GPUTime = timeReport.webGL;\n metrics.CPUTime = timeReport.total - (metrics.GPUTime + timeReport.idle);\n metrics.CPUDensity = metrics.CPUTime / timeReport.total;\n metrics.GPUDensity = metrics.GPUTime / timeReport.total;\n }\n \n this.emit('submittingResult');\n\n if (!slice.isFinished)\n throw new Error('Cannot record result for slice that is not finished');\n\n if (slice.resultStorageDetails === 'pattern') /* this is a remote-storage slice */\n payloadData.result = await sendResultToRemote(slice);\n else {\n // It is possible for slice.result to be undefined when there are upstream errors.\n if (!slice.result)\n throw new Error('slice.result is undefined. This is ok when there are upstream errors.');\n payloadData.result = encodeDataURI(slice.result.result); /* XXXwg - result.result is awful */\n }\n\n try {\n if (slice.completed) {\n\n /* work function returned a result */\n const { success, payload } = await this.resultSubmitterConnection.send(\n 'result',\n payloadData,\n );\n\n if (!success) {\n throw new DCPError('Unable to submit result for work done', payload);\n }\n\n const receipt = {\n accepted: true,\n payment: payload.slicePaymentAmount,\n };\n this.emit('submittedResult', payload);\n this.emit('dccCredit', receipt);\n } else {\n\n /* slice did not complete for some reason */\n let statusPayloadData = {\n worker: this.workerOpaqueId,\n slices: [\n {\n sliceNumber: sliceNumber,\n job: jobAddress,\n status: 'return', // special state looked for in status.js / result-submitter\n reason: 'uncaught', // special state looked for in status.js / result-submitter\n /** @todo XXXpfr: Is there error info we can use here? */ \n // error: slice.error, \n authorizationMessage,\n }\n ], \n };\n \n await this.resultSubmitterConnection.send('status', statusPayloadData);\n }\n } catch(error) {\n console.info(`1014: Failed to submit results for slice ${payloadData.slice} of job ${payloadData.job}`, error);\n this.emit('submitSliceFailed', error);\n }\n finally\n {\n this.emit('submitFinished');\n // Remove the slice from the slices array\n this.removeElement(this.slices, slice);\n }\n }\n}\n\n/** \n * Send a work function's result to a server that speaks our DCP Storage Server protocol.\n * A sample storage server has been implemented in https://gitlab.com/Distributed-Compute-Protocol/dcp-storage-server\n * as part of DCP-1479.\n *\n * @param {Slice} slice slice object whose result we are sending\n * @returns {Promise<object>} object with properties contentType and uri\n * @throws if the remote server returned a non-ok status object, or an HTTP status not in the 2xx range.\n */\nasync function sendResultToRemote(slice) {\n const postParams = {\n ...JSON.parse(slice.resultStorageParams), // These come in as a String from the db currently, @todo - output as an object\n };\n\n const sliceResultUri = makeSliceURI('pattern', slice.resultStorageDetails, {\n slice: slice.sliceNumber,\n job: slice.jobAddress,\n //juuid: slice.job.uuid -- what should be here?\n });\n\n const url = new DcpURL(sliceResultUri);\n\n if (dcpConfig.worker.allowOrigins.any.indexOf(url.origin) === -1 &&\n dcpConfig.worker.allowOriginResultStorage.indexOf(url.origin) === -1) {\n throw new Error(`Invalid origin for remote result storage: '${url.origin}'`);\n }\n \n postParams.slice = slice.sliceNumber;\n postParams.job = slice.jobAddress;\n postParams.contentType = 'application/json'; // Currently data will be outputed as a JSON object, @todo: Support file upload\n\n if (slice.result.result)\n postParams.result = slice.result.result;\n else\n postParams.error = new Error('no result'); /** @todo XXXwg return the error object from the sandbox */\n\n const formBodyArray = [];\n for (const property in postParams) {\n const encodedKey = encodeURIComponent(property);\n const encodedValue = encodeURIComponent(postParams[property]);\n formBodyArray.push(`${encodedKey}=${encodedValue}`);\n }\n const formBody = formBodyArray.join('&');\n\n let postPromise = new Promise(function supervisorPostResult(resolve, reject) {\n let deeperErrorStack = new Error().stack;\n deeperErrorStack = deeperErrorStack.substring(deeperErrorStack.indexOf('\\n') + 1);\n const xhr = new XMLHttpRequest();\n xhr.onloadend = function supervisor$$recordResult$onloadend() {\n try {\n let o;\n \n delete xhr.onloadend;\n if (xhr.status >= 200 && xhr.status < 300) {\n /* Successful post: record the resultant URL; needed for dcp-client application to be able to fetch the result */\n switch(xhr.getResponseHeader('content-type'))\n {\n default: /* support lousy web servers */\n case 'application/json': {\n o = JSON.parse(xhr.responseText);\n break;\n }\n case 'application/x-kvin': {\n o = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\").deserialize(xhr.responseText);\n break;\n }\n }\n\n if (o.status === 'ok')\n return resolve(o);\n \n return reject(new Error(`Remote server ${url} did not set status=ok (o.error ? ${JSON.stringify(o)} : error)`));\n } else {\n const error = new Error(`HTTP Error ${xhr.status} sending slice results to ${url}`);\n //\n // Note: This impl is mostly grafted from justFetch in dcp/src/protocol-v3/index.\n // It is a safe assumption that this function has never been tested.\n // Refine the following error info when we support remote-storage.\n //\n error.request = xhr;\n error.request.location = url;\n error.request.method = 'POST';\n error.request.status = xhr.status;\n error.stack += '\\n----------\\n' + deeperErrorStack;\n throw error;\n }\n } catch (e) {\n return reject(e);\n }\n\n return reject(new Error('impossible'));\n }\n \n xhr.onreadystatechange = function() {\n if (this.readyState == 4 && this.status == 200) {\n debugging('supervisor') && console.log('Response text', this.responseText);\n }\n }\n \n xhr.open('POST', url.href, true);\n xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');\n xhr.send(formBody);\n });\n\n return postPromise;\n}\n\n/**\n * Return DCPv4-specific connection options, composed of type-specific, URL-specific, \n * and worker-specific options, any/all of which can override the dcpConfig.dcp.connectOptions.\n * The order of precedence is the order of specificity.\n */\nfunction connectionOptions(url, label) {\n return leafMerge(/* ordered from most to least specific */\n dcpConfig.worker.dcp.connectionOptions.default,\n dcpConfig.worker.dcp.connectionOptions[label],\n dcpConfig.worker.dcp.connectionOptions[url.href]);\n}\n\n\n/**\n * Add a slice to the slice payload being built. If a sliceList already exists for the \n * job-status-authMessage tuple, then the slice will be added to that, otherwise a new \n * sliceList will be added to the payload.\n *\n * @param {Object[]} slicePayload Slice payload being built. Will be mutated in place.\n * @param {Address} job Address of job the slice belongs to\n * @param {Number} sliceNumber Slice number\n * @param {String} status Status update, eg. progress or scheduled\n * @param {Object} authorizationMessage authorizationMessage for the slice\n *\n * @returns {Object[]} mutated slicePayload array\n */\nfunction addToSlicePayload(slicePayload, job, sliceNumber, status, authorizationMessage) {\n // Try to find a sliceList in the payload which matches the job, status, and auth message\n let sliceList = slicePayload.find(desc => {\n return desc.job === job && desc.status === status && desc.authorizationMessage === authorizationMessage;\n });\n\n // If we didn't find a sliceList, start a new one and add it to the payload\n if (!sliceList) {\n sliceList = {\n job,\n sliceNumbers: [],\n status,\n authorizationMessage: authorizationMessage,\n };\n slicePayload.push(sliceList);\n }\n\n sliceList.sliceNumbers.push(sliceNumber);\n\n return slicePayload;\n}\n\n/** @type {number | boolean} */\nSupervisor.lastAssignFailTimerMs = false;\n/** @type {boolean} */\nSupervisor.startSandboxWork_beenCalled = false;\n/** @type {boolean} */\nSupervisor.debugBuild = (__webpack_require__(/*! dcp/common/dcp-build */ \"./src/common/dcp-build.js\").build === 'debug');\n\nexports.Supervisor = Supervisor;\n\n\n//# sourceURL=webpack:///./src/dcp-client/worker/supervisor.js?");
|
|
5407
5407
|
|
|
5408
5408
|
/***/ }),
|
|
5409
5409
|
|
|
@@ -5470,7 +5470,7 @@ eval("/**\n * @file keychain.js - Represents the collection of keystores current
|
|
|
5470
5470
|
/*! no static exports found */
|
|
5471
5471
|
/***/ (function(module, exports) {
|
|
5472
5472
|
|
|
5473
|
-
eval("/**\n * A Small Modal Class\n * @module Modal\n */\n/* globals Event dcpConfig */\nclass Modal {\n constructor (title, message, callback = false, exitHandler = false, {\n continueLabel = 'Continue',\n cancelLabel = 'Cancel',\n cancelVisible = true\n } = {}) {\n const modal = document.createElement('div')\n modal.className = 'dcp-modal-container-old day'\n modal.innerHTML = `\n <dialog class=\"dcp-modal-content\">\n <div class=\"dcp-modal-header\">\n <h2>${title}<button type=\"button\" class=\"close\">×</button></h2>\n ${message ? '<p>' + message + '</p>' : ''}\n </div>\n <div class=\"dcp-modal-loading hidden\">\n <div class='loading'></div>\n </div>\n <form onsubmit='return false' method=\"dialog\">\n <div class=\"dcp-modal-body\"></div>\n <div class=\"dcp-modal-footer ${cancelVisible ? '' : 'centered'}\">\n <button type=\"submit\" class=\"continue green-modal-button\">${continueLabel}</button>\n <button type=\"button\" class=\"cancel green-modal-button\">${cancelLabel}</button>\n </div>\n </form>\n </dialog>`\n\n // To give a reference to do developer who wants to override the form submit.\n // May occur if they want to validate the information in the backend\n // without closing the modal prematurely.\n this.form = modal.querySelector('.dcp-modal-content form')\n this.continueButton = modal.querySelector('.dcp-modal-footer button.continue')\n this.cancelButton = modal.querySelector('.dcp-modal-footer button.cancel')\n this.closeButton = modal.querySelector('.dcp-modal-header .close')\n if (!cancelVisible) {\n this.cancelButton.style.display = 'none'\n }\n\n // To remove the event listener, the reference to the original function\n // added is required.\n this.formSubmitHandler = this.continue.bind(this)\n\n modal.addEventListener('keydown', function (event) {\n event.stopPropagation()\n // 27 is the keycode for the escape key.\n if (event.keyCode === 27) this.close()\n }.bind(this))\n\n this.container = modal\n this.callback = callback\n this.exitHandler = exitHandler\n document.body.appendChild(modal)\n }\n\n changeFormSubmitHandler (newFormSubmitHandler) {\n this.formSubmitHandler = newFormSubmitHandler\n }\n\n /**\n * Validates the form values in the modal and calls the modal's callback\n */\n async continue (event) {\n // To further prevent form submission from trying to redirect from the\n // current page.\n if (event instanceof Event) {\n event.preventDefault()\n }\n let fieldsAreValid = true\n let formElements = this.container.querySelectorAll('.dcp-modal-body select, .dcp-modal-body input, .dcp-modal-body textarea')\n\n const formValues = []\n if (typeof formElements.length === 'undefined') formElements = [formElements]\n // Separate into two loops to enable input validation requiring formValues\n // that come after it. e.g. Two password fields matching.\n for (let i = 0; i < formElements.length; i++) {\n switch (formElements[i].type) {\n case 'file':\n formValues.push(formElements[i])\n break\n case 'checkbox':\n formValues.push(formElements[i].checked)\n break\n default:\n formValues.push(formElements[i].value)\n break\n }\n }\n for (let i = 0; i < formElements.length; i++) {\n if (formElements[i].validation) {\n // Optional fields are allowed to be empty but still can't be wrong if not empty.\n if (!(formElements[i].value === '' && !formElements[i].required)) {\n if (typeof formElements[i].validation === 'function') {\n if (!formElements[i].validation(formValues)) {\n fieldsAreValid = false\n formElements[i].classList.add('is-invalid')\n }\n } else if (!formElements[i].validation.test(formElements[i].value)) {\n fieldsAreValid = false\n formElements[i].classList.add('is-invalid')\n }\n }\n }\n }\n\n if (!fieldsAreValid) return\n\n this.loading()\n if (typeof this.callback === 'function') {\n try {\n return this.callback(formValues)\n } catch (error) {\n console.error('Unexpected error in modal.continue:', error);\n return this.close(false)\n }\n }\n this.close(true)\n }\n\n loading () {\n this.container.querySelector('.dcp-modal-loading').classList.remove('hidden')\n this.container.querySelector('.dcp-modal-body').classList.add('hidden')\n this.container.querySelector('.dcp-modal-footer').classList.add('hidden')\n }\n\n open () {\n this.form.addEventListener('submit', async (event) => {\n const success = await this.formSubmitHandler(event)\n if (success === false) {\n return\n }\n this.close(true)\n })\n // When the user clicks on <span> (x), close the modal\n this.closeButton.addEventListener('click', this.close.bind(this))\n this.cancelButton.addEventListener('click', this.close.bind(this))\n\n // Prevent lingering outlines after clicking some form elements.\n this.container.querySelectorAll('.dcp-modal-body button, .dcp-modal-body input[type=\"checkbox\"]').forEach(element => {\n element.addEventListener('click', () => {\n element.blur()\n })\n })\n\n // Show the modal.\n this.container.style.display = 'block'\n\n const formElements = this.container.querySelectorAll('.dcp-modal-body select, .dcp-modal-body input')\n if (formElements.length) {\n formElements[0].focus()\n if (formElements[0].type === 'text') {\n formElements[0].select()\n }\n for (const el of formElements) {\n if (el.realType) {\n el.type = el.realType\n }\n }\n } else {\n // With no form elements to allow for form submission on enter, focus the\n // continue button.\n this.container.querySelector('.dcp-modal-footer button.continue').focus()\n }\n } // TODO: This should return a promise with the action resolving it\n\n /**\n * Shows the modal and returns a promise of the result of the modal (e.g. was\n * it closed, did its action succeed?)\n */\n showModal () {\n return new Promise((resolve, reject) => {\n this.form.addEventListener('submit', handleContinue.bind(this))\n this.cancelButton.addEventListener('click', handleCancel.bind(this))\n this.closeButton.addEventListener('click', handleCancel.bind(this))\n\n // Prevent lingering outlines after clicking some form elements.\n this.container.querySelectorAll('.dcp-modal-body button, .dcp-modal-body input[type=\"checkbox\"]').forEach(element => {\n element.addEventListener('click', () => {\n element.blur()\n })\n })\n\n // Show the modal.\n this.container.style.display = 'block'\n\n const formElements = this.container.querySelectorAll('.dcp-modal-body select, .dcp-modal-body input')\n if (formElements.length) {\n formElements[0].focus()\n if (formElements[0].type === 'text') {\n formElements[0].select()\n }\n for (const el of formElements) {\n if (el.realType) {\n el.type = el.realType\n }\n }\n } else {\n // With no form elements to allow for form submission on enter, focus the\n // continue button.\n this.continueButton.focus()\n }\n\n async function handleContinue (event) {\n let result\n try {\n result = await this.formSubmitHandler(event)\n } catch (error) {\n reject(error)\n }\n this.close(true)\n resolve(result)\n }\n\n async function handleCancel () {\n let result\n try {\n result = await this.close()\n } catch (error) {\n reject(error)\n }\n resolve(result)\n }\n })\n }\n\n close (success = false) {\n this.container.style.display = 'none'\n if (this.container.parentNode) {\n this.container.parentNode.removeChild(this.container)\n }\n\n // @todo this needs to remove eventlisteners to prevent memory leaks\n\n if ((success !== true) && typeof this.exitHandler === 'function') {\n return this.exitHandler(this)\n }\n }\n\n /**\n * Adds different form elements to the modal depending on the case.\n *\n * @param {*} elements - The properties of the form elements to add.\n * @returns {HTMLElement} The input form elements.\n */\n addFormElement (...elements) {\n const body = this.container.querySelector('.dcp-modal-body')\n const inputElements = []\n let label\n for (let i = 0; i < elements.length; i++) {\n let row = document.createElement('div')\n row.className = 'row'\n\n let col, input\n switch (elements[i].type) {\n case 'button':\n col = document.createElement('div')\n col.className = 'col-md-12'\n\n input = document.createElement('button')\n input.innerHTML = elements[i].label\n input.type = 'button'\n input.classList.add('green-modal-button')\n if (!elements[i].onclick) {\n throw new Error('A button in the modal body should have an on click event handler.')\n }\n input.addEventListener('click', elements[i].onclick)\n\n col.appendChild(input)\n row.appendChild(col)\n break\n case 'textarea':\n col = document.createElement('div')\n col.className = 'col-md-12'\n\n input = document.createElement('textarea')\n input.className = 'text-input-field form-control'\n if (elements[i].placeholder) input.placeholder = elements[i].placeholder\n\n col.appendChild(input)\n row.appendChild(col)\n break\n case 'text':\n case 'email':\n case 'number':\n case 'password': {\n const inputCol = document.createElement('div')\n\n input = document.createElement('input')\n input.type = elements[i].type\n input.validation = elements[i].validation\n input.autocomplete = elements[i].autocomplete || (elements[i].type === 'password' ? 'off' : 'on')\n input.className = 'text-input-field form-control'\n\n // Adding bootstraps custom feedback styles.\n let invalidFeedback = null\n if (elements[i].invalidFeedback) {\n invalidFeedback = document.createElement('div')\n invalidFeedback.className = 'invalid-feedback'\n invalidFeedback.innerText = elements[i].invalidFeedback\n }\n\n if (elements[i].type === 'password') {\n elements[i].realType = 'password'\n }\n\n if (elements[i].label) {\n const labelCol = document.createElement('div')\n label = document.createElement('label')\n label.innerText = elements[i].label\n const inputId = 'dcp-modal-input-' + this.container.querySelectorAll('input[type=\"text\"], input[type=\"email\"], input[type=\"number\"], input[type=\"password\"]').length\n label.setAttribute('for', inputId)\n input.id = inputId\n labelCol.classList.add('col-md-6', 'label-column')\n labelCol.appendChild(label)\n row.appendChild(labelCol)\n inputCol.className = 'col-md-6'\n } else {\n inputCol.className = 'col-md-12'\n }\n\n inputCol.appendChild(input)\n if (invalidFeedback !== null) {\n inputCol.appendChild(invalidFeedback)\n }\n row.appendChild(inputCol)\n break\n }\n case 'select':\n col = document.createElement('div')\n col.className = 'col-md-4'\n\n label = document.createElement('span')\n label.innerText = elements[i].label\n\n col.appendChild(label)\n row.appendChild(col)\n\n col = document.createElement('div')\n col.className = 'col-md-8'\n\n input = document.createElement('select')\n\n col.appendChild(input)\n row.appendChild(col)\n break\n case 'checkbox': {\n row.classList.add('checkbox-row')\n const checkboxLabelCol = document.createElement('div')\n checkboxLabelCol.classList.add('label-column', 'checkbox-label-column')\n\n label = document.createElement('label')\n label.innerText = elements[i].label\n label.for = 'dcp-checkbox-input-' + this.container.querySelectorAll('input[type=\"checkbox\"]').length\n label.setAttribute('for', label.for)\n label.className = 'checkbox-label'\n\n checkboxLabelCol.appendChild(label)\n\n const checkboxCol = document.createElement('div')\n checkboxCol.classList.add('checkbox-column')\n\n input = document.createElement('input')\n input.type = 'checkbox'\n input.id = label.for\n if (elements[i].checked) {\n input.checked = true\n }\n\n checkboxCol.appendChild(input)\n\n if (elements[i].labelToTheRightOfCheckbox) {\n checkboxCol.classList.add('col-md-5')\n row.appendChild(checkboxCol)\n checkboxLabelCol.classList.add('col-md-7')\n row.appendChild(checkboxLabelCol)\n } else {\n checkboxLabelCol.classList.add('col-md-6')\n checkboxCol.classList.add('col-md-6')\n row.appendChild(checkboxLabelCol)\n row.appendChild(checkboxCol)\n }\n break\n }\n case 'file':\n [input, row] = this.addFileInput(elements[i], input, row)\n break\n case 'label':\n row.classList.add('label-row')\n label = document.createElement('label')\n label.innerText = elements[i].label\n row.appendChild(label)\n break\n }\n\n // Copy other possibly specified element properties:\n const inputPropertyNames = ['title', 'inputmode', 'value', 'minLength', 'maxLength', 'size', 'required', 'pattern', 'min', 'max', 'step', 'placeholder', 'accept', 'multiple', 'id', 'onkeypress', 'oninput', 'for', 'readonly', 'autocomplete']\n for (const propertyName of inputPropertyNames) {\n if (Object.prototype.hasOwnProperty.call(elements[i], propertyName)) {\n if (propertyName === 'for' && !label.hasAttribute(propertyName)) {\n label.setAttribute(propertyName, elements[i][propertyName])\n }\n if (propertyName.startsWith('on')) {\n input.addEventListener(propertyName.slice(2), elements[i][propertyName])\n } else {\n input.setAttribute(propertyName, elements[i][propertyName])\n }\n }\n }\n\n inputElements.push(input)\n body.appendChild(row)\n }\n\n if (inputElements.length === 1) return inputElements[0]\n else return inputElements\n }\n\n /**\n * Adds a drag and drop file form element to the modal.\n *\n * @param {*} fileInputProperties - An object specifying some of the\n * properties of the file input element.\n * @param {*} fileInput - Placeholders to help create the file\n * input.\n * @param {HTMLDivElement} row - Placeholders to help create the file\n * input.\n */\n addFileInput (fileInputProperties, fileInput, row) {\n // Adding the upload label.\n const uploadLabel = document.createElement('label')\n uploadLabel.innerText = fileInputProperties.label\n row.appendChild(uploadLabel)\n const body = this.container.querySelector('.dcp-modal-body')\n body.appendChild(row)\n const fileSelectionRow = document.createElement('div')\n fileSelectionRow.id = 'file-selection-row'\n\n // Adding the drag and drop file upload input.\n const dropContainer = document.createElement('div')\n dropContainer.id = 'drop-container'\n\n // Adding an image of a wallet\n const imageContainer = document.createElement('div')\n imageContainer.id = 'image-container'\n const walletImage = document.createElement('span')\n walletImage.classList.add('fas', 'fa-wallet')\n imageContainer.appendChild(walletImage)\n\n // Adding some text prompts\n const dropMessage = document.createElement('span')\n dropMessage.innerText = 'Drop a keystore file here'\n const orMessage = document.createElement('span')\n orMessage.innerText = 'or'\n\n // Adding the manual file input element (hiding the default one)\n const fileInputContainer = document.createElement('div')\n const fileInputLabel = document.createElement('label')\n // Linking the label to the file input so that clicking on the label\n // activates the file input.\n fileInputLabel.setAttribute('for', 'file-input')\n fileInputLabel.innerText = 'Browse'\n fileInput = document.createElement('input')\n fileInput.type = fileInputProperties.type\n fileInput.id = 'file-input'\n // To remove the lingering outline after selecting the file.\n fileInput.addEventListener('click', () => {\n fileInput.blur()\n })\n fileInputContainer.append(fileInput, fileInputLabel)\n\n // Creating the final row element to append to the modal body.\n dropContainer.append(imageContainer, dropMessage, orMessage, fileInputContainer)\n fileSelectionRow.appendChild(dropContainer)\n\n // Adding functionality to the drag and drop file input.\n dropContainer.addEventListener('drop', selectDroppedFile.bind(this))\n dropContainer.addEventListener('drop', unhighlightDropArea)\n // Prevent file from being opened by the browser.\n dropContainer.ondragover = highlightDropArea\n dropContainer.ondragenter = highlightDropArea\n dropContainer.ondragleave = unhighlightDropArea\n\n fileInput.addEventListener('change', handleFileChange)\n\n const fileNamePlaceholder = document.createElement('center')\n fileNamePlaceholder.id = 'file-name-placeholder'\n fileNamePlaceholder.className = 'row'\n fileNamePlaceholder.innerText = ''\n fileSelectionRow.appendChild(fileNamePlaceholder)\n fileNamePlaceholder.classList.add('hidden')\n\n // Check if the continue button is invalid on the keystore upload modal and\n // click it if it should no longer be invalid.\n this.continueButton.addEventListener('invalid', () => {\n const fileFormElements = this.container.querySelectorAll('.dcp-modal-body input[type=\"file\"], .dcp-modal-body input[type=\"text\"]')\n const filledInFileFormElements = Array.from(fileFormElements).filter(fileFormElement => fileFormElement.value !== '')\n if (fileFormElements.length !== 0 && filledInFileFormElements.length !== 0) {\n this.continueButton.setCustomValidity('')\n // Clicking instead of dispatching a submit event to ensure other form validation is used before submitting the form.\n this.continueButton.click()\n }\n })\n\n return [fileInput, fileSelectionRow]\n\n /**\n * Checks that the dropped items contain only a single keystore file.\n * If valid, sets the file input's value to the dropped file.\n * @param {DragEvent} event - Contains the files dropped.\n */\n function selectDroppedFile (event) {\n // Prevent file from being opened.\n event.preventDefault()\n\n // Check if only one file was dropped.\n const wasOneFileDropped = event.dataTransfer.items.length === 1 ||\n event.dataTransfer.files.length === 1\n updateFileSelectionStatus(wasOneFileDropped)\n if (!wasOneFileDropped) {\n fileInput.setCustomValidity('Only one file can be uploaded.')\n fileInput.reportValidity()\n return\n } else {\n fileInput.setCustomValidity('')\n }\n\n // Now to use the DataTransfer interface to access the file(s), setting\n // the value of the file input.\n const file = event.dataTransfer.files[0]\n\n if (checkFileExtension(file)) {\n fileInput.files = event.dataTransfer.files\n fileInput.dispatchEvent(new Event('change'))\n }\n }\n\n function handleFileChange () {\n if (checkFileExtension(this.files[0]) && this.files.length === 1) {\n fileNamePlaceholder.innerText = `Selected File: ${this.files[0].name}`\n updateFileSelectionStatus(true)\n // Invoke a callback if additional functionality is required.\n if (typeof fileInputProperties.callback === 'function') {\n fileInputProperties.callback(this.files[0])\n }\n }\n }\n\n /**\n * Checks if the file extension on the inputted file is correct.\n * @param {File} file - The file to check\n * @returns {boolean} True if the file extension is valid, false otherwise.\n */\n function checkFileExtension (file) {\n // If there's no restriction, return true.\n if (!fileInputProperties.extension) {\n return true\n }\n const fileExtension = file.name.split('.').pop()\n const isValidExtension = fileExtension === fileInputProperties.extension\n updateFileSelectionStatus(isValidExtension)\n if (!isValidExtension) {\n fileInput.setCustomValidity(`Only a .${fileInputProperties.extension} file can be uploaded.`)\n fileInput.reportValidity()\n fileNamePlaceholder.classList.add('hidden')\n } else {\n fileInput.setCustomValidity('')\n }\n return isValidExtension\n }\n\n /**\n * Updates the file input to reflect the validity of the current file\n * selection.\n * @param {boolean} isValidFileSelection - True if a single .keystore file\n * was selected. False otherwise.\n */\n function updateFileSelectionStatus (isValidFileSelection) {\n imageContainer.innerHTML = ''\n const statusImage = document.createElement('span')\n statusImage.classList.add('fas', isValidFileSelection ? 'fa-check' : 'fa-times')\n statusImage.style.color = isValidFileSelection ? 'green' : 'red'\n imageContainer.appendChild(statusImage)\n\n if (!isValidFileSelection) {\n fileInput.value = null\n fileNamePlaceholder.classList.add('hidden')\n } else {\n fileNamePlaceholder.classList.remove('hidden')\n }\n\n // If the modal contains a password field for a keystore file, change its\n // visibility.\n const walletPasswordInputContainer = document.querySelector('.dcp-modal-body input[type=\"password\"]').parentElement.parentElement\n if (walletPasswordInputContainer) {\n if (isValidFileSelection) {\n walletPasswordInputContainer.classList.remove('hidden')\n const walletPasswordInput = document.querySelector('.dcp-modal-body input[type=\"password\"]')\n walletPasswordInput.focus()\n } else {\n walletPasswordInputContainer.classList.add('hidden')\n }\n }\n }\n\n function highlightDropArea (event) {\n event.preventDefault()\n this.classList.add('highlight')\n }\n\n function unhighlightDropArea (event) {\n event.preventDefault()\n this.classList.remove('highlight')\n }\n }\n\n /**\n * Sets up a custom tooltip to pop up when the passwords do not match, but are\n * valid otherwise.\n */\n addFormValidationForPasswordConfirmation () {\n const [newPassword, confirmPassword] = document.querySelectorAll('.dcp-modal-body input[type=\"password\"]')\n if (!newPassword || !confirmPassword) {\n throw Error('New Password field and Confirm Password fields not present.')\n }\n\n newPassword.addEventListener('input', checkMatchingPasswords)\n confirmPassword.addEventListener('input', checkMatchingPasswords)\n\n function checkMatchingPasswords () {\n if (newPassword.value !== confirmPassword.value &&\n newPassword.validity.valid &&\n confirmPassword.validity.valid) {\n newPassword.setCustomValidity('Both passwords must match.')\n } else if (newPassword.value === confirmPassword.value ||\n newPassword.validity.tooShort ||\n newPassword.validity.patternMismatch ||\n newPassword.validity.valueMissing ||\n confirmPassword.validity.tooShort ||\n confirmPassword.validity.patternMismatch ||\n confirmPassword.validity.valueMissing) {\n // If the passwords fields match or have become invalidated some other\n // way again, reset the custom message.\n newPassword.setCustomValidity('')\n }\n }\n }\n\n updateInvalidEmailMessage() {\n const email = document.querySelector('.dcp-modal-body input[id=\"email\"')\n if (!email){\n throw Error(\"Email field not present\")\n }\n email.addEventListener('input', checkValidEmail);\n function checkValidEmail() {\n if (!email.validity.patternMismatch &&\n !email.validity.valueMissing) {\n email.setCustomValidity('')\n } else {\n email.setCustomValidity(\"Enter a valid email address.\")\n }\n\n }\n }\n\n /**\n * Adds message(s) to the modal's body.\n * @param {string} messages - The message(s) to add to the modal's body.\n * @returns Paragraph element(s) containing the message(s) added to the\n * modal's body.\n */\n addMessage (...messages) {\n const elements = []\n const body = this.container.querySelector('.dcp-modal-body')\n for (let i = 0; i < messages.length; i++) {\n const row = document.createElement('div')\n row.className = 'row'\n\n const paragraph = document.createElement('p')\n paragraph.innerHTML = messages[i]\n paragraph.classList.add('message')\n row.appendChild(paragraph)\n body.appendChild(row)\n\n elements.push(paragraph)\n }\n\n if (elements.length === 1) return elements[0]\n else return elements\n }\n\n addHorizontalRule () {\n const body = this.container.querySelector('.dcp-modal-body')\n body.appendChild(document.createElement('hr'))\n }\n\n // Does what it says. Still ill advised to use unless you have to.\n addCustomHTML (htmlStr, browseCallback) {\n const elements = []\n const body = this.container.querySelector('.dcp-modal-body')\n body.innerHTML += htmlStr\n body.querySelector('#browse-button').addEventListener('click', browseCallback.bind(this, this))\n\n if (elements.length === 1) return elements[0]\n else return elements\n }\n\n addButton (...buttons) {\n const elements = []\n const body = this.container.querySelector('.dcp-modal-body')\n for (let i = 0; i < buttons.length; i++) {\n const row = document.createElement('div')\n row.className = 'row'\n\n let col = document.createElement('div')\n col.className = 'col-md-4'\n\n const description = document.createElement('span')\n description.innerText = buttons[i].description\n\n col.appendChild(description)\n row.appendChild(col)\n\n col = document.createElement('div')\n col.className = 'col-md-8'\n\n const button = document.createElement('button')\n button.innerText = buttons[i].label\n button.addEventListener('click', buttons[i].callback.bind(this, this))\n\n elements.push(button)\n\n col.appendChild(button)\n row.appendChild(col)\n\n body.appendChild(row)\n }\n\n if (elements.length === 1) return elements[0]\n else return elements\n }\n}\n\nexports.Modal = Modal;\n\n// Inject our special stylesheet from dcp-client only if we're on the portal webpage.\nif (typeof window !== 'undefined' && typeof document !== 'undefined' && dcpConfig.portal.location.hostname === window.location.hostname) {\n // <link rel='stylesheet' href='/css/dashboard.css'>\n const stylesheet = document.createElement('link')\n stylesheet.rel = 'stylesheet'\n // Needed for the duplicate check done later.\n stylesheet.id = 'dcp-modal-styles'\n\n const dcpClientBundle = document.getElementById('_dcp_client_bundle')\n let src\n if (dcpClientBundle) {\n src = dcpClientBundle.src.replace('dcp-client-bundle.js', 'dcp-modal-style.css')\n } else {\n src = dcpConfig.portal.location.href + 'dcp-client/dist/dcp-modal-style.css'\n }\n\n stylesheet.href = src\n // If the style was injected before, don't inject it again.\n // Could occur when loading a file that imports Modal.js and loading\n // comput.min.js in the same HTML file.\n if (document.getElementById(stylesheet.id) === null) {\n document.getElementsByTagName('head')[0].appendChild(stylesheet)\n }\n\n if (typeof {\"version\":\"73443b4915fb3f02711ce4129ebfe98a2c48966b\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"4.1.11\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#develop\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#5385c2356a9b6a39f736cc33fba7cdac0f976adc\"},\"built\":\"Tue Sep 14 2021 11:10:56 GMT-0400 (Eastern Daylight Time)\",\"config\":{\"generated\":\"Tue 14 Sep 2021 11:10:55 AM EDT by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"4.46.0\",\"node\":\"v12.22.6\"} !== 'undefined' && typeof window.Modal === 'undefined') {\n window.Modal = Modal\n }\n}\n\n\n//# sourceURL=webpack:///./src/protocol-v3/modal.js?");
|
|
5473
|
+
eval("/**\n * A Small Modal Class\n * @module Modal\n */\n/* globals Event dcpConfig */\nclass Modal {\n constructor (title, message, callback = false, exitHandler = false, {\n continueLabel = 'Continue',\n cancelLabel = 'Cancel',\n cancelVisible = true\n } = {}) {\n const modal = document.createElement('div')\n modal.className = 'dcp-modal-container-old day'\n modal.innerHTML = `\n <dialog class=\"dcp-modal-content\">\n <div class=\"dcp-modal-header\">\n <h2>${title}<button type=\"button\" class=\"close\">×</button></h2>\n ${message ? '<p>' + message + '</p>' : ''}\n </div>\n <div class=\"dcp-modal-loading hidden\">\n <div class='loading'></div>\n </div>\n <form onsubmit='return false' method=\"dialog\">\n <div class=\"dcp-modal-body\"></div>\n <div class=\"dcp-modal-footer ${cancelVisible ? '' : 'centered'}\">\n <button type=\"submit\" class=\"continue green-modal-button\">${continueLabel}</button>\n <button type=\"button\" class=\"cancel green-modal-button\">${cancelLabel}</button>\n </div>\n </form>\n </dialog>`\n\n // To give a reference to do developer who wants to override the form submit.\n // May occur if they want to validate the information in the backend\n // without closing the modal prematurely.\n this.form = modal.querySelector('.dcp-modal-content form')\n this.continueButton = modal.querySelector('.dcp-modal-footer button.continue')\n this.cancelButton = modal.querySelector('.dcp-modal-footer button.cancel')\n this.closeButton = modal.querySelector('.dcp-modal-header .close')\n if (!cancelVisible) {\n this.cancelButton.style.display = 'none'\n }\n\n // To remove the event listener, the reference to the original function\n // added is required.\n this.formSubmitHandler = this.continue.bind(this)\n\n modal.addEventListener('keydown', function (event) {\n event.stopPropagation()\n // 27 is the keycode for the escape key.\n if (event.keyCode === 27) this.close()\n }.bind(this))\n\n this.container = modal\n this.callback = callback\n this.exitHandler = exitHandler\n document.body.appendChild(modal)\n }\n\n changeFormSubmitHandler (newFormSubmitHandler) {\n this.formSubmitHandler = newFormSubmitHandler\n }\n\n /**\n * Validates the form values in the modal and calls the modal's callback\n */\n async continue (event) {\n // To further prevent form submission from trying to redirect from the\n // current page.\n if (event instanceof Event) {\n event.preventDefault()\n }\n let fieldsAreValid = true\n let formElements = this.container.querySelectorAll('.dcp-modal-body select, .dcp-modal-body input, .dcp-modal-body textarea')\n\n const formValues = []\n if (typeof formElements.length === 'undefined') formElements = [formElements]\n // Separate into two loops to enable input validation requiring formValues\n // that come after it. e.g. Two password fields matching.\n for (let i = 0; i < formElements.length; i++) {\n switch (formElements[i].type) {\n case 'file':\n formValues.push(formElements[i])\n break\n case 'checkbox':\n formValues.push(formElements[i].checked)\n break\n default:\n formValues.push(formElements[i].value)\n break\n }\n }\n for (let i = 0; i < formElements.length; i++) {\n if (formElements[i].validation) {\n // Optional fields are allowed to be empty but still can't be wrong if not empty.\n if (!(formElements[i].value === '' && !formElements[i].required)) {\n if (typeof formElements[i].validation === 'function') {\n if (!formElements[i].validation(formValues)) {\n fieldsAreValid = false\n formElements[i].classList.add('is-invalid')\n }\n } else if (!formElements[i].validation.test(formElements[i].value)) {\n fieldsAreValid = false\n formElements[i].classList.add('is-invalid')\n }\n }\n }\n }\n\n if (!fieldsAreValid) return\n\n this.loading()\n if (typeof this.callback === 'function') {\n try {\n return this.callback(formValues)\n } catch (error) {\n console.error('Unexpected error in modal.continue:', error);\n return this.close(false)\n }\n }\n this.close(true)\n }\n\n loading () {\n this.container.querySelector('.dcp-modal-loading').classList.remove('hidden')\n this.container.querySelector('.dcp-modal-body').classList.add('hidden')\n this.container.querySelector('.dcp-modal-footer').classList.add('hidden')\n }\n\n open () {\n this.form.addEventListener('submit', async (event) => {\n const success = await this.formSubmitHandler(event)\n if (success === false) {\n return\n }\n this.close(true)\n })\n // When the user clicks on <span> (x), close the modal\n this.closeButton.addEventListener('click', this.close.bind(this))\n this.cancelButton.addEventListener('click', this.close.bind(this))\n\n // Prevent lingering outlines after clicking some form elements.\n this.container.querySelectorAll('.dcp-modal-body button, .dcp-modal-body input[type=\"checkbox\"]').forEach(element => {\n element.addEventListener('click', () => {\n element.blur()\n })\n })\n\n // Show the modal.\n this.container.style.display = 'block'\n\n const formElements = this.container.querySelectorAll('.dcp-modal-body select, .dcp-modal-body input')\n if (formElements.length) {\n formElements[0].focus()\n if (formElements[0].type === 'text') {\n formElements[0].select()\n }\n for (const el of formElements) {\n if (el.realType) {\n el.type = el.realType\n }\n }\n } else {\n // With no form elements to allow for form submission on enter, focus the\n // continue button.\n this.container.querySelector('.dcp-modal-footer button.continue').focus()\n }\n } // TODO: This should return a promise with the action resolving it\n\n /**\n * Shows the modal and returns a promise of the result of the modal (e.g. was\n * it closed, did its action succeed?)\n */\n showModal () {\n return new Promise((resolve, reject) => {\n this.form.addEventListener('submit', handleContinue.bind(this))\n this.cancelButton.addEventListener('click', handleCancel.bind(this))\n this.closeButton.addEventListener('click', handleCancel.bind(this))\n\n // Prevent lingering outlines after clicking some form elements.\n this.container.querySelectorAll('.dcp-modal-body button, .dcp-modal-body input[type=\"checkbox\"]').forEach(element => {\n element.addEventListener('click', () => {\n element.blur()\n })\n })\n\n // Show the modal.\n this.container.style.display = 'block'\n\n const formElements = this.container.querySelectorAll('.dcp-modal-body select, .dcp-modal-body input')\n if (formElements.length) {\n formElements[0].focus()\n if (formElements[0].type === 'text') {\n formElements[0].select()\n }\n for (const el of formElements) {\n if (el.realType) {\n el.type = el.realType\n }\n }\n } else {\n // With no form elements to allow for form submission on enter, focus the\n // continue button.\n this.continueButton.focus()\n }\n\n async function handleContinue (event) {\n let result\n try {\n result = await this.formSubmitHandler(event)\n } catch (error) {\n reject(error)\n }\n this.close(true)\n resolve(result)\n }\n\n async function handleCancel () {\n let result\n try {\n result = await this.close()\n } catch (error) {\n reject(error)\n }\n resolve(result)\n }\n })\n }\n\n close (success = false) {\n this.container.style.display = 'none'\n if (this.container.parentNode) {\n this.container.parentNode.removeChild(this.container)\n }\n\n // @todo this needs to remove eventlisteners to prevent memory leaks\n\n if ((success !== true) && typeof this.exitHandler === 'function') {\n return this.exitHandler(this)\n }\n }\n\n /**\n * Adds different form elements to the modal depending on the case.\n *\n * @param {*} elements - The properties of the form elements to add.\n * @returns {HTMLElement} The input form elements.\n */\n addFormElement (...elements) {\n const body = this.container.querySelector('.dcp-modal-body')\n const inputElements = []\n let label\n for (let i = 0; i < elements.length; i++) {\n let row = document.createElement('div')\n row.className = 'row'\n\n let col, input\n switch (elements[i].type) {\n case 'button':\n col = document.createElement('div')\n col.className = 'col-md-12'\n\n input = document.createElement('button')\n input.innerHTML = elements[i].label\n input.type = 'button'\n input.classList.add('green-modal-button')\n if (!elements[i].onclick) {\n throw new Error('A button in the modal body should have an on click event handler.')\n }\n input.addEventListener('click', elements[i].onclick)\n\n col.appendChild(input)\n row.appendChild(col)\n break\n case 'textarea':\n col = document.createElement('div')\n col.className = 'col-md-12'\n\n input = document.createElement('textarea')\n input.className = 'text-input-field form-control'\n if (elements[i].placeholder) input.placeholder = elements[i].placeholder\n\n col.appendChild(input)\n row.appendChild(col)\n break\n case 'text':\n case 'email':\n case 'number':\n case 'password': {\n const inputCol = document.createElement('div')\n\n input = document.createElement('input')\n input.type = elements[i].type\n input.validation = elements[i].validation\n input.autocomplete = elements[i].autocomplete || (elements[i].type === 'password' ? 'off' : 'on')\n input.className = 'text-input-field form-control'\n\n // Adding bootstraps custom feedback styles.\n let invalidFeedback = null\n if (elements[i].invalidFeedback) {\n invalidFeedback = document.createElement('div')\n invalidFeedback.className = 'invalid-feedback'\n invalidFeedback.innerText = elements[i].invalidFeedback\n }\n\n if (elements[i].type === 'password') {\n elements[i].realType = 'password'\n }\n\n if (elements[i].label) {\n const labelCol = document.createElement('div')\n label = document.createElement('label')\n label.innerText = elements[i].label\n const inputId = 'dcp-modal-input-' + this.container.querySelectorAll('input[type=\"text\"], input[type=\"email\"], input[type=\"number\"], input[type=\"password\"]').length\n label.setAttribute('for', inputId)\n input.id = inputId\n labelCol.classList.add('col-md-6', 'label-column')\n labelCol.appendChild(label)\n row.appendChild(labelCol)\n inputCol.className = 'col-md-6'\n } else {\n inputCol.className = 'col-md-12'\n }\n\n inputCol.appendChild(input)\n if (invalidFeedback !== null) {\n inputCol.appendChild(invalidFeedback)\n }\n row.appendChild(inputCol)\n break\n }\n case 'select':\n col = document.createElement('div')\n col.className = 'col-md-4'\n\n label = document.createElement('span')\n label.innerText = elements[i].label\n\n col.appendChild(label)\n row.appendChild(col)\n\n col = document.createElement('div')\n col.className = 'col-md-8'\n\n input = document.createElement('select')\n\n col.appendChild(input)\n row.appendChild(col)\n break\n case 'checkbox': {\n row.classList.add('checkbox-row')\n const checkboxLabelCol = document.createElement('div')\n checkboxLabelCol.classList.add('label-column', 'checkbox-label-column')\n\n label = document.createElement('label')\n label.innerText = elements[i].label\n label.for = 'dcp-checkbox-input-' + this.container.querySelectorAll('input[type=\"checkbox\"]').length\n label.setAttribute('for', label.for)\n label.className = 'checkbox-label'\n\n checkboxLabelCol.appendChild(label)\n\n const checkboxCol = document.createElement('div')\n checkboxCol.classList.add('checkbox-column')\n\n input = document.createElement('input')\n input.type = 'checkbox'\n input.id = label.for\n if (elements[i].checked) {\n input.checked = true\n }\n\n checkboxCol.appendChild(input)\n\n if (elements[i].labelToTheRightOfCheckbox) {\n checkboxCol.classList.add('col-md-5')\n row.appendChild(checkboxCol)\n checkboxLabelCol.classList.add('col-md-7')\n row.appendChild(checkboxLabelCol)\n } else {\n checkboxLabelCol.classList.add('col-md-6')\n checkboxCol.classList.add('col-md-6')\n row.appendChild(checkboxLabelCol)\n row.appendChild(checkboxCol)\n }\n break\n }\n case 'file':\n [input, row] = this.addFileInput(elements[i], input, row)\n break\n case 'label':\n row.classList.add('label-row')\n label = document.createElement('label')\n label.innerText = elements[i].label\n row.appendChild(label)\n break\n }\n\n // Copy other possibly specified element properties:\n const inputPropertyNames = ['title', 'inputmode', 'value', 'minLength', 'maxLength', 'size', 'required', 'pattern', 'min', 'max', 'step', 'placeholder', 'accept', 'multiple', 'id', 'onkeypress', 'oninput', 'for', 'readonly', 'autocomplete']\n for (const propertyName of inputPropertyNames) {\n if (Object.prototype.hasOwnProperty.call(elements[i], propertyName)) {\n if (propertyName === 'for' && !label.hasAttribute(propertyName)) {\n label.setAttribute(propertyName, elements[i][propertyName])\n }\n if (propertyName.startsWith('on')) {\n input.addEventListener(propertyName.slice(2), elements[i][propertyName])\n } else {\n input.setAttribute(propertyName, elements[i][propertyName])\n }\n }\n }\n\n inputElements.push(input)\n body.appendChild(row)\n }\n\n if (inputElements.length === 1) return inputElements[0]\n else return inputElements\n }\n\n /**\n * Adds a drag and drop file form element to the modal.\n *\n * @param {*} fileInputProperties - An object specifying some of the\n * properties of the file input element.\n * @param {*} fileInput - Placeholders to help create the file\n * input.\n * @param {HTMLDivElement} row - Placeholders to help create the file\n * input.\n */\n addFileInput (fileInputProperties, fileInput, row) {\n // Adding the upload label.\n const uploadLabel = document.createElement('label')\n uploadLabel.innerText = fileInputProperties.label\n row.appendChild(uploadLabel)\n const body = this.container.querySelector('.dcp-modal-body')\n body.appendChild(row)\n const fileSelectionRow = document.createElement('div')\n fileSelectionRow.id = 'file-selection-row'\n\n // Adding the drag and drop file upload input.\n const dropContainer = document.createElement('div')\n dropContainer.id = 'drop-container'\n\n // Adding an image of a wallet\n const imageContainer = document.createElement('div')\n imageContainer.id = 'image-container'\n const walletImage = document.createElement('span')\n walletImage.classList.add('fas', 'fa-wallet')\n imageContainer.appendChild(walletImage)\n\n // Adding some text prompts\n const dropMessage = document.createElement('span')\n dropMessage.innerText = 'Drop a keystore file here'\n const orMessage = document.createElement('span')\n orMessage.innerText = 'or'\n\n // Adding the manual file input element (hiding the default one)\n const fileInputContainer = document.createElement('div')\n const fileInputLabel = document.createElement('label')\n // Linking the label to the file input so that clicking on the label\n // activates the file input.\n fileInputLabel.setAttribute('for', 'file-input')\n fileInputLabel.innerText = 'Browse'\n fileInput = document.createElement('input')\n fileInput.type = fileInputProperties.type\n fileInput.id = 'file-input'\n // To remove the lingering outline after selecting the file.\n fileInput.addEventListener('click', () => {\n fileInput.blur()\n })\n fileInputContainer.append(fileInput, fileInputLabel)\n\n // Creating the final row element to append to the modal body.\n dropContainer.append(imageContainer, dropMessage, orMessage, fileInputContainer)\n fileSelectionRow.appendChild(dropContainer)\n\n // Adding functionality to the drag and drop file input.\n dropContainer.addEventListener('drop', selectDroppedFile.bind(this))\n dropContainer.addEventListener('drop', unhighlightDropArea)\n // Prevent file from being opened by the browser.\n dropContainer.ondragover = highlightDropArea\n dropContainer.ondragenter = highlightDropArea\n dropContainer.ondragleave = unhighlightDropArea\n\n fileInput.addEventListener('change', handleFileChange)\n\n const fileNamePlaceholder = document.createElement('center')\n fileNamePlaceholder.id = 'file-name-placeholder'\n fileNamePlaceholder.className = 'row'\n fileNamePlaceholder.innerText = ''\n fileSelectionRow.appendChild(fileNamePlaceholder)\n fileNamePlaceholder.classList.add('hidden')\n\n // Check if the continue button is invalid on the keystore upload modal and\n // click it if it should no longer be invalid.\n this.continueButton.addEventListener('invalid', () => {\n const fileFormElements = this.container.querySelectorAll('.dcp-modal-body input[type=\"file\"], .dcp-modal-body input[type=\"text\"]')\n const filledInFileFormElements = Array.from(fileFormElements).filter(fileFormElement => fileFormElement.value !== '')\n if (fileFormElements.length !== 0 && filledInFileFormElements.length !== 0) {\n this.continueButton.setCustomValidity('')\n // Clicking instead of dispatching a submit event to ensure other form validation is used before submitting the form.\n this.continueButton.click()\n }\n })\n\n return [fileInput, fileSelectionRow]\n\n /**\n * Checks that the dropped items contain only a single keystore file.\n * If valid, sets the file input's value to the dropped file.\n * @param {DragEvent} event - Contains the files dropped.\n */\n function selectDroppedFile (event) {\n // Prevent file from being opened.\n event.preventDefault()\n\n // Check if only one file was dropped.\n const wasOneFileDropped = event.dataTransfer.items.length === 1 ||\n event.dataTransfer.files.length === 1\n updateFileSelectionStatus(wasOneFileDropped)\n if (!wasOneFileDropped) {\n fileInput.setCustomValidity('Only one file can be uploaded.')\n fileInput.reportValidity()\n return\n } else {\n fileInput.setCustomValidity('')\n }\n\n // Now to use the DataTransfer interface to access the file(s), setting\n // the value of the file input.\n const file = event.dataTransfer.files[0]\n\n if (checkFileExtension(file)) {\n fileInput.files = event.dataTransfer.files\n fileInput.dispatchEvent(new Event('change'))\n }\n }\n\n function handleFileChange () {\n if (checkFileExtension(this.files[0]) && this.files.length === 1) {\n fileNamePlaceholder.innerText = `Selected File: ${this.files[0].name}`\n updateFileSelectionStatus(true)\n // Invoke a callback if additional functionality is required.\n if (typeof fileInputProperties.callback === 'function') {\n fileInputProperties.callback(this.files[0])\n }\n }\n }\n\n /**\n * Checks if the file extension on the inputted file is correct.\n * @param {File} file - The file to check\n * @returns {boolean} True if the file extension is valid, false otherwise.\n */\n function checkFileExtension (file) {\n // If there's no restriction, return true.\n if (!fileInputProperties.extension) {\n return true\n }\n const fileExtension = file.name.split('.').pop()\n const isValidExtension = fileExtension === fileInputProperties.extension\n updateFileSelectionStatus(isValidExtension)\n if (!isValidExtension) {\n fileInput.setCustomValidity(`Only a .${fileInputProperties.extension} file can be uploaded.`)\n fileInput.reportValidity()\n fileNamePlaceholder.classList.add('hidden')\n } else {\n fileInput.setCustomValidity('')\n }\n return isValidExtension\n }\n\n /**\n * Updates the file input to reflect the validity of the current file\n * selection.\n * @param {boolean} isValidFileSelection - True if a single .keystore file\n * was selected. False otherwise.\n */\n function updateFileSelectionStatus (isValidFileSelection) {\n imageContainer.innerHTML = ''\n const statusImage = document.createElement('span')\n statusImage.classList.add('fas', isValidFileSelection ? 'fa-check' : 'fa-times')\n statusImage.style.color = isValidFileSelection ? 'green' : 'red'\n imageContainer.appendChild(statusImage)\n\n if (!isValidFileSelection) {\n fileInput.value = null\n fileNamePlaceholder.classList.add('hidden')\n } else {\n fileNamePlaceholder.classList.remove('hidden')\n }\n\n // If the modal contains a password field for a keystore file, change its\n // visibility.\n const walletPasswordInputContainer = document.querySelector('.dcp-modal-body input[type=\"password\"]').parentElement.parentElement\n if (walletPasswordInputContainer) {\n if (isValidFileSelection) {\n walletPasswordInputContainer.classList.remove('hidden')\n const walletPasswordInput = document.querySelector('.dcp-modal-body input[type=\"password\"]')\n walletPasswordInput.focus()\n } else {\n walletPasswordInputContainer.classList.add('hidden')\n }\n }\n }\n\n function highlightDropArea (event) {\n event.preventDefault()\n this.classList.add('highlight')\n }\n\n function unhighlightDropArea (event) {\n event.preventDefault()\n this.classList.remove('highlight')\n }\n }\n\n /**\n * Sets up a custom tooltip to pop up when the passwords do not match, but are\n * valid otherwise.\n */\n addFormValidationForPasswordConfirmation () {\n const [newPassword, confirmPassword] = document.querySelectorAll('.dcp-modal-body input[type=\"password\"]')\n if (!newPassword || !confirmPassword) {\n throw Error('New Password field and Confirm Password fields not present.')\n }\n\n newPassword.addEventListener('input', checkMatchingPasswords)\n confirmPassword.addEventListener('input', checkMatchingPasswords)\n\n function checkMatchingPasswords () {\n if (newPassword.value !== confirmPassword.value &&\n newPassword.validity.valid &&\n confirmPassword.validity.valid) {\n newPassword.setCustomValidity('Both passwords must match.')\n } else if (newPassword.value === confirmPassword.value ||\n newPassword.validity.tooShort ||\n newPassword.validity.patternMismatch ||\n newPassword.validity.valueMissing ||\n confirmPassword.validity.tooShort ||\n confirmPassword.validity.patternMismatch ||\n confirmPassword.validity.valueMissing) {\n // If the passwords fields match or have become invalidated some other\n // way again, reset the custom message.\n newPassword.setCustomValidity('')\n }\n }\n }\n\n updateInvalidEmailMessage() {\n const email = document.querySelector('.dcp-modal-body input[id=\"email\"')\n if (!email){\n throw Error(\"Email field not present\")\n }\n email.addEventListener('input', checkValidEmail);\n function checkValidEmail() {\n if (!email.validity.patternMismatch &&\n !email.validity.valueMissing) {\n email.setCustomValidity('')\n } else {\n email.setCustomValidity(\"Enter a valid email address.\")\n }\n\n }\n }\n\n /**\n * Adds message(s) to the modal's body.\n * @param {string} messages - The message(s) to add to the modal's body.\n * @returns Paragraph element(s) containing the message(s) added to the\n * modal's body.\n */\n addMessage (...messages) {\n const elements = []\n const body = this.container.querySelector('.dcp-modal-body')\n for (let i = 0; i < messages.length; i++) {\n const row = document.createElement('div')\n row.className = 'row'\n\n const paragraph = document.createElement('p')\n paragraph.innerHTML = messages[i]\n paragraph.classList.add('message')\n row.appendChild(paragraph)\n body.appendChild(row)\n\n elements.push(paragraph)\n }\n\n if (elements.length === 1) return elements[0]\n else return elements\n }\n\n addHorizontalRule () {\n const body = this.container.querySelector('.dcp-modal-body')\n body.appendChild(document.createElement('hr'))\n }\n\n // Does what it says. Still ill advised to use unless you have to.\n addCustomHTML (htmlStr, browseCallback) {\n const elements = []\n const body = this.container.querySelector('.dcp-modal-body')\n body.innerHTML += htmlStr\n body.querySelector('#browse-button').addEventListener('click', browseCallback.bind(this, this))\n\n if (elements.length === 1) return elements[0]\n else return elements\n }\n\n addButton (...buttons) {\n const elements = []\n const body = this.container.querySelector('.dcp-modal-body')\n for (let i = 0; i < buttons.length; i++) {\n const row = document.createElement('div')\n row.className = 'row'\n\n let col = document.createElement('div')\n col.className = 'col-md-4'\n\n const description = document.createElement('span')\n description.innerText = buttons[i].description\n\n col.appendChild(description)\n row.appendChild(col)\n\n col = document.createElement('div')\n col.className = 'col-md-8'\n\n const button = document.createElement('button')\n button.innerText = buttons[i].label\n button.addEventListener('click', buttons[i].callback.bind(this, this))\n\n elements.push(button)\n\n col.appendChild(button)\n row.appendChild(col)\n\n body.appendChild(row)\n }\n\n if (elements.length === 1) return elements[0]\n else return elements\n }\n}\n\nexports.Modal = Modal;\n\n// Inject our special stylesheet from dcp-client only if we're on the portal webpage.\nif (typeof window !== 'undefined' && typeof document !== 'undefined' && dcpConfig.portal.location.hostname === window.location.hostname) {\n // <link rel='stylesheet' href='/css/dashboard.css'>\n const stylesheet = document.createElement('link')\n stylesheet.rel = 'stylesheet'\n // Needed for the duplicate check done later.\n stylesheet.id = 'dcp-modal-styles'\n\n const dcpClientBundle = document.getElementById('_dcp_client_bundle')\n let src\n if (dcpClientBundle) {\n src = dcpClientBundle.src.replace('dcp-client-bundle.js', 'dcp-modal-style.css')\n } else {\n src = dcpConfig.portal.location.href + 'dcp-client/dist/dcp-modal-style.css'\n }\n\n stylesheet.href = src\n // If the style was injected before, don't inject it again.\n // Could occur when loading a file that imports Modal.js and loading\n // comput.min.js in the same HTML file.\n if (document.getElementById(stylesheet.id) === null) {\n document.getElementsByTagName('head')[0].appendChild(stylesheet)\n }\n\n if (typeof {\"version\":\"dd98e423ca01eaeeba22e0bd0d948e358ffc7b43\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"4.1.11\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#release\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#9275023e3d51fbc2437111118c8d3bbb73bfebc1\"},\"built\":\"Mon Sep 20 2021 11:03:08 GMT-0400 (Eastern Daylight Time)\",\"config\":{\"generated\":\"Mon 20 Sep 2021 11:03:07 AM EDT by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"4.46.0\",\"node\":\"v12.22.6\"} !== 'undefined' && typeof window.Modal === 'undefined') {\n window.Modal = Modal\n }\n}\n\n\n//# sourceURL=webpack:///./src/protocol-v3/modal.js?");
|
|
5474
5474
|
|
|
5475
5475
|
/***/ }),
|
|
5476
5476
|
|
|
@@ -5771,7 +5771,7 @@ eval("/**\n * @file utils/http.js Helper module for things rel
|
|
|
5771
5771
|
/*! no static exports found */
|
|
5772
5772
|
/***/ (function(module, exports, __webpack_require__) {
|
|
5773
5773
|
|
|
5774
|
-
eval("/* WEBPACK VAR INJECTION */(function(process) {/**\n * @file src/utils/index.js\n * @author Ryan Rossiter\n * @date Feb 2020\n *\n * Place to put little JS utilities. If they are more than a few lines, please `require` and `export` \n * them instead of making this monolithic.\n */\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n\n module.exports.paramUtils = __webpack_require__(/*! ./assert-params */ \"./src/utils/assert-params.js\");\n module.exports.httpUtils = __webpack_require__(/*! ./http */ \"./src/utils/http.js\");\n module.exports.serialize = __webpack_require__(/*! ./serialize */ \"./src/utils/serialize.js\");\n Object.assign(exports, __webpack_require__(/*! ./web-format-date */ \"./src/utils/web-format-date.js\"));\n Object.assign(exports, __webpack_require__(/*! ./confirm-prompt */ \"./src/utils/confirm-prompt.js\"));\n Object.assign(exports, __webpack_require__(/*! ./message-to-buffer */ \"./src/utils/message-to-buffer.js\"));\n\n/** @typedef {import('dcp/dcp-client/worker/slice').Slice} Slice */\n/** @typedef {import('dcp/dcp-client/worker/sandbox').Sandbox} Sandbox */\n\n /**\n * Writes object properties into another object matching shape.\n * For example: if obj = { a: { b: 1, c: 2}}\n * and source = { a: {c: 0, d: 1}}\n * then obj will be updated to { a: { b: 1, c: 0, d: 1 } }\n * compare this to Object.assign which gives { a: { c: 0, d: 1 } }\n */\nconst setObjProps = module.exports.setObjProps = (obj, source) => {\n for (let p in source) {\n if (typeof source[p] === 'object') setObjProps(obj[p], source[p]);\n else obj[p] = source[p];\n }\n}\n\n\n/**\n * Generates a new random opaqueId i.e. a 22-character base64 string.\n * Used for job and slice ids.\n */\nmodule.exports.generateOpaqueId = function () {\n const generate = __webpack_require__(/*! nanoid/generate */ \"./node_modules/nanoid/generate.js\");\n return generate('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+', 22);\n}\n\n/**\n * Accepts an object and a list of properties to apply to the object to retreive a final value.\n * E.g.\n * object = { a: { b: { c: 4 } } }\n * listOfProperties = [\"a\", \"b\", \"c\"]\n * return = 4\n */\nmodule.exports.getNestedFromListOfProperties = function (object, listOfProperties) {\n return listOfProperties.reduce((o, k) => {\n if (typeof o !== 'object') return undefined;\n return o[k];\n }, object);\n}\n\n/**\n * Accepts an object and a dot-separated property name to retrieve from the object.\n * E.g.\n * object = { a: { b: { c: 4 } } }\n * property = 'a.b.c'\n * return = 4\n *\n * @param {object} object \n * @param {string} property \n */\nmodule.exports.getNestedProperty = function (object, property) {\n if (typeof property !== 'string') return undefined;\n return module.exports.getNestedFromListOfProperties(object, property.split(\".\"));\n}\n\n/**\n * Accepts an object and a list of properties and a value and mutates the object\n * to have the specified value at the location denoted by the list of properties.\n * Similar to getNestedFromListOfProperties, but sets instead of gets.\n */\nmodule.exports.setNestedFromListOfProperties = function (object, listOfProperties, value) {\n if(!listOfProperties.length || listOfProperties.length < 1) {\n throw new Error(\"listOfProperties must be an array of length >= 1\");\n }\n const indexOfLastProp = listOfProperties.length - 1;\n const pathToParent = listOfProperties.slice(0, indexOfLastProp);\n const parent = module.exports.getNestedFromListOfProperties(object, pathToParent);\n if(!parent) {\n throw new Error(\"Could not find value at:\", pathToParent, \"in object:\", object);\n }\n const lastProperty = listOfProperties[indexOfLastProp];\n parent[lastProperty] = value;\n}\n\n/**\n * Block the event loop for a specified time\n * @milliseconds the number of milliseconds to wait (integer)\n */\nexports.msleep = function utils$$msleep(milliseconds) {\n try\n {\n let sab = new SharedArrayBuffer(4);\n let int32 = new Int32Array(sab);\n Atomics.wait(int32, 0, 0, milliseconds);\n }\n catch(error)\n {\n console.error('Cannot msleep;', error);\n }\n}\n\n/**\n * Block the event loop for a specified time\n * @seconds the number of seconds to wait (float)\n */\nexports.sleep = function utils$$sleep(seconds) {\n return exports.msleep(seconds * 1000);\n}\n\n/** Resolve a promise after a specified time */\nexports.asleepMs = function asleepMs(ms) {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\n/** Resolve a promise after a specified time */\nexports.asleep = function asleep(seconds) {\n return new Promise(resolve => setTimeout(resolve, seconds * 1000));\n}\n\n/** \n * Returns the number of millisecond in a time expression.\n * @param s {number} The number of seconds\n * @returns {number}\n */\n/**\n * @param s {string} A complex time expression using m, w, d, s, h. '10d 6h 1s' means 10 days, 6 hours, and 1 second.\n */\nexports.ms = function utils$$ms(s)\n{\n let ms = 0;\n \n if (typeof s === 'number')\n return s * 1000;\n\n assert(typeof s === 'string');\n \n for (let expr of s.match(/[0-9.]+[smhdw]/g))\n {\n let unit = expr.slice(-1);\n let value = +expr.slice(0, -1);\n\n switch(unit)\n {\n case 's': {\n ms += value * 1000;\n break;\n }\n case 'm': {\n ms += value * 1000 * 60;\n break;\n }\n case 'h': {\n ms += value * 1000 * 60 * 60;\n break;\n }\n case 'd': {\n ms = value * 1000 * 60 * 60 * 24;\n break;\n }\n case 'w': {\n ms = value * 1000 * 60 * 60 * 24 * 7;\n break;\n }\n default: {\n throw new Error(`invalid time unit ${unit}`);\n }\n }\n }\n\n return ms;\n}\n\n/**\n * Returns a percentage as a number.\n * @param n {number} this number is returned\n */\n/**\n * @param n {string} this string is converted to a number and returned; it is divided by 100 if it ends in %.\n */\nexports.pct = function utils$$pct(n)\n{\n if (typeof n === 'number')\n return n / 100;\n\n if (n.match(/%$/))\n return +n / 100;\n\n return +n;\n}\n\n/**\n * Coerce human-written or registry-provided values into Boolean in a sensisble way.\n */\nexports.booley = function utils$$booley(check)\n{\n switch (typeof check)\n {\n case 'undefined':\n return false;\n case 'string':\n return check && (check !== 'false');\n case 'boolean':\n return check;\n case 'number':\n case 'object':\n return Boolean(check);\n default:\n throw new Error(`can't coerce ${typeof check} to booley`);\n }\n}\n\ntry {\n exports.useChalk = requireNative ? requireNative('tty').isatty(0) || process.env.FORCE_COLOR : false;\n}\ncatch (error) {\n if (error.message.includes('no native require'))\n exports.useChalk = false;\n else\n throw error;\n}\n\n/** \n * Factory function which constructs an error message relating to version mismatches\n * @param {string} oldThingLabel The name of the thing that is too old\n * @param {string} upgradeThingLabel [optional] The name of the thing that needs to be upgraded to fix that; if\n * unspecified, we use oldThingLabel.\n * @param {string} newThingLabel What the thing is that is that is newer than oldThingLabel\n * @param {string} minimumVersion The minimum version of the old thing that the new thing supports. \n * Obstensibibly semver, but unparsed.\n * @param {string} code [optional] A code property to add to the return value\n *\n * @returns instance of Error\n */\nexports.versionError = function dcpClient$$versionError(oldThingLabel, upgradeThingLabel, newThingLabel, minimumVersion, code)\n{\n function repeat(what, len) {\n let out = '';\n while (len--)\n out += what;\n return out;\n }\n\n function bold(string) {\n if (__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").platform === 'nodejs') {\n const chalk = new requireNative('chalk').constructor({enabled: exports.useChalk});\n\n return chalk.yellow(string);\n }\n\n return string;\n }\n \n var mainMessage = `${oldThingLabel} is too old; ${newThingLabel} needs version ${minimumVersion}`;\n var upgradeMessage = bold(`Please upgrade ${upgradeThingLabel || oldThingLabel}${repeat(\" \", mainMessage.length - 15 - upgradeThingLabel.length)}`);\n var error = new Error(`\n****${repeat('*', mainMessage.length)}****\n*** ${repeat(' ', mainMessage.length)} ***\n*** ${mainMessage} ***\n*** ${upgradeMessage} ***\n*** ${repeat(' ', mainMessage.length)} ***\n****${repeat('*', mainMessage.length)}****\n`);\n\n if (code)\n error.code = code;\n return error;\n}\n\n/*********************************************************************************************/\n\n//\n// Sandbox and Slice debugging and logging tools.\n//\n\n/**\n * Previous short form perf time stamp.\n * #Private\n * @type {Date}\n */\nlet previousTime = new Date();\n\n/**\n * Short form perf time format with timespan diff from last call.\n * @returns {string}\n */\nexports.shortTime = function util$$shortTime() {\n const currentTime = new Date();\n const diff = currentTime.getTime() - previousTime.getTime();\n previousTime = currentTime;\n return `diff=${diff}: ${currentTime.getMinutes()}.${currentTime.getSeconds()}.${currentTime.getMilliseconds()}`;\n}\n\n/**\n * Log message for duplicate slice.\n * @parameter {Slice} slice\n * @parameter {function} log\n */\nexports.duplicateSlice = function utils$$duplicateSlice(slice, log) {\n log ? log(slice) : console.log(`\\tDANGER: Found duplicate slice ${slice.sliceNumber}.${slice.jobAddress}.${slice.status}.`);\n}\n\n/**\n * Log message for duplicate sandbox.\n * @parameter {Sandbox} sandbox\n * @parameter {function} log\n */\nexports.duplicateSandbox = function utils$$duplicateSandbox(sandbox, log) {\n log ? log(sandbox) : console.log(`\\tDANGER: Found duplicate sandbox ${sandbox.id}.${sandbox.jobAddress}.${sandbox.state}.`);\n}\n\n/**\n * Feel free to customize this function to your needs.\n * Log theArray.\n * @parameter {Array<*>} theArray\n * @parameter {string} header\n */\nexports.dumpArray = function utils$$dumpArray(theArray, header, log, headerLog) {\n if (header) headerLog ? headerLog(`\\n${header}`) : console.log(`\\n${header}`);\n theArray.forEach(x => log ? log(x) : console.log(`\\t${JSON.stringify(x)}.`));\n}\n\n/**\n * Log sliceArray.\n * @parameter {Slice[]} sliceArray\n * @parameter {string} header\n */\nexports.dumpSlices = function utils$$dumpSlices(sliceArray, header) {\n if (header) console.log(`\\n${header}`);\n sliceArray.forEach(x => console.log(`\\t${x.sliceNumber}.${x.jobAddress}.${x.status}`));\n}\n\n/**\n * Log sandboxArray.\n * @parameter {Sandbox[]} sandboxArray\n * @parameter {string} header\n */\nexports.dumpSandboxes = function utils$$dumpSandboxes(sandboxArray, header) {\n if (header) console.log(`\\n${header}`);\n sandboxArray.forEach(x => console.log(`\\t${x.id}.${x.jobAddress}.${x.state}`));\n}\n\n/**\n * Feel free to customize this function to your needs.\n * If the elements of theArray are not unique, log the duplicates and log the full array.\n * @parameter {Array<*>} theArray\n * @parameter {string} [header]\n * @parameter {function} [log]\n */\nexports.dumpIfNotUnique = function utils$$dumpIfNotUnique(theArray, header, log, headerLog) {\n if (!exports.isUnique(theArray, header, log, headerLog))\n exports.dumpArray(theArray, undefined, log);\n}\n\n/**\n * If the elements of sliceArray are not unique, log the duplicates and log the full array.\n * @parameter {Slice[]} sliceArray\n * @parameter {string} header\n */\nexports.dumpSlicesIfNotUnique = function utils$$dumpSlicesIfNotUnique(sliceArray, header) {\n if (!exports.isUniqueSlices(sliceArray, header))\n exports.dumpSlices(sliceArray);\n}\n\n/**\n * If the elements of sandboxArray are not unique, log the duplicates and log the full array.\n * @parameter {Sandbox[]} sandboxArray\n * @parameter {string} header\n */\nexports.dumpSandboxesIfNotUnique = function utils$$dumpSandboxesIfNotUnique(sandboxArray, header) {\n if (!exports.isUniqueSandboxes(sandboxArray, header))\n exports.dumpSandboxes(sandboxArray);\n}\n\n/**\n * Feel free to customize this function to your needs.\n * Checks whether the elements of theArray are unique and if not, log the duplicates.\n * @parameter {Array<*>} theArray\n * @parameter {function} log\n * @returns {boolean}\n */\nexports.isUnique = function utils$$isUnique(theArray, header, log, headerLog) {\n return theArray.length === exports.makeUnique(theArray,\n header,\n x => { log ? log(x) : console.log(`\\tisUnique: Found duplicate ${JSON.stringify(x)}.`); },\n headerLog);\n}\n\n/**\n * Checks whether the elements of sliceArray are unique and if not, log the duplicates.\n * @parameter {Slice[]} sliceArray\n * @parameter {string} header\n * @parameter {function} log\n * @returns {boolean}\n */\nexports.isUniqueSlices = function utils$$isUniqueSlices(sliceArray, header, log) {\n return sliceArray.length === exports.makeUniqueSlices(sliceArray, header, log).length;\n}\n\n/**\n * Checks whether the elements of sandboxArray are unique and if not, log the duplicates.\n * @parameter {Sandbox[]} sandboxArray\n * @parameter {string} header\n * @parameter {function} log\n * @returns {boolean}\n */\nexports.isUniqueSandboxes = function utils$$isUniqueSandboxes(sandboxArray, header, log) {\n return sandboxArray.length === exports.makeUniqueSandboxes(sandboxArray, header, log).length;\n}\n\n/**\n * Feel free to customize this function to your needs.\n * Returns a copy of theArray with all duplicates removed, while logging the duplicates.\n * @parameter {Array<*>} theArray\n * @parameter {function} log\n * @returns {Array<*>}\n */\nexports.makeUnique = function utils$$makeUnique(theArray, header, log, headerLog) {\n const result = [];\n let once = true;\n theArray.forEach(x => {\n if (result.indexOf(x) >= 0) {\n if (once && header) headerLog ? headerLog(`\\n${header}`) : console.log(`\\n${header}`); once = false;\n log ? log(x) : console.log(`\\tmakeUnique: Found duplicate ${JSON.stringify(x)}.`);\n } else result.push(x);\n });\n return result;\n}\n\n/**\n * Returns a copy of sliceArray with all duplicates removed, while logging the duplicates.\n * @parameter {Slice[]} sliceArray\n * @parameter {string} header\n * @parameter {function} log\n * @returns {Slice[]}\n */\nexports.makeUniqueSlices = function utils$$makeUniqueSlices(sliceArray, header, log) {\n const slices = [];\n let once = true;\n sliceArray.forEach(x => {\n if (slices.indexOf(x) >= 0) {\n if (once && header) console.log(`\\n${header}`); once = false;\n exports.duplicateSlice(x, log);\n } else slices.push(x);\n });\n return slices;\n}\n\n/**\n * Returns a copy of sandboxArray with all duplicates removed, while logging the duplicates.\n * @parameter {Sandbox[]} sandboxArray\n * @parameter {string} header\n * @parameter {function} log\n * @returns {Sandbox[]}\n */\nexports.makeUniqueSandboxes = function utils$$makeUniqueSandboxes(sandboxArray, header, log) {\n const sandboxes = [];\n let once = true;\n sandboxArray.forEach(x => {\n if (sandboxes.indexOf(x) >= 0) {\n if (once && header) console.log(`\\n${header}`); once = false;\n exports.duplicateSandbox(x, log);\n } else sandboxes.push(x);\n });\n return sandboxes;\n}\n\n/*********************************************************************************************/\n\nexports.shuffle = function utils$$shuffle(array) {\n var currentIndex = array.length, randomIndex;\n\n // While there remain elements to shuffle...\n while (0 !== currentIndex) {\n\n // Pick a remaining element...\n randomIndex = Math.floor(Math.random() * currentIndex);\n currentIndex--;\n\n // And swap it with the current element.\n [array[currentIndex], array[randomIndex]] = [\n array[randomIndex], array[currentIndex]];\n }\n\n return array;\n}\n\n/*********************************************************************************************/\n\nif (__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").platform === 'nodejs') {\n // Assigning the properties explicitly for VSCode intellisense.\n const tmpFiles = __webpack_require__(/*! ./tmpfiles */ \"./src/utils/tmpfiles.js\");\n exports.catFile = tmpFiles.catFile;\n exports.createTempFile = tmpFiles.createTempFile;\n exports.systemTempDir = tmpFiles.systemTempDir;\n\n Object.assign(exports, __webpack_require__(/*! ./readln */ \"./src/utils/readln.js\"));\n}\nObject.assign(exports, __webpack_require__(/*! ./sh */ \"./src/utils/sh.js\"));\nObject.assign(exports, __webpack_require__(/*! ./eventUtils */ \"./src/utils/eventUtils.js\"));\nObject.assign(exports, __webpack_require__(/*! ./obj-merge */ \"./src/utils/obj-merge.js\"));\nObject.assign(exports, __webpack_require__(/*! ./make-slice-uri */ \"./src/utils/make-slice-uri.js\"));\nObject.assign(exports, __webpack_require__(/*! ./just-fetch */ \"./src/utils/just-fetch.js\"));\nObject.assign(exports, __webpack_require__(/*! ./inventory */ \"./src/utils/inventory.js\"));\nObject.assign(exports, __webpack_require__(/*! ./fetch-keystore */ \"./src/utils/fetch-keystore.js\"));\n\nmodule.exports.fetchURI = __webpack_require__(/*! ./fetch-uri */ \"./src/utils/fetch-uri.js\").fetchURI;\nmodule.exports.encodeDataURI = __webpack_require__(/*! ./encodeDataURI */ \"./src/utils/encodeDataURI.js\").encodeDataURI;\n\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../node_modules/process/browser.js */ \"./node_modules/process/browser.js\")))\n\n//# sourceURL=webpack:///./src/utils/index.js?");
|
|
5774
|
+
eval("/* WEBPACK VAR INJECTION */(function(process) {/**\n * @file src/utils/index.js\n * @author Ryan Rossiter\n * @date Feb 2020\n *\n * Place to put little JS utilities. If they are more than a few lines, please `require` and `export` \n * them instead of making this monolithic.\n */\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n\n module.exports.paramUtils = __webpack_require__(/*! ./assert-params */ \"./src/utils/assert-params.js\");\n module.exports.httpUtils = __webpack_require__(/*! ./http */ \"./src/utils/http.js\");\n module.exports.serialize = __webpack_require__(/*! ./serialize */ \"./src/utils/serialize.js\");\n Object.assign(exports, __webpack_require__(/*! ./web-format-date */ \"./src/utils/web-format-date.js\"));\n Object.assign(exports, __webpack_require__(/*! ./confirm-prompt */ \"./src/utils/confirm-prompt.js\"));\n Object.assign(exports, __webpack_require__(/*! ./message-to-buffer */ \"./src/utils/message-to-buffer.js\"));\n\n/** @typedef {import('dcp/dcp-client/worker/slice').Slice} Slice */\n/** @typedef {import('dcp/dcp-client/worker/sandbox').Sandbox} Sandbox */\n\n /**\n * Writes object properties into another object matching shape.\n * For example: if obj = { a: { b: 1, c: 2}}\n * and source = { a: {c: 0, d: 1}}\n * then obj will be updated to { a: { b: 1, c: 0, d: 1 } }\n * compare this to Object.assign which gives { a: { c: 0, d: 1 } }\n */\nconst setObjProps = module.exports.setObjProps = (obj, source) => {\n for (let p in source) {\n if (typeof source[p] === 'object') setObjProps(obj[p], source[p]);\n else obj[p] = source[p];\n }\n}\n\n\n/**\n * Generates a new random opaqueId i.e. a 22-character base64 string.\n * Used for job and slice ids.\n */\nmodule.exports.generateOpaqueId = function () {\n const generate = __webpack_require__(/*! nanoid/generate */ \"./node_modules/nanoid/generate.js\");\n return generate('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+', 22);\n}\n\n/**\n * Accepts an object and a list of properties to apply to the object to retreive a final value.\n * E.g.\n * object = { a: { b: { c: 4 } } }\n * listOfProperties = [\"a\", \"b\", \"c\"]\n * return = 4\n */\nmodule.exports.getNestedFromListOfProperties = function (object, listOfProperties) {\n return listOfProperties.reduce((o, k) => {\n if (typeof o !== 'object') return undefined;\n return o[k];\n }, object);\n}\n\n/**\n * Accepts an object and a dot-separated property name to retrieve from the object.\n * E.g.\n * object = { a: { b: { c: 4 } } }\n * property = 'a.b.c'\n * return = 4\n *\n * @param {object} object \n * @param {string} property \n */\nmodule.exports.getNestedProperty = function (object, property) {\n if (typeof property !== 'string') return undefined;\n return module.exports.getNestedFromListOfProperties(object, property.split(\".\"));\n}\n\n/**\n * Accepts an object and a list of properties and a value and mutates the object\n * to have the specified value at the location denoted by the list of properties.\n * Similar to getNestedFromListOfProperties, but sets instead of gets.\n */\nmodule.exports.setNestedFromListOfProperties = function (object, listOfProperties, value) {\n if(!listOfProperties.length || listOfProperties.length < 1) {\n throw new Error(\"listOfProperties must be an array of length >= 1\");\n }\n const indexOfLastProp = listOfProperties.length - 1;\n const pathToParent = listOfProperties.slice(0, indexOfLastProp);\n const parent = module.exports.getNestedFromListOfProperties(object, pathToParent);\n if(!parent) {\n throw new Error(\"Could not find value at:\", pathToParent, \"in object:\", object);\n }\n const lastProperty = listOfProperties[indexOfLastProp];\n parent[lastProperty] = value;\n}\n\n/**\n * Block the event loop for a specified time\n * @milliseconds the number of milliseconds to wait (integer)\n */\nexports.msleep = function utils$$msleep(milliseconds) {\n try\n {\n let sab = new SharedArrayBuffer(4);\n let int32 = new Int32Array(sab);\n Atomics.wait(int32, 0, 0, milliseconds);\n }\n catch(error)\n {\n console.error('Cannot msleep;', error);\n }\n}\n\n/**\n * Block the event loop for a specified time\n * @seconds the number of seconds to wait (float)\n */\nexports.sleep = function utils$$sleep(seconds) {\n return exports.msleep(seconds * 1000);\n}\n\n/** Resolve a promise after a specified time */\nexports.asleepMs = function asleepMs(ms) {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\n/** Resolve a promise after a specified time */\nexports.asleep = function asleep(seconds) {\n return new Promise(resolve => setTimeout(resolve, seconds * 1000));\n}\n\n/** \n * Returns the number of millisecond in a time expression.\n * @param s {number} The number of seconds\n * @returns {number}\n */\n/**\n * @param s {string} A complex time expression using m, w, d, s, h. '10d 6h 1s' means 10 days, 6 hours, and 1 second.\n */\nexports.ms = function utils$$ms(s)\n{\n let ms = 0;\n \n if (typeof s === 'number')\n return s * 1000;\n\n assert(typeof s === 'string');\n \n for (let expr of s.match(/[0-9.]+[smhdw]/g))\n {\n let unit = expr.slice(-1);\n let value = +expr.slice(0, -1);\n\n switch(unit)\n {\n case 's': {\n ms += value * 1000;\n break;\n }\n case 'm': {\n ms += value * 1000 * 60;\n break;\n }\n case 'h': {\n ms += value * 1000 * 60 * 60;\n break;\n }\n case 'd': {\n ms = value * 1000 * 60 * 60 * 24;\n break;\n }\n case 'w': {\n ms = value * 1000 * 60 * 60 * 24 * 7;\n break;\n }\n default: {\n throw new Error(`invalid time unit ${unit}`);\n }\n }\n }\n\n return ms;\n}\n\n/**\n * Returns a percentage as a number.\n * @param n {number} this number is returned\n */\n/**\n * @param n {string} this string is converted to a number and returned; it is divided by 100 if it ends in %.\n */\nexports.pct = function utils$$pct(n)\n{\n if (typeof n === 'number')\n return n / 100;\n\n if (n.match(/%$/))\n return +n / 100;\n\n return +n;\n}\n\n/**\n * Coerce human-written or registry-provided values into Boolean in a sensisble way.\n */\nexports.booley = function utils$$booley(check)\n{\n switch (typeof check)\n {\n case 'undefined':\n return false;\n case 'string':\n return check && (check !== 'false');\n case 'boolean':\n return check;\n case 'number':\n case 'object':\n return Boolean(check);\n default:\n throw new Error(`can't coerce ${typeof check} to booley`);\n }\n}\n\ntry {\n exports.useChalk = requireNative ? requireNative('tty').isatty(0) || process.env.FORCE_COLOR : false;\n}\ncatch (error) {\n if (error.message.includes('no native require'))\n exports.useChalk = false;\n else\n throw error;\n}\n\n/** \n * Factory function which constructs an error message relating to version mismatches\n * @param {string} oldThingLabel The name of the thing that is too old\n * @param {string} upgradeThingLabel [optional] The name of the thing that needs to be upgraded to fix that; if\n * unspecified, we use oldThingLabel.\n * @param {string} newThingLabel What the thing is that is that is newer than oldThingLabel\n * @param {string} minimumVersion The minimum version of the old thing that the new thing supports. \n * Obstensibibly semver, but unparsed.\n * @param {string} code [optional] A code property to add to the return value\n *\n * @returns instance of Error\n */\nexports.versionError = function dcpClient$$versionError(oldThingLabel, upgradeThingLabel, newThingLabel, minimumVersion, code)\n{\n function repeat(what, len) {\n let out = '';\n while (len--)\n out += what;\n return out;\n }\n\n function bold(string) {\n if (__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").platform === 'nodejs') {\n const chalk = new requireNative('chalk').constructor({enabled: exports.useChalk});\n\n return chalk.yellow(string);\n }\n\n return string;\n }\n \n var mainMessage = `${oldThingLabel} is too old; ${newThingLabel} needs version ${minimumVersion}`;\n var upgradeMessage = bold(`Please upgrade ${upgradeThingLabel || oldThingLabel}${repeat(\" \", mainMessage.length - 15 - upgradeThingLabel.length)}`);\n var error = new Error(`\n****${repeat('*', mainMessage.length)}****\n*** ${repeat(' ', mainMessage.length)} ***\n*** ${mainMessage} ***\n*** ${upgradeMessage} ***\n*** ${repeat(' ', mainMessage.length)} ***\n****${repeat('*', mainMessage.length)}****\n`);\n\n if (code)\n error.code = code;\n return error;\n}\n\n/*********************************************************************************************/\n\n//\n// Sandbox and Slice debugging and logging tools.\n//\n\n/**\n * Previous short form perf time stamp.\n * #Private\n * @type {Date}\n */\nlet previousTime = new Date();\n\n/**\n * Short form perf time format with timespan diff from last call.\n * @returns {string}\n */\nexports.shortTime = function util$$shortTime() {\n const currentTime = new Date();\n const diff = currentTime.getTime() - previousTime.getTime();\n previousTime = currentTime;\n return `diff=${diff}: ${currentTime.getMinutes()}.${currentTime.getSeconds()}.${currentTime.getMilliseconds()}`;\n}\n\n/**\n * Log message for duplicate slice.\n * @parameter {Slice} slice\n * @parameter {function} log\n */\nexports.duplicateSlice = function utils$$duplicateSlice(slice, log) {\n log ? log(slice) : console.log(`\\tDANGER: Found duplicate slice ${slice.sliceNumber}.${slice.jobAddress}.${slice.status}.`);\n}\n\n/**\n * Log message for duplicate sandbox.\n * @parameter {Sandbox} sandbox\n * @parameter {function} log\n */\nexports.duplicateSandbox = function utils$$duplicateSandbox(sandbox, log) {\n log ? log(sandbox) : console.log(`\\tDANGER: Found duplicate sandbox ${sandbox.id}.${sandbox.jobAddress}.${sandbox.state}.`);\n}\n\n/**\n * Feel free to customize this function to your needs.\n * Log theArray.\n * @parameter {Array<*>} theArray\n * @parameter {string} header\n */\nexports.dumpArray = function utils$$dumpArray(theArray, header, log, headerLog) {\n if (header) headerLog ? headerLog(`\\n${header}`) : console.log(`\\n${header}`);\n theArray.forEach(x => log ? log(x) : console.log(`\\t${JSON.stringify(x)}.`));\n}\n\n/**\n * Log sliceArray.\n * @parameter {Slice[]} sliceArray\n * @parameter {string} header\n */\nexports.dumpSlices = function utils$$dumpSlices(sliceArray, header) {\n if (header) console.log(`\\n${header}`);\n sliceArray.forEach(x => console.log(`\\t${x.sliceNumber}.${x.jobAddress}.${x.status}`));\n}\n\n/**\n * Log sandboxArray.\n * @parameter {Sandbox[]} sandboxArray\n * @parameter {string} header\n */\nexports.dumpSandboxes = function utils$$dumpSandboxes(sandboxArray, header) {\n if (header) console.log(`\\n${header}`);\n sandboxArray.forEach(x => console.log(`\\t${x.id}.${x.jobAddress}.${x.state}`));\n}\n\n/**\n * Feel free to customize this function to your needs.\n * If the elements of theArray are not unique, log the duplicates and log the full array.\n * @parameter {Array<*>} theArray\n * @parameter {string} [header]\n * @parameter {function} [log]\n */\nexports.dumpIfNotUnique = function utils$$dumpIfNotUnique(theArray, header, log, headerLog) {\n if (!exports.isUnique(theArray, header, log, headerLog))\n exports.dumpArray(theArray, undefined, log);\n}\n\n/**\n * If the elements of sliceArray are not unique, log the duplicates and log the full array.\n * @parameter {Slice[]} sliceArray\n * @parameter {string} header\n */\nexports.dumpSlicesIfNotUnique = function utils$$dumpSlicesIfNotUnique(sliceArray, header) {\n if (!exports.isUniqueSlices(sliceArray, header))\n exports.dumpSlices(sliceArray);\n}\n\n/**\n * If the elements of sandboxArray are not unique, log the duplicates and log the full array.\n * @parameter {Sandbox[]} sandboxArray\n * @parameter {string} header\n */\nexports.dumpSandboxesIfNotUnique = function utils$$dumpSandboxesIfNotUnique(sandboxArray, header) {\n if (!exports.isUniqueSandboxes(sandboxArray, header))\n exports.dumpSandboxes(sandboxArray);\n}\n\n/**\n * Feel free to customize this function to your needs.\n * Checks whether the elements of theArray are unique and if not, log the duplicates.\n * @parameter {Array<*>} theArray\n * @parameter {function} log\n * @returns {boolean}\n */\nexports.isUnique = function utils$$isUnique(theArray, header, log, headerLog) {\n return theArray.length === exports.makeUnique(theArray,\n header,\n x => { log ? log(x) : console.log(`\\tisUnique: Found duplicate ${JSON.stringify(x)}.`); },\n headerLog);\n}\n\n/**\n * Checks whether the elements of sliceArray are unique and if not, log the duplicates.\n * @parameter {Slice[]} sliceArray\n * @parameter {string} header\n * @parameter {function} log\n * @returns {boolean}\n */\nexports.isUniqueSlices = function utils$$isUniqueSlices(sliceArray, header, log) {\n return sliceArray.length === exports.makeUniqueSlices(sliceArray, header, log).length;\n}\n\n/**\n * Checks whether the elements of sandboxArray are unique and if not, log the duplicates.\n * @parameter {Sandbox[]} sandboxArray\n * @parameter {string} header\n * @parameter {function} log\n * @returns {boolean}\n */\nexports.isUniqueSandboxes = function utils$$isUniqueSandboxes(sandboxArray, header, log) {\n return sandboxArray.length === exports.makeUniqueSandboxes(sandboxArray, header, log).length;\n}\n\n/**\n * Feel free to customize this function to your needs.\n * Returns a copy of theArray with all duplicates removed, while logging the duplicates.\n * @parameter {Array<*>} theArray\n * @parameter {function} log\n * @returns {Array<*>}\n */\nexports.makeUnique = function utils$$makeUnique(theArray, header, log, headerLog) {\n const result = [];\n let once = true;\n theArray.forEach(x => {\n if (result.indexOf(x) >= 0) {\n if (once && header) headerLog ? headerLog(`\\n${header}`) : console.log(`\\n${header}`); once = false;\n log ? log(x) : console.log(`\\tmakeUnique: Found duplicate ${JSON.stringify(x)}.`);\n } else result.push(x);\n });\n return result;\n}\n\n/**\n * Returns a copy of sliceArray with all duplicates removed, while logging the duplicates.\n * @parameter {Slice[]} sliceArray\n * @parameter {string} header\n * @parameter {function} log\n * @returns {Slice[]}\n */\nexports.makeUniqueSlices = function utils$$makeUniqueSlices(sliceArray, header, log) {\n const slices = [];\n let once = true;\n sliceArray.forEach(x => {\n if (slices.indexOf(x) >= 0) {\n if (once && header) console.log(`\\n${header}`); once = false;\n exports.duplicateSlice(x, log);\n } else slices.push(x);\n });\n return slices;\n}\n\n/**\n * Returns a copy of sandboxArray with all duplicates removed, while logging the duplicates.\n * @parameter {Sandbox[]} sandboxArray\n * @parameter {string} header\n * @parameter {function} log\n * @returns {Sandbox[]}\n */\nexports.makeUniqueSandboxes = function utils$$makeUniqueSandboxes(sandboxArray, header, log) {\n const sandboxes = [];\n let once = true;\n sandboxArray.forEach(x => {\n if (sandboxes.indexOf(x) >= 0) {\n if (once && header) console.log(`\\n${header}`); once = false;\n exports.duplicateSandbox(x, log);\n } else sandboxes.push(x);\n });\n return sandboxes;\n}\n\n/**\n * Calls truncated JSON.stringify on theAnything.\n * @param {object} theAnything\n * @param {string} header\n * @param {number} valueLength\n */\nexports.dumpJSON = function utils$$dumpJSON(theAnything, header = 'dumpJSON: ', valueLength = 128) {\n if (theAnything) {\n const strV = String(JSON.stringify(theAnything)).slice(0, valueLength);\n console.log(`${header}, ${strV}`);\n } else {\n console.log(`${header}, undefined or null`);\n }\n}\n\n/**\n * Iterates over all property [key, value]-pairs of theObject and call truncated JSON.stringify on property values.\n * @param {object} theObject\n * @param {string} header\n * @param {number} valueLength\n */\nexports.dumpObject = function utils$$dumpObject(theObject , header = 'dumpObject: ', valueLength = 128) {\n for (const [key, value] of Object.entries(theObject)) {\n if (value) {\n const strV = String(JSON.stringify(value)).slice(0, valueLength);\n console.log(`${header} property ${key}, value ${strV}`);\n } else {\n console.log(`${header} property ${key}, value undefined or null`);\n }\n }\n}\n\n/*********************************************************************************************/\n\nexports.shuffle = function utils$$shuffle(array) {\n var currentIndex = array.length, randomIndex;\n\n // While there remain elements to shuffle...\n while (0 !== currentIndex) {\n\n // Pick a remaining element...\n randomIndex = Math.floor(Math.random() * currentIndex);\n currentIndex--;\n\n // And swap it with the current element.\n [array[currentIndex], array[randomIndex]] = [\n array[randomIndex], array[currentIndex]];\n }\n\n return array;\n}\n\n/*********************************************************************************************/\n\nif (__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").platform === 'nodejs') {\n // Assigning the properties explicitly for VSCode intellisense.\n const tmpFiles = __webpack_require__(/*! ./tmpfiles */ \"./src/utils/tmpfiles.js\");\n exports.catFile = tmpFiles.catFile;\n exports.createTempFile = tmpFiles.createTempFile;\n exports.systemTempDir = tmpFiles.systemTempDir;\n\n Object.assign(exports, __webpack_require__(/*! ./readln */ \"./src/utils/readln.js\"));\n}\nObject.assign(exports, __webpack_require__(/*! ./sh */ \"./src/utils/sh.js\"));\nObject.assign(exports, __webpack_require__(/*! ./eventUtils */ \"./src/utils/eventUtils.js\"));\nObject.assign(exports, __webpack_require__(/*! ./obj-merge */ \"./src/utils/obj-merge.js\"));\nObject.assign(exports, __webpack_require__(/*! ./make-slice-uri */ \"./src/utils/make-slice-uri.js\"));\nObject.assign(exports, __webpack_require__(/*! ./just-fetch */ \"./src/utils/just-fetch.js\"));\nObject.assign(exports, __webpack_require__(/*! ./inventory */ \"./src/utils/inventory.js\"));\nObject.assign(exports, __webpack_require__(/*! ./fetch-keystore */ \"./src/utils/fetch-keystore.js\"));\n\nmodule.exports.fetchURI = __webpack_require__(/*! ./fetch-uri */ \"./src/utils/fetch-uri.js\").fetchURI;\nmodule.exports.encodeDataURI = __webpack_require__(/*! ./encodeDataURI */ \"./src/utils/encodeDataURI.js\").encodeDataURI;\n\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../node_modules/process/browser.js */ \"./node_modules/process/browser.js\")))\n\n//# sourceURL=webpack:///./src/utils/index.js?");
|
|
5775
5775
|
|
|
5776
5776
|
/***/ }),
|
|
5777
5777
|
|
|
@@ -5794,7 +5794,7 @@ eval("/**\n * @file src/utils/inventory.js\n * Inventory met
|
|
|
5794
5794
|
/*! no static exports found */
|
|
5795
5795
|
/***/ (function(module, exports, __webpack_require__) {
|
|
5796
5796
|
|
|
5797
|
-
eval("/* WEBPACK VAR INJECTION */(function(process) {/**\n * @file just-fetch.js\n * @author Ryan Rossiter <ryan@kingsds.network>\n * @date June 2020\n *\n * Cross-platform method for performing an HTTP request.\n */\n\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\n\n /**\n * Make a request, as a promise, resolves with a string or json object\n *\n * @param {string | object} url - path of file to be fetched; if object must be instanceof URL or DcpURL\n * @param {string} [type = \"string\"] - expected type of the response\n * @param {string} [method = \"GET\"] - type of request to make\n * @param {boolean} [bustCache = false] - whether to add cache poison to the end of the url\n * @param {any} [body] - what to send as the body of the message. Objects are turned into into \n * standard CGI key=value encoding when the method is 'POST'\n * @returns {Promise} - resolves with response (json if type=\"JSON\", string otherwise), rejects on fail\n */\nexports.justFetch = function justFetch(url, type = 'string', method = 'GET', bustCache = false, body = undefined) {\n return new Promise((resolve, reject) => {\n let deeperErrorStack = new Error().stack;\n deeperErrorStack = deeperErrorStack.substring(deeperErrorStack.indexOf('\\n') + 1);\n\n if (bustCache)\n {\n let bustParam = ((typeof document !== 'undefined' && document.head && document.head.getAttribute('version')) || Date.now()); /* cache buster */\n\n if (typeof url === 'object' && url instanceof DcpURL)\n url = url.href;\n if (typeof url === 'string')\n url = new URL(url);\n url.search += (url.search ? '&' : '') + encodeURI(bustParam);\n }\n\n if (typeof url === 'object' && url.href) {\n url = url.href;\n }\n\n const xhr = new XMLHttpRequest();\n xhr.onloadend = function Protocol$$justFetch$onloadend() {\n try {\n delete xhr.onloadend;\n\n if (xhr.status >= 200 && xhr.status < 300) {\n if (xhr.getResponseHeader('content-type') && type === 'string') {\n type = xhr.getResponseHeader('content-type').split(';')[0];\n }\n \n let data = xhr.responseText;\n if (type === 'JSON' || type === 'application/json') {\n
|
|
5797
|
+
eval("/* WEBPACK VAR INJECTION */(function(process) {/**\n * @file just-fetch.js\n * @author Ryan Rossiter <ryan@kingsds.network>\n * @date June 2020\n *\n * Cross-platform method for performing an HTTP request.\n */\n\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\n\n /**\n * Make a request, as a promise, resolves with a string or json object\n *\n * @param {string | object} url - path of file to be fetched; if object must be instanceof URL or DcpURL\n * @param {string} [type = \"string\"] - expected type of the response\n * @param {string} [method = \"GET\"] - type of request to make\n * @param {boolean} [bustCache = false] - whether to add cache poison to the end of the url\n * @param {any} [body] - what to send as the body of the message. Objects are turned into into \n * standard CGI key=value encoding when the method is 'POST'\n * @returns {Promise} - resolves with response (json if type=\"JSON\", string otherwise), rejects on fail\n */\nexports.justFetch = function justFetch(url, type = 'string', method = 'GET', bustCache = false, body = undefined) {\n return new Promise((resolve, reject) => {\n let deeperErrorStack = new Error().stack;\n deeperErrorStack = deeperErrorStack.substring(deeperErrorStack.indexOf('\\n') + 1);\n\n if (bustCache)\n {\n let bustParam = ((typeof document !== 'undefined' && document.head && document.head.getAttribute('version')) || Date.now()); /* cache buster */\n\n if (typeof url === 'object' && url instanceof DcpURL)\n url = url.href;\n if (typeof url === 'string')\n url = new URL(url);\n url.search += (url.search ? '&' : '') + encodeURI(bustParam);\n }\n\n if (typeof url === 'object' && url.href) {\n url = url.href;\n }\n\n const xhr = new XMLHttpRequest();\n xhr.onloadend = function Protocol$$justFetch$onloadend() {\n try {\n delete xhr.onloadend;\n\n if (xhr.status >= 200 && xhr.status < 300) {\n if (xhr.getResponseHeader('content-type') && type === 'string') {\n type = xhr.getResponseHeader('content-type').split(';')[0];\n }\n \n let data = xhr.responseText;\n if (type === 'JSON' || type === 'application/json') {\n data = JSON.parse(data);\n }\n\n if (type === 'application/x-kvin' ) {\n data = kvin.deserialize(data);\n }\n \n resolve(data);\n } else {\n function makeFetchError()\n {\n if (xhr.status)\n return new Error(`HTTP Error ${xhr.status} fetching ${url}`);\n\n /* NodeJS via xmlhttprequest-ssl leaves an Error duck */\n if (xhr.statusText && xhr.statusText.code && xhr.statusText.message) {\n let error = new Error(xhr.statusText.message);\n let stack = error.stack;\n\n Object.assign(error, xhr.statusText);\n if (xhr.statusText.stack)\n error.stack = xhr.statusText.stack.split('\\n').slice(1).join('\\n') + '---\\n' + stack;\n else\n error.stack = stack;\n return error;\n }\n\n return new Error(`Network error fetching ${url}`);\n }\n\n const error = makeFetchError();\n \n error.request = Object.assign({}, xhr); /* Shallow clone so that console.log will work in node */\n error.request.method = method;\n error.request.location = url;\n error.stack += '\\n' + deeperErrorStack;\n if (xhr.status)\n error.code = 'HTTP_' + xhr.status;\n\n throw error;\n }\n } catch (e) {\n reject(e);\n }\n };\n\n xhr.open(method, url);\n if (!body)\n xhr.send();\n else\n {\n if (typeof body === 'object' && method.toUpperCase() === 'POST')\n {\n let entries = Object.entries(body);\n body = entries.map((kvp) => `${encodeURIComponent(kvp[0])}=${encodeURIComponent(kvp[1])}`).join('&');\n }\n xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');\n xhr.send(body);\n }\n });\n }\n\n/** Reformat an error (rejection) message from utils::justFetch, so that debugging code \n * can include (for example) a text-rendered version of the remote 404 page.\n *\n * @param {object} error The rejection from justFetch()\n * @returns {string} An error message, formatted with ANSI color when the output\n * is a terminal, suitable for writing directly to stdout. If\n * the response included html content (eg a 404 page), it is \n * rendered to text in this string.\n */\nexports.justFetchPrettyError = function utils$$justFetchPrettyError(error) {\n const chalk = new requireNative('chalk').constructor({enabled: __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\").useChalk});\n var message, headers={};\n\n if (!error.request || !error.request.status)\n return error;\n\n error.request.getAllResponseHeaders().replace(/\\r/g,'').split('\\n').forEach(function(line) {\n var colon = line.indexOf(': ')\n headers[line.slice(0,colon)] = line.slice(colon+2)\n })\n\n message = `HTTP Status: ${error.request.status} for ${error.request.method} ${error.request.location}`\n\n switch(headers['content-type'].replace(/;.*$/, '')) {\n case 'text/plain':\n message += '\\n' + chalk.grey(error.request.responseText)\n break;\n case 'text/html': {\n let html = error.request.responseText;\n\n html = html.replace(/\\n<a/gi, ' <a'); /* html-to-text bug, affects google 301s /wg jun 2020 */\n message += chalk.grey(__webpack_require__(/*! html-to-text */ \"./node_modules/html-to-text/index.js\").fromString(html, {\n wordwrap: parseInt(process.env.COLUMNS, 10) || 80,\n hideLinkHrefIfSameAsText: true,\n format: {\n heading: function (elem, fn, options) {\n var h = fn(elem.children, options);\n return '\\n====\\n' + chalk.yellow(chalk.bold(h.toUpperCase())) + '\\n====\\n';\n }\n }\n }));\n break;\n }\n }\n\n return message;\n} \n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../node_modules/process/browser.js */ \"./node_modules/process/browser.js\")))\n\n//# sourceURL=webpack:///./src/utils/just-fetch.js?");
|
|
5798
5798
|
|
|
5799
5799
|
/***/ }),
|
|
5800
5800
|
|