dcp-client 4.2.1 → 4.2.2

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.
@@ -3821,7 +3821,7 @@ eval("// Copyright Joyent, Inc. and other Node contributors.\n//\n// Permission
3821
3821
  /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
3822
3822
 
3823
3823
  "use strict";
3824
- eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (/* binding */ Modal)\n/* harmony export */ });\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\":\"f6edd2d1ee975ba6c7161426873f38cbfe50f23c\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#dfd9f7d577ded7fa0b1e18a8e2afa8a477ddd58d\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#prod-20220404\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#dfd9f7d577ded7fa0b1e18a8e2afa8a477ddd58d\"},\"built\":\"Wed Apr 06 2022 15:53:41 GMT-0400 (Eastern Daylight Time)\",\"config\":{\"generated\":\"Wed 06 Apr 2022 03:53:41 PM EDT by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"5.70.0\",\"node\":\"v12.22.11\"} !== 'undefined' && typeof window.Modal === 'undefined') {\n window.Modal = Modal\n }\n}\n\n\n//# sourceURL=webpack://dcp/./portal/www/js/modal.js?");
3824
+ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (/* binding */ Modal)\n/* harmony export */ });\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\":\"616fa5bc5f2d049ae1c1a9a30f3bd18b156fae01\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"4.2.2\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#prod-20220413\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#9a58c66e64194b3b2c0f5c6f257688ed23faeb9c\"},\"built\":\"Thu Apr 14 2022 13:43:18 GMT-0400 (Eastern Daylight Time)\",\"config\":{\"generated\":\"Thu 14 Apr 2022 01:43:15 PM EDT by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"5.70.0\",\"node\":\"v12.22.12\"} !== 'undefined' && typeof window.Modal === 'undefined') {\n window.Modal = Modal\n }\n}\n\n\n//# sourceURL=webpack://dcp/./portal/www/js/modal.js?");
3825
3825
 
3826
3826
  /***/ }),
3827
3827
 
@@ -3913,7 +3913,7 @@ eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_mod
3913
3913
  \*********************************/
3914
3914
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
3915
3915
 
3916
- eval("/**\n * @file src/dcp-error.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date Feb 2020\n *\n * A super-simple error class with a constructor that accepts an error 'code'\n * and 'info' in addition to the message. Also has another form which accepts an\n * error object to wrap as is second argument which is helpful for dealing with\n * Error Payloads.\n */\n\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n\nexports.DCPError = class DCPError extends Error {\n /**\n * @param {string} [message] - The error message\n * @param {Array<string | object> | Array<object> | Array} [args] - An array consisting of\n * an error code and info object, or an Error object to wrap.\n */\n constructor(message, ...args) {\n super(message);\n this.name = DCPError.name;\n this.timestamp = new Date();\n switch (typeof args[0]) {\n // Form 1: Error code and info object\n case 'string': {\n const [code = '', info = null] = args;\n if (code !== '') {\n this.code = code;\n }\n\n if (info !== null) {\n this.info = info;\n }\n this.stack = this.stack.replace(/\\n[^\\n]*\\n/, ''); /* remove constructor from stack */\n break;\n }\n // Form 2: Error object to wrap\n case 'object': {\n const [error] = args;\n if (error.code) {\n this.code = error.code;\n }\n\n if (error.info) {\n this.info = error.info;\n }\n\n if (error.process) {\n this.process = error.process;\n }\n\n if (error.timestamp) {\n this.timestamp = error.timestamp;\n }\n\n this.stack = `${this.stack}\\nRethrow ${error.stack}`;\n break;\n }\n default:\n break;\n }\n\n if (!this.process) {\n try {\n if ((__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").isBrowserPlatform)) {\n this.process = `<browser ${window.location.pathname}>`;\n } else {\n this.process = requireNative('path').basename(\n requireNative('process').argv[1] || '<node repl>',\n );\n }\n } catch (e) {\n console.error(e);\n }\n }\n }\n};\n\n\n//# sourceURL=webpack://dcp/./src/common/dcp-error.js?");
3916
+ eval("/**\n * @file src/dcp-error.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date Feb 2020\n *\n * A super-simple error class with a constructor that accepts an error 'code'\n * and 'info' in addition to the message. Also has another form which accepts an\n * error object to wrap as is second argument which is helpful for dealing with\n * Error Payloads.\n */\n\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n\nexports.DCPError = class DCPError extends Error {\n /**\n * @param {string} [message] - The error message\n * @param {Array<string | object> | Array<object> | Array} [args] - An array consisting of\n * an error code and info object, or an Error object to wrap.\n */\n constructor(message, ...args) {\n super(message);\n this.name = DCPError.name;\n this.timestamp = new Date();\n switch (typeof args[0]) {\n // Form 1: Error code and info object\n case 'string': {\n const [code = '', info = null] = args;\n if (code !== '') {\n this.code = code;\n }\n\n if (info !== null) {\n this.info = info;\n }\n this.stack = this.stack.replace(/\\n[^\\n]*\\n/, ''); /* remove constructor from stack */\n break;\n }\n // Form 2: Error object to wrap\n case 'object': {\n const [error] = args;\n if (error.code) {\n this.code = error.code;\n }\n\n if (error.info) {\n this.info = error.info;\n }\n\n if (error.process) {\n this.process = error.process;\n }\n\n if (error.timestamp) {\n this.timestamp = error.timestamp;\n }\n\n this.stack = `${this.stack}\\nRethrow ${error.stack ? error.stack : 'stack is not defined'}`;\n break;\n }\n default:\n break;\n }\n\n if (!this.process) {\n try {\n if ((__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").isBrowserPlatform)) {\n this.process = `<browser ${window.location.pathname}>`;\n } else {\n this.process = requireNative('path').basename(\n requireNative('process').argv[1] || '<node repl>',\n );\n }\n } catch (e) {\n console.error(e);\n }\n }\n }\n};\n\n\n//# sourceURL=webpack://dcp/./src/common/dcp-error.js?");
3917
3917
 
3918
3918
  /***/ }),
3919
3919
 
@@ -4025,7 +4025,7 @@ eval("/**\n * @file hash.js\n * General purpose utility rout
4025
4025
  /***/ ((__unused_webpack_module, exports) => {
4026
4026
 
4027
4027
  "use strict";
4028
- 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\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.jobStatus = new SchedulerConstantGroup(\n 'cancelled',\n 'corrupted',\n 'estimation',\n 'finished',\n 'running',\n 'paused',\n);\n\n/**\n * The list of all possible slice status in the status column of the\n * activeSlices table.\n */\nexports.sliceStatus = new SchedulerConstantGroup(\n 'overdue',\n 'tiebreaker',\n 'scheduled',\n 'working',\n 'paused',\n 'returned',\n 'new',\n);\n\n/** Currently used bit masks for flags column of jobs table. There is capacity for 31 bit masks. */\nexports.jobFlags = {\n localExec: 1 << 0, /* local exec job; prevent from joining compute groups */\n open: 1 << 1, /* job still open, i.e. able to add more slices */\n workerConsole: 1 << 2, /* job is allowed to log to worker's console - if worker permits also */\n force100pctCPUDensity: 1 << 29, /* Temporary flag that considers the wall clock vs cpu time */\n};\n\nfunction SchedulerConstantGroup()\n{\n var argv = Array.from(arguments);\n var dcpConfig = globalThis.hasOwnProperty('dcpConfig') ? globalThis.dcpConfig : {};\n\n for (let el of argv)\n this[el] = el;\n\n if (dcpConfig.build === 'debug')\n {\n let px = new Proxy(this, {\n get: (target, prop) => {\n if (!target.hasOwnProperty(prop))\n throw new Error(`no such constant: '${prop}'`);\n return target[prop];\n },\n set: (target, prop) => {\n throw new Error('constants are immutable!');\n },\n delete: (prop) => {\n throw new Error('constant groups are immutable!');\n },\n });\n\n return px;\n }\n}\n\nexports.workerIdLength = 22;\n\n\n/** Temporary arrays for backwards compatibility - do not use for new code */\nexports.sliceStatuses = Object.keys(exports.sliceStatus);\nexports.jobStatuses = Object.keys(exports.jobStatus);\n\n\n//# sourceURL=webpack://dcp/./src/common/scheduler-constants.js?");
4028
+ 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\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.jobStatus = new SchedulerConstantGroup(\n 'cancelled',\n 'corrupted',\n 'estimation',\n 'finished',\n 'running',\n 'paused',\n);\n\n/**\n * The list of all possible slice status in the status column of the\n * activeSlices table.\n */\nexports.sliceStatus = new SchedulerConstantGroup(\n 'overdue',\n 'tiebreaker',\n 'scheduled',\n 'working',\n 'paused',\n 'returned',\n 'new',\n);\n\n/** Currently used bit masks for flags column of jobs table. There is capacity for 31 bit masks. */\nexports.jobFlags = {\n localExec: 1 << 0, /* local exec job; prevent from joining compute groups */\n open: 1 << 1, /* job still open, i.e. able to add more slices */\n workerConsole: 1 << 2, /* job is allowed to log to worker's console - if worker permits also */\n greedyEstimation: 1 << 3, /* Allows job in estimation to have requestTask return multiple estimation slices. */\n force100pctCPUDensity: 1 << 29, /* Temporary flag that considers the wall clock vs cpu time */\n};\n\nfunction SchedulerConstantGroup()\n{\n var argv = Array.from(arguments);\n var dcpConfig = globalThis.hasOwnProperty('dcpConfig') ? globalThis.dcpConfig : {};\n\n for (let el of argv)\n this[el] = el;\n\n if (dcpConfig.build === 'debug')\n {\n let px = new Proxy(this, {\n get: (target, prop) => {\n if (!target.hasOwnProperty(prop))\n throw new Error(`no such constant: '${prop}'`);\n return target[prop];\n },\n set: (target, prop) => {\n throw new Error('constants are immutable!');\n },\n delete: (prop) => {\n throw new Error('constant groups are immutable!');\n },\n });\n\n return px;\n }\n}\n\nexports.workerIdLength = 22;\n\n\n/** Temporary arrays for backwards compatibility - do not use for new code */\nexports.sliceStatuses = Object.keys(exports.sliceStatus);\nexports.jobStatuses = Object.keys(exports.jobStatus);\n\n\n//# sourceURL=webpack://dcp/./src/common/scheduler-constants.js?");
4029
4029
 
4030
4030
  /***/ }),
4031
4031
 
@@ -4125,7 +4125,7 @@ eval("/**\n * @file password.js\n * Modal providing a way to
4125
4125
  \**********************************************/
4126
4126
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4127
4127
 
4128
- 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=f6edd2d1ee975ba6c7161426873f38cbfe50f23c'; /* 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://dcp/./src/dcp-client/client-modal/utils.js?");
4128
+ 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=616fa5bc5f2d049ae1c1a9a30f3bd18b156fae01'; /* 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://dcp/./src/dcp-client/client-modal/utils.js?");
4129
4129
 
4130
4130
  /***/ }),
4131
4131
 
@@ -4135,7 +4135,7 @@ eval("/**\n * @file client-modal/utils.js\n * @author KC Erb\n * @date Mar 2020\
4135
4135
  \************************************************/
4136
4136
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4137
4137
 
4138
- 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://dcp/./src/dcp-client/compute-groups/index.js?");
4138
+ 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(null, true);\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://dcp/./src/dcp-client/compute-groups/index.js?");
4139
4139
 
4140
4140
  /***/ }),
4141
4141
 
@@ -4155,7 +4155,7 @@ eval("/**\n * @file Module that implements Compute API\n * @module dcp/comput
4155
4155
  \*********************************/
4156
4156
  /***/ ((module, exports, __webpack_require__) => {
4157
4157
 
4158
- eval("/* module decorator */ module = __webpack_require__.nmd(module);\n/**\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 : 0\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-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/cjs/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\":\"f6edd2d1ee975ba6c7161426873f38cbfe50f23c\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#dfd9f7d577ded7fa0b1e18a8e2afa8a477ddd58d\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#prod-20220404\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#dfd9f7d577ded7fa0b1e18a8e2afa8a477ddd58d\"},\"built\":\"Wed Apr 06 2022 15:53:41 GMT-0400 (Eastern Daylight Time)\",\"config\":{\"generated\":\"Wed 06 Apr 2022 03:53:41 PM EDT by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"5.70.0\",\"node\":\"v12.22.11\"},\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\n//# sourceURL=webpack://dcp/./src/dcp-client/index.js?");
4158
+ eval("/* module decorator */ module = __webpack_require__.nmd(module);\n/**\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 : 0\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-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/cjs/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\":\"616fa5bc5f2d049ae1c1a9a30f3bd18b156fae01\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"4.2.2\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#prod-20220413\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#9a58c66e64194b3b2c0f5c6f257688ed23faeb9c\"},\"built\":\"Thu Apr 14 2022 13:43:18 GMT-0400 (Eastern Daylight Time)\",\"config\":{\"generated\":\"Thu 14 Apr 2022 01:43:15 PM EDT by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"5.70.0\",\"node\":\"v12.22.12\"},\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\n//# sourceURL=webpack://dcp/./src/dcp-client/index.js?");
4159
4159
 
4160
4160
  /***/ }),
4161
4161
 
@@ -4166,7 +4166,7 @@ eval("/* module decorator */ module = __webpack_require__.nmd(module);\n/**\n *
4166
4166
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4167
4167
 
4168
4168
  "use strict";
4169
- 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, createTempFile } = __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 { sliceStatus } = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst { jobStatus } = __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\");\nlet tunedKvin;\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 useStrict: 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(sliceStatus.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 this.force100pctCPUDensity = false;\n this.workerConsole = false;\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 /**\n * A number (can be null, undefined, or infinity) describing the estimationSlicesRemaining in the jpd (dcp-2593)\n * @type {number}\n * @access public\n */\n this.estimationSlices = undefined;\n \n /**\n * tunable parameters per job\n * @access public\n * @param {object} tuning \n * @param {string} tuning.kvin Encode the TypedArray into a string, trying multiple methods to determine optimum \n * size/performance. The this.tune variable affects the behavior of this code this:\n * @param {boolean} speed If true, only do naive encoding: floats get represented as byte-per-digit strings\n * @param {boolean} size If true, try the naive, ab8, and ab16 encodings; pick the smallest\n * If both are false try the naive encoding if under typedArrayPackThreshold and use if smaller\n * than ab8; otherwise, use ab8\n */\n this.tuning = {\n kvin: {\n size: false,\n speed: false,\n },\n }\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 const { encodeDataURI } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\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 owner: 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 const start = Date.now();\n this.emit('x-dbg-uploadStart', slicePile.length);\n payload = await this.deployConnection.send('addSliceData', {\n job: this.address,\n dataValues: kvinMarshal(slicePile),\n });\n if (!payload.success) {\n this.emit('x-dbg-uploadBackoff', slicePile.length);\n throw new DCPError('Cannot upload slice data to scheduler','EUPLOADSCHED');\n }\n else {\n this.emit('x-dbg-uploadProgress', Date.now() - start);\n break;\n }\n }\n catch (error)\n {\n if (--errorTolerance <= 0) {\n this.emit('x-dbg-uploadError', error);\n throw error;\n }\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;//this line will be removed in another ticket\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 pd.force100pctCPUDensity = this[INTERNAL_SYMBOL].force100pctCPUDensity;\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 if (this.estimationSlices === Infinity)\n this[INTERNAL_SYMBOL].payloadDetails.estimationSlices = null;\n else if (this.estimationSlices <= 0)\n throw new Error('Incorrect value for estimationSlices; it can be an integer or Infinity!');\n else\n this[INTERNAL_SYMBOL].payloadDetails.estimationSlices = this.estimationSlices;\n \n if(this.tuning.kvin.speed || this.tuning.kvin.size)\n {\n tunedKvin = new kvin.KVIN();\n tunedKvin.tune = 'size';\n if(this.tuning.kvin.speed)\n tunedKvin.tune = 'speed';\n // If both size and speed are true, kvin will optimize based on speed\n if(this.tuning.kvin.speed && this.tuning.kvin.size)\n console.log('Slices and arguments are being uploaded with speed optimization.');\n }\n /* eagerly connect to depedent services for better performance */\n this.eventSubscriber.eventRouterConnection.keepalive();\n this.deployConnection.keepalive();\n\n this.readyStateChange('exec');\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 this.readyStateChange('compute-groups');\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 // Upload slice data after CGs, but before `deployed` readystate.\n // This way, work can begin on the first slices while continuing to\n // upload additional slice input data\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 this.readyStateChange('uploading');\n\n await this.addSlices(data).then(() => {\n return this.close();\n });\n }\n\n this.readyStateChange('deployed');\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.readyStateChange('reconnected');\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 /**\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\n let errorMsg = event.reason;\n if (event.error)\n errorMsg = errorMsg +`\\n Recent error massage: ${event.error.message}`\n \n reject(new DCPError(errorMsg, event.code));\n };\n\n this[INTERNAL_SYMBOL].events.once('stopped', async (stopEvent) => {\n if (this.receivedStop)\n {\n // The result submitter will ensure the client receives the stop event through the event router\n // by repeatedly sending stop messages if it detects something might have gone wrong. Sometimes\n // this detection is 'overeager', causing multiple stop events to be sent by the result submitter.\n // If multiple are received, ignore all after the first one.\n return;\n }\n this.receivedStop = true;\n this.emit('stopped', stopEvent.runStatus);\n switch (stopEvent.runStatus) {\n case jobStatus.finished:\n if (this.collateResults) {\n let report = await this.getJobInfo();\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 if (remainSliceNumbers.length)\n {\n const 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 jobStatus.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 jobStatus.cancelled, jobStatus.finished, and sliceStatus.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) => {\n this[INTERNAL_SYMBOL].events.emit('stopped', ev)\n });\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.readyStateChange('preauth');\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.readyStateChange('deploying');\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 mvMultSlicePayment: +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 force100pctCPUDensity: this.force100pctCPUDensity,\n estimationSlices: payloadDetails.estimationSlices,\n workerConsole: this.workerConsole,\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 = kvinMarshal(payloadDetails.arguments.map(e => new URL(e)))\n } else if (payloadDetails.arguments) {\n try {\n submitPayload.marshaledArguments = kvinMarshal(Array.from(payloadDetails.arguments));\n } catch(e) {\n throw new Error(`Could not convert job arguments to Array (${e.message})`);\n }\n }\n \n if (payloadDetails.localExec)\n {\n const workFunctionFile = createTempFile('dcp-localExec-workFunction-XXXXXXXXX', 'js');\n const argumentsFile = createTempFile('dcp-localExec-arguments-XXXXXXXXX', 'js');\n \n // For allowed origins of the localexec worker. Only allow the origins (files in this case) in this list.\n this.localExecAllowedFiles = [workFunctionFile.filename, argumentsFile.filename];\n\n // get the workFunctionURI string before writing to file to prevent the need to double-decode the work function in the worker.\n const workFunction = await fetchURI(payloadDetails.workFunctionURI);\n workFunctionFile.writeSync(workFunction);\n \n const workFunctionFileURL = new URL('file://' + workFunctionFile);\n submitPayload.workFunctionURI = workFunctionFileURL.href;\n payloadDetails.workFunctionURI = workFunctionFileURL.href;\n \n if (submitPayload.marshaledArguments)\n {\n argumentsFile.writeSync(JSON.stringify(submitPayload.marshaledArguments));\n const argumentsFileURL = new URL('file://' + argumentsFile.filename);\n submitPayload.marshaledArguments = kvinMarshal([argumentsFileURL]);\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 // Yes, it is possible for deployed.payload to be undefined.\n if (deployed.payload) {\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 } else {\n throw new DCPError('Failed to submit job to scheduler', submitPayload);\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.readyStateChange('listeners');\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 this[INTERNAL_SYMBOL].payloadDetails.estimationSlices = 0;\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 allowedOrigins: this.localExecAllowedFiles,\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/**\n * marshal the value using kvin or instance of the kvin (tunedKvin)\n * tunedKvin is defined if job.tuning.kvin is specified.\n *\n * @param {any} value \n * @return {object} A marshaled object\n * \n */\nfunction kvinMarshal (value) {\n if (tunedKvin)\n return tunedKvin.marshal(value);\n\n return kvin.marshal(value);\n}\n\nObject.assign(exports, {\n Job,\n SlicePaymentOffer,\n ResultHandle,\n});\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/job/index.js?");
4169
+ 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, createTempFile } = __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 { sliceStatus } = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst { jobStatus } = __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\");\nlet tunedKvin;\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 useStrict: 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(sliceStatus.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 this.force100pctCPUDensity = false;\n this.workerConsole = false;\n\n // The following 3 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 /**\n * A number (can be null, undefined, or infinity) describing the estimationSlicesRemaining in the jpd (dcp-2593)\n * @type {number}\n * @access public\n */\n this.estimationSlices = undefined;\n \n /**\n * tunable parameters per job\n * @access public\n * @param {object} tuning \n * @param {string} tuning.kvin Encode the TypedArray into a string, trying multiple methods to determine optimum \n * size/performance. The this.tune variable affects the behavior of this code this:\n * @param {boolean} speed If true, only do naive encoding: floats get represented as byte-per-digit strings\n * @param {boolean} size If true, try the naive, ab8, and ab16 encodings; pick the smallest\n * If both are false try the naive encoding if under typedArrayPackThreshold and use if smaller\n * than ab8; otherwise, use ab8\n */\n this.tuning = {\n kvin: {\n size: false,\n speed: false,\n },\n }\n\n /**\n * When true, allows a job in estimation to have requestTask return multiple estimation slices.\n * This flag applies independent of infinite estimation, viz., this.estimationSlices === null .\n * @type {boolean}\n * @access public\n */\n this.greedyEstimation = false;\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 const { encodeDataURI } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\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 owner: 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 const start = Date.now();\n this.emit('x-dbg-uploadStart', slicePile.length);\n payload = await this.deployConnection.send('addSliceData', {\n job: this.address,\n dataValues: kvinMarshal(slicePile),\n });\n if (!payload.success) {\n this.emit('x-dbg-uploadBackoff', slicePile.length);\n throw new DCPError('Cannot upload slice data to scheduler','EUPLOADSCHED');\n }\n else {\n this.emit('x-dbg-uploadProgress', Date.now() - start);\n break;\n }\n }\n catch (error)\n {\n if (--errorTolerance <= 0) {\n this.emit('x-dbg-uploadError', error);\n throw error;\n }\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;//this line will be removed in another ticket\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 pd.force100pctCPUDensity = this[INTERNAL_SYMBOL].force100pctCPUDensity;\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 if (this.estimationSlices === Infinity)\n this[INTERNAL_SYMBOL].payloadDetails.estimationSlices = null;\n else if (this.estimationSlices <= 0)\n throw new Error('Incorrect value for estimationSlices; it can be an integer or Infinity!');\n else\n this[INTERNAL_SYMBOL].payloadDetails.estimationSlices = this.estimationSlices;\n\n this[INTERNAL_SYMBOL].payloadDetails.greedyEstimation = this.greedyEstimation;\n\n if(this.tuning.kvin.speed || this.tuning.kvin.size)\n {\n tunedKvin = new kvin.KVIN();\n tunedKvin.tune = 'size';\n if(this.tuning.kvin.speed)\n tunedKvin.tune = 'speed';\n // If both size and speed are true, kvin will optimize based on speed\n if(this.tuning.kvin.speed && this.tuning.kvin.size)\n console.log('Slices and arguments are being uploaded with speed optimization.');\n }\n /* eagerly connect to depedent services for better performance */\n this.eventSubscriber.eventRouterConnection.keepalive();\n this.deployConnection.keepalive();\n\n this.readyStateChange('exec');\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 this.emit('accepted');\n\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 this.readyStateChange('compute-groups');\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 // Upload slice data after CGs, but before `deployed` readystate.\n // This way, work can begin on the first slices while continuing to\n // upload additional slice input data\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 this.readyStateChange('uploading');\n\n await this.addSlices(data).then(() => {\n return this.close();\n });\n }\n\n this.readyStateChange('deployed');\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.readyStateChange('reconnected');\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 /**\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\n let errorMsg = event.reason;\n if (event.error)\n errorMsg = errorMsg +`\\n Recent error massage: ${event.error.message}`\n \n reject(new DCPError(errorMsg, event.code));\n };\n\n this[INTERNAL_SYMBOL].events.once('stopped', async (stopEvent) => {\n if (this.receivedStop)\n {\n // The result submitter will ensure the client receives the stop event through the event router\n // by repeatedly sending stop messages if it detects something might have gone wrong. Sometimes\n // this detection is 'overeager', causing multiple stop events to be sent by the result submitter.\n // If multiple are received, ignore all after the first one.\n return;\n }\n this.receivedStop = true;\n this.emit('stopped', stopEvent.runStatus);\n switch (stopEvent.runStatus) {\n case jobStatus.finished:\n if (this.collateResults) {\n let report = await this.getJobInfo();\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 if (remainSliceNumbers.length)\n {\n const 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 jobStatus.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 jobStatus.cancelled, jobStatus.finished, and sliceStatus.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) => {\n this[INTERNAL_SYMBOL].events.emit('stopped', ev)\n });\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.readyStateChange('preauth');\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.readyStateChange('deploying');\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 mvMultSlicePayment: +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 force100pctCPUDensity: this.force100pctCPUDensity,\n estimationSlices: payloadDetails.estimationSlices,\n greedyEstimation: payloadDetails.greedyEstimation,\n workerConsole: this.workerConsole,\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 = kvinMarshal(payloadDetails.arguments.map(e => new URL(e)))\n } else if (payloadDetails.arguments) {\n try {\n submitPayload.marshaledArguments = kvinMarshal(Array.from(payloadDetails.arguments));\n } catch(e) {\n throw new Error(`Could not convert job arguments to Array (${e.message})`);\n }\n }\n \n if (payloadDetails.localExec)\n {\n const workFunctionFile = createTempFile('dcp-localExec-workFunction-XXXXXXXXX', 'js');\n const argumentsFile = createTempFile('dcp-localExec-arguments-XXXXXXXXX', 'js');\n \n // For allowed origins of the localexec worker. Only allow the origins (files in this case) in this list.\n this.localExecAllowedFiles = [workFunctionFile.filename, argumentsFile.filename];\n\n // get the workFunctionURI string before writing to file to prevent the need to double-decode the work function in the worker.\n const workFunction = await fetchURI(payloadDetails.workFunctionURI);\n workFunctionFile.writeSync(workFunction);\n \n const workFunctionFileURL = new URL('file://' + workFunctionFile);\n submitPayload.workFunctionURI = workFunctionFileURL.href;\n payloadDetails.workFunctionURI = workFunctionFileURL.href;\n \n if (submitPayload.marshaledArguments)\n {\n argumentsFile.writeSync(JSON.stringify(submitPayload.marshaledArguments));\n const argumentsFileURL = new URL('file://' + argumentsFile.filename);\n submitPayload.marshaledArguments = kvinMarshal([argumentsFileURL]);\n }\n }\n\n // XXXpfr Excellent tracing.\n if (debugging('dcp-client')) {\n dumpObject(submitPayload, 'Submit: Job Index: Examine submitPayload', 256);\n }\n\n // Deploy the job!\n const deployed = await this.deployConnection.send('submit', submitPayload, myId);\n\n if (!deployed.success) {\n // Yes, it is possible for deployed.payload to be undefined.\n if (deployed.payload) {\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 } else {\n throw new DCPError('Failed to submit job to scheduler', submitPayload);\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.readyStateChange('listeners');\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 this[INTERNAL_SYMBOL].payloadDetails.estimationSlices = 0;\n this[INTERNAL_SYMBOL].payloadDetails.greedyEstimation = false;\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 allowedOrigins: this.localExecAllowedFiles,\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/**\n * marshal the value using kvin or instance of the kvin (tunedKvin)\n * tunedKvin is defined if job.tuning.kvin is specified.\n *\n * @param {any} value \n * @return {object} A marshaled object\n * \n */\nfunction kvinMarshal (value) {\n if (tunedKvin)\n return tunedKvin.marshal(value);\n\n return kvin.marshal(value);\n}\n\nObject.assign(exports, {\n Job,\n SlicePaymentOffer,\n ResultHandle,\n});\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/job/index.js?");
4170
4170
 
4171
4171
  /***/ }),
4172
4172
 
@@ -4187,7 +4187,7 @@ eval("/**\n * @file node-modules.js Node-specific support for sen
4187
4187
  \*********************************************/
4188
4188
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4189
4189
 
4190
- 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((keysList, valueAvailable, sliceNumber) => {\n if (valueAvailable) keysList.push(this[RH_INTERNAL_SYMBOL].keys[sliceNumber - 1]);\n return keysList;\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((keyValuePairs, valueAvailable, sliceNumber) => {\n if (valueAvailable) keyValuePairs.push([\n String(this[RH_INTERNAL_SYMBOL].keys[sliceNumber - 1]),\n this[RH_INTERNAL_SYMBOL].values[sliceNumber],\n ]);\n return keyValuePairs;\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] - if true, emits a `result` event for new results as they are added to the handle, or emits for all results if set to 'all'\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\n if (emitEvents && (emitEvents === 'all' || !this[RH_INTERNAL_SYMBOL].valuesAvailable[r.slice])) {\n job.emit('result', { sliceNumber: r.slice, result: this[RH_INTERNAL_SYMBOL].values[r.slice] });\n }\n this[RH_INTERNAL_SYMBOL].valuesAvailable[r.slice] = true;\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://dcp/./src/dcp-client/job/result-handle.js?");
4190
+ 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((keysList, valueAvailable, sliceNumber) => {\n if (valueAvailable) keysList.push(this[RH_INTERNAL_SYMBOL].keys[sliceNumber - 1]);\n return keysList;\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((keyValuePairs, valueAvailable, sliceNumber) => {\n if (valueAvailable) keyValuePairs.push([\n String(this[RH_INTERNAL_SYMBOL].keys[sliceNumber - 1]),\n this[RH_INTERNAL_SYMBOL].values[sliceNumber],\n ]);\n return keyValuePairs;\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] - if true, emits a `result` event for new results as they are added to the handle, or emits for all results if set to 'all'\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\n if (emitEvents && (emitEvents === 'all' || !this[RH_INTERNAL_SYMBOL].valuesAvailable[r.slice])) {\n job.emit('result', { sliceNumber: r.slice, result: this[RH_INTERNAL_SYMBOL].values[r.slice] });\n }\n this[RH_INTERNAL_SYMBOL].valuesAvailable[r.slice] = true;\n }));\n\n job.emit('resultsUpdated');\n await conn.close(null, true);\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://dcp/./src/dcp-client/job/result-handle.js?");
4191
4191
 
4192
4192
  /***/ }),
4193
4193
 
@@ -4257,7 +4257,7 @@ eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_mod
4257
4257
  \*************************************************/
4258
4258
  /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
4259
4259
 
4260
- 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=f6edd2d1ee975ba6c7161426873f38cbfe50f23c,' + Date.now() + hash;\n\n window.location.replace(newUrl);\n }\n}\n\nObject.assign(module.exports, {\n SchedMsgWeb\n});\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/schedmsg/schedmsg-web.js?");
4260
+ 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=616fa5bc5f2d049ae1c1a9a30f3bd18b156fae01,' + Date.now() + hash;\n\n window.location.replace(newUrl);\n }\n}\n\nObject.assign(module.exports, {\n SchedMsgWeb\n});\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/schedmsg/schedmsg-web.js?");
4261
4261
 
4262
4262
  /***/ }),
4263
4263
 
@@ -4444,7 +4444,7 @@ eval("/**\n * @file worker/supervisor-cache.js\n *\n * A cache for the superviso
4444
4444
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4445
4445
 
4446
4446
  "use strict";
4447
- eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_modules/process/browser.js */ \"./node_modules/process/browser.js\");\n/**\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 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, SLICE_STATUS_UNASSIGNED, SLICE_STATUS_FAILED } = __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, a$sleepMs, justFetch, compressJobArray, toJobMap,\n compressSandboxes, compressSlices, truncateAddress, dumpSandboxesIfNotUnique, dumpSlicesIfNotUnique, generateOpaqueId } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst { sliceStatus } = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst { calculateJoinHash } = __webpack_require__(/*! dcp/dcp-client/compute-groups */ \"./src/dcp-client/compute-groups/index.js\");\n\nconst supervisorTuning = dcpConfig.future('worker.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 {*} address\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 /** @type {Sandbox[]} */\n this.readiedSandboxes = [];\n\n /** @type {Sandbox[]} */\n this.assignedSandboxes = [];\n\n /** @type {Slice[]} */\n this.slices = [];\n\n /** @type {Slice[]} */\n this.queuedSlices = [];\n\n /** @type {Slice[]} */\n this.lostSlices = [];\n\n /** @type {boolean} */\n this.matching = false;\n\n /** @type {boolean} */\n this.isFetchingNewWork = false;\n\n /** @type {number} */\n this.numberOfCoresReserved = 0;\n\n /** @type {number} */\n this.addressTruncationLength = 20; // Set to -1 for no truncation.\n\n /** @type {Object[]} */\n this.rejectedJobs = [];\n this.rejectedJobReasons = [];\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 // In localExec, do not allow work function or arguments to come from the 'any' origins\n this.allowedOrigins = []\n if (this.options.localExec)\n {\n dcpConfig.worker.allowOrigins.fetchData = dcpConfig.worker.allowOrigins.fetchData.concat(dcpConfig.worker.allowOrigins.any)\n dcpConfig.worker.allowOrigins.sendResults = dcpConfig.worker.allowOrigins.sendResults.concat(dcpConfig.worker.allowOrigins.any)\n }\n else\n this.allowedOrigins = dcpConfig.worker.allowOrigins.any;\n\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.allocatedSandboxes.filter(sb => !!sb.requiresGPU).length,\n // enumerable: true,\n // configurable: false,\n // });\n\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 /** @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(), tuning.watchdogInterval * 1000);\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 !== constants.workerIdLength) {\n this._workerOpaqueId = generateOpaqueId();\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 if (!this.taskDistributorConnection)\n this.openTaskDistributorConn();\n \n if (!this.eventRouterConnection)\n this.openEventRouterConn();\n \n if (!this.resultSubmitterConnection)\n this.openResultSubmitterConn();\n\n if (!this.packageManagerConnection)\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 properties and functions for creating filtered views\n // of the slices and sandboxes array.\n //\n /** XXXpfr @todo Write sort w/o using promises so we can get rid of async on all the compress functions. */\n\n /**\n * @deprecated -- Please do not use this.workingSandboxes; use this.allocatedSandboxes instead.\n * Sandboxes that are in WORKING state.\n *\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 * Use instead of this.workingSandboxes.\n *\n * When a sandbox is paired with a slice, execution is pending and sandbox.allocated=true and\n * sandbox.slice=slice and sandbox.jobAddress=slice.jobAddress. This is what 'allocated' means.\n * Immediately upon the exit of sandbox.work, sandbox.allocated=false is set and if an exception\n * wasn't thrown the sandbox is placed in this.assignedSandboxes.\n * Thus from the pov of supervisor, this.allocatedSandboxes is deterministic and this.workingSandboxes is not.\n * Please try to not use this.workingSandboxes. It is deprecated.\n *\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Sandbox[]}\n */\n get allocatedSandboxes() {\n return this.sandboxes.filter(sandbox => sandbox.allocated);\n }\n\n /**\n * Slices that are allocated.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Slice[]}\n */\n get allocatedSlices() {\n return this.slices.filter(slice => slice.allocated);\n }\n\n /**\n * This property is used as the target number of sandboxes to be associated with slices and start 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 start 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.allocatedSandboxes.length - this.numberOfCoresReserved;\n }\n \n /**\n * Call acquire(numberOfCoresToReserve) to reserve numberOfCoresToReserve unallocated sandboxes as measured by unallocatedSpace.\n * Call release() to undo the previous acquire.\n * This pseudo-mutex technique helps prevent races in scheduling slices in Supervisor.\n * @param {number} numberOfCoresToReserve\n */\n acquire(numberOfCoresToReserve) { \n this.numberOfCoresReserved = numberOfCoresToReserve; \n }\n release() { \n this.numberOfCoresReserved = 0; \n }\n\n /**\n * Remove from this.slices.\n * @param {Slice} slice\n */\n removeSlice(slice) {\n this.removeElement(this.slices, slice);\n if (Supervisor.debugBuild) {\n if (this.queuedSlices.indexOf(slice) !== -1)\n throw new Error(`removeSlice: slice ${slice.identifier} is in queuedSlices; inconsistent state.`);\n if (this.lostSlices.length > 0) {\n console.warn(`removeSlice: slice ${slice.identifier}, found lostSlices ${this.lostSlices.map(s => s.identifier)}`);\n if (this.lostSlices.indexOf(slice) !== -1)\n throw new Error(`removeSlice: slice ${slice.identifier} is in lostSlices; inconsistent state.`);\n }\n }\n }\n\n /**\n * Remove from this.slices.\n * @param {Slice[]} slices\n */\n removeSlices(slices) {\n this.slices = this.slices.filter(slice => slices.indexOf(slice) === -1);\n }\n\n /**\n * Remove from this.queuedSlices.\n * @param {Slice[]} slices\n */\n removeQueuedSlices(slices) {\n this.queuedSlices = this.queuedSlices.filter(slice => slices.indexOf(slice) === -1);\n }\n\n /**\n * Remove from this.sandboxes and this.assignedSandboxes .\n * @param {Sandbox} sandbox\n */\n removeSandbox(sandbox) {\n debugging('scheduler') && console.log(`removeSandbox ${sandbox.identifier}`);\n this.removeElement(this.sandboxes, sandbox);\n this.removeElement(this.assignedSandboxes, sandbox);\n \n if (Supervisor.debugBuild && this.readiedSandboxes.indexOf(sandbox) !== -1)\n throw new Error(`removeSandbox: sandbox ${sandbox.identifier} is in readiedSandboxes; inconsistent state.`);\n }\n\n /**\n * Remove from this.sandboxes and this.assignedSandboxes .\n * @param {Sandbox[]} sandboxes\n */\n async removeSandboxes(sandboxes) {\n debugging('scheduler') && console.log(`removeSandboxes: Remove ${sandboxes.length} sandboxes ${await this.dumpSandboxes(sandboxes)}`);\n this.sandboxes = this.sandboxes.filter(sandbox => sandboxes.indexOf(sandbox) === -1);\n this.assignedSandboxes = this.assignedSandboxes.filter(sandbox => sandboxes.indexOf(sandbox) === -1);\n \n if (Supervisor.debugBuild) {\n const readied = this.readiedSandboxes.filter(sandbox => sandboxes.indexOf(sandbox) !== -1);\n if (readied.length > 0)\n throw new Error(`removeSandboxes: sandboxes ${readied.map(s => s.identifier)} are in readiedSandboxes; inconsistent state.`);\n }\n }\n\n /**\n * Remove element from theArray.\n * @param {Array<*>} theArray\n * @param {object|number} element\n * @param {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 * Log sliceArray.\n * @param {Slice[]} sliceArray\n * @param {string} [header]\n * @returns {Promise<string>}\n */\n async dumpSlices(sliceArray, header) {\n if (header) console.log(`\\n${header}`);\n return compressSlices(sliceArray, this.addressTruncationLength);\n }\n\n /**\n * Log sandboxArray.\n * @param {Sandbox[]} sandboxArray\n * @param {string} [header]\n * @returns {Promise<string>}\n */\n async dumpSandboxes(sandboxArray, header) {\n if (header) console.log(`\\n${header}`);\n return compressSandboxes(sandboxArray, this.addressTruncationLength);\n }\n\n /**\n * If the elements of sandboxSliceArray are not unique, log the duplicates and dump the array.\n * @param {SandboxSlice[]} sandboxSliceArray\n * @param {string} header\n */\n dumpSandboxSlicesIfNotUnique(sandboxSliceArray, header) {\n if (!this.isUniqueSandboxSlices(sandboxSliceArray, header))\n console.log(this.dumpSandboxSlices(sandboxSliceArray));\n }\n\n /**\n * Log { sandbox, slice }.\n * @param {Sandbox} sandbox\n * @param {Slice} slice\n * @returns {string}\n */\n dumpSandboxAndSlice(sandbox, slice) {\n return `${sandbox.id}~${slice.sliceNumber}.${this.dumpJobAddress(slice.jobAddress)}`;\n }\n\n /**\n * Log { sandbox, slice } with state/status.\n * @param {Sandbox} sandbox\n * @param {Slice} slice\n * @returns {string}\n */\n dumpStatefulSandboxAndSlice(sandbox, slice) {\n return `${sandbox.id}.${sandbox.state}~${slice.sliceNumber}.${this.dumpJobAddress(slice.jobAddress)}.${slice.status}`;\n }\n\n /**\n * Truncates jobAddress.toString() to this.addressTruncationLength digits.\n * @param {address} jobAddress\n * @returns {string}\n */\n dumpJobAddress(jobAddress) {\n return truncateAddress(jobAddress, this.addressTruncationLength /* digits*/);\n }\n\n /**\n * Dump sandboxSliceArray.\n * @param {SandboxSlice[]} sandboxSliceArray - input array of { sandbox, slice }\n * @param {string} [header] - optional header\n * @param {boolean} [stateFul] - when true, also includes slice.status and sandbox.state.\n * @returns {string}\n */\n dumpSandboxSlices(sandboxSliceArray, header, stateFul=false) {\n if (header) console.log(`\\n${header}`);\n const jobMap = {};\n sandboxSliceArray.forEach(ss => {\n const sss = stateFul ? `${ss.sandbox.id}.${ss.sandbox.state}~${ss.slice.sliceNumber}.${ss.slice.status}` : `${ss.sandbox.id}~${ss.slice.sliceNumber}`;\n if (!jobMap[ss.slice.jobAddress]) jobMap[ss.slice.jobAddress] = sss;\n else jobMap[ss.slice.jobAddress] += `,${sss}`;\n });\n let output = '';\n for (const [jobAddress, sss] of Object.entries(jobMap))\n output += `${this.dumpJobAddress(jobAddress)}:[${sss}]:`;\n return output;\n }\n\n /**\n * Check sandboxSliceArray for duplicates.\n * @param {SandboxSlice[]} sandboxSliceArray\n * @param {string} [header]\n * @param {function} [log]\n * @returns {boolean}\n */\n isUniqueSandboxSlices(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(`\\tWarning: Found duplicate sandbox ${x.sandbox.identifier}.`);\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(`\\tWarning: Found duplicate slice ${x.slice.identifier}.`);\n } else {\n slices.push(x.slice);\n if (sandboxIndex < 0) result.push(x);\n }\n });\n return sandboxSliceArray.length === result.length;\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 when allocateLocalSandboxes is false.\n *\n * @param {number} numSandboxes - the number of sandboxes to create\n * @param {boolean} [allocateLocalSandboxes=false] - when true, do not place in this.readiedSandboxes\n * @returns {Promise<Sandbox[]>} - resolves with array of created sandboxes, rejects otherwise\n * @throws when given a numSandboxes is not a number or if numSandboxes is Infinity\n */\n async readySandboxes (numSandboxes, allocateLocalSandboxes = false) {\n debugging('supervisor') && console.debug(`readySandboxes: Readying ${numSandboxes} sandboxes, total sandboxes ${this.sandboxes.length}, matching ${this.matching}, fetching ${this.isFetchingNewWork}`);\n \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 // !authorizationMessage <==> sliceNumber === 0.\n const authorizationMessage = sandbox.slice.getAuthorizationMessage();\n\n if (authorizationMessage) {\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 });\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 }\n else\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 // Send a work emit message from the sandbox to the event router\n // !authorizationMessage <==> sliceNumber === 0.\n const authorizationMessage = sandbox.slice.getAuthorizationMessage();\n \n if (!authorizationMessage)\n {\n console.warn(`workEmit: missing authorization message for job ${jobAddress}, slice: ${sliceNumber}`);\n return Promise.resolve();\n }\n \n const workEmitPromise = this.eventRouterConnection.send('workEmit', {\n eventName,\n payload,\n job: jobAddress,\n slice: sliceNumber,\n worker: this.workerOpaqueId,\n authorizationMessage,\n }).catch(error => {\n console.warn(`workEmit: unable to send message to event router ${error.message}`);\n if (Supervisor.debugBuild)\n console.error('workEmit error:', error);\n });\n\n if (Supervisor.debugBuild) {\n workEmitPromise.then(result => {\n if (!result || !result.success)\n console.warn('workEmit: event router did not accept event', result);\n });\n }\n }\n });\n\n // When any sbx completes, \n sandbox.addListener('complete', () => {\n this.watchdog();\n });\n\n sandbox.on('sandboxError', (error) => handleSandboxError(this, sandbox, error));\n \n sandbox.on('rejectedWorkMetrics', (data) =>{\n function updateRejectedMetrics(report) {\n ['total', 'CPU', 'webGL'].forEach((key) => {\n if (report[key]) sandbox.slice.rejectedTimeReport[key] += report[key];\n })\n }\n \n // If the slice already has rejected metrics, add this data to it. If not, assign this data to slices rejected metrics property\n if (sandbox.slice) {\n (sandbox.slice.rejectedTimeReport) ? updateRejectedMetrics(data.timeReport) : sandbox.slice.rejectedTimeReport = data.timeReport;\n }\n })\n \n // If the sandbox terminated and we are not shutting down, then should return all work which is currently\n // not being computed if all sandboxes are dead and the attempt to create a new one fails.\n sandbox.on('terminated',async () => {\n if (this.sandboxes.length > 0) {\n let terminatedSandboxes = this.sandboxes.filter(sbx => sbx.isTerminated);\n if (terminatedSandboxes.length === this.sandboxes.length) {\n debugging('supervisor') && console.debug(`readySandboxes: Create 1 sandbox in the sandbox-terminated-handler, total sandboxes ${this.sandboxes.length}, matching ${this.matching}, fetching ${this.isFetchingNewWork}`);\n await this.readySandboxes(1);\n \n // If we cannot create a new sandbox, that probably means we're on a screensaver worker\n // and the screensaver is down. So return the slices to the scheduler.\n if (this.sandboxes.length !== terminatedSandboxes.length + 1) {\n this.returnSlices(this.queuedSlices).then(() => {\n this.queuedSlices.length = 0;\n });\n }\n }\n }\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 if (!allocateLocalSandboxes) this.readiedSandboxes.push(sandbox);\n this.sandboxes.push(sandbox);\n sandboxes.push(sandbox);\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 debugging('supervisor') && console.log(`readySandboxes: Readied ${sandboxes.length} sandboxes ${JSON.stringify(sandboxes.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 * @param {boolean} [verifySandboxIsNotTerminated=true] - if true, check sandbox is not already terminated\n */\n returnSandbox (sandbox, slice, verifySandboxIsNotTerminated=true) {\n if (!slice || slice.failed || sandbox.isTerminated) {\n debugging('supervisor') && console.log(`Supervisor.returnSandbox: ${sandbox.identifier}${slice ? `~${slice.identifier}` : ''}, # of sandboxes ${this.sandboxes.length}`);\n \n this.removeSandbox(sandbox);\n \n if (!sandbox.isTerminated) {\n sandbox.terminate(false);\n } else {\n if (Supervisor.debugBuild && verifySandboxIsNotTerminated)\n throw new Error('returnSandbox: Unexpected terminated sandbox ${sandbox.identifier}.')\n }\n }\n }\n\n /**\n * Terminates sandboxes, in order of creation, when the total started sandboxes exceeds the total allowed sandboxes.\n *\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 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('supervisor') && console.log(`pruneSandboxes: Terminating readied sandbox ${sandbox.identifier}`);\n this.readiedSandboxes.splice(index, 1);\n this.returnSandbox(sandbox);\n\n if (--numOver <= 0) break;\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('supervisor') && console.log(`pruneSandboxes: Terminating assigned sandbox ${sandbox.identifier}`);\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 //\n // Note: this.slices is the disjoint union of:\n // this.allocatedSlices, \n // this.queuedSlices, \n // this.slices.filter(slice => !slice.isUnassigned) .\n // When a slice is not in these 3 arrays, the slice is lost.\n //\n \n const currentLostSlices = this.slices.filter(slice => slice.isUnassigned \n && this.queuedSlices.indexOf(slice) === -1\n && this.allocatedSlices.indexOf(slice) === -1);\n\n if (currentLostSlices.length > 0) {\n this.lostSlices.push(...currentLostSlices);\n // Try to recover.\n // Needs more work and testing.\n // Test when we can come up with a decent lost slice repro case.\n // --> this.queuedSlices.push(...currentLostSlices);\n }\n\n if (this.lostSlices.length > 0) {\n if (true) { // Keep this on for awhile, until we know lost slices aren't happening.\n console.warn('Supervisor.watchdog: Found lost slices!');\n for (const slice of this.lostSlices)\n console.warn('\\t', slice.identifier);\n }\n this.lostSlices = this.lostSlices.filter(slice => slice.isUnassigned);\n }\n\n const slices = [];\n this.queuedSlices.forEach(slice => {\n assert(slice && slice.sliceNumber > 0);\n addToSlicePayload(slices, slice, sliceStatus.scheduled);\n });\n\n this.allocatedSlices.forEach(slice => {\n assert(slice && slice.sliceNumber > 0);\n addToSlicePayload(slices, slice, 'progress'); // Beacon.\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 * Returns the number of unallocated sandbox slots to send to fetchTask.\n *\n * @returns {number}\n */\n numberOfAvailableSandboxSlots() {\n let numCores;\n if (this.options.priorityOnly && this.options.jobAddresses.length === 0) {\n numCores = 0;\n } else if (this.queuedSlices.length > 1) {\n // We have slices queued, no need to fetch\n numCores = 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 the last 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 numCores = this.unallocatedSpace - longSliceCount;\n }\n return numCores;\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 slices are fetched it calls this.distributeQueuedSlices.\n *\n * @returns {Promise<void>}, unallocatedSpace ${this.unallocatedSpace}\n */\n async work()\n {\n // When inside matchSlicesWithSandboxes, don't reenter Supervisor.work to fetch new work or create new sandboxes.\n if (this.matching) {\n // Interesting and noisy.\n // debugging('supervisor') && console.log(`Supervisor.work: Do not interleave work, fetch or matching slices with sandboxes: queuedSlices ${this.queuedSlices.length}, unallocatedSpace ${this.unallocatedSpace}, matching ${this.matching}, fetching ${this.isFetchingNewWork}`);\n return Promise.resolve();\n }\n\n await this.setDefaultIdentityKeystore();\n\n // Instantiate connections that don't exist.\n this.instantiateAllConnections();\n\n const numCores = this.numberOfAvailableSandboxSlots();\n\n debugging() && console.log(`Supervisor.work: Try to get ${numCores} slices in working sandboxes, unallocatedSpace ${this.unallocatedSpace}, queued slices ${this.queuedSlices.length}, # of sandboxes ${this.sandboxes.length}, matching ${this.matching}, fetching: ${this.isFetchingNewWork}`);\n \n // Fetch a new task if we have no more slices queued, then start workers\n try {\n if (numCores > 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 if (this.maxWorkingSandboxes > this.sandboxes.length) {\n // Note: The old technique had \n // while (this.maxWorkingSandboxes > this.sandboxes.length) {....\n // and sometimes we'd get far too many sandboxes, because it would keep looping while waiting for\n // this.readySandboxes(this.maxWorkingSandboxes - this.sandboxes.length);\n // to construct the rest of the sandboxes. The fix is to only loop when the 1st \n // await this.readySandboxes(1) \n // is failing.\n let needFirstSandbox = true;\n while (needFirstSandbox) {\n debugging('supervisor') && console.log(`Supervisor.work: ready 1 sandbox, # of sandboxes ${this.sandboxes.length}, matching ${this.matching}, fetching ${this.isFetchingNewWork}`);\n await this.readySandboxes(1)\n .then(() => {\n debugging('supervisor') && console.log(`Supervisor.work: ready ${this.maxWorkingSandboxes - this.sandboxes.length} sandbox(es), # of sandboxes ${this.sandboxes.length}, matching ${this.matching}, fetching ${this.isFetchingNewWork}`);\n this.readySandboxes(this.maxWorkingSandboxes - this.sandboxes.length);\n needFirstSandbox = false;\n }).catch(error => {\n console.warn('906: failed to ready sandboxes; will retry', error.code, error.message);\n });\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.close('Fetch timed out', Math.random() > 0.5).catch(error => {\n console.error(`931: Failed to close task-distributor connection`, error);\n });\n this.resultSubmitterConnection.close('Fetch timed out', Math.random() > 0.5).catch(error => {\n console.error(`920: Failed to close result-submitter connection`, error);\n });\n this.isFetchingNewWork = false;\n this.instantiateAllConnections();\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 (e)\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: ${e}`);\n this.isFetchingNewWork = false; // <-- done in the `finally` block, below\n clearTimeout(fetchTimeout);\n 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.resultSubmitterConnection.close('Failed to connect to result-submitter', true).catch(error => {\n console.error(`942: Failed to close result-submitter connection`, error);\n });\n return Promise.resolve();\n }\n await this.fetchTask(numCores).finally(() => {\n clearTimeout(fetchTimeout);\n this.isFetchingNewWork = false;\n });\n }\n\n this.distributeQueuedSlices().then(() => debugging('supervisor') && 'supervisor: finished distributeQueuedSlices()').catch((e) => {\n // We should never get here, because distributeQueuedSlices was changed\n // to try to catch everything and return slices and sandboxes.\n // If we do catch here it may mean a slice was lost. \n console.error('Supervisor.work catch handler for distributeQueuedSlices.', e);\n });\n // No catch(), because it will bubble outward to the caller\n } finally {\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 * Remove all unreferenced jobs in this.cache .\n * @param {*[]} newJobs -- Jobs that should not be removed from this.cache.\n */\n cleanJobCache(newJobs = []) {\n /* Delete all jobs in the supervisorCache that are not represented in this newJobs,\n * or in this.queuedSlices, or there is no sandbox assigned to these jobs.\n * Note: There can easily be 200+ places to check; using a lookup structure to maintain O(n).\n */\n if (this.cache.jobs.length > 0) {\n const jobAddressMap = {};\n Object.keys(newJobs).forEach(jobAddress => { jobAddressMap[jobAddress] = 1; });\n this.slices.forEach(slice => { if (!jobAddressMap[slice.jobAddress]) jobAddressMap[slice.jobAddress] = 1; });\n this.cache.jobs.forEach(jobAddress => {\n if (!jobAddressMap[jobAddress]) {\n this.cache.remove('job', jobAddress);\n // Remove and return the corresponding sandboxes from this.sandboxes.\n const deadSandboxes = this.sandboxes.filter(sb => sb.jobAddress === jobAddress);\n if (deadSandboxes.length > 0) {\n deadSandboxes.forEach(sandbox => { this.returnSandbox(sandbox); });\n debugging('supervisor') && console.log(`Supervisor.fetchTask: Deleting job ${jobAddress} from cache and assigned sandboxes ${deadSandboxes.map(s => s.id)}, # of sandboxes ${this.sandboxes.length}.`);\n }\n }\n });\n }\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 // Don't reenter\n if (this.matching || numCores <= 0) {\n // Interesting and noisy.\n // debugging('supervisor') && console.log(`Supervisor.fetchTask: Do not nest work, fetch or matching slices with sandboxes: queuedSlices ${this.queuedSlices.length}, unallocatedSpace ${this.unallocatedSpace}, matching ${this.matching}, fetching ${this.isFetchingNewWork}, numCores ${numCores}`);\n return Promise.resolve();\n }\n\n //\n // Oversubscription mitigation.\n // Update when there are less available sandbox slots than numCores.\n const checkNumCores = this.numberOfAvailableSandboxSlots();\n if (numCores > checkNumCores) numCores = checkNumCores;\n if (numCores <= 0) return Promise.resolve();\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 rejectedJobs: this.rejectedJobs,\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('supervisor') && console.log(`fetchTask wants ${numCores} slice(s), unallocatedSpace ${this.unallocatedSpace}, queuedSlices ${this.queuedSlices.length}`);\n try {\n debugging('requestTask') && console.debug('fetchTask: 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 Promise.resolve();\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 * messageLightWeight: { workerId: worker, jobSlices, schedulerId, jobCommissions }\n * messageBody: { newJobs: await getNewJobsForTask(dbScheduler, task, request), task }\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 \n /*\n * Ensure all jobs received from the scheduler are:\n * 1. If we have specified specific jobs the worker may work on, the received jobs are in the specified job list\n * 2. If we are in localExec, at most 1 unique job type was received (since localExec workers are designated for only\n * one job)\n * If the received jobs are not within these parameters, stop the worker since the scheduler cannot be trusted at that point.\n */\n if ((this.options.jobAddresses.length && !Object.keys(newJobs).every((ele) => this.options.jobAddresses.includes(ele)))\n || (this.options.localExec && Object.keys(newJobs).length > 1))\n {\n console.error(\"Worker received slices it shouldn't have. Rejecting the work and stopping.\");\n process.exit(1);\n }\n\n debugging() && console.log(`Supervisor.fetchTask: task: ${task.length}/${numCores}, jobs: ${Object.keys(newJobs).length}, jobSlices: ${await compressJobArray(authorizationMessage.auth.jobSlices, true /* skipFirst*/, this.addressTruncationLength /* digits*/)}`);\n\n // Delete all jobs in the supervisorCache that are not represented in this task,\n // or in this.queuedSlices, or there is no sandbox assigned to these jobs.\n this.cleanJobCache(newJobs);\n\n for (const jobAddress of Object.keys(newJobs))\n if (!this.cache.cache.job[jobAddress])\n this.cache.store('job', jobAddress, newJobs[jobAddress]);\n\n // Memoize authMessage onto the Slice object, this should\n // follow it for its entire life in the worker.\n const tmpQueuedSlices = task.map(taskElement => new Slice(taskElement, authorizationMessage));\n\n // Make sure old stuff is up front.\n // matchSlicesWithSandboxes dequeues this.queuedSlices as follows:\n // slicesToMatch = this.queuedSlices.slice(0, numCores);\n this.slices.push(...tmpQueuedSlices);\n this.queuedSlices.push(...tmpQueuedSlices);\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 * Note: When a sandbox is paired with a slice, execution is pending and sandbox.allocated=true and\n * sandbox.slice=slice and sandbox.jobAddress=slice.jobAddress. This is what 'allocated' means.\n * Immediately upon the exit of sandbox.work, sandbox.allocated=false is set and if an exception\n * wasn't thrown, the paired slice is placed in this.assignedSandboxes.\n * Thus from the pov of supervisor, this.allocatedSandboxes is deterministic and this.workingSandboxes is not.\n * Please try to not use this.workingSandboxes. It is deprecated.\n *\n * The input is numCores, this,queuedSlices, this.assignedSandboxes and this.readiedSandboxes.\n * If there are not enough sandboxes, new readied sandboxes will be created using\n * await this.readySandboxes(...)\n * And it is this await boundary that has caused many bugs.\n * We try not to make assumptions about non-local state across the await boundary.\n *\n * @param {number} numCores - The number of available sandbox slots.\n * @param {boolean} [throwExceptions=true] - Whether to throw exceptions when checking for sanity.\n * @returns {Promise<SandboxSlice[]>} Returns SandboxSlice[], may have length zero.\n */\n async matchSlicesWithSandboxes (numCores, throwExceptions = true) {\n\n const sandboxSlices = [];\n if (this.queuedSlices.length === 0 || this.matching || numCores <= 0) {\n // Interesting and noisy.\n // debugging('supervisor') && console.log(`Supervisor.matchSlicesWithSandboxes: Do not nest work, fetch or matching slices with sandboxes: queuedSlices ${this.queuedSlices.length}, unallocatedSpace ${this.unallocatedSpace}, matching ${this.matching}, fetching ${this.isFetchingNewWork}, numCores ${numCores}`);\n return sandboxSlices;\n }\n\n //\n // Oversubscription mitigation.\n // Update when there are less available sandbox slots than numCores.\n // We cannot use this.unallocatedSpace here because its value is artificially low or zero, because in\n // this.distributedQueuedSlices we use the pseudo-mutex trick: this.acquire(howManySandboxSlotsToReserve)/this.release().\n // Note: Do not use this.numberOfCoresReserved outside of a function locked with this.acquire(howManySandboxSlotsToReserve) .\n const checkNumCores = this.numberOfCoresReserved; // # of locked sandbox slots.\n if (numCores > checkNumCores) numCores = checkNumCores;\n if (numCores <= 0) return sandboxSlices;\n\n // Don't ask for more than we have.\n if (numCores > this.queuedSlices.length)\n numCores = this.queuedSlices.length;\n\n debugging('supervisor') && console.log(`matchSlicesWithSandboxes: numCores ${numCores}, queued slices ${this.queuedSlices.length}: assigned ${this.assignedSandboxes.length}, readied ${this.readiedSandboxes.length}, unallocated ${this.unallocatedSpace}, # of sandboxes: ${this.sandboxes.length}`);\n\n if (debugging('supervisor')) {\n dumpSlicesIfNotUnique(this.queuedSlices, 'Warning: this.queuedSlices slices are not unique -- this is ok when slice is rescheduled.');\n dumpSandboxesIfNotUnique(this.readiedSandboxes, 'Warning: this.readiedSandboxes sandboxes are not unique!');\n dumpSandboxesIfNotUnique(this.assignedSandboxes, 'Warning: this.assignedSandboxes sandboxes are not unique!');\n }\n\n // Three functions to validate slice and sandbox.\n function checkSlice(slice, checkAllocated=true) {\n if (!slice.isUnassigned) throw new DCPError(`Slice must be unassigned: ${slice.identifier}`);\n if (checkAllocated && slice.allocated) throw new DCPError(`Slice must not already be allocated: ${slice.identifier}`);\n }\n function checkSandbox(sandbox, isAssigned) {\n if (sandbox.allocated) throw new DCPError(`Assigned sandbox must not be already allocated: ${sandbox.identifier}`);\n if (isAssigned && !sandbox.isAssigned) throw new DCPError(`Assigned sandbox is not marked as assigned: ${sandbox.identifier}`);\n if (!isAssigned && !sandbox.isReadyForAssign) throw new DCPError(`Readied sandbox is not marked as ready for assign: ${sandbox.identifier}`);\n }\n\n // Sanity checks.\n if (throwExceptions) {\n this.assignedSandboxes.forEach(sandbox => { checkSandbox(sandbox, true /* isAssigned*/); });\n this.readiedSandboxes.forEach(sandbox => { checkSandbox(sandbox, false /* isAssigned*/); });\n this.queuedSlices.forEach(slice => { checkSlice(slice); });\n } else {\n this.assignedSandboxes = this.assignedSandboxes.filter(sandbox => !sandbox.allocated && sandbox.isAssigned);\n this.readiedSandboxes = this.readiedSandboxes.filter(sandbox => !sandbox.allocated && sandbox.isReadyForAssign);\n this.queuedSlices = this.queuedSlices.filter(slice => !slice.allocated && slice.isUnassigned);\n }\n\n const sandboxKind = {\n assigned: 0,\n ready: 1,\n new: 2,\n };\n\n const ceci = this;\n /**\n * Auxiliary function to pair a sandbox with a slice and mark the sandbox as allocated.\n * An allocated sandbox is reserved and will not be released until the slice completes execution on the sandbox.\n *\n * @param {Sandbox} sandbox\n * @param {Slice} slice\n * @param {number} kind\n */\n function pair(sandbox, slice, kind) {\n checkSandbox(sandbox, kind === sandboxKind.assigned);\n checkSlice(slice, kind === sandboxKind.assigned);\n slice.allocated = true;\n sandbox.allocated = true;\n sandbox.jobAddress = slice.jobAddress; // So we can know which jobs to not delete from this.cache .\n sandbox.slice = slice;\n sandboxSlices.push({ sandbox, slice });\n if (debugging('supervisor')) {\n let fragment = 'New readied';\n if (kind === sandboxKind.assigned) fragment = 'Assigned';\n else if (kind === sandboxKind.ready) fragment = 'Readied';\n console.log(`matchSlicesWithSandboxes.pair: ${fragment} sandbox matched ${ceci.dumpSandboxAndSlice(sandbox, slice)}`);\n }\n }\n\n // These three arrays are used to track/store slices and sandboxes,\n // so that when an exception occurs, the following arrays are restored:\n // this.queuedSlices, this.assignedSandboxes, this.realizedSandboxes.\n let slicesToMatch = [];\n let trackAssignedSandboxes = [];\n let trackReadiedSandboxes = [];\n try\n {\n this.matching = true;\n\n let assignedCounter = 0; // How many assigned sandboxes are being used.\n let readyCounter = 0; // How many sandboxes used from the existing this.readiedSandboxes.\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. First we match with assigned sandboxes in the order\n // that they appear in this.queuedSlices. Then we match in-order with existing this.readiedSandboxes\n // Then we match in-order with new new readied sandboxes created through\n // await this.readySandboxes(newCounter, true /* allocateLocalSandboxes*/);\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 // Note: The ordering of sandboxSlices only partially corresponds to the order of this.queuedSlices.\n // It's easy to do. When pairing with assigned sandboxes, any slice in this.queuedSlices which doesn't\n // have an assigned sandbox, will add null to the sandboxSlices array. Then when pairing with readied sandboxes,\n // we fill-in the null entries in the sandboxSlices array.\n //\n /** XXXpfr @todo When it is needed, fix the ordering as described above. */\n\n // Get the slices that are being matched.\n slicesToMatch = this.queuedSlices.slice(0, numCores);\n this.queuedSlices = this.queuedSlices.slice(numCores);\n\n debugging('supervisor') && console.log(`matchSlicesWithSandboxes: slicesToMatch ${await this.dumpSlices(slicesToMatch)}`);\n\n // Create object map: jobAddress -> sandboxes with sandboxes.jobAddress === jobAddress .\n const jobSandboxMap = toJobMap(this.assignedSandboxes, sandbox => sandbox);\n \n // Create array to hold slices which do not have assigned sandboxes.\n // These slices will need to be paired with existing and possibly new readied sandboxes.\n // Specifically, the sandboxes from existing this.readiedSandboxes and new sandboxes\n // created through await this.readySandboxes(newCounter, true /* allocateLocalSandboxes*/);\n const slicesThatNeedSandboxes = [];\n\n // Pair assigned sandboxes with slices.\n for (const slice of slicesToMatch) {\n const assigned = jobSandboxMap[slice.jobAddress];\n if (assigned && assigned.length > 0) {\n // Pair.\n const sandbox = assigned.pop();\n pair(sandbox, slice, sandboxKind.assigned);\n this.removeElement(this.assignedSandboxes, sandbox);\n // Track.\n trackAssignedSandboxes.push(sandbox);\n assignedCounter++;\n } else {\n // Don't lose track of these slices.\n slice.allocated = true;\n slicesThatNeedSandboxes.push(slice);\n }\n }\n\n // Pair readied sandboxes with slices.\n readyCounter = Math.min(slicesThatNeedSandboxes.length, this.readiedSandboxes.length);\n newCounter = slicesThatNeedSandboxes.length - readyCounter;\n // Track.\n trackReadiedSandboxes = this.readiedSandboxes.slice(0, readyCounter);\n this.readiedSandboxes = this.readiedSandboxes.slice(readyCounter);\n for (const sandbox of trackReadiedSandboxes) {\n // Pair.\n const slice = slicesThatNeedSandboxes.pop();\n pair(sandbox, slice, sandboxKind.ready);\n }\n \n debugging('supervisor') && console.log(`matchSlicesWithSandboxes: assignedCounter ${assignedCounter}, readyCounter ${readyCounter}, newCounter ${newCounter}, numCores ${numCores}`)\n\n // Validate algorithm consistency.\n if (Supervisor.debugBuild && assignedCounter + readyCounter + newCounter !== numCores) {\n // Structured assert.\n throw new DCPError(`matchSlicesWithSandboxes: Algorithm is corrupt ${assignedCounter} + ${readyCounter} + ${newCounter} !== ${numCores}`);\n }\n\n // Here is an await boundary.\n // Accessing non-local data across an await boundary may result in the unexpected.\n\n // Create new readied sandboxes to associate with slicesThatNeedSandboxes.\n if (newCounter > 0) {\n // When allocateLocalSandboxes is true, this.readySandboxes does not place the new sandboxes\n // on this.readiedSandboxes. Hence the new sandboxes are private and nobody else can see them.\n debugging('supervisor') && console.log(`matchSlicesWithSandboxes: creating ${newCounter} new sandboxes, # of sandboxes ${this.sandboxes.length}`);\n const readied = await this.readySandboxes(newCounter, true /* allocateLocalSandboxes*/);\n // Track.\n trackReadiedSandboxes.push(...readied);\n\n for (const sandbox of readied) {\n assert(slicesThatNeedSandboxes.length > 0);\n // Pair\n const slice = slicesThatNeedSandboxes.pop();\n pair(sandbox, slice, sandboxKind.new);\n }\n \n // Put back any extras. There should not be any unless readySandboxes returned less than asked for.\n if (slicesThatNeedSandboxes.length > 0) {\n slicesThatNeedSandboxes.forEach(slice => {\n slice.allocated = false;\n this.queuedSlices.push(slice);\n });\n }\n }\n\n if ( false || debugging()) {\n console.log(`matchSlicesWithSandboxes: Matches: ${ this.dumpSandboxSlices(sandboxSlices) }`);\n this.dumpSandboxSlicesIfNotUnique(sandboxSlices, 'Warning: sandboxSlices; { sandbox, slice } pairs are not unique!');\n }\n } catch (e) {\n // Clear allocations.\n slicesToMatch.forEach(slice => { slice.allocated = false; });\n trackAssignedSandboxes.forEach(sandbox => { sandbox.allocated = false; sandbox.slice = null; });\n trackReadiedSandboxes.forEach(sandbox => { sandbox.allocated = false; sandbox.slice = null; sandbox.jobAddress = null; });\n \n // Filter out redundancies -- there shouldn't be any...\n slicesToMatch = slicesToMatch.filter(slice => this.queuedSlices.indexOf(slice) === -1);\n trackAssignedSandboxes = trackAssignedSandboxes.filter(sb => this.assignedSandboxes.indexOf(sb) === -1);\n trackReadiedSandboxes = trackReadiedSandboxes.filter(sb => this.readiedSandboxes.indexOf(sb) === -1);\n\n // Sanity checks.\n slicesToMatch.forEach(slice => { checkSlice(slice) });\n trackAssignedSandboxes.forEach(sandbox => { checkSandbox(sandbox, true /* isAssigned*/); });\n trackReadiedSandboxes.forEach(sandbox => { checkSandbox(sandbox, false /* isAssigned*/); });\n\n // Restore arrays.\n this.queuedSlices.push(...slicesToMatch);\n this.assignedSandboxes.push(...trackAssignedSandboxes);\n this.readiedSandboxes.push(...trackReadiedSandboxes);\n \n console.error('Error in matchSlicesWithSandboxes: Attempting to recover slices and sandboxes.', e);\n return [];\n } finally {\n this.matching = false;\n }\n\n debugging('supervisor') && console.log(`matchSlicesWithSandboxes: allocated ${sandboxSlices.length} sandboxes, queuedSlices ${this.queuedSlices.length}, unallocatedSpace ${this.unallocatedSpace}, matching ${this.matching}, fetching ${this.isFetchingNewWork}, # of sandboxes: ${this.sandboxes.length}.`);\n\n return sandboxSlices;\n }\n\n disassociateSandboxAndSlice(sandbox, slice) {\n this.returnSandbox(sandbox);\n sandbox.slice = null;\n this.returnSlice(slice);\n }\n\n /**\n * This method will call this.startSandboxWork(sandbox, slice) for each element { sandbox, slice }\n * of the array returned by this.matchSlicesWithSandboxes(availableSandboxes) until all allocated sandboxes\n * are working. It is possible for a sandbox to interleave with calling distributeQueuedSlices and leave a sandbox\n * that is not working. Moreover, this.queuedSlices may be exhausted before all sandboxes are working.\n * @returns {Promise<void>}\n */\n async distributeQueuedSlices () {\n const numCores = this.unallocatedSpace;\n\n // If there's nothing there, or we're reentering, bail out.\n if (this.queuedSlices.length === 0 || numCores <= 0 || this.matching) {\n // Interesting and noisy.\n // debugging('supervisor') && console.log(`Supervisor.distributeQueuedSlices: Do not nest work, fetch or matching slices with sandboxes: queuedSlices ${this.queuedSlices.length}, matching ${this.matching}, fetching ${this.isFetchingNewWork}, numCores ${numCores}`);\n return Promise.resolve();\n }\n\n //\n // Use the pseudo-mutex to prevent uncontrolled interleaving with fetchTask,\n // matchSlicesWithSandboxes and distributeQueuedSlices\n let sandboxSlices;\n this.acquire(numCores);\n try {\n sandboxSlices = await this.matchSlicesWithSandboxes(numCores);\n } finally {\n this.release();\n }\n\n debugging('supervisor') && console.log(`distributeQueuedSlices: ${sandboxSlices.length} sandboxSlices ${this.dumpSandboxSlices(sandboxSlices)}, matching ${this.matching}, fetching ${this.isFetchingNewWork}`);\n\n for (let sandboxSlice of sandboxSlices) {\n\n const { sandbox, slice } = sandboxSlice;\n try {\n if (sandbox.isReadyForAssign) {\n try {\n let timeoutMs = Math.floor(Math.min(+Supervisor.lastAssignFailTimerMs || 0, 10 * 60 * 1000 /* 10m */));\n await a$sleepMs(timeoutMs);\n await this.assignJobToSandbox(sandbox, slice.jobAddress);\n } catch (e) {\n console.error(`Supervisor.distributeQueuedSlices: Could not assign slice ${slice.identifier} to sandbox ${sandbox.identifier}.`);\n if (Supervisor.debugBuild) console.error(`...exception`, e);\n Supervisor.lastAssignFailTimerMs = Supervisor.lastAssignFailTimerMs ? +Supervisor.lastAssignFailTimerMs * 1.25 : Math.random() * 200;\n this.disassociateSandboxAndSlice(sandbox, 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 } catch (e) {\n // We should never get here.\n console.error(`Supervisor.distributeQueuedSlices: Failed to execute slice ${slice.identifier} in sandbox ${sandbox.identifier}.`);\n if (Supervisor.debugBuild) console.error('...exception', e);\n this.disassociateSandboxAndSlice(sandbox, slice);\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 * Handles reassigning or returning a slice that was rejected by a sandbox.\n * \n * The sandbox will be terminated by this.returnSandbox in finalizeSandboxAndSlice. In this case,\n * if the slice does not have a rejected property already, reassign the slice to a new sandbox\n * and add a rejected property to the slice to indicate it has already rejected once, then set slice = null\n * in the return SandboxSlice so that finalizeSandboxAndSlice won't return slice to scheduler.\n * \n * If the slice rejects with a reason, or has a rejected time stamp (ie. has been rejected once already)\n * then return the slice and all slices from the job to the scheduler and\n * terminate all sandboxes with that jobAddress.\n * @param {Sandbox} sandbox \n * @param {Slice} slice\n * @returns {Promise<SandboxSlice>}\n */\n async handleWorkReject(sandbox, slice, rejectReason) {\n if (!this.rejectedJobReasons[slice.jobAddress])\n this.rejectedJobReasons[slice.jobAddress] = [];\n\n this.rejectedJobReasons[slice.jobAddress].push(rejectReason); // memoize reasons\n\n // First time rejecting without a reason. Try assigning slice to a new sandbox.\n if (rejectReason === 'false' && !slice.rejected) {\n // Set rejected.\n slice.rejected = Date.now();\n // Schedule the slice for execution.\n this.scheduleSlice(slice, true /* placeInTheFrontOfTheQueue*/, false /* noDuplicateExecution*/);\n \n // Null out slice so this.returnSlice will not be called in finalizeSandboxAndSlice.\n // But we still want this.returnSandbox to terminate the sandbox.\n slice = null;\n } else { // Slice has a reason OR rejected without a reason already and got stamped.\n \n // Purge all slices and sandboxes associated with slice.jobAddress .\n this.purgeAllWork(slice.jobAddress);\n // Clear jobAddress from this.cache .\n this.cleanJobCache();\n\n //\n // this.purgeAllWork(jobAddress) terminates all sandboxes with jobAddress,\n // and it also returns to scheduler all slices with jobAddress.\n // Therefore null out slice and sandbox so finalizeSandboxAndSlice doesn't do anything.\n // \n sandbox = null;\n slice = null;\n\n // Add to array of rejected jobs.\n let rejectedJob = {\n address: slice.jobAddress,\n reasons: this.rejectedJobReasons[slice.jobAddress],\n }\n this.rejectedJobs.push(rejectedJob);\n\n // Tell everyone all about it, when allowed.\n if (dcpConfig.worker.allowConsoleAccess || Supervisor.debugBuild)\n console.warn('Supervisor.handleWorkReject: The slice ${slice.identifier} was rejected twice; slice will be returned to the scheduler.');\n console.warn('Supervisor.handleWorkReject: All slices with the same jobAddress returned to the scheduler.');\n console.warn('Supervisor.handleWorkReject: All sandboxes with the same jobAddress are terminated.');\n }\n return { sandbox, slice };\n }\n\n /**\n * Schedule the slice to be executed.\n * If slice is already executing and noDuplicateExecution is true, return the slice with reason.\n * @param {Slice} slice\n * @param {boolean} [placeInTheFrontOfTheQueue=false]\n * @param {boolean} [noDuplicateExecution=true]\n * @param {string} [reason]\n */\n scheduleSlice(slice, placeInTheFrontOfTheQueue = false, noDuplicateExecution = true, reason) {\n // When noDuplicateExecution, if slice is already executing, do nothing.\n let workingSlices = [];\n if (noDuplicateExecution)\n workingSlices = this.allocatedSlices;\n\n if (!workingSlices.indexOf(slice)) {\n // Reset slice state to allow execution.\n slice.status = SLICE_STATUS_UNASSIGNED;\n // Enqueue in the to-be-executed queue.\n if (placeInTheFrontOfTheQueue) this.queuedSlices.unshift(slice);\n else this.queuedSlices.push(slice);\n }\n }\n\n /**\n * Purge all slices and sandboxes with this jobAddress.\n * @param {address} jobAddress\n * @param {boolean} [onlyPurgeQueuedAndAllocated=false]\n */\n purgeAllWork(jobAddress, onlyPurgeQueuedAndAllocated = false) {\n // Purge all slices and sandboxes associated with jobAddress .\n const deadSandboxes = this.sandboxes.filter(sandbox => sandbox.jobAddress === jobAddress);\n\n if (deadSandboxes.length > 0) {\n debugging('supervisor') && console.log(`purgeAllWork(${this.dumpJobAddress(jobAddress)}): sandboxes purged ${deadSandboxes.map(s => s.id)}, # of sandboxes ${this.sandboxes.length}`);\n deadSandboxes.forEach(sandbox => this.returnSandbox(sandbox));\n }\n\n let deadSlices;\n if (onlyPurgeQueuedAndAllocated) {\n deadSlices = this.queuedSlices.filter(slice => slice.jobAddress === jobAddress);\n if (deadSlices.length > 0 || this.allocatedSlices.length > 0)\n debugging('supervisor') && console.log(`purgeAllWork(${this.dumpJobAddress(jobAddress)}): dead queuedSlices ${deadSlices.map(s => s.sliceNumber)}, dead allocatedSlices ${this.allocatedSlices.map(s => s.sliceNumber)}`);\n deadSlices.push(...this.allocatedSlices);\n } else {\n deadSlices = this.slices.filter(slice => slice.jobAddress === jobAddress);\n }\n\n if (deadSlices.length > 0) {\n debugging('supervisor') && console.log(`purgeAllWork(${this.dumpJobAddress(jobAddress)}): slices purged ${deadSlices.map(s => s.sliceNumber)}, # of sandboxes ${this.sandboxes.length}`);\n this.returnSlices(deadSlices);\n this.removeQueuedSlices(deadSlices);\n }\n debugging('supervisor') && console.log(`purgeAllWork(${this.dumpJobAddress(jobAddress)}): Finished: slices ${this.slices.length}, queuedSlices ${this.queuedSlices.length}, assigned ${this.assignedSandboxes.length}, readied ${this.readiedSandboxes.length}, # of sandboxes ${this.sandboxes.length}`);\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, reason = 'unknown';\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('startSandboxWork: slice.markAsWorking exception:', e);\n return Promise.resolve();\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(`startSandboxWork: Started ${this.dumpStatefulSandboxAndSlice(sandbox, slice)}, total sandbox count: ${this.sandboxes.length}, matching ${this.matching}, fetching ${this.isFetchingNewWork}`);\n let result;\n try {\n result = await sandbox.work(slice, startDelayMs);\n } finally {\n sandbox.allocated = false;\n slice.allocated = false;\n }\n slice.collectResult(result, true);\n // In watchdog, all sandboxes in working state, have their slice 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 this.assignedSandboxes.push(sandbox);\n debugging() && console.log(`startSandboxWork: Finished ${this.dumpStatefulSandboxAndSlice(sandbox, slice)}, total sandbox count: ${this.sandboxes.length}, matching ${this.matching}, fetching ${this.isFetchingNewWork}`);\n } catch(error) {\n let logLevel;\n\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, so just return the slice in the finally block.\n // For extra safety the sandbox is terminated.\n slice.result = null;\n slice.status = SLICE_STATUS_FAILED; /** XXXpfr @todo terminating sandbox? */\n }\n\n let errorString;\n switch (error.errorCode) {\n case 'ENOPROGRESS':\n reason = 'ENOPROGRESS';\n errorString = 'Supervisor.startSandboxWork - No progress error in sandbox.\\n';\n break;\n case 'ESLICETOOSLOW':\n reason = 'ESLICETOOSLOW';\n errorString = 'Supervisor.startSandboxWork - Slice too slow error in sandbox.\\n';\n break;\n case 'EUNCAUGHT':\n reason = 'EUNCAUGHT';\n errorString = `Supervisor.startSandboxWork - Uncaught error in sandbox ${error.message}.\\n`;\n break;\n case 'EFETCH':\n // reason = 'EFETCH'; The status.js processing cannot handle 'EFETCH'\n reason = 'unknown';\n errorString = `Supervisor.startSandboxWork - Could not fetch data: ${error.message}.\\n`;\n break;\n }\n\n if (error.name === 'EWORKREJECT') {\n error.stack = 'Sandbox was terminated by work.reject()';\n const ss = await this.handleWorkReject(sandbox, slice, error.message);\n sandbox = ss.sandbox; slice = ss.slice;\n }\n\n const errorObject = {\n jobAddress: slice.jobAddress.substr(0,10),\n sliceNumber: slice.sliceNumber,\n sandbox: sandbox.id,\n jobName: sandbox.public ? sandbox.public.name : 'unnamed',\n };\n\n // Always display max info under debug builds, otherwise maximal error\n // messages are displayed to the worker, only if both worker and client agree.\n let workerConsole = sandbox.supervisorCache.cache.job[slice.jobAddress].workerConsole;\n const displayMaxInfo = Supervisor.debugBuild || (workerConsole && dcpConfig.worker.allowConsoleAccess);\n\n if (!displayMaxInfo && errorString) {\n console[logLevel](errorString, errorObject);\n } else if (!displayMaxInfo && error.name === 'EWORKREJECT') {\n console[logLevel](`Supervisor.startSandboxWork - Sandbox rejected work: ${error.message}`)\n } else {\n if (displayMaxInfo)\n errorObject.stack += '\\n --------------------\\n' + (error.stack.split('\\n').slice(1).join('\\n'));\n console[logLevel](`Supervisor.startSandboxWork - Sandbox failed: ${error.message}\\n`, errorObject);\n }\n } finally {\n await this.finalizeSandboxAndSlice(sandbox, slice, reason);\n }\n }\n\n /**\n * If slice && slice.result, then call await this.recordResult(slice) and this.returnSandbox(sandbox, slice) will have no effect.\n * If slice && !slice.result, then call this.returnSlice(slice, reason) and then this.returnSandbox(sandbox, slice) which terminates sandbox.\n * If !slice && sandbox, then terminate the sandbox with this.returnSandbox(sandbox, slice) .\n * If !slice && !sandbox, then do nothing.\n * @param {Sandbox} [sandbox]\n * @param {Slice} [slice]\n * @param {string} [reason]\n */\n async finalizeSandboxAndSlice(sandbox, slice, reason) {\n debugging('supervisor') && console.log(`finalizeSandboxAndSlice: sandbox ${sandbox ? sandbox.identifier : 'nade'}, slice ${slice ? slice.identifier : 'nade'}`);\n if (slice) {\n if (slice.result) await this.recordResult(slice);\n else this.returnSlice(slice, reason);\n }\n // It is possible that sandbox is already terminated\n // Because sandbox.allocated=false as soon as sandbox.work(...) completes.\n // But the await at or in finalizeSandboxAndSlice may allow pruneSandboxes to slither in.\n if (sandbox) this.returnSandbox(sandbox, slice, false /* verifySandboxIsNotTerminated*/);\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-allocated 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 debugging('supervisor') && console.log('stopWork(${forceTerminate}): terminating sandboxes and returning slices to scheduler.');\n if (forceTerminate) {\n for (const sandbox of this.sandboxes) {\n this.returnSandbox(sandbox);\n }\n\n await this.returnSlices(this.slices).then(() => {\n this.queuedSlices.length = 0;\n });\n } else {\n // Only terminate idle sandboxes and return only queued slices\n let idleSandboxes = this.sandboxes.filter(w => !w.allocated);\n for (const sandbox of idleSandboxes) {\n this.returnSandbox(sandbox);\n }\n\n await this.returnSlices(this.queuedSlices).then(() => {\n this.queuedSlices.length = 0;\n });\n\n await new Promise((resolve, reject) => {\n let sandboxesRemaining = this.allocatedSandboxes.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 }\n\n if (this.resultSubmitterConnection) {\n this.resultSubmitterConnection.off('close', this.openResultSubmitterConn);\n this.resultSubmitterConnection.close();\n this.resultSubmitterConnection = null;\n }\n\n if (this.taskDistributorConnection) {\n this.taskDistributorConnection.off('close', this.openTaskDistributorConn);\n this.taskDistributorConnection.close();\n this.taskDistributorConnection = null;\n }\n\n if (this.packageManagerConnection) {\n this.packageManagerConnection.off('close', this.openPackageManagerConn);\n this.packageManagerConnection.close();\n this.packageManagerConnection = null;\n }\n\n if (this.eventRouterConnection) {\n this.eventRouterConnection.off('close', this.openEventRouterConn);\n this.eventRouterConnection.close();\n this.eventRouterConnection = null;\n }\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 an exception is thrown by sandbox.work(slice, startDelayMs) .\n * Or when the supervisor tells it to forcibly stop working.\n *\n * @param {Slice} slice - The slice to return to the scheduler.\n * @param {string} [reason] - Optional reason for the return: 'ENOPROGRESS', 'EUNCAUGHT', 'ESLICETOOSLOW', 'unknown'.\n * @returns {Promise<*>} - Response from the scheduler.\n */\n returnSlice (slice, reason) {\n // When sliceNumber === 0 don't send a status message.\n if (slice.sliceNumber === 0) return Promise.resolve();\n \n debugging() && console.log(`Supervisor.returnSlice: Returning slice ${slice.identifier} with reason ${reason}.`);\n \n const payload = slice.getReturnMessagePayload(this.workerOpaqueId, reason);\n return this.resultSubmitterConnection.send('status', payload)\n .then(response => {\n return response;\n }).catch(error => {\n console.error('Failed to return slice', {\n sliceNumber: slice.sliceNumber,\n jobAddress: slice.jobAddress,\n status: slice.status,\n error,\n });\n });\n }\n\n /**\n * Bulk-return multiple slices, possibly for assorted jobs.\n * Returns slices to the scheduler to be redistributed.\n * Called in the sandbox terminate handler and purgeAllWork(jobAddress)\n * and stopWork(forceTerminate).\n *\n * @param {Slice[]} slices - The slices to return to the scheduler.\n * @returns {Promise<void>} - Response from the scheduler.\n */\n async returnSlices(slices) {\n if (!slices || !slices.length) return Promise.resolve();\n \n const slicePayload = [];\n slices.forEach(slice => { addToReturnSlicePayload(slicePayload, slice); });\n this.removeSlices(slices);\n\n debugging('supervisor') && console.log(`Supervisor.returnSlices: Returning slices ${await this.dumpSlices(slices)}.`);\n\n return this.resultSubmitterConnection.send('status', {\n worker: this.workerOpaqueId,\n slices: slicePayload,\n }).then(response => {\n return response;\n }).catch(error => {\n const errorInfo = slices.map(slice => slice.identifier);\n console.error('Failed to return slice(s)', { errorInfo, error });\n // Just in case the caller is expecing a DCP response\n return { success: false, payload: {} };\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 // 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 slice ${slice.identifier}. This is ok when there are upstream errors.`);\n\n debugging('supervisor') && console.log(`supervisor: recording result for slice ${slice.identifier}.`);\n\n const jobAddress = slice.jobAddress;\n const sliceNumber = slice.sliceNumber;\n const authorizationMessage = slice.getAuthorizationMessage();\n\n /* @see result-submitter::result for full message details */\n const metrics = { GPUTime: 0, CPUTime: 0, CPUDensity: 0, GPUDensity: 0 };\n const payloadData = {\n slice: sliceNumber,\n job: jobAddress,\n worker: this.workerOpaqueId,\n paymentAddress: this.paymentAddress,\n metrics,\n authorizationMessage,\n }\n \n const timeReport = slice.timeReport;\n if (timeReport && timeReport.total > 0) {\n metrics.GPUTime = timeReport.webGL;\n metrics.CPUTime = timeReport.CPU;\n metrics.CPUDensity = metrics.CPUTime / timeReport.total;\n metrics.GPUDensity = metrics.GPUTime / timeReport.total;\n metrics.CPUTime = 1 + Math.floor(metrics.CPUTime);\n metrics.GPUTime = 1 + Math.floor(metrics.GPUTime);\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('supervisor') && 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 payload;\n }\n \n if (false) {}\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 /* slice did not complete for some reason */\n let statusPayloadData = slice.getReturnMessagePayload(this.workerOpaqueId);\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 } finally {\n this.emit('submitFinished');\n // Remove the slice from the slices array.\n this.removeSlice(slice);\n debugging('supervisor') && console.log(`recordResult: Completed for slice ${slice.identifier}.`);\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-rds .\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 postParams.content = JSON.stringify(result);\n } else {\n postParams.error = JSON.stringify(slice.error);\n }\n\n debugging('supervisor') && 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 * Sandbox has had an error which is not from the work function: kill it\n * and try to redo the slice.\n */\nfunction handleSandboxError(supervisor, sandbox, error) {\n const slice = sandbox.slice;\n\n slice.sandboxErrorCount = (slice.sandboxErrorCount || 0) + 1;\n sandbox.slice = null;\n supervisor.returnSandbox(sandbox); /* terminate the sandbox */\n slice.status = SLICE_STATUS_UNASSIGNED; /* ToT */\n console.warn(`Supervisor.handleSandboxError: Sandbox ${sandbox.identifier}...(${sandbox.public.name}/${slice.sandboxErrorCount}) with slice ${slice.identifier} had error.`, error);\n\n if (slice.sandboxErrorCount < dcpConfig.worker.maxSandboxErrorsPerSlice)\n supervisor.queuedSlices.push(slice);\n else {\n slice.error = error;\n supervisor.returnSlice(slice);\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 {Slice} slice - The slice.\n * @param {String} status - Status update, eg. progress or scheduled.\n *\n * @returns {Object[]} mutated slicePayload array\n */\nfunction addToSlicePayload(slicePayload, slice, status) {\n // getAuthorizationMessage helps enforces the equivalence\n // !authorizationMessage <==> sliceNumber === 0\n const authorizationMessage = slice.getAuthorizationMessage();\n if (!authorizationMessage) return;\n\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 === slice.jobAddress\n && desc.status === status\n && 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: slice.jobAddress,\n sliceNumbers: [],\n status,\n authorizationMessage,\n };\n slicePayload.push(sliceList);\n }\n\n sliceList.sliceNumbers.push(slice.sliceNumber);\n\n return slicePayload;\n}\n\n/**\n * Add a slice to the returnSlice payload being built. If a sliceList already exists for the\n * job-isEstimation-authMessage-reason 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 {Slice} slice - The slice.\n * @param {String} [reason] - Optional reason to further characterize status; e.g. 'ENOPROGRESS', 'EUNCAUGHT', 'ESLICETOOSLOW', 'unknown'.\n *\n * @returns {Object[]} mutated slicePayload array\n */\nfunction addToReturnSlicePayload(slicePayload, slice, reason) {\n // getAuthorizationMessage helps enforces the equivalence\n // !authorizationMessage <==> sliceNumber === 0\n const authorizationMessage = slice.getAuthorizationMessage();\n if (!authorizationMessage) return;\n\n if (!reason) reason = slice.error ? 'EUNCAUGHT' : 'unknown';\n\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 === slice.jobAddress\n && desc.isEstimationSlice === slice.isEstimationSlice\n && desc.authorizationMessage === authorizationMessage\n && desc.reason === reason;\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: slice.jobAddress,\n sliceNumbers: [],\n status: 'return',\n isEstimationSlice: slice.isEstimationSlice,\n authorizationMessage,\n reason,\n };\n slicePayload.push(sliceList);\n }\n\n sliceList.sliceNumbers.push(slice.sliceNumber);\n\n return slicePayload;\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/** @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://dcp/./src/dcp-client/worker/supervisor.js?");
4447
+ eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_modules/process/browser.js */ \"./node_modules/process/browser.js\");\n/**\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 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, SLICE_STATUS_UNASSIGNED, SLICE_STATUS_FAILED } = __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, a$sleepMs, justFetch, compressJobArray, toJobMap,\n compressSandboxes, compressSlices, truncateAddress, dumpSandboxesIfNotUnique, dumpSlicesIfNotUnique, generateOpaqueId } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst { sliceStatus } = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst { calculateJoinHash } = __webpack_require__(/*! dcp/dcp-client/compute-groups */ \"./src/dcp-client/compute-groups/index.js\");\nconst RingBuffer = __webpack_require__(/*! dcp/utils/ringBuffer */ \"./src/utils/ringBuffer.js\");\nconst supervisorTuning = dcpConfig.future('worker.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 {*} address\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 /** @type {Sandbox[]} */\n this.readiedSandboxes = [];\n\n /** @type {Sandbox[]} */\n this.assignedSandboxes = [];\n\n /** @type {Slice[]} */\n this.slices = [];\n\n /** @type {Slice[]} */\n this.queuedSlices = [];\n\n /** @type {Slice[]} */\n this.lostSlices = [];\n\n /** @type {boolean} */\n this.matching = false;\n\n /** @type {boolean} */\n this.isFetchingNewWork = false;\n\n /** @type {number} */\n this.numberOfCoresReserved = 0;\n\n /** @type {number} */\n this.addressTruncationLength = 20; // Set to -1 for no truncation.\n\n /** @type {Object[]} */\n this.rejectedJobs = [];\n this.rejectedJobReasons = [];\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 // In localExec, do not allow work function or arguments to come from the 'any' origins\n this.allowedOrigins = []\n if (this.options.localExec)\n {\n dcpConfig.worker.allowOrigins.fetchData = dcpConfig.worker.allowOrigins.fetchData.concat(dcpConfig.worker.allowOrigins.any)\n dcpConfig.worker.allowOrigins.sendResults = dcpConfig.worker.allowOrigins.sendResults.concat(dcpConfig.worker.allowOrigins.any)\n }\n else\n this.allowedOrigins = dcpConfig.worker.allowOrigins.any;\n\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.allocatedSandboxes.filter(sb => !!sb.requiresGPU).length,\n // enumerable: true,\n // configurable: false,\n // });\n\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 /** @type {number} */\n this.lastProgressReport = 0;\n\n /** \n * An N-slot ring buffer of job addresses. Stores all jobs that have had no more than 1 slice run in the ring buffer.\n * Required for the implementation of discrete jobs \n * @type {RingBuffer} \n */\n this.ringBufferofJobs = new RingBuffer(200); // N = 200 should be more than enough.\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(), tuning.watchdogInterval * 1000);\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 !== constants.workerIdLength) {\n this._workerOpaqueId = generateOpaqueId();\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 if (!this.taskDistributorConnection)\n this.openTaskDistributorConn();\n \n if (!this.eventRouterConnection)\n this.openEventRouterConn();\n \n if (!this.resultSubmitterConnection)\n this.openResultSubmitterConn();\n\n if (!this.packageManagerConnection)\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 properties and functions for creating filtered views\n // of the slices and sandboxes array.\n //\n /** XXXpfr @todo Write sort w/o using promises so we can get rid of async on all the compress functions. */\n\n /**\n * @deprecated -- Please do not use this.workingSandboxes; use this.allocatedSandboxes instead.\n * Sandboxes that are in WORKING state.\n *\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 * Use instead of this.workingSandboxes.\n *\n * When a sandbox is paired with a slice, execution is pending and sandbox.allocated=true and\n * sandbox.slice=slice and sandbox.jobAddress=slice.jobAddress. This is what 'allocated' means.\n * Immediately upon the exit of sandbox.work, sandbox.allocated=false is set and if an exception\n * wasn't thrown the sandbox is placed in this.assignedSandboxes.\n * Thus from the pov of supervisor, this.allocatedSandboxes is deterministic and this.workingSandboxes is not.\n * Please try to not use this.workingSandboxes. It is deprecated.\n *\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Sandbox[]}\n */\n get allocatedSandboxes() {\n return this.sandboxes.filter(sandbox => sandbox.allocated);\n }\n\n /**\n * Slices that are allocated.\n * Warning: Do not rely on this information being 100% accurate -- it may change in the next instant.\n * @type {Slice[]}\n */\n get allocatedSlices() {\n return this.slices.filter(slice => slice.allocated);\n }\n\n /**\n * This property is used as the target number of sandboxes to be associated with slices and start 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 start 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.allocatedSandboxes.length - this.numberOfCoresReserved;\n }\n \n /**\n * Call acquire(numberOfCoresToReserve) to reserve numberOfCoresToReserve unallocated sandboxes as measured by unallocatedSpace.\n * Call release() to undo the previous acquire.\n * This pseudo-mutex technique helps prevent races in scheduling slices in Supervisor.\n * @param {number} numberOfCoresToReserve\n */\n acquire(numberOfCoresToReserve) { \n this.numberOfCoresReserved = numberOfCoresToReserve; \n }\n release() { \n this.numberOfCoresReserved = 0; \n }\n\n /**\n * Remove from this.slices.\n * @param {Slice} slice\n */\n removeSlice(slice) {\n this.removeElement(this.slices, slice);\n if (Supervisor.debugBuild) {\n if (this.queuedSlices.indexOf(slice) !== -1)\n throw new Error(`removeSlice: slice ${slice.identifier} is in queuedSlices; inconsistent state.`);\n if (this.lostSlices.length > 0) {\n console.warn(`removeSlice: slice ${slice.identifier}, found lostSlices ${this.lostSlices.map(s => s.identifier)}`);\n if (this.lostSlices.indexOf(slice) !== -1)\n throw new Error(`removeSlice: slice ${slice.identifier} is in lostSlices; inconsistent state.`);\n }\n }\n }\n\n /**\n * Remove from this.slices.\n * @param {Slice[]} slices\n */\n removeSlices(slices) {\n this.slices = this.slices.filter(slice => slices.indexOf(slice) === -1);\n }\n\n /**\n * Remove from this.queuedSlices.\n * @param {Slice[]} slices\n */\n removeQueuedSlices(slices) {\n this.queuedSlices = this.queuedSlices.filter(slice => slices.indexOf(slice) === -1);\n }\n\n /**\n * Remove from this.sandboxes, this.assignedSandboxes and this.readiedSandboxes.\n * @param {Sandbox} sandbox\n */\n removeSandbox(sandbox) {\n debugging('scheduler') && console.log(`removeSandbox ${sandbox.identifier}`);\n this.removeElement(this.sandboxes, sandbox);\n this.removeElement(this.assignedSandboxes, sandbox);\n\n if (false)\n {}\n\n this.removeElement(this.readiedSandboxes, sandbox);\n }\n\n /**\n * Remove from this.sandboxes and this.assignedSandboxes .\n * @param {Sandbox[]} sandboxes\n */\n async removeSandboxes(sandboxes) {\n debugging('scheduler') && console.log(`removeSandboxes: Remove ${sandboxes.length} sandboxes ${await this.dumpSandboxes(sandboxes)}`);\n this.sandboxes = this.sandboxes.filter(sandbox => sandboxes.indexOf(sandbox) === -1);\n this.assignedSandboxes = this.assignedSandboxes.filter(sandbox => sandboxes.indexOf(sandbox) === -1);\n\n if (Supervisor.debugBuild) {\n const readied = this.readiedSandboxes.filter(sandbox => sandboxes.indexOf(sandbox) !== -1);\n if (readied.length > 0)\n throw new Error(`removeSandboxes: sandboxes ${readied.map(s => s.identifier)} are in readiedSandboxes; inconsistent state.`);\n }\n }\n\n /**\n * Remove element from theArray.\n * @param {Array<*>} theArray\n * @param {object|number} element\n * @param {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 * Log sliceArray.\n * @param {Slice[]} sliceArray\n * @param {string} [header]\n * @returns {Promise<string>}\n */\n async dumpSlices(sliceArray, header) {\n if (header) console.log(`\\n${header}`);\n return compressSlices(sliceArray, this.addressTruncationLength);\n }\n\n /**\n * Log sandboxArray.\n * @param {Sandbox[]} sandboxArray\n * @param {string} [header]\n * @returns {Promise<string>}\n */\n async dumpSandboxes(sandboxArray, header) {\n if (header) console.log(`\\n${header}`);\n return compressSandboxes(sandboxArray, this.addressTruncationLength);\n }\n\n /**\n * If the elements of sandboxSliceArray are not unique, log the duplicates and dump the array.\n * @param {SandboxSlice[]} sandboxSliceArray\n * @param {string} header\n */\n dumpSandboxSlicesIfNotUnique(sandboxSliceArray, header) {\n if (!this.isUniqueSandboxSlices(sandboxSliceArray, header))\n console.log(this.dumpSandboxSlices(sandboxSliceArray));\n }\n\n /**\n * Log { sandbox, slice }.\n * @param {Sandbox} sandbox\n * @param {Slice} slice\n * @returns {string}\n */\n dumpSandboxAndSlice(sandbox, slice) {\n return `${sandbox.id}~${slice.sliceNumber}.${this.dumpJobAddress(slice.jobAddress)}`;\n }\n\n /**\n * Log { sandbox, slice } with state/status.\n * @param {Sandbox} sandbox\n * @param {Slice} slice\n * @returns {string}\n */\n dumpStatefulSandboxAndSlice(sandbox, slice) {\n return `${sandbox.id}.${sandbox.state}~${slice.sliceNumber}.${this.dumpJobAddress(slice.jobAddress)}.${slice.status}`;\n }\n\n /**\n * Truncates jobAddress.toString() to this.addressTruncationLength digits.\n * @param {address} jobAddress\n * @returns {string}\n */\n dumpJobAddress(jobAddress) {\n return truncateAddress(jobAddress, this.addressTruncationLength /* digits*/);\n }\n\n /**\n * Dump sandboxSliceArray.\n * @param {SandboxSlice[]} sandboxSliceArray - input array of { sandbox, slice }\n * @param {string} [header] - optional header\n * @param {boolean} [stateFul] - when true, also includes slice.status and sandbox.state.\n * @returns {string}\n */\n dumpSandboxSlices(sandboxSliceArray, header, stateFul=false) {\n if (header) console.log(`\\n${header}`);\n const jobMap = {};\n sandboxSliceArray.forEach(ss => {\n const sss = stateFul ? `${ss.sandbox.id}.${ss.sandbox.state}~${ss.slice.sliceNumber}.${ss.slice.status}` : `${ss.sandbox.id}~${ss.slice.sliceNumber}`;\n if (!jobMap[ss.slice.jobAddress]) jobMap[ss.slice.jobAddress] = sss;\n else jobMap[ss.slice.jobAddress] += `,${sss}`;\n });\n let output = '';\n for (const [jobAddress, sss] of Object.entries(jobMap))\n output += `${this.dumpJobAddress(jobAddress)}:[${sss}]:`;\n return output;\n }\n\n /**\n * Check sandboxSliceArray for duplicates.\n * @param {SandboxSlice[]} sandboxSliceArray\n * @param {string} [header]\n * @param {function} [log]\n * @returns {boolean}\n */\n isUniqueSandboxSlices(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(`\\tWarning: Found duplicate sandbox ${x.sandbox.identifier}.`);\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(`\\tWarning: Found duplicate slice ${x.slice.identifier}.`);\n } else {\n slices.push(x.slice);\n if (sandboxIndex < 0) result.push(x);\n }\n });\n return sandboxSliceArray.length === result.length;\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 when allocateLocalSandboxes is false.\n *\n * @param {number} numSandboxes - the number of sandboxes to create\n * @param {boolean} [allocateLocalSandboxes=false] - when true, do not place in this.readiedSandboxes\n * @returns {Promise<Sandbox[]>} - resolves with array of created sandboxes, rejects otherwise\n * @throws when given a numSandboxes is not a number or if numSandboxes is Infinity\n */\n async readySandboxes (numSandboxes, allocateLocalSandboxes = false) {\n debugging('supervisor') && console.debug(`readySandboxes: Readying ${numSandboxes} sandboxes, total sandboxes ${this.sandboxes.length}, matching ${this.matching}, fetching ${this.isFetchingNewWork}`);\n \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 // !authorizationMessage <==> sliceNumber === 0.\n const authorizationMessage = sandbox.slice.getAuthorizationMessage();\n\n if (authorizationMessage) {\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 });\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 }\n else\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 // Send a work emit message from the sandbox to the event router\n // !authorizationMessage <==> sliceNumber === 0.\n const authorizationMessage = sandbox.slice.getAuthorizationMessage();\n \n if (!authorizationMessage)\n {\n console.warn(`workEmit: missing authorization message for job ${jobAddress}, slice: ${sliceNumber}`);\n return Promise.resolve();\n }\n \n const workEmitPromise = this.eventRouterConnection.send('workEmit', {\n eventName,\n payload,\n job: jobAddress,\n slice: sliceNumber,\n worker: this.workerOpaqueId,\n authorizationMessage,\n }).catch(error => {\n console.warn(`workEmit: unable to send message to event router ${error.message}`);\n if (Supervisor.debugBuild)\n console.error('workEmit error:', error);\n });\n\n if (Supervisor.debugBuild) {\n workEmitPromise.then(result => {\n if (!result || !result.success)\n console.warn('workEmit: event router did not accept event', result);\n });\n }\n }\n });\n\n // When any sbx completes, \n sandbox.addListener('complete', () => {\n this.watchdog();\n });\n\n sandbox.on('sandboxError', (error) => handleSandboxError(this, sandbox, error));\n \n sandbox.on('rejectedWorkMetrics', (data) =>{\n function updateRejectedMetrics(report) {\n ['total', 'CPU', 'webGL'].forEach((key) => {\n if (report[key]) sandbox.slice.rejectedTimeReport[key] += report[key];\n })\n }\n \n // If the slice already has rejected metrics, add this data to it. If not, assign this data to slices rejected metrics property\n if (sandbox.slice) {\n (sandbox.slice.rejectedTimeReport) ? updateRejectedMetrics(data.timeReport) : sandbox.slice.rejectedTimeReport = data.timeReport;\n }\n })\n \n // If the sandbox terminated and we are not shutting down, then should return all work which is currently\n // not being computed if all sandboxes are dead and the attempt to create a new one fails.\n sandbox.on('terminated',async () => {\n if (this.sandboxes.length > 0) {\n let terminatedSandboxes = this.sandboxes.filter(sbx => sbx.isTerminated);\n if (terminatedSandboxes.length === this.sandboxes.length) {\n debugging('supervisor') && console.debug(`readySandboxes: Create 1 sandbox in the sandbox-terminated-handler, total sandboxes ${this.sandboxes.length}, matching ${this.matching}, fetching ${this.isFetchingNewWork}`);\n await this.readySandboxes(1);\n \n // If we cannot create a new sandbox, that probably means we're on a screensaver worker\n // and the screensaver is down. So return the slices to the scheduler.\n if (this.sandboxes.length !== terminatedSandboxes.length + 1) {\n this.returnSlices(this.queuedSlices).then(() => {\n this.queuedSlices.length = 0;\n });\n }\n }\n }\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 if (!allocateLocalSandboxes) this.readiedSandboxes.push(sandbox);\n this.sandboxes.push(sandbox);\n sandboxes.push(sandbox);\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 debugging('supervisor') && console.log(`readySandboxes: Readied ${sandboxes.length} sandboxes ${JSON.stringify(sandboxes.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 * @param {boolean} [verifySandboxIsNotTerminated=true] - if true, check sandbox is not already terminated\n */\n returnSandbox (sandbox, slice, verifySandboxIsNotTerminated=true) {\n if (!slice || slice.failed || sandbox.isTerminated) {\n \n this.removeSandbox(sandbox);\n \n if (!sandbox.isTerminated) {\n debugging('supervisor') && console.log(`Supervisor.returnSandbox: Terminating ${sandbox.identifier}${slice ? `~${slice.identifier}` : ''}, # of sandboxes ${this.sandboxes.length}`);\n sandbox.terminate(false);\n } else {\n debugging('supervisor') && console.log(`Supervisor.returnSandbox: Already terminated ${sandbox.identifier}${slice ? `~${slice.identifier}` : ''}, # of sandboxes ${this.sandboxes.length}`);\n if (false)\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 * @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 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('supervisor') && console.log(`pruneSandboxes: Terminating readied sandbox ${sandbox.identifier}`);\n this.readiedSandboxes.splice(index, 1);\n this.returnSandbox(sandbox);\n\n if (--numOver <= 0) break;\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('supervisor') && console.log(`pruneSandboxes: Terminating assigned sandbox ${sandbox.identifier}`);\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 //\n // Note: this.slices is the disjoint union of:\n // this.allocatedSlices, \n // this.queuedSlices, \n // this.slices.filter(slice => !slice.isUnassigned) .\n // When a slice is not in these 3 arrays, the slice is lost.\n //\n \n const currentLostSlices = this.slices.filter(slice => slice.isUnassigned \n && this.queuedSlices.indexOf(slice) === -1\n && this.allocatedSlices.indexOf(slice) === -1);\n\n if (currentLostSlices.length > 0) {\n this.lostSlices.push(...currentLostSlices);\n // Try to recover.\n // Needs more work and testing.\n // Test when we can come up with a decent lost slice repro case.\n // --> this.queuedSlices.push(...currentLostSlices);\n }\n\n if (this.lostSlices.length > 0) {\n if (true) { // Keep this on for awhile, until we know lost slices aren't happening.\n console.warn('Supervisor.watchdog: Found lost slices!');\n for (const slice of this.lostSlices)\n console.warn('\\t', slice.identifier);\n }\n this.lostSlices = this.lostSlices.filter(slice => slice.isUnassigned);\n }\n\n const slices = [];\n this.queuedSlices.forEach(slice => {\n assert(slice && slice.sliceNumber > 0);\n addToSlicePayload(slices, slice, sliceStatus.scheduled);\n });\n\n this.allocatedSlices.forEach(slice => {\n assert(slice && slice.sliceNumber > 0);\n addToSlicePayload(slices, slice, 'progress'); // Beacon.\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 * Returns the number of unallocated sandbox slots to send to fetchTask.\n *\n * @returns {number}\n */\n numberOfAvailableSandboxSlots() {\n let numCores;\n if (this.options.priorityOnly && this.options.jobAddresses.length === 0) {\n numCores = 0;\n } else if (this.queuedSlices.length > 1) {\n // We have slices queued, no need to fetch\n numCores = 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 the last 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 numCores = this.unallocatedSpace - longSliceCount;\n }\n return numCores;\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 slices are fetched it calls this.distributeQueuedSlices.\n *\n * @returns {Promise<void>}, unallocatedSpace ${this.unallocatedSpace}\n */\n async work()\n {\n // When inside matchSlicesWithSandboxes, don't reenter Supervisor.work to fetch new work or create new sandboxes.\n if (this.matching) {\n // Interesting and noisy.\n // debugging('supervisor') && console.log(`Supervisor.work: Do not interleave work, fetch or matching slices with sandboxes: queuedSlices ${this.queuedSlices.length}, unallocatedSpace ${this.unallocatedSpace}, matching ${this.matching}, fetching ${this.isFetchingNewWork}`);\n return Promise.resolve();\n }\n\n await this.setDefaultIdentityKeystore();\n\n // Instantiate connections that don't exist.\n this.instantiateAllConnections();\n\n const numCores = this.numberOfAvailableSandboxSlots();\n\n debugging() && console.log(`Supervisor.work: Try to get ${numCores} slices in working sandboxes, unallocatedSpace ${this.unallocatedSpace}, queued slices ${this.queuedSlices.length}, # of sandboxes ${this.sandboxes.length}, matching ${this.matching}, fetching: ${this.isFetchingNewWork}`);\n \n // Fetch a new task if we have no more slices queued, then start workers\n try {\n if (numCores > 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 if (this.maxWorkingSandboxes > this.sandboxes.length) {\n // Note: The old technique had \n // while (this.maxWorkingSandboxes > this.sandboxes.length) {....\n // and sometimes we'd get far too many sandboxes, because it would keep looping while waiting for\n // this.readySandboxes(this.maxWorkingSandboxes - this.sandboxes.length);\n // to construct the rest of the sandboxes. The fix is to only loop when the 1st \n // await this.readySandboxes(1) \n // is failing.\n let needFirstSandbox = true;\n while (needFirstSandbox) {\n debugging('supervisor') && console.log(`Supervisor.work: ready 1 sandbox, # of sandboxes ${this.sandboxes.length}, matching ${this.matching}, fetching ${this.isFetchingNewWork}`);\n await this.readySandboxes(1)\n .then(() => {\n debugging('supervisor') && console.log(`Supervisor.work: ready ${this.maxWorkingSandboxes - this.sandboxes.length} sandbox(es), # of sandboxes ${this.sandboxes.length}, matching ${this.matching}, fetching ${this.isFetchingNewWork}`);\n this.readySandboxes(this.maxWorkingSandboxes - this.sandboxes.length);\n needFirstSandbox = false;\n }).catch(error => {\n console.warn('906: failed to ready sandboxes; will retry', error.code, error.message);\n });\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.close('Fetch timed out', Math.random() > 0.5).catch(error => {\n console.error(`931: Failed to close task-distributor connection`, error);\n });\n this.resultSubmitterConnection.close('Fetch timed out', Math.random() > 0.5).catch(error => {\n console.error(`920: Failed to close result-submitter connection`, error);\n });\n this.isFetchingNewWork = false;\n this.instantiateAllConnections();\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 (e)\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: ${e}`);\n this.isFetchingNewWork = false; // <-- done in the `finally` block, below\n clearTimeout(fetchTimeout);\n 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.resultSubmitterConnection.close('Failed to connect to result-submitter', true).catch(error => {\n console.error(`942: Failed to close result-submitter connection`, error);\n });\n return Promise.resolve();\n }\n await this.fetchTask(numCores).finally(() => {\n clearTimeout(fetchTimeout);\n this.isFetchingNewWork = false;\n });\n }\n\n this.distributeQueuedSlices().then(() => debugging('supervisor') && 'supervisor: finished distributeQueuedSlices()').catch((e) => {\n // We should never get here, because distributeQueuedSlices was changed\n // to try to catch everything and return slices and sandboxes.\n // If we do catch here it may mean a slice was lost. \n console.error('Supervisor.work catch handler for distributeQueuedSlices.', e);\n });\n // No catch(), because it will bubble outward to the caller\n } finally {\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 * Remove all unreferenced jobs in this.cache .\n * @param {*[]} newJobs -- Jobs that should not be removed from this.cache.\n */\n cleanJobCache(newJobs = []) {\n /* Delete all jobs in the supervisorCache that are not represented in this newJobs,\n * or in this.queuedSlices, or there is no sandbox assigned to these jobs.\n * Note: There can easily be 200+ places to check; using a lookup structure to maintain O(n).\n */\n if (this.cache.jobs.length > 0) {\n const jobAddressMap = {};\n Object.keys(newJobs).forEach(jobAddress => { jobAddressMap[jobAddress] = 1; });\n this.slices.forEach(slice => { if (!jobAddressMap[slice.jobAddress]) jobAddressMap[slice.jobAddress] = 1; });\n this.cache.jobs.forEach(jobAddress => {\n if (!jobAddressMap[jobAddress]) {\n this.cache.remove('job', jobAddress);\n // Remove and return the corresponding sandboxes from this.sandboxes.\n const deadSandboxes = this.sandboxes.filter(sb => sb.jobAddress === jobAddress);\n if (deadSandboxes.length > 0) {\n deadSandboxes.forEach(sandbox => { this.returnSandbox(sandbox); });\n debugging('supervisor') && console.log(`Supervisor.fetchTask: Deleting job ${jobAddress} from cache and assigned sandboxes ${deadSandboxes.map(s => s.id)}, # of sandboxes ${this.sandboxes.length}.`);\n }\n }\n });\n }\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 // Don't reenter\n if (this.matching || numCores <= 0) {\n // Interesting and noisy.\n debugging('supervisor') && console.log(`Supervisor.fetchTask: Do not nest work, fetch or matching slices with sandboxes: queuedSlices ${this.queuedSlices.length}, unallocatedSpace ${this.unallocatedSpace}, matching ${this.matching}, fetching ${this.isFetchingNewWork}, numCores ${numCores}`);\n return Promise.resolve();\n }\n\n //\n // Oversubscription mitigation.\n // Update when there are less available sandbox slots than numCores.\n const checkNumCores = this.numberOfAvailableSandboxSlots();\n if (numCores > checkNumCores) numCores = checkNumCores;\n if (numCores <= 0) return Promise.resolve();\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.ringBufferofJobs.buf, //Only discrete jobs\n rejectedJobs: this.rejectedJobs,\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('supervisor') && console.log(`fetchTask wants ${numCores} slice(s), unallocatedSpace ${this.unallocatedSpace}, queuedSlices ${this.queuedSlices.length}`);\n try {\n debugging('requestTask') && console.debug('fetchTask: 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 Promise.resolve();\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 * messageLightWeight: { workerId: worker, jobSlices, schedulerId, jobCommissions }\n * messageBody: { newJobs: await getNewJobsForTask(dbScheduler, task, request), task }\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 \n /*\n * Ensure all jobs received from the scheduler are:\n * 1. If we have specified specific jobs the worker may work on, the received jobs are in the specified job list\n * 2. If we are in localExec, at most 1 unique job type was received (since localExec workers are designated for only\n * one job)\n * If the received jobs are not within these parameters, stop the worker since the scheduler cannot be trusted at that point.\n */\n if ((this.options.jobAddresses.length && !Object.keys(newJobs).every((ele) => this.options.jobAddresses.includes(ele)))\n || (this.options.localExec && Object.keys(newJobs).length > 1))\n {\n console.error(\"Worker received slices it shouldn't have. Rejecting the work and stopping.\");\n process.exit(1);\n }\n\n debugging() && console.log(`Supervisor.fetchTask: task: ${task.length}/${numCores}, jobs: ${Object.keys(newJobs).length}, jobSlices: ${await compressJobArray(authorizationMessage.auth.jobSlices, true /* skipFirst*/, this.addressTruncationLength /* digits*/)}`);\n\n // Delete all jobs in the supervisorCache that are not represented in this task,\n // or in this.queuedSlices, or there is no sandbox assigned to these jobs.\n this.cleanJobCache(newJobs);\n\n for (const jobAddress of Object.keys(newJobs))\n if (!this.cache.cache.job[jobAddress])\n this.cache.store('job', jobAddress, newJobs[jobAddress]);\n\n // Memoize authMessage onto the Slice object, this should\n // follow it for its entire life in the worker.\n const tmpQueuedSlices = task.map(taskElement => new Slice(taskElement, authorizationMessage));\n\n // Make sure old stuff is up front.\n // matchSlicesWithSandboxes dequeues this.queuedSlices as follows:\n // slicesToMatch = this.queuedSlices.slice(0, numCores);\n this.slices.push(...tmpQueuedSlices);\n this.queuedSlices.push(...tmpQueuedSlices);\n \n // Populating the ring buffer based on job's discrete property \n Object.values(newJobs).forEach(job => {\n if(job.requirements.discrete && this.ringBufferofJobs.find(element => element === job.address) === undefined) {\n this.ringBufferofJobs.push(job.address);\n }\n });\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 * Note: When a sandbox is paired with a slice, execution is pending and sandbox.allocated=true and\n * sandbox.slice=slice and sandbox.jobAddress=slice.jobAddress. This is what 'allocated' means.\n * Immediately upon the exit of sandbox.work, sandbox.allocated=false is set and if an exception\n * wasn't thrown, the paired slice is placed in this.assignedSandboxes.\n * Thus from the pov of supervisor, this.allocatedSandboxes is deterministic and this.workingSandboxes is not.\n * Please try to not use this.workingSandboxes. It is deprecated.\n *\n * The input is numCores, this,queuedSlices, this.assignedSandboxes and this.readiedSandboxes.\n * If there are not enough sandboxes, new readied sandboxes will be created using\n * await this.readySandboxes(...)\n * And it is this await boundary that has caused many bugs.\n * We try not to make assumptions about non-local state across the await boundary.\n *\n * @param {number} numCores - The number of available sandbox slots.\n * @param {boolean} [throwExceptions=true] - Whether to throw exceptions when checking for sanity.\n * @returns {Promise<SandboxSlice[]>} Returns SandboxSlice[], may have length zero.\n */\n async matchSlicesWithSandboxes (numCores, throwExceptions = true) {\n\n const sandboxSlices = [];\n if (this.queuedSlices.length === 0 || this.matching || numCores <= 0) {\n // Interesting and noisy.\n // debugging('supervisor') && console.log(`Supervisor.matchSlicesWithSandboxes: Do not nest work, fetch or matching slices with sandboxes: queuedSlices ${this.queuedSlices.length}, unallocatedSpace ${this.unallocatedSpace}, matching ${this.matching}, fetching ${this.isFetchingNewWork}, numCores ${numCores}`);\n return sandboxSlices;\n }\n\n //\n // Oversubscription mitigation.\n // Update when there are less available sandbox slots than numCores.\n // We cannot use this.unallocatedSpace here because its value is artificially low or zero, because in\n // this.distributedQueuedSlices we use the pseudo-mutex trick: this.acquire(howManySandboxSlotsToReserve)/this.release().\n // Note: Do not use this.numberOfCoresReserved outside of a function locked with this.acquire(howManySandboxSlotsToReserve) .\n const checkNumCores = this.numberOfCoresReserved; // # of locked sandbox slots.\n if (numCores > checkNumCores) numCores = checkNumCores;\n if (numCores <= 0) return sandboxSlices;\n\n // Don't ask for more than we have.\n if (numCores > this.queuedSlices.length)\n numCores = this.queuedSlices.length;\n\n debugging('supervisor') && console.log(`matchSlicesWithSandboxes: numCores ${numCores}, queued slices ${this.queuedSlices.length}: assigned ${this.assignedSandboxes.length}, readied ${this.readiedSandboxes.length}, unallocated ${this.unallocatedSpace}, # of sandboxes: ${this.sandboxes.length}`);\n\n if (debugging('supervisor')) {\n dumpSlicesIfNotUnique(this.queuedSlices, 'Warning: this.queuedSlices slices are not unique -- this is ok when slice is rescheduled.');\n dumpSandboxesIfNotUnique(this.readiedSandboxes, 'Warning: this.readiedSandboxes sandboxes are not unique!');\n dumpSandboxesIfNotUnique(this.assignedSandboxes, 'Warning: this.assignedSandboxes sandboxes are not unique!');\n }\n\n // Three functions to validate slice and sandbox.\n function checkSlice(slice, checkAllocated=true) {\n if (!slice.isUnassigned) throw new DCPError(`Slice must be unassigned: ${slice.identifier}`);\n if (checkAllocated && slice.allocated) throw new DCPError(`Slice must not already be allocated: ${slice.identifier}`);\n }\n function checkSandbox(sandbox, isAssigned) {\n if (sandbox.allocated) throw new DCPError(`Assigned sandbox must not be already allocated: ${sandbox.identifier}`);\n if (isAssigned && !sandbox.isAssigned) throw new DCPError(`Assigned sandbox is not marked as assigned: ${sandbox.identifier}`);\n if (!isAssigned && !sandbox.isReadyForAssign) throw new DCPError(`Readied sandbox is not marked as ready for assign: ${sandbox.identifier}`);\n }\n\n // Sanity checks.\n if (throwExceptions) {\n this.assignedSandboxes.forEach(sandbox => { checkSandbox(sandbox, true /* isAssigned*/); });\n this.readiedSandboxes.forEach(sandbox => { checkSandbox(sandbox, false /* isAssigned*/); });\n this.queuedSlices.forEach(slice => { checkSlice(slice); });\n } else {\n this.assignedSandboxes = this.assignedSandboxes.filter(sandbox => !sandbox.allocated && sandbox.isAssigned);\n this.readiedSandboxes = this.readiedSandboxes.filter(sandbox => !sandbox.allocated && sandbox.isReadyForAssign);\n this.queuedSlices = this.queuedSlices.filter(slice => !slice.allocated && slice.isUnassigned);\n }\n\n const sandboxKind = {\n assigned: 0,\n ready: 1,\n new: 2,\n };\n\n const ceci = this;\n /**\n * Auxiliary function to pair a sandbox with a slice and mark the sandbox as allocated.\n * An allocated sandbox is reserved and will not be released until the slice completes execution on the sandbox.\n *\n * @param {Sandbox} sandbox\n * @param {Slice} slice\n * @param {number} kind\n */\n function pair(sandbox, slice, kind) {\n checkSandbox(sandbox, kind === sandboxKind.assigned);\n checkSlice(slice, kind === sandboxKind.assigned);\n slice.allocated = true;\n sandbox.allocated = true;\n sandbox.jobAddress = slice.jobAddress; // So we can know which jobs to not delete from this.cache .\n sandbox.slice = slice;\n sandboxSlices.push({ sandbox, slice });\n if (Supervisor.sliceTiming) slice['pairingDelta'] = Date.now();\n if (debugging('supervisor')) {\n let fragment = 'New readied';\n if (kind === sandboxKind.assigned) fragment = 'Assigned';\n else if (kind === sandboxKind.ready) fragment = 'Readied';\n console.log(`matchSlicesWithSandboxes.pair: ${fragment} sandbox matched ${ceci.dumpSandboxAndSlice(sandbox, slice)}`);\n }\n }\n\n // These three arrays are used to track/store slices and sandboxes,\n // so that when an exception occurs, the following arrays are restored:\n // this.queuedSlices, this.assignedSandboxes, this.realizedSandboxes.\n let slicesToMatch = [];\n let trackAssignedSandboxes = [];\n let trackReadiedSandboxes = [];\n try\n {\n this.matching = true;\n\n let assignedCounter = 0; // How many assigned sandboxes are being used.\n let readyCounter = 0; // How many sandboxes used from the existing this.readiedSandboxes.\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. First we match with assigned sandboxes in the order\n // that they appear in this.queuedSlices. Then we match in-order with existing this.readiedSandboxes\n // Then we match in-order with new new readied sandboxes created through\n // await this.readySandboxes(newCounter, true /* allocateLocalSandboxes*/);\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 // Note: The ordering of sandboxSlices only partially corresponds to the order of this.queuedSlices.\n // It's easy to do. When pairing with assigned sandboxes, any slice in this.queuedSlices which doesn't\n // have an assigned sandbox, will add null to the sandboxSlices array. Then when pairing with readied sandboxes,\n // we fill-in the null entries in the sandboxSlices array.\n //\n /** XXXpfr @todo When it is needed, fix the ordering as described above. */\n\n // Get the slices that are being matched.\n slicesToMatch = this.queuedSlices.slice(0, numCores);\n this.queuedSlices = this.queuedSlices.slice(numCores);\n\n debugging('supervisor') && console.log(`matchSlicesWithSandboxes: slicesToMatch ${await this.dumpSlices(slicesToMatch)}`);\n\n // Create object map: jobAddress -> sandboxes with sandboxes.jobAddress === jobAddress .\n const jobSandboxMap = toJobMap(this.assignedSandboxes, sandbox => sandbox);\n \n // Create array to hold slices which do not have assigned sandboxes.\n // These slices will need to be paired with existing and possibly new readied sandboxes.\n // Specifically, the sandboxes from existing this.readiedSandboxes and new sandboxes\n // created through await this.readySandboxes(newCounter, true /* allocateLocalSandboxes*/);\n const slicesThatNeedSandboxes = [];\n\n // Pair assigned sandboxes with slices.\n for (const slice of slicesToMatch) {\n const assigned = jobSandboxMap[slice.jobAddress];\n if (assigned && assigned.length > 0) {\n // Pair.\n const sandbox = assigned.pop();\n pair(sandbox, slice, sandboxKind.assigned);\n this.removeElement(this.assignedSandboxes, sandbox);\n // Track.\n trackAssignedSandboxes.push(sandbox);\n assignedCounter++;\n } else {\n // Don't lose track of these slices.\n slice.allocated = true;\n slicesThatNeedSandboxes.push(slice);\n }\n }\n\n // Pair readied sandboxes with slices.\n readyCounter = Math.min(slicesThatNeedSandboxes.length, this.readiedSandboxes.length);\n newCounter = slicesThatNeedSandboxes.length - readyCounter;\n // Track.\n trackReadiedSandboxes = this.readiedSandboxes.slice(0, readyCounter);\n this.readiedSandboxes = this.readiedSandboxes.slice(readyCounter);\n for (const sandbox of trackReadiedSandboxes) {\n // Pair.\n const slice = slicesThatNeedSandboxes.pop();\n pair(sandbox, slice, sandboxKind.ready);\n }\n \n debugging('supervisor') && console.log(`matchSlicesWithSandboxes: assignedCounter ${assignedCounter}, readyCounter ${readyCounter}, newCounter ${newCounter}, numCores ${numCores}`)\n\n // Validate algorithm consistency.\n if (Supervisor.debugBuild && assignedCounter + readyCounter + newCounter !== numCores) {\n // Structured assert.\n throw new DCPError(`matchSlicesWithSandboxes: Algorithm is corrupt ${assignedCounter} + ${readyCounter} + ${newCounter} !== ${numCores}`);\n }\n\n // Here is an await boundary.\n // Accessing non-local data across an await boundary may result in the unexpected.\n\n // Create new readied sandboxes to associate with slicesThatNeedSandboxes.\n if (newCounter > 0) {\n // When allocateLocalSandboxes is true, this.readySandboxes does not place the new sandboxes\n // on this.readiedSandboxes. Hence the new sandboxes are private and nobody else can see them.\n debugging('supervisor') && console.log(`matchSlicesWithSandboxes: creating ${newCounter} new sandboxes, # of sandboxes ${this.sandboxes.length}`);\n const readied = await this.readySandboxes(newCounter, true /* allocateLocalSandboxes*/);\n // Track.\n trackReadiedSandboxes.push(...readied);\n\n for (const sandbox of readied) {\n assert(slicesThatNeedSandboxes.length > 0);\n // Pair\n const slice = slicesThatNeedSandboxes.pop();\n pair(sandbox, slice, sandboxKind.new);\n }\n \n // Put back any extras. There should not be any unless readySandboxes returned less than asked for.\n if (slicesThatNeedSandboxes.length > 0) {\n slicesThatNeedSandboxes.forEach(slice => {\n slice.allocated = false;\n this.queuedSlices.push(slice);\n });\n }\n }\n\n if ( false || debugging()) {\n console.log(`matchSlicesWithSandboxes: Matches: ${ this.dumpSandboxSlices(sandboxSlices) }`);\n this.dumpSandboxSlicesIfNotUnique(sandboxSlices, 'Warning: sandboxSlices; { sandbox, slice } pairs are not unique!');\n }\n } catch (e) {\n // Clear allocations.\n slicesToMatch.forEach(slice => { slice.allocated = false; });\n trackAssignedSandboxes.forEach(sandbox => { sandbox.allocated = false; sandbox.slice = null; });\n trackReadiedSandboxes.forEach(sandbox => { sandbox.allocated = false; sandbox.slice = null; sandbox.jobAddress = null; });\n \n // Filter out redundancies -- there shouldn't be any...\n slicesToMatch = slicesToMatch.filter(slice => this.queuedSlices.indexOf(slice) === -1);\n trackAssignedSandboxes = trackAssignedSandboxes.filter(sb => this.assignedSandboxes.indexOf(sb) === -1);\n trackReadiedSandboxes = trackReadiedSandboxes.filter(sb => this.readiedSandboxes.indexOf(sb) === -1);\n\n // Sanity checks.\n slicesToMatch.forEach(slice => { checkSlice(slice) });\n trackAssignedSandboxes.forEach(sandbox => { checkSandbox(sandbox, true /* isAssigned*/); });\n trackReadiedSandboxes.forEach(sandbox => { checkSandbox(sandbox, false /* isAssigned*/); });\n\n // Restore arrays.\n this.queuedSlices.push(...slicesToMatch);\n this.assignedSandboxes.push(...trackAssignedSandboxes);\n this.readiedSandboxes.push(...trackReadiedSandboxes);\n \n console.error('Error in matchSlicesWithSandboxes: Attempting to recover slices and sandboxes.', e);\n return [];\n } finally {\n this.matching = false;\n }\n\n debugging('supervisor') && console.log(`matchSlicesWithSandboxes: allocated ${sandboxSlices.length} sandboxes, queuedSlices ${this.queuedSlices.length}, unallocatedSpace ${this.unallocatedSpace}, matching ${this.matching}, fetching ${this.isFetchingNewWork}, # of sandboxes: ${this.sandboxes.length}.`);\n\n return sandboxSlices;\n }\n\n disassociateSandboxAndSlice(sandbox, slice) {\n this.returnSandbox(sandbox);\n sandbox.slice = null;\n this.returnSlice(slice);\n }\n\n /**\n * This method will call this.startSandboxWork(sandbox, slice) for each element { sandbox, slice }\n * of the array returned by this.matchSlicesWithSandboxes(availableSandboxes) until all allocated sandboxes\n * are working. It is possible for a sandbox to interleave with calling distributeQueuedSlices and leave a sandbox\n * that is not working. Moreover, this.queuedSlices may be exhausted before all sandboxes are working.\n * @returns {Promise<void>}\n */\n async distributeQueuedSlices () {\n const numCores = this.unallocatedSpace;\n\n // If there's nothing there, or we're reentering, bail out.\n if (this.queuedSlices.length === 0 || numCores <= 0 || this.matching) {\n // Interesting and noisy.\n // debugging('supervisor') && console.log(`Supervisor.distributeQueuedSlices: Do not nest work, fetch or matching slices with sandboxes: queuedSlices ${this.queuedSlices.length}, matching ${this.matching}, fetching ${this.isFetchingNewWork}, numCores ${numCores}`);\n return Promise.resolve();\n }\n\n //\n // Use the pseudo-mutex to prevent uncontrolled interleaving with fetchTask,\n // matchSlicesWithSandboxes and distributeQueuedSlices\n let sandboxSlices;\n this.acquire(numCores);\n try {\n sandboxSlices = await this.matchSlicesWithSandboxes(numCores);\n } finally {\n this.release();\n }\n\n debugging('supervisor') && console.log(`distributeQueuedSlices: ${sandboxSlices.length} sandboxSlices ${this.dumpSandboxSlices(sandboxSlices)}, matching ${this.matching}, fetching ${this.isFetchingNewWork}`);\n\n for (let sandboxSlice of sandboxSlices) {\n\n const { sandbox, slice } = sandboxSlice;\n try {\n if (sandbox.isReadyForAssign) {\n try {\n let timeoutMs = Math.floor(Math.min(+Supervisor.lastAssignFailTimerMs || 0, 10 * 60 * 1000 /* 10m */));\n await a$sleepMs(timeoutMs);\n await this.assignJobToSandbox(sandbox, slice.jobAddress);\n } catch (e) {\n console.error(`Supervisor.distributeQueuedSlices: Could not assign slice ${slice.identifier} to sandbox ${sandbox.identifier}.`);\n if (Supervisor.debugBuild) console.error(`...exception`, e);\n Supervisor.lastAssignFailTimerMs = Supervisor.lastAssignFailTimerMs ? +Supervisor.lastAssignFailTimerMs * 1.25 : Math.random() * 200;\n this.disassociateSandboxAndSlice(sandbox, 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 } catch (e) {\n // We should never get here.\n console.error(`Supervisor.distributeQueuedSlices: Failed to execute slice ${slice.identifier} in sandbox ${sandbox.identifier}.`);\n if (Supervisor.debugBuild) console.error('...exception', e);\n this.disassociateSandboxAndSlice(sandbox, slice);\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 * Handles reassigning or returning a slice that was rejected by a sandbox.\n * \n * The sandbox will be terminated by this.returnSandbox in finalizeSandboxAndSlice. In this case,\n * if the slice does not have a rejected property already, reassign the slice to a new sandbox\n * and add a rejected property to the slice to indicate it has already rejected once, then set slice = null\n * in the return SandboxSlice so that finalizeSandboxAndSlice won't return slice to scheduler.\n * \n * If the slice rejects with a reason, or has a rejected time stamp (ie. has been rejected once already)\n * then return the slice and all slices from the job to the scheduler and\n * terminate all sandboxes with that jobAddress.\n * @param {Sandbox} sandbox \n * @param {Slice} slice\n * @returns {Promise<SandboxSlice>}\n */\n async handleWorkReject(sandbox, slice, rejectReason) {\n if (!this.rejectedJobReasons[slice.jobAddress])\n this.rejectedJobReasons[slice.jobAddress] = [];\n\n this.rejectedJobReasons[slice.jobAddress].push(rejectReason); // memoize reasons\n\n // First time rejecting without a reason. Try assigning slice to a new sandbox.\n if (rejectReason === 'false' && !slice.rejected) {\n // Set rejected.\n slice.rejected = Date.now();\n // Schedule the slice for execution.\n this.scheduleSlice(slice, true /* placeInTheFrontOfTheQueue*/, false /* noDuplicateExecution*/);\n \n // Null out slice so this.returnSlice will not be called in finalizeSandboxAndSlice.\n // But we still want this.returnSandbox to terminate the sandbox.\n slice = null;\n } else { // Slice has a reason OR rejected without a reason already and got stamped.\n \n // Purge all slices and sandboxes associated with slice.jobAddress .\n this.purgeAllWork(slice.jobAddress);\n // Clear jobAddress from this.cache .\n this.cleanJobCache();\n\n //\n // this.purgeAllWork(jobAddress) terminates all sandboxes with jobAddress,\n // and it also returns to scheduler all slices with jobAddress.\n // Therefore null out slice and sandbox so finalizeSandboxAndSlice doesn't do anything.\n // \n sandbox = null;\n slice = null;\n\n // Add to array of rejected jobs.\n let rejectedJob = {\n address: slice.jobAddress,\n reasons: this.rejectedJobReasons[slice.jobAddress],\n }\n this.rejectedJobs.push(rejectedJob);\n\n // Tell everyone all about it, when allowed.\n if (dcpConfig.worker.allowConsoleAccess || Supervisor.debugBuild)\n console.warn('Supervisor.handleWorkReject: The slice ${slice.identifier} was rejected twice; slice will be returned to the scheduler.');\n console.warn('Supervisor.handleWorkReject: All slices with the same jobAddress returned to the scheduler.');\n console.warn('Supervisor.handleWorkReject: All sandboxes with the same jobAddress are terminated.');\n }\n return { sandbox, slice };\n }\n\n /**\n * Schedule the slice to be executed.\n * If slice is already executing and noDuplicateExecution is true, return the slice with reason.\n * @param {Slice} slice\n * @param {boolean} [placeInTheFrontOfTheQueue=false]\n * @param {boolean} [noDuplicateExecution=true]\n * @param {string} [reason]\n */\n scheduleSlice(slice, placeInTheFrontOfTheQueue = false, noDuplicateExecution = true, reason) {\n // When noDuplicateExecution, if slice is already executing, do nothing.\n let workingSlices = [];\n if (noDuplicateExecution)\n workingSlices = this.allocatedSlices;\n\n if (!workingSlices.indexOf(slice)) {\n // Reset slice state to allow execution.\n slice.status = SLICE_STATUS_UNASSIGNED;\n // Enqueue in the to-be-executed queue.\n if (placeInTheFrontOfTheQueue) this.queuedSlices.unshift(slice);\n else this.queuedSlices.push(slice);\n }\n }\n\n /**\n * Purge all slices and sandboxes with this jobAddress.\n * @param {address} jobAddress\n * @param {boolean} [onlyPurgeQueuedAndAllocated=false]\n */\n purgeAllWork(jobAddress, onlyPurgeQueuedAndAllocated = false) {\n // Purge all slices and sandboxes associated with jobAddress .\n const deadSandboxes = this.sandboxes.filter(sandbox => sandbox.jobAddress === jobAddress);\n\n if (deadSandboxes.length > 0) {\n debugging('supervisor') && console.log(`purgeAllWork(${this.dumpJobAddress(jobAddress)}): sandboxes purged ${deadSandboxes.map(s => s.id)}, # of sandboxes ${this.sandboxes.length}`);\n deadSandboxes.forEach(sandbox => this.returnSandbox(sandbox));\n }\n\n let deadSlices;\n if (onlyPurgeQueuedAndAllocated) {\n deadSlices = this.queuedSlices.filter(slice => slice.jobAddress === jobAddress);\n if (deadSlices.length > 0 || this.allocatedSlices.length > 0)\n debugging('supervisor') && console.log(`purgeAllWork(${this.dumpJobAddress(jobAddress)}): dead queuedSlices ${deadSlices.map(s => s.sliceNumber)}, dead allocatedSlices ${this.allocatedSlices.map(s => s.sliceNumber)}`);\n deadSlices.push(...this.allocatedSlices);\n } else {\n deadSlices = this.slices.filter(slice => slice.jobAddress === jobAddress);\n }\n\n if (deadSlices.length > 0) {\n debugging('supervisor') && console.log(`purgeAllWork(${this.dumpJobAddress(jobAddress)}): slices purged ${deadSlices.map(s => s.sliceNumber)}, # of sandboxes ${this.sandboxes.length}`);\n this.returnSlices(deadSlices);\n this.removeQueuedSlices(deadSlices);\n }\n debugging('supervisor') && console.log(`purgeAllWork(${this.dumpJobAddress(jobAddress)}): Finished: slices ${this.slices.length}, queuedSlices ${this.queuedSlices.length}, assigned ${this.assignedSandboxes.length}, readied ${this.readiedSandboxes.length}, # of sandboxes ${this.sandboxes.length}`);\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, reason = 'unknown';\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('startSandboxWork: slice.markAsWorking exception:', e);\n return Promise.resolve();\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(`startSandboxWork: Started ${this.dumpStatefulSandboxAndSlice(sandbox, slice)}, total sandbox count: ${this.sandboxes.length}, matching ${this.matching}, fetching ${this.isFetchingNewWork}`);\n if (Supervisor.sliceTiming) {\n slice['pairingDelta'] = Date.now() - slice['pairingDelta'];\n slice['executionDelta'] = Date.now();\n }\n let result;\n try {\n result = await sandbox.work(slice, startDelayMs);\n } finally {\n sandbox.allocated = false;\n slice.allocated = false;\n }\n if (Supervisor.sliceTiming) {\n slice['executionDelta'] = Date.now() - slice['executionDelta'];\n slice['resultDelta'] = Date.now();\n }\n slice.collectResult(result, true);\n // In watchdog, all sandboxes in working state, have their slice 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 this.assignedSandboxes.push(sandbox);\n debugging() && console.log(`startSandboxWork: Finished ${this.dumpStatefulSandboxAndSlice(sandbox, slice)}, total sandbox count: ${this.sandboxes.length}, matching ${this.matching}, fetching ${this.isFetchingNewWork}`);\n } catch(error) {\n let logLevel;\n\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, so just return the slice in the finally block.\n // For extra safety the sandbox is terminated.\n slice.result = null;\n slice.status = SLICE_STATUS_FAILED; /** XXXpfr @todo terminating sandbox? */\n }\n\n let errorString;\n switch (error.errorCode) {\n case 'ENOPROGRESS':\n reason = 'ENOPROGRESS';\n errorString = 'Supervisor.startSandboxWork - No progress error in sandbox.\\n';\n break;\n case 'ESLICETOOSLOW':\n reason = 'ESLICETOOSLOW';\n errorString = 'Supervisor.startSandboxWork - Slice too slow error in sandbox.\\n';\n break;\n case 'EUNCAUGHT':\n reason = 'EUNCAUGHT';\n errorString = `Supervisor.startSandboxWork - Uncaught error in sandbox ${error.message}.\\n`;\n break;\n case 'EFETCH':\n // reason = 'EFETCH'; The status.js processing cannot handle 'EFETCH'\n reason = 'unknown';\n errorString = `Supervisor.startSandboxWork - Could not fetch data: ${error.message}.\\n`;\n break;\n }\n\n if (error.name === 'EWORKREJECT') {\n error.stack = 'Sandbox was terminated by work.reject()';\n const ss = await this.handleWorkReject(sandbox, slice, error.message);\n sandbox = ss.sandbox; slice = ss.slice;\n }\n\n const errorObject = {\n jobAddress: slice.jobAddress.substr(0,10),\n sliceNumber: slice.sliceNumber,\n sandbox: sandbox.id,\n jobName: sandbox.public ? sandbox.public.name : 'unnamed',\n };\n\n // Always display max info under debug builds, otherwise maximal error\n // messages are displayed to the worker, only if both worker and client agree.\n let workerConsole = sandbox.supervisorCache.cache.job[slice.jobAddress].workerConsole;\n const displayMaxInfo = Supervisor.debugBuild || (workerConsole && dcpConfig.worker.allowConsoleAccess);\n\n if (!displayMaxInfo && errorString) {\n console[logLevel](errorString, errorObject);\n } else if (!displayMaxInfo && error.name === 'EWORKREJECT') {\n console[logLevel](`Supervisor.startSandboxWork - Sandbox rejected work: ${error.message}`)\n } else {\n if (displayMaxInfo)\n errorObject.stack += '\\n --------------------\\n' + (error.stack.split('\\n').slice(1).join('\\n'));\n console[logLevel](`Supervisor.startSandboxWork - Sandbox failed: ${error.message}\\n`, errorObject);\n }\n } finally {\n await this.finalizeSandboxAndSlice(sandbox, slice, reason);\n }\n }\n\n /**\n * If slice && slice.result, then call await this.recordResult(slice) and this.returnSandbox(sandbox, slice) will have no effect.\n * If slice && !slice.result, then call this.returnSlice(slice, reason) and then this.returnSandbox(sandbox, slice) which terminates sandbox.\n * If !slice && sandbox, then terminate the sandbox with this.returnSandbox(sandbox, slice) .\n * If !slice && !sandbox, then do nothing.\n * @param {Sandbox} [sandbox]\n * @param {Slice} [slice]\n * @param {string} [reason]\n */\n async finalizeSandboxAndSlice(sandbox, slice, reason) {\n debugging('supervisor') && console.log(`finalizeSandboxAndSlice: sandbox ${sandbox ? sandbox.identifier : 'nade'}, slice ${slice ? slice.identifier : 'nade'}`);\n if (slice) {\n if (slice.result) await this.recordResult(slice);\n else this.returnSlice(slice, reason);\n }\n // It is possible that sandbox is already terminated\n // Because sandbox.allocated=false as soon as sandbox.work(...) completes.\n // But the await at or in finalizeSandboxAndSlice may allow pruneSandboxes to slither in.\n if (sandbox) this.returnSandbox(sandbox, slice, false /* verifySandboxIsNotTerminated*/);\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-allocated 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 debugging('supervisor') && console.log('stopWork(${forceTerminate}): terminating sandboxes and returning slices to scheduler.');\n if (forceTerminate) {\n for (const sandbox of this.sandboxes) {\n this.returnSandbox(sandbox, null, false /* verifySandboxIsNotTerminated*/);\n }\n\n await this.returnSlices(this.slices).then(() => {\n this.queuedSlices.length = 0;\n });\n } else {\n // Only terminate idle sandboxes and return only queued slices\n let idleSandboxes = this.sandboxes.filter(w => !w.allocated);\n for (const sandbox of idleSandboxes) {\n this.returnSandbox(sandbox, null, false /* verifySandboxIsNotTerminated*/);\n }\n\n await this.returnSlices(this.queuedSlices).then(() => {\n this.queuedSlices.length = 0;\n });\n\n await new Promise((resolve, reject) => {\n let sandboxesRemaining = this.allocatedSandboxes.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 }\n\n if (this.resultSubmitterConnection) {\n this.resultSubmitterConnection.off('close', this.openResultSubmitterConn);\n this.resultSubmitterConnection.close();\n this.resultSubmitterConnection = null;\n }\n\n if (this.taskDistributorConnection) {\n this.taskDistributorConnection.off('close', this.openTaskDistributorConn);\n this.taskDistributorConnection.close();\n this.taskDistributorConnection = null;\n }\n\n if (this.packageManagerConnection) {\n this.packageManagerConnection.off('close', this.openPackageManagerConn);\n this.packageManagerConnection.close();\n this.packageManagerConnection = null;\n }\n\n if (this.eventRouterConnection) {\n this.eventRouterConnection.off('close', this.openEventRouterConn);\n this.eventRouterConnection.close();\n this.eventRouterConnection = null;\n }\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 an exception is thrown by sandbox.work(slice, startDelayMs) .\n * Or when the supervisor tells it to forcibly stop working.\n *\n * @param {Slice} slice - The slice to return to the scheduler.\n * @param {string} [reason] - Optional reason for the return: 'ENOPROGRESS', 'EUNCAUGHT', 'ESLICETOOSLOW', 'unknown'.\n * @returns {Promise<*>} - Response from the scheduler.\n */\n returnSlice (slice, reason) {\n // When sliceNumber === 0 don't send a status message.\n if (slice.sliceNumber === 0) return Promise.resolve();\n \n debugging() && console.log(`Supervisor.returnSlice: Returning slice ${slice.identifier} with reason ${reason}.`);\n \n const payload = slice.getReturnMessagePayload(this.workerOpaqueId, reason);\n return this.resultSubmitterConnection.send('status', payload)\n .then(response => {\n return response;\n }).catch(error => {\n console.error('Failed to return slice', {\n sliceNumber: slice.sliceNumber,\n jobAddress: slice.jobAddress,\n status: slice.status,\n error,\n });\n });\n }\n\n /**\n * Bulk-return multiple slices, possibly for assorted jobs.\n * Returns slices to the scheduler to be redistributed.\n * Called in the sandbox terminate handler and purgeAllWork(jobAddress)\n * and stopWork(forceTerminate).\n *\n * @param {Slice[]} slices - The slices to return to the scheduler.\n * @returns {Promise<void>} - Response from the scheduler.\n */\n async returnSlices(slices) {\n if (!slices || !slices.length) return Promise.resolve();\n \n const slicePayload = [];\n slices.forEach(slice => { addToReturnSlicePayload(slicePayload, slice); });\n this.removeSlices(slices);\n\n debugging('supervisor') && console.log(`Supervisor.returnSlices: Returning slices ${await this.dumpSlices(slices)}.`);\n\n return this.resultSubmitterConnection.send('status', {\n worker: this.workerOpaqueId,\n slices: slicePayload,\n }).then(response => {\n return response;\n }).catch(error => {\n const errorInfo = slices.map(slice => slice.identifier);\n console.error('Failed to return slice(s)', { errorInfo, error });\n // Just in case the caller is expecing a DCP response\n return { success: false, payload: {} };\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 // 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 slice ${slice.identifier}. This is ok when there are upstream errors.`);\n\n debugging('supervisor') && console.log(`supervisor: recording result for slice ${slice.identifier}.`);\n\n const jobAddress = slice.jobAddress;\n const sliceNumber = slice.sliceNumber;\n const authorizationMessage = slice.getAuthorizationMessage();\n\n /* @see result-submitter::result for full message details */\n const metrics = { GPUTime: 0, CPUTime: 0, CPUDensity: 0, GPUDensity: 0 };\n const payloadData = {\n slice: sliceNumber,\n job: jobAddress,\n worker: this.workerOpaqueId,\n paymentAddress: this.paymentAddress,\n metrics,\n authorizationMessage,\n }\n \n const timeReport = slice.timeReport;\n if (timeReport && timeReport.total > 0) {\n metrics.GPUTime = timeReport.webGL;\n metrics.CPUTime = timeReport.CPU;\n metrics.CPUDensity = metrics.CPUTime / timeReport.total;\n metrics.GPUDensity = metrics.GPUTime / timeReport.total;\n metrics.CPUTime = 1 + Math.floor(metrics.CPUTime);\n metrics.GPUTime = 1 + Math.floor(metrics.GPUTime);\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('supervisor') && 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 payload;\n }\n\n if (false) {}\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 /* slice did not complete for some reason */\n \n // If the slice from a job never completes and the job address exists in the ringBufferofJobs, \n // then we remove it to allow for another slice (from the same job) to be obtained by fetchTask\n this.ringBufferofJobs.buf = this.ringBufferofJobs.filter(element => element !== jobAddress);\n\n let statusPayloadData = slice.getReturnMessagePayload(this.workerOpaqueId);\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 } finally {\n this.emit('submitFinished');\n // Remove the slice from the slices array.\n this.removeSlice(slice);\n if (Supervisor.sliceTiming) {\n slice['resultDelta'] = Date.now() - slice['resultDelta'];\n console.log(`recordResult(${slice['pairingDelta']}, ${slice['executionDelta']}, ${slice['resultDelta']}): Completed slice ${slice.identifier}.`);\n } else\n debugging('supervisor') && console.log(`recordResult: Completed slice ${slice.identifier}.`);\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-rds .\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 postParams.content = JSON.stringify(result);\n } else {\n postParams.error = JSON.stringify(slice.error);\n }\n\n debugging('supervisor') && 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 * Sandbox has had an error which is not from the work function: kill it\n * and try to redo the slice.\n */\nfunction handleSandboxError(supervisor, sandbox, error) {\n const slice = sandbox.slice;\n\n slice.sandboxErrorCount = (slice.sandboxErrorCount || 0) + 1;\n sandbox.slice = null;\n supervisor.returnSandbox(sandbox); /* terminate the sandbox */\n slice.status = SLICE_STATUS_UNASSIGNED; /* ToT */\n console.warn(`Supervisor.handleSandboxError: Sandbox ${sandbox.identifier}...(${sandbox.public.name}/${slice.sandboxErrorCount}) with slice ${slice.identifier} had error.`, error);\n\n if (slice.sandboxErrorCount < dcpConfig.worker.maxSandboxErrorsPerSlice)\n supervisor.queuedSlices.push(slice);\n else {\n slice.error = error;\n supervisor.returnSlice(slice);\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 {Slice} slice - The slice.\n * @param {String} status - Status update, eg. progress or scheduled.\n *\n * @returns {Object[]} mutated slicePayload array\n */\nfunction addToSlicePayload(slicePayload, slice, status) {\n // getAuthorizationMessage helps enforces the equivalence\n // !authorizationMessage <==> sliceNumber === 0\n const authorizationMessage = slice.getAuthorizationMessage();\n if (!authorizationMessage) return;\n\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 === slice.jobAddress\n && desc.status === status\n && 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: slice.jobAddress,\n sliceNumbers: [],\n status,\n authorizationMessage,\n };\n slicePayload.push(sliceList);\n }\n\n sliceList.sliceNumbers.push(slice.sliceNumber);\n\n return slicePayload;\n}\n\n/**\n * Add a slice to the returnSlice payload being built. If a sliceList already exists for the\n * job-isEstimation-authMessage-reason 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 {Slice} slice - The slice.\n * @param {String} [reason] - Optional reason to further characterize status; e.g. 'ENOPROGRESS', 'EUNCAUGHT', 'ESLICETOOSLOW', 'unknown'.\n *\n * @returns {Object[]} mutated slicePayload array\n */\nfunction addToReturnSlicePayload(slicePayload, slice, reason) {\n // getAuthorizationMessage helps enforces the equivalence\n // !authorizationMessage <==> sliceNumber === 0\n const authorizationMessage = slice.getAuthorizationMessage();\n if (!authorizationMessage) return;\n\n if (!reason) reason = slice.error ? 'EUNCAUGHT' : 'unknown';\n\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 === slice.jobAddress\n && desc.isEstimationSlice === slice.isEstimationSlice\n && desc.authorizationMessage === authorizationMessage\n && desc.reason === reason;\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: slice.jobAddress,\n sliceNumbers: [],\n status: 'return',\n isEstimationSlice: slice.isEstimationSlice,\n authorizationMessage,\n reason,\n };\n slicePayload.push(sliceList);\n }\n\n sliceList.sliceNumbers.push(slice.sliceNumber);\n\n return slicePayload;\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/** @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/** @type {boolean} */\nSupervisor.sliceTiming = false;\n\nexports.Supervisor = Supervisor;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/worker/supervisor.js?");
4448
4448
 
4449
4449
  /***/ }),
4450
4450
 
@@ -4487,7 +4487,7 @@ eval("/**\n * @file protocol/connection/ack.js\n * @author KC Erb, k
4487
4487
  /***/ ((module) => {
4488
4488
 
4489
4489
  "use strict";
4490
- 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 if (this.messages && this.messages.length)\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://dcp/./src/protocol-v4/connection/batch.js?");
4490
+ 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.type = 'batch';\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\nObject.assign(module.exports, {\n ConnectionBatch\n});\n\n\n//# sourceURL=webpack://dcp/./src/protocol-v4/connection/batch.js?");
4491
4491
 
4492
4492
  /***/ }),
4493
4493
 
@@ -4507,7 +4507,7 @@ eval("/**\n * @file connection-constants.js\n * Constants fo
4507
4507
  \**********************************************************/
4508
4508
  /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
4509
4509
 
4510
- eval("/**\n * @file protocol/connection/message.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The ConnectionMessage is a factory for a message class that is bound\n * to a connection instance.\n */\n\nconst { Message } = __webpack_require__(/*! ../message */ \"./src/protocol-v4/message.js\");\n\nconst ConnectionMessage = (connection) => class extends Message {\n constructor() {\n super();\n\n this.connection = connection;\n }\n\n static get connection() {\n return connection;\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 if (!this.id) {\n throw new Error(\"Cannot dehydrate a message without an ID\");\n }\n\n return {\n ...super.toJSON(),\n id: this.id,\n nonce: this.nonce,\n dcpsid: this.connection.dcpsid,\n };\n }\n\n /**\n * Helper function to send this message.\n */\n send() {\n return this.connection.send(this);\n }\n\n async sign() {\n const identity = this.connection.identity || await this.connection.identityPromise;\n await identity.unlock(null, this.connection.unlockTimeout, true);\n return super.sign(identity);\n }\n}\n\nObject.assign(module.exports, {\n ConnectionMessage\n});\n\n\n//# sourceURL=webpack://dcp/./src/protocol-v4/connection/connection-message.js?");
4510
+ eval("/**\n * @file protocol/connection/message.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The ConnectionMessage is a factory for a message class that is bound\n * to a connection instance.\n */\n\nconst { Message } = __webpack_require__(/*! ../message */ \"./src/protocol-v4/message.js\");\n\nconst ConnectionMessage = (connection) => class extends Message {\n constructor() {\n super();\n\n this.connection = connection;\n this.batchable = true;\n }\n\n static get connection() {\n return connection;\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 if (!this.id) {\n throw new Error(\"Cannot dehydrate a message without an ID\");\n }\n\n return {\n ...super.toJSON(),\n id: this.id,\n nonce: this.nonce,\n dcpsid: this.connection.dcpsid,\n };\n }\n\n /**\n * Helper function to send this message.\n */\n send() {\n return this.connection.send(this);\n }\n\n async sign() {\n const identity = this.connection.identity || await this.connection.identityPromise;\n await identity.unlock(null, this.connection.unlockTimeout, true);\n return super.sign(identity);\n }\n}\n\nObject.assign(module.exports, {\n ConnectionMessage\n});\n\n\n//# sourceURL=webpack://dcp/./src/protocol-v4/connection/connection-message.js?");
4511
4511
 
4512
4512
  /***/ }),
4513
4513
 
@@ -4518,7 +4518,7 @@ eval("/**\n * @file protocol/connection/message.js\n * @author Ryan
4518
4518
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4519
4519
 
4520
4520
  "use strict";
4521
- eval("/**\n * @file protocol/connection/connection.js\n * @author Ryan Rossiter\n * @author KC Erb\n * @author Wes Garland\n * @date January 2020, Feb 2021, Mar 2022\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 disconnected close-wait closing closed\n * =====================================================================================================================================\n * |-- i:connect ---->\n * |-- t:newTarget -->\n * X--------------------------------------------------------------------------------> doClose()\n * |-- transportDisconnectHandler -------------------------->\n * |-- i:connect ---------->\n * |-- t:establishTarget -->\n * |-- transportDisconnectHandler -->\n * <-- reconnect -------------------|\n * X--------- doClose() ------->\n * X- doClose() ->\n * XXX------------|---------------------|--------------|-----------------------------------|------------> <------------| 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 * Error Codes relevant to DCP Connections:\n * DCPC-1001 - CONNECTION CANNOT SEND WHEN CLOSED\n * DCPC-1002 - MESSAGE CAME FROM INVALID SENDER\n * DCPC-1003 - MESSAGE SIGNATURE INVALID \n * DCPC-1004 - TRYING TO CONNECT AFTER ALREADY CONNECTED\n * DCPC-1005 - TRYING TO ESTABLISH TARGET AFTER TARGET ALREADY ESTABLISHED\n * DCPC-1006 - CONNECTION COULD NOT BE ESTABLISHED WITHIN 30 SECONDS\n * DCPC-1007 - RECEIVED MESSAGE PAYLOAD BEFORE CONNECT OPERATION\n * DCPC-1008 - TARGET RESPONDED WITH INVALID DCPSID\n * DCPC-1009 - MESSAGE IS OF UNKNOWN TYPE\n * DCPC-1010 - DUPLICATE TRANSMISSION RECEIPT\n * DCPC-1011 - DEFAULT ERROR CODE WHEN PEER SENDS CLOSE MESSAGE\n * DCPC-1012 - MESSAGE IS OF TYPE 'UNHANDLED MESSAGE'\n * DCPC-1013 - MESSAGE IS INVALID\n * DCPC-1014 - DEFAULT ERROR CODE WHEN CLOSING WITH REASON THATS NOT INSTANCE OF ERROR\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, a$sleepMs } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.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 { getGlobalIdentityCache } = __webpack_require__(/*! ./identity-cache */ \"./src/protocol-v4/connection/identity-cache.js\");\nconst { makeEBOIterator, setImmediateN } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\n\nconst { ConnectionMessage } = __webpack_require__(/*! ./connection-message */ \"./src/protocol-v4/connection/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 { role } = __webpack_require__(/*! ./connection-constants */ \"./src/protocol-v4/connection/connection-constants.js\");\n\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', /* initiator: establish first transport instance connection; target: listening */\n 'established',\n 'disconnected', /* connection is still valid, but underlying transport is no longer connected */\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.1.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 var _role;\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 if (target)\n _role = role.initiator;\n else\n _role = role.target;\n \n super(`Protocol Connection (${role})`);\n this.role = _role;\n \n if (target) {\n this.debugLabel = 'connection(i):';\n this._target = target;\n this.hasNtp = false;\n } else {\n this.debugLabel = 'connection(t):';\n this.hasNtp = true;\n }\n\n if (idKeystore) {\n this.identityPromise = Promise.resolve(idKeystore);\n } else {\n /* Always resolved by the time a session is established */\n debugging('connection') && console.debug('loading identity from wallet');\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 });\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.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\n this.receiver = new Receiver(this, this.messageLedger);\n\n debugging('connection') && console.debug(this.debugLabel, `new; ${target && target.location || '<target>'}`);\n\n /* Create a connection config as this.connectionOptions which takes into\n * account system defaults and overrides for specific urls, origins, etc.\n *\n * Having this as an exposed method instead of hidden in the constructor\n * is due to the lazy determination of the connection url.\n */\n this.configureConnectionForUrl = (url) => {\n this.url = url;\n this.connectionOptions = leafMerge(\n ({ /* hardcoded defaults insulate us from missing web config */\n 'connectTimeout': 90,\n 'allowBatch': true,\n 'maxMessagesPerBatch': 100,\n 'identityUnlockTimeout': 300,\n 'ttl': {\n 'min': 15,\n 'max': 600,\n 'default': 120\n },\n 'transports': [ 'socketio' ],\n }),\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.role === role.initiator ? this.url.href : 'target'],\n connectionOptions\n );\n \n this.unlockTimeout = this.connectionOptions.identityUnlockTimeout;\n this.connectionOptions.id = this._id;\n this.backoffTimeIterator = makeEBOIterator(500, dcpConfig.build === 'debug' ? 3000 : 20000); /** XXXwg make this configurable */\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 this.loggableDest = this.role === role.initiator ? this.url : '<target>';\n }\n\n /* By default, unsent messages cause .send() to reject for DCP intiators, but not targets. When\n * messages are unsent but not rejected, the send promise resolves with an instance of Error.\n *\n * Note: \"unsent messages\" are messages we tried to send, but couldn't be verified as sent because \n * the connection closed. It is plausible that they reached the other end, but also plausible that\n * they did not.\n *\n * @note XXX this is expedient, but not really correct. DCP is supposed to be completely peer-to-peer;\n * what we need to do is unify around one way or the other of handling unsent messages (probably\n * rejection), but -- importantly -- the daemons need to cross their Ts and dot their Is when it\n * comes to handling this stuff. My current thinking is that we could should use a specific DCP\n * error code for that, and make it non-fatal at the unhandledRejection when it's for a response,\n * but not a command.\n */\n this.rejectUnsentMessages = this.role === role.initiator;\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 * @note this API is wrong. It should be using DCP Config fragments instead of (url,ks) /wg Mar 2022\n */\n static async newTarget(url, ks, transport) {\n const pk = await ks.getPrivateKey();\n const ksUnlocked = await new wallet.Keystore(pk, '') /* needed for daemon operation */\n const target = new Connection(undefined, ksUnlocked); \n\n assert(target.role === role.target);\n target._target = { location: url };\n target.transport = transport;\n\n await target.doPreConnectTasks();\n\n target.state.set('initial', 'connecting'); /* connecting => listen */\n return target;\n }\n\n /**\n * Non-API function which is mostly a design wart. This needs to be invoked\n * - after we know the connection URL\n * - before we make a message\n * - in an async way because there is a DNS lookup\n */\n async doPreConnectTasks()\n {\n if (!this.state.is('connecting') || this.sender)\n return;\n\n if (this.role === role.initiator && this._target.hasOwnProperty('friendLocation') && await a$isFriendlyUrl(this._target.friendLocation))\n this.configureConnectionForUrl(this._target.friendLocation);\n else\n this.configureConnectionForUrl(this._target.location);\n \n this.sender = new Sender(this); // create sender before promises so that we can still enqueue messages before hopping off the event loop\n }\n \n /**\n * API to establish a DCP connection. Implied by send().\n *\n * When invoked by the initator, this method establishes the connection by connecting\n * to the target url provided to the constructor.\n */\n async connect()\n {\n if (this.state.is('initial'))\n {\n this.connectPromise = this.a$_connect();\n return this.connectPromise;\n }\n\n if (this.state.is('disconnected'))\n {\n this.connectPromise = this.a$_reconnect();\n return this.connectPromise;\n }\n \n if (this.state.is('connecting'))\n {\n assert(this.connectPromise);\n return this.connectPromise;\n }\n\n if (this.state.is('established'))\n return;\n \n if (this.state.in(['closed', 'close-wait', 'closing']))\n throw new Error('connection already closed', 'DCPC-1016');\n\n throw new Error('impossible');\n }\n\n /**\n * Performs a reconnection for connections which are in the disconnected state, and\n * tries to send any in-flight or enqueued messages as soon as that happens.\n */\n async a$_reconnect()\n {\n assert(this.state.is('disconnected'));\n\n this.state.set('disconnected', 'connecting');\n debugging() && console.log(`391: entering a$connectToTarget...`);\n const connected = await this.a$connectToTarget();\n\n // If we didn't connect / bailed early, that suggests we collided with\n // another reconnect handler, so this attempt can be abandoned\n if (connected === false) {\n debugging('connection') && console.log(this.debugLabel, `396: Aborted extra reconnection attempt`);\n return;\n }\n\n this.state.set('connecting', 'established');\n \n debugging('connection') && console.log(this.debugLabel, `402: Reconnected`);\n\n this.emit('connect'); // UI hint: \"internet available\" \n this.sender.notifyTransportReady();\n }\n\n async a$_connect() {\n var presharedPeerAddress;\n \n assert(this.role === role.initiator);\n\n this.state.set('initial', 'connecting');\n\n // This has to happen after updating the state, or we get races due to \n // \"test->async->act on test result\" races\n await this.doPreConnectTasks();\n\n await this.a$connectToTarget();\n const establishResults = await this.sender.establish().catch(error => {\n debugging('connection') && console.debug(this.debugLabel, `Could not establish DCP session over ${this.transport.name}:`, error);\n this.close(error, true);\n throw error;\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\n if ( false\n || typeof identity !== 'object'\n || typeof identity.address !== 'object'\n || !(identity.address instanceof wallet.Address))\n identity = { address: new wallet.Address(identity) }; /* map strings and Addresses to ks ducks */\n\n presharedPeerAddress = identity.address;\n debugging('connection') && console.debug(this.debugLabel, '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.notifyTransportReady();\n }\n\n /**\n * unreference any objects entrained by this connection so that it does not prevent\n * the node program from exiting naturally.\n */\n unref()\n {\n if (this.connectAbortTimer && this.connectAbortTimer.unref)\n this.connectAbortTimer.unref();\n }\n\n /**\n * Method is invoked when the transport disconnects. Transport instance is responsible for its own\n * finalization; Connection instance is responsible for finding a new transport, resuming the\n * connection, and retransmitting any in-flight message.\n */\n transportDisconnectHandler()\n {\n try\n { \n if (this.state.in(['disconnected', 'closing', 'close-wait', 'closed'])) /* transports may fire this more than once */\n return;\n \n this.state.set(['connecting', 'established'], 'disconnected');\n this.emit('disconnect'); /* UI hint: \"internet unavailable\" */\n debugging('connection') && console.debug(this.debugLabel, `Transport disconnected from ${this.url}; ${this.sender.inFlight ? 'have' : 'no'} in-flight message`);\n\n if (!this.dcpsid)\n {\n debugging('connection') && console.debug(this.debugLabel, 'Not reconnecting - no session');\n return;\n }\n \n if (this.role === role.target)\n {\n /* targets generally can't reconnect due to NAT */\n debugging('connection') && console.debug(this.debugLabel, `Waiting for initiator to reconnect for ${this.dcpsid}`);\n return;\n }\n\n if (!this.sender.inFlight && this.connectionOptions.onDemand)\n debugging('connection') && console.debug(this.debugLabel, `Not reconnecting ${this.dcpsid} until next message`);\n else\n this.connect();\n }\n catch(error)\n {\n debugging('connection') && console.debug(error);\n this.close(error, true);\n\n if (error.code !== 'DCPC-1016')\n {\n /* Unreached unless there are bugs. */\n throw error;\n }\n }\n }\n \n /**\n * Initiators only\n *\n * Connect to a target\n * - Rejects when we give up on all transports.\n * - Resolves when we have connected to target using a transport.\n *\n * The connection attempt will keep a node program \"alive\" while it is happening.\n * The `autoUnref` connectionOption and unref() methods offer ways to make this not\n * happen.\n */\n async a$connectToTarget()\n {\n const that = this;\n const availableTransports = [].concat(this.connectionOptions.transports);\n var quitMsg = false; /* not falsey => reject asap, value is error message */\n var quitCode = undefined;\n var boSleepIntr; /* if not falsey, a function that interrupts the backoff sleep */\n var transportConnectIntr; /* if not falsey, a function that interrupts the current connection attempt */\n\n // If there is already a connectAbortTimer, then we should signal the caller\n // that we were called in error\n if (this.connectAbortTimer)\n return false;\n\n /* This timer has the lifetime of the entire connection attempt. When we time out,\n * we set the quitMsg to get the retry loop to quit, then we interrupt the timer so\n * that we don't have to wait for the current backoff to expire before we notice, and\n * we expire the current attempt to connect right away as well.\n */\n this.connectAbortTimer = setTimeout(() => {\n quitMsg = 'connection timeout';\n if (boSleepIntr) boSleepIntr();\n if (transportConnectIntr) transportConnectIntr();\n }, this.connectionOptions.connectTimeout * 1000);\n\n if (this.connectionOptions.autoUnref)\n this.unref();\n\n /* cleanup code called on return/throw */\n function cleanup_ctt()\n {\n clearTimeout(that.connectAbortTimer);\n delete that.connectAbortTimer;\n }\n\n /* Connect to target with a specific transport. Resolves with { bool success, obj transport } */\n function a$connectWithTransport(transportName)\n { \n transportConnectIntr = false;\n\n return new Promise((connectWithTransport_resolve, connectWithTransport_reject) => { \n const TransportClass = Transport.require(transportName);\n const transport = new TransportClass(that.url, Object.assign({ connectionId: that.id }, that.connectionOptions[transportName]));\n var ret = { transport };\n\n function cleanup_cwt()\n {\n for (let eventName of transport.eventNames())\n for (let listener of transport.listeners(eventName))\n transport.off(eventName, listener);\n }\n \n /* In the case where we have a race condition in the transport implementation, arrange things\n * so that we resolve with whatever fired last if we have a double-fire on the same pass of \n * the event loop.\n */\n transport.on('connect', () => { cleanup_cwt(); ret.success=true; connectWithTransport_resolve(ret) });\n transport.on('error', (error) => { cleanup_cwt(); connectWithTransport_reject(error) });\n transport.on('connect-failed', (error) => {\n cleanup_cwt();\n ret.success = false;\n ret.error = error;\n debugging() && console.log(`Error connecting to ${that.url};`, error);\n connectWithTransport_resolve(ret);\n });\n \n /* let the connectAbortTimer interrupt this connect attempt */\n transportConnectIntr = () => { transport.close(true) };\n });\n }\n \n if (availableTransports.length === 0)\n {\n cleanup_ctt();\n return Promise.reject(new DCPError('no transports defined', 'DCPC-1015'));\n }\n \n /* Loop while trying each available transport in turn. Sleep with exponential backoff between runs */\n while (!quitMsg)\n {\n for (let transportName of availableTransports)\n {\n try\n {\n const { success, error, transport } = await a$connectWithTransport(transportName);\n \n if (success === true)\n { /* have successfully connected to target */\n transport.on('message', (m) => this.handleMessage(m));\n transport.on('end', () => this.transportDisconnectHandler());\n transport.on('close', () => this.transportDisconnectHandler());\n\n transportConnectIntr = false;\n cleanup_ctt();\n\n this.transport = transport;\n transport.peerVersion = this.peerVersion;\n // a connect event will be emitted in the caller, as well as a\n // call to this.sender.notifyTransportReady();\n \n return true; \n }\n\n if (error && error.httpStatus)\n {\n switch(error.httpStatus)\n {\n case 301: case 302: case 303: case 307: case 308:\n debugging('connection') && console.debug(this.debugLabel, `HTTP status ${error.httpStatus}; won't try again with ${transportName}`);\n availableTransports.splice(availableTransports.indexOf(transportName), 1);\n break;\n case 400: case 403: case 404:\n debugging('connection') && console.debug(this.debugLabel, `HTTP status ${error.httpStatus}; won't try again with ${transportName}`);\n quitMsg = error.message;\n quitCode = 'HTTP_' + error.httpStatus || 0;\n default:\n debugging('connection') && console.debug(this.debugLabel, `HTTP status ${error.httpStatus}; will try again with ${transportName}`);\n break;\n }\n }\n }\n catch (impossbleError)\n {\n /* transport connection attempts should never throw. */\n debugging('connection') && console.debug(this.debugLabel, `Error connecting to ${this.url} with ${transportName}; won't try again:`, impossibleError);\n availableTransports.splice(availableTransports.indexOf(transportName), 1);\n }\n }\n \n if (availableTransports.length === 0)\n {\n quitMsg = 'all transports exhausted';\n break;\n }\n \n /* Go to (interruptible) sleep for a while before trying again */\n const backoffTimeMs = this.backoffTimeIterator.next().value;\n debugging('connection') && console.debug(this.debugLabel, 'trying again in', Number(backoffTimeMs / 1000).toFixed(2), 'seconds');\n const boSleepPromise = a$sleepMs(backoffTimeMs);\n boSleepIntr = boSleepPromise.intr;\n await boSleepPromise;\n boSleepIntr = false;\n } \n\n /* The only way we get here is for us to discover that the connection is unconnectable - eg \n * reject timer has expired or similar.\n */\n cleanup_ctt();\n throw new DCPError(quitMsg, 'DCPC-1016', quitCode);\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 assert(this.role === role.target);\n \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.loggableDest = this.role === role.initiator ? this.url : peerAddress;\n this.state.set('connecting', 'established'); /* established => dcpsid has been set */\n debugging('connection') && console.debug(this.debugLabel, `Established session ${this.dcpsid} with ${this.peerAddress} for ${this.url}`);\n }\n\n ensureIdentity (peerAddress, presharedPeerAddress)\n {\n let idc = getGlobalIdentityCache();\n let noConflict = idc.learnIdentity(this.url, peerAddress, presharedPeerAddress);\n\n if (!noConflict)\n throw new DCPError(`**** Security Error: Identity address ${peerAddress} does not match the saved key for ${this.url}`, 'DCPC-EADDRCHANGE');\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 *\n * XXXwg this code needs an audit re error handling: what message error should we be emitting?\n * why do we keep working after we find an error?\n *\n * @param {string} JSON-encoded unvalidated message object\n */\n async handleMessage (messageJSON) {\n var validation;\n var message;\n var messageError;\n var messageValid = true;\n\n if (this.state.is('closed')) {\n debugging('connection') && console.warn(this.debugLabel, 'handleMessage was called on a closed connection.');\n return;\n }\n\n try\n {\n message = typeof messageJSON === 'object' ? messageJSON : JSON.parse(messageJSON);\n }\n catch(error)\n {\n console.error('connection::handleMessage received unparseable message from peer:', error);\n this.emit('error', error);\n return;\n }\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 nonce=${message.body.nonce}:`, message.body);\n this.transport.send(this.lastAckSigned);\n return;\n }\n\n debugging('connection') && console.debug(this.debugLabel, `received message ${message.body.type} ${message.body.id}; nonce=`, message.body.nonce);\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 DCPError('Message payload received before connection operation', 'DCPC-1007');\n }\n }\n else\n {\n if (message.owner !== this.authorizedSender)\n {\n messageError = new DCPError('Message came from invalid sender.', 'DCPC-1002');\n debugging('connection') && console.debug(this.debugLabel, 'Message owner was not an authorized sender - aborting connection');\n this.close(messageError, true);\n this.emit('error', messageError);\n return;\n }\n }\n\n if (this.role === role.target && this.state.in(['connecting']))\n {\n await this.doPreConnectTasks();\n\n // while connecting, the target gets its nonce from the initiator\n this.sender.nonce = message.body.nonce;\n }\n\n validation = this.validateSignature(message);\n if (validation.success !== true)\n {\n messageError = new DCPError(`invalid message signature: ${validation.errorMessage}`, 'DCPC-1003');\n debugging('connection') && console.debug(this.debugLabel, 'Message signature failed validation -', validation.errorMessage);\n this.close(messageError, true);\n messageValid = false;\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 messageError = new DCPError(`target could not process message (${message.payload && message.payload.name || 'unknown error'})`,'DCPC-1012');\n debugging('connection') && console.warn(this.debugLabel, \"Target Error - target could not process message.\", JSON.stringify(message.body),\n \"Aborting connection.\");\n this.close(messageError, true);\n messageValid = false;\n }\n\n validation = this.validateMessage(message);\n if (validation.success !== true)\n {\n messageError = new DCPError(`invalid message: ${validation.errorMessage}`, 'DCPC-1013');\n debugging('connection') && console.debug(this.debugLabel, 'Message failed validation -', validation.errorMessage);\n this.close(messageError, true);\n messageValid = false;;\n }\n\n if (!messageValid) {\n message.body.type = 'unhandled-message'\n this.emit('error', messageError);\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 \n this.receiver.handleMessage(message);\n }\n\n async ackMessage(message) {\n debugging('connection') && console.debug(this.debugLabel, `acking message. ${message.body.id}; token=`, message.body.ackToken);\n const ack = new this.Ack(message);\n const signedMessage = await ack.sign(this.identity);\n try {\n this.transport.send(signedMessage);\n }\n catch(error) {\n console.warn(this.debugLabel, `Failed to ack message ${message.id}: `, error.message);\n };\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 if (!message.signature) {\n debugging('connection') && console.warn(\"Message does not have signature, aborting connection\");\n return { success: false, errorMessage: \"message is missing signature\" };\n }\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' }; // eslint-disable-line no-unreachable\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 * 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.\n *\n * @note: This function is called when the remote end of the transport sends\n * a close command\n */\n closeWait (errorCode = null)\n {\n const preCloseState = this.state.valueOf();\n var reason;\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 // continue with close in either case\n reason = `Received close from peer with Error Code ${errorCode}`;\n if (this.role === role.target)\n reason += ` (${this.url})`;\n else\n reason += ` (${this.debugLabel}${this.peerAddress.address})`;\n\n reason = new DCPError(reason, errorCode || 'DCPC-1011');\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='requested', 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} immediate=${immediate} reason:`, reason);\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(reason) {\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 if (reason instanceof Error)\n closeMessage.payload.errorCode = reason.code;\n this.sender.enqueue(closeMessage)\n await new Promise(r => setImmediateN(r, 30));\n this.messageLedger.fulfillMessagePromise(closeMessage.id, {});\n }\n }\n\n /** sends close message to peer but doesn't require response \n */\n async closeImmediately(reason) {\n if(this.sender.inFlight)\n this.sender.clearFlightDeck(this.sender.inFlight.message, this.sender.nonce);\n \n let closeMessage = this.messageFactory.buildMessage('close');\n if (reason instanceof Error)\n closeMessage.payload.errorCode = reason.code;\n closeMessage.ackToken = this.sender.makeAckToken();\n closeMessage.nonce = this.sender.nonce;\n let signedCloseMessage = await closeMessage.sign();\n\n /* Overwrite the in-flight message because we don't care to deliver pending messages */\n this.sender.inFlight = { message: closeMessage, signedMessage: signedCloseMessage };\n debugging('connection') && console.debug(this.debugLabel, 'sending close message to peer');\n\n try\n {\n this.transport.send(signedCloseMessage);\n }\n catch(error)\n {\n debugging('connection') && console.debug(this.debugLabel, 'failed to send close message to ${this.loggableDest}:', error);\n }\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 // 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 //\n // This implies that no API functions which call doClose may await between\n // their invocation and their call to doClose!\n this.emit('close', dcpsid /* should be undefined in initial state */);\n\n assert(this.state.in(['closing', 'close-wait']));\n if (preCloseState === 'established' && !immediate) {\n try {\n if (immediate) {\n await this.closeImmediately(reason);\n } else {\n await this.closeGracefully(reason);\n }\n } catch(e) {\n debugging() && console.warn(`Warning: could not send close message to peer. 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.role === role.initiator)\n message = `Connection closed (url: ${this.url}, dcpsid: ${dcpsid})`;\n else\n message = `Connection closed (peer: ${this.peerAddress.address} dcpsid: ${dcpsid})`;\n }\n rejectErr = new DCPError(message, 'DCPC-1014');\n }\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(this.debugLabel, `Warning: could not shutdown sender; dcpsid=,${dcpsid}`, e); }\n \n try { this.transport.close(); }\n catch(e) { debugging() && console.warn(this.debugLabel, `Warning: could not close transport; dcpsid=,${dcpsid}`, e); }\n }\n } catch(error) {\n debugging() && console.warn(this.debugLabel, `could not close connection; dcpsid=${this.dcpsid}, url=${this.url ? this.url.href : 'unknown url'}:`, error);\n }\n finally\n {\n this.emit('end'); /* end event resolves promises on other threads for closeWait and close (ugh) */\n this.state.set(['closing', 'close-wait'], 'closed');\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() via this.sender.enqueue().\n * \n * @param {...any} args\n * @returns {Promise<Response>} a promise which resolves to a response.\n */\n async send(...args)\n {\n if (!this.state.is('connected'))\n await this.connect();\n\n if (this.state.in(['closing', 'close-wait', 'closed']))\n throw new DCPError(`Connection (${this._id}) is ${this.state}; cannot send. (${this.loggableDest})`, 'DCPC-1001');\n\n const message = this.messageFactory.buildMessage(...args);\n return this.sender.enqueue(message);\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.hasNtp) {\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/**\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 a$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 tha 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.debug(`scheduler config location ${schedulerConfigLocation} is secure`); /* from casual eavesdropping */\n schedulerConfigSecure = true;\n }\n\n if (isDebugBuild)\n {\n debugging('strict-mode') && console.debug('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.role === role.initiator && 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://dcp/./src/protocol-v4/connection/connection.js?");
4521
+ eval("/**\n * @file protocol/connection/connection.js\n * @author Ryan Rossiter\n * @author KC Erb\n * @author Wes Garland\n * @date January 2020, Feb 2021, Mar 2022\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 disconnected close-wait closing closed\n * =====================================================================================================================================\n * |-- i:connect ---->\n * |-- t:newTarget -->\n * X--------------------------------------------------------------------------------> doClose()\n * |-- transportDisconnectHandler -------------------------->\n * |-- i:connect ---------->\n * |-- t:establishTarget -->\n * |-- transportDisconnectHandler -->\n * <-- reconnect -------------------|\n * X--------- doClose() ------->\n * X- doClose() ->\n * XXX------------|---------------------|--------------|-----------------------------------|------------> <------------| 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 * Error Codes relevant to DCP Connections:\n * DCPC-1001 - CONNECTION CANNOT SEND WHEN CLOSED\n * DCPC-1002 - MESSAGE CAME FROM INVALID SENDER\n * DCPC-1003 - MESSAGE SIGNATURE INVALID \n * DCPC-1004 - TRYING TO CONNECT AFTER ALREADY CONNECTED\n * DCPC-1005 - TRYING TO ESTABLISH TARGET AFTER TARGET ALREADY ESTABLISHED\n * DCPC-1006 - CONNECTION COULD NOT BE ESTABLISHED WITHIN 30 SECONDS\n * DCPC-1007 - RECEIVED MESSAGE PAYLOAD BEFORE CONNECT OPERATION\n * DCPC-1008 - TARGET RESPONDED WITH INVALID DCPSID\n * DCPC-1009 - MESSAGE IS OF UNKNOWN TYPE\n * DCPC-1010 - DUPLICATE TRANSMISSION RECEIPT\n * DCPC-1011 - DEFAULT ERROR CODE WHEN PEER SENDS CLOSE MESSAGE\n * DCPC-1012 - MESSAGE IS OF TYPE 'UNHANDLED MESSAGE'\n * DCPC-1013 - MESSAGE IS INVALID\n * DCPC-1014 - DEFAULT ERROR CODE WHEN CLOSING WITH REASON THATS NOT INSTANCE OF ERROR\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, a$sleepMs } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.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 { getGlobalIdentityCache } = __webpack_require__(/*! ./identity-cache */ \"./src/protocol-v4/connection/identity-cache.js\");\nconst { makeEBOIterator, setImmediateN } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\n\nconst { ConnectionMessage } = __webpack_require__(/*! ./connection-message */ \"./src/protocol-v4/connection/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 { role } = __webpack_require__(/*! ./connection-constants */ \"./src/protocol-v4/connection/connection-constants.js\");\n\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', /* initiator: establish first transport instance connection; target: listening */\n 'established',\n 'disconnected', /* connection is still valid, but underlying transport is no longer connected */\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.1.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 var _role;\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 if (target)\n _role = role.initiator;\n else\n _role = role.target;\n \n super(`Protocol Connection (${role})`);\n this.role = _role;\n \n if (target) {\n this.debugLabel = 'connection(i):';\n this._target = target;\n this.hasNtp = false;\n } else {\n this.debugLabel = 'connection(t):';\n this.hasNtp = true;\n }\n\n if (idKeystore) {\n this.identityPromise = Promise.resolve(idKeystore);\n } else {\n /* Always resolved by the time a session is established */\n debugging('connection') && console.debug('loading identity from wallet');\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 });\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.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\n this.receiver = new Receiver(this, this.messageLedger);\n\n debugging('connection') && console.debug(this.debugLabel, `new; ${target && target.location || '<target>'}`);\n\n /* Create a connection config as this.connectionOptions which takes into\n * account system defaults and overrides for specific urls, origins, etc.\n *\n * Having this as an exposed method instead of hidden in the constructor\n * is due to the lazy determination of the connection url.\n */\n this.configureConnectionForUrl = (url) => {\n this.url = url;\n this.connectionOptions = leafMerge(\n ({ /* hardcoded defaults insulate us from missing web config */\n 'connectTimeout': 90,\n 'allowBatch': true,\n 'maxMessagesPerBatch': 100,\n 'identityUnlockTimeout': 300,\n 'ttl': {\n 'min': 15,\n 'max': 600,\n 'default': 120\n },\n 'transports': [ 'socketio' ],\n }),\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.role === role.initiator ? this.url.href : 'target'],\n connectionOptions\n );\n \n this.unlockTimeout = this.connectionOptions.identityUnlockTimeout;\n this.connectionOptions.id = this._id;\n this.backoffTimeIterator = makeEBOIterator(500, dcpConfig.build === 'debug' ? 3000 : 20000); /** XXXwg make this configurable */\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 this.loggableDest = this.role === role.initiator ? this.url : '<target>';\n }\n\n /* By default, unsent messages cause .send() to reject for DCP intiators, but not targets. When\n * messages are unsent but not rejected, the send promise resolves with an instance of Error.\n *\n * Note: \"unsent messages\" are messages we tried to send, but couldn't be verified as sent because \n * the connection closed. It is plausible that they reached the other end, but also plausible that\n * they did not.\n *\n * @note XXX this is expedient, but not really correct. DCP is supposed to be completely peer-to-peer;\n * what we need to do is unify around one way or the other of handling unsent messages (probably\n * rejection), but -- importantly -- the daemons need to cross their Ts and dot their Is when it\n * comes to handling this stuff. My current thinking is that we could should use a specific DCP\n * error code for that, and make it non-fatal at the unhandledRejection when it's for a response,\n * but not a command.\n */\n this.rejectUnsentMessages = this.role === role.initiator;\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 * @note this API is wrong. It should be using DCP Config fragments instead of (url,ks) /wg Mar 2022\n */\n static async newTarget(url, ks, transport) {\n const pk = await ks.getPrivateKey();\n const ksUnlocked = await new wallet.Keystore(pk, '') /* needed for daemon operation */\n const target = new Connection(undefined, ksUnlocked); \n\n assert(target.role === role.target);\n target._target = { location: url };\n target.transport = transport;\n\n await target.doPreConnectTasks();\n\n target.state.set('initial', 'connecting'); /* connecting => listen */\n return target;\n }\n\n /**\n * Non-API function which is mostly a design wart. This needs to be invoked\n * - after we know the connection URL\n * - before we make a message\n * - in an async way because there is a DNS lookup\n */\n async doPreConnectTasks()\n {\n if (!this.state.is('connecting') || this.sender)\n return;\n\n if (this.role === role.initiator && this._target.hasOwnProperty('friendLocation') && await a$isFriendlyUrl(this._target.friendLocation))\n this.configureConnectionForUrl(this._target.friendLocation);\n else\n this.configureConnectionForUrl(this._target.location);\n \n this.sender = new Sender(this); // create sender before promises so that we can still enqueue messages before hopping off the event loop\n }\n \n /**\n * API to establish a DCP connection. Implied by send().\n *\n * When invoked by the initator, this method establishes the connection by connecting\n * to the target url provided to the constructor.\n */\n async connect()\n {\n if (this.state.is('initial'))\n {\n this.connectPromise = this.a$_connect();\n return this.connectPromise;\n }\n\n if (this.state.is('disconnected'))\n {\n this.connectPromise = this.a$_reconnect();\n return this.connectPromise;\n }\n \n if (this.state.is('connecting'))\n {\n assert(this.connectPromise);\n return this.connectPromise;\n }\n\n if (this.state.is('established'))\n return;\n \n if (this.state.in(['closed', 'close-wait', 'closing']))\n throw new DCPError('Connection already closed', 'DCPC-1016');\n\n throw new Error('impossible');\n }\n\n /**\n * Performs a reconnection for connections which are in the disconnected state, and\n * tries to send any in-flight or enqueued messages as soon as that happens.\n */\n async a$_reconnect()\n {\n assert(this.state.is('disconnected'));\n\n this.state.set('disconnected', 'connecting');\n debugging() && console.log(`391: entering a$connectToTarget...`);\n const connected = await this.a$connectToTarget();\n\n // If we didn't connect / bailed early, that suggests we collided with\n // another reconnect handler, so this attempt can be abandoned\n if (connected === false) {\n debugging('connection') && console.log(this.debugLabel, `396: Aborted extra reconnection attempt`);\n return;\n }\n\n this.state.set('connecting', 'established');\n \n debugging('connection') && console.log(this.debugLabel, `402: Reconnected`);\n\n this.emit('connect'); // UI hint: \"internet available\" \n this.sender.notifyTransportReady();\n\n await this.send('reconnected');\n }\n\n async a$_connect() {\n var presharedPeerAddress;\n \n assert(this.role === role.initiator);\n\n this.state.set('initial', 'connecting');\n\n // This has to happen after updating the state, or we get races due to \n // \"test->async->act on test result\" races\n await this.doPreConnectTasks();\n\n await this.a$connectToTarget();\n const establishResults = await this.sender.establish().catch(error => {\n debugging('connection') && console.debug(this.debugLabel, `Could not establish DCP session over ${this.transport.name}:`, error);\n this.close(error, true);\n throw error;\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\n if ( false\n || typeof identity !== 'object'\n || typeof identity.address !== 'object'\n || !(identity.address instanceof wallet.Address))\n identity = { address: new wallet.Address(identity) }; /* map strings and Addresses to ks ducks */\n\n presharedPeerAddress = identity.address;\n debugging('connection') && console.debug(this.debugLabel, '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.notifyTransportReady();\n }\n\n /**\n * unreference any objects entrained by this connection so that it does not prevent\n * the node program from exiting naturally.\n */\n unref()\n {\n if (this.connectAbortTimer && this.connectAbortTimer.unref)\n this.connectAbortTimer.unref();\n }\n\n /**\n * Method is invoked when the transport disconnects. Transport instance is responsible for its own\n * finalization; Connection instance is responsible for finding a new transport, resuming the\n * connection, and retransmitting any in-flight message.\n */\n transportDisconnectHandler()\n {\n try\n { \n if (this.state.in(['disconnected', 'closing', 'close-wait', 'closed'])) /* transports may fire this more than once */\n return;\n \n this.state.set(['connecting', 'established'], 'disconnected');\n this.emit('disconnect'); /* UI hint: \"internet unavailable\" */\n debugging('connection') && console.debug(this.debugLabel, `Transport disconnected from ${this.url}; ${this.sender.inFlight ? 'have' : 'no'} in-flight message`);\n\n if (!this.dcpsid)\n {\n debugging('connection') && console.debug(this.debugLabel, 'Not reconnecting - no session');\n return;\n }\n \n if (this.role === role.target)\n {\n /* targets generally can't reconnect due to NAT */\n debugging('connection') && console.debug(this.debugLabel, `Waiting for initiator to reconnect for ${this.dcpsid}`);\n return;\n }\n\n if (!this.sender.inFlight && this.connectionOptions.onDemand)\n debugging('connection') && console.debug(this.debugLabel, `Not reconnecting ${this.dcpsid} until next message`);\n else\n this.connect();\n }\n catch(error)\n {\n debugging('connection') && console.debug(error);\n this.close(error, true);\n\n if (error.code !== 'DCPC-1016')\n {\n /* Unreached unless there are bugs. */\n throw error;\n }\n }\n }\n \n /**\n * Initiators only\n *\n * Connect to a target\n * - Rejects when we give up on all transports.\n * - Resolves when we have connected to target using a transport.\n *\n * The connection attempt will keep a node program \"alive\" while it is happening.\n * The `autoUnref` connectionOption and unref() methods offer ways to make this not\n * happen.\n */\n async a$connectToTarget()\n {\n const that = this;\n const availableTransports = [].concat(this.connectionOptions.transports);\n var quitMsg = false; /* not falsey => reject asap, value is error message */\n var quitCode = undefined;\n var boSleepIntr; /* if not falsey, a function that interrupts the backoff sleep */\n var transportConnectIntr; /* if not falsey, a function that interrupts the current connection attempt */\n\n // If there is already a connectAbortTimer, then we should signal the caller\n // that we were called in error\n if (this.connectAbortTimer)\n return false;\n\n /* This timer has the lifetime of the entire connection attempt. When we time out,\n * we set the quitMsg to get the retry loop to quit, then we interrupt the timer so\n * that we don't have to wait for the current backoff to expire before we notice, and\n * we expire the current attempt to connect right away as well.\n */\n this.connectAbortTimer = setTimeout(() => {\n quitMsg = 'connection timeout';\n if (boSleepIntr) boSleepIntr();\n if (transportConnectIntr) transportConnectIntr();\n }, this.connectionOptions.connectTimeout * 1000);\n\n if (this.connectionOptions.autoUnref)\n this.unref();\n\n /* cleanup code called on return/throw */\n function cleanup_ctt()\n {\n clearTimeout(that.connectAbortTimer);\n delete that.connectAbortTimer;\n }\n\n /* Connect to target with a specific transport. Resolves with { bool success, obj transport } */\n function a$connectWithTransport(transportName)\n { \n transportConnectIntr = false;\n\n return new Promise((connectWithTransport_resolve, connectWithTransport_reject) => { \n const TransportClass = Transport.require(transportName);\n const transport = new TransportClass(that.url, Object.assign({ connectionId: that.id }, that.connectionOptions[transportName]));\n var ret = { transport };\n\n function cleanup_cwt()\n {\n for (let eventName of transport.eventNames())\n for (let listener of transport.listeners(eventName))\n transport.off(eventName, listener);\n }\n \n /* In the case where we have a race condition in the transport implementation, arrange things\n * so that we resolve with whatever fired last if we have a double-fire on the same pass of \n * the event loop.\n */\n transport.on('connect', () => { cleanup_cwt(); ret.success=true; connectWithTransport_resolve(ret) });\n transport.on('error', (error) => { cleanup_cwt(); connectWithTransport_reject(error) });\n transport.on('connect-failed', (error) => {\n cleanup_cwt();\n ret.success = false;\n ret.error = error;\n debugging() && console.log(`Error connecting to ${that.url};`, error);\n connectWithTransport_resolve(ret);\n });\n \n /* let the connectAbortTimer interrupt this connect attempt */\n transportConnectIntr = () => { transport.close(true) };\n });\n }\n \n if (availableTransports.length === 0)\n {\n cleanup_ctt();\n return Promise.reject(new DCPError('no transports defined', 'DCPC-1015'));\n }\n \n /* Loop while trying each available transport in turn. Sleep with exponential backoff between runs */\n while (!quitMsg)\n {\n for (let transportName of availableTransports)\n {\n try\n {\n const { success, error, transport } = await a$connectWithTransport(transportName);\n \n if (success === true)\n { /* have successfully connected to target */\n transport.on('message', (m) => this.handleMessage(m));\n transport.on('end', () => this.transportDisconnectHandler());\n transport.on('close', () => this.transportDisconnectHandler());\n\n transportConnectIntr = false;\n cleanup_ctt();\n\n this.transport = transport;\n transport.peerVersion = this.peerVersion;\n // a connect event will be emitted in the caller, as well as a\n // call to this.sender.notifyTransportReady();\n \n return true; \n }\n\n if (error && error.httpStatus)\n {\n switch(error.httpStatus)\n {\n case 301: case 302: case 303: case 307: case 308:\n debugging('connection') && console.debug(this.debugLabel, `HTTP status ${error.httpStatus}; won't try again with ${transportName}`);\n availableTransports.splice(availableTransports.indexOf(transportName), 1);\n break;\n case 400: case 403: case 404:\n debugging('connection') && console.debug(this.debugLabel, `HTTP status ${error.httpStatus}; won't try again with ${transportName}`);\n quitMsg = error.message;\n quitCode = 'HTTP_' + error.httpStatus || 0;\n default:\n debugging('connection') && console.debug(this.debugLabel, `HTTP status ${error.httpStatus}; will try again with ${transportName}`);\n break;\n }\n }\n }\n catch (impossbleError)\n {\n /* transport connection attempts should never throw. */\n debugging('connection') && console.debug(this.debugLabel, `Error connecting to ${this.url} with ${transportName}; won't try again:`, impossibleError);\n availableTransports.splice(availableTransports.indexOf(transportName), 1);\n }\n }\n \n if (availableTransports.length === 0)\n {\n quitMsg = 'all transports exhausted';\n break;\n }\n \n /* Go to (interruptible) sleep for a while before trying again */\n const backoffTimeMs = this.backoffTimeIterator.next().value;\n debugging('connection') && console.debug(this.debugLabel, 'trying again in', Number(backoffTimeMs / 1000).toFixed(2), 'seconds');\n const boSleepPromise = a$sleepMs(backoffTimeMs);\n boSleepIntr = boSleepPromise.intr;\n await boSleepPromise;\n boSleepIntr = false;\n } \n\n /* The only way we get here is for us to discover that the connection is unconnectable - eg \n * reject timer has expired or similar.\n */\n cleanup_ctt();\n throw new DCPError(quitMsg, 'DCPC-1016', quitCode);\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 assert(this.role === role.target);\n \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.loggableDest = this.role === role.initiator ? this.url : peerAddress;\n this.state.set('connecting', 'established'); /* established => dcpsid has been set */\n debugging('connection') && console.debug(this.debugLabel, `Established session ${this.dcpsid} with ${this.peerAddress} for ${this.url}`);\n }\n\n ensureIdentity (peerAddress, presharedPeerAddress)\n {\n let idc = getGlobalIdentityCache();\n let noConflict = idc.learnIdentity(this.url, peerAddress, presharedPeerAddress);\n\n if (!noConflict)\n throw new DCPError(`**** Security Error: Identity address ${peerAddress} does not match the saved key for ${this.url}`, 'DCPC-EADDRCHANGE');\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 *\n * XXXwg this code needs an audit re error handling: what message error should we be emitting?\n * why do we keep working after we find an error?\n *\n * @param {string} JSON-encoded unvalidated message object\n */\n async handleMessage (messageJSON) {\n var validation;\n var message;\n var messageError;\n var messageValid = true;\n\n if (this.state.is('closed')) {\n debugging('connection') && console.warn(this.debugLabel, 'handleMessage was called on a closed connection.');\n return;\n }\n\n try\n {\n message = typeof messageJSON === 'object' ? messageJSON : JSON.parse(messageJSON);\n }\n catch(error)\n {\n console.error('connection::handleMessage received unparseable message from peer:', error);\n this.emit('error', error);\n return;\n }\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 nonce=${message.body.nonce}:`, message.body);\n this.sendAck(this.lastAckSigned);\n return;\n }\n\n debugging('connection') && console.debug(this.debugLabel, `received message ${message.body.type} ${message.body.id}; nonce=`, message.body.nonce);\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 DCPError('Message payload received before connection operation', 'DCPC-1007');\n }\n }\n else\n {\n if (message.owner !== this.authorizedSender)\n {\n messageError = new DCPError('Message came from invalid sender.', 'DCPC-1002');\n debugging('connection') && console.debug(this.debugLabel, 'Message owner was not an authorized sender - aborting connection');\n this.close(messageError, true);\n this.emit('error', messageError);\n return;\n }\n }\n\n if (this.role === role.target && this.state.in(['connecting']))\n {\n await this.doPreConnectTasks();\n\n // while connecting, the target gets its nonce from the initiator\n this.sender.nonce = message.body.nonce;\n }\n\n validation = this.validateSignature(message);\n if (validation.success !== true)\n {\n messageError = new DCPError(`invalid message signature: ${validation.errorMessage}`, 'DCPC-1003');\n debugging('connection') && console.debug(this.debugLabel, 'Message signature failed validation -', validation.errorMessage);\n this.close(messageError, true);\n messageValid = false;\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 messageError = new DCPError(`target could not process message (${message.payload && message.payload.name || 'unknown error'})`,'DCPC-1012');\n debugging('connection') && console.warn(this.debugLabel, \"Target Error - target could not process message.\", JSON.stringify(message.body),\n \"Aborting connection.\");\n this.close(messageError, true);\n messageValid = false;\n }\n\n validation = this.validateMessage(message);\n if (validation.success !== true)\n {\n messageError = new DCPError(`invalid message: ${validation.errorMessage}`, 'DCPC-1013');\n debugging('connection') && console.debug(this.debugLabel, 'Message failed validation -', validation.errorMessage);\n this.close(messageError, true);\n messageValid = false;;\n }\n\n if (!messageValid) {\n message.body.type = 'unhandled-message'\n this.emit('error', messageError);\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 \n this.receiver.handleMessage(message);\n }\n\n async ackMessage(message) {\n debugging('connection') && console.debug(this.debugLabel, 'acking message of type: ', message.body.type);\n const ack = new this.Ack(message);\n const signedMessage = await ack.sign(this.identity);\n this.sendAck(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 if (!message.signature) {\n debugging('connection') && console.warn(\"Message does not have signature, aborting connection\");\n return { success: false, errorMessage: \"message is missing signature\" };\n }\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' }; // eslint-disable-line no-unreachable\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 * 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.\n *\n * @note: This function is called when the remote end of the transport sends\n * a close command\n */\n closeWait (errorCode = null)\n {\n const preCloseState = this.state.valueOf();\n var reason;\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 // continue with close in either case\n reason = `Received close from peer with Error Code ${errorCode}`;\n if (this.role === role.target)\n reason += ` (${this.url})`;\n else\n reason += ` (${this.debugLabel}${this.peerAddress.address})`;\n\n reason = new DCPError(reason, errorCode || 'DCPC-1011');\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='requested', 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} immediate=${immediate} reason:`, reason);\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(reason) {\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 if (reason instanceof Error)\n closeMessage.payload.errorCode = reason.code;\n closeMessage.ackToken = this.sender.makeAckToken();\n this.sender.enqueue(closeMessage)\n await new Promise(r => setImmediateN(r, 30));\n this.messageLedger.fulfillMessagePromise(closeMessage.id, {});\n }\n }\n\n /** sends close message to peer but doesn't require response \n */\n async closeImmediately(reason) {\n if(this.sender.inFlight)\n this.sender.clearFlightDeck(this.sender.inFlight.message, this.sender.nonce);\n \n let closeMessage = this.messageFactory.buildMessage('close');\n if (reason instanceof Error)\n closeMessage.payload.errorCode = reason.code;\n closeMessage.ackToken = this.sender.makeAckToken();\n closeMessage.nonce = this.sender.nonce;\n let signedCloseMessage = await closeMessage.sign();\n\n /* Overwrite the in-flight message because we don't care to deliver pending messages */\n this.sender.inFlight = { message: closeMessage, signedMessage: signedCloseMessage };\n debugging('connection') && console.debug(this.debugLabel, 'sending close message to peer');\n\n try\n {\n this.transport.send(signedCloseMessage);\n }\n catch(error)\n {\n debugging('connection') && console.debug(this.debugLabel, 'failed to send close message to ${this.loggableDest}:', error);\n }\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 // 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 //\n // This implies that no API functions which call doClose may await between\n // their invocation and their call to doClose!\n this.emit('close', dcpsid /* should be undefined in initial state */);\n\n assert(this.state.in(['closing', 'close-wait']));\n if (preCloseState === 'established' && !immediate) {\n try {\n if (immediate) {\n await this.closeImmediately(reason);\n } else {\n await this.closeGracefully(reason);\n }\n } catch(e) {\n debugging() && console.warn(`Warning: could not send close message to peer. 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.role === role.initiator)\n message = `Connection closed (url: ${this.url}, dcpsid: ${dcpsid})`;\n else\n message = `Connection closed (peer: ${this.peerAddress.address} dcpsid: ${dcpsid})`;\n }\n rejectErr = new DCPError(message, 'DCPC-1014');\n }\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(this.debugLabel, `Warning: could not shutdown sender; dcpsid=,${dcpsid}`, e); }\n \n try { this.transport.close(); }\n catch(e) { debugging() && console.warn(this.debugLabel, `Warning: could not close transport; dcpsid=,${dcpsid}`, e); }\n }\n } catch(error) {\n debugging() && console.warn(this.debugLabel, `could not close connection; dcpsid=${this.dcpsid}, url=${this.url ? this.url.href : 'unknown url'}:`, error);\n }\n finally\n {\n this.emit('end'); /* end event resolves promises on other threads for closeWait and close (ugh) */\n this.state.set(['closing', 'close-wait'], 'closed');\n }\n }\n /**\n * Prepares a non-batchable message that can be sent directly over the wire. Returns when\n * the message has been signed and is ready to be sent. If entered through Connection.send\n * which has 'canBatch = true', will return the message instead. In this case, enqueue \n * handled by `async send()`. The connection will not be able to send any messages until the\n * prepared message here is either sent or discarded.\n * @param {ConnectionMessage|String|Array} messageData Data to build message with. If an array, \n * use format [operation, data (optional), identity (optional)]\n * @param {Boolean} canBatch Set to true when a message prepared through Connection.\n * Returns the message instead of sending it to the queue.\n * @returns {Promise<Object>} a promise which resolves to { message, signedMessage }\n */\n \n async prepareMessage(messageData, canBatch = false)\n {\n let message;\n \n if (Array.isArray(messageData))\n message = messageData[0];\n else\n message = messageData;\n \n if (!message.id)\n {\n message = this.messageFactory.buildMessage(...messageData); \n }\n \n message.ackToken = this.sender.makeAckToken();\n message.batchable = canBatch;\n \n if (canBatch)\n return message;\n \n const preparedMessage = await new Promise((resolve) =>\n {\n this.sender.queue.push(message)\n this.sender.requestQueueService()\n // This event is fired in the sender by serviceQueue() when the message is at the top of the queue\n // and has a nonce it can sign with. At this point, we may return the prepared message.\n this.once(`${message.id} ready`, (message) => resolve(message))\n })\n \n return preparedMessage;\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(). If the first argument has a 'signedMessage'\n * property, the message is assumed to be prepared and is sent immediately. If not, and the first\n * argument does not have a 'type' property, it will be sent to `async prepare()`, and then put\n * in the message queue.\n * \n * @param {...any} args 3 forms:\n * [operation]\n * [operation, data]\n * [operation, data, identity]\n * @returns {Promise<Response>} a promise which resolves to a response.\n */\n async send(...args)\n {\n if (!this.state.is('connected'))\n await this.connect();\n\n let message = args[0];\n // ie. already prepared\n if (message.signedMessage)\n return this.sendPreparedMessage(message);\n \n // ie. message not hyrdated or is a response, which needs ack token\n if (!message.id || message.type === 'response')\n message = await this.prepareMessage(args, true);\n\n if (this.state.in(['closing', 'close-wait', 'closed']))\n throw new DCPError(`Connection (${this._id}) is ${this.state}; cannot send. (${this.loggableDest})`, 'DCPC-1001');\n\n return this.sender.enqueue(message);\n }\n \n /**\n * Set the sender's flight deck with the given message and send it.\n * Can only be passed a prepared message, which is a promise that only\n * resolves to a message when it is signed with the nonce, so it must\n * be the next message to be sent (or discarded).\n * @param {Object} messageObject\n * @returns {Promise<Response>} \n */\n sendPreparedMessage(messageObject)\n {\n if (!messageObject.signedMessage) return;\n \n const { message, signedMessage } = messageObject;\n assert(!this.sender.inFlight);\n this.sender.inFlight = { message: message, signedMessage: signedMessage };\n setImmediate(() => this.sender.sendInFlightMessage());\n \n return this.sender.messageLedger.addMessage(message);\n }\n \n /**\n * Send a signed ack directly over the wire. If we get a SocketIO.Send: Not Connected error, \n * wait until we're connected and then resend the ack.\n * @param {String} ack \n */\n sendAck(ack)\n {\n try\n {\n this.transport.send(ack)\n }\n catch(error)\n {\n // Transport was lost\n if (error.code === 'DCPC-1105')\n this.once('connect', () => this.sendAck(ack));\n else\n console.error(`${this.debugLabel} Error acking message to ${this.loggableDest}: ${error}`);\n }\n }\n \n /**\n * Discard a prepared message by removing it from the queue.\n * Returns nonce to sender and provokes queue service.\n * @param {Object} messageObject { message, signedMessage } message to discard \n */\n discardMessage(messageObject)\n {\n let { message } = messageObject;\n this.sender.clearFlightDeck(message, message.nonce);\n message.type = 'unhandled-message';\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.hasNtp) {\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/**\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 a$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 tha 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.debug(`scheduler config location ${schedulerConfigLocation} is secure`); /* from casual eavesdropping */\n schedulerConfigSecure = true;\n }\n\n if (isDebugBuild)\n {\n debugging('strict-mode') && console.debug('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.role === role.initiator && 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://dcp/./src/protocol-v4/connection/connection.js?");
4522
4522
 
4523
4523
  /***/ }),
4524
4524
 
@@ -4583,7 +4583,7 @@ eval("/**\n * @file protocol/connection/message-ledger.js\n * @author
4583
4583
  \************************************************/
4584
4584
  /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
4585
4585
 
4586
- 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\");\nconst { role } = __webpack_require__(/*! ./connection-constants */ \"./src/protocol-v4/connection/connection-constants.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').nanoid;\n perf = requireNative('perf_hooks').performance;\n} else {\n nanoid = (__webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\").nanoid);\n perf = performance;\n}\n\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 {\n constructor(connection, messageLedger) {\n\n this.connection = connection;\n this.messageLedger = messageLedger;\n const id = this.connection._id;\n this.debugLabel = this.connection.role === role.initiator ? `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.connectionOptions.ttl.default || this.connection.connectionOptions.ttl.min;\n } else {\n if (validity > this.connection.connectionOptions.ttl.max) ttl = this.connection.connectionOptions.ttl.max;\n else if (validity.ttl < this.connection.connectionOptions.ttl.min) ttl = this.connection.connectionOptions.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 if (validityStampCache.insert(validity.stamp, expirationTime) !== true)\n {\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('messages') && 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 debugging('receiver') && console.warn('Ignoring unhandled message', JSON.stringify(messageBody));\n } else {\n throw new DCPError(`Message in batch has unknown type: ${messageBody.type}`, 'DCPC-1009');\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 handleMessage(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('receiver') && 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 /** XXXwg regardless the cause, we should not be ignoring requests. Spec says we need to send back *something* */\n ( true) && console.debug(this.debugLabel, 'Request has invalid TTL; ignoring request. Validity=', req.payload.validity);\n return;\n } else {\n let reqId = req.id;\n\n this.connection.openRequests[reqId] = msgBody;\n setTimeout(() => {\n /* FIXME XXXwg this is completely insane */\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('receiver') && 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 debugging('receiver') && console.debug(this.debugLabel, `Initiator version ${initiatorVersion} does not meet target targetCompatibility '${targetCompatibility}'`);\n const versionErr = new DCPError(`Initiator version ${initiatorVersion} does not meet target targetCompatibility '${targetCompatibility}'`, 'DCPC-EVERSION');\n const connectResponse = new this.connection.Response(req, versionErr);\n await this.connection.sender.specialFirstSend(connectResponse);\n throw versionErr;\n }\n\n // Memoize the peer version onto the Connection\n this.connection.peerVersion = initiatorVersion;\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}`, 'DCPC-EDCPSID');\n }\n\n // Play silly buggers with the protocol version, so 5.0.0 clients will still\n // respect us, but more recent clients will behave better\n const version = (this.connection.peerVersion === '5.0.0')\n ? '5.0.0' // how do you do, fellow 5.0.0\n : this.connection.constructor.VERSION;\n\n // Append our half of the dcpsid\n const dcpsid = topHalfSid + bottomHalfSid;\n const connectResponse = new this.connection.Response(req, {\n 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('receiver') && console.debug(this.debugLabel, 'first request handled. Connection state:', this.connection.state.valueOf());\n }\n\n handleOperation(req) {\n switch (req.payload.operation) {\n case 'connect':\n throw new DCPError('Request operation \"connect\" received after establishment', 'DCPC-ETOOMANYCONNECT');\n case 'close':\n this.connection.closeWait(req.payload.errorCode).catch(error => console.error('Unexpected error closing initiator:', error.message));\n break;\n \n case 'keepalive':\n req.respond().catch(error => {\n if (error.code === 'DCPC-1011')\n return;\n throw error;\n });\n break;\n\n default:\n this.connection.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('receiver') && 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://dcp/./src/protocol-v4/connection/receiver.js?");
4586
+ 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\");\nconst { role } = __webpack_require__(/*! ./connection-constants */ \"./src/protocol-v4/connection/connection-constants.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').nanoid;\n perf = requireNative('perf_hooks').performance;\n} else {\n nanoid = (__webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\").nanoid);\n perf = performance;\n}\n\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 {\n constructor(connection, messageLedger) {\n\n this.connection = connection;\n this.messageLedger = messageLedger;\n const id = this.connection._id;\n this.debugLabel = this.connection.role === role.initiator ? `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.connectionOptions.ttl.default || this.connection.connectionOptions.ttl.min;\n } else {\n if (validity > this.connection.connectionOptions.ttl.max) ttl = this.connection.connectionOptions.ttl.max;\n else if (validity.ttl < this.connection.connectionOptions.ttl.min) ttl = this.connection.connectionOptions.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 if (validityStampCache.insert(validity.stamp, expirationTime) !== true)\n {\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('messages') && 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 debugging('receiver') && console.warn('Ignoring unhandled message', JSON.stringify(messageBody));\n } else {\n throw new DCPError(`Message in batch has unknown type: ${messageBody.type}`, 'DCPC-1009');\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 handleMessage(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('receiver') && 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 /** XXXwg regardless the cause, we should not be ignoring requests. Spec says we need to send back *something* */\n ( true) && console.debug(this.debugLabel, 'Request has invalid TTL; ignoring request. Validity=', req.payload.validity);\n return;\n } else {\n let reqId = req.id;\n\n this.connection.openRequests[reqId] = msgBody;\n setTimeout(() => {\n /* FIXME XXXwg this is completely insane */\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('receiver') && 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 debugging('receiver') && console.debug(this.debugLabel, `Initiator version ${initiatorVersion} does not meet target targetCompatibility '${targetCompatibility}'`);\n const versionErr = new DCPError(`Initiator version ${initiatorVersion} does not meet target targetCompatibility '${targetCompatibility}'`, 'DCPC-EVERSION');\n const connectResponse = new this.connection.Response(req, versionErr);\n await this.connection.sender.specialFirstSend(connectResponse);\n throw versionErr;\n }\n\n // Memoize the peer version onto the Connection\n this.connection.peerVersion = initiatorVersion;\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}`, 'DCPC-EDCPSID');\n }\n\n // Play silly buggers with the protocol version, so 5.0.0 clients will still\n // respect us, but more recent clients will behave better\n const version = (this.connection.peerVersion === '5.0.0')\n ? '5.0.0' // how do you do, fellow 5.0.0\n : this.connection.constructor.VERSION;\n\n // Append our half of the dcpsid\n const dcpsid = topHalfSid + bottomHalfSid;\n const connectResponse = new this.connection.Response(req, {\n 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('receiver') && console.debug(this.debugLabel, 'first request handled. Connection state:', this.connection.state.valueOf());\n }\n\n handleOperation(req) {\n switch (req.payload.operation) {\n case 'connect':\n throw new DCPError('Request operation \"connect\" received after establishment', 'DCPC-ETOOMANYCONNECT');\n case 'close':\n this.connection.closeWait(req.payload.errorCode).catch(error => console.error('Unexpected error closing initiator:', error.message));\n break;\n \n case 'keepalive':\n req.respond().catch(error => {\n if (error.code === 'DCPC-1011')\n return;\n throw error;\n });\n break;\n \n case 'reconnected':\n this.connection.emit('connect');\n this.connection.sender.notifyTransportReady();\n req.respond().catch(error => {\n if (error.code === 'DCPC-1011')\n return;\n throw error;\n });\n break;\n \n default:\n this.connection.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('receiver') && 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://dcp/./src/protocol-v4/connection/receiver.js?");
4587
4587
 
4588
4588
  /***/ }),
4589
4589
 
@@ -4594,7 +4594,7 @@ eval("/**\n * @file protocol/connection/receiver.js\n * @author Ryan
4594
4594
  /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
4595
4595
 
4596
4596
  "use strict";
4597
- 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 { assert } = __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\");\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').nanoid;\n} else {\n nanoid = (__webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\").nanoid);\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.connectionOptions.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 obj.ackToken = this.ackToken;\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, guardianAddress, accessorAddress) {\n if (!resourceKeystore)\n resourceKeystore = await wallet.get();\n\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 (!guardianAddress || !accessorAddress && this.connection.state.in(['initial', 'connecting']))\n await this.connection.connect();\n\n assert(this.connection.identity);\n assert(this.connection.peerAddress);\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://dcp/./src/protocol-v4/connection/request.js?");
4597
+ 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 { assert } = __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\");\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').nanoid;\n} else {\n nanoid = (__webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\").nanoid);\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.type = 'request';\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.connectionOptions.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 obj.ackToken = this.ackToken;\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, guardianAddress, accessorAddress) {\n if (!resourceKeystore)\n resourceKeystore = await wallet.get();\n\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 (!guardianAddress || !accessorAddress && this.connection.state.in(['initial', 'connecting']))\n await this.connection.connect();\n\n assert(this.connection.identity);\n assert(this.connection.peerAddress);\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://dcp/./src/protocol-v4/connection/request.js?");
4598
4598
 
4599
4599
  /***/ }),
4600
4600
 
@@ -4605,7 +4605,7 @@ eval("/**\n * @file protocol/connection/request.js\n * @author Ryan
4605
4605
  /***/ ((module) => {
4606
4606
 
4607
4607
  "use strict";
4608
- 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.ackToken = this.ackToken\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://dcp/./src/protocol-v4/connection/response.js?");
4608
+ 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 this.type = 'response';\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.ackToken = this.ackToken\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://dcp/./src/protocol-v4/connection/response.js?");
4609
4609
 
4610
4610
  /***/ }),
4611
4611
 
@@ -4616,7 +4616,7 @@ eval("/**\n * @file protocol/connection/response.js\n * @author Ryan
4616
4616
  /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
4617
4617
 
4618
4618
  "use strict";
4619
- eval("/**\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\");\nconst { role } = __webpack_require__(/*! ./connection-constants */ \"./src/protocol-v4/connection/connection-constants.js\");\nconst { setImmediate } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.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').nanoid;\n} else {\n nanoid = (__webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\").nanoid);\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.role === role.initiator ? `sender(i#${id}):` : `sender(t#${id}):`;\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('sender') && console.debug(this.debugLabel, 'sending initial connect message');\n const resp = await this.specialFirstSend(connectRequest);\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 debugging('sender') && console.log(this.debugLabel, `Target's version (${targetVersion}) is not compatible (must meet ${initiatorCompatibility})`);\n throw new DCPError(`Target's version (${targetVersion}) is not compatible (must meet ${initiatorCompatibility})`, 'DCPC-ETARGETVERSION');\n }\n\n // Memoize the peer version onto the Connection\n this.connection.peerVersion = targetVersion;\n if (this.connection.transport)\n this.connection.transport.peerVersion = targetVersion;\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 DCPError(`Target responded with invalid DCPSID: ${resp.dcpsid}`, 'DCPC-1008');\n }\n debugging('sender') && console.debug(this.debugLabel, 'connection established.');\n return {\n dcpsid: resp.dcpsid,\n peerAddress: resp.owner,\n };\n }\n\n /**\n * Invoked when the connection class becomes aware that a transport is available upon which\n * we can deliver traffic, this method either sends the current in-flight message (from a \n * previous transport instance) or services the queue.\n */\n notifyTransportReady()\n {\n debugging('sender') && console.debug(this.debugLabel, `Notified transport is ready. inflight ${!!this.inFlight}, state ${this.connection.state}`)\n if (this.inFlight)\n this.sendInFlightMessage();\n else\n this.connection.keepalive(); // This will pump the message queue\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 const messageSentPromise = this.messageLedger.addMessage(message);\n await this.createAndSend([message]); /* XXXwg - messageLedger may leak messages when createAndSend 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 debugging('sender') && console.debug(this.debugLabel, 'enqueueing message', message.id);\n debugging('sender') && debugging('verbose') && !debugging('all') && console.debug(this.debugLabel, 'message:', message);\n this.queue.push(message);\n \n if (this.connection.state.is('initial'))\n this.connection.connect();\n else if (this.connection.state.is('disconnected'))\n this.connection.connect();\n\n setImmediate(() => this.serviceQueue());\n return this.messageLedger.addMessage(message);\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 debugging('sender') && console.debug(this.debugLabel, `servicing queue. for ${this.connection.state} inFlight=${!!this.inFlight} connection ${this.connection.loggableDest}`);\n this.createAndSend();\n } else {\n debugging('sender') && 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 * @returns a Promise which resolves to a number which represents the number of messages sent. This is \n * either 0, 1, or the number of messages in a batch (possibly 1).\n */\n async createAndSend(messages=[]) {\n if (messages.length === 0) {\n messages = this.queue.splice(0, this.batchSize);\n debugging('sender') && console.debug(\n this.debugLabel, \n `pulled ${messages.length} message${messages.length === 1? '' : 's'} from queue.`\n );\n }\n if (messages.length === 0)\n {\n debugging('sender') && console.debug(this.debugLabel, 'createAndSend: nothing to send');\n return 0;\n }\n assert(!this.inFlight);\n \n let op;\n let opIsReq = false;\n if (messages.length === 1) { // ie. no need to batch, can send single message\n op = messages[0];\n op.ackToken = this.makeAckToken();\n opIsReq = true;\n } else {\n op = new this.connection.Batch(messages, this.makeAckToken());\n debugging('sender') && console.debug(this.debugLabel, `created a batch of length ${messages.length}`);\n }\n\n op.nonce = this.nonce;\n this.inFlight = { message: op };\n let signedOp = await op.sign();\n this.inFlight.signedMessage = signedOp;\n if (!opIsReq)\n this.messageLedger.addBatch(op);\n\n this.connection.emit('send', this.inFlight);\n this.sendInFlightMessage();\n return messages.length;\n }\n \n /**\n * Sends the message stored in the `inFlight` var over the transport.\n * `clearFlightDeck` is the only method that should be resetting this.inFlight\n * and thus closing the loop.\n */\n sendInFlightMessage()\n {\n assert(this.inFlight);\n\n debugging('sender') && console.debug(this.debugLabel, `sending in-flight message ${this.inFlight.message.id}`);\n\n try\n {\n const op = this.inFlight.message;\n let type;\n \n /** XXXwg todo - figure out why Request/Response have name=Message and get rid of tests */\n if (op instanceof this.connection.Request)\n type = 'Request';\n else if (op instanceof this.connection.Response)\n type = 'Response';\n else \n type = op.constructor.name;\n \n debugging('sender') && console.debug(this.debugLabel, `sending ${type}`, this.inFlight.message.id,\n `(${this.inFlight.signedMessage.length} bytes)`);\n this.connection.transport.send(this.inFlight.signedMessage);\n }\n catch (error)\n {\n console.error(`Error while sending message ${this.inFlight.message.id} to ${this.connection.loggableDest}:`, error);\n debugging('sender') && console.debug(this.debugLabel, 'call stack:', new Error().stack);\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 if (this.inFlight !== null) {\n debugging('sender') && console.debug(this.debugLabel, 'clearing flight deck. nonce =', nonce);\n assert(message === this.inFlight.message);\n this.inFlight = null;\n this.nonce = nonce;\n this.serviceQueue();\n }\n }\n\n /**\n * When a connection is closed the sender needs to cancel its send efforts.\n */\n shutdown() {\n debugging('sender') && console.debug(this.debugLabel, 'shutting down.');\n this.inFlight = null;\n this.nonce = null;\n this.queue = [];\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 this._batchSize = this.connection.connectionOptions.allowBatch ? batchSize : 1;\n return this._batchSize;\n }\n}\n\nObject.assign(module.exports, {\n Sender,\n});\n\n\n//# sourceURL=webpack://dcp/./src/protocol-v4/connection/sender.js?");
4619
+ eval("/**\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 { a$sleepMs } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.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\");\nconst { role } = __webpack_require__(/*! ./connection-constants */ \"./src/protocol-v4/connection/connection-constants.js\");\nconst { setImmediate } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.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').nanoid;\n} else {\n nanoid = (__webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\").nanoid);\n}\n\nclass Sender {\n constructor(connection) {\n this.connection = connection;\n this.messageLedger = this.connection.messageLedger;\n\n this.queue = [];\n this.isServicingQueue = false;\n this.inFlight = null;\n this.nonce = null;\n this._ackTokenId = 0;\n const id = this.connection._id;\n this.debugLabel = this.connection.role === role.initiator ? `sender(i#${id}):` : `sender(t#${id}):`;\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('sender') && console.debug(this.debugLabel, 'sending initial connect message');\n const resp = await this.specialFirstSend(connectRequest);\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 debugging('sender') && console.log(this.debugLabel, `Target's version (${targetVersion}) is not compatible (must meet ${initiatorCompatibility})`);\n throw new DCPError(`Target's version (${targetVersion}) is not compatible (must meet ${initiatorCompatibility})`, 'DCPC-ETARGETVERSION');\n }\n\n // Memoize the peer version onto the Connection\n this.connection.peerVersion = targetVersion;\n if (this.connection.transport)\n this.connection.transport.peerVersion = targetVersion;\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 DCPError(`Target responded with invalid DCPSID: ${resp.dcpsid}`, 'DCPC-1008');\n }\n debugging('sender') && console.debug(this.debugLabel, 'connection established.');\n return {\n dcpsid: resp.dcpsid,\n peerAddress: resp.owner,\n };\n }\n\n /**\n * Invoked when the connection class becomes aware that a transport is available upon which\n * we can deliver traffic, this method either sends the current in-flight message (from a \n * previous transport instance) or services the queue.\n */\n notifyTransportReady()\n {\n debugging('sender') && console.debug(this.debugLabel, `Notified transport is ready. inflight ${!!this.inFlight}, state ${this.connection.state}`)\n if (this.inFlight)\n this.sendInFlightMessage();\n else\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 * We finish preparing the 'connect' request, put it in flight and send it out.\n * @param {Message} message \n */\n async specialFirstSend(message) { /* XXXwg - special first send *will* cause double dcpsid if invoked twice */\n message.ackToken = this.makeAckToken();\n let signedMessage = await message.sign();\n const messageSentPromise = this.messageLedger.addMessage(message);\n \n this.inFlight = { message: message, signedMessage: signedMessage };\n if (message instanceof this.connection.Response) {\n this.connection.registerConnectResponse(this.inFlight.message); /* XXXwg todo - audit this.inFlight */\n }\n this.sendInFlightMessage();\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<Connection.Response>} from messageLedger that resolves when message response received.\n */\n enqueue(message) {\n if (message.payload && message.payload.operation)\n debugging('connection') && console.debug(this.debugLabel, `enqueueing ${message.type} message for operation:`, message.payload.operation);\n else\n debugging('connection') && console.debug(this.debugLabel, `enqueueing ${message.type} message`);\n \n this.queue.push(message);\n \n if (this.connection.state.is('initial'))\n this.connection.connect();\n else if (this.connection.state.is('disconnected'))\n this.connection.connect();\n \n setImmediate(() => this.requestQueueService());\n return this.messageLedger.addMessage(message);\n }\n \n /**\n * Checks if we're able to send a message in our current state, and\n * services the queue if so.\n */\n requestQueueService()\n {\n if (this.inFlight || !this.nonce || this.isServicingQueue) return;\n this.serviceQueue();\n }\n /**\n * Pulls a message from the queue. If the message is batchable, pauses for 30ms to allow other batchable\n * messages to enter the queue. If the message is not batchable, ie. is here just to finish being prepared,\n * then sign the message and emit `message.id ready` to resolve a `Connetion.prepareMessage` promise elsewhere.\n * When a message is signed, our nonce is deleted so that it may not be used in another message.\n */\n async serviceQueue ()\n {\n if (!this.inFlight && this.connection.state.in(['established', 'closing', 'close-wait']))\n {\n this.isServicingQueue = true;\n let nextMessage = this.queue.shift();\n if (!nextMessage)\n {\n debugging('sender') && console.debug(`${this.debugLabel} serviceQueue: nothing to send`)\n this.isServicingQueue = false;\n return;\n }\n // Buffer the dequeuing of batchable messages to emulate the intention of Nagle's\n // algorithm, used to reduce the number of packets being sent over the network.\n if (nextMessage.batchable)\n {\n await a$sleepMs(30);\n }\n\n if (!nextMessage.batchable)\n {\n nextMessage.nonce = this.nonce;\n delete this.nonce;\n let signedMessage = await nextMessage.sign();\n let preparedMessage = { message: nextMessage, signedMessage: signedMessage }\n this.connection.emit(`${nextMessage.id} ready`, preparedMessage);\n }\n else\n { \n let rawBatch = [];\n do\n {\n rawBatch.push(nextMessage);\n nextMessage = this.queue.shift();\n } while (nextMessage && nextMessage.batchable && rawBatch.length < this._maxBatchSize)\n // if dequeuing a prepared message stopped the loop, put the message back in front \n if (nextMessage) this.queue.unshift(nextMessage); \n this.createAndSend(rawBatch);\n }\n this.isServicingQueue = false;\n }\n else\n {\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 * Takes an array of messages and batches them if there's more than one. Signs the resultant message and puts it in flight.\n * @param {Array<Connection.Message>} [messages] Array of messages to batch together for a transmission. \n * if not provided, draws from queue.\n *\n * @returns a Promise which resolves to a number which represents the number of messages sent. This is \n * either 0, 1, or the number of messages in a batch (possibly 1).\n */\n async createAndSend(messages) {\n assert(!this.inFlight);\n \n // By default, let `message` be first entry in input array.\n // If more than one entry in array, reassign `message` as batch object.\n let message = messages[0];\n if (messages.length > 1)\n message = new this.connection.Batch(messages, this.makeAckToken()); \n\n message.nonce = this.nonce;\n delete this.nonce;\n \n if (message instanceof this.connection.Batch) this.messageLedger.addBatch(message);\n \n this.inFlight = { message: message };\n let signedMessage = await message.sign();\n\n // Connection may start shutting down while awaiting and sets this.inFlight=null\n if(!this.inFlight) return;\n\n this.inFlight.signedMessage = signedMessage;\n this.connection.emit('send', this.inFlight);\n \n debugging('connection') && console.debug(this.debugLabel, `sending a signed ${message.type} of ${messages.length}`);\n this.sendInFlightMessage();\n }\n \n /**\n * Sends the message stored in the `inFlight` var over the transport.\n * `clearFlightDeck` is the only method that should be resetting this.inFlight\n * and thus closing the loop.\n */\n sendInFlightMessage()\n {\n assert(this.inFlight);\n\n debugging('sender') && console.debug(this.debugLabel, `sending in-flight message ${this.inFlight.message.id}`);\n\n try\n {\n const message = this.inFlight.message;\n let type;\n \n /** XXXwg todo - figure out why Request/Response have name=Message and get rid of tests */\n if (message instanceof this.connection.Request)\n type = 'Request';\n else if (message instanceof this.connection.Response)\n type = 'Response';\n else \n type = message.constructor.name;\n \n debugging('sender') && console.debug(this.debugLabel, `sending ${type}`, this.inFlight.message.id,\n `(${this.inFlight.signedMessage.length} bytes)`);\n this.connection.transport.send(this.inFlight.signedMessage);\n }\n catch (error)\n {\n console.error(`Error while sending message ${this.inFlight.message.id} to ${this.connection.loggableDest}:`, error);\n debugging('sender') && console.debug(this.debugLabel, 'call stack:', new Error().stack);\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 if (this.inFlight !== null) {\n debugging('sender') && console.debug(this.debugLabel, 'clearing flight deck. nonce =', nonce);\n assert(message === this.inFlight.message);\n this.inFlight = null;\n this.nonce = nonce;\n this.requestQueueService();\n }\n }\n\n /**\n * When a connection is closed the sender needs to cancel its send efforts.\n */\n shutdown() {\n debugging('sender') && console.debug(this.debugLabel, 'shutting down.');\n this.inFlight = null;\n this.nonce = null;\n this.queue = [];\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._maxBatchSize) return this._maxBatchSize;\n const maxBatchSize = Math.max(this.connection.connectionOptions.maxMessagesPerBatch, 1);\n this._maxBatchSize = this.connection.connectionOptions.allowBatch ? maxBatchSize : 1;\n return this._maxBatchSize;\n }\n}\n\nObject.assign(module.exports, {\n Sender,\n});\n\n\n//# sourceURL=webpack://dcp/./src/protocol-v4/connection/sender.js?");
4620
4620
 
4621
4621
  /***/ }),
4622
4622
 
@@ -4754,7 +4754,7 @@ eval("/**\n * @file utils/http.js Helper module for things rel
4754
4754
  /***/ ((module, exports, __webpack_require__) => {
4755
4755
 
4756
4756
  "use strict";
4757
- eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_modules/process/browser.js */ \"./node_modules/process/browser.js\");\n/* provided dependency */ var Buffer = __webpack_require__(/*! ./node_modules/node-polyfill-webpack-plugin/node_modules/buffer/index.js */ \"./node_modules/node-polyfill-webpack-plugin/node_modules/buffer/index.js\")[\"Buffer\"];\n/**\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\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst schedulerConstants = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.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 utils$$generateOpaqueId()\n{\n if (!utils$$generateOpaqueId.nanoid)\n {\n const nanoidModule = (DCP_ENV.platform === 'nodejs') ? requireNative('nanoid') : __webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\");\n utils$$generateOpaqueId.nanoid = nanoidModule.customAlphabet('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+', schedulerConstants.workerIdLength);\n }\n\n return utils$$generateOpaqueId.nanoid();\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/** \n * Resolve a promise after a specified time.\n * \n * @param {number} ms the number of milliseconds after which to resolve the promise.\n * @returns Promise with an extra property, intr(). Calling this function causes the promise\n * to resolve immediately. If the promise was interrupted, it will resolve with false;\n * otherwise, true.\n */\nexports.a$sleepMs = function a$sleepMs(ms)\n{\n var interrupt;\n \n const ret = new Promise((resolve, reject) => {\n var resolved = false;\n const timerHnd = setTimeout(() => { resolved=true; resolve(false) }, ms);\n function a$sleepMs_intr()\n {\n clearTimeout(timerHnd);\n if (!resolved)\n resolve(true);\n }\n \n interrupt = a$sleepMs_intr;\n });\n\n ret.intr = () => interrupt();\n return ret;\n}\n\n/** \n * @see: a$sleepMs\n * @param {number} ms the number of milliseconds after which to resolve the promise.\n */\nexports.a$sleep = function a$sleep(seconds) {\n return exports.a$sleepMs(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/** XXXpfr @todo Write sort w/o using promises so we can get rid of async on all the compress functions. */\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 sliceArray.\n * @param {Slice[]} sliceArray\n * @param {string} [header]\n */\nexports.dumpSlices = async function utils$$dumpSlices(sliceArray, header) {\n if (header) console.log(`\\n${header}`);\n console.log(await exports.compressSlices(sliceArray));\n}\n\n/**\n * Log sandboxArray.\n * @param {Sandbox[]} sandboxArray\n * @param {string} [header]\n */\nexports.dumpSandboxes = async function utils$$dumpSandboxes(sandboxArray, header) {\n if (header) console.log(`\\n${header}`);\n console.log(await exports.compressSandboxes(sandboxArray));\n}\n\n/**\n * If the elements of sliceArray are not unique, log the duplicates and log the full array.\n * @param {Slice[]} sliceArray\n * @param {string} [header]\n */\nexports.dumpSlicesIfNotUnique = async function utils$$dumpSlicesIfNotUnique(sliceArray, header) {\n if (!exports.isUniqueSlices(sliceArray, header))\n console.log(await exports.compressSlices(sliceArray));\n}\n\n/**\n * If the elements of sandboxArray are not unique, log the duplicates and log the full array.\n * @param {Sandbox[]} sandboxArray\n * @param {string} [header]\n */\nexports.dumpSandboxesIfNotUnique = async function utils$$dumpSandboxesIfNotUnique(sandboxArray, header) {\n if (!exports.isUniqueSandboxes(sandboxArray, header))\n console.log(await exports.compressSandboxes(sandboxArray));\n}\n\n/**\n * Checks whether the elements of sliceArray are unique and if not, log the duplicates.\n * @param {Slice[]} sliceArray\n * @param {string} [header]\n * @param {function} [log]\n * @returns {boolean}\n */\nexports.isUniqueSlices = function utils$$isUniqueSlices(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 log ? log(x) : console.log(`\\tFound duplicate slice ${x.identifier}.`);\n } else slices.push(x);\n });\n return sliceArray.length === slices.length;\n}\n\n/**\n * Checks whether the elements of sandboxArray are unique and if not, log the duplicates.\n * @param {Sandbox[]} sandboxArray\n * @param {string} [header]\n * @param {function} [log]\n * @returns {boolean}\n */\nexports.isUniqueSandboxes = function utils$$isUniqueSandboxes(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 log ? log(x) : console.log(`\\tFound duplicate sandbox ${x.identifier}.`);\n } else sandboxes.push(x);\n });\n return sandboxArray.length === sandboxes.length;\n}\n\n/**\n * Quck and dirty JSON serialization that ignores cycles.\n *\n * @param {*} o - entity to be serialized.\n * @param {number} [len=512] - number of string elements to return.\n * @returns {string}\n */\nexports.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 {*} theAnything\n * @param {string} [header='dumpJSON]\n * @param {number} [valueLength=512]\n */\nexports.dumpJSON = function utils$$dumpJSON(theAnything, header = 'dumpJSON: ', valueLength = 512) {\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='dumpObject']\n * @param {number} [valueLength=512]\n */\nexports.dumpObject = function utils$$dumpObject(theObject, header = 'dumpObject: ', valueLength = 512) {\n for (const [key, value] of Object.entries(theObject)) {\n if (value) {\n const strV = String(exports.stringify(value, 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 * @param {object[]} jobArray\n * @param {function} functor\n * @returns {object}\n */\nexports.toJobMap = function utils$$toJobMap(jobArray, functor) {\n const jobMap = {};\n for (const x of jobArray) {\n if (!jobMap[x.jobAddress]) jobMap[x.jobAddress] = [functor(x)];\n else jobMap[x.jobAddress].push(functor(x));\n }\n return jobMap;\n}\n\n/**\n * @param {Sandbox[]} sandboxArray\n * @param {number} [digits=-1]\n * @returns {Promise<string>}\n */\nexports.compressSandboxes = async function utils$$compressSandboxes(sandboxArray, digits = -1) {\n const jobSandboxMap = exports.toJobMap(sandboxArray, sbx => sbx.id);\n return exports.compressJobMap(jobSandboxMap, false /* skipFirst*/, digits);\n}\n\n/**\n * @param {Slice[]} sliceArray\n * @param {number} [digits=-1]\n * @returns {Promise<string>}\n */\nexports.compressSlices = async function utils$$compressSlices(sliceArray, digits = -1) {\n const jobSliceMap = exports.toJobMap(sliceArray, slice => slice.sliceNumber);\n return exports.compressJobMap(jobSliceMap, false /* skipFirst*/, digits);\n}\n\n/**\n * @param {object} jobMap\n * @param {boolean} [skipFirst=false]\n * @param {number} [digits=-1]\n * @returns {Promise<string>}\n */\nexports.compressJobMap = async function utils$$compressJobMap(jobMap, skipFirst = false, digits = -1) {\n return exports.compressJobArray(Object.entries(jobMap), skipFirst, digits);\n}\n\n/**\n * @param {object[]} jobArray\n * @param {boolean} [skipFirst=false]\n * @param {number} [digits=-1]\n * @returns {Promise<string>}\n */\nexports.compressJobArray = async function utils$$compressJobArray(jobArray, skipFirst = false, digits = -1) {\n let output = '';\n for (let k = 0; k < jobArray.length; k++) {\n output += await exports.compressJobValue(jobArray[k], skipFirst, digits);\n }\n return output;\n}\n\n/**\n * @param {object|object[]} jobValue\n * @param {boolean} [skipFirst=false]\n * @param {number} [digits=-1]\n * @returns {Promise<string>}\n */\nexports.compressJobValue = async function utils$$compressJobValue(jobValue, skipFirst = false, digits = -1) {\n if (jobValue.job && jobValue.slices)\n return exports.compressJobEntry(jobValue.job, (skipFirst ? jobValue.slices.slice(1) : jobValue.slices), digits);\n if (jobValue.length === 2)\n return exports.compressJobEntry(jobValue[0], (skipFirst ? jobValue[1].slice(1) : jobValue[1]), digits);\n return 'nada';\n}\n\n/**\n * @param {*} jobAddress\n * @param {Array<number|Number>} sliceNumbers\n * @param {number} [digits=-1]\n * @returns {Promise<string>}\n */\nexports.compressJobEntry = async function utils$$compressJobEntry(jobAddress, sliceNumbers, digits = -1) {\n return `${exports.truncateAddress(jobAddress, digits)}:[${await exports.compressRange(sliceNumbers)}]:`;\n}\n\n/**\n * @param {*} address\n * @param {number} [digits=-1]\n * @returns {string}\n */\nexports.truncateAddress = function utils$$truncateAddress(address, digits = -1) {\n let fragment = address.toString();\n if (digits < 0) return fragment.startsWith('0x') ? fragment.substring(2) : fragment;\n return fragment.startsWith('0x') ? fragment.substring(2, 2 + digits) : fragment.substring(0, digits);\n}\n\n/**\n * Input [2, 3, 4, 7, 5, 8, 9, 13, 14, 15] returns '2-5,7-9,13-15'\n * Input [2, 3, 4, 7, 5, 8, 9, 13] returns '2-5,7-9,13'\n * Input [2, 3, 4, 7, 5, 4, 4, 8, 9] returns '2-4,4,4-5,7-9'\n * @param {Array<number|Number>} numberArray\n * @returns {Promise<string>}\n */\nexports.compressRange = async function utils$$compressRange(numberArray) {\n assert(numberArray && Array.isArray(numberArray));\n await numberArray.sort((x, y) => x - y); // increasing...\n let start = numberArray[0];\n let output = `${start}`;\n for (let k = 1; k < numberArray.length; k++) {\n assert(typeof numberArray[k] === 'number' || numberArray[k] && numberArray[k].constructor.name === 'Number');\n if (numberArray[k] - numberArray[k - 1] !== 1) {\n output += (numberArray[k - 1] > start) ? `-${numberArray[k - 1]},` : ',';\n start = numberArray[k];\n output += `${start}`;\n } else if (k === numberArray.length - 1) {\n output += `-${numberArray[k]}`;\n }\n }\n return output;\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 === schedulerConstants.jobStatus.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\nexports.hashGeneration = function utils$$hashGeneration(object) {\n const { sha256 } = (__webpack_require__(/*! dcp/dcp-client/wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.util);\n return sha256(Buffer.from(JSON.stringify(object)));\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-data-uri */ \"./src/utils/make-data-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//# sourceURL=webpack://dcp/./src/utils/index.js?");
4757
+ eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_modules/process/browser.js */ \"./node_modules/process/browser.js\");\n/* provided dependency */ var Buffer = __webpack_require__(/*! ./node_modules/node-polyfill-webpack-plugin/node_modules/buffer/index.js */ \"./node_modules/node-polyfill-webpack-plugin/node_modules/buffer/index.js\")[\"Buffer\"];\n/**\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\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst schedulerConstants = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.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 utils$$generateOpaqueId()\n{\n if (!utils$$generateOpaqueId.nanoid)\n {\n const nanoidModule = (DCP_ENV.platform === 'nodejs') ? requireNative('nanoid') : __webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\");\n utils$$generateOpaqueId.nanoid = nanoidModule.customAlphabet('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+', schedulerConstants.workerIdLength);\n }\n\n return utils$$generateOpaqueId.nanoid();\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/** \n * Resolve a promise after a specified time.\n * \n * @param {number} ms the number of milliseconds after which to resolve the promise.\n * @returns Promise with an extra property, intr(). Calling this function causes the promise\n * to resolve immediately. If the promise was interrupted, it will resolve with false;\n * otherwise, true.\n */\nexports.a$sleepMs = function a$sleepMs(ms)\n{\n var interrupt;\n \n const ret = new Promise((resolve, reject) => {\n var resolved = false;\n const timerHnd = setTimeout(() => { resolved=true; resolve(false) }, ms);\n function a$sleepMs_intr()\n {\n clearTimeout(timerHnd);\n if (!resolved)\n resolve(true);\n }\n \n interrupt = a$sleepMs_intr;\n });\n\n ret.intr = () => interrupt();\n return ret;\n}\n\n/** \n * @see: a$sleepMs\n * @param {number} ms the number of milliseconds after which to resolve the promise.\n */\nexports.a$sleep = function a$sleep(seconds) {\n return exports.a$sleepMs(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/** XXXpfr @todo Write sort w/o using promises so we can get rid of async on all the compress functions. */\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 sliceArray.\n * @param {Slice[]} sliceArray\n * @param {string} [header]\n */\nexports.dumpSlices = async function utils$$dumpSlices(sliceArray, header) {\n if (header) console.log(`\\n${header}`);\n console.log(await exports.compressSlices(sliceArray));\n}\n\n/**\n * Log sandboxArray.\n * @param {Sandbox[]} sandboxArray\n * @param {string} [header]\n */\nexports.dumpSandboxes = async function utils$$dumpSandboxes(sandboxArray, header) {\n if (header) console.log(`\\n${header}`);\n console.log(await exports.compressSandboxes(sandboxArray));\n}\n\n/**\n * If the elements of sliceArray are not unique, log the duplicates and log the full array.\n * @param {Slice[]} sliceArray\n * @param {string} [header]\n */\nexports.dumpSlicesIfNotUnique = async function utils$$dumpSlicesIfNotUnique(sliceArray, header) {\n if (!exports.isUniqueSlices(sliceArray, header))\n console.log(await exports.compressSlices(sliceArray));\n}\n\n/**\n * If the elements of sandboxArray are not unique, log the duplicates and log the full array.\n * @param {Sandbox[]} sandboxArray\n * @param {string} [header]\n */\nexports.dumpSandboxesIfNotUnique = async function utils$$dumpSandboxesIfNotUnique(sandboxArray, header) {\n if (!exports.isUniqueSandboxes(sandboxArray, header))\n console.log(await exports.compressSandboxes(sandboxArray));\n}\n\n/**\n * Checks whether the elements of sliceArray are unique and if not, log the duplicates.\n * @param {Slice[]} sliceArray\n * @param {string} [header]\n * @param {function} [log]\n * @returns {boolean}\n */\nexports.isUniqueSlices = function utils$$isUniqueSlices(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 log ? log(x) : console.log(`\\tFound duplicate slice ${x.identifier}.`);\n } else slices.push(x);\n });\n return sliceArray.length === slices.length;\n}\n\n/**\n * Checks whether the elements of sandboxArray are unique and if not, log the duplicates.\n * @param {Sandbox[]} sandboxArray\n * @param {string} [header]\n * @param {function} [log]\n * @returns {boolean}\n */\nexports.isUniqueSandboxes = function utils$$isUniqueSandboxes(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 log ? log(x) : console.log(`\\tFound duplicate sandbox ${x.identifier}.`);\n } else sandboxes.push(x);\n });\n return sandboxArray.length === sandboxes.length;\n}\n\n/**\n * Quck and dirty JSON serialization that ignores cycles.\n *\n * @param {*} o - entity to be serialized.\n * @param {number} [len=512] - number of string elements to return.\n * @returns {string}\n */\nexports.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 {*} theAnything\n * @param {string} [header='dumpJSON]\n * @param {number} [valueLength=512]\n */\nexports.dumpJSON = function utils$$dumpJSON(theAnything, header = 'dumpJSON: ', valueLength = 512) {\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='dumpObject']\n * @param {number} [valueLength=512]\n */\nexports.dumpObject = function utils$$dumpObject(theObject, header = 'dumpObject: ', valueLength = 512) {\n for (const [key, value] of Object.entries(theObject)) {\n if (value) {\n const strV = String(exports.stringify(value, 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 * @param {object[]} jobArray\n * @param {function} functor\n * @returns {object}\n */\nexports.toJobMap = function utils$$toJobMap(jobArray, functor) {\n const jobMap = {};\n for (const x of jobArray) {\n if (!jobMap[x.jobAddress]) jobMap[x.jobAddress] = [functor(x)];\n else jobMap[x.jobAddress].push(functor(x));\n }\n return jobMap;\n}\n\n/**\n * @param {Sandbox[]} sandboxArray\n * @param {number} [digits=-1]\n * @returns {Promise<string>}\n */\nexports.compressSandboxes = async function utils$$compressSandboxes(sandboxArray, digits = -1) {\n const jobSandboxMap = exports.toJobMap(sandboxArray, sbx => sbx.id);\n return exports.compressJobMap(jobSandboxMap, false /* skipFirst*/, digits);\n}\n\n/**\n * @param {Slice[]} sliceArray\n * @param {number} [digits=-1]\n * @returns {Promise<string>}\n */\nexports.compressSlices = async function utils$$compressSlices(sliceArray, digits = -1) {\n const jobSliceMap = exports.toJobMap(sliceArray, slice => slice.sliceNumber);\n return exports.compressJobMap(jobSliceMap, false /* skipFirst*/, digits);\n}\n\n/**\n * @param {object} jobMap\n * @param {boolean} [skipFirst=false]\n * @param {number} [digits=-1]\n * @returns {Promise<string>}\n */\nexports.compressJobMap = async function utils$$compressJobMap(jobMap, skipFirst = false, digits = -1) {\n return exports.compressJobArray(Object.entries(jobMap), skipFirst, digits);\n}\n\n/**\n * @param {object[]} jobArray\n * @param {boolean} [skipFirst=false]\n * @param {number} [digits=-1]\n * @returns {Promise<string>}\n */\nexports.compressJobArray = async function utils$$compressJobArray(jobArray, skipFirst = false, digits = -1) {\n let output = '';\n for (let k = 0; k < jobArray.length; k++) {\n output += await exports.compressJobValue(jobArray[k], skipFirst, digits);\n }\n return output;\n}\n\n/**\n * @param {object|object[]} jobValue\n * @param {boolean} [skipFirst=false]\n * @param {number} [digits=-1]\n * @returns {Promise<string>}\n */\nexports.compressJobValue = async function utils$$compressJobValue(jobValue, skipFirst = false, digits = -1) {\n if (jobValue.job && jobValue.slices)\n return exports.compressJobEntry(jobValue.job, (skipFirst ? jobValue.slices.slice(1) : jobValue.slices), digits);\n if (jobValue.length === 2)\n return exports.compressJobEntry(jobValue[0], (skipFirst ? jobValue[1].slice(1) : jobValue[1]), digits);\n return 'nada';\n}\n\n/**\n * @param {*} jobAddress\n * @param {Array<number|Number>} sliceNumbers\n * @param {number} [digits=-1]\n * @returns {Promise<string>}\n */\nexports.compressJobEntry = async function utils$$compressJobEntry(jobAddress, sliceNumbers, digits = -1) {\n return `${exports.truncateAddress(jobAddress, digits)}:[${await exports.compressRange(sliceNumbers)}]:`;\n}\n\nexports.compressEnhancedJobEntry = async function utils$$compressEnhancedJobEntry(job, slices, digits = -1) {\n return `${job.id}.${exports.truncateAddress(job.address, digits)}:[${await exports.compressEnhancedRange(slices)}]:`;\n}\n\n/**\n * @param {*} address\n * @param {number} [digits=-1]\n * @returns {string}\n */\nexports.truncateAddress = function utils$$truncateAddress(address, digits = -1) {\n let value = address.toString();\n if (digits < 0) return value.startsWith('0x') ? value.substring(2) : fragment;\n return value.startsWith('0x') ? value.substring(2, 2 + digits) : value.substring(0, digits);\n}\n\n/**\n * Input [2, 3, 4, 7, 5, 8, 9, 13, 14, 15] returns '2-5,7-9,13-15'\n * Input [2, 3, 4, 7, 5, 8, 9, 13] returns '2-5,7-9,13'\n * Input [2, 3, 4, 7, 5, 4, 4, 8, 9] returns '2-4,4,4-5,7-9'\n * @param {Array<number|Number>} numberArray\n * @returns {Promise<string>}\n */\nexports.compressRange = async function utils$$compressRange(numberArray) {\n assert(numberArray && Array.isArray(numberArray));\n await numberArray.sort((x, y) => x - y); // increasing...\n let start = numberArray[0];\n let output = `${start}`;\n for (let k = 1; k < numberArray.length; k++) {\n assert(typeof numberArray[k] === 'number' || numberArray[k] && numberArray[k].constructor.name === 'Number');\n if (numberArray[k] - numberArray[k - 1] !== 1) {\n output += (numberArray[k - 1] > start) ? `-${numberArray[k - 1]},` : ',';\n start = numberArray[k];\n output += `${start}`;\n } else if (k === numberArray.length - 1) {\n output += `-${numberArray[k]}`;\n }\n }\n return output;\n}\n\nexports.compressEnhancedRange = async function utils$$compressEnhancedRange(slices) {\n assert(slices && Array.isArray(slices));\n await slices.sort((x, y) => x.sliceNumber - y.sliceNumber); // increasing...\n let start = slices[0];\n let output = fragment(start);\n for (let k = 1; k < slices.length; k++) {\n if (slices[k].sliceNumber - slices[k - 1].sliceNumber !== 1) {\n output += (slices[k - 1].sliceNumber > start.sliceNumber) ? `-${fragment(slices[k - 1])},` : ',';\n start = slices[k];\n output += `${fragment(start)}`;\n } else if (k === slices.length - 1) {\n output += `-${fragment(slices[k])}`;\n }\n }\n return output;\n}\n\nfunction fragment(slice) {\n return `${slice.sliceNumber}.${slice.isEstimationSlice}.${slice.isLongSlice}`;\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 === schedulerConstants.jobStatus.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\nexports.hashGeneration = function utils$$hashGeneration(object) {\n const { sha256 } = (__webpack_require__(/*! dcp/dcp-client/wallet/keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth.util);\n return sha256(Buffer.from(JSON.stringify(object)));\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-data-uri */ \"./src/utils/make-data-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//# sourceURL=webpack://dcp/./src/utils/index.js?");
4758
4758
 
4759
4759
  /***/ }),
4760
4760
 
@@ -4821,6 +4821,17 @@ eval("/* provided dependency */ var Buffer = __webpack_require__(/*! ./node_modu
4821
4821
 
4822
4822
  /***/ }),
4823
4823
 
4824
+ /***/ "./src/utils/ringBuffer.js":
4825
+ /*!*********************************!*\
4826
+ !*** ./src/utils/ringBuffer.js ***!
4827
+ \*********************************/
4828
+ /***/ ((module) => {
4829
+
4830
+ "use strict";
4831
+ eval("/**\n * @file ringBuffer.js\n * @author Wes Garland, wes@kingsds.network, Original author \n * Arnab Sagar, arnab@kingsds.network\n * @date Mar 2022 \n * \n * Implementation of an N-slot ring buffer, used for storing the most recent job addresses.\n */\n\n\n\nvar RingBuffer = function (size)\n{\n this.size = size;\n this.clear();\n}\n\nRingBuffer.prototype.push = function RingBuffer$$push(el)\n{\n this.buf[this.next] = el;\n this.next = (this.next + 1) % this.size;\n}\n\nRingBuffer.prototype.clear = function RingBuffer$$clear()\n{\n this.buf = [];\n this.next = 0;\n}\n\nfor (let method of ['forEach', 'map', 'filter', 'find', 'slice'])\n{\n RingBuffer.prototype[method] = function RingBuffer$$autoMethod()\n {\n var top = this.buf.slice(this.next);\n var bottom = this.buf.slice(0, this.next);\n var tmp = top.concat(bottom);\n \n return tmp[method].apply(tmp, arguments);\n }\n}\n\nmodule.exports = RingBuffer;\n\n\n//# sourceURL=webpack://dcp/./src/utils/ringBuffer.js?");
4832
+
4833
+ /***/ }),
4834
+
4824
4835
  /***/ "./src/utils/serialize.js":
4825
4836
  /*!********************************!*\
4826
4837
  !*** ./src/utils/serialize.js ***!