dcp-client 4.1.13 → 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
- eval("module.exports = JSON.parse(\"{\\\"dcp_root\\\":\\\"/home/erose/git/release/dcp/dist\\\",\\\"generated\\\":\\\"Fri 22 Oct 2021 04:01:16 PM EDT by erose on dione\\\",\\\"build\\\":\\\"release\\\",\\\"install\\\":{\\\"generated\\\":\\\"Fri 22 Oct 2021 04:01:16 PM EDT by erose on dione\\\",\\\"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?");
99
+ <<<<<<< HEAD
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\\\",\\\"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\\\",\\\"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\\\",\\\"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\\\",\\\"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\\\",\\\"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\\\",\\\"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
 
@@ -4737,6 +4743,24 @@ eval("\n\nvar alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr
4737
4743
 
4738
4744
  /***/ }),
4739
4745
 
4746
+ /***/ "./portal/www/js/modal.js":
4747
+ /*!********************************!*\
4748
+ !*** ./portal/www/js/modal.js ***!
4749
+ \********************************/
4750
+ /*! exports provided: default */
4751
+ /***/ (function(module, __webpack_exports__, __webpack_require__) {
4752
+
4753
+ "use strict";
4754
+ <<<<<<< HEAD
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
4761
+
4762
+ /***/ }),
4763
+
4740
4764
  /***/ "./src/common/cli.js":
4741
4765
  /*!***************************!*\
4742
4766
  !*** ./src/common/cli.js ***!
@@ -4855,7 +4879,7 @@ eval("/* WEBPACK VAR INJECTION */(function(setImmediate) {/**\n * @author W
4855
4879
  /*! no static exports found */
4856
4880
  /***/ (function(module, exports, __webpack_require__) {
4857
4881
 
4858
- eval("/**\n * @file event-interceptor.js\n * Implement a class which allows to add extra functions which fire before or after the\n * other functions in a Node.js EventEmitter.\n * \n * @author Wes Garland, wes@kingsds.network\n * @date July 2021\n *\n * @note Currently only works with Node.js EventEmitters!\n */\n\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('events');\n\n/**\n * @construcotr\n * Constructor for a class which allows to add extra functions which fire before or after\n * the other functions in a Node.js EventEmitter.\n *\n * @param {object} eventEmitter the instance of EventEmitter to intercept\n * @param {string} eventName the name of the event to intercept\n * @param {function} beforeFn the function to fire before the usual events. If this function\n * returns false, the other ends (and afterFn) will not fire.\n * @param {function} afterFn the function to fire after the other handlers fire. This will\n * run even if preventDefault or stopPropagation are used in the\n * regular event handlers on Node 16.5+.\n */\nfunction EventInterceptor(eventEmitter, eventName, beforeFn, afterFn)\n{\n this.eventEmitter = eventEmitter;\n this.eventName = eventName;\n this.beforeFn = beforeFn;\n this.afterFn = afterFn;\n\n this.interceptedListeners = [];\n this._interceptFn = this._interceptFn.bind(this); /* create unique function pointer we can use to compare against later */\n this.repair();\n}\n\n/**\n * Patch up an instance of EventInterceptor so that it will work properly after allowing 3rd party code\n * to \"mess\" with the event emitter.\n */\nEventInterceptor.prototype.repair = function EventInterceptor$repair()\n{\n const newListeners = this.eventEmitter.listeners(this.eventName);\n var seenInterceptFn = false;\n \n for (let i=0; i < newListeners.length; i++)\n {\n let listenerFn = newListeners[i];\n\n if (listenerFn === this._interceptFn)\n {\n seenInterceptFn = true;\n continue;\n }\n \n this.interceptedListeners.push(listenerFn);\n this.eventEmitter.removeListener(this.eventName, listenerFn);\n }\n\n if (!seenInterceptFn)\n this.eventEmitter.addListener(this.eventName, this._interceptFn);\n}\n\n\n/** Internal function which implements the interception */ \nEventInterceptor.prototype._interceptFn = function EventInterceptor$_interceptFn()\n{\n try\n {\n (this.debug || debugging(this.eventName)) && console.debug('intercepted event', this.eventName);\n if (this.beforeFn && (this.beforeFn.apply(this.eventEmitter, arguments) === false)) /* false prevents bubble */\n return;\n }\n catch(e)\n {\n console.error(e);\n }\n\n (this.debug || debugging(this.eventName)) && console.debug('fallthrough to default event handlers for', this.eventName);\n for (let i=0; i < this.interceptedListeners.length; i++)\n this.interceptedListeners[i].apply(this.eventEmitter, arguments);\n\n if (this.afterFn)\n this.afterFn.apply(this.eventEmitter, arguments);\n}\n \nexports.EventInterceptor = EventInterceptor;\n\n\n//# sourceURL=webpack:///./src/common/dcp-events/event-interceptor.js?");
4882
+ eval("/**\n * @file event-interceptor.js\n * Implement a class which allows to add extra functions which fire before or after the\n * other functions in a Node.js EventEmitter.\n * \n * @author Wes Garland, wes@kingsds.network\n * @date July 2021\n *\n * @note Currently only works with Node.js EventEmitters!\n */\n\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('events');\n\n/**\n * @construcotr\n * Constructor for a class which allows to add extra functions which fire before or after\n * the other functions in a Node.js EventEmitter.\n *\n * @param {object} eventEmitter the instance of EventEmitter to intercept\n * @param {string} eventName the name of the event to intercept\n * @param {function} beforeFn the function to fire before the usual events. If this function\n * returns false, the other ends (and afterFn) will not fire.\n * @param {function} afterFn the function to fire after the other handlers fire. This will\n * run even if preventDefault or stopPropagation are used in the\n * regular event handlers on Node 16.5+.\n */\nfunction EventInterceptor(eventEmitter, eventName, beforeFn, afterFn)\n{\n this.eventEmitter = eventEmitter;\n this.eventName = eventName;\n this.beforeFn = beforeFn;\n this.afterFn = afterFn;\n\n this.interceptedListeners = [];\n this._interceptFn = this._interceptFn.bind(this); /* create unique function pointer we can use to compare against later */\n this.repair();\n}\n\n/**\n * Patch up an instance of EventInterceptor so that it will work properly after allowing 3rd party code\n * to \"mess\" with the event emitter.\n */\nEventInterceptor.prototype.repair = function EventInterceptor$repair()\n{\n const newListeners = this.eventEmitter.listeners(this.eventName);\n var seenInterceptFn = false;\n \n for (let i=0; i < newListeners.length; i++)\n {\n let listenerFn = newListeners[i];\n\n if (listenerFn === this._interceptFn)\n {\n seenInterceptFn = true;\n continue;\n }\n \n this.interceptedListeners.push(listenerFn);\n this.eventEmitter.removeListener(this.eventName, listenerFn);\n }\n\n if (!seenInterceptFn)\n this.eventEmitter.addListener(this.eventName, this._interceptFn);\n}\n\n\n/** Internal function which implements the interception */ \nEventInterceptor.prototype._interceptFn = function EventInterceptor$_interceptFn()\n{\n try\n {\n debugging(this.eventName) && !debugging('all') && console.debug('intercepted event', this.eventName);\n if (this.beforeFn && (this.beforeFn.apply(this.eventEmitter, arguments) === false)) /* false prevents bubble */\n return;\n }\n catch(e)\n {\n console.error(e);\n }\n\n debugging(this.eventName) && !debugging('all') && console.debug('fallthrough to default event handlers for', this.eventName);\n for (let i=0; i < this.interceptedListeners.length; i++)\n this.interceptedListeners[i].apply(this.eventEmitter, arguments);\n\n if (this.afterFn)\n this.afterFn.apply(this.eventEmitter, arguments);\n}\n \nexports.EventInterceptor = EventInterceptor;\n\n\n//# sourceURL=webpack:///./src/common/dcp-events/event-interceptor.js?");
4859
4883
 
4860
4884
  /***/ }),
4861
4885
 
@@ -4866,7 +4890,7 @@ eval("/**\n * @file event-interceptor.js\n * Implement a cla
4866
4890
  /*! no static exports found */
4867
4891
  /***/ (function(module, exports, __webpack_require__) {
4868
4892
 
4869
- eval("exports.EventInterceptor = __webpack_require__(/*! ./event-interceptor */ \"./src/common/dcp-events/event-interceptor.js\").EventInterceptor;\nexports.EventEmitter = __webpack_require__(/*! ./event-emitter */ \"./src/common/dcp-events/event-emitter.js\").EventEmitter;\nexports.PropagatingEventEmitter = __webpack_require__(/*! ./propagating-event-emitter */ \"./src/common/dcp-events/propagating-event-emitter.js\").PropagatingEventEmitter;\n\n\n\n// Event constants\nexports.eventTypes = { };\n[\n 'nofunds',\n 'cancel',\n 'readyStateChange', // @TODO - change this to be emitted from the scheduler as the scheduler says it reaches specific points in setting up the job.\n 'complete', // @TODO - ensure this event works. It might not be needed / might need a name change.\n].forEach(eventName => exports.eventTypes[eventName] = { reliable: true });\n\n[\n 'console',\n 'result',\n 'error',\n 'status',\n 'stop',\n 'noprogress',\n 'custom',\n 'schedmsg::command',\n 'schedmsg::broadcast',\n].forEach(eventName => exports.eventTypes[eventName] = { reliable: false });\n\nexports.events = {};\n\nObject.keys(exports.eventTypes).forEach(eventName => exports.events[eventName] = eventName);\n\n\n//# sourceURL=webpack:///./src/common/dcp-events/index.js?");
4893
+ eval("exports.EventInterceptor = __webpack_require__(/*! ./event-interceptor */ \"./src/common/dcp-events/event-interceptor.js\").EventInterceptor;\nexports.EventEmitter = __webpack_require__(/*! ./event-emitter */ \"./src/common/dcp-events/event-emitter.js\").EventEmitter;\nexports.PropagatingEventEmitter = __webpack_require__(/*! ./propagating-event-emitter */ \"./src/common/dcp-events/propagating-event-emitter.js\").PropagatingEventEmitter;\n\n\n\n// Event constants\nexports.eventTypes = { };\n[\n 'nofunds',\n 'readyStateChange', // @TODO - change this to be emitted from the scheduler as the scheduler says it reaches specific points in setting up the job.\n 'complete', // @TODO - ensure this event works. It might not be needed / might need a name change.\n].forEach(eventName => exports.eventTypes[eventName] = { reliable: true });\n\n[\n 'stop',\n 'console',\n 'result',\n 'error',\n 'status',\n 'noprogress',\n 'custom',\n 'schedmsg::command',\n 'schedmsg::broadcast',\n].forEach(eventName => exports.eventTypes[eventName] = { reliable: false });\n\nexports.events = {};\n\nObject.keys(exports.eventTypes).forEach(eventName => exports.events[eventName] = eventName);\n\n\n//# sourceURL=webpack:///./src/common/dcp-events/index.js?");
4870
4894
 
4871
4895
  /***/ }),
4872
4896
 
@@ -4955,7 +4979,7 @@ eval("/**\n * @file hash.js\n * General purpose utility rout
4955
4979
  /*! no static exports found */
4956
4980
  /***/ (function(module, exports) {
4957
4981
 
4958
- eval("/**\n * @file scheduler-constants.js\n * Contants and constant-like precomputed values for use with/by DCPv4.\n * All values in this module are completely deterministic and will change\n * only if the source code changes.\n * @author Wes Garland, wes@kingsds.network\n * @date Nov 2020\n */\n\n/** Pre-defined, hard-coded compute groups */\nexports.computeGroups = {\n public: {\n opaqueId: 'WHhetL7mj1w1mw1XV6dxyC', \n id: 1,\n name: 'Public Compute Group (open access)',\n joinKey: 'public',\n joinSecret: '',\n joinKeystore : false /* string / object/ falsey */\n },\n};\n\n/**\n * The list of all possible job status in the status column of the jobs table.\n */\nexports.jobStatuses = [\n 'cancelled',\n 'corrupted',\n 'estimation',\n 'finished',\n 'running',\n];\n\n/**\n * The list of all possible slice status in the status column of the\n * activeSlices table.\n */\nexports.sliceStatuses = [\n 'overdue',\n 'tiebreaker',\n 'scheduled',\n 'working',\n 'paused',\n 'returned',\n 'new',\n];\n\n/** Currently used bit masks for jobFlags column in jobs table. There is capacity for 31 bit masks. */\nexports.jobFlagMasks = {\n localExec: 1, /* 1 << 0 */\n /*\n * E.g. To create the next 3 flags with names mask1, mask2, mask3:\n * mask1: 1 << 1,\n * mask2: 1 << 2,\n * mask3: 1 << 3,\n */\n};\n\n\n//# sourceURL=webpack:///./src/common/scheduler-constants.js?");
4982
+ eval("/**\n * @file scheduler-constants.js\n * Contants and constant-like precomputed values for use with/by DCPv4.\n * All values in this module are completely deterministic and will change\n * only if the source code changes.\n * @author Wes Garland, wes@kingsds.network\n * @date Nov 2020\n */\n\n/** Pre-defined, hard-coded compute groups */\nexports.computeGroups = {\n public: {\n opaqueId: 'WHhetL7mj1w1mw1XV6dxyC', \n id: 1,\n name: 'Public Compute Group (open access)',\n joinKey: 'public',\n joinSecret: '',\n joinKeystore : false /* string / object/ falsey */\n },\n};\n\n/**\n * The list of all possible job status in the status column of the jobs table.\n */\nexports.jobStatuses = [\n 'cancelled',\n 'corrupted',\n 'estimation',\n 'finished',\n 'running',\n];\n\n/**\n * The list of all possible slice status in the status column of the\n * activeSlices table.\n */\nexports.sliceStatuses = [\n 'overdue',\n 'tiebreaker',\n 'scheduled',\n 'working',\n 'paused',\n 'returned',\n 'new',\n];\n\n/** Currently used bit masks for jobFlags column in jobs table. There is capacity for 31 bit masks. */\nexports.jobFlags = {\n localExec: 1, /* 1 << 0 */\n open: 1 << 1, \n /*\n * E.g. To create the next 3 flags with names mask1, mask2, mask3:\n * mask1: 1 << 1,\n * mask2: 1 << 2,\n * mask3: 1 << 3,\n */\n};\n\n// Temporary, until bugfix/refactor-result-submitter comes in\nexports.jobFlagMasks = exports.jobFlags;\n\n\n\n//# sourceURL=webpack:///./src/common/scheduler-constants.js?");
4959
4983
 
4960
4984
  /***/ }),
4961
4985
 
@@ -5054,7 +5078,13 @@ eval("/**\n * @file password.js\n * Modal providing a way to
5054
5078
  /*! no static exports found */
5055
5079
  /***/ (function(module, exports, __webpack_require__) {
5056
5080
 
5057
- 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=e4988ba0b1c993e062fc7c536188bda3ea4de883'; /* 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?");
5081
+ <<<<<<< HEAD
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
5058
5088
 
5059
5089
  /***/ }),
5060
5090
 
@@ -5065,7 +5095,7 @@ eval("/**\n * @file client-modal/utils.js\n * @author KC Erb\n * @date Mar 2020\
5065
5095
  /*! no static exports found */
5066
5096
  /***/ (function(module, exports, __webpack_require__) {
5067
5097
 
5068
- eval("/**\n * @file Client facing module that implements Compute Groups API\n * @module dcp/compute-groups\n * @access public\n * @author Kayra E-A <kayra@kingsds.network>\n * @date Sept 2020\n */\n\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst hash = __webpack_require__(/*! ../../common/hash */ \"./src/common/hash.js\");\n\nconst { DCPError } = __webpack_require__(/*! ../../common/dcp-error */ \"./src/common/dcp-error.js\");\n\n/**\n * Establishes the client connection to the computeGroups microservice if it does not exist already from the default config.\n * \n * @returns {protocolV4.Connection}\n * @access public\n * @example\n * const result = await exports.serviceConnection.send('createGroup', {\n name: name,\n description: description,\n });\n */\n\n\nexports.serviceConnection = null;\n\nconst openServiceConn = function openServiceConn()\n{\n exports.serviceConnection = new protocolV4.Connection(dcpConfig.scheduler.services.computeGroups);\n exports.serviceConnection.on('close', openServiceConn)\n}\n\n\n/**\n * Resets the client connection to the computeGroups microservice.\n */\nexports.closeServiceConnection = async function closeServiceConnection() {\n if (exports.serviceConnection)\n {\n exports.serviceConnection.off('close', openServiceConn);\n exports.serviceConnection.close();\n }\n \n exports.serviceConnection = null;\n};\n\n/**\n * KeepAlive for the service connection to compute groups\n */\nexports.keepAlive = async function keepAlive() {\n if (!exports.serviceConnection)\n openServiceConn();\n \n exports.serviceConnection.keepalive().catch(err => console.error('Warning: keepalive failed for compute groups service', err));\n}\n\n /**\n * Fetches the public compute group ID from the scheduler constants.\n */\nexports.getPublicComputeGroupId = function getPublicComputeGroupId() {\n return __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups.public.id;\n};\n\n/**\n * Creates a Compute Group, optionally specifying certain aspects of it.\n *\n * @param {string} name The name of the compute group\n * @param {object} [options] Additional properties to specify\n * @param {string} [options.description] Description of the compute group. Is\n * auto-populated based on name and creation date if not specified.\n * @param {string} [options.joinKey] A \"username\" for the compute group. Must be\n * unique and can be specified without a joinSecret.\n * @param {string} [options.joinSecret] A \"password\" for the compute group. Must\n * be specified alongside a joinKey.\n * @returns {Promise<number>} The id of the compute group that was just created\n * @access public\n * @example\n * const groupId = await computeGroup.create('myGroup', {\n * description: 'This compute group is computing for...!'\n * joinKey: '...',\n * joinSecret: '...',\n * });\n */\nexports.create = async function create(name, options) {\n let description, joinKey, joinHash;\n\n if(options) {\n description = options.description;\n joinKey = options.joinKey;\n if(options.joinSecret)\n joinHash = exports.calculateJoinHash({joinKey, joinSecret: options.joinSecret});\n }\n\n if (typeof name !== 'string' || name === '') {\n throw new TypeError(\n `A compute group's name must be a non-empty string, not ${name}`,\n );\n }\n\n if (options && typeof options.joinSecret !== 'undefined' && typeof joinKey === 'undefined') {\n throw new Error(\n \"A compute group's join secret cannot be specified without a join key\",\n );\n }\n \n if (!exports.serviceConnection)\n openServiceConn();\n\n const { success, payload } = await exports.serviceConnection.send(\n 'createGroup',\n {\n name,\n description,\n joinKey,\n joinHash,\n },\n );\n\n if (!success) {\n switch (payload.code) {\n case 'ER_DUP_ENTRY':\n /**\n * If it's a mysql duplicate entry error, it's most likely referring to\n * the unique joinKey column.\n */\n throw new DCPError(\n `Cannot create another compute group with the joinKey '${joinKey}'. It is already in use.`,\n );\n default:\n throw new DCPError(\n `Unable to create Compute Group called ${name}`,\n payload,\n );\n }\n }\n\n return payload;\n};\n\n /**\n * Function that changes the properties of an existing Compute Group. The id (computeGroupId, this will be changed later) must be\n * specified as a number. Must be the owner of the Compute Group to change its properties. The joinSecret is entered as a string and stored as\n * an ethutil hash with 'eh1-' appended to the beginning, indicating an ethereum hash.\n * \n * Changing joinKey and joinSecret will always happen in tandem, since joinKey is salt for joinSecret Ethereum hash. \n * The joinSecret hash is calculated here on the client side - microservice assumes data is good.\n *\n *\n * @param {string} id - the of of the Compute Group.\n * @param {object} prototype - the object that contains the list of properties to change. This is name, description, joinKey, joinSecret, joinAddress\n * @param {string} prototype.name - the name of the Compute Group.\n * @param {string} prototype.description - the description of the Compute Group.\n * @param {string} prototype.joinKey - the joinKey for joining the Compute Group. Analogous to username.\n * @param {string} prototype.joinSecret - the joinSecret for joining the Compute Group. Analogous to password\n * @param {string} prototype.joinAddress - the joinAddress of the Compute Group.\n * @access public\n * @example\n * await computeGroup.changeGroup( id = 234, {name = 'new name', description = 'new description', joinKey = 'string', joinSecret = 'another string', joinAddress = 'ethAddress'} );\n */\nexports.changeGroup = async function changeGroup( id, prototype ) {\n\n if ( +(prototype.joinKey !== undefined) ^ +(prototype.joinSecret !== undefined) ){\n throw new DCPError(`Changes to joinKey or joinSecret must happen together!`);\n }\n\n if (!exports.serviceConnection)\n openServiceConn();\n\n const joinHash = exports.calculateJoinHash(prototype); /* warning, dcp 1905 */\n\n prototype.joinSecret = joinHash; /* change with dcp 1905 */\n\n const { success, payload } = await exports.serviceConnection.send('changeGroup', {\n id: id,\n prototype: prototype,\n });\n\n if (!success) {\n throw new DCPError(`Cannot change the compute group ${id}: ${payload}`);\n }\n};\n\n /**\n * Async function that deletes a Compute Group from the database. The Compute Group ID must be specified as a number.\n * Must be the owner of the Compute Group to delete it.\n * \n * @param {number} computeGroupId - the ID of the Compute Group that is to be deleted. Analogous to row ID in table.\n * @access public\n * @example\n * await computeGroup.deleteGroup(456);\n */\nexports.deleteGroup = async function deleteGroup(computeGroupId) {\n if (!exports.serviceConnection)\n openServiceConn();\n \n const { success, payload } = await exports.serviceConnection.send('deleteGroup', {\n id: computeGroupId,\n });\n\n if (!success) {\n throw new DCPError(`Cannot delete the compute group ${computeGroupId}: ${payload}`);\n }\n};\n\n/**\n * Async function that adds a job to a specified compute group. Both Compute\n * Group ID and Job ID must be specified as number Must be the owner of the\n * Compute Group to add a Job to it. The joinKey and the joinSecret/joinHash\n * (user/password) must also be provided.\n *\n * @param {Address} job the opaque id of the Job that will be added to the Compute Group.\n * @param {object} descriptor the descriptor object for the compute group. This descriptor\n * needs to contain enough information to authorize access to the\n * compute group. Properties may include:\n * - id\n * - joinKey\n * - joinSecret\n * - joinHash\n * - joinAddress\n * \n * Additional, either the joinKey or the compute group id MUST be specified so\n * that we can identify the compute group in question.\n *\n * All compute groups can have jobs submitted to them, provided either the joinKey\n * or the id (maybe deprecated in the future?) are specified, and the message is\n * signed by the compute group owner.\n *\n * FUTURE - after DCP-1910\n * keystore A keystore used to grant access to job deployment within this compute group.\n * This can be either the ownerKeystore or the joinAddress keystore when the\n * compute group is in deployAccessType='join' mode.\n *\n * @access public\n * @example\n * await computeGroup.addJob(computeGroupId = 456, job = 'P+Y4IApeFQLrYS2W7MkVg7');\n */\nexports.addJob = async function addJob(job, descriptor)\n{\n var result;\n var message = Object.assign({}, {job: job});\n message.computeGroupId = descriptor.id;\n message.joinKey = descriptor.joinKey;\n \n if(descriptor.joinHash) {\n message.joinHashHash = hash.calculate(hash.eh1, descriptor.joinHash, exports.serviceConnection.dcpsid);\n } else if(descriptor.joinSecret) {\n message.joinHashHash = hash.calculate(hash.eh1, exports.calculateJoinHash(descriptor), exports.serviceConnection.dcpsid);\n }\n \n if (!message.job)\n throw new Error('job not specified');\n if (!message.computeGroupId && !message.joinKey)\n throw new Error('compute group not identified');\n \n if (!exports.serviceConnection)\n openServiceConn();\n\n result = await exports.serviceConnection.send('addJob', message);\n\n if (!result.success)\n throw new DCPError(result.payload.message);\n};\n\n /**\n * Async function that lists all the current Jobs in a Compute Group.\n * The Compute Group ID must be specified. Must be of type number.\n * Must be the owner of the Compute Group to list its current Jobs.\n *\n * @param {number} computeGroupId - the ID of the Compute Group from which we will list its jobs.\n * @returns {Array} - the array of Jobs belonging to the computeGroup.\n * @access public\n * @example\n * let listOfJobs = await computeGroup.listJobs(computeGroupId = 456);\n */\nexports.listJobs = async function listJobs(computeGroupId) {\n if (!exports.serviceConnection)\n openServiceConn();\n\n const { success, payload } = await exports.serviceConnection.send('listJobs', {\n computeGroupId: computeGroupId,\n });\n\n if (!success) {\n throw new DCPError(`Cannot list jobs for compute group ${computeGroupId}: ${payload}`);\n }\n\n return payload;\n};\n\n /**\n * Async function that removes a Compute Group from the the computeGroups table in the scheduler_dcp mysql database.\n * Both Compute Group ID and Job ID must be specified to remove Job from the group. Both of type number.\n * Must be the owner of the Compute Group to remove a Job from it.\n *\n * @param {number} computeGroupId - the ID of the Compute Group where the job will be removed. Analogous to row ID in table.\n * @param {address} job - the address of the Job that will be removed from the Compute Group.\n * @access public\n * @example\n * await computeGroup.removeJob(computeGroupId = 123456, job = 'P+Y4IApeFQLrYS2W7MkVg7');\n */\nexports.removeJob = async function removeJob(computeGroupId, job, joinKey, joinSecret) {\n var joinHashHash;\n if (!exports.serviceConnection)\n openServiceConn();\n\n if(joinSecret)\n joinHashHash = hash.calculate(hash.eh1, exports.calculateJoinHash({joinKey, joinSecret}, joinSecret), exports.serviceConnection.dcpsid);\n const { success, payload } = await exports.serviceConnection.send('removeJob', {\n computeGroupId: parseInt(computeGroupId),\n job: job,\n joinKey: joinKey,\n joinHashHash: joinHashHash,\n });\n\n if (!success) {\n throw new DCPError(`Cannot remove job ${job} from compute group ${computeGroupId}: ${payload}`);\n }\n};\n\n/**\n * Async function that cancel the specified job.\n *\n * @param {number} computeGroupId - the ID of the Compute Group.\n * @param {address} job - the address of the job that will be cancelled.\n * @access public\n * @example\n * await computeGroup.cancelJob(computeGroupId = 123456, job = 'P+Y4IApeFQLrYS2W7MkVg7');\n */\nexports.cancelJob = async function cancelJob(computeGroupId, job) {\n if (!exports.serviceConnection)\n openServiceConn();\n\n const { success, payload } = await exports.serviceConnection.send('cancelJob', {\n computeGroupId: parseInt(computeGroupId),\n job: job\n });\n\n if (!success) {\n throw new DCPError(`Cannot cancel job ${job} for compute group ${computeGroupId}: ${payload}`);\n }\n};\n\n/**\n * Async function that cancel all jobs.\n *\n * @param {number} computeGroupId - the ID of the Compute Group.\n * @access public\n * @example\n * await computeGroup.cancelAllJobs(computeGroupId = 123456);\n */\nexports.cancelAllJobs = async function cancelAllJobs(computeGroupId) {\n if (!exports.serviceConnection)\n openServiceConn();\n\n const { success, payload } = await exports.serviceConnection.send('cancelAllJobs', {\n computeGroupId: parseInt(computeGroupId)\n });\n\n if (!success) {\n throw new DCPError(`Cannot cancel all jobs for compute group ${computeGroupId}: ${payload}`);\n }\n};\n\n/**\n * Calculate a joinHash for a compute group. This is an eh1- hash of the cg salt and \n * joinSecret components of a compute group description.\n *\n * @param {object} details an object containing the cg salt, which is\n * the joinKey if the compute group uses one;\n * otherwise it is the joinAddress. This object\n * may also contain the joinSecret.\n * @param {string} joinSecret the join secret -- plain text -- that is\n * the \"password\" for the compute group. If not\n * specified, we use details.joinSecret.\n */\nexports.calculateJoinHash = function computeGroups$calculateJoinHash(details, joinSecret)\n{\n if (typeof joinSecret === 'undefined')\n joinSecret = details.joinSecret;\n\n return hash.calculate(hash.eh1, `${details.joinKey || details.joinAddress} ${details.joinSecret}`);\n}\n\n\n//# sourceURL=webpack:///./src/dcp-client/compute-groups/index.js?");
5098
+ eval("/**\n * @file Client facing module that implements Compute Groups API\n * @module dcp/compute-groups\n * @access public\n * @author Kayra E-A <kayra@kingsds.network>\n * @date Sept 2020\n */\n\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst hash = __webpack_require__(/*! ../../common/hash */ \"./src/common/hash.js\");\n\nconst { DCPError } = __webpack_require__(/*! ../../common/dcp-error */ \"./src/common/dcp-error.js\");\n\n/**\n * Establishes the client connection to the computeGroups microservice if it does not exist already from the default config.\n * \n * @returns {protocolV4.Connection}\n * @access public\n * @example\n * const result = await exports.serviceConnection.send('createGroup', {\n name: name,\n description: description,\n });\n */\n\n\nexports.serviceConnection = null;\n\nconst openServiceConn = function openServiceConn()\n{\n exports.serviceConnection = new protocolV4.Connection(dcpConfig.scheduler.services.computeGroups);\n exports.serviceConnection.on('close', openServiceConn)\n}\n\n\n/**\n * Resets the client connection to the computeGroups microservice.\n */\nexports.closeServiceConnection = async function closeServiceConnection() {\n if (exports.serviceConnection)\n {\n exports.serviceConnection.off('close', openServiceConn);\n exports.serviceConnection.close();\n }\n \n exports.serviceConnection = null;\n};\n\n/**\n * KeepAlive for the service connection to compute groups\n */\nexports.keepAlive = async function keepAlive() {\n if (!exports.serviceConnection)\n openServiceConn();\n \n exports.serviceConnection.keepalive().catch(err => console.error('Warning: keepalive failed for compute groups service', err));\n}\n\n /**\n * Fetches the public compute group ID from the scheduler constants.\n */\nexports.getPublicComputeGroupId = function getPublicComputeGroupId() {\n return __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups.public.id;\n};\n\n/**\n * Creates a Compute Group, optionally specifying certain aspects of it.\n *\n * @param {string} name The name of the compute group\n * @param {object} [options] Additional properties to specify\n * @param {string} [options.description] Description of the compute group. Is\n * auto-populated based on name and creation date if not specified.\n * @param {string} [options.joinKey] A \"username\" for the compute group. Must be\n * unique and can be specified without a joinSecret.\n * @param {string} [options.joinSecret] A \"password\" for the compute group. Must\n * be specified alongside a joinKey.\n * @returns {Promise<number>} The id of the compute group that was just created\n * @access public\n * @example\n * const groupId = await computeGroup.create('myGroup', {\n * description: 'This compute group is computing for...!'\n * joinKey: '...',\n * joinSecret: '...',\n * });\n */\nexports.create = async function create(name, options) {\n let description, joinKey, joinHash;\n\n if(options) {\n description = options.description;\n joinKey = options.joinKey;\n if(options.joinSecret)\n joinHash = exports.calculateJoinHash({joinKey, joinSecret: options.joinSecret});\n }\n\n if (typeof name !== 'string' || name === '') {\n throw new TypeError(\n `A compute group's name must be a non-empty string, not ${name}`,\n );\n }\n\n if (options && typeof options.joinSecret !== 'undefined' && typeof joinKey === 'undefined') {\n throw new Error(\n \"A compute group's join secret cannot be specified without a join key\",\n );\n }\n \n if (!exports.serviceConnection)\n openServiceConn();\n\n const { success, payload } = await exports.serviceConnection.send(\n 'createGroup',\n {\n name,\n description,\n joinKey,\n joinHash,\n },\n );\n\n if (!success) {\n switch (payload.code) {\n case 'ER_DUP_ENTRY':\n /**\n * If it's a mysql duplicate entry error, it's most likely referring to\n * the unique joinKey column.\n */\n throw new DCPError(\n `Cannot create another compute group with the joinKey '${joinKey}'. It is already in use.`,\n );\n default:\n throw new DCPError(\n `Unable to create Compute Group called ${name}`,\n payload,\n );\n }\n }\n\n return payload;\n};\n\n /**\n * Function that changes the properties of an existing Compute Group. The id (computeGroupId, this will be changed later) must be\n * specified as a number. Must be the owner of the Compute Group to change its properties. The joinSecret is entered as a string and stored as\n * an ethutil hash with 'eh1-' appended to the beginning, indicating an ethereum hash.\n * \n * Changing joinKey and joinSecret will always happen in tandem, since joinKey is salt for joinSecret Ethereum hash. \n * The joinSecret hash is calculated here on the client side - microservice assumes data is good.\n *\n *\n * @param {string} id - the of of the Compute Group.\n * @param {object} prototype - the object that contains the list of properties to change. This is name, description, joinKey, joinSecret, joinAddress\n * @param {string} prototype.name - the name of the Compute Group.\n * @param {string} prototype.description - the description of the Compute Group.\n * @param {string} prototype.joinKey - the joinKey for joining the Compute Group. Analogous to username.\n * @param {string} prototype.joinSecret - the joinSecret for joining the Compute Group. Analogous to password\n * @param {string} prototype.joinAddress - the joinAddress of the Compute Group.\n * @access public\n * @example\n * await computeGroup.changeGroup( id = 234, {name = 'new name', description = 'new description', joinKey = 'string', joinSecret = 'another string', joinAddress = 'ethAddress'} );\n */\nexports.changeGroup = async function changeGroup( id, prototype ) {\n\n if ( +(prototype.joinKey !== undefined) ^ +(prototype.joinSecret !== undefined) ){\n throw new DCPError(`Changes to joinKey or joinSecret must happen together!`);\n }\n\n if (!exports.serviceConnection)\n openServiceConn();\n\n const joinHash = exports.calculateJoinHash(prototype); /* warning, dcp 1905 */\n\n prototype.joinSecret = joinHash; /* change with dcp 1905 */\n\n const { success, payload } = await exports.serviceConnection.send('changeGroup', {\n id: id,\n prototype: prototype,\n });\n\n if (!success) {\n throw new DCPError(`Cannot change the compute group ${id}: ${payload}`);\n }\n};\n\n /**\n * Async function that deletes a Compute Group from the database. The Compute Group ID must be specified as a number.\n * Must be the owner of the Compute Group to delete it.\n * \n * @param {number} computeGroupId - the ID of the Compute Group that is to be deleted. Analogous to row ID in table.\n * @access public\n * @example\n * await computeGroup.deleteGroup(456);\n */\nexports.deleteGroup = async function deleteGroup(computeGroupId) {\n if (!exports.serviceConnection)\n openServiceConn();\n \n const { success, payload } = await exports.serviceConnection.send('deleteGroup', {\n id: computeGroupId,\n });\n\n if (!success) {\n throw new DCPError(`Cannot delete the compute group ${computeGroupId}: ${payload}`);\n }\n};\n\n/**\n * Async function that adds a job to a specified compute group. Both Compute\n * Group ID and Job ID must be specified as number Must be the owner of the\n * Compute Group to add a Job to it. The joinKey and the joinSecret/joinHash\n * (user/password) must also be provided.\n *\n * @param {Address} job the opaque id of the Job that will be added to the Compute Group.\n * @param {object} descriptor the descriptor object for the compute group. This descriptor\n * needs to contain enough information to authorize access to the\n * compute group. Properties may include:\n * - id\n * - joinKey\n * - joinSecret\n * - joinHash\n * - joinAddress\n * \n * Additional, either the joinKey or the compute group id MUST be specified so\n * that we can identify the compute group in question.\n *\n * All compute groups can have jobs submitted to them, provided either the joinKey\n * or the id (maybe deprecated in the future?) are specified, and the message is\n * signed by the compute group owner.\n *\n * FUTURE - after DCP-1910\n * keystore A keystore used to grant access to job deployment within this compute group.\n * This can be either the ownerKeystore or the joinAddress keystore when the\n * compute group is in deployAccessType='join' mode.\n *\n * @access public\n * @example\n * await computeGroup.addJob(computeGroupId = 456, job = 'P+Y4IApeFQLrYS2W7MkVg7');\n */\nexports.addJobToGroups = async function addJobToGroups(job, computeGroups)\n{\n let result;\n const cgArray = [];\n if (!job)\n throw new Error('job not specified');\n let keyInfo = Object.assign({}, {job: job});\n \n if (!exports.serviceConnection)\n openServiceConn();\n \n for (const group of computeGroups) \n {\n let message = { computeGroupId: group.id, joinKey: group.joinKey };\n \n if (group.joinHash) \n {\n message.joinHashHash = hash.calculate(hash.eh1, group.joinHash, exports.serviceConnection.dcpsid);\n } \n else if(group.joinSecret) \n {\n message.joinHashHash = hash.calculate(hash.eh1, exports.calculateJoinHash(group), exports.serviceConnection.dcpsid);\n }\n \n if (!message.computeGroupId && !message.joinKey)\n throw new Error('compute group not identified');\n cgArray.push(message);\n }\n \n keyInfo.cgArray = cgArray;\n \n result = await exports.serviceConnection.send('addJobToGroups', keyInfo);\n\n if (!result.success)\n throw new DCPError(result.payload.message);\n};\n\n /**\n * Async function that lists all the current Jobs in a Compute Group.\n * The Compute Group ID must be specified. Must be of type number.\n * Must be the owner of the Compute Group to list its current Jobs.\n *\n * @param {number} computeGroupId - the ID of the Compute Group from which we will list its jobs.\n * @returns {Array} - the array of Jobs belonging to the computeGroup.\n * @access public\n * @example\n * let listOfJobs = await computeGroup.listJobs(computeGroupId = 456);\n */\nexports.listJobs = async function listJobs(computeGroupId) {\n if (!exports.serviceConnection)\n openServiceConn();\n\n const { success, payload } = await exports.serviceConnection.send('listJobs', {\n computeGroupId: computeGroupId,\n });\n\n if (!success) {\n throw new DCPError(`Cannot list jobs for compute group ${computeGroupId}: ${payload}`);\n }\n\n return payload;\n};\n\n /**\n * Async function that removes a Compute Group from the the computeGroups table in the scheduler_dcp mysql database.\n * Both Compute Group ID and Job ID must be specified to remove Job from the group. Both of type number.\n * Must be the owner of the Compute Group to remove a Job from it.\n *\n * @param {number} computeGroupId - the ID of the Compute Group where the job will be removed. Analogous to row ID in table.\n * @param {address} job - the address of the Job that will be removed from the Compute Group.\n * @access public\n * @example\n * await computeGroup.removeJob(computeGroupId = 123456, job = 'P+Y4IApeFQLrYS2W7MkVg7');\n */\nexports.removeJob = async function removeJob(computeGroupId, job, joinKey, joinSecret) {\n var joinHashHash;\n if (!exports.serviceConnection)\n openServiceConn();\n\n if(joinSecret)\n joinHashHash = hash.calculate(hash.eh1, exports.calculateJoinHash({joinKey, joinSecret}, joinSecret), exports.serviceConnection.dcpsid);\n const { success, payload } = await exports.serviceConnection.send('removeJob', {\n computeGroupId: parseInt(computeGroupId),\n job: job,\n joinKey: joinKey,\n joinHashHash: joinHashHash,\n });\n\n if (!success) {\n throw new DCPError(`Cannot remove job ${job} from compute group ${computeGroupId}: ${payload}`);\n }\n};\n\n/**\n * Async function that cancel the specified job.\n *\n * @param {number} computeGroupId - the ID of the Compute Group.\n * @param {address} job - the address of the job that will be cancelled.\n * @access public\n * @example\n * await computeGroup.cancelJob(computeGroupId = 123456, job = 'P+Y4IApeFQLrYS2W7MkVg7');\n */\nexports.cancelJob = async function cancelJob(computeGroupId, job) {\n if (!exports.serviceConnection)\n openServiceConn();\n\n const { success, payload } = await exports.serviceConnection.send('cancelJob', {\n computeGroupId: parseInt(computeGroupId),\n job: job\n });\n\n if (!success) {\n throw new DCPError(`Cannot cancel job ${job} for compute group ${computeGroupId}: ${payload}`);\n }\n};\n\n/**\n * Async function that cancel all jobs.\n *\n * @param {number} computeGroupId - the ID of the Compute Group.\n * @access public\n * @example\n * await computeGroup.cancelAllJobs(computeGroupId = 123456);\n */\nexports.cancelAllJobs = async function cancelAllJobs(computeGroupId) {\n if (!exports.serviceConnection)\n openServiceConn();\n\n const { success, payload } = await exports.serviceConnection.send('cancelAllJobs', {\n computeGroupId: parseInt(computeGroupId)\n });\n\n if (!success) {\n throw new DCPError(`Cannot cancel all jobs for compute group ${computeGroupId}: ${payload}`);\n }\n};\n\n/**\n * Calculate a joinHash for a compute group. This is an eh1- hash of the cg salt and \n * joinSecret components of a compute group description.\n *\n * @param {object} details an object containing the cg salt, which is\n * the joinKey if the compute group uses one;\n * otherwise it is the joinAddress. This object\n * may also contain the joinSecret.\n * @param {string} joinSecret the join secret -- plain text -- that is\n * the \"password\" for the compute group. If not\n * specified, we use details.joinSecret.\n */\nexports.calculateJoinHash = function computeGroups$calculateJoinHash(details, joinSecret)\n{\n if (typeof joinSecret === 'undefined')\n joinSecret = details.joinSecret;\n\n return hash.calculate(hash.eh1, `${details.joinKey || details.joinAddress} ${details.joinSecret}`);\n}\n\n\n//# sourceURL=webpack:///./src/dcp-client/compute-groups/index.js?");
5069
5099
 
5070
5100
  /***/ }),
5071
5101
 
@@ -5076,7 +5106,7 @@ eval("/**\n * @file Client facing module that implements Compute Groups API\n
5076
5106
  /*! no static exports found */
5077
5107
  /***/ (function(module, exports, __webpack_require__) {
5078
5108
 
5079
- 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 openBankConn()\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?");
5080
5110
 
5081
5111
  /***/ }),
5082
5112
 
@@ -5087,7 +5117,13 @@ eval("/**\n * @file Module that implements Compute API\n * @module dcp/comput
5087
5117
  /*! no static exports found */
5088
5118
  /***/ (function(module, exports, __webpack_require__) {
5089
5119
 
5090
- eval("/* WEBPACK VAR INJECTION */(function(module) {/**\n * @file dcp-client-bundle-src.js\n * Top-level file which gets webpacked into the bundle consumed by dcp-client 2.5\n * @author Wes Garland, wes@kingsds.network\n * @date July 2019\n */\n\n{\n let thisScript = typeof document !== 'undefined' ? (typeof document.currentScript !== 'undefined' && document.currentScript) || document.getElementById('_dcp_client_bundle') : {}\n let realModuleDeclare\n\n if ( false || typeof module.declare === 'undefined') {\n realModuleDeclare = ( true) ? module.declare : undefined\n if (false) {}\n module.declare = function moduleUnWrapper (deps, factory) {\n factory(null, module.exports, module)\n return module.exports\n }\n }\n\n let _debugging = () => false\n dcpConfig.future = __webpack_require__(/*! ../common/config-future.js */ \"./src/common/config-future.js\").futureFactory(_debugging, dcpConfig);\n\n /* These modules are official API and must be part of DCP Client */\n let officialApi = {\n 'protocol': __webpack_require__(/*! ../protocol-v4 */ \"./src/protocol-v4/index.js\"),\n 'compute': __webpack_require__(/*! ./compute */ \"./src/dcp-client/compute.js\").compute,\n 'worker': __webpack_require__(/*! ./worker */ \"./src/dcp-client/worker/index.js\"),\n 'wallet': __webpack_require__(/*! ./wallet */ \"./src/dcp-client/wallet/index.js\"),\n };\n\n /* Allow client programs to use modules which happen to be in the bundle anyhow */\n let conveniencePeers = {\n 'ethereumjs-tx': __webpack_require__(/*! ./wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.tx,\n 'ethereumjs-wallet': __webpack_require__(/*! ./wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.wallet,\n 'ethereumjs-util': __webpack_require__(/*! ./wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.util,\n 'socket.io-client': __webpack_require__(/*! socket.io-client */ \"./node_modules/socket.io-client/build/index.js\"),\n 'bignumber.js': __webpack_require__(/*! bignumber.js */ \"./node_modules/bignumber.js/bignumber.js\"),\n 'semver': __webpack_require__(/*! semver */ \"./node_modules/semver/semver.js\"),\n };\n\n /* Some of these modules are API-track. Some of them need to be published to be\n * available for top-level resolution by DCP internals. Those (mostly) should have\n * been written using relative module paths.....\n */\n let modules = Object.assign({\n 'protocol-v3': __webpack_require__(/*! ../protocol-v3 */ \"./src/protocol-v3/index.js\").protocol,\n 'dcp-build': {\"version\":\"e4988ba0b1c993e062fc7c536188bda3ea4de883\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"4.1.12\",\"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#169b90c5603b765cbd334308bb8ac57eddb28378\"},\"built\":\"Fri Oct 22 2021 16:01:33 GMT-0400 (Eastern Daylight Time)\",\"config\":{\"generated\":\"Fri 22 Oct 2021 04:01:30 PM EDT by erose on dione\",\"build\":\"debug\"},\"webpack\":\"4.46.0\",\"node\":\"v12.22.6\"},\n 'dcp-xhr': __webpack_require__(/*! ../common/dcp-xhr */ \"./src/common/dcp-xhr.js\"),\n 'dcp-env': __webpack_require__(/*! ../common/dcp-env */ \"./src/common/dcp-env.js\"),\n 'dcp-url': __webpack_require__(/*! ../common/dcp-url */ \"./src/common/dcp-url.js\"),\n 'cli': __webpack_require__(/*! ../common/cli */ \"./src/common/cli.js\"),\n 'dcp-timers': __webpack_require__(/*! ../common/dcp-timers */ \"./src/common/dcp-timers.js\"),\n 'dcp-dot-dir': __webpack_require__(/*! ../common/dcp-dot-dir */ \"./src/common/dcp-dot-dir.js\"),\n 'dcp-assert': __webpack_require__(/*! ../common/dcp-assert */ \"./src/common/dcp-assert.js\"),\n 'dcp-events': __webpack_require__(/*! ../common/dcp-events */ \"./src/common/dcp-events/index.js\"),\n 'utils': __webpack_require__(/*! ../utils */ \"./src/utils/index.js\"),\n 'debugging': __webpack_require__(/*! ../debugging */ \"./src/debugging.js\"),\n 'publish': __webpack_require__(/*! ../common/dcp-publish */ \"./src/common/dcp-publish.js\"),\n 'compute-groups': {\n ...__webpack_require__(/*! ./compute-groups */ \"./src/dcp-client/compute-groups/index.js\"),\n publicGroupOpaqueId: __webpack_require__(/*! ../common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups.public.opaqueId,\n },\n 'bank-util': __webpack_require__(/*! ./bank-util */ \"./src/dcp-client/bank-util.js\"),\n 'protocol-v4': __webpack_require__(/*! ../protocol-v4 */ \"./src/protocol-v4/index.js\"), /* deprecated */\n 'client-modal': __webpack_require__(/*! ./client-modal */ \"./src/dcp-client/client-modal/index.js\"),\n 'legacy-modal': __webpack_require__(/*! ../protocol-v3/modal */ \"./src/protocol-v3/modal.js\").Modal,\n 'eth': __webpack_require__(/*! ./wallet/eth */ \"./src/dcp-client/wallet/eth.js\"),\n 'serialize': __webpack_require__(/*! ../utils/serialize */ \"./src/utils/serialize.js\"),\n 'job': __webpack_require__(/*! ./job */ \"./src/dcp-client/job/index.js\"),\n 'range-object': __webpack_require__(/*! ./range-object */ \"./src/dcp-client/range-object.js\"),\n 'stats-ranges': __webpack_require__(/*! ./stats-ranges */ \"./src/dcp-client/stats-ranges.js\"),\n 'standard-objects': {}\n }, conveniencePeers, officialApi);\n\n /* Export the JS Standard Classes (etc) from the global object of the bundle evaluation context,\n * in case we have code somewhere that needs to use these for instanceof checks.\n */\n ;[ Object, Function, Boolean, Symbol,\n Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError,\n Number, Math, Date,\n String, RegExp,\n Array, Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array,\n Map, Set, WeakMap, WeakSet,\n ArrayBuffer, DataView, JSON,\n Promise, \n Reflect, Proxy, Intl, WebAssembly, __webpack_require__\n ].forEach(function (obj) {\n if (obj.name && (typeof obj === 'function' || typeof obj === 'object'))\n modules['standard-objects'][obj.name] = obj\n })\n\n if (typeof BigInt !== 'undefined')\n modules['standard-objects']['BigInt'] === BigInt;\n if (typeof BigInt64Array !== 'undefined')\n modules['standard-objects']['BigInt64Array'] === BigInt64Array;\n if (typeof BigInt64Array !== 'undefined')\n modules['standard-objects']['BigUint64Array'] === BigUint64Array;\n\n module.declare([], function(require, exports, module) {\n Object.assign(exports, modules)\n exports['dcp-config'] = dcpConfig\n })\n if (realModuleDeclare)\n module.declare = realModuleDeclare\n\n bundleExports = thisScript.exports = exports; /* must be last expression evaluated! */\n}\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../node_modules/webpack/buildin/module.js */ \"./node_modules/webpack/buildin/module.js\")(module)))\n\n//# sourceURL=webpack:///./src/dcp-client/index.js?");
5120
+ <<<<<<< HEAD
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
5091
5127
 
5092
5128
  /***/ }),
5093
5129
 
@@ -5099,7 +5135,7 @@ eval("/* WEBPACK VAR INJECTION */(function(module) {/**\n * @file dcp-cli
5099
5135
  /***/ (function(module, exports, __webpack_require__) {
5100
5136
 
5101
5137
  "use strict";
5102
- eval("/**\n * @file job/index.js\n * @author Eddie Roosenmaallen, eddie@kingsds.network\n * Matthew Palma, mpalma@kingsds.network\n * @date November 2018\n *\n * This module implements the Compute API's Job Handle\n *\n */\n\n/** @typedef {import('dcp/dcp-client/wallet/keystore').Keystore} Keystore */\n\n\nconst BigNumber = __webpack_require__(/*! bignumber.js */ \"./node_modules/bignumber.js/bignumber.js\");\nconst { v4: uuidv4 } = __webpack_require__(/*! uuid */ \"./node_modules/uuid/dist/esm-browser/index.js\");\nconst { EventEmitter, PropagatingEventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { RangeObject, MultiRangeObject, DistributionRange, SuperRangeObject } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst { fetchURI, encodeDataURI, dumpObject } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst serialize = __webpack_require__(/*! dcp/utils/serialize */ \"./src/utils/serialize.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst { EventSubscriber } = __webpack_require__(/*! dcp/events/event-subscriber */ \"./src/events/event-subscriber.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst ClientModal = __webpack_require__(/*! dcp/dcp-client/client-modal */ \"./src/dcp-client/client-modal/index.js\");\nconst { Worker } = __webpack_require__(/*! dcp/dcp-client/worker */ \"./src/dcp-client/worker/index.js\");\nconst { RemoteDataSet } = __webpack_require__(/*! dcp/dcp-client/remote-data-set */ \"./src/dcp-client/remote-data-set.js\");\nconst { 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 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 ev.message = kvin.parse(ev.message);\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 async addSliceData(dataValues) {\n if (!Array.isArray(this[INTERNAL_SYMBOL].payloadDetails.data)) {\n throw new TypeError('Only data-by-value jobs may dynamically add slices');\n }\n\n const { payload } = await this.deployConnection.send('addSliceData', {\n job: this.address,\n dataValues,\n });\n\n if (payload.success && typeof payload.lastSliceNumber !== 'undefined')\n this.status.total = payload.lastSliceNumber;\n\n return payload;\n }\n\n /**\n * job.snapshot(): Private function used to populate the payloadDetails from private data,\n * inferred data, etc. Once this function has run, the payloadDetails are\n * considered authoritatively up to date until the calling function returns\n * or awaits.\n */\n [SNAPSHOT]() {\n const pd = this[INTERNAL_SYMBOL].payloadDetails;\n\n pd.type = 'ad-hoc'; /* @todo implement appliances */\n pd.uuid = this.uuid;\n pd.workFunctionURI = this[INTERNAL_SYMBOL].workFunctionURI;\n pd.dependencies = this[INTERNAL_SYMBOL].dependencies;\n pd.requirePath = this[INTERNAL_SYMBOL].requirePath;\n pd.modulePath = this[INTERNAL_SYMBOL].modulePath;\n pd.resultStorageType = this[INTERNAL_SYMBOL].resultStorageType;\n pd.resultStorageDetails = this[INTERNAL_SYMBOL].resultStorageDetails;\n pd.resultStorageParams = this[INTERNAL_SYMBOL].resultStorageParams;\n\n pd.requirements = this.requirements;\n applyObjectSchema(pd.requirements, DEFAULT_REQUIREMENTS, 'Requirements Object');\n \n // @todo: 'figure this out' - wise words from eddie /mp jan 2019\n if (!pd.options) { pd.options = {}; }\n if (!pd.public) { pd.public = {}; } \n\n for (let p of ['name', 'description', 'link']) {\n if (typeof this.public[p] === 'string') {\n pd.public[p] = this.public[p];\n }\n }\n\n // The max value that the client is willing to spend to deploy\n // (list on the scheduler, doesn't include compute payment)\n /// maxDeployPayment is the max the user is willing to pay to DCP (as a\n /// Hold), in addition to the per-slice offer and associated scrape.\n /// Currently calculated as `deployCost = costPerKB *\n /// (JSON.stringify(job).length / 1024) // 1e-9 per kb`\n // @todo: figure this out / er nov 2018\n pd.maxDeployPayment = 1;\n \n /// payloadDetails.timing can be provided as an initial estimate of slice time, to\n /// give a more useful useful calculated heap value (heap.value is more or less\n /// dcc-per-millisecond)\n pd.timing = pd.timing || 1; \n }\n\n /** Escrow additional funds for this job\n * @access public\n * @param {number|BigNumber} fundsRequired - A number or BigNumber instance representing the funds to escrow for this job\n */\n async escrow (fundsRequired) {\n if ((typeof fundsRequired !== 'number' && !BigNumber.isBigNumber(fundsRequired))\n || fundsRequired <= 0 || !Number.isFinite(fundsRequired) || Number.isNaN(fundsRequired)) {\n throw new Error(`Job.escrow: fundsRequired must be a number greater than zero. (not ${fundsRequired})`);\n }\n\n const response = await this.bankConnection.send('embiggenFeeStructure', {\n feeStructureAddress: this[INTERNAL_SYMBOL].payloadDetails.feeStructureId,\n additionalEscrow: BigNumber(fundsRequired),\n fromAddress: this.paymentAccountKeystore.address,\n }, this.paymentAccountKeystore);\n\n this.receipt = response.payload;\n\n return this.receipt;\n }\n\n async _pack () {\n var retval = __webpack_require__(/*! ./node-modules */ \"./src/dcp-client/job/node-modules.js\").createModuleBundle(this[INTERNAL_SYMBOL].dependencies);\n return retval;\n }\n\n /** \n * Collect all of the dependencies together, throw them into a BravoJS\n * module which sideloads them as a side effect of declaration, and transmit\n * them to the package manager. Then we return the package descriptor object,\n * which is guaranteed to have only one file in it.\n *\n * @returns {object} with properties name and files[0]\n */\n async _publishLocalModules() {\n const { tempFile, hash, unresolved } = await this._pack();\n\n if (!tempFile) {\n return { unresolved };\n }\n\n const sideloaderFilename = tempFile.filename;\n const pkg = {\n name: `dcp-pkg-v1-localhost-${hash.toString('hex')}`,\n version: '1.0.0',\n files: {\n [sideloaderFilename]: `${sideloaderModuleIdentifier}.js`,\n },\n }\n\n await dcpPublish.publish(pkg);\n tempFile.remove();\n\n return { pkg, unresolved };\n }\n\n /**\n * Deploys the job to the scheduler.\n * @param {number | object} [slicePaymentOffer=compute.marketValue] - Amount\n * in DCC that the user is willing to pay per slice.\n * @param {Keystore} [paymentAccountKeystore=wallet.get] - An instance of the\n * Wallet API Keystore that's used as the payment account when executing the\n * job.\n * @param {object} [initialSliceProfile] - An object describing the cost the\n * user believes the average slice will incur.\n * @access public\n * @emits Job#accepted\n */\n async exec(slicePaymentOffer = __webpack_require__(/*! ../compute */ \"./src/dcp-client/compute.js\").compute.marketValue, paymentAccountKeystore, initialSliceProfile) {\n if (this[INTERNAL_SYMBOL].connected) {\n throw new Error('Exec called twice on the same job handle.');\n }\n\n /* eagerly connect to depedent services for better performance */\n this.eventSubscriber.eventRouterConnection.keepalive();\n this.deployConnection.keepalive();\n\n this.readyState = 'exec'\n this.emit('readystatechange', this.readyState)\n\n if (typeof slicePaymentOffer !== 'undefined') this.setSlicePaymentOffer(slicePaymentOffer);\n if (typeof initialSliceProfile !== 'undefined') this.initialSliceProfile = initialSliceProfile;\n if (typeof paymentAccountKeystore !== 'undefined') {\n /** XXX @todo deprecate use of ethereum wallet objects */\n if (typeof paymentAccountKeystore === 'object' && paymentAccountKeystore.hasOwnProperty('_privKey')) {\n console.warn('* deprecated API * - job.exec invoked with ethereum wallet object as paymentAccountKeystore') /* /wg oct 2019 */\n paymentAccountKeystore = paymentAccountKeystore._privKey\n }\n /** XXX @todo deprecate use of private keys */\n if (wallet.isPrivateKey(paymentAccountKeystore)) {\n console.warn('* deprecated API * - job.exec invoked with private key as paymentAccountKeystore') /* /wg dec 2019 */\n paymentAccountKeystore = await new wallet.Keystore(paymentAccountKeystore, '');\n }\n\n this.setPaymentAccountKeystore(paymentAccountKeystore)\n }\n\n // Unlock\n if (this[INTERNAL_SYMBOL].paymentAccountKeystore) {\n // Throws if they fail to unlock, we allow this since the keystore was set programmatically. \n await this[INTERNAL_SYMBOL].paymentAccountKeystore.unlock(undefined, parseFloat(dcpConfig.job.maxDeployTime));\n } else {\n // If not set programmatically, we keep trying to get an unlocked keystore ... forever.\n let locked = true;\n let safety = 0; // no while loop shall go unguarded\n let ks;\n do {\n ks = null;\n // custom message for the browser modal to denote the purpose of keystore submission\n let msg = `This application is requesting a keystore file to execute ${this.public.description || this.public.name || 'this job'}. Please upload the corresponding keystore file. If you upload a keystore file which has been encrypted with a passphrase, the application will not be able to use it until it prompts for a passphrase and you enter it.`;\n try {\n ks = await wallet.get({ contextId: this.contextId, jobName: this.public.name, msg});\n } catch (e) {\n if (e.code !== ClientModal.CancelErrorCode) throw e;\n };\n if (ks) {\n try {\n await ks.unlock(undefined, parseFloat(dcpConfig.job.maxDeployTime));\n locked = false;\n } catch (e) {\n const expectedCodes = [wallet.unlockFailErrorCode, ClientModal.CancelErrorCode];\n if (!expectedCodes.includes(e.code)) throw e;\n }\n }\n if (safety++ > 1000) throw new Error('EINFINITY: job.exec tried wallet.get more than 1000 times.')\n } while (locked);\n this.setPaymentAccountKeystore(ks)\n }\n\n // We either have a valid keystore + password or we have rejected by this point.\n if (!this.slicePaymentOffer) {\n throw new Error('A payment profile must be assigned before executing the job');\n } else {\n let pd = this[INTERNAL_SYMBOL].payloadDetails;\n pd.feeStructure = this[INTERNAL_SYMBOL].slicePaymentOffer.toFeeStructure(pd.data.length);\n }\n\n if (!this.address) {\n try {\n this.readyStateChange('init');\n await this[DEPLOY_JOB]();\n // localExec jobs are not entered in any compute group.\n if (!this[INTERNAL_SYMBOL].payloadDetails.localExec) {\n // Add this job to its currently-defined compute groups (if any)\n let failedComputeGroups = 0;\n for (const group of this.computeGroups) {\n try\n {\n await computeGroups.addJob(this.address, group);\n this.emit('addComputeGroup', group);\n } catch (error)\n {\n failedComputeGroups++;\n error.message = `Error from compute group with joinKey '${group.joinKey}': ` + error.message;\n this.emit('error', error)\n }\n }\n if (this.computeGroups.length > 0 && failedComputeGroups === this.computeGroups.length)\n {\n this.emit('error', 'Failed to join all specified compute groups, cancelling the job')\n this.cancel('No valid compute groups.');\n }\n\n computeGroups\n .closeServiceConnection()\n .catch((err) =>\n console.error(\n 'Warning: could not close compute groups service connection',\n err,\n ),\n );\n }\n\n this.readyState = 'deployed';\n this.emit('readystatechange', this.readyState);\n this.emit('accepted');\n } catch (error) {\n if (ON_BROWSER) {\n await ClientModal.alert(error, { title: 'Failed to deploy job!' });\n }\n\n throw error;\n }\n } else {\n await this[ADD_LISTENERS]();\n\n this.readyState = 'reconnected';\n this.emit('readystatechange', this.readyState);\n }\n\n this[ON_STATUS](this[INTERNAL_SYMBOL].payloadDetails.status);\n this[INTERNAL_SYMBOL].connected = true;\n\n return new Promise((resolve, reject) => {\n const onComplete = () => resolve(this.results);\n const onCancel = (event) => {\n const reason = `Job was cancelled by ${\n event.reason ? event.reason : 'Cancelled for unknown reason'\n }.`;\n\n /**\n * FIXME(DCP-1150): Remove this since normal cancel event is noisy\n * enough to not need stopped event too.\n */\n if (ON_BROWSER && !this[INTERNAL_SYMBOL].listeningForCancel)\n this[INTERNAL_SYMBOL].cancelAlert(event.reason);\n this.emit('cancel', event);\n reject(new DCPError(event.reason, event.code));\n };\n\n this[INTERNAL_SYMBOL].events.once('stopped', 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 try {\n result = kvin.unmarshal(result)\n } catch (error) {\n if (error.message === \"Invalid serialization format\"){\n //Means the slice was returned from a web-worker, which doesn't double-serialize the results like the sa-worker does\n }\n else {\n throw error\n }\n }\n\n if (this[INTERNAL_SYMBOL].results[ev.sliceNumber]) {\n const changed = JSON.stringify(this[INTERNAL_SYMBOL].results[ev.sliceNumber]) !== JSON.stringify(result);\n this.emit('duplicate-result', { sliceNumber: ev.sliceNumber, changed });\n }\n\n this[INTERNAL_SYMBOL].results[ev.sliceNumber] = result;\n this[INTERNAL_SYMBOL].resultsAvailable[ev.sliceNumber] = true;\n this.emit('result', { sliceNumber: ev.sliceNumber, result });\n this.emit('resultsUpdated');\n }\n\n /**\n * Receives status events from the scheduler, updates the local status object\n * and emits a 'status' event\n *\n * @param {object} ev - the status event received from\n * protocol.listen('/status/0xThisGenAdr')\n * @param {boolean} emitStatus - value indicating whether or not the status\n * event should be emitted\n */\n [ON_STATUS]({ runStatus, total, distributed, computed }, emitStatus = true) {\n Object.assign(this[INTERNAL_SYMBOL].status, {\n runStatus,\n total,\n distributed,\n computed,\n });\n\n if (emitStatus) {\n this.emit('status', this.status);\n }\n }\n\n /**\n * Sends a request to the scheduler to deploy the job.\n */\n async [DEPLOY_JOB] () {\n const { payloadDetails } = this[INTERNAL_SYMBOL];\n \n this[SNAPSHOT](); /* .payloadDetails now up to date */\n \n /* Send sideloader bundle to the package server */\n if (DCP_ENV.platform === 'nodejs' && this[INTERNAL_SYMBOL].dependencies.length) {\n let {pkg, unresolved} = await this._publishLocalModules();\n\n payloadDetails.dependencies = unresolved;\n if (pkg)\n payloadDetails.dependencies.push(pkg.name + '/' + sideloaderModuleIdentifier);\n }\n \n this.readyState = 'preauth';\n this.emit('readystatechange', this.readyState);\n\n /* eagerly connect to dependent services for better performance */\n computeGroups.keepAlive()\n \n if (!this.deployConnection || !this.deployConnection.state.is('established')) {\n try {\n await this.deployConnection.connect();\n }\n catch (error) {\n console.error('1046: connection failed', error);\n throw error;\n }\n }\n\n const adhocId = payloadDetails.uuid.slice(payloadDetails.uuid.length - 6, payloadDetails.uuid.length);\n const schedId = this.deployConnection.peerAddress;\n const myId = this.deployConnection.identity;\n const preauthToken = await bankUtil.preAuthorizePayment(schedId, payloadDetails.maxDeployPayment, this.paymentAccountKeystore);\n const { dataRange, dataValues, dataPattern, sliceCount } = marshalInputData(payloadDetails.data);\n\n this.readyState = 'deploying';\n this.emit('readystatechange', this.readyState);\n \n /* Payload format is documented in scheduler-v4/libexec/job-submit/operations/submit.js */\n const submitPayload = {\n owner: myId.address,\n paymentAccount: this.paymentAccountKeystore.address,\n priority: 0, // @nyi\n\n workFunctionURI: payloadDetails.workFunctionURI,\n uuid: payloadDetails.uuid,\n mvScalarSlicePayment: +payloadDetails.feeStructure.marketValue || 0, // @todo: improve feeStructure internals to better reflect v4\n absoluteSlicePayment: +payloadDetails.feeStructure.maxPerRequest || 0,\n requirePath: payloadDetails.requirePath,\n modulePath: payloadDetails.modulePath,\n dependencies: payloadDetails.dependencies,\n requirements: payloadDetails.requirements, /* capex */\n localExec: payloadDetails.localExec,\n\n description: payloadDetails.public.description || 'Discreetly making the world smarter',\n name: payloadDetails.public.name || 'Ad-Hoc Job' + adhocId,\n \n preauthToken, // XXXwg/er @todo: validate this after fleshing out the stub(s)\n\n resultStorageType: payloadDetails.resultStorageType, // @todo: implement other result types\n resultStorageDetails: payloadDetails.resultStorageDetails, // Content depends on resultStorageType\n resultStorageParams: payloadDetails.resultStorageParams, // Post params for off-prem storage\n dataRange,\n marshalizedDataValues: dataValues ? kvin.marshal(dataValues) : dataValues, // serialize input data to avoid passing them as json\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.argumentsURI = payloadDetails.arguments[0].href;\n } else if (payloadDetails.arguments instanceof RemoteDataSet) {\n submitPayload.argumentsURI = Array.from(payloadDetails.arguments).join();\n submitPayload.argumentsURI = 'RemoteDataSet:'+ submitPayload.argumentsURI;\n } else if (payloadDetails.arguments) {\n try {\n submitPayload.marshalizedArguments = kvin.marshal(Array.from(payloadDetails.arguments));\n } catch(e) {\n throw new Error(`Could not convert job arguments to Array (${e.message})`);\n }\n }\n\n // XXXpfr Excellent tracing.\n if (debugging('dcp-client')) {\n dumpObject(submitPayload, 'Submit: Job Index: Examine submitPayload', 128);\n }\n\n // Deploy the job!\n const deployed = await this.deployConnection.send('submit', submitPayload, myId);\n\n if (!deployed.success) {\n if(deployed.payload.code === 'ENOTFOUND') {\n throw new DCPError(`Failed to submit job to scheduler. Account: ${submitPayload.paymentAccount} was not found or does not have sufficient balance (${deployed.payload.info.deployCost} DCCs needed to deploy this job)`, deployed.payload);\n } else {\n throw new DCPError('Failed to submit job to scheduler', deployed.payload);\n }\n }\n\n this.address = payloadDetails.address = deployed.payload.job;\n this[INTERNAL_SYMBOL].deployCost = deployed.payload.deployCost;\n \n if (!payloadDetails.status)\n payloadDetails.status = {\n runStatus: null,\n total: 0,\n computed: 0,\n distributed: 0,\n };\n \n payloadDetails.runStatus = payloadDetails.status.runStatus = deployed.payload.status;\n payloadDetails.status.total = deployed.payload.lastSliceNumber;\n payloadDetails.running = true;\n \n this.readyState = 'listeners';\n\n this.emit('readystatechange', this.readyState);\n\n const listenersP = this[ADD_LISTENERS]();\n\n this[INTERNAL_SYMBOL].payloadDetails = {\n ...this[INTERNAL_SYMBOL].payloadDetails,\n ...payloadDetails,\n };\n \n return listenersP;\n }\n\n /**\n * This function is identical to exec, except that the job is executed locally\n * in the client.\n * @async\n * @param {number} cores - the number of local cores in which to execute the job.\n * @param {...any} args - The remaining arguments are identical to the arguments of exec\n * @return {Promise<ResultHandle>} - resolves with the results of the job, rejects on an error\n * @access public\n */\n localExec (cores = 1, ...args) {\n this[INTERNAL_SYMBOL].payloadDetails.localExec = true;\n\n let worker;\n this.on('accepted', () => {\n // Start a worker for this job\n worker = new Worker({\n localExec: true,\n jobAddresses: [this.address],\n paymentAddress: this[INTERNAL_SYMBOL].paymentAccountKeystore.address,\n maxWorkingSandboxes: cores,\n sandboxOptions: {\n ignoreNoProgress: true,\n SandboxConstructor: (DCP_ENV.platform === 'nodejs' &&\n __webpack_require__(/*! ../worker/evaluators */ \"./src/dcp-client/worker/evaluators/index.js\").nodeEvaluatorFactory())\n },\n });\n\n worker.start().catch((e) => {\n console.error(\"Failed to start worker for localExec:\");\n console.error(e.message);\n });\n });\n\n return this.exec(...args).finally(() => {\n if (worker) {\n setTimeout(() => {\n // stop the worker\n worker.stop(true);\n }, 3000);\n }\n });\n }\n\n /**\n * The current job status. Will have undefined values when the handle hasn't had exec called on it yet.\n * @access public\n * @readonly\n * @type {object}\n * @property {number} total Total number of slices in the job\n * @property {number} distributed Number of slices that have been distributed\n * @property {number} computed Number of slices that have completed execution (returned a result)\n * @property {string} runStatus Current runStatus of the job\n */\n get status () {\n // Shallow-copy to prevent modification\n return { ...this[INTERNAL_SYMBOL].status };\n }\n\n get requirePath() {\n return this[INTERNAL_SYMBOL].requirePath;\n }\n\n /**\n * This function specifies a module dependency (when the argument is a string)\n * or a list of dependencies (when the argument is an array) of the work\n * function. This function can be invoked multiple times before deployment.\n * @param {string | string[]} modulePaths - A string or array describing one\n * or more dependencies of the job.\n * @access public\n */\n requires(modulePaths) {\n if (\n typeof modulePaths !== 'string' &&\n (!Array.isArray(modulePaths) ||\n modulePaths.some((modulePath) => typeof modulePath !== 'string'))\n ) {\n throw new TypeError(\n 'The argument to dependencies is not a string or an array of strings',\n );\n } else if (modulePaths.length === 0) {\n throw new RangeError(\n 'The argument to dependencies cannot be an empty string or array',\n );\n } else if (\n Array.isArray(modulePaths) &&\n modulePaths.some((modulePath) => modulePath.length === 0)\n ) {\n throw new RangeError(\n 'The argument to dependencies cannot be an array containing an empty string',\n );\n }\n\n if (!Array.isArray(modulePaths)) {\n modulePaths = [modulePaths];\n }\n\n for (const modulePath of modulePaths) {\n if (modulePath[0] !== '.' && modulePath.indexOf('/') !== -1) {\n const modulePrefixRegEx = /^(.*)\\/.*?$/;\n const [, modulePrefix] = modulePath.match(modulePrefixRegEx);\n if (\n modulePrefix &&\n this[INTERNAL_SYMBOL].requirePath.indexOf(modulePrefix) === -1\n ) {\n this[INTERNAL_SYMBOL].requirePath.push(modulePrefix);\n }\n }\n\n this[INTERNAL_SYMBOL].dependencies.push(modulePath);\n }\n }\n\n get slicePaymentOffer () {\n return this[INTERNAL_SYMBOL].slicePaymentOffer;\n }\n\n /**\n * The keystore that will be used to pay for the job. Can be set with {@link Job.setPaymentAccountKeystore} or by providing a keystore to {@link Job.exec}.\n * @readonly\n * @access public\n * @type {module:dcp/wallet.AuthKeystore}\n */\n get paymentAccountKeystore () {\n return this[INTERNAL_SYMBOL].paymentAccountKeystore;\n }\n\n /** Set the account upon which funds will be drawn to pay for the job.\n * @param {module:dcp/wallet.AuthKeystore} keystore A keystore that representa a bank account.\n * @access public\n */\n setPaymentAccountKeystore (keystore) {\n if (this.address) {\n if (!keystore.address.eq(this[INTERNAL_SYMBOL].payloadDetails.owner)) {\n let message = \"Cannot change payment account after job has been deployed\";\n this.emit('EPERM', message);\n throw new Error(`EPERM: ${message}`);\n }\n }\n \n if (!(keystore instanceof wallet.Keystore)) {\n let e = new Error('Not an instance of Keystore: ' + keystore.toString())\n console.log(`Not an instance of Keystore: ${keystore}`)\n throw e\n }\n this[INTERNAL_SYMBOL].paymentAccountKeystore = keystore;\n }\n\n /** Set the slice payment offer. This is equivalent to the first argument to exec.\n * @param {number} slicePaymentOffer - The number of DCC the user is willing to pay to compute one slice of this job\n */\n setSlicePaymentOffer (slicePaymentOffer) {\n this[INTERNAL_SYMBOL].slicePaymentOffer = new SlicePaymentOffer(slicePaymentOffer);\n }\n\n /**\n * @param {URL} locationUrl - A URL object \n * @param {object} postParams - An object with any parameters that a user would like to be passed to a \n * remote result location. This object is capable of carry API keys for S3, \n * DropBox, etc. These parameters are passed as parameters in an \n * application/x-www-form-urlencoded request.\n */\n setResultStorage(locationUrl, postParams) {\n if (locationUrl instanceof URL || locationUrl instanceof DcpURL) {\n this[INTERNAL_SYMBOL].resultStorageDetails = locationUrl;\n } else {\n const e = new Error('Not an instance of a DCP URL: ' + locationUrl);\n console.log('Not an instance of a DCP URL ' + locationUrl);\n throw e;\n }\n\n // resultStorageParams contains any post params required for off-prem storage\n if (typeof postParams !== 'undefined' && typeof postParams === 'object' ) {\n this[INTERNAL_SYMBOL].resultStorageParams = postParams;\n } else {\n const e = new Error('Not an instance of a object: ' + postParams);\n console.log('Not an instance of an object ' + postParams);\n throw e;\n } \n\n // Some type of object here\n this[INTERNAL_SYMBOL].resultStorageType = 'pattern';\n } \n}\n\n/**\n * @typedef {object} MarshaledInputData\n * @property {any[]} [dataValues]\n * @property {string} [dataPattern]\n * @property {RangeObject} [dataRange]\n * @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?");
5103
5139
 
5104
5140
  /***/ }),
5105
5141
 
@@ -5122,7 +5158,7 @@ eval("/**\n * @file node-modules.js Node-specific support for sen
5122
5158
  /*! no static exports found */
5123
5159
  /***/ (function(module, exports, __webpack_require__) {
5124
5160
 
5125
- 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\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\n\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 reult 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] = kvin.unmarshal(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?");
5126
5162
 
5127
5163
  /***/ }),
5128
5164
 
@@ -5166,7 +5202,7 @@ eval("/**\n * @file remote-data-pattern.js - Contains the class definition wit
5166
5202
  /*! no static exports found */
5167
5203
  /***/ (function(module, exports, __webpack_require__) {
5168
5204
 
5169
- eval("/**\n * @file remote-data-set.js - Contains the class definition for (array of) URI's.\n * \n * @see {@link https://kingsds.atlassian.net/browse/DCP-1478|Jira Issue}\n * @author Bryan Hoang <bryan@kingsds.network>\n * Nazila Akhavan <nazila@kingsds.network>\n * @date Oct. 2020, Sep. 2021\n * \n */\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\")\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp-client');\n\nconst debug = (...args) => {\n if (debugging('compute:remote-data-set')) {\n args.unshift('dcp-client:compute:remote-data-set');\n console.debug(...args);\n }\n};\n\n/**\n * Instances of this class store URIs representing data, arguments, etc. The\n * primary purpose of this class, at this time, is to allow us to easily\n * differentiate between Arrays of stuff and Arrays of URIs which point at\n * stuff. This job logs the contents of the URLs described by a URI, not the\n * URLs themselves:\n *\n * @example\n * const a = [ 'http://abc.com/1.json', 'http://def.com/two.json' ];\n * const { RemoteDataSet } = require('dcp/compute');\n * let b = new RemoteDataSet(...a);\n * OR( b = new RemoteDataSet(a))\n * const job = compute.for(b, function(el) { console.log(el)});\n * job.on('console', console.log);\n * await job.exec();\n */\nmodule.exports.RemoteDataSet = function (uris)\n{\n if (!Array.isArray(uris))\n uris = Array.from(arguments);\n this.length = uris.length;\n\n for (let i = 0; i < uris.length; i++)\n {\n if (typeof uris[i] === \"string\")\n this[i] = new URL(uris[i]).href;\n else if (DcpURL.isURL(uris[i]))\n this[i] = uris[i].href;\n else\n throw new DCPError(`invalid uri at position ${i + 1}: ${uris[i]}`);\n }\n}\n\nmodule.exports.RemoteDataSet.isRemoteDataSet = function (r)\n{\n if (r instanceof module.exports.RemoteDataSet)\n return true;\n\n return false;\n}\n\nmodule.exports.RemoteDataSet.isProtoRemoteDataSetLike = function (r)\n{\n if (Array.isArray(r))\n {\n for (let i = 0; i < r.length; i++)\n {\n if (!DcpURL.isURL(r[i]))\n return false;\n }\n return true;\n }\n\n return false;\n}\n \nmodule.exports.RemoteDataSet.prototype = new Array(); \n\n//# sourceURL=webpack:///./src/dcp-client/remote-data-set.js?");
5205
+ eval("/**\n * @file remote-data-set.js - Contains the class definition for (array of) URI's.\n * \n * @see {@link https://kingsds.atlassian.net/browse/DCP-1478|Jira Issue}\n * @author Bryan Hoang <bryan@kingsds.network>\n * Nazila Akhavan <nazila@kingsds.network>\n * @date Oct. 2020, Sep. 2021\n * \n */\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\")\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp-client');\n\nconst debug = (...args) => {\n if (debugging('compute:remote-data-set')) {\n args.unshift('dcp-client:compute:remote-data-set');\n console.debug(...args);\n }\n};\n\n/**\n * Instances of this class store URIs representing data, arguments, etc. The\n * primary purpose of this class, at this time, is to allow us to easily\n * differentiate between Arrays of stuff and Arrays of URIs which point at\n * stuff. This job logs the contents of the URLs described by a URI, not the\n * URLs themselves:\n *\n * @example\n * const a = [ 'http://abc.com/1.json', 'http://def.com/two.json' ];\n * const { RemoteDataSet } = require('dcp/compute');\n * let b = new RemoteDataSet(...a);\n * OR( b = new RemoteDataSet(a))\n * const job = compute.for(b, function(el) { console.log(el)});\n * job.on('console', console.log);\n * await job.exec();\n */\nmodule.exports.RemoteDataSet = function (uris)\n{\n if (!Array.isArray(uris))\n uris = Array.from(arguments);\n this.length = uris.length;\n\n for (let i = 0; i < uris.length; i++)\n {\n if (typeof uris[i] === \"string\" && uris[i].startsWith('data:'))\n this[i] = new URL(uris[i]);\n else if (typeof uris[i] === \"string\")\n this[i] = new URL(uris[i]).href;\n else if (DcpURL.isURL(uris[i]))\n this[i] = uris[i].href;\n else\n throw new DCPError(`invalid uri at position ${i + 1}: ${uris[i]}`);\n }\n}\n\nmodule.exports.RemoteDataSet.isRemoteDataSet = function (r)\n{\n if (r instanceof module.exports.RemoteDataSet)\n return true;\n\n return false;\n}\n\nmodule.exports.RemoteDataSet.isProtoRemoteDataSetLike = function (r)\n{\n if (Array.isArray(r))\n {\n for (let i = 0; i < r.length; i++)\n {\n if (!DcpURL.isURL(r[i]))\n return false;\n }\n return true;\n }\n\n return false;\n}\n \nmodule.exports.RemoteDataSet.prototype = new Array(); \n\n//# sourceURL=webpack:///./src/dcp-client/remote-data-set.js?");
5170
5206
 
5171
5207
  /***/ }),
5172
5208
 
@@ -5199,7 +5235,13 @@ eval("/* WEBPACK VAR INJECTION */(function(process) {/**\n * @file /src/sch
5199
5235
  /*! no static exports found */
5200
5236
  /***/ (function(module, exports, __webpack_require__) {
5201
5237
 
5202
- eval("/**\n * @file /src/schedmsg/schedmsg-web.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date March 2020\n *\n * This is the SchedMsg implementation for commands that are browser-specific\n * or have browser-specific behaviour.\n */\n\nconst { SchedMsg } = __webpack_require__(/*! ./schedmsg */ \"./src/dcp-client/schedmsg/schedmsg.js\");\n\nclass SchedMsgWeb extends SchedMsg {\n constructor(worker) {\n super(worker);\n this.modal = null;\n\n this.registerHandler('announce', this.onAnnouncement.bind(this));\n this.registerHandler('openPopup', this.onOpenPopup.bind(this));\n this.registerHandler('reload', this.onReload.bind(this));\n }\n\n onAnnouncement({ message }) {\n if (this.modal) {\n this.modal.close();\n }\n\n this.modal = window.userInterface.alert('Announcement', '' /* subtitle */, message,\n /* onClose */ () => this.modal = null);\n }\n\n onOpenPopup({ href }) {\n window.open(href);\n }\n\n onReload() {\n const hash = window.location.hash;\n\n let newUrl = window.location.href.replace(/#.*/, '');\n newUrl += (newUrl.indexOf('?') === -1 ? '?' : '&');\n newUrl += 'dcp=e4988ba0b1c993e062fc7c536188bda3ea4de883,' + 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?");
5238
+ <<<<<<< HEAD
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
5203
5245
 
5204
5246
  /***/ }),
5205
5247
 
@@ -5210,7 +5252,7 @@ eval("/**\n * @file /src/schedmsg/schedmsg-web.js\n * @author Ryan Rossi
5210
5252
  /*! no static exports found */
5211
5253
  /***/ (function(module, exports, __webpack_require__) {
5212
5254
 
5213
- eval("/**\n * @file /src/schedmsg/schedmsg.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date March 2020\n *\n * This is the client logic for subscribing to and handling SchedMsg events.\n * It inherits from PropagatingEventEmitter, and will register default event\n * callbacks for the commands, but they can be overridden by registering\n * additional event callbacks that return 'false'. This will prevent the default\n * behaviour from occuring.\n */\n\nconst { EventSubscriber } = __webpack_require__(/*! dcp/events/event-subscriber */ \"./src/events/event-subscriber.js\");\nconst { PropagatingEventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\n\n/** @typedef {import('../worker').Worker} Worker */\n\n/**\n * SchedMsg is the class that will subscribe to receive commands from the scheduler.\n * These commands could be used to reload the worker, or broacast a message.\n * \n * SchedMsg extends the PropagatingEventEmitter.\n * When a command is received from the scheduler, it will be emitted on the Schedsg\n * instance. SchedMsg will register default handlers for these commands, but they can\n * be overridden by listening for the event and returning `false` from callback you provide.\n * @access public\n */\nclass SchedMsg extends PropagatingEventEmitter {\n /**\n * This command instructs the worker to immediately stop working, and can optionally disable the worker to prevent restarting. The user will need to manually intervene to restart the worker.\n * \n * @event SchedMsg#kill\n * @access public\n * @type {object}\n * @property {boolean} temporary When false, the worker will be disabled.\n *//**\n * This command instructs the worker to restart, e.g. call `worker.stop()` then `worker.start()`.\n * \n * @event SchedMsg#restart\n * @access public\n *//**\n * This command instructs the worker to stop working on a specific job.\n * \n * @event SchedMsg#remove\n * @access public\n * @type {object}\n * @property {string} jobAddress The address of the job to stop working on.\n *//**\n * This command is an announcement from the scheduler, the provided message should be displayed to the user (modal on web, console on node).\n * \n * @event SchedMsg#announce\n * @access public\n * @type {object}\n * @property {string} message The message to be displayed to the user.\n *//**\n * This command instructs the worker to \"hard\" reload, in the browser this will trigger a page refresh and in node it will exit the process.\n * \n * @event SchedMsg#reload\n * @access public\n *//**\n * This *web-only* command will open a new webpage to the provided URL.\n * \n * @event SchedMsg#openPopup\n * @access public\n * @type {object}\n * @property {string} href The URL to open the new page to.\n */\n\n /**\n * @constructor\n * @param {Worker} worker \n */\n constructor(worker) {\n super('SchedMsg');\n\n this.eventSubscriber = new EventSubscriber(this);\n\n this.worker = worker;\n this.handlers = {};\n\n this.registerHandler('kill', (opts) => this.onKill(opts));\n this.registerHandler('restart', (opts) => this.onRestart(opts));\n this.registerHandler('remove', (opts) => this.onRemove(opts));\n this.registerHandler('addPriorityJob', (opts) => this.onAddPriorityJob(opts));\n this.registerHandler('removePriorityJob', (opts) => this.onRemovePriorityJob(opts));\n this.registerHandler('pingWorkerId', (opts) => this.onPingWorkerId(opts));\n }\n\n async start() {\n if (!this.eventSubscriber.eventRouterConnection.state.is('established')) {\n await this.eventSubscriber.eventRouterConnection.connect();\n }\n const workerId = this.worker.supervisor.workerId || this.worker.supervisor.workerOpaqueId;\n const identityAddress = this.eventSubscriber.eventRouterConnection.identity.address;\n this.identityAddress = identityAddress;\n this.workerId = workerId;\n this.eventIntercepts = {}\n this.eventIntercepts['schedmsg::broadcast'] = this.onMessage.bind(this);\n this.eventIntercepts['schedmsg::command'] = this.onMessage.bind(this);\n\n await this.eventSubscriber.subscribe('schedmsg::broadcast', {\n filter: {\n $or: [\n {target: identityAddress},\n {target: workerId},\n ],\n },\n });\n await this.eventSubscriber.subscribe('schedmsg::command', {\n filter: {\n $or: [\n {target: identityAddress},\n {target: workerId},\n ],\n },\n });\n }\n\n async stop() {\n this.eventSubscriber.unsubscribe('schedmsg::broadcast')\n this.eventSubscriber.unsubscribe('schedmsg::command')\n\n await this.eventSubscriber.close();\n }\n\n registerHandler(command, callback) {\n if (this.listenerCount(command) > 0) {\n throw new Error(`There is a handler already registered for command '${command}'`);\n }\n\n this.on(command, callback);\n }\n\n onMessage(msg) {\n const { command, payload } = msg;\n\n if (this.listenerCount(command) > 0) {\n this.emit(command, payload);\n } else {\n console.warn(`No SchedMsg handler registered for command '${command}'`);\n }\n }\n\n onKill({ temporary }) {\n console.log(\n \"!!!!!!!!!!!!!!!\\n\\n\",\n \"Kill command received from scheduler. Stopping worker...\",\n \"\\n\\n!!!!!!!!!!!!!!!\");\n\n if (this.worker.working) {\n this.worker.stop(true);\n }\n\n if (!temporary) {\n this.worker.constructor.disableWorker();\n }\n }\n\n onRestart() {\n if (this.worker.working) {\n this.worker.stop();\n setTimeout(() => {\n this.worker.start();\n }, Math.random() * 10000 /* stagger workers coming back online */);\n }\n }\n\n onRemove({ jobAddress }) {\n for (let sandbox of this.worker.supervisor.workingSandboxes) {\n if (sandbox.jobAddress === jobAddress) {\n this.worker.supervisor.returnSandbox(sandbox, true);\n }\n }\n }\n\n async onAddPriorityJob({ jobAddress, immediate }) {\n console.log('Received addPriorityJob schedmsg, adding job:', jobAddress);\n\n const supervisor = this.worker.supervisor;\n supervisor.options.jobAddresses.push(jobAddress);\n\n if (immediate) {\n // return all slices that aren't in the priority job list\n await Promise.all(\n supervisor.slices\n .filter((slice) => !supervisor.options.jobAddresses.includes(slice.jobAddress))\n .map((slice) => supervisor.returnSlice(slice))\n ).catch((e) => {\n console.error(\"Error while returning slice before handling addPriorityJob schedmsg:\");\n console.error(e);\n });\n\n // stop all current sandboxes that aren't working on priority slices\n for (let sandbox of supervisor.workingSandboxes) {\n if (!supervisor.options.jobAddresses.includes(sandbox.jobAddress)) {\n supervisor.returnSandbox(sandbox, true);\n }\n }\n }\n\n this.eventIntercepts['stop'] = () => {\n supervisor.options.jobAddresses =\n supervisor.options.jobAddresses.filter((Address) => Address !== jobAddress);\n }\n this.eventSubscriber.subscribe('job::optionalEvent', {\n filter: { job: jobAddress , eventName: 'stop' },\n })\n .catch((e) => {\n console.error(\"Failed to subscribe to stop event for addPriorityJob schedmsg:\");\n console.error(e);\n });\n }\n\n async onRemovePriorityJob({ jobAddress, immediate }) {\n console.log('Received removePriorityJob schedmsg, removing job:', jobAddress);\n\n const supervisor = this.worker.supervisor;\n supervisor.options.jobAddresses =\n supervisor.options.jobAddresses.filter((address) => address !== jobAddress);\n\n if (immediate) {\n // return all slices belonging to this job\n await Promise.all(\n supervisor.slices\n .filter((slice) => slice.jobAddress === jobAddress)\n .map((slice) => supervisor.returnSlice(slice))\n ).catch((e) => {\n console.error(\"Error while returning slice for immediately removed job:\");\n console.error(e);\n });\n\n // stop all current sandboxes that are working on this job\n for (let sandbox of supervisor.workingSandboxes) {\n if (sandbox.jobAddress === jobAddress) {\n supervisor.returnSandbox(sandbox, true);\n }\n }\n }\n }\n async onPingWorkerId() {\n const workerIdentity = this.eventSubscriber.eventRouterConnection.identity;\n let thanatosConnection;\n try {\n thanatosConnection = new protocolV4.Connection(dcpConfig.scheduler.services.thanatos.location, workerIdentity);\n await thanatosConnection.send('worker-pong', {\n workerId: this.workerId,\n });\n } finally {\n await thanatosConnection.close();\n }\n }\n}\n\nexports.SchedMsg = SchedMsg;\n\n\n//# sourceURL=webpack:///./src/dcp-client/schedmsg/schedmsg.js?");
5255
+ eval("/**\n * @file /src/schedmsg/schedmsg.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date March 2020\n *\n * This is the client logic for subscribing to and handling SchedMsg events.\n * It inherits from PropagatingEventEmitter, and will register default event\n * callbacks for the commands, but they can be overridden by registering\n * additional event callbacks that return 'false'. This will prevent the default\n * behaviour from occuring.\n */\n\nconst { EventSubscriber } = __webpack_require__(/*! dcp/events/event-subscriber */ \"./src/events/event-subscriber.js\");\nconst { PropagatingEventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\n\n/** @typedef {import('../worker').Worker} Worker */\n\n/**\n * SchedMsg is the class that will subscribe to receive commands from the scheduler.\n * These commands could be used to reload the worker, or broacast a message.\n * \n * SchedMsg extends the PropagatingEventEmitter.\n * When a command is received from the scheduler, it will be emitted on the Schedsg\n * instance. SchedMsg will register default handlers for these commands, but they can\n * be overridden by listening for the event and returning `false` from callback you provide.\n * @access public\n */\nclass SchedMsg extends PropagatingEventEmitter {\n /**\n * This command instructs the worker to immediately stop working, and can optionally disable the worker to prevent restarting. The user will need to manually intervene to restart the worker.\n * \n * @event SchedMsg#kill\n * @access public\n * @type {object}\n * @property {boolean} temporary When false, the worker will be disabled.\n *//**\n * This command instructs the worker to restart, e.g. call `worker.stop()` then `worker.start()`.\n * \n * @event SchedMsg#restart\n * @access public\n *//**\n * This command instructs the worker to stop working on a specific job.\n * \n * @event SchedMsg#remove\n * @access public\n * @type {object}\n * @property {string} jobAddress The address of the job to stop working on.\n *//**\n * This command is an announcement from the scheduler, the provided message should be displayed to the user (modal on web, console on node).\n * \n * @event SchedMsg#announce\n * @access public\n * @type {object}\n * @property {string} message The message to be displayed to the user.\n *//**\n * This command instructs the worker to \"hard\" reload, in the browser this will trigger a page refresh and in node it will exit the process.\n * \n * @event SchedMsg#reload\n * @access public\n *//**\n * This *web-only* command will open a new webpage to the provided URL.\n * \n * @event SchedMsg#openPopup\n * @access public\n * @type {object}\n * @property {string} href The URL to open the new page to.\n */\n\n /**\n * @constructor\n * @param {Worker} worker \n */\n constructor(worker) {\n super('SchedMsg');\n\n this.eventSubscriber = new EventSubscriber(this);\n\n this.worker = worker;\n this.handlers = {};\n\n this.registerHandler('kill', (opts) => this.onKill(opts));\n this.registerHandler('restart', (opts) => this.onRestart(opts));\n this.registerHandler('remove', (opts) => this.onRemove(opts));\n this.registerHandler('addPriorityJob', (opts) => this.onAddPriorityJob(opts));\n this.registerHandler('removePriorityJob', (opts) => this.onRemovePriorityJob(opts));\n this.registerHandler('pingWorkerId', (opts) => this.onPingWorkerId(opts));\n }\n\n async start() {\n const workerId = this.worker.supervisor.workerId || this.worker.supervisor.workerOpaqueId;\n const identityAddress = this.eventSubscriber.eventRouterConnection.identity.address;\n this.identityAddress = identityAddress;\n this.workerId = workerId;\n this.eventIntercepts = {}\n this.eventIntercepts['schedmsg::broadcast'] = this.onMessage.bind(this);\n this.eventIntercepts['schedmsg::command'] = this.onMessage.bind(this);\n\n await this.eventSubscriber.subscribe('schedmsg::broadcast', {\n filter: {\n $or: [\n {target: identityAddress},\n {target: workerId},\n ],\n },\n });\n await this.eventSubscriber.subscribe('schedmsg::command', {\n filter: {\n $or: [\n {target: identityAddress},\n {target: workerId},\n ],\n },\n });\n }\n\n async stop() {\n this.eventSubscriber.unsubscribe('schedmsg::broadcast')\n this.eventSubscriber.unsubscribe('schedmsg::command')\n\n await this.eventSubscriber.close();\n }\n\n registerHandler(command, callback) {\n if (this.listenerCount(command) > 0) {\n throw new Error(`There is a handler already registered for command '${command}'`);\n }\n\n this.on(command, callback);\n }\n\n onMessage(msg) {\n const { command, payload } = msg;\n\n if (this.listenerCount(command) > 0) {\n this.emit(command, payload);\n } else {\n console.warn(`No SchedMsg handler registered for command '${command}'`);\n }\n }\n\n onKill({ temporary }) {\n console.log(\n \"!!!!!!!!!!!!!!!\\n\\n\",\n \"Kill command received from scheduler. Stopping worker...\",\n \"\\n\\n!!!!!!!!!!!!!!!\");\n\n if (this.worker.working) {\n this.worker.stop(true);\n }\n\n if (!temporary) {\n this.worker.constructor.disableWorker();\n }\n }\n\n onRestart() {\n if (this.worker.working) {\n this.worker.stop();\n setTimeout(() => {\n this.worker.start();\n }, Math.random() * 10000 /* stagger workers coming back online */);\n }\n }\n\n onRemove({ jobAddress }) {\n for (let sandbox of this.worker.supervisor.workingSandboxes) {\n if (sandbox.jobAddress === jobAddress) {\n this.worker.supervisor.returnSandbox(sandbox, true);\n }\n }\n }\n\n async onAddPriorityJob({ jobAddress, immediate }) {\n console.log('Received addPriorityJob schedmsg, adding job:', jobAddress);\n\n const supervisor = this.worker.supervisor;\n supervisor.options.jobAddresses.push(jobAddress);\n\n if (immediate) {\n // return all slices that aren't in the priority job list\n await Promise.all(\n supervisor.slices\n .filter((slice) => !supervisor.options.jobAddresses.includes(slice.jobAddress))\n .map((slice) => supervisor.returnSlice(slice))\n ).catch((e) => {\n console.error(\"Error while returning slice before handling addPriorityJob schedmsg:\");\n console.error(e);\n });\n\n // stop all current sandboxes that aren't working on priority slices\n for (let sandbox of supervisor.workingSandboxes) {\n if (!supervisor.options.jobAddresses.includes(sandbox.jobAddress)) {\n supervisor.returnSandbox(sandbox, true);\n }\n }\n }\n\n this.eventIntercepts['stop'] = () => {\n supervisor.options.jobAddresses =\n supervisor.options.jobAddresses.filter((Address) => Address !== jobAddress);\n }\n this.eventSubscriber.subscribe('job::optionalEvent', {\n filter: { job: jobAddress , eventName: 'stop' },\n })\n .catch((e) => {\n console.error(\"Failed to subscribe to stop event for addPriorityJob schedmsg:\");\n console.error(e);\n });\n }\n\n async onRemovePriorityJob({ jobAddress, immediate }) {\n console.log('Received removePriorityJob schedmsg, removing job:', jobAddress);\n\n const supervisor = this.worker.supervisor;\n supervisor.options.jobAddresses =\n supervisor.options.jobAddresses.filter((address) => address !== jobAddress);\n\n if (immediate) {\n // return all slices belonging to this job\n await Promise.all(\n supervisor.slices\n .filter((slice) => slice.jobAddress === jobAddress)\n .map((slice) => supervisor.returnSlice(slice))\n ).catch((e) => {\n console.error(\"Error while returning slice for immediately removed job:\");\n console.error(e);\n });\n\n // stop all current sandboxes that are working on this job\n for (let sandbox of supervisor.workingSandboxes) {\n if (sandbox.jobAddress === jobAddress) {\n supervisor.returnSandbox(sandbox, true);\n }\n }\n }\n }\n async onPingWorkerId() {\n const workerIdentity = this.eventSubscriber.eventRouterConnection.identity;\n let thanatosConnection;\n try {\n thanatosConnection = new protocolV4.Connection(dcpConfig.scheduler.services.thanatos.location, workerIdentity);\n await thanatosConnection.send('worker-pong', {\n workerId: this.workerId,\n });\n } finally {\n await thanatosConnection.close();\n }\n }\n}\n\nexports.SchedMsg = SchedMsg;\n\n\n//# sourceURL=webpack:///./src/dcp-client/schedmsg/schedmsg.js?");
5214
5256
 
5215
5257
  /***/ }),
5216
5258
 
@@ -5323,7 +5365,7 @@ eval("/** \n * @file webpack-native-bridge.js\n * Glue logic
5323
5365
  /*! no static exports found */
5324
5366
  /***/ (function(module, exports, __webpack_require__) {
5325
5367
 
5326
- eval("/**\n * @file evaluators/browser.js\n * Library which implements a browser-based DCP Evaluator. This evaluator\n * uses Web Workers to implement the base evaluator, and passes messages into\n * the worker via the postMessage/onMessage interface. The sandbox code is\n * loaded from the evaluator via the importScripts() mechanism. That mechanism\n * is disabled by the sandbox before the sandbox runs any untrusted code from\n * the Supervisor.\n *\n * @author Andrew Fryer, andrewf@kingsds.networka\n * Wes Garland, wes@kingsds.network\n * @date Aug 2020, Mar 2021\n */\n\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\n\nexports.sandboxSetupDefs = __webpack_require__(/*! dcp-client/generated/sandbox-definitions.json */ \"./node_modules/dcp-client/generated/sandbox-definitions.json\").browser;\n\n/**\n * Generate the list of URLs that must be fetched by the Web Worker instance in order for it \n * to become a Sandbox that the Supervisor can talk to.\n *\n * The sandbox code is defined by the current dcp-client package, which is a dependency of dcp;\n * when this file is built into the dcp-client package, the file list is entrained in the\n * webpack bundle and used here, unless overridden in exports.sandboxSetupDefs.\n *\n * The sandboxSetupDefs contains a list of strings. Each string is a dependency, and they\n * represent scripts which must be run in order.\n *\n * If the dependency string contains a '/' character, the dependency is treated as a module in an\n * NPM package; this means that we can require.resolve() it to find its location authoritatively.\n *\n * If the dependency string does not contain a '/' character, the dependency is assumed to be\n * located in the require.resolve('dcp-client/libexec/sandbox') directory.\n *\n * Note: we fake require.resolve here by using knowledge of DCP web servers' file layouts, since\n * there is no good way to do that from inside webpack in web browser. Any third-party\n * serving up a custom worker will need to either have the same uri path->filesystem \n * mapping, or they will need to monkey-patch this function.\n */\nexports.generateSandboxURLs = function browserEvaluator$$generateSandboxURLs() {\n const { currentScript } = document;\n var urls = [];\n var baseHref = currentScript ? new DcpURL(currentScript.src).origin : dcpConfig.portal.location.origin;\n var baseUrl = new DcpURL(baseHref);\n\n for (let def of exports.sandboxSetupDefs) {\n if (def.match(/\\//))\n urls.push(baseUrl.resolve(def)); \n else\n urls.push(baseUrl.resolve('dcp-client/libexec/sandbox/' + def + '.js'))\n }\n\n return urls;\n}\n\n/**\n * Instantiate a new Web Worker which will execute the sandbox startup code,\n * creating a DCP Evaluator.\n */\nexports.BrowserEvaluator = function BrowserEvaluator(_options) {\n var options = Object.assign({}, _options);\n\n if (typeof Worker === 'undefined') {\n throw new Error(\"Can't instantiate a browser evaluator, `Worker` is not defined.\");\n }\n\n /* Create a script fragment which knows how to fetch the code to start the sandbox, and send it to the Web Worker\n * (evaluator). Which pieces of JavaScript to fetch from the server via importScripts() is determined from \n * generateSandboxURLs(), which in turn comes from the DCP Worker code which was embedded in this webpack bundle.\n */\n const initSandboxCode = `importScripts(${exports.generateSandboxURLs().map(script => '\"' + script + '\"').join()})`;\n const blob = window.URL.createObjectURL(new Blob([initSandboxCode], { type: 'application/javascript' }));\n return new Worker(blob, options);\n};\n\n\n//# sourceURL=webpack:///./src/dcp-client/worker/evaluators/browser.js?");
5368
+ eval("/**\n * @file evaluators/browser.js\n * Library which implements a browser-based DCP Evaluator. This evaluator\n * uses Web Workers to implement the base evaluator, and passes messages into\n * the worker via the postMessage/onMessage interface. The sandbox code is\n * loaded from the evaluator via the importScripts() mechanism. That mechanism\n * is disabled by the sandbox before the sandbox runs any untrusted code from\n * the Supervisor.\n *\n * @author Andrew Fryer, andrewf@kingsds.networka\n * Wes Garland, wes@kingsds.network\n * @date Aug 2020, Mar 2021\n */\n\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\n\nexports.sandboxSetupDefs = __webpack_require__(/*! dcp-client/generated/sandbox-definitions.json */ \"./node_modules/dcp-client/generated/sandbox-definitions.json\").browser;\n\n/**\n * Generate the list of URLs that must be fetched by the Web Worker instance in order for it \n * to become a Sandbox that the Supervisor can talk to.\n *\n * The sandbox code is defined by the current dcp-client package, which is a dependency of dcp;\n * when this file is built into the dcp-client package, the file list is entrained in the\n * webpack bundle and used here, unless overridden in exports.sandboxSetupDefs.\n *\n * The sandboxSetupDefs contains a list of strings. Each string is a dependency, and they\n * represent scripts which must be run in order.\n *\n * If the dependency string contains a '/' character, the dependency is treated as a module in an\n * NPM package; this means that we can require.resolve() it to find its location authoritatively.\n *\n * If the dependency string does not contain a '/' character, the dependency is assumed to be\n * located in the require.resolve('dcp-client/libexec/sandbox') directory.\n *\n * Note: we fake require.resolve here by using knowledge of DCP web servers' file layouts, since\n * there is no good way to do that from inside webpack in web browser. Any third-party\n * serving up a custom worker will need to either have the same uri path->filesystem \n * mapping, or they will need to monkey-patch this function.\n */\nexports.generateSandboxURLs = function browserEvaluator$$generateSandboxURLs() {\n const { currentScript } = document;\n var urls = [];\n var baseHref = currentScript ? new DcpURL(currentScript.src).origin : dcpConfig.portal.location.origin;\n var baseUrl = new DcpURL(baseHref);\n\n for (let def of exports.sandboxSetupDefs) {\n if (def.match(/\\//))\n urls.push(baseUrl.resolve(def)); \n else\n urls.push(baseUrl.resolve('dcp-client/libexec/sandbox/' + def + '.js'))\n }\n\n return urls;\n}\n\n/**\n * Instantiate a new Web Worker which will execute the sandbox startup code,\n * creating a DCP Evaluator.\n */\nexports.BrowserEvaluator = function BrowserEvaluator(_options) {\n var options = Object.assign({}, _options);\n var urls = exports.generateSandboxURLs();\n\n if (typeof Worker === 'undefined') {\n throw new Error(\"Can't instantiate a browser evaluator, `Worker` is not defined.\");\n }\n\n /* Create a script fragment which knows how to fetch the code to start the sandbox, and send it to the Web Worker\n * (evaluator). Which pieces of JavaScript to fetch from the server via importScripts() is determined from \n * generateSandboxURLs(), which in turn comes from the DCP Worker code which was embedded in this webpack bundle.\n */\n\n // Cache busting\n if (dcpConfig.build === 'debug')\n urls = urls.map(url => url + '?' + Date.now());\n\n const initSandboxCode = `importScripts(${urls.map(script => '\"' + script + '\"').join()})`;\n const blob = window.URL.createObjectURL(new Blob([initSandboxCode], { type: 'application/javascript' }));\n return new Worker(blob, options);\n};\n\n\n//# sourceURL=webpack:///./src/dcp-client/worker/evaluators/browser.js?");
5327
5369
 
5328
5370
  /***/ }),
5329
5371
 
@@ -5345,7 +5387,7 @@ eval("exports.BrowserEvaluator = __webpack_require__(/*! ./browser */ \"./src/dc
5345
5387
  /*! no static exports found */
5346
5388
  /***/ (function(module, exports, __webpack_require__) {
5347
5389
 
5348
- eval("/**\n * @file node-localExec.js Node-specific support for creating/running localExec\n * jobs with the Compute API.\n * @author Wes Garland, wes@kingsds.network\n * @date May 2020\n */\n\n/* global dcpConfig */\n// @ts-nocheck\n\nconst utils = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst isDebugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp-client')('localExec');\n\nconst debug = (...args) => {\n if (isDebugging) {\n console.debug('dcp-client:localExec', ...args);\n }\n};\n\n/**\n * Factory function which makes NodeEvaluator constructors based on the current configuration.\n */\nexports.nodeEvaluatorFactory = function evaluators$$nodeEvaluatorFactory(inTesting = false)\n{\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n const path = __webpack_require__(/*! path */ \"./node_modules/path-browserify/index.js\");\n var controlCodeFiles; /* Evaluator's control code establishes the sandbox environment and postMessage/onmessage implementation */\n\n if (DCP_ENV.platform !== 'nodejs') {\n throw new Error(`Can't create NodeEvaluator constructor: platform is '${DCP_ENV.platform}'; must be 'nodejs'.`);\n }\n\n /**\n * The `standaloneWorker.js` and `evaluator-node.js` currently resides in\n * `dcp-client` and that this code can be executed from source or from the\n * dcp-client bundle, we need to detect whether we're running in either mode.\n *\n * If we're running from source, `dcp-rtlink` has most likely been initialized\n * so we can resolve `dcp-client` in the `node_modules` folder of the `dcp`\n * repo.\n *\n * Else if we're running from the bundle, `dcp-rtlink` has not been\n * initialized so this code is in the bundle being evaluated in the context of\n * `dcp-client/index.js`. In that case, we need to switch to relative paths to\n * resolve the modules.\n *\n * @todo TODO(bryan-hoang): Improve detection of whether the dcp-rtlink module has\n * been initialized. e.g. Exposed an API through the dcp-rtlink module\n */\n const hasRtlinkBeenInitialized =\n Object.keys(requireNative('module')._cache)\n .map((key) => path.basename(key))\n .indexOf('rtLink.js') !== -1;\n const dcpClientModulePrefix = hasRtlinkBeenInitialized ? 'dcp-client' : '.';\n const { workerFactory } = requireNative(\n `${dcpClientModulePrefix}/lib/standaloneWorker`,\n );\n const { Evaluator } = requireNative(`${dcpClientModulePrefix}/libexec/evaluator-node`);\n \n /**\n * @param socket \"server\" socket for the node evaluator\n */\n function localExecConnectHandler(socket)\n {\n new Evaluator(socket, socket, controlCodeFiles);\n }\n\n /**\n * Constructor for a NodeEvaluator. An instance of this class becomes the SandboxConstructor \n * in the implementation of job::localExec() when running under NodeJS.\n *\n * @todo Eliminate the extra wrapper here. It was previously necessary when we had to\n * capture the factory argument vector in a closure. It should be possible now to\n * implement this as a single function.\n */\n function NodeEvaluator()\n {\n const child_process = requireNative('child_process');\n const process = requireNative('process');\n const env = Object.assign({}, process.env, {\n /**\n * Assume the user is okay with the security implications of executing\n * their work locally. Needed to start the script without a warning\n * and delay. All other environment properties are inherited from the\n * calling process.\n */\n I_WANT_AN_INSECURE_DCP_WORKER: 'badly',\n DCP_SCHEDULER_LOCATION: `${dcpConfig.scheduler.location.href}`,\n });\n var command;\n var pipe = __webpack_require__(/*! dcp/utils/tmpfiles */ \"./src/utils/tmpfiles.js\").createTempSocket(undefined, 'localExec-pipe', localExecConnectHandler);\n var options = {\n readStream: pipe,\n writeStream: pipe\n };\n\n /**\n * First, the set up files for a node evaluator are needed, so the output of\n * the `dcp-evaluator-start` script from the `dcp-worker` package will be\n * used to be the authoritative source of the locations of the files.\n */\n try {\n command = requireNative.resolve('dcp-worker/bin/dcp-evaluator-start');\n } catch (e) {\n /* Sometimes require can't resolve this module through `npm link`, even though it is\n * the \"right way\" to resolve a peer dependency, so we try to resolve from the main\n * module's POV also. This makes use of the 'main' section in dcp-worker/package.json.\n */\n const mainModuleRequire = requireNative('module').createRequireFromPath(\n `${requireNative.main.path}/`,\n );\n try{\n command = mainModuleRequire.resolve('dcp-worker/bin/dcp-evaluator-start');\n } catch (err) {\n throw new DCPError('Could not load dcp-worker', 'ENOWORKER');\n }\n }\n\n if (!command)\n throw new Error('Could not locate dcp-worker/bin/dcp-evaluator-start');\n\n /* Ask dcp-evaluator-start for the sandbox setup files for a node evaluator */\n let sandboxType = 'node';\n if (inTesting)\n sandboxType = 'nodeTesting';\n delete env.DCP_DEBUG; /* don't polute stdio pipeline with debug status messages */\n const child = child_process.spawnSync(command, [ `--sandbox-type=${sandboxType}`, '-FJ' ],\n {\n encoding: 'utf8',\n env\n },\n );\n\n debug('Raw localExec evaluator output:', child.stdout);\n if (child.error) {\n console.log(\"ERROR: \",child.error);\n }\n\n if (child.status !== 0) {\n console.error('\\n',child.stderr, child.stdout);\n process.exit(child.status)\n }\n\n controlCodeFiles = JSON.parse(child.stdout);\n debug('Read controlCodeFiles:', controlCodeFiles);\n\n return new (workerFactory(options))();\n }\n\n NodeEvaluator.prototype = new EventEmitter('nodeLocalExec');\n return NodeEvaluator;\n}\n\n\n//# sourceURL=webpack:///./src/dcp-client/worker/evaluators/node-localExec.js?");
5390
+ eval("/**\n * @file node-localExec.js Node-specific support for creating/running localExec\n * jobs with the Compute API.\n * @author Wes Garland, wes@kingsds.network\n * @date May 2020\n */\n\n/* global dcpConfig */\n// @ts-nocheck\n\nconst utils = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst isDebugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp-client')('localExec');\n\nconst debug = (...args) => {\n if (isDebugging) {\n console.debug('dcp-client:localExec', ...args);\n }\n};\n\n/**\n * Factory function which makes NodeEvaluator constructors based on the current configuration.\n */\nexports.nodeEvaluatorFactory = function evaluators$$nodeEvaluatorFactory(inTesting = false)\n{\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n const path = __webpack_require__(/*! path */ \"./node_modules/path-browserify/index.js\");\n var controlCodeFiles; /* Evaluator's control code establishes the sandbox environment and postMessage/onmessage implementation */\n\n if (DCP_ENV.platform !== 'nodejs') {\n throw new Error(`Can't create NodeEvaluator constructor: platform is '${DCP_ENV.platform}'; must be 'nodejs'.`);\n }\n\n /**\n * The `standaloneWorker.js` and `evaluator-node.js` currently resides in\n * `dcp-client` and that this code can be executed from source or from the\n * dcp-client bundle, we need to detect whether we're running in either mode.\n *\n * If we're running from source, `dcp-rtlink` has most likely been initialized\n * so we can resolve `dcp-client` in the `node_modules` folder of the `dcp`\n * repo.\n *\n * Else if we're running from the bundle, `dcp-rtlink` has not been\n * initialized so this code is in the bundle being evaluated in the context of\n * `dcp-client/index.js`. In that case, we need to switch to relative paths to\n * resolve the modules.\n *\n * @todo TODO(bryan-hoang): Improve detection of whether the dcp-rtlink module has\n * been initialized. e.g. Exposed an API through the dcp-rtlink module\n */\n const hasRtlinkBeenInitialized =\n Object.keys(requireNative('module')._cache)\n .map((key) => path.basename(key))\n .indexOf('rtLink.js') !== -1;\n const dcpClientModulePrefix = hasRtlinkBeenInitialized ? 'dcp-client' : '.';\n const { workerFactory } = requireNative(\n `${dcpClientModulePrefix}/lib/standaloneWorker`,\n );\n const { Evaluator } = requireNative(`${dcpClientModulePrefix}/libexec/evaluator-node`);\n \n /**\n * @param socket \"server\" socket for the node evaluator\n */\n function localExecConnectHandler(socket)\n {\n new Evaluator(socket, socket, controlCodeFiles);\n }\n\n /**\n * Constructor for a NodeEvaluator. An instance of this class becomes the SandboxConstructor \n * in the implementation of job::localExec() when running under NodeJS.\n *\n * @todo Eliminate the extra wrapper here. It was previously necessary when we had to\n * capture the factory argument vector in a closure. It should be possible now to\n * implement this as a single function.\n */\n function NodeEvaluator()\n {\n const child_process = requireNative('child_process');\n const process = requireNative('process');\n const env = Object.assign({}, process.env, {\n /**\n * Assume the user is okay with the security implications of executing\n * their work locally. Needed to start the script without a warning\n * and delay. All other environment properties are inherited from the\n * calling process.\n */\n I_WANT_AN_INSECURE_DCP_WORKER: 'badly',\n DCP_SCHEDULER_LOCATION: `${dcpConfig.scheduler.location.href}`,\n });\n var command;\n var pipe = __webpack_require__(/*! dcp/utils/tmpfiles */ \"./src/utils/tmpfiles.js\").createTempSocket(undefined, 'localExec-pipe', localExecConnectHandler);\n var options = {\n readStream: pipe,\n writeStream: pipe\n };\n\n /**\n * First, the set up files for a node evaluator are needed, so the output of\n * the `dcp-evaluator-start` script from the `dcp-worker` package will be\n * used to be the authoritative source of the locations of the files.\n */\n try {\n command = requireNative.resolve('dcp-worker/bin/dcp-evaluator-start');\n } catch (e) {\n /* Sometimes require can't resolve this module through `npm link`, even though it is\n * the \"right way\" to resolve a peer dependency, so we try to resolve from the main\n * module's POV also. This makes use of the 'main' section in dcp-worker/package.json.\n */\n const mainModuleRequire = requireNative('module').createRequireFromPath(\n `${requireNative.main.path}/`,\n );\n try{\n command = mainModuleRequire.resolve('dcp-worker/bin/dcp-evaluator-start');\n } catch (err) {\n throw new DCPError('Could not load dcp-worker', 'ENOWORKER');\n }\n }\n\n if (!command)\n throw new Error('Could not locate dcp-worker/bin/dcp-evaluator-start');\n\n /* Ask dcp-evaluator-start for the sandbox setup files for a node evaluator */\n let sandboxType = 'node';\n if (inTesting)\n sandboxType = 'nodeTesting';\n delete env.DCP_DEBUG; /* don't polute stdio pipeline with debug status messages */\n const child = child_process.spawnSync(process.argv0,\n [command, `--sandbox-type=${sandboxType}`, '-FJ' ], { encoding: 'utf8', env });\n\n debug('Raw localExec evaluator output:', child.stdout);\n if (child.error) {\n console.log(\"ERROR: \",child.error);\n }\n\n if (child.status !== 0) {\n console.error('\\n',child.stderr, child.stdout);\n process.exit(child.status)\n }\n\n controlCodeFiles = JSON.parse(child.stdout);\n debug('Read controlCodeFiles:', controlCodeFiles);\n\n return new (workerFactory(options))();\n }\n\n NodeEvaluator.prototype = new EventEmitter('nodeLocalExec');\n return NodeEvaluator;\n}\n\n\n//# sourceURL=webpack:///./src/dcp-client/worker/evaluators/node-localExec.js?");
5349
5391
 
5350
5392
  /***/ }),
5351
5393
 
@@ -5368,7 +5410,7 @@ eval("/**\n * @file This module implements the Worker API, used to create worker
5368
5410
  /***/ (function(module, exports, __webpack_require__) {
5369
5411
 
5370
5412
  "use strict";
5371
- eval("// NOTE - need timeout/postmessage function\n/**\n * @file dcp-client/worker/sandbox.js\n *\n * A sandbox that when constructed and assigned can do work for\n * a distributed slice. A sandbox runs for a single slice at a time.\n *\n * Usage:\n * let sandbox = new Sandbox()\n * await sandbox.start()\n * let results = await sandbox.run(slice)\n *\n * Debug flags:\n * Sandbox.debugWork = true // - turns off 30 second timeout to let user debug sandbox innards more easily\n * Sandbox.debugState = true // - logs all state transitions for this sandbox\n * Sandbox.debugEvents = true // - logs all events received from the sandbox\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * Ryan Rossiter, ryan@kingsds.network\n * @date May 2019\n * @module sandbox\n */\n/* global dcpConfig */\n// @ts-check\n\n\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('worker');\nconst { assert, assertEq3 } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\nconst scopedKvin = new kvin.KVIN({Object: ({}).constructor,\n Array: ([]).constructor, \n Function: (()=>{}).constructor});\n\nlet timeDilation = 1;\nif (DCP_ENV.platform === 'nodejs') {\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n /** Make timers 10x slower when running in niim */\n timeDilation = (requireNative('module')._cache.niim instanceof requireNative('module').Module) ? 10 : 1;\n}\n\n/**\n * Wraps console.debug to emulate debug module prefixing messages on npm.\n * @param {...any} args\n */\nconst debug = (...args) => {\n if (debugging()) {\n console.debug('dcp-client:worker:sandbox', ...args);\n }\n};\n\nconst nanoid = __webpack_require__(/*! nanoid/non-secure */ \"./node_modules/nanoid/non-secure/index.js\");\n\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { fetchURI, encodeDataURI } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n\n// Sandbox states\nconst UNREADY = 'UNREADY' // No Sandbox (web worker, saworker, etc) has been constructed yet\nconst READYING = 'READYING' // Sandbox is being constructed and environment (bravojs, env) is being set up\nconst READY_FOR_ASSIGN = 'READY_FOR_ASSIGN' // Sandbox is ready to be assigned\nconst ASSIGNING = 'ASSIGNING' // Sandbox is running through assigning steps\nconst ASSIGNED = 'ASSIGNED' // Sandbox is assigned but not working\nconst WORKING = 'WORKING' // Sandbox is working\nconst TERMINATED = 'TERMINATED'\nconst EVAL_RESULT_PREFIX = 'evalResult::';\n\nclass SandboxError extends Error {}\nclass NoProgressError extends SandboxError { constructor(...args) { super(...args); this.errorCode = 'ENOPROGRESS'; } }\nclass SliceTooSlowError extends SandboxError { constructor(...args) { super(...args); this.errorCode = 'ESLICETOOSLOW'; } }\nclass UncaughtExceptionError extends SandboxError { constructor(...args) { super(...args); this.errorCode = 'EUNCAUGHTERROR'; } }\n\n/** @typedef {import('dcp/common/dcp-events').EventEmitter} EventEmitter */\n/** @typedef {import('./slice').Slice} Slice */\n/** @typedef {import('./supervisor-cache').SupervisorCache} SupervisorCache */\n/** @typedef {*} opaqueId */\n\n/**\n * @access public\n * @typedef {object} SandboxOptions\n * @constructor {function} [SandboxConstructor]\n * @property {boolean} [ignoreNoProgress] - When true, the sandbox will not be stopped for not calling progress\n */\n\nclass Sandbox extends EventEmitter {\n /**\n * A Sandbox (i.e. a worker sandbox) which executes distributed slices.\n *\n * @constructor\n * @param {SupervisorCache} cache\n * @param {SandboxOptions} options\n */\n constructor (cache, options, origins) {\n super('Sandbox');\n /** @type {SupervisorCache} */\n this.supervisorCache = cache;\n /** @type {SandboxOptions} */\n this.options = {\n ignoreNoProgress: false,\n ...options,\n SandboxConstructor: options.SandboxConstructor ||\n __webpack_require__(/*! ./evaluators */ \"./src/dcp-client/worker/evaluators/index.js\").BrowserEvaluator,\n }\n this.allowedOrigins = origins;\n\n /** @type {opaqueId} */\n this.jobAddress = null;\n /** @type {object} */\n this.evaluatorHandle = null;\n /** @type {object} */\n this.capabilities = null;\n /** @type {EventEmitter} */\n this.ee = new EventEmitter('SandboxInternal')\n\n /** @type {string} */\n this._state = UNREADY;\n /** @type {boolean} */\n this.allocated = false;\n /** @type {number?} */\n this.progress = 100;\n /** @type {object} */\n this.progressReports = null;\n /** @type {object} */\n this.progressTimeout = null;\n /** @type {object} */\n this.sliceTimeout = null;\n\n /** @type {Slice} */\n this.slice = null;\n\n /** @type {number?} */\n this.started = null;\n /** @type {number?} */\n this.sliceStartTime = null;\n /** @type {boolean} */\n this.requiresGPU = false;\n /** @type {string|URL} */\n this.packageURL = dcpConfig.packageManager.location\n /** @type {number?} */\n this.id = Sandbox.getNewId();\n\n this.ringMessageHandlers = [\n this.handleRing0Message,\n this.handleRing1Message,\n this.handleRing2Message,\n this.handleRing3Message,\n ];\n\n this.resetSliceTimeReport();\n }\n\n static getNewId() {\n return Sandbox.idCounter++;\n }\n\n get state () {\n return this._state\n }\n\n set state (value) {\n if (Sandbox.debugState) {\n console.debug(`sandbox - changing state of ${this.id}... ${this._state} -> ${value}`)\n }\n\n if (this.state === TERMINATED && value !== TERMINATED) {\n // For safety!\n throw new Error(`Sandbox set state violation, attepted to change state from ${this.state} to ${value}`);\n }\n\n this._state = value;\n }\n\n get isReadyForAssign () {\n return this.state === READY_FOR_ASSIGN;\n }\n\n get isAssigned () {\n return this.state === ASSIGNED;\n }\n\n get isWorking () {\n return this.state === WORKING;\n }\n\n get isTerminated () {\n return this.state === TERMINATED;\n }\n\n changeWorkingToAssigned () {\n if (this.isWorking)\n this.state = ASSIGNED;\n }\n\n setIsAssigning () {\n this.state = ASSIGNING;\n }\n\n /**\n * Readies the sandbox. This will result in the sandbox being ready and not assigned,\n * it will need to be assigned with a job before it is able to do work.\n *\n * @todo maybe preload specific modules or let the cache pass in what modules to load?\n * @throws on failure to ready\n */\n async start(delay = 0) {\n this.started = Date.now();\n this.state = READYING;\n\n if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay * timeDilation));\n\n try {\n // RING 0\n this.evaluatorHandle = new this.options.SandboxConstructor({\n name: `DCP Sandbox #${this.id}`,\n });\n this.evaluatorHandle.onmessage = this.onmessage.bind(this);\n this.evaluatorHandle.onerror = this.onerror.bind(this);\n\n // Now in RING 1\n\n // Now in RING 2\n await this.describe();\n this.state = READY_FOR_ASSIGN;\n this.emit('ready', this);\n } catch (error) {\n console.warn('Failed to start the sandbox -', error.message);\n this.terminate(false);\n throw error;\n }\n }\n\n /**\n * This will assign the sandbox with a job, loading its sandbox code\n * into the sandbox.\n *\n * @param {string} jobAddress The address of the job to assign to\n * @throws on initialization failure\n */\n async assign(jobAddress) {\n this.jobAddress = jobAddress;\n this.job = await this.supervisorCache.fetchJob(jobAddress, this.allowedOrigins);\n\n assertEq3(this.job.address, jobAddress);\n assert(typeof this.job === 'object');\n assert(typeof this.job.requirements === 'object');\n assert(Array.isArray(this.job.dependencies));\n assert(Array.isArray(this.job.requirePath));\n\n // Extract public data from job, with defaults\n this.public = Object.assign({\n name: `Anonymous Job ${this.job.address.slice(0, 6)}`,\n description: 'Discreetly helping make the world smarter.',\n link: 'https://distributed.computer/about',\n }, this.job.public);\n\n // Future: We may want other filename tags for appliances // RR Nov 2019\n\n // Important: The order of applying requirements before loading the sandbox code\n // is important for modules and sandbox code to set globals over the whitelist.\n await this.applySandboxRequirements(this.job.requirements);\n await this.assignEvaluator();\n this.state = ASSIGNED;\n }\n\n async assignEvaluator() {\n debug('Begin assigning job to evaluator');\n const ceci = this;\n\n return new Promise(function sandbox$$assignEvaluatorPromise(resolve, reject) {\n if(!DCP_ENV.isBrowserPlatform) {\n if (!ceci.job.arguments._serializeVerId) {\n ceci.job.arguments = scopedKvin.marshal(ceci.job.arguments);\n }\n }\n \n const message = {\n request: 'assign',\n job: ceci.job,\n sandboxConfig: {\n worker: dcpConfig.worker,\n },\n };\n\n const onSuccess = (event) => {\n // eslint-disable-next-line no-use-before-define\n ceci.ee.removeListener('reject', onFailListener);\n ceci.emit('assigned', event.jobAddress);\n debug('Job assigned to evaluator');\n resolve();\n };\n\n const onFail = (error) => {\n // eslint-disable-next-line no-use-before-define\n ceci.ee.removeListener('assigned', onSuccessListener);\n reject(error);\n };\n\n const onSuccessListener = ceci.ee.once('assigned', onSuccess);\n const onFailListener = ceci.ee.once('reject', onFail);\n ceci.evaluatorHandle.postMessage(message);\n });\n }\n\n /**\n * Evaluates a string inside the sandbox.\n *\n * @param {string} code - the code to evaluate in the sandbox\n * @param {string} filename - the name of the 'file' to help with debugging,\n * no longer working though?\n * @returns {Promise} - resolves with eval result on success, rejects\n * otherwise\n */\n eval(code, filename) {\n var ceci = this;\n \n return new Promise(function sandbox$$eval$Promise(resolve, reject) {\n let msgId = nanoid();\n let msg = {\n request: 'eval',\n data: code,\n filename,\n msgId, \n }\n\n const eventId = EVAL_RESULT_PREFIX + msgId;\n\n let onSuccess = (event) => {\n ceci.ee.removeListener('reject', onFailListener)\n resolve(event)\n }\n\n let onFail = (event) => {\n ceci.ee.removeListener(eventId, onSuccessListener)\n reject(event)\n }\n\n let onSuccessListener = ceci.ee.once(eventId, onSuccess);\n let onFailListener = ceci.ee.once('reject', onFail)\n\n ceci.evaluatorHandle.postMessage(msg)\n })\n }\n\n /**\n * Resets the state of the bootstrap, without resetting the sandbox function if assigned.\n * Mostly used to reset the progress status before reusing a sandbox on another slice.\n * Must be called after @start.\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n resetSandboxState () {\n var ceci = this;\n\n return new Promise(function sandbox$resetSandboxStatePromise(resolve, reject) {\n let successCb, failTimeout;\n let msg = {\n request: 'resetState',\n };\n\n successCb = ceci.ee.once('resetStateDone', function sandbox$resetSandboxState$success () {\n if (failTimeout === false)\n return; /* already rejected */\n clearTimeout(failTimeout);\n failTimeout = false;\n resolve();\n });\n\n failTimeout = setTimeout(function sandbox$resetSandboxState$fail() {\n if (failTimeout === false)\n return; /* already resolved */\n \n ceci.ee.removeListener('resetStateDone', successCb);\n ceci.terminate(false);\n failTimeout = false;\n\n reject(new Error('resetState never received resetStateDone event from sandbox'));\n }, 3000 * timeDilation); /* XXXwg need tuneable */\n\n assert(ceci.evaluatorHandle); // It is possible that ceci.terminate nulls out evaluatorHandle before getting here.\n ceci.evaluatorHandle.postMessage(msg);\n });\n }\n\n /**\n * Clear all timers that are set inside the sandbox (evaluator) environment.\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n clearSandboxTimers() {\n var ceci = this;\n \n return new Promise(function sandbox$clearSandboxTimersPromise(resolve, reject) {\n let successCb, failTimeout;\n let msg = {\n request: 'clearTimers',\n };\n\n successCb = ceci.ee.once('clearTimersDone', function sandbox$clearSandboxTimers$success() {\n if (failTimeout === false)\n return; /* already rejected */\n clearTimeout(failTimeout);\n failTimeout = false;\n resolve();\n });\n\n failTimeout = setTimeout(function sanbox$clearSandboxTimers$fail() {\n if (failTimeout === false)\n return; /* already resolved */\n \n ceci.ee.removeListener('clearTimersDone', successCb);\n ceci.terminate(false);\n failTimeout = false;\n \n reject(new Error('clearTimers never received clearTimersDone event from sandbox'));\n }, 3000 * timeDilation); /* XXXwg need tuneable */\n\n if (ceci.evaluatorHandle) // Sometimes ceci.terminate nulls out evaluatorHandle before getting here.\n ceci.evaluatorHandle.postMessage(msg);\n });\n }\n\n /**\n * Sends a post message to describe its capabilities.\n *\n * Side effect: Sets the capabilities property of the current sandbox.\n *\n * @returns {Promise} Resolves with the sandbox's capabilities. Rejects with\n * an error saying a response was not received.\n * @memberof Sandbox\n */\n describe() {\n debug('Beginning to describe evaluator');\n var ceci = this;\n \n return new Promise(function sandbox$describePromise(resolve, reject) {\n if (ceci.evaluatorHandle === null) {\n return reject(new Error('Evaluator has not been initialized.'));\n }\n\n /**\n * Opted to create a flag for the describe response being received so that\n * we don't have to *hoist* the timeout's id to clear it in the response\n * handler.\n */\n let didReceiveDescribeResponse = false;\n const describeResponseHandler = ceci.ee.once('describe', (data) => {\n didReceiveDescribeResponse = true;\n const { capabilities } = data;\n if (typeof capabilities === 'undefined') {\n reject(\n new Error('Did not receive capabilities from describe response.'),\n );\n }\n ceci.capabilities = capabilities;\n\n // Currently only used in tests. May use the event in the future.\n ceci.emit('described', capabilities);\n debug('Evaluator has been described');\n resolve(capabilities);\n });\n const describeResponseFailedHandler = () => {\n if (!didReceiveDescribeResponse) {\n ceci.ee.removeListener('describe', describeResponseHandler);\n ceci.terminate(false);\n reject(\n new Error(\n 'Describe message timed-out. No describe response was received from the describe command.',\n ),\n );\n }\n };\n\n const message = {\n request: 'describe',\n };\n\n // Arbitrarily set the waiting time.\n setTimeout(describeResponseFailedHandler, 6000 * timeDilation); /* XXXwg need tuneable */\n assert(ceci.evaluatorHandle); // It is possible that ceci.terminate nulls out evaluatorHandle before getting here.\n ceci.evaluatorHandle.postMessage(message);\n });\n }\n\n /**\n * Passes the job's requirements object into the sandbox so that the global\n * access lists can be updated accordingly.\n *\n * e.g. disallow access to OffscreenCanvas without\n * environment.offscreenCanvas=true present.\n *\n * Must be called after @start.\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n applySandboxRequirements(requirements) {\n var ceci = this;\n \n return new Promise(function sandbox$applySandboxRequirementsPromise(resolve, reject) {\n const message = {\n requirements,\n request: 'applyRequirements',\n };\n let wereRequirementsApplied = false;\n\n const successCb = ceci.ee.once(\n 'applyRequirementsDone',\n function sandbox$applyRequirements$success() {\n wereRequirementsApplied = true;\n resolve();\n },\n );\n\n assert(typeof message.requirements === 'object');\n ceci.evaluatorHandle.postMessage(message);\n\n setTimeout(function sandbox$finishApplySandboxRequirements() {\n if (!wereRequirementsApplied) {\n ceci.ee.removeListener('applyRequirementsDone', successCb);\n ceci.terminate(false);\n reject(\n new Error(\n 'applyRequirements never received applyRequirementsDone response from sandbox',\n ),\n );\n }\n }, 3000 * timeDilation); /* XXXwg needs tunable */\n });\n }\n\n /**\n * Executes a slice received from the supervisor.\n * Must be called after @start.\n *\n * @param {Slice} slice - bare minimum data required for the job/job code to be executed on\n * @param {number} [delay = 0] the delay that this method should wait before beginning work, used to avoid starting all sandboxes at once\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n\n async work (slice, delay = 0) {\n var ceci = this;\n\n if (!ceci.isAssigned) {\n throw new Error(\"Sandbox.run: Sandbox is not ready to work, state=\" + ceci.state);\n }\n\n ceci.state = WORKING;\n ceci.slice = slice;\n assert(slice);\n\n // cf. DCP-1720\n this.resetSliceTimeReport();\n \n // Now wait for the delay if provided, prevents many sandboxes starting at once from crashing the supervisor\n if (delay > 0) await new Promise(resolve => setTimeout(resolve, (delay + 1) * timeDilation));\n if (!ceci.isWorking) return; // sandbox.terminate could have been called during the delay timeout\n\n // Prepare the sandbox to begin work\n // will be replaced by `assign` message that should be called before emitting a `work` message\n if (ceci.jobAddress !== slice.jobAddress) {\n throw new Error(`Sandbox.run: Sandbox is already assigned and jobAddress doesn't match previous (${ceci.jobAddress} !== ${slice.jobAddress})`);\n }\n\n let sliceHnd = { job: ceci.public, sandbox: ceci };\n await ceci.resetSandboxState();\n if (!ceci.slice) {\n console.error(`Slice for job ${ceci.jobAddress} vanished during work initialization - aborting`);\n return;\n }\n\n let inputDatum;\n let dataError = false;\n try {\n inputDatum = await fetchURI(\n ceci.slice.datumUri,\n this.allowedOrigins,\n dcpConfig.worker.allowOrigins.fetchData,\n );\n } catch (err) {\n dataError = err;\n dataError.errorCode = 'EUNCAUGHTERROR'\n ceci.emit('workEmit', {\n eventName: 'error',\n payload: {\n message: dataError.message,\n stack:dataError.stack,\n name: ceci.public.name\n }\n });\n }\n\n debugging('sandbox') && debug(`Fetched datum: ${inputDatum}`);\n\n if (!ceci.slice) {\n console.error(`Slice for job ${ceci.jobAddress} vanished after data fetch - aborting`);\n return;\n }\n\n ceci.resetProgressTimeout();\n ceci.resetSliceTimeout();\n\n return new Promise(function sandbox$$workPromise(resolve, reject) {\n let onSuccess, onFail\n\n onSuccess = ceci.ee.once('resolve', function sandbox$$work$success (event) {\n ceci.ee.removeListener('reject', onFail)\n resolve(event)\n }.bind(ceci))\n\n onFail = ceci.ee.once('reject', function sandbox$$work$fail (err) {\n ceci.ee.removeListener('resolve', onSuccess)\n reject(err)\n }.bind(ceci))\n\n ceci.sliceStartTime = Date.now();\n ceci.progress = null;\n ceci.progressReports = {\n last: undefined,\n lastDeterministic: undefined,\n };\n\n ceci.resetProgressTimeout();\n ceci.resetSliceTimeout();\n ceci.emit('sliceStart', sliceHnd); /** @todo: remove */\n ceci.emit('start', sliceHnd);\n \n if(dataError){\n ceci.ee.removeListener('resolve', onSuccess);\n ceci.ee.removeListener('reject', onFail);\n setTimeout(() => reject(dataError), 0)\n\n } else {\n // Try to only pass the data required - keeps the message size low\n // and prevents sandbox code from snooping on internal data \n if(!DCP_ENV.isBrowserPlatform) {\n inputDatum = scopedKvin.marshal(inputDatum);\n }\n ceci.evaluatorHandle.postMessage({\n request: 'main',\n data: inputDatum,\n })\n }\n })\n .then(async function sandbox$$work$then(event) {\n // Ceci is the complete callback when the slice completes\n // prevent any hanging timers from being fired\n await ceci.clearSandboxTimers();\n\n // TODO: Should sliceHnd just be replaced with ceci.public?\n ceci.emit('slice', sliceHnd); /** @todo: decide which event is right */\n ceci.emit('sliceFinish', event);\n ceci.emit('complete', event);\n\n ceci.changeWorkingToAssigned();\n ceci.slice = false;\n return event;\n })\n .catch((err) => {\n // Ceci is the reject callback for when the slice throws an error\n ceci.terminate(false);\n\n ceci.emit('error', err, 'slice');\n ceci.emit('sliceError', sliceHnd, err); /** @todo: remove */\n\n if (err instanceof NoProgressError) {\n ceci.emit('workEmit', {\n eventName: 'noProgress',\n payload: {\n timestamp: Date.now() - ceci.sliceStartTime,\n data: ceci.slice.data, /** XXXpfr @todo slice.data is legacy from v3; figure out what to put here. */\n progressReports: ceci.progressReports,\n }\n });\n }\n throw err;\n })\n .finally(function sandbox$$work$finally() {\n ceci.emit('sliceEnd', sliceHnd); /** @todo: remove */\n ceci.emit('end', sliceHnd);\n });\n }\n\n resetProgressTimeout() {\n if (this.progressTimeout) {\n clearTimeout(this.progressTimeout);\n this.progressTimeout = null;\n }\n\n this.progressTimeout = setTimeout(() => {\n if (this.options.ignoreNoProgress) {\n return console.warn(\"ENOPROGRESS silenced by localExec: In a remote worker, this slice would be stopped for not calling progress frequently enough.\");\n }\n\n this.ee.emit('reject', new NoProgressError(`No progress event was received in the last ${dcpConfig.worker.sandbox.progressTimeout / 1000} seconds.`));\n }, +dcpConfig.worker.sandbox.progressTimeout * timeDilation);\n }\n\n resetSliceTimeout() {\n if (this.sliceTimeout) clearTimeout(this.sliceTimeout);\n\n this.sliceTimeout = setTimeout(() => {\n if (Sandbox.debugWork) return console.warn(\"Sandbox.debugWork: Ignoring slice timeout\");\n\n this.ee.emit('reject', new SliceTooSlowError(`Slice took longer than ${dcpConfig.worker.sandbox.sliceTimeout / 1000} seconds.`));\n }, +dcpConfig.worker.sandbox.sliceTimeout * timeDilation);\n }\n \n async handleRing0Message(data) {\n debugging('event:ring-0') && debug('event:ring-0', data);\n //handling a true ring 0 message\n switch (data.request) {\n case 'sandboxLoaded':\n // emit externally\n this.emit('sandboxLoaded', this)\n this.emit('workerLoaded', this) /** @todo: remove */\n break;\n\n case 'scriptLoaded':\n // emit externally\n this.emit('scriptLoaded', data);\n \n if(data.result !== \"success\") {\n this.onerror(data);\n }\n break;\n \n case 'clearTimersDone':\n this.ee.emit(data.request, data);\n break;\n case 'measurement':\n this.updateTime(data);\n break;\n case 'error':\n // Warning: rejecting here with just event.data.error causes issues\n // where the reject handlers modify the object so it interferes with the\n // workEmit event payload, wrapping in an Error instance copies the values\n let e = new Error(\n data.error.message,\n data.error.fileName,\n data.error.lineNumber);\n e.stack = data.error.stack;\n e.name = data.error.name;\n \n if (this.ee.listenerCount('reject') > 0) {\n this.ee.emit('reject', e);\n } else {\n // This will happen if the error is thrown during initialization\n throw e;\n }\n\n break;\n default:\n let error = new Error('Received unhandled request from sandbox: ' + data.request + '\\n\\t' + JSON.stringify(data));\n console.error(error);\n break; \n }\n }\n\n async handleRing1Message(data) {\n switch (data.request) {\n case 'applyRequirementsDone':\n // emit internally\n this.ee.emit(data.request, data)\n break;\n default:\n let error = new Error('Received unhandled request from sandbox ring 1: ' + data.request + '\\n\\t' + JSON.stringify(data));\n console.error(error)\n break; \n }\n }\n\n async handleRing2Message(data) {\n debugging('event:ring-2') && debug('event:ring-2', data);\n switch (data.request) {\n case 'dependency': {\n let moduleData;\n try {\n moduleData = await this.supervisorCache.fetchModule(data.data);\n } catch (error) {\n /*\n * In the event of an error here, we want to let the client know there was a problem in\n * loading their module. However, there hasn't yet been an actual slice assigned to the sandbox.\n * Therefore, we assign 'slice 0' to the sandbox, a slice that will never exist, and is used\n * purely for this purpose. \n */\n this.slice = {\n jobAddress: this.jobAddress,\n sliceNumber: 0,\n };\n\n const payload = {\n name: error.name,\n timestamp: error.timestamp,\n message: error.message,\n };\n\n this.emit('workEmit', {\n eventName: 'error',\n payload,\n });\n this.ee.emit('reject', error);\n break;\n }\n this.evaluatorHandle.postMessage({\n request: 'moduleGroup',\n data: moduleData,\n id: data.id,\n });\n break;\n }\n case 'error':\n /*\n * Ring 2 error messages will only fire for problems inside of the worker that are separate from\n * the work function. In most cases there are other handlers for situations where 'error' may be emitted\n * such as timeouts if the expected message isn't recieved. Thus, we will output the error, but nothing else.\n */\n console.error(data.error);\n break;\n case 'describe':\n case 'evalResult':\n case 'resetStateDone':\n case 'assigned':\n // emit internally\n this.ee.emit(data.request, data);\n break;\n case 'reject':\n // emit internally\n this.ee.emit(data.request, data.error);\n break;\n default: {\n const error = new Error(\n `Received unhandled request from sandbox ring 2. Data: ${JSON.stringify(\n data,\n null,\n 2,\n )}`,\n );\n console.error(error);\n break;\n }\n }\n }\n\n async handleRing3Message(data) {\n switch (data.request) {\n case 'complete':\n clearTimeout(this.progressTimeout);\n clearTimeout(this.sliceTimeout);\n this.progressTimeout = this.sliceTimeout = null;\n\n if (this.progress === null) {\n if (this.options.ignoreNoProgress) {\n console.warn(\"ENOPROGRESS silenced by localExec: Progress was not called during this slice's execution, in a remote sandbox this would cause the slice to be failed\");\n } else {\n // If a progress update was never received (progress === null) then reject\n this.ee.emit('reject', new NoProgressError('Sandbox never emitted a progress event.'));\n break;\n }\n }\n this.progress = 100;\n data.timeReport = this.sliceTimeReport;\n this.ee.emit('resolve', data);\n break;\n case 'progress':\n let { progress, indeterminate, throttledReports, value } = data;\n this.progress = progress;\n const progressReport = {\n timestamp: Date.now() - this.sliceStartTime,\n progress,\n value,\n throttledReports,\n }\n this.progressReports.last = progressReport;\n if (!indeterminate) {\n this.progressReports.lastDeterministic = progressReport;\n }\n\n this.resetProgressTimeout();\n\n this.emit('sliceProgress', data);\n break;\n\n case 'noProgress':\n let { message } = data;\n\n this.ee.emit('reject', new NoProgressError(message));\n break;\n case 'console':\n if (DCP_ENV.isBrowserPlatform){\n data.payload.message = kvin.serialize(data.payload.message)\n }\n this.emit('workEmit', {\n eventName: 'console',\n payload: data.payload\n });\n break;\n\n case 'emitEvent':/* ad-hoc event from the sandbox (work.emit) */\n this.emit('workEmit', {\n eventName: 'custom',\n payload: data.payload\n })\n break;\n case 'measurement':\n this.updateTime(data);\n break;\n case 'workError': {\n this.emit('workEmit', {\n eventName: 'error',\n payload: data.error,\n });\n\n // Warning: rejecting here with just .data.error causes issues\n // where the reject handlers modify the object so it interferes with the\n // workEmit payload, wrapping in an Error instance copies the values\n const wrappedError = new UncaughtExceptionError(\n data.error.message,\n data.error.fileName,\n data.error.lineNumber,\n );\n wrappedError.stack = data.error.stack;\n wrappedError.name = data.error.name;\n\n if (this.ee.listenerCount('reject') > 0) {\n this.ee.emit('reject', wrappedError);\n } else {\n // This will happen if the error is thrown during initialization\n throw wrappedError;\n }\n break;\n }\n default:\n let error = new Error('Received unhandled request from sandbox ring 3: ' + data.request + '\\n\\t' + JSON.stringify(data));\n console.error(error)\n break; \n }\n }\n\n /**\n * Handles progress and completion events from sandbox.\n * Unless explicitly returned out of this function will re-emit the event\n * on @this.ee where the name of the event is event.data.request.\n *\n * @param {object} event - event received from the sandbox\n */\n async onmessage(event) {\n debugging('event') && debug('event', event);\n if (Sandbox.debugEvents) {\n console.debug('sandbox - eventDebug:', {\n id: this.id,\n state: this.state,\n event: JSON.stringify(event)\n })\n }\n\n const { data } = event;\n const ringLevel = data.ringSource\n\n // Give the data to a handler depending on ring level\n if (ringLevel === -1) {\n console.error('Message sent directly from raw postMessage. Terminating worker...');\n console.debug(event);\n return this.terminate(true);\n } else {\n const handler = this.ringMessageHandlers[ringLevel];\n if (handler) {\n handler.call(this, data.value);\n } else {\n console.warn(`No handler defined for message from ring ${ringLevel}`);\n console.debug(event);\n }\n }\n }\n\n /**\n * Error handler for the internal sandbox.\n * Currently just logs the errors that the sandbox spits out.\n */\n onerror(event) {\n console.error('Sandbox emitted an error:', event);\n this.terminate(true, true);\n }\n\n /**\n * Clears the timeout and terminates the sandbox.\n *\n * @param {boolean} [reject = true] - if true call reject\n * @param {boolean} [immediate = false] - passed to terminate, used by standaloneWorker to immediately close the connection\n */\n terminate (reject = true, immediate = false) {\n this.state = TERMINATED\n\n clearTimeout(this.progressTimeout);\n clearTimeout(this.sliceTimeout);\n this.progressTimeout = this.sliceTimeout = null;\n \n if (this.evaluatorHandle && typeof this.evaluatorHandle.terminate === 'function') {\n try {\n this.evaluatorHandle.terminate(immediate);\n this.evaluatorHandle = null;\n } catch (e) {\n console.error(\"Sandbox.terminate: Encountered an error while terminating the sandbox:\", e);\n } finally {\n this.emit('terminate', this);\n this.emit('workerStop', this); /** @todo: remove */\n }\n }\n\n if (reject) {\n this.ee.emit('reject', new Error(`Sandbox was terminated or timed out.`));\n }\n }\n\n /**\n * Attempts to stop the sandbox from doing completing its current\n * set of work without terminating the working.\n */\n stop () {\n throw new Error('Sandbox.stop is not yet implemented.')\n }\n\n /**\n * ringNPostMessage can send a `measurement` request and update these\n * totals.\n */\n updateTime (measurementEvent) {\n ['total', 'idle', 'webGL'].forEach((key) => {\n if (measurementEvent[key]) this.sliceTimeReport[key] += measurementEvent[key];\n })\n }\n\n resetSliceTimeReport () {\n this.sliceTimeReport = {\n total: 0,\n idle: 0,\n webGL: 0,\n }\n }\n}\n\nSandbox.idCounter = 1;\nSandbox.debugWork = false;\nSandbox.debugState = false;\nSandbox.debugEvents = false;\n\nexports.Sandbox = Sandbox;\nexports.SandboxError = SandboxError;\nexports.NoProgressError = NoProgressError;\nexports.SliceTooSlowError = SliceTooSlowError;\nexports.UncaughtExceptionError = UncaughtExceptionError;\n\n\n//# sourceURL=webpack:///./src/dcp-client/worker/sandbox.js?");
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?");
5372
5414
 
5373
5415
  /***/ }),
5374
5416
 
@@ -5391,7 +5433,7 @@ eval("/**\n * @file worker/slice.js\n *\n * A wrapper for the slice object retur
5391
5433
  /*! no static exports found */
5392
5434
  /***/ (function(module, exports, __webpack_require__) {
5393
5435
 
5394
- eval("/**\n * @file worker/supervisor-cache.js\n *\n * A cache for the supervisor, anything the supervisor\n * may request that is cacheable (the same every time its requested)\n * can be cached in this class.\n *\n * Currently only jobs and modules are cached.\n *\n * If a cached job is not accessed in tuning.jobCacheTTL ms\n * it will be invalidated.\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * @date May 2019\n */\n\n/* global dcpConfig */\n\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('worker');\nconst { justFetch, fetchURI, dumpObject } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\n\nconst tuning = dcpConfig.future('supervisor.tuning', {\n jobCacheTTL: 300, /* seconds */\n jobCacheCleanupInterval: 60, /* seconds */\n});\n\nclass SupervisorCache extends EventEmitter {\n constructor (supervisor) {\n var timer;\n \n super('SupervisorCache')\n this.supervisor = supervisor;\n this.cache = {\n job: {},\n module: {},\n dependency: {},\n }\n \n this.promises = {\n job: {},\n module: {},\n dependency: {},\n }\n\n this.lastAccess = {}\n for (let key in this.cache) {\n this.lastAccess[key] = {}\n }\n\n timer = setInterval(this.checkTTL, tuning.jobCacheCleanupInterval);\n if (timer.unref)\n timer.unref();\n }\n\n /**\n * Returns an object listing what jobs and modules are currently cached.\n * @returns {object} - in the form: { job: [0xgen1, 0xgen2,...], modules: [modGroup1, modGroup2,...] }\n */\n get cacheDescription () {\n let description = {}\n for (let key in this.cache) {\n description[key] = Object.keys(this.cache[key])\n }\n return description\n }\n\n /**\n * Returns an array of all jobId currently cached\n * @returns {Array} all the jobId's in the cache\n */\n get jobs () {\n return Object.keys(this.cache.job);\n }\n\n /**\n * Runs on an interval and checks to see if a cached object \n * should be invalidated because it hasn't been accessed recently.\n */\n checkTTL () {\n for (let key in this.lastAccess) {\n for (let address in this.lastAccess[key]) {\n let lastAccess = this.lastAccess[key][address]\n let now = Date.now()\n if (lastAccess + (1000 * tuning.jobCacheTTL) < now) { /* dcpConfig.supervisor.cacheTTL */\n delete this.cache[key][address]\n }\n }\n }\n }\n\n /**\n * Attempts to look up an item from the cache.\n * If item is found its last access time is updated.\n *\n * @param {string} key - the cache to look in (job or module)\n * @param {string} id - the items identifier (jobAddress or module group name)\n *\n * @returns {any} The value of the stored item or null if nothing is found\n */\n fetch(key, id) {\n if (typeof this.cache[key] === 'undefined') {\n throw new Error(`${key} does not relate to any cache.`);\n }\n if (this.cache[key][id]) {\n this.lastAccess[key][id] = Date.now();\n return this.cache[key][id];\n }\n return null;\n }\n\n /**\n * Stores a fetched value for one of the caches.\n *\n * @param {string} key - the cache to store the item in\n * @param {string} id - the items identifier (job Address or module group name)\n * @param {any} value - the item to store\n *\n * @returns {any} - @value passed in\n */\n store (key, id, value) {\n if (typeof this.cache[key] === 'undefined') {\n throw new Error(`${key} does not relate to any cache.`)\n }\n this.cache[key][id] = value\n this.lastAccess[key][id] = Date.now()\n return value\n }\n\n /** \n * Fetch a job from the job cache. If the job has components which are \n * which need to be fetched over the network, they are fetched before the\n * returned promise is resolved.\n *\n * The job cache is initially populated during fetchTask.\n */\n async fetchJob(address, allowedOrigins) {\n let job = this.fetch('job', address);\n\n if (!job) {\n let e = new Error(`No job in supervisor cache with address ${address}`);\n e.code = 'ENOJOB';\n throw e;\n }\n\n // XXXpfr Excellent tracing.\n if (debugging('worker')) {\n dumpObject(job, 'SupervisorCache.fetchJob: job', 128);\n }\n\n if (!job.workFunction) {\n job.workFunction = await fetchURI(job.codeLocation, allowedOrigins, dcpConfig.worker.allowOrigins.fetchWorkFunctions);\n delete job.codeLocation;\n }\n if (!job.arguments) {\n if (job.argumentsLocation.startsWith('RemoteDataSet:')){\n let promises = [];\n job.argumentsLocation = job.argumentsLocation.replace('RemoteDataSet:','');\n let uris = job.argumentsLocation.split(\",\");\n for (let i = 0; i < uris.length; i++) {\n promises.push(fetchURI(uris[i], allowedOrigins, dcpConfig.worker.allowOrigins.fetchArguments));\n }\n job.arguments = await Promise.all(promises);\n } else {\n job.arguments = await fetchURI(job.argumentsLocation, allowedOrigins, dcpConfig.worker.allowOrigins.fetchArguments);\n }\n delete job.argumentsLocation;\n }\n \n return job;\n }\n\n /**\n * Attempts to fetch a module group from the cache and\n * if it's not found it attempts to fetch then store\n * the module group from the package manager.\n *\n * @param {array} modulesArray - the array of modules requested \n * - (when stringified it's the identifier of the module group)\n *\n * @returns {Promise<object>} - the module group\n * @throws when the module group can not be fetched\n */\n async fetchModule(modulesArray) {\n const cacheKey = JSON.stringify(modulesArray);\n let modules = this.fetch('module', cacheKey);\n if (modules !== null) {\n return modules;\n }\n\n if (this.promises.module[cacheKey]) {\n return this.promises.module[cacheKey];\n }\n\n const {\n success,\n payload: responsePayload,\n } = await this.supervisor.packageManagerConnection.send('fetchModule', {\n modules: modulesArray,\n });\n\n if (!success) {\n /**\n * Preserving the error message by not rewrapping it with DCPError incase\n * we want to let clients know which module couldn't be fetched.\n */\n throw responsePayload;\n }\n\n this.promises.module[cacheKey] = responsePayload;\n modules = await this.promises.module[cacheKey];\n delete this.promises.module[cacheKey];\n return this.store('module', cacheKey, modules);\n }\n\n /**\n * Attempts to fetch a dependency from the cache and\n * if it's not found it attempts to fetch then store\n * the dependency from the package manager.\n *\n * @param {string} dependencyUri - The URI of the dependency\n *\n * @returns {Promise<string>} file contents\n * @throws when the dependency can not be fetched\n */\n async fetchDependency(dependencyUri) {\n let dependency = this.fetch('dependency', dependencyUri);\n if (dependency !== null) {\n return dependency;\n }\n\n if (this.promises.dependency[dependencyUri]) {\n return this.promises.dependency[dependencyUri];\n }\n\n const url = dcpConfig.packageManager.location.resolve(dependencyUri);\n this.promises.dependency[dependencyUri] = justFetch(url, 'string', 'GET', true);\n\n dependency = await this.promises.dependency[dependencyUri];\n\n delete this.promises.dependency[dependencyUri];\n\n return this.store('dependency', dependencyUri, dependency);\n }\n}\n\nexports.SupervisorCache = SupervisorCache;\n\n\n//# sourceURL=webpack:///./src/dcp-client/worker/supervisor-cache.js?");
5436
+ eval("/**\n * @file worker/supervisor-cache.js\n *\n * A cache for the supervisor, anything the supervisor\n * may request that is cacheable (the same every time its requested)\n * can be cached in this class.\n *\n * Currently only jobs and modules are cached.\n *\n * If a cached job is not accessed in tuning.jobCacheTTL ms\n * it will be invalidated.\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * @date May 2019\n */\n\n/* global dcpConfig */\n\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('worker');\nconst { justFetch, fetchURI, dumpObject } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\n\nconst tuning = dcpConfig.future('supervisor.tuning', {\n jobCacheTTL: 300, /* seconds */\n jobCacheCleanupInterval: 60, /* seconds */\n});\n\nclass SupervisorCache extends EventEmitter {\n constructor (supervisor) {\n var timer;\n \n super('SupervisorCache')\n this.supervisor = supervisor;\n this.cache = {\n job: {},\n module: {},\n dependency: {},\n }\n \n this.promises = {\n job: {},\n module: {},\n dependency: {},\n }\n\n this.lastAccess = {}\n for (let key in this.cache) {\n this.lastAccess[key] = {}\n }\n\n timer = setInterval(this.checkTTL, tuning.jobCacheCleanupInterval);\n if (timer.unref)\n timer.unref();\n }\n\n /**\n * Returns an object listing what jobs and modules are currently cached.\n * @returns {object} - in the form: { job: [0xgen1, 0xgen2,...], modules: [modGroup1, modGroup2,...] }\n */\n get cacheDescription () {\n let description = {}\n for (let key in this.cache) {\n description[key] = Object.keys(this.cache[key])\n }\n return description\n }\n\n /**\n * Returns an array of all jobId currently cached\n * @returns {Array} all the jobId's in the cache\n */\n get jobs () {\n return Object.keys(this.cache.job);\n }\n\n /**\n * Runs on an interval and checks to see if a cached object \n * should be invalidated because it hasn't been accessed recently.\n */\n checkTTL () {\n for (let key in this.lastAccess) {\n for (let address in this.lastAccess[key]) {\n let lastAccess = this.lastAccess[key][address]\n let now = Date.now()\n if (lastAccess + (1000 * tuning.jobCacheTTL) < now) { /* dcpConfig.supervisor.cacheTTL */\n delete this.cache[key][address]\n }\n }\n }\n }\n\n /**\n * Attempts to look up an item from the cache.\n * If item is found its last access time is updated.\n *\n * @param {string} key - the cache to look in (job or module)\n * @param {string} id - the items identifier (jobAddress or module group name)\n *\n * @returns {any} The value of the stored item or null if nothing is found\n */\n fetch(key, id) {\n if (typeof this.cache[key] === 'undefined') {\n throw new Error(`${key} does not relate to any cache.`);\n }\n if (this.cache[key][id]) {\n this.lastAccess[key][id] = Date.now();\n return this.cache[key][id];\n }\n return null;\n }\n\n /**\n * Stores a fetched value for one of the caches.\n *\n * @param {string} key - the cache to store the item in\n * @param {string} id - the items identifier (job Address or module group name)\n * @param {any} value - the item to store\n *\n * @returns {any} - @value passed in\n */\n store (key, id, value) {\n if (typeof this.cache[key] === 'undefined') {\n throw new Error(`${key} does not relate to any cache.`)\n }\n this.cache[key][id] = value\n this.lastAccess[key][id] = Date.now()\n return value\n }\n\n /** \n * Fetch a job from the job cache. If the job has components which are \n * which need to be fetched over the network, they are fetched before the\n * returned promise is resolved.\n *\n * The job cache is initially populated during fetchTask.\n */\n async fetchJob(address, allowedOrigins) {\n let job = this.fetch('job', address);\n\n if (!job) {\n let e = new Error(`No job in supervisor cache with address ${address}`);\n e.code = 'ENOJOB';\n throw e;\n }\n\n // XXXpfr Excellent tracing.\n if (debugging('worker')) {\n dumpObject(job, 'SupervisorCache.fetchJob: job', 128);\n }\n\n if (!job.workFunction) {\n job.workFunction = await fetchURI(job.codeLocation, allowedOrigins, dcpConfig.worker.allowOrigins.fetchWorkFunctions);\n delete job.codeLocation;\n }\n if (!job.arguments) {\n let promises = [];\n let uris = job.argumentsLocation;\n if (uris)\n for (let i = 0; i < uris.length; i++)\n promises.push(fetchURI(uris[i].value, allowedOrigins, dcpConfig.worker.allowOrigins.fetchArguments));\n\n job.arguments = await Promise.all(promises);\n delete job.argumentsLocation;\n }\n \n return job;\n }\n\n /**\n * Attempts to fetch a module group from the cache and\n * if it's not found it attempts to fetch then store\n * the module group from the package manager.\n *\n * @param {array} modulesArray - the array of modules requested \n * - (when stringified it's the identifier of the module group)\n *\n * @returns {Promise<object>} - the module group\n * @throws when the module group can not be fetched\n */\n async fetchModule(modulesArray) {\n const cacheKey = JSON.stringify(modulesArray);\n let modules = this.fetch('module', cacheKey);\n if (modules !== null) {\n return modules;\n }\n\n if (this.promises.module[cacheKey]) {\n return this.promises.module[cacheKey];\n }\n\n const {\n success,\n payload: responsePayload,\n } = await this.supervisor.packageManagerConnection.send('fetchModule', {\n modules: modulesArray,\n });\n\n if (!success) {\n /**\n * Preserving the error message by not rewrapping it with DCPError incase\n * we want to let clients know which module couldn't be fetched.\n */\n throw responsePayload;\n }\n\n this.promises.module[cacheKey] = responsePayload;\n modules = await this.promises.module[cacheKey];\n delete this.promises.module[cacheKey];\n return this.store('module', cacheKey, modules);\n }\n\n /**\n * Attempts to fetch a dependency from the cache and\n * if it's not found it attempts to fetch then store\n * the dependency from the package manager.\n *\n * @param {string} dependencyUri - The URI of the dependency\n *\n * @returns {Promise<string>} file contents\n * @throws when the dependency can not be fetched\n */\n async fetchDependency(dependencyUri) {\n let dependency = this.fetch('dependency', dependencyUri);\n if (dependency !== null) {\n return dependency;\n }\n\n if (this.promises.dependency[dependencyUri]) {\n return this.promises.dependency[dependencyUri];\n }\n\n const url = dcpConfig.packageManager.location.resolve(dependencyUri);\n this.promises.dependency[dependencyUri] = justFetch(url, 'string', 'GET', true);\n\n dependency = await this.promises.dependency[dependencyUri];\n\n delete this.promises.dependency[dependencyUri];\n\n return this.store('dependency', dependencyUri, dependency);\n }\n}\n\nexports.SupervisorCache = SupervisorCache;\n\n\n//# sourceURL=webpack:///./src/dcp-client/worker/supervisor-cache.js?");
5395
5437
 
5396
5438
  /***/ }),
5397
5439
 
@@ -5403,7 +5445,7 @@ eval("/**\n * @file worker/supervisor-cache.js\n *\n * A cache for the superviso
5403
5445
  /***/ (function(module, exports, __webpack_require__) {
5404
5446
 
5405
5447
  "use strict";
5406
- eval("/**\n * @file worker/supervisor.js\n *\n * The component that controls each of the sandboxes\n * and distributes work to them. Also communicates with the\n * scheduler to fetch said work.\n *\n * The supervisor readies sandboxes before/while fetching slices.\n * This means sometimes there are extra instantiated WebWorkers\n * that are idle (in this.readiedSandboxes). Readied sandboxes can\n * be used for any slice. After a readied sandbox is given a slice\n * it becomes assigned to slice's job and can only do work\n * for that job.\n *\n * After a sandbox completes its work, the sandbox becomes cached\n * and can be reused if another slice with a matching job is fetched.\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * Ryan Rossiter, ryan@kingsds.network\n * @date May 2019\n */\n\n/* global dcpConfig */\n// @ts-check\n\n\nconst nanoid = __webpack_require__(/*! nanoid/non-secure */ \"./node_modules/nanoid/non-secure/index.js\");\nconst constants = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst hash = __webpack_require__(/*! dcp/common/hash */ \"./src/common/hash.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\n\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('worker');\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { Sandbox, SandboxError } = __webpack_require__(/*! ./sandbox */ \"./src/dcp-client/worker/sandbox.js\");\nconst { Slice } = __webpack_require__(/*! ./slice */ \"./src/dcp-client/worker/slice.js\");\nconst { SupervisorCache } = __webpack_require__(/*! ./supervisor-cache */ \"./src/dcp-client/worker/supervisor-cache.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { localStorage } = __webpack_require__(/*! dcp/common/dcp-localstorage */ \"./src/common/dcp-localstorage.js\");\nconst { booley, encodeDataURI, makeSliceURI, leafMerge, asleepMs, shortTime,\n dumpSandboxes, dumpSlices, dumpSandboxesIfNotUnique, dumpSlicesIfNotUnique } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst { calculateJoinHash } = __webpack_require__(/*! dcp/dcp-client/compute-groups */ \"./src/dcp-client/compute-groups/index.js\");\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 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 this.taskDistributorConnection.close();\n this.isFetchingNewWork = false;\n }, 3 * 60 * 1000); // max out at 3 minutes to fetch\n\n // ensure result submitter connection before fetching tasks\n try\n {\n await this.resultSubmitterConnection.keepalive();\n }\n catch (error)\n {\n console.error('Failed to connect to result submitter, refusing to fetch slices. Will try again at next fetch cycle.')\n debugging('supervisor') && console.log(`Error: ${error}`);\n this.isFetchingNewWork = false;\n return;\n }\n await this.fetchTask(slicesToFetch);\n clearTimeout(fetchTimeout);\n }\n \n this.distributeQueuedSlices().then(() => debugging('supervisor') && 'supervisor: finished distributeQueuedSlices()').catch((e)=>console.error(e)) ;\n// await this.distributeQueuedSlices();\n // No catch(), because it will bubble outward to the caller\n } finally {\n this.isFetchingNewWork = false;\n }\n }\n\n /**\n * Generate the workerComputeGroups property of the requestTask message. \n * \n * Concatenate the compute groups object from dcpConfig with the list of compute groups\n * from the supervisor, and remove the public group if accidentally present. Finally,\n * we transform joinSecrets/joinHash into joinHashHash for secure transmission.\n *\n * @note computeGroup objects with joinSecrets are mutated to record their hashes. This\n * affects the supervisor options and dcpConfig. Re-adding a joinSecret property\n * to one of these will cause the hash to be recomputed.\n */\n generateWorkerComputeGroups()\n {\n var computeGroups = Object.values(dcpConfig.worker.computeGroups || {});\n if (this.options.computeGroups)\n computeGroups = computeGroups.concat(this.options.computeGroups);\n computeGroups = computeGroups.filter(group => group.id !== constants.computeGroups.public.id);\n const hashedComputeGroups = [];\n for (const group of computeGroups)\n {\n const groupCopy = Object.assign({}, group);\n if ((group.joinSecret || group.joinHash) && (!group.joinHashHash || this.lastDcpsid !== this.taskDistributorConnection.dcpsid))\n {\n let joinHash;\n if(group.joinHash) {\n joinHash = group.joinHash;\n } else {\n joinHash = calculateJoinHash(groupCopy);\n } \n\n groupCopy.joinHashHash = hash.calculate(hash.eh1, joinHash, this.taskDistributorConnection.dcpsid);\n delete groupCopy.joinSecret;\n delete groupCopy.joinHash;\n debugging('computeGroups') && console.debug(`Calculated joinHash=${joinHash} for`, groupCopy);\n }\n hashedComputeGroups.push(groupCopy);\n }\n this.lastDcpsid = this.taskDistributorConnection.dcpsid;\n debugging('computeGroups') && console.debug('Requesting ', computeGroups.length, 'non-public groups for session', this.lastDcpsid);\n return hashedComputeGroups;\n }\n\n /**\n * Fetches a task, which contains job information and slices for sandboxes and\n * manages events related to fetching tasks so the UI can more clearly display\n * to user what is actually happening.\n * @param {number} numCores\n * @returns {Promise<void>} The requestTask request, resolve on success, rejects otherwise.\n * @emits Supervisor#fetchingTask\n * @emits Supervisor#fetchedTask\n */\n async fetchTask(numCores)\n {\n this.emit('fetchingTask');\n debugging('supervisor') && console.debug('supervisor: fetching task');\n const requestPayload = {\n numCores,\n coreStats: this.getStatisticsCPU(),\n numGPUs: this.defaultMaxGPUs,\n capabilities: this.capabilities,\n paymentAddress: this.paymentAddress,\n jobAddresses: this.options.jobAddresses, // when set, only fetches slices for these jobs\n localExec: this.options.localExec,\n workerComputeGroups: this.generateWorkerComputeGroups(),\n minimumWage: dcpConfig.worker.minimumWage || this.options.minimumWage,\n readyJobs: [ /* list of jobs addresses XXXwg */ ],\n previouslyWorkedJobs: this.cache.jobs,\n };\n\n // workers should be part of the public compute group by default\n if (!booley(dcpConfig.worker.leavePublicGroup) && !booley(this.options.leavePublicGroup))\n requestPayload.workerComputeGroups.push(constants.computeGroups.public);\n debugging('computeGroups') && console.log(`Fetching work for ${requestPayload.workerComputeGroups.length} ComputeGroups: `, requestPayload.workerComputeGroups);\n \n debugging() && console.log(`${shortTime()} fetchTask wants ${numCores} slices, unallocatedSpace ${this.unallocatedSpace}, queuedSlices ${this.queuedSlices.length}`);\n try {\n debugging('requestTask') && debug('requestPayload', requestPayload);\n\n let result = await this.taskDistributorConnection.send('requestTask', requestPayload);\n let responsePayload = result.payload; \n \n if (!result.success) { \n debugging() && console.log('Task fetch failure; request=', requestPayload);\n debugging() && console.log('Task fetch failure; response=', result.payload);\n throw new DCPError('Unable to fetch task for worker', responsePayload);\n }\n\n const sliceCount = responsePayload.body.task.length || 0;\n\n /**\n * The fetchedTask event fires when the supervisor has finished trying to\n * fetch work from the scheduler (task-manager). The data emitted is the\n * number of new slices to work on in the fetched task.\n *\n * @event Supervisor#fetchedTask\n * @type {number}\n */\n this.emit('fetchedTask', sliceCount);\n\n if (sliceCount < 1) {\n return;\n }\n\n /*\n * DCP-1698 Send auth msg with tasks to worker, then validate authority of worker to send slice info back to scheduler.\n * payload structure: { owner: this.address, signature: signature, auth: messageLightWeight, body: messageBody };\n * XXXpfr: @todo Do not think we need 'owner', think about it.\n */\n const { body, ...authorizationMessage } = responsePayload;\n const { newJobs, task } = body;\n assert(newJobs); // It should not be possible to have !newJobs -- we throw on !success.\n for (const jobAddress of Object.keys(newJobs)) {\n this.cache.store('job', jobAddress, newJobs[jobAddress]);\n }\n debugging('supervisor') && console.log(`${shortTime()} fetchTask: Task has ${task.length} slices from ${Object.keys(newJobs).length} new jobs.`);\n\n const tmpQueuedSlices = [];\n for (const taskElement of task) {\n\n if (!this.authorizationMessages[taskElement.jobAddress]) {\n this.authorizationMessages[taskElement.jobAddress] = {};\n // All workers are authorized to send messages to the '0' slice if it is assigned the job.\n this.authorizationMessages[taskElement.jobAddress][0] = authorizationMessage;\n }\n this.authorizationMessages[taskElement.jobAddress][taskElement.sliceNumber] = authorizationMessage;\n\n tmpQueuedSlices.push(new Slice(taskElement));\n debugging('supervisor') && console.log(`/t${shortTime()} fetchTask: slice ${taskElement.sliceNumber}, jobAddress ${taskElement.jobAddress}`);\n }\n\n /** XXXpfr @todo Try to get rid of this.slices. */\n this.slices.push(...tmpQueuedSlices);\n\n this.queuedSlices = [...tmpQueuedSlices, ...this.queuedSlices]; /* Make sure the old ones are up front. */\n\n } catch (error) {\n this.emit('fetchTaskFailed', error);\n debugging('supervisor') && console.debug(`Supervisor.fetchTask failed!: error: ${error}`);\n }\n }\n\n /**\n * For each slice in this.queuedSlices, match with a sandbox in the following order:\n * 1. Try to find an already assigned sandbox in this.assignedSandboxes for the slice's job.\n * 2. Find a ready sandbox in this.readiedSandboxes that is unassigned.\n * 3. Ready a new sandbox and use that.\n *\n * Take great care in assuring sandboxes and slices are uniquely associated, viz.,\n * a given slice cannot be associated with multiple sandboxes and a given sandbox cannot be associated with multiple slices.\n * The lack of such uniqueness has been the root cause of several difficult bugs.\n *\n * @param {number} numSlices The number of slices to match with sandboxes.\n * @returns {Promise<SandboxSlice[]>} Returns SandboxSlice[], may have length zero.\n */\n async matchSlicesWithSandboxes (numSlices) {\n const sandboxSlices = [];\n if (numSlices <= 0 || this.queuedSlices.length == 0 || this.matching)\n return sandboxSlices;\n\n // 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\n // sandbox.public.name is defined in Sandbox.assign.\n // It is possible to get here with !sandbox.public.\n const jobName = sandbox.public ? sandbox.public.name : 'unnamed';\n const errorObject = {\n jobAddress: slice.jobAddress.substr(0,10),\n sliceNumber: slice.sliceNumber,\n sandbox: sandbox.id,\n jobName: jobName,\n };\n\n // XXXpfr Enabled Informative sandbox errors when in debug mode.\n if (!Supervisor.debugBuild && error.errorCode === 'EUNCAUGHTERROR') {\n console[logLevel](`Supervisor.startSandboxWork: Uncaught error in sandbox, could not compute.`, errorObject);\n } else {\n 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})`, 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 const metrics = { GPUTime: 0, CPUTime: 0, CPUDensity: 0, GPUDensity: 0 }; \n \n debugging('supervisor') && console.log('supervisor: recording result');\n assert(slice && slice.hasOwnProperty('result'), slice.sliceNumber, slice.jobAddress);\n\n const jobAddress = slice.jobAddress;\n const sliceNumber = slice.sliceNumber;\n const authorizationMessage = this.checkAuthorization('Supervisor.recordResult:', jobAddress, sliceNumber);\n if (!authorizationMessage) return; // Question: Is it better to proceed and have a cascade of errors?\n\n /* @see result-submitter::result for full message details */\n var payloadData = {\n slice: sliceNumber,\n job: jobAddress,\n worker: this.workerOpaqueId,\n paymentAddress: this.paymentAddress,\n metrics,\n authorizationMessage,\n }\n const isValidTimeReport = slice.result && slice.result.timeReport && slice.result.timeReport.total > 0;\n if (isValidTimeReport) {\n const timeReport = slice.result.timeReport;\n assert(timeReport.total);\n metrics.GPUTime = timeReport.webGL;\n metrics.CPUTime = timeReport.total - (metrics.GPUTime + timeReport.idle);\n metrics.CPUDensity = metrics.CPUTime / timeReport.total;\n metrics.GPUDensity = metrics.GPUTime / timeReport.total;\n }\n \n this.emit('submittingResult');\n\n if (!slice.isFinished)\n throw new Error('Cannot record result for slice that is not finished');\n\n if (slice.resultStorageType === 'pattern') /* this is a remote-storage slice */\n payloadData.result = await sendResultToRemote(slice);\n else {\n // It is possible for slice.result to be undefined when there are upstream errors.\n if (!slice.result)\n throw new Error('slice.result is undefined. This is ok when there are upstream errors.');\n payloadData.result = encodeDataURI(slice.result.result); /* XXXwg - result.result is awful */\n }\n\n try {\n if (slice.completed) {\n\n /* work function returned a result */\n const { success, payload } = await this.resultSubmitterConnection.send(\n 'result',\n payloadData,\n );\n\n if (!success) {\n throw new DCPError('Unable to submit result for work done', payload);\n }\n\n const receipt = {\n accepted: true,\n payment: payload.slicePaymentAmount,\n };\n this.emit('submittedResult', payload);\n this.emit('dccCredit', receipt);\n } else {\n\n /* slice did not complete for some reason */\n let statusPayloadData = {\n worker: this.workerOpaqueId,\n slices: [\n {\n sliceNumber: sliceNumber,\n job: jobAddress,\n status: 'return', // special state looked for in status.js / result-submitter\n reason: 'uncaught', // special state looked for in status.js / result-submitter\n /** @todo XXXpfr: Is there error info we can use here? */ \n // error: slice.error, \n authorizationMessage,\n }\n ], \n };\n \n await this.resultSubmitterConnection.send('status', statusPayloadData);\n }\n } catch(error) {\n console.info(`1014: Failed to submit results for slice ${payloadData.slice} of job ${payloadData.job}`, error);\n this.emit('submitSliceFailed', error);\n }\n finally\n {\n this.emit('submitFinished');\n // Remove the slice from the slices array\n this.removeElement(this.slices, slice);\n }\n }\n}\n\n/** \n * Send a work function's result to a server that speaks our DCP Storage Server protocol.\n * A sample storage server has been implemented in https://gitlab.com/Distributed-Compute-Protocol/dcp-storage-server\n * as part of DCP-1479.\n *\n * @param {Slice} slice slice object whose result we are sending\n * @returns {Promise<object>} object with properties contentType and uri\n * @throws if the remote server returned a non-ok status object, or an HTTP status not in the 2xx range.\n */\nasync function sendResultToRemote(slice) {\n const postParams = {\n ...slice.resultStorageParams\n };\n\n const sliceResultUri = makeSliceURI('pattern', slice.resultStorageDetails, {\n slice: slice.sliceNumber,\n job: slice.jobAddress,\n //juuid: slice.job.uuid -- what should be here?\n });\n\n const url = new DcpURL(sliceResultUri);\n\n if (dcpConfig.worker.allowOrigins.any.indexOf(url.origin) === -1 &&\n dcpConfig.worker.allowOriginResultStorage.indexOf(url.origin) === -1) {\n throw new Error(`Invalid origin for remote result storage: '${url.origin}'`);\n }\n \n postParams.slice = slice.sliceNumber;\n postParams.job = slice.jobAddress;\n postParams.contentType = 'application/json'; // Currently data will be outputed as a JSON object, @todo: Support file upload\n\n if (slice.result.result)\n postParams.result = slice.result.result;\n else\n postParams.error = new Error('no result'); /** @todo XXXwg return the error object from the sandbox */\n\n const formBodyArray = [];\n for (const property in postParams) {\n const encodedKey = encodeURIComponent(property);\n const encodedValue = encodeURIComponent(postParams[property]);\n formBodyArray.push(`${encodedKey}=${encodedValue}`);\n }\n const formBody = formBodyArray.join('&');\n\n let postPromise = new Promise(function supervisorPostResult(resolve, reject) {\n let deeperErrorStack = new Error().stack;\n deeperErrorStack = deeperErrorStack.substring(deeperErrorStack.indexOf('\\n') + 1);\n const xhr = new XMLHttpRequest();\n xhr.onloadend = function supervisor$$recordResult$onloadend() {\n try {\n let o;\n \n delete xhr.onloadend;\n if (xhr.status >= 200 && xhr.status < 300) {\n /* Successful post: record the resultant URL; needed for dcp-client application to be able to fetch the result */\n switch(xhr.getResponseHeader('content-type'))\n {\n default: /* support lousy web servers */\n case 'application/json': {\n o = JSON.parse(xhr.responseText);\n break;\n }\n case 'application/x-kvin': {\n o = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\").deserialize(xhr.responseText);\n break;\n }\n }\n\n if (o.status === 'ok')\n return resolve(o);\n \n return reject(new Error(`Remote server ${url} did not set status=ok (o.error ? ${JSON.stringify(o)} : error)`));\n } else {\n const error = new Error(`HTTP Error ${xhr.status} sending slice results to ${url}`);\n //\n // Note: This impl is mostly grafted from justFetch in dcp/src/protocol-v3/index.\n // It is a safe assumption that this function has never been tested.\n // Refine the following error info when we support remote-storage.\n //\n error.request = xhr;\n error.request.location = url;\n error.request.method = 'POST';\n error.request.status = xhr.status;\n error.stack += '\\n----------\\n' + deeperErrorStack;\n throw error;\n }\n } catch (e) {\n return reject(e);\n }\n\n return reject(new Error('impossible'));\n }\n \n xhr.onreadystatechange = function() {\n if (this.readyState == 4 && this.status == 200) {\n debugging('supervisor') && console.log('Response text', this.responseText);\n }\n }\n \n xhr.open('POST', url.href, true);\n xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');\n xhr.send(formBody);\n });\n\n return postPromise;\n}\n\n/**\n * Return DCPv4-specific connection options, composed of type-specific, URL-specific, \n * and worker-specific options, any/all of which can override the dcpConfig.dcp.connectOptions.\n * The order of precedence is the order of specificity.\n */\nfunction connectionOptions(url, label) {\n return leafMerge(/* ordered from most to least specific */\n dcpConfig.worker.dcp.connectionOptions.default,\n dcpConfig.worker.dcp.connectionOptions[label],\n dcpConfig.worker.dcp.connectionOptions[url.href]);\n}\n\n\n/**\n * Add a slice to the slice payload being built. If a sliceList already exists for the \n * job-status-authMessage tuple, then the slice will be added to that, otherwise a new \n * sliceList will be added to the payload.\n *\n * @param {Object[]} slicePayload Slice payload being built. Will be mutated in place.\n * @param {Address} job Address of job the slice belongs to\n * @param {Number} sliceNumber Slice number\n * @param {String} status Status update, eg. progress or scheduled\n * @param {Object} authorizationMessage authorizationMessage for the slice\n *\n * @returns {Object[]} mutated slicePayload array\n */\nfunction addToSlicePayload(slicePayload, job, sliceNumber, status, authorizationMessage) {\n // Try to find a sliceList in the payload which matches the job, status, and auth message\n let sliceList = slicePayload.find(desc => {\n return desc.job === job && desc.status === status && desc.authorizationMessage === authorizationMessage;\n });\n\n // If we didn't find a sliceList, start a new one and add it to the payload\n if (!sliceList) {\n sliceList = {\n job,\n sliceNumbers: [],\n status,\n authorizationMessage: authorizationMessage,\n };\n slicePayload.push(sliceList);\n }\n\n sliceList.sliceNumbers.push(sliceNumber);\n\n return slicePayload;\n}\n\n/** @type {number | boolean} */\nSupervisor.lastAssignFailTimerMs = false;\n/** @type {boolean} */\nSupervisor.startSandboxWork_beenCalled = false;\n/** @type {boolean} */\nSupervisor.debugBuild = (__webpack_require__(/*! dcp/common/dcp-build */ \"./src/common/dcp-build.js\").build === 'debug');\n\nexports.Supervisor = Supervisor;\n\n\n//# sourceURL=webpack:///./src/dcp-client/worker/supervisor.js?");
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?");
5407
5449
 
5408
5450
  /***/ }),
5409
5451
 
@@ -5426,51 +5468,7 @@ eval("/* WEBPACK VAR INJECTION */(function(process) {/**\n * @file debugg
5426
5468
  /*! no static exports found */
5427
5469
  /***/ (function(module, exports, __webpack_require__) {
5428
5470
 
5429
- 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');\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\nlet idCounter = 1; // Start at 1 so it's truthy :)\n\nclass EventSubscriber {\n constructor(eventEmitter) {\n this.eventEmitter = eventEmitter;\n\n this.subscriptions = new Map();\n\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 openBankConn()\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.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 const { token, event: data } = req.payload.data;\n const event = data.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\n {\n const label = event.eventName;\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?");
5430
-
5431
- /***/ }),
5432
-
5433
- /***/ "./src/protocol-v3/index.js":
5434
- /*!**********************************!*\
5435
- !*** ./src/protocol-v3/index.js ***!
5436
- \**********************************/
5437
- /*! no static exports found */
5438
- /***/ (function(module, exports, __webpack_require__) {
5439
-
5440
- eval("/* WEBPACK VAR INJECTION */(function(process) {/**\n * @file src/protocol-v3/index.js\n * Portable (browser, node) version of the DCPv3 Protocol.\n *\n * This module may also invoke fixups on the global dcpConfig when it is loaded; this implies \n * that dcpConfig must be loaded first, but not used until this module loads.\n *\n * @author Greg Agnew, gagnew@kingsds.network\n * Matthew Palma, mpalma@kingsds.network\n * Eddie Roosenmaallen, eddie@kingsds.network\n * @date Mar 2018\n *\n * *note* - This module is completely deprecated, however nooks and crannies are still being used \n * by the portal web code.\n */\n\nif (typeof dcpConfig === 'undefined') {\n throw new Error('dcpConfig is not defined')\n}\n\nconst ethereumjs = __webpack_require__(/*! ethereumjs-tx */ \"./node_modules/ethereumjs-tx/es5/index.js\")\nethereumjs.Wallet = __webpack_require__(/*! ethereumjs-wallet */ \"./node_modules/ethereumjs-wallet/index.js\")\nethereumjs.Util = __webpack_require__(/*! ethereumjs-util */ \"./node_modules/ethereumjs-util/dist/index.js\")\nconst io = __webpack_require__(/*! socket.io-client */ \"./node_modules/socket.io-client/build/index.js\")\n\nconst { Keychain } = __webpack_require__(/*! ./keychain */ \"./src/protocol-v3/keychain.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 { messageToBuffer } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n\nif (typeof XMLHttpRequest === 'undefined') {\n var XMLHttpRequest = __webpack_require__(/*! dcp/common/dcp-xhr */ \"./src/common/dcp-xhr.js\").XMLHttpRequest;\n}\n\nif (dcpConfig.needs && dcpConfig.needs.urlPatchup) {\n DCPURL.patchup(dcpConfig)\n}\n\n/**\n * Format an error message that describes the current state of an error code originating\n * from an XHR request.\n *\n * @param url The URL associated with the request\n * @param xhr The object passed to the onload / onerror / etc event handler\n * @param plain True when we want to force non-colourized output\n *\n * @returns {string} An error message, formatted with ANSI color when the output\n * is a terminal, suitable for writing directly to stdout. If\n * the response included html content (eg a 404 page), it is \n * rendered to text in this string.\n */\nfunction formatXHRErrorMessage(url, xhr, plain) {\n let chalk, message, headers={};\n\n if (!plain && __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").platform === 'nodejs') {\n chalk = new requireNative('chalk').constructor({enabled: requireNative('tty').isatty(0)})\n } else if (!plain && __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").isBrowserPlatform) {\n chalk = {\n grey: (a) => `<font color=\"grey\">${a}</font>`,\n yellow: (a) => `<font color=\"yellow\">${a}</font>`,\n bold: (a) => `<bold>${a}</bold>`\n }\n } else {\n let nop = function nop(a) { return a }\n chalk = { grey: nop, bold: nop, yellow: nop }\n }\n \n xhr.getAllResponseHeaders().replace(/\\r/g,'').split('\\n').forEach(function(line) {\n var colon = line.indexOf(': ')\n headers[line.slice(0,colon)] = line.slice(colon+2)\n })\n message = `HTTP Status: ${xhr.status} for ${url}`\n\n if (typeof headers['content-type'] !== 'undefined') {\n switch(headers['content-type'].replace(/;.*$/, '')) {\n case 'text/plain':\n message += '\\n' + chalk.grey(xhr.responseText);\n break;\n case 'text/html':\n message += '\\n' + chalk.grey(__webpack_require__(/*! html-to-text */ \"./node_modules/html-to-text/index.js\").fromString(xhr.responseText, {\n wordwrap: parseInt(process.env.COLUMNS, 10) || 80,\n format: {\n heading: function (elem, fn, options) {\n var h = fn(elem.children, options);\n return '====\\n' + chalk.yellow(chalk.bold(h.toUpperCase())) + '\\n====';\n }\n }}))\n break;\n }\n }\n\n return message\n}\n\n/**\n * The DCP Protocol; this class handles all communications between DCP agents\n * @module Protocol\n * @requires Constants\n */\nclass Protocol {\n constructor () {\n this.eth = ethereumjs\n this.keychain = new Keychain(this)\n }\n}\n\nObject.assign(exports, {\n protocol: new Protocol(),\n});\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../node_modules/process/browser.js */ \"./node_modules/process/browser.js\")))\n\n//# sourceURL=webpack:///./src/protocol-v3/index.js?");
5441
-
5442
- /***/ }),
5443
-
5444
- /***/ "./src/protocol-v3/keychain-web-interface.js":
5445
- /*!***************************************************!*\
5446
- !*** ./src/protocol-v3/keychain-web-interface.js ***!
5447
- \***************************************************/
5448
- /*! no static exports found */
5449
- /***/ (function(module, exports, __webpack_require__) {
5450
-
5451
- eval("/**\n * @file keychain-web-interface.js - Manages the activities related to using\n * wallets and keystores.\n * @file keychain-web-interface.js\n * @author Greg Agnew, gagnew@kingsds.network\n * @date Nov 2018\n */\n\nconst { Modal } = __webpack_require__(/*! ./modal */ \"./src/protocol-v3/modal.js\");\n\nexports.KeychainWebInterface = class KeychainWebInterface {\n constructor (keychain) {\n this.keychain = keychain\n this.eth = __webpack_require__(/*! ethereumjs-tx */ \"./node_modules/ethereumjs-tx/es5/index.js\")\n this.eth.Wallet = __webpack_require__(/*! ethereumjs-wallet */ \"./node_modules/ethereumjs-wallet/index.js\")\n this.eth.Util = __webpack_require__(/*! ethereumjs-util */ \"./node_modules/ethereumjs-util/dist/index.js\")\n // Is this supposed to represent multiple modals being open?\n this.openPrompts = {}\n }\n\n /**\n * Prompts the user to select which keystore to remove form the keychain and\n * acts accordingly.\n * @deprecated Needs to be revisited despite only being referenced once in\n * keychain.js under... removeKeystore()\n */\n // TODO: May need to revisit this function in the future.\n removeKeystore () {\n const addresses = Object.keys(this.keychain.keys)\n // Nothing seems to be done with the return value.\n // if (addresses.length === 0) {\n // return false\n // }\n\n if (addresses.length === 0) {\n return false\n }\n\n let prompt = new Modal('Keystore', 'Please select the keystore address to delete', function (formValues) {\n delete this.keychain.keys[formValues[0]]\n this.keychain.emit('change', 'removeKeystore', formValues[0])\n }.bind(this))\n let select = prompt.addFormElement({\n label: 'Keystore Address:',\n type: 'select'\n })\n\n for (let i = 0; i < addresses.length; i++) {\n let option = document.createElement('option')\n option.value = addresses[i]\n option.innerText = addresses[i]\n select.appendChild(option)\n }\n\n prompt.open()\n }\n\n /**\n * Prompts the user to unlock a wallet if it has a password and acts\n * appropriately.\n *\n * @param {string} address - The address of the wallet to unlock.\n * @returns {Promise} The unlocked wallet.\n */\n promptToUnlockKeystore (address) {\n return new Promise((resolve, reject) => {\n const label = this.keychain.keys[address].keystore.label\n const formattedAddress = `<span class=key-address-font>${address}</span>`\n const promptMessage = `The keystore '${label}' with public address ${formattedAddress} is encrypted.<br>Please enter the keystore's passphrase.`\n // No callback. Want the backend form validation to happen on submit.\n const prompt = new Modal('Unlock Keystore', promptMessage, null, onCancelledUnlock.bind(this))\n prompt.addFormElement({\n placeholder: 'Passphrase',\n type: 'password',\n required: true,\n invalidFeedback: 'The passphrase is incorrect. Please try again.'\n })\n // Override the submit action to check the wallet password before the\n // modal is closed.\n prompt.changeFormSubmitHandler(tryToUnlockKeystore.bind(this))\n prompt.open()\n\n function tryToUnlockKeystore (event) {\n event.preventDefault()\n const passphraseInput = prompt.form.querySelector('input[type=\"password\"]')\n const passphrase = passphraseInput.value\n const keystore = this.keychain.keys[address].keystore\n let wallet\n try {\n wallet = this.eth.Wallet.fromV3(keystore, passphrase, true)\n } catch (error) {\n passphraseInput.classList.add('is-invalid')\n // Need the timeout to focus the elements once the main thread becomes\n // idle, else the element doesn't get focused.\n window.setTimeout(() => passphraseInput.focus())\n return false;\n }\n this.keychain.keys[address].wallet = wallet\n this.keychain.keys[address].privateKey = wallet.getPrivateKeyString()\n prompt.continue()\n resolve(this.keychain.keys[address].wallet)\n this.keychain.emit('change', 'unlockKeystore', this.keychain.keys[keystore.address])\n }\n\n function onCancelledUnlock (modal) {\n reject(Error('Cancelled Keystore Unlock.'))\n }\n })\n }\n\n useKeystore (keystore = false, password = null) {\n let addresses = Object.keys(this.keychain.keys)\n\n if (addresses.length === 0) {\n return this.keychain.newKeystore(password)\n }\n\n let prompt = new Modal('Keystore', 'Please select the address to use', function (formValues) {\n this.keychain.currentAddress = formValues[0]\n this.emit('change', 'useKeystore', this.keychain.keys[this.keychain.currentAddress])\n }.bind(this))\n let select = prompt.addFormElement({\n label: 'Keystore Address:',\n type: 'select'\n })\n\n for (let i = 0; i < addresses.length; i++) {\n let option = document.createElement('option')\n option.value = addresses[i]\n option.innerText = addresses[i]\n select.appendChild(option)\n }\n\n prompt.open()\n }\n\n promptToCreateNewKeystore (useKeystore = true, wallet) {\n return new Promise((resolve, reject) => {\n const formattedAddress = `<span class=\"key-address-font\">${wallet.getAddressString()}</span>`\n const promptMessage =\n `New Bank Account: ${formattedAddress}`\n const prompt = new Modal('New Keystore', promptMessage, tryToCreateKeystore.bind(this), onCancel)\n prompt.addFormElement({\n placeholder: 'Label',\n type: 'text',\n autocomplete: 'off'\n }, {\n placeholder: 'Passphrase (Optional)',\n type: 'password',\n autocomplete: 'new-password',\n validation: validate$ConfirmPasswordMatches\n }, {\n placeholder: 'Confirm Passphrase',\n type: 'password',\n autocomplete: 'new-password',\n validation: validate$ConfirmPasswordMatches\n })\n prompt.addFormValidationForPasswordConfirmation()\n prompt.open()\n\n function tryToCreateKeystore (formValues) {\n const [label, password, confirmPassword] = formValues;\n if (password !== confirmPassword) {\n throw new Error('Passphrases do not match')\n }\n const keystore = wallet.toV3(password, { n: 1024 })\n keystore.label = label || 'Unnamed'\n this.keychain.addPrivateKey_fromEthV3Keystore(keystore, password, useKeystore);\n resolve(keystore)\n }\n\n function onCancel (modal) {\n reject(Error('Cancelled Keystore Creation.'))\n }\n\n function validate$ConfirmPasswordMatches (formValues) {\n return formValues[1] === formValues[2]\n }\n })\n }\n\n newKeystoreOptions (password = null, useKeystore = true) {\n let key = 'newKeystoreOptions-'\n if (typeof this.openPrompts[key] !== 'undefined') return this.openPrompts[key].promise\n\n this.openPrompts[key] = {}\n this.openPrompts[key].promise = new Promise((resolve, reject) => {\n this.openPrompts[key].resolve = resolve\n this.openPrompts[key].reject = reject\n })\n\n let resolve = this.openPrompts[key].resolve\n let reject = this.openPrompts[key].reject\n\n let prompt = new Modal(\n 'Choose Keystore',\n 'Please select a Keystore.',\n async (formValues) => { // callback (onSuccess)\n if (!formValues[0].match(/^(0x)?([a-fA-F0-9]){64}$/)) {\n const alert = new Modal('Keystore creation error', 'Invalid private key', Function)\n alert.addMessage('You must create a new Keystore, load one from disk, or enter a valid private key')\n alert.open()\n delete this.openPrompts[key]\n return reject('No valid key')\n }\n let keystore = this.keychain.addPrivateKey(formValues[0], true)\n\n delete this.openPrompts[key]\n resolve(keystore)\n },\n (modal) => { // exitHandler (onFail?)\n delete this.openPrompts[key]\n reject(new Error('Prompt Closed'))\n }\n )\n\n prompt.addButton({\n description: 'Create a new Keystore',\n label: 'Generate',\n callback: (modal) => {\n modal.close(true)\n delete this.openPrompts[key]\n resolve(this.keychain.newKeystore())\n }\n })\n prompt.addButton({\n description: 'Load a Keystore from a File',\n label: 'Add Keystore',\n callback: (modal) => {\n modal.close(true)\n delete this.openPrompts[key]\n resolve(this.keychain.loadKeystore())\n }\n })\n prompt.addFormElement({\n placeholder: 'Private Key String',\n type: 'text'\n })\n prompt.open()\n\n return this.openPrompts[key].promise\n }\n\n // Exactly like newKeystoreOptions but has drag n drop feature.\n newKeystoreDragDrop (password = null, useKeystore = true) {\n let key = 'newKeystoreOptions-'\n if (typeof this.openPrompts[key] !== 'undefined') return this.openPrompts[key].promise\n\n this.openPrompts[key] = {}\n this.openPrompts[key].promise = new Promise((resolve, reject) => {\n this.openPrompts[key].resolve = resolve\n this.openPrompts[key].reject = reject\n })\n\n let resolve = this.openPrompts[key].resolve\n let reject = this.openPrompts[key].reject\n\n let prompt = new Modal(\n 'Choose Keystore',\n '',\n async (formValues) => { // callback (onSuccess)\n // console.log('form values', formValues[0])\n\n if (formValues[0].match(/^([a-fA-F0-9]){64}$/)) {\n formValues[0] = '0x' + formValues[0]\n }\n\n if (!formValues[0].match(/^(0x)?([a-fA-F0-9]){64}$/)) {\n const alert = new Modal('Keystore creation error', 'Invalid private key', Function)\n alert.addMessage('You must load a Keystore, or enter a valid private key')\n alert.open()\n delete this.openPrompts[key]\n return reject('No valid key')\n }\n let keystore = this.keychain.addPrivateKey(formValues[0], true)\n\n delete this.openPrompts[key]\n resolve(keystore)\n },\n (modal) => { // exitHandler (onFail?)\n delete this.openPrompts[key]\n reject(new Error('Prompt Closed'))\n }\n )\n\n prompt.addCustomHTML(`\n <div style=\"display:none;\">\n <div id=\"list\"></div>\n </div>\n\n <span style=\"float: left; margin-top: -9px;\">Upload Keystore:</span>\n <div id=\"special-idea\" style=\"height: 170px;\n border: 2px #c0bebe dashed;\n margin: 22px auto;\n padding-top: 13px;\n margin-top: -9px;\n background: #f7f7f7;\n border-radius: 10px;\n width: 219px;\"><!--ondrop=\"drop(event)\" ondragover=\"allowDrop(event)\"-->\n <span style=\"font-size: 30px; color: #00000087;\"><i class=\"fas fa-wallet\"></i></span><br/><br/>\n Drop keystore file here<br><br>\n <span style=\"color:#00000080;\">or</span><br><br>\n <div id=\"browse-button\" style=\"background: #00a473;\n padding: 5px;\n color: white;\n width: 79px;\n margin: auto;\n border-radius: 3px;\">Browse</div>\n </div>\n <span style=\"float: left; margin-top: 10px;\">Or insert private key:</span>\n <input id=\"key-input\" type=\"text\" placeholder=\"0x\" style=\" width: 100%;\n margin-top: 8px;\n margin-top: 20px;\n font-family: 'Roboto Mono', monospace;\n font-size: 14px;\n padding: 5px;\">\n `, (modal) => {\n modal.close(true)\n delete this.openPrompts[key]\n resolve(this.keychain.loadKeystore())\n })\n\n/*\n prompt.addButton({\n description: 'Load a Keystore from a File',\n label: 'Add Keystore',\n callback: (modal) => {\n modal.close(true)\n delete this.openPrompts[key]\n resolve(this.keychain.loadKeystore())\n }\n })\n\n prompt.addFormElement({\n placeholder: 'Private Key String',\n type: 'text'\n })\n*/\n prompt.open()\n\n return this.openPrompts[key].promise\n }\n\n saveKeystore (wallet) {\n return new Promise((resolve, reject) => {\n let address = wallet.getAddressString()\n let prompt = new Modal('Save Keystore', 'Please enter the passphrase for ' + address + '.',\n function (formValues) {\n let keystore = wallet.toV3(formValues[0], { n: 1024 })\n this.keychain.addPrivateKey_fromEthV3Keystore(keystore, formValues[0], true)\n\n resolve(keystore)\n }.bind(this),\n function (modal) {\n reject(false) // eslint-disable-line\n })\n prompt.addFormElement({\n placeholder: 'Passphrase',\n type: 'password'\n })\n prompt.open()\n })\n }\n\n promptToUploadKeystore (useKeystore = true) {\n return new Promise((resolve, reject) => {\n const promptMessage = 'Please choose a Keystore file to upload and enter the Keystore\\'s passphrase if it has one, or enter a private key.'\n const prompt = new Modal('Upload Keystore', promptMessage, onContinue.bind(this), handleExitingUpload)\n let keystore\n prompt.addFormElement({\n label: '',\n type: 'file',\n callback: (file) => {\n // Read the file selected by the file input whenever the selection\n // changes.\n let reader = new FileReader()\n reader.onload = () => { keystore = reader.result }\n reader.readAsText(file)\n }\n }, {\n placeholder: 'Passphrase',\n type: 'password'\n })\n // To make the user fill in at least one of the forms.\n prompt.changeFormSubmitHandler(validateOnSubmit)\n // parent.parent to hide the entire row container for the password field.\n const passwordContainer = prompt.container.querySelector('input[type=\"password\"]').parentElement.parentElement\n passwordContainer.classList.add('hidden')\n prompt.open()\n\n function onContinue (formValues) {\n const modalBody = prompt.container.querySelector('div.dcp-modal-body')\n modalBody.innerText = 'Loading...'\n setTimeout(tryToLoadWallet.bind(this, formValues), 50)\n }\n\n function tryToLoadWallet (formValues) {\n const [fileInputElement, password] = formValues\n let wallet\n try {\n // Checking if the keystore is coming from the file input or a private\n // key.\n if (fileInputElement.value && typeof keystore === 'string') {\n try {\n keystore = JSON.parse(keystore)\n } catch (error) {\n reject(Error('Unable to parse keystore file.'))\n }\n // console.log('449: Unpacking keystore:', keystore)\n const { label, flags } = keystore\n wallet = this.eth.Wallet.fromV3(keystore, password, true)\n keystore = wallet.toV3(password, { n: 1024 })\n keystore.label = label\n keystore.flags = flags\n // console.log('452: repacked keystore:', keystore)\n this.keychain.addPrivateKey_fromEthV3Keystore(keystore, password, useKeystore)\n } else {\n const privateKeyBuffer = this.eth.Util.toBuffer(privateKey)\n wallet = this.eth.Wallet.fromPrivateKey(privateKeyBuffer)\n keystore = wallet.toV3('', { n: 1024 })\n // Give the new keystore a default name.\n keystore.label = 'Unnamed'\n this.keychain.addPrivateKey_fromEthV3Keystore(keystore, '', useKeystore)\n }\n } catch (error) {\n return reject(error)\n }\n resolve(keystore)\n }\n\n function handleExitingUpload (modal) {\n reject(Error('Cancelled Keystore Upload.'))\n }\n\n function validateOnSubmit (event) {\n try {\n event.preventDefault()\n } catch (error) {\n console.log('Failed to block regular form validation', error)\n }\n const fileFormElements = prompt.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 (filledInFileFormElements.length === 0) {\n const continueButton = prompt.container.querySelector('.dcp-modal-footer button.continue')\n continueButton.setCustomValidity('Please select a keystore file or enter a private key.')\n // The timeout ensures the validity is reported correctly once the\n // main thread opens up.\n window.setTimeout(() => continueButton.reportValidity())\n return false\n }\n prompt.continue()\n return true\n }\n })\n }\n}\n\n\n//# sourceURL=webpack:///./src/protocol-v3/keychain-web-interface.js?");
5452
-
5453
- /***/ }),
5454
-
5455
- /***/ "./src/protocol-v3/keychain.js":
5456
- /*!*************************************!*\
5457
- !*** ./src/protocol-v3/keychain.js ***!
5458
- \*************************************/
5459
- /*! no static exports found */
5460
- /***/ (function(module, exports, __webpack_require__) {
5461
-
5462
- eval("/**\n * @file keychain.js - Represents the collection of keystores currently owned\n * by the user and defines functions to interect with the\n * keystores.\n * @module Keychain\n */\n\n/* globals dcpConfig, protocol */\n\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { KeychainWebInterface } = __webpack_require__(/*! ./keychain-web-interface */ \"./src/protocol-v3/keychain-web-interface.js\");\nconst { messageToBuffer } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\")\nconst compute = __webpack_require__(/*! dcp/dcp-client/compute */ \"./src/dcp-client/compute.js\")\nconst ethUtil = wallet._internalEth.util;\nconst ethWallet = wallet._internalEth.wallet;\n\nvar lastUsedTimestamp = 0\nexports.Keychain = class Keychain extends EventEmitter {\n constructor (protocol) {\n super('Keychain')\n\n this.protocol = protocol\n // All of the keystores on the keychain.\n this.keys = {}\n this.currentAddress = null\n\n if (__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").platform !== 'nodejs') {\n this.interface = new KeychainWebInterface(this)\n } else {\n this.interface = new Proxy({}, {\n get: function KeychainNodeInterfaceGetter(prop) {\n throw new Error(`KeychainNodeInterface::${prop} is no longer supported`);\n }\n });\n }\n }\n\n /** \n * Add a Keystore object (from the wallet api) to the current keychain.\n * This keystore will be unlocked on demand when the private key is needed by the protocol.\n *\n * @param {object} keystore instance of wallet.Keystore\n * @param {boolean} useKeystore OPTIONAL falsey to not select the new key.\n * Default: do not select it\n * @emits change { keys }\n */\n addKeystore(keystore, useKeystore) {\n if (!(keystore instanceof wallet.Keystore))\n throw new Error(`not a Keystore object (${keystore})`)\n\n let pubAddr = keystore.address.toString();\n if (useKeystore) {\n this.currentAddress = pubAddr;\n }\n \n if (this.keys.hasOwnProperty(pubAddr)) {\n console.log('Warning: Keychain already has an entry for ' + pubAddr)\n if (this.keys[pubAddr] instanceof wallet.Keystore && this.keys[pubAddr].isUnlocked())\n return\n }\n\n this.keys[pubAddr] = keystore;\n \n this.emit('change', 'addPrivateKey', this.keys[pubAddr])\n return keystore\n }\n\n /**\n * Add a private key via an ethv3 keystore to the keychain, optionally unlocking it\n * @param keystore Keystore to add\n * @param password OPTIONAL password to use to unlock the new keystore. Default: do not unlock\n * @param useKeystore OPTIONAL falsey to not select the new keystore. Default: select it\n * @emits change { keys }\n * @returns {string} currently-selected keystore address\n */\n addPrivateKey_fromEthV3Keystore (keystore, password = null, useKeystore = true) {\n if (typeof this.keys[keystore.address] !== 'undefined' &&\n typeof this.keys[keystore.address].keystore !== 'undefined' &&\n typeof this.keys[keystore.address].keystore.crypto !== 'undefined') {\n return this.keys[keystore.address]\n }\n\n this.keys[keystore.address] = {\n keystore\n }\n\n if (password !== null) {\n let ew = ethWallet.fromV3(keystore, password, true);\n this.keys[keystore.address].wallet = ew;\n this.keys[keystore.address].privateKey = ew.getPrivateKeyString();\n }\n\n if (useKeystore) {\n this.currentAddress = keystore.address\n }\n\n this.emit('change', 'addKeystore', this.keys[keystore.address])\n\n return this.keys[keystore.address]\n }\n\n /**\n * Add an arbitrary public key to the keychain\n * @param {string} privateKey Private key to add\n * @param useKeystore OPTIONAL falsey to not select the new key.\n * Default: do not select it\n * @emits change { keys }\n * @returns the keystore (wallet).\n */\n addPrivateKey (privateKey, useKeystore = false) {\n let address = ethUtil.privateToAddress(privateKey).toString('hex')\n\n this.keys[address] = {\n privateKey,\n keystore: {\n address\n }\n }\n\n if (useKeystore) {\n this.currentAddress = address\n }\n\n this.emit('change', 'addPrivateKey', this.keys[address])\n return this.keys[address]\n }\n\n /**\n * Remove a keystore from the keyring\n * If not specified, present a dialog to select the keystore to remove\n * @param keystore OPTIONAL keystore address to remove. Default: present a dialog to select\n * @emits change { removeKeystore }\n */\n removeKeystore (keystore) {\n if (keystore && typeof this.keys[keystore.address] !== 'undefined') {\n delete this.keys[keystore.address]\n // Try to change the active wallet if needed.\n if (this.currentAddress === keystore.address) {\n const keystoreAddresses = Object.keys(this.keys)\n if (keystoreAddresses.length > 0) {\n const firstKeystore = this.keys[keystoreAddresses[0]].keystore\n this.useKeystore(firstKeystore)\n } else {\n this.currentAddress = null\n }\n }\n this.emit('change', 'removeKeystore', keystore)\n if (new wallet.Address(keystore.address).eq(compute.paymentAddress)) {\n compute.paymentAddress = this.currentAddress;\n }\n } else {\n this.interface.removeKeystore()\n }\n }\n\n /**\n * Remove all keystores from the keyring\n * If not specified, present a dialog to select the keystore to remove\n * @emits change { removeKeystore } for each key\n */\n removeKeystores () {\n let addresses = Object.keys(this.keys)\n\n this.keys = {}\n this.currentAddress = null\n\n for (let i = 0; i < addresses.length; i++) {\n this.emit('change', 'removeKeystore', addresses[i])\n }\n }\n\n /**\n * Unlock a keystore on the keychain, adding its wallet and private key to the keychain\n * @param address Address of keystore to unlock\n * @emits change { keys }\n * @returns Promise< unlocked Wallet >\n */\n unlockKeystore (address, password = false, openInterface = true) {\n return new Promise((resolve, reject) => {\n if (typeof this.keys[address] === 'undefined') {\n return reject(Error('Wallet to unlock not found.'))\n }\n let keystore = this.keys[address].keystore\n let ew;\n try {\n if (password !== false) {\n ew = ethWallet.fromV3(keystore, password, true);\n } else {\n ew = ethWallet.fromV3(keystore, '', true);\n }\n\n this.keys[keystore.address].wallet = ew;\n this.keys[keystore.address].privateKey = ew.getPrivateKeyString();\n resolve(this.keys[address].wallet)\n this.emit('change', 'unlockKeystore', this.keys[keystore.address])\n } catch (error) {\n if (openInterface === false) return reject(error)\n\n let ew\n try {\n ew = this.interface.promptToUnlockKeystore(address);\n } catch (error) {\n return reject(error)\n }\n return resolve(ew)\n }\n })\n }\n\n /**\n * Lock a keystore, removing its wallet and private key from the keychain\n * @param address Address of keystore to unlock\n * @emits change { keys }\n */\n lockKeystore (address) {\n if (typeof this.keys[address] === 'undefined') return false\n delete this.keys[address].privateKey\n delete this.keys[address].wallet\n this.emit('change', 'lockKeystore', this.keys[address])\n }\n\n /**\n * Select active keystore\n * @param keystore OPTIONAL Keystore object to select. Default: prompt to select keystore and provide password\n * @param password OPTIONAL password to unlock keystore (if not already unlocked)\n * @emits change { keys }\n */\n useKeystore (keystore, password = null) {\n if (keystore) {\n if (typeof this.keys[keystore.address] === 'undefined') {\n this.addKeystore(keystore, password, true)\n } else {\n this.currentAddress = keystore.address\n }\n this.emit('change', 'useKeystore', this.keys[this.currentAddress])\n } else {\n this.interface.useKeystore(keystore, password)\n }\n }\n\n /**\n * Get a keystore from the keychain\n * @param address OPTIONAL Public address of the keystore to return\n * @returns Promise< requested keystore >\n */\n getKeystore (address = this.currentAddress) {\n return new Promise((resolve, reject) => {\n if (address === null) {\n return resolve(this.newKeystoreOptions())\n }\n\n if (typeof this.keys[address] === 'undefined') {\n return reject(new Error('Address not found.'))\n }\n\n let key = this.keys[address]\n if (typeof key.keystore !== 'undefined') {\n return resolve(key.keystore)\n } else if (typeof key.wallet !== 'undefined') {\n return resolve(this.saveKeystore(key.wallet))\n } else if (typeof key.privateKey !== 'undefined') {\n key.wallet = ethWallet.fromPrivateKey(key.privateKey)\n return resolve(this.saveKeystore(key.wallet))\n }\n })\n }\n\n /**\n * Get a keystore from the keychain\n * @param address OPTIONAL Public address of the keystore to return\n * @returns Promise< requested keystore >\n */\n getKeystoreDragDrop (address = this.currentAddress) {\n return new Promise((resolve, reject) => {\n if (address === null) {\n return resolve(this.newKeystoreDragDrop())\n }\n\n if (typeof this.keys[address] === 'undefined') {\n return reject(new Error('Address not found.'))\n }\n\n let key = this.keys[address]\n if (typeof key.keystore !== 'undefined') {\n return resolve(key.keystore)\n } else if (typeof key.wallet !== 'undefined') {\n return resolve(this.saveKeystore(key.wallet))\n } else if (typeof key.privateKey !== 'undefined') {\n key.wallet = ethWallet.fromPrivateKey(key.privateKey)\n return resolve(this.saveKeystore(key.wallet))\n }\n })\n }\n\n /**\n * Generate a new keystore\n * @param password OPTIONAL Password to encrypt new keystore. Default: prompt for password\n * @param useKeystore OPTIONAL falsey to not select new keystore\n * @emits change { keys }\n * @returns Promise< new Keystore >\n */\n async newKeystore (password = null, useKeystore = true) {\n let ew = ethWallet.generate();\n\n if (password !== null) {\n let keystore = ew.toV3(password, {n: 1024});\n this.addKeystore(keystore, password, useKeystore)\n return keystore\n } else {\n let keystore = await this.interface.promptToCreateNewKeystore(useKeystore, ew);\n return keystore\n }\n }\n\n /**\n * Present a dialog to load or create a keystore\n * @param password OPTIONAL password\n */\n newKeystoreOptions (password = null, useKeystore = true) {\n return this.interface.newKeystoreOptions(password, useKeystore)\n }\n\n /**\n * DIFFERENT TODO Present a dialog to load or create a keystore\n * @param password OPTIONAL password\n */\n newKeystoreDragDrop (password = null, useKeystore = true) {\n return this.interface.newKeystoreDragDrop(password, useKeystore)\n }\n\n saveKeystore (wallet) {\n return this.interface.saveKeystore(wallet)\n }\n\n loadKeystore (useKeystore = true) {\n return this.interface.promptToUploadKeystore(useKeystore)\n }\n\n sign (body, privateKey = false, address = false) {\n return new Promise((resolve, reject) => {\n const t0 = Date.now();\n let t1;\n \n let complete = function (privateKey, address) {\n if (address) {\n address = '0x' + address.substr(-40);\n }\n // console.log(`329: key wrangling took ${Date.now() - t0}ms`);\n if (typeof body.timestamp === 'undefined') {\n body.timestamp = Date.now()\n while (body.timestamp <= lastUsedTimestamp) {\n body.timestamp = lastUsedTimestamp + 1 // pseudo add a milisecond\n }\n }\n\n if (typeof body.version === 'undefined') {\n body.version = this.protocol.version\n }\n\n lastUsedTimestamp = body.timestamp\n\n t1 = Date.now();\n let signedMessage = {\n owner: address || ethUtil.bufferToHex(ethUtil.privateToAddress(privateKey)),\n message: body\n }\n // console.log(`348* deriving address took ${Date.now() - t1}ms`);\n \n t1 = Date.now();\n signedMessage.signature = JSON.parse(JSON.stringify(ethUtil.ecsign(\n ethUtil.hashPersonalMessage(messageToBuffer(JSON.stringify(body))),\n ethUtil.toBuffer(privateKey))))\n\n // console.log(`352: signing took ${Date.now() - t1}ms`);\n // console.log(` overall time in .sign: ${Date.now() - t0}ms`);\n resolve(signedMessage)\n }.bind(this)\n \n if (privateKey instanceof wallet.Keystore) {\n let ks = privateKey;\n // console.log('362: privateKey looks like a DCP keystore');\n return ks.getPrivateKey().then(pk => complete(pk.toString()));\n }\n\n if (wallet.isPrivateKey(privateKey)) {\n let pk = new wallet.PrivateKey(privateKey);\n return complete(pk.toString());\n } \n\n if (typeof privateKey === 'string') {\n // console.log('367: privateKey looks like a bare private key', privateKey);\n return complete(privateKey, address)\n }\n\n if (privateKey instanceof Uint8Array) {\n // console.log('383: privateKey is a Uint8Array');\n let key = '0x';\n privateKey.forEach(n => key += n.toString(16).padStart(2, '0'));\n return complete(key, address);\n }\n\n if (typeof privateKey === 'object') {\n if (privateKey.getAddressString && privateKey.getPrivateKeyString) {\n // console.log('370: privateKey looks like an Ethereumjs wallet');\n return complete(privateKey.getPrivateKeyString(), privateKey.getAddressString());\n }\n console.warn('369: privateKey is an object, but not a DCP key-thinger', privateKey);\n }\n \n if (this.currentAddress === null) {\n // console.log('372: ask for a keystore, any keystore...');\n this.newKeystoreOptions().then(keystore => {\n complete(this.keys[this.currentAddress].privateKey)\n })\n } else {\n let key = this.keys[this.currentAddress]\n if (key instanceof wallet.Keystore) {\n key.getPrivateKey().then(function(pk) {\n complete(pk.toString())\n })\n } else if (typeof key.privateKey !== 'undefined') {\n complete(key.privateKey)\n } else if (typeof key.wallet !== 'undefined') {\n // console.log('385: extracting protocol key from eth wallet', key);\n key.privateKey = key.wallet.getPrivateKeyString()\n complete(key.privateKey, this.currentAddress)\n } else if (typeof key.keystore !== 'undefined') {\n // console.log('389: unlocking protocol key', key);\n this.unlockKeystore(key.keystore.address).then(() => {\n complete(key.privateKey, key.keystore.address)\n })\n }\n }\n })\n }\n\n /**\n * Returns true if the keystore has a password, false otherwise.\n * @param {string} address - The public address of the keystore to check.\n * @throws {Error} If the keystore's encryption cannot be determined.\n * @returns {boolean} True if the keystore has a password, false otherwise.\n */\n doesKeystoreHavePassword (address) {\n if (typeof this.keys[address] === 'undefined') {\n throw new Error('Keystore to check not found.')\n }\n const keystore = this.keys[address].keystore\n try {\n // The main check for the wallet's encryption.\n ethWallet.fromV3(keystore, '', true)\n } catch (error) {\n // If empty password fails, the wallet is encrypted.\n if (error.message === 'Key derivation failed - possibly wrong passphrase') {\n return true\n }\n throw error\n }\n // No errors => unencrypted.\n return false\n }\n}\n\n\n//# sourceURL=webpack:///./src/protocol-v3/keychain.js?");
5463
-
5464
- /***/ }),
5465
-
5466
- /***/ "./src/protocol-v3/modal.js":
5467
- /*!**********************************!*\
5468
- !*** ./src/protocol-v3/modal.js ***!
5469
- \**********************************/
5470
- /*! no static exports found */
5471
- /***/ (function(module, exports) {
5472
-
5473
- eval("/**\n * A Small Modal Class\n * @module Modal\n */\n/* globals Event dcpConfig */\nclass Modal {\n constructor (title, message, callback = false, exitHandler = false, {\n continueLabel = 'Continue',\n cancelLabel = 'Cancel',\n cancelVisible = true\n } = {}) {\n const modal = document.createElement('div')\n modal.className = 'dcp-modal-container-old day'\n modal.innerHTML = `\n <dialog class=\"dcp-modal-content\">\n <div class=\"dcp-modal-header\">\n <h2>${title}<button type=\"button\" class=\"close\">&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\nexports.Modal = Modal;\n\n// Inject our special stylesheet from dcp-client only if we're on the portal webpage.\nif (typeof window !== 'undefined' && typeof document !== 'undefined' && dcpConfig.portal.location.hostname === window.location.hostname) {\n // <link rel='stylesheet' href='/css/dashboard.css'>\n const stylesheet = document.createElement('link')\n stylesheet.rel = 'stylesheet'\n // Needed for the duplicate check done later.\n stylesheet.id = 'dcp-modal-styles'\n\n const dcpClientBundle = document.getElementById('_dcp_client_bundle')\n let src\n if (dcpClientBundle) {\n src = dcpClientBundle.src.replace('dcp-client-bundle.js', 'dcp-modal-style.css')\n } else {\n src = dcpConfig.portal.location.href + 'dcp-client/dist/dcp-modal-style.css'\n }\n\n stylesheet.href = src\n // If the style was injected before, don't inject it again.\n // Could occur when loading a file that imports Modal.js and loading\n // comput.min.js in the same HTML file.\n if (document.getElementById(stylesheet.id) === null) {\n document.getElementsByTagName('head')[0].appendChild(stylesheet)\n }\n\n if (typeof {\"version\":\"e4988ba0b1c993e062fc7c536188bda3ea4de883\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"4.1.12\",\"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#169b90c5603b765cbd334308bb8ac57eddb28378\"},\"built\":\"Fri Oct 22 2021 16:01:33 GMT-0400 (Eastern Daylight Time)\",\"config\":{\"generated\":\"Fri 22 Oct 2021 04:01:30 PM EDT by erose on dione\",\"build\":\"debug\"},\"webpack\":\"4.46.0\",\"node\":\"v12.22.6\"} !== 'undefined' && typeof window.Modal === 'undefined') {\n window.Modal = Modal\n }\n}\n\n\n//# sourceURL=webpack:///./src/protocol-v3/modal.js?");
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?");
5474
5472
 
5475
5473
  /***/ }),
5476
5474
 
@@ -5481,7 +5479,8 @@ eval("/**\n * A Small Modal Class\n * @module Modal\n */\n/* globals Event dcpCo
5481
5479
  /*! no static exports found */
5482
5480
  /***/ (function(module, exports, __webpack_require__) {
5483
5481
 
5484
- eval("/**\n * @file protocol/connection/ack.js\n * @author KC Erb, kcerb@kingsds.network\n * @date January 2021\n *\n * The ConnectionAck is a factory for an ACK message class that is bound\n * to a connection instance.\n * \n * The ACK message is a type of message that bears the minimal information needed\n * to confirm to the recipient that the sender has received a message/batch.\n * \n * ACKs are a little different depending on weather they are being sent or received.\n * When sent, an ack is acknowledging a message that has been received.\n * When received, an ack corresponds to a message that the receiver has sent.\n * \n * This asymmetry is reflected in the constructor.\n */\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\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}\nlet nonceSerialId = 0;\nconst ConnectionAck = (ConnectionMessage) => class Ack extends ConnectionMessage {\n /**\n * Create an AckMessage using details from the messaging we are ACKing.\n * or\n * Rehydrate an AckMessage from JSON.\n * @param {Message|object} messageOrAckJSON The message we are acknowledging receipt of or JSON.\n */\n constructor(messageOrAckJSON) {\n super();\n if (messageOrAckJSON.body) {\n let message = messageOrAckJSON;\n this.token = message.body.ackToken;\n this.messageId = message.body.id;\n this.type = 'ack';\n this.id = this.connection.messageFactory.generateMessageId();\n this.nonce = `${this.connection._id}-${nonceSerialId++}-${nanoid()}`;\n } else {\n let ackJSON = messageOrAckJSON;\n this.token = ackJSON.token;\n this.messageId = ackJSON.messageId;\n this.type = ackJSON.type;\n this.id = ackJSON.id;\n this.nonce = ackJSON.nonce;\n }\n }\n\n /**\n * This method is used to dehydrate the ack into a plain object before it is sent to the peer.\n * @returns {object} - An object that will be sent as-is to the peer.\n */\n toJSON() {\n const obj = super.toJSON();\n obj.id = this.id;\n obj.messageId = this.messageId;\n obj.token = this.token;\n obj.type = this.type;\n\n return obj;\n }\n}\n\nmodule.exports.ConnectionAck = ConnectionAck;\n\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/ack.js?");
5482
+ "use strict";
5483
+ eval("/**\n * @file protocol/connection/ack.js\n * @author KC Erb, kcerb@kingsds.network\n * @date January 2021\n *\n * The ConnectionAck is a factory for an ACK message class that is bound\n * to a connection instance.\n * \n * The ACK message is a type of message that bears the minimal information needed\n * to confirm to the recipient that the sender has received a message/batch.\n * \n * ACKs are a little different depending on weather they are being sent or received.\n * When sent, an ack is acknowledging a message that has been received.\n * When received, an ack corresponds to a message that the receiver has sent.\n * \n * This asymmetry is reflected in the constructor.\n */\n\n\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\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}\nlet nonceSerialId = 0;\nconst ConnectionAck = (ConnectionMessage) => class Ack extends ConnectionMessage {\n /**\n * Create an AckMessage using details from the messaging we are ACKing.\n * or\n * Rehydrate an AckMessage from JSON.\n * @param {Message|object} messageOrAckJSON The message we are acknowledging receipt of or JSON.\n */\n constructor(messageOrAckJSON) {\n super();\n if (messageOrAckJSON.body) {\n let message = messageOrAckJSON;\n this.token = message.body.ackToken;\n this.messageId = message.body.id;\n this.type = 'ack';\n this.id = this.connection.messageFactory.generateMessageId();\n this.nonce = `${this.connection._id}-${nonceSerialId++}-${nanoid()}`;\n } else {\n let ackJSON = messageOrAckJSON;\n this.token = ackJSON.token;\n this.messageId = ackJSON.messageId;\n this.type = ackJSON.type;\n this.id = ackJSON.id;\n this.nonce = ackJSON.nonce;\n }\n }\n\n /**\n * This method is used to dehydrate the ack into a plain object before it is sent to the peer.\n * @returns {object} - An object that will be sent as-is to the peer.\n */\n toJSON() {\n const obj = super.toJSON();\n obj.id = this.id;\n obj.messageId = this.messageId;\n obj.token = this.token;\n obj.type = this.type;\n\n return obj;\n }\n}\n\nmodule.exports.ConnectionAck = ConnectionAck;\n\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/ack.js?");
5485
5484
 
5486
5485
  /***/ }),
5487
5486
 
@@ -5490,9 +5489,10 @@ eval("/**\n * @file protocol/connection/ack.js\n * @author KC Erb, k
5490
5489
  !*** ./src/protocol-v4/connection/batch.js ***!
5491
5490
  \*********************************************/
5492
5491
  /*! no static exports found */
5493
- /***/ (function(module, exports) {
5492
+ /***/ (function(module, exports, __webpack_require__) {
5494
5493
 
5495
- eval("/**\n * @file protocol/connection/batch.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The ConnectionBatch is a factory for a batch message class that is bound\n * to a connection instance.\n * \n * The batch message is a type of message that can contain an array of request\n * and response messages.\n */\n\nconst ConnectionBatch = (ConnectionMessage) => class Batch extends ConnectionMessage {\n constructor(messages=[], ackToken) {\n super();\n // batch messages are internal so auto-assign ID on construction\n this.id = this.connection.messageFactory.generateMessageId();\n this.messages = messages;\n this.messageObjects = null;\n this.ackToken = ackToken;\n }\n\n /**\n * This static method is used by the connection to rehydrate a batch that was received.\n * Note that the messageObjects property is populated instead of the messages property\n * because the messages are still in their dehydrated object format.\n * @param {object} batchObj\n */\n static fromJSON(batchObj) {\n const batch = new this();\n batch.id = batchObj.id;\n batch.dcpsid = batchObj.dcpsid;\n batch.nonce = batchObj.nonce;\n batch.messageObjects = batchObj.payload;\n return batch;\n }\n\n /**\n * This method is used to dehydrate the request into a plain object before it is sent to the peer.\n * @returns {object} - An object that will be sent as-is to the peer\n */\n async toJSON() {\n const obj = super.toJSON();\n obj.type = 'batch';\n obj.ackToken = this.ackToken;\n obj.payload = await Promise.all(\n this.messages.map((msg) => msg.toJSON())\n );\n\n return obj;\n }\n\n /**\n * Update own send props and of included messages\n */\n updateSendProps() {\n this.messages.forEach(m => m.updateSendProps());\n super.updateSendProps();\n }\n}\n\nObject.assign(module.exports, {\n ConnectionBatch\n});\n\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/batch.js?");
5494
+ "use strict";
5495
+ eval("/**\n * @file protocol/connection/batch.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The ConnectionBatch is a factory for a batch message class that is bound\n * to a connection instance.\n * \n * The batch message is a type of message that can contain an array of request\n * and response messages.\n */\n\n\nconst ConnectionBatch = (ConnectionMessage) => class Batch extends ConnectionMessage {\n constructor(messages=[], ackToken) {\n super();\n // batch messages are internal so auto-assign ID on construction\n this.id = this.connection.messageFactory.generateMessageId();\n this.messages = messages;\n this.messageObjects = null;\n this.ackToken = ackToken;\n }\n\n /**\n * This static method is used by the connection to rehydrate a batch that was received.\n * Note that the messageObjects property is populated instead of the messages property\n * because the messages are still in their dehydrated object format.\n * @param {object} batchObj\n */\n static fromJSON(batchObj) {\n const batch = new this();\n batch.id = batchObj.id;\n batch.dcpsid = batchObj.dcpsid;\n batch.nonce = batchObj.nonce;\n batch.messageObjects = batchObj.payload;\n return batch;\n }\n\n /**\n * This method is used to dehydrate the request into a plain object before it is sent to the peer.\n * @returns {object} - An object that will be sent as-is to the peer\n */\n async toJSON() {\n const obj = super.toJSON();\n obj.type = 'batch';\n obj.ackToken = this.ackToken;\n obj.payload = await Promise.all(\n this.messages.map((msg) => msg.toJSON())\n );\n\n return obj;\n }\n\n /**\n * Update own send props and of included messages\n */\n updateSendProps() {\n this.messages.forEach(m => m.updateSendProps());\n super.updateSendProps();\n }\n}\n\nObject.assign(module.exports, {\n ConnectionBatch\n});\n\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/batch.js?");
5496
5496
 
5497
5497
  /***/ }),
5498
5498
 
@@ -5504,7 +5504,7 @@ eval("/**\n * @file protocol/connection/batch.js\n * @author Ryan Ro
5504
5504
  /***/ (function(module, exports, __webpack_require__) {
5505
5505
 
5506
5506
  "use strict";
5507
- eval("/**\n * @file protocol/connection/connection.js\n * @author Ryan Rossiter\n * @author KC Erb\n * @date January 2020, Feb 2021\n *\n * A Connection object represents a connection to another DCP entity. \n * A DCP connection may 'live' longer than the underlying protocol's connection,\n * and the underlying protocol connection (or, indeed, protocol) may change\n * throughout the life of the DCP connection.\n * \n * DCP connections are uniquely identified by the DCP Session ID, specified by\n * the dcpsid property, present in every message body. This session id negotiated during connection,\n * with the initiator and target each providing half of the string.\n *\n * \n * State Transition Diagram for Connection.state:\n *\n * initial connecting established waiting close-wait closing closed\n * ====================================================================================================\n * |- connect() ->\n * |----- connect() ----->\n * |- establishTarget() ->\n * <-- revive() --|\n * |--------- doClose() ------->\n * |- doClose() ->\n * |--------------|---------------------|-------------> failTransport()\n * |--------------|---------------------|--------------|------------|------------> <------------| doClose()\n *\n * failTransport() takes a state from anywhere, sets it to waiting,\n * and sends it back to where it came from. doclose() takes a state\n * from anywhere and sends it to the coClose() state.\n *\n * Not until the established state can we count on things like a dcpsid, \n * peerAddress, identityPromise resolution and so on.\n */\n\n\n\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp');\nconst debuggingScheduler = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('scheduler');\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/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 { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { leafMerge } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\nconst { Synchronizer } = __webpack_require__(/*! dcp/common/concurrency */ \"./src/common/concurrency.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\n\nconst perf = typeof performance === 'undefined'\n ? requireNative('perf_hooks').performance\n : performance;\n\nconst { Transport } = __webpack_require__(/*! ../transport */ \"./src/protocol-v4/transport/index.js\");\nconst { Sender } = __webpack_require__(/*! ./sender */ \"./src/protocol-v4/connection/sender.js\");\nconst { Receiver } = __webpack_require__(/*! ./receiver */ \"./src/protocol-v4/connection/receiver.js\");\nconst { MessageFactory } = __webpack_require__(/*! ./message-factory */ \"./src/protocol-v4/connection/message-factory.js\");\nconst { MessageLedger } = __webpack_require__(/*! ./message-ledger */ \"./src/protocol-v4/connection/message-ledger.js\");\nconst { ValidityStampCache } = __webpack_require__(/*! ./validity-stamp-cache */ \"./src/protocol-v4/connection/validity-stamp-cache.js\");\nconst { getGlobalIdentityCache } = __webpack_require__(/*! ./identity-cache */ \"./src/protocol-v4/connection/identity-cache.js\");\nconst { makeEBOIterator } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\n\nconst { ConnectionMessage } = __webpack_require__(/*! ./message */ \"./src/protocol-v4/connection/message.js\");\nconst { ConnectionRequest } = __webpack_require__(/*! ./request */ \"./src/protocol-v4/connection/request.js\");\nconst { ConnectionResponse } = __webpack_require__(/*! ./response */ \"./src/protocol-v4/connection/response.js\");\nconst { ConnectionBatch } = __webpack_require__(/*! ./batch */ \"./src/protocol-v4/connection/batch.js\");\nconst { ConnectionAck } = __webpack_require__(/*! ./ack */ \"./src/protocol-v4/connection/ack.js\");\nconst { ErrorPayloadCtorFactory } = __webpack_require__(/*! ./error-payload */ \"./src/protocol-v4/connection/error-payload.js\");\nconst isDebugBuild = __webpack_require__(/*! dcp/common/dcp-build */ \"./src/common/dcp-build.js\").build === 'debug';\n\nlet globalConnectionId = 0;\n\nconst CONNECTION_STATES = [\n 'initial',\n 'connecting',\n 'established',\n 'waiting', /* Targets only, when send fails */\n 'close-wait', /* Target of close message is in this state until response is acknowledged */\n 'closing',\n 'closed',\n]\n\nclass Connection extends EventEmitter {\n static get VERSION() {\n return '5.0.0'; // Semver format\n }\n\n static get VERSION_COMPATIBILITY() {\n return '5.0.0'; // Semver format, can be a range\n }\n\n /**\n * @constructor Connection form 4:\n * Create a DCP Connection object for an initiator.\n * @param {string} target The string version (ie href) of the URL of the target to connect to.\n * @param {wallet.IdKeystore} [idKeystore]\n * @param {Object} [connectionOptions]\n * @see form 1\n */\n /**\n * @constructor Connection form 3:\n * Create a DCP Connection object for an initiator.\n * @param {DcpURL|URL} target The URL of the target to connect to.\n * @param {wallet.IdKeystore} [idKeystore]\n * @param {Object} [connectionOptions]\n * @see form 1\n */\n /**\n * @constructor Connection form 2:\n * Create a DCP Connection object for a target.\n * @param {wallet.IdKeystore} [idKeystore]\n * @param {Object} [connectionOptions]\n * @see form 1\n */\n /**\n * @constructor Connection form 1\n * Create a DCP Connection object. \n * \n * @note Connection objects exist for the lifetime of a given DCP connection \n * (session), whether or not the underlying transport (eg internet protocol) is connected or not. Once \n * the DCP session has ended, this object has no purpose and is not reusable.\n * \n * @param {object|undefined} target Undefined when we are the target, or an object describing the target. This object \n * may contain the following properties; 'location' is mandatory:\n * - location: a DcpURL that is valid from the Internet\n * - friendLocation: a DcpURL that is valid from an intranet; if\n * both location and friendLocation specified, the best\n * one will be chosen by examining IP addresses\n * - identity: a object with an address property which is an\n * instanceof wallet.Address which corresponds to the peer's\n * identity; this overrides the identity cache unless\n * connectionOptions.strict is truey.\n * @param {wallet.IdKeystore} [idKeystore] The keystore used to sign messages; used for non-repudiation.\n * If not specified, a dynamically-generated keystore will be used.\n * \n * @param {Object} [connectionOptions] Extra connection options that aren't defined via dcpConfig.dcp.connectionOptions.\n * These options include:\n * - identityUnlockTimeout: Number of (floating-point) seconds to leave the identity \n * keystore unlocked between invocations of Connection.send\n */\n constructor(target, idKeystore, connectionOptions)\n {\n /* polymorphism strategy: rewrite all to form 1 before super */\n if (target instanceof wallet.Keystore) /* form 2 */\n { \n connectionOptions = arguments[2];\n idKeystore = arguments[1];\n target = undefined;\n }\n if (typeof connectionOptions === 'undefined')\n connectionOptions = {};\n\n if (target instanceof URL) /* form 3.2 */\n target = { location: new DcpURL(target) };\n else if (DcpURL.isURL(target)) /* form 3.1 */\n target = { location: new DcpURL(target) };\n else if (target instanceof String || typeof target === 'string') /* form 4 */\n target = { location: new DcpURL(target) };\n\n assert((typeof target === 'undefined') || (typeof target === 'object' && DcpURL.isURL(target.location)));\n assert(typeof connectionOptions === 'object');\n \n super(`Protocol Connection (${target ? 'initiator' : 'target'})`);\n\n if (target) {\n this.isInitiator = true;\n this.debugLabel = 'connection(i):';\n this._target = target;\n } else {\n this.isInitiator = false;\n this.debugLabel = 'connection(t):';\n }\n\n if (idKeystore) {\n this.identityPromise = (async () => await idKeystore)();\n } else {\n this.identityPromise = wallet.getId();\n }\n\n this.identityPromise.then((keystore) => {\n this.identity = keystore;\n debugging('connection') && console.debug(this.debugLabel, 'identity is', keystore.address);\n return keystore;\n });\n\n this.ttl = {\n ntp: false,\n }\n \n // Init internal state / vars\n this.state = new Synchronizer(CONNECTION_STATES[0], CONNECTION_STATES);\n this.state.on('change', (s) => this.emit('readyStateChange', s) );\n\n this._id = globalConnectionId++;\n this.debugLabel = this.debugLabel.replace(')', `#${this._id})`);\n debugging('connection') && console.debug(this.debugLabel, 'connection id is', this._id, `target is ${target && target.location}`);\n this.dcpsid = null;\n this.peerAddress = null;\n this.transport = null;\n this.maxConnectionTimeout = null;\n this.messageFactory = new MessageFactory(this);\n this.messageLedger = new MessageLedger(this);\n\n this.Message = ConnectionMessage(this);\n this.Request = ConnectionRequest(this.Message);\n this.Response = ConnectionResponse(this.Message);\n this.Batch = ConnectionBatch(this.Message);\n this.Ack = ConnectionAck(this.Message);\n this.ErrorPayload = ErrorPayloadCtorFactory(this);\n this.connectTime = Date.now();\n\n this.openRequests = {};\n ValidityStampCache.init();\n\n this.receiver = new Receiver(this, this.messageLedger);\n this.receiver.on('request', (req) => this.emit('request', req));\n\n debugging('connection') && console.debug(this.debugLabel, `new; ${target && target.location || '<target>'}`);\n\n this.configureConnectionForUrl = (url) => {\n if (typeof connectionOptions.ttl === 'number')\n connectionOptions = Object.assign({}, connectionOptions, { ttl: { default: connectionOptions.ttl } });\n \n this.url = url;\n this.connectionOptions = leafMerge(/* ordered from least to most specific */\n dcpConfig.dcp.connectionOptions.default,\n this.url && dcpConfig.dcp.connectionOptions[this.url.hostname],\n this.url && dcpConfig.dcp.connectionOptions[this.url.origin],\n dcpConfig.dcp.connectionOptions[this.isInitiator ? this.url.href : 'target'],\n connectionOptions\n );\n\n // ensure we're using a _copy_ of the transports list, not mutating the original\n this.connectionOptions.transports = [...this.connectionOptions.transports];\n this.originalConnectionTransports = [].concat(this.connectionOptions.transports);\n this.transportsTried = 0;\n\n this.ttl = leafMerge(this.ttl, this.connectionOptions.ttl);\n this.unlockTimeout = this.connectionOptions.identityUnlockTimeout;\n this.connectionOptions.id = this._id;\n this.EBOIterator = makeEBOIterator(1000, 60000);\n\n assert(this.unlockTimeout >= 0);\n assert(typeof this.connectionOptions.ttl.min === 'number');\n assert(typeof this.connectionOptions.ttl.max === 'number');\n assert(typeof this.connectionOptions.ttl.default === 'number');\n\n this.secureLocation = determineIfSecureLocation(this);\n }\n }\n\n /**\n * This method is an instantiator/factory function for building a connection\n * that will act as the target in a new protocol connection. It's a little\n * like making a new connection and calling `connect` on it, except that\n * instead of having a url to connect to we have a transport which should\n * be ready to emit the connect message from the initiator.\n * \n * @param {wallet.Keystore} ks - Keystore to associate to the new connection.\n */\n static async newTarget(url, ks, transport) {\n const target = new Connection(undefined, ks, {\n ttl: {\n ntp: true, // targets should always be NTP-synced\n }\n });\n\n target.configureConnectionForUrl(url);\n assert(target.connectionOptions.ttl.ntp);\n \n await target.identityPromise;\n target.transport = transport;\n target.sender = new Sender(target);\n target.state.set('initial', 'connecting');\n return target;\n }\n\n /**\n * When invoked by the initator, this method establishes the connection by connecting\n * to the target provided to the constructor.\n * \n * When a dcpConfig-fragment is used as the target object, we determine automatically if \n * we should be using the location or the friendLocation property at this point.\n *\n * This function is also where the connectionOptions are merged together. This means \n * that if either of these objects come from dcpConfig and we HUP a service running this\n * code, that the new configuration will be used to connect, instead of the configuration \n * that was defined at the time of construction.\n *\n * @throws when called by a target, or when called more than once\n */\n async connect() {\n var presharedPeerAddress;\n \n let transportName;\n \n if (!this.state.in(['initial', 'connecting'])) {\n throw new Error(`Can only invoke connect in initial/connecting state. Current state: ${this.state.valueOf()}.`, );\n }\n\n // make connect re-entrant if called during the connecting phase\n if (this.state.is('connecting'))\n {\n /* nth connect attempt on same Connection instance before actual connection -- swallow attempts and wait until connected */\n do\n {\n await this.state.until(['established', 'close-wait', 'closing', 'closed']);\n } while(this.state.is('waiting'));\n\n if (!this.state.is('established'))\n throw new Error(`Could not establish connection to ${this.url}; in state ${this.state}`);\n return;\n }\n\n this.state.set('initial', 'connecting');\n\n if (this._target && this._target.hasOwnProperty('friendLocation') && await isFriendlyURL(this._target.friendLocation))\n this.configureConnectionForUrl(this._target.friendLocation);\n else\n this.configureConnectionForUrl(this._target.location);\n\n const transport = this.fetchTransport();\n this.transport = transport.transport;\n transportName = transport.moduleName;\n this.sender = new Sender(this);\n // create sender before promises so that we can still enqueue messages before hopping off the event loop\n await this.identityPromise;\n await this.connectToTarget(transportName);\n const establishResults = await this.sender.establish().catch( (err) => {\n console.error('Failed to establish connection.');\n this.close(err, true);\n throw err;\n });\n const dcpsid = establishResults.dcpsid;\n const peerAddress = wallet.Address(establishResults.peerAddress);\n\n if (!this.connectionOptions.strict && this._target.identity)\n {\n if (determineIfSecureConfig())\n {\n presharedPeerAddress = (await this._target.identity).address;\n debugging('connection') && console.debug('Using preshared peer address', presharedPeerAddress);\n }\n }\n this.ensureIdentity(peerAddress, presharedPeerAddress); /** XXXwg possible resource leak: need cleanup; need try {} catch->emit(cleanup) */\n \n // checks have passed, now we can set props\n this.peerAddress = peerAddress;\n if (this.dcpsid)\n throw new DCPError(`Reached impossible state in connection.js; dcpsid already specified ${this.dcpsid} (${this.url})`, 'DCPC-1004');\n this.dcpsid = dcpsid;\n \n // Update state\n this.state.set('connecting', 'established'); /* established => dcpsid has been set */\n this.emit('connected', this.url);\n this.sender.finishSetup();\n }\n\n moreTransportsExist() {\n return this.connectionOptions\n && this.connectionOptions.transports\n && Array.isArray(this.connectionOptions.transports)\n && this.connectionOptions.transports.length > 0;\n }\n \n /**\n * Initiators only.\n * Try to get and establish a transport from the transports list.\n * @returns {Transport} a new transport instance or false if no transports are left to try\n */\n fetchTransport() {\n //We want to cycle our possible transports until the maxConnectionTimeout runs out or we have a connection\n const moduleName = this.connectionOptions.transports.shift();\n this.connectionOptions.transports.push(moduleName);\n this.transportsTried++;\n debugging() && console.log(this.debugLabel, 'fetchTransport trying ', moduleName, `remaining transports: ${this.connectionOptions.transports.length}`)\n if (!moduleName) {\n console.error('Error: All transports have failed to connect/transmit within limits. Closing connection.');\n this.close('all transports failed', true).catch(error => {\n console.error(`Error: failed to close transport:`, error);\n });\n throw new DCPError(`Failed to connect to ${this.url}, no transport could be established`, 'ENOTRANSPORT');\n }\n const TransportClass = Transport.require(moduleName);\n const transport = new TransportClass(null, this.connectionOptions);\n transport.on('message', (m) => {\n this.onMessage(JSON.parse(m))\n });\n return { transport, moduleName };\n }\n\n /**\n * Initiators only\n * Resolves when we have connected to target using a transport.\n * Calls \"failTransport\" if we run out of time / attempts to connect.\n * Let's transport handle failure but also is watching for it too.\n */\n async connectToTarget(currentTransport) {\n debugging('connection') && console.log(this.debugLabel, `Using transport \"${this.transport.name}\" ... connecting to ${this.url}.`);\n const globalWaitMs = dcpConfig.dcp.maxConnectionTimeout;\n const connectWaitMs = this.transport.connect(this.url, this.connectionOptions);\n\n this.maxConnectionTimeout = this.maxConnectionTimeout ? this.maxConnectionTimeout\n : setTimeout(this.close.bind(this, `Timeout to have a transport connect reached at ${globalWaitMs} ms`, true), globalWaitMs);\n\n await new Promise( (resolve) => {\n // Listener for a connect-failed, will invoke EBO algorithm to retry the connection\n this.transport.once('connect-failed', () => {\n if (this.transportsTried >= this.connectionOptions.transports.length)\n {\n this.transportsTried = 0;\n const backoffTime = this.EBOIterator.next().value;\n debugging('connection') && console.debug(this.debugLabel, 'connecting in', backoffTime / 1000, 'seconds');\n this.failTransport('Connect-failed event handler fired.', backoffTime);\n }\n else\n {\n this.failTransport('Connect-failed event handler fired.');\n }\n });\n\n this.transport.once('connected', () => {\n clearTimeout(this.maxConnectionTimeout);\n this.maxConnectionTimeout = null;\n /* When we successfully connect a transport, we know that transport has potential to consistently work,\n * and disconnects may happen for unrelated reason (such as switching wifi causing a disconnect). \n * Thus, we recreate our transport options list with this transport at the top\n */\n this.connectionOptions.transport = [currentTransport].concat(this.originalConnectionTransports);\n resolve();\n });\n });\n this.transport.on('reconnect', () => {\n debugging('connection') && console.log(this.debugLabel, `Transport reconnected to ${this.url}`);\n // Send a keepalive to the remote end, to establish the new Transport with this Connection\n this.keepalive();\n this.emit('reconnect');\n });\n }\n\n /**\n * Target gains full status once dcpsid and peerAddress\n * are provided by first connect request.\n * @param {string} dcpsid dcpsid\n * @param {wallet.Address} peerAddress Address of peer\n */\n establishTarget(dcpsid, peerAddress) {\n this.connectResponseId = Symbol(); // un-register ConnectResponse\n this.peerAddress = peerAddress;\n if (this.dcpsid)\n throw new DCPError(`Reached impossible state in connection.js; dcpsid already specified ${this.dcpsid}!=${dcpsid} (${this.url})`, 'DCPC-1005');\n this.dcpsid = dcpsid;\n this.state.set('connecting', 'established'); /* established => dcpsid has been set */\n }\n\n ensureIdentity (peerAddress, presharedPeerAddress) {\n let identityCache = getGlobalIdentityCache();\n \n if (!identityCache.validateIdentity(this.url.href, peerAddress)) {\n if (presharedPeerAddress)\n {\n console.error(`**** Warning: preshared peer address ${presharedPeerAddress} does not match address ${identityCache.getIdentity(this.url.href)} provided for ${this.url} in identity cache ****`);\n __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\").sleep(10);\n identityCache.insert(this.url.href, peerAddress);\n }\n throw new DCPError(`Identity address ${peerAddress} does not match the saved key for ${this.url}`,'EADDRCHANGE');\n }\n }\n\n /**\n * Check that the transport has given us a message worth dealing with then\n * either let the receiver handle it (message) or the message ledger (ack).\n * @param {object} message parsed (but not validated) message object\n */\n async onMessage (message) {\n if (this.state.is('closed')) {\n debugging('connection') && console.warn(this.debugLabel, 'onMessage was called on a closed connection.');\n return;\n }\n /**\n * We always ack a duplicate transmission.\n * This must happen before validation since during startup we may lack a\n * nonce or dcpsid (depending on whether initiator or target + race).\n */\n if (this.isDuplicateTransmission(message)) {\n debugging('connection') && console.debug(this.debugLabel, 'duplicate message:', message.body);\n this.transport.send(this.lastAckSigned);\n return;\n }\n\n let messageIsValid = false;\n try {\n messageIsValid = this.isMessageValid(message);\n } catch (error) {\n console.error('Message passed to validator threw an Error:', error);\n }\n /* messageIsValid with either be true, or a string with the reason the message isn't valid*/\n if (messageIsValid !== true) {\n if (debugging('connection'))\n this.close(`invalid message: ${messageIsValid}\\n ${JSON.stringify(message)}`, true);\n else\n this.close(`invalid message: ${messageIsValid}`, true);\n return;\n }\n\n if (message.body.type === \"ack\") {\n const ack = new this.Ack(message.body);\n this.messageLedger.handleAck(ack);\n return;\n } else if (message.body.type !== 'unhandled-message') {\n this.lastMessage = message;\n await this.ackMessage(message);\n }\n this.receiver.onMessage(message);\n }\n\n async ackMessage(message) {\n debugging('connection') && console.debug(this.debugLabel, 'acking message.');\n const ack = new this.Ack(message);\n const signedMessage = await ack.sign(this.identity);\n this.transport.send(signedMessage);\n this.lastAck = ack;\n this.lastAckSigned = signedMessage;\n }\n\n /**\n * Checks if the batch we just received has the same nonce\n * as the most-recently received batch.\n * @param {object} messageJSON\n */\n isDuplicateTransmission(messageJSON) {\n return this.lastMessage && this.lastMessage.body.nonce === messageJSON.body.nonce;\n }\n\n /**\n * This method is used to perform validation on all types of messages.\n * It validates the signature, DCPSID, nonce, and the peerAddress.\n * @param {Object} message\n */\n isMessageValid(message) {\n const owner = new wallet.Address(message.owner);\n const signatureValid = owner.verifySignature(message.body, message.signature);\n // keeps track of the connection url so that if the message is invalid, the source can be traced easily\n // if message is invalid, it will return the reason for the error plus the URL, otherwise will return true.\n let currURL = \"Could not retrieve connection URL\"\n if (this.url) {\n currURL = this.url.href;\n }\n if (!signatureValid) {\n debugging('connection') && console.warn(\"Message has an invalid signature, aborting connection\");\n return \"invalid message signature \" + currURL;\n } else if (message.body.type === 'unhandled-message') {\n // this special message type may not have a dcpsid, peerAddress, etc.\n debugging('connection') && console.warn(\"Target Error - target could not process message.\", JSON.stringify(message.body),\n \"Aborting connection.\");\n return \"target could not process message \" + currURL;\n } else if (this.state.in(['established', 'closing', 'close-wait'])) {\n const body = message.body;\n\n /**\n * Security note:\n * We don't require the dcpsid to match on an ack because the connect response\n * ack doesn't have a dcpsid until after it is processed. Also ack's are protected\n * by ack tokens and signatures, so this doesn't leave a hole, just an inconsistency.\n */\n if (message.body.type !== 'ack' && body.dcpsid !== this.dcpsid) {\n debugging('connection') && console.warn(\"Received message's DCPSID does not match, aborting connection\\n\",\n \"Wallet owner:\", JSON.stringify(owner), '\\n',\n \"(ours)\", this.dcpsid, (Date.now() - this.connectTime)/1000, \"seconds after connecting - state:\", this.state._, \"\\n\", \n \"(theirs)\", body.dcpsid);\n if(body.dcpsid.substring(0, body.dcpsid.length/2) !== this.dcpsid.substring(0, this.dcpsid.length/2)){\n debugging('connection') && console.warn(\" Left half of both DCPSID is different\");\n }\n if(body.dcpsid.substring(body.dcpsid.length/2 + 1, body.dcpsid.length) !== this.dcpsid.substring(this.dcpsid.length/2 + 1, body.dcpsid.length)){\n debugging('connection') && console.warn(\" Right half of both DCPSID is different\");\n }\n debugging('connection') && console.log(\"The full message that caused this error:\");\n debugging('connection') && console.log(JSON.stringify(message, null, 2));\n debugging('connection') && console.log(\"The stack up to this point is:\");\n debugging('connection') && console.trace();\n debugging('connection') && console.log(\"The target has address:\")\n debugging('connection') && console.log(JSON.stringify(this.peerAddress));\n debugging('connection') && console.log(\"The transport is:\");\n debugging('connection') && console.log(this.transport);\n return \"DCPSID do not match\";\n } else if (!this.peerAddress.eq(owner)) {\n debugging('connection') && console.warn(\"Received message's signature address does not match peer address, aborting connection\\n\",\n \"(signature addr)\", owner, '\\n',\n \"(peer addr)\", this.peerAddress);\n return \"received message signature does not match peer address\";\n } else if (message.body.type !== 'ack' && this.lastAck.nonce !== message.body.nonce) {\n debugging('connection') && console.warn(\"Received message's nonce does not match expected nonce, aborting connection\\n\");\n debugging('connection') && console.debug(this.debugLabel, this.lastAck.nonce, message.body.nonce);\n return \"received message's nonce does not match expected nonce \" + currURL;\n }\n } else {\n // only validate signature for unestablished connections\n // while connecting, the target gets its nonce from the initiator\n assert(this.state.is('connecting'));\n if (!this.isInitiator) this.sender.nonce = message.body.nonce;\n }\n\n return true;\n }\n\n /**\n * Targets Only.\n * The receiver creates a special connect response and the connection\n * needs to know about it to get ready for the ack. See `isWaitingForAck`.\n * @param {Message} message message we are sending out and waiting to\n * ack'd, probably a batch containing the response.\n */\n registerConnectResponse(message) {\n this.connectResponseId = message.id;\n }\n\n /**\n * Targets only\n * During the connection process a target sends a connect\n * response to an initiator and the initiator will ack it. Since transports\n * are not tightly coupled, we have no authoritative way to route the ack back\n * to the right connection. So a connection briefly registers the ack it\n * is looking for in this case. It will formally validate the ack after routing.\n * @param {string} messageId id of the message this ack is acknowledging.\n */\n isWaitingForAck(messageId) {\n return messageId === this.connectResponseId;\n }\n\n /**\n * Targets: nothing to do, we are waiting for initiator.\n * Initiators: give up on current transport and try next available option.\n * For now we just have socketio so that means we throw an error.\n * @return {Promise<boolean>} indicating whether we were able to find a new transport.\n */\n async failTransport (failureDescription, retryWaitTime = 0) {\n let transportName;\n const preWaitingState = this.state.valueOf(); /* XXXwg bogus - need explicit and reasonable source states */\n\n /* If we are already in process of closing or already waiting, we don't want to beat \n * the dead horse. Let it stay dead with no transport\n */\n if (['closing', 'closed', 'close-wait', 'waiting'].includes(preWaitingState))\n return;\n\n this.state.testAndSet(preWaitingState, 'waiting');\n debugging('connection') && console.log(this.debugLabel, `Transport \"${this.transport.name}\" has failed.`);\n \n if (this.isInitiator) {\n if (!this.moreTransportsExist()) {\n console.error('Transport failed:', failureDescription);\n // fetchTransport will immediately fail\n }\n debugging('connection') && console.log(this.debugLabel, `Attempting to use next transport.`);\n this.transport.close();\n const transport = this.fetchTransport();\n this.transport = transport.transport;\n transportName = transport.moduleName;\n \n await new Promise(r => setTimeout(r, retryWaitTime));\n\n this.state.set('waiting', preWaitingState);\n await this.connectToTarget(transportName);\n }\n }\n\n /**\n * Targets only\n * Give target another transport to try sending messages on again.\n */\n revive(transport) {\n this.transport = transport;\n this.state.set('waiting', 'established');\n }\n\n /**\n * Put connection into close-wait state so that a call to `close`\n * in this state will *not* trigger sending a `close` message to the peer.\n * Then call close once the passed promise resolves.\n *\n * @note: This function is called when the remote end of the transport sends\n * a close command, and the waitPromise in that case is the response.\n */\n async closeWait (waitPromise)\n {\n const preCloseState = this.state.valueOf();\n\n debugging('connection') && console.debug(this.debugLabel, `responding to close. state=closeWait dcpsid=${this.dcpsid}`);\n\n if (this.state.is('closed'))\n {\n debugging('connection') && console.debug(this.debugLabel, `remote asked us to close a closed connection; dcpsid=${this.dcpsid}`);\n return;\n }\n\n // wait for waitPromise to resolve, reject if not within time limit\n try {\n await new Promise((resolve, reject) => {\n const closeWaitTimeout = setTimeout(() => {\n reject(new Error('Connection.closeWait timed out'));\n }, 10000); /* hardcode 10s wait /wg mar 2021 */\n waitPromise.then(resolve, reject).finally(() => clearTimeout(closeWaitTimeout));\n })\n debugging('connection') && console.debug(this.debugLabel, `close response ACK'd; closing. dcpsid=${this.dcpsid}`);\n } catch (error) {\n debugging('connection') && console.debug(this.debugLabel, `${error.message}; closing. dcpsid=${this.dcpsid}`);\n }\n // continue with close in either case\n let reason = 'received close from peer';\n if (this.url)\n reason += ` (${this.url})`;\n\n // If we're already closing, wait for it to complete then resolve\n // WARNING: any place we transition to closing or close-wait, we MUST guarantedd\n // that 'end' will be emitted, or this code will hang forever!\n if (this.state.in(['close-wait', 'closing'])) {\n return new Promise((resolve) => {\n this.once('end', resolve) /* eventually fired by doClose elsewhere */\n });\n }\n\n if (this.state.is('closed')) /* closed somehow on us during await */\n return;\n\n this.state.set(preCloseState, 'close-wait');\n return this.doClose(preCloseState, reason, true);\n }\n\n /**\n * This method will begin gracefully closing the protocol connection.\n * It will only close after sending all pending messages.\n * \n * @param {string|Error} [reason] Either an Error or a message to use in the Error that will reject pending sends.\n * @param {boolean} [immediate] If true, does not wait to send messages or the `close` request.\n *\n * @return a Promise which resolves when the connection has been confirmed closed and the end event has been fired.\n */\n close (reason=null, immediate=false)\n {\n if (this.state.is('closed')) return;\n\n const preCloseState = this.state.valueOf();\n debugging('connection') && \n console.debug(this.debugLabel, \n `close; dcpsid=${this.dcpsid} state=${preCloseState} reason=${reason} immediate=${immediate} stack=${new Error().stack}`);\n\n // If we're already closing, wait for it to complete then resolve\n if (this.state.in(['close-wait', 'closing'])) {\n return new Promise((resolve) => {\n this.once('end', resolve)\n });\n }\n\n // Put in closing state no matter the current state\n this.state.set(preCloseState, 'closing');\n\n // Perform actual work of closing\n return this.doClose(preCloseState, reason, immediate);\n }\n\n /**\n * sends close message to peer and waits for response\n * @note: This function is not reentrant!\n */\n async closeGracefully() {\n await new Promise((resolve, reject) => {\n // reject if we don't resolve within time limit\n const timeoutTimer = setTimeout(() => {\n reject(new DCPError('Connection.close timed out', 'DCPC-1003'));\n }, this.connectionOptions.closeTimeout * 1000);\n \n if (this.transport)\n {\n /* If we got as far as initializing a transport during connect(), send close\n * message to peer, should get a response before time is up.\n */\n const closeMessage = this.messageFactory.buildMessage('close');\n this.sender.enqueue(closeMessage).then(resolve, reject).finally(() => clearTimeout(timeoutTimer));\n }\n })\n // hop off event loop so that close response ack can get out\n await new Promise(r=>setTimeout(r));\n }\n\n /**\n * Core close functionality shared by `close` and `closeWait`\n *\n * @param {string} preCloseState the state that the connection was in at the start of the\n * invocation of close() or closeWait()\n *\n * @note: this function is not reentrant due to closeGracefully\n */\n async doClose(preCloseState, reason, immediate) {\n const dcpsid = this.dcpsid;\n\n try\n {\n assert(this.state.valueOf() === 'closing' || this.state.valueOf() === 'close-wait');\n \n // Emit the close event the moment we know we are going to close, \n // so we can catch the close event and reopen the connection\n this.emit('close', dcpsid /* should be undefined in initial state */);\n \n if (preCloseState === 'established' && !immediate) {\n try {\n await this.closeGracefully();\n } catch(e) {\n debugging() && console.warn(`Warning: could not close connection gracefully. connectionid=${this._id}, dcpsid=,${this.dcpsid}, url=${this.url ? this.url.href : 'unknown url'} - (${e.message})`);\n }\n }\n\n // can delete these now that we've sent the close message\n this.dcpsid = null;\n this.peerAddress = null;\n\n /* build error message */\n let rejectErr;\n if (reason instanceof Error) {\n rejectErr = reason;\n } else {\n let message;\n if (typeof reason === 'string' || reason instanceof String ) {\n message = reason;\n } else {\n if (this.url)\n message = `Connection closed (url: ${this.url}, dcpsid: ${dcpsid})`;\n else\n message = `Connection closed (dcpsid: ${dcpsid})`;\n }\n rejectErr = new DCPError(message, 'DCPC-1002');\n }\n \n if (preCloseState !== 'initial')\n {\n // If we are closing the connection, we want to resolve all of the .send() promises with success = false */\n // Reject any pending transmissions in the message ledger\n this.messageLedger.silentFailMessage = true;\n this.messageLedger.failAllTransmissions(rejectErr);\n \n if (this.transport)\n {\n try { this.sender.shutdown(); }\n catch(e) { debugging() && console.warn(`Warning: could not shutdown sender; dcpsid=,${dcpsid}`, e); }\n\n try { this.transport.close(); }\n catch(e) { debugging() && console.warn(`Warning: could not close transport; dcpsid=,${dcpsid}`, e); }\n }\n }\n\n this.state.setIf('closing', 'closed');\n this.state.setIf('close-wait', 'closed');\n }\n finally\n {\n this.emit('end'); /* end event resolves promises on other threads for closeWait and close */\n }\n }\n\n /**\n * Sends a message to the connected peer. If the connection has not yet been established,\n * this routine will first invoke this.connect().\n * \n * @param {...any} args\n * @returns {Promise<Response>} a promise which resolves to a response.\n */\n async send(...args) {\n if (this.state.in(['initial','connecting'])) {\n // Original message will be rejected so we don't want a second reject\n // here since it would be uncaught.\n await this.connect().catch( e => {\n throw new DCPError(`Connection (${this._id}) failed to connect to ${this.url ? this.url.href : '<unknown url>'}: ${e.message}`, e.code);\n });\n }\n\n if (this.state.in(['closing', 'closed'])) {\n let e = new DCPError(`Connection (${this._id}) in state '${this.state}' cannot send. (url: ${this.url})`, 'DCPC-1001');\n throw e;\n }\n\n const message = this.messageFactory.buildMessage(...args);\n return this.sender.enqueue(message);\n }\n\n /**\n * Sends a v3-over-v4 request\n * @param url {string} v3 DcpURL\n * @param message {object} v3 \"message\" value; may be empty\n * @param authKey {object} (v3 signing|v4 authorizing) keystore, if different from identity\n * @return {Promise<object>} Resolves with v3 route's response data\n */\n async sendv3(url, message = {}, authKey = null) {\n const v3Data = {\n url,\n message,\n };\n \n const req = new this.Request('v3', v3Data);\n\n if (authKey && !this.identity.address.eq(authKey.address)) {\n await req.authorize(authKey);\n }\n \n let response = await req.send()\n\n .catch((error) => {\n return {\n success: false,\n payload: error,\n }\n });\n\n // fail at v4-protocol-level? throw it\n if (!response.success) {\n throw response.payload;\n }\n \n // fail at v3-level? also throw it\n if (response.payload.v3status === 'reject')\n throw response.payload.v3rejection;\n \n return response.payload.v3resolution;\n }\n\n /**\n * This routine returns the current time for the purposes of\n * populating the Request message payload.validity.time property.\n * \n * @returns {Number} the integer number of seconds which have elapsed since the epoch\n */\n currentTime() {\n let msSinceEpoch;\n if (this.ttl.ntp) {\n msSinceEpoch = Date.now();\n } else {\n const msSinceLastReceipt = perf.now() - this.receiver.lastResponseTiming.receivedMs;\n msSinceEpoch = this.receiver.lastResponseTiming.time * 1000 + msSinceLastReceipt;\n }\n return Math.floor(msSinceEpoch / 1000);\n }\n\n /**\n * This method sends a keepalive to the peer, and resolves when the response has been received.\n */\n keepalive() {\n return this.send('keepalive');\n }\n\n /**\n * This method checks to see a service connection is established, typically will be given a timeOut condition when invoked\n */\n async setCheckConnectionTimeout(source) {\n // failTimeout is checked to account for a race condition if the connection is established at the same time as the fail timeout expires \n let failTimeout = false;\n function checkConnectionEstablished() {\n if(failTimeout){\n failTimeout = false;\n throw new DCPError(`Connection to ${this.url} from ${source} not established within 30 seconds, instead is in state ${this.state}`, 'DCP-1006');\n }\n }\n failTimeout = setTimeout(checkConnectionEstablished.bind(this), 30000);\n this.once('connected', () => {\n if(failTimeout){\n clearTimeout(failTimeout);\n failTimeout = false;\n }\n });\n await this.connect();\n }\n\n}\n\n/**\n * Returns true if friendLocation should work in place of location from this host.\n * This allows us to transparently configure inter-daemon communication that uses\n * local LAN IPs instead of bouncing off the firewall for NAT.\n */\nasync function isFriendlyURL(url)\n{\n var remoteIp, dnsA;\n var ifaces;\n \n if (url.hostname === 'localhost')\n return true;\n\n switch(url.protocol)\n {\n case 'http:':\n case 'https:':\n case 'ws:':\n case 'tcp:':\n case 'udp:':\n case 'dcpsaw:':\n break;\n default:\n return false;\n }\n\n /* Consider same-origin match friendly */\n if (__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").isBrowserPlatform)\n return url.origin === window.location.origin;\n\n /* Convert an IP address to a 32-bit int in network order */\n function i32(addr)\n {\n var ret = 0;\n var octets = addr.split('.');\n\n ret |= octets[0] << 24; /* Note: JS ints are signed 32, but that doesn't matter for masking */\n ret |= octets[1] << 16;\n ret |= octets[2] << 8;\n ret |= octets[3] << 0;\n\n return ret;\n }\n \n /* Consider machines in same IPv4 subnet friendly */\n dnsA = await requireNative('dns').promises.lookup(url.hostname, {family: 4});\n if (!dnsA)\n return false;\n remoteIp = i32(dnsA.address);\n ifaces = requireNative('os').networkInterfaces();\n for (let ifaceName of Object.keys(ifaces))\n {\n for (let alias of ifaces[ifaceName])\n {\n if (alias.family != 'IPv4')\n continue;\n\n let i32_addr = i32(alias.address);\n let i32_mask = i32(alias.netmask);\n\n if ((i32_addr & i32_mask) === (remoteIp & i32_mask))\n return true;\n }\n }\n\n return false;\n}\n\n/** \n * Determine if we got the scheduler config from a secure source, eg https or local disk.\n * We assume that all https transactions have PKI-CA verified.\n *\n * @note protocol::getSchedulerConfigLocation() is populated via node-libs/config.js or dcp-client/index.js\n *\n * @returns true or falsey\n */\nfunction determineIfSecureConfig()\n{\n var schedulerConfigLocation = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\").getSchedulerConfigLocation();\n var schedulerConfigSecure;\n \n if (schedulerConfigLocation && (schedulerConfigLocation.protocol === 'https:' || schedulerConfigLocation.protocol === 'file:'))\n {\n debugging('strict-mode') && console.log(`scheduler config location ${schedulerConfigLocation} is secure`); /* from casual eavesdropping */\n schedulerConfigSecure = true;\n }\n\n if (isDebugBuild)\n {\n debugging('strict-mode') && console.log(`scheduler config location is always secure for debug builds`);\n schedulerConfigSecure = 'debug';\n }\n\n debuggingScheduler() && console.debug(`Config Location ${schedulerConfigLocation} is ${!schedulerConfigSecure ? 'not secure' : 'secure-' + schedulerConfigSecure}`);\n return schedulerConfigSecure;\n}\n\n/**\n * Determine if a URL is secure by examinining the protocol, connection, and information about the \n * process; in particular, we try to determine if the dcp config was securely provided, because if \n * it wasn't, then we can't have a secure location, since the origin could be compromised.\n * \n * \"Secure\" in this case means \"secure against casual eavesdropping\", and this information should only\n * be used to refuse to send secrets over the transport or similar.\n *\n * @returns true or falsey\n */\nfunction determineIfSecureLocation(conn)\n{\n var isSecureConfig = determineIfSecureConfig();\n var secureLocation;\n \n if (!isSecureConfig) /* can't have a secure location without a secure configuration */\n return null;\n \n if (isDebugBuild || conn.url.protocol === 'https:' || conn.url.protocol === 'tcps:')\n secureLocation = true;\n else if (conn.isInitiator && conn._target.hasOwnProperty('friendLocation') && conn.url === conn._target.friendLocation)\n secureLocation = true;\n else if (conn.connectionOptions.allowUnencryptedSecrets)\n secureLocation = 'override';\n else\n secureLocation = false;\n\n (debugging() || debuggingScheduler()) && console.debug(`Location ${conn.url} is ${!secureLocation ? 'not secure' : 'secure-' + secureLocation}`);\n \n return secureLocation;\n}\n\nexports.Connection = Connection;\n\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/connection.js?");
5507
+ eval("/**\n * @file protocol/connection/connection.js\n * @author Ryan Rossiter\n * @author KC Erb\n * @date January 2020, Feb 2021\n *\n * A Connection object represents a connection to another DCP entity. \n * A DCP connection may 'live' longer than the underlying protocol's connection,\n * and the underlying protocol connection (or, indeed, protocol) may change\n * throughout the life of the DCP connection.\n * \n * DCP connections are uniquely identified by the DCP Session ID, specified by\n * the dcpsid property, present in every message body. This session id negotiated during connection,\n * with the initiator and target each providing half of the string.\n *\n * \n * State Transition Diagram for Connection.state:\n *\n * initial connecting established waiting close-wait closing closed\n * ====================================================================================================\n * |- connect() ->\n * |-----------------------------------------------------------------------------------> doClose()\n * |----- connect() ----->\n * |- establishTarget() ->\n * <-- revive() --|\n * |--------- doClose() ------->\n * |- doClose() ->\n * |--------------|---------------------|-------------> failTransport()\n * |--------------|---------------------|--------------|------------|------------> <------------| doClose()\n *\n * failTransport() takes a state from anywhere, sets it to waiting,\n * and sends it back to where it came from. doclose() takes a state\n * from anywhere and sends it to the coClose() state.\n *\n * Not until the established state can we count on things like a dcpsid, \n * peerAddress, identityPromise resolution and so on.\n */\n\n\n\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp');\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/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 { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { leafMerge, stringify } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\nconst { Synchronizer } = __webpack_require__(/*! dcp/common/concurrency */ \"./src/common/concurrency.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\n\nconst perf = typeof performance === 'undefined'\n ? requireNative('perf_hooks').performance\n : performance;\n\nconst { Transport } = __webpack_require__(/*! ../transport */ \"./src/protocol-v4/transport/index.js\");\nconst { Sender } = __webpack_require__(/*! ./sender */ \"./src/protocol-v4/connection/sender.js\");\nconst { Receiver } = __webpack_require__(/*! ./receiver */ \"./src/protocol-v4/connection/receiver.js\");\nconst { MessageFactory } = __webpack_require__(/*! ./message-factory */ \"./src/protocol-v4/connection/message-factory.js\");\nconst { MessageLedger } = __webpack_require__(/*! ./message-ledger */ \"./src/protocol-v4/connection/message-ledger.js\");\nconst { ValidityStampCache } = __webpack_require__(/*! ./validity-stamp-cache */ \"./src/protocol-v4/connection/validity-stamp-cache.js\");\nconst { getGlobalIdentityCache } = __webpack_require__(/*! ./identity-cache */ \"./src/protocol-v4/connection/identity-cache.js\");\nconst { makeEBOIterator } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\n\nconst { ConnectionMessage } = __webpack_require__(/*! ./message */ \"./src/protocol-v4/connection/message.js\");\nconst { ConnectionRequest } = __webpack_require__(/*! ./request */ \"./src/protocol-v4/connection/request.js\");\nconst { ConnectionResponse } = __webpack_require__(/*! ./response */ \"./src/protocol-v4/connection/response.js\");\nconst { ConnectionBatch } = __webpack_require__(/*! ./batch */ \"./src/protocol-v4/connection/batch.js\");\nconst { ConnectionAck } = __webpack_require__(/*! ./ack */ \"./src/protocol-v4/connection/ack.js\");\nconst { ErrorPayloadCtorFactory } = __webpack_require__(/*! ./error-payload */ \"./src/protocol-v4/connection/error-payload.js\");\nconst isDebugBuild = __webpack_require__(/*! dcp/common/dcp-build */ \"./src/common/dcp-build.js\").build === 'debug';\n\nlet globalConnectionId = 0;\n\nconst CONNECTION_STATES = [\n 'initial',\n 'connecting',\n 'established',\n 'waiting', /* Targets only, when send fails */\n 'close-wait', /* Target of close message is in this state until response is acknowledged */\n 'closing',\n 'closed',\n]\n\nclass Connection extends EventEmitter {\n static get VERSION() {\n return '5.0.0'; // Semver format\n }\n\n static get VERSION_COMPATIBILITY() {\n return '5.0.0'; // Semver format, can be a range\n }\n\n /**\n * @constructor Connection form 4:\n * Create a DCP Connection object for an initiator.\n * @param {string} target The string version (ie href) of the URL of the target to connect to.\n * @param {wallet.IdKeystore} [idKeystore]\n * @param {Object} [connectionOptions]\n * @see form 1\n */\n /**\n * @constructor Connection form 3:\n * Create a DCP Connection object for an initiator.\n * @param {DcpURL|URL} target The URL of the target to connect to.\n * @param {wallet.IdKeystore} [idKeystore]\n * @param {Object} [connectionOptions]\n * @see form 1\n */\n /**\n * @constructor Connection form 2:\n * Create a DCP Connection object for a target.\n * @param {wallet.IdKeystore} [idKeystore]\n * @param {Object} [connectionOptions]\n * @see form 1\n */\n /**\n * @constructor Connection form 1\n * Create a DCP Connection object. \n * \n * @note Connection objects exist for the lifetime of a given DCP connection \n * (session), whether or not the underlying transport (eg internet protocol) is connected or not. Once \n * the DCP session has ended, this object has no purpose and is not reusable.\n * \n * @param {object|undefined} target Undefined when we are the target, or an object describing the target. This object \n * may contain the following properties; 'location' is mandatory:\n * - location: a DcpURL that is valid from the Internet\n * - friendLocation: a DcpURL that is valid from an intranet; if\n * both location and friendLocation specified, the best\n * one will be chosen by examining IP addresses\n * - identity: a object with an address property which is an\n * instanceof wallet.Address which corresponds to the peer's\n * identity; this overrides the identity cache unless\n * connectionOptions.strict is truey.\n * @param {wallet.IdKeystore} [idKeystore] The keystore used to sign messages; used for non-repudiation.\n * If not specified, a dynamically-generated keystore will be used.\n * \n * @param {Object} [connectionOptions] Extra connection options that aren't defined via dcpConfig.dcp.connectionOptions.\n * These options include:\n * - identityUnlockTimeout: Number of (floating-point) seconds to leave the identity \n * keystore unlocked between invocations of Connection.send\n */\n constructor(target, idKeystore, connectionOptions)\n {\n /* polymorphism strategy: rewrite all to form 1 before super */\n if (target instanceof wallet.Keystore) /* form 2 */\n { \n connectionOptions = arguments[2];\n idKeystore = arguments[1];\n target = undefined;\n }\n if (typeof connectionOptions === 'undefined')\n connectionOptions = {};\n\n if (target instanceof URL) /* form 3.2 */\n target = { location: new DcpURL(target) };\n else if (DcpURL.isURL(target)) /* form 3.1 */\n target = { location: new DcpURL(target) };\n else if (target instanceof String || typeof target === 'string') /* form 4 */\n target = { location: new DcpURL(target) };\n\n assert((typeof target === 'undefined') || (typeof target === 'object' && DcpURL.isURL(target.location)));\n assert(typeof connectionOptions === 'object');\n \n super(`Protocol Connection (${target ? 'initiator' : 'target'})`);\n\n if (target) {\n this.isInitiator = true;\n this.debugLabel = 'connection(i):';\n this._target = target;\n } else {\n this.isInitiator = false;\n this.debugLabel = 'connection(t):';\n }\n\n if (idKeystore) {\n this.identityPromise = (async () => await idKeystore)();\n } else {\n this.identityPromise = wallet.getId();\n }\n\n this.identityPromise.then((keystore) => {\n this.identity = keystore;\n debugging('connection') && console.debug(this.debugLabel, 'identity is', keystore.address);\n return keystore;\n });\n\n this.ttl = {\n ntp: false,\n }\n \n // Init internal state / vars\n this.state = new Synchronizer(CONNECTION_STATES[0], CONNECTION_STATES);\n this.state.on('change', (s) => this.emit('readyStateChange', s) );\n\n this._id = globalConnectionId++;\n this.debugLabel = this.debugLabel.replace(')', `#${this._id})`);\n debugging('connection') && console.debug(this.debugLabel, 'connection id is', this._id, `target is ${target && target.location}`);\n this.dcpsid = null;\n this.peerAddress = null;\n this.transport = null;\n this.maxConnectionTimeout = null;\n this.messageFactory = new MessageFactory(this);\n this.messageLedger = new MessageLedger(this);\n this.authorizedSender = null;\n \n this.Message = ConnectionMessage(this);\n this.Request = ConnectionRequest(this.Message);\n this.Response = ConnectionResponse(this.Message);\n this.Batch = ConnectionBatch(this.Message);\n this.Ack = ConnectionAck(this.Message);\n this.ErrorPayload = ErrorPayloadCtorFactory(this);\n this.connectTime = Date.now();\n\n this.openRequests = {};\n ValidityStampCache.init();\n\n this.receiver = new Receiver(this, this.messageLedger);\n this.receiver.on('request', (req) => this.emit('request', req));\n\n debugging('connection') && console.debug(this.debugLabel, `new; ${target && target.location || '<target>'}`);\n\n this.configureConnectionForUrl = (url) => {\n if (typeof connectionOptions.ttl === 'number')\n connectionOptions = Object.assign({}, connectionOptions, { ttl: { default: connectionOptions.ttl } });\n \n this.url = url;\n this.connectionOptions = leafMerge(/* ordered from least to most specific */\n dcpConfig.dcp.connectionOptions.default,\n this.url && dcpConfig.dcp.connectionOptions[this.url.hostname],\n this.url && dcpConfig.dcp.connectionOptions[this.url.origin],\n dcpConfig.dcp.connectionOptions[this.isInitiator ? this.url.href : 'target'],\n connectionOptions\n );\n\n // ensure we're using a _copy_ of the transports list, not mutating the original\n this.connectionOptions.transports = [...this.connectionOptions.transports];\n this.originalConnectionTransports = [].concat(this.connectionOptions.transports);\n this.transportsTried = 0;\n\n this.ttl = leafMerge(this.ttl, this.connectionOptions.ttl);\n this.unlockTimeout = this.connectionOptions.identityUnlockTimeout;\n this.connectionOptions.id = this._id;\n this.EBOIterator = makeEBOIterator(1000, 60000);\n\n assert(this.unlockTimeout >= 0);\n assert(typeof this.connectionOptions.ttl.min === 'number');\n assert(typeof this.connectionOptions.ttl.max === 'number');\n assert(typeof this.connectionOptions.ttl.default === 'number');\n\n this.secureLocation = determineIfSecureLocation(this);\n }\n }\n\n /**\n * This method is an instantiator/factory function for building a connection\n * that will act as the target in a new protocol connection. It's a little\n * like making a new connection and calling `connect` on it, except that\n * instead of having a url to connect to we have a transport which should\n * be ready to emit the connect message from the initiator.\n * \n * @param {wallet.Keystore} ks - Keystore to associate to the new connection.\n */\n static async newTarget(url, ks, transport) {\n const target = new Connection(undefined, ks, {\n ttl: {\n ntp: true, // targets should always be NTP-synced\n }\n });\n\n target.configureConnectionForUrl(url);\n assert(target.connectionOptions.ttl.ntp);\n \n await target.identityPromise;\n target.transport = transport;\n target.sender = new Sender(target);\n target.state.set('initial', 'connecting');\n return target;\n }\n\n /**\n * When invoked by the initator, this method establishes the connection by connecting\n * to the target provided to the constructor.\n * \n * When a dcpConfig-fragment is used as the target object, we determine automatically if \n * we should be using the location or the friendLocation property at this point.\n *\n * This function is also where the connectionOptions are merged together. This means \n * that if either of these objects come from dcpConfig and we HUP a service running this\n * code, that the new configuration will be used to connect, instead of the configuration \n * that was defined at the time of construction.\n *\n * @throws when called by a target, or when called more than once\n */\n async connect() {\n var presharedPeerAddress;\n \n let transportName;\n \n if (!this.state.in(['initial', 'connecting'])) {\n throw new Error(`Can only invoke connect in initial/connecting state. Current state: ${this.state.valueOf()}.`, );\n }\n\n // make connect re-entrant if called during the connecting phase\n if (this.state.is('connecting'))\n {\n /* nth connect attempt on same Connection instance before actual connection -- swallow attempts and wait until connected */\n do\n {\n await this.state.until(['established', 'close-wait', 'closing', 'closed']);\n } while(this.state.is('waiting'));\n\n if (!this.state.is('established'))\n throw new Error(`Could not establish connection to ${this.url}; in state ${this.state}`);\n return;\n }\n\n this.state.set('initial', 'connecting');\n\n if (this._target && this._target.hasOwnProperty('friendLocation') && await isFriendlyURL(this._target.friendLocation))\n this.configureConnectionForUrl(this._target.friendLocation);\n else\n this.configureConnectionForUrl(this._target.location);\n\n const transport = this.fetchTransport();\n this.transport = transport.transport;\n transportName = transport.moduleName;\n this.sender = new Sender(this);\n // create sender before promises so that we can still enqueue messages before hopping off the event loop\n await this.identityPromise;\n await this.connectToTarget(transportName);\n const establishResults = await this.sender.establish().catch( (err) => {\n console.error('Failed to establish connection.');\n this.close(err, true);\n throw err;\n });\n const dcpsid = establishResults.dcpsid;\n const peerAddress = wallet.Address(establishResults.peerAddress);\n\n if (!this.connectionOptions.strict && this._target.identity)\n {\n if (determineIfSecureConfig())\n {\n let identity = await this._target.identity;\n if (!(identity instanceof wallet.Keystore))\n identity = { address: new wallet.Address(identity) }; /* map strings and Addresses to ks ducks */\n\n presharedPeerAddress = identity.address;\n debugging('connection') && console.debug('Using preshared peer address', presharedPeerAddress);\n }\n }\n this.ensureIdentity(peerAddress, presharedPeerAddress); /** XXXwg possible resource leak: need cleanup; need try {} catch->emit(cleanup) */\n \n // checks have passed, now we can set props\n this.peerAddress = peerAddress;\n if (this.dcpsid)\n throw new DCPError(`Reached impossible state in connection.js; dcpsid already specified ${this.dcpsid} (${this.url})`, 'DCPC-1004');\n this.dcpsid = dcpsid;\n\n // Update state\n this.state.set('connecting', 'established'); /* established => dcpsid has been set */\n this.emit('connected', this.url);\n this.sender.finishSetup();\n }\n\n moreTransportsExist() {\n return this.connectionOptions\n && this.connectionOptions.transports\n && Array.isArray(this.connectionOptions.transports)\n && this.connectionOptions.transports.length > 0;\n }\n \n /**\n * Initiators only.\n * Try to get and establish a transport from the transports list.\n * @returns {Transport} a new transport instance or false if no transports are left to try\n */\n fetchTransport() {\n //We want to cycle our possible transports until the maxConnectionTimeout runs out or we have a connection\n const moduleName = this.connectionOptions.transports.shift();\n this.connectionOptions.transports.push(moduleName);\n this.transportsTried++;\n debugging() && console.log(this.debugLabel, 'fetchTransport trying ', moduleName, `remaining transports: ${this.connectionOptions.transports.length}`)\n if (!moduleName) {\n console.error('Error: All transports have failed to connect/transmit within limits. Closing connection.');\n this.close('all transports failed', true).catch(error => {\n console.error(`Error: failed to close transport:`, error);\n });\n throw new DCPError(`Failed to connect to ${this.url}, no transport could be established`, 'ENOTRANSPORT');\n }\n const TransportClass = Transport.require(moduleName);\n const transport = new TransportClass(null, this.connectionOptions);\n transport.on('message', (m) => {\n this.onMessage(JSON.parse(m))\n });\n return { transport, moduleName };\n }\n\n /**\n * Initiators only\n * Resolves when we have connected to target using a transport.\n * Calls \"failTransport\" if we run out of time / attempts to connect.\n * Let's transport handle failure but also is watching for it too.\n */\n async connectToTarget(currentTransport) {\n debugging('connection') && console.log(this.debugLabel, `Using transport \"${this.transport.name}\" ... connecting to ${this.url}.`);\n const globalWaitMs = dcpConfig.dcp.maxConnectionTimeout;\n const connectWaitMs = this.transport.connect(this.url, this.connectionOptions);\n\n this.maxConnectionTimeout = this.maxConnectionTimeout ? this.maxConnectionTimeout\n : setTimeout(this.close.bind(this, `Timeout to have a transport connect reached at ${globalWaitMs} ms`, true), globalWaitMs);\n\n await new Promise( (resolve) => {\n // Listener for a connect-failed, will invoke EBO algorithm to retry the connection\n this.transport.once('connect-failed', async () => {\n if (this.transportsTried >= this.connectionOptions.transports.length)\n {\n this.transportsTried = 0;\n const backoffTime = this.EBOIterator.next().value;\n debugging('connection') && console.debug(this.debugLabel, 'connecting in', backoffTime / 1000, 'seconds');\n await this.failTransport('Connect-failed event handler fired.', backoffTime);\n }\n else\n {\n await this.failTransport('Connect-failed event handler fired.');\n }\n resolve();\n });\n\n this.transport.once('connected', () => {\n clearTimeout(this.maxConnectionTimeout);\n this.maxConnectionTimeout = null;\n /* When we successfully connect a transport, we know that transport has potential to consistently work,\n * and disconnects may happen for unrelated reason (such as switching wifi causing a disconnect). \n * Thus, we recreate our transport options list with this transport at the top\n */\n this.connectionOptions.transport = [currentTransport].concat(this.originalConnectionTransports);\n resolve();\n });\n });\n this.transport.on('reconnect', () => {\n debugging('connection') && console.log(this.debugLabel, `Transport reconnected to ${this.url}`);\n /**\n * This may not work at the moment.\n * We don't want to naively send a keepalive because it might go to a closing/closed connection and throw an exception.\n * DCP-2528 is the bug for removing the keepalive in the reconnect handler until we can do it right.\n * DCP-2527 is the bug for making the reconnect handler work properly.\n */\n this.emit('reconnect');\n });\n }\n\n /**\n * Target gains full status once dcpsid and peerAddress\n * are provided by first connect request.\n * @param {string} dcpsid dcpsid\n * @param {wallet.Address} peerAddress Address of peer\n */\n establishTarget(dcpsid, peerAddress) {\n this.connectResponseId = Symbol(); // un-register ConnectResponse\n this.peerAddress = peerAddress;\n if (this.dcpsid)\n throw new DCPError(`Reached impossible state in connection.js; dcpsid already specified ${this.dcpsid}!=${dcpsid} (${this.url})`, 'DCPC-1005');\n this.dcpsid = dcpsid;\n this.state.set('connecting', 'established'); /* established => dcpsid has been set */\n }\n\n ensureIdentity (peerAddress, presharedPeerAddress) {\n let identityCache = getGlobalIdentityCache();\n \n if (!identityCache.validateIdentity(this.url.href, peerAddress)) {\n if (presharedPeerAddress)\n {\n console.error(`**** Warning: preshared peer address ${presharedPeerAddress} does not match address ${identityCache.getIdentity(this.url.href)} provided for ${this.url} in identity cache ****`);\n __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\").sleep(10);\n identityCache.insert(this.url.href, peerAddress);\n }\n throw new DCPError(`Identity address ${peerAddress} does not match the saved key for ${this.url}`,'EADDRCHANGE');\n }\n }\n\n /**\n * Check that the transport has given us a message worth dealing with then\n * either let the receiver handle it (message) or the message ledger (ack).\n * @param {object} message parsed (but not validated) message object\n */\n async onMessage (message) {\n var validation;\n \n if (this.state.is('closed')) {\n debugging('connection') && console.warn(this.debugLabel, 'onMessage was called on a closed connection.');\n return;\n }\n /**\n * We always ack a duplicate transmission.\n * This must happen before validation since during startup we may lack a\n * nonce or dcpsid (depending on whether initiator or target + race).\n */\n if (this.isDuplicateTransmission(message)) {\n debugging('connection') && console.debug(this.debugLabel, 'duplicate message:', message.body);\n this.transport.send(this.lastAckSigned);\n return;\n }\n\n /* Capture the initial identity of the remote end during the connect operation */\n if (this.authorizedSender === null)\n {\n let messageBody = message.body;\n let payload = messageBody.payload;\n \n if (payload && message.body.type === 'batch')\n {\n for (let i=0; i < payload.length; i++)\n {\n let innerMessageBody = payload[i];\n\n if (innerMessageBody.payload && innerMessageBody.payload.operation === 'connect' && (innerMessageBody.type === 'response' || innerMessageBody.type === 'request'))\n {\n messageBody = innerMessageBody;\n payload = innerMessageBody.payload;\n break;\n }\n }\n }\n\n if (payload)\n {\n if (payload.operation === 'connect' && (messageBody.type === 'response' || messageBody.type === 'request'))\n this.authorizedSender = message.owner;\n else\n throw new Error('Message payload received before connection operation');\n }\n }\n else\n {\n if (message.owner !== this.authorizedSender)\n {\n this.close(new DCPError('Message came from invalid sender.', 'DCPC-1002'), true);\n return;\n }\n }\n\n if (!this.initiator && this.state.in(['connecting']))\n {\n // while connecting, the target gets its nonce from the initiator\n if (!this.isInitiator) this.sender.nonce = message.body.nonce;\n }\n\n validation = this.validateSignature(message);\n if (validation.success !== true)\n {\n debugging('connection') && console.debug('Message failed validation -', validation.errorMessage);\n this.close(`invalid message: ${validation.errorMessage}`, true);\n }\n\n if (message.body.type === 'unhandled-message')\n {\n /* This special message type may not have a dcpsid, peerAddress, etc., so it might not\n * validate. It is also not a \"real\" message and only used to report ConnectionManager routing \n * errors, so we just report here, drop it, and close the connection.\n *\n * Note also that this is probably the wrong way to handle this case - restarting daemons - but\n * that is a problem for another day. /wg nov 2021\n */\n debugging('connection') && console.warn(\"Target Error - target could not process message.\", JSON.stringify(message.body),\n \"Aborting connection.\");\n this.close(`target could not process message (${message.payload && message.payload.name || 'unknown error'})`);\n }\n\n validation = this.validateMessage(message);\n if (validation.success !== true)\n {\n debugging('connection') && console.debug('Message failed validation -', validation.errorMessage);\n this.close(`invalid message: ${validation.errorMessage}`, true);\n }\n\n if (message.body.type === \"ack\") {\n const ack = new this.Ack(message.body);\n this.messageLedger.handleAck(ack);\n return;\n } else if (message.body.type !== 'unhandled-message') {\n this.lastMessage = message;\n await this.ackMessage(message);\n }\n this.receiver.onMessage(message);\n }\n\n async ackMessage(message) {\n debugging('connection') && console.debug(this.debugLabel, 'acking message.');\n const ack = new this.Ack(message);\n const signedMessage = await ack.sign(this.identity);\n this.transport.send(signedMessage);\n this.lastAck = ack;\n this.lastAckSigned = signedMessage;\n }\n\n /**\n * Checks if the batch we just received has the same nonce\n * as the most-recently received batch.\n * @param {object} messageJSON\n */\n isDuplicateTransmission(messageJSON) {\n return this.lastMessage && this.lastMessage.body.nonce === messageJSON.body.nonce;\n }\n\n /**\n * Validate that the signature was generated from this message body\n * @param {Object} message\n * @returns {Object} with properties 'success' and 'errorMessage'. When the message is valid on its \n * face, the success property is true, otherwise it is is false. When it is false,\n * the errorMessage property will be a string explaining why.\n */\n validateSignature(message)\n {\n const owner = new wallet.Address(message.owner);\n const signatureValid = owner.verifySignature(message.body, message.signature);\n\n if (!signatureValid)\n {\n debugging('connection') && console.warn(\"Message has an invalid signature, aborting connection\");\n return { success: false, errorMessage: \"invalid message signature\" };\n }\n\n return { success: true };\n }\n \n /**\n * This method is used to perform validation on all types of messages.\n * It validates the DCPSID, nonce, and the peerAddress.\n * @param {Object} message\n * @returns {Object} with properties 'success' and 'errorMessage'. When the message is valid on its \n * face, the success property is true, otherwise it is is false. When it is false,\n * the errorMessage property will be a string explaining why.\n *\n */\n validateMessage(message)\n {\n try\n {\n if (this.peerAddress && !this.peerAddress.eq(message.owner))\n {\n debugging('connection') && console.warn(\"Received message's signature address does not match peer address, aborting connection\\n\",\n \"(signature addr)\", message.owner, '\\n',\n \"(peer addr)\", this.peerAddress);\n return { success: false, errorMessage: \"received message signature does not match peer address\" };\n }\n\n if (this.state.in(['established', 'closing', 'close-wait']) && message.body.type !== 'unhandled-message')\n {\n const body = message.body;\n\n assert(this.peerAddress); /* should be set in connect */\n /**\n * Security note:\n * We don't require the dcpsid to match on an ack because the connect response\n * ack doesn't have a dcpsid until after it is processed. Also ack's are protected\n * by ack tokens and signatures, so this doesn't leave a hole, just an inconsistency.\n */\n if (body.type !== 'ack' && body.dcpsid !== this.dcpsid)\n {\n debugging('connection') && console.warn(\"Received message's DCPSID does not match, aborting connection\\n\",\n \"Message owner:\", message.owner, '\\n',\n \"(ours)\", this.dcpsid, (Date.now() - this.connectTime)/1000, \"seconds after connecting - state:\", this.state._, \"\\n\", \n \"(theirs)\", body.dcpsid);\n if(body.dcpsid.substring(0, body.dcpsid.length/2) !== this.dcpsid.substring(0, this.dcpsid.length/2)){\n debugging('connection') && console.warn(\" Left half of both DCPSID is different\");\n }\n if(body.dcpsid.substring(body.dcpsid.length/2 + 1, body.dcpsid.length) !== this.dcpsid.substring(this.dcpsid.length/2 + 1, body.dcpsid.length)){\n debugging('connection') && console.warn(\" Right half of both DCPSID is different\");\n }\n return { success: false, errorMessage: \"DCPSID do not match\" };\n }\n\n if (body.type !== 'ack' && this.lastAck.nonce !== body.nonce)\n {\n debugging('connection') && console.warn(\"Received message's nonce does not match expected nonce, aborting connection\\n\");\n debugging('connection') && console.debug(this.debugLabel, this.lastAck.nonce, body.nonce);\n return { success: false, errorMessage: \"received message's nonce does not match expected nonce\" };\n }\n }\n\n return { success: true };\n }\n catch(error)\n {\n console.error('message validator failure:', error);\n return { success: false, errorMessage: 'validator exception ' + error.message };\n }\n\n return { success: false, errorMessage: 'impossible code reached' };\n }\n\n /**\n * Targets Only.\n * The receiver creates a special connect response and the connection\n * needs to know about it to get ready for the ack. See `isWaitingForAck`.\n * @param {Message} message message we are sending out and waiting to\n * ack'd, probably a batch containing the response.\n */\n registerConnectResponse(message) {\n this.connectResponseId = message.id;\n }\n\n /**\n * Targets only\n * During the connection process a target sends a connect\n * response to an initiator and the initiator will ack it. Since transports\n * are not tightly coupled, we have no authoritative way to route the ack back\n * to the right connection. So a connection briefly registers the ack it\n * is looking for in this case. It will formally validate the ack after routing.\n * @param {string} messageId id of the message this ack is acknowledging.\n */\n isWaitingForAck(messageId) {\n return messageId === this.connectResponseId;\n }\n\n /**\n * Targets: nothing to do, we are waiting for initiator.\n * Initiators: give up on current transport and try next available option.\n * For now we just have socketio so that means we throw an error.\n * @return {Promise<boolean>} indicating whether we were able to find a new transport.\n */\n async failTransport (failureDescription, retryWaitTime = 0) {\n let transportName;\n const preWaitingState = this.state.valueOf(); /* XXXwg bogus - need explicit and reasonable source states */\n\n /* If we are already in process of closing or already waiting, we don't want to beat \n * the dead horse. Let it stay dead with no transport\n */\n if (['closing', 'closed', 'close-wait', 'waiting'].includes(preWaitingState))\n return;\n\n this.state.testAndSet(preWaitingState, 'waiting');\n debugging('connection') && console.log(this.debugLabel, `Transport \"${this.transport.name}\" has failed.`);\n \n if (this.isInitiator) {\n if (!this.moreTransportsExist()) {\n console.error('Transport failed:', failureDescription);\n // fetchTransport will immediately fail\n }\n debugging('connection') && console.log(this.debugLabel, `Attempting to use next transport.`);\n this.transport.close();\n const transport = this.fetchTransport();\n this.transport = transport.transport;\n transportName = transport.moduleName;\n \n await new Promise(r => setTimeout(r, retryWaitTime));\n \n if (['closing', 'closed', 'close-wait'].includes(this.state.valueOf())) // If some other process closes the connection, don't try to reconnect\n return;\n\n this.state.set('waiting', preWaitingState);\n await this.connectToTarget(transportName);\n }\n }\n\n /**\n * Targets only\n * Give target another transport to try sending messages on again.\n */\n revive(transport) {\n this.transport = transport;\n this.state.set('waiting', 'established');\n }\n\n /**\n * Put connection into close-wait state so that a call to `close`\n * in this state will *not* trigger sending a `close` message to the peer.\n * Then call close once the passed promise resolves.\n *\n * @note: This function is called when the remote end of the transport sends\n * a close command, and the waitPromise in that case is the response.\n */\n async closeWait (waitPromise)\n {\n const preCloseState = this.state.valueOf();\n\n debugging('connection') && console.debug(this.debugLabel, `responding to close. state=closeWait dcpsid=${this.dcpsid}`);\n\n if (this.state.is('closed'))\n {\n debugging('connection') && console.debug(this.debugLabel, `remote asked us to close a closed connection; dcpsid=${this.dcpsid}`);\n return;\n }\n\n // wait for waitPromise to resolve, reject if not within time limit\n try {\n await new Promise((resolve, reject) => {\n const closeWaitTimeout = setTimeout(() => {\n reject(new Error('Connection.closeWait timed out'));\n }, 10000); /* hardcode 10s wait /wg mar 2021 */\n waitPromise.then(resolve, reject).finally(() => clearTimeout(closeWaitTimeout));\n })\n debugging('connection') && console.debug(this.debugLabel, `close response ACK'd; closing. dcpsid=${this.dcpsid}`);\n } catch (error) {\n debugging('connection') && console.debug(this.debugLabel, `${error.message}; closing. dcpsid=${this.dcpsid}`);\n }\n // continue with close in either case\n let reason = 'received close from peer';\n if (this.url)\n reason += ` (${this.url})`;\n\n // If we're already closing, wait for it to complete then resolve\n // WARNING: any place we transition to closing or close-wait, we MUST guarantedd\n // that 'end' will be emitted, or this code will hang forever!\n if (this.state.in(['close-wait', 'closing'])) {\n return new Promise((resolve) => {\n this.once('end', resolve) /* eventually fired by doClose elsewhere */\n });\n }\n\n if (this.state.is('closed')) /* closed somehow on us during await */\n return;\n\n this.state.set(preCloseState, 'close-wait');\n return this.doClose(preCloseState, reason, true);\n }\n\n /**\n * This method will begin gracefully closing the protocol connection.\n * It will only close after sending all pending messages.\n * \n * @param {string|Error} [reason] Either an Error or a message to use in the Error that will reject pending sends.\n * @param {boolean} [immediate] If true, does not wait to send messages or the `close` request.\n *\n * @return a Promise which resolves when the connection has been confirmed closed and the end event has been fired.\n */\n close (reason=null, immediate=false)\n {\n if (this.state.is('closed')) return Promise.resolve();\n\n const preCloseState = this.state.valueOf();\n debugging('connection') && \n console.debug(this.debugLabel, \n `close; dcpsid=${this.dcpsid} state=${preCloseState} reason=${reason} immediate=${immediate} stack=${new Error().stack}`);\n\n // If we're already closing, wait for it to complete then resolve\n if (this.state.in(['close-wait', 'closing'])) {\n return new Promise((resolve) => {\n this.once('end', resolve)\n });\n }\n\n // Put in closing state no matter the current state\n this.state.set(preCloseState, 'closing');\n\n // Perform actual work of closing\n return this.doClose(preCloseState, reason, immediate);\n }\n\n /**\n * sends close message to peer and waits for response\n * @note: This function is not reentrant!\n */\n async closeGracefully() {\n await new Promise((resolve, reject) => {\n // If we are closing the connection gracefully, we don't care about failed messages. Close it silently\n this.messageLedger.silentFailMessage = true;\n // reject if we don't resolve within time limit\n const timeoutTimer = setTimeout(() => {\n reject(new DCPError('Connection.close timed out', 'DCPC-1003'));\n }, this.connectionOptions.closeTimeout * 1000);\n \n if (this.transport)\n {\n /* If we got as far as initializing a transport during connect(), send close\n * message to peer, should get a response before time is up.\n */\n const closeMessage = this.messageFactory.buildMessage('close');\n this.sender.enqueue(closeMessage).then(resolve, reject).finally(() => clearTimeout(timeoutTimer));\n }\n })\n // hop off event loop so that close response ack can get out\n await new Promise(r=>setTimeout(r));\n }\n\n /**\n * Core close functionality shared by `close` and `closeWait`\n *\n * @param {string} preCloseState the state that the connection was in at the start of the\n * invocation of close() or closeWait()\n *\n * @note: this function is not reentrant due to closeGracefully\n */\n async doClose(preCloseState, reason, immediate) {\n const dcpsid = this.dcpsid;\n\n try\n {\n assert(this.state.valueOf() === 'closing' || this.state.valueOf() === 'close-wait');\n \n // Emit the close event the moment we know we are going to close, \n // so we can catch the close event and reopen the connection\n this.emit('close', dcpsid /* should be undefined in initial state */);\n \n if (preCloseState === 'established' && !immediate) {\n try {\n await this.closeGracefully();\n } catch(e) {\n debugging() && console.warn(`Warning: could not close connection gracefully. connectionid=${this._id}, dcpsid=,${this.dcpsid}, url=${this.url ? this.url.href : 'unknown url'} - (${e.message})`);\n }\n }\n\n // can delete these now that we've sent the close message\n this.dcpsid = null;\n this.peerAddress = null;\n\n /* build error message */\n let rejectErr;\n if (reason instanceof Error) {\n rejectErr = reason;\n } else {\n let message;\n if (typeof reason === 'string' || reason instanceof String ) {\n message = reason;\n } else {\n if (this.url)\n message = `Connection closed (url: ${this.url}, dcpsid: ${dcpsid})`;\n else\n message = `Connection closed (dcpsid: ${dcpsid})`;\n }\n rejectErr = new DCPError(message, 'DCPC-1002');\n }\n \n if (preCloseState !== 'initial')\n {\n // Reject any pending transmissions in the message ledger\n this.messageLedger.failAllTransmissions(rejectErr);\n \n if (this.transport)\n {\n try { this.sender.shutdown(); }\n catch(e) { debugging() && console.warn(`Warning: could not shutdown sender; dcpsid=,${dcpsid}`, e); }\n\n try { this.transport.close(); }\n catch(e) { debugging() && console.warn(`Warning: could not close transport; dcpsid=,${dcpsid}`, e); }\n }\n }\n } catch(e) {\n debugging() && console.warn(`Warning: could not close connection. connectionid=${this._id}, dcpsid=,${this.dcpsid}, url=${this.url ? this.url.href : 'unknown url'} - (${e.message})`)\n }\n\n this.state.setIf('closing', 'closed');\n this.state.setIf('close-wait', 'closed');\n\n if (this.transport)\n {\n this.emit('end'); /* end event resolves promises on other threads for closeWait and close */\n }\n }\n\n /**\n * Sends a message to the connected peer. If the connection has not yet been established,\n * this routine will first invoke this.connect().\n * \n * @param {...any} args\n * @returns {Promise<Response>} a promise which resolves to a response.\n */\n async send(...args) {\n if (this.state.in(['initial','connecting'])) {\n // Original message will be rejected so we don't want a second reject\n // here since it would be uncaught.\n await this.connect().catch( e => {\n throw new DCPError(`Connection (${this._id}) failed to connect to ${this.url ? this.url.href : '<unknown url>'}: ${e.message}`, e.code);\n });\n }\n\n const message = this.messageFactory.buildMessage(...args);\n\n if (this.state.in(['closing', 'closed'])) {\n let additionalInfo = \"\";\n const debugBuild = (__webpack_require__(/*! dcp/common/dcp-build */ \"./src/common/dcp-build.js\").build === 'debug');\n if (debugBuild) {\n const currentStack = new Error().stack;\n additionalInfo = `, message: ${stringify(message)}, this: ${stringify(this)}, state.lastStack ${this.state.lastStack}\\n, currentStack ${currentStack}`;\n }\n let e = new DCPError(`Connection (${this._id}) in state '${this.state}' cannot send. (url: ${this.url}${additionalInfo})`, 'DCPC-1001');\n throw e;\n }\n\n return this.sender.enqueue(message);\n }\n\n /**\n * Sends a v3-over-v4 request\n * @param url {string} v3 DcpURL\n * @param message {object} v3 \"message\" value; may be empty\n * @param authKey {object} (v3 signing|v4 authorizing) keystore, if different from identity\n * @return {Promise<object>} Resolves with v3 route's response data\n */\n async sendv3(url, message = {}, authKey = null) {\n const v3Data = {\n url,\n message,\n };\n \n const req = new this.Request('v3', v3Data);\n\n if (authKey && !this.identity.address.eq(authKey.address)) {\n await req.authorize(authKey);\n }\n \n let response = await req.send()\n\n .catch((error) => {\n return {\n success: false,\n payload: error,\n }\n });\n\n // fail at v4-protocol-level? throw it\n if (!response.success) {\n throw response.payload;\n }\n \n // fail at v3-level? also throw it\n if (response.payload.v3status === 'reject')\n throw response.payload.v3rejection;\n \n return response.payload.v3resolution;\n }\n\n /**\n * This routine returns the current time for the purposes of\n * populating the Request message payload.validity.time property.\n * \n * @returns {Number} the integer number of seconds which have elapsed since the epoch\n */\n currentTime() {\n let msSinceEpoch;\n if (this.ttl.ntp) {\n msSinceEpoch = Date.now();\n } else {\n const msSinceLastReceipt = perf.now() - this.receiver.lastResponseTiming.receivedMs;\n msSinceEpoch = this.receiver.lastResponseTiming.time * 1000 + msSinceLastReceipt;\n }\n return Math.floor(msSinceEpoch / 1000);\n }\n\n /**\n * This method sends a keepalive to the peer, and resolves when the response has been received.\n */\n keepalive() {\n return this.send('keepalive');\n }\n\n /**\n * This method checks to see a service connection is established, typically will be given a timeOut condition when invoked\n */\n async setCheckConnectionTimeout(source) {\n // failTimeout is checked to account for a race condition if the connection is established at the same time as the fail timeout expires \n let failTimeout = false;\n function checkConnectionEstablished() {\n if(failTimeout){\n failTimeout = false;\n throw new DCPError(`Connection to ${this.url} from ${source} not established within 30 seconds, instead is in state ${this.state}`, 'DCP-1006');\n }\n }\n failTimeout = setTimeout(checkConnectionEstablished.bind(this), 30000);\n this.once('connected', () => {\n if(failTimeout){\n clearTimeout(failTimeout);\n failTimeout = false;\n }\n });\n await this.connect();\n }\n\n}\n\n/**\n * Returns true if friendLocation should work in place of location from this host.\n * This allows us to transparently configure inter-daemon communication that uses\n * local LAN IPs instead of bouncing off the firewall for NAT.\n */\nasync function isFriendlyURL(url)\n{\n var remoteIp, dnsA;\n var ifaces;\n \n if (url.hostname === 'localhost')\n return true;\n\n switch(url.protocol)\n {\n case 'http:':\n case 'https:':\n case 'ws:':\n case 'tcp:':\n case 'udp:':\n case 'dcpsaw:':\n break;\n default:\n return false;\n }\n\n /* Consider same-origin match friendly */\n if (__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").isBrowserPlatform)\n return url.origin === window.location.origin;\n\n /* Convert an IP address to a 32-bit int in network order */\n function i32(addr)\n {\n var ret = 0;\n var octets = addr.split('.');\n\n ret |= octets[0] << 24; /* Note: JS ints are signed 32, but that doesn't matter for masking */\n ret |= octets[1] << 16;\n ret |= octets[2] << 8;\n ret |= octets[3] << 0;\n\n return ret;\n }\n \n /* Consider machines in same IPv4 subnet friendly */\n dnsA = await requireNative('dns').promises.lookup(url.hostname, {family: 4});\n if (!dnsA)\n return false;\n remoteIp = i32(dnsA.address);\n ifaces = requireNative('os').networkInterfaces();\n for (let ifaceName of Object.keys(ifaces))\n {\n for (let alias of ifaces[ifaceName])\n {\n if (alias.family != 'IPv4')\n continue;\n\n let i32_addr = i32(alias.address);\n let i32_mask = i32(alias.netmask);\n\n if ((i32_addr & i32_mask) === (remoteIp & i32_mask))\n return true;\n }\n }\n\n return false;\n}\n\n/** \n * Determine if we got the scheduler config from a secure source, eg https or local disk.\n * We assume that all https transactions have PKI-CA verified.\n *\n * @note protocol::getSchedulerConfigLocation() is populated via node-libs/config.js or dcp-client/index.js\n *\n * @returns true or falsey\n */\nfunction determineIfSecureConfig()\n{\n var schedulerConfigLocation = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\").getSchedulerConfigLocation();\n var schedulerConfigSecure;\n\n if (schedulerConfigLocation && (schedulerConfigLocation.protocol === 'https:' || schedulerConfigLocation.protocol === 'file:'))\n {\n debugging('strict-mode') && console.log(`scheduler config location ${schedulerConfigLocation} is secure`); /* from casual eavesdropping */\n schedulerConfigSecure = true;\n }\n\n if (isDebugBuild)\n {\n debugging('strict-mode') && console.log(`scheduler config location is always secure for debug builds`);\n schedulerConfigSecure = 'debug';\n }\n\n debugging('strict-mode') && console.debug(`Config Location ${schedulerConfigLocation} is ${!schedulerConfigSecure ? 'not secure' : 'secure-' + schedulerConfigSecure}`);\n return schedulerConfigSecure;\n}\n\n/**\n * Determine if a URL is secure by examinining the protocol, connection, and information about the \n * process; in particular, we try to determine if the dcp config was securely provided, because if \n * it wasn't, then we can't have a secure location, since the origin could be compromised.\n * \n * \"Secure\" in this case means \"secure against casual eavesdropping\", and this information should only\n * be used to refuse to send secrets over the transport or similar.\n *\n * @returns true or falsey\n */\nfunction determineIfSecureLocation(conn)\n{\n var isSecureConfig = determineIfSecureConfig();\n var secureLocation;\n \n if (!isSecureConfig) /* can't have a secure location without a secure configuration */\n return null;\n \n if (isDebugBuild || conn.url.protocol === 'https:' || conn.url.protocol === 'tcps:')\n secureLocation = true;\n else if (conn.isInitiator && conn._target.hasOwnProperty('friendLocation') && conn.url === conn._target.friendLocation)\n secureLocation = true;\n else if (conn.connectionOptions.allowUnencryptedSecrets)\n secureLocation = 'override';\n else\n secureLocation = false;\n\n debugging('strict-mode') && console.debug(`Location ${conn.url} is ${!secureLocation ? 'not secure' : 'secure-' + secureLocation}`);\n \n return secureLocation;\n}\n\nexports.Connection = Connection;\n\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/connection.js?");
5508
5508
 
5509
5509
  /***/ }),
5510
5510
 
@@ -5527,7 +5527,8 @@ eval("/** @file error-payload.js\n * @author Eddie Roosenmaallen <edd
5527
5527
  /*! no static exports found */
5528
5528
  /***/ (function(module, exports, __webpack_require__) {
5529
5529
 
5530
- eval("/* WEBPACK VAR INJECTION */(function(Buffer) {/**\n * @file protocol/connection/identity-cache.js\n * @author Nazila Akhavan, Nazila@kingsds.network\n * @date January 2020\n *\n * The identity cache is used to ensure that for any connection hostname and identity are match,\n * then insert them into the cache (browser/ node). It is also used to clear the cache. \n * \n * @todo: Consider replacing the node API calls with dcp-localstorage // RR March 2020\n */\n\nconst IDENTITY_CACHE_LOCAL_STORAGE_KEY = 'idCache';\nconst env = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst { fs, requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { Address } = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\n \nclass IdentityCache {\n constructor() {\n this.identities = { /* hostname: address */};\n\n if (env.isBrowserPlatform) {\n try {\n this.identities = JSON.parse(window.localStorage.getItem(IDENTITY_CACHE_LOCAL_STORAGE_KEY)) || {};\n this.initIdentities();\n } catch (e) {\n console.warn(\"Failed to parse identity cache from local storage\", e);\n }\n } else if (fs.existsSync(this.cacheFilePath)) {\n let fd = fs.openSync(this.cacheFilePath)\n let stat = fs.fstatSync(fd);\n let buffer = Buffer.alloc(stat.size);\n fs.readSync(fd, buffer, 0, buffer.length, 0);\n fs.closeSync(fd);\n \n const cacheFile = buffer.toString('ascii');\n if (buffer.length) {\n this.identities = JSON.parse(cacheFile);\n this.initIdentities();\n }\n }\n }\n\n get cacheFolder() {\n return __webpack_require__(/*! dcp/common/dcp-dot-dir */ \"./src/common/dcp-dot-dir.js\").dotDcpDir;\n }\n\n get cacheFilePath() {\n return this.cacheFolder + '/identity-cache.json';\n }\n\n /**\n * Initialize Address instances for the identities after they are\n * retrieved from the cache.\n */\n initIdentities() {\n for (let hostname in this.identities) {\n this.identities[hostname] = new Address(this.identities[hostname]);\n }\n }\n \n validateIdentity (hostname, id) {\n if (typeof hostname !== 'string')\n hostname = String(hostname);\n \n if (hostname in this.identities) {\n if (!this.identities[hostname].eq(id)) {\n return false;\n }\n } else {\n this.insert(hostname, String(id));\n }\n\n return true;\n }\n\n getIdentity(hostname) {\n if (typeof hostname !== 'string')\n hostname = String(hostname);\n \n return this.identities[hostname];\n }\n\n /** Save identities to the local storage or identity-cache.json */\n saveIdentities() {\n if (env.isBrowserPlatform) {\n window.localStorage.setItem('idCache', JSON.stringify(this.identities, null, 2));\n\n } else if (env.platform === 'nodejs') {\n try {\n if (!fs.existsSync(this.cacheFolder)){\n fs.mkdirSync(this.cacheFolder, { recursive: true });\n };\n\n fs.writeFileSync(this.cacheFilePath, JSON.stringify(this.identities));\n\n fs.chmod(this.cacheFilePath, 0o700, (err) => {\n if (err) throw err;\n });\n } catch (e) {\n // There are reasons mkdir might fail - such as not enough file handles. They shouldn't\n // be considered exceptional, and shouldn't cause an uncaught exception.\n console.error(e.message);\n }\n }\n }\n \n /**\n * Add a identity to the cache\n * @param {string} id \n */\n insert(hostname, id) {\n this.identities[hostname] = new Address(id);\n this.saveIdentities();\n }\n\n clearAll () {\n this.identities = { };\n this.saveIdentities();\n }\n\n clearIdentity (id) {\n for (let hostname in this.identities) {\n if (this.identities[hostname].eq(id)) {\n delete this.identities[hostname];\n }\n }\n this.saveIdentities();\n }\n}\n\nlet singleton = null;\n\n/** @returns {IdentityCache} */\nexports.getGlobalIdentityCache = function() {\n if (!singleton) {\n singleton = new IdentityCache();\n }\n\n return singleton;\n}\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../../node_modules/node-libs-browser/node_modules/buffer/index.js */ \"./node_modules/node-libs-browser/node_modules/buffer/index.js\").Buffer))\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/identity-cache.js?");
5530
+ "use strict";
5531
+ eval("/* WEBPACK VAR INJECTION */(function(Buffer) {/**\n * @file protocol/connection/identity-cache.js\n * @author Nazila Akhavan, Nazila@kingsds.network\n * @date January 2020\n *\n * The identity cache is used to ensure that for any connection hostname and identity are match,\n * then insert them into the cache (browser/ node). It is also used to clear the cache. \n * \n * @todo: Consider replacing the node API calls with dcp-localstorage // RR March 2020\n */\n\n\nconst IDENTITY_CACHE_LOCAL_STORAGE_KEY = 'idCache';\nconst env = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst { fs, requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { Address } = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\n \nclass IdentityCache {\n constructor() {\n this.identities = { /* hostname: address */};\n\n if (env.isBrowserPlatform) {\n try {\n this.identities = JSON.parse(window.localStorage.getItem(IDENTITY_CACHE_LOCAL_STORAGE_KEY)) || {};\n this.initIdentities();\n } catch (e) {\n console.warn(\"Failed to parse identity cache from local storage\", e);\n }\n } else if (fs.existsSync(this.cacheFilePath)) {\n let fd = fs.openSync(this.cacheFilePath)\n let stat = fs.fstatSync(fd);\n let buffer = Buffer.alloc(stat.size);\n fs.readSync(fd, buffer, 0, buffer.length, 0);\n fs.closeSync(fd);\n \n const cacheFile = buffer.toString('ascii');\n if (buffer.length) {\n this.identities = JSON.parse(cacheFile);\n this.initIdentities();\n }\n }\n }\n\n get cacheFolder() {\n return __webpack_require__(/*! dcp/common/dcp-dot-dir */ \"./src/common/dcp-dot-dir.js\").dotDcpDir;\n }\n\n get cacheFilePath() {\n return this.cacheFolder + '/identity-cache.json';\n }\n\n /**\n * Initialize Address instances for the identities after they are\n * retrieved from the cache.\n */\n initIdentities() {\n for (let hostname in this.identities) {\n this.identities[hostname] = new Address(this.identities[hostname]);\n }\n }\n \n validateIdentity (hostname, id) {\n if (typeof hostname !== 'string')\n hostname = String(hostname);\n \n if (hostname in this.identities) {\n if (!this.identities[hostname].eq(id)) {\n return false;\n }\n } else {\n this.insert(hostname, String(id));\n }\n\n return true;\n }\n\n getIdentity(hostname) {\n if (typeof hostname !== 'string')\n hostname = String(hostname);\n \n return this.identities[hostname];\n }\n\n /** Save identities to the local storage or identity-cache.json */\n saveIdentities() {\n if (env.isBrowserPlatform) {\n window.localStorage.setItem('idCache', JSON.stringify(this.identities, null, 2));\n\n } else if (env.platform === 'nodejs') {\n try {\n if (!fs.existsSync(this.cacheFolder)){\n fs.mkdirSync(this.cacheFolder, { recursive: true });\n };\n\n fs.writeFileSync(this.cacheFilePath, JSON.stringify(this.identities));\n\n fs.chmod(this.cacheFilePath, 0o700, (err) => {\n if (err) throw err;\n });\n } catch (e) {\n // There are reasons mkdir might fail - such as not enough file handles. They shouldn't\n // be considered exceptional, and shouldn't cause an uncaught exception.\n console.error(e.message);\n }\n }\n }\n \n /**\n * Add a identity to the cache\n * @param {string} id \n */\n insert(hostname, id) {\n this.identities[hostname] = new Address(id);\n this.saveIdentities();\n }\n\n clearAll () {\n this.identities = { };\n this.saveIdentities();\n }\n\n clearIdentity (id) {\n for (let hostname in this.identities) {\n if (this.identities[hostname].eq(id)) {\n delete this.identities[hostname];\n }\n }\n this.saveIdentities();\n }\n}\n\nlet singleton = null;\n\n/** @returns {IdentityCache} */\nexports.getGlobalIdentityCache = function() {\n if (!singleton) {\n singleton = new IdentityCache();\n }\n\n return singleton;\n}\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../../node_modules/node-libs-browser/node_modules/buffer/index.js */ \"./node_modules/node-libs-browser/node_modules/buffer/index.js\").Buffer))\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/identity-cache.js?");
5531
5532
 
5532
5533
  /***/ }),
5533
5534
 
@@ -5538,7 +5539,8 @@ eval("/* WEBPACK VAR INJECTION */(function(Buffer) {/**\n * @file protoco
5538
5539
  /*! no static exports found */
5539
5540
  /***/ (function(module, exports, __webpack_require__) {
5540
5541
 
5541
- eval("/**\n * @file protocol/connection/index.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date May 2020\n */\n\nexports.Connection = __webpack_require__(/*! ./connection */ \"./src/protocol-v4/connection/connection.js\").Connection;\n\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/index.js?");
5542
+ "use strict";
5543
+ eval("/**\n * @file protocol/connection/index.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date May 2020\n */\n\n\nexports.Connection = __webpack_require__(/*! ./connection */ \"./src/protocol-v4/connection/connection.js\").Connection;\n\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/index.js?");
5542
5544
 
5543
5545
  /***/ }),
5544
5546
 
@@ -5549,7 +5551,8 @@ eval("/**\n * @file protocol/connection/index.js\n * @author Ryan Ro
5549
5551
  /*! no static exports found */
5550
5552
  /***/ (function(module, exports, __webpack_require__) {
5551
5553
 
5552
- eval("/**\n * @file protocol/connection/message-factory.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The MessageFactory class accepts either instances of message, or arguments\n * that it will use to construct a new Connection.Request. It also performs validation\n * on the message, assigning IDs to requests if required, before returning the well-formed message.\n */\n\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.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\nconst { Message } = __webpack_require__(/*! ../message */ \"./src/protocol-v4/message.js\");\n\nclass MessageFactory {\n constructor(connection) {\n this.connection = connection;\n this._msgId = 0;\n }\n\n generateMessageId() {\n return `${this.connection._id}-${this._msgId++}-${Date.now()}-${nanoid()}`;\n }\n\n buildMessage(...args) {\n let message;\n if (!(args[0] instanceof Message)) {\n message = new this.connection.Request(...args);\n } else {\n message = args[0];\n }\n\n if (!(message instanceof this.connection.Message)) {\n throw new Error(`Connection.send: The provided message is an instance of protocol.message but it did not originate from this connection. This connection has id ${this.connection._id}, but the connection associated with the message you used has id ${message.connection._id}.`);\n } else if (message instanceof this.connection.Request) {\n message.id = this.generateMessageId();\n } else if (message instanceof this.connection.Response) {\n if (!message.id) throw new Error(\"Connection.send: Cannot send a response that is not correlated to a request (it must have an ID)\");\n }\n\n return message;\n }\n}\n\nObject.assign(module.exports, {\n MessageFactory,\n});\n\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/message-factory.js?");
5554
+ "use strict";
5555
+ eval("/**\n * @file protocol/connection/message-factory.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The MessageFactory class accepts either instances of message, or arguments\n * that it will use to construct a new Connection.Request. It also performs validation\n * on the message, assigning IDs to requests if required, before returning the well-formed message.\n */\n\n\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.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\nconst { Message } = __webpack_require__(/*! ../message */ \"./src/protocol-v4/message.js\");\n\nclass MessageFactory {\n constructor(connection) {\n this.connection = connection;\n this._msgId = 0;\n }\n\n generateMessageId() {\n return `${this.connection._id}-${this._msgId++}-${Date.now()}-${nanoid()}`;\n }\n\n buildMessage(...args) {\n let message;\n if (!(args[0] instanceof Message)) {\n message = new this.connection.Request(...args);\n } else {\n message = args[0];\n }\n\n if (!(message instanceof this.connection.Message)) {\n throw new Error(`Connection.send: The provided message is an instance of protocol.message but it did not originate from this connection. This connection has id ${this.connection._id}, but the connection associated with the message you used has id ${message.connection._id}.`);\n } else if (message instanceof this.connection.Request) {\n message.id = this.generateMessageId();\n } else if (message instanceof this.connection.Response) {\n if (!message.id) throw new Error(\"Connection.send: Cannot send a response that is not correlated to a request (it must have an ID)\");\n }\n\n return message;\n }\n}\n\nObject.assign(module.exports, {\n MessageFactory,\n});\n\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/message-factory.js?");
5553
5556
 
5554
5557
  /***/ }),
5555
5558
 
@@ -5560,7 +5563,8 @@ eval("/**\n * @file protocol/connection/message-factory.js\n * @author
5560
5563
  /*! no static exports found */
5561
5564
  /***/ (function(module, exports, __webpack_require__) {
5562
5565
 
5563
- eval("/**\n * @file protocol/connection/message-ledger.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The MessageLedger is responsible for storing receipts for messages\n * that are given to the sender. The receipts are used to resolve the\n * promise that was returned from the sender, to indicate when the message\n * is complete.\n * \n * Request receipt promises are resolved once a response has been received.\n * Response receipt promises are resolved once the response has been sent.\n */\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp');\nconst ONE_DAY_MS = 24 * 60 * 60 * 1000;\nclass MessageLedger {\n constructor(connection) {\n this.connection = connection;\n const id = this.connection._id;\n this.debugLabel = connection.isInitiator? `message-ledger(i#${id}):` : `message-ledger(t#${id}):`;\n this.receipts = new Map(); // id => {message, resolve, reject\n this.batches = new Map(); // id -> [...messages]\n this.silentFailMessage = false;\n }\n\n /**\n * This method is invoked by the sender upon sending a new request.\n * It stores a receipt of the message and returns a promise that will\n * be resolved when the message's lifecycle has completed.\n * \n * @param {Message} message - The message to keep a receipt of its eventual transmission \n */\n addMessage(message) {\n if (this.receipts.has(message.id)) {\n throw new Error(`Duplicate transmission receipt ${JSON.stringify(message)}`);\n }\n this.ref();\n return new Promise((resolve, reject) => {\n this.receipts.set(message.id, { message, resolve, reject });\n }).catch((error) =>\n {\n if (!this.silentFailMessage)\n {\n throw error;\n }\n\n // If we \"silently fail\", we still need to resolve with the right-shaped\n // return value or things will break in hard-to-debug ways.\n return {\n success: false,\n payload: error,\n }\n });\n }\n\n /**\n * Analogous to timer/socket/server#ref. Calling ref 1 or more times means\n * the event loop will not exit until this is `unref`d.\n */\n ref() {\n if (this.refTimer) return;\n this.refTimer = setTimeout( () => {\n this.refTimer = null;\n this.ref();\n }, ONE_DAY_MS);\n }\n\n /**\n * Analogous to timer/socket/server#unref. Calling unref 1 or more times means\n * the event loop will not be prevented from exiting by this message-ledger.\n */\n unref() {\n clearTimeout(this.refTimer);\n this.refTimer = null;\n }\n\n /**\n * Add a batch to the ledger\n * @param {BatchMessage} batch A batch message which we are trying to send.\n */\n addBatch(batch) {\n this.batches.set(batch.id, batch);\n }\n\n /**\n * Remove a batch from the ledger now that its messages have been received.\n * Also mark the messages as sent.\n * @param {object} ack ack message \n */\n handleAck(ack) {\n const id = ack.messageId;\n if (this.receipts.has(id)) {\n this.handleReceiptAck(id, ack);\n } else if (this.batches.has(id)) {\n this.handleBatchAck(id, ack);\n } else {\n /* Do nothing, we don't mind spurious acks */\n debugging('message-ledger') && console.debug(this.debugLabel, 'received unhandled ack', ack.toJSON());\n }\n }\n\n /**\n * Find message in receipts and mark it as sent, also resolve/delete if its a response.\n * !Note: This is untested code since current implementation always batches before sending.\n * @param {string} id id of message that we have a receipt for\n * @param {object} ack ack message\n */\n handleReceiptAck(id, ack) {\n const receipt = this.receipts.get(id);\n if (receipt.message.body.ackToken === ack.token) {\n this.fulfillResponsePromise(receipt.message);\n /* We don't delete a receipt until the promise can be fulfilled. */\n }\n }\n\n /**\n * Iterate over messages in batch and mark each as sent.\n * @param {string} id id of batch that we have sent\n * @param {object} ack ack message\n */\n handleBatchAck(id, ack) {\n const batch = this.batches.get(id);\n if (batch.ackToken === ack.token) {\n debugging('message-ledger') && console.debug(this.debugLabel, 'received batch ack');\n batch.messages.forEach( (m) => this.fulfillResponsePromise(m) );\n this.connection.sender.clearFlightDeck(batch, ack.nonce);\n this.batches.delete(id);\n } else {\n // we just ignore spurious acks\n debugging('message-ledger') && console.debug(this.debugLabel, 'received batch ack with wrong token', batch.ackToken, ack);\n }\n }\n /**\n * Like fulfillMessagePromise but ensures only responses are fulfilled.\n * @param {Connection.Message} message The response that we are fulfilling.\n */\n fulfillResponsePromise(message) {\n if (message instanceof this.connection.Response) {\n this.fulfillMessagePromise(message.id, true)\n }\n }\n\n /**\n * This method resolves\n * \n * @param {Message.id|string} id \n * @param {*|Error} value - Error instance to reject, resolves with value otherwise\n */\n fulfillMessagePromise(id, value) {\n if (this.receipts.has(id)) {\n let receipt = this.receipts.get(id);\n if (value instanceof Error) {\n receipt.reject(value);\n } else {\n receipt.resolve(value);\n }\n\n this.receipts.delete(id);\n if (this.receipts.size === 0) this.unref();\n } else {\n /* Do nothing, we don't mind duplicate responses */\n debugging('message-ledger') && console.debug(this.debugLabel, 'received duplicate response', id);\n }\n }\n\n /**\n * Fail all pending transmission receipts with err.\n * \n * @param {Error} err \n */\n failAllTransmissions(err) {\n if (!(err instanceof Error)) {\n throw new Error(\"MessageLedger.failAllTransmissions: Argument must be instance of Error\");\n }\n \n for (let id of this.receipts.keys()) {\n this.fulfillMessagePromise(id, err);\n }\n }\n}\n\nObject.assign(module.exports, {\n MessageLedger,\n});\n\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/message-ledger.js?");
5566
+ "use strict";
5567
+ eval("/**\n * @file protocol/connection/message-ledger.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The MessageLedger is responsible for storing receipts for messages\n * that are given to the sender. The receipts are used to resolve the\n * promise that was returned from the sender, to indicate when the message\n * is complete.\n * \n * Request receipt promises are resolved once a response has been received.\n * Response receipt promises are resolved once the response has been sent.\n */\n\n\nconst debugging = __webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope('dcp');\nconst ONE_DAY_MS = 24 * 60 * 60 * 1000;\nclass MessageLedger {\n constructor(connection) {\n this.connection = connection;\n const id = this.connection._id;\n this.debugLabel = connection.isInitiator? `message-ledger(i#${id}):` : `message-ledger(t#${id}):`;\n this.receipts = new Map(); // id => {message, resolve, reject\n this.batches = new Map(); // id -> [...messages]\n this.silentFailMessage = false;\n }\n\n /**\n * This method is invoked by the sender upon sending a new request.\n * It stores a receipt of the message and returns a promise that will\n * be resolved when the message's lifecycle has completed.\n * \n * @param {Message} message - The message to keep a receipt of its eventual transmission \n */\n addMessage(message) {\n if (this.receipts.has(message.id)) {\n throw new Error(`Duplicate transmission receipt ${JSON.stringify(message)}`);\n }\n this.ref();\n return new Promise((resolve, reject) => {\n this.receipts.set(message.id, { message, resolve, reject });\n }).catch((error) =>\n {\n if (!this.silentFailMessage)\n {\n throw error;\n }\n\n // If we \"silently fail\", we still need to resolve with the right-shaped\n // return value or things will break in hard-to-debug ways.\n return {\n success: false,\n payload: error,\n }\n });\n }\n\n /**\n * Analogous to timer/socket/server#ref. Calling ref 1 or more times means\n * the event loop will not exit until this is `unref`d.\n */\n ref() {\n if (this.refTimer) return;\n this.refTimer = setTimeout( () => {\n this.refTimer = null;\n this.ref();\n }, ONE_DAY_MS);\n }\n\n /**\n * Analogous to timer/socket/server#unref. Calling unref 1 or more times means\n * the event loop will not be prevented from exiting by this message-ledger.\n */\n unref() {\n clearTimeout(this.refTimer);\n this.refTimer = null;\n }\n\n /**\n * Add a batch to the ledger\n * @param {BatchMessage} batch A batch message which we are trying to send.\n */\n addBatch(batch) {\n this.batches.set(batch.id, batch);\n }\n\n /**\n * Remove a batch from the ledger now that its messages have been received.\n * Also mark the messages as sent.\n * @param {object} ack ack message \n */\n handleAck(ack) {\n const id = ack.messageId;\n if (this.receipts.has(id)) {\n this.handleReceiptAck(id, ack);\n } else if (this.batches.has(id)) {\n this.handleBatchAck(id, ack);\n } else {\n /* Do nothing, we don't mind spurious acks */\n debugging('message-ledger') && console.debug(this.debugLabel, 'received unhandled ack', ack.toJSON());\n }\n }\n\n /**\n * Find message in receipts and mark it as sent, also resolve/delete if its a response.\n * !Note: This is untested code since current implementation always batches before sending.\n * @param {string} id id of message that we have a receipt for\n * @param {object} ack ack message\n */\n handleReceiptAck(id, ack) {\n const receipt = this.receipts.get(id);\n if (receipt.message.body.ackToken === ack.token) {\n this.fulfillResponsePromise(receipt.message);\n /* We don't delete a receipt until the promise can be fulfilled. */\n }\n }\n\n /**\n * Iterate over messages in batch and mark each as sent.\n * @param {string} id id of batch that we have sent\n * @param {object} ack ack message\n */\n handleBatchAck(id, ack) {\n const batch = this.batches.get(id);\n if (batch.ackToken === ack.token) {\n debugging('message-ledger') && console.debug(this.debugLabel, 'received batch ack');\n batch.messages.forEach( (m) => this.fulfillResponsePromise(m) );\n this.connection.sender.clearFlightDeck(batch, ack.nonce);\n this.batches.delete(id);\n } else {\n // we just ignore spurious acks\n debugging('message-ledger') && console.debug(this.debugLabel, 'received batch ack with wrong token', batch.ackToken, ack);\n }\n }\n /**\n * Like fulfillMessagePromise but ensures only responses are fulfilled.\n * @param {Connection.Message} message The response that we are fulfilling.\n */\n fulfillResponsePromise(message) {\n if (message instanceof this.connection.Response) {\n this.fulfillMessagePromise(message.id, true)\n }\n }\n\n /**\n * This method resolves\n * \n * @param {Message.id|string} id \n * @param {*|Error} value - Error instance to reject, resolves with value otherwise\n */\n fulfillMessagePromise(id, value) {\n if (this.receipts.has(id)) {\n let receipt = this.receipts.get(id);\n if (value instanceof Error) {\n receipt.reject(value);\n } else {\n receipt.resolve(value);\n }\n\n this.receipts.delete(id);\n if (this.receipts.size === 0) this.unref();\n } else {\n /* Do nothing, we don't mind duplicate responses */\n debugging('message-ledger') && console.debug(this.debugLabel, 'received duplicate response', id);\n }\n }\n\n /**\n * Fail all pending transmission receipts with err.\n * \n * @param {Error} err \n */\n failAllTransmissions(err) {\n if (!(err instanceof Error)) {\n console.error(new Error(\"MessageLedger.failAllTransmissions: Argument must be instance of Error\"));\n err = new Error('invalid error: ' + err);\n }\n \n for (let id of this.receipts.keys()) {\n debugging('message-ledger') && console.debug('failing transmission:', id, '\\n ' + JSON.stringify(this.receipts.get(id)));\n this.fulfillMessagePromise(id, err);\n }\n }\n}\n\nObject.assign(module.exports, {\n MessageLedger,\n});\n\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/message-ledger.js?");
5564
5568
 
5565
5569
  /***/ }),
5566
5570
 
@@ -5582,7 +5586,7 @@ eval("/**\n * @file protocol/connection/message.js\n * @author Ryan
5582
5586
  /*! no static exports found */
5583
5587
  /***/ (function(module, exports, __webpack_require__) {
5584
5588
 
5585
- eval("/**\n * @file protocol/connection/receiver.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The receiver class is responsible for listening on a transport instance\n * for messages, and for parsing and validating messages as they are received.\n * Upon a valid message, it will do the following:\n * On Request: emit on the 'request' event\n * On Response: resolve the request receipt that corresponds to the message ID\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\");\n\nlet nanoid;\nlet perf;\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 perf = requireNative('perf_hooks').performance;\n} else {\n nanoid = __webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\");\n perf = performance;\n}\n\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { ValidityStampCache } = __webpack_require__(/*! ./validity-stamp-cache */ \"./src/protocol-v4/connection/validity-stamp-cache.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');\n\nclass Receiver extends EventEmitter {\n constructor(connection, messageLedger) {\n super(\"Protocol Receiver\");\n\n this.connection = connection;\n this.messageLedger = messageLedger;\n const id = this.connection._id;\n this.debugLabel = this.connection.isInitiator? `receiver(i#${id}):` : `receiver(t#${id}):`;\n\n // Used by non-ntp connections to determine \"the present\"\n // time: seconds since epoch when response was sent\n // receivedMs: monotonically increasing timestamp of receipt in milliseconds\n this.lastResponseTiming = {\n time: Date.now() / 1000,\n receivedMs: perf.now(),\n }\n }\n\n /**\n * This method validates the ttl period on a request.\n * @param {Connection.Request} req\n * @returns {boolean} true if the ttl is valid, false otherwise\n */\n isTtlValid(req) {\n const validity = req.payload.validity;\n if (!validity || !validity.stamp || typeof validity.time !== 'number') {\n const err = new DCPError(\n 'Invalid request. Validity property must have `time` and `stamp`.',\n 'EINVAL',\n );\n req.respond(err);\n return false;\n } else {\n // determine ttl of the request\n let ttl;\n if (!validity.ttl || typeof validity.ttl !== 'number') {\n ttl = this.connection.ttl.default || this.connection.ttl.min;\n } else {\n if (validity > this.connection.ttl.max) ttl = this.connection.ttl.max;\n else if (validity.ttl < this.connection.ttl.min) ttl = this.connection.ttl.min;\n else ttl = validity.ttl;\n }\n\n const now = this.connection.currentTime();\n const minimumPresent = now - dcpConfig.dcp.validitySlopValue;\n const maximumPresent = now + dcpConfig.dcp.validitySlopValue;\n const expirationTime = validity.time + ttl;\n\n if (validity.time > maximumPresent) {\n const err = new DCPError(\n `Request was sent from the future (${validity.time} > ${maximumPresent})`,\n 'ETIMETRAVEL',\n );\n req.respond(err);\n return false;\n } else if (expirationTime < minimumPresent) {\n const err = new DCPError(\n `Request validity has expired (${expirationTime} < ${minimumPresent})`,\n 'EEXPIRED',\n {\n timestamp: validity.time,\n offset: now - validity.time,\n },\n );\n req.respond(err);\n return false;\n }\n\n try {\n ValidityStampCache.insert(validity.stamp, expirationTime);\n } catch (e) {\n const err = new DCPError(\n `Duplicate request validity stamp: ${validity.stamp}`,\n 'EDUP',\n );\n req.respond(err);\n return false;\n }\n }\n\n return true;\n }\n\n /**\n * Central method for receiver to begin processing a message.\n * This method trusts that the message has a valid dcpsid, signature,\n * and nonce.\n * @param {object} messageJSON validated message object\n */\n onMessage(messageJSON) {\n const body = messageJSON.body;\n const owner = new wallet.Address(messageJSON.owner);\n if (body.type === 'response') {\n this.onResponse(body, owner);\n } else if (body.type === 'request') {\n this.onRequest(body, owner);\n } else if (body.type === 'batch') {\n this.onBatch(body, owner);\n } else {\n throw new DCPError(`Message in batch has unknown type: ${body.type}`, 'EINTERNAL');\n }\n }\n\n /**\n * This method is invoked when the connection receives a new request.\n * It should validate the request against the session state, and handle\n * special-case session operations like 'connect' and 'close'.\n *\n * @param {Object} msgBody\n * @param {wallet.Address} owner\n */\n onRequest(msgBody, owner) {\n debugging('connection') && console.debug(this.debugLabel, 'received request.');\n const req = this.connection.Request.fromJSON(msgBody, owner);\n\n if (this.connection.state.isNot('established')) { /* XXXwg narrow down this list */\n if (req.payload.operation !== 'connect') {\n const message = \"First request operation received from peer was not 'connect'\";\n debugging('connection') && console.error(this.debugLabel, message);\n this.connection.close(message, true);\n return\n }\n this.handleFirstRequest(req).catch( (err) => {\n console.error(this.debugLabel, `bad first request was sent to target. ${err.code || '(no code)'}`, err, err.code);\n this.connection.close(err, true);\n });\n } else if (!this.isTtlValid(req)) {\n return;\n } else {\n let reqId = req.id;\n \n this.connection.openRequests[reqId] = msgBody;\n setTimeout(() => {\n /* req.id frequently mutates between memoization and timeout */\n delete this.connection.openRequests[reqId];\n }, 1000);\n this.handleOperation(req);\n }\n }\n\n async handleFirstRequest(req) {\n const cacheSize = 10; /* 0 should be enough */\n var cache; /* we keep a cache of recently-seen half sid in case sender retransmits a connect message */\n var bottomHalfSid;\n \n debugging('connection') && console.debug(this.debugLabel, 'received first request.');\n if (!this.handleFirstRequest.cache)\n this.handleFirstRequest.cache = {};\n cache = this.handleFirstRequest.cache;\n \n const initiatorVersion = req.payload.data.version;\n const targetCompatibility = this.connection.constructor.VERSION_COMPATIBILITY;\n const versionCompatible = semver.satisfies(initiatorVersion, targetCompatibility);\n if (!versionCompatible) {\n const versionErr = new DCPError(targetCompatibility, 'EVERSION');\n const connectResponse = new this.connection.Response(req, versionErr);\n await this.connection.sender.specialFirstSend(connectResponse);\n throw versionErr;\n }\n\n const topHalfSid = req.payload.data.sid;\n\n if (cache.hasOwnProperty(topHalfSid)) {\n bottomHalfSid = cache[topHalfSid];\n console.error(`*** Warning: remote presented recently-seen half sid ${topHalfSid}; reusing ${bottomHalfSid}`);\n } else {\n bottomHalfSid = nanoid();\n cache[topHalfSid] = bottomHalfSid;\n if (Object.keys(cache).length > cacheSize) {\n for (let prop in cache) {\n delete cache[prop];\n break;\n }\n }\n }\n\n if (typeof topHalfSid !== 'string' || topHalfSid.length < bottomHalfSid.length) {\n throw new DCPError(`Initiator sent an invalid DCPSID: ${topHalfSid}`, 'EDCPSID');\n }\n\n // Append our half of the dcpsid\n const dcpsid = topHalfSid + bottomHalfSid;\n const connectResponse = new this.connection.Response(req, {\n version: this.connection.constructor.VERSION,\n operation: 'connect'\n });\n\n // we must emit `established` before sending this response to ensure\n // that when the initiator sends its first request we are in that state.\n this.connection.establishTarget(dcpsid, req.owner);\n try {\n await this.connection.sender.specialFirstSend(connectResponse);\n } catch (error) {\n console.error('Failed to send `connect` response.');\n return;\n }\n\n // usually established, but it's possible to close before this fires.\n debugging('connection') && console.debug(\n this.debugLabel, \n 'first request handled. Connection state:', \n this.connection.state.valueOf()\n );\n }\n\n handleOperation(req) {\n switch (req.payload.operation) {\n case 'connect':\n throw new DCPError('Request operation \"connect\" received after establishment', 'ETOOMANYCONNECT');\n case 'close':\n this.connection.closeWait(req.respond());\n break;\n \n case 'keepalive':\n req.respond();\n break;\n\n default:\n this.emit('request', req);\n break;\n }\n }\n\n /**\n * This method is invoked when the connection receives a new response.\n * It should validate the response against the session state.\n *\n * @param {Object} msgBody\n * @param {wallet.Address} owner\n */\n onResponse(msgBody, owner) {\n debugging('connection') && console.debug(this.debugLabel, 'received response.');\n const resp = this.connection.Response.fromJSON(msgBody, owner);\n\n this.lastResponseTiming.time = resp.time;\n this.lastResponseTiming.receivedMs = perf.now();\n\n this.messageLedger.fulfillMessagePromise(resp.id, resp);\n }\n\n /**\n * This method handles rehydrating Batch Requests and then\n * passes internal messages on to appropriate handlers.\n * @param {Object} msgBody\n * @param {wallet.Address} owner\n */\n onBatch(msgBody, owner) {\n const batch = this.connection.Batch.fromJSON(msgBody, owner);\n\n for (let messageObj of batch.messageObjects) {\n const type = messageObj.type;\n switch (type) {\n case 'request':\n this.onRequest(messageObj, owner); \n break;\n case 'response':\n this.onResponse(messageObj, owner);\n break;\n default:\n throw new DCPError(`Message in batch has unknown type: ${type}`, 'EINTERNAL');\n }\n }\n }\n}\n\nObject.assign(module.exports, {\n Receiver,\n});\n\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/receiver.js?");
5589
+ eval("/**\n * @file protocol/connection/receiver.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The receiver class is responsible for listening on a transport instance\n * for messages, and for parsing and validating messages as they are received.\n * Upon a valid message, it will do the following:\n * On Request: emit on the 'request' event\n * On Response: resolve the request receipt that corresponds to the message ID\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\");\n\nlet nanoid;\nlet perf;\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 perf = requireNative('perf_hooks').performance;\n} else {\n nanoid = __webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\");\n perf = performance;\n}\n\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { ValidityStampCache } = __webpack_require__(/*! ./validity-stamp-cache */ \"./src/protocol-v4/connection/validity-stamp-cache.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');\n\nclass Receiver extends EventEmitter {\n constructor(connection, messageLedger) {\n super(\"Protocol Receiver\");\n\n this.connection = connection;\n this.messageLedger = messageLedger;\n const id = this.connection._id;\n this.debugLabel = this.connection.isInitiator? `receiver(i#${id}):` : `receiver(t#${id}):`;\n\n // Used by non-ntp connections to determine \"the present\"\n // time: seconds since epoch when response was sent\n // receivedMs: monotonically increasing timestamp of receipt in milliseconds\n this.lastResponseTiming = {\n time: Date.now() / 1000,\n receivedMs: perf.now(),\n }\n }\n\n /**\n * This method validates the ttl period on a request.\n * @param {Connection.Request} req\n * @returns {boolean} true if the ttl is valid, false otherwise\n */\n isTtlValid(req) {\n const validity = req.payload.validity;\n if (!validity || !validity.stamp || typeof validity.time !== 'number') {\n const err = new DCPError(\n 'Invalid request. Validity property must have `time` and `stamp`.',\n 'EINVAL',\n );\n req.respond(err);\n return false;\n } else {\n // determine ttl of the request\n let ttl;\n if (!validity.ttl || typeof validity.ttl !== 'number') {\n ttl = this.connection.ttl.default || this.connection.ttl.min;\n } else {\n if (validity > this.connection.ttl.max) ttl = this.connection.ttl.max;\n else if (validity.ttl < this.connection.ttl.min) ttl = this.connection.ttl.min;\n else ttl = validity.ttl;\n }\n\n const now = this.connection.currentTime();\n const minimumPresent = now - dcpConfig.dcp.validitySlopValue;\n const maximumPresent = now + dcpConfig.dcp.validitySlopValue;\n const expirationTime = validity.time + ttl;\n\n if (validity.time > maximumPresent) {\n const err = new DCPError(\n `Request was sent from the future (${validity.time} > ${maximumPresent})`,\n 'ETIMETRAVEL',\n );\n req.respond(err);\n return false;\n } else if (expirationTime < minimumPresent) {\n const err = new DCPError(\n `Request validity has expired (${expirationTime} < ${minimumPresent})`,\n 'EEXPIRED',\n {\n timestamp: validity.time,\n offset: now - validity.time,\n },\n );\n req.respond(err);\n return false;\n }\n\n try {\n ValidityStampCache.insert(validity.stamp, expirationTime);\n } catch (e) {\n const err = new DCPError(\n `Duplicate request validity stamp: ${validity.stamp}`,\n 'EDUP',\n );\n req.respond(err);\n return false;\n }\n }\n\n return true;\n }\n\n messageBodyDebugLogger(body, batchInfo)\n {\n const dcpsid = this.connection.dcpsid || '<new>';\n\n function now()\n {\n const d = new Date();\n var ms = (d % 1000) + '';\n var ds;\n\n switch (ms.length)\n {\n case 1:\n ms = '00' + ms;\n break;\n case 2:\n ms = '0' + ms;\n break;\n }\n \n ds = d.toLocaleTimeString().replace(/ .*/, '.' + ms);\n return ds;\n }\n \n if (body.type === 'response') {\n debugging() && console.debug(this.debugLabel, `${now()}: session ${dcpsid.slice(0,6)}${batchInfo} sent response`, (body.payload && body.payload.operation) || '');\n } else if (body.type === 'request') {\n debugging() && console.debug(this.debugLabel, `${now()}: session ${dcpsid.slice(0,6)}${batchInfo} sent request ${body.payload && body.payload.operation}`);\n } else if (body.type === 'batch') {\n debugging() && console.debug(this.debugLabel, `${now()}: session ${dcpsid.slice(0,6)}${batchInfo} sent batch of length ${body.payload.length}, ${body.id.slice(-6)}`);\n } else {\n debugging() && console.debug(this.debugLabel, `${now()}: session ${dcpsid.slice(0,6)}${batchInfo} sent invalid message body`, body);\n }\n }\n\n /**\n * Central implementation for dispatching events based on message bodies.\n */\n dispatchMessageBody(messageBody, owner, batchDebugLabel)\n {\n debugging() && this.messageBodyDebugLogger(messageBody, batchDebugLabel);\n \n if (messageBody.type === 'response') {\n this.onResponse(messageBody, owner);\n } else if (messageBody.type === 'request') {\n this.onRequest(messageBody, owner);\n } else if (messageBody.type === 'batch') {\n this.onBatch(messageBody, owner);\n } else if (messageBody.type === 'unhandled-message') {\n console.warn('Ignoring unhandled message', JSON.stringify(messageBody));\n } else {\n throw new DCPError(`Message in batch has unknown type: ${messageBody.type}`, 'EINTERNAL');\n }\n }\n \n /**\n * Central method for receiver to begin processing a message.\n * This method trusts that the message has a valid dcpsid, signature,\n * and nonce.\n * @param {object} message validated message object\n */\n onMessage(message) {\n const owner = new wallet.Address(message.owner);\n\n this.dispatchMessageBody(message.body, owner, '');\n }\n\n /**\n * This method is invoked when the connection receives a new request.\n * It should validate the request against the session state, and handle\n * special-case session operations like 'connect' and 'close'.\n *\n * @param {Object} msgBody\n * @param {wallet.Address} owner\n */\n onRequest(msgBody, owner) {\n const req = this.connection.Request.fromJSON(msgBody, owner);\n\n if (this.connection.state.isNot('established')) { /* XXXwg narrow down this list */\n if (req.payload.operation !== 'connect') {\n const message = \"First request operation received from peer was not 'connect'\";\n debugging('connection') && console.error(this.debugLabel, message);\n this.connection.close(message, true);\n return\n }\n this.handleFirstRequest(req).catch( (err) => {\n console.error(this.debugLabel, `bad first request was sent to target. ${err.code || '(no code)'}`, err, err.code);\n this.connection.close(err, true);\n });\n } else if (!this.isTtlValid(req)) {\n return;\n } else {\n let reqId = req.id;\n \n this.connection.openRequests[reqId] = msgBody;\n setTimeout(() => {\n /* req.id frequently mutates between memoization and timeout */\n delete this.connection.openRequests[reqId];\n }, 1000);\n this.handleOperation(req);\n }\n }\n\n async handleFirstRequest(req) {\n const cacheSize = 10; /* 0 should be enough */\n var cache; /* we keep a cache of recently-seen half sid in case sender retransmits a connect message */\n var bottomHalfSid;\n \n debugging('connection') && console.debug(this.debugLabel, 'received first request.');\n if (!this.handleFirstRequest.cache)\n this.handleFirstRequest.cache = {};\n cache = this.handleFirstRequest.cache;\n \n const initiatorVersion = req.payload.data.version;\n const targetCompatibility = this.connection.constructor.VERSION_COMPATIBILITY;\n const versionCompatible = semver.satisfies(initiatorVersion, targetCompatibility);\n if (!versionCompatible) {\n const versionErr = new DCPError(targetCompatibility, 'EVERSION');\n const connectResponse = new this.connection.Response(req, versionErr);\n await this.connection.sender.specialFirstSend(connectResponse);\n throw versionErr;\n }\n\n const topHalfSid = req.payload.data.sid;\n\n if (cache.hasOwnProperty(topHalfSid)) {\n bottomHalfSid = cache[topHalfSid];\n console.error(`*** Warning: remote presented recently-seen half sid ${topHalfSid}; reusing ${bottomHalfSid}`);\n } else {\n bottomHalfSid = nanoid();\n cache[topHalfSid] = bottomHalfSid;\n if (Object.keys(cache).length > cacheSize) {\n for (let prop in cache) {\n delete cache[prop];\n break;\n }\n }\n }\n\n if (typeof topHalfSid !== 'string' || topHalfSid.length < bottomHalfSid.length) {\n throw new DCPError(`Initiator sent an invalid DCPSID: ${topHalfSid}`, 'EDCPSID');\n }\n\n // Append our half of the dcpsid\n const dcpsid = topHalfSid + bottomHalfSid;\n const connectResponse = new this.connection.Response(req, {\n version: this.connection.constructor.VERSION,\n operation: 'connect'\n });\n\n // we must emit `established` before sending this response to ensure\n // that when the initiator sends its first request we are in that state.\n this.connection.establishTarget(dcpsid, req.owner);\n try {\n await this.connection.sender.specialFirstSend(connectResponse);\n } catch (error) {\n console.error('Failed to send `connect` response.');\n return;\n }\n\n // usually established, but it's possible to close before this fires.\n debugging('connection') && console.debug(\n this.debugLabel, \n 'first request handled. Connection state:', \n this.connection.state.valueOf()\n );\n }\n\n handleOperation(req) {\n switch (req.payload.operation) {\n case 'connect':\n throw new DCPError('Request operation \"connect\" received after establishment', 'ETOOMANYCONNECT');\n case 'close':\n this.connection.closeWait(req.respond()).catch(error => console.error('Unexpected error closing initiator:', error.message));\n break;\n \n case 'keepalive':\n req.respond();\n break;\n\n default:\n this.emit('request', req);\n break;\n }\n }\n\n /**\n * This method is invoked when the connection receives a new response.\n * It should validate the response against the session state.\n *\n * @param {Object} msgBody\n * @param {wallet.Address} owner\n */\n onResponse(msgBody, owner) {\n debugging('connection') && console.debug(this.debugLabel, 'received response.');\n const resp = this.connection.Response.fromJSON(msgBody, owner);\n\n this.lastResponseTiming.time = resp.time;\n this.lastResponseTiming.receivedMs = perf.now();\n\n this.messageLedger.fulfillMessagePromise(resp.id, resp);\n }\n\n /**\n * This method handles rehydrating Batch Requests and then\n * passes internal messages on to appropriate handlers.\n * @param {Object} msgBody\n * @param {wallet.Address} owner\n */\n onBatch(msgBody, owner) {\n const batch = this.connection.Batch.fromJSON(msgBody, owner);\n var batchDebugLabel;\n \n debugging() && (batchDebugLabel = ' batch ' + batch.id.slice(-6));\n\n for (let messageBody of batch.messageObjects)\n this.dispatchMessageBody(messageBody, owner, batchDebugLabel);\n }\n}\n\nObject.assign(module.exports, {\n Receiver,\n});\n\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/receiver.js?");
5586
5590
 
5587
5591
  /***/ }),
5588
5592
 
@@ -5593,7 +5597,8 @@ eval("/**\n * @file protocol/connection/receiver.js\n * @author Ryan
5593
5597
  /*! no static exports found */
5594
5598
  /***/ (function(module, exports, __webpack_require__) {
5595
5599
 
5596
- eval("/**\n * @file protocol/connection/request.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The ConnectionRequest is a factory for a request class that extends a bound\n * ConnectionMessage class.\n */\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\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\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\n\nconst ConnectionRequest = (ConnectionMessage) => class extends ConnectionMessage {\n /**\n * There are 3 forms for this constructor:\n * 1. new Connection.Request(): A new request is constructed\n * 2. new Connection.Request({object} payload, {wallet.Keystore} [keystore]): A new Request Message is constructed, with the passed object specifying the payload (operation, data)\n * 3. new Connection.Request({string} operation, {any} [data], {wallet.Keystore} [keystore])\n */\n constructor(...args) {\n super();\n\n this.resourceKeystores = new Map(); // resourceAddress => keystore\n this.auth = {};\n this.payload = {\n operation: null,\n allow: [],\n validity: {\n stamp: nanoid(),\n ttl: this.connection.ttl.default,\n time: null, // set on send\n }\n };\n\n let ks;\n if (args[args.length - 1] instanceof wallet.Keystore) {\n ks = args.pop();\n }\n\n if (typeof args[0] === 'object') {\n Object.assign(this.payload, args[0]);\n } else if (typeof args[0] === 'string') {\n Object.assign(this.payload, {\n operation: args[0],\n data: args[1],\n });\n }\n\n if (typeof this.payload.operation !== 'string') {\n throw new Error(\"Request operation must be a string\");\n }\n\n if (ks instanceof wallet.Keystore) {\n // Defer awaiting this promise until toJSON\n this.ctorAuthPromise = this.authorize(ks);\n }\n }\n\n /**\n * This static method is used by the connection to rehydrate a request that was received.\n * @param {object} requestObj\n * @param {wallet.Address} owner\n */\n static fromJSON(requestObj, owner) {\n if (requestObj.type !== 'request') {\n throw new Error(`Request.fromJSON: Object.type is not 'request', it is '${requestObj.type}'`)\n }\n\n const req = new this(requestObj.payload);\n req.id = requestObj.id;\n req.nonce = requestObj.nonce;\n req.dcpsid = requestObj.dcpsid;\n req.auth = requestObj.auth || {};\n req.owner = owner;\n return req;\n }\n\n /**\n * This method is used to dehydrate the request into a plain object before it is sent to the peer.\n * @returns {object} - An object that will be sent as-is to the peer\n */\n async toJSON() {\n this.payload.validity.time = this.connection.currentTime();\n\n const obj = super.toJSON();\n obj.type = 'request';\n\n // Since we can't await authorize in the constructor, do it here\n await this.ctorAuthPromise;\n\n obj.auth = {};\n for (const [resourceAddress, ks] of this.resourceKeystores) {\n obj.auth[resourceAddress] = await ks.makeSignature(obj.payload);\n }\n\n return obj;\n }\n\n respond(...args) {\n const res = new this.connection.Response(this, ...args);\n return res.send();\n }\n\n /**\n *\n * @param {wallet.Keystore} [resourceKeystore] - optional resource keystore to provide authorization with,\n * shorthand for this.authorize(resourceKeystore)\n */\n async send(resourceKeystore) {\n if (resourceKeystore) {\n await this.authorize(resourceKeystore);\n }\n\n return super.send();\n }\n\n /**\n * This method authorizes `guardian` to use `resource` if `guardian`'s peer is `accessor`.\n *\n * @note: If the guardianAddress is not specified, the default is the peer address. \n * If the connection is not established, this method will complete the connection\n * then complete the authorization.\n *\n * @param {wallet.Keystore} [resourceKeystore=wallet.get()]\n * @param {wallet.Address} [guardianAddress=this.connection.peerAddress]\n * @param {wallet.Address} [accessorAddress=this.connection.identity.address]\n */\n async authorize(resourceKeystore = wallet.get(), guardianAddress, accessorAddress) {\n resourceKeystore = await resourceKeystore; // in case wallet.get was used\n if (!(resourceKeystore instanceof wallet.Keystore)) throw new Error(\"First argument to authorize must be instance of wallet.Keystore\");\n \n // to supply default guardian and accessor, the connection must be established\n if (this.connection.state.in(['initial', 'connecting'])) {\n await this.connection.connect();\n }\n if (!guardianAddress) {\n guardianAddress = wallet.Address(this.connection.peerAddress);\n }\n\n if (!accessorAddress) {\n accessorAddress = wallet.Address(this.connection.identity.address);\n }\n\n guardianAddress = wallet.Address(guardianAddress);\n accessorAddress = wallet.Address(accessorAddress);\n\n const resourceAddress = resourceKeystore.address;\n if (!this.doesAuthorize(resourceAddress, guardianAddress, accessorAddress, false)) {\n this.payload.allow.push({\n resource: resourceAddress.toString(),\n guardian: guardianAddress.toString(),\n accessor: accessorAddress.toString(),\n });\n\n if (!this.resourceKeystores.has(resourceAddress.toString())) {\n // Call unlock once so that we know it will be available to sign with\n await resourceKeystore.unlock();\n this.resourceKeystores.set(resourceAddress.toString(), resourceKeystore);\n }\n }\n }\n\n /**\n * This method is primarily used by the recipient of a message to verify\n * that it has been signed by the owner of the resource in question.\n *\n * @param {wallet.Address} resourceAddress\n * @param {wallet.Address} [guardianAddress=this.connection.identity.address]\n * @param {wallet.Address} [accessorAddress=this.connection.peerAddress]\n * @param {boolean} [validateSignature=true] - whether or not to also validate the signature provided on the auth object\n * @returns {boolean}\n */\n doesAuthorize(resourceAddress, guardianAddress = this.connection.identity.address, accessorAddress = this.connection.peerAddress, validateSignature=true) {\n resourceAddress = wallet.Address(resourceAddress);\n guardianAddress = wallet.Address(guardianAddress);\n accessorAddress = wallet.Address(accessorAddress);\n\n const inAllowList = this.payload.allow.some(({ resource, guardian, accessor }) => {\n return resourceAddress.eq(resource)\n && guardianAddress.eq(guardian)\n && accessorAddress.eq(accessor);\n });\n\n let isGuardianAuthorized = inAllowList;\n if (isGuardianAuthorized && validateSignature) {\n const signature = this.auth[resourceAddress.toString()];\n isGuardianAuthorized = !!(signature && resourceAddress.verifySignature(this.payload, signature));\n }\n\n return isGuardianAuthorized;\n }\n}\n\nObject.assign(module.exports, {\n ConnectionRequest\n});\n\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/request.js?");
5600
+ "use strict";
5601
+ eval("/**\n * @file protocol/connection/request.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The ConnectionRequest is a factory for a request class that extends a bound\n * ConnectionMessage class.\n */\n\n\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\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\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\n\nconst ConnectionRequest = (ConnectionMessage) => class extends ConnectionMessage {\n /**\n * There are 3 forms for this constructor:\n * 1. new Connection.Request(): A new request is constructed\n * 2. new Connection.Request({object} payload, {wallet.Keystore} [keystore]): A new Request Message is constructed, with the passed object specifying the payload (operation, data)\n * 3. new Connection.Request({string} operation, {any} [data], {wallet.Keystore} [keystore])\n */\n constructor(...args) {\n super();\n\n this.resourceKeystores = new Map(); // resourceAddress => keystore\n this.auth = {};\n this.payload = {\n operation: null,\n allow: [],\n validity: {\n stamp: nanoid(),\n ttl: this.connection.ttl.default,\n time: null, // set on send\n }\n };\n\n let ks;\n if (args[args.length - 1] instanceof wallet.Keystore) {\n ks = args.pop();\n }\n\n if (typeof args[0] === 'object') {\n Object.assign(this.payload, args[0]);\n } else if (typeof args[0] === 'string') {\n Object.assign(this.payload, {\n operation: args[0],\n data: args[1],\n });\n }\n\n if (typeof this.payload.operation !== 'string') {\n throw new Error(\"Request operation must be a string\");\n }\n\n if (ks instanceof wallet.Keystore) {\n // Defer awaiting this promise until toJSON\n this.ctorAuthPromise = this.authorize(ks);\n }\n }\n\n /**\n * This static method is used by the connection to rehydrate a request that was received.\n * @param {object} requestObj\n * @param {wallet.Address} owner\n */\n static fromJSON(requestObj, owner) {\n if (requestObj.type !== 'request') {\n throw new Error(`Request.fromJSON: Object.type is not 'request', it is '${requestObj.type}'`)\n }\n\n const req = new this(requestObj.payload);\n req.id = requestObj.id;\n req.nonce = requestObj.nonce;\n req.dcpsid = requestObj.dcpsid;\n req.auth = requestObj.auth || {};\n req.owner = owner;\n return req;\n }\n\n /**\n * This method is used to dehydrate the request into a plain object before it is sent to the peer.\n * @returns {object} - An object that will be sent as-is to the peer\n */\n async toJSON() {\n this.payload.validity.time = this.connection.currentTime();\n\n const obj = super.toJSON();\n obj.type = 'request';\n\n // Since we can't await authorize in the constructor, do it here\n await this.ctorAuthPromise;\n\n obj.auth = {};\n for (const [resourceAddress, ks] of this.resourceKeystores) {\n obj.auth[resourceAddress] = await ks.makeSignature(obj.payload);\n }\n\n return obj;\n }\n\n respond(...args) {\n const res = new this.connection.Response(this, ...args);\n return res.send();\n }\n\n /**\n *\n * @param {wallet.Keystore} [resourceKeystore] - optional resource keystore to provide authorization with,\n * shorthand for this.authorize(resourceKeystore)\n */\n async send(resourceKeystore) {\n if (resourceKeystore) {\n await this.authorize(resourceKeystore);\n }\n\n return super.send();\n }\n\n /**\n * This method authorizes `guardian` to use `resource` if `guardian`'s peer is `accessor`.\n *\n * @note: If the guardianAddress is not specified, the default is the peer address. \n * If the connection is not established, this method will complete the connection\n * then complete the authorization.\n *\n * @param {wallet.Keystore} [resourceKeystore=wallet.get()]\n * @param {wallet.Address} [guardianAddress=this.connection.peerAddress]\n * @param {wallet.Address} [accessorAddress=this.connection.identity.address]\n */\n async authorize(resourceKeystore = wallet.get(), guardianAddress, accessorAddress) {\n resourceKeystore = await resourceKeystore; // in case wallet.get was used\n if (!(resourceKeystore instanceof wallet.Keystore)) throw new Error(\"First argument to authorize must be instance of wallet.Keystore\");\n \n // to supply default guardian and accessor, the connection must be established\n if (this.connection.state.in(['initial', 'connecting'])) {\n await this.connection.connect();\n }\n if (!guardianAddress) {\n guardianAddress = wallet.Address(this.connection.peerAddress);\n }\n\n if (!accessorAddress) {\n accessorAddress = wallet.Address(this.connection.identity.address);\n }\n\n guardianAddress = wallet.Address(guardianAddress);\n accessorAddress = wallet.Address(accessorAddress);\n\n const resourceAddress = resourceKeystore.address;\n if (!this.doesAuthorize(resourceAddress, guardianAddress, accessorAddress, false)) {\n this.payload.allow.push({\n resource: resourceAddress.toString(),\n guardian: guardianAddress.toString(),\n accessor: accessorAddress.toString(),\n });\n\n if (!this.resourceKeystores.has(resourceAddress.toString())) {\n // Call unlock once so that we know it will be available to sign with\n await resourceKeystore.unlock();\n this.resourceKeystores.set(resourceAddress.toString(), resourceKeystore);\n }\n }\n }\n\n /**\n * This method is primarily used by the recipient of a message to verify\n * that it has been signed by the owner of the resource in question.\n *\n * @param {wallet.Address} resourceAddress\n * @param {wallet.Address} [guardianAddress=this.connection.identity.address]\n * @param {wallet.Address} [accessorAddress=this.connection.peerAddress]\n * @param {boolean} [validateSignature=true] - whether or not to also validate the signature provided on the auth object\n * @returns {boolean}\n */\n doesAuthorize(resourceAddress, guardianAddress = this.connection.identity.address, accessorAddress = this.connection.peerAddress, validateSignature=true) {\n resourceAddress = wallet.Address(resourceAddress);\n guardianAddress = wallet.Address(guardianAddress);\n accessorAddress = wallet.Address(accessorAddress);\n\n const inAllowList = this.payload.allow.some(({ resource, guardian, accessor }) => {\n return resourceAddress.eq(resource)\n && guardianAddress.eq(guardian)\n && accessorAddress.eq(accessor);\n });\n\n let isGuardianAuthorized = inAllowList;\n if (isGuardianAuthorized && validateSignature) {\n const signature = this.auth[resourceAddress.toString()];\n isGuardianAuthorized = !!(signature && resourceAddress.verifySignature(this.payload, signature));\n }\n\n return isGuardianAuthorized;\n }\n}\n\nObject.assign(module.exports, {\n ConnectionRequest\n});\n\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/request.js?");
5597
5602
 
5598
5603
  /***/ }),
5599
5604
 
@@ -5602,9 +5607,10 @@ eval("/**\n * @file protocol/connection/request.js\n * @author Ryan
5602
5607
  !*** ./src/protocol-v4/connection/response.js ***!
5603
5608
  \************************************************/
5604
5609
  /*! no static exports found */
5605
- /***/ (function(module, exports) {
5610
+ /***/ (function(module, exports, __webpack_require__) {
5606
5611
 
5607
- eval("/**\n * @file protocol/connection/response.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The ConnectionResponse is a factory for a response class that extends a bound\n * ConnectionMessage class.\n */\n\nconst ConnectionResponse = (ConnectionMessage) => class extends ConnectionMessage {\n /**\n * @param {Connection.Request} request - The request that this instance will be a response to\n * @param {*|Error} data - The payload data, or an instance of Error if the response should contain an ErrorPayload\n */\n constructor(request, data) {\n super();\n\n if (request) {\n this.id = request.id;\n }\n\n if (data instanceof Error) {\n this.success = false;\n this.payload = new this.connection.ErrorPayload(data);\n } else if (data instanceof this.connection.ErrorPayload) {\n this.success = false;\n this.payload = data;\n } else {\n this.success = true;\n this.payload = data;\n }\n\n if (this.success && this.payload)\n this.emit('payload', data);\n }\n \n static get connection() {\n return ConnectionMessage.connection;\n }\n\n /**\n * This static method is used by the connection to rehydrate a response that was received.\n * @param {object} responseObj\n * @param {wallet.Address} owner\n */\n static fromJSON(responseObj, owner) {\n if (responseObj.type !== 'response') {\n throw new Error(`Response.fromJSON: Object.type is not 'response', it is '${responseObj.type}'`)\n }\n\n const data = responseObj.success\n ? responseObj.payload\n : new this.connection.ErrorPayload(responseObj.payload);\n const res = new this(null, data);\n res.id = responseObj.id;\n res.success = responseObj.success;\n res.time = responseObj.time;\n res.nonce = responseObj.nonce;\n res.dcpsid = responseObj.dcpsid;\n res.owner = owner;\n return res;\n }\n\n /**\n * This method is used to dehydrate the response into a plain object before it is sent to the peer.\n * @returns {object} - An object that will be sent as-is to the peer\n */\n toJSON() {\n const obj = super.toJSON();\n obj.success = this.success;\n obj.type = 'response';\n obj.time = this.connection.currentTime();\n\n return obj;\n }\n}\n\nObject.assign(module.exports, {\n ConnectionResponse\n});\n\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/response.js?");
5612
+ "use strict";
5613
+ eval("/**\n * @file protocol/connection/response.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The ConnectionResponse is a factory for a response class that extends a bound\n * ConnectionMessage class.\n */\n\n\nconst ConnectionResponse = (ConnectionMessage) => class extends ConnectionMessage {\n /**\n * @param {Connection.Request} request - The request that this instance will be a response to\n * @param {*|Error} data - The payload data, or an instance of Error if the response should contain an ErrorPayload\n */\n constructor(request, data) {\n super();\n\n if (request) {\n this.id = request.id;\n }\n\n if (data instanceof Error) {\n this.success = false;\n this.payload = new this.connection.ErrorPayload(data);\n } else if (data instanceof this.connection.ErrorPayload) {\n this.success = false;\n this.payload = data;\n } else {\n this.success = true;\n this.payload = data;\n }\n\n if (this.success && this.payload)\n this.emit('payload', data);\n }\n \n static get connection() {\n return ConnectionMessage.connection;\n }\n\n /**\n * This static method is used by the connection to rehydrate a response that was received.\n * @param {object} responseObj\n * @param {wallet.Address} owner\n */\n static fromJSON(responseObj, owner) {\n if (responseObj.type !== 'response') {\n throw new Error(`Response.fromJSON: Object.type is not 'response', it is '${responseObj.type}'`)\n }\n\n const data = responseObj.success\n ? responseObj.payload\n : new this.connection.ErrorPayload(responseObj.payload);\n const res = new this(null, data);\n res.id = responseObj.id;\n res.success = responseObj.success;\n res.time = responseObj.time;\n res.nonce = responseObj.nonce;\n res.dcpsid = responseObj.dcpsid;\n res.owner = owner;\n return res;\n }\n\n /**\n * This method is used to dehydrate the response into a plain object before it is sent to the peer.\n * @returns {object} - An object that will be sent as-is to the peer\n */\n toJSON() {\n const obj = super.toJSON();\n obj.success = this.success;\n obj.type = 'response';\n obj.time = this.connection.currentTime();\n\n return obj;\n }\n}\n\nObject.assign(module.exports, {\n ConnectionResponse\n});\n\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/response.js?");
5608
5614
 
5609
5615
  /***/ }),
5610
5616
 
@@ -5615,7 +5621,8 @@ eval("/**\n * @file protocol/connection/response.js\n * @author Ryan
5615
5621
  /*! no static exports found */
5616
5622
  /***/ (function(module, exports, __webpack_require__) {
5617
5623
 
5618
- 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\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 debugging('connection') && console.debug(this.debugLabel, 'enqueueing message with op:', message.payload && message.payload.operation);\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?");
5624
+ "use strict";
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?");
5619
5626
 
5620
5627
  /***/ }),
5621
5628
 
@@ -5626,7 +5633,8 @@ eval("/* WEBPACK VAR INJECTION */(function(setImmediate) {/**\n * @file p
5626
5633
  /*! no static exports found */
5627
5634
  /***/ (function(module, exports, __webpack_require__) {
5628
5635
 
5629
- eval("/**\n * @file protocol/connection/validity-stamp-cache.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The validity stamp cache is used to ensure that no request from any connection\n * should have the same validity stamp as any other request (within the ttl period).\n * \n * It periodically sweeps the cache for stamps that have passed their expiry period.\n */\n\n const DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\n\nclass ValidityStampCache {\n constructor() {\n this.stamps = { /* stamp: expiry time */ };\n this.purgeInterval = null;\n }\n\n /**\n * Used to setup the purgeInterval, and is called everytime a\n * connection is created. Since this class is a singleton,\n * only setup the interval if it's not already set.\n */\n init() {\n if (!this.purgeInterval) {\n this.purgeInterval = setInterval(() => {\n this.purge();\n }, dcpConfig.dcp.validityStampCachePurgeInterval);\n if (DCP_ENV.platform === 'nodejs') this.purgeInterval.unref();\n }\n }\n\n /**\n * Remove stamps from the cache that have expired.\n */\n purge() {\n for (let stamp in this.stamps) {\n if (this.stamps[stamp] < Date.now()) {\n delete this.stamps[stamp];\n }\n }\n }\n\n /**\n * Add a stamp to the cache\n * @param {string} stamp \n * @param {number} expiry \n */\n insert(stamp, expiry) {\n if (stamp in this.stamps && this.stamps[stamp] > Date.now()) {\n throw new Error(\"Duplicate stamp\");\n }\n\n // store ms for comparison with Date.now\n this.stamps[stamp] = expiry * 1000;\n }\n}\n\nObject.assign(module.exports, {\n ValidityStampCache: new ValidityStampCache()\n});\n\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/validity-stamp-cache.js?");
5636
+ "use strict";
5637
+ eval("/**\n * @file protocol/connection/validity-stamp-cache.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The validity stamp cache is used to ensure that no request from any connection\n * should have the same validity stamp as any other request (within the ttl period).\n * \n * It periodically sweeps the cache for stamps that have passed their expiry period.\n */\n\n\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\n\nclass ValidityStampCache {\n constructor() {\n this.stamps = { /* stamp: expiry time */ };\n this.purgeInterval = null;\n }\n\n /**\n * Used to setup the purgeInterval, and is called everytime a\n * connection is created. Since this class is a singleton,\n * only setup the interval if it's not already set.\n */\n init() {\n if (!this.purgeInterval) {\n this.purgeInterval = setInterval(() => {\n this.purge();\n }, dcpConfig.dcp.validityStampCachePurgeInterval);\n if (DCP_ENV.platform === 'nodejs') this.purgeInterval.unref();\n }\n }\n\n /**\n * Remove stamps from the cache that have expired.\n */\n purge() {\n for (let stamp in this.stamps) {\n if (this.stamps[stamp] < Date.now()) {\n delete this.stamps[stamp];\n }\n }\n }\n\n /**\n * Add a stamp to the cache\n * @param {string} stamp \n * @param {number} expiry \n */\n insert(stamp, expiry) {\n if (stamp in this.stamps && this.stamps[stamp] > Date.now()) {\n throw new Error(\"Duplicate stamp\");\n }\n\n // store ms for comparison with Date.now\n this.stamps[stamp] = expiry * 1000;\n }\n}\n\nObject.assign(module.exports, {\n ValidityStampCache: new ValidityStampCache()\n});\n\n\n//# sourceURL=webpack:///./src/protocol-v4/connection/validity-stamp-cache.js?");
5630
5638
 
5631
5639
  /***/ }),
5632
5640
 
@@ -5648,7 +5656,8 @@ eval("/**\n * @file protocol/index.js\n * @author Ryan Rossiter, rya
5648
5656
  /*! no static exports found */
5649
5657
  /***/ (function(module, exports, __webpack_require__) {
5650
5658
 
5651
- eval("/**\n * @file protocol/connection.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * This message class is used as a base class for messages/requests/responses\n * within a protocol connection. It contains common fields and methods to all\n * message types, like id and payload.\n */\n\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\n\nclass Message extends EventEmitter {\n constructor() {\n super(\"Protocol Message\");\n\n if (this.constructor === Message) {\n throw new Error(\"Message cannot be instantiated, use Connection.Message instead.\");\n }\n\n this.connection = null;\n this.id = null;\n this.nonce = null;\n this.payload = null;\n this.owner = null;\n }\n\n /**\n * Init props which are incremented at each send attempt by `updateSendProps`.\n */\n initSendProps() {\n this.firstSend = Date.now();\n this.sendAttempts = 0;\n this.msUntilNextSend = 0\n this.sendEBO = this.connection.transport.makeBackoffIterator();\n this.failReason = '';\n }\n\n /**\n * Increment counters / timers for this message indicating that we are sending it.\n */\n updateSendProps() {\n const nextDelay = this.sendEBO.next().value;\n this.msUntilNextSend = Math.min(nextDelay, this.msUntilTimeout);\n this.sendAttempts++;\n }\n\n /**\n * If transport has provided a maxRetries or timeout (as a number, including 0)\n * we check this message against the provided the limit(s).\n */\n get overSendLimits() {\n const maxRetriesProvided = typeof this.connection.transport.maxRetries === 'number';\n const timeoutProvided = typeof this.connection.transport.timeout === 'number';\n let overLimits = false;\n if (maxRetriesProvided && this.outOfAttempts) {\n overLimits = true;\n this.failReason = `Out of retries. allowed=${this.connection.transport.maxRetries} sendAttempts=${this.sendAttempts}.`;\n } else if (this.sendAttempts > 100) {\n overLimits = true;\n this.failReason = 'wow something might be wrong';\n }\n\n if (timeoutProvided && this.outOfTime) {\n overLimits = true;\n const timeSinceFirstSend = (Date.now() - this.firstSend) / 1000;\n this.failReason = `Out of time (allowed ${this.connection.transport.timeout}). Total time: ${timeSinceFirstSend}.`;\n }\n return overLimits;\n }\n\n get msUntilTimeout() {\n if (typeof this.connection.transport.timeout === 'number') {\n const timeoutTimestampMs = this.firstSend + this.connection.transport.timeout * 1000;\n return timeoutTimestampMs - Date.now();\n } else {\n return Infinity;\n }\n }\n\n get outOfAttempts() {\n return this.sendAttempts > this.connection.transport.maxRetries;\n }\n\n get outOfTime() {\n return this.msUntilTimeout <= 0;\n }\n\n /**\n * This method is invoked when sending a message to a peer. It uses\n * the internal data of the message and generates an object that is ready\n * to be serialized and signed.\n */\n toJSON() {\n return {\n payload: this.payload,\n };\n }\n\n async sign(identityKeystore) {\n return identityKeystore.makeSignedMessage(await this.toJSON());\n }\n}\n\nexports.Message = Message;\n\n\n//# sourceURL=webpack:///./src/protocol-v4/message.js?");
5659
+ "use strict";
5660
+ eval("/**\n * @file protocol/connection.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * This message class is used as a base class for messages/requests/responses\n * within a protocol connection. It contains common fields and methods to all\n * message types, like id and payload.\n */\n\n\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\n\nclass Message extends EventEmitter {\n constructor() {\n super(\"Protocol Message\");\n\n if (this.constructor === Message) {\n throw new Error(\"Message cannot be instantiated, use Connection.Message instead.\");\n }\n\n this.connection = null;\n this.id = null;\n this.nonce = null;\n this.payload = null;\n this.owner = null;\n }\n\n /**\n * Init props which are incremented at each send attempt by `updateSendProps`.\n */\n initSendProps() {\n this.firstSend = Date.now();\n this.sendAttempts = 0;\n this.msUntilNextSend = 0\n this.sendEBO = this.connection.transport.makeBackoffIterator();\n this.failReason = '';\n }\n\n /**\n * Increment counters / timers for this message indicating that we are sending it.\n */\n updateSendProps() {\n const nextDelay = this.sendEBO.next().value;\n this.msUntilNextSend = Math.min(nextDelay, this.msUntilTimeout);\n this.sendAttempts++;\n }\n\n /**\n * If transport has provided a maxRetries or timeout (as a number, including 0)\n * we check this message against the provided the limit(s).\n */\n get overSendLimits() {\n const maxRetriesProvided = typeof this.connection.transport.maxRetries === 'number';\n const timeoutProvided = typeof this.connection.transport.timeout === 'number';\n let overLimits = false;\n if (maxRetriesProvided && this.outOfAttempts) {\n overLimits = true;\n this.failReason = `Out of retries. allowed=${this.connection.transport.maxRetries} sendAttempts=${this.sendAttempts}.`;\n } else if (this.sendAttempts > 100) {\n overLimits = true;\n this.failReason = 'wow something might be wrong';\n }\n\n if (timeoutProvided && this.outOfTime) {\n overLimits = true;\n const timeSinceFirstSend = (Date.now() - this.firstSend) / 1000;\n this.failReason = `Out of time (allowed ${this.connection.transport.timeout}). Total time: ${timeSinceFirstSend}.`;\n }\n return overLimits;\n }\n\n get msUntilTimeout() {\n if (typeof this.connection.transport.timeout === 'number') {\n const timeoutTimestampMs = this.firstSend + this.connection.transport.timeout * 1000;\n return timeoutTimestampMs - Date.now();\n } else {\n return Infinity;\n }\n }\n\n get outOfAttempts() {\n return this.sendAttempts > this.connection.transport.maxRetries;\n }\n\n get outOfTime() {\n return this.msUntilTimeout <= 0;\n }\n\n /**\n * This method is invoked when sending a message to a peer. It uses\n * the internal data of the message and generates an object that is ready\n * to be serialized and signed.\n */\n toJSON() {\n return {\n payload: this.payload,\n };\n }\n\n async sign(identityKeystore) {\n return identityKeystore.makeSignedMessage(await this.toJSON());\n }\n}\n\nexports.Message = Message;\n\n\n//# sourceURL=webpack:///./src/protocol-v4/message.js?");
5652
5661
 
5653
5662
  /***/ }),
5654
5663
 
@@ -5670,7 +5679,8 @@ eval("var map = {\n\t\"./\": \"./src/protocol-v4/transport/index.js\",\n\t\"./in
5670
5679
  /*! no static exports found */
5671
5680
  /***/ (function(module, exports, __webpack_require__) {
5672
5681
 
5673
- eval("/**\n * @file protocol/transport/index.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * This module is the base class for transports used by protocol.\n * A protocol transport knows how to communicate with peers using\n * a specific method (WebSocket, HTTP, postMessage, etc).\n */\n\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { makeEBOIterator } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\n\n// Children should configure the exponential-backoff algorithm\n// but the parent here provides a fallback.\nconst DEFAULT_EBO_CONFIG = { \n slot: 1,\n max: 60,\n};\n\n/**\n * @class\n * @emits Transport#message when a request is received from the peer.\n */\nclass Transport extends EventEmitter {\n constructor(transportName=\"Protocol Transport\") {\n super(transportName);\n // subclasses should override this with the module name to match\n // the dcp config key. Like `socketio`. But it defaults to this \n // event-emitter style name for now since it's not spec'd, just helpful.\n this.name = transportName;\n }\n\n /**\n * This static method will essentially just require('') the\n * transport module but provides a space for logic / guards.\n * \n * @param {string} moduleName - Identifier used to decide which transport module to load.\n */\n static require(moduleName) {\n // Make sure nobody is doing something sneaky like ../../something-secret\n if (!moduleName.match(/^[a-zA-Z0-9\\-]+$/)) {\n throw new Error(`Module name contains invalid characters: \"${moduleName}\"`);\n }\n\n const transportModule = __webpack_require__(\"./src/protocol-v4/transport sync recursive ^\\\\.\\\\/.*$\")(`./${moduleName}`);\n assert('TransportClass' in transportModule);\n\n return transportModule.TransportClass;\n }\n\n /**\n * Child knows how to get, merge, etc its own special config. But some config is shared by all\n * transports to help the connection determine when to fail the transport.\n * @param {object} config An object which may bear the keys used by the super here.\n */\n initConfig(config) {\n this.maxRetries = config.maxRetries;\n this.timeout = config.timeout;\n this.eboConfig = config.ebo || DEFAULT_EBO_CONFIG;\n }\n\n /**\n * Used by initiator to connect to target at url. Responsible for configuring own\n * timeout and retries and either emitting `connected` or `connect-failed` events.\n * @param {object} url Some way of helping the implementing class figure out where to connect to.\n * @param {object} connectionOptions configuration object\n * @returns {number} number of seconds that connection should wait before giving up on this transport's first connection.\n */\n connect(url, connectionOptions) {\n throw new Error(\"Transport.connect should be overridden\");\n }\n\n // Must guarantee that the underlying conn is closed if it resolves,\n // will throw if it can't be closed\n async close() {\n throw new Error(\"Transport.close should be overridden\");\n }\n\n /**\n * @param {string} message - A signed message (will be transported as-is) to send.\n * @returns {object} - A plain object containing the raw response data.\n * @throws on a transmission error.\n */\n send(message) {\n throw new Error(\"Transport.send should be overridden\");\n }\n\n /**\n * Get an EBO iterator for this transport to determine each new send delay.\n * Shift the iterator 1 unit to the right so that we exclude 0.\n * @returns {Iterator} an ebo iterator configured for this transport\n */\n * makeBackoffIterator() {\n const slotMs = this.eboConfig.slot * 1000;\n const maxMs = this.eboConfig.max * 1000;\n const eboIterator = makeEBOIterator(slotMs, maxMs, this.eboConfig.base);\n while (true) {\n yield eboIterator.next().value + slotMs;\n }\n }\n}\n\nexports.Transport = Transport;\n\n\n//# sourceURL=webpack:///./src/protocol-v4/transport/index.js?");
5682
+ "use strict";
5683
+ eval("/**\n * @file protocol/transport/index.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * This module is the base class for transports used by protocol.\n * A protocol transport knows how to communicate with peers using\n * a specific method (WebSocket, HTTP, postMessage, etc).\n */\n\n\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { makeEBOIterator } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\n\n// Children should configure the exponential-backoff algorithm\n// but the parent here provides a fallback.\nconst DEFAULT_EBO_CONFIG = { \n slot: 1,\n max: 60,\n};\n\n/**\n * @class\n * @emits Transport#message when a request is received from the peer.\n */\nclass Transport extends EventEmitter {\n constructor(transportName=\"Protocol Transport\") {\n super(transportName);\n // subclasses should override this with the module name to match\n // the dcp config key. Like `socketio`. But it defaults to this \n // event-emitter style name for now since it's not spec'd, just helpful.\n this.name = transportName;\n }\n\n /**\n * This static method will essentially just require('') the\n * transport module but provides a space for logic / guards.\n * \n * @param {string} moduleName - Identifier used to decide which transport module to load.\n */\n static require(moduleName) {\n // Make sure nobody is doing something sneaky like ../../something-secret\n if (!moduleName.match(/^[a-zA-Z0-9\\-]+$/)) {\n throw new Error(`Module name contains invalid characters: \"${moduleName}\"`);\n }\n\n const transportModule = __webpack_require__(\"./src/protocol-v4/transport sync recursive ^\\\\.\\\\/.*$\")(`./${moduleName}`);\n assert('TransportClass' in transportModule);\n\n return transportModule.TransportClass;\n }\n\n /**\n * Child knows how to get, merge, etc its own special config. But some config is shared by all\n * transports to help the connection determine when to fail the transport.\n * @param {object} config An object which may bear the keys used by the super here.\n */\n initConfig(config) {\n this.maxRetries = config.maxRetries;\n this.timeout = config.timeout;\n this.eboConfig = config.ebo || DEFAULT_EBO_CONFIG;\n }\n\n /**\n * Used by initiator to connect to target at url. Responsible for configuring own\n * timeout and retries and either emitting `connected` or `connect-failed` events.\n * @param {object} url Some way of helping the implementing class figure out where to connect to.\n * @param {object} connectionOptions configuration object\n * @returns {number} number of seconds that connection should wait before giving up on this transport's first connection.\n */\n connect(url, connectionOptions) {\n throw new Error(\"Transport.connect should be overridden\");\n }\n\n // Must guarantee that the underlying conn is closed if it resolves,\n // will throw if it can't be closed\n async close() {\n throw new Error(\"Transport.close should be overridden\");\n }\n\n /**\n * @param {string} message - A signed message (will be transported as-is) to send.\n * @returns {object} - A plain object containing the raw response data.\n * @throws on a transmission error.\n */\n send(message) {\n throw new Error(\"Transport.send should be overridden\");\n }\n\n /**\n * Get an EBO iterator for this transport to determine each new send delay.\n * Shift the iterator 1 unit to the right so that we exclude 0.\n * @returns {Iterator} an ebo iterator configured for this transport\n */\n * makeBackoffIterator() {\n const slotMs = this.eboConfig.slot * 1000;\n const maxMs = this.eboConfig.max * 1000;\n const eboIterator = makeEBOIterator(slotMs, maxMs, this.eboConfig.base);\n while (true) {\n yield eboIterator.next().value + slotMs;\n }\n }\n}\n\nexports.Transport = Transport;\n\n\n//# sourceURL=webpack:///./src/protocol-v4/transport/index.js?");
5674
5684
 
5675
5685
  /***/ }),
5676
5686
 
@@ -5681,7 +5691,8 @@ eval("/**\n * @file protocol/transport/index.js\n * @author Ryan Ros
5681
5691
  /*! no static exports found */
5682
5692
  /***/ (function(module, exports, __webpack_require__) {
5683
5693
 
5684
- 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\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.urlLabel}; 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?");
5694
+ "use strict";
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?");
5685
5696
 
5686
5697
  /***/ }),
5687
5698
 
@@ -5692,7 +5703,8 @@ eval("/**\n * @file protocol/transport/websocket.js\n * @author Ryan
5692
5703
  /*! no static exports found */
5693
5704
  /***/ (function(module, exports, __webpack_require__) {
5694
5705
 
5695
- eval("/* WEBPACK VAR INJECTION */(function(process) {/**\n * @file utils/assert-params.js\n * @author KC Erb <kcerb@kingsds.network>\n * @date January 2020\n *\n * This module provides methods for asserting the shape and types of an object.\n */\n\nconst path = __webpack_require__(/*! path */ \"./node_modules/path-browserify/index.js\");\nconst semver = __webpack_require__(/*! semver */ \"./node_modules/semver/semver.js\");\nconst { BigNumber } = __webpack_require__(/*! bignumber.js */ \"./node_modules/bignumber.js/bignumber.js\");\n\nconst { rehydrateRange } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\n\nconst customTypes = {\n 'amount': validateAmount,\n 'semver': validateSemver,\n 'date': validateDate,\n};\n\nvar progName;\nif (__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").isBrowserPlatform)\n progname = window.location.href;\nelse\n progName = path.basename(process.mainModule ? process.mainModule.filename : 'node-repl', '.js');\n\n/**\n * @todo FIX THIS - decouple validation and marshaling\n *\n * This function marshals wire-formatted data into the final data types used by the operation, \n * while simultaneously validating said data.\n *\n * The sub-functions throw exceptions when trying to marshal and invalid data is encountered. These\n * exceptions are then caught and used to generate an exception in this function.\n */\nfunction assertParams(obj, parameters, locationLabel) {\n for (const paramName in parameters) {\n if (parameters.hasOwnProperty(paramName)) {\n let reqVal = obj[paramName];\n const spec = parameters[paramName];\n try {\n obj[paramName] = assertParamPassesSpec(reqVal, spec)\n } catch (e) {\n if (exports.assertTriggersDebugger)\n debugger; // allow-debugger\n if (String(reqVal) === \"[object Object]\") reqVal = JSON.stringify(reqVal);\n throw new DCPError(\n `\"${paramName}\" with value \"${reqVal}\" in ${locationLabel || progName} failed to pass spec. Error message was: ` +\n e.message,\n 'EBADPARAM',\n );\n }\n }\n }\n}\n\n/**\n * Check that a given value passes a specification of that value.\n * @param {*} val The value to check\n * @param {obj} spec Must have `type` key to check type, other validation keys are type specific.\n * @returns the default val if present and val is undefined. If val is anything else, returns the passed in val\n * if `spec.type` is primitive or the result of new spec.type(val).\n * @throws when val could not be constructed or does not pass spec (specs are expected to throw as well)\n */\nfunction assertParamPassesSpec(val, spec) {\n if (spec.hasOwnProperty('default') && val === undefined) return spec.default\n\n if (typeof spec.type === 'string') {\n val = validatePrimitive(val, spec)\n } else {\n val = new spec.type(val)\n if (!val) throw new Error(`Failed to construct ${spec.type}.`)\n }\n if (typeof spec.validate === 'function') spec.validate(val)\n return val\n}\n\n/** Validate that a value meets a \"primitive\" or \"pseudoprimitive\" spec\n * @param val {*} Value to test\n * @param spec {object} requirements the value must meet\n * @param spec.type {string} Either the name of a js primitive type, or a DCP pseudoprimitive\n *\n * Spec types and descriptors:\n * 'string' regex\n * 'number' min, max\n * 'bigint' min, max\n * 'boolean' (nothing extra)\n * 'array' (nothing extra)\n * Custom types:\n * If the type is a key in the customTypes object, the corresponding function will be used to validate it.\n */\nfunction validatePrimitive(val, spec) {\n if (spec.type in customTypes) {\n return customTypes[spec.type](val);\n }\n // simple type checking\n else if (['string', 'number', 'bigint', 'boolean'].includes(spec.type)) {\n if (typeof val !== spec.type) throw new Error(`Expected ${spec.type}, got: ${typeof val}.`)\n }\n\n if (Array.isArray(spec.in) && !spec.in.includes(val)) {\n throw new Error(`Expected ${val} to be one of: ${spec.in}`);\n }\n\n // primitive validation\n switch (spec.type) {\n case 'string':\n if (spec.regex instanceof RegExp && !spec.regex.test(val))\n throw new Error(`String ${val} did not match regex ${spec.regex}.`)\n break;\n case 'number':\n if (val > spec.max) throw new Error(`Number must not be > ${spec.max}, got: ${val}.`)\n if (val < spec.min) throw new Error(`Number must not be < ${spec.min}, got: ${val}.`)\n break;\n case 'object':\n // recurse via `parameters` key\n if (typeof spec.parameters === 'object')\n assertParams(val, spec.parameters)\n break;\n\n /* -- fake primitives -- */\n case 'array':\n if (!Array.isArray(val)) throw new Error(`Expected array but ${val} didn't pass the \"Array.isArray\" check.`)\n break;\n // case 'map':\n // break;\n default:\n // do nothing, we consider it valid if any other string is provided such as bigint\n }\n\n return val;\n}\n\n/**\n * Validate and coerce an \"amount\" value\n * @param {string | number | BigNumber} amount - Value to process\n * @return {BigNumber}\n */\nfunction validateAmount(amount) {\n return new BigNumber(amount);\n}\n\nfunction validateSemver(val) {\n if (semver.valid(val)) {\n return val;\n }\n\n throw new TypeError('Invalid semver format');\n}\n\nfunction validateDate(value) {\n return new Date(value);\n}\n\nObject.assign(module.exports, {\n assertParams,\n assertParamPassesSpec,\n validatePrimitive,\n validateAmount,\n validateDate,\n});\n\nexports.assertTriggersDebugger = !!__webpack_require__(/*! process */ \"./node_modules/process/browser.js\").env.DCP_ASSERT_TRIGGERS_DEBUGGER; /* sync: dcp-assert.js */\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../node_modules/process/browser.js */ \"./node_modules/process/browser.js\")))\n\n//# sourceURL=webpack:///./src/utils/assert-params.js?");
5706
+ "use strict";
5707
+ eval("/* WEBPACK VAR INJECTION */(function(process) {/**\n * @file utils/assert-params.js\n * @author KC Erb <kcerb@kingsds.network>\n * @date January 2020\n *\n * This module provides methods for asserting the shape and types of an object.\n */\n\n\nconst path = __webpack_require__(/*! path */ \"./node_modules/path-browserify/index.js\");\nconst semver = __webpack_require__(/*! semver */ \"./node_modules/semver/semver.js\");\nconst { BigNumber } = __webpack_require__(/*! bignumber.js */ \"./node_modules/bignumber.js/bignumber.js\");\n\nconst { rehydrateRange } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\n\nconst customTypes = {\n 'amount': validateAmount,\n 'semver': validateSemver,\n 'date': validateDate,\n};\n\nvar progName;\nif (__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").isBrowserPlatform)\n progName = window.location.href;\nelse\n progName = path.basename(process.mainModule ? process.mainModule.filename : 'node-repl', '.js');\n\n/**\n * @todo FIX THIS - decouple validation and marshaling\n *\n * This function marshals wire-formatted data into the final data types used by the operation, \n * while simultaneously validating said data.\n *\n * The sub-functions throw exceptions when trying to marshal and invalid data is encountered. These\n * exceptions are then caught and used to generate an exception in this function.\n */\nfunction assertParams(obj, parameters, locationLabel) {\n for (const paramName in parameters) {\n if (parameters.hasOwnProperty(paramName)) {\n let reqVal = obj[paramName];\n const spec = parameters[paramName];\n try {\n obj[paramName] = assertParamPassesSpec(reqVal, spec)\n } catch (e) {\n if (exports.assertTriggersDebugger)\n debugger; // allow-debugger\n if (String(reqVal) === \"[object Object]\") reqVal = JSON.stringify(reqVal);\n throw new DCPError(\n `\"${paramName}\" with value \"${reqVal}\" in ${locationLabel || progName} failed to pass spec. Error message was: ` +\n e.message,\n 'EBADPARAM',\n );\n }\n }\n }\n}\n\n/**\n * Check that a given value passes a specification of that value.\n * @param {*} val The value to check\n * @param {obj} spec Must have `type` key to check type, other validation keys are type specific.\n * @returns the default val if present and val is undefined. If val is anything else, returns the passed in val\n * if `spec.type` is primitive or the result of new spec.type(val).\n * @throws when val could not be constructed or does not pass spec (specs are expected to throw as well)\n */\nfunction assertParamPassesSpec(val, spec) {\n if (spec.hasOwnProperty('default') && val === undefined) return spec.default\n\n if (typeof spec.type === 'string') {\n val = validatePrimitive(val, spec)\n } else {\n val = new spec.type(val)\n if (!val) throw new Error(`Failed to construct ${spec.type}.`)\n }\n if (typeof spec.validate === 'function') spec.validate(val)\n return val\n}\n\n/** Validate that a value meets a \"primitive\" or \"pseudoprimitive\" spec\n * @param val {*} Value to test\n * @param spec {object} requirements the value must meet\n * @param spec.type {string} Either the name of a js primitive type, or a DCP pseudoprimitive\n *\n * Spec types and descriptors:\n * 'string' regex\n * 'number' min, max\n * 'bigint' min, max\n * 'boolean' (nothing extra)\n * 'array' (nothing extra)\n * Custom types:\n * If the type is a key in the customTypes object, the corresponding function will be used to validate it.\n */\nfunction validatePrimitive(val, spec) {\n if (spec.type in customTypes) {\n return customTypes[spec.type](val);\n }\n // simple type checking\n else if (['string', 'number', 'bigint', 'boolean'].includes(spec.type)) {\n if (typeof val !== spec.type) throw new Error(`Expected ${spec.type}, got: ${typeof val}.`)\n }\n\n if (Array.isArray(spec.in) && !spec.in.includes(val)) {\n throw new Error(`Expected ${val} to be one of: ${spec.in}`);\n }\n\n // primitive validation\n switch (spec.type) {\n case 'string':\n if (spec.regex instanceof RegExp && !spec.regex.test(val))\n throw new Error(`String ${val} did not match regex ${spec.regex}.`)\n break;\n case 'number':\n if (val > spec.max) throw new Error(`Number must not be > ${spec.max}, got: ${val}.`)\n if (val < spec.min) throw new Error(`Number must not be < ${spec.min}, got: ${val}.`)\n break;\n case 'object':\n // recurse via `parameters` key\n if (typeof spec.parameters === 'object')\n assertParams(val, spec.parameters)\n break;\n\n /* -- fake primitives -- */\n case 'array':\n if (!Array.isArray(val)) throw new Error(`Expected array but ${val} didn't pass the \"Array.isArray\" check.`)\n break;\n // case 'map':\n // break;\n default:\n // do nothing, we consider it valid if any other string is provided such as bigint\n }\n\n return val;\n}\n\n/**\n * Validate and coerce an \"amount\" value\n * @param {string | number | BigNumber} amount - Value to process\n * @return {BigNumber}\n */\nfunction validateAmount(amount) {\n return new BigNumber(amount);\n}\n\nfunction validateSemver(val) {\n if (semver.valid(val)) {\n return val;\n }\n\n throw new TypeError('Invalid semver format');\n}\n\nfunction validateDate(value) {\n return new Date(value);\n}\n\nObject.assign(module.exports, {\n assertParams,\n assertParamPassesSpec,\n validatePrimitive,\n validateAmount,\n validateDate,\n});\n\nexports.assertTriggersDebugger = !!__webpack_require__(/*! process */ \"./node_modules/process/browser.js\").env.DCP_ASSERT_TRIGGERS_DEBUGGER; /* sync: dcp-assert.js */\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../node_modules/process/browser.js */ \"./node_modules/process/browser.js\")))\n\n//# sourceURL=webpack:///./src/utils/assert-params.js?");
5696
5708
 
5697
5709
  /***/ }),
5698
5710
 
@@ -5749,7 +5761,7 @@ eval("/**\n * @file fetch-keystore.js\n * Utility code to fe
5749
5761
  /***/ (function(module, exports, __webpack_require__) {
5750
5762
 
5751
5763
  "use strict";
5752
- eval("/* WEBPACK VAR INJECTION */(function(global) {/**\n * @file fetch-uri.js\n * @author Nazila Akhavan <nazila@kingsds.network>, Wes Garland <wes@kingsds.network>\n * @date Sep 2020, Nov 2020\n *\n * Fetch URLs/ Data that is stored in the database.\n * Bootstrap some our own needs via custom MIME Types in data URLs.\n */\n\n\n/* global dcpConfig atob:writable */\n\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.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\nconst { justFetch } = __webpack_require__(/*! ./just-fetch */ \"./src/utils/just-fetch.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\n\n/** @typedef {import('dcp/common/dcp-url').DcpURL} DcpURL */\n\n/**\n * Fetch the data at a given URI and return it; data: URIs are decoded directly,\n * http or https URIs are resolved by GET queries. No data will be fetched\n * unless the origin is null, matches dcpConfig.global.allowFetchOrigins, or\n * allowedOrigins.\n *\n * @param {URL | DcpURL | string} uri - Path of the\n * file to be fetched\n * @param {string[][]} _allowedOrigins - One more arrays of URL origins that are\n * allowed. data: URIs are always allowed.\n * @returns Promise which resolves to the data given by the URI\n */\nexports.fetchURI = async function fetchURI(uri, ..._allowedOrigins) {\n const url = typeof uri === 'string' ? new URL(uri) : uri;\n let data;\n\n if (url.protocol === 'data:') {\n data = exports.parseDataURI(uri);\n } else {\n /* Implementation designed to avoid GC churn on many array concats */\n\n let allowed = false;\n\n for (let i = 1; i < arguments.length; i++) {\n const allowedOrigins = arguments[i];\n if (!Array.isArray(allowedOrigins)) {\n throw new Error('Allowed Origins are not all arrays');\n }\n if (allowedOrigins.indexOf(url.origin) !== -1) {\n allowed = true;\n break;\n }\n }\n\n if (!allowed) {\n throw new Error(`Not allowed to fetch from the origin '${url.origin}'`);\n }\n\n data = await justFetch(url, 'string', 'GET');\n }\n\n return data;\n}\n\n/**\n * Instanciate an object based on a data: uri which has properties matching\n * the parameter attributes of the uri.\n *\n * Properties:\n * - contentType: boxed string which is the content type (eg 'image/png')\n * - contentType.major: string which is the major part of the content type (eg 'image')\n * - contentType.minor: string which is the minor part of the content type (eg 'png')\n * - length: the length of the media type section of the data: URI\n * - parameters: object which holds any parameters which were specified in\n * the URI; keys are lowercased attributes.\n *\n * @note The parameters.charset property is treated a little differently to make\n * it easier on API consumers; it is *always* defined, and lower case.\n * If the charset was not specified, it is false.\n */ \nfunction MediaType(uri) {\n var mtArr;\n var mtStr = /(^data:)([^,]*)/.exec(uri)[2];\n\n assert(uri.startsWith('data:'));\n this.length = mtStr.length;\n if (mtStr === '')\n mtStr = 'text/plain;charset=US-ASCII';\n\n mtArr = mtStr.split(';');\n this.contentType = new String(mtArr.shift());\n [ this.contentType.major, this.contentType.minor ] = this.contentType.split('/');\n \n this.parameters = {}\n for (let parameter of mtArr) {\n let [ attribute, value ] = parameter.split('=');\n if (!value)\n value = true;\n this.parameters[attribute.toLowerCase()] = value;\n }\n\n if (typeof this.parameters.charset === 'undefined')\n this.parameters.charset = false;\n else\n this.parameters.charset = this.parameters.charset.toLowerCase();\n}\n\n/**\n * Parse a data: URI, returning the JavaScript value it encodes. The return type is selected\n * based on the content-type.\n *\n * <pre>\n * MIME Type Return Type\n * ------------------ ----------------------------------------------------------------------------------------------------\n * text/plain or none string primitive\n * text/* A boxed string with the contentType property set, and the charset property set if it was specified.\n * application/json whatever JSON.parse returns on the data when it is treated as a string\n * application/x-kvin whatever kvin.deserialze returns on the data when it is treated as a string\n * image/* A Uint8Array of the decoded contents with the contentType property set.\n * * A Uint8Array of the decoded contents with the contentType property set and the charset property \n * set if it was specified.\n * </pre>\n *\n * @param {string} uri the URI\n */\nexports.parseDataURI = function(uri) {\n if (typeof atob !== 'function') {\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n // eslint-disable-next-line no-global-assign\n global.atob = requireNative('atob');\n }\n\n var mediaType = new MediaType(uri);\n var data = uri.slice(5 + mediaType.length + 1); /* data: mediaType comma */\n\n switch(mediaType.parameters.charset)\n {\n default: {\n if (mediaType.contenType.major === 'text')\n throw new Error(`Character set ${mediaType.parameters.charset} not supported`);\n }\n case false:\n case 'utf8':\n case 'us-ascii': {\n data = mediaType.parameters.base64 ? atob(data) : decodeURI(data);\n break;\n }\n }\n \n if (mediaType.contentType == 'text/plain')\n return data;\n\n if (mediaType.contentType.major === 'text') {\n let bso = new String(data);\n if (mediaType.parameters.charset)\n bso.charset = mediaType.parameters.charset;\n return bso;\n }\n\n if (mediaType.contentType == 'application/json')\n return JSON.parse(data);\n\n if (mediaType.contentType == 'application/x-kvin')\n return scopedKvin.deserialize(data);\n\n if (mediaType.contentType.major === 'image') {\n let ui8 = Uint8Array.from(data, c => c.charCodeAt(0));\n ui8.contentType = mediaType.contentType;\n }\n\n const ui8 = Uint8Array.from(data, c => c.charCodeAt(0));\n ui8.contentType = mediaType.contentType.toString();\n if (mediaType.parameters.charset)\n ui8.charset = charset;\n return ui8;\n};\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../node_modules/webpack/buildin/global.js */ \"./node_modules/webpack/buildin/global.js\")))\n\n//# sourceURL=webpack:///./src/utils/fetch-uri.js?");
5764
+ eval("/* WEBPACK VAR INJECTION */(function(global) {/**\n * @file fetch-uri.js\n * @author Nazila Akhavan <nazila@kingsds.network>, Wes Garland <wes@kingsds.network>\n * @date Sep 2020, Nov 2020\n *\n * Fetch URLs/ Data that is stored in the database.\n * Bootstrap some our own needs via custom MIME Types in data URLs.\n */\n\n\n/* global dcpConfig atob:writable */\n\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.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\nconst { justFetch } = __webpack_require__(/*! ./just-fetch */ \"./src/utils/just-fetch.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\n\n/** @typedef {import('dcp/common/dcp-url').DcpURL} DcpURL */\n\n/**\n * Fetch the data at a given URI and return it; data: URIs are decoded directly,\n * http or https URIs are resolved by GET queries. No data will be fetched\n * unless the origin is null, matches dcpConfig.global.allowFetchOrigins, or\n * allowedOrigins.\n *\n * @param {URL | DcpURL | string} uri - Path of the\n * file to be fetched\n * @param {string[][]} _allowedOrigins - One more arrays of URL origins that are\n * allowed. data: URIs are always allowed.\n * @returns Promise which resolves to the data given by the URI\n */\nexports.fetchURI = async function fetchURI(uri, ..._allowedOrigins) {\n const url = typeof uri === 'string' ? new URL(uri) : uri;\n let data;\n\n if (url.protocol === 'data:') {\n data = exports.parseDataURI(uri);\n } else {\n /* Implementation designed to avoid GC churn on many array concats */\n\n let allowed = false;\n\n for (let i = 1; i < arguments.length; i++) {\n const allowedOrigins = arguments[i];\n if (!Array.isArray(allowedOrigins)) {\n throw new Error('Allowed Origins are not all arrays');\n }\n if (allowedOrigins.indexOf(url.origin) !== -1) {\n allowed = true;\n break;\n }\n }\n\n if (!allowed) {\n let e = new DCPError(`Not allowed to fetch from the origin '${url.origin}'.\n If fetching from a remote data set, note that workers must be allowed to fetch data from remote URLs.\n For more information on how to implement these permissions, please visit https://docs.dcp.dev/advanced/data-uri.html#worker`\n ,'EFETCH');\n \n throw e;\n }\n\n data = await justFetch(url, 'string', 'GET');\n }\n\n return data;\n}\n\n/**\n * Instanciate an object based on a data: uri which has properties matching\n * the parameter attributes of the uri.\n *\n * Properties:\n * - contentType: boxed string which is the content type (eg 'image/png')\n * - contentType.major: string which is the major part of the content type (eg 'image')\n * - contentType.minor: string which is the minor part of the content type (eg 'png')\n * - length: the length of the media type section of the data: URI\n * - parameters: object which holds any parameters which were specified in\n * the URI; keys are lowercased attributes.\n *\n * @note The parameters.charset property is treated a little differently to make\n * it easier on API consumers; it is *always* defined, and lower case.\n * If the charset was not specified, it is false.\n */ \nfunction MediaType(uri) {\n var mtArr;\n var mtStr = /(^data:)([^,]*)/.exec(uri)[2];\n\n assert(uri.startsWith('data:'));\n this.length = mtStr.length;\n if (mtStr === '')\n mtStr = 'text/plain;charset=US-ASCII';\n\n mtArr = mtStr.split(';');\n this.contentType = new String(mtArr.shift());\n [ this.contentType.major, this.contentType.minor ] = this.contentType.split('/');\n \n this.parameters = {}\n for (let parameter of mtArr) {\n let [ attribute, value ] = parameter.split('=');\n if (!value)\n value = true;\n this.parameters[attribute.toLowerCase()] = value;\n }\n\n if (typeof this.parameters.charset === 'undefined')\n this.parameters.charset = false;\n else\n this.parameters.charset = this.parameters.charset.toLowerCase();\n}\n\n/**\n * Parse a data: URI, returning the JavaScript value it encodes. The return type is selected\n * based on the content-type.\n *\n * <pre>\n * MIME Type Return Type\n * ------------------ ----------------------------------------------------------------------------------------------------\n * text/plain or none string primitive\n * text/* A boxed string with the contentType property set, and the charset property set if it was specified.\n * application/json whatever JSON.parse returns on the data when it is treated as a string\n * application/x-kvin whatever kvin.deserialze returns on the data when it is treated as a string\n * image/* A Uint8Array of the decoded contents with the contentType property set.\n * * A Uint8Array of the decoded contents with the contentType property set and the charset property \n * set if it was specified.\n * </pre>\n *\n * @param {string} uri the URI\n */\nexports.parseDataURI = function(uri) {\n if (typeof atob !== 'function') {\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n // eslint-disable-next-line no-global-assign\n global.atob = requireNative('atob');\n }\n\n var mediaType = new MediaType(uri);\n var data = uri.slice(5 + mediaType.length + 1); /* data: mediaType comma */\n\n switch(mediaType.parameters.charset)\n {\n default: {\n if (mediaType.contenType.major === 'text')\n throw new Error(`Character set ${mediaType.parameters.charset} not supported`);\n }\n case false:\n case 'utf8':\n case 'us-ascii': {\n data = mediaType.parameters.base64 ? atob(data) : decodeURI(data);\n break;\n }\n }\n \n if (mediaType.contentType == 'text/plain')\n return data;\n\n if (mediaType.contentType.major === 'text') {\n let bso = new String(data);\n if (mediaType.parameters.charset)\n bso.charset = mediaType.parameters.charset;\n return bso;\n }\n\n if (mediaType.contentType == 'application/json')\n return JSON.parse(data);\n\n if (mediaType.contentType == 'application/x-kvin')\n return scopedKvin.deserialize(data);\n\n if (mediaType.contentType.major === 'image') {\n let ui8 = Uint8Array.from(data, c => c.charCodeAt(0));\n ui8.contentType = mediaType.contentType;\n }\n\n const ui8 = Uint8Array.from(data, c => c.charCodeAt(0));\n ui8.contentType = mediaType.contentType.toString();\n if (mediaType.parameters.charset)\n ui8.charset = charset;\n return ui8;\n};\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../node_modules/webpack/buildin/global.js */ \"./node_modules/webpack/buildin/global.js\")))\n\n//# sourceURL=webpack:///./src/utils/fetch-uri.js?");
5753
5765
 
5754
5766
  /***/ }),
5755
5767
 
@@ -5771,7 +5783,8 @@ eval("/**\n * @file utils/http.js Helper module for things rel
5771
5783
  /*! no static exports found */
5772
5784
  /***/ (function(module, exports, __webpack_require__) {
5773
5785
 
5774
- eval("/* WEBPACK VAR INJECTION */(function(process) {/**\n * @file src/utils/index.js\n * @author Ryan Rossiter\n * @date Feb 2020\n *\n * Place to put little JS utilities. If they are more than a few lines, please `require` and `export` \n * them instead of making this monolithic.\n */\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n\n module.exports.paramUtils = __webpack_require__(/*! ./assert-params */ \"./src/utils/assert-params.js\");\n module.exports.httpUtils = __webpack_require__(/*! ./http */ \"./src/utils/http.js\");\n module.exports.serialize = __webpack_require__(/*! ./serialize */ \"./src/utils/serialize.js\");\n Object.assign(exports, __webpack_require__(/*! ./web-format-date */ \"./src/utils/web-format-date.js\"));\n Object.assign(exports, __webpack_require__(/*! ./confirm-prompt */ \"./src/utils/confirm-prompt.js\"));\n Object.assign(exports, __webpack_require__(/*! ./message-to-buffer */ \"./src/utils/message-to-buffer.js\"));\n\n/** @typedef {import('dcp/dcp-client/worker/slice').Slice} Slice */\n/** @typedef {import('dcp/dcp-client/worker/sandbox').Sandbox} Sandbox */\n\n /**\n * Writes object properties into another object matching shape.\n * For example: if obj = { a: { b: 1, c: 2}}\n * and source = { a: {c: 0, d: 1}}\n * then obj will be updated to { a: { b: 1, c: 0, d: 1 } }\n * compare this to Object.assign which gives { a: { c: 0, d: 1 } }\n */\nconst setObjProps = module.exports.setObjProps = (obj, source) => {\n for (let p in source) {\n if (typeof source[p] === 'object') setObjProps(obj[p], source[p]);\n else obj[p] = source[p];\n }\n}\n\n\n/**\n * Generates a new random opaqueId i.e. a 22-character base64 string.\n * Used for job and slice ids.\n */\nmodule.exports.generateOpaqueId = function () {\n const generate = __webpack_require__(/*! nanoid/generate */ \"./node_modules/nanoid/generate.js\");\n return generate('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+', 22);\n}\n\n/**\n * Accepts an object and a list of properties to apply to the object to retreive a final value.\n * E.g.\n * object = { a: { b: { c: 4 } } }\n * listOfProperties = [\"a\", \"b\", \"c\"]\n * return = 4\n */\nmodule.exports.getNestedFromListOfProperties = function (object, listOfProperties) {\n return listOfProperties.reduce((o, k) => {\n if (typeof o !== 'object') return undefined;\n return o[k];\n }, object);\n}\n\n/**\n * Accepts an object and a dot-separated property name to retrieve from the object.\n * E.g.\n * object = { a: { b: { c: 4 } } }\n * property = 'a.b.c'\n * return = 4\n *\n * @param {object} object \n * @param {string} property \n */\nmodule.exports.getNestedProperty = function (object, property) {\n if (typeof property !== 'string') return undefined;\n return module.exports.getNestedFromListOfProperties(object, property.split(\".\"));\n}\n\n/**\n * Accepts an object and a list of properties and a value and mutates the object\n * to have the specified value at the location denoted by the list of properties.\n * Similar to getNestedFromListOfProperties, but sets instead of gets.\n */\nmodule.exports.setNestedFromListOfProperties = function (object, listOfProperties, value) {\n if(!listOfProperties.length || listOfProperties.length < 1) {\n throw new Error(\"listOfProperties must be an array of length >= 1\");\n }\n const indexOfLastProp = listOfProperties.length - 1;\n const pathToParent = listOfProperties.slice(0, indexOfLastProp);\n const parent = module.exports.getNestedFromListOfProperties(object, pathToParent);\n if(!parent) {\n throw new Error(\"Could not find value at:\", pathToParent, \"in object:\", object);\n }\n const lastProperty = listOfProperties[indexOfLastProp];\n parent[lastProperty] = value;\n}\n\n/**\n * Block the event loop for a specified time\n * @milliseconds the number of milliseconds to wait (integer)\n */\nexports.msleep = function utils$$msleep(milliseconds) {\n try\n {\n let sab = new SharedArrayBuffer(4);\n let int32 = new Int32Array(sab);\n Atomics.wait(int32, 0, 0, milliseconds);\n }\n catch(error)\n {\n console.error('Cannot msleep;', error);\n }\n}\n\n/**\n * Block the event loop for a specified time\n * @seconds the number of seconds to wait (float)\n */\nexports.sleep = function utils$$sleep(seconds) {\n return exports.msleep(seconds * 1000);\n}\n\n/** Resolve a promise after a specified time */\nexports.asleepMs = function asleepMs(ms) {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\n/** Resolve a promise after a specified time */\nexports.asleep = function asleep(seconds) {\n return new Promise(resolve => setTimeout(resolve, seconds * 1000));\n}\n\n/** \n * Returns the number of millisecond in a time expression.\n * @param s {number} The number of seconds\n * @returns {number}\n */\n/**\n * @param s {string} A complex time expression using m, w, d, s, h. '10d 6h 1s' means 10 days, 6 hours, and 1 second.\n */\nexports.ms = function utils$$ms(s)\n{\n let ms = 0;\n \n if (typeof s === 'number')\n return s * 1000;\n\n assert(typeof s === 'string');\n \n for (let expr of s.match(/[0-9.]+[smhdw]/g))\n {\n let unit = expr.slice(-1);\n let value = +expr.slice(0, -1);\n\n switch(unit)\n {\n case 's': {\n ms += value * 1000;\n break;\n }\n case 'm': {\n ms += value * 1000 * 60;\n break;\n }\n case 'h': {\n ms += value * 1000 * 60 * 60;\n break;\n }\n case 'd': {\n ms = value * 1000 * 60 * 60 * 24;\n break;\n }\n case 'w': {\n ms = value * 1000 * 60 * 60 * 24 * 7;\n break;\n }\n default: {\n throw new Error(`invalid time unit ${unit}`);\n }\n }\n }\n\n return ms;\n}\n\n/**\n * Returns a percentage as a number.\n * @param n {number} this number is returned\n */\n/**\n * @param n {string} this string is converted to a number and returned; it is divided by 100 if it ends in %.\n */\nexports.pct = function utils$$pct(n)\n{\n if (typeof n === 'number')\n return n / 100;\n\n if (n.match(/%$/))\n return +n / 100;\n\n return +n;\n}\n\n/**\n * Coerce human-written or registry-provided values into Boolean in a sensisble way.\n */\nexports.booley = function utils$$booley(check)\n{\n switch (typeof check)\n {\n case 'undefined':\n return false;\n case 'string':\n return check && (check !== 'false');\n case 'boolean':\n return check;\n case 'number':\n case 'object':\n return Boolean(check);\n default:\n throw new Error(`can't coerce ${typeof check} to booley`);\n }\n}\n\ntry {\n exports.useChalk = requireNative ? requireNative('tty').isatty(0) || process.env.FORCE_COLOR : false;\n}\ncatch (error) {\n if (error.message.includes('no native require'))\n exports.useChalk = false;\n else\n throw error;\n}\n\n/** \n * Factory function which constructs an error message relating to version mismatches\n * @param {string} oldThingLabel The name of the thing that is too old\n * @param {string} upgradeThingLabel [optional] The name of the thing that needs to be upgraded to fix that; if\n * unspecified, we use oldThingLabel.\n * @param {string} newThingLabel What the thing is that is that is newer than oldThingLabel\n * @param {string} minimumVersion The minimum version of the old thing that the new thing supports. \n * Obstensibibly semver, but unparsed.\n * @param {string} code [optional] A code property to add to the return value\n *\n * @returns instance of Error\n */\nexports.versionError = function dcpClient$$versionError(oldThingLabel, upgradeThingLabel, newThingLabel, minimumVersion, code)\n{\n function repeat(what, len) {\n let out = '';\n while (len--)\n out += what;\n return out;\n }\n\n function bold(string) {\n if (__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").platform === 'nodejs') {\n const chalk = new requireNative('chalk').constructor({enabled: exports.useChalk});\n\n return chalk.yellow(string);\n }\n\n return string;\n }\n \n var mainMessage = `${oldThingLabel} is too old; ${newThingLabel} needs version ${minimumVersion}`;\n var upgradeMessage = bold(`Please upgrade ${upgradeThingLabel || oldThingLabel}${repeat(\" \", mainMessage.length - 15 - upgradeThingLabel.length)}`);\n var error = new Error(`\n****${repeat('*', mainMessage.length)}****\n*** ${repeat(' ', mainMessage.length)} ***\n*** ${mainMessage} ***\n*** ${upgradeMessage} ***\n*** ${repeat(' ', mainMessage.length)} ***\n****${repeat('*', mainMessage.length)}****\n`);\n\n if (code)\n error.code = code;\n return error;\n}\n\n/*********************************************************************************************/\n\n//\n// Sandbox and Slice debugging and logging tools.\n//\n\n/**\n * Previous short form perf time stamp.\n * #Private\n * @type {Date}\n */\nlet previousTime = new Date();\n\n/**\n * Short form perf time format with timespan diff from last call.\n * @returns {string}\n */\nexports.shortTime = function util$$shortTime() {\n const currentTime = new Date();\n const diff = currentTime.getTime() - previousTime.getTime();\n previousTime = currentTime;\n return `diff=${diff}: ${currentTime.getMinutes()}.${currentTime.getSeconds()}.${currentTime.getMilliseconds()}`;\n}\n\n/**\n * Log message for duplicate slice.\n * @parameter {Slice} slice\n * @parameter {function} log\n */\nexports.duplicateSlice = function utils$$duplicateSlice(slice, log) {\n log ? log(slice) : console.log(`\\tDANGER: Found duplicate slice ${slice.sliceNumber}.${slice.jobAddress}.${slice.status}.`);\n}\n\n/**\n * Log message for duplicate sandbox.\n * @parameter {Sandbox} sandbox\n * @parameter {function} log\n */\nexports.duplicateSandbox = function utils$$duplicateSandbox(sandbox, log) {\n log ? log(sandbox) : console.log(`\\tDANGER: Found duplicate sandbox ${sandbox.id}.${sandbox.jobAddress}.${sandbox.state}.`);\n}\n\n/**\n * Feel free to customize this function to your needs.\n * Log theArray.\n * @parameter {Array<*>} theArray\n * @parameter {string} header\n */\nexports.dumpArray = function utils$$dumpArray(theArray, header, log, headerLog) {\n if (header) headerLog ? headerLog(`\\n${header}`) : console.log(`\\n${header}`);\n theArray.forEach(x => log ? log(x) : console.log(`\\t${JSON.stringify(x)}.`));\n}\n\n/**\n * Log sliceArray.\n * @parameter {Slice[]} sliceArray\n * @parameter {string} header\n */\nexports.dumpSlices = function utils$$dumpSlices(sliceArray, header) {\n if (header) console.log(`\\n${header}`);\n sliceArray.forEach(x => console.log(`\\t${x.sliceNumber}.${x.jobAddress}.${x.status}`));\n}\n\n/**\n * Log sandboxArray.\n * @parameter {Sandbox[]} sandboxArray\n * @parameter {string} header\n */\nexports.dumpSandboxes = function utils$$dumpSandboxes(sandboxArray, header) {\n if (header) console.log(`\\n${header}`);\n sandboxArray.forEach(x => console.log(`\\t${x.id}.${x.jobAddress}.${x.state}`));\n}\n\n/**\n * Feel free to customize this function to your needs.\n * If the elements of theArray are not unique, log the duplicates and log the full array.\n * @parameter {Array<*>} theArray\n * @parameter {string} [header]\n * @parameter {function} [log]\n */\nexports.dumpIfNotUnique = function utils$$dumpIfNotUnique(theArray, header, log, headerLog) {\n if (!exports.isUnique(theArray, header, log, headerLog))\n exports.dumpArray(theArray, undefined, log);\n}\n\n/**\n * If the elements of sliceArray are not unique, log the duplicates and log the full array.\n * @parameter {Slice[]} sliceArray\n * @parameter {string} header\n */\nexports.dumpSlicesIfNotUnique = function utils$$dumpSlicesIfNotUnique(sliceArray, header) {\n if (!exports.isUniqueSlices(sliceArray, header))\n exports.dumpSlices(sliceArray);\n}\n\n/**\n * If the elements of sandboxArray are not unique, log the duplicates and log the full array.\n * @parameter {Sandbox[]} sandboxArray\n * @parameter {string} header\n */\nexports.dumpSandboxesIfNotUnique = function utils$$dumpSandboxesIfNotUnique(sandboxArray, header) {\n if (!exports.isUniqueSandboxes(sandboxArray, header))\n exports.dumpSandboxes(sandboxArray);\n}\n\n/**\n * Feel free to customize this function to your needs.\n * Checks whether the elements of theArray are unique and if not, log the duplicates.\n * @parameter {Array<*>} theArray\n * @parameter {function} log\n * @returns {boolean}\n */\nexports.isUnique = function utils$$isUnique(theArray, header, log, headerLog) {\n return theArray.length === exports.makeUnique(theArray,\n header,\n x => { log ? log(x) : console.log(`\\tisUnique: Found duplicate ${JSON.stringify(x)}.`); },\n headerLog);\n}\n\n/**\n * Checks whether the elements of sliceArray are unique and if not, log the duplicates.\n * @parameter {Slice[]} sliceArray\n * @parameter {string} header\n * @parameter {function} log\n * @returns {boolean}\n */\nexports.isUniqueSlices = function utils$$isUniqueSlices(sliceArray, header, log) {\n return sliceArray.length === exports.makeUniqueSlices(sliceArray, header, log).length;\n}\n\n/**\n * Checks whether the elements of sandboxArray are unique and if not, log the duplicates.\n * @parameter {Sandbox[]} sandboxArray\n * @parameter {string} header\n * @parameter {function} log\n * @returns {boolean}\n */\nexports.isUniqueSandboxes = function utils$$isUniqueSandboxes(sandboxArray, header, log) {\n return sandboxArray.length === exports.makeUniqueSandboxes(sandboxArray, header, log).length;\n}\n\n/**\n * Feel free to customize this function to your needs.\n * Returns a copy of theArray with all duplicates removed, while logging the duplicates.\n * @parameter {Array<*>} theArray\n * @parameter {function} log\n * @returns {Array<*>}\n */\nexports.makeUnique = function utils$$makeUnique(theArray, header, log, headerLog) {\n const result = [];\n let once = true;\n theArray.forEach(x => {\n if (result.indexOf(x) >= 0) {\n if (once && header) headerLog ? headerLog(`\\n${header}`) : console.log(`\\n${header}`); once = false;\n log ? log(x) : console.log(`\\tmakeUnique: Found duplicate ${JSON.stringify(x)}.`);\n } else result.push(x);\n });\n return result;\n}\n\n/**\n * Returns a copy of sliceArray with all duplicates removed, while logging the duplicates.\n * @parameter {Slice[]} sliceArray\n * @parameter {string} header\n * @parameter {function} log\n * @returns {Slice[]}\n */\nexports.makeUniqueSlices = function utils$$makeUniqueSlices(sliceArray, header, log) {\n const slices = [];\n let once = true;\n sliceArray.forEach(x => {\n if (slices.indexOf(x) >= 0) {\n if (once && header) console.log(`\\n${header}`); once = false;\n exports.duplicateSlice(x, log);\n } else slices.push(x);\n });\n return slices;\n}\n\n/**\n * Returns a copy of sandboxArray with all duplicates removed, while logging the duplicates.\n * @parameter {Sandbox[]} sandboxArray\n * @parameter {string} header\n * @parameter {function} log\n * @returns {Sandbox[]}\n */\nexports.makeUniqueSandboxes = function utils$$makeUniqueSandboxes(sandboxArray, header, log) {\n const sandboxes = [];\n let once = true;\n sandboxArray.forEach(x => {\n if (sandboxes.indexOf(x) >= 0) {\n if (once && header) console.log(`\\n${header}`); once = false;\n exports.duplicateSandbox(x, log);\n } else sandboxes.push(x);\n });\n return sandboxes;\n}\n\n/**\n * Calls truncated JSON.stringify on theAnything.\n * @param {object} theAnything\n * @param {string} header\n * @param {number} valueLength\n */\nexports.dumpJSON = function utils$$dumpJSON(theAnything, header = 'dumpJSON: ', valueLength = 128) {\n if (theAnything) {\n const strV = String(JSON.stringify(theAnything)).slice(0, valueLength);\n console.log(`${header}, ${strV}`);\n } else {\n console.log(`${header}, undefined or null`);\n }\n}\n\n/**\n * Iterates over all property [key, value]-pairs of theObject and call truncated JSON.stringify on property values.\n * @param {object} theObject\n * @param {string} header\n * @param {number} valueLength\n */\nexports.dumpObject = function utils$$dumpObject(theObject , header = 'dumpObject: ', valueLength = 128) {\n for (const [key, value] of Object.entries(theObject)) {\n if (value) {\n const strV = String(JSON.stringify(value)).slice(0, valueLength);\n console.log(`${header} property ${key}, value ${strV}`);\n } else {\n console.log(`${header} property ${key}, value undefined or null`);\n }\n }\n}\n\n/*********************************************************************************************/\n\nexports.shuffle = function utils$$shuffle(array) {\n var currentIndex = array.length, randomIndex;\n\n // While there remain elements to shuffle...\n while (0 !== currentIndex) {\n\n // Pick a remaining element...\n randomIndex = Math.floor(Math.random() * currentIndex);\n currentIndex--;\n\n // And swap it with the current element.\n [array[currentIndex], array[randomIndex]] = [\n array[randomIndex], array[currentIndex]];\n }\n\n return array;\n}\n\n/*********************************************************************************************/\n\nif (__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").platform === 'nodejs') {\n // Assigning the properties explicitly for VSCode intellisense.\n const tmpFiles = __webpack_require__(/*! ./tmpfiles */ \"./src/utils/tmpfiles.js\");\n exports.catFile = tmpFiles.catFile;\n exports.createTempFile = tmpFiles.createTempFile;\n exports.systemTempDir = tmpFiles.systemTempDir;\n\n Object.assign(exports, __webpack_require__(/*! ./readln */ \"./src/utils/readln.js\"));\n}\nObject.assign(exports, __webpack_require__(/*! ./sh */ \"./src/utils/sh.js\"));\nObject.assign(exports, __webpack_require__(/*! ./eventUtils */ \"./src/utils/eventUtils.js\"));\nObject.assign(exports, __webpack_require__(/*! ./obj-merge */ \"./src/utils/obj-merge.js\"));\nObject.assign(exports, __webpack_require__(/*! ./make-slice-uri */ \"./src/utils/make-slice-uri.js\"));\nObject.assign(exports, __webpack_require__(/*! ./just-fetch */ \"./src/utils/just-fetch.js\"));\nObject.assign(exports, __webpack_require__(/*! ./inventory */ \"./src/utils/inventory.js\"));\nObject.assign(exports, __webpack_require__(/*! ./fetch-keystore */ \"./src/utils/fetch-keystore.js\"));\n\nmodule.exports.fetchURI = __webpack_require__(/*! ./fetch-uri */ \"./src/utils/fetch-uri.js\").fetchURI;\nmodule.exports.encodeDataURI = __webpack_require__(/*! ./encodeDataURI */ \"./src/utils/encodeDataURI.js\").encodeDataURI;\n\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../node_modules/process/browser.js */ \"./node_modules/process/browser.js\")))\n\n//# sourceURL=webpack:///./src/utils/index.js?");
5786
+ "use strict";
5787
+ eval("/* WEBPACK VAR INJECTION */(function(process) {/**\n * @file src/utils/index.js\n * @author Ryan Rossiter\n * @date Feb 2020\n *\n * Place to put little JS utilities. If they are more than a few lines, please `require` and `export` \n * them instead of making this monolithic.\n */\n\n\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n\n module.exports.paramUtils = __webpack_require__(/*! ./assert-params */ \"./src/utils/assert-params.js\");\n module.exports.httpUtils = __webpack_require__(/*! ./http */ \"./src/utils/http.js\");\n module.exports.serialize = __webpack_require__(/*! ./serialize */ \"./src/utils/serialize.js\");\n Object.assign(exports, __webpack_require__(/*! ./web-format-date */ \"./src/utils/web-format-date.js\"));\n Object.assign(exports, __webpack_require__(/*! ./confirm-prompt */ \"./src/utils/confirm-prompt.js\"));\n Object.assign(exports, __webpack_require__(/*! ./message-to-buffer */ \"./src/utils/message-to-buffer.js\"));\n\n/** @typedef {import('dcp/dcp-client/worker/slice').Slice} Slice */\n/** @typedef {import('dcp/dcp-client/worker/sandbox').Sandbox} Sandbox */\n\n /**\n * Writes object properties into another object matching shape.\n * For example: if obj = { a: { b: 1, c: 2}}\n * and source = { a: {c: 0, d: 1}}\n * then obj will be updated to { a: { b: 1, c: 0, d: 1 } }\n * compare this to Object.assign which gives { a: { c: 0, d: 1 } }\n */\nconst setObjProps = module.exports.setObjProps = (obj, source) => {\n for (let p in source) {\n if (typeof source[p] === 'object') setObjProps(obj[p], source[p]);\n else obj[p] = source[p];\n }\n}\n\n\n/**\n * Generates a new random opaqueId i.e. a 22-character base64 string.\n * Used for job and slice ids.\n */\nmodule.exports.generateOpaqueId = function () {\n const generate = __webpack_require__(/*! nanoid/generate */ \"./node_modules/nanoid/generate.js\");\n return generate('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+', 22);\n}\n\n/**\n * Accepts an object and a list of properties to apply to the object to retreive a final value.\n * E.g.\n * object = { a: { b: { c: 4 } } }\n * listOfProperties = [\"a\", \"b\", \"c\"]\n * return = 4\n */\nmodule.exports.getNestedFromListOfProperties = function (object, listOfProperties) {\n return listOfProperties.reduce((o, k) => {\n if (typeof o !== 'object') return undefined;\n return o[k];\n }, object);\n}\n\n/**\n * Accepts an object and a dot-separated property name to retrieve from the object.\n * E.g.\n * object = { a: { b: { c: 4 } } }\n * property = 'a.b.c'\n * return = 4\n *\n * @param {object} object \n * @param {string} property \n */\nmodule.exports.getNestedProperty = function (object, property) {\n if (typeof property !== 'string') return undefined;\n return module.exports.getNestedFromListOfProperties(object, property.split(\".\"));\n}\n\n/**\n * Accepts an object and a list of properties and a value and mutates the object\n * to have the specified value at the location denoted by the list of properties.\n * Similar to getNestedFromListOfProperties, but sets instead of gets.\n */\nmodule.exports.setNestedFromListOfProperties = function (object, listOfProperties, value) {\n if(!listOfProperties.length || listOfProperties.length < 1) {\n throw new Error(\"listOfProperties must be an array of length >= 1\");\n }\n const indexOfLastProp = listOfProperties.length - 1;\n const pathToParent = listOfProperties.slice(0, indexOfLastProp);\n const parent = module.exports.getNestedFromListOfProperties(object, pathToParent);\n if(!parent) {\n throw new Error(\"Could not find value at:\", pathToParent, \"in object:\", object);\n }\n const lastProperty = listOfProperties[indexOfLastProp];\n parent[lastProperty] = value;\n}\n\n/**\n * Block the event loop for a specified time\n * @milliseconds the number of milliseconds to wait (integer)\n */\nexports.msleep = function utils$$msleep(milliseconds) {\n try\n {\n let sab = new SharedArrayBuffer(4);\n let int32 = new Int32Array(sab);\n Atomics.wait(int32, 0, 0, milliseconds);\n }\n catch(error)\n {\n console.error('Cannot msleep;', error);\n }\n}\n\n/**\n * Block the event loop for a specified time\n * @seconds the number of seconds to wait (float)\n */\nexports.sleep = function utils$$sleep(seconds) {\n return exports.msleep(seconds * 1000);\n}\n\n/** Resolve a promise after a specified time */\nexports.asleepMs = function asleepMs(ms) {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\n/** Resolve a promise after a specified time */\nexports.asleep = function asleep(seconds) {\n return new Promise(resolve => setTimeout(resolve, seconds * 1000));\n}\n\n/** \n * Returns the number of millisecond in a time expression.\n * @param s {number} The number of seconds\n * @returns {number}\n */\n/**\n * @param s {string} A complex time expression using m, w, d, s, h. '10d 6h 1s' means 10 days, 6 hours, and 1 second.\n */\nexports.ms = function utils$$ms(s)\n{\n let ms = 0;\n \n if (typeof s === 'number')\n return s * 1000;\n\n assert(typeof s === 'string');\n \n for (let expr of s.match(/[0-9.]+[smhdw]/g))\n {\n let unit = expr.slice(-1);\n let value = +expr.slice(0, -1);\n\n switch(unit)\n {\n case 's': {\n ms += value * 1000;\n break;\n }\n case 'm': {\n ms += value * 1000 * 60;\n break;\n }\n case 'h': {\n ms += value * 1000 * 60 * 60;\n break;\n }\n case 'd': {\n ms = value * 1000 * 60 * 60 * 24;\n break;\n }\n case 'w': {\n ms = value * 1000 * 60 * 60 * 24 * 7;\n break;\n }\n default: {\n throw new Error(`invalid time unit ${unit}`);\n }\n }\n }\n\n return ms;\n}\n\n/**\n * Returns a percentage as a number.\n * @param n {number} this number is returned\n */\n/**\n * @param n {string} this string is converted to a number and returned; it is divided by 100 if it ends in %.\n */\nexports.pct = function utils$$pct(n)\n{\n if (typeof n === 'number')\n return n / 100;\n\n if (n.match(/%$/))\n return +n / 100;\n\n return +n;\n}\n\n/**\n * Coerce human-written or registry-provided values into Boolean in a sensisble way.\n */\nexports.booley = function utils$$booley(check)\n{\n switch (typeof check)\n {\n case 'undefined':\n return false;\n case 'string':\n return check && (check !== 'false');\n case 'boolean':\n return check;\n case 'number':\n case 'object':\n return Boolean(check);\n default:\n throw new Error(`can't coerce ${typeof check} to booley`);\n }\n}\n\ntry {\n exports.useChalk = requireNative ? requireNative('tty').isatty(0) || process.env.FORCE_COLOR : false;\n}\ncatch (error) {\n if (error.message.includes('no native require'))\n exports.useChalk = false;\n else\n throw error;\n}\n\n/** \n * Factory function which constructs an error message relating to version mismatches\n * @param {string} oldThingLabel The name of the thing that is too old\n * @param {string} upgradeThingLabel [optional] The name of the thing that needs to be upgraded to fix that; if\n * unspecified, we use oldThingLabel.\n * @param {string} newThingLabel What the thing is that is that is newer than oldThingLabel\n * @param {string} minimumVersion The minimum version of the old thing that the new thing supports. \n * Obstensibibly semver, but unparsed.\n * @param {string} code [optional] A code property to add to the return value\n *\n * @returns instance of Error\n */\nexports.versionError = function dcpClient$$versionError(oldThingLabel, upgradeThingLabel, newThingLabel, minimumVersion, code)\n{\n function repeat(what, len) {\n let out = '';\n while (len--)\n out += what;\n return out;\n }\n\n function bold(string) {\n if (__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").platform === 'nodejs') {\n const chalk = new requireNative('chalk').constructor({enabled: exports.useChalk});\n\n return chalk.yellow(string);\n }\n\n return string;\n }\n \n var mainMessage = `${oldThingLabel} is too old; ${newThingLabel} needs version ${minimumVersion}`;\n var upgradeMessage = bold(`Please upgrade ${upgradeThingLabel || oldThingLabel}${repeat(\" \", mainMessage.length - 15 - upgradeThingLabel.length)}`);\n var error = new Error(`\n****${repeat('*', mainMessage.length)}****\n*** ${repeat(' ', mainMessage.length)} ***\n*** ${mainMessage} ***\n*** ${upgradeMessage} ***\n*** ${repeat(' ', mainMessage.length)} ***\n****${repeat('*', mainMessage.length)}****\n`);\n\n if (code)\n error.code = code;\n return error;\n}\n\n/*********************************************************************************************/\n\n//\n// Sandbox and Slice debugging and logging tools.\n//\n\n/**\n * Previous short form perf time stamp.\n * #Private\n * @type {Date}\n */\nlet previousTime = new Date();\n\n/**\n * Short form perf time format with timespan diff from last call.\n * @returns {string}\n */\nexports.shortTime = function util$$shortTime() {\n const currentTime = new Date();\n const diff = currentTime.getTime() - previousTime.getTime();\n previousTime = currentTime;\n return `diff=${diff}: ${currentTime.getMinutes()}.${currentTime.getSeconds()}.${currentTime.getMilliseconds()}`;\n}\n\n/**\n * Log message for duplicate slice.\n * @parameter {Slice} slice\n * @parameter {function} log\n */\nexports.duplicateSlice = function utils$$duplicateSlice(slice, log) {\n log ? log(slice) : console.log(`\\tDANGER: Found duplicate slice ${slice.sliceNumber}.${slice.jobAddress}.${slice.status}.`);\n}\n\n/**\n * Log message for duplicate sandbox.\n * @parameter {Sandbox} sandbox\n * @parameter {function} log\n */\nexports.duplicateSandbox = function utils$$duplicateSandbox(sandbox, log) {\n log ? log(sandbox) : console.log(`\\tDANGER: Found duplicate sandbox ${sandbox.id}.${sandbox.jobAddress}.${sandbox.state}.`);\n}\n\n/**\n * Feel free to customize this function to your needs.\n * Log theArray.\n * @parameter {Array<*>} theArray\n * @parameter {string} header\n */\nexports.dumpArray = function utils$$dumpArray(theArray, header, log, headerLog) {\n if (header) headerLog ? headerLog(`\\n${header}`) : console.log(`\\n${header}`);\n theArray.forEach(x => log ? log(x) : console.log(`\\t${JSON.stringify(x)}.`));\n}\n\n/**\n * Log sliceArray.\n * @parameter {Slice[]} sliceArray\n * @parameter {string} header\n */\nexports.dumpSlices = function utils$$dumpSlices(sliceArray, header) {\n if (header) console.log(`\\n${header}`);\n sliceArray.forEach(x => console.log(`\\t${x.sliceNumber}.${x.jobAddress}.${x.status}`));\n}\n\n/**\n * Log sandboxArray.\n * @parameter {Sandbox[]} sandboxArray\n * @parameter {string} header\n */\nexports.dumpSandboxes = function utils$$dumpSandboxes(sandboxArray, header) {\n if (header) console.log(`\\n${header}`);\n sandboxArray.forEach(x => console.log(`\\t${x.id}.${x.jobAddress}.${x.state}`));\n}\n\n/**\n * Feel free to customize this function to your needs.\n * If the elements of theArray are not unique, log the duplicates and log the full array.\n * @parameter {Array<*>} theArray\n * @parameter {string} [header]\n * @parameter {function} [log]\n */\nexports.dumpIfNotUnique = function utils$$dumpIfNotUnique(theArray, header, log, headerLog) {\n if (!exports.isUnique(theArray, header, log, headerLog))\n exports.dumpArray(theArray, undefined, log);\n}\n\n/**\n * If the elements of sliceArray are not unique, log the duplicates and log the full array.\n * @parameter {Slice[]} sliceArray\n * @parameter {string} header\n */\nexports.dumpSlicesIfNotUnique = function utils$$dumpSlicesIfNotUnique(sliceArray, header) {\n if (!exports.isUniqueSlices(sliceArray, header))\n exports.dumpSlices(sliceArray);\n}\n\n/**\n * If the elements of sandboxArray are not unique, log the duplicates and log the full array.\n * @parameter {Sandbox[]} sandboxArray\n * @parameter {string} header\n */\nexports.dumpSandboxesIfNotUnique = function utils$$dumpSandboxesIfNotUnique(sandboxArray, header) {\n if (!exports.isUniqueSandboxes(sandboxArray, header))\n exports.dumpSandboxes(sandboxArray);\n}\n\n/**\n * Feel free to customize this function to your needs.\n * Checks whether the elements of theArray are unique and if not, log the duplicates.\n * @parameter {Array<*>} theArray\n * @parameter {function} log\n * @returns {boolean}\n */\nexports.isUnique = function utils$$isUnique(theArray, header, log, headerLog) {\n return theArray.length === exports.makeUnique(theArray,\n header,\n x => { log ? log(x) : console.log(`\\tisUnique: Found duplicate ${JSON.stringify(x)}.`); },\n headerLog);\n}\n\n/**\n * Checks whether the elements of sliceArray are unique and if not, log the duplicates.\n * @parameter {Slice[]} sliceArray\n * @parameter {string} header\n * @parameter {function} log\n * @returns {boolean}\n */\nexports.isUniqueSlices = function utils$$isUniqueSlices(sliceArray, header, log) {\n return sliceArray.length === exports.makeUniqueSlices(sliceArray, header, log).length;\n}\n\n/**\n * Checks whether the elements of sandboxArray are unique and if not, log the duplicates.\n * @parameter {Sandbox[]} sandboxArray\n * @parameter {string} header\n * @parameter {function} log\n * @returns {boolean}\n */\nexports.isUniqueSandboxes = function utils$$isUniqueSandboxes(sandboxArray, header, log) {\n return sandboxArray.length === exports.makeUniqueSandboxes(sandboxArray, header, log).length;\n}\n\n/**\n * Feel free to customize this function to your needs.\n * Returns a copy of theArray with all duplicates removed, while logging the duplicates.\n * @parameter {Array<*>} theArray\n * @parameter {function} log\n * @returns {Array<*>}\n */\nexports.makeUnique = function utils$$makeUnique(theArray, header, log, headerLog) {\n const result = [];\n let once = true;\n theArray.forEach(x => {\n if (result.indexOf(x) >= 0) {\n if (once && header) headerLog ? headerLog(`\\n${header}`) : console.log(`\\n${header}`); once = false;\n log ? log(x) : console.log(`\\tmakeUnique: Found duplicate ${JSON.stringify(x)}.`);\n } else result.push(x);\n });\n return result;\n}\n\n/**\n * Returns a copy of sliceArray with all duplicates removed, while logging the duplicates.\n * @parameter {Slice[]} sliceArray\n * @parameter {string} header\n * @parameter {function} log\n * @returns {Slice[]}\n */\nexports.makeUniqueSlices = function utils$$makeUniqueSlices(sliceArray, header, log) {\n const slices = [];\n let once = true;\n sliceArray.forEach(x => {\n if (slices.indexOf(x) >= 0) {\n if (once && header) console.log(`\\n${header}`); once = false;\n exports.duplicateSlice(x, log);\n } else slices.push(x);\n });\n return slices;\n}\n\n/**\n * Returns a copy of sandboxArray with all duplicates removed, while logging the duplicates.\n * @parameter {Sandbox[]} sandboxArray\n * @parameter {string} header\n * @parameter {function} log\n * @returns {Sandbox[]}\n */\nexports.makeUniqueSandboxes = function utils$$makeUniqueSandboxes(sandboxArray, header, log) {\n const sandboxes = [];\n let once = true;\n sandboxArray.forEach(x => {\n if (sandboxes.indexOf(x) >= 0) {\n if (once && header) console.log(`\\n${header}`); once = false;\n exports.duplicateSandbox(x, log);\n } else sandboxes.push(x);\n });\n return sandboxes;\n}\n\n/**\n * Quck and dirty JSON serialization that ignores cycles.\n *\n * @param {*} o - entity to be serialized.\n * @param {number} len - number of string elements to return.\n * @returns {string}\n */\n exports.stringify = function _stringify(o, len = 512) {\n if (!o) return 'nada';\n let cache = [];\n const str = JSON.stringify(o, (key, value) => {\n if (typeof value === 'object' && value !== null) {\n if (cache.includes(value)) return;\n cache.push(value);\n }\n return value;\n });\n cache = null;\n return str.slice(0, len);\n}\n\n/**\n * Calls truncated JSON.stringify on theAnything.\n * @param {object} theAnything\n * @param {string} header\n * @param {number} valueLength\n */\nexports.dumpJSON = function utils$$dumpJSON(theAnything, header = 'dumpJSON: ', valueLength = 128) {\n if (theAnything) {\n const strV = String(exports.stringify(theAnything, valueLength));\n console.log(`${header}, ${strV}`);\n } else {\n console.log(`${header}, undefined or null`);\n }\n}\n\n/**\n * Iterates over all property [key, value]-pairs of theObject and call truncated JSON.stringify on property values.\n * @param {object} theObject\n * @param {string} header\n * @param {number} valueLength\n */\nexports.dumpObject = function utils$$dumpObject(theObject , header = 'dumpObject: ', valueLength = 128) {\n for (const [key, value] of Object.entries(theObject)) {\n if (value) {\n const strV = String(exports.stringify(theObject, valueLength));\n console.log(`${header} property ${key}, value ${strV}`);\n } else {\n console.log(`${header} property ${key}, value undefined or null`);\n }\n }\n}\n\n/*********************************************************************************************/\n\nexports.shuffle = function utils$$shuffle(jobDescriptors, partitionPortions) {\n let jobDescriptorsInEstimation = [];\n let partitionPortionsInEstimation = [];\n for (let [index, jobDescriptor] of jobDescriptors.entries()) {\n if(jobDescriptor.job.status === 'estimation') {\n jobDescriptorsInEstimation.push(jobDescriptor);\n jobDescriptors.splice(index, 1);\n partitionPortionsInEstimation.push(partitionPortions[index]);\n partitionPortions.splice(index, 1) \n }\n }\n \n let currentIndex = jobDescriptors.length, randomIndex;\n // While there remain elements to shuffle...\n while (0 !== currentIndex && jobDescriptors.length > 0) {\n\n // Pick a remaining element...\n randomIndex = Math.floor(Math.random() * currentIndex);\n currentIndex--;\n\n // And swap it with the current element.\n [jobDescriptors[currentIndex], jobDescriptors[randomIndex]] = [\n jobDescriptors[randomIndex], jobDescriptors[currentIndex]];\n\n [partitionPortions[currentIndex], partitionPortions[randomIndex]] = [\n partitionPortions[randomIndex], partitionPortions[currentIndex]];\n }\n \n return [\n [...jobDescriptorsInEstimation, ...jobDescriptors],\n [...partitionPortionsInEstimation, ...partitionPortions]\n ]\n}\n/*********************************************************************************************/\n\nif (__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").platform === 'nodejs') {\n // Assigning the properties explicitly for VSCode intellisense.\n const tmpFiles = __webpack_require__(/*! ./tmpfiles */ \"./src/utils/tmpfiles.js\");\n exports.catFile = tmpFiles.catFile;\n exports.createTempFile = tmpFiles.createTempFile;\n exports.systemTempDir = tmpFiles.systemTempDir;\n\n Object.assign(exports, __webpack_require__(/*! ./readln */ \"./src/utils/readln.js\"));\n}\nObject.assign(exports, __webpack_require__(/*! ./sh */ \"./src/utils/sh.js\"));\nObject.assign(exports, __webpack_require__(/*! ./eventUtils */ \"./src/utils/eventUtils.js\"));\nObject.assign(exports, __webpack_require__(/*! ./obj-merge */ \"./src/utils/obj-merge.js\"));\nObject.assign(exports, __webpack_require__(/*! ./make-slice-uri */ \"./src/utils/make-slice-uri.js\"));\nObject.assign(exports, __webpack_require__(/*! ./just-fetch */ \"./src/utils/just-fetch.js\"));\nObject.assign(exports, __webpack_require__(/*! ./inventory */ \"./src/utils/inventory.js\"));\nObject.assign(exports, __webpack_require__(/*! ./fetch-keystore */ \"./src/utils/fetch-keystore.js\"));\n\nmodule.exports.fetchURI = __webpack_require__(/*! ./fetch-uri */ \"./src/utils/fetch-uri.js\").fetchURI;\nmodule.exports.encodeDataURI = __webpack_require__(/*! ./encodeDataURI */ \"./src/utils/encodeDataURI.js\").encodeDataURI;\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../node_modules/process/browser.js */ \"./node_modules/process/browser.js\")))\n\n//# sourceURL=webpack:///./src/utils/index.js?");
5775
5788
 
5776
5789
  /***/ }),
5777
5790
 
@@ -5783,7 +5796,7 @@ eval("/* WEBPACK VAR INJECTION */(function(process) {/**\n * @file src/utils/ind
5783
5796
  /***/ (function(module, exports, __webpack_require__) {
5784
5797
 
5785
5798
  "use strict";
5786
- eval("/**\n * @file src/utils/inventory.js\n * Inventory method and related helpers, originally written for Supervisor2.\n *\n * @author Wes Garland, wes@kingsds.network\n * @date Mar 2021\n */\n\n\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst inspect = Symbol.for('nodejs.util.inspect.custom');\n\n/** \n * Inventory constructor. This is very much like an Array, but methods `splice`, `fill`, `copyWithin`,\n * and direct access to elements is prohibited.\n *\n * Methods added to Inventory which are not on Array:\n * - add: add an element to the Inventory\n * - delete: remove an element from the Inventory\n * - duplicate: return a new exports.Inventory, seeded with this Inventory's items\n *\n * Events 'add' and 'delete' are emitted whenever elements are added or deleted from\n * the inventory.\n *\n * @constructor Inventory\n * @param {string} name [optional] Name of the inventory; used for debugging\n * @param {object} initialValues [optional] An Array-like Object of initial values to add\n * to the Inventory without emitting the 'add' event.\n * @returns {object} instanceof Inventory\n */\nexports.Inventory = function supervisor$$Inventory(name, initialValues, compare)\n{\n var ee = new EventEmitter(name || 'Inventory');\n var px;\n\n if (initialValues)\n {\n if (!Array.isArray(initialValues))\n initialValues = Array.from(initialValues);\n else\n initialValues = [].concat(initialValues);\n }\n \n px = new Proxy([].concat(initialValues || []), {\n get: (target, prop, receiver) => {\n let promiscuous = false; /* see default case */\n\n if (typeof prop !== 'symbol' && !isNaN(+prop) && !promiscuous)\n throw new ReferenceError('indexed access is not allowed');\n\n switch(prop)\n {\n case inspect:\n {\n return `[Object supervisor$$Inventory <${name || 'anonymous'}>]`;\n }\n case 'fill':\n case 'copyWithin':\n case 'splice':\n {\n throw new Error(`method ${prop} is not allowed`);\n }\n case 'delete':\n {\n return (element) => ee.emit('delete', deleteArrayElement(target, element));\n }\n case 'pop':\n case 'shift':\n {\n return () => {\n let el = target[prop]();\n ee.emit('delete', el);\n return el;\n }\n }\n case 'add': {\n return (el) => {\n ee.emit('add', el);\n target.push(el);\n return el;\n }\n }\n case 'push':\n case 'unshift':\n {\n return (el) => {\n ee.emit('add', el);\n return target[prop](el);\n }\n }\n case 'on':\n case 'addListener':\n case 'addEventListener':\n case 'removeListener':\n case 'removeEventListener':\n case 'debug':\n case 'debugLabel':\n {\n if (typeof ee[prop] === 'function')\n return ee[prop].bind(ee);\n return ee[prop];\n }\n case 'duplicate':\n {\n return new exports.Inventory(target);\n }\n case 'top':\n {\n return () => target[0];\n }\n default:\n {\n if (typeof target[prop] !== 'function')\n return target[prop];\n\n /* Allow internal methods, eg. \"push\", to use numeric properties */\n return function supervisor$$Inventory$pxWrapper() {\n promiscuous = true;\n const ret = target[prop].apply(target, arguments)\n promiscuous = false;\n return ret;\n }\n }\n }\n },\n deleteProperty: function supervisor$$Inventory_deleteProperty()\n {\n return false;\n },\n defineProperty: function supervisor$$Inventory_defineProperty()\n {\n return false;\n },\n getPrototypeOf: function supervisor$$Inventory_getPrototypeOf(target)\n {\n return exports.Inventory;\n }\n });\n\n return px;\n}\n\n/**\n * Delete an element from an array without adding new index gaps.\n * @param arr The array to manipulate\n * @param element Either the element index (number) or the actual element (non-number) to remove\n * @returns the removed element\n */\nexports.deleteArrayElement = function deleteArrayElement(arr, element)\n{\n let idx;\n \n if (typeof element === 'number') {\n idx = element;\n } else {\n idx = arr.indexOf(element);\n if (idx === -1)\n throw new Error('invalid array element');\n }\n \n return arr.splice(idx, 1)[0];\n}\n\n\n//# sourceURL=webpack:///./src/utils/inventory.js?");
5799
+ eval("/**\n * @file src/utils/inventory.js\n * Inventory method and related helpers, originally written for Supervisor2.\n *\n * @author Wes Garland, wes@kingsds.network\n * @date Mar 2021\n */\n\n\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst inspect = Symbol.for('nodejs.util.inspect.custom');\n\n/** \n * Inventory constructor. This is very much like an Array, but methods `splice`, `fill`, `copyWithin`,\n * and direct access to elements is prohibited.\n *\n * Methods added to Inventory which are not on Array:\n * - add: add an element to the Inventory\n * - delete: remove an element from the Inventory\n * - replace: replace one item in the inventory with another\n * - duplicate: return a new exports.Inventory, seeded with this Inventory's items\n *\n * Events 'add' and 'delete' are emitted whenever elements are added or deleted from\n * the inventory. Both 'add' and 'delete' are emitted during a replace operation.\n *\n * @constructor Inventory\n * @param {string} name [optional] Name of the inventory; used for debugging\n * @param {object} initialValues [optional] An Array-like Object of initial values to add\n * to the Inventory without emitting the 'add' event.\n * @returns {object} instanceof Inventory\n */\nexports.Inventory = function Inventory(name, initialValues, compare)\n{\n var ee = new EventEmitter(name || 'Inventory');\n var px;\n\n if (initialValues)\n {\n if (!Array.isArray(initialValues))\n initialValues = Array.from(initialValues);\n else\n initialValues = [].concat(initialValues);\n }\n \n px = new Proxy([].concat(initialValues || []), {\n get: (target, prop, receiver) => {\n let promiscuous = false; /* see default case */\n\n if (typeof prop !== 'symbol' && !isNaN(+prop) && !promiscuous)\n throw new ReferenceError('indexed access is not allowed');\n\n const locate = (element) => {\n const idx = target.indexOf(element);\n if (idx === -1)\n throw new Error('invalid inventory item');\n return idx;\n }\n \n switch(prop)\n {\n case inspect:\n {\n return `[Object Inventory <${name || 'anonymous'}, ${target.length} items>]`;\n }\n case 'fill':\n case 'copyWithin':\n case 'splice':\n {\n throw new Error(`method ${prop} is not allowed`);\n }\n case 'delete':\n {\n return (element) => {\n const idx = locate(element);\n target.splice(idx, 1)[0];\n ee.emit('delete', element);\n }\n }\n case 'replace':\n {\n return (old, neu) => {\n const idx = locate(old);\n ee.emit('delete', target[idx]);\n target[idx] = neu;\n ee.emit('add', target[idx]);\n };\n }\n case 'pop':\n case 'shift':\n {\n return () => {\n let el = target[prop]();\n ee.emit('delete', el);\n return el;\n }\n }\n case 'add': {\n return (el) => {\n ee.emit('add', el);\n target.push(el);\n return el;\n }\n }\n case 'push':\n case 'unshift':\n {\n return (el) => {\n ee.emit('add', el);\n return target[prop](el);\n }\n }\n case 'on':\n case 'addListener':\n case 'addEventListener':\n case 'removeListener':\n case 'removeEventListener':\n case 'debug':\n case 'debugLabel':\n {\n if (typeof ee[prop] === 'function')\n return ee[prop].bind(ee);\n return ee[prop];\n }\n case 'duplicate':\n {\n return (__name, __compare) => new exports.Inventory(__name || name, target, __compare || compare);\n }\n case 'top':\n {\n return () => target[0];\n }\n case 'count':\n {\n return (arg) => target.filter(arg).length;\n }\n default:\n {\n if (typeof target[prop] !== 'function')\n return target[prop];\n\n /* Allow internal methods, eg. \"push\", to use numeric properties */\n return function Inventory$pxWrapper() {\n promiscuous = true;\n const ret = target[prop].apply(target, arguments)\n promiscuous = false;\n return ret;\n }\n }\n }\n },\n deleteProperty: function Inventory_deleteProperty()\n {\n return false;\n },\n defineProperty: function Inventory_defineProperty()\n {\n return false;\n },\n getPrototypeOf: function Inventory_getPrototypeOf(target)\n {\n return exports.Inventory;\n }\n });\n\n return px;\n}\n\n\n//# sourceURL=webpack:///./src/utils/inventory.js?");
5787
5800
 
5788
5801
  /***/ }),
5789
5802
 
@@ -5794,7 +5807,8 @@ eval("/**\n * @file src/utils/inventory.js\n * Inventory met
5794
5807
  /*! no static exports found */
5795
5808
  /***/ (function(module, exports, __webpack_require__) {
5796
5809
 
5797
- eval("/* WEBPACK VAR INJECTION */(function(process) {/**\n * @file just-fetch.js\n * @author Ryan Rossiter <ryan@kingsds.network>\n * @date June 2020\n *\n * Cross-platform method for performing an HTTP request.\n */\n\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\n\n /**\n * Make a request, as a promise, resolves with a string or json object\n *\n * @param {string | object} url - path of file to be fetched; if object must be instanceof URL or DcpURL\n * @param {string} [type = \"string\"] - expected type of the response\n * @param {string} [method = \"GET\"] - type of request to make\n * @param {boolean} [bustCache = false] - whether to add cache poison to the end of the url\n * @param {any} [body] - what to send as the body of the message. Objects are turned into into \n * standard CGI key=value encoding when the method is 'POST'\n * @returns {Promise} - resolves with response (json if type=\"JSON\", string otherwise), rejects on fail\n */\nexports.justFetch = function justFetch(url, type = 'string', method = 'GET', bustCache = false, body = undefined) {\n return new Promise((resolve, reject) => {\n let deeperErrorStack = new Error().stack;\n deeperErrorStack = deeperErrorStack.substring(deeperErrorStack.indexOf('\\n') + 1);\n\n if (bustCache)\n {\n let bustParam = ((typeof document !== 'undefined' && document.head && document.head.getAttribute('version')) || Date.now()); /* cache buster */\n\n if (typeof url === 'object' && url instanceof DcpURL)\n url = url.href;\n if (typeof url === 'string')\n url = new URL(url);\n url.search += (url.search ? '&' : '') + encodeURI(bustParam);\n }\n\n if (typeof url === 'object' && url.href) {\n url = url.href;\n }\n\n const xhr = new XMLHttpRequest();\n xhr.onloadend = function Protocol$$justFetch$onloadend() {\n try {\n delete xhr.onloadend;\n\n if (xhr.status >= 200 && xhr.status < 300) {\n if (xhr.getResponseHeader('content-type') && type === 'string') {\n type = xhr.getResponseHeader('content-type').split(';')[0];\n }\n \n let data = xhr.responseText;\n if (type === 'JSON' || type === 'application/json') {\n data = JSON.parse(data);\n }\n\n if (type === 'application/x-kvin' ) {\n data = kvin.deserialize(data);\n }\n \n resolve(data);\n } else {\n function makeFetchError()\n {\n if (xhr.status)\n return new Error(`HTTP Error ${xhr.status} fetching ${url}`);\n\n /* NodeJS via xmlhttprequest-ssl leaves an Error duck */\n if (xhr.statusText && xhr.statusText.code && xhr.statusText.message) {\n let error = new Error(xhr.statusText.message);\n let stack = error.stack;\n\n Object.assign(error, xhr.statusText);\n if (xhr.statusText.stack)\n error.stack = xhr.statusText.stack.split('\\n').slice(1).join('\\n') + '---\\n' + stack;\n else\n error.stack = stack;\n return error;\n }\n\n return new Error(`Network error fetching ${url}`);\n }\n\n const error = makeFetchError();\n \n error.request = Object.assign({}, xhr); /* Shallow clone so that console.log will work in node */\n error.request.method = method;\n error.request.location = url;\n error.stack += '\\n' + deeperErrorStack;\n if (xhr.status)\n error.code = 'HTTP_' + xhr.status;\n\n throw error;\n }\n } catch (e) {\n reject(e);\n }\n };\n\n xhr.open(method, url);\n if (!body)\n xhr.send();\n else\n {\n if (typeof body === 'object' && method.toUpperCase() === 'POST')\n {\n let entries = Object.entries(body);\n body = entries.map((kvp) => `${encodeURIComponent(kvp[0])}=${encodeURIComponent(kvp[1])}`).join('&');\n }\n xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');\n xhr.send(body);\n }\n });\n }\n\n/** Reformat an error (rejection) message from utils::justFetch, so that debugging code \n * can include (for example) a text-rendered version of the remote 404 page.\n *\n * @param {object} error The rejection from justFetch()\n * @returns {string} An error message, formatted with ANSI color when the output\n * is a terminal, suitable for writing directly to stdout. If\n * the response included html content (eg a 404 page), it is \n * rendered to text in this string.\n */\nexports.justFetchPrettyError = function utils$$justFetchPrettyError(error) {\n const chalk = new requireNative('chalk').constructor({enabled: __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\").useChalk});\n var message, headers={};\n\n if (!error.request || !error.request.status)\n return error;\n\n error.request.getAllResponseHeaders().replace(/\\r/g,'').split('\\n').forEach(function(line) {\n var colon = line.indexOf(': ')\n headers[line.slice(0,colon)] = line.slice(colon+2)\n })\n\n message = `HTTP Status: ${error.request.status} for ${error.request.method} ${error.request.location}`\n\n switch(headers['content-type'].replace(/;.*$/, '')) {\n case 'text/plain':\n message += '\\n' + chalk.grey(error.request.responseText)\n break;\n case 'text/html': {\n let html = error.request.responseText;\n\n html = html.replace(/\\n<a/gi, ' <a'); /* html-to-text bug, affects google 301s /wg jun 2020 */\n message += chalk.grey(__webpack_require__(/*! html-to-text */ \"./node_modules/html-to-text/index.js\").fromString(html, {\n wordwrap: parseInt(process.env.COLUMNS, 10) || 80,\n hideLinkHrefIfSameAsText: true,\n format: {\n heading: function (elem, fn, options) {\n var h = fn(elem.children, options);\n return '\\n====\\n' + chalk.yellow(chalk.bold(h.toUpperCase())) + '\\n====\\n';\n }\n }\n }));\n break;\n }\n }\n\n return message;\n} \n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../node_modules/process/browser.js */ \"./node_modules/process/browser.js\")))\n\n//# sourceURL=webpack:///./src/utils/just-fetch.js?");
5810
+ "use strict";
5811
+ eval("/* WEBPACK VAR INJECTION */(function(process) {/**\n * @file just-fetch.js\n * @author Ryan Rossiter <ryan@kingsds.network>\n * @date June 2020\n *\n * Cross-platform method for performing an HTTP request.\n */\n\n\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\n\n /**\n * Make a request, as a promise, resolves with a string or json object\n *\n * @param {string | object} url - path of file to be fetched; if object must be instanceof URL or DcpURL\n * @param {string} [type = \"string\"] - expected type of the response\n * @param {string} [method = \"GET\"] - type of request to make\n * @param {boolean} [bustCache = false] - whether to add cache poison to the end of the url\n * @param {any} [body] - what to send as the body of the message. Objects are turned into into \n * standard CGI key=value encoding when the method is 'POST'\n * @returns {Promise} - resolves with response (json if type=\"JSON\", string otherwise), rejects on fail\n */\nexports.justFetch = function justFetch(url, type = 'string', method = 'GET', bustCache = false, body = undefined) {\n return new Promise((resolve, reject) => {\n let deeperErrorStack = new Error().stack;\n deeperErrorStack = deeperErrorStack.substring(deeperErrorStack.indexOf('\\n') + 1);\n\n if (bustCache)\n {\n let bustParam = ((typeof document !== 'undefined' && document.head && document.head.getAttribute('version')) || Date.now()); /* cache buster */\n\n if (typeof url === 'object' && url instanceof DcpURL)\n url = url.href;\n if (typeof url === 'string')\n url = new URL(url);\n url.search += (url.search ? '&' : '') + encodeURI(bustParam);\n }\n\n if (typeof url === 'object' && url.href) {\n url = url.href;\n }\n\n const xhr = new XMLHttpRequest();\n xhr.onloadend = function Protocol$$justFetch$onloadend() {\n try {\n delete xhr.onloadend;\n\n if (xhr.status >= 200 && xhr.status < 300) {\n if (xhr.getResponseHeader('content-type') && type === 'string') {\n type = xhr.getResponseHeader('content-type').split(';')[0];\n }\n \n let data = xhr.responseText;\n if (type === 'JSON' || type === 'application/json') {\n data = JSON.parse(data);\n }\n\n if (type === 'application/x-kvin' ) {\n data = kvin.deserialize(data);\n }\n \n resolve(data);\n } else {\n function makeFetchError()\n {\n if (xhr.status)\n return new Error(`HTTP Error ${xhr.status} fetching ${url}`);\n\n /* NodeJS via xmlhttprequest-ssl leaves an Error duck */\n if (xhr.statusText && xhr.statusText.code && xhr.statusText.message) {\n let error = new Error(xhr.statusText.message);\n let stack = error.stack;\n\n Object.assign(error, xhr.statusText);\n if (xhr.statusText.stack)\n error.stack = xhr.statusText.stack.split('\\n').slice(1).join('\\n') + '---\\n' + stack;\n else\n error.stack = stack;\n return error;\n }\n\n return new Error(`Network error fetching ${url}`);\n }\n\n const error = makeFetchError();\n \n error.request = Object.assign({}, xhr); /* Shallow clone so that console.log will work in node */\n error.request.method = method;\n error.request.location = url;\n error.stack += '\\n' + deeperErrorStack;\n if (xhr.status)\n error.code = 'HTTP_' + xhr.status;\n\n throw error;\n }\n } catch (e) {\n reject(e);\n }\n };\n\n xhr.open(method, url);\n if (!body)\n xhr.send();\n else\n {\n if (typeof body === 'object' && method.toUpperCase() === 'POST')\n {\n let entries = Object.entries(body);\n body = entries.map((kvp) => `${encodeURIComponent(kvp[0])}=${encodeURIComponent(kvp[1])}`).join('&');\n }\n xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');\n xhr.send(body);\n }\n });\n }\n\n/** Reformat an error (rejection) message from utils::justFetch, so that debugging code \n * can include (for example) a text-rendered version of the remote 404 page.\n *\n * @param {object} error The rejection from justFetch()\n * @returns {string} An error message, formatted with ANSI color when the output\n * is a terminal, suitable for writing directly to stdout. If\n * the response included html content (eg a 404 page), it is \n * rendered to text in this string.\n */\nexports.justFetchPrettyError = function utils$$justFetchPrettyError(error) {\n const chalk = new requireNative('chalk').constructor({enabled: __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\").useChalk});\n var message, headers={};\n\n if (!error.request || !error.request.status)\n return error;\n\n error.request.getAllResponseHeaders().replace(/\\r/g,'').split('\\n').forEach(function(line) {\n var colon = line.indexOf(': ')\n headers[line.slice(0,colon)] = line.slice(colon+2)\n })\n\n message = `HTTP Status: ${error.request.status} for ${error.request.method} ${error.request.location}`\n\n switch(headers['content-type'].replace(/;.*$/, '')) {\n case 'text/plain':\n message += '\\n' + chalk.grey(error.request.responseText)\n break;\n case 'text/html': {\n let html = error.request.responseText;\n\n html = html.replace(/\\n<a/gi, ' <a'); /* html-to-text bug, affects google 301s /wg jun 2020 */\n message += chalk.grey(__webpack_require__(/*! html-to-text */ \"./node_modules/html-to-text/index.js\").fromString(html, {\n wordwrap: parseInt(process.env.COLUMNS, 10) || 80,\n hideLinkHrefIfSameAsText: true,\n format: {\n heading: function (elem, fn, options) {\n var h = fn(elem.children, options);\n return '\\n====\\n' + chalk.yellow(chalk.bold(h.toUpperCase())) + '\\n====\\n';\n }\n }\n }));\n break;\n }\n }\n\n return message;\n} \n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../node_modules/process/browser.js */ \"./node_modules/process/browser.js\")))\n\n//# sourceURL=webpack:///./src/utils/just-fetch.js?");
5798
5812
 
5799
5813
  /***/ }),
5800
5814
 
@@ -5805,7 +5819,7 @@ eval("/* WEBPACK VAR INJECTION */(function(process) {/**\n * @file just-f
5805
5819
  /*! no static exports found */
5806
5820
  /***/ (function(module, exports, __webpack_require__) {
5807
5821
 
5808
- eval("/**\n * @file make-slice-uri.js\n * @author Eddie Roosenmaallen, eddie@kingsds.netework\n * @date Oct 2020\n */\n\nconst { rehydrateRange } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst { encodeDataURI } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { sha256 } = __webpack_require__(/*! ethereumjs-util */ \"./node_modules/ethereumjs-util/dist/index.js\");\n\n/** \n * Make a sliceURI, given the storage type and details.\n * @param {string} storageType The storage type (range|pattern)\n * @param {string} storageDetails Details about the storage and/or data, which\n * varies based on storageType.\n * - range → storageDetails is a range descriptor. The 'slice' parameter is used to specify\n * element of the range to create the URL for.\n * - pattern → storageDetails is a pattern to form a URI-encoded URI that can be fetched\n * by the supervisor, created by substituting parameters into the pattern.\n * @param {object} parameters An object used for making substitutions in URIs. Each object key is\n * treated as a label which can appear between curly braces in the pattern;\n * the corresponding value is then substituted into the string. A special\n * key, hash, is used to indicate that the substituted value should be the\n * the result of calculating the hash of all the other values, appended\n * together in alphabetical order of their keys.\n * @returns {string} the slice URI. This could be any URI suitable for the fetchURI function, including\n * http:, https: and data: URIs.\n */\nexports.makeSliceURI = function makeSliceURI(storageType, storageDetails, parameters) {\n const { encodeDataURI } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n\n if (!parameters || !(typeof parameters === 'object')) {\n throw new Error('parameters object is required');\n }\n\n if (!storageDetails) {\n throw new Error('storageDetails is required');\n }\n\n switch (storageType) {\n case 'range': {\n const ro = rehydrateRange(storageDetails);\n if (!parameters.slice) {\n throw new Error('Slice parameter is required for range storageType');\n }\n\n // -1 to prevent an OBOE since slice numbers start at 1.\n return encodeDataURI(ro[parameters.slice - 1]);\n }\n case 'pattern':\n const uri = Object.entries(parameters).reduce(\n (uri, [key, value]) => {\n const re = new RegExp(`{${key}}`, 'g');\n return uri.replace(re, value)\n },\n storageDetails\n );\n\n const hash = sha256(JSON.stringify(parameters)).toString('hex');\n return uri.replace(/{hash}/g, hash);\n default:\n throw new Error(`Unknown storageType parameter \"${storageType}\"`);\n }\n}\n\n\n//# sourceURL=webpack:///./src/utils/make-slice-uri.js?");
5822
+ eval("/**\n * @file make-slice-uri.js\n * @author Eddie Roosenmaallen, eddie@kingsds.netework\n * @date Oct 2020\n */\n\nconst { rehydrateRange } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst { encodeDataURI } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { sha256 } = __webpack_require__(/*! ethereumjs-util */ \"./node_modules/ethereumjs-util/dist/index.js\");\n\n/** \n * Make a sliceURI, given the storage type and details.\n * @param {string} storageType The storage type (range|pattern)\n * @param {string} storageDetails Details about the storage and/or data, which\n * varies based on storageType.\n * - range → storageDetails is a range descriptor. The 'slice' parameter is used to specify\n * element of the range to create the URL for.\n * - pattern → storageDetails is a pattern to form a URI-encoded URI that can be fetched\n * by the supervisor, created by substituting parameters into the pattern.\n * @param {object} parameters An object used for making substitutions in URIs. Each object key is\n * treated as a label which can appear between curly braces in the pattern;\n * the corresponding value is then substituted into the string. A special\n * key, hash, is used to indicate that the substituted value should be the\n * the result of calculating the hash of all the other values, appended\n * together in alphabetical order of their keys.\n * @returns {string} the slice URI. This could be any URI suitable for the fetchURI function, including\n * http:, https: and data: URIs.\n */\nexports.makeDataURI = function makeDataURI(storageType, storageDetails, parameters) {\n const { encodeDataURI } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n\n if (!parameters || !(typeof parameters === 'object')) {\n throw new Error('parameters object is required');\n }\n\n if (!storageDetails) {\n throw new Error('storageDetails is required');\n }\n\n switch (storageType) {\n case 'range': {\n const ro = rehydrateRange(storageDetails);\n if (!parameters.slice) {\n throw new Error('Slice parameter is required for range storageType');\n }\n\n // -1 to prevent an OBOE since slice numbers start at 1.\n return encodeDataURI(ro[parameters.slice - 1]);\n }\n case 'pattern':\n const uri = Object.entries(parameters).reduce(\n (uri, [key, value]) => {\n const re = new RegExp(`{${key}}`, 'g');\n return uri.replace(re, value)\n },\n storageDetails\n );\n\n const hash = sha256(JSON.stringify(parameters)).toString('hex');\n return uri.replace(/{hash}/g, hash);\n default:\n throw new Error(`Unknown storageType parameter \"${storageType}\"`);\n }\n}\n\n\n//# sourceURL=webpack:///./src/utils/make-slice-uri.js?");
5809
5823
 
5810
5824
  /***/ }),
5811
5825
 
@@ -5873,7 +5887,7 @@ eval("/* WEBPACK VAR INJECTION */(function(process) {/** \n * @file sh.js
5873
5887
  /***/ (function(module, exports, __webpack_require__) {
5874
5888
 
5875
5889
  "use strict";
5876
- eval("/* WEBPACK VAR INJECTION */(function(Buffer) {/** \n * @file tmpfiles.js Utilities for securely and portably creating temporary\n * files with NodeJS.\n * @author Wes Garland, wes@kingsds.network\n * @date April 2020\n */\n\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst path = requireNative('path');\nconst fs = requireNative('fs');\nconst process = requireNative('process');\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\n\nlet systemTempDir = process.env.TMP || process.env.TEMP;\nif (!systemTempDir) {\n if (process.platform === 'win32') {\n if (process.env.LOCALAPPDATA) \n systemTempDir = path.join(systemTempdir, 'Temp');\n else if (process.env.HOMEDRIVE && process.env.HOMEPATH)\n systemTempDir = path.join(process.env.HOMEDRIVE + process.env.HOMEPATH, 'AppData', 'Local', 'Temp');\n else if (process.env.SystemRoot)\n systemTempDir = path.join(systemRoot, 'Temp');\n else if (process.env.SystemDrive)\n systemTempDir = path.join(systemDrive + '\\\\Windows', 'Temp');\n else\n systemTempDir = 'C:\\\\WINDOWS\\\\Temp'\n }\n else\n systemTempDir = '/tmp';\n}\n\nexports.systemTempDir = systemTempDir;\n\n/**\n * Create a temporary thing in the filesystem.\n *\n * @param {function} createThingFn The function which creates the thing; must take as its argument\n * a fully-pathed filename and throw an exception failure. It must\n * return a handle to the thing, which will become the return value\n * of this function.\n * @param {string} basenamePattern A string specifying the file basename pattern. The\n * character X will be replaced with randomly-generated\n * characters, similar to mkstemp(3).\n * @param {string} extension [optional] The filename extension to use; pass false for none\n *\n * @return a handle created by createThingFn\n */\nexports.createTempThing = function createTempThing(createThingFn, basenamePattern, extension)\n{\n const generate = requireNative('nanoid/generate');\n\n var entropy = generate('1234567890abcdefghijklmnopqrstuvwyxzkABCDEFGHIJKLMNOPQRSTUVWXYZ', 64);\n var basename = '';\n var filename; /* The filename of the temp thing we are creating */\n var thing; /* The temp thing we are creating */\n var removed = false; /* Used by removeFn to avoid races */\n \n if (!basenamePattern)\n basenamePattern = `dcp-${process.pid}-XXXXXXXX`;\n\n const re = /[xX]{3,}/;\n assert(re.test(basenamePattern))\n\n let tries = 0 \n loop: while (\"goto considered harmful considered harmful\") {\n let j = Math.floor(Math.random() * entropy.length);\n for (let i=0; i < basenamePattern.length; i++) {\n if (basenamePattern[i] === 'X')\n basename += entropy[j++ % entropy.length];\n else\n basename += basenamePattern[i];\n }\n\n if (extension !== false)\n basename += '.' + (extension || 'dcp');\n filename = path.join(exports.systemTempDir, basename);\n if (fs.existsSync(filename)) {\n tries = tries++; \n basename='';\n if (tries > 1000)\n throw e\n continue loop;\n }\n try {\n thing = createThingFn(filename);\n } catch(e) {\n if (e.code === 'EEXIST')\n continue;\n throw e;\n }\n break loop;\n }\n\n function removeFn()\n {\n process.off('exit', removeFn);\n if (!process.env.DCP_DEBUG_TMP_FILES && !removed)\n {\n try\n {\n try\n {\n fs.unlinkSync(filename);\n removed = true;\n }\n catch(error)\n {\n if (fs.lstatSync(filename).isDirectory())\n fs.rmdirSync(filename, { recursive: true, maxRetries: 2 });\n else\n throw error;\n }\n }\n catch(error)\n {\n console.warn(`Warning: unable to clean up '${filename}'`, error.message);\n }\n }\n }\n\n process.on('exit', removeFn);\n\n return { thing, removeFn, filename };\n}\n\n/** \n * Securely create a temporary file in the system temp directory. A cleanup is registered to\n * automatically remove the file when the process is closed.\n * \n * @param {string} basenamePattern A string specifying the file basename pattern. The\n * character X will be replaced with randomly-generated\n * characters, similar to mkstemp(3).\n * @param {string} extension [optional] The filename extension to use; pass false for none\n * @param perms [optional] The permissions argument to openSync.\n *\n * @returns {object} An object with properties:\n * fd: the open file descriptor\n * filename: the name of the newly-created file\n * toString: method which returns the filename\n * write: method which writes to the file descriptor (like fs.write)\n * writeSync: method which writes to the file descriptor (like fs.writeSync)\n * close: method which closes the file descriptor (like fs.close)\n * closeSync: method which closes the file descriptor (like fs.closeSync)\n * chmod: method which changes the file mode (like fs.fchmod)\n * chmodSync: method which changes the file mode (like fs.fchmodSync)\n * remove: method which remove the file from disk and cleans up the registered\n * process.exit cleanup. Does not close the file descriptor!\n */\nexports.createTempFile = function utils$$createTempFile(basenamePattern, extension, perms) {\n const inspect = requireNative('util').inspect.custom;\n let f = fs.constants;\n let hnd;\n\n /** Atomically create a file using the permissions in the closed-over environment.\n * Atomic here means that either the file will be created or it won't be, and the\n * check-then-create race condition is handled by the OS.\n */\n function atomicCreateFile(filename) {\n return fs.openSync(filename, f.O_RDWR | f.O_CREAT | f.O_EXCL, perms | 0o600);\n }\n\n const { thing: fd, removeFn, filename } = exports.createTempThing(atomicCreateFile, basenamePattern, extension);\n\n hnd = { fd, filename };\n hnd.toString = () => filename;\n hnd.write = function() { return fs.write .apply(null, [fd].concat(Array.from(arguments))) };\n hnd.writeSync = function() { return fs.writeSync .apply(null, [fd].concat(Array.from(arguments))) };\n hnd.chmod = function() { return fs.fchmod .apply(null, [fd].concat(Array.from(arguments))) };\n hnd.chmodSync = function() { return fs.fchmodSync.apply(null, [fd].concat(Array.from(arguments))) };\n hnd.close = function() { fs.close .apply(null, [fd].concat(Array.from(arguments))); delete hnd.fd; return hnd };\n hnd.closeSync = function() { fs.closeSync .apply(null, [fd].concat(Array.from(arguments))); delete hnd.fd; return hnd };\n hnd.remove = removeFn;\n\n hnd[inspect] = () => '[Object utils$$tempFile(' + filename + ')]';\n \n return hnd;\n}\n\n/** \n * Securely create a temporary socket (eg for IPC) in the system temp directory. A cleanup is registered to\n * automatically remove the file when the process is closed.\n * \n * @param {string} basenamePattern A string specifying the file basename pattern. The\n * character X will be replaced with randomly-generated\n * characters, similar to mkstemp(3).\n * @param {string} extension [optional] The filename extension to use; pass false for none\n * @param perms [optional] The permissions argument to openSync. Default is 0600;\n * pass false to use OS default (0666 & umask(2)).\n * @param {function} connectHandler [optional] The callback to invoke when the socket is connected to\n * @returns {object} a socket, created with net.createConnection, using a path argument\n */\nexports.createTempSocket = function utils$$createTempSocket(basenamePattern, extension, connectHandler, perms) {\n const inspect = requireNative('util').inspect.custom;\n\n function createSocket(filename) {\n const net = requireNative('net');\n var server = net.createServer(connectHandler);\n var socket = server.listen(filename); /* creates the socket (named pipe) */\n\n socket.unref();\n\n return net.createConnection(filename);\n }\n\n const { thing: socket, removeFn, filename } = exports.createTempThing(createSocket, basenamePattern, extension);\n\n if (filename && perms !== false) {\n try {\n fs.chmodSync(filename, perms || 0o600);\n } catch(error) {\n socket.close();\n console.log(`Could not change permissions on named pipe ${filename}; closing`);\n throw error;\n }\n }\n\n socket[inspect] = () => '[Object utils$$tempSocket(' + filename + ')]';\n socket.on('close', removeFn);\n return socket;\n}\n\n/**\n * Copy the entire source file into the target, appending at the current\n * file offset. If source file has been closed, we will re-open for the\n * the duration of this call in read-only mode. This routine does not attempt\n * to preserve the file position of the input file.\n *\n * @param source {object} The file to copy, handle from createTempFile\n * @param target {object} The file to append to, handle from createTempFile\n * @param target2 {object} An optioanl stream which also receives the same \n * data via its write method. For npm create-hash.\n */ \nexports.catFile = function utils$$catFile(source, target, target2) {\n var buf = Buffer.allocUnsafe(8192 * 64);\n var pos = 0;\n var nRead;\n var sourceFd = source.fd;\n var targetFd = target.fd;\n\n if (!sourceFd && sourceFd !== 0) {\n sourceFd = fs.openSync(source.filename, fs.O_RDONLY, 0o400);\n }\n \n do {\n nRead = fs.readSync(sourceFd, buf, 0, buf.length, pos);\n fs.writeSync(targetFd, buf.slice(0, nRead));\n pos += nRead;\n if (target2)\n target2.write(buf);\n } while(nRead === buf.length);\n\n if (!source.fd && sourceFd !== 0) {\n fs.closeSync(sourceFd);\n }\n}\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../node_modules/node-libs-browser/node_modules/buffer/index.js */ \"./node_modules/node-libs-browser/node_modules/buffer/index.js\").Buffer))\n\n//# sourceURL=webpack:///./src/utils/tmpfiles.js?");
5890
+ eval("/* WEBPACK VAR INJECTION */(function(Buffer) {/** \n * @file tmpfiles.js Utilities for securely and portably creating temporary\n * files with NodeJS.\n * @author Wes Garland, wes@kingsds.network\n * @date April 2020\n */\n\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst path = requireNative('path');\nconst fs = requireNative('fs');\nconst os = requireNative('os');\nconst process = requireNative('process');\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\n\nlet systemTempDir = process.env.TMP || process.env.TEMP;\nif (!systemTempDir) {\n if (process.platform === 'win32') {\n if (process.env.LOCALAPPDATA) \n systemTempDir = path.join(systemTempdir, 'Temp');\n else if (process.env.HOMEDRIVE && process.env.HOMEPATH)\n systemTempDir = path.join(process.env.HOMEDRIVE + process.env.HOMEPATH, 'AppData', 'Local', 'Temp');\n else if (process.env.SystemRoot)\n systemTempDir = path.join(systemRoot, 'Temp');\n else if (process.env.SystemDrive)\n systemTempDir = path.join(systemDrive + '\\\\Windows', 'Temp');\n else\n systemTempDir = 'C:\\\\WINDOWS\\\\Temp'\n }\n else\n systemTempDir = '/tmp';\n}\n\nexports.systemTempDir = systemTempDir;\n\n/**\n * Create a temporary thing in the filesystem.\n *\n * @param {function} createThingFn The function which creates the thing; must take as its argument\n * a fully-pathed filename and throw an exception failure. It must\n * return a handle to the thing, which will become the return value\n * of this function.\n * @param {string} basenamePattern A string specifying the file basename pattern. The\n * character X will be replaced with randomly-generated\n * characters, similar to mkstemp(3).\n * @param {string} extension [optional] The filename extension to use; pass false for none\n *\n * @return a handle created by createThingFn\n */\nexports.createTempThing = function createTempThing(createThingFn, basenamePattern, extension)\n{\n const generate = requireNative('nanoid/generate');\n\n var entropy = generate('1234567890abcdefghijklmnopqrstuvwyxzkABCDEFGHIJKLMNOPQRSTUVWXYZ', 64);\n var basename = '';\n var filename; /* The filename of the temp thing we are creating */\n var thing; /* The temp thing we are creating */\n var removed = false; /* Used by removeFn to avoid races */\n var pipeDir;\n \n if (os.platform() === 'win32')\n pipeDir = '\\\\\\\\.\\\\pipe\\\\';\n else\n pipeDir = exports.systemTempDir;\n \n if (!basenamePattern)\n basenamePattern = `dcp-${process.pid}-XXXXXXXX`;\n\n const re = /[xX]{3,}/;\n assert(re.test(basenamePattern))\n\n let tries = 0 \n loop: while (\"goto considered harmful considered harmful\") {\n let j = Math.floor(Math.random() * entropy.length);\n for (let i=0; i < basenamePattern.length; i++) {\n if (basenamePattern[i] === 'X')\n basename += entropy[j++ % entropy.length];\n else\n basename += basenamePattern[i];\n }\n\n if (extension !== false)\n basename += '.' + (extension || 'dcp');\n filename = path.join(pipeDir, basename);\n if (fs.existsSync(filename)) {\n tries = tries++; \n basename='';\n if (tries > 1000)\n throw e\n continue loop;\n }\n try {\n thing = createThingFn(filename);\n } catch(e) {\n if (e.code === 'EEXIST')\n continue;\n throw e;\n }\n break loop;\n }\n\n function removeFn()\n {\n process.off('exit', removeFn);\n if (!process.env.DCP_DEBUG_TMP_FILES && !removed)\n {\n try\n {\n try\n {\n fs.unlinkSync(filename);\n removed = true;\n }\n catch(error)\n {\n if (fs.lstatSync(filename).isDirectory())\n fs.rmdirSync(filename, { recursive: true, maxRetries: 2 });\n else\n throw error;\n }\n }\n catch(error)\n {\n console.warn(`Warning: unable to clean up '${filename}'`, error.message);\n }\n }\n }\n \n if (os.platform() !== 'win32')\n process.on('exit', removeFn); /* Pipe namespace automatically cleaned up on win32 */\n\n return { thing, removeFn, filename };\n}\n\n/** \n * Securely create a temporary file in the system temp directory. A cleanup is registered to\n * automatically remove the file when the process is closed.\n * \n * @param {string} basenamePattern A string specifying the file basename pattern. The\n * character X will be replaced with randomly-generated\n * characters, similar to mkstemp(3).\n * @param {string} extension [optional] The filename extension to use; pass false for none\n * @param perms [optional] The permissions argument to openSync.\n *\n * @returns {object} An object with properties:\n * fd: the open file descriptor\n * filename: the name of the newly-created file\n * toString: method which returns the filename\n * write: method which writes to the file descriptor (like fs.write)\n * writeSync: method which writes to the file descriptor (like fs.writeSync)\n * close: method which closes the file descriptor (like fs.close)\n * closeSync: method which closes the file descriptor (like fs.closeSync)\n * chmod: method which changes the file mode (like fs.fchmod)\n * chmodSync: method which changes the file mode (like fs.fchmodSync)\n * remove: method which remove the file from disk and cleans up the registered\n * process.exit cleanup. Does not close the file descriptor!\n */\nexports.createTempFile = function utils$$createTempFile(basenamePattern, extension, perms) {\n const inspect = requireNative('util').inspect.custom;\n let f = fs.constants;\n let hnd;\n\n /** Atomically create a file using the permissions in the closed-over environment.\n * Atomic here means that either the file will be created or it won't be, and the\n * check-then-create race condition is handled by the OS.\n */\n function atomicCreateFile(filename) {\n return fs.openSync(filename, f.O_RDWR | f.O_CREAT | f.O_EXCL, perms | 0o600);\n }\n\n const { thing: fd, removeFn, filename } = exports.createTempThing(atomicCreateFile, basenamePattern, extension);\n\n hnd = { fd, filename };\n hnd.toString = () => filename;\n hnd.write = function() { return fs.write .apply(null, [fd].concat(Array.from(arguments))) };\n hnd.writeSync = function() { return fs.writeSync .apply(null, [fd].concat(Array.from(arguments))) };\n hnd.chmod = function() { return fs.fchmod .apply(null, [fd].concat(Array.from(arguments))) };\n hnd.chmodSync = function() { return fs.fchmodSync.apply(null, [fd].concat(Array.from(arguments))) };\n hnd.close = function() { fs.close .apply(null, [fd].concat(Array.from(arguments))); delete hnd.fd; return hnd };\n hnd.closeSync = function() { fs.closeSync .apply(null, [fd].concat(Array.from(arguments))); delete hnd.fd; return hnd };\n hnd.remove = removeFn;\n\n hnd[inspect] = () => '[Object utils$$tempFile(' + filename + ')]';\n \n return hnd;\n}\n\n/** \n * Securely create a temporary socket (eg for IPC) in the system temp directory. A cleanup is registered to\n * automatically remove the file when the process is closed.\n * \n * @param {string} basenamePattern A string specifying the file basename pattern. The\n * character X will be replaced with randomly-generated\n * characters, similar to mkstemp(3).\n * @param {string} extension [optional] The filename extension to use; pass false for none\n * @param perms [optional] The permissions argument to openSync. Default is 0600;\n * pass false to use OS default (0666 & umask(2)).\n * @param {function} connectHandler [optional] The callback to invoke when the socket is connected to\n * @returns {object} a socket, created with net.createConnection, using a path argument\n */\nexports.createTempSocket = function utils$$createTempSocket(basenamePattern, extension, connectHandler, perms) {\n const inspect = requireNative('util').inspect.custom;\n\n\n function createSocket(filename)\n {\n const net = requireNative('net');\n var server = net.createServer(connectHandler);\n var socket = server.listen(filename); /* creates the socket (named pipe) */\n\n socket.unref();\n\n return net.createConnection(filename);\n }\n\n const { thing: socket, removeFn, filename } = exports.createTempThing(createSocket, basenamePattern, extension);\n assert(os.platform() !== 'win32' || filename.startsWith('\\\\\\\\.\\\\pipe\\\\'));\n\n if (!socket)\n throw new Error(`Could not create socket ${filename || 'like ' + basenamePattern + '.' + extension}`); /* this should be impossible */\n\n if (filename && perms !== false) {\n try {\n fs.chmodSync(filename, perms || 0o600);\n } catch(error) {\n if (socket.close)\n socket.close();\n console.log(`Could not change permissions on named pipe ${filename} (${error.code || error.message}); closing`);\n throw error;\n }\n }\n\n socket[inspect] = () => '[Object utils$$tempSocket(' + filename + ')]';\n socket.on('close', removeFn);\n return socket;\n}\n\n/**\n * Copy the entire source file into the target, appending at the current\n * file offset. If source file has been closed, we will re-open for the\n * the duration of this call in read-only mode. This routine does not attempt\n * to preserve the file position of the input file.\n *\n * @param source {object} The file to copy, handle from createTempFile\n * @param target {object} The file to append to, handle from createTempFile\n * @param target2 {object} An optioanl stream which also receives the same \n * data via its write method. For npm create-hash.\n */ \nexports.catFile = function utils$$catFile(source, target, target2) {\n var buf = Buffer.allocUnsafe(8192 * 64);\n var pos = 0;\n var nRead;\n var sourceFd = source.fd;\n var targetFd = target.fd;\n\n if (!sourceFd && sourceFd !== 0) {\n sourceFd = fs.openSync(source.filename, fs.O_RDONLY, 0o400);\n }\n \n do {\n nRead = fs.readSync(sourceFd, buf, 0, buf.length, pos);\n fs.writeSync(targetFd, buf.slice(0, nRead));\n pos += nRead;\n if (target2)\n target2.write(buf);\n } while(nRead === buf.length);\n\n if (!source.fd && sourceFd !== 0) {\n fs.closeSync(sourceFd);\n }\n}\n\n/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../node_modules/node-libs-browser/node_modules/buffer/index.js */ \"./node_modules/node-libs-browser/node_modules/buffer/index.js\").Buffer))\n\n//# sourceURL=webpack:///./src/utils/tmpfiles.js?");
5877
5891
 
5878
5892
  /***/ }),
5879
5893