dcp-client 4.1.16 → 4.1.17

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.
@@ -96,7 +96,13 @@
96
96
  /*! exports provided: dcp_root, generated, build, install, default */
97
97
  /***/ (function(module) {
98
98
 
99
+ <<<<<<< HEAD
99
100
  eval("module.exports = JSON.parse(\"{\\\"dcp_root\\\":\\\"/home/erose/git/release/dcp/dist\\\",\\\"generated\\\":\\\"Fri 06 Aug 2021 11:03:07 AM EDT by erose on lorge\\\",\\\"build\\\":\\\"release\\\",\\\"install\\\":{\\\"generated\\\":\\\"Fri 06 Aug 2021 11:03:07 AM EDT by erose on lorge\\\",\\\"prefix\\\":\\\"/home/erose/git/release/dcp/dist\\\",\\\"build\\\":\\\"release\\\",\\\"node\\\":\\\"/usr/bin/node\\\",\\\"defaultUser\\\":\\\"erose\\\",\\\"defaultGroup\\\":\\\"erose\\\"}}\");\n\n//# sourceURL=webpack:///./etc/local-config.json?");
101
+ ||||||| 412fbe6
102
+ eval("module.exports = JSON.parse(\"{\\\"dcp_root\\\":\\\"/var/dcp\\\",\\\"generated\\\":\\\"Wed 22 Sep 2021 02:44:38 PM EDT by erose on lorge\\\",\\\"build\\\":\\\"debug\\\",\\\"install\\\":{\\\"generated\\\":\\\"Wed 22 Sep 2021 02:44:38 PM EDT by erose on lorge\\\",\\\"prefix\\\":\\\"/var/dcp\\\",\\\"build\\\":\\\"debug\\\",\\\"node\\\":\\\"/usr/bin/node\\\",\\\"defaultUser\\\":\\\"erose\\\",\\\"defaultGroup\\\":\\\"erose\\\"}}\");\n\n//# sourceURL=webpack:///./etc/local-config.json?");
103
+ =======
104
+ eval("module.exports = JSON.parse(\"{\\\"dcp_root\\\":\\\"/var/dcp\\\",\\\"generated\\\":\\\"Wed 12 Jan 2022 09:28:46 AM EST by erose on lorge\\\",\\\"build\\\":\\\"debug\\\",\\\"install\\\":{\\\"generated\\\":\\\"Wed 12 Jan 2022 09:28:46 AM EST by erose on lorge\\\",\\\"prefix\\\":\\\"/var/dcp\\\",\\\"build\\\":\\\"debug\\\",\\\"node\\\":\\\"/usr/bin/node\\\",\\\"defaultUser\\\":\\\"erose\\\",\\\"defaultGroup\\\":\\\"erose\\\"}}\");\n\n//# sourceURL=webpack:///./etc/local-config.json?");
105
+ >>>>>>> origin/prod-20220111
100
106
 
101
107
  /***/ }),
102
108
 
@@ -1660,7 +1666,7 @@ eval("\n\nexports.randomBytes = exports.rng = exports.pseudoRandomBytes = export
1660
1666
  /*! exports provided: browser, node, native, webGpuNative, nodeTesting, testing, default */
1661
1667
  /***/ (function(module) {
1662
1668
 
1663
- eval("module.exports = JSON.parse(\"{\\\"browser\\\":[\\\"deny-node\\\",\\\"kvin/kvin.js\\\",\\\"script-load-wrapper\\\",\\\"primitive-timers\\\",\\\"wrap-event-listeners\\\",\\\"event-loop-virtualization\\\",\\\"access-lists\\\",\\\"bravojs-init\\\",\\\"bravojs/bravo.js\\\",\\\"bravojs-env\\\",\\\"calculate-capabilities\\\",\\\"bootstrap\\\"],\\\"node\\\":[\\\"kvin/kvin.js\\\",\\\"sa-ww-simulation\\\",\\\"script-load-wrapper\\\",\\\"primitive-timers\\\",\\\"wrap-event-listeners\\\",\\\"event-loop-virtualization\\\",\\\"access-lists\\\",\\\"bravojs-init\\\",\\\"bravojs/bravo.js\\\",\\\"bravojs-env\\\",\\\"calculate-capabilities\\\",\\\"bootstrap\\\"],\\\"native\\\":[\\\"deny-node\\\",\\\"kvin/kvin.js\\\",\\\"sa-ww-simulation\\\",\\\"script-load-wrapper\\\",\\\"wrap-event-listeners\\\",\\\"event-loop-virtualization\\\",\\\"access-lists\\\",\\\"bravojs-init\\\",\\\"bravojs/bravo.js\\\",\\\"bravojs-env\\\",\\\"calculate-capabilities\\\",\\\"bootstrap\\\"],\\\"webGpuNative\\\":[\\\"deny-node\\\",\\\"kvin/kvin.js\\\",\\\"sa-ww-simulation\\\",\\\"script-load-wrapper\\\",\\\"wrap-event-listeners\\\",\\\"event-loop-virtualization\\\",\\\"access-lists\\\",\\\"bravojs-init\\\",\\\"bravojs/bravo.js\\\",\\\"bravojs-env\\\",\\\"calculate-capabilities\\\",\\\"webgpu-worker-environment.js\\\",\\\"bootstrap\\\"],\\\"nodeTesting\\\":[\\\"kvin/kvin.js\\\",\\\"sa-ww-simulation\\\",\\\"script-load-wrapper\\\",\\\"primitive-timers\\\",\\\"wrap-event-listeners\\\",\\\"event-loop-virtualization\\\",\\\"access-lists\\\",\\\"bravojs-init\\\",\\\"bravojs/bravo.js\\\",\\\"bravojs-env\\\",\\\"calculate-capabilities\\\",\\\"bootstrap\\\",\\\"testing.js\\\"],\\\"testing\\\":[\\\"deny-node\\\",\\\"kvin/kvin.js\\\",\\\"sa-ww-simulation\\\",\\\"script-load-wrapper\\\",\\\"wrap-event-listeners\\\",\\\"event-loop-virtualization\\\",\\\"access-lists\\\",\\\"bravojs-init\\\",\\\"bravojs/bravo.js\\\",\\\"bravojs-env\\\",\\\"calculate-capabilities\\\",\\\"bootstrap\\\",\\\"testing.js\\\"]}\");\n\n//# sourceURL=webpack:///./node_modules/dcp-client/generated/sandbox-definitions.json?");
1669
+ eval("module.exports = JSON.parse(\"{\\\"browser\\\":[\\\"deny-node\\\",\\\"kvin/kvin.js\\\",\\\"script-load-wrapper\\\",\\\"wrap-event-listeners\\\",\\\"event-loop-virtualization\\\",\\\"access-lists\\\",\\\"bravojs-init\\\",\\\"bravojs/bravo.js\\\",\\\"bravojs-env\\\",\\\"calculate-capabilities\\\",\\\"bootstrap\\\"],\\\"node\\\":[\\\"kvin/kvin.js\\\",\\\"sa-ww-simulation\\\",\\\"script-load-wrapper\\\",\\\"wrap-event-listeners\\\",\\\"event-loop-virtualization\\\",\\\"access-lists\\\",\\\"bravojs-init\\\",\\\"bravojs/bravo.js\\\",\\\"bravojs-env\\\",\\\"calculate-capabilities\\\",\\\"bootstrap\\\"],\\\"native\\\":[\\\"deny-node\\\",\\\"kvin/kvin.js\\\",\\\"sa-ww-simulation\\\",\\\"script-load-wrapper\\\",\\\"native-event-loop\\\",\\\"wrap-event-listeners\\\",\\\"event-loop-virtualization\\\",\\\"access-lists\\\",\\\"bravojs-init\\\",\\\"bravojs/bravo.js\\\",\\\"bravojs-env\\\",\\\"calculate-capabilities\\\",\\\"bootstrap\\\"],\\\"webGpuNative\\\":[\\\"deny-node\\\",\\\"kvin/kvin.js\\\",\\\"sa-ww-simulation\\\",\\\"script-load-wrapper\\\",\\\"native-event-loop\\\",\\\"wrap-event-listeners\\\",\\\"event-loop-virtualization\\\",\\\"access-lists\\\",\\\"bravojs-init\\\",\\\"bravojs/bravo.js\\\",\\\"bravojs-env\\\",\\\"calculate-capabilities\\\",\\\"webgpu-worker-environment.js\\\",\\\"bootstrap\\\"],\\\"nodeTesting\\\":[\\\"kvin/kvin.js\\\",\\\"sa-ww-simulation\\\",\\\"script-load-wrapper\\\",\\\"wrap-event-listeners\\\",\\\"event-loop-virtualization\\\",\\\"access-lists\\\",\\\"bravojs-init\\\",\\\"bravojs/bravo.js\\\",\\\"bravojs-env\\\",\\\"calculate-capabilities\\\",\\\"bootstrap\\\",\\\"testing.js\\\"],\\\"testing\\\":[\\\"deny-node\\\",\\\"kvin/kvin.js\\\",\\\"sa-ww-simulation\\\",\\\"script-load-wrapper\\\",\\\"native-event-loop\\\",\\\"wrap-event-listeners\\\",\\\"event-loop-virtualization\\\",\\\"access-lists\\\",\\\"bravojs-init\\\",\\\"bravojs/bravo.js\\\",\\\"bravojs-env\\\",\\\"calculate-capabilities\\\",\\\"bootstrap\\\",\\\"testing.js\\\"]}\");\n\n//# sourceURL=webpack:///./node_modules/dcp-client/generated/sandbox-definitions.json?");
1664
1670
 
1665
1671
  /***/ }),
1666
1672
 
@@ -4745,7 +4751,13 @@ eval("\n\nvar alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr
4745
4751
  /***/ (function(module, __webpack_exports__, __webpack_require__) {
4746
4752
 
4747
4753
  "use strict";
4754
+ <<<<<<< HEAD
4748
4755
  eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return Modal; });\n/**\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\">&times;</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\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\":\"2a07ad83519f5b6750b47f5937cf446b64c218a8\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"4.1.15\",\"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#6d217f06ecec68b8910ec67d5d6bc5d23d513222\"},\"built\":\"Tue Dec 21 2021 15:04:00 GMT-0500 (Eastern Standard Time)\",\"config\":{\"generated\":\"Tue 21 Dec 2021 03:03:57 PM EST by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"4.46.0\",\"node\":\"v12.22.8\"} !== 'undefined' && typeof window.Modal === 'undefined') {\n window.Modal = Modal\n }\n}\n\n\n//# sourceURL=webpack:///./portal/www/js/modal.js?");
4756
+ ||||||| 412fbe6
4757
+ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return Modal; });\n/**\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\">&times;</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\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\":\"f2e5c6725c53bade3b332a35a26ae8beb2897dcd\",\"branch\":\"prod-20211216\",\"dcpClient\":{\"version\":\"4.1.15\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#prod-20211216\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#113c6cac8b2580c1fca75b5d6bd99ff1b1e986c5\"},\"built\":\"Mon Dec 20 2021 14:18:11 GMT-0500 (Eastern Standard Time)\",\"config\":{\"generated\":\"Mon 20 Dec 2021 02:18:11 PM EST by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"4.46.0\",\"node\":\"v12.22.8\"} !== 'undefined' && typeof window.Modal === 'undefined') {\n window.Modal = Modal\n }\n}\n\n\n//# sourceURL=webpack:///./portal/www/js/modal.js?");
4758
+ =======
4759
+ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return Modal; });\n/**\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\">&times;</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\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\":\"87a7eadd2dd11f5196813e252eb61438232b50da\",\"branch\":\"prod-20220111\",\"dcpClient\":{\"version\":\"4.1.17\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#prod-20220111\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#952b8321d5019c93003262d910c04d487c582418\"},\"built\":\"Thu Jan 13 2022 15:38:17 GMT-0500 (Eastern Standard Time)\",\"config\":{\"generated\":\"Thu 13 Jan 2022 03:38:16 PM EST by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"4.46.0\",\"node\":\"v12.22.9\"} !== 'undefined' && typeof window.Modal === 'undefined') {\n window.Modal = Modal\n }\n}\n\n\n//# sourceURL=webpack:///./portal/www/js/modal.js?");
4760
+ >>>>>>> origin/prod-20220111
4749
4761
 
4750
4762
  /***/ }),
4751
4763
 
@@ -5066,7 +5078,13 @@ eval("/**\n * @file password.js\n * Modal providing a way to
5066
5078
  /*! no static exports found */
5067
5079
  /***/ (function(module, exports, __webpack_require__) {
5068
5080
 
5081
+ <<<<<<< HEAD
5069
5082
  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=2a07ad83519f5b6750b47f5937cf446b64c218a8'; /* 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?");
5083
+ ||||||| 412fbe6
5084
+ 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=f2e5c6725c53bade3b332a35a26ae8beb2897dcd'; /* 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?");
5085
+ =======
5086
+ 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=87a7eadd2dd11f5196813e252eb61438232b50da'; /* 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?");
5087
+ >>>>>>> origin/prod-20220111
5070
5088
 
5071
5089
  /***/ }),
5072
5090
 
@@ -5088,7 +5106,7 @@ eval("/**\n * @file Client facing module that implements Compute Groups API\n
5088
5106
  /*! no static exports found */
5089
5107
  /***/ (function(module, exports, __webpack_require__) {
5090
5108
 
5091
- eval("/**\n * @file Module that implements Compute API\n * @module dcp/compute\n * @access public\n */\n\n/* global dcpConfig */\n\n/**\n * @access private - Will change soon!\n * @typedef {object} JobRequirements\n * @property {object} environment\n * @property {boolean} environment.fdlibm - Workers express capability 'fdlibm' in order for client applications to have confidence that results will be bitwise identical across workers. This library is recommended, but not required, for implementaiton of Google chrome, Mozilla Firefox and the DCP standalone worker use fdlibm but Microsoft Edge and Apple Safari do not use it.\n * @property {boolean} environment.offscreenCanvas When present, this capability indicates that the worker environment has the OffscreenCanvas symbol defined.\n * @property {object} details\n * @property {object} details.offscreenCanvas\n * @property {boolean} details.offscreenCanvas.bigTexture4096 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 4096.\n * @property {boolean} details.offscreenCanvas.bigTexture8192 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 8192.\n * @property {boolean} details.offscreenCanvas.bigTexture16384 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 16384.\n * @property {boolean} details.offscreenCanvas.bigTexture32768 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 32768.\n * @property {object} engine\n * @property {boolean} engine.es7 This capability enforces that the worker is running an es7-compliant JavaScript engine.\n * @property {boolean} engine.spidermonkey The capability enforces that the worker will run in the SpiderMonkey JS engine.\n * @property {boolean} browser.chrome The capability to exclude Chrome browsers.\n */\n\n/**\n * This JSDoc 'namespace' is a convenient way to document some globals that are available only in\n * the sandbox environment provided to work functions.\n * @access public\n * @namespace sandboxEnv\n */\n\n/**\n * This function emits a progress event. Progress events should be emitted approximately once per second; a task which fails to emit a \n * progress event for a certain period of time will be cancelled by the supervisor. The argument to this function is interpreted to six \n * significant digits, and must increase for every call. *All work functions must emit at least one progress event* - this requirement \n * will be enforced by the estimator. * The period of time mentioned above will be at least 30 wall-clock seconds and at least 30 \n * benchmark-adjusted seconds\n * @access public\n * @method progress\n * @param {string|number|undefined} n a number between 0 and 1 (inclusive) that represents a best-guess \n * at the completed portion of the task as a ratio of completed work to total work. If the argument is a string ending in the `%` symbol, \n * it will be interpreted as a number in the usual mathematical sense. If the argument is `undefined`, the slice progress is noted but \n * the amount of progress is considered to be indeterminate.\n * @memberof module:dcp/compute~sandboxEnv\n * @returns {boolean} true\n */\n\n/**\n * Emit a `console` event to the `JobHandle` in the client. If the sandbox is running in an environment with a native console object, the native method may also be invoked.\n * \n * @member console\n * @property {function} log fire console event with 'log' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} debug fire console event with 'debug' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} info fire console event with 'info' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} warn fire console event with 'warn' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} error fire console event with 'error' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * \n * **Note** some ES environments (Chrome, Firefox) implement C-style print formatting in this method. This is currently not supported.\n * @memberof module:dcp/compute~sandboxEnv\n * @emits Job#console\n * @access public\n */\n\n/**\n * @member work An object that contains the following properties\n * @property {function} emit (eventName, ...) - send arbitrary args with `eventName`. This event is received client-side with those args (see {@link Job#work}).\n * @property {object} job\n * @property {object} job.public Property set on job handle, see {@link Job#public}.\n * @memberof module:dcp/compute~sandboxEnv\n * @access public\n */\n\n/**\n * @member {OffscreenCanvas} OffscreenCanvas As defined in the {@link https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas|HTML Standard}, provides a canvas which can be rendered off-screen. If this interface is not available in a given worker, the worker will not report capability \"offscreenCanvas\".\n * @memberof module:dcp/compute~sandboxEnv\n * @access public\n */\n\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events/event-emitter */ \"./src/common/dcp-events/event-emitter.js\");\nconst { RangeObject, rehydrateRange } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst { RemoteDataSet } = __webpack_require__(/*! dcp/dcp-client/remote-data-set */ \"./src/dcp-client/remote-data-set.js\");\nconst { RemoteDataPattern } = __webpack_require__(/*! dcp/dcp-client/remote-data-pattern */ \"./src/dcp-client/remote-data-pattern.js\");\nconst { Job, ResultHandle } = __webpack_require__(/*! dcp/dcp-client/job */ \"./src/dcp-client/job/index.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { disableWorker } = __webpack_require__(/*! dcp/dcp-client/worker */ \"./src/dcp-client/worker/index.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');\n\nconst debug = (...args) => {\n if (debugging()) {\n args.unshift('dcp-client:compute');\n console.debug(...args);\n }\n};\n\nconst MARKET_VALUE = Symbol('Market Value');\nconst indirectEval = eval; // eslint-disable-line no-eval\n\n/**\n * Instance methods of this class are exported as static members of {@link module:dcp/compute|dcp/compute}.\n * @example\n * let job = compute.for(1, 3, function (i) {\n * progress('100%');\n * return i*10;\n * })\n * let results = await job.exec();\n */\nclass Compute extends EventEmitter {\n constructor () {\n super('Compute');\n\n /** @todo move these into some central config */\n this.defaultMaxGPUs = 1;\n this.GPUsAssigned = 0;\n this.allowSlicesToFinishBeforeStopping = false;\n\n this.schedulerURL = undefined; // Will default to dcpConfig.scheduler.location if not overidden\n this.bankURL = undefined; // Will default to dcpConfig.bank.services.bankTeller.location if not overidden\n\n Object.defineProperty(this.marketValue, MARKET_VALUE, {\n value: true,\n writable: false,\n configurable: false,\n enumerable: false,\n });\n\n const ceci = this;\n this.RemoteDataSet = RemoteDataSet;\n this.RemoteDataPattern = RemoteDataPattern;\n // Initialize to null so these properties are recognized for the Job class\n this.phemeConnection = null;\n this.deployConnection = null;\n this.openPhemeConn = function openPhemeConn()\n {\n ceci.phemeConnection = new protocolV4.Connection(dcpConfig.scheduler.services.pheme);\n ceci.phemeConnection.on('close', ceci.openPhemeConn);\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\n get defaultMaxWorkers () {\n return portalWorker.supervisor.maxWorkingSandboxes\n }\n\n set defaultMaxWorkers (value) {\n portalWorker.supervisor.maxWorkingSandboxes = value\n }\n\n get paymentAddress () {\n return portalWorker.supervisor.paymentAddress\n }\n\n set paymentAddress (value) {\n portalWorker.supervisor.paymentAddress = value\n }\n\n get mining () {\n return portalWorker.working\n }\n\n /** Return a special object that represents the current market value\n * @param {number} factor - OPTIONAL multiplier. default: 1.0\n */\n marketValue (factor = 1) {\n return {\n [MARKET_VALUE]: factor\n };\n }\n\n /** Describes the payment required to compute such a slice on a worker\n * or in a market working at the rates described in the WorkValue object.\n * This function does not take into account job-related overhead.\n * \n * @param {object} sliceProfile A SliceProfile-shaped object\n * @param {object} workValue A WorkValue-shaped object\n * @returns {number} Payment value in DCC describing rate to compute slice\n */\n calculateSlicePayment(sliceProfile, workValue) {\n return sliceProfile.cpuHours * workValue.cpuHour\n + sliceProfile.gpuHours * workValue.gpuHour\n + sliceProfile.inputMBytes * workValue.inputMByte\n + sliceProfile.outputMBytes * workValue.outputMByte;\n }\n\n \n /**\n * Deprecated `mine` API. Use `work` instead\n */\n async mine (numberOfCores, paymentAddress = this.paymentAddress) {\n return this.work(numberOfCores, paymentAddress);\n }\n\n /**\n * Instruct the Worker to stop working, then emit workerInactive\n */\n async stopWorking() {\n console.warn('Using the Compute API for controlling workers is deprecated. Use the Worker API instead.')\n await portalWorker.stop(!this.allowSlicesToFinishBeforeStopping)\n }\n\n /**\n * Deprecated `stopMining` API. Use `stopWorking` instead\n */\n stopMining () {\n console.log(\"Warning: used deprecated 'stopMining' API\");\n this.stopWorking();\n }\n \n /**\n * Flag worker as disabled\n */\n disableWorker() {\n disableWorker();\n }\n\n /**\n * Deprecated `disableMiner` API. Use `disableWorker` instead\n */\n disableMiner() {\n this.disableWorker();\n }\n\n /**\n * Experimental replacement for mine() /* wg aug 2019\n * @param {number} [numberOfCores = this.defaultMaxWorkers] - Number of parallel processes to run\n * @param {string} [paymentAddress = this.paymentAddress || wallet.get().address] - The address of the account to work for\n */\n async work(numCores, paymentAddress) {\n console.warn('Using the Compute API for controlling workers is deprecated. Use the Worker API instead.');\n\n portalWorker.supervisor.paymentAddress = paymentAddress || this.paymentAddress || (await wallet.get()).address;\n portalWorker.supervisor.maxWorkingSandboxes = numCores || this.defaultMaxWorkers || 1;\n await portalWorker.start();\n\n return portalWorker;\n }\n\n /**\n * Form 1. Create a Job for the distributed computer by specifying the start,\n * end, and step for a RangeObject with a given work function and arguments.\n *\n * @param {number} start First number in a range (see {@link RangeObject})\n * @param {number} end Last number in a range (see {@link RangeObject})\n * @param {number} [step=1] the space between numbers in a range (see\n * {@link RangeObject})\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * let job = compute.for(1, 3, function (i) {\n * progress('100%')\n * return i*10\n * })\n */\n /**\n * Form 2.a Create a Job for the distributed computer by specifying the input\n * as a RemoteDataSet with a given work function and arguments.\n *\n * @param {RemoteDataSet} remoteDataSet Array, ES6 function* generator, or any\n * other type of iterable object.\n * \n * Form 2.b Create a Job for the distributed computer by specifying the input\n * as a RemoteDataPattern with a given work function and arguments.\n * @param {RemoteDataPattern} RemoteDataPattern Array, ES6 function* generator, or any\n * other type of iterable object.\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * const remoteData = ['https://example.com/data'];\n * const job = compute.for(new RemoteDataSet(...remoteData), function(i) {\n * progress(1)\n * return i\n * })\n * \n * * @example\n * const pattern = 'https://example.com/data/{slice}.json';\n * const NumberOfSlices = 5;\n * const job = compute.for(new RemoteDataSet(pattern, NumberOfSlices), function(i) {\n * progress(1)\n * return i\n * })\n */\n /**\n * Form 3. Create a Job for the distributed computer by specifying the input\n * as an Iterable object with a given work function and arguments.\n *\n * @param {Iterable} iterableObject Array, ES6 function* generator, or any\n * other type of iterable object.\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * let job = compute.for([123,456], function(i) {\n * progress(1)\n * return i\n * })\n */\n /**\n * Form 4. Create a Job for the distributed computer by specifying the input\n * as a RangeObject with a given work function and arguments.\n *\n * @param {RangeObject} rangeObject Either a RangeObject or an object literal\n * which can be used to create a RangeObject (see {@link RangeObject}).\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * let job = compute.for({start: 10, end: 13, step: 2}, (i) => progress(1) && i)\n */\n /**\n * Form 5. Similar to Form 4, except with a multi range object containing an\n * array of range objects in the key ranges. These are used to create\n * multi-dimensional ranges, like nested loops. If they were written as\n * traditional loops, the outermost loop would be the leftmost range object,\n * and the innermost loop would be the rightmost range object.\n *\n * @param {MultiRangeObject} multiRange Either a MultiRangeObject or any valid\n * argument to the constructor.\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * let job = compute.for([{start: 1, end: 2}, {start: 3, end: 5}], function(i,j) {\n * return [i, j, i*j]\n * })\n */\n for(...args) {\n // args, or work function\n const lastArg = args[args.length - 1];\n // work function, if args provided\n const secondLastArg = args[args.length - 2];\n let range = null;\n let work;\n let genArgs;\n\n // All forms: extract work and optional args from the end and validate it.\n if (isValidWorkFunctionType(secondLastArg)) {\n // for(..., workFunction, arguments)\n work = secondLastArg\n if (lastArg && Array.from(lastArg).length > 0 || typeof lastArg === 'string') {\n // array or string, use as-is (string should be a URI)\n genArgs = lastArg;\n } else if (lastArg === undefined) {\n genArgs = [];\n } else {\n throw new TypeError(\n 'Work function arguments (the last parameter), should be an array.',\n );\n }\n } else if (isValidWorkFunctionType(lastArg)) {\n // for(..., workFunction)\n work = lastArg;\n genArgs = [];\n }\n\n if (!isWorkFunctionValid(work)) {\n throw new TypeError(\n `Work parameter must be a function or a string that evaluates to a function, or a URL that points to the location of a function.\\nReceived type ${typeof work} (${work})`,\n );\n }\n\n // Extract the proper information from every form.\n const [firstArg, secondArg, thirdArg] = args;\n if (['object', 'string'].includes(typeof firstArg)) {\n // Forms 2 & 3 (data is iterable, eg. an Array or RemoteDataSet)\n // Forms 4 & 5 (data is RangeObject & Friends)\n range = rehydrateRange(firstArg);\n } else if (typeof firstArg === 'number' && typeof secondArg === 'number') {\n // Form 1 (data is start, end[, step])\n const start = firstArg;\n const end = secondArg;\n\n /**\n * If there is an argument between the end and the work fn, use it as step\n * otherwise set step to undefined.\n */\n const step = typeof thirdArg === 'number' ? thirdArg : undefined;\n range = new RangeObject(start, end, step);\n } else {\n throw new TypeError(\n 'Parameters do not match one of the accepted call forms.',\n );\n }\n\n if (range.length === 0) {\n throw new Error('Length of input set must be greater than zero.');\n }\n\n debug('Work:', work);\n debug('Input Set:', range);\n debug('Arguments:', genArgs);\n return new Job(work, range, genArgs);\n\n /**\n * @param {any} workFunction\n */\n function isValidWorkFunctionType(workFunction) {\n return (\n typeof workFunction === 'function' ||\n typeof workFunction === 'string' ||\n DcpURL.isURL(workFunction)\n );\n }\n\n /**\n * Checks if the workFunction can be evaluated to a function or if it's\n * protocol is HTTPS,\n * @param {function | string | URL | DcpURL} workFunction\n */\n function isWorkFunctionValid(workFunction) {\n switch (typeof workFunction) {\n case 'function':\n return true;\n case 'string': {\n return isValidFunction(workFunction);\n }\n case 'object':\n return DcpURL.isURL(workFunction);\n default:\n return false;\n }\n }\n\n /**\n * Evaluates to true if the string can be coerced into a javascript\n * function.\n * @param {string} workFunction\n */\n function isValidFunction(workFunction) {\n try {\n const fn = indirectEval(`(${workFunction})`);\n return typeof fn === 'function';\n } catch (e) {\n return false;\n }\n }\n }\n\n /**\n * Form 1.\n * Create a job which will run a work function once\n *\n * @param {function | string | URL | DcpURL} work Work function as string or function literal.\n * @param {object} [workFunctionArgs=[]] optional Array-like object which contains arguments which are passed to the work func\n * @returns {Job}\n * @access public\n * @example\n * const job = compute.do(function (i) {\n * progress('100%')\n * return i*10\n * })\n */\n /**\n * Form 2.\n * Create a job which will run a work function on the values 1..n\n *\n * @param {number} n Number of times to run the work function.\n * @param {function | string | URL | DcpURL} work Work function as string or function literal.\n * @param {object} [workFunctionArgs=[]] optional Array-like object which contains arguments which are passed to the work func\n * @returns {Job}\n * @access public\n * @example\n * const job = compute.do(5, function (i) {\n * progress('100%')\n * return i*10\n * })\n */\n do(...args) {\n let n;\n let work;\n let workFunctionArgs;\n if (typeof args[0] !== 'number') {\n // Form 1.\n n = 1;\n [work, workFunctionArgs] = args;\n } else {\n // Form 2.\n [n, work, workFunctionArgs] = args;\n }\n return this.for(0, n - 1, work, workFunctionArgs);\n }\n\n /** Cancel a running job, by opaque id\n *\n * @param {string} id Address of the job to cancel.\n * @param {wallet.Keystore} ownerKeystore The keystore of the job owner\n * @param {string} reason If provided, will be sent on to client\n * @returns {object} The status of the funds that were released from escrow.\n * @throws when id or ownerKeystore are not provided.\n * @access public\n */\n async cancel (id, ownerKeystore, reason = '') {\n if (typeof id !== 'string') {\n throw new Error(`compute.cancel: Job ID must be a string, not ${typeof id}.`);\n }\n\n if (!(ownerKeystore instanceof wallet.Keystore)) {\n throw new Error('compute.cancel: ownerKeystore must be instance of wallet.Keystore');\n }\n\n if (!this.deployConnection)\n this.openDeployConn();\n\n const response = await this.deployConnection.send('cancelJob', {\n job: id,\n ownerAddress: ownerKeystore.address,\n reason,\n }, ownerKeystore);\n\n return response.payload;\n }\n\n /**\n * Reconnect/resume a job, by opaque id\n *\n * @param {string} id Opaque id of the Job to resume/reconnect.\n * @param {wallet.Keystore} [ownerKeystore=wallet.get()] The keystore of the job owner\n * @returns {Job}\n * @throws when id is not provided.\n * @access public\n */\n async resume (id, ownerKeystore) {\n if (typeof id !== 'string') {\n throw new Error(`compute.resume: Job ID must be a string, not ${typeof id}.`);\n }\n\n if (!(ownerKeystore instanceof wallet.Keystore)) {\n ownerKeystore = await wallet.get();\n }\n\n const response = await this.schedulerConnection.send('fetchJob', {\n job: id, // XXX@TODO change 'address' to 'job' once we port this operation to v4\n owner: ownerKeystore.address,\n }, ownerKeystore);\n\n const job = new Job(response.payload);\n job.setPaymentAccountKeystore(ownerKeystore);\n return job;\n }\n\n /**\n * Form 1. Query the status of a job that has been deployed to the scheduler.\n *\n * @access public\n * @param {string|Job} job A job ID or a Job instance\n * @param {wallet.Keystore} ownerKeystore The identity keystore of the owner\n * of the job\n * @throws when ownerKeystore is not provided\n * @returns {Promise<object>} A status object describing a given job.\n */\n /**\n * Form 2. Query the status of jobs deployed to the scheduler.\n *\n * @access public\n * @param {object} [opts] Query options\n * @param {number} [opts.startTime] Jobs older than this will be excluded from\n * the results.\n * @param {number} [opts.endTime] Jobs newer than this will be excluded from\n * the results.\n * @param {wallet.Keystore} ownerKeystore The identity keystore of the owner\n * of the jobs to query\n * @throws when ownerKeystore is not provided\n * @returns {Promise<Array<Object>>} An array of status objects corresponding to the status of\n * jobs deployed by the given payment account.\n */\n async status(...args) {\n if (args.length !== 2) {\n throw new Error('Only 2 arguments must be passed to compute.status');\n }\n\n const ownerKeystore = args.pop();\n if (!(ownerKeystore instanceof wallet.Keystore)) {\n throw new TypeError(\n 'compute.status: ownerKeystore must be an instance of Keystore',\n );\n }\n\n const requestPayload = {\n idKeystoreOwner: ownerKeystore.address,\n };\n\n const firstArg = args.shift();\n if (typeof firstArg === 'string' || firstArg instanceof Job) {\n // Form 1\n requestPayload.jobId = firstArg instanceof Job ? firstArg.id : firstArg;\n } else if (typeof firstArg === 'object') {\n // Form 2\n if (\n (typeof firstArg.startTime !== 'undefined' &&\n !(firstArg.startTime instanceof Date)) ||\n (typeof firstArg.endTime !== 'undefined' &&\n !(firstArg.endTime instanceof Date))\n ) {\n throw new TypeError(\n 'startTime and or endTime were not specified as instances of Date',\n );\n }\n\n requestPayload.startDeployTime = firstArg.startTime;\n requestPayload.endDeployTime = firstArg.endTime;\n } else {\n throw new TypeError(`Unknown first argument type ${typeof firstArg}`);\n }\n\n if (!this.deployConnection)\n this.openDeployConn();\n\n const {\n success,\n payload: responsePayload,\n } = await this.deployConnection.send(\n 'listJobs',\n requestPayload,\n ownerKeystore,\n );\n\n if (!success) {\n throw new DCPError('Request for job status failed', responsePayload);\n }\n\n if (typeof firstArg === 'string' || firstArg instanceof Job) {\n return responsePayload[0];\n }\n\n return responsePayload;\n }\n\n /**\n * Returns information for the job with the provided ID. It will use\n * `wallet.getId()` to retrieve the auth keystore.\n *\n * @param {string} jobAddress\n * @returns {Promise<object>} status object describing a given job\n * @access public\n */\n async getJobInfo(jobAddress) {\n if (!this.phemeConnection)\n this.openPhemeConn();\n\n const ks = await wallet.getId();\n const { payload: jobResponsePayload } = await this.phemeConnection.send('fetchJobReport', \n {\n jobOwner: ks.address,\n job: jobAddress\n });\n\n const { payload: sliceResponsePayload } = await this.phemeConnection.send(\n 'fetchSliceReport', \n {\n jobOwner: ks.address,\n job: jobAddress\n },\n );\n\n const jobReport = {};\n Object.assign(jobReport, jobResponsePayload, sliceResponsePayload);\n\n return jobReport;\n }\n\n /**\n * Returns information for the slices of the job with the provided ID. It will\n * use `wallet.getId()` to retrieve the auth keystore.\n *\n * @param {string} jobAddress\n * @returns {Promise<object>} A slice status object for the given job.\n * @access public\n */\n async getSliceInfo(jobAddress) {\n if (!this.phemeConnection)\n this.openPhemeConn();\n\n const identityKeystore = await wallet.getId();\n const {\n success,\n payload: responsePayload,\n } = await this.phemeConnection.send(\n 'fetchSliceReport',\n {\n job: jobAddress,\n jobOwner: identityKeystore.address,\n },\n identityKeystore,\n );\n\n if (!success) {\n throw new DCPError('Request for slice info failed', responsePayload);\n }\n\n return responsePayload;\n }\n}\n\nexports.RemoteDataSet = RemoteDataSet;\nexports.RemoteDataPattern = RemoteDataPattern;\nexports.MARKET_VALUE = MARKET_VALUE;\nexports.compute = new Compute(); /* :( */\nexports.compute.getPublicComputeGroupId = () => __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups.public.id;\nexports.compute.getPublicComputeGroupDescriptor = () => __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups.public;\nexports.compute.ResultHandle = ResultHandle; // export as compute.ResultHandle for instanceof checking\nexports.version = {\n api: '1.5.2',\n provides: '1.0.0' /* dcpConfig.scheduler.compatibility.operations.compute */\n};\n\n\n//# sourceURL=webpack:///./src/dcp-client/compute.js?");
5109
+ eval("/**\n * @file Module that implements Compute API\n * @module dcp/compute\n * @access public\n */\n\n/* global dcpConfig */\n\n/**\n * @access private - Will change soon!\n * @typedef {object} JobRequirements\n * @property {object} environment\n * @property {boolean} environment.fdlibm - Workers express capability 'fdlibm' in order for client applications to have confidence that results will be bitwise identical across workers. This library is recommended, but not required, for implementaiton of Google chrome, Mozilla Firefox and the DCP standalone worker use fdlibm but Microsoft Edge and Apple Safari do not use it.\n * @property {boolean} environment.offscreenCanvas When present, this capability indicates that the worker environment has the OffscreenCanvas symbol defined.\n * @property {object} details\n * @property {object} details.offscreenCanvas\n * @property {boolean} details.offscreenCanvas.bigTexture4096 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 4096.\n * @property {boolean} details.offscreenCanvas.bigTexture8192 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 8192.\n * @property {boolean} details.offscreenCanvas.bigTexture16384 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 16384.\n * @property {boolean} details.offscreenCanvas.bigTexture32768 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 32768.\n * @property {object} engine\n * @property {boolean} engine.es7 This capability enforces that the worker is running an es7-compliant JavaScript engine.\n * @property {boolean} engine.spidermonkey The capability enforces that the worker will run in the SpiderMonkey JS engine.\n * @property {boolean} browser.chrome The capability to exclude Chrome browsers.\n */\n\n/**\n * This JSDoc 'namespace' is a convenient way to document some globals that are available only in\n * the sandbox environment provided to work functions.\n * @access public\n * @namespace sandboxEnv\n */\n\n/**\n * This function emits a progress event. Progress events should be emitted approximately once per second; a task which fails to emit a \n * progress event for a certain period of time will be cancelled by the supervisor. The argument to this function is interpreted to six \n * significant digits, and must increase for every call. *All work functions must emit at least one progress event* - this requirement \n * will be enforced by the estimator. * The period of time mentioned above will be at least 30 wall-clock seconds and at least 30 \n * benchmark-adjusted seconds\n * @access public\n * @method progress\n * @param {string|number|undefined} n a number between 0 and 1 (inclusive) that represents a best-guess \n * at the completed portion of the task as a ratio of completed work to total work. If the argument is a string ending in the `%` symbol, \n * it will be interpreted as a number in the usual mathematical sense. If the argument is `undefined`, the slice progress is noted but \n * the amount of progress is considered to be indeterminate.\n * @memberof module:dcp/compute~sandboxEnv\n * @returns {boolean} true\n */\n\n/**\n * Emit a `console` event to the `JobHandle` in the client. If the sandbox is running in an environment with a native console object, the native method may also be invoked.\n * \n * @member console\n * @property {function} log fire console event with 'log' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} debug fire console event with 'debug' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} info fire console event with 'info' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} warn fire console event with 'warn' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} error fire console event with 'error' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * \n * **Note** some ES environments (Chrome, Firefox) implement C-style print formatting in this method. This is currently not supported.\n * @memberof module:dcp/compute~sandboxEnv\n * @emits Job#console\n * @access public\n */\n\n/**\n * @member work An object that contains the following properties\n * @property {function} emit (eventName, ...) - send arbitrary args with `eventName`. This event is received client-side with those args (see {@link Job#work}).\n * @property {object} job\n * @property {object} job.public Property set on job handle, see {@link Job#public}.\n * @memberof module:dcp/compute~sandboxEnv\n * @access public\n */\n\n/**\n * @member {OffscreenCanvas} OffscreenCanvas As defined in the {@link https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas|HTML Standard}, provides a canvas which can be rendered off-screen. If this interface is not available in a given worker, the worker will not report capability \"offscreenCanvas\".\n * @memberof module:dcp/compute~sandboxEnv\n * @access public\n */\n\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events/event-emitter */ \"./src/common/dcp-events/event-emitter.js\");\nconst { RangeObject, rehydrateRange } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst { RemoteDataSet } = __webpack_require__(/*! dcp/dcp-client/remote-data-set */ \"./src/dcp-client/remote-data-set.js\");\nconst { RemoteDataPattern } = __webpack_require__(/*! dcp/dcp-client/remote-data-pattern */ \"./src/dcp-client/remote-data-pattern.js\");\nconst { Job, ResultHandle } = __webpack_require__(/*! dcp/dcp-client/job */ \"./src/dcp-client/job/index.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { disableWorker } = __webpack_require__(/*! dcp/dcp-client/worker */ \"./src/dcp-client/worker/index.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');\n\nconst debug = (...args) => {\n if (debugging()) {\n args.unshift('dcp-client:compute');\n console.debug(...args);\n }\n};\n\nconst MARKET_VALUE = Symbol('Market Value');\nconst indirectEval = eval; // eslint-disable-line no-eval\n\n/**\n * Instance methods of this class are exported as static members of {@link module:dcp/compute|dcp/compute}.\n * @example\n * let job = compute.for(1, 3, function (i) {\n * progress('100%');\n * return i*10;\n * })\n * let results = await job.exec();\n */\nclass Compute extends EventEmitter {\n constructor () {\n super('Compute');\n\n /** @todo move these into some central config */\n this.defaultMaxGPUs = 1;\n this.GPUsAssigned = 0;\n this.allowSlicesToFinishBeforeStopping = false;\n\n this.schedulerURL = undefined; // Will default to dcpConfig.scheduler.location if not overidden\n this.bankURL = undefined; // Will default to dcpConfig.bank.services.bankTeller.location if not overidden\n\n Object.defineProperty(this.marketValue, MARKET_VALUE, {\n value: true,\n writable: false,\n configurable: false,\n enumerable: false,\n });\n\n const ceci = this;\n this.RemoteDataSet = RemoteDataSet;\n this.RemoteDataPattern = RemoteDataPattern;\n // Initialize to null so these properties are recognized for the Job class\n this.phemeConnection = null;\n this.deployConnection = null;\n this.openPhemeConn = function openPhemeConn()\n {\n ceci.phemeConnection = new protocolV4.Connection(dcpConfig.scheduler.services.pheme);\n ceci.phemeConnection.on('close', ceci.openPhemeConn);\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\n get defaultMaxWorkers () {\n return portalWorker.supervisor.maxWorkingSandboxes\n }\n\n set defaultMaxWorkers (value) {\n portalWorker.supervisor.maxWorkingSandboxes = value\n }\n\n get paymentAddress () {\n return portalWorker.supervisor.paymentAddress\n }\n\n set paymentAddress (value) {\n portalWorker.supervisor.paymentAddress = value\n }\n\n get mining () {\n return portalWorker.working\n }\n\n /** Return a special object that represents the current market value\n * @param {number} factor - OPTIONAL multiplier. default: 1.0\n */\n marketValue (factor = 1) {\n return {\n [MARKET_VALUE]: factor\n };\n }\n\n /** Describes the payment required to compute such a slice on a worker\n * or in a market working at the rates described in the WorkValue object.\n * This function does not take into account job-related overhead.\n * \n * @param {object} sliceProfile A SliceProfile-shaped object\n * @param {object} workValue A WorkValue-shaped object\n * @returns {number} Payment value in DCC describing rate to compute slice\n */\n calculateSlicePayment(sliceProfile, workValue) {\n return sliceProfile.cpuHours * workValue.cpuHour\n + sliceProfile.gpuHours * workValue.gpuHour\n + sliceProfile.inputMBytes * workValue.inputMByte\n + sliceProfile.outputMBytes * workValue.outputMByte;\n }\n\n \n /**\n * Deprecated `mine` API. Use `work` instead\n */\n async mine (numberOfCores, paymentAddress = this.paymentAddress) {\n return this.work(numberOfCores, paymentAddress);\n }\n\n /**\n * Instruct the Worker to stop working, then emit workerInactive\n */\n async stopWorking() {\n console.warn('Using the Compute API for controlling workers is deprecated. Use the Worker API instead.')\n await portalWorker.stop(!this.allowSlicesToFinishBeforeStopping)\n }\n\n /**\n * Deprecated `stopMining` API. Use `stopWorking` instead\n */\n stopMining () {\n console.log(\"Warning: used deprecated 'stopMining' API\");\n this.stopWorking();\n }\n \n /**\n * Flag worker as disabled\n */\n disableWorker() {\n disableWorker();\n }\n\n /**\n * Deprecated `disableMiner` API. Use `disableWorker` instead\n */\n disableMiner() {\n this.disableWorker();\n }\n\n /**\n * Experimental replacement for mine() /* wg aug 2019\n * @param {number} [numberOfCores = this.defaultMaxWorkers] - Number of parallel processes to run\n * @param {string} [paymentAddress = this.paymentAddress || wallet.get().address] - The address of the account to work for\n */\n async work(numCores, paymentAddress) {\n console.warn('Using the Compute API for controlling workers is deprecated. Use the Worker API instead.');\n\n portalWorker.supervisor.paymentAddress = paymentAddress || this.paymentAddress || (await wallet.get()).address;\n portalWorker.supervisor.maxWorkingSandboxes = numCores || this.defaultMaxWorkers || 1;\n await portalWorker.start();\n\n return portalWorker;\n }\n\n /**\n * Form 1. Create a Job for the distributed computer by specifying the start,\n * end, and step for a RangeObject with a given work function and arguments.\n *\n * @param {number} start First number in a range (see {@link RangeObject})\n * @param {number} end Last number in a range (see {@link RangeObject})\n * @param {number} [step=1] the space between numbers in a range (see\n * {@link RangeObject})\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * let job = compute.for(1, 3, function (i) {\n * progress('100%')\n * return i*10\n * })\n */\n /**\n * Form 2.a Create a Job for the distributed computer by specifying the input\n * as a RemoteDataSet with a given work function and arguments.\n *\n * @param {RemoteDataSet} remoteDataSet Array, ES6 function* generator, or any\n * other type of iterable object.\n * \n * Form 2.b Create a Job for the distributed computer by specifying the input\n * as a RemoteDataPattern with a given work function and arguments.\n * @param {RemoteDataPattern} RemoteDataPattern Array, ES6 function* generator, or any\n * other type of iterable object.\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * const remoteData = ['https://example.com/data'];\n * const job = compute.for(new RemoteDataSet(...remoteData), function(i) {\n * progress(1)\n * return i\n * })\n * \n * * @example\n * const pattern = 'https://example.com/data/{slice}.json';\n * const NumberOfSlices = 5;\n * const job = compute.for(new RemoteDataSet(pattern, NumberOfSlices), function(i) {\n * progress(1)\n * return i\n * })\n */\n /**\n * Form 3. Create a Job for the distributed computer by specifying the input\n * as an Iterable object with a given work function and arguments.\n *\n * @param {Iterable} iterableObject Array, ES6 function* generator, or any\n * other type of iterable object.\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * let job = compute.for([123,456], function(i) {\n * progress(1)\n * return i\n * })\n */\n /**\n * Form 4. Create a Job for the distributed computer by specifying the input\n * as a RangeObject with a given work function and arguments.\n *\n * @param {RangeObject} rangeObject Either a RangeObject or an object literal\n * which can be used to create a RangeObject (see {@link RangeObject}).\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * let job = compute.for({start: 10, end: 13, step: 2}, (i) => progress(1) && i)\n */\n /**\n * Form 5. Similar to Form 4, except with a multi range object containing an\n * array of range objects in the key ranges. These are used to create\n * multi-dimensional ranges, like nested loops. If they were written as\n * traditional loops, the outermost loop would be the leftmost range object,\n * and the innermost loop would be the rightmost range object.\n *\n * @param {MultiRangeObject} multiRange Either a MultiRangeObject or any valid\n * argument to the constructor.\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * let job = compute.for([{start: 1, end: 2}, {start: 3, end: 5}], function(i,j) {\n * return [i, j, i*j]\n * })\n */\n for(...args) {\n // args, or work function\n const lastArg = args[args.length - 1];\n // work function, if args provided\n const secondLastArg = args[args.length - 2];\n let range = null;\n let work;\n let genArgs;\n\n // All forms: extract work and optional args from the end and validate it.\n if (isValidWorkFunctionType(secondLastArg)) {\n // for(..., workFunction, arguments)\n work = secondLastArg\n if (lastArg && Array.from(lastArg).length > 0 || typeof lastArg === 'string') {\n // array or string, use as-is (string should be a URI)\n genArgs = lastArg;\n } else if (lastArg === undefined) {\n genArgs = [];\n } else {\n throw new TypeError(\n 'Work function arguments (the last parameter), should be an array.',\n );\n }\n } else if (isValidWorkFunctionType(lastArg)) {\n // for(..., workFunction)\n work = lastArg;\n genArgs = [];\n }\n\n if (!isWorkFunctionValid(work)) {\n throw new TypeError(\n `Work parameter must be a function or a string that evaluates to a function, or a URL that points to the location of a function.\\nReceived type ${typeof work} (${work})`,\n );\n }\n\n // Extract the proper information from every form.\n const [firstArg, secondArg, thirdArg] = args;\n if (['object', 'string'].includes(typeof firstArg)) {\n // Forms 2 & 3 (data is iterable, eg. an Array or RemoteDataSet)\n // Forms 4 & 5 (data is RangeObject & Friends)\n range = rehydrateRange(firstArg);\n } else if (typeof firstArg === 'number' && typeof secondArg === 'number') {\n // Form 1 (data is start, end[, step])\n const start = firstArg;\n const end = secondArg;\n\n /**\n * If there is an argument between the end and the work fn, use it as step\n * otherwise set step to undefined.\n */\n const step = typeof thirdArg === 'number' ? thirdArg : undefined;\n range = new RangeObject(start, end, step);\n } else {\n throw new TypeError(\n 'Parameters do not match one of the accepted call forms.',\n );\n }\n\n if (range.length === 0) {\n throw new Error('Length of input set must be greater than zero.');\n }\n\n debug('Work:', work);\n debug('Input Set:', range);\n debug('Arguments:', genArgs);\n return new Job(work, range, genArgs);\n\n /**\n * @param {any} workFunction\n */\n function isValidWorkFunctionType(workFunction) {\n return (\n typeof workFunction === 'function' ||\n typeof workFunction === 'string' ||\n DcpURL.isURL(workFunction)\n );\n }\n\n /**\n * Checks if the workFunction can be evaluated to a function or if it's\n * protocol is HTTPS,\n * @param {function | string | URL | DcpURL} workFunction\n */\n function isWorkFunctionValid(workFunction) {\n switch (typeof workFunction) {\n case 'function':\n return true;\n case 'string': {\n return isValidFunction(workFunction);\n }\n case 'object':\n return DcpURL.isURL(workFunction);\n default:\n return false;\n }\n }\n\n /**\n * Evaluates to true if the string can be coerced into a javascript\n * function.\n * @param {string} workFunction\n */\n function isValidFunction(workFunction) {\n try {\n const fn = indirectEval(`(${workFunction})`);\n return typeof fn === 'function';\n } catch (e) {\n return false;\n }\n }\n }\n\n /**\n * Form 1.\n * Create a job which will run a work function once\n *\n * @param {function | string | URL | DcpURL} work Work function as string or function literal.\n * @param {object} [workFunctionArgs=[]] optional Array-like object which contains arguments which are passed to the work func\n * @returns {Job}\n * @access public\n * @example\n * const job = compute.do(function (i) {\n * progress('100%')\n * return i*10\n * })\n */\n /**\n * Form 2.\n * Create a job which will run a work function on the values 1..n\n *\n * @param {number} n Number of times to run the work function.\n * @param {function | string | URL | DcpURL} work Work function as string or function literal.\n * @param {object} [workFunctionArgs=[]] optional Array-like object which contains arguments which are passed to the work func\n * @returns {Job}\n * @access public\n * @example\n * const job = compute.do(5, function (i) {\n * progress('100%')\n * return i*10\n * })\n */\n do(...args) {\n let n;\n let work;\n let workFunctionArgs;\n if (typeof args[0] !== 'number') {\n // Form 1.\n n = 1;\n [work, workFunctionArgs] = args;\n } else {\n // Form 2.\n [n, work, workFunctionArgs] = args;\n }\n return this.for(0, n - 1, work, workFunctionArgs);\n }\n\n /** Cancel a running job, by opaque id\n *\n * @param {string} id Address of the job to cancel.\n * @param {wallet.Keystore} ownerKeystore The keystore of the job owner\n * @param {string} reason If provided, will be sent on to client\n * @returns {object} The status of the funds that were released from escrow.\n * @throws when id or ownerKeystore are not provided.\n * @access public\n */\n async cancel (id, ownerKeystore, reason = '') {\n if (typeof id !== 'string') {\n throw new Error(`compute.cancel: Job ID must be a string, not ${typeof id}.`);\n }\n\n if (!(ownerKeystore instanceof wallet.Keystore)) {\n throw new Error('compute.cancel: ownerKeystore must be instance of wallet.Keystore');\n }\n\n if (!this.deployConnection)\n this.openDeployConn();\n\n const response = await this.deployConnection.send('cancelJob', {\n job: id,\n ownerAddress: ownerKeystore.address,\n reason,\n }, ownerKeystore);\n\n this.deployConnection.off('close', this.deployConnection)\n await this.deployConnection.close();\n\n return response.payload;\n }\n\n /**\n * Reconnect/resume a job, by opaque id\n *\n * @param {string} id Opaque id of the Job to resume/reconnect.\n * @param {wallet.Keystore} [ownerKeystore=wallet.get()] The keystore of the job owner\n * @returns {Job}\n * @throws when id is not provided.\n * @access public\n */\n async resume (id, ownerKeystore) {\n if (typeof id !== 'string') {\n throw new Error(`compute.resume: Job ID must be a string, not ${typeof id}.`);\n }\n\n if (!(ownerKeystore instanceof wallet.Keystore)) {\n ownerKeystore = await wallet.get();\n }\n\n const response = await this.schedulerConnection.send('fetchJob', {\n job: id, // XXX@TODO change 'address' to 'job' once we port this operation to v4\n owner: ownerKeystore.address,\n }, ownerKeystore);\n\n const job = new Job(response.payload);\n job.setPaymentAccountKeystore(ownerKeystore);\n return job;\n }\n\n /**\n * Form 1. Query the status of a job that has been deployed to the scheduler.\n *\n * @access public\n * @param {string|Job} job A job ID or a Job instance\n * @param {wallet.Keystore} ownerKeystore The identity keystore of the owner\n * of the job\n * @throws when ownerKeystore is not provided\n * @returns {Promise<object>} A status object describing a given job.\n */\n /**\n * Form 2. Query the status of jobs deployed to the scheduler.\n *\n * @access public\n * @param {object} [opts] Query options\n * @param {number} [opts.startTime] Jobs older than this will be excluded from\n * the results.\n * @param {number} [opts.endTime] Jobs newer than this will be excluded from\n * the results.\n * @param {wallet.Keystore} ownerKeystore The identity keystore of the owner\n * of the jobs to query\n * @throws when ownerKeystore is not provided\n * @returns {Promise<Array<Object>>} An array of status objects corresponding to the status of\n * jobs deployed by the given payment account.\n */\n async status(...args) {\n if (args.length !== 2) {\n throw new Error('Only 2 arguments must be passed to compute.status');\n }\n\n const ownerKeystore = args.pop();\n if (!(ownerKeystore instanceof wallet.Keystore)) {\n throw new TypeError(\n 'compute.status: ownerKeystore must be an instance of Keystore',\n );\n }\n\n const requestPayload = {\n idKeystoreOwner: ownerKeystore.address,\n };\n\n const firstArg = args.shift();\n if (typeof firstArg === 'string' || firstArg instanceof Job) {\n // Form 1\n requestPayload.jobId = firstArg instanceof Job ? firstArg.id : firstArg;\n } else if (typeof firstArg === 'object') {\n // Form 2\n if (\n (typeof firstArg.startTime !== 'undefined' &&\n !(firstArg.startTime instanceof Date)) ||\n (typeof firstArg.endTime !== 'undefined' &&\n !(firstArg.endTime instanceof Date))\n ) {\n throw new TypeError(\n 'startTime and or endTime were not specified as instances of Date',\n );\n }\n\n requestPayload.startDeployTime = firstArg.startTime;\n requestPayload.endDeployTime = firstArg.endTime;\n } else {\n throw new TypeError(`Unknown first argument type ${typeof firstArg}`);\n }\n\n if (!this.deployConnection)\n this.openDeployConn();\n\n const {\n success,\n payload: responsePayload,\n } = await this.deployConnection.send(\n 'listJobs',\n requestPayload,\n ownerKeystore,\n );\n\n if (!success) {\n throw new DCPError('Request for job status failed', responsePayload);\n }\n\n if (typeof firstArg === 'string' || firstArg instanceof Job) {\n return responsePayload[0];\n }\n\n this.deployConnection.off('close', this.deployConnection)\n await this.deployConnection.close();\n\n return responsePayload;\n }\n\n /**\n * Returns information for the job with the provided ID. It will use\n * `wallet.getId()` to retrieve the auth keystore.\n *\n * @param {string} jobAddress\n * @returns {Promise<object>} status object describing a given job\n * @access public\n */\n async getJobInfo(jobAddress) {\n if (!this.phemeConnection)\n this.openPhemeConn();\n\n const ks = await wallet.getId();\n const { payload: jobResponsePayload } = await this.phemeConnection.send('fetchJobReport', \n {\n jobOwner: ks.address,\n job: jobAddress\n });\n\n const { payload: sliceResponsePayload } = await this.phemeConnection.send(\n 'fetchSliceReport', \n {\n jobOwner: ks.address,\n job: jobAddress\n },\n );\n\n const jobReport = {};\n Object.assign(jobReport, jobResponsePayload, sliceResponsePayload);\n\n this.phemeConnection.off('close', this.phemeConnection)\n await this.phemeConnection.close();\n\n return jobReport;\n }\n\n /**\n * Returns information for the slices of the job with the provided ID. It will\n * use `wallet.getId()` to retrieve the auth keystore.\n *\n * @param {string} jobAddress\n * @returns {Promise<object>} A slice status object for the given job.\n * @access public\n */\n async getSliceInfo(jobAddress) {\n if (!this.phemeConnection)\n this.openPhemeConn();\n\n const identityKeystore = await wallet.getId();\n const {\n success,\n payload: responsePayload,\n } = await this.phemeConnection.send(\n 'fetchSliceReport',\n {\n job: jobAddress,\n jobOwner: identityKeystore.address,\n },\n identityKeystore,\n );\n\n if (!success) {\n throw new DCPError('Request for slice info failed', responsePayload);\n }\n\n this.phemeConnection.off('close', this.phemeConnection)\n await this.phemeConnection.close();\n\n return responsePayload;\n }\n}\n\nexports.RemoteDataSet = RemoteDataSet;\nexports.RemoteDataPattern = RemoteDataPattern;\nexports.MARKET_VALUE = MARKET_VALUE;\nexports.compute = new Compute(); /* :( */\nexports.compute.getPublicComputeGroupId = () => __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups.public.id;\nexports.compute.getPublicComputeGroupDescriptor = () => __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups.public;\nexports.compute.ResultHandle = ResultHandle; // export as compute.ResultHandle for instanceof checking\nexports.version = {\n api: '1.5.2',\n provides: '1.0.0' /* dcpConfig.scheduler.compatibility.operations.compute */\n};\n\n\n//# sourceURL=webpack:///./src/dcp-client/compute.js?");
5092
5110
 
5093
5111
  /***/ }),
5094
5112
 
@@ -5099,7 +5117,13 @@ eval("/**\n * @file Module that implements Compute API\n * @module dcp/comput
5099
5117
  /*! no static exports found */
5100
5118
  /***/ (function(module, exports, __webpack_require__) {
5101
5119
 
5120
+ <<<<<<< HEAD
5102
5121
  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 'dcp-build': {\"version\":\"2a07ad83519f5b6750b47f5937cf446b64c218a8\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"4.1.15\",\"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#6d217f06ecec68b8910ec67d5d6bc5d23d513222\"},\"built\":\"Tue Dec 21 2021 15:04:00 GMT-0500 (Eastern Standard Time)\",\"config\":{\"generated\":\"Tue 21 Dec 2021 03:03:57 PM EST by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"4.46.0\",\"node\":\"v12.22.8\"},\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__(/*! ../../portal/www/js/modal */ \"./portal/www/js/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?");
5122
+ ||||||| 412fbe6
5123
+ 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 'dcp-build': {\"version\":\"f2e5c6725c53bade3b332a35a26ae8beb2897dcd\",\"branch\":\"prod-20211216\",\"dcpClient\":{\"version\":\"4.1.15\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#prod-20211216\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#113c6cac8b2580c1fca75b5d6bd99ff1b1e986c5\"},\"built\":\"Mon Dec 20 2021 14:18:11 GMT-0500 (Eastern Standard Time)\",\"config\":{\"generated\":\"Mon 20 Dec 2021 02:18:11 PM EST by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"4.46.0\",\"node\":\"v12.22.8\"},\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__(/*! ../../portal/www/js/modal */ \"./portal/www/js/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?");
5124
+ =======
5125
+ 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 'dcp-build': {\"version\":\"87a7eadd2dd11f5196813e252eb61438232b50da\",\"branch\":\"prod-20220111\",\"dcpClient\":{\"version\":\"4.1.17\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#prod-20220111\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#952b8321d5019c93003262d910c04d487c582418\"},\"built\":\"Thu Jan 13 2022 15:38:17 GMT-0500 (Eastern Standard Time)\",\"config\":{\"generated\":\"Thu 13 Jan 2022 03:38:16 PM EST by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"4.46.0\",\"node\":\"v12.22.9\"},\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__(/*! ../../portal/www/js/modal */ \"./portal/www/js/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?");
5126
+ >>>>>>> origin/prod-20220111
5103
5127
 
5104
5128
  /***/ }),
5105
5129
 
@@ -5111,7 +5135,7 @@ eval("/* WEBPACK VAR INJECTION */(function(module) {/**\n * @file dcp-cli
5111
5135
  /***/ (function(module, exports, __webpack_require__) {
5112
5136
 
5113
5137
  "use strict";
5114
- 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 { getTextEncoder } = __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 { RemoteDataPattern } = __webpack_require__(/*! dcp/dcp-client/remote-data-pattern */ \"./src/dcp-client/remote-data-pattern.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 TextEncoder = getTextEncoder();\nlet dannyDebugCounter = 0;\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 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)) { return inputData }\n if (RemoteDataPattern.isRemoteDataPattern(inputData)) { return inputData }\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 this.collateResults = true;\n this.listeningForResults = false;\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(this);\n \n // Some events can't be emitted 'naturally' without having weird/wrong output.\n // An example of this is results. When results are returned from the scheduler,\n // They come in as a dataURI of kvin-ified results. We need to parse all that before\n // We actually send it to the client. For such events, we will intercept them, parse\n // them as needed, then emit the event with the 'fixed' data to the client.\n \n const ceci = this\n const parseConsole = function deserializeConsoleMessage(ev) {\n ceci.emit('console', ev);\n }\n \n this.eventIntercepts = {\n result: (ev) => this[ON_RESULT](ev),\n status: (ev) => this[ON_STATUS](ev),\n cancel: (ev) => this[INTERNAL_SYMBOL].events.emit('stopped', ev),\n console: parseConsole,\n }\n\n this.eventTypes = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\").eventTypes;\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 });\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 const wrangledArguments = wrangleData(arguments[2] || []);\n \n log('wrangledInputData:', wrangledInputData);\n log('wrangledArguments:', wrangledArguments);\n \n Object.assign(this[INTERNAL_SYMBOL].payloadDetails, {\n request: 'main',\n data: wrangledInputData,\n arguments: wrangledArguments,\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 // 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 /**\n * Helper function that tries to upload slicePile to scheduler for the job with the given address\n * If the connection throws, we will continue trying to upload until it has thrown errorTolerance times\n * However, if the upload is unsuccessful, we throw immediately.\n * @param {Array} slicePile \n * @returns payload containing success property (pertaining to success of adding slices to job) as well as lastSliceNumber of job \n */\n async safeSliceUpload(slicePile)\n {\n let payload = undefined; // future return value\n let errorTolerance = dcpConfig.job.sliceUploadErrorTolerance; // copy number of times we will tolerate non-success when uploading slices directly from config\n await this.deployConnection.keepalive();\n while (true) // eslint-disable-line no-constant-condition\n {\n try\n {\n payload = await this.deployConnection.send('addSliceData', {\n job: this.address,\n dataValues: kvin.marshal(slicePile),\n });\n if (!payload.success)\n throw new DCPError('Cannot upload slice data to scheduler','EUPLOADSCHED');\n else\n break;\n }\n catch (error)\n {\n if (--errorTolerance <= 0)\n throw error;\n }\n }\n return payload;\n }\n \n /**\n * This function contains the actual logic behind staggered slice uploads\n * to the scheduler which makes quicker deployment possible.\n * \n * Note that we pass in mostToTake so that the uploadLogic function can update \n * it to the new value it needs to be, and then pass it back to the wrapper \n * function (addSlices) which actually does the work of picking up slices \n * and thus uses this value\n * @param {Array} pile the actual array of slices being uploaded to scheduler\n * @param {Number} mostToTake number of slices that should be taken by the wrapper function (addSlices) \n * which actually does the work of picking up slices and thus uses this value.\n * We pass in mostToTake so that the uploadLogic function can update it to the \n * new value it needs to be, and then pass it back to the wrapper\n * @returns payload containing success property (pertaining to success of adding slices to job) as well as lastSliceNumber of job\n */\n async sliceUploadLogic(pile, mostToTake)\n {\n const slicesTaken = pile.length;\n \n let pileSize = 0; // total size of the pile's slices in bytes\n \n // calculate pileSize by finding sum of bytesizes of each slice (after each is marshalled)\n for (let i = 0; i < slicesTaken; ++i)\n {\n let sliceSize = (new TextEncoder()).encode(kvin.stringify(pile[i])).length;\n pileSize += sliceSize;\n }\n\n let newMostToTake;\n let uploadedSlices;\n \n // if the pile is larger than the ceiling but we only took one slice, there's no smaller pile we can make\n // so we upload it anyway but we don't try taking more next time cause we were over the ceiling (which \n // is a hard limit on upload sizes)\n if ((pileSize > dcpConfig.job.uploadSlicesCeiling) && (slicesTaken === 1))\n {\n uploadedSlices = await this.safeSliceUpload(pile);\n newMostToTake = 1;\n }\n \n // if the pile is larger than the target but we only took one slice, there's no smaller pile we can make\n // so we upload it anyway and still try taking more\n else if ((pileSize > dcpConfig.job.uploadSlicesTarget) && (slicesTaken === 1))\n {\n uploadedSlices = await this.safeSliceUpload(pile);\n newMostToTake = mostToTake * dcpConfig.job.uploadIncreaseFactor;\n }\n \n // otherwise, if the pile is smaller than the soft ceiling, send up the pile anyway (since piles are expensive to make) \n // but remember to include incrementFactor times as many slices in the next pile\n else if (pileSize <= dcpConfig.job.uploadSlicesTarget)\n {\n uploadedSlices = await this.safeSliceUpload(pile);\n newMostToTake = mostToTake * dcpConfig.job.uploadIncreaseFactor;\n }\n \n // if the pile is over the ceiling then we do not upload and begin reassembling our piles from scratch\n else if (pileSize > dcpConfig.job.uploadSlicesCeiling)\n {\n newMostToTake = -1;\n }\n \n // if the pile is over the target (but implicitly under the ceiling), then upload the pile to scheduler but lower mostToTake\n // by a smaller factor than incrementFactor to allow us to begin \"centering\" sizes of piles around the target\n else if (pileSize > dcpConfig.job.uploadSlicesTarget)\n {\n uploadedSlices = await this.safeSliceUpload(pile);\n newMostToTake = Math.ceil(mostToTake / ((2 / 3) * dcpConfig.job.uploadIncreaseFactor));\n }\n\n if (uploadedSlices && uploadedSlices.success && typeof uploadedSlices.payload.lastSliceNumber !== 'undefined')\n // must check if uploadedSlices exists first since if pileSize > ceiling then there will be no uploadedSlices\n this.status.total = uploadedSlices.payload.lastSliceNumber;\n\n let payload = uploadedSlices ? uploadedSlices.payload : undefined;\n return { payload, newMostToTake }; // in case the user needs lastSliceNumber's value\n }\n \n /**\n * Uploads slices to the scheduler in a staggered fashion\n * @param {Array} dataValues actual array of slices being uploaded to scheduler\n * @returns payload containing success property (pertaining to success of adding slices to job) as well as lastSliceNumber of job\n */\n async addSlices(dataValues)\n {\n if (!Array.isArray(dataValues))\n throw new TypeError('Only data-by-value jobs may dynamically add slices');\n\n let mostToTake = 1; // maximum number of slices we could take in per pile\n let payload = undefined; // used in return value\n let slicesTaken = 0; // number of slices in the pile already\n let pile = [];\n \n for (let slice of dataValues)\n {\n pile.push(slice);\n slicesTaken++;\n if (slicesTaken === mostToTake)\n {\n let total = await this.sliceUploadLogic(pile, mostToTake);\n payload = total.payload;\n \n if (total.newMostToTake < 0)\n {\n /* if total.newMostToTake == -1 (only non-positive value returned), then the pile was not successfully\n * uploaded because it was over the ceiling and we need to upload the pile *itself* again, recursively\n */\n payload = await this.addSlices(pile);\n /* and next time, the number of slices we take is the number from this time *divided* by the incrementFactor\n * since we know invariably that number of slices was under the ceiling AND target\n * if you're curious why that's an invariant, this is because mostToTake only ever *increases* by being multiplied by \n * a factor of incrementFactor within sliceUploadLogic, and this only occurs when the pile being uploaded that time\n * was under the target\n */\n mostToTake = mostToTake / dcpConfig.job.uploadIncreaseFactor;\n }\n else\n {\n /* in all other cases (other than the pile size being over the ceiling) the sliceUploadLogic helper \n * determines the number of slices we should pick up next time, so we just use the value it spits out\n */\n mostToTake = total.newMostToTake;\n }\n \n // reset slicesTaken and pile since at this point we know for sure the pile has been uploaded\n pile = [];\n slicesTaken = 0;\n }\n else\n {\n continue;\n }\n }\n // upload the pile one last time in case we continued off the last slice with a non-empty pile\n if (pile.length !== 0)\n {\n let finalObj = await this.sliceUploadLogic(pile, mostToTake);\n payload = finalObj.payload;\n mostToTake = finalObj.newMostToTake;\n \n if (mostToTake < 0)\n {\n // if you need documentation on the next two lines, look inside the if (total.newMostToTake < 0) just above\n payload = await this.addSlices(pile);\n mostToTake = mostToTake / dcpConfig.job.uploadIncreaseFactor;\n }\n }\n\n // and finally assign whatever mostToTake was at the end of this run of the function to be returned \n // as part of the payload in case addSlices was called recursively\n payload.mostToTake = mostToTake;\n \n /* contains the job's lastSliceNumber (the only externally-meaningful value returned from \n * the uploading of slices to the scheduler) in case the calling function needs it \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 (as well as public group, if included)\n await computeGroups.addJobToGroups(this.address, this.computeGroups);\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 let data = this[INTERNAL_SYMBOL].dataValues;\n \n // if job data is by value then upload data to the scheduler in a staggered fashion\n if (Array.isArray(data))\n await this.addSlices(data).then(() => {\n return this.close();\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', async (stopEvent) => {\n this.emit('stopped', stopEvent.runStatus);\n switch (stopEvent.runStatus) {\n case 'finished':\n if (this.collateResults) {\n let report = await this.getJobInfo();\n\n if(this[INTERNAL_SYMBOL].resultsAvailable.length < report.totalSlices) {\n // fetch results for remain slices\n let fetchedSliceNumbers = this[INTERNAL_SYMBOL].resultsAvailable.reduce((a,e,i) => {\n if(e) a.push(i);\n return a;\n }, []);\n\n let allSliceNumbers = Array.from(Array(report.totalSlices)).map((e,i)=>i+1);\n let remainSliceNumbers = allSliceNumbers.filter( function(e) {\n return !fetchedSliceNumbers.includes(e);\n });\n\n let promises = remainSliceNumbers.map(sliceNumber => this.results.fetch([sliceNumber], true));\n await Promise.all(promises);\n }\n }\n \n this.emit('complete', this.results);\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 // Always listen to the stop event. It will resolve the work function promise, so is always needed.\n this.on('stop', (ev) => this[INTERNAL_SYMBOL].events.emit('stopped', ev));\n\n // Connect listeners that were set up before exec\n const evts = Array.from(this[INTERNAL_SYMBOL].subscribedEvents);\n if (evts.includes('result'))\n this.listeningForResults = true;\n this[INTERNAL_SYMBOL].subscribedEvents.clear();\n await this[LISTEN_TO_EVENTS](evts);\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 if (this.collateResults && !this.listeningForResults) {\n // automatically add a listener for results\n this.on('result', () => {});\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 * @param {string[]} events \n */\n async [LISTEN_TO_EVENTS](events) {\n\n const reliableEvents = [];\n const optionalEvents = [];\n for (let eventName of events) {\n eventName = eventName.toLowerCase();\n if (this[INTERNAL_SYMBOL].subscribedEvents.has(eventName))\n {\n // already subscribed to this event\n continue;\n }\n else\n {\n this[INTERNAL_SYMBOL].subscribedEvents.add(eventName);\n \n if (this.eventTypes[eventName] && this.eventTypes[eventName].reliable)\n {\n reliableEvents.push(eventName)\n }\n else if (this.eventTypes[eventName] && !this.eventTypes[eventName].reliable)\n {\n optionalEvents.push(eventName)\n }\n else\n {\n // console.debug('606: listening for unexpected/unsupported event:', eventName);\n }\n }\n }\n await this.eventSubscriber.subscribeManyEvents(reliableEvents, optionalEvents, { filter: { job: this.address } })\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 }\n else\n {\n this[INTERNAL_SYMBOL].subscribedWorkerEvents.add(eventName);\n this.eventIntercepts.custom = (ev) => this.work.emit(eventName, ev)\n const optionalEvents = ['custom'];\n await this.eventSubscriber.subscribeManyEvents([], optionalEvents, { filter: { job: this.address } });\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\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, sliceCount } = marshalInputData(payloadDetails.data);\n if(dataValues)\n this[INTERNAL_SYMBOL].dataValues = dataValues;\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 dataPattern,\n sliceCount\n };\n\n /* Determine thee type of the arguments option and set the submit message payload accordingly. */\n if (Array.isArray(payloadDetails.arguments) && payloadDetails.arguments.length === 1 && payloadDetails.arguments[0] instanceof DcpURL) {\n submitPayload.arguments = payloadDetails.arguments[0].href;\n } else if (payloadDetails.arguments instanceof RemoteDataSet) {\n submitPayload.marshaledArguments = kvin.marshal(payloadDetails.arguments.map(e => new URL(e)))\n } else if (payloadDetails.arguments) {\n try {\n submitPayload.marshaledArguments = 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 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 /** close an open job to indicate we are done adding data so it is okay to finish\n * the job at the appropriate time\n */\n async close() {\n return this.deployConnection.send('closeJob', {\n job: this.id,\n });\n }\n}\n\n/**\n * @typedef {object} MarshaledInputData\n * @property {any[]} [dataValues]\n * @property {string} [dataPattern]\n * @property {RangeObject} [dataRange]\n * @property {number} [sliceCount]\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 } else if(data instanceof RemoteDataSet) {\n marshalledInputData.dataValues = data.map(e => new URL(e));\n } else if(data instanceof RemoteDataPattern) {\n marshalledInputData.dataPattern = data['pattern'];\n marshalledInputData.sliceCount = data['sliceCount'];\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?");
5138
+ 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 { getTextEncoder } = __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 { RemoteDataPattern } = __webpack_require__(/*! dcp/dcp-client/remote-data-pattern */ \"./src/dcp-client/remote-data-pattern.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 TextEncoder = getTextEncoder();\nlet dannyDebugCounter = 0;\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 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)) { return inputData }\n if (RemoteDataPattern.isRemoteDataPattern(inputData)) { return inputData }\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 this.collateResults = true;\n this.listeningForResults = false;\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(this);\n \n // Some events can't be emitted 'naturally' without having weird/wrong output.\n // An example of this is results. When results are returned from the scheduler,\n // They come in as a dataURI of kvin-ified results. We need to parse all that before\n // We actually send it to the client. For such events, we will intercept them, parse\n // them as needed, then emit the event with the 'fixed' data to the client.\n \n const ceci = this\n const parseConsole = function deserializeConsoleMessage(ev) {\n ceci.emit('console', ev);\n }\n \n this.eventIntercepts = {\n result: (ev) => this[ON_RESULT](ev),\n status: (ev) => this[ON_STATUS](ev),\n cancel: (ev) => this[INTERNAL_SYMBOL].events.emit('stopped', ev),\n console: parseConsole,\n }\n\n this.eventTypes = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\").eventTypes;\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 });\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 const wrangledArguments = wrangleData(arguments[2] || []);\n \n log('wrangledInputData:', wrangledInputData);\n log('wrangledArguments:', wrangledArguments);\n \n Object.assign(this[INTERNAL_SYMBOL].payloadDetails, {\n request: 'main',\n data: wrangledInputData,\n arguments: wrangledArguments,\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 // 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 /**\n * Helper function that tries to upload slicePile to scheduler for the job with the given address\n * If the connection throws, we will continue trying to upload until it has thrown errorTolerance times\n * However, if the upload is unsuccessful, we throw immediately.\n * @param {Array} slicePile \n * @returns payload containing success property (pertaining to success of adding slices to job) as well as lastSliceNumber of job \n */\n async safeSliceUpload(slicePile)\n {\n let payload = undefined; // future return value\n let errorTolerance = dcpConfig.job.sliceUploadErrorTolerance; // copy number of times we will tolerate non-success when uploading slices directly from config\n await this.deployConnection.keepalive();\n while (true) // eslint-disable-line no-constant-condition\n {\n try\n {\n payload = await this.deployConnection.send('addSliceData', {\n job: this.address,\n dataValues: kvin.marshal(slicePile),\n });\n if (!payload.success)\n throw new DCPError('Cannot upload slice data to scheduler','EUPLOADSCHED');\n else\n break;\n }\n catch (error)\n {\n if (--errorTolerance <= 0)\n throw error;\n }\n }\n return payload;\n }\n \n /**\n * This function contains the actual logic behind staggered slice uploads\n * to the scheduler which makes quicker deployment possible.\n * \n * Note that we pass in mostToTake so that the uploadLogic function can update \n * it to the new value it needs to be, and then pass it back to the wrapper \n * function (addSlices) which actually does the work of picking up slices \n * and thus uses this value\n * @param {Array} pile the actual array of slices being uploaded to scheduler\n * @param {Number} mostToTake number of slices that should be taken by the wrapper function (addSlices) \n * which actually does the work of picking up slices and thus uses this value.\n * We pass in mostToTake so that the uploadLogic function can update it to the \n * new value it needs to be, and then pass it back to the wrapper\n * @returns payload containing success property (pertaining to success of adding slices to job) as well as lastSliceNumber of job\n */\n async sliceUploadLogic(pile, mostToTake)\n {\n const slicesTaken = pile.length;\n \n let pileSize = 0; // total size of the pile's slices in bytes\n \n // calculate pileSize by finding sum of bytesizes of each slice (after each is marshalled)\n for (let i = 0; i < slicesTaken; ++i)\n {\n let sliceSize = (new TextEncoder()).encode(kvin.stringify(pile[i])).length;\n pileSize += sliceSize;\n }\n\n let newMostToTake;\n let uploadedSlices;\n \n // if the pile is larger than the ceiling but we only took one slice, there's no smaller pile we can make\n // so we upload it anyway but we don't try taking more next time cause we were over the ceiling (which \n // is a hard limit on upload sizes)\n if ((pileSize > dcpConfig.job.uploadSlicesCeiling) && (slicesTaken === 1))\n {\n uploadedSlices = await this.safeSliceUpload(pile);\n newMostToTake = 1;\n }\n \n // if the pile is larger than the target but we only took one slice, there's no smaller pile we can make\n // so we upload it anyway and still try taking more\n else if ((pileSize > dcpConfig.job.uploadSlicesTarget) && (slicesTaken === 1))\n {\n uploadedSlices = await this.safeSliceUpload(pile);\n newMostToTake = mostToTake * dcpConfig.job.uploadIncreaseFactor;\n }\n \n // otherwise, if the pile is smaller than the soft ceiling, send up the pile anyway (since piles are expensive to make) \n // but remember to include incrementFactor times as many slices in the next pile\n else if (pileSize <= dcpConfig.job.uploadSlicesTarget)\n {\n uploadedSlices = await this.safeSliceUpload(pile);\n newMostToTake = mostToTake * dcpConfig.job.uploadIncreaseFactor;\n }\n \n // if the pile is over the ceiling then we do not upload and begin reassembling our piles from scratch\n else if (pileSize > dcpConfig.job.uploadSlicesCeiling)\n {\n newMostToTake = -1;\n }\n \n // if the pile is over the target (but implicitly under the ceiling), then upload the pile to scheduler but lower mostToTake\n // by a smaller factor than incrementFactor to allow us to begin \"centering\" sizes of piles around the target\n else if (pileSize > dcpConfig.job.uploadSlicesTarget)\n {\n uploadedSlices = await this.safeSliceUpload(pile);\n newMostToTake = Math.ceil(mostToTake / ((2 / 3) * dcpConfig.job.uploadIncreaseFactor));\n }\n\n if (uploadedSlices && uploadedSlices.success && typeof uploadedSlices.payload.lastSliceNumber !== 'undefined')\n // must check if uploadedSlices exists first since if pileSize > ceiling then there will be no uploadedSlices\n this.status.total = uploadedSlices.payload.lastSliceNumber;\n\n let payload = uploadedSlices ? uploadedSlices.payload : undefined;\n return { payload, newMostToTake }; // in case the user needs lastSliceNumber's value\n }\n \n /**\n * Uploads slices to the scheduler in a staggered fashion\n * @param {Array} dataValues actual array of slices being uploaded to scheduler\n * @returns payload containing success property (pertaining to success of adding slices to job) as well as lastSliceNumber of job\n */\n async addSlices(dataValues)\n {\n if (!Array.isArray(dataValues))\n throw new TypeError('Only data-by-value jobs may dynamically add slices');\n\n let mostToTake = 1; // maximum number of slices we could take in per pile\n let payload = undefined; // used in return value\n let slicesTaken = 0; // number of slices in the pile already\n let pile = [];\n \n for (let slice of dataValues)\n {\n pile.push(slice);\n slicesTaken++;\n if (slicesTaken === mostToTake)\n {\n let total = await this.sliceUploadLogic(pile, mostToTake);\n payload = total.payload;\n \n if (total.newMostToTake < 0)\n {\n /* if total.newMostToTake == -1 (only non-positive value returned), then the pile was not successfully\n * uploaded because it was over the ceiling and we need to upload the pile *itself* again, recursively\n */\n payload = await this.addSlices(pile);\n /* and next time, the number of slices we take is the number from this time *divided* by the incrementFactor\n * since we know invariably that number of slices was under the ceiling AND target\n * if you're curious why that's an invariant, this is because mostToTake only ever *increases* by being multiplied by \n * a factor of incrementFactor within sliceUploadLogic, and this only occurs when the pile being uploaded that time\n * was under the target\n */\n mostToTake = mostToTake / dcpConfig.job.uploadIncreaseFactor;\n }\n else\n {\n /* in all other cases (other than the pile size being over the ceiling) the sliceUploadLogic helper \n * determines the number of slices we should pick up next time, so we just use the value it spits out\n */\n mostToTake = total.newMostToTake;\n }\n \n // reset slicesTaken and pile since at this point we know for sure the pile has been uploaded\n pile = [];\n slicesTaken = 0;\n }\n else\n {\n continue;\n }\n }\n // upload the pile one last time in case we continued off the last slice with a non-empty pile\n if (pile.length !== 0)\n {\n let finalObj = await this.sliceUploadLogic(pile, mostToTake);\n payload = finalObj.payload;\n mostToTake = finalObj.newMostToTake;\n \n if (mostToTake < 0)\n {\n // if you need documentation on the next two lines, look inside the if (total.newMostToTake < 0) just above\n payload = await this.addSlices(pile);\n mostToTake = mostToTake / dcpConfig.job.uploadIncreaseFactor;\n }\n }\n\n // and finally assign whatever mostToTake was at the end of this run of the function to be returned \n // as part of the payload in case addSlices was called recursively\n payload.mostToTake = mostToTake;\n \n /* contains the job's lastSliceNumber (the only externally-meaningful value returned from \n * the uploading of slices to the scheduler) in case the calling function needs it \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 (as well as public group, if included)\n await computeGroups.addJobToGroups(this.address, this.computeGroups);\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 let data = this[INTERNAL_SYMBOL].dataValues;\n \n // if job data is by value then upload data to the scheduler in a staggered fashion\n if (Array.isArray(data))\n await this.addSlices(data).then(() => {\n return this.close();\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', async (stopEvent) => {\n this.emit('stopped', stopEvent.runStatus);\n switch (stopEvent.runStatus) {\n case 'finished':\n if (this.collateResults) {\n let report = await this.getJobInfo();\n\n // this reducer will count nonempty elements\n if(this[INTERNAL_SYMBOL].resultsAvailable.reduce((a, r) => {\n return a + (r ? 1 : 0);\n }, 0) < report.totalSlices) {\n // fetch results for remain slices\n let fetchedSliceNumbers = this[INTERNAL_SYMBOL].resultsAvailable.reduce((a,e,i) => {\n if(e) a.push(i);\n return a;\n }, []);\n\n let allSliceNumbers = Array.from(Array(report.totalSlices)).map((e,i)=>i+1);\n let remainSliceNumbers = allSliceNumbers.filter( function(e) {\n return !fetchedSliceNumbers.includes(e);\n });\n\n let promises = remainSliceNumbers.map(sliceNumber => this.results.fetch([sliceNumber], true));\n await Promise.all(promises);\n }\n }\n \n this.emit('complete', this.results);\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 this.bankConnection.off('close', this.openDeployConn)\n await this.bankConnection.close().catch(handleErr);\n \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 // Always listen to the stop event. It will resolve the work function promise, so is always needed.\n this.on('stop', (ev) => this[INTERNAL_SYMBOL].events.emit('stopped', ev));\n\n // Connect listeners that were set up before exec\n const evts = Array.from(this[INTERNAL_SYMBOL].subscribedEvents);\n if (evts.includes('result'))\n this.listeningForResults = true;\n this[INTERNAL_SYMBOL].subscribedEvents.clear();\n await this[LISTEN_TO_EVENTS](evts);\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 if (this.collateResults && !this.listeningForResults) {\n // automatically add a listener for results\n this.on('result', () => {});\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 * @param {string[]} events \n */\n async [LISTEN_TO_EVENTS](events) {\n\n const reliableEvents = [];\n const optionalEvents = [];\n for (let eventName of events) {\n eventName = eventName.toLowerCase();\n if (this[INTERNAL_SYMBOL].subscribedEvents.has(eventName))\n {\n // already subscribed to this event\n continue;\n }\n else\n {\n this[INTERNAL_SYMBOL].subscribedEvents.add(eventName);\n \n if (this.eventTypes[eventName] && this.eventTypes[eventName].reliable)\n {\n reliableEvents.push(eventName)\n }\n else if (this.eventTypes[eventName] && !this.eventTypes[eventName].reliable)\n {\n optionalEvents.push(eventName)\n }\n else\n {\n // console.debug('606: listening for unexpected/unsupported event:', eventName);\n }\n }\n }\n await this.eventSubscriber.subscribeManyEvents(reliableEvents, optionalEvents, { filter: { job: this.address } })\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 }\n else\n {\n this[INTERNAL_SYMBOL].subscribedWorkerEvents.add(eventName);\n this.eventIntercepts.custom = (ev) => this.work.emit(eventName, ev)\n const optionalEvents = ['custom'];\n await this.eventSubscriber.subscribeManyEvents([], optionalEvents, { filter: { job: this.address } });\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\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 const adhocId = payloadDetails.uuid.slice(payloadDetails.uuid.length - 6, payloadDetails.uuid.length);\n const schedId = await dcpConfig.scheduler.identity;\n const myId = await wallet.getId();\n const preauthToken = await bankUtil.preAuthorizePayment(schedId, payloadDetails.maxDeployPayment, this.paymentAccountKeystore);\n const { dataRange, dataValues, dataPattern, sliceCount } = marshalInputData(payloadDetails.data);\n if(dataValues)\n this[INTERNAL_SYMBOL].dataValues = dataValues;\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 dataPattern,\n sliceCount\n };\n\n /* Determine thee type of the arguments option and set the submit message payload accordingly. */\n if (Array.isArray(payloadDetails.arguments) && payloadDetails.arguments.length === 1 && payloadDetails.arguments[0] instanceof DcpURL) {\n submitPayload.arguments = payloadDetails.arguments[0].href;\n } else if (payloadDetails.arguments instanceof RemoteDataSet) {\n submitPayload.marshaledArguments = kvin.marshal(payloadDetails.arguments.map(e => new URL(e)))\n } else if (payloadDetails.arguments) {\n try {\n submitPayload.marshaledArguments = 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 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 /** close an open job to indicate we are done adding data so it is okay to finish\n * the job at the appropriate time\n */\n async close() {\n return this.deployConnection.send('closeJob', {\n job: this.id,\n });\n }\n}\n\n/**\n * @typedef {object} MarshaledInputData\n * @property {any[]} [dataValues]\n * @property {string} [dataPattern]\n * @property {RangeObject} [dataRange]\n * @property {number} [sliceCount]\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 } else if(data instanceof RemoteDataSet) {\n marshalledInputData.dataValues = data.map(e => new URL(e));\n } else if(data instanceof RemoteDataPattern) {\n marshalledInputData.dataPattern = data['pattern'];\n marshalledInputData.sliceCount = data['sliceCount'];\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?");
5115
5139
 
5116
5140
  /***/ }),
5117
5141
 
@@ -5134,7 +5158,7 @@ eval("/**\n * @file node-modules.js Node-specific support for sen
5134
5158
  /*! no static exports found */
5135
5159
  /***/ (function(module, exports, __webpack_require__) {
5136
5160
 
5137
- eval("/**\n * @file job/result-handle.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date June 2020\n *\n * The ResultHandle acts as a proxy for a job's results, querying\n * internal results when available or the scheduler when the results\n * are not available locally.\n */\n\nconst { rehydrateRange } = __webpack_require__(/*! ../range-object */ \"./src/dcp-client/range-object.js\");\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 { fetchURI } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n\nconst RH_INTERNAL_SYMBOL = Symbol('Result Handle Internals');\n// Array methods that don't modify the array, will be applied to the ResultHandle's values object\nconst RH_ARRAY_METHODS = [\n 'slice', 'filter', 'concat', 'find', 'findIndex', 'indexOf', 'map', 'reduce', 'includes', 'toString', 'forEach'\n];\n\n/**\n * This class represents a handle on a job's results. It can be used for accessing the job's results, or for querying the scheduler to fetch results.\n * In addition to the properties and methods of this class, the following standard array methods are also available for accessing the available results: `slice`, `filter`, `concat`, `find`, `findIndex`, `indexOf`, `map`, `reduce`, `includes`, `toString`, and `forEach`.\n * The results can also be accessed by index (`results[i]`), but an error will be thrown if the result for that index is not yet available.\n * @access public\n */\nclass ResultHandle extends Array {\n constructor(job) {\n super();\n\n const { Job, JOB_INTERNAL_SYMBOL } = __webpack_require__(/*! . */ \"./src/dcp-client/job/index.js\");\n if (!(job instanceof Job)) {\n throw new TypeError('ResultHandle must be constructed from a Job object');\n }\n\n /**\n * The length of the available results array.\n * @type {number}\n * @access public\n */\n this.length = 0; // overridden by the proxy, here for JSDoc purposes\n\n this[RH_INTERNAL_SYMBOL] = {\n job,\n keys: job[JOB_INTERNAL_SYMBOL].payloadDetails.data,\n values: job[JOB_INTERNAL_SYMBOL].results,\n valuesAvailable: job[JOB_INTERNAL_SYMBOL].resultsAvailable,\n };\n\n return new Proxy(this, {\n get: (target, name) => {\n if ((typeof name === 'string' || typeof name === 'number') && Number.isInteger(parseFloat(name))) {\n let i = parseFloat(name);\n if (target.isAvailable(i)) {\n return target[RH_INTERNAL_SYMBOL].values[i];\n } else {\n throw new Error(`Result ${i} is not available. It has either not been computed or you need to fetch it.`);\n }\n } else if (name === 'length') {\n return target.getLength();\n } else if (name === 'constructor') {\n return Array.constructor;\n } else if (RH_ARRAY_METHODS.includes(name)) {\n let values = target.values();\n return values[name].bind(values);\n } else if (name === RH_INTERNAL_SYMBOL) {\n return target[RH_INTERNAL_SYMBOL];\n } else {\n // only return methods on this class, don't allow access\n // to array methods other than the whitelisted ones\n let p = target.__proto__[name];\n if (typeof p === 'function') {\n return p.bind(target);\n } else {\n return p;\n }\n }\n }\n });\n }\n\n toJSON() {\n return JSON.stringify(this.values());\n }\n\n isAvailable(index) {\n return this[RH_INTERNAL_SYMBOL].valuesAvailable[index];\n }\n\n reset() {\n // quickly empty the values and valuesAvailable array\n this[RH_INTERNAL_SYMBOL].values.length\n = this[RH_INTERNAL_SYMBOL].valuesAvailable.length = 0;\n\n this[RH_INTERNAL_SYMBOL].values.length\n = this[RH_INTERNAL_SYMBOL].valuesAvailable.length\n = this[RH_INTERNAL_SYMBOL].keys.length;\n }\n\n /**\n * Returns an array of input values. Will only return input values that have a completed result available.\n * @access public\n * @returns {Array<*>}\n */\n keys() {\n // Keys can be a RangeObject, faster to iterate over valuesAvailable\n return this[RH_INTERNAL_SYMBOL].valuesAvailable.reduce((a, v, i) => {\n if (v) a.push(this[RH_INTERNAL_SYMBOL].keys[i]);\n return a;\n }, []);\n }\n\n /**\n * Returns an array of results. Will only return results that have been received from the scheduler, if only one result is complete the array will contain one value.\n * @access public\n * @returns {Array<*>}\n */\n values() {\n return this[RH_INTERNAL_SYMBOL].values.filter((v, i) => this.isAvailable(i));\n }\n\n // Aliased as length in proxy, can't use getter because Array.length is not configurable\n getLength() {\n return this[RH_INTERNAL_SYMBOL].valuesAvailable.reduce((n, v) => (n + (v ? 1 : 0)), 0);\n }\n\n [Symbol.iterator]() {\n let index = 0;\n let values = this.values(); // use available values\n\n return {\n next: () => ({\n value: values[index++],\n done: index > values.length\n })\n };\n }\n\n /** \n * Returns an array of [input, output] pairs, in sliceNumber order.\n * Return value is undefined if the input is not an ES5 primitive.\n * @access public\n * @returns {Array<*>}\n */\n entries() {\n return this[RH_INTERNAL_SYMBOL].valuesAvailable.reduce((a, v, i) => {\n if (v) a.push([\n String(this[RH_INTERNAL_SYMBOL].keys[i]),\n this[RH_INTERNAL_SYMBOL].values[i],\n ]);\n return a;\n }, []);\n }\n\n /** \n * Returns an Object associating input and output values where the inputs are ES5 primitive types.\n * Return value is undefined if the input is not an ES5 primitive.\n * @access public\n * @returns {object}\n */\n fromEntries() {\n return this.entries().reduce((o, [k, v]) => {\n o[k] = v; return o;\n }, {});\n }\n\n /** \n * Return the nth input value/input vector \n * @access public\n * @param {number} n Index in the input set to return the value for.\n * @returns {*} Input set value\n */\n key(n) {\n return this[RH_INTERNAL_SYMBOL].keys[n];\n }\n\n /** \n * Return the value corresponding to the provided key \n * @access public\n * @param {*} key Corresponds to a value in the job's input set.\n * @returns {*} Result corresponding to the input value\n */\n lookupValue(key) {\n // use keys instead of _keys so we only lookup on available results\n let ind = this.keys().indexOf(key);\n return this.values()[ind];\n }\n\n /**\n * Sends request to scheduler to fetch results, the retrieved results will be populated on this object.\n * @param {RangeObject} [rangeObject] - range object to query results\n * @param {string} [emitEvents] - set to 'all' to get a `result` event for each result as they are added to the handle.\n * @access public\n * @emits Job#resultsUpdated\n */\n async fetch(rangeObject, emitEvents) {\n const range = rangeObject && rehydrateRange(rangeObject);\n\n const job = this[RH_INTERNAL_SYMBOL].job;\n \n // Fetch any existing results\n let ks = await wallet.getId();\n const conn = new protocolV4.Connection(dcpConfig.scheduler.services.resultSubmitter.location, ks);\n\n const { success, payload } = await conn.send('fetchResult', {\n job: job.id,\n owner: job.paymentAccountKeystore.address,\n range,\n }, job.paymentAccountKeystore);\n\n // Unpack results, using fetchURI to decode/fetch result URIs\n await Promise.all(payload.map(async r => {\n if (!r) {\n console.warn(`ResultHandle.fetch: Received result was not defined (${r}), ignoring...`);\n return;\n }\n\n this[RH_INTERNAL_SYMBOL].values[r.slice] = await fetchURI(decodeURIComponent(r.value));\n this[RH_INTERNAL_SYMBOL].valuesAvailable[r.slice] = true;\n\n if (emitEvents && (emitEvents === 'all' || !this[RH_INTERNAL_SYMBOL].valuesAvailable[r.slice])) {\n job.emit('result', { sliceNumber: r.slice, result: r.result, taskId: r.task });\n }\n }));\n\n job.emit('resultsUpdated');\n }\n\n async list(rangeObject) {\n const range = rehydrateRange(rangeObject);\n throw new Error(\"ResultHandle.list not implemented\");\n }\n\n async delete(rangeObject) {\n const range = rehydrateRange(rangeObject);\n throw new Error(\"ResultHandle.delete not implemented\");\n }\n\n async stat(rangeObject) {\n const range = rehydrateRange(rangeObject);\n throw new Error(\"ResultHandle.stat not implemented\");\n }\n}\n\nObject.assign(exports, {\n ResultHandle,\n});\n\n\n//# sourceURL=webpack:///./src/dcp-client/job/result-handle.js?");
5161
+ eval("/**\n * @file job/result-handle.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date June 2020\n *\n * The ResultHandle acts as a proxy for a job's results, querying\n * internal results when available or the scheduler when the results\n * are not available locally.\n */\n\nconst { rehydrateRange } = __webpack_require__(/*! ../range-object */ \"./src/dcp-client/range-object.js\");\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 { fetchURI } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n\nconst RH_INTERNAL_SYMBOL = Symbol('Result Handle Internals');\n// Array methods that don't modify the array, will be applied to the ResultHandle's values object\nconst RH_ARRAY_METHODS = [\n 'slice', 'filter', 'concat', 'find', 'findIndex', 'indexOf', 'map', 'reduce', 'includes', 'toString', 'forEach'\n];\n\n/**\n * This class represents a handle on a job's results. It can be used for accessing the job's results, or for querying the scheduler to fetch results.\n * In addition to the properties and methods of this class, the following standard array methods are also available for accessing the available results: `slice`, `filter`, `concat`, `find`, `findIndex`, `indexOf`, `map`, `reduce`, `includes`, `toString`, and `forEach`.\n * The results can also be accessed by index (`results[i]`), but an error will be thrown if the result for that index is not yet available.\n * @access public\n */\nclass ResultHandle extends Array {\n constructor(job) {\n super();\n\n const { Job, JOB_INTERNAL_SYMBOL } = __webpack_require__(/*! . */ \"./src/dcp-client/job/index.js\");\n if (!(job instanceof Job)) {\n throw new TypeError('ResultHandle must be constructed from a Job object');\n }\n\n /**\n * The length of the available results array.\n * @type {number}\n * @access public\n */\n this.length = 0; // overridden by the proxy, here for JSDoc purposes\n\n this[RH_INTERNAL_SYMBOL] = {\n job,\n keys: job[JOB_INTERNAL_SYMBOL].payloadDetails.data,\n values: job[JOB_INTERNAL_SYMBOL].results,\n valuesAvailable: job[JOB_INTERNAL_SYMBOL].resultsAvailable,\n };\n\n return new Proxy(this, {\n get: (target, name) => {\n if ((typeof name === 'string' || typeof name === 'number') && Number.isInteger(parseFloat(name))) {\n let i = parseFloat(name);\n if (target.isAvailable(i)) {\n return target[RH_INTERNAL_SYMBOL].values[i];\n } else {\n throw new Error(`Result ${i} is not available. It has either not been computed or you need to fetch it.`);\n }\n } else if (name === 'length') {\n return target.getLength();\n } else if (name === 'constructor') {\n return Array.constructor;\n } else if (RH_ARRAY_METHODS.includes(name)) {\n let values = target.values();\n return values[name].bind(values);\n } else if (name === RH_INTERNAL_SYMBOL) {\n return target[RH_INTERNAL_SYMBOL];\n } else {\n // only return methods on this class, don't allow access\n // to array methods other than the whitelisted ones\n let p = target.__proto__[name];\n if (typeof p === 'function') {\n return p.bind(target);\n } else {\n return p;\n }\n }\n }\n });\n }\n\n toJSON() {\n return JSON.stringify(this.values());\n }\n\n isAvailable(index) {\n return this[RH_INTERNAL_SYMBOL].valuesAvailable[index];\n }\n\n reset() {\n // quickly empty the values and valuesAvailable array\n this[RH_INTERNAL_SYMBOL].values.length\n = this[RH_INTERNAL_SYMBOL].valuesAvailable.length = 0;\n\n this[RH_INTERNAL_SYMBOL].values.length\n = this[RH_INTERNAL_SYMBOL].valuesAvailable.length\n = this[RH_INTERNAL_SYMBOL].keys.length;\n }\n\n /**\n * Returns an array of input values. Will only return input values that have a completed result available.\n * @access public\n * @returns {Array<*>}\n */\n keys() {\n // Keys can be a RangeObject, faster to iterate over valuesAvailable\n return this[RH_INTERNAL_SYMBOL].valuesAvailable.reduce((a, v, i) => {\n if (v) a.push(this[RH_INTERNAL_SYMBOL].keys[i]);\n return a;\n }, []);\n }\n\n /**\n * Returns an array of results. Will only return results that have been received from the scheduler, if only one result is complete the array will contain one value.\n * @access public\n * @returns {Array<*>}\n */\n values() {\n return this[RH_INTERNAL_SYMBOL].values.filter((v, i) => this.isAvailable(i));\n }\n\n // Aliased as length in proxy, can't use getter because Array.length is not configurable\n getLength() {\n return this[RH_INTERNAL_SYMBOL].valuesAvailable.reduce((n, v) => (n + (v ? 1 : 0)), 0);\n }\n\n [Symbol.iterator]() {\n let index = 0;\n let values = this.values(); // use available values\n\n return {\n next: () => ({\n value: values[index++],\n done: index > values.length\n })\n };\n }\n\n /** \n * Returns an array of [input, output] pairs, in sliceNumber order.\n * Return value is undefined if the input is not an ES5 primitive.\n * @access public\n * @returns {Array<*>}\n */\n entries() {\n return this[RH_INTERNAL_SYMBOL].valuesAvailable.reduce((a, v, i) => {\n if (v) a.push([\n String(this[RH_INTERNAL_SYMBOL].keys[i]),\n this[RH_INTERNAL_SYMBOL].values[i],\n ]);\n return a;\n }, []);\n }\n\n /** \n * Returns an Object associating input and output values where the inputs are ES5 primitive types.\n * Return value is undefined if the input is not an ES5 primitive.\n * @access public\n * @returns {object}\n */\n fromEntries() {\n return this.entries().reduce((o, [k, v]) => {\n o[k] = v; return o;\n }, {});\n }\n\n /** \n * Return the nth input value/input vector \n * @access public\n * @param {number} n Index in the input set to return the value for.\n * @returns {*} Input set value\n */\n key(n) {\n return this[RH_INTERNAL_SYMBOL].keys[n];\n }\n\n /** \n * Return the value corresponding to the provided key \n * @access public\n * @param {*} key Corresponds to a value in the job's input set.\n * @returns {*} Result corresponding to the input value\n */\n lookupValue(key) {\n // use keys instead of _keys so we only lookup on available results\n let ind = this.keys().indexOf(key);\n return this.values()[ind];\n }\n\n /**\n * Sends request to scheduler to fetch results, the retrieved results will be populated on this object.\n * @param {RangeObject} [rangeObject] - range object to query results\n * @param {string} [emitEvents] - set to 'all' to get a `result` event for each result as they are added to the handle.\n * @access public\n * @emits Job#resultsUpdated\n */\n async fetch(rangeObject, emitEvents) {\n const range = rangeObject && rehydrateRange(rangeObject);\n\n const job = this[RH_INTERNAL_SYMBOL].job;\n \n // Fetch any existing results\n let ks = await wallet.getId();\n const conn = new protocolV4.Connection(dcpConfig.scheduler.services.resultSubmitter.location, ks);\n\n const { success, payload } = await conn.send('fetchResult', {\n job: job.id,\n owner: job.paymentAccountKeystore.address,\n range,\n }, job.paymentAccountKeystore);\n\n // Unpack results, using fetchURI to decode/fetch result URIs\n await Promise.all(payload.map(async r => {\n if (!r) {\n console.warn(`ResultHandle.fetch: Received result was not defined (${r}), ignoring...`);\n return;\n }\n\n this[RH_INTERNAL_SYMBOL].values[r.slice] = await fetchURI(decodeURIComponent(r.value));\n this[RH_INTERNAL_SYMBOL].valuesAvailable[r.slice] = true;\n\n if (emitEvents && (emitEvents === 'all' || !this[RH_INTERNAL_SYMBOL].valuesAvailable[r.slice])) {\n job.emit('result', { sliceNumber: r.slice, result: r.result, taskId: r.task });\n }\n }));\n\n job.emit('resultsUpdated');\n await conn.close();\n }\n\n async list(rangeObject) {\n const range = rehydrateRange(rangeObject);\n throw new Error(\"ResultHandle.list not implemented\");\n }\n\n async delete(rangeObject) {\n const range = rehydrateRange(rangeObject);\n throw new Error(\"ResultHandle.delete not implemented\");\n }\n\n async stat(rangeObject) {\n const range = rehydrateRange(rangeObject);\n throw new Error(\"ResultHandle.stat not implemented\");\n }\n}\n\nObject.assign(exports, {\n ResultHandle,\n});\n\n\n//# sourceURL=webpack:///./src/dcp-client/job/result-handle.js?");
5138
5162
 
5139
5163
  /***/ }),
5140
5164
 
@@ -5211,7 +5235,13 @@ eval("/* WEBPACK VAR INJECTION */(function(process) {/**\n * @file /src/sch
5211
5235
  /*! no static exports found */
5212
5236
  /***/ (function(module, exports, __webpack_require__) {
5213
5237
 
5238
+ <<<<<<< HEAD
5214
5239
  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=2a07ad83519f5b6750b47f5937cf446b64c218a8,' + 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?");
5240
+ ||||||| 412fbe6
5241
+ 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=f2e5c6725c53bade3b332a35a26ae8beb2897dcd,' + 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?");
5242
+ =======
5243
+ 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=87a7eadd2dd11f5196813e252eb61438232b50da,' + 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?");
5244
+ >>>>>>> origin/prod-20220111
5215
5245
 
5216
5246
  /***/ }),
5217
5247
 
@@ -5380,7 +5410,7 @@ eval("/**\n * @file This module implements the Worker API, used to create worker
5380
5410
  /***/ (function(module, exports, __webpack_require__) {
5381
5411
 
5382
5412
  "use strict";
5383
- 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'; } }\nclass RemoteFetchError extends SandboxError { constructor(...args) { super(...args); this.errorCode = 'EFETCH'; }}\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.onerror = this.onerror.bind(this);\n\n const messageHandler = this.onmessage.bind(this);\n this.evaluatorHandle.onmessage = function onmessage(event)\n {\n let data;\n if (event.data.serialized)\n {\n data = kvin.parse(event.data.message);\n }\n else\n {\n data = kvin.unmarshal(event.data);\n }\n messageHandler({ data });\n }\n\n const evaluatorPostMessage = this.evaluatorHandle.postMessage.bind(this.evaluatorHandle);\n this.evaluatorHandle.postMessage = function postMessage(message)\n {\n evaluatorPostMessage(scopedKvin.marshal(message));\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 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 if(err.code === 'EFETCH')\n dataError.errorCode = 'EFETCH'\n else\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('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 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\n if (err instanceof NoProgressError) {\n ceci.emit('workEmit', {\n eventName: 'noProgress',\n payload: {\n timestamp: Date.now() - ceci.sliceStartTime,\n data: ceci.slice.datumUri,\n progressReports: ceci.progressReports,\n }\n });\n }\n throw err;\n })\n .finally(function sandbox$$work$finally() {\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 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 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 }\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;\nexports.RemoteFetchError = RemoteFetchError;\n\n\n//# sourceURL=webpack:///./src/dcp-client/worker/sandbox.js?");
5413
+ 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'; } }\nclass RemoteFetchError extends SandboxError { constructor(...args) { super(...args); this.errorCode = 'EFETCH'; }}\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.onerror = this.onerror.bind(this);\n\n const messageHandler = this.onmessage.bind(this);\n this.evaluatorHandle.onmessage = function onmessage(event)\n {\n let data;\n if (event.data.serialized)\n {\n data = kvin.parse(event.data.message);\n }\n else\n {\n data = kvin.unmarshal(event.data);\n }\n messageHandler({ data });\n }\n\n const evaluatorPostMessage = this.evaluatorHandle.postMessage.bind(this.evaluatorHandle);\n this.evaluatorHandle.postMessage = function postMessage(message)\n {\n evaluatorPostMessage(scopedKvin.marshal(message));\n }\n\n const ceci = this;\n this.evaluatorHandle.addEventListener('end', () => ceci.terminate(true));\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 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 if(err.code === 'EFETCH')\n dataError.errorCode = 'EFETCH'\n else\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('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 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\n if (err instanceof NoProgressError) {\n ceci.emit('workEmit', {\n eventName: 'noProgress',\n payload: {\n timestamp: Date.now() - ceci.sliceStartTime,\n data: ceci.slice.datumUri,\n progressReports: ceci.progressReports,\n }\n });\n }\n throw err;\n })\n .finally(function sandbox$$work$finally() {\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 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 'totalCPUTime':\n this.updateTime(data);\n this.completeData.timeReport = this.sliceTimeReport\n this.ee.emit('resolve', this.completeData);\n delete this.completeData;\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.evaluatorHandle.postMessage({ request: 'resetAndGetCPUTime' })\n this.progress = 100;\n this.completeData = data;\n // The timing report and resolve will be emitted when the CPU time is received. \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 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 }\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', 'CPU', 'webGL'].forEach((key) => {\n if (measurementEvent[key]) this.sliceTimeReport[key] += measurementEvent[key];\n })\n }\n\n resetSliceTimeReport () {\n this.sliceTimeReport = {\n total: 0,\n CPU: 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;\nexports.RemoteFetchError = RemoteFetchError;\n\n\n//# sourceURL=webpack:///./src/dcp-client/worker/sandbox.js?");
5384
5414
 
5385
5415
  /***/ }),
5386
5416
 
@@ -5415,7 +5445,7 @@ eval("/**\n * @file worker/supervisor-cache.js\n *\n * A cache for the superviso
5415
5445
  /***/ (function(module, exports, __webpack_require__) {
5416
5446
 
5417
5447
  "use strict";
5418
- 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, makeDataURI, leafMerge, asleepMs, shortTime, justFetch,\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 /** @type {boolean} */\n this.doNotPrune = 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 slice = this.slices.find(s => s.jobAddress === jobAddress && s.sliceNumber === sliceNumber);\n if (slice && slice.authorizationMessage)\n return slice.authorizationMessage;\n\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 (Supervisor.debugBuild) {\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 } else { // Need else otherwise assert fires.\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\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 */\n 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 pruneSandboxes () {\n let numOver = this.sandboxes.length - (dcpConfig.worker.maxAllowedSandboxes + this.maxWorkingSandboxes);\n if (numOver <= 0) return;\n \n // Don't kill readied sandboxes while creating readied sandboxes.\n if (!this.doNotPrune) {\n for (let index = 0; index < this.readiedSandboxes.length; ) {\n const sandbox = this.readiedSandboxes[index];\n // If the sandbox is allocated, advance to the next one in the list.\n if (sandbox.allocated) {\n index++;\n continue;\n }\n // Otherwise, remove this sandbox but look at the same array index in the next loop.\n debugging() && console.log(`pruneSandboxes: Killing readied sandbox ${sandbox.id}`);\n this.readiedSandboxes.splice(index, 1);\n this.returnSandbox(sandbox);\n\n if (--numOver <= 0) break;\n }\n }\n\n if (numOver <= 0) return;\n for (let index = 0; index < this.assignedSandboxes.length; ) {\n const sandbox = this.assignedSandboxes[index];\n // If the sandbox is allocated, advance to the next one in the list.\n if (sandbox.allocated) {\n index++;\n continue;\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 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 reconnect at next watchdog interval`);\n \n this.taskDistributorConnection && this.taskDistributorConnection.close().catch(error => {\n console.error(`931: Failed to close task-distributor connection`, error);\n });\n this.resultSumitterConnection && this.resultSumitterConnection.close().catch(error => {\n console.error(`920: Failed to close result-submitter connection`, error);\n });\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 clearTimeout(fetchTimeout);\n this.taskDistributorConnection && this.taskDistributorConnection.close().catch(error => {\n console.error(`939: Failed to close task-distributor connection`, error);\n });\n this.resultSumitterConnection && this.resultSumitterConnection.close().catch(error => {\n console.error(`942: Failed to close result-submitter connection`, error);\n });\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.replace(/\\s+/g, ''); // strip whitespace\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 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 // Memoize authMessage onto the Slice object itself, this should\n // follow it for its entire life in the worker\n const slice = new Slice(taskElement);\n slice.authorizationMessage = authorizationMessage;\n\n tmpQueuedSlices.push(slice);\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 // Don't ask for more than we have.\n if (numSlices > this.queuedSlices.length)\n numSlices = this.queuedSlices.length;\n\n debugging() && console.log(`${shortTime()} matchSlicesWithSandboxes: numSlices ${numSlices}, queued slices ${this.queuedSlices.length}: assigned ${this.assignedSandboxes.length}, readied ${this.readiedSandboxes.length}, total sandbox count: ${this.sandboxes.length}`);\n\n // Auxiliary function to mark sandboxes as allocated so they cannot be pruned out from under us.\n function markAsAllocated(sandboxArray, numMarked) {\n if (numMarked > sandboxArray.length) numMarked = sandboxArray.length;\n for (let k = 1; k <= numMarked; k++) {\n assert(!sandboxArray[sandboxArray.length - k].allocated);\n sandboxArray[sandboxArray.length - k].allocated = true;\n }\n }\n\n try\n {\n this.matching = true;\n const jobAssignedMap = {};\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 if (debugging()) {\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 let counter = 0; // General counter.\n let assignedCounter = 0; // How many assigned sandboxes are being used.\n let readyCounter = 0; // How many sandboxes used from the existing this.queuedSlices.\n let newCounter = 0; // How many sandboxes that needed to be newly created.\n\n //\n // The Ideas:\n // 1) We match each slice with a sandbox, in the order that they appear in this.queuedSlices.\n // This allows us to try different orderings of execution of slices. E.g. Wes suggested\n // trying to execute slices from different jobs with maximal job diversity -- specifically\n // if there are 3 jobs j1,j2,j3, with slices s11, s12 from j1, s21, s22, s23 from j2 and\n // s31, s32 from j3, then we try to schedule, in order s11, s21, s31, s12, s22, s32, s23.\n //\n // 2) Before matching slices with sandboxes, we allocate available assigned and readied sandboxes\n // and if more are needed then we create and allocate new ones.\n //\n // 3) Finally we match slices with sandboxes and return an array of sandboxSlice pairs.\n //\n \n //\n // General description of the complexity of promise races.\n // NOTE: Supervisor.returnSandbox does not call any promises and hence does not need to be async, so that has been dropped.\n // The added benefit is that Supervisor.pruneSandboxes is also not async anymore and hence there is better safety with the\n // race between Supervisor.pruneSandboxes and Supervisor.readySandboxes. Specifically, during pruneSandboxes there are no\n // promise calls which could result in 'async readySandboxes' (which creates new 'readied' sandboxes) being called in the midst\n // of pruning 'readied' sandboxes. There is still a race in 'async matchSlicesWithSandboxes' when calling 'async readySandboxes'\n // where it is possible to interleave with 'async watchdog' which calls pruneSandboxes, but this race is prevented by setting\n // 'this.doNotPrune = true;' in 'async matchSlicesWithSandboxes' right before the call to 'async readySandboxes', because\n // 'if (!this.doNotPrune)' guards pruning 'readied' sandboxes.\n //\n\n // Associate assigned sandboxes for a given jobAddress with the corresponding count of slices owned\n // by the same jobAddress.\n for (let index = this.queuedSlices.length - 1; index >= 0; index--) {\n const slice = this.queuedSlices[index];\n assert(slice.isUnassigned);\n\n const jobAddress = slice.jobAddress.valueOf();\n let assignedState = jobAssignedMap[jobAddress];\n\n if (!assignedState) {\n // Get all assigned sandboxes for jobAddress.\n assignedState = { sandboxes: this.assignedSandboxes.filter(sb => sb.jobAddress.valueOf() === jobAddress), count: 1 };\n jobAssignedMap[jobAddress] = assignedState;\n } else {\n assignedState.count++;\n }\n\n if (++counter >= numSlices)\n break;\n }\n counter = 0;\n\n // Calculate how many new sandboxes are needed.\n for (const [jobAddress, assignedState] of Object.entries(jobAssignedMap)) {\n // Count the # of assigned sandboxes we can use.\n const assignedUsedCount = Math.min(assignedState.count, assignedState.sandboxes.length);\n assignedCounter += assignedUsedCount;\n\n // Mark the assigned sandboxes we can use as allocated, so they're not pruned out from under us.\n markAsAllocated(assignedState.sandboxes, assignedUsedCount);\n\n // Count slices w/o assigned sandbox.\n const count = Math.max(assignedState.count - assignedState.sandboxes.length, 0);\n counter += count;\n debugging() && console.log(`matchSlicesWithSandboxes: job ${jobAddress}, assigned ${assignedState.sandboxes.length}, slices ${assignedState.count}, need ${count}/${this.readiedSandboxes.length} readied sandboxes.`);\n }\n\n counter -= this.readiedSandboxes.length;\n if (counter > 0) { // Number of new sandboxes needed.\n newCounter = counter;\n readyCounter = this.readiedSandboxes.length;\n // Don't prune readied sandboxes while creating readied sandboxes -- yes, it happens...\n this.doNotPrune = true;\n try { await this.readySandboxes(newCounter); } finally { this.doNotPrune = false; }\n } else {\n readyCounter = this.readiedSandboxes.length + counter;\n }\n counter = 0;\n\n debugging() && console.log(`matchSlicesWithSandboxes: newCounter ${newCounter}, readyCounter ${readyCounter}, assignedCounter ${assignedCounter}, total readied ${this.readiedSandboxes.length}`);\n\n // Validate algorithm consistency.\n if (Supervisor.debugBuild && assignedCounter + readyCounter + newCounter !== numSlices) {\n // Structured assert.\n throw new DCPError(`matchSlicesWithSandboxes: Algorithm is corrupt ${assignedCounter} + ${readyCounter} + ${newCounter} !== ${numSlices}`);\n }\n\n // Mark the readied sandboxes we can use as allocated, so they're not pruned out from under us.\n // Note: We could run markAsAllocated before the async call to readySandboxes, which would gaurd from pruning,\n // then run it again on the brand new sandboxes just created, but it makes the code hard to read and isn't necessary,\n // beacause the doNotPrune trick seems to work. We also check if we unexpectedly run out of sandboxes (assert in debug.)\n markAsAllocated(this.readiedSandboxes, readyCounter + newCounter);\n\n while (this.queuedSlices.length > 0) {\n const slice = this.queuedSlices.pop();\n assert(slice.isUnassigned);\n\n const jobAddress = slice.jobAddress.valueOf();\n const assignedSandboxesForJob = jobAssignedMap[jobAddress].sandboxes;\n assert(assignedSandboxesForJob);\n\n if (assignedSandboxesForJob.length > 0) {\n const sandbox = assignedSandboxesForJob.pop();\n this.removeElement(this.assignedSandboxes, sandbox);\n assert(jobAddress === sandbox.jobAddress.valueOf());\n\n debugging() && console.log(`matchSlicesWithSandboxes: Assigned sandbox matched ${this.dumpSandboxAndSlice(sandbox, slice)}`);\n sandboxSlices.push({ sandbox, slice });\n } else {\n // There may be a race between creating and pruning, though it should be fixed with the doNotPrune trick.\n assert(this.readiedSandboxes.length > 0);\n if (this.readiedSandboxes.length > 0) {\n const sandbox = this.readiedSandboxes.pop();\n debugging() && console.log(`matchSlicesWithSandboxes: Readied sandbox matched ${this.dumpSandboxAndSlice(sandbox, slice)}`);\n sandboxSlices.push({ sandbox, slice });\n }\n }\n\n if (++counter >= numSlices)\n break;\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 if (debugging()) {\n console.log(`matchSlicesWithSandboxes: found ${sandboxSlices.length} sandboxes for jobs ${JSON.stringify(Object.keys(jobAssignedMap))}: assigned ${assignedCounter}, 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 } 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 * 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); // Returns Promise.\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 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 if (error.errorCode && error.errorCode === 'ESLICETOOSLOW')\n {\n const authorizationMessage = this.checkAuthorization('Supervisor.ESLICETOOSLOW:', slice.jobAddress, slice.sliceNumber);\n if (!authorizationMessage) return;\n this.resultSubmitterConnection.send('status', {\n worker: this.workerOpaqueId,\n slices: [{\n job: slice.jobAddress,\n sliceNumber: slice.sliceNumber,\n status: 'return',\n reason: 'tooslow',\n authorizationMessage\n }],\n })\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.\\n`, errorObject);\n } else if (!Supervisor.debugBuild && error.errorCode === 'EFETCH') {\n console[logLevel](`Supervisor.startSandboxWork - Could not fetch data: ${error.message}`);\n } else {\n if (Supervisor.debugBuild) // Don't show stack traces in release builds.\n errorObject.stack = error.stack;\n console[logLevel](`Supervisor.startSandboxWork - Sandbox failed: ${error.message})\\n`, 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 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 debugging('supervisor') && console.log('supervisor: recording result');\n\n // It is possible for slice.result to be undefined when there are upstream errors.\n if ( !(slice && slice.result))\n throw new Error(`recordResult: slice.result is undefined for sliceNumber ${slice.sliceNumber}, jobAdress ${slice.jobAddress}. This is ok when there are upstream errors.`);\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 const metrics = { GPUTime: 0, CPUTime: 0, CPUDensity: 0, GPUDensity: 0 };\n var payloadData = {\n slice: sliceNumber,\n job: jobAddress,\n worker: this.workerOpaqueId,\n paymentAddress: this.paymentAddress,\n metrics,\n authorizationMessage,\n }\n const timeReport = slice.result.timeReport;\n if (timeReport && timeReport.total > 0) {\n metrics.GPUTime = timeReport.webGL;\n metrics.CPUTime = timeReport.total - (metrics.GPUTime + timeReport.idle);\n metrics.CPUTime = metrics.CPUTime > 0 ? metrics.CPUTime : 0;// Prevent negative cpu;should be removed after a fix in dcp-2113\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.resultStorageType === 'pattern') { /* This is a remote-storage slice. */\n const remoteResult = await this.sendResultToRemote(slice);\n payloadData.result = encodeDataURI(JSON.stringify(remoteResult));\n } else {\n payloadData.result = encodeDataURI(slice.result.result); /* XXXwg - result.result is awful */\n }\n debugging() && console.log('Supervisor.recordResult: payloadData.result', payloadData.result.slice(0, 512));\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 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 * Send a work function's result to a server that speaks our DCP Remote Data Server protocol.\n * The data server dcp-rds is been implemented in https://gitlab.com/Distributed-Compute-Protocol/dcp-slice-service .\n *\n * @param {Slice} slice - Slice object whose result we are sending.\n * @returns {Promise<object>} - Object of the form { success: true, href: 'http://127.0.0.1:3521/methods/download/jobs/34/result/10' } .\n * @throws When HTTP status not in the 2xx range.\n */\n async sendResultToRemote(slice) {\n const postParams = {\n ...slice.resultStorageParams\n };\n\n const sliceResultUri = makeDataURI('pattern', slice.resultStorageDetails, {\n slice: slice.sliceNumber,\n job: slice.jobAddress,\n });\n\n debugging() && console.log('sendResultToRemote sliceResultUri: ', sliceResultUri);\n\n const url = new DcpURL(sliceResultUri);\n\n // Note: sendResultToRemote was made a member function of class Supervisor to enable access to this.alowedOrigins .\n if (this.allowedOrigins.indexOf(url.origin) === -1 &&\n dcpConfig.worker.allowOrigins.sendResults.indexOf(url.origin) === -1) {\n throw new Error(`Invalid origin for remote result storage: '${url.origin}'`);\n }\n\n postParams.element = slice.sliceNumber;\n postParams.contentType = 'application/json'; // Currently data will be outputed as a JSON object, @todo: Support file upload.\n\n debugging() && console.log('sendResultToRemote: postParams: ', postParams);\n\n let result = slice.result.result;\n if (result) {\n // The value being sent may be fetched with python requests so cannot be kvin serialized.\n if (result.hasOwnProperty('_serializeVerId')) {\n throw new Error('sendResultToRemote: slice.result.result must not be kvin serialized.')\n // result = require('kvin').unmarshal(result);\n }\n postParams.content = JSON.stringify(result);\n } else {\n postParams.error = JSON.stringify(slice.error);\n }\n\n debugging() && console.log('sendResultToRemote: content: ', (result ? postParams.content : postParams.error).slice(0, 512));\n\n //\n // Notes:\n // 1) In recordResults the response from justFetch is JSON serialized and encodeDataURI is called.\n // payloadData.result = await this.sendResultToRemote(slice);\n // payloadData.result = encodeDataURI(JSON.stringify(payloadData.result));\n // 2) We do further processing after the call to sendResultToRemote in recordResult, because\n // if we did it here there would be a perf hit. When the return value is a promise, it gets\n // folded into sendResultToRemote's main promise. If justFetch's promise wasn't a return value then\n // justFetch would be separately added to the micro-task-queue.\n return await justFetch(url, 'JSON', 'POST', false, postParams);\n }\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?");
5448
+ 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, makeDataURI, leafMerge, asleepMs, shortTime, justFetch,\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 /** @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 /** @type {boolean} */\n this.doNotPrune = 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 * Find the authorization message wrt jobAddress and sliceNumber.\n * @parameter {string} text\n * @parameter {opaqueId} jobAddress\n * @parameter {number} sliceNumber\n * @returns {SignedAuthorizationMessageObject}\n */\n findAuthorizationMessage(text, jobAddress, sliceNumber) {\n const slice = this.slices.find(s => s.jobAddress === jobAddress && s.sliceNumber === sliceNumber);\n if (slice && slice.authorizationMessage)\n return slice.authorizationMessage;\n else\n throw new DCPError(`Undefined authorization. ${jobAddress}, ${sliceNumber}`, 'NOAUTH')\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.findAuthorizationMessage('Supervisor.readySandboxes sandboxStart:', jobAddress, sliceNumber);\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 (Supervisor.debugBuild) {\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 } else { // Need else otherwise assert fires.\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.findAuthorizationMessage('Supervisor.readySandboxes workEmit:', jobAddress, sliceNumber);\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\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 sandboxes.push(sandbox);\n })\n .catch((err) => {\n errors.push(err);\n this.returnSandbox(sandbox);\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 }\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 */\n 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\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 pruneSandboxes () {\n let numOver = this.sandboxes.length - (dcpConfig.worker.maxAllowedSandboxes + this.maxWorkingSandboxes);\n if (numOver <= 0) return;\n \n // Don't kill readied sandboxes while creating readied sandboxes.\n if (!this.doNotPrune) {\n for (let index = 0; index < this.readiedSandboxes.length; ) {\n const sandbox = this.readiedSandboxes[index];\n // If the sandbox is allocated, advance to the next one in the list.\n if (sandbox.allocated) {\n index++;\n continue;\n }\n // Otherwise, remove this sandbox but look at the same array index in the next loop.\n debugging() && console.log(`pruneSandboxes: Killing readied sandbox ${sandbox.id}`);\n this.readiedSandboxes.splice(index, 1);\n this.returnSandbox(sandbox);\n\n if (--numOver <= 0) break;\n }\n }\n\n if (numOver <= 0) return;\n for (let index = 0; index < this.assignedSandboxes.length; ) {\n const sandbox = this.assignedSandboxes[index];\n // If the sandbox is allocated, advance to the next one in the list.\n if (sandbox.allocated) {\n index++;\n continue;\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 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.findAuthorizationMessage('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.findAuthorizationMessage('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}; isFetchingNewWork: ${this.isFetchingNewWork}`);\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 the evaluator cannot start (ie. if the evalServer is not running),\n * then the while loop will keep retrying until the evalServer comes online\n */\n while (this.maxWorkingSandboxes > this.sandboxes.length) {\n await this.readySandboxes(1)\n .then(() => this.readySandboxes(this.maxWorkingSandboxes - this.sandboxes.length))\n .catch (error => {\n console.warn(`906: failed to ready sandboxes; will retry`, error.code, error.message);\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 reconnect at next watchdog interval`);\n \n this.taskDistributorConnection && this.taskDistributorConnection.close('Fetch timed out', true).catch(error => {\n console.error(`931: Failed to close task-distributor connection`, error);\n });\n this.resultSumitterConnection && this.resultSumitterConnection.close('Fetch timed out', true).catch(error => {\n console.error(`920: Failed to close result-submitter connection`, error);\n });\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; // <-- done in the `finally` block, below\n clearTimeout(fetchTimeout);\n this.taskDistributorConnection && this.taskDistributorConnection.close('Failed to connect to result-submitter', true).catch(error => {\n console.error(`939: Failed to close task-distributor connection`, error);\n });\n this.resultSumitterConnection && this.resultSumitterConnection.close('Failed to connect to result-submitter', true).catch(error => {\n console.error(`942: Failed to close result-submitter connection`, error);\n });\n return;\n }\n await this.fetchTask(slicesToFetch).finally(() => {\n clearTimeout(fetchTimeout);\n this.isFetchingNewWork = false;\n });\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 \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.replace(/\\s+/g, ''); // strip whitespace\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 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 // Memoize authMessage onto the Slice object, this should\n // follow it for its entire life in the worker\n const slice = new Slice(taskElement);\n slice.authorizationMessage = authorizationMessage;\n\n tmpQueuedSlices.push(slice);\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 // Don't ask for more than we have.\n if (numSlices > this.queuedSlices.length)\n numSlices = this.queuedSlices.length;\n\n debugging() && console.log(`${shortTime()} matchSlicesWithSandboxes: numSlices ${numSlices}, queued slices ${this.queuedSlices.length}: assigned ${this.assignedSandboxes.length}, readied ${this.readiedSandboxes.length}, total sandbox count: ${this.sandboxes.length}`);\n\n // Auxiliary function to mark sandboxes as allocated so they cannot be pruned out from under us.\n function markAsAllocated(sandboxArray, numMarked) {\n if (numMarked > sandboxArray.length) numMarked = sandboxArray.length;\n for (let k = 1; k <= numMarked; k++) {\n assert(!sandboxArray[sandboxArray.length - k].allocated);\n sandboxArray[sandboxArray.length - k].allocated = true;\n }\n }\n\n // Filter and sandboxes that are not ready out of the `readiedSandboxes` list. Ie filter\n // out sandboxes that have been terminated before being used.\n this.readiedSandboxes = this.readiedSandboxes.filter((sandbox) => sandbox.state === 'READY_FOR_ASSIGN')\n\n try\n {\n this.matching = true;\n const jobAssignedMap = {};\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 if (debugging()) {\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 let counter = 0; // General counter.\n let assignedCounter = 0; // How many assigned sandboxes are being used.\n let readyCounter = 0; // How many sandboxes used from the existing this.queuedSlices.\n let newCounter = 0; // How many sandboxes that needed to be newly created.\n\n //\n // The Ideas:\n // 1) We match each slice with a sandbox, in the order that they appear in this.queuedSlices.\n // This allows us to try different orderings of execution of slices. E.g. Wes suggested\n // trying to execute slices from different jobs with maximal job diversity -- specifically\n // if there are 3 jobs j1,j2,j3, with slices s11, s12 from j1, s21, s22, s23 from j2 and\n // s31, s32 from j3, then we try to schedule, in order s11, s21, s31, s12, s22, s32, s23.\n //\n // 2) Before matching slices with sandboxes, we allocate available assigned and readied sandboxes\n // and if more are needed then we create and allocate new ones.\n //\n // 3) Finally we match slices with sandboxes and return an array of sandboxSlice pairs.\n //\n \n //\n // General description of the complexity of promise races.\n // NOTE: Supervisor.returnSandbox does not call any promises and hence does not need to be async, so that has been dropped.\n // The added benefit is that Supervisor.pruneSandboxes is also not async anymore and hence there is better safety with the\n // race between Supervisor.pruneSandboxes and Supervisor.readySandboxes. Specifically, during pruneSandboxes there are no\n // promise calls which could result in 'async readySandboxes' (which creates new 'readied' sandboxes) being called in the midst\n // of pruning 'readied' sandboxes. There is still a race in 'async matchSlicesWithSandboxes' when calling 'async readySandboxes'\n // where it is possible to interleave with 'async watchdog' which calls pruneSandboxes, but this race is prevented by setting\n // 'this.doNotPrune = true;' in 'async matchSlicesWithSandboxes' right before the call to 'async readySandboxes', because\n // 'if (!this.doNotPrune)' guards pruning 'readied' sandboxes.\n //\n\n // Associate assigned sandboxes for a given jobAddress with the corresponding count of slices owned\n // by the same jobAddress.\n for (let index = this.queuedSlices.length - 1; index >= 0; index--) {\n const slice = this.queuedSlices[index];\n assert(slice.isUnassigned);\n\n const jobAddress = slice.jobAddress.valueOf();\n let assignedState = jobAssignedMap[jobAddress];\n\n if (!assignedState) {\n // Get all assigned sandboxes for jobAddress.\n assignedState = { sandboxes: this.assignedSandboxes.filter(sb => sb.jobAddress.valueOf() === jobAddress), count: 1 };\n jobAssignedMap[jobAddress] = assignedState;\n } else {\n assignedState.count++;\n }\n\n if (++counter >= numSlices)\n break;\n }\n counter = 0;\n\n // Calculate how many new sandboxes are needed.\n for (const [jobAddress, assignedState] of Object.entries(jobAssignedMap)) {\n // Count the # of assigned sandboxes we can use.\n const assignedUsedCount = Math.min(assignedState.count, assignedState.sandboxes.length);\n assignedCounter += assignedUsedCount;\n\n // Mark the assigned sandboxes we can use as allocated, so they're not pruned out from under us.\n markAsAllocated(assignedState.sandboxes, assignedUsedCount);\n\n // Count slices w/o assigned sandbox.\n const count = Math.max(assignedState.count - assignedState.sandboxes.length, 0);\n counter += count;\n debugging() && console.log(`matchSlicesWithSandboxes: job ${jobAddress}, assigned ${assignedState.sandboxes.length}, slices ${assignedState.count}, need ${count}/${this.readiedSandboxes.length} readied sandboxes.`);\n }\n\n counter -= this.readiedSandboxes.length;\n if (counter > 0) { // Number of new sandboxes needed.\n newCounter = counter;\n readyCounter = this.readiedSandboxes.length;\n // Don't prune readied sandboxes while creating readied sandboxes -- yes, it happens...\n this.doNotPrune = true;\n try { await this.readySandboxes(newCounter); } finally { this.doNotPrune = false; }\n } else {\n readyCounter = this.readiedSandboxes.length + counter;\n }\n counter = 0;\n\n debugging() && console.log(`matchSlicesWithSandboxes: newCounter ${newCounter}, readyCounter ${readyCounter}, assignedCounter ${assignedCounter}, total readied ${this.readiedSandboxes.length}`);\n\n // Validate algorithm consistency.\n if (Supervisor.debugBuild && assignedCounter + readyCounter + newCounter !== numSlices) {\n // Structured assert.\n throw new DCPError(`matchSlicesWithSandboxes: Algorithm is corrupt ${assignedCounter} + ${readyCounter} + ${newCounter} !== ${numSlices}`);\n }\n\n // Mark the readied sandboxes we can use as allocated, so they're not pruned out from under us.\n // Note: We could run markAsAllocated before the async call to readySandboxes, which would gaurd from pruning,\n // then run it again on the brand new sandboxes just created, but it makes the code hard to read and isn't necessary,\n // beacause the doNotPrune trick seems to work. We also check if we unexpectedly run out of sandboxes (assert in debug.)\n markAsAllocated(this.readiedSandboxes, readyCounter + newCounter);\n\n while (this.queuedSlices.length > 0) {\n const slice = this.queuedSlices.pop();\n assert(slice.isUnassigned);\n\n const jobAddress = slice.jobAddress.valueOf();\n const assignedSandboxesForJob = jobAssignedMap[jobAddress].sandboxes;\n assert(assignedSandboxesForJob);\n\n if (assignedSandboxesForJob.length > 0) {\n const sandbox = assignedSandboxesForJob.pop();\n this.removeElement(this.assignedSandboxes, sandbox);\n assert(jobAddress === sandbox.jobAddress.valueOf());\n\n debugging() && console.log(`matchSlicesWithSandboxes: Assigned sandbox matched ${this.dumpSandboxAndSlice(sandbox, slice)}`);\n sandboxSlices.push({ sandbox, slice });\n } else {\n // There may be a race between creating and pruning, though it should be fixed with the doNotPrune trick.\n assert(this.readiedSandboxes.length > 0);\n if (this.readiedSandboxes.length > 0) {\n const sandbox = this.readiedSandboxes.pop();\n debugging() && console.log(`matchSlicesWithSandboxes: Readied sandbox matched ${this.dumpSandboxAndSlice(sandbox, slice)}`);\n sandboxSlices.push({ sandbox, slice });\n }\n }\n\n if (++counter >= numSlices)\n break;\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 if (debugging()) {\n console.log(`matchSlicesWithSandboxes: found ${sandboxSlices.length} sandboxes for jobs ${JSON.stringify(Object.keys(jobAssignedMap))}: assigned ${assignedCounter}, 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 } 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 * 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); // Returns Promise.\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 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 if (error.errorCode && error.errorCode === 'ESLICETOOSLOW')\n {\n const authorizationMessage = this.findAuthorizationMessage('Supervisor.ESLICETOOSLOW:', slice.jobAddress, slice.sliceNumber);\n\n this.resultSubmitterConnection.send('status', {\n worker: this.workerOpaqueId,\n slices: [{\n job: slice.jobAddress,\n sliceNumber: slice.sliceNumber,\n status: 'return',\n reason: 'tooslow',\n authorizationMessage\n }],\n })\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.\\n`, errorObject);\n } else if (!Supervisor.debugBuild && error.errorCode === 'EFETCH') {\n console[logLevel](`Supervisor.startSandboxWork - Could not fetch data: ${error.message}`);\n } else {\n if (Supervisor.debugBuild) // Don't show stack traces in release builds.\n errorObject.stack = error.stack;\n console[logLevel](`Supervisor.startSandboxWork - Sandbox failed: ${error.message})\\n`, errorObject);\n }\n } finally {\n sandbox.allocated = false;\n\n if (slice.result) {\n await this.recordResult(slice);\n } else {\n this.returnSlice(slice);\n }\n 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\n const jobAddress = slice.jobAddress;\n const sliceNumber = slice.sliceNumber;\n \n const authorizationMessage = this.findAuthorizationMessage('Supervisor.returnSlice:', jobAddress, sliceNumber);\n \n // Remove the slice from the slices array.\n this.removeElement(this.slices, slice);\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.findAuthorizationMessage('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 debugging('supervisor') && console.log('supervisor: recording result');\n\n // It is possible for slice.result to be undefined when there are upstream errors.\n if ( !(slice && slice.result))\n throw new Error(`recordResult: slice.result is undefined for sliceNumber ${slice.sliceNumber}, jobAdress ${slice.jobAddress}. This is ok when there are upstream errors.`);\n\n const jobAddress = slice.jobAddress;\n const sliceNumber = slice.sliceNumber;\n const authorizationMessage = this.findAuthorizationMessage('Supervisor.recordResult:', jobAddress, sliceNumber);\n\n /* @see result-submitter::result for full message details */\n const metrics = { GPUTime: 0, CPUTime: 0, CPUDensity: 0, GPUDensity: 0 };\n var payloadData = {\n slice: sliceNumber,\n job: jobAddress,\n worker: this.workerOpaqueId,\n paymentAddress: this.paymentAddress,\n metrics,\n authorizationMessage,\n }\n const timeReport = slice.result.timeReport;\n if (timeReport && timeReport.total > 0) {\n metrics.GPUTime = timeReport.webGL;\n metrics.CPUTime = timeReport.CPU;\n metrics.CPUTime = metrics.CPUTime > 0 ? metrics.CPUTime : 0;// Prevent negative cpu;should be removed after a fix in dcp-2113\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.resultStorageType === 'pattern') { /* This is a remote-storage slice. */\n const remoteResult = await this.sendResultToRemote(slice);\n payloadData.result = encodeDataURI(JSON.stringify(remoteResult));\n } else {\n payloadData.result = encodeDataURI(slice.result.result); /* XXXwg - result.result is awful */\n }\n debugging() && console.log('Supervisor.recordResult: payloadData.result', payloadData.result.slice(0, 512));\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 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 * Send a work function's result to a server that speaks our DCP Remote Data Server protocol.\n * The data server dcp-rds is been implemented in https://gitlab.com/Distributed-Compute-Protocol/dcp-slice-service .\n *\n * @param {Slice} slice - Slice object whose result we are sending.\n * @returns {Promise<object>} - Object of the form { success: true, href: 'http://127.0.0.1:3521/methods/download/jobs/34/result/10' } .\n * @throws When HTTP status not in the 2xx range.\n */\n async sendResultToRemote(slice) {\n const postParams = {\n ...slice.resultStorageParams\n };\n\n const sliceResultUri = makeDataURI('pattern', slice.resultStorageDetails, {\n slice: slice.sliceNumber,\n job: slice.jobAddress,\n });\n\n debugging() && console.log('sendResultToRemote sliceResultUri: ', sliceResultUri);\n\n const url = new DcpURL(sliceResultUri);\n\n // Note: sendResultToRemote was made a member function of class Supervisor to enable access to this.alowedOrigins .\n if (this.allowedOrigins.indexOf(url.origin) === -1 &&\n dcpConfig.worker.allowOrigins.sendResults.indexOf(url.origin) === -1) {\n throw new Error(`Invalid origin for remote result storage: '${url.origin}'`);\n }\n\n postParams.element = slice.sliceNumber;\n postParams.contentType = 'application/json'; // Currently data will be outputed as a JSON object, @todo: Support file upload.\n\n debugging() && console.log('sendResultToRemote: postParams: ', postParams);\n\n let result = slice.result.result;\n if (result) {\n // The value being sent may be fetched with python requests so cannot be kvin serialized.\n if (result.hasOwnProperty('_serializeVerId')) {\n throw new Error('sendResultToRemote: slice.result.result must not be kvin serialized.')\n // result = require('kvin').unmarshal(result);\n }\n postParams.content = JSON.stringify(result);\n } else {\n postParams.error = JSON.stringify(slice.error);\n }\n\n debugging() && console.log('sendResultToRemote: content: ', (result ? postParams.content : postParams.error).slice(0, 512));\n\n //\n // Notes:\n // 1) In recordResults the response from justFetch is JSON serialized and encodeDataURI is called.\n // payloadData.result = await this.sendResultToRemote(slice);\n // payloadData.result = encodeDataURI(JSON.stringify(payloadData.result));\n // 2) We do further processing after the call to sendResultToRemote in recordResult, because\n // if we did it here there would be a perf hit. When the return value is a promise, it gets\n // folded into sendResultToRemote's main promise. If justFetch's promise wasn't a return value then\n // justFetch would be separately added to the micro-task-queue.\n return await justFetch(url, 'JSON', 'POST', false, postParams);\n }\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?");
5419
5449
 
5420
5450
  /***/ }),
5421
5451
 
@@ -5438,7 +5468,7 @@ eval("/* WEBPACK VAR INJECTION */(function(process) {/**\n * @file debugg
5438
5468
  /*! no static exports found */
5439
5469
  /***/ (function(module, exports, __webpack_require__) {
5440
5470
 
5441
- eval("/**\n * @file events/event-subscriber.js\n * @author Ryan Rossiter <ryan@kingsds.network>\n * @date March 2020\n * \n * This file is the client-side companion to the event-router.\n * It maintains a map of subscription tokens that the event router has provisioned\n * for it, and calls the associated callbacks when the event router emits a new event.\n */\n\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('event-subscriber');\nconst { v4: uuidv4 } = __webpack_require__(/*! uuid */ \"./node_modules/uuid/dist/esm-browser/index.js\");\n\n\n/** @typedef {import('./common-types').DcpEvent} DcpEvent */\n/** @typedef {import('./common-types').SubscriptionToken} SubscriptionToken */\n/** @typedef {import('./common-types').EventLabel} EventLabel */\n/** @typedef {import('./common-types').SubscriptionOptions} SubscriptionOptions */\n\n\n/**\n * A container for callback and SubscriptionToken pairs\n * @typedef {Object} CallbackHandle\n * @property {SubscriptionToken} token\n * @property {function} callback\n */\n\n/**\n * @typedef {Object} Subscription\n * @property {EventLabel} label - Event label\n * @property {function[]} callbacks - The callbacks that will be invoked when an event is received for this subscription\n * @property {SubscriptionOptions} options - Additional options\n * @property {string} optionsStr - A string used to memoize stringifying the options object\n */\n\n\nclass EventSubscriber {\n constructor(eventEmitter) {\n this.eventEmitter = eventEmitter;\n \n this.subscriptions = new Map();\n this.pastEventIDs = new Array(1000);\n /** @type {Interval} Interval to keep the connection alive when no messages\n * are being received */\n this._keepaliveInterval = null;\n\n this.onEventRouterConnectionInterrupted = () => {\n this.openEventRouterConn();\n this.setupEventRouterConnectionEvents();\n this.reestablishSubscriptions();\n }\n\n const ceci = this;\n\n this.eventRouterConnection = null;\n this.openEventRouterConn = function openEventRouterConn()\n {\n ceci.eventRouterConnection = new protocolV4.Connection(dcpConfig.scheduler.services.eventRouter);\n ceci.eventRouterConnection.on('close', ceci.onEventRouterConnectionInterrupted);\n }\n this.openEventRouterConn();\n this.setupEventRouterConnectionEvents();\n }\n\n async close() {\n debugging() && console.log(`event-subscriber: closing EventSubscriber connection`);\n this.eventRouterConnection.off('close', this.onEventRouterConnectionInterrupted);\n await this.eventRouterConnection.close()\n .finally(() => {\n this.subscriptions.clear();\n this.setKeepalive(false);\n this.hookedEventRouterConnectionEvents = false;\n });\n }\n\n async reestablishSubscriptions() {\n let subs = this.subscriptions.entries();\n debugging() && console.log(`event-subscriber: reestablishing ${this.subscriptions.size} subscriptions`);\n\n const eventTypes = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\").eventTypes;\n const reliableEvents = [];\n const optionalEvents = [];\n for (let [label, oldToken] of subs) {\n if (oldToken.options)\n {\n // schedmsg special case\n await this.provisionSubscriptionToken(label, oldToken.options);\n }\n else if (eventTypes[label] && eventTypes[label].reliable)\n {\n reliableEvents.push(label);\n }\n else\n {\n optionalEvents.push(label);\n }\n }\n try\n {\n if (reliableEvents.length || optionalEvents.length) {\n const message = {\n reliableEvents,\n optionalEvents,\n options: this.options\n }\n\n await this.provisionSubscriptionTokens(message);\n }\n \n }\n catch (error)\n {\n debugging() && console.warn(`event-subscriber: error establishing subscription (${sub.label}):`, error);\n }\n }\n\n setupEventRouterConnectionEvents() {\n debugging() && console.log(`event-subscriber: setupEventRouterConnectionEvents`)\n this.eventRouterConnection.on('request', (req) => {\n const { operation } = req.payload;\n if (operation === 'event') this.onEventRequest(req);\n else req.respond(new Error(`Unknown EventSubscriber operation: ${operation}`));\n });\n }\n\n async onEventRequest(req) {\n debugging('verbose') && console.log(`event-subscriber: onEventRequest`);\n // data = req.payload.data.event\n const { token, event: data } = req.payload.data;\n const event = data.data;\n const eventId = data.eventId;\n debugging() && console.debug(`131: event ${eventId} (${event.eventName}):`, req.payload.data);\n if (this.subscriptions.has(event.eventName))\n {\n const { token: eventToken } = this.subscriptions.get(event.eventName);\n debugging('verbose') && console.log(`event-subscriber: event: ${event.eventName}`);\n if (eventToken !== token)\n {\n req.respond(new Error(`No subscription registered for token ${token}`));\n }\n else if (this.pastEventIDs.includes(eventId))\n {\n req.respond(new Error(`This event has already been sent: ${eventId}`));\n }\n else\n {\n const label = event.eventName;\n // on each request, we push the eventIds to keep track.\n // this will be filtered in the re-established events.\n if (this.pastEventIDs.length >= 1000)\n {\n this.pastEventIDs.shift();\n }\n this.pastEventIDs.push(eventId);\n delete event.eventName;\n if(this.eventEmitter.eventIntercepts)\n {\n if (this.eventEmitter.eventIntercepts[label])\n {\n this.eventEmitter.eventIntercepts[label](event)\n }\n else\n {\n this.eventEmitter.emit(label, event);\n }\n }\n else\n {\n this.eventEmitter.emit(label, event);\n }\n req.respond();\n }\n }\n else\n {\n console.warn(`No subscription registered for label ${event.eventName}`);\n req.respond(new Error(`No subscription registered for token ${event.eventName}`));\n }\n }\n\n /**\n * Registers this subscription with the event router, and saves the returned\n * subscription token into the this.subscriptions map.\n * @param {EventLabel} label\n * @param {SubscriptionOptions} options\n */\n async provisionSubscriptionToken(label, options) {\n debugging('verbose') && console.log(`event-subscriber: provisionSubscriptionToken(${label})`);\n const res = await this.eventRouterConnection.send('subscribe', {\n label, options,\n });\n \n const token = res.payload.token;\n this.subscriptions.set(label, { token, options });\n\n }\n\n async provisionSubscriptionTokens(message) {\n debugging('verbose') && console.log(`event-subscriber: provisionSubscriptionTokens(${message})`);\n const res = await this.eventRouterConnection.send('subscribeMany', message);\n if (!res.success)\n {\n throw new Error(`Failed to subscribe to events, ${res.payload}`);\n }\n \n const tokenEventPairs = res.payload.tokens;\n tokenEventPairs.forEach((tokenAndEvent) => {\n const label = tokenAndEvent.event;\n const token = tokenAndEvent.token;\n this.subscriptions.set(label, { token });\n })\n \n }\n\n /**\n * Unregisters a subscription from the event router\n * @param {EventLabel} label\n * @param {SubscriptionToken} token \n */\n async releaseSubscriptionToken(label, token) {\n debugging('verbose') && console.log(`event-subscriber: releaseSubscriptionToken(${label})`);\n await this.eventRouterConnection.send('unsubscribe', {\n label, token,\n });\n }\n\n /**\n * @param {EventLabel} label - Event label\n * @param {function} callback\n * @param {SubscriptionOptions} options - Additional options\n */\n async subscribeManyEvents(reliableEvents, optionalEvents, options={}) {\n this.options = options;\n const noSubReliableEvents = [];\n reliableEvents.forEach((ev) => {\n for (let [t, sub] of this.subscriptions)\n {\n // search for a subscription with identical label,\n // to avoid making duplicate subscriptions\n if (ev === sub.label)\n {\n return;\n }\n }\n if (!noSubReliableEvents.includes(ev)) {\n noSubReliableEvents.push(ev);\n }\n })\n\n const noSubOptionalEvents = [];\n optionalEvents.forEach((ev) => {\n for (let [t, sub] of this.subscriptions)\n {\n // search for a subscription with identical label,\n // to avoid making duplicate subscriptions\n if (ev === sub.label)\n {\n return;\n }\n }\n if (!noSubOptionalEvents.includes(ev)) {\n noSubOptionalEvents.push(ev);\n }\n })\n\n if (reliableEvents.length || optionalEvents.length)\n {\n const message = {\n reliableEvents: noSubReliableEvents,\n optionalEvents: noSubOptionalEvents,\n options,\n }\n debugging() && console.log('event-subscriber: provisioning:', message);\n await this.provisionSubscriptionTokens(message);\n }\n \n // Start the keepalive interval\n this.setKeepalive();\n }\n \n async subscribe(label, options={}) {\n debugging() && console.log(`event-subscriber: subscribe(${label})`);\n let token;\n for (let [subLabel, t] of this.subscriptions) {\n // search for a subscription with identical label and filter,\n // to avoid making duplicate subscriptions\n if (label === subLabel) {\n token = t;\n break;\n }\n }\n\n if (!token) {\n await this.provisionSubscriptionToken(label, options);\n }\n\n // Start the keepalive interval\n this.setKeepalive();\n }\n\n /**\n * @param {string} label\n */\n async unsubscribe(label) {\n debugging() && console.log(`event-subscriber: unsubscribe)${label})`);\n if (!this.subscriptions.has(label)) {\n throw new Error(`Unknown subscription label ${label}`);\n }\n \n const token = this.subscriptions.get(label)\n await this.releaseSubscriptionToken(label, token);\n this.subscriptions.delete(label);\n\n // If no subscriptions remain, disable the keepalive and allow the Connection\n // to expire\n if (this.subscriptions.size === 0)\n this.setKeepalive(false);\n }\n\n /**\n * De/activate an interval to send keepalives over the event-router connection\n *\n * @param {Boolean} keepalive If true, activate the interval. If false, remove it\n */\n setKeepalive(keepalive = true) {\n if (!keepalive) {\n if (this._keepaliveInterval) {\n clearInterval(this._keepaliveInterval);\n this._keepaliveInterval = null;\n }\n return;\n }\n\n if (!this._keepaliveInterval) {\n this._keepaliveInterval = setInterval(() => {\n this.eventRouterConnection.keepalive();\n }, 3 * 60 * 1000);\n }\n }\n}\n\nexports.EventSubscriber = EventSubscriber;\n\n\n//# sourceURL=webpack:///./src/events/event-subscriber.js?");
5471
+ eval("/**\n * @file events/event-subscriber.js\n * @author Ryan Rossiter <ryan@kingsds.network>\n * @date March 2020\n * \n * This file is the client-side companion to the event-router.\n * It maintains a map of subscription tokens that the event router has provisioned\n * for it, and calls the associated callbacks when the event router emits a new event.\n */\n\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('event-subscriber');\nconst { v4: uuidv4 } = __webpack_require__(/*! uuid */ \"./node_modules/uuid/dist/esm-browser/index.js\");\n\n\n/** @typedef {import('./common-types').DcpEvent} DcpEvent */\n/** @typedef {import('./common-types').SubscriptionToken} SubscriptionToken */\n/** @typedef {import('./common-types').EventLabel} EventLabel */\n/** @typedef {import('./common-types').SubscriptionOptions} SubscriptionOptions */\n\n\n/**\n * A container for callback and SubscriptionToken pairs\n * @typedef {Object} CallbackHandle\n * @property {SubscriptionToken} token\n * @property {function} callback\n */\n\n/**\n * @typedef {Object} Subscription\n * @property {EventLabel} label - Event label\n * @property {function[]} callbacks - The callbacks that will be invoked when an event is received for this subscription\n * @property {SubscriptionOptions} options - Additional options\n * @property {string} optionsStr - A string used to memoize stringifying the options object\n */\n\n\nclass EventSubscriber {\n constructor(eventEmitter) {\n this.eventEmitter = eventEmitter;\n \n this.subscriptions = new Map();\n this.pastEventIDs = new Array(1000);\n /** @type {Interval} Interval to keep the connection alive when no messages\n * are being received */\n this._keepaliveInterval = null;\n\n this.onEventRouterConnectionInterrupted = () => {\n this.openEventRouterConn();\n this.setupEventRouterConnectionEvents();\n this.reestablishSubscriptions();\n }\n\n const ceci = this;\n\n this.eventRouterConnection = null;\n this.openEventRouterConn = function openEventRouterConn()\n {\n ceci.eventRouterConnection = new protocolV4.Connection(dcpConfig.scheduler.services.eventRouter);\n ceci.eventRouterConnection.on('close', ceci.onEventRouterConnectionInterrupted);\n }\n this.openEventRouterConn();\n this.setupEventRouterConnectionEvents();\n }\n\n async close() {\n debugging() && console.log(`event-subscriber: closing EventSubscriber connection`);\n\n let subs = this.subscriptions.entries();\n for (let [label] of subs) {\n this.unsubscribe(label)\n }\n\n this.eventRouterConnection.off('close', this.onEventRouterConnectionInterrupted);\n await this.eventRouterConnection.close()\n .finally(() => {\n this.subscriptions.clear();\n this.setKeepalive(false);\n this.hookedEventRouterConnectionEvents = false;\n });\n }\n\n async reestablishSubscriptions() {\n let subs = this.subscriptions.entries();\n debugging() && console.log(`event-subscriber: reestablishing ${this.subscriptions.size} subscriptions`);\n\n const eventTypes = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\").eventTypes;\n const reliableEvents = [];\n const optionalEvents = [];\n for (let [label, oldToken] of subs) {\n if (oldToken.options)\n {\n // schedmsg special case\n await this.provisionSubscriptionToken(label, oldToken.options);\n }\n else if (eventTypes[label] && eventTypes[label].reliable)\n {\n reliableEvents.push(label);\n }\n else\n {\n optionalEvents.push(label);\n }\n }\n try\n {\n if (reliableEvents.length || optionalEvents.length) {\n const message = {\n reliableEvents,\n optionalEvents,\n options: this.options\n }\n\n await this.provisionSubscriptionTokens(message);\n }\n \n }\n catch (error)\n {\n debugging() && console.warn(`event-subscriber: error establishing subscription (${sub.label}):`, error);\n }\n }\n\n setupEventRouterConnectionEvents() {\n debugging() && console.log(`event-subscriber: setupEventRouterConnectionEvents`)\n this.eventRouterConnection.on('request', (req) => {\n const { operation } = req.payload;\n if (operation === 'event') this.onEventRequest(req);\n else req.respond(new Error(`Unknown EventSubscriber operation: ${operation}`));\n });\n }\n\n async onEventRequest(req) {\n debugging('verbose') && console.log(`event-subscriber: onEventRequest`);\n // data = req.payload.data.event\n const { token, event: data } = req.payload.data;\n const event = data.data;\n const eventId = data.eventId;\n debugging() && console.debug(`131: event ${eventId} (${event.eventName}):`, req.payload.data);\n if (this.subscriptions.has(event.eventName))\n {\n const { token: eventToken } = this.subscriptions.get(event.eventName);\n debugging('verbose') && console.log(`event-subscriber: event: ${event.eventName}`);\n if (eventToken !== token)\n {\n req.respond(new Error(`No subscription registered for token ${token}`));\n }\n else if (this.pastEventIDs.includes(eventId))\n {\n req.respond(new Error(`This event has already been sent: ${eventId}`));\n }\n else\n {\n const label = event.eventName;\n // on each request, we push the eventIds to keep track.\n // this will be filtered in the re-established events.\n if (this.pastEventIDs.length >= 1000)\n {\n this.pastEventIDs.shift();\n }\n this.pastEventIDs.push(eventId);\n delete event.eventName;\n if(this.eventEmitter.eventIntercepts)\n {\n if (this.eventEmitter.eventIntercepts[label])\n {\n this.eventEmitter.eventIntercepts[label](event)\n }\n else\n {\n this.eventEmitter.emit(label, event);\n }\n }\n else\n {\n this.eventEmitter.emit(label, event);\n }\n req.respond();\n }\n }\n else\n {\n console.warn(`No subscription registered for label ${event.eventName}`);\n req.respond(new Error(`No subscription registered for token ${event.eventName}`));\n }\n }\n\n /**\n * Registers this subscription with the event router, and saves the returned\n * subscription token into the this.subscriptions map.\n * @param {EventLabel} label\n * @param {SubscriptionOptions} options\n */\n async provisionSubscriptionToken(label, options) {\n debugging('verbose') && console.log(`event-subscriber: provisionSubscriptionToken(${label})`);\n const res = await this.eventRouterConnection.send('subscribe', {\n label, options,\n });\n \n const token = res.payload.token;\n this.subscriptions.set(label, { token, options });\n\n }\n\n async provisionSubscriptionTokens(message) {\n debugging('verbose') && console.log(`event-subscriber: provisionSubscriptionTokens(${message})`);\n const res = await this.eventRouterConnection.send('subscribeMany', message);\n if (!res.success)\n {\n throw new Error(`Failed to subscribe to events, ${res.payload}`);\n }\n \n const tokenEventPairs = res.payload.tokens;\n tokenEventPairs.forEach((tokenAndEvent) => {\n const label = tokenAndEvent.event;\n const token = tokenAndEvent.token;\n this.subscriptions.set(label, { token });\n })\n \n }\n\n /**\n * Unregisters a subscription from the event router\n * @param {EventLabel} label\n * @param {SubscriptionToken} token \n */\n async releaseSubscriptionToken(label, token) {\n debugging('verbose') && console.log(`event-subscriber: releaseSubscriptionToken(${label})`);\n await this.eventRouterConnection.send('unsubscribe', {\n label, token,\n });\n }\n\n /**\n * @param {EventLabel} label - Event label\n * @param {function} callback\n * @param {SubscriptionOptions} options - Additional options\n */\n async subscribeManyEvents(reliableEvents, optionalEvents, options={}) {\n this.options = options;\n const noSubReliableEvents = [];\n reliableEvents.forEach((ev) => {\n for (let [t, sub] of this.subscriptions)\n {\n // search for a subscription with identical label,\n // to avoid making duplicate subscriptions\n if (ev === sub.label)\n {\n return;\n }\n }\n if (!noSubReliableEvents.includes(ev)) {\n noSubReliableEvents.push(ev);\n }\n })\n\n const noSubOptionalEvents = [];\n optionalEvents.forEach((ev) => {\n for (let [t, sub] of this.subscriptions)\n {\n // search for a subscription with identical label,\n // to avoid making duplicate subscriptions\n if (ev === sub.label)\n {\n return;\n }\n }\n if (!noSubOptionalEvents.includes(ev)) {\n noSubOptionalEvents.push(ev);\n }\n })\n\n if (reliableEvents.length || optionalEvents.length)\n {\n const message = {\n reliableEvents: noSubReliableEvents,\n optionalEvents: noSubOptionalEvents,\n options,\n }\n debugging() && console.log('event-subscriber: provisioning:', message);\n await this.provisionSubscriptionTokens(message);\n }\n \n // Start the keepalive interval\n this.setKeepalive();\n }\n \n async subscribe(label, options={}) {\n debugging() && console.log(`event-subscriber: subscribe(${label})`);\n let token;\n for (let [subLabel, t] of this.subscriptions) {\n // search for a subscription with identical label and filter,\n // to avoid making duplicate subscriptions\n if (label === subLabel) {\n token = t;\n break;\n }\n }\n\n if (!token) {\n await this.provisionSubscriptionToken(label, options);\n }\n\n // Start the keepalive interval\n this.setKeepalive();\n }\n\n /**\n * @param {string} label\n */\n async unsubscribe(label) {\n debugging() && console.log(`event-subscriber: unsubscribe)${label})`);\n if (!this.subscriptions.has(label)) {\n throw new Error(`Unknown subscription label ${label}`);\n }\n \n const token = this.subscriptions.get(label)\n await this.releaseSubscriptionToken(label, token);\n this.subscriptions.delete(label);\n\n // If no subscriptions remain, disable the keepalive and allow the Connection\n // to expire\n if (this.subscriptions.size === 0)\n this.setKeepalive(false);\n }\n\n /**\n * De/activate an interval to send keepalives over the event-router connection\n *\n * @param {Boolean} keepalive If true, activate the interval. If false, remove it\n */\n setKeepalive(keepalive = true) {\n if (!keepalive) {\n if (this._keepaliveInterval) {\n clearInterval(this._keepaliveInterval);\n this._keepaliveInterval = null;\n }\n return;\n }\n\n if (!this._keepaliveInterval) {\n this._keepaliveInterval = setInterval(() => {\n this.eventRouterConnection.keepalive();\n }, 3 * 60 * 1000);\n }\n }\n}\n\nexports.EventSubscriber = EventSubscriber;\n\n\n//# sourceURL=webpack:///./src/events/event-subscriber.js?");
5442
5472
 
5443
5473
  /***/ }),
5444
5474
 
@@ -5592,7 +5622,7 @@ eval("/**\n * @file protocol/connection/response.js\n * @author Ryan
5592
5622
  /***/ (function(module, exports, __webpack_require__) {
5593
5623
 
5594
5624
  "use strict";
5595
- eval("/* WEBPACK VAR INJECTION */(function(setImmediate) {/**\n * @file protocol/connection/sender.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The sender class is responsible for accepting Connection.Message instances,\n * and sending them to the peer via the provided transport instance.\n * Messages are queued in an array, and are sent in FIFO order - with the exception\n * of requests being skipped until there is a nonce available to send them with.\n */\n\n\nconst semver = __webpack_require__(/*! semver */ \"./node_modules/semver/semver.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp');\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\n\nlet nanoid;\nif (DCP_ENV.platform === 'nodejs') {\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n nanoid = requireNative('nanoid');\n} else {\n nanoid = __webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\");\n}\n\nclass Sender {\n constructor(connection) {\n this.connection = connection;\n this.messageLedger = this.connection.messageLedger;\n\n this.queue = [];\n this.inFlight = null;\n this.nonce = null;\n this._ackTokenId = 0;\n const id = this.connection._id;\n this.debugLabel = this.connection.isInitiator? `sender(i#${id}):` : `sender(t#${id}):`;\n\n // Always re-engage sending machinery once connection is re-established\n // further: reset send props for queued/inFlight messages\n // since re-establishment resets timeouts, send count, etc.\n this.connection.state.on('change', (newState, oldState) => {\n if (oldState === 'waiting' && newState === 'established') {\n this.queue.forEach( m => m.initSendProps());\n \n if (this.inFlight) {\n this.inFlight.message.initSendProps();\n this.sendBatch();\n } else {\n this.requestQueueService();\n }\n }\n })\n }\n\n /**\n * We generate a unique token with each message we send so that \n * the peer can quickly ack the message and prove identity/uniqueness\n */\n makeAckToken() {\n return `${this.connection._id}-${this._ackTokenId++}-${nanoid()}`;\n }\n\n /**\n * Initiates the session by sending the 'connect' operation.\n * Once the response has been received, the session is established.\n * Lots of one-off code in here since it's difficult to re-use the \n * same methods meant for normal messages.\n * \n * @returns {Object} Session info\n */\n async establish() {\n assert(this.connection.transport);\n const halfSid = nanoid();\n const initiatorVersion = this.connection.constructor.VERSION;\n const connectRequest = new this.connection.Request('connect', {\n version: initiatorVersion,\n sid: halfSid,\n });\n connectRequest.id = `${this.connection._id}-connect-${nanoid()}`;\n debugging('connection') && console.debug(this.debugLabel, 'sending initial connect message');\n const resp = await this.specialFirstSend(connectRequest);\n\n if (resp.payload instanceof this.connection.ErrorPayload) {\n assert(resp.payload.type === 'protocol');\n throw new DCPError(resp.payload.message, resp.payload.code);\n }\n const targetVersion = resp.payload.version;\n const initiatorCompatibility = this.connection.constructor.VERSION_COMPATIBILITY;\n const versionCompatible = semver.satisfies(targetVersion, initiatorCompatibility);\n if (!versionCompatible) {\n throw new DCPError(`Target's version (${targetVersion}) is not compatible (must meet ${initiatorCompatibility})`, 'ETARGETVERSION');\n }\n\n // verify that our dcpsid half is still there, followed by a string\n // provided by the target that is at least the same length.\n // escape special chars:\n const regexSafeSid = halfSid.replace(/[-\\/\\\\^$*+?.()|[\\\\]{}]/g, '\\\\$&');\n const sidRegex = new RegExp(`^${regexSafeSid}.{${halfSid.length},}$`);\n if (typeof resp.dcpsid !== 'string' || !resp.dcpsid.match(sidRegex)) {\n throw new Error(`Target responded with invalid DCPSID: ${resp.dcpsid}`);\n }\n debugging('connection') && console.debug(this.debugLabel, 'connection established.');\n return {\n dcpsid: resp.dcpsid,\n peerAddress: resp.owner,\n };\n }\n \n /**\n * Perform any remaining `establish` tasks\n * now that connection is fully ready.\n */\n finishSetup() {\n this.requestQueueService();\n }\n\n /**\n * We cannot use the normal enqueue logic for the first message\n * and we need many of the same bits of logic (but not all) so\n * this is the place for that kind of one-off logic.\n * @param {Message} message \n */\n async specialFirstSend(message) { /* XXXwg - special first send *will* cause double dcpsid if invoked twice */\n message.initSendProps();\n const messageSentPromise = this.messageLedger.addMessage(message);\n await this.createAndSendBatch([message]); /* XXXwg - messageLedger may leak messages when createAndSendBatch throws */\n if (message instanceof this.connection.Response) {\n this.connection.registerConnectResponse(this.inFlight.message); /* XXXwg todo - audit this.inFlight */\n }\n return messageSentPromise;\n }\n\n /**\n * Places message into `queue` but only schedules queue to be serviced\n * if connection is ready to send. Otherwise other tools will have to\n * handle making it ready to send again.\n * @param {Connection.Request|Connection.Response} message\n * @returns {Promise} from messageLedger\n * @resolves to a response if sending a request, or resolves when the response was sent.\n */\n enqueue(message) {\n /* Note - it looks like message.type is always undefined in here, but that somehow \n * the message is mutated between enqueue and transmission. /wg Nov 2021\n */\n if (message.payload)\n debugging('connection') && console.debug(this.debugLabel, `enqueueing ${message.type} message for operation:`, message.payload && message.payload.operation);\n else\n debugging('connection') && console.debug(this.debugLabel, `enqueueing ${message.type} message`);\n \n message.initSendProps();\n this.queue.push(message);\n this.requestQueueService();\n return this.messageLedger.addMessage(message);\n }\n\n /**\n * Kick off the send loops (outer and inner) on the next event cycle.\n * This is a 'request' because if the loop was already going to run\n * anyways this method has no effect.\n */\n requestQueueService () {\n if (this.requestQueueServiceTimeout) return;\n this.requestQueueServiceTimeout = setTimeout( () => {\n this.requestQueueServiceTimeout = null;\n this.serviceQueue();\n });\n }\n\n /**\n * Creates a batch and puts it in flight if the connection is established \n * and no batch is already in flight.\n */\n async serviceQueue () {\n if (!this.inFlight && this.connection.state.in(['established', 'closing', 'close-wait'])) {\n await this.createAndSendBatch();\n } else {\n debugging('connection') && console.debug(this.debugLabel, `ignoring call to service queue. inFlight=${!!this.inFlight} state=${this.connection.state.valueOf()}`);\n }\n }\n\n /**\n * Creates a batch if it can find any messages to send and sends them.\n * If there are no messages to send, simply returns.\n * @param {Array<Connection.Message>} [messages] Array of messages to batch together for a transmission. \n * if not provided, draws from queue.\n */\n async createAndSendBatch(messages=[]) {\n if (messages.length === 0) {\n messages = this.queue.splice(0, this.batchSize);\n debugging('connection') && console.debug(\n this.debugLabel, \n `pulled ${messages.length} message${messages.length === 1? '' : 's'} from queue.`\n );\n }\n if (messages.length === 0) return;\n assert(!this.inFlight);\n\n let batch = new this.connection.Batch(messages, this.makeAckToken());\n batch.nonce = this.nonce;\n batch.initSendProps();\n let signedBatch = await batch.sign();\n this.inFlight = { message: batch, signedMessage: signedBatch };\n this.messageLedger.addBatch(batch);\n // user can modify what is in flight, so we hop off the event loop /* XXXwg very dangerous - check spec */\n this.connection.emit('send', this.inFlight);\n await new Promise(r => setImmediate(r));\n // now we're back to sending\n debugging('connection') && console.debug(this.debugLabel, `sending a signed batch of ${messages.length}`);\n this.sendBatch();\n }\n\n /**\n * Sends the message stored in the `inFlight` var over the transport.\n * Schedules a future time to send it again.\n * `clearFlightDeck` is the only method that should be clearing this timer\n * and thus closing the loop.\n */\n sendBatch() {\n assert(this.inFlight);\n let batch = this.inFlight.message;\n let signedBatch = this.inFlight.signedMessage;\n try {\n batch.updateSendProps();\n debugging('connection') && console.debug(this.debugLabel, 'sending message. Next send in (ms):', batch.msUntilNextSend);\n this.retryLoopTimeout = setTimeout(this.sendBatchConditionally.bind(this), batch.msUntilNextSend);\n this.connection.transport.send(signedBatch);\n } catch (error) {\n console.error(\"Error while signing and sending message:\", error);\n }\n }\n\n /**\n * Calls `sendBatch` if the batch we are sending has not exceeded its limits\n */\n sendBatchConditionally() {\n // Prevent \"'message' of null\" error\n if (!this.inFlight)\n return;\n let batch = this.inFlight.message;\n if (batch.overSendLimits || (batch.messages[0].payload && batch.messages[0].payload.operation === 'connect')) {\n debugging('connection') && console.debug(this.debugLabel, `Failing transport; reason: ${batch.failReason}`);\n this.connection.failTransport(`Failing transport; reason: ${batch.failReason}`);\n } else {\n this.sendBatch();\n }\n }\n\n /**\n * Clear a message from the flight deck. For the foreseeable future the deck\n * only holds one message at a time, so we just assert that it matches.\n * @param {Connection.Message} message message that can be cleared from the flight deck\n */\n clearFlightDeck(message, nonce) {\n debugging('connection') && console.debug(this.debugLabel, 'clearing flight deck. nonce =', nonce);\n assert(message === this.inFlight.message);\n this.inFlight = null;\n this.nonce = nonce;\n clearTimeout(this.retryLoopTimeout);\n this.requestQueueService();\n }\n\n /**\n * When a connection is closed the sender needs to cancel its send efforts.\n */\n shutdown() {\n debugging('connection') && console.debug(this.debugLabel, 'shutting down.');\n this.inFlight = null;\n this.nonce = null;\n this.queue = [];\n clearTimeout(this.requestQueueServiceTimeout);\n clearTimeout(this.retryLoopTimeout);\n }\n\n // When allowBatch=false, set the batchSize to 1 so each\n // message gets sent individually (not in a batch)\n get batchSize() {\n if (this._batchSize) return this._batchSize;\n const batchSize = Math.max(this.connection.connectionOptions.maxMessagesPerBatch, 1);\n return this._batchSize = this.connection.connectionOptions.allowBatch ? batchSize : 1;\n }\n}\n\nObject.assign(module.exports, {\n Sender,\n});\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../../node_modules/timers-browserify/main.js */ \"./node_modules/timers-browserify/main.js\").setImmediate))\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/sender.js?");
5625
+ eval("/* WEBPACK VAR INJECTION */(function(setImmediate) {/**\n * @file protocol/connection/sender.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The sender class is responsible for accepting Connection.Message instances,\n * and sending them to the peer via the provided transport instance.\n * Messages are queued in an array, and are sent in FIFO order - with the exception\n * of requests being skipped until there is a nonce available to send them with.\n */\n\n\nconst semver = __webpack_require__(/*! semver */ \"./node_modules/semver/semver.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp');\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\n\nlet nanoid;\nif (DCP_ENV.platform === 'nodejs') {\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n nanoid = requireNative('nanoid');\n} else {\n nanoid = __webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\");\n}\n\nclass Sender {\n constructor(connection) {\n this.connection = connection;\n this.messageLedger = this.connection.messageLedger;\n\n this.queue = [];\n this.inFlight = null;\n this.nonce = null;\n this._ackTokenId = 0;\n const id = this.connection._id;\n this.debugLabel = this.connection.isInitiator? `sender(i#${id}):` : `sender(t#${id}):`;\n\n // Always re-engage sending machinery once connection is re-established\n // further: reset send props for queued/inFlight messages\n // since re-establishment resets timeouts, send count, etc.\n this.connection.state.on('change', (newState, oldState) => {\n if (oldState === 'waiting' && newState === 'established') {\n this.queue.forEach( m => m.initSendProps());\n \n if (this.inFlight) {\n this.inFlight.message.initSendProps();\n this.sendBatch();\n } else {\n this.requestQueueService();\n }\n }\n })\n }\n\n /**\n * We generate a unique token with each message we send so that \n * the peer can quickly ack the message and prove identity/uniqueness\n */\n makeAckToken() {\n return `${this.connection._id}-${this._ackTokenId++}-${nanoid()}`;\n }\n\n /**\n * Initiates the session by sending the 'connect' operation.\n * Once the response has been received, the session is established.\n * Lots of one-off code in here since it's difficult to re-use the \n * same methods meant for normal messages.\n * \n * @returns {Object} Session info\n */\n async establish() {\n assert(this.connection.transport);\n const halfSid = nanoid();\n const initiatorVersion = this.connection.constructor.VERSION;\n const connectRequest = new this.connection.Request('connect', {\n version: initiatorVersion,\n sid: halfSid,\n });\n connectRequest.id = `${this.connection._id}-connect-${nanoid()}`;\n debugging('connection') && console.debug(this.debugLabel, 'sending initial connect message');\n const resp = await this.specialFirstSend(connectRequest);\n\n if (resp.payload instanceof this.connection.ErrorPayload) {\n assert(resp.payload.type === 'protocol');\n throw new DCPError(resp.payload.message, resp.payload.code);\n }\n const targetVersion = resp.payload.version;\n const initiatorCompatibility = this.connection.constructor.VERSION_COMPATIBILITY;\n const versionCompatible = semver.satisfies(targetVersion, initiatorCompatibility);\n if (!versionCompatible) {\n throw new DCPError(`Target's version (${targetVersion}) is not compatible (must meet ${initiatorCompatibility})`, 'ETARGETVERSION');\n }\n\n // verify that our dcpsid half is still there, followed by a string\n // provided by the target that is at least the same length.\n // escape special chars:\n const regexSafeSid = halfSid.replace(/[-\\/\\\\^$*+?.()|[\\\\]{}]/g, '\\\\$&');\n const sidRegex = new RegExp(`^${regexSafeSid}.{${halfSid.length},}$`);\n if (typeof resp.dcpsid !== 'string' || !resp.dcpsid.match(sidRegex)) {\n throw new Error(`Target responded with invalid DCPSID: ${resp.dcpsid}`);\n }\n debugging('connection') && console.debug(this.debugLabel, 'connection established.');\n return {\n dcpsid: resp.dcpsid,\n peerAddress: resp.owner,\n };\n }\n \n /**\n * Perform any remaining `establish` tasks\n * now that connection is fully ready.\n */\n finishSetup() {\n this.requestQueueService();\n }\n\n /**\n * We cannot use the normal enqueue logic for the first message\n * and we need many of the same bits of logic (but not all) so\n * this is the place for that kind of one-off logic.\n * @param {Message} message \n */\n async specialFirstSend(message) { /* XXXwg - special first send *will* cause double dcpsid if invoked twice */\n message.initSendProps();\n const messageSentPromise = this.messageLedger.addMessage(message);\n await this.createAndSendBatch([message]); /* XXXwg - messageLedger may leak messages when createAndSendBatch throws */\n if (message instanceof this.connection.Response) {\n this.connection.registerConnectResponse(this.inFlight.message); /* XXXwg todo - audit this.inFlight */\n }\n return messageSentPromise;\n }\n\n /**\n * Places message into `queue` but only schedules queue to be serviced\n * if connection is ready to send. Otherwise other tools will have to\n * handle making it ready to send again.\n * @param {Connection.Request|Connection.Response} message\n * @returns {Promise} from messageLedger\n * @resolves to a response if sending a request, or resolves when the response was sent.\n */\n enqueue(message) {\n /* Note - it looks like message.type is always undefined in here, but that somehow \n * the message is mutated between enqueue and transmission. /wg Nov 2021\n */\n if (message.payload)\n debugging('connection') && console.debug(this.debugLabel, `enqueueing ${message.type} message for operation:`, message.payload && message.payload.operation);\n else\n debugging('connection') && console.debug(this.debugLabel, `enqueueing ${message.type} message`);\n \n message.initSendProps();\n this.queue.push(message);\n this.requestQueueService();\n return this.messageLedger.addMessage(message);\n }\n\n /**\n * Kick off the send loops (outer and inner) on the next event cycle.\n * This is a 'request' because if the loop was already going to run\n * anyways this method has no effect.\n */\n requestQueueService () {\n if (this.requestQueueServiceTimeout) return;\n this.requestQueueServiceTimeout = setTimeout( () => {\n this.requestQueueServiceTimeout = null;\n this.serviceQueue();\n });\n }\n\n /**\n * Creates a batch and puts it in flight if the connection is established \n * and no batch is already in flight.\n */\n async serviceQueue () {\n if (!this.inFlight && this.connection.state.in(['established', 'closing', 'close-wait'])) {\n await this.createAndSendBatch();\n } else {\n debugging('connection') && console.debug(this.debugLabel, `ignoring call to service queue. inFlight=${!!this.inFlight} state=${this.connection.state.valueOf()}`);\n }\n }\n\n /**\n * Creates a batch if it can find any messages to send and sends them.\n * If there are no messages to send, simply returns.\n * @param {Array<Connection.Message>} [messages] Array of messages to batch together for a transmission. \n * if not provided, draws from queue.\n */\n async createAndSendBatch(messages=[]) {\n if (messages.length === 0) {\n messages = this.queue.splice(0, this.batchSize);\n debugging('connection') && console.debug(\n this.debugLabel, \n `pulled ${messages.length} message${messages.length === 1? '' : 's'} from queue.`\n );\n }\n if (messages.length === 0) return;\n assert(!this.inFlight);\n\n let batch = new this.connection.Batch(messages, this.makeAckToken());\n batch.nonce = this.nonce;\n batch.initSendProps();\n let signedBatch = await batch.sign();\n this.inFlight = { message: batch, signedMessage: signedBatch };\n this.messageLedger.addBatch(batch);\n // user can modify what is in flight, so we hop off the event loop /* XXXwg very dangerous - check spec */\n this.connection.emit('send', this.inFlight);\n await new Promise(r => setImmediate(r));\n // Connection may start shutting down while awaiting and sets this.inFlight=null\n if(!this.inFlight) return;\n // now we're back to sending\n debugging('connection') && console.debug(this.debugLabel, `sending a signed batch of ${messages.length}`);\n this.sendBatch();\n }\n\n /**\n * Sends the message stored in the `inFlight` var over the transport.\n * Schedules a future time to send it again.\n * `clearFlightDeck` is the only method that should be clearing this timer\n * and thus closing the loop.\n */\n sendBatch() {\n assert(this.inFlight);\n let batch = this.inFlight.message;\n let signedBatch = this.inFlight.signedMessage;\n try {\n batch.updateSendProps();\n debugging('connection') && console.debug(this.debugLabel, 'sending message. Next send in (ms):', batch.msUntilNextSend);\n this.retryLoopTimeout = setTimeout(this.sendBatchConditionally.bind(this), batch.msUntilNextSend);\n this.connection.transport.send(signedBatch);\n } catch (error) {\n console.error(`Error while signing and sending message to ${this.connection.url}:`, error);\n }\n }\n\n /**\n * Calls `sendBatch` if the batch we are sending has not exceeded its limits\n */\n sendBatchConditionally() {\n // Prevent \"'message' of null\" error\n if (!this.inFlight)\n return;\n let batch = this.inFlight.message;\n if (batch.overSendLimits || (batch.messages[0].payload && batch.messages[0].payload.operation === 'connect')) {\n debugging('connection') && console.debug(this.debugLabel, `Failing transport; reason: ${batch.failReason}`);\n this.connection.failTransport(`Failing transport; reason: ${batch.failReason}`);\n } else {\n this.sendBatch();\n }\n }\n\n /**\n * Clear a message from the flight deck. For the foreseeable future the deck\n * only holds one message at a time, so we just assert that it matches.\n * @param {Connection.Message} message message that can be cleared from the flight deck\n */\n clearFlightDeck(message, nonce) {\n debugging('connection') && console.debug(this.debugLabel, 'clearing flight deck. nonce =', nonce);\n assert(message === this.inFlight.message);\n this.inFlight = null;\n this.nonce = nonce;\n clearTimeout(this.retryLoopTimeout);\n this.requestQueueService();\n }\n\n /**\n * When a connection is closed the sender needs to cancel its send efforts.\n */\n shutdown() {\n debugging('connection') && console.debug(this.debugLabel, 'shutting down.');\n this.inFlight = null;\n this.nonce = null;\n this.queue = [];\n clearTimeout(this.requestQueueServiceTimeout);\n clearTimeout(this.retryLoopTimeout);\n }\n\n // When allowBatch=false, set the batchSize to 1 so each\n // message gets sent individually (not in a batch)\n get batchSize() {\n if (this._batchSize) return this._batchSize;\n const batchSize = Math.max(this.connection.connectionOptions.maxMessagesPerBatch, 1);\n return this._batchSize = this.connection.connectionOptions.allowBatch ? batchSize : 1;\n }\n}\n\nObject.assign(module.exports, {\n Sender,\n});\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../../node_modules/timers-browserify/main.js */ \"./node_modules/timers-browserify/main.js\").setImmediate))\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/sender.js?");
5596
5626
 
5597
5627
  /***/ }),
5598
5628
 
@@ -5662,7 +5692,7 @@ eval("/**\n * @file protocol/transport/index.js\n * @author Ryan Ros
5662
5692
  /***/ (function(module, exports, __webpack_require__) {
5663
5693
 
5664
5694
  "use strict";
5665
- eval("/**\n * @file protocol/transport/websocket.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * This module implements the SocketIO Transport that is\n * used by the protocol to connect and communicate with peers using\n * SocketIO connections.\n * \n * We listen to some socketio events for our own debugging but mostly use\n * our own mechanisms for detecting a bad transport and retrying. These are\n * independent of SocketIO's efforts to reconnect, find failures and so on.\n */\n\n\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp');\nconst { leafMerge } = __webpack_require__(/*! dcp/utils/obj-merge */ \"./src/utils/obj-merge.js\");\nconst socketio = __webpack_require__(/*! socket.io-client */ \"./node_modules/socket.io-client/build/index.js\");\nconst { Transport } = __webpack_require__(/*! . */ \"./src/protocol-v4/transport/index.js\");\nvar failToConnectTimeout;\nvar failToReconnectTimeout;\n\n// How should we retry sending messages over a socket? \n// (this ignore socketio's own efforts to maintain its own connections)\n// Times in seconds to match dcpConfig standard\nconst DEFAULT_EBO_CONFIG = { \n slot: 10,\n max: 600,\n};\n\nclass SocketIOTransport extends Transport {\n /**\n * \n * @param {Socket} [socket] Target has a socket in hand, so we use it directly, \n * otherwise initiator is excepected to call `connect` when ready.\n * @param {object} [connectionOptions] A protocol-v4 connectionOptions object, built from a\n * combination of defaults and various dcpConfig components.\n * Socket.io-specific settings are in the socketio property; \n * generic properties may also be used by this library.\n */\n constructor(socket, connectionOptions={}) {\n super(\"Protocol SocketIO Transport\");\n this.socket = socket;\n // helps targets figure out which transport they are working with;\n this.name = 'socketio';\n this.shouldClose = false;\n\n\n if (this.socket) {\n this.debugLabel = 'socketio(t):';\n this.socket.debugInfo = { target: true };\n if (this.socket.handshake && this.socket.handshake.url)\n this.socket.debugInfo.urlLabel = this.socket.handshake.url;\n else\n this.socket.debugInfo.urlLabel = '<target>-' + this.socket.id;\n this.init();\n } else {\n this.debugLabel = 'socketio(i):';\n }\n\n if (connectionOptions.id)\n this.debugLabel = this.debugLabel.replace('):', `#${connectionOptions.id}):`);\n\n let config = leafMerge(\n { ebo: DEFAULT_EBO_CONFIG },\n connectionOptions.socketio\n )\n\n super.initConfig(config);\n }\n\n /**\n * Connect socket's message event to our own event emitter. Listen to other socket.io \n * events for debugging.\n */\n init() {\n debugging('socketio') && console.debug(this.debugLabel, `initializing socketio connection for ${this.socket.debugInfo.urlLabel}`);\n \n this.socket.on('message', (msg) => {\n this.emit('message', msg);\n });\n \n // be noisy about internal efforts to reconnect, but don't do anything about them\n this.socket.on('disconnect', (reason) => {\n const maxReconnectWait = this.eboConfig.max * 800;\n\n debugging('socketio') && console.debug(this.debugLabel, `disconnected from ${(this.socket.debugInfo && this.socket.debugInfo.urlLabel) || '<new>'}; reason=${reason}`);\n if (reason === 'io server disconnect') {\n // the disconnection was initiated by the server, you need to reconnect manually\n this.socket.connect();\n }\n // else the socket will automatically try to reconnect\n\n if (!this.shouldClose) {\n var finished = false;\n \n debugging('socketio') && console.debug(this.debugLabel, `waiting up to ${maxReconnectWait/1000}s for reconnection...`);\n /**\n * Hold the nodejs process open for as long as socket.io will keep\n * trying to reconnect. Without it, the process exits before reconnect\n * can happen if all open connections to the server have died\n * unexpectedly (e.g. in a full platform restart).\n */\n failToReconnectTimeout = setTimeout(() => {\n if (finished)\n return;\n finished = true;\n this.emit('connect-failed');\n debugging('socketio') && console.debug(this.debugLabel, `failed to reconnect to ${this.socket.debugInfo.urlLabel}`);\n }, maxReconnectWait);\n\n this.socket.once('connect', () => {\n if (finished)\n return;\n finished = true;\n clearTimeout(failToReconnectTimeout);\n debugging('socketio') && console.debug(this.debugLabel, `reconnected to ${this.socket.debugInfo.urlLabel}`);\n this.emit('reconnect');\n });\n }\n });\n this.socket.on('connect_error', () => {\n clearTimeout(failToConnectTimeout);\n clearTimeout(failToReconnectTimeout);\n this.emit('connect-failed');\n });\n }\n\n /** \n * Initiate a connection with socket.io to the given target.\n * @param {object} url The URL of the target\n * @param {object} connectionOptions\n * @returns {number} time until connection should consider that this connect attempt has failed.\n */\n connect(url, connectionOptions={}) {\n var finished = false;\n\n if (this.shouldClose)\n throw new Error('Cannot connect to SocketIOTransport instance which has already closed.');\n\n debugging('socketio') && console.debug(this.debugLabel, 'connecting to', url && url.href);\n this.validateConnectionAttempt(url);\n\n // only autoUnref if it is possible/sane to do so (ie, in nodejs):\n const autoUnref = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").platform === 'nodejs';\n this.socket = socketio(url.origin, { path: url.pathname, autoUnref });\n this.socket.debugInfo = { urlLabel: url.href };\n this.init();\n\n const maxConnectWait = this.eboConfig.max * 1000;\n failToConnectTimeout = setTimeout(() => {\n if (finished)\n return;\n finished = true;\n this.emit('connect-failed');\n }, maxConnectWait);\n \n this.socket.once('connect', () => {\n if (finished)\n return;\n finished = true;\n clearTimeout(failToConnectTimeout);\n debugging('socketio') && console.debug(this.debugLabel, 'connected to', url.href);\n this.emit('connected');\n });\n\n return maxConnectWait + 100; // give transport a chance to fail itself\n }\n\n /** \n * Close the current instance of Socket. First we adjust the instance so that a subsequent \n * use of `send` or `open` on this instance will fast-fail, even if we throw during close.\n * Then we clean up our data structures as needed.\n */\n close()\n {\n let socket = this.socket;\n \n this.socket = false;\n this.shouldClose = true; /* Mark this so that we don't try to reanimate the corpse */\n clearTimeout(failToReconnectTimeout);\n \n if (!socket) {\n debugging() && console.debug('closing socketio transport instance that was never used');\n return;\n }\n\n socket.disconnect();\n if (!socket.disconnected) {\n throw new Error(\"Failed to close SocketIOTransport\");\n }\n }\n\n send(message) {\n debugging('socketio') && console.debug(this.debugLabel, 'sending message', message);\n\n if (!this.socket) {\n throw new Error(\"SocketIOTransport.send: Not connected\");\n }\n \n // add special logic for detecting a special message which cannot be sent.\n if (this.debugMode && message.includes(\"__debug__: disconnect\")) {\n if (!this.debugCount) this.debugCount = 1;\n this.debugCount++;\n return;\n }\n\n this.socket.send(message);\n }\n\n validateConnectionAttempt(url) {\n if (this.socket) {\n if (this.socket.connected) {\n debugging('socketio') && console.debug(this.debugLabel, 'already connected');\n throw new Error(\"SocketIOTransport.connect: Already connected\");\n } else {\n debugging('socketio') && console.debug(this.debugLabel, 'already connected but closed');\n throw new Error(\"SocketIOTransport.connect: Socket was connected and closed\");\n }\n } else {\n if (!url)\n throw new Error(\"SocketIOTransport.connect: URL must be provided\");\n }\n }\n}\n\nexports.TransportClass = SocketIOTransport;\n\n\n//# sourceURL=webpack:///./src/protocol-v4/transport/socketio.js?");
5695
+ eval("/**\n * @file protocol/transport/websocket.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * This module implements the SocketIO Transport that is\n * used by the protocol to connect and communicate with peers using\n * SocketIO connections.\n * \n * We listen to some socketio events for our own debugging but mostly use\n * our own mechanisms for detecting a bad transport and retrying. These are\n * independent of SocketIO's efforts to reconnect, find failures and so on.\n */\n\n\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp');\nconst { leafMerge } = __webpack_require__(/*! dcp/utils/obj-merge */ \"./src/utils/obj-merge.js\");\nconst socketio = __webpack_require__(/*! socket.io-client */ \"./node_modules/socket.io-client/build/index.js\");\nconst { Transport } = __webpack_require__(/*! . */ \"./src/protocol-v4/transport/index.js\");\n\n// How should we retry sending messages over a socket? \n// (this ignore socketio's own efforts to maintain its own connections)\n// Times in seconds to match dcpConfig standard\nconst DEFAULT_EBO_CONFIG = { \n slot: 10,\n max: 600,\n};\n\nclass SocketIOTransport extends Transport {\n /**\n * \n * @param {Socket} [socket] Target has a socket in hand, so we use it directly, \n * otherwise initiator is excepected to call `connect` when ready.\n * @param {object} [connectionOptions] A protocol-v4 connectionOptions object, built from a\n * combination of defaults and various dcpConfig components.\n * Socket.io-specific settings are in the socketio property; \n * generic properties may also be used by this library.\n */\n constructor(socket, connectionOptions={}) {\n super(\"Protocol SocketIO Transport\");\n this.socket = socket;\n // helps targets figure out which transport they are working with;\n this.name = 'socketio';\n this.shouldClose = false;\n this.failToConnectTimeout = null;\n this.failToReconnectTimeout = null;\n\n if (this.socket) {\n this.debugLabel = 'socketio(t):';\n this.socket.debugInfo = { target: true };\n if (this.socket.handshake && this.socket.handshake.url)\n this.socket.debugInfo.urlLabel = this.socket.handshake.url;\n else\n this.socket.debugInfo.urlLabel = '<target>-' + this.socket.id;\n this.init();\n } else {\n this.debugLabel = 'socketio(i):';\n }\n\n if (connectionOptions.id)\n this.debugLabel = this.debugLabel.replace('):', `#${connectionOptions.id}):`);\n\n let config = leafMerge(\n { ebo: DEFAULT_EBO_CONFIG },\n connectionOptions.socketio\n )\n\n super.initConfig(config);\n }\n\n /**\n * Connect socket's message event to our own event emitter. Listen to other socket.io \n * events for debugging.\n */\n init() {\n debugging('socketio') && console.debug(this.debugLabel, `initializing socketio connection for ${this.socket.debugInfo.urlLabel}`);\n \n this.socket.on('message', (msg) => {\n this.emit('message', msg);\n });\n \n // be noisy about internal efforts to reconnect, but don't do anything about them\n this.socket.on('disconnect', (reason) => {\n const maxReconnectWait = this.eboConfig.max * 800;\n\n debugging('socketio') && console.debug(this.debugLabel, `disconnected from ${(this.socket.debugInfo && this.socket.debugInfo.urlLabel) || '<new>'}; reason=${reason}`);\n if (reason === 'io server disconnect') {\n // the disconnection was initiated by the server, you need to reconnect manually\n this.socket.connect();\n }\n // else the socket will automatically try to reconnect\n\n if (!this.shouldClose) {\n var finished = false;\n \n debugging('socketio') && console.debug(this.debugLabel, `waiting up to ${maxReconnectWait/1000}s for reconnection...`);\n /**\n * Hold the nodejs process open for as long as socket.io will keep\n * trying to reconnect. Without it, the process exits before reconnect\n * can happen if all open connections to the server have died\n * unexpectedly (e.g. in a full platform restart).\n */\n this.failToReconnectTimeout = setTimeout(() => {\n if (finished)\n return;\n finished = true;\n this.emit('connect-failed');\n debugging('socketio') && console.debug(this.debugLabel, `failed to reconnect to ${this.socket.debugInfo.urlLabel}`);\n }, maxReconnectWait);\n\n this.socket.once('connect', () => {\n if (finished)\n return;\n finished = true;\n clearTimeout(this.failToReconnectTimeout);\n debugging('socketio') && console.debug(this.debugLabel, `reconnected to ${this.socket.debugInfo.urlLabel}`);\n this.emit('reconnect');\n });\n }\n });\n this.socket.on('connect_error', () => {\n clearTimeout(this.failToConnectTimeout);\n clearTimeout(this.failToReconnectTimeout);\n this.emit('connect-failed');\n });\n }\n\n /** \n * Initiate a connection with socket.io to the given target.\n * @param {object} url The URL of the target\n * @param {object} connectionOptions\n * @returns {number} time until connection should consider that this connect attempt has failed.\n */\n connect(url, connectionOptions={}) {\n var finished = false;\n\n if (this.shouldClose)\n throw new Error('Cannot connect to SocketIOTransport instance which has already closed.');\n\n debugging('socketio') && console.debug(this.debugLabel, 'connecting to', url && url.href);\n this.validateConnectionAttempt(url);\n\n // only autoUnref if it is possible/sane to do so (ie, in nodejs):\n const autoUnref = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").platform === 'nodejs';\n this.socket = socketio(url.origin, { path: url.pathname, autoUnref });\n this.socket.debugInfo = { urlLabel: url.href };\n this.init();\n\n const maxConnectWait = this.eboConfig.max * 1000;\n this.failToConnectTimeout = setTimeout(() => {\n if (finished)\n return;\n finished = true;\n this.emit('connect-failed');\n }, maxConnectWait);\n \n this.socket.once('connect', () => {\n if (finished)\n return;\n finished = true;\n clearTimeout(this.failToConnectTimeout);\n debugging('socketio') && console.debug(this.debugLabel, 'connected to', url.href);\n this.emit('connected');\n });\n\n return maxConnectWait + 100; // give transport a chance to fail itself\n }\n\n /** \n * Close the current instance of Socket. First we adjust the instance so that a subsequent \n * use of `send` or `open` on this instance will fast-fail, even if we throw during close.\n * Then we clean up our data structures as needed.\n */\n close()\n {\n let socket = this.socket;\n \n this.socket = false;\n this.shouldClose = true; /* Mark this so that we don't try to reanimate the corpse */\n clearTimeout(this.failToReconnectTimeout);\n \n if (!socket) {\n debugging() && console.debug('closing socketio transport instance that was never used');\n return;\n }\n\n socket.disconnect();\n if (!socket.disconnected) {\n throw new Error(\"Failed to close SocketIOTransport\");\n }\n }\n\n send(message) {\n debugging('socketio') && console.debug(this.debugLabel, 'sending message', message);\n\n if (!this.socket) {\n throw new Error(\"SocketIOTransport.send: Not connected\");\n }\n \n // add special logic for detecting a special message which cannot be sent.\n if (this.debugMode && message.includes(\"__debug__: disconnect\")) {\n if (!this.debugCount) this.debugCount = 1;\n this.debugCount++;\n return;\n }\n\n this.socket.send(message);\n }\n\n validateConnectionAttempt(url) {\n if (this.socket) {\n if (this.socket.connected) {\n debugging('socketio') && console.debug(this.debugLabel, 'already connected');\n throw new Error(\"SocketIOTransport.connect: Already connected\");\n } else {\n debugging('socketio') && console.debug(this.debugLabel, 'already connected but closed');\n throw new Error(\"SocketIOTransport.connect: Socket was connected and closed\");\n }\n } else {\n if (!url)\n throw new Error(\"SocketIOTransport.connect: URL must be provided\");\n }\n }\n}\n\nexports.TransportClass = SocketIOTransport;\n\n\n//# sourceURL=webpack:///./src/protocol-v4/transport/socketio.js?");
5666
5696
 
5667
5697
  /***/ }),
5668
5698