dcp-client 4.2.8 → 4.2.9

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.
@@ -3831,7 +3831,7 @@ eval("// Copyright Joyent, Inc. and other Node contributors.\n//\n// Permission
3831
3831
  /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
3832
3832
 
3833
3833
  "use strict";
3834
- 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\":\"fca68c239b7cda20a34513139c096b25d2576370\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"4.2.8\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#prod-20220620\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#e6f31a3a6da1574b3d3bf252bd80208e07937946\"},\"built\":\"Tue Jun 21 2022 13:58:11 GMT-0400 (Eastern Daylight Saving Time)\",\"config\":{\"generated\":\"Tue 21 Jun 2022 01:58:09 PM EDT by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"5.70.0\",\"node\":\"v14.19.3\"} !== 'undefined' && typeof window.Modal === 'undefined') {\n window.Modal = Modal\n }\n}\n\n\n//# sourceURL=webpack://dcp/./portal/www/js/modal.js?");
3834
+ 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\":\"b5864d51c1e7a9b3024698306574b60003a91d62\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"4.2.9\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#prod-20220718\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#15ef46ae27df39b0d1383bd282d36f8c3227905e\"},\"built\":\"Fri Jul 22 2022 09:34:47 GMT-0400 (Eastern Daylight Saving Time)\",\"config\":{\"generated\":\"Fri 22 Jul 2022 09:34:45 AM EDT by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"5.70.0\",\"node\":\"v14.20.0\"} !== 'undefined' && typeof window.Modal === 'undefined') {\n window.Modal = Modal\n }\n}\n\n\n//# sourceURL=webpack://dcp/./portal/www/js/modal.js?");
3835
3835
 
3836
3836
  /***/ }),
3837
3837
 
@@ -3852,7 +3852,7 @@ eval("/** @file Provide a standard set of DCP CLI options and related util
3852
3852
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
3853
3853
 
3854
3854
  "use strict";
3855
- eval("/**\n * @file concurrency.js\n * Concurrency primitives for DCP.\n *\n * @author Wes Garland, wes@kingsds.network\n * @date Dec 2020\n */\n\n\nconst inspect = Symbol.for('nodejs.util.inspect.custom');\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst { assert, assertHas } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { setImmediate } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\n\n/**\n * Synchronizer constructor. Instance objects are used to accurately manage \n * transitions between various states. Observing calls to set (possibly via testAndSet\n * or setIf) can allow a developer to develop an accurate state transition diagram.\n *\n * @param {any} initial initial value of the synchronizer\n * @param {Array} assertValues [optional] if present, assert that all set values are in this \n * array when running a debug build\n */\nexports.Synchronizer = function concurrency$$Synchronizer(initial, assertValues)\n{\n this._ = initial; /* current/internal state of the synchronizer is stored on this._ */\n this.lastStack = new Error().stack;\n\n if (!assertValues)\n return;\n\n for (let av of assertValues)\n assert(typeof av !== 'undefined');\n \n this.assertValues = [].concat(assertValues);\n assertHas(this.assertValues, this._);\n}\nexports.Synchronizer.prototype = new EventEmitter('Synchronizer');\nexports.Synchronizer.prototype[inspect] = function ()\n{\n return `[Object Synchronizer <${this._}>]`;\n};\n\n/**\n * Factory function which makes a new Synchronizer that has the same current value as this\n * synchronizer and the same assertValues (if applicable), but not the same event listeners.\n */\nexports.Synchronizer.prototype.duplicate = function concurrency$$Synchronizer$duplicate()\n{\n return new exports.Synchronizer(this._, this.assertValues);\n}\n\n/** \n * Set the synchronizer a given state.\n *\n * @param old The state the synchronizer is currently in\n * @param neu The new state in which to place the synchronizer\n * @throws code DCP_SYNCHRONIZER_ESYNC if the synchronizer was not in the old state\n */\nexports.Synchronizer.prototype.set = function concurrency$$Synchronizer$set(old, neu)\n{\n const that = this;\n assert(typeof old !== 'undefined', typeof neu !== 'undefined');\n \n if (!Array.isArray(old))\n assert(old !== neu);\n else\n {\n let idx = old.indexOf(this._);\n if (idx === -1)\n throwup();\n \n assert(old.indexOf(neu) === -1);\n for (let state of old)\n assertHas(this.assertValues, state);\n\n old = old[idx];\n }\n\n if (this._ !== old)\n throwup();\n\n this._ = neu;\n this.lastStack = new Error().stack;\n assertHas(this.assertValues, this._);\n this.emit('change', this._, old);\n\n function throwup()\n {\n let lastCaller = that.lastStack.split('\\n')[2].replace(/^ *at /, '');\n throw new DCPError(`Cannot transition synchronizer from ${old}->${neu}; was in ${that._} from ${lastCaller}`, 'DCP_SYNCHRONIZER_ESYNC');\n }\n}\n\n/**\n * Returns a promise which resolves when this Synchronizer achieves the desired state, or\n * rejects when the Synchronizer is destroyed.\n *\n * @param {any} which The state we are waiting for\n * @returns {promise}\n */\nexports.Synchronizer.prototype.until = function concurrency$$Synchronizer$until(which)\n{\n var that = this;\n \n function pWrapper(resolve, reject)\n {\n if (that._ === which)\n {\n setImmediate(resolve);\n return;\n }\n\n function changeHandlerOne(newState)\n {\n if (newState !== which || that._ !== which)\n return;\n\n that.removeEventListener('change', changeHandlerOne);\n that.removeEventListener('destroy', destroyHandler);\n resolve();\n }\n\n function changeHandlerMany(newState)\n {\n if (which.indexOf(newState) === -1 && which.indexOf(that._) === -1)\n return;\n\n that.removeEventListener('change', changeHandlerMany);\n that.removeEventListener('destroy', destroyHandler);\n resolve();\n }\n\n function destroyHandler()\n {\n that.removeEventListener(changeHandlerMany);\n that.removeEventListener(changeHandlerOne);\n that.removeEventListener(destroyHandler);\n reject();\n }\n\n that.on('change', Array.isArray(which) ? changeHandlerMany : changeHandlerOne);\n that.on('destroy', destroyHandler);\n }\n\n return new Promise(pWrapper);\n}\n\n/**\n * Destroy a synchronizer. Once this function has been invoked, no further operations on\n * the synchronizer are permitted.\n *\n * Causes subsequent attempts to read or write the interval state to throw code DCP_SYNCHRONIZER_EINVAL .\n */\nexports.Synchronizer.prototype.destroy = function concurrency$$Synchronizer$destroy()\n{\n Object.defineProperty(this, '_', {\n get: () => { throw new DCPError('synchronizer has been destroyed', 'DCP_SYNCHRONIZER_EINVAL'); },\n set: () => { throw new DCPError('synchronizer has been destroyed', 'DCP_SYNCHRONIZER_EINVAL'); },\n\n configurable: false,\n enumerable: false,\n writable: false,\n });\n\n this.on('destroy', () => this.removeAllListeners());\n this.emit('destroy');\n}\n\n/** \n * Set the synchronizer instance to a given state, providing it was previously in\n * the supplied 'old' state\n *\n * @param old The state the synchronizer is currently in\n * @param neu The new state in which to place the synchronizer\n *\n * @returns true if the synchronizer state was changed, otherwise false\n */\nexports.Synchronizer.prototype.testAndSet = function concurrency$$Synchronizer$testAndSet(old, neu)\n{\n assertHas(this.assertValues, old);\n assertHas(this.assertValues, neu);\n\n if (this._ !== old)\n return false;\n \n this.set(old, neu);\n \n return true;\n}\nexports.Synchronizer.prototype.setIf = exports.Synchronizer.prototype.testAndSet;\n\n/**\n * Test to see if the synchronizer is in a given state.\n * @param {any} test the state we're checking\n *\n * @returns {boolean} true if synchronizer was in the test state\n */\nexports.Synchronizer.prototype.test = function concurrency$$Synchronizer$test(test) {\n assertHas(this.assertValues, test);\n return test === this._;\n}\nexports.Synchronizer.prototype.is = exports.Synchronizer.prototype.test;\n\nexports.Synchronizer.prototype.isNot = function concurrency$$Synchronizer$isNot(test)\n{\n assertHas(this.assertValues, test);\n return test !== this._;\n}\n\nexports.Synchronizer.prototype[\"in\"] = function concurrency$$Synchronizer$in(list) {\n for (let item of list)\n assertHas(this.assertValues, item);\n\n return list.indexOf(this._) !== -1;\n}\n\n/**\n * `valueOf` functionality for the synchronizer; provides the value of the\n * underlying current state; can be used for equality comparisons, etc.\n */\nexports.Synchronizer.prototype.valueOf = function concurrency$$Synchronizer$valueOf() {\n return this._.valueOf();\n}\nexports.Synchronizer.prototype.toString = exports.Synchronizer.prototype.valueOf;\n\n\n//# sourceURL=webpack://dcp/./src/common/concurrency.js?");
3855
+ eval("/**\n * @file concurrency.js\n * Concurrency primitives for DCP.\n *\n * @author Wes Garland, wes@kingsds.network\n * @date Dec 2020\n */\n\n\nconst inspect = Symbol.for('nodejs.util.inspect.custom');\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst { assert, assertHas } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { setImmediate } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\n\n/**\n * Synchronizer constructor. Instance objects are used to accurately manage \n * transitions between various states. Observing calls to set (possibly via testAndSet\n * or setIf) can allow a developer to develop an accurate state transition diagram.\n *\n * @param {any} initial initial value of the synchronizer\n * @param {Array} assertValues [optional] if present, assert that all set values are in this \n * array when running a debug build\n */\nexports.Synchronizer = function concurrency$$Synchronizer(initial, assertValues)\n{\n this._ = initial; /* current/internal state of the synchronizer is stored on this._ */\n this.lastStack = new Error().stack;\n\n if (!assertValues)\n return;\n\n for (let av of assertValues)\n assert(typeof av !== 'undefined');\n \n this.assertValues = [].concat(assertValues);\n assertHas(this.assertValues, this._);\n}\nexports.Synchronizer.prototype = new EventEmitter('Synchronizer');\nexports.Synchronizer.prototype[inspect] = function ()\n{\n return `[Object Synchronizer <${this._}>]`;\n};\n\n/**\n * Factory function which makes a new Synchronizer that has the same current value as this\n * synchronizer and the same assertValues (if applicable), but not the same event listeners.\n */\nexports.Synchronizer.prototype.duplicate = function concurrency$$Synchronizer$duplicate()\n{\n return new exports.Synchronizer(this._, this.assertValues);\n}\n\n/** \n * Set the synchronizer a given state.\n *\n * @param old The state the synchronizer is currently in\n * @param neu The new state in which to place the synchronizer\n * @throws code DCP_SYNCHRONIZER_ESYNC if the synchronizer was not in the old state\n */\nexports.Synchronizer.prototype.set = function concurrency$$Synchronizer$set(old, neu)\n{\n const that = this;\n assert(typeof old !== 'undefined', typeof neu !== 'undefined');\n \n if (!Array.isArray(old))\n assert(old !== neu);\n else\n {\n let idx = old.indexOf(this._);\n if (idx === -1)\n throwup();\n \n assert(old.indexOf(neu) === -1);\n for (let state of old)\n assertHas(this.assertValues, state);\n\n old = old[idx];\n }\n\n if (this._ !== old)\n throwup();\n\n this._ = neu;\n this.lastStack = new Error().stack;\n assertHas(this.assertValues, this._);\n this.emit('change', this._, old);\n\n function throwup()\n {\n let lastCaller = that.lastStack.split('\\n')[2].replace(/^ *at /, '');\n throw new DCPError(`Cannot transition synchronizer from ${old}->${neu}; was in ${that._} from ${lastCaller}`, 'DCP_SYNCHRONIZER_ESYNC');\n }\n}\n\n/**\n * Returns a promise which resolves when this Synchronizer achieves the desired state, or\n * rejects when the Synchronizer is destroyed.\n *\n * @param {any} which The state we are waiting for\n * @returns {promise}\n */\nexports.Synchronizer.prototype.until = function concurrency$$Synchronizer$until(which)\n{\n var that = this;\n \n function pWrapper(resolve, reject)\n {\n if (that._ === which)\n {\n setImmediate(resolve);\n return;\n }\n\n function changeHandlerOne(newState)\n {\n if (newState !== which || that._ !== which)\n return;\n\n that.removeEventListener('change', changeHandlerOne);\n that.removeEventListener('destroy', destroyHandler);\n resolve();\n }\n\n function changeHandlerMany(newState)\n {\n if (which.indexOf(newState) === -1 && which.indexOf(that._) === -1)\n return;\n\n that.removeEventListener('change', changeHandlerMany);\n that.removeEventListener('destroy', destroyHandler);\n resolve();\n }\n\n function destroyHandler()\n {\n that.removeEventListener(changeHandlerMany);\n that.removeEventListener(changeHandlerOne);\n that.removeEventListener(destroyHandler);\n reject();\n }\n\n that.on('change', Array.isArray(which) ? changeHandlerMany : changeHandlerOne);\n that.on('destroy', destroyHandler);\n }\n\n return new Promise(pWrapper);\n}\n\n/**\n * Destroy a synchronizer. Once this function has been invoked, no further operations on\n * the synchronizer are permitted.\n *\n * Causes subsequent attempts to read or write the interval state to throw code DCP_SYNCHRONIZER_EINVAL .\n */\nexports.Synchronizer.prototype.destroy = function concurrency$$Synchronizer$destroy()\n{\n Object.defineProperty(this, '_', {\n get: () => { throw new DCPError('synchronizer has been destroyed', 'DCP_SYNCHRONIZER_EINVAL'); },\n set: () => { throw new DCPError('synchronizer has been destroyed', 'DCP_SYNCHRONIZER_EINVAL'); },\n\n configurable: false,\n enumerable: false,\n });\n\n this.on('destroy', () => this.removeAllListeners());\n this.emit('destroy');\n}\n\n/** \n * Set the synchronizer instance to a given state, providing it was previously in\n * the supplied 'old' state\n *\n * @param old The state the synchronizer is currently in\n * @param neu The new state in which to place the synchronizer\n *\n * @returns true if the synchronizer state was changed, otherwise false\n */\nexports.Synchronizer.prototype.testAndSet = function concurrency$$Synchronizer$testAndSet(old, neu)\n{\n assertHas(this.assertValues, old);\n assertHas(this.assertValues, neu);\n\n if (this._ !== old)\n return false;\n \n this.set(old, neu);\n \n return true;\n}\nexports.Synchronizer.prototype.setIf = exports.Synchronizer.prototype.testAndSet;\n\n/**\n * Test to see if the synchronizer is in a given state.\n * @param {any} test the state we're checking\n *\n * @returns {boolean} true if synchronizer was in the test state\n */\nexports.Synchronizer.prototype.test = function concurrency$$Synchronizer$test(test) {\n assertHas(this.assertValues, test);\n return test === this._;\n}\nexports.Synchronizer.prototype.is = exports.Synchronizer.prototype.test;\n\nexports.Synchronizer.prototype.isNot = function concurrency$$Synchronizer$isNot(test)\n{\n assertHas(this.assertValues, test);\n return test !== this._;\n}\n\nexports.Synchronizer.prototype[\"in\"] = function concurrency$$Synchronizer$in(list) {\n for (let item of list)\n assertHas(this.assertValues, item);\n\n return list.indexOf(this._) !== -1;\n}\n\n/**\n * `valueOf` functionality for the synchronizer; provides the value of the\n * underlying current state; can be used for equality comparisons, etc.\n */\nexports.Synchronizer.prototype.valueOf = function concurrency$$Synchronizer$valueOf() {\n return this._.valueOf();\n}\nexports.Synchronizer.prototype.toString = exports.Synchronizer.prototype.valueOf;\n\n\n//# sourceURL=webpack://dcp/./src/common/concurrency.js?");
3856
3856
 
3857
3857
  /***/ }),
3858
3858
 
@@ -3995,7 +3995,7 @@ eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_mod
3995
3995
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
3996
3996
 
3997
3997
  "use strict";
3998
- eval("/**\n * @file dcp-url.js Module for working with URLs; primarily intended for\n * use in daemon configuration etc. Works in Node 10 and\n * current browsers. Probably works older Node, too.\n *\n * @group core-api\n * @module dcp-url\n * @author Wes Garland, wes@kingsds.network\n * @date May 2019\n */\n\n\n\nconst { assertIsA } = __webpack_require__(/*! ./dcp-assert */ \"./src/common/dcp-assert.js\");\nconst inspect = Symbol.for('nodejs.util.inspect.custom');\n\n/** Class for working with URLs; primarily intended for use in creating and communicating with\n * DCP daemons; used by the configuration library to implement the url() function.\n *\n * @constructor\n * @param url A string describing a full URL, e.g. \"http://localhost/\". Will be\n * parsed by Node's url.parse().\n * This object can be passed as an options argument to a NodeJS server\n * constructor, or used like 'window.location'.\n *\n * @returns {object} An instance of exports.DcpURL with at least the following properties:\n * <ul>\n * <li> hostname\n * <li> port\n * <li> protocol\n * <li> pathname\n * <li> href\n * <li> origin\n * </ul>\n *//** Class for working with URLs; primarily intended for use in creating and communicating with\n * DCP daemons; used by the configuration library to implement the url() function.\n *\n * @constructor\n * @param {object} An instance of URL, or the return value of Node's url.parse\n * @returns {object} An instance of exports.URL with at least the following properties:\n * <ul>\n * <li> hostname\n * <li> port\n * <li> protocol\n * <li> pathname\n * <li> href\n * <li> origin\n * </ul>\n */\nexports.DcpURL = function dcpUrl$$DcpURL(url, __steal) {\n var backingStore = {};\n var w3cUrl;\n\n if (typeof url === \"object\") {\n w3cUrl = __steal ? url : new URL(url.href);\n } else {\n assertIsA(url, 'string');\n w3cUrl = new URL(url);\n }\n\n function copy(target, source) {\n for (let prop in source) {\n if (typeof source[prop] !== 'undefined' && typeof source[prop] !== 'function')\n target[prop] = source[prop];\n }\n }\n copy(backingStore, w3cUrl);\n\n function munge(recalcProp) {\n /* make use of W3C URL magic getters/setters */\n if (recalcProp) {\n w3cUrl[recalcProp] = backingStore[recalcProp];\n copy(backingStore, w3cUrl);\n }\n\n if (!backingStore.protocol || (!backingStore.hostname && backingStore.protocol !== 'file:' && backingStore.protocol !== 'data:'))\n throw new Error(`Invalid URL: '${w3cUrl}`);\n\n if (backingStore.protocol === 'data:')\n backingStore.origin = null;\n if (backingStore.port)\n backingStore.port = +backingStore.port;\n \n if (!backingStore.port) {\n switch(backingStore.protocol) {\n case 'http:':\n backingStore.port = 80;\n break;\n case 'https:':\n backingStore.port = 443;\n break;\n case 'ftp:':\n backingStore.port = 21;\n break;\n case 'ftps:':\n backingStore.port = 990;\n break;\n }\n } \n\n switch (backingStore.hostname.toLowerCase())\n {\n case '::':\n case 'inaddr_any':\n case 'any/0':\n case 'any':\n backingStore.hostname = 'inaddr_any';\n break;\n } \n }\n munge();\n\n /* @todo - this would be better off solved by a Proxy of URL,\n * but there are enough hairy bits throughout the system\n * that such a refactor is risky without significant\n * testing. In particular, we need to be careful about\n * instanceof DcpURL vs URL, dcpConfig patchups, and\n * maintaining .resolve, data:, and numeric ports. /wg Feb 2021\n */\n for (let p of Object.getOwnPropertyNames(backingStore)) {\n Object.defineProperty(this, p, {\n enumerable: true,\n writeable: true,\n configurable: true,\n get: () => backingStore[p],\n set: (value) => {\n backingStore[p] = value;\n munge(p);\n }\n });\n }\n\n /* Definte searchParams in terms of search in a way that is iterable and mutable */\n delete this.searchParams;\n this.searchParams = decodeSearchParams(this.search);\n\n /* Define a replacement search method which accounts for muteable searchParams */\n delete this.search;\n Object.defineProperty(this, 'search', {\n writeable: false,\n configurable: false,\n enumerable: true,\n get: () => {\n let entries = Object.entries(this.searchParams);\n\n if (entries.length === 0)\n return '';\n\n return '?' + entries.map((kvp) => `${encodeURIComponent(kvp[0])}=${encodeURIComponent(kvp[1])}`).join('&');\n },\n set: (value) => {\n this.searchParams = decodeSearchParams(value);\n },\n });\n\n /* Define a replacement search method which accounts for muteable searchParams */\n delete this.href;\n Object.defineProperty(this, 'href', {\n writeable: false,\n configurable: false,\n enumerable: true,\n get: () => {\n let origin = this.protocol === 'file:' ? 'file://' : this.origin;\n return origin + this.pathname + this.search + this.hash;\n }\n });\n}\n\nexports.DcpURL.prototype[inspect] = function dcpURL$$URL$inspect() {\n return '[object ' + this.constructor.name + ' ' + this.toString() + ']';\n}\n\n/** \n * @returns the full text version of URL \n */\nexports.DcpURL.prototype.toString = function dcpURL$$URL$toString() {\n return this.href\n}\n\n/** \n * @returns the full text version of URL \n */\nexports.DcpURL.prototype.valueOf = function dcpURL$$URL$valueOf() {\n return this.href\n}\n\n/** Form 1: Compare two URLs to see if they match origins per the Same-Origin Policy.\n * @see https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy\n *\n * @param {string} otherURL The URL whose origin we are comparing to this one.\n * It will be parsed using the same rules as strings\n * passed into the exports.URL() constructor.\n *//** Form 2: Compare two URLs to see if they match origins per the Same-Origin Policy.\n * @see https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy\n *\n * @param {object} otherURL The URL whose origin we are comparing to this one.\n * Can be an instance of exports.URL() or a similar\n * object, such as the result of Node url.parse(),\n * browser's window.location, etc.\n */\nexports.DcpURL.prototype.sameOrigin = function dcpURL$$URL$sameOrigin(otherURL) {\n if (typeof otherURL === \"string\")\n otherURL = new (exports.DcpURL)(otherURL)\n\n if (this.protocol === 'file:')\n return false;\n \n return this.protocol === otherURL.protocol && this.hostname === otherURL.hostname && this.port === otherURL.port\n}\n \n/** Resolves a target URL relative to the URL described by this instance in a manner similar to that\n * of a Web browser resolving an anchor tag HREF.\n * \n * @param {string} path The HREF URL being resolved\n * @returns {string} The new URL string\n */\nexports.DcpURL.prototype.resolve = function dcpURL$$URL$resolve(path) {\n return (new URL(path, this.href)).href;\n}\n\n/** Resolves a target URL relative to the URL described by this instance in a manner similar to that\n * of a Web browser resolving an anchor tag HREF.\n * \n * @param {string} path The HREF URL being resolved\n * @returns {string} The new URL as an instance of DcpURL\n */\nexports.DcpURL.prototype.resolveUrl = function dcpURL$$URL$resolveUrl(path) {\n return new exports.DcpURL(new URL(path, this.href), true /* __steal */);\n}\n\nexports.DcpURL.prototype.clone = function dcpURL$$URL$clone(url) {\n return new this.constructor(this.href);\n}\n \n/** Patch an object graph, transforming instances of URL into instances of exports.URL.\n graphs are supported; depth limited to stack size. Edges are composed of\n * own properties.\n * \n * @param obj {object} The object graph to patch\n * @param seen {Array} An array of objects which should not be traversed \n */\nexports.patchup = function dcpURL$URL$patchObject(obj, seen) {\n if (!seen)\n seen = []\n seen.push(obj)\n\n Object.keys(obj).forEach((p) => {\n if ((typeof obj[p] !== 'object') || (obj[p] === null) || seen.indexOf(obj[p]) !== -1)\n return\n \n /**\n * Checking the constructor's name in case the object is an instance of a\n * DcpURL created in a different context, preventing potential `instanceof`\n * checks in the future from failing.\n */\n if (obj[p] instanceof URL || obj[p].constructor.name === 'dcpUrl$$URL')\n obj[p] = new (exports.DcpURL)(obj[p])\n else\n exports.patchup(obj[p], seen)\n })\n}\n\n/** Tests if an object is either a URL object or a DcpURL. \n * \n * @param {object} obj The object to test\n * @returns {boolean} True if the object is a URL or DcpURl, otherwise False\n */\nexports.DcpURL.isURL = function dcpURL$URL$isaURL(obj) {\n if (obj === undefined || obj === null) {\n return false;\n }\n\n return (\n obj instanceof URL ||\n obj instanceof exports.DcpURL\n )\n}\n\n/**\n * Take a url search string and return a searchParams object\n */\nfunction decodeSearchParams(search)\n{\n var searchParams = {};\n \n if (search && search.length)\n {\n for (let kvp of search.slice(1).split('&'))\n {\n let [ key, value ] = kvp.split('=');\n searchParams[key] = value && decodeURIComponent(value.replace(/\\+/g, ' '));\n }\n }\n\n return searchParams;\n}\n\n\n//# sourceURL=webpack://dcp/./src/common/dcp-url.js?");
3998
+ eval("/**\n * @file dcp-url.js Module for working with URLs; primarily intended for\n * use in daemon configuration etc. Works in Node 10 and\n * current browsers. Probably works older Node, too.\n *\n * @group core-api\n * @module dcp-url\n * @author Wes Garland, wes@kingsds.network\n * @date May 2019\n */\n\n\n\nconst { assertIsA } = __webpack_require__(/*! ./dcp-assert */ \"./src/common/dcp-assert.js\");\nconst inspect = Symbol.for('nodejs.util.inspect.custom');\n\n/** Class for working with URLs; primarily intended for use in creating and communicating with\n * DCP daemons; used by the configuration library to implement the url() function.\n *\n * @constructor\n * @param url A string describing a full URL, e.g. \"http://localhost/\". Will be\n * parsed by Node's url.parse().\n * This object can be passed as an options argument to a NodeJS server\n * constructor, or used like 'window.location'.\n *\n * @returns {object} An instance of exports.DcpURL with at least the following properties:\n * <ul>\n * <li> hostname\n * <li> port\n * <li> protocol\n * <li> pathname\n * <li> href\n * <li> origin\n * </ul>\n *//** Class for working with URLs; primarily intended for use in creating and communicating with\n * DCP daemons; used by the configuration library to implement the url() function.\n *\n * @constructor\n * @param {object} An instance of URL, or the return value of Node's url.parse\n * @returns {object} An instance of exports.URL with at least the following properties:\n * <ul>\n * <li> hostname\n * <li> port\n * <li> protocol\n * <li> pathname\n * <li> href\n * <li> origin\n * </ul>\n */\nexports.DcpURL = function dcpUrl$$DcpURL(url, __steal) {\n var backingStore = {};\n var w3cUrl;\n\n if (typeof url === \"object\") {\n w3cUrl = __steal ? url : new URL(url.href);\n } else {\n assertIsA(url, 'string');\n w3cUrl = new URL(url);\n }\n\n function copy(target, source) {\n for (let prop in source) {\n if (typeof source[prop] !== 'undefined' && typeof source[prop] !== 'function')\n target[prop] = source[prop];\n }\n }\n copy(backingStore, w3cUrl);\n\n function munge(recalcProp) {\n /* make use of W3C URL magic getters/setters */\n if (recalcProp) {\n w3cUrl[recalcProp] = backingStore[recalcProp];\n copy(backingStore, w3cUrl);\n }\n\n if (!backingStore.protocol || (!backingStore.hostname && backingStore.protocol !== 'file:' && backingStore.protocol !== 'data:'))\n throw new Error(`Invalid URL: '${w3cUrl}`);\n\n if (backingStore.protocol === 'data:')\n backingStore.origin = null;\n if (backingStore.port)\n backingStore.port = +backingStore.port;\n \n if (!backingStore.port) {\n switch(backingStore.protocol) {\n case 'http:':\n backingStore.port = 80;\n break;\n case 'https:':\n backingStore.port = 443;\n break;\n case 'ftp:':\n backingStore.port = 21;\n break;\n case 'ftps:':\n backingStore.port = 990;\n break;\n }\n } \n\n switch (backingStore.hostname.toLowerCase())\n {\n case '::':\n case 'inaddr_any':\n case 'any/0':\n case 'any':\n backingStore.hostname = 'inaddr_any';\n break;\n } \n }\n munge();\n\n /* @todo - this would be better off solved by a Proxy of URL,\n * but there are enough hairy bits throughout the system\n * that such a refactor is risky without significant\n * testing. In particular, we need to be careful about\n * instanceof DcpURL vs URL, dcpConfig patchups, and\n * maintaining .resolve, data:, and numeric ports. /wg Feb 2021\n */\n for (let p of Object.getOwnPropertyNames(backingStore)) {\n Object.defineProperty(this, p, {\n enumerable: true,\n configurable: true,\n get: () => backingStore[p],\n set: (value) => {\n backingStore[p] = value;\n munge(p);\n }\n });\n }\n\n /* Definte searchParams in terms of search in a way that is iterable and mutable */\n delete this.searchParams;\n this.searchParams = decodeSearchParams(this.search);\n\n /* Define a replacement search method which accounts for muteable searchParams */\n delete this.search;\n Object.defineProperty(this, 'search', {\n configurable: false,\n enumerable: true,\n get: () => {\n let entries = Object.entries(this.searchParams);\n\n if (entries.length === 0)\n return '';\n\n return '?' + entries.map((kvp) => `${encodeURIComponent(kvp[0])}=${encodeURIComponent(kvp[1])}`).join('&');\n },\n set: (value) => {\n this.searchParams = decodeSearchParams(value);\n },\n });\n\n /* Define a replacement search method which accounts for muteable searchParams */\n delete this.href;\n Object.defineProperty(this, 'href', {\n configurable: false,\n enumerable: true,\n get: () => {\n let origin = this.protocol === 'file:' ? 'file://' : this.origin;\n return origin + this.pathname + this.search + this.hash;\n }\n });\n}\n\nexports.DcpURL.prototype[inspect] = function dcpURL$$URL$inspect() {\n return '[object ' + this.constructor.name + ' ' + this.toString() + ']';\n}\n\n/** \n * @returns the full text version of URL \n */\nexports.DcpURL.prototype.toString = function dcpURL$$URL$toString() {\n return this.href\n}\n\n/** \n * @returns the full text version of URL \n */\nexports.DcpURL.prototype.valueOf = function dcpURL$$URL$valueOf() {\n return this.href\n}\n\n/** Form 1: Compare two URLs to see if they match origins per the Same-Origin Policy.\n * @see https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy\n *\n * @param {string} otherURL The URL whose origin we are comparing to this one.\n * It will be parsed using the same rules as strings\n * passed into the exports.URL() constructor.\n *//** Form 2: Compare two URLs to see if they match origins per the Same-Origin Policy.\n * @see https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy\n *\n * @param {object} otherURL The URL whose origin we are comparing to this one.\n * Can be an instance of exports.URL() or a similar\n * object, such as the result of Node url.parse(),\n * browser's window.location, etc.\n */\nexports.DcpURL.prototype.sameOrigin = function dcpURL$$URL$sameOrigin(otherURL) {\n if (typeof otherURL === \"string\")\n otherURL = new (exports.DcpURL)(otherURL)\n\n if (this.protocol === 'file:')\n return false;\n \n return this.protocol === otherURL.protocol && this.hostname === otherURL.hostname && this.port === otherURL.port\n}\n \n/** Resolves a target URL relative to the URL described by this instance in a manner similar to that\n * of a Web browser resolving an anchor tag HREF.\n * \n * @param {string} path The HREF URL being resolved\n * @returns {string} The new URL string\n */\nexports.DcpURL.prototype.resolve = function dcpURL$$URL$resolve(path) {\n return (new URL(path, this.href)).href;\n}\n\n/** Resolves a target URL relative to the URL described by this instance in a manner similar to that\n * of a Web browser resolving an anchor tag HREF.\n * \n * @param {string} path The HREF URL being resolved\n * @returns {string} The new URL as an instance of DcpURL\n */\nexports.DcpURL.prototype.resolveUrl = function dcpURL$$URL$resolveUrl(path) {\n return new exports.DcpURL(new URL(path, this.href), true /* __steal */);\n}\n\nexports.DcpURL.prototype.clone = function dcpURL$$URL$clone(url) {\n return new this.constructor(this.href);\n}\n \n/** Patch an object graph, transforming instances of URL into instances of exports.URL.\n graphs are supported; depth limited to stack size. Edges are composed of\n * own properties.\n * \n * @param obj {object} The object graph to patch\n * @param seen {Array} An array of objects which should not be traversed \n */\nexports.patchup = function dcpURL$URL$patchObject(obj, seen) {\n if (!seen)\n seen = []\n seen.push(obj)\n\n Object.keys(obj).forEach((p) => {\n if ((typeof obj[p] !== 'object') || (obj[p] === null) || seen.indexOf(obj[p]) !== -1)\n return\n \n /**\n * Checking the constructor's name in case the object is an instance of a\n * DcpURL created in a different context, preventing potential `instanceof`\n * checks in the future from failing.\n */\n if (obj[p] instanceof URL || obj[p].constructor.name === 'dcpUrl$$URL')\n obj[p] = new (exports.DcpURL)(obj[p])\n else\n exports.patchup(obj[p], seen)\n })\n}\n\n/** Tests if an object is either a URL object or a DcpURL. \n * \n * @param {object} obj The object to test\n * @returns {boolean} True if the object is a URL or DcpURl, otherwise False\n */\nexports.DcpURL.isURL = function dcpURL$URL$isaURL(obj) {\n if (obj === undefined || obj === null) {\n return false;\n }\n\n return (\n obj instanceof URL ||\n obj instanceof exports.DcpURL\n )\n}\n\n/**\n * Take a url search string and return a searchParams object\n */\nfunction decodeSearchParams(search)\n{\n var searchParams = {};\n \n if (search && search.length)\n {\n for (let kvp of search.slice(1).split('&'))\n {\n let [ key, value ] = kvp.split('=');\n searchParams[key] = value && decodeURIComponent(value.replace(/\\+/g, ' '));\n }\n }\n\n return searchParams;\n}\n\n\n//# sourceURL=webpack://dcp/./src/common/dcp-url.js?");
3999
3999
 
4000
4000
  /***/ }),
4001
4001
 
@@ -4116,7 +4116,7 @@ eval("/**\n * @file passwordCreation.js\n * Modal providing
4116
4116
  \******************************************************/
4117
4117
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4118
4118
 
4119
- eval("/**\n * @file password.js\n * Modal providing a way to enter a password.\n * \n * @author KC Erb - kcerb@kingsds.network\n * @author Nazila Kharazian - nazila@kingsds.network\n * @author Andrew Fryer - andrewf@kingds.network\n * @date Apr 2020\n */\nconst utils = __webpack_require__(/*! ./utils */ \"./src/dcp-client/client-modal/utils.js\");\n\nexports.config = {\n id: 'dcp-modal-password',\n required: ['dcp-modal-password-form', 'dcp-modal-password-hidden-username', 'dcp-modal-password-prompt', 'dcp-modal-password-input'],\n path: \"./templates/password-entry-modal.html\",\n};\n\n/**\n * @param {string} username The hidden username provided to give a hint to password managers.\n * @param {string} label We expect and use the label property to identify which thing this password is associated to.\n * @throws If modal is closed without the form being submitted (submitting the form closes the modal).\n */\nexports.passwordEntry = async function (username, label, maxTries, tryPassphrase, onClose=null) {\n const [elements, formPromise] = await utils.initModal(exports.config, onClose);\n let [modal, form, hiddenUsername, prompt, passwordInput] = elements;\n\n hiddenUsername.value = username;\n prompt.innerText = `Please enter the passphrase for \"${label}\".`;\n\n const validate = function() {\n const passwordIsCorrect = tryPassphrase(passwordInput.value); // This passes the password out of the modal.\n if(passwordIsCorrect) {\n passwordInput.setCustomValidity(\"\");\n } else {\n // This prevents the submit event from firing so that Firefox won't offer to save an incorrect password.\n passwordInput.setCustomValidity(\"Passphrase is incorrect\");\n }\n };\n passwordInput.onchange = validate;\n validate();\n\n const invalidHandler = function(e) {\n // supress tooltip\n e.preventDefault();\n\n prompt.innerText = `Incorrect passphrase for \"${label}\". Please try again.`;\n passwordInput.value = \"\";\n passwordInput.focus();\n };\n passwordInput.oninvalid = invalidHandler;\n\n const submitHandler = function(e) {\n setTimeout(function(){\n history.replaceState(null, document.title); // This should signal to the browser that the login was successful.\n }, 1);\n };\n // Don't overwrite the existing event handler!\n let existingSubmitHandler = form.onsubmit;\n form.onsubmit = function(e) {\n submitHandler(e);\n existingSubmitHandler(e);\n };\n\n passwordInput.focus();\n await formPromise;\n\n // At this point, form validation has ensured that the last pasword entered was correct,\n // and this password was passed out of the modal via tryPassphrase.\n\n utils.MicroModal.close(modal.id);\n}\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/client-modal/passwordEntry.js?");
4119
+ eval("/**\n * @file password.js\n * Modal providing a way to enter a password.\n * \n * @author KC Erb - kcerb@kingsds.network\n * @author Nazila Kharazian - nazila@kingsds.network\n * @author Andrew Fryer - andrewf@kingds.network\n * @date Apr 2020\n */\nconst utils = __webpack_require__(/*! ./utils */ \"./src/dcp-client/client-modal/utils.js\");\n\nexports.config = {\n id: 'dcp-modal-password',\n required: ['dcp-modal-password-form', 'dcp-modal-password-hidden-username', 'dcp-modal-password-prompt', 'dcp-modal-password-input'],\n path: \"./templates/password-entry-modal.html\",\n};\n\n/**\n * @param {string} username The hidden username provided to give a hint to password managers.\n * @param {string} label We expect and use the label property to identify which thing this password is associated to.\n * @throws If modal is closed without the form being submitted (submitting the form closes the modal).\n */\nexports.passwordEntry = async function (username, label, maxTries, tryPassphrase, onClose=null)\n{\n const [elements, formPromise] = await utils.initModal(exports.config, onClose);\n let [modal, form, hiddenUsername, prompt, passwordInput] = elements;\n\n hiddenUsername.value = username;\n prompt.innerText = `Please enter the passphrase for \"${label}\".`;\n\n const submitHandler = function(e)\n {\n setTimeout(function(){\n history.replaceState(null, document.title); // This should signal to the browser that the login was successful.\n }, 1);\n };\n\n // Don't overwrite the existing event handler!\n let existingSubmitHandler = form.onsubmit;\n form.onsubmit = async function(e)\n {\n // waiting for password validation promise to resolve\n const passwordIsValid = await tryPassphrase(passwordInput.value);\n if (passwordIsValid)\n {\n submitHandler(e);\n existingSubmitHandler(e);\n }\n else\n {\n prompt.innerText = `Incorrect passphrase for \"${label}\". Please try again.`;\n passwordInput.focus();\n }\n form.reset();\n };\n \n passwordInput.focus();\n await formPromise;\n\n // At this point, form validation has ensured that the last pasword entered was correct,\n // and this password was passed out of the modal via tryPassphrase.\n\n utils.MicroModal.close(modal.id);\n}\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/client-modal/passwordEntry.js?");
4120
4120
 
4121
4121
  /***/ }),
4122
4122
 
@@ -4126,7 +4126,7 @@ eval("/**\n * @file password.js\n * Modal providing a way to
4126
4126
  \**********************************************/
4127
4127
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4128
4128
 
4129
- 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=fca68c239b7cda20a34513139c096b25d2576370'; /* 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
+ 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=b5864d51c1e7a9b3024698306574b60003a91d62'; /* 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?");
4130
4130
 
4131
4131
  /***/ }),
4132
4132
 
@@ -4137,7 +4137,7 @@ eval("/**\n * @file client-modal/utils.js\n * @author KC Erb\n * @date Mar 2020\
4137
4137
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4138
4138
 
4139
4139
  "use strict";
4140
- 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 * Wes Garland <wes@kingsds.network>\n * Paul <paul@kingsds.network>\n * @date Sept 2020\n * February 2022\n * May 2022\n */\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\");\nconst { DCPError } = __webpack_require__(/*! ../../common/dcp-error */ \"./src/common/dcp-error.js\");\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('scheduler');\nconst { Address } = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst constants = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst { clientError, reconstructServiceError } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n\n/** @typedef {import('dcp/utils').apiServiceType} apiServiceType */\n/** @typedef {import('dcp/utils').apiClientType} apiClientType */\n/** @typedef {string} opaqueId */\n\n/**\n * @typedef {object} cgAccessType\n * @property {opaqueId} [id]\n * @property {string} [joinKey]\n */\n\n/**\n * @typedef {object} cgClientJoinType\n * @property {opaqueId} [id]\n * @property {Address} [joinAddress]\n * @property {string} [joinKey]\n * @property {string} [joinSecret]\n * @property {string} [joinHash]\n */\n\n/**\n * @typedef {object} cgServiceJoinType\n * @property {opaqueId} [id]\n * @property {Address} [joinAddress]\n * @property {string} [joinKey]\n * @property {string} [joinHashHash]\n */\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\nexports.serviceConnection = null;\n\n//\n// Reference counting pattern:\n// For every time addRef is called,\n// closeServiceConnection must eventually be called.\n// Reference counting allows multiple execs in a Promise.all .\n//\nvar refCount = 0;\nexports.addRef = function addRef() {\n refCount++;\n}\n\nconst openAndConnectServiceConn = async function openAndConnectServiceConn()\n{\n exports.serviceConnection = new protocolV4.Connection(dcpConfig.scheduler.services.computeGroups);\n exports.serviceConnection.on('close', openAndConnectServiceConn);\n await exports.serviceConnection.connect();\n refCount = 0; // Help with sanity.\n}\n\n/**\n * Resets the client connection to the computeGroups microservice.\n */\nexports.closeServiceConnection = async function closeServiceConnection() {\n if (refCount > 0) refCount--;\n if (exports.serviceConnection && refCount < 1)\n {\n exports.serviceConnection.off('close', openAndConnectServiceConn);\n exports.serviceConnection.close(null, true);\n refCount = 0; // Help with sanity.\n }\n\n exports.serviceConnection = null;\n};\n\n/**\n * (Used in jobs/index.js)\n * KeepAlive for the service connection to compute groups.\n */\nexports.keepAlive = async function keepAlive() {\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n exports.serviceConnection.keepalive().catch(err => console.error('Warning: keepalive failed for compute groups service', err));\n}\n\n/**\n * Checks whether descriptor corresponds to the public compute group from the scheduler constants.\n */\nexports.isPublicComputeGroup = function isPublicComputeGroup(descriptor) {\n return descriptor.id === constants.computeGroups.public.id\n && descriptor.opaqueId === constants.computeGroups.public.opaqueId;\n};\n\n/**\n * Returns a compute group identification snippet for diagnostic messages,\n * @param {object} descriptor - Must have one of the properties joinKey, id (id:=opaqueId). Specifically\n * descriptor = { joinKey: 'dcpDemo' } or descriptor = { id: 'bYcYGQ3NOpFnP4FKs6IBQd' },\n * where the corresponding row in table computeGroups have attributes\n * joinKey:='dcpDemo' or opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd' .\n * @returns {string}\n */\nfunction cgId(descriptor) {\n return (descriptor.joinKey) ? `joinKey ${descriptor.joinKey}` : `id ${descriptor.id}`;\n}\n\n/**\n * Verify sufficient information in descriptor to access a compute group.\n * Emit diagnostics about unnecessary information.\n * @param {cgAccessType} descriptor \n * @param {string} methodName \n */\nfunction validateCGDescriptor(descriptor, methodName) {\n for (const prop in descriptor) {\n if ([ 'id', 'joinKey' ].includes(prop)) continue;\n if ([ 'joinAddress', 'joinHash', 'joinSecret' ].includes(prop))\n console.warn(`It is not necessary to specify '${prop}' in the descriptor ${JSON.stringify(descriptor)} when calling ${methodName}`);\n else\n console.error(`Do not specify '${prop}' in the descriptor ${JSON.stringify(descriptor)} when calling ${methodName}`);\n }\n}\n\n/**\n * Verify sufficient information in descriptor to authorize a compute group.\n * Emit diagnostics about unnecessary information.\n * @param {cgClientJoinType} joinDescriptor \n * @param {string} methodName \n */\nfunction validateCGJoinDescriptor(joinDescriptor, methodName) {\n for (const prop in joinDescriptor) {\n if ([ 'id', 'joinKey', 'joinSecret', 'joinHash', 'joinAddress' ].includes(prop)) continue;\n console.error(`Do not specify '${prop}' in the descriptor ${JSON.stringify(joinDescriptor)} when calling ${methodName}`);\n }\n}\n\n/**\n * Build message to go across the wire.\n * Verify sufficient information in descriptor to access a compute group.\n * Emit diagnostics about unnecessary information.\n * @param {cgAccessType} descriptor\n * @param {string} methodName\n * @returns {cgAccessType}\n */\nfunction buildCGMessage(descriptor, methodName)\n{\n if (exports.isPublicComputeGroup(descriptor)) return descriptor;\n\n const message = {};\n // Construct message.joinKey xor message.id .\n if (descriptor.joinKey) message.joinKey = descriptor.joinKey;\n else if (descriptor.id) message.id = descriptor.id; // id:=opaqueId\n\n debugging('computeGroups') && console.debug(`${methodName}:buildCGMessage: descriptor`, descriptor, 'message', message);\n\n validateCGDescriptor(descriptor, methodName);\n\n return message;\n}\n\n/**\n * Build message so that joinHash, joinSecret, opaqueId do not go across the wire.\n * Verify sufficient information in descriptor to authorize a compute group.\n * Emit diagnostics about unnecessary information.\n * @param {cgClientJoinType} descriptor\n * @param {string} methodName\n * @returns {cgServiceJoinType}\n */\nfunction buildCGJoinMessage(descriptor, methodName)\n{\n if (exports.isPublicComputeGroup(descriptor)) return descriptor;\n\n const message = {};\n // Construct message.joinKey xor message.id .\n if (descriptor.joinKey) message.joinKey = descriptor.joinKey;\n else if (descriptor.id) message.id = descriptor.id; // id:=opaqueId\n // Construct message.joinAddress .\n if (descriptor.joinAddress) message.joinAddress = descriptor.joinAddress;\n\n debugging('computeGroups') && console.debug(`${methodName}:buildCGJoinMessage: descriptor`, descriptor, 'message', message);\n\n validateCGJoinDescriptor(descriptor, methodName);\n\n // Construct message.joinHashHash .\n if (descriptor.joinSecret) message.joinHashHash = hash.calculate(hash.eh1, exports.calculateJoinHash(descriptor), exports.serviceConnection.dcpsid);\n if (descriptor.joinHash) message.joinHashHash = hash.calculate(hash.eh1, descriptor.joinHash, exports.serviceConnection.dcpsid);\n\n return message;\n}\n\nfunction hasSufficientJoinInfo(joinDescriptor) {\n // Verify joinDescriptor has sufficient information to authorize a compute group (not guarenteed).\n return (joinDescriptor.joinKey && (joinDescriptor.joinSecret || joinDescriptor.joinHash))\n || (joinDescriptor.id && joinDescriptor.joinAddress)\n || exports.isPublicComputeGroup(joinDescriptor);\n}\n\nconst newCGPrototype = { type: 'object',\n parameters: {\n // name: { type: 'string', default: undefined }, /* name of group (length <= 255) */\n // description: { type: 'string', default: undefined }, /* description of group (length <= 255) */\n // id: { type: 'string', default: undefined }, /* opaqueId, the unique identifier of the compute group; nanoid (length === 22) */\n // joinKey: { type: 'string', default: undefined }, /* basically the login (length <= 255) */\n // joinSecret: { type: 'string', default: undefined }, /* basically the password (length <= 255) */\n // joinHash: { type: 'string', default: undefined }, /* basically the password, the joinSecret seeded & hashed */\n // joinAddress: { type: Address, default: undefined }, /* signature gives alternative to login/password */\n\n commissionRate: { type: 'BigNumber', default: undefined }, /* commission, see DCP-1889 */\n deployFee: { type: 'BigNumber', default: undefined }, /* number of DCC to take for every deployment */\n deployAccess: { type: 'string', default: undefined }, /* can be \"owner\"|\"join\" (dcp-1910) */\n addJobFee: { type: 'BigNumber', default: undefined }, /* fee required each time a job joins a compute group */\n maxTotalPayment: { type: 'BigNumber', default: undefined }, /* limit on maximum job payment, NULL => Infinity */\n\n /* Administrative limits on group. NULL => Infinity: Should all be integers or undefined. */\n maxConcurrentJobs: { type: 'number', default: undefined },\n maxConcurrentWorkers: { type: 'number', default: undefined },\n maxConcurrentSandboxes: { type: 'number', default: undefined },\n maxConcurrentCPUs: { type: 'number', default: undefined },\n maxConcurrentGPUs: { type: 'number', default: undefined },\n maxConcurrentEscrow: { type: 'BigNumber', default: undefined },\n },\n};\n\n/**\n * Async function that creates a new Compute Group.\n *\n * The joinDescriptor is of the form { joinKey, joinSecret }, { joinKey, joinHash } or { id, joinAddress }.\n * where id will correspond to the attribute opaqueId in the new row in the computeGroups table.\n *\n * This function can only be called with ADMIN permission.\n * Properties not appearing in newCGPrototype.parameters are not allowed in otherProperties.\n *\n * @param {cgClientJoinType} joinDescriptor - Must have properly defined { joinKey, joinSecret }, { joinKey, joinHash }\n * or { id, joinAddress }, where id will correspond to the attribute opaqueId\n * in the new row in the computeGroups table.\n * @param {string} [name] - The name of the compute group.\n * @param {string} [description] - The description of the compute group.\n * @param {object} [otherProperties] - The 5 attributes of table computeGroup related to commissions and fees.\n * commissionRate: notNull(zDec18), // commission, see DCP-1889\n * deployFee: notNull(zDec18), // number of DCC to take for every deployment\n * deployAccess: string, // can be \"owner\"|\"join\" (dcp-1910)\n * addJobFee: notNull(zDec18), // fee required each time a job joins a compute group\n * maxTotalPayment: dec18, // limit on maximum job payment, NULL => Infinity\n * And the 6 attributes of table computeGroup related to limits.\n * maxConcurrentJobs: integer,\n * maxConcurrentWorkers: integer,\n * maxConcurrentSandboxes: integer,\n * maxConcurrentCPUs: integer,\n * maxConcurrentGPUs: integer,\n * maxConcurrentEscrow: dec18,\n * @returns {Promise<apiClientType>} - { success, payload: computeGroup.id }\n * @access public\n * @example\n * await computeGroup.createGroup({ joinKey: 'dcpDemo', joinSecret: 'theSecret' }, 'myCGName', 'myCGDescription', { deployFee: 0.00015 });\n * await computeGroup.createGroup({ joinKey: 'dcpDemo2', joinHash: 'eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2' });\n * await computeGroup.createGroup({ id: 'bYcYGQ3NOpFnP4FKs6IBQd', joinAddress: 'c15053fc30d4bdf91e2e0bba79578f8b649e55ea' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo',\n * row2: joinKey:='dcpDemo2', joinHash:='eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2'\n * row3: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd', joinAddress:='c15053fc30d4bdf91e2e0bba79578f8b649e55ea' .\n */\nexports.createGroup = async function createGroup(joinDescriptor, name, description, otherProperties)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n validateCGJoinDescriptor(joinDescriptor, 'createGroup');\n\n // Verify joinDescriptor has sufficient information to authorize a compute group (not guarenteed).\n if (!hasSufficientJoinInfo(joinDescriptor))\n return clientError(`createGroup: Insufficient information to authorize compute group: ${JSON.stringify(joinDescriptor)}.`);\n\n // Validate the properties in otherProperties.\n for (const methodName in otherProperties) {\n if (!Object.keys(newCGPrototype.parameters).includes(methodName))\n return clientError(`createGroup: Property ${methodName} cannot be speicfied in otherProperties. Can only specify ${JSON.stringify(Object.keys(newCGPrototype.parameters))}`);\n }\n\n // Translate joinSecret to joinHash.\n if (joinDescriptor.joinSecret) {\n joinDescriptor.joinHash = exports.calculateJoinHash(joinDescriptor);\n delete joinDescriptor.joinSecret;\n }\n\n if (otherProperties && (otherProperties.commissionRate < 0 || otherProperties.commissionRate >= 1))\n return clientError(`client-createGroup: commissionRate ${otherProperties.commissionRate} must be between 0 and 1 (0 <= commissionRate < 1).`);\n\n debugging('computeGroups') && console.debug('client-createGroup: input:', joinDescriptor, name, description, otherProperties);\n\n const { success, payload } = await exports.serviceConnection.send('createGroup', { joinDescriptor, name, description, otherProperties });\n\n if (!success) return clientError(`Cannot create new compute group, with ${cgId(joinDescriptor)}.`);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n debugging('computeGroups') && console.debug('client-createGroup: payload', payload);\n\n return payload;\n};\n\nconst changeCGPrototype = { type: 'object',\n parameters: {\n name: { type: 'string', default: undefined }, /* name of group (length <= 255) */\n description: { type: 'string', default: undefined }, /* description of group (length <= 255) */\n joinHash: { type: 'string', default: undefined }, /* basically the password, seeded & hashed (length <= 255) */\n joinAddress: { type: Address, default: undefined }, /* signature gives alternative to login/password */\n\n commissionRate: { type: 'BigNumber', default: undefined }, /* commission, see DCP-1889 */\n deployFee: { type: 'BigNumber', default: undefined }, /* number of DCC to take for every deployment */\n deployAccess: { type: 'string', default: undefined }, /* can be \"owner\"|\"join\" (dcp-1910) */\n addJobFee: { type: 'BigNumber', default: undefined }, /* fee required each time a job joins a compute group */\n maxTotalPayment: { type: 'BigNumber', default: undefined }, /* limit on maximum job payment, NULL => Infinity */\n\n /* Administrative limits on group. NULL => Infinity: Should all be integers or undefined. */\n maxConcurrentJobs: { type: 'number', default: undefined },\n maxConcurrentWorkers: { type: 'number', default: undefined },\n maxConcurrentSandboxes: { type: 'number', default: undefined },\n maxConcurrentCPUs: { type: 'number', default: undefined },\n maxConcurrentGPUs: { type: 'number', default: undefined },\n maxConcurrentEscrow: { type: 'BigNumber', default: undefined },\n },\n};\n\n/**\n * Async function that changes a new Compute Group.\n * \n * The parameter newDescriptor contains the new property values,\n * and the properties that are allowed to be changed appear in changeCGPrototype.parameters.\n * \n * The descriptor must have joinKey or id, where id:=opaqueId.\n * Must own the compute group or be ADMIN to use changeGroup.\n * \n * @param {cgAccessType} descriptor - Must have joinkey or id, where id:=opaqueId.\n * @param {object} newDescriptor - Properties not appearing in changeCGPrototype.parameters are not allowed.\n * @returns {Promise<apiClientType>}\n * await computeGroup.changeGroup({ joinKey: 'dcpDemo' }, { joinSecret: 'myNewPasswrd' });\n * await computeGroup.changeGroup({ id: 'bYcYGQ3NOpFnP4FKs6IBQd' }, { name: 'myNewName', deployFee: 0.0001 });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo',\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd'\n */\nexports.changeGroup = async function changeGroup(descriptor, newDescriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify descriptor has sufficient information to access a compute group (not guarenteed).\n if (!descriptor.joinKey && !descriptor.id)\n return clientError(`changeGroup: Insufficient information to identify compute group: ${JSON.stringify(descriptor)}.`);\n\n // Validate the properties in newDescriptor.\n for (const methodName in newDescriptor) {\n if (!Object.keys(changeCGPrototype.parameters).includes(methodName))\n return clientError(`changeGroup: Not allowed to change property ${methodName}. Can only change ${JSON.stringify(Object.keys(changeCGPrototype.parameters))}`);\n }\n\n // Translate joinSecret to joinHash.\n if (newDescriptor.joinSecret) {\n newDescriptor.joinHash = exports.calculateJoinHash(newDescriptor);\n delete newDescriptor.joinSecret;\n }\n\n descriptor = buildCGMessage(descriptor, 'changeGroup');\n debugging('computeGroups') && console.debug('change compute group client:', descriptor, newDescriptor);\n const { success, payload } = await exports.serviceConnection.send('changeGroup', { descriptor, newDescriptor });\n\n if (!success) throw new DCPError(`Cannot change compute group with ${cgId(descriptor)}:`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that deletes a compute group.\n * \n * The descriptor must have joinkey or id, where id:=opaqueId.\n * \n * Must either own the group or be ADMIN.\n * If not ADMIN, then the following config must be true:\n * dcpConfig.scheduler.services.computeGroups.usersCanDeleteGroups\n * \n * @param {cgAccessType} descriptor - Must contain joinKey or id (id:=opaqueId) \n * @returns {Promise<apiClientType>}\n * await computeGroup.deleteGroup({ joinKey: 'dcpDemo' });\n * await computeGroup.deleteGroup({ id: 'bYcYGQ3NOpFnP4FKs6IBQd' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo',\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd'\n */\nexports.deleteGroup = async function deleteGroup(descriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify descriptor has sufficient information to access a compute group (not guarenteed).\n if (!descriptor.joinKey && !descriptor.id)\n return clientError(`deleteGroup: Insufficient information to identify compute group: ${JSON.stringify(descriptor)}.`);\n\n descriptor = buildCGMessage(descriptor, 'deleteGroup');\n debugging('computeGroups') && console.debug('delete compute group client:', descriptor);\n const { success, payload } = await exports.serviceConnection.send('deleteGroup', { descriptor });\n\n if (!success) throw new DCPError(`Cannot delete compute group with ${cgId(descriptor)}:`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that adds a job to a specified compute group. \n * \n * Must be the owner of the job.\n *\n * Useful feedback is provided from this function, as it\n * will make its way back to the application developer, *after* they have made the\n * deployment fee micropayment.\n *\n * On the client side the access model in place is that if you know the (user/password)\n * joinKey+joinSecret/joinKey+joinHash/joinKey+joinHashHash/id+joinAddress,\n * you can add the job to the compute groups, where id:=opaqueId from table computeGroups.\n * On the service side the corresponding access model is\n * joinKey+joinHashHash/id+joinAddress .\n * Access is also allowed if the compute group owner is the connection peerAddress.\n * \n * Unless the compute group owner is the connection peerAddress, element of the descriptor array must contain\n * { joinKey, joinSecret }, { joinKey, joinHash } or { id, joinAddress }\n * where the value of id in { id, joinAddress } is the opaqueId attribute of the row in table computeGroups.\n *\n * @param {Address} job The address of the Job that will be added to the Compute Group.\n * @param {Array} computeGroups Array of descriptor objects for the compute groups. This descriptor\n * needs to contain enough information to authorize access to the\n * compute group. Properties may include:\n * - id (id:=opaqueId)\n * - joinKey\n * - joinSecret\n * - joinHash\n * - joinAddress\n * \n * Additional, either the joinKey or 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 are specified, and the message contains valid join permission and the \n * job is owned by the caller of addJobToGroups.\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 * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * await computeGroup.addJobToGroups('P+Y4IApeFQLrYS2W7MkVg7', \n * [ { joinKey: 'dcpDemo', joinSecret: 'theSecret' },\n * { joinKey: 'dcpDemo2', joinHash: 'eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2' }, \n * { id: 'bYcYGQ3NOpFnP4FKs6IBQd', joinAddress: 'c15053fc30d4bdf91e2e0bba79578f8b649e55ea' } ]);\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo',\n * row2: joinKey:='dcpDemo2', joinHash:='eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2'\n * row3: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd', joinAddress:='c15053fc30d4bdf91e2e0bba79578f8b649e55ea' .\n */\nexports.addJobToGroups = async function addJobToGroups(job, computeGroups)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n const cgArray = [];\n for (const joinDescriptor of computeGroups)\n {\n // Verify joinDescriptor has sufficient information to authorize a compute group (not guarenteed).\n if (!hasSufficientJoinInfo(joinDescriptor))\n console.error(`addJobToGroups: Insufficient information to authorize compute group: ${JSON.stringify(joinDescriptor)}.`);\n\n // Translate so that neither joinHash nor joinSecret goes across the wire.\n const message = buildCGJoinMessage(joinDescriptor, 'addJobToGroups');\n debugging('computeGroups') && console.debug(`addJobToGroups client: job ${job}, message`, message);\n\n cgArray.push(message);\n }\n\n const { success, payload } = await exports.serviceConnection.send('addJobToGroups', { job, cgArray });\n\n debugging('computeGroups') && console.debug('addJobToGroups payload', payload);\n\n if (!success) throw new DCPError(`Cannot add job ${job} to compute groups.`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n // If the server reported success but did not return a list of CGs (eg. v.4.2.5 server),\n // assume (and inform the client) we added all groups successfully\n return payload || computeGroups;\n};\n\n/**\n * Async function that lists all the Jobs in a Compute Group.\n * \n * The descriptor must have one of the properties joinkey, id (id:=opaqueId).\n * Must be the owner of the Compute Group to list jobs from it.\n * The job does not need to be owned.\n * \n * The descriptor is of the form { joinKey: 'dcpDemo' } or { id: 'bYcYGQ3NOpFnP4FKs6IBQd' }.\n * where 'bYcYGQ3NOpFnP4FKs6IBQd' is the opaqueId of the Compute Group.\n *\n * @param {cgAccessType} descriptor - Must have one of the properties joinKey, id (id:=opaqueId). Specifically\n * descriptor = { joinKey: 'dcpDemo' } or descriptor = { id: opaqueId }\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * let listOfJobs1 = await computeGroup.listJobs({ joinKey: 'dcpDemo' });\n * let listOfJobs2 = await computeGroup.listJobs({ id: 'bYcYGQ3NOpFnP4FKs6IBQd' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo'\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd'\n */\nexports.listJobs = async function listJobs(descriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify descriptor has sufficient information to access a compute group (not guarenteed).\n if (!descriptor.joinKey && !descriptor.id)\n return clientError(`listJobs: Insufficient information to identify compute group: ${JSON.stringify(descriptor)}.`);\n\n descriptor = buildCGMessage(descriptor, 'listJobs');\n debugging('computeGroups') && console.debug('listJob client: descriptor', descriptor);\n const { success, payload } = await exports.serviceConnection.send('listJobs', { descriptor });\n\n if (!success) throw new DCPError(`Cannot list jobs for compute group with ${cgId(descriptor)}`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that removes a job from a Compute Group.\n * \n * The descriptor must have one of the properties joinkey, id (id:=opaqueId).\n * Must be the owner of the Compute Group to remove a job from it.\n * The job does not need to be owned.\n * \n * The descriptor is of the form { joinKey: 'dcpDemo' } or { id: 'bYcYGQ3NOpFnP4FKs6IBQd' }.\n * where 'bYcYGQ3NOpFnP4FKs6IBQd' is the opaqueId of the Compute Group.\n *\n * @param {Address} job - The address of the Job that will be added to the Compute Group.\n * @param {cgAccessType} descriptor - { joinKey: 'dcpDemo' } or { id: 'bYcYGQ3NOpFnP4FKs6IBQd' }\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * await computeGroup.removeJob( 'P+Y4IApeFQLrYS2W7MkVg7', { joinKey: 'dcpDemo' });\n * await computeGroup.removeJob( 'P+Y4IApeFQLrYS2W7MkVg7', { id: 'bYcYGQ3NOpFnP4FKs6IBQd' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo'\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd'\n */\nexports.removeJob = async function removeJob(job, descriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify descriptor has sufficient information to access a compute group (not guarenteed).\n if (!descriptor.joinKey && !descriptor.id)\n return clientError(`removeJob: Insufficient information to identify compute group: ${JSON.stringify(descriptor)}.`);\n\n descriptor = buildCGMessage(descriptor, 'removeJob');\n debugging('computeGroups') && console.debug(`removeJob client: job ${job}, descriptor`, descriptor);\n const { success, payload } = await exports.serviceConnection.send('removeJob', { job, descriptor });\n\n if (!success) throw new DCPError(`Cannot remove job ${job} from compute group with ${cgId(descriptor)}`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that removes all jobs from a Compute Group.\n * \n * The descriptor must have one of the properties joinkey, id (id:=opaqueId).\n * Must be the owner of the Compute Group to remove jobs from it.\n * \n * The descriptor is of the form { joinKey: 'dcpDemo' } or { id: 'bYcYGQ3NOpFnP4FKs6IBQd' }.\n * where 'bYcYGQ3NOpFnP4FKs6IBQd' is the opaqueId of the Compute Group.\n *\n * @param {cgAccessType} descriptor - { joinKey: 'dcpDemo' } or { id: 'bYcYGQ3NOpFnP4FKs6IBQd' }\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * await computeGroup.removeAllJobs({ joinKey: 'dcpDemo' });\n * await computeGroup.removeAllJobs({ id: 'bYcYGQ3NOpFnP4FKs6IBQd' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo'\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd'\n */\nexports.removeAllJobs = async function removeAllJobs(descriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify descriptor has sufficient information to access a compute group (not guarenteed).\n if (!descriptor.joinKey && !descriptor.id)\n return clientError(`removeAllJobs: Insufficient information to identify compute group: ${JSON.stringify(descriptor)}.`);\n\n descriptor = buildCGMessage(descriptor, 'removeAllJobs');\n debugging('computeGroups') && console.debug('removeAllJobs client: descriptor', descriptor);\n const { success, payload } = await exports.serviceConnection.send('removeAllJobs', { descriptor });\n\n if (!success) throw new DCPError(`Cannot remove all jobs from compute group with ${cgId(descriptor)}:`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that cancels the specified owned job.\n * \n * Must be the owner of the job.\n *\n * On the client side the access model in place is that if you know the (user/password)\n * joinKey+joinSecret/joinKey+joinHash/joinKey+joinHashHash/id+joinAddress,\n * you can cancel the job in the compute group, where id:=opaqueId from table computeGroups.\n * On the service side the corresponding access model is\n * joinKey+joinHashHash/id+joinAddress .\n * Access is also allowed if the compute group owner is the connection peerAddress.\n * \n * Unless the compute group owner is the connection peerAddress, the descriptor must contain\n * { joinKey, joinHashHash } or { id, joinAddress }\n * where the value of id in { id, joinAddress } is the opaqueId attribute of the row in table computeGroups.\n * \n * @param {Address} job - The address of the Job that will be added to the Compute Group.\n * @param {cgClientJoinType} joinDescriptor - Array of descriptor objects for the compute groups. This descriptor\n * needs to contain enough information to authorize access to the\n * compute group. Properties may include:\n * - id (id:=opaqueId)\n * - joinKey\n * - joinSecret\n * - joinHash\n * - joinAddress\n *\n * Additional, either the joinKey or id MUST be specified so\n * that we can identify the compute group in question.\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * await computeGroup.cancelJob( 'P+Y4IApeFQLrYS2W7MkVg7', { joinKey: 'dcpDemo', joinHash: 'eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2' } );\n * await computeGroup.cancelJob( 'P+Y4IApeFQLrYS2W7MkVg7', { id: 'bYcYGQ3NOpFnP4FKs6IBQd', joinAddress: 'c15053fc30d4bdf91e2e0bba79578f8b649e55ea' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo', joinHash:='eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2'\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd', joinAddress:='c15053fc30d4bdf91e2e0bba79578f8b649e55ea' .\n */\nexports.cancelJob = async function cancelJob(job, joinDescriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify joinDescriptor has sufficient information to authorize a compute group (not guarenteed).\n if (!hasSufficientJoinInfo(joinDescriptor))\n return clientError(`cancelJob: Insufficient information to authorize compute group: ${JSON.stringify(joinDescriptor)}.`);\n\n // Translate so that neither joinHash nor joinSecret goes across the wire.\n joinDescriptor = buildCGJoinMessage(joinDescriptor, 'cancelJob');\n debugging('computeGroups') && console.debug(`cancelJob client: job ${job}, descriptor`, joinDescriptor);\n const { success, payload } = await exports.serviceConnection.send('cancelJob', { job, joinDescriptor });\n\n if (!success) throw new DCPError(`Cannot cancel job ${job} for compute group with ${cgId(joinDescriptor)}:`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that cancels the owned job in the Compute Group.\n * \n * On the client side the access model in place is that if you know the (user/password)\n * joinKey+joinSecret/joinKey+joinHash/joinKey+joinHashHash/id+joinAddress,\n * you can cancel the jobs in the compute group, where id:=opaqueId from table computeGroups.\n * On the service side the corresponding access model is\n * joinKey+joinHashHash/id+joinAddress .\n * Access is also allowed if the compute group owner is the connection peerAddress.\n * \n * Unless the compute group owner is the connection peerAddress, the descriptor must contain\n * { joinKey, joinHashHash } or { id, joinAddress }\n * where the value of id in { id, joinAddress } is the opaqueId attribute of the row in table computeGroups.\n * \n * @param {cgClientJoinType} joinDescriptor - Array of descriptor objects for the compute groups. This descriptor\n * needs to contain enough information to authorize access to the\n * compute group. Properties may include:\n * - id (id:=opaqueId)\n * - joinKey\n * - joinSecret\n * - joinHash\n * - joinAddress\n * \n * Additional, either the joinKey or id MUST be specified so\n * that we can identify the compute group in question.\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * await computeGroup.cancelAllJobs( { joinKey: 'dcpDemo', joinHash: 'eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2' } );\n * await computeGroup.cancelAllJobs( { id: 'bYcYGQ3NOpFnP4FKs6IBQd', joinAddress: 'c15053fc30d4bdf91e2e0bba79578f8b649e55ea' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo', joinHash:='eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2'\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd', joinAddress:='c15053fc30d4bdf91e2e0bba79578f8b649e55ea' .\n */\nexports.cancelAllJobs = async function cancelAllJobs(joinDescriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify joinDescriptor has sufficient information to authorize a compute group (not guarenteed).\n if (!hasSufficientJoinInfo(joinDescriptor))\n return clientError(`cancelAllJobs: Insufficient information to authorize compute group: ${JSON.stringify(joinDescriptor)}.`);\n\n // Translate so that neither joinHash nor joinSecret goes across the wire.\n joinDescriptor = buildCGJoinMessage(joinDescriptor, 'cancelAllJobs');\n debugging('computeGroups') && console.debug('cancelAllJobs client: descriptor', joinDescriptor);\n const { success, payload } = await exports.serviceConnection.send('cancelAllJobs', { joinDescriptor });\n\n if (!success) throw new DCPError(`Cannot cancel owned jobs for compute group with ${cgId(joinDescriptor)}:`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\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} ${joinSecret}`);\n}\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/compute-groups/index.js?");
4140
+ 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 * Wes Garland <wes@kingsds.network>\n * Paul <paul@kingsds.network>\n * @date Sept 2020\n * February 2022\n * May 2022\n */\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\");\nconst { DCPError } = __webpack_require__(/*! ../../common/dcp-error */ \"./src/common/dcp-error.js\");\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('scheduler');\nconst { Address } = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst constants = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst { clientError, reconstructServiceError } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n\n/** @typedef {import('dcp/utils').apiServiceType} apiServiceType */\n/** @typedef {import('dcp/utils').apiClientType} apiClientType */\n/** @typedef {string} opaqueId */\n\n/**\n * @typedef {object} cgAccessType\n * @property {opaqueId} [id]\n * @property {string} [joinKey]\n */\n\n/**\n * @typedef {object} cgClientJoinType\n * @property {opaqueId} [id]\n * @property {Address} [joinAddress]\n * @property {string} [joinKey]\n * @property {string} [joinSecret]\n * @property {string} [joinHash]\n */\n\n/**\n * @typedef {object} cgServiceJoinType\n * @property {opaqueId} [id]\n * @property {Address} [joinAddress]\n * @property {string} [joinKey]\n * @property {string} [joinHashHash]\n */\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\nexports.serviceConnection = null;\n\n//\n// Reference counting pattern:\n// For every time addRef is called,\n// closeServiceConnection must eventually be called.\n// Reference counting allows multiple execs in a Promise.all .\n//\nvar refCount = 0;\nexports.addRef = function addRef() {\n refCount++;\n}\n\nconst openAndConnectServiceConn = async function openAndConnectServiceConn()\n{\n exports.serviceConnection = new protocolV4.Connection(dcpConfig.scheduler.services.computeGroups);\n exports.serviceConnection.on('close', openAndConnectServiceConn);\n await exports.serviceConnection.connect();\n refCount = 0; // Help with sanity.\n}\n\n/**\n * Resets the client connection to the computeGroups microservice.\n */\nexports.closeServiceConnection = async function closeServiceConnection() {\n if (refCount > 0) refCount--;\n if (exports.serviceConnection && refCount < 1)\n {\n exports.serviceConnection.off('close', openAndConnectServiceConn);\n exports.serviceConnection.close(null, true);\n refCount = 0; // Help with sanity.\n exports.serviceConnection = null;\n }\n};\n\n/**\n * (Used in jobs/index.js)\n * KeepAlive for the service connection to compute groups.\n */\nexports.keepAlive = async function keepAlive() {\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n exports.serviceConnection.keepalive().catch(err => console.error('Warning: keepalive failed for compute groups service', err));\n}\n\n/**\n * Checks whether descriptor corresponds to the public compute group from the scheduler constants.\n */\nexports.isPublicComputeGroup = function isPublicComputeGroup(descriptor) {\n return descriptor.id === constants.computeGroups.public.id\n && descriptor.opaqueId === constants.computeGroups.public.opaqueId;\n};\n\n/**\n * Returns a compute group identification snippet for diagnostic messages,\n * @param {object} descriptor - Must have one of the properties joinKey, id (id:=opaqueId). Specifically\n * descriptor = { joinKey: 'dcpDemo' } or descriptor = { id: 'bYcYGQ3NOpFnP4FKs6IBQd' },\n * where the corresponding row in table computeGroups have attributes\n * joinKey:='dcpDemo' or opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd' .\n * @returns {string}\n */\nfunction cgId(descriptor) {\n return (descriptor.joinKey) ? `joinKey ${descriptor.joinKey}` : `id ${descriptor.id}`;\n}\n\n/**\n * Verify sufficient information in descriptor to access a compute group.\n * Emit diagnostics about unnecessary information.\n * @param {cgAccessType} descriptor \n * @param {string} methodName \n */\nfunction validateCGDescriptor(descriptor, methodName) {\n for (const prop in descriptor) {\n if ([ 'id', 'joinKey' ].includes(prop)) continue;\n if ([ 'joinAddress', 'joinHash', 'joinSecret' ].includes(prop))\n console.warn(`It is not necessary to specify '${prop}' in the descriptor ${JSON.stringify(descriptor)} when calling ${methodName}`);\n else\n console.error(`Do not specify '${prop}' in the descriptor ${JSON.stringify(descriptor)} when calling ${methodName}`);\n }\n}\n\n/**\n * Verify sufficient information in descriptor to authorize a compute group.\n * Emit diagnostics about unnecessary information.\n * @param {cgClientJoinType} joinDescriptor \n * @param {string} methodName \n */\nfunction validateCGJoinDescriptor(joinDescriptor, methodName) {\n for (const prop in joinDescriptor) {\n if ([ 'id', 'joinKey', 'joinSecret', 'joinHash', 'joinAddress' ].includes(prop)) continue;\n console.error(`Do not specify '${prop}' in the descriptor ${JSON.stringify(joinDescriptor)} when calling ${methodName}`);\n }\n}\n\n/**\n * Build message to go across the wire.\n * Verify sufficient information in descriptor to access a compute group.\n * Emit diagnostics about unnecessary information.\n * @param {cgAccessType} descriptor\n * @param {string} methodName\n * @returns {cgAccessType}\n */\nfunction buildCGMessage(descriptor, methodName)\n{\n if (exports.isPublicComputeGroup(descriptor)) return descriptor;\n\n const message = {};\n // Construct message.joinKey xor message.id .\n if (descriptor.joinKey) message.joinKey = descriptor.joinKey;\n else if (descriptor.id) message.id = descriptor.id; // id:=opaqueId\n\n debugging('computeGroups') && console.debug(`${methodName}:buildCGMessage: descriptor`, descriptor, 'message', message);\n\n validateCGDescriptor(descriptor, methodName);\n\n return message;\n}\n\n/**\n * Build message so that joinHash, joinSecret, opaqueId do not go across the wire.\n * Verify sufficient information in descriptor to authorize a compute group.\n * Emit diagnostics about unnecessary information.\n * @param {cgClientJoinType} descriptor\n * @param {string} methodName\n * @returns {cgServiceJoinType}\n */\nfunction buildCGJoinMessage(descriptor, methodName)\n{\n if (exports.isPublicComputeGroup(descriptor)) return descriptor;\n\n const message = {};\n // Construct message.joinKey xor message.id .\n if (descriptor.joinKey) message.joinKey = descriptor.joinKey;\n else if (descriptor.id) message.id = descriptor.id; // id:=opaqueId\n // Construct message.joinAddress .\n if (descriptor.joinAddress) message.joinAddress = descriptor.joinAddress;\n\n debugging('computeGroups') && console.debug(`${methodName}:buildCGJoinMessage: descriptor`, descriptor, 'message', message);\n\n validateCGJoinDescriptor(descriptor, methodName);\n\n // Construct message.joinHashHash .\n if (descriptor.joinSecret) message.joinHashHash = hash.calculate(hash.eh1, exports.calculateJoinHash(descriptor), exports.serviceConnection.dcpsid);\n if (descriptor.joinHash) message.joinHashHash = hash.calculate(hash.eh1, descriptor.joinHash, exports.serviceConnection.dcpsid);\n\n return message;\n}\n\nfunction hasSufficientJoinInfo(joinDescriptor) {\n // Verify joinDescriptor has sufficient information to authorize a compute group (not guarenteed).\n return (joinDescriptor.joinKey && (joinDescriptor.joinSecret || joinDescriptor.joinHash))\n || (joinDescriptor.id && joinDescriptor.joinAddress)\n || exports.isPublicComputeGroup(joinDescriptor);\n}\n\nconst newCGPrototype = { type: 'object',\n parameters: {\n // name: { type: 'string', default: undefined }, /* name of group (length <= 255) */\n // description: { type: 'string', default: undefined }, /* description of group (length <= 255) */\n // id: { type: 'string', default: undefined }, /* opaqueId, the unique identifier of the compute group; nanoid (length === 22) */\n // joinKey: { type: 'string', default: undefined }, /* basically the login (length <= 255) */\n // joinSecret: { type: 'string', default: undefined }, /* basically the password (length <= 255) */\n // joinHash: { type: 'string', default: undefined }, /* basically the password, the joinSecret seeded & hashed */\n // joinAddress: { type: Address, default: undefined }, /* signature gives alternative to login/password */\n\n commissionRate: { type: 'BigNumber', default: undefined }, /* commission, see DCP-1889 */\n deployFee: { type: 'BigNumber', default: undefined }, /* number of DCC to take for every deployment */\n deployAccess: { type: 'string', default: undefined }, /* can be \"owner\"|\"join\" (dcp-1910) */\n addJobFee: { type: 'BigNumber', default: undefined }, /* fee required each time a job joins a compute group */\n maxTotalPayment: { type: 'BigNumber', default: undefined }, /* limit on maximum job payment, NULL => Infinity */\n\n /* Administrative limits on group. NULL => Infinity: Should all be integers or undefined. */\n maxConcurrentJobs: { type: 'number', default: undefined },\n maxConcurrentWorkers: { type: 'number', default: undefined },\n maxConcurrentSandboxes: { type: 'number', default: undefined },\n maxConcurrentCPUs: { type: 'number', default: undefined },\n maxConcurrentGPUs: { type: 'number', default: undefined },\n maxConcurrentEscrow: { type: 'BigNumber', default: undefined },\n },\n};\n\n/**\n * Async function that creates a new Compute Group.\n *\n * The joinDescriptor is of the form { joinKey, joinSecret }, { joinKey, joinHash } or { id, joinAddress }.\n * where id will correspond to the attribute opaqueId in the new row in the computeGroups table.\n *\n * This function can only be called with ADMIN permission.\n * Properties not appearing in newCGPrototype.parameters are not allowed in otherProperties.\n *\n * @param {cgClientJoinType} joinDescriptor - Must have properly defined { joinKey, joinSecret }, { joinKey, joinHash }\n * or { id, joinAddress }, where id will correspond to the attribute opaqueId\n * in the new row in the computeGroups table.\n * @param {string} [name] - The name of the compute group.\n * @param {string} [description] - The description of the compute group.\n * @param {object} [otherProperties] - The 5 attributes of table computeGroup related to commissions and fees.\n * commissionRate: notNull(zDec18), // commission, see DCP-1889\n * deployFee: notNull(zDec18), // number of DCC to take for every deployment\n * deployAccess: string, // can be \"owner\"|\"join\" (dcp-1910)\n * addJobFee: notNull(zDec18), // fee required each time a job joins a compute group\n * maxTotalPayment: dec18, // limit on maximum job payment, NULL => Infinity\n * And the 6 attributes of table computeGroup related to limits.\n * maxConcurrentJobs: integer,\n * maxConcurrentWorkers: integer,\n * maxConcurrentSandboxes: integer,\n * maxConcurrentCPUs: integer,\n * maxConcurrentGPUs: integer,\n * maxConcurrentEscrow: dec18,\n * @returns {Promise<apiClientType>} - { success, payload: computeGroup.id }\n * @access public\n * @example\n * await computeGroup.createGroup({ joinKey: 'dcpDemo', joinSecret: 'theSecret' }, 'myCGName', 'myCGDescription', { deployFee: 0.00015 });\n * await computeGroup.createGroup({ joinKey: 'dcpDemo2', joinHash: 'eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2' });\n * await computeGroup.createGroup({ id: 'bYcYGQ3NOpFnP4FKs6IBQd', joinAddress: 'c15053fc30d4bdf91e2e0bba79578f8b649e55ea' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo',\n * row2: joinKey:='dcpDemo2', joinHash:='eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2'\n * row3: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd', joinAddress:='c15053fc30d4bdf91e2e0bba79578f8b649e55ea' .\n */\nexports.createGroup = async function createGroup(joinDescriptor, name, description, otherProperties)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n validateCGJoinDescriptor(joinDescriptor, 'createGroup');\n\n // Verify joinDescriptor has sufficient information to authorize a compute group (not guarenteed).\n if (!hasSufficientJoinInfo(joinDescriptor))\n return clientError(`createGroup: Insufficient information to authorize compute group: ${JSON.stringify(joinDescriptor)}.`);\n\n // Validate the properties in otherProperties.\n for (const methodName in otherProperties) {\n if (!Object.keys(newCGPrototype.parameters).includes(methodName))\n return clientError(`createGroup: Property ${methodName} cannot be speicfied in otherProperties. Can only specify ${JSON.stringify(Object.keys(newCGPrototype.parameters))}`);\n }\n\n // Translate joinSecret to joinHash.\n if (joinDescriptor.joinSecret) {\n joinDescriptor.joinHash = exports.calculateJoinHash(joinDescriptor);\n delete joinDescriptor.joinSecret;\n }\n\n if (otherProperties && (otherProperties.commissionRate < 0 || otherProperties.commissionRate >= 1))\n return clientError(`client-createGroup: commissionRate ${otherProperties.commissionRate} must be between 0 and 1 (0 <= commissionRate < 1).`);\n\n debugging('computeGroups') && console.debug('client-createGroup: input:', joinDescriptor, name, description, otherProperties);\n\n const { success, payload } = await exports.serviceConnection.send('createGroup', { joinDescriptor, name, description, otherProperties });\n\n if (!success) return clientError(`Cannot create new compute group, with ${cgId(joinDescriptor)}.`);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n debugging('computeGroups') && console.debug('client-createGroup: payload', payload);\n\n return payload;\n};\n\nconst changeCGPrototype = { type: 'object',\n parameters: {\n name: { type: 'string', default: undefined }, /* name of group (length <= 255) */\n description: { type: 'string', default: undefined }, /* description of group (length <= 255) */\n joinHash: { type: 'string', default: undefined }, /* basically the password, seeded & hashed (length <= 255) */\n joinAddress: { type: Address, default: undefined }, /* signature gives alternative to login/password */\n\n commissionRate: { type: 'BigNumber', default: undefined }, /* commission, see DCP-1889 */\n deployFee: { type: 'BigNumber', default: undefined }, /* number of DCC to take for every deployment */\n deployAccess: { type: 'string', default: undefined }, /* can be \"owner\"|\"join\" (dcp-1910) */\n addJobFee: { type: 'BigNumber', default: undefined }, /* fee required each time a job joins a compute group */\n maxTotalPayment: { type: 'BigNumber', default: undefined }, /* limit on maximum job payment, NULL => Infinity */\n\n /* Administrative limits on group. NULL => Infinity: Should all be integers or undefined. */\n maxConcurrentJobs: { type: 'number', default: undefined },\n maxConcurrentWorkers: { type: 'number', default: undefined },\n maxConcurrentSandboxes: { type: 'number', default: undefined },\n maxConcurrentCPUs: { type: 'number', default: undefined },\n maxConcurrentGPUs: { type: 'number', default: undefined },\n maxConcurrentEscrow: { type: 'BigNumber', default: undefined },\n },\n};\n\n/**\n * Async function that changes a new Compute Group.\n * \n * The parameter newDescriptor contains the new property values,\n * and the properties that are allowed to be changed appear in changeCGPrototype.parameters.\n * \n * The descriptor must have joinKey or id, where id:=opaqueId.\n * Must own the compute group or be ADMIN to use changeGroup.\n * \n * @param {cgAccessType} descriptor - Must have joinkey or id, where id:=opaqueId.\n * @param {object} newDescriptor - Properties not appearing in changeCGPrototype.parameters are not allowed.\n * @returns {Promise<apiClientType>}\n * await computeGroup.changeGroup({ joinKey: 'dcpDemo' }, { joinSecret: 'myNewPasswrd' });\n * await computeGroup.changeGroup({ id: 'bYcYGQ3NOpFnP4FKs6IBQd' }, { name: 'myNewName', deployFee: 0.0001 });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo',\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd'\n */\nexports.changeGroup = async function changeGroup(descriptor, newDescriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify descriptor has sufficient information to access a compute group (not guarenteed).\n if (!descriptor.joinKey && !descriptor.id)\n return clientError(`changeGroup: Insufficient information to identify compute group: ${JSON.stringify(descriptor)}.`);\n\n // Validate the properties in newDescriptor.\n for (const methodName in newDescriptor) {\n if (!Object.keys(changeCGPrototype.parameters).includes(methodName))\n return clientError(`changeGroup: Not allowed to change property ${methodName}. Can only change ${JSON.stringify(Object.keys(changeCGPrototype.parameters))}`);\n }\n\n // Translate joinSecret to joinHash.\n if (newDescriptor.joinSecret) {\n newDescriptor.joinHash = exports.calculateJoinHash(newDescriptor);\n delete newDescriptor.joinSecret;\n }\n\n descriptor = buildCGMessage(descriptor, 'changeGroup');\n debugging('computeGroups') && console.debug('change compute group client:', descriptor, newDescriptor);\n const { success, payload } = await exports.serviceConnection.send('changeGroup', { descriptor, newDescriptor });\n\n if (!success) throw new DCPError(`Cannot change compute group with ${cgId(descriptor)}:`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that deletes a compute group.\n * \n * The descriptor must have joinkey or id, where id:=opaqueId.\n * \n * Must either own the group or be ADMIN.\n * If not ADMIN, then the following config must be true:\n * dcpConfig.scheduler.services.computeGroups.usersCanDeleteGroups\n * \n * @param {cgAccessType} descriptor - Must contain joinKey or id (id:=opaqueId) \n * @returns {Promise<apiClientType>}\n * await computeGroup.deleteGroup({ joinKey: 'dcpDemo' });\n * await computeGroup.deleteGroup({ id: 'bYcYGQ3NOpFnP4FKs6IBQd' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo',\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd'\n */\nexports.deleteGroup = async function deleteGroup(descriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify descriptor has sufficient information to access a compute group (not guarenteed).\n if (!descriptor.joinKey && !descriptor.id)\n return clientError(`deleteGroup: Insufficient information to identify compute group: ${JSON.stringify(descriptor)}.`);\n\n descriptor = buildCGMessage(descriptor, 'deleteGroup');\n debugging('computeGroups') && console.debug('delete compute group client:', descriptor);\n const { success, payload } = await exports.serviceConnection.send('deleteGroup', { descriptor });\n\n if (!success) throw new DCPError(`Cannot delete compute group with ${cgId(descriptor)}:`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that adds a job to a specified compute group. \n * \n * Must be the owner of the job.\n *\n * Useful feedback is provided from this function, as it\n * will make its way back to the application developer, *after* they have made the\n * deployment fee micropayment.\n *\n * On the client side the access model in place is that if you know the (user/password)\n * joinKey+joinSecret/joinKey+joinHash/joinKey+joinHashHash/id+joinAddress,\n * you can add the job to the compute groups, where id:=opaqueId from table computeGroups.\n * On the service side the corresponding access model is\n * joinKey+joinHashHash/id+joinAddress .\n * Access is also allowed if the compute group owner is the connection peerAddress.\n * \n * Unless the compute group owner is the connection peerAddress, element of the descriptor array must contain\n * { joinKey, joinSecret }, { joinKey, joinHash } or { id, joinAddress }\n * where the value of id in { id, joinAddress } is the opaqueId attribute of the row in table computeGroups.\n *\n * @param {Address} job The address of the Job that will be added to the Compute Group.\n * @param {Array} computeGroups Array of descriptor objects for the compute groups. This descriptor\n * needs to contain enough information to authorize access to the\n * compute group. Properties may include:\n * - id (id:=opaqueId)\n * - joinKey\n * - joinSecret\n * - joinHash\n * - joinAddress\n * \n * Additional, either the joinKey or 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 are specified, and the message contains valid join permission and the \n * job is owned by the caller of addJobToGroups.\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 * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * await computeGroup.addJobToGroups('P+Y4IApeFQLrYS2W7MkVg7', \n * [ { joinKey: 'dcpDemo', joinSecret: 'theSecret' },\n * { joinKey: 'dcpDemo2', joinHash: 'eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2' }, \n * { id: 'bYcYGQ3NOpFnP4FKs6IBQd', joinAddress: 'c15053fc30d4bdf91e2e0bba79578f8b649e55ea' } ]);\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo',\n * row2: joinKey:='dcpDemo2', joinHash:='eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2'\n * row3: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd', joinAddress:='c15053fc30d4bdf91e2e0bba79578f8b649e55ea' .\n */\nexports.addJobToGroups = async function addJobToGroups(job, computeGroups)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n const cgArray = [];\n for (const joinDescriptor of computeGroups)\n {\n // Verify joinDescriptor has sufficient information to authorize a compute group (not guarenteed).\n if (!hasSufficientJoinInfo(joinDescriptor))\n console.error(`addJobToGroups: Insufficient information to authorize compute group: ${JSON.stringify(joinDescriptor)}.`);\n\n // Translate so that neither joinHash nor joinSecret goes across the wire.\n const message = buildCGJoinMessage(joinDescriptor, 'addJobToGroups');\n debugging('computeGroups') && console.debug(`addJobToGroups client: job ${job}, message`, message);\n\n cgArray.push(message);\n }\n\n const { success, payload } = await exports.serviceConnection.send('addJobToGroups', { job, cgArray });\n\n debugging('computeGroups') && console.debug('addJobToGroups payload', payload);\n\n if (!success) throw new DCPError(`Cannot add job ${job} to compute groups.`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n // If the server reported success but did not return a list of CGs (eg. v.4.2.5 server),\n // assume (and inform the client) we added all groups successfully\n return payload || computeGroups;\n};\n\n/**\n * Async function that lists all the Jobs in a Compute Group.\n * \n * The descriptor must have one of the properties joinkey, id (id:=opaqueId).\n * Must be the owner of the Compute Group to list jobs from it.\n * The job does not need to be owned.\n * \n * The descriptor is of the form { joinKey: 'dcpDemo' } or { id: 'bYcYGQ3NOpFnP4FKs6IBQd' }.\n * where 'bYcYGQ3NOpFnP4FKs6IBQd' is the opaqueId of the Compute Group.\n *\n * @param {cgAccessType} descriptor - Must have one of the properties joinKey, id (id:=opaqueId). Specifically\n * descriptor = { joinKey: 'dcpDemo' } or descriptor = { id: opaqueId }\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * let listOfJobs1 = await computeGroup.listJobs({ joinKey: 'dcpDemo' });\n * let listOfJobs2 = await computeGroup.listJobs({ id: 'bYcYGQ3NOpFnP4FKs6IBQd' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo'\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd'\n */\nexports.listJobs = async function listJobs(descriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify descriptor has sufficient information to access a compute group (not guarenteed).\n if (!descriptor.joinKey && !descriptor.id)\n return clientError(`listJobs: Insufficient information to identify compute group: ${JSON.stringify(descriptor)}.`);\n\n descriptor = buildCGMessage(descriptor, 'listJobs');\n debugging('computeGroups') && console.debug('listJob client: descriptor', descriptor);\n const { success, payload } = await exports.serviceConnection.send('listJobs', { descriptor });\n\n if (!success) throw new DCPError(`Cannot list jobs for compute group with ${cgId(descriptor)}`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that removes a job from a Compute Group.\n * \n * The descriptor must have one of the properties joinkey, id (id:=opaqueId).\n * Must be the owner of the Compute Group to remove a job from it.\n * The job does not need to be owned.\n * \n * The descriptor is of the form { joinKey: 'dcpDemo' } or { id: 'bYcYGQ3NOpFnP4FKs6IBQd' }.\n * where 'bYcYGQ3NOpFnP4FKs6IBQd' is the opaqueId of the Compute Group.\n *\n * @param {Address} job - The address of the Job that will be added to the Compute Group.\n * @param {cgAccessType} descriptor - { joinKey: 'dcpDemo' } or { id: 'bYcYGQ3NOpFnP4FKs6IBQd' }\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * await computeGroup.removeJob( 'P+Y4IApeFQLrYS2W7MkVg7', { joinKey: 'dcpDemo' });\n * await computeGroup.removeJob( 'P+Y4IApeFQLrYS2W7MkVg7', { id: 'bYcYGQ3NOpFnP4FKs6IBQd' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo'\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd'\n */\nexports.removeJob = async function removeJob(job, descriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify descriptor has sufficient information to access a compute group (not guarenteed).\n if (!descriptor.joinKey && !descriptor.id)\n return clientError(`removeJob: Insufficient information to identify compute group: ${JSON.stringify(descriptor)}.`);\n\n descriptor = buildCGMessage(descriptor, 'removeJob');\n debugging('computeGroups') && console.debug(`removeJob client: job ${job}, descriptor`, descriptor);\n const { success, payload } = await exports.serviceConnection.send('removeJob', { job, descriptor });\n\n if (!success) throw new DCPError(`Cannot remove job ${job} from compute group with ${cgId(descriptor)}`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that removes all jobs from a Compute Group.\n * \n * The descriptor must have one of the properties joinkey, id (id:=opaqueId).\n * Must be the owner of the Compute Group to remove jobs from it.\n * \n * The descriptor is of the form { joinKey: 'dcpDemo' } or { id: 'bYcYGQ3NOpFnP4FKs6IBQd' }.\n * where 'bYcYGQ3NOpFnP4FKs6IBQd' is the opaqueId of the Compute Group.\n *\n * @param {cgAccessType} descriptor - { joinKey: 'dcpDemo' } or { id: 'bYcYGQ3NOpFnP4FKs6IBQd' }\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * await computeGroup.removeAllJobs({ joinKey: 'dcpDemo' });\n * await computeGroup.removeAllJobs({ id: 'bYcYGQ3NOpFnP4FKs6IBQd' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo'\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd'\n */\nexports.removeAllJobs = async function removeAllJobs(descriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify descriptor has sufficient information to access a compute group (not guarenteed).\n if (!descriptor.joinKey && !descriptor.id)\n return clientError(`removeAllJobs: Insufficient information to identify compute group: ${JSON.stringify(descriptor)}.`);\n\n descriptor = buildCGMessage(descriptor, 'removeAllJobs');\n debugging('computeGroups') && console.debug('removeAllJobs client: descriptor', descriptor);\n const { success, payload } = await exports.serviceConnection.send('removeAllJobs', { descriptor });\n\n if (!success) throw new DCPError(`Cannot remove all jobs from compute group with ${cgId(descriptor)}:`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that cancels the specified owned job.\n * \n * Must be the owner of the job.\n *\n * On the client side the access model in place is that if you know the (user/password)\n * joinKey+joinSecret/joinKey+joinHash/joinKey+joinHashHash/id+joinAddress,\n * you can cancel the job in the compute group, where id:=opaqueId from table computeGroups.\n * On the service side the corresponding access model is\n * joinKey+joinHashHash/id+joinAddress .\n * Access is also allowed if the compute group owner is the connection peerAddress.\n * \n * Unless the compute group owner is the connection peerAddress, the descriptor must contain\n * { joinKey, joinHashHash } or { id, joinAddress }\n * where the value of id in { id, joinAddress } is the opaqueId attribute of the row in table computeGroups.\n * \n * @param {Address} job - The address of the Job that will be added to the Compute Group.\n * @param {cgClientJoinType} joinDescriptor - Array of descriptor objects for the compute groups. This descriptor\n * needs to contain enough information to authorize access to the\n * compute group. Properties may include:\n * - id (id:=opaqueId)\n * - joinKey\n * - joinSecret\n * - joinHash\n * - joinAddress\n *\n * Additional, either the joinKey or id MUST be specified so\n * that we can identify the compute group in question.\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * await computeGroup.cancelJob( 'P+Y4IApeFQLrYS2W7MkVg7', { joinKey: 'dcpDemo', joinHash: 'eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2' } );\n * await computeGroup.cancelJob( 'P+Y4IApeFQLrYS2W7MkVg7', { id: 'bYcYGQ3NOpFnP4FKs6IBQd', joinAddress: 'c15053fc30d4bdf91e2e0bba79578f8b649e55ea' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo', joinHash:='eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2'\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd', joinAddress:='c15053fc30d4bdf91e2e0bba79578f8b649e55ea' .\n */\nexports.cancelJob = async function cancelJob(job, joinDescriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify joinDescriptor has sufficient information to authorize a compute group (not guarenteed).\n if (!hasSufficientJoinInfo(joinDescriptor))\n return clientError(`cancelJob: Insufficient information to authorize compute group: ${JSON.stringify(joinDescriptor)}.`);\n\n // Translate so that neither joinHash nor joinSecret goes across the wire.\n joinDescriptor = buildCGJoinMessage(joinDescriptor, 'cancelJob');\n debugging('computeGroups') && console.debug(`cancelJob client: job ${job}, descriptor`, joinDescriptor);\n const { success, payload } = await exports.serviceConnection.send('cancelJob', { job, joinDescriptor });\n\n if (!success) throw new DCPError(`Cannot cancel job ${job} for compute group with ${cgId(joinDescriptor)}:`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\n};\n\n/**\n * Async function that cancels the owned job in the Compute Group.\n * \n * On the client side the access model in place is that if you know the (user/password)\n * joinKey+joinSecret/joinKey+joinHash/joinKey+joinHashHash/id+joinAddress,\n * you can cancel the jobs in the compute group, where id:=opaqueId from table computeGroups.\n * On the service side the corresponding access model is\n * joinKey+joinHashHash/id+joinAddress .\n * Access is also allowed if the compute group owner is the connection peerAddress.\n * \n * Unless the compute group owner is the connection peerAddress, the descriptor must contain\n * { joinKey, joinHashHash } or { id, joinAddress }\n * where the value of id in { id, joinAddress } is the opaqueId attribute of the row in table computeGroups.\n * \n * @param {cgClientJoinType} joinDescriptor - Array of descriptor objects for the compute groups. This descriptor\n * needs to contain enough information to authorize access to the\n * compute group. Properties may include:\n * - id (id:=opaqueId)\n * - joinKey\n * - joinSecret\n * - joinHash\n * - joinAddress\n * \n * Additional, either the joinKey or id MUST be specified so\n * that we can identify the compute group in question.\n * @returns {Promise<apiClientType>}\n * @access public\n * @example\n * await computeGroup.cancelAllJobs( { joinKey: 'dcpDemo', joinHash: 'eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2' } );\n * await computeGroup.cancelAllJobs( { id: 'bYcYGQ3NOpFnP4FKs6IBQd', joinAddress: 'c15053fc30d4bdf91e2e0bba79578f8b649e55ea' });\n * where the corresponding rows in table computeGroups have attributes\n * row1: joinKey:='dcpDemo', joinHash:='eh1-e063976b20a92da97a27b9873465c6f2c9d6e4370befa86c8c1dd312c78befc2'\n * row2: opaqueId:='bYcYGQ3NOpFnP4FKs6IBQd', joinAddress:='c15053fc30d4bdf91e2e0bba79578f8b649e55ea' .\n */\nexports.cancelAllJobs = async function cancelAllJobs(joinDescriptor)\n{\n if (!exports.serviceConnection)\n await openAndConnectServiceConn();\n\n // Verify joinDescriptor has sufficient information to authorize a compute group (not guarenteed).\n if (!hasSufficientJoinInfo(joinDescriptor))\n return clientError(`cancelAllJobs: Insufficient information to authorize compute group: ${JSON.stringify(joinDescriptor)}.`);\n\n // Translate so that neither joinHash nor joinSecret goes across the wire.\n joinDescriptor = buildCGJoinMessage(joinDescriptor, 'cancelAllJobs');\n debugging('computeGroups') && console.debug('cancelAllJobs client: descriptor', joinDescriptor);\n const { success, payload } = await exports.serviceConnection.send('cancelAllJobs', { joinDescriptor });\n\n if (!success) throw new DCPError(`Cannot cancel owned jobs for compute group with ${cgId(joinDescriptor)}:`, payload);\n if (payload && !payload.success) return reconstructServiceError(payload);\n\n return payload;\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} ${joinSecret}`);\n}\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/compute-groups/index.js?");
4141
4141
 
4142
4142
  /***/ }),
4143
4143
 
@@ -4147,7 +4147,7 @@ eval("/**\n * @file Client facing module that implements Compute Groups API\n
4147
4147
  \***********************************/
4148
4148
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4149
4149
 
4150
- eval("/**\n * @file Module that implements Compute API\n * @module dcp/compute\n * @access public\n */\n\n/* global dcpConfig */\n\n/**\n * @access private - Will change soon!\n * @typedef {object} JobRequirements\n * @property {object} environment\n * @property {boolean} environment.fdlibm - Workers express capability 'fdlibm' in order for client applications to have confidence that results will be bitwise identical across workers. This library is recommended, but not required, for implementaiton of Google chrome, Mozilla Firefox and the DCP standalone worker use fdlibm but Microsoft Edge and Apple Safari do not use it.\n * @property {boolean} environment.offscreenCanvas When present, this capability indicates that the worker environment has the OffscreenCanvas symbol defined.\n * @property {object} details\n * @property {object} details.offscreenCanvas\n * @property {boolean} details.offscreenCanvas.bigTexture4096 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 4096.\n * @property {boolean} details.offscreenCanvas.bigTexture8192 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 8192.\n * @property {boolean} details.offscreenCanvas.bigTexture16384 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 16384.\n * @property {boolean} details.offscreenCanvas.bigTexture32768 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 32768.\n * @property {object} engine\n * @property {boolean} engine.es7 This capability enforces that the worker is running an es7-compliant JavaScript engine.\n * @property {boolean} engine.spidermonkey The capability enforces that the worker will run in the SpiderMonkey JS engine.\n * @property {boolean} browser.chrome The capability to exclude Chrome browsers.\n */\n\n/**\n * This JSDoc 'namespace' is a convenient way to document some globals that are available only in\n * the sandbox environment provided to work functions.\n * @access public\n * @namespace sandboxEnv\n */\n\n/**\n * This function emits a progress event. Progress events should be emitted approximately once per second; a task which fails to emit a \n * progress event for a certain period of time will be cancelled by the supervisor. The argument to this function is interpreted to six \n * significant digits, and must increase for every call. *All work functions must emit at least one progress event* - this requirement \n * will be enforced by the estimator. * The period of time mentioned above will be at least 30 wall-clock seconds and at least 30 \n * benchmark-adjusted seconds\n * @access public\n * @method progress\n * @param {string|number|undefined} n a number between 0 and 1 (inclusive) that represents a best-guess \n * at the completed portion of the task as a ratio of completed work to total work. If the argument is a string ending in the `%` symbol, \n * it will be interpreted as a number in the usual mathematical sense. If the argument is `undefined`, the slice progress is noted but \n * the amount of progress is considered to be indeterminate.\n * @memberof module:dcp/compute~sandboxEnv\n * @returns {boolean} true\n */\n\n/**\n * Emit a `console` event to the `JobHandle` in the client. If the sandbox is running in an environment with a native console object, the native method may also be invoked.\n * \n * @member console\n * @property {function} log fire console event with 'log' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} debug fire console event with 'debug' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} info fire console event with 'info' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} warn fire console event with 'warn' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} error fire console event with 'error' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * \n * **Note** some ES environments (Chrome, Firefox) implement C-style print formatting in this method. This is currently not supported.\n * @memberof module:dcp/compute~sandboxEnv\n * @emits Job#console\n * @access public\n */\n\n/**\n * @member work An object that contains the following properties\n * @property {function} emit (eventName, ...) - send arbitrary args with `eventName`. This event is received client-side with those args (see {@link Job#work}).\n * @property {object} job\n * @property {object} job.public Property set on job handle, see {@link Job#public}.\n * @memberof module:dcp/compute~sandboxEnv\n * @access public\n */\n\n/**\n * @member {OffscreenCanvas} OffscreenCanvas As defined in the {@link https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas|HTML Standard}, provides a canvas which can be rendered off-screen. If this interface is not available in a given worker, the worker will not report capability \"offscreenCanvas\".\n * @memberof module:dcp/compute~sandboxEnv\n * @access public\n */\n\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events/event-emitter */ \"./src/common/dcp-events/event-emitter.js\");\nconst { RangeObject, rehydrateRange } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst { RemoteDataSet } = __webpack_require__(/*! dcp/dcp-client/remote-data-set */ \"./src/dcp-client/remote-data-set.js\");\nconst { RemoteDataPattern } = __webpack_require__(/*! dcp/dcp-client/remote-data-pattern */ \"./src/dcp-client/remote-data-pattern.js\");\nconst { Job, ResultHandle } = __webpack_require__(/*! dcp/dcp-client/job */ \"./src/dcp-client/job/index.js\");\nconst { jobStatuses } = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { disableWorker } = __webpack_require__(/*! dcp/dcp-client/worker */ \"./src/dcp-client/worker/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('dcp-client');\n\nconst debug = (...args) => {\n if (debugging()) {\n args.unshift('dcp-client:compute');\n console.debug(...args);\n }\n};\n\nconst MARKET_VALUE = Symbol('Market Value');\nconst indirectEval = eval; // eslint-disable-line no-eval\n\n/**\n * Instance methods of this class are exported as static members of {@link module:dcp/compute|dcp/compute}.\n * @example\n * let job = compute.for(1, 3, function (i) {\n * progress('100%');\n * return i*10;\n * })\n * let results = await job.exec();\n */\nclass Compute extends EventEmitter {\n constructor () {\n super('Compute');\n\n /** @todo move these into some central config */\n this.defaultMaxGPUs = 1;\n this.GPUsAssigned = 0;\n this.allowSlicesToFinishBeforeStopping = false;\n\n this.schedulerURL = undefined; // Will default to dcpConfig.scheduler.location if not overidden\n this.bankURL = undefined; // Will default to dcpConfig.bank.services.bankTeller.location if not overidden\n\n Object.defineProperty(this.marketValue, MARKET_VALUE, {\n value: true,\n writable: false,\n configurable: false,\n enumerable: false,\n });\n\n const ceci = this;\n this.RemoteDataSet = RemoteDataSet;\n this.RemoteDataPattern = RemoteDataPattern;\n // Initialize to null so these properties are recognized for the Job class\n this.phemeConnection = null;\n this.phemeUses = 0;\n this.deployConnection = null;\n this.openPhemeConn = function openPhemeConn()\n {\n ceci.phemeConnection = new protocolV4.Connection(dcpConfig.scheduler.services.pheme);\n ceci.phemeConnection.on('close', ceci.openPhemeConn);\n }\n\n this.openDeployConn = function openDeployConn()\n {\n ceci.deployConnection = new protocolV4.Connection(dcpConfig.scheduler.services.jobSubmit);\n ceci.deployConnection.on('close', ceci.openDeployConn);\n }\n }\n\n get defaultMaxWorkers () {\n return portalWorker.supervisor.maxWorkingSandboxes\n }\n\n set defaultMaxWorkers (value) {\n portalWorker.supervisor.maxWorkingSandboxes = value\n }\n\n get paymentAddress () {\n return portalWorker.supervisor.paymentAddress\n }\n\n set paymentAddress (value) {\n portalWorker.supervisor.paymentAddress = value\n }\n\n get mining () {\n return portalWorker.working\n }\n\n /** Return a special object that represents the current market value\n * @param {number} factor - OPTIONAL multiplier. default: 1.0\n */\n marketValue (factor = 1) {\n return {\n [MARKET_VALUE]: factor\n };\n }\n\n /** Describes the payment required to compute such a slice on a worker\n * or in a market working at the rates described in the WorkValue object.\n * This function does not take into account job-related overhead.\n * \n * @param {object} sliceProfile A SliceProfile-shaped object\n * @param {object} workValue A WorkValue-shaped object\n * @returns {number} Payment value in DCC describing rate to compute slice\n */\n calculateSlicePayment(sliceProfile, workValue) {\n return sliceProfile.cpuHours * workValue.cpuHour\n + sliceProfile.gpuHours * workValue.gpuHour\n + sliceProfile.inputMBytes * workValue.inputMByte\n + sliceProfile.outputMBytes * workValue.outputMByte;\n }\n\n \n /**\n * Deprecated `mine` API. Use `work` instead\n */\n async mine (numberOfCores, paymentAddress = this.paymentAddress) {\n return this.work(numberOfCores, paymentAddress);\n }\n\n /**\n * Instruct the Worker to stop working, then emit workerInactive\n */\n async stopWorking() {\n console.warn('Using the Compute API for controlling workers is deprecated. Use the Worker API instead.')\n await portalWorker.stop(!this.allowSlicesToFinishBeforeStopping)\n }\n\n /**\n * Deprecated `stopMining` API. Use `stopWorking` instead\n */\n stopMining () {\n console.log(\"Warning: used deprecated 'stopMining' API\");\n this.stopWorking();\n }\n \n /**\n * Flag worker as disabled\n */\n disableWorker() {\n disableWorker();\n }\n\n /**\n * Deprecated `disableMiner` API. Use `disableWorker` instead\n */\n disableMiner() {\n this.disableWorker();\n }\n\n /**\n * Experimental replacement for mine() /* wg aug 2019\n * @param {number} [numberOfCores = this.defaultMaxWorkers] - Number of parallel processes to run\n * @param {string} [paymentAddress = this.paymentAddress || wallet.get().address] - The address of the account to work for\n */\n async work(numCores, paymentAddress) {\n console.warn('Using the Compute API for controlling workers is deprecated. Use the Worker API instead.');\n\n portalWorker.supervisor.paymentAddress = paymentAddress || this.paymentAddress || (await wallet.get()).address;\n portalWorker.supervisor.maxWorkingSandboxes = numCores || this.defaultMaxWorkers || 1;\n await portalWorker.start();\n\n return portalWorker;\n }\n\n /**\n * Form 1. Create a Job for the distributed computer by specifying the start,\n * end, and step for a RangeObject with a given work function and arguments.\n *\n * @param {number} start First number in a range (see {@link RangeObject})\n * @param {number} end Last number in a range (see {@link RangeObject})\n * @param {number} [step=1] the space between numbers in a range (see\n * {@link RangeObject})\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * let job = compute.for(1, 3, function (i) {\n * progress('100%')\n * return i*10\n * })\n */\n /**\n * Form 2.a Create a Job for the distributed computer by specifying the input\n * as a RemoteDataSet with a given work function and arguments.\n *\n * @param {RemoteDataSet} remoteDataSet Array, ES6 function* generator, or any\n * other type of iterable object.\n * \n * Form 2.b Create a Job for the distributed computer by specifying the input\n * as a RemoteDataPattern with a given work function and arguments.\n * @param {RemoteDataPattern} RemoteDataPattern Array, ES6 function* generator, or any\n * other type of iterable object.\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * const remoteData = ['https://example.com/data'];\n * const job = compute.for(new RemoteDataSet(...remoteData), function(i) {\n * progress(1)\n * return i\n * })\n * \n * * @example\n * const pattern = 'https://example.com/data/{slice}.json';\n * const NumberOfSlices = 5;\n * const job = compute.for(new RemoteDataSet(pattern, NumberOfSlices), function(i) {\n * progress(1)\n * return i\n * })\n */\n /**\n * Form 3. Create a Job for the distributed computer by specifying the input\n * as an Iterable object with a given work function and arguments.\n *\n * @param {Iterable} iterableObject Array, ES6 function* generator, or any\n * other type of iterable object.\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * let job = compute.for([123,456], function(i) {\n * progress(1)\n * return i\n * })\n */\n /**\n * Form 4. Create a Job for the distributed computer by specifying the input\n * as a RangeObject with a given work function and arguments.\n *\n * @param {RangeObject} rangeObject Either a RangeObject or an object literal\n * which can be used to create a RangeObject (see {@link RangeObject}).\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * let job = compute.for({start: 10, end: 13, step: 2}, (i) => progress(1) && i)\n */\n /**\n * Form 5. Similar to Form 4, except with a multi range object containing an\n * array of range objects in the key ranges. These are used to create\n * multi-dimensional ranges, like nested loops. If they were written as\n * traditional loops, the outermost loop would be the leftmost range object,\n * and the innermost loop would be the rightmost range object.\n *\n * @param {MultiRangeObject} multiRange Either a MultiRangeObject or any valid\n * argument to the constructor.\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * let job = compute.for([{start: 1, end: 2}, {start: 3, end: 5}], function(i,j) {\n * return [i, j, i*j]\n * })\n */\n for(...args) {\n // args, or work function\n const lastArg = args[args.length - 1];\n // work function, if args provided\n const secondLastArg = args[args.length - 2];\n let range = null;\n let work;\n let genArgs;\n\n // All forms: extract work and optional args from the end and validate it.\n if (isValidWorkFunctionType(secondLastArg)) {\n // for(..., workFunction, arguments)\n work = secondLastArg\n if (lastArg && Array.from(lastArg).length > 0 || typeof lastArg === 'string') {\n // array or string, use as-is (string should be a URI)\n genArgs = lastArg;\n } else if (lastArg === undefined) {\n genArgs = [];\n } else {\n throw new TypeError(\n 'Work function arguments (the last parameter), should be an array.',\n );\n }\n } else if (isValidWorkFunctionType(lastArg)) {\n // for(..., workFunction)\n work = lastArg;\n genArgs = [];\n }\n\n if (!isWorkFunctionValid(work)) {\n throw new TypeError(\n `Work parameter must be a function or a string that evaluates to a function, or a URL that points to the location of a function.\\nReceived type ${typeof work} (${work})`,\n );\n }\n\n // Extract the proper information from every form.\n const [firstArg, secondArg, thirdArg] = args;\n if (['object', 'string'].includes(typeof firstArg)) {\n // Forms 2 & 3 (data is iterable, eg. an Array or RemoteDataSet)\n // Forms 4 & 5 (data is RangeObject & Friends)\n range = rehydrateRange(firstArg);\n } else if (typeof firstArg === 'number' && typeof secondArg === 'number') {\n // Form 1 (data is start, end[, step])\n const start = firstArg;\n const end = secondArg;\n\n /**\n * If there is an argument between the end and the work fn, use it as step\n * otherwise set step to undefined.\n */\n const step = typeof thirdArg === 'number' ? thirdArg : undefined;\n range = new RangeObject(start, end, step);\n } else {\n throw new TypeError(\n 'Parameters do not match one of the accepted call forms.',\n );\n }\n\n if (range.length === 0) {\n throw new Error('Length of input set must be greater than zero.');\n }\n\n debug('Work:', work);\n debug('Input Set:', range);\n debug('Arguments:', genArgs);\n return new Job(work, range, genArgs);\n\n /**\n * @param {any} workFunction\n */\n function isValidWorkFunctionType(workFunction) {\n return (\n typeof workFunction === 'function' ||\n typeof workFunction === 'string' ||\n DcpURL.isURL(workFunction)\n );\n }\n\n /**\n * Checks if the workFunction can be evaluated to a function or if it's\n * protocol is HTTPS,\n * @param {function | string | URL | DcpURL} workFunction\n */\n function isWorkFunctionValid(workFunction) {\n switch (typeof workFunction) {\n case 'function':\n return true;\n case 'string': {\n return isValidFunction(workFunction);\n }\n case 'object':\n return DcpURL.isURL(workFunction);\n default:\n return false;\n }\n }\n\n /**\n * Evaluates to true if the string can be coerced into a javascript\n * function.\n * @param {string} workFunction\n */\n function isValidFunction(workFunction) {\n try {\n const fn = indirectEval(`(${workFunction})`);\n return typeof fn === 'function';\n } catch (e) {\n return false;\n }\n }\n }\n\n /**\n * Form 1.\n * Create a job which will run a work function once\n *\n * @param {function | string | URL | DcpURL} work Work function as string or function literal.\n * @param {object} [workFunctionArgs=[]] optional Array-like object which contains arguments which are passed to the work func\n * @returns {Job}\n * @access public\n * @example\n * const job = compute.do(function (i) {\n * progress('100%')\n * return i*10\n * })\n */\n /**\n * Form 2.\n * Create a job which will run a work function on the values 1..n\n *\n * @param {number} n Number of times to run the work function.\n * @param {function | string | URL | DcpURL} work Work function as string or function literal.\n * @param {object} [workFunctionArgs=[]] optional Array-like object which contains arguments which are passed to the work func\n * @returns {Job}\n * @access public\n * @example\n * const job = compute.do(5, function (i) {\n * progress('100%')\n * return i*10\n * })\n */\n do(...args) {\n let n;\n let work;\n let workFunctionArgs;\n if (typeof args[0] !== 'number') {\n // Form 1.\n n = 1;\n [work, workFunctionArgs] = args;\n } else {\n // Form 2.\n [n, work, workFunctionArgs] = args;\n }\n return this.for(0, n - 1, work, workFunctionArgs);\n }\n\n /** Cancel a running job, by opaque id\n *\n * @param {string} id Address of the job to cancel.\n * @param {wallet.Keystore} ownerKeystore The keystore of the job owner\n * @param {string} reason If provided, will be sent on to client\n * @returns {object} The status of the funds that were released from escrow.\n * @throws when id or ownerKeystore are not provided.\n * @access public\n */\n async cancel (id, ownerKeystore, reason = '') {\n if (typeof id !== 'string') {\n throw new Error(`compute.cancel: Job ID must be a string, not ${typeof id}.`);\n }\n\n if (!(ownerKeystore instanceof wallet.Keystore)) {\n throw new Error('compute.cancel: ownerKeystore must be instance of wallet.Keystore');\n }\n\n if (!this.deployConnection)\n this.openDeployConn();\n\n const response = await this.deployConnection.send('cancelJob', {\n job: id,\n owner: ownerKeystore.address,\n reason,\n }, ownerKeystore);\n\n this.deployConnection.off('close', this.deployConnection)\n await this.deployConnection.close();\n\n return response.payload;\n }\n\n /**\n * Reconnect/resume a job, by opaque id\n *\n * @param {string} id Opaque id of the Job to resume/reconnect.\n * @param {wallet.Keystore} [ownerKeystore=wallet.get()] The keystore of the job owner\n * @returns {Job}\n * @throws when id is not provided.\n * @access public\n */\n async resume (id, ownerKeystore) {\n if (typeof id !== 'string') {\n throw new Error(`compute.resume: Job ID must be a string, not ${typeof id}.`);\n }\n\n if (!(ownerKeystore instanceof wallet.Keystore)) {\n ownerKeystore = await wallet.get();\n }\n\n const response = await this.schedulerConnection.send('fetchJob', {\n job: id, // XXX@TODO change 'address' to 'job' once we port this operation to v4\n owner: ownerKeystore.address,\n }, ownerKeystore);\n\n const job = new Job(response.payload);\n job.setPaymentAccountKeystore(ownerKeystore);\n return job;\n }\n\n /**\n * Form 1. Query the status of a job that has been deployed to the scheduler.\n *\n * @access public\n * @param {string|Job} job A job ID or a Job instance\n * @param {wallet.Keystore} ownerKeystore The identity keystore of the owner\n * of the job\n * @throws when ownerKeystore is not provided\n * @returns {Promise<object>} A status object describing a given job.\n */\n /**\n * Form 2. Query the status of jobs deployed to the scheduler.\n *\n * @access public\n * @param {object} [opts] Query options\n * @param {number} [opts.startTime] Jobs older than this will be excluded from\n * the results.\n * @param {number} [opts.endTime] Jobs newer than this will be excluded from\n * the results.\n * @param {wallet.Keystore} ownerKeystore The identity keystore of the owner\n * of the jobs to query\n * @throws when ownerKeystore is not provided\n * @returns {Promise<Array<Object>>} An array of status objects corresponding to the status of\n * jobs deployed by the given payment account.\n */\n async status(...args) {\n if (args.length !== 2) {\n throw new Error('Only 2 arguments must be passed to compute.status');\n }\n\n const ownerKeystore = args.pop();\n if (!(ownerKeystore instanceof wallet.Keystore)) {\n throw new TypeError(\n 'compute.status: ownerKeystore must be an instance of Keystore',\n );\n }\n\n const requestPayload = {\n idKeystoreOwner: ownerKeystore.address,\n };\n\n const firstArg = args.shift();\n if (typeof firstArg === 'string' || firstArg instanceof Job) {\n // Form 1\n requestPayload.jobId = firstArg instanceof Job ? firstArg.id : firstArg;\n requestPayload.statuses = jobStatuses;\n } else if (typeof firstArg === 'object') {\n // Form 2\n if (\n (typeof firstArg.startTime !== 'undefined' &&\n !(firstArg.startTime instanceof Date)) ||\n (typeof firstArg.endTime !== 'undefined' &&\n !(firstArg.endTime instanceof Date))\n ) {\n throw new TypeError(\n 'startTime and or endTime were not specified as instances of Date',\n );\n }\n\n requestPayload.startDeployTime = firstArg.startTime;\n requestPayload.endDeployTime = firstArg.endTime;\n \n if (typeof firstArg.statuses !== undefined\n && !(firstArg.statuses instanceof Array)\n || !(firstArg.statuses.every((status) => jobStatuses.includes(status)))\n ) {\n throw new TypeError(`Job statuses must be specified in an Array and can only include ${jobStatuses}`)\n }\n \n requestPayload.statuses = firstArg.statuses || jobStatuses;\n \n } else {\n throw new TypeError(`Unknown first argument type ${typeof firstArg}`);\n }\n\n if (!this.deployConnection)\n this.openDeployConn();\n\n const {\n success,\n payload: responsePayload,\n } = await this.deployConnection.send(\n 'listJobs',\n requestPayload,\n ownerKeystore,\n );\n\n if (!success) {\n throw new DCPError('Request for job status failed', responsePayload);\n }\n\n if (typeof firstArg === 'string' || firstArg instanceof Job) {\n return responsePayload[0];\n }\n\n this.deployConnection.off('close', this.deployConnection)\n await this.deployConnection.close();\n\n return responsePayload;\n }\n\n /**\n * Returns information for the job with the provided ID. It will use\n * `wallet.getId()` to retrieve the auth keystore.\n *\n * @param {string} jobAddress\n * @returns {Promise<object>} status object describing a given job\n * @access public\n */\n async getJobInfo(jobAddress) {\n this.phemeUses++;\n if (!this.phemeConnection)\n this.openPhemeConn();\n\n const ks = await wallet.getId();\n const { payload: jobResponsePayload } = await this.phemeConnection.send('fetchJobReport', \n {\n jobOwner: ks.address,\n job: jobAddress\n });\n\n const { payload: sliceResponsePayload } = await this.phemeConnection.send(\n 'fetchSliceReport', \n {\n jobOwner: ks.address,\n job: jobAddress\n },\n );\n\n const jobReport = {};\n Object.assign(jobReport, jobResponsePayload, sliceResponsePayload);\n\n if (this.phemeConnection && this.phemeUses === 1)\n {\n this.phemeConnection.off('close', this.phemeConnection)\n await this.phemeConnection.close();\n this.phemeConnection = null;\n }\n this.phemeUses--;\n\n return jobReport;\n }\n\n /**\n * Returns information for the slices of the job with the provided ID. It will\n * use `wallet.getId()` to retrieve the auth keystore.\n *\n * @param {string} jobAddress\n * @returns {Promise<object>} A slice status object for the given job.\n * @access public\n */\n async getSliceInfo(jobAddress) {\n this.phemeUses++;\n if (!this.phemeConnection)\n this.openPhemeConn();\n\n const identityKeystore = await wallet.getId();\n const {\n success,\n payload: responsePayload,\n } = await this.phemeConnection.send(\n 'fetchSliceReport',\n {\n job: jobAddress,\n jobOwner: identityKeystore.address,\n },\n identityKeystore,\n );\n\n if (!success) {\n throw new DCPError('Request for slice info failed', responsePayload);\n }\n\n if (this.phemeConnection && this.phemeUses === 1)\n {\n this.phemeConnection.off('close', this.phemeConnection)\n await this.phemeConnection.close();\n this.phemeConnection = null;\n }\n this.phemeUses--;\n\n return responsePayload;\n }\n}\n\nexports.RemoteDataSet = RemoteDataSet;\nexports.RemoteDataPattern = RemoteDataPattern;\nexports.MARKET_VALUE = MARKET_VALUE;\nexports.compute = new Compute(); /* :( */\nexports.compute.getPublicComputeGroupId = () => (__webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups[\"public\"].id);\nexports.compute.getPublicComputeGroupDescriptor = () => (__webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups[\"public\"]);\nexports.compute.ResultHandle = ResultHandle; // export as compute.ResultHandle for instanceof checking\nexports.version = {\n api: '1.5.2',\n provides: '1.0.0' /* dcpConfig.scheduler.compatibility.operations.compute */\n};\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/compute.js?");
4150
+ eval("/**\n * @file Module that implements Compute API\n * @module dcp/compute\n * @access public\n */\n\n/* global dcpConfig */\n\n/**\n * @access private - Will change soon!\n * @typedef {object} JobRequirements\n * @property {object} environment\n * @property {boolean} environment.fdlibm - Workers express capability 'fdlibm' in order for client applications to have confidence that results will be bitwise identical across workers. This library is recommended, but not required, for implementaiton of Google chrome, Mozilla Firefox and the DCP standalone worker use fdlibm but Microsoft Edge and Apple Safari do not use it.\n * @property {boolean} environment.offscreenCanvas When present, this capability indicates that the worker environment has the OffscreenCanvas symbol defined.\n * @property {object} details\n * @property {object} details.offscreenCanvas\n * @property {boolean} details.offscreenCanvas.bigTexture4096 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 4096.\n * @property {boolean} details.offscreenCanvas.bigTexture8192 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 8192.\n * @property {boolean} details.offscreenCanvas.bigTexture16384 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 16384.\n * @property {boolean} details.offscreenCanvas.bigTexture32768 This capability indicates that the worker's WebGL MAX_TEXTURE_SIZE is at least 32768.\n * @property {object} engine\n * @property {boolean} engine.es7 This capability enforces that the worker is running an es7-compliant JavaScript engine.\n * @property {boolean} engine.spidermonkey The capability enforces that the worker will run in the SpiderMonkey JS engine.\n * @property {boolean} browser.chrome The capability to exclude Chrome browsers.\n */\n\n/**\n * This JSDoc 'namespace' is a convenient way to document some globals that are available only in\n * the sandbox environment provided to work functions.\n * @access public\n * @namespace sandboxEnv\n */\n\n/**\n * This function emits a progress event. Progress events should be emitted approximately once per second; a task which fails to emit a \n * progress event for a certain period of time will be cancelled by the supervisor. The argument to this function is interpreted to six \n * significant digits, and must increase for every call. *All work functions must emit at least one progress event* - this requirement \n * will be enforced by the estimator. * The period of time mentioned above will be at least 30 wall-clock seconds and at least 30 \n * benchmark-adjusted seconds\n * @access public\n * @method progress\n * @param {string|number|undefined} n a number between 0 and 1 (inclusive) that represents a best-guess \n * at the completed portion of the task as a ratio of completed work to total work. If the argument is a string ending in the `%` symbol, \n * it will be interpreted as a number in the usual mathematical sense. If the argument is `undefined`, the slice progress is noted but \n * the amount of progress is considered to be indeterminate.\n * @memberof module:dcp/compute~sandboxEnv\n * @returns {boolean} true\n */\n\n/**\n * Emit a `console` event to the `JobHandle` in the client. If the sandbox is running in an environment with a native console object, the native method may also be invoked.\n * \n * @member console\n * @property {function} log fire console event with 'log' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} debug fire console event with 'debug' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} info fire console event with 'info' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} warn fire console event with 'warn' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * @property {function} error fire console event with 'error' as `level` (see {@link Job#event:console|Job#event:console.level}).\n * \n * **Note** some ES environments (Chrome, Firefox) implement C-style print formatting in this method. This is currently not supported.\n * @memberof module:dcp/compute~sandboxEnv\n * @emits Job#console\n * @access public\n */\n\n/**\n * @member work An object that contains the following properties\n * @property {function} emit (eventName, ...) - send arbitrary args with `eventName`. This event is received client-side with those args (see {@link Job#work}).\n * @property {object} job\n * @property {object} job.public Property set on job handle, see {@link Job#public}.\n * @memberof module:dcp/compute~sandboxEnv\n * @access public\n */\n\n/**\n * @member {OffscreenCanvas} OffscreenCanvas As defined in the {@link https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas|HTML Standard}, provides a canvas which can be rendered off-screen. If this interface is not available in a given worker, the worker will not report capability \"offscreenCanvas\".\n * @memberof module:dcp/compute~sandboxEnv\n * @access public\n */\n\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events/event-emitter */ \"./src/common/dcp-events/event-emitter.js\");\nconst { RangeObject, rehydrateRange } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst { RemoteDataSet } = __webpack_require__(/*! dcp/dcp-client/remote-data-set */ \"./src/dcp-client/remote-data-set.js\");\nconst { RemoteDataPattern } = __webpack_require__(/*! dcp/dcp-client/remote-data-pattern */ \"./src/dcp-client/remote-data-pattern.js\");\nconst { Job, ResultHandle } = __webpack_require__(/*! dcp/dcp-client/job */ \"./src/dcp-client/job/index.js\");\nconst { jobStatuses } = __webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { disableWorker } = __webpack_require__(/*! dcp/dcp-client/worker */ \"./src/dcp-client/worker/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('dcp-client');\n\nconst debug = (...args) => {\n if (debugging()) {\n args.unshift('dcp-client:compute');\n console.debug(...args);\n }\n};\n\nconst MARKET_VALUE = Symbol('Market Value');\nconst indirectEval = eval; // eslint-disable-line no-eval\n\n/**\n * Instance methods of this class are exported as static members of {@link module:dcp/compute|dcp/compute}.\n * @example\n * let job = compute.for(1, 3, function (i) {\n * progress('100%');\n * return i*10;\n * })\n * let results = await job.exec();\n */\nclass Compute extends EventEmitter {\n constructor () {\n super('Compute');\n\n /** @todo move these into some central config */\n this.defaultMaxGPUs = 1;\n this.GPUsAssigned = 0;\n this.allowSlicesToFinishBeforeStopping = false;\n\n this.schedulerURL = undefined; // Will default to dcpConfig.scheduler.location if not overidden\n this.bankURL = undefined; // Will default to dcpConfig.bank.services.bankTeller.location if not overidden\n\n Object.defineProperty(this.marketValue, MARKET_VALUE, {\n value: true,\n writable: false,\n configurable: false,\n enumerable: false,\n });\n\n const ceci = this;\n this.RemoteDataSet = RemoteDataSet;\n this.RemoteDataPattern = RemoteDataPattern;\n // Initialize to null so these properties are recognized for the Job class\n this.phemeConnection = null;\n this.phemeUses = 0;\n this.deployConnection = null;\n this.openPhemeConn = function openPhemeConn()\n {\n ceci.phemeConnection = new protocolV4.Connection(dcpConfig.scheduler.services.pheme);\n ceci.phemeConnection.on('close', ceci.openPhemeConn);\n }\n\n this.openDeployConn = function openDeployConn()\n {\n ceci.deployConnection = new protocolV4.Connection(dcpConfig.scheduler.services.jobSubmit);\n ceci.deployConnection.on('close', ceci.openDeployConn);\n }\n }\n\n get defaultMaxWorkers () {\n return portalWorker.supervisor.maxWorkingSandboxes\n }\n\n set defaultMaxWorkers (value) {\n portalWorker.supervisor.maxWorkingSandboxes = value\n }\n\n get paymentAddress () {\n return portalWorker.supervisor.paymentAddress\n }\n\n set paymentAddress (value) {\n portalWorker.supervisor.paymentAddress = value\n }\n\n get mining () {\n return portalWorker.working\n }\n\n /** Return a special object that represents the current market value\n * @param {number} factor - OPTIONAL multiplier. default: 1.0\n */\n marketValue (factor = 1) {\n return {\n [MARKET_VALUE]: factor\n };\n }\n\n /** Describes the payment required to compute such a slice on a worker\n * or in a market working at the rates described in the WorkValue object.\n * This function does not take into account job-related overhead.\n * \n * @param {object} sliceProfile A SliceProfile-shaped object\n * @param {object} workValue A WorkValue-shaped object\n * @returns {number} Payment value in DCC describing rate to compute slice\n */\n calculateSlicePayment(sliceProfile, workValue) {\n return sliceProfile.cpuHours * workValue.cpuHour\n + sliceProfile.gpuHours * workValue.gpuHour\n + sliceProfile.inputMBytes * workValue.inputMByte\n + sliceProfile.outputMBytes * workValue.outputMByte;\n }\n\n \n /**\n * Deprecated `mine` API. Use `work` instead\n */\n async mine (numberOfCores, paymentAddress = this.paymentAddress) {\n return this.work(numberOfCores, paymentAddress);\n }\n\n /**\n * Instruct the Worker to stop working, then emit workerInactive\n */\n async stopWorking() {\n console.warn('Using the Compute API for controlling workers is deprecated. Use the Worker API instead.')\n await portalWorker.stop(!this.allowSlicesToFinishBeforeStopping)\n }\n\n /**\n * Deprecated `stopMining` API. Use `stopWorking` instead\n */\n stopMining () {\n console.log(\"Warning: used deprecated 'stopMining' API\");\n this.stopWorking();\n }\n \n /**\n * Flag worker as disabled\n */\n disableWorker() {\n disableWorker();\n }\n\n /**\n * Deprecated `disableMiner` API. Use `disableWorker` instead\n */\n disableMiner() {\n this.disableWorker();\n }\n\n /**\n * Experimental replacement for mine() /* wg aug 2019\n * @param {number} [numberOfCores = this.defaultMaxWorkers] - Number of parallel processes to run\n * @param {string} [paymentAddress = this.paymentAddress || wallet.get().address] - The address of the account to work for\n */\n async work(numCores, paymentAddress) {\n console.warn('Using the Compute API for controlling workers is deprecated. Use the Worker API instead.');\n\n portalWorker.supervisor.paymentAddress = paymentAddress || this.paymentAddress || (await wallet.get()).address;\n portalWorker.supervisor.maxWorkingSandboxes = numCores || this.defaultMaxWorkers || 1;\n await portalWorker.start();\n\n return portalWorker;\n }\n\n /**\n * Form 1. Create a Job for the distributed computer by specifying the start,\n * end, and step for a RangeObject with a given work function and arguments.\n *\n * @param {number} start First number in a range (see {@link RangeObject})\n * @param {number} end Last number in a range (see {@link RangeObject})\n * @param {number} [step=1] the space between numbers in a range (see\n * {@link RangeObject})\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * let job = compute.for(1, 3, function (i) {\n * progress('100%')\n * return i*10\n * })\n */\n /**\n * Form 2.a Create a Job for the distributed computer by specifying the input\n * as a RemoteDataSet with a given work function and arguments.\n *\n * @param {RemoteDataSet} remoteDataSet Array, ES6 function* generator, or any\n * other type of iterable object.\n * \n * Form 2.b Create a Job for the distributed computer by specifying the input\n * as a RemoteDataPattern with a given work function and arguments.\n * @param {RemoteDataPattern} RemoteDataPattern Array, ES6 function* generator, or any\n * other type of iterable object.\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * const remoteData = ['https://example.com/data'];\n * const job = compute.for(new RemoteDataSet(...remoteData), function(i) {\n * progress(1)\n * return i\n * })\n * \n * * @example\n * const pattern = 'https://example.com/data/{slice}.json';\n * const NumberOfSlices = 5;\n * const job = compute.for(new RemoteDataSet(pattern, NumberOfSlices), function(i) {\n * progress(1)\n * return i\n * })\n */\n /**\n * Form 3. Create a Job for the distributed computer by specifying the input\n * as an Iterable object with a given work function and arguments.\n *\n * @param {Iterable} iterableObject Array, ES6 function* generator, or any\n * other type of iterable object.\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * let job = compute.for([123,456], function(i) {\n * progress(1)\n * return i\n * })\n */\n /**\n * Form 4. Create a Job for the distributed computer by specifying the input\n * as a RangeObject with a given work function and arguments.\n *\n * @param {RangeObject} rangeObject Either a RangeObject or an object literal\n * which can be used to create a RangeObject (see {@link RangeObject}).\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * let job = compute.for({start: 10, end: 13, step: 2}, (i) => progress(1) && i)\n */\n /**\n * Form 5. Similar to Form 4, except with a multi range object containing an\n * array of range objects in the key ranges. These are used to create\n * multi-dimensional ranges, like nested loops. If they were written as\n * traditional loops, the outermost loop would be the leftmost range object,\n * and the innermost loop would be the rightmost range object.\n *\n * @param {MultiRangeObject} multiRange Either a MultiRangeObject or any valid\n * argument to the constructor.\n * @param {function | string | URL | DcpURL} work Work function as string or\n * function literal.\n * @param {object} [arguments=[]] optional Array-like object which contains\n * arguments that are passed to the work function\n * @returns {Job}\n * @access public\n * @example\n * let job = compute.for([{start: 1, end: 2}, {start: 3, end: 5}], function(i,j) {\n * return [i, j, i*j]\n * })\n */\n for(...args) {\n // args, or work function\n const lastArg = args[args.length - 1];\n // work function, if args provided\n const secondLastArg = args[args.length - 2];\n let range = null;\n let work;\n let genArgs;\n\n // All forms: extract work and optional args from the end and validate it.\n if (isValidWorkFunctionType(secondLastArg)) {\n // for(..., workFunction, arguments)\n work = secondLastArg\n if (lastArg && Array.from(lastArg).length > 0 || typeof lastArg === 'string') {\n // array or string, use as-is (string should be a URI)\n genArgs = lastArg;\n } else if (lastArg === undefined) {\n genArgs = [];\n } else {\n throw new TypeError(\n 'Work function arguments (the last parameter), should be an array.',\n );\n }\n } else if (isValidWorkFunctionType(lastArg)) {\n // for(..., workFunction)\n work = lastArg;\n genArgs = [];\n }\n\n if (!isWorkFunctionValid(work)) {\n throw new TypeError(\n `Work parameter must be a function or a string that evaluates to a function, or a URL that points to the location of a function.\\nReceived type ${typeof work} (${work})`,\n );\n }\n\n // Extract the proper information from every form.\n const [firstArg, secondArg, thirdArg] = args;\n if (['object', 'string'].includes(typeof firstArg)) {\n // Forms 2 & 3 (data is iterable, eg. an Array or RemoteDataSet)\n // Forms 4 & 5 (data is RangeObject & Friends)\n range = rehydrateRange(firstArg);\n } else if (typeof firstArg === 'number' && typeof secondArg === 'number') {\n // Form 1 (data is start, end[, step])\n const start = firstArg;\n const end = secondArg;\n\n /**\n * If there is an argument between the end and the work fn, use it as step\n * otherwise set step to undefined.\n */\n const step = typeof thirdArg === 'number' ? thirdArg : undefined;\n range = new RangeObject(start, end, step);\n } else {\n throw new TypeError(\n 'Parameters do not match one of the accepted call forms.',\n );\n }\n\n if (range.length === 0) {\n throw new Error('Length of input set must be greater than zero.');\n }\n\n debug('Work:', work);\n debug('Input Set:', range);\n debug('Arguments:', genArgs);\n return new Job(work, range, genArgs);\n\n /**\n * @param {any} workFunction\n */\n function isValidWorkFunctionType(workFunction) {\n return (\n typeof workFunction === 'function' ||\n typeof workFunction === 'string' ||\n DcpURL.isURL(workFunction)\n );\n }\n\n /**\n * Checks if the workFunction can be evaluated to a function or if it's\n * protocol is HTTPS,\n * @param {function | string | URL | DcpURL} workFunction\n */\n function isWorkFunctionValid(workFunction) {\n switch (typeof workFunction) {\n case 'function':\n return true;\n case 'string': {\n return isValidFunction(workFunction);\n }\n case 'object':\n return DcpURL.isURL(workFunction);\n default:\n return false;\n }\n }\n\n /**\n * Evaluates to true if the string can be coerced into a javascript\n * function.\n * @param {string} workFunction\n */\n function isValidFunction(workFunction) {\n try {\n const fn = indirectEval(`(${workFunction})`);\n return typeof fn === 'function';\n } catch (e) {\n return false;\n }\n }\n }\n\n /**\n * Form 1.\n * Create a job which will run a work function once\n *\n * @param {function | string | URL | DcpURL} work Work function as string or function literal.\n * @param {object} [workFunctionArgs=[]] optional Array-like object which contains arguments which are passed to the work func\n * @returns {Job}\n * @access public\n * @example\n * const job = compute.do(function (i) {\n * progress('100%')\n * return i*10\n * })\n */\n /**\n * Form 2.\n * Create a job which will run a work function on the values 1..n\n *\n * @param {number} n Number of times to run the work function.\n * @param {function | string | URL | DcpURL} work Work function as string or function literal.\n * @param {object} [workFunctionArgs=[]] optional Array-like object which contains arguments which are passed to the work func\n * @returns {Job}\n * @access public\n * @example\n * const job = compute.do(5, function (i) {\n * progress('100%')\n * return i*10\n * })\n */\n do(...args) {\n let n;\n let work;\n let workFunctionArgs;\n if (typeof args[0] !== 'number') {\n // Form 1.\n n = 1;\n [work, workFunctionArgs] = args;\n } else {\n // Form 2.\n [n, work, workFunctionArgs] = args;\n }\n return this.for(0, n - 1, work, workFunctionArgs);\n }\n\n /** Cancel a running job, by opaque id\n *\n * @param {string} id Address of the job to cancel.\n * @param {wallet.Keystore} ownerKeystore The keystore of the job owner\n * @param {string} reason If provided, will be sent on to client\n * @returns {object} The status of the funds that were released from escrow.\n * @throws when id or ownerKeystore are not provided.\n * @access public\n */\n async cancel (id, ownerKeystore, reason = '') {\n if (typeof id !== 'string') {\n throw new Error(`compute.cancel: Job ID must be a string, not ${typeof id}.`);\n }\n\n if (!(ownerKeystore instanceof wallet.Keystore)) {\n throw new Error('compute.cancel: ownerKeystore must be instance of wallet.Keystore');\n }\n\n if (!this.deployConnection)\n this.openDeployConn();\n\n const response = await this.deployConnection.send('cancelJob', {\n job: id,\n owner: ownerKeystore.address,\n reason,\n }, ownerKeystore);\n\n this.deployConnection.off('close', this.deployConnection)\n await this.deployConnection.close();\n\n return response.payload;\n }\n\n /**\n * Reconnect/resume a job, by opaque id\n *\n * @param {string} id Opaque id of the Job to resume/reconnect.\n * @param {wallet.Keystore} [ownerKeystore=wallet.get()] The keystore of the job owner\n * @returns {Job}\n * @throws when id is not provided.\n * @access public\n */\n async resume (id, ownerKeystore) {\n if (typeof id !== 'string') {\n throw new Error(`compute.resume: Job ID must be a string, not ${typeof id}.`);\n }\n\n if (!(ownerKeystore instanceof wallet.Keystore)) {\n ownerKeystore = await wallet.get();\n }\n\n const response = await this.schedulerConnection.send('fetchJob', {\n job: id, // XXX@TODO change 'address' to 'job' once we port this operation to v4\n owner: ownerKeystore.address,\n }, ownerKeystore);\n\n const job = new Job(response.payload);\n job.setPaymentAccountKeystore(ownerKeystore);\n return job;\n }\n\n /**\n * Form 1. Query the status of a job that has been deployed to the scheduler.\n *\n * @access public\n * @param {string|Job} job A job ID or a Job instance\n * @throws when ownerKeystore is not provided\n * @returns {Promise<object>} A status object describing a given job.\n */\n /**\n * Form 2. Query the status of jobs deployed to the scheduler.\n *\n * @access public\n * @param {object} [opts] Query options\n * @param {number} [opts.startTime] Jobs older than this will be excluded from\n * the results.\n * @param {number} [opts.endTime] Jobs newer than this will be excluded from\n * the results.\n * @throws when ownerKeystore is not provided\n * @returns {Promise<Array<Object>>} An array of status objects corresponding to the status of\n * jobs deployed by the given payment account.\n */\n async status(...args) {\n const requestPayload = {};\n\n const firstArg = args.shift();\n if (typeof firstArg === 'string' || firstArg instanceof Job) {\n // Form 1\n requestPayload.jobAddress = firstArg instanceof Job ? firstArg.id : firstArg;\n requestPayload.statuses = jobStatuses;\n } else if (typeof firstArg === 'object') {\n // Form 2\n if (\n (typeof firstArg.startTime !== 'undefined' &&\n typeof firstArg.startTime !== 'number' &&\n !(firstArg.startTime instanceof Date)) ||\n (typeof firstArg.endTime !== 'undefined' &&\n typeof firstArg.endTime !== 'number' &&\n !(firstArg.endTime instanceof Date))\n ) {\n throw new TypeError(\n 'startTime and or endTime were not specified as instances of Date or milliseconds since epoch',\n );\n }\n\n requestPayload.startDeployTime = firstArg.startTime;\n requestPayload.endDeployTime = firstArg.endTime;\n \n if (typeof firstArg.statuses !== 'undefined'\n && (!(firstArg.statuses instanceof Array)\n || !(firstArg.statuses.every((status) => jobStatuses.includes(status))))\n ) {\n throw new TypeError(`Job statuses must be specified in an Array and can only include ${jobStatuses}`)\n }\n \n requestPayload.statuses = firstArg.statuses || jobStatuses;\n \n } else {\n throw new TypeError(`Unknown first argument type ${typeof firstArg}`);\n }\n\n if (!this.deployConnection)\n this.openDeployConn();\n\n const {\n success,\n payload: responsePayload,\n } = await this.deployConnection.send(\n 'listJobs',\n requestPayload\n );\n\n if (!success) {\n throw new DCPError('Request for job status failed', responsePayload);\n }\n\n this.deployConnection.off('close', this.deployConnection)\n await this.deployConnection.close();\n if (responsePayload.length > 0 && (typeof firstArg === 'string' || firstArg instanceof Job)) {\n return responsePayload[0];\n }\n\n return responsePayload;\n }\n\n /**\n * Returns information for the job with the provided ID. It will use\n * `wallet.getId()` to retrieve the auth keystore.\n *\n * @param {string} jobAddress\n * @returns {Promise<object>} status object describing a given job\n * @access public\n */\n async getJobInfo(jobAddress) {\n this.phemeUses++;\n if (!this.phemeConnection)\n this.openPhemeConn();\n\n const ks = await wallet.getId();\n const { payload: jobResponsePayload } = await this.phemeConnection.send('fetchJobReport', \n {\n jobOwner: ks.address,\n job: jobAddress\n });\n\n const { payload: sliceResponsePayload } = await this.phemeConnection.send(\n 'fetchSliceReport', \n {\n jobOwner: ks.address,\n job: jobAddress\n },\n );\n\n const jobReport = {};\n Object.assign(jobReport, jobResponsePayload, sliceResponsePayload);\n\n if (this.phemeConnection && this.phemeUses === 1)\n {\n this.phemeConnection.off('close', this.openPhemeConn);\n this.phemeConnection.close();\n this.phemeConnection = null;\n }\n this.phemeUses--;\n\n return jobReport;\n }\n\n /**\n * Returns information for the slices of the job with the provided ID. It will\n * use `wallet.getId()` to retrieve the auth keystore.\n *\n * @param {string} jobAddress\n * @returns {Promise<object>} A slice status object for the given job.\n * @access public\n */\n async getSliceInfo(jobAddress) {\n this.phemeUses++;\n if (!this.phemeConnection)\n this.openPhemeConn();\n\n const identityKeystore = await wallet.getId();\n const {\n success,\n payload: responsePayload,\n } = await this.phemeConnection.send(\n 'fetchSliceReport',\n {\n job: jobAddress,\n jobOwner: identityKeystore.address,\n },\n identityKeystore,\n );\n\n if (!success) {\n throw new DCPError('Request for slice info failed', responsePayload);\n }\n\n if (this.phemeConnection && this.phemeUses === 1)\n {\n this.phemeConnection.off('close', this.openPhemeConn)\n this.phemeConnection.close();\n this.phemeConnection = null;\n }\n this.phemeUses--;\n\n return responsePayload;\n }\n}\n\nexports.RemoteDataSet = RemoteDataSet;\nexports.RemoteDataPattern = RemoteDataPattern;\nexports.MARKET_VALUE = MARKET_VALUE;\nexports.compute = new Compute(); /* :( */\nexports.compute.getPublicComputeGroupId = () => (__webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups[\"public\"].id);\nexports.compute.getPublicComputeGroupDescriptor = () => (__webpack_require__(/*! dcp/common/scheduler-constants */ \"./src/common/scheduler-constants.js\").computeGroups[\"public\"]);\nexports.compute.ResultHandle = ResultHandle; // export as compute.ResultHandle for instanceof checking\nexports.compute.Job = Job;\nexports.version = {\n api: '1.5.2',\n provides: '1.0.0' /* dcpConfig.scheduler.compatibility.operations.compute */\n};\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/compute.js?");
4151
4151
 
4152
4152
  /***/ }),
4153
4153
 
@@ -4157,7 +4157,7 @@ eval("/**\n * @file Module that implements Compute API\n * @module dcp/comput
4157
4157
  \*********************************/
4158
4158
  /***/ ((module, exports, __webpack_require__) => {
4159
4159
 
4160
- 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\":\"fca68c239b7cda20a34513139c096b25d2576370\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"4.2.8\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#prod-20220620\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#e6f31a3a6da1574b3d3bf252bd80208e07937946\"},\"built\":\"Tue Jun 21 2022 13:58:11 GMT-0400 (Eastern Daylight Saving Time)\",\"config\":{\"generated\":\"Tue 21 Jun 2022 01:58:09 PM EDT by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"5.70.0\",\"node\":\"v14.19.3\"},\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?");
4160
+ 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\":\"b5864d51c1e7a9b3024698306574b60003a91d62\",\"branch\":\"release\",\"dcpClient\":{\"version\":\"4.2.9\",\"from\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#prod-20220718\",\"resolved\":\"git+ssh://git@gitlab.com/Distributed-Compute-Protocol/dcp-client.git#15ef46ae27df39b0d1383bd282d36f8c3227905e\"},\"built\":\"Fri Jul 22 2022 09:34:47 GMT-0400 (Eastern Daylight Saving Time)\",\"config\":{\"generated\":\"Fri 22 Jul 2022 09:34:45 AM EDT by erose on lorge\",\"build\":\"debug\"},\"webpack\":\"5.70.0\",\"node\":\"v14.20.0\"},\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?");
4161
4161
 
4162
4162
  /***/ }),
4163
4163
 
@@ -4168,7 +4168,7 @@ eval("/* module decorator */ module = __webpack_require__.nmd(module);\n/**\n *
4168
4168
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4169
4169
 
4170
4170
  "use strict";
4171
- eval("/**\n * @file job/index.js\n * @author Eddie Roosenmaallen, eddie@kingsds.network\n * Matthew Palma, mpalma@kingsds.network\n * Wes Garland, wes@kingsds.network\n * Paul, paul@kingsds.network\n * @date November 2018\n * November 2018\n * February 2022\n * May 2022\n *\n * This module implements the Compute API's Job Handle\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();\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 this.isCI = 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 {object} pileMessage \n * @returns payload containing success property (pertaining to success of adding slices to job) as well as lastSliceNumber of job \n */\n async safeSliceUpload(pileMessage)\n {\n let payload = undefined;\n let errorTolerance = dcpConfig.job.sliceUploadErrorTolerance; // copy number of times we will tolerate non-success when uploading slices directly from config\n\n while (true) // eslint-disable-line no-constant-condition\n {\n try\n {\n const start = Date.now();\n this.emit('x-dbg-uploadStart', pileMessage.signedMessage.length);\n payload = await this.deployConnection.sendPreparedMessage(pileMessage);\n if (!payload.success) {\n this.emit('x-dbg-uploadBackoff', pileMessage.signedMessage.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 await this.deployConnection.keepalive();\n let pileMessage = await this.deployConnection.prepare('addSliceData', {\n job: this.address,\n dataValues: kvinMarshal(pile),\n });\n \n let pileSize = pileMessage.signedMessage.length;\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(pileMessage);\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(pileMessage);\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(pileMessage);\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(pileMessage);\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 = dcpConfig.job.uploadInitialNumberOfSlices; // 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: new 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\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.readyStateChange('listeners');\n const listenersP = this[ADD_LISTENERS]();\n\n // localExec jobs are not entered in any compute group.\n if (!this[INTERNAL_SYMBOL].payloadDetails.localExec && this.computeGroups && this.computeGroups.length)\n {\n this.readyStateChange('compute-groups');\n computeGroups.addRef(); // Just in case we're doing a Promise.all on multiple execs.\n\n // Add this job to its currently-defined compute groups (as well as public group, if included)\n let success;\n try {\n const cgPayload = await computeGroups.addJobToGroups(this.address, this.computeGroups);\n success = true; // To support older version of CG service where addJobToGroups had void/undefined return.\n if (cgPayload) success = cgPayload.success;\n debugging('dcp-client') && console.debug('job/index: addJobToGroups cgPayload:', cgPayload ? cgPayload : 'cgPayload is not defined; probably from legacy CG service.');\n } catch (e) {\n debugging('dcp-client') && console.debug('job/index: addJobToGroups threw exception:', e);\n success = false;\n }\n\n computeGroups.closeServiceConnection().catch((err) => {\n console.error('Warning: could not close compute groups service connection', err)\n });\n\n /* Could not put the job in any compute group, even though the user wanted it to run. Cancel the job. */\n if (!success)\n {\n await this.cancel('compute-groups::Unable to join any compute groups');\n throw new DCPError(`Access Denied::Failed to add job ${this.address} to any of the desired compute groups`, 'DCPL-1100');\n }\n }\n\n await listenersP;\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 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 message: ${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 }).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.openBankConn)\n await this.bankConnection.close().catch(handleErr);\n \n await computeGroups.closeServiceConnection().catch((err) => {\n console.error('Warning: could not close compute groups service connection', err)\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 if (debugging('dcp-client')) {\n console.debug('subscribedEvents', evts);\n console.debug('subscribedWorkerEvents', workEvts);\n }\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 if (debugging('dcp-client')) {\n console.debug('reliableEvents', reliableEvents);\n console.debug('optionalEvents', optionalEvents);\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 debugging('dcp-client') && console.debug('ON_RESULT', _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 try\n {\n let {pkg, unresolved} = await this._publishLocalModules();\n\n payloadDetails.dependencies = unresolved;\n if (pkg)\n payloadDetails.dependencies.push(pkg.name + '/' + sideloaderModuleIdentifier); \n }\n catch(error)\n {\n throw new DCPError(`Error trying to communicate with package manager server: ${error}`);\n }\n \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 // The following check is needed for when using dcp-rtlink and loading the config through source, instead of using the dcp-client bundle\n let schedIdAddress = schedId;\n if(schedId.address){ \n schedIdAddress = schedId.address;\n }\n const myId = await wallet.getId();\n const preauthToken = await bankUtil.preAuthorizePayment(schedIdAddress, 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: Number(payloadDetails.feeStructure.marketValue) || 0, // @todo: improve feeStructure internals to better reflect v4\n absoluteSlicePayment: Number(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 isCI: this.isCI,\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 && !DCP_ENV.isBrowserPlatform)\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: submitPayload', 256);\n console.debug('Before Deploy', myId);\n }\n\n // Deploy the job!\n const deployed = await this.deployConnection.send('submit', submitPayload, myId);\n\n if (!deployed.success) {\n // close all of the connections so that we don't cause node processes to hang.\n const handleErr = (e) =>\n {\n console.error('Error while closing job connection:');\n console.error(e);\n }\n \n this.eventSubscriber.close().catch(handleErr);\n \n this.deployConnection.off('close', this.openDeployConn);\n this.deployConnection.close().catch(handleErr);\n \n this.bankConnection.off('close', this.openBankConn)\n this.bankConnection.close().catch(handleErr);\n \n computeGroups.closeServiceConnection().catch(handleErr);\n \n // Yes, it is possible for deployed or 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 throw new DCPError('Failed to submit job to scheduler', deployed.payload);\n }\n throw new DCPError('Failed to submit job to scheduler (no payload)', deployed ? deployed : '');\n }\n\n debugging('dcp-client') && console.debug('After Deploy', JSON.stringify(deployed));\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[INTERNAL_SYMBOL].payloadDetails = {\n ...this[INTERNAL_SYMBOL].payloadDetails,\n ...payloadDetails,\n };\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 this[INTERNAL_SYMBOL].payloadDetails.isCI = 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|DcpURL} 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?");
4171
+ eval("/**\n * @file job/index.js\n * @author Eddie Roosenmaallen, eddie@kingsds.network\n * Matthew Palma, mpalma@kingsds.network\n * Wes Garland, wes@kingsds.network\n * Paul, paul@kingsds.network\n * Ryan Saweczko, ryansaweczko@kingsds.network\n * @date November 2018\n * November 2018\n * February 2022\n * May 2022\n * Jun 2022\n *\n * This module implements the Compute API's Job Handle\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, SparseRangeObject } = __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 { addSlices } = __webpack_require__(/*! ./upload-slices */ \"./src/dcp-client/job/upload-slices.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 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\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};\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 */\n const wrangleData = (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 (inputData instanceof SparseRangeObject) { return inputData }\n if (inputData instanceof MultiRangeObject) { return inputData }\n if (MultiRangeObject.isProtoMultiRangelike(inputData)) { return new MultiRangeObject(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/**\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 /**\n * Fired when the job is accepted by the scheduler on deploy.\n * \n * @event Job#accepted\n * @access public\n * @type {object}\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 @todo: is this in the spec? is there a no progress data? should there be?\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 * @todo: is this a thing, should it be a thing (the payload)\n * Event payload is the estimated funds required to complete the job\n * \n * @event Job#nofunds\n * @access public\n * @type {BigNumber}\n *//**\n * Fired when the job cannot be deployed due to no bank account / not enough balance to deploy the job\n * \n * @event Job#ENOFUNDS\n * @access public\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('application_worker_address'[, data[, arguments]])\n * @form2a new Job('worker source'[, data[, arguments]])\n * @form2b new Job(worker_function[, data[, arguments]])\n */\n constructor ()\n {\n super('Job');\n if (typeof arguments[0] === 'function')\n arguments[0] = arguments[0].toString();\n\n if (typeof arguments[0] === 'string')\n {\n const { encodeDataURI } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n this.workFunctionURI = encodeDataURI(arguments[0], 'application/javascript');\n } \n else if (DcpURL.isURL(arguments[0]))\n this.workFunctionURI = arguments[0].href;\n\n this.jobInputData = wrangleData(arguments[1] || []);\n this.jobArguments = wrangleData(arguments[2] || []);\n \n log('wrangledInputData:', this.jobInputData);\n log('wrangledArguments:', this.jobArguments);\n\n this.initEventSystems();\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 // 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 this.maxDeployPayment = 1;\n\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\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 /**\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 * 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 * 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 /* For API interface to end-users only */\n Object.defineProperty(this, 'id', {\n get: () => this.address,\n set: (id) => { this.address = id }\n });\n \n this.uuid = uuidv4(); /** @see {@link https://kingsds.atlassian.net/browse/DCP-1475?atlOrigin=eyJpIjoiNzg3NmEzOWE0OWI4NGZkNmI5NjU0MWNmZGY2OTYzZDUiLCJwIjoiaiJ9|Jira Issue} */\n this.dependencies = []; /* dependencies of the work function */\n this.requirePath = []; /* require path for dependencies */\n this.connected = false; /* true when exec or resume called */\n this.results = new ResultHandle(this); /* result handle */\n this.collateResults = true; /* option to receive results as they are computed & ensure all are received on finish */\n this.force100pctCPUDensity = false; /* tell scheduler to assume this job uses 100% cpu density */\n this.workerConsole = false; /* tell workers to log more information about failures in the evaluator this job causes */\n this.address = null; /* job address, created by scheduler during exec call. */\n this.paymentAccountKeystore = null; /* keystore for payment for job to come from */\n this.status = { /* job status details */\n runStatus: null,\n total: null,\n distributed: null,\n computed: null\n };\n // Compute groups. Add to public compute group by default\n this.computeGroups = [ Object.assign({}, schedulerConstants.computeGroups.public) ];\n \n // Update the ready state as we go through job deployment\n this.readyState = sliceStatus.new;\n const that = this;\n this.readyStateChange = function job$$readyStateChange (readyState)\n {\n that.readyState = readyState;\n that.emit('readyStateChange', that.readyState);\n }\n }\n \n /**\n * Initialize the various event systems the job handle requires. These include:\n * - an internal event emitter (this.ee)\n * - an event emitter for any events emitted on `work.emit` within work functions (this.work)\n * - an event subscriber to subscribe (to receive) events from the scheduler (this.eventSubscriber)\n */\n initEventSystems ()\n {\n // Handle the various event-related things required in the constructor\n\n // Internal event emitter for events within job handle\n this.ee = new EventEmitter('Job Internal');\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 work function\n * work.emit('myEventName', 1, [2], \"three\");\n * // client-side\n * job.work.on('myEventName', (num, arr, string) => { });\n */\n this.work = new EventEmitter('job.work');\n this.listenForCustomEvents = false;\n\n // Event subscriber - receive events from the event router\n this.eventSubscriber = new EventSubscriber(this);\n \n // Some events from the event subscriber can't be emitted immediately upon receipt without having \n // weird/wrong output due to things like serialization. We allow interceptors in the event subscriber\n // to handle this.\n const that = this\n var lastConsoleEv;\n var sameCounter = 1;\n const parseConsole = function deserializeConsoleMessage(ev) {\n if (tunedKvin)\n ev.message = tunedKvin.unmarshal(ev.message);\n else \n ev.message = kvin.unmarshal(ev.message);\n \n if (lastConsoleEv && ev.message[0] === lastConsoleEv.message[0] && ev.sliceNumber === lastConsoleEv.sliceNumber && ev.level === lastConsoleEv.level)\n ev.same = ++sameCounter;\n else\n sameCounter = 1;\n lastConsoleEv = ev;\n \n /* if we have the same message being logged (same sliceNumber, message, log level), the console event object will have the sole property same, nothing else */\n if (ev.same > 1)\n that.emit('console', { same: ev.same });\n else\n {\n delete ev.same;\n that.emit('console', ev);\n }\n }\n\n this.eventIntercepts = {\n result: (ev) => this.handleResult(ev),\n status: (ev) => this.handleStatus(ev),\n cancel: (ev) => this.ee.emit('stopped', ev),\n custom: (ev) => this.work.emit(ev.customEvent, 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 this.listenForCustomEvents = true;\n });\n this.desiredEvents = []\n this.on('newListener', (evt) => {\n if (!this.connected && evt !== 'newListener')\n this.desiredEvents.push(evt);\n if (evt === 'cancel')\n this.listeningForCancel = true;\n });\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 {\n const response = await this.useDeployConnection('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 {\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 getJobInfo ()\n {\n return (__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 getSliceInfo ()\n {\n return (__webpack_require__(/*! ../compute */ \"./src/dcp-client/compute.js\").compute.getSliceInfo)(this.address);\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 {\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 const bankConnection = new protocolV4.Connection(dcpConfig.bank.services.bankTeller);\n\n /*\n * escrow has been broken for an unknown amount of time. `feeStructureId` is not defined anywhere in the job class, and hasn't\n * for a period of time. When fixed, `this[INTERNAL_SYMBOL].payloadDetails.feeStructureId` will likely become just `this.feeStructureId`,\n * but it's being left alone until someone spends the time to fix escrow. / rs Jul 2022\n */\n const response = await bankConnection.send('embiggenFeeStructure', {\n feeStructureAddress: this[INTERNAL_SYMBOL].payloadDetails.feeStructureId,\n additionalEscrow: new BigNumber(fundsRequired),\n fromAddress: this.paymentAccountKeystore.address,\n }, this.paymentAccountKeystore);\n\n bankConnection.close();\n const receipt = response.payload;\n return receipt;\n }\n\n /**\n * create bundles for local dependencies\n */\n _pack ()\n {\n var retval = (__webpack_require__(/*! ./node-modules */ \"./src/dcp-client/job/node-modules.js\").createModuleBundle)(this.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 {\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 * 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 {\n if (typeof modulePaths !== 'string' && (!Array.isArray(modulePaths) || modulePaths.some((modulePath) => typeof modulePath !== 'string')))\n throw new TypeError('The argument to dependencies is not a string or an array of strings');\n else if (modulePaths.length === 0)\n throw new RangeError('The argument to dependencies cannot be an empty string or array');\n else if (Array.isArray(modulePaths) && modulePaths.some((modulePath) => modulePath.length === 0))\n throw new RangeError('The argument to dependencies cannot be an array containing an empty string');\n\n if (!Array.isArray(modulePaths))\n modulePaths = [modulePaths];\n\n for (const modulePath of modulePaths)\n {\n if (modulePath[0] !== '.' && modulePath.indexOf('/') !== -1)\n {\n const modulePrefixRegEx = /^(.*)\\/.*?$/;\n const [, modulePrefix] = modulePath.match(modulePrefixRegEx);\n if (modulePrefix && this.requirePath.indexOf(modulePrefix) === -1)\n this.requirePath.push(modulePrefix);\n }\n this.dependencies.push(modulePath);\n }\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 {\n if (this.address)\n {\n if (!keystore.address.eq(this.paymentAccountKeystore))\n {\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 throw new Error('Not an instance of Keystore: ' + keystore.toString());\n this.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 {\n this.slicePaymentOffer = new SlicePaymentOffer(slicePaymentOffer);\n }\n \n \n /**\n * @param {URL|DcpURL} 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 {\n if (locationUrl instanceof URL || locationUrl instanceof DcpURL)\n this.resultStorageDetails = locationUrl;\n else\n throw new Error('Not an instance of a DCP URL: ' + locationUrl);\n \n\n // resultStorageParams contains any post params required for off-prem storage\n if (typeof postParams !== 'undefined' && typeof postParams === 'object' )\n this.resultStorageParams = postParams;\n else\n throw new Error('Not an instance of a object: ' + postParams);\n\n // Some type of object here\n this.resultStorageType = 'pattern';\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 {\n this.inLocalExec = true;\n this.estimationSlices = 0;\n this.greedyEstimation = false;\n this.isCI = 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.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 * 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 {\n if (this.connected)\n throw new Error('Exec called twice on the same job handle.');\n \n if (this.estimationSlices === Infinity)\n this.estimationSlices = null;\n else if (this.estimationSlices < 0)\n throw new Error('Incorrect value for estimationSlices; it can be an integer or Infinity!');\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 \n this.readyStateChange('exec');\n if ((typeof slicePaymentOffer === 'number') || (typeof slicePaymentOffer === 'object')\n || ((this.slicePaymentOffer === null || this.slicePaymentOffer === undefined) && typeof slicePaymentOffer === 'function'))\n this.setSlicePaymentOffer(slicePaymentOffer);\n if (typeof initialSliceProfile !== 'undefined')\n this.initialSliceProfile = initialSliceProfile;\n \n if (typeof paymentAccountKeystore !== 'undefined')\n {\n /** XXX @todo deprecate use of ethereum wallet objects */\n if (typeof paymentAccountKeystore === 'object' && paymentAccountKeystore.hasOwnProperty('_privKey'))\n {\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 {\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 if (this.paymentAccountKeystore)\n // Throws if they fail to unlock, we allow this since the keystore was set programmatically. \n await this.paymentAccountKeystore.unlock(undefined, parseFloat(dcpConfig.job.maxDeployTime));\n else\n {\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 {\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 {\n ks = await wallet.get({ contextId: this.contextId, jobName: this.public.name, msg});\n }\n catch (e)\n {\n if (e.code !== ClientModal.CancelErrorCode) throw e;\n };\n if (ks)\n {\n try\n {\n await ks.unlock(undefined, parseFloat(dcpConfig.job.maxDeployTime));\n locked = false;\n }\n catch (e)\n {\n // prompt user again if user enters password incorrectly, exit modal otherwise\n if (e.code !== wallet.unlockFailErrorCode) 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 this.feeStructure = this.slicePaymentOffer.toFeeStructure(this.jobInputData.length);\n\n if (!this.address)\n {\n try\n {\n this.readyStateChange('init');\n await this.deployJob();\n const listenersPromise = this.addInitialEvents();\n const computeGroupsPromise = this.joinComputeGroups();\n let uploadSlicePromise;\n // if job data is by value then upload data to the scheduler in a staggered fashion\n if (Array.isArray(this.dataValues))\n {\n this.readyStateChange('uploading');\n uploadSlicePromise = addSlices(this.dataValues, this.address, tunedKvin).then(() => this.close());\n debugging('slice-upload') && uploadSlicePromise.then(() => console.debug('831: x-dbg-uploadComplete'));\n }\n \n // await all promises for operations that can be done after the job is deployed\n await Promise.all([listenersPromise, computeGroupsPromise, uploadSlicePromise]);\n\n this.readyStateChange('deployed');\n this.emit('accepted', { job: this });\n }\n catch (error)\n {\n if (ON_BROWSER)\n await ClientModal.alert(error, { title: 'Failed to deploy job!' });\n throw error;\n }\n }\n else\n {\n // reconnecting to an old job\n await this.addInitialEvents();\n this.readyStateChange('reconnected');\n }\n\n this.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.listeningForCancel)\n ClientModal.alert('More details in console...', { title: 'Job Canceled' });\n this.emit('cancel', event);\n\n let errorMsg = event.reason;\n if (event.error)\n errorMsg = errorMsg +`\\n Recent error message: ${event.error.message}`\n \n reject(new DCPError(errorMsg, event.code));\n };\n\n this.ee.once('stopped', async (stopEvent) => {\n // There is a chance the result submitter will emit finished > 1 time. Only handle it once.\n if (this.receivedStop)\n return;\n this.receivedStop = true;\n this.emit('stopped', stopEvent.runStatus);\n switch (stopEvent.runStatus) {\n case jobStatus.finished:\n if (this.collateResults)\n {\n let report = await this.getJobInfo();\n let allSliceNumbers = Array.from(Array(report.totalSlices)).map((e,i)=>i+1);\n let remainSliceNumbers = allSliceNumbers.filter((e) => !this.results.isAvailable(e));\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(new Error(`Unknown event \"${stopEvent.runStatus}\" caused the job to be stopped.`));\n break;\n }\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 this.closeDeployConnection();\n await this.eventSubscriber.close().catch(handleErr);\n await computeGroups.closeServiceConnection().catch((err) => {\n console.error('Warning: could not close compute groups service connection', err);\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 addInitialEvents ()\n {\n this.readyStateChange('listeners');\n\n // This is important: We need to flush the task queue before adding listeners\n // because we queue pending listeners by listening to the newListener event (in the constructor).\n // If we don't flush here, then the newListener events may fire after this function has run,\n // and the events won't be properly set up.\n await new Promise(resolve => setTimeout(resolve, 0));\n\n // @todo: Listen for an estimated cost, probably emit an \"estimated\" event when it comes in?\n // also @todo: Do the estimation task(s) on the scheduler and send an \"estimated\" event\n\n // Always listen to the stop event. It will resolve the work function promise, so is always needed.\n this.on('stop', (ev) => {this.ee.emit('stopped', ev)});\n\n // Connect listeners that were set up before exec\n if (this.desiredEvents.includes('result'))\n this.listeningForResults = true;\n await this.subscribeNewEvents(this.desiredEvents);\n\n // Connect listeners that are set up after exec\n this.on('newListener', (evt) => {\n if (evt === 'newListener' || this.desiredEvents.includes(evt))\n return;\n this.subscribeNewEvents([evt]);\n });\n \n // automatically add a listener for results if collateResults is on\n if (this.collateResults && !this.listeningForResults)\n this.on('result', () => {});\n\n debugging('dcp-client') && console.debug('subscribedEvents', this.desiredEvents);\n\n // If we have listeners for job.work, subscribe to custom events\n if (this.listenForCustomEvents)\n await this.subscribeCustomEvents();\n // Connect work event listeners that are set up after exec\n else\n this.work.on('newListener', () => this.subscribeCustomEvents());\n }\n \n /**\n * Subscribes to either reliable events or optional events. It is assumed that\n * any call to this function will include only new events.\n * @param {string[]} events \n */\n async subscribeNewEvents (events)\n {\n const reliableEvents = [];\n const optionalEvents = [];\n for (let eventName of events)\n {\n eventName = eventName.toLowerCase();\n if (this.eventTypes[eventName] && this.eventTypes[eventName].reliable)\n reliableEvents.push(eventName);\n else if (this.eventTypes[eventName] && !this.eventTypes[eventName].reliable)\n optionalEvents.push(eventName);\n else\n debugging('dcp-client') && console.debug(`Job handler has listener ${eventName} which isn't an event-router event.`);\n }\n if (debugging('dcp-client'))\n {\n console.debug('reliableEvents', reliableEvents);\n console.debug('optionalEvents', optionalEvents);\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 */\n async subscribeCustomEvents ()\n {\n if (!this.listeningForCustomEvents)\n await this.eventSubscriber.subscribeManyEvents([], ['custom'], { filter: { job: this.address } });\n this.listeningForCustomEvents = true\n }\n \n async joinComputeGroups ()\n {\n // localExec jobs are not entered in any compute group.\n if (!this.inLocalExec && this.computeGroups && this.computeGroups.length)\n {\n this.readyStateChange('compute-groups');\n computeGroups.addRef(); // Just in case we're doing a Promise.all on multiple execs.\n\n // Add this job to its currently-defined compute groups (as well as public group, if included)\n let success;\n try\n {\n const cgPayload = await computeGroups.addJobToGroups(this.address, this.computeGroups);\n success = true; // To support older version of CG service where addJobToGroups had void/undefined return.\n if (cgPayload) success = cgPayload.success;\n debugging('dcp-client') && console.debug('job/index: addJobToGroups cgPayload:', cgPayload ? cgPayload : 'cgPayload is not defined; probably from legacy CG service.');\n }\n catch (e)\n {\n debugging('dcp-client') && console.debug('job/index: addJobToGroups threw exception:', e);\n success = false;\n }\n\n computeGroups.closeServiceConnection().catch((err) => {\n console.error('Warning: could not close compute groups service connection', err)\n });\n\n /* Could not put the job in any compute group, even though the user wanted it to run. Cancel the job. */\n if (!success)\n {\n await this.cancel('compute-groups::Unable to join any compute groups');\n throw new DCPError(`Access Denied::Failed to add job ${this.address} to any of the desired compute groups`, 'DCPL-1100');\n }\n }\n }\n \n /**\n * Takes result events as input, stores the result and fires off\n * events on the job handle as required. (result, duplicate-result)\n *\n * @param {object} ev - the event recieved from protocol.listen('/results/0xThisGenAdr')\n */\n async handleResult (ev)\n {\n if (this.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 const { result: _result, time } = ev.result;\n debugging('dcp-client') && console.debug('handleResult', _result);\n let result = await fetchURI(_result);\n\n if (this.results.isAvailable(ev.sliceNumber))\n {\n const changed = JSON.stringify(this.results[ev.sliceNumber]) !== JSON.stringify(result);\n this.emit('duplicate-result', { sliceNumber: ev.sliceNumber, changed });\n }\n\n this.results.newResult(result, ev.sliceNumber);\n this.emit('result', { job: this.address, 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 handleStatus ({ runStatus, total, distributed, computed }, emitStatus = true)\n {\n Object.assign(this.status, {\n runStatus,\n total,\n distributed,\n computed,\n });\n\n if (emitStatus)\n this.emit('status', { ...this.status, job: this.address });\n }\n \n /**\n * Sends a request to the scheduler to deploy the job.\n */\n async deployJob ()\n {\n var moduleDependencies; \n \n /* Send sideloader bundle to the package server */\n if (DCP_ENV.platform === 'nodejs' && this.dependencies.length)\n {\n try\n {\n let { pkg, unresolved } = await this._publishLocalModules();\n\n moduleDependencies = unresolved;\n if (pkg)\n moduleDependencies.push(pkg.name + '/' + sideloaderModuleIdentifier); \n }\n catch(error)\n {\n throw new DCPError(`Error trying to communicate with package manager server: ${error}`);\n }\n }\n else\n moduleDependencies = this.dependencies;\n \n this.readyStateChange('preauth');\n\n const adhocId = this.uuid.slice(this.uuid.length - 6, this.uuid.length);\n const schedId = await dcpConfig.scheduler.identity;\n // The following check is needed for when using dcp-rtlink and loading the config through source, instead of using the dcp-client bundle\n let schedIdAddress = schedId;\n if(schedId.address)\n schedIdAddress = schedId.address;\n const myId = await wallet.getId();\n const preauthToken = await bankUtil.preAuthorizePayment(schedIdAddress, this.maxDeployPayment, this.paymentAccountKeystore);\n const { dataRange, dataValues, dataPattern, sliceCount } = marshalInputData(this.jobInputData);\n if(dataValues)\n this.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: this.workFunctionURI,\n uuid: this.uuid,\n mvMultSlicePayment: Number(this.feeStructure.marketValue) || 0, // @todo: improve feeStructure internals to better reflect v4\n absoluteSlicePayment: Number(this.feeStructure.maxPerRequest) || 0,\n requirePath: this.requirePath,\n dependencies: moduleDependencies,\n requirements: this.requirements, /* capex */\n localExec: this.inLocalExec,\n force100pctCPUDensity: this.force100pctCPUDensity,\n estimationSlices: this.estimationSlices,\n greedyEstimation: this.greedyEstimation,\n workerConsole: this.workerConsole,\n isCI: this.isCI,\n\n description: this.public.description || 'Discreetly making the world smarter',\n name: this.public.name || 'Ad-Hoc Job' + adhocId,\n link: this.public.link || '',\n\n preauthToken, // XXXwg/er @todo: validate this after fleshing out the stub(s)\n\n resultStorageType: this.resultStorageType, // @todo: implement other result types\n resultStorageDetails: this.resultStorageDetails, // Content depends on resultStorageType\n resultStorageParams: this.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(this.jobArguments) && this.jobArguments.length === 1 && this.jobArguments[0] instanceof DcpURL)\n submitPayload.arguments = this.jobArguments[0].href;\n else if (this.jobArguments instanceof RemoteDataSet)\n submitPayload.marshaledArguments = kvinMarshal(this.jobArguments.map(e => new URL(e)))\n else if (this.jobArguments)\n {\n try\n {\n submitPayload.marshaledArguments = kvinMarshal(Array.from(this.jobArguments));\n }\n catch(e)\n {\n throw new Error(`Could not convert job arguments to Array (${e.message})`);\n }\n }\n \n if (this.inLocalExec && !DCP_ENV.isBrowserPlatform)\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(this.workFunctionURI);\n workFunctionFile.writeSync(workFunction);\n \n const workFunctionFileURL = new URL('file://' + workFunctionFile);\n submitPayload.workFunctionURI = workFunctionFileURL.href;\n this.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 {\n dumpObject(submitPayload, 'Submit: Job Index: submitPayload', 256);\n console.debug('Before Deploy', myId);\n }\n\n // Deploy the job! If we get an error, try again a few times until threshold of errors is reached, then actually throw it\n let deployed\n let deployAttempts = 0;\n while (deployAttempts++ < (dcpConfig.job.deployAttempts || 10))\n {\n try\n {\n deployed = await this.useDeployConnection('submit', submitPayload, myId);\n break;\n }\n catch (e)\n {\n if (deployAttempts < 10)\n debugging('dcp-client') && console.debug('Error when trying to deploy job, trying again', e);\n else\n throw e;\n }\n }\n\n if (!deployed.success)\n {\n // close all of the connections so that we don't cause node processes to hang.\n const handleErr = (e) => {\n console.error('Error while closing job connection:');\n console.error(e);\n };\n \n this.closeDeployConnection();\n this.eventSubscriber.close().catch(handleErr);\n computeGroups.closeServiceConnection().catch(handleErr);\n \n // Yes, it is possible for deployed or deployed.payload to be undefined.\n if (deployed.payload)\n {\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 throw new DCPError('Failed to submit job to scheduler', deployed.payload);\n }\n throw new DCPError('Failed to submit job to scheduler (no payload)', deployed ? deployed : '');\n }\n\n debugging('dcp-client') && console.debug('After Deploy', JSON.stringify(deployed));\n\n this.address = deployed.payload.job;\n this.deployCost = deployed.payload.deployCost;\n\n if (!this.status)\n this.status = {\n runStatus: null,\n total: 0,\n computed: 0,\n distributed: 0,\n };\n \n this.status.runStatus = deployed.payload.status;\n this.status.total = deployed.payload.lastSliceNumber;\n this.running = true;\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 close ()\n {\n return this.useDeployConnection('closeJob', {\n job: this.id,\n });\n }\n \n /** Use the connection to job submit service. Will open a new connection if one does not exist,\n * and close the connection if it is idle for more than 10 seconds (tuneable).\n */\n useDeployConnection(...args)\n {\n if (!this.deployConnection)\n {\n this.deployConnection = new protocolV4.Connection(dcpConfig.scheduler.services.jobSubmit); \n this.deployConnection.on('close', () => { this.deployConnection = null; });\n }\n if (this.deployConnectionTimeout)\n clearTimeout(this.deployConnectionTimeout);\n \n const deployPromise = this.deployConnection.send(...args);\n \n this.deployConnectionTimeout = setTimeout(() => this.deployConnection.close(), (dcpConfig.job.deployCloseTimeout || 10 * 1000));\n if (!ON_BROWSER)\n this.deployConnectionTimeout.unref();\n \n return deployPromise;\n }\n \n /**\n * Close the connection to the job submit (if it exists), and clear the close timeout (if needed).\n */\n closeDeployConnection()\n {\n if (this.deployConnection)\n this.deployConnection.close();\n if (this.deployConnectionTimeout)\n clearTimeout(this.deployConnectionTimeout);\n }\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{\n if (!(data instanceof Object || data instanceof SuperRangeObject))\n throw new TypeError(`Invalid job data type: ${typeof data}`);\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 {\n marshalledInputData.dataPattern = data['pattern'];\n marshalledInputData.sliceCount = data['sliceCount'];\n }\n\n log('marshalledInputData:', marshalledInputData);\n return marshalledInputData;\n}\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\n\n\nexports.Job = Job;\nexports.SlicePaymentOffer = SlicePaymentOffer;\nexports.ResultHandle = ResultHandle;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/job/index.js?");
4172
4172
 
4173
4173
  /***/ }),
4174
4174
 
@@ -4189,7 +4189,7 @@ eval("/**\n * @file node-modules.js Node-specific support for sen
4189
4189
  \*********************************************/
4190
4190
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4191
4191
 
4192
- eval("/**\n * @file job/result-handle.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date June 2020\n *\n * The ResultHandle acts as a proxy for a job's results, querying\n * internal results when available or the scheduler when the results\n * are not available locally.\n */\n\nconst { rehydrateRange } = __webpack_require__(/*! ../range-object */ \"./src/dcp-client/range-object.js\");\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { fetchURI } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.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 if (ind === -1)\n throw new DCPError('Argument passed into the function was not found in input set', 'ERANGE');\n\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), dcpConfig.scheduler.location.origin);\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?");
4192
+ eval("/**\n * @file job/result-handle.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date June 2020\n *\n * The ResultHandle acts as a proxy for a job's results, querying\n * internal results when available or the scheduler when the results\n * are not available locally.\n */\n\nconst { rehydrateRange } = __webpack_require__(/*! ../range-object */ \"./src/dcp-client/range-object.js\");\nconst protocolV4 = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\nconst { fetchURI } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst customInspectSymbol = Symbol.for('nodejs.util.inspect.custom');\n\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{\n constructor(job)\n {\n super();\n\n const { Job } = __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 * 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.job = job;\n this.realKeys = job.jobInputData;\n this.realValues = [];\n this.valuesAvailable = [];\n\n return new Proxy(this, {\n get: (target, name) => {\n if ((typeof name === 'string' || typeof name === 'number') && Number.isInteger(parseFloat(name)))\n {\n let i = parseFloat(name);\n if (target.isAvailable(i))\n return target.realValues[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 {\n let values = target.values();\n return values[name].bind(values);\n }\n else\n {\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 {\n return JSON.stringify(this.values());\n }\n\n isAvailable(index)\n {\n return this.valuesAvailable[index];\n }\n\n reset() {\n // quickly empty the realValues array\n this.realValues.length = this.valuesAvailable.length = 0;\n this.realValues.length = this.valuesAvailable.length =this.realKeys.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.valuesAvailable.reduce((keysList, valueAvailable, sliceNumber) => {\n if (valueAvailable) keysList.push(this.realKeys[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.realValues.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.valuesAvailable.reduce((n, v) => (n + (v ? 1 : 0)), 0);\n }\n \n [customInspectSymbol]() /* overrides the output when console.log(result) is called */\n { \n return this.values(); \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.valuesAvailable.reduce((keyValuePairs, valueAvailable, sliceNumber) => {\n if (valueAvailable) keyValuePairs.push([\n String(this.realKeys[sliceNumber - 1]),\n this.realValues[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.realKeys[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 if (ind === -1)\n throw new DCPError('Argument passed into the function was not found in input set', 'ERANGE');\n\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.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.realValues[r.slice] = await fetchURI(decodeURIComponent(r.value), dcpConfig.scheduler.location.origin);\n\n if (emitEvents && (emitEvents === 'all' || !this.valuesAvailable[r.slice])) {\n job.emit('result', { sliceNumber: r.slice, result: this.realValues[r.slice] });\n }\n this.valuesAvailable[r.slice] = true;\n }));\n\n job.emit('resultsUpdated');\n await conn.close(null, true);\n }\n \n /**\n * Handle adding a new result\n */\n newResult(result, index)\n {\n if (this.valuesAvailable[index])\n throw new Error(`ResultHandle: result ${index} already exists, cannot add it as new result.`)\n this.realValues[index] = result;\n this.valuesAvailable[index] = 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?");
4193
4193
 
4194
4194
  /***/ }),
4195
4195
 
@@ -4203,13 +4203,23 @@ eval("/**\n * @file job/slice-payment-offer.js\n * @author Ryan Ross
4203
4203
 
4204
4204
  /***/ }),
4205
4205
 
4206
+ /***/ "./src/dcp-client/job/upload-slices.js":
4207
+ /*!*********************************************!*\
4208
+ !*** ./src/dcp-client/job/upload-slices.js ***!
4209
+ \*********************************************/
4210
+ /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4211
+
4212
+ eval("/**\n * @file job/upload-slices.js\n * @author Ryan Saweczko, ryansaweczko@kingsds.network\n * Danny Akbarzadeh, danny@kingsds.network\n * \n * @date Jun 2022\n *\n * Implement functions to upload slices to the scheduler after a job has been deployed.\n * This area will have it's own connection to the job submit service which it is responsible\n * for handling.\n */\n\nconst { Connection } = __webpack_require__(/*! dcp/protocol-v4 */ \"./src/protocol-v4/index.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('dcp-client');\n\nlet uploadConnection = null;\nlet uploadRefs = 0;\nlet tunedKvin;\n\nfunction createNewConnection()\n{\n uploadConnection = new Connection(dcpConfig.scheduler.services.jobSubmit);\n debugging('slice-upload') && console.debug('025: opening new uploadConnection', uploadConnection.id);\n uploadConnection.on('close', () => {\n debugging('slice-upload') && console.debug('027: uploadConnection is closing', uploadConnection?.id);\n uploadConnection = null;\n });\n return uploadConnection.connect();\n}\n\n/*\nHelper function: reprepare a message if a connection breaks before error tolerance\n*/\nfunction reprepareMessage(details)\n{\n return uploadConnection.prepare('addSliceData', {\n job: details.job,\n dataValues: kvinMarshal(details.pile),\n });\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 {object} pileMessage \n * @param {object} originalMessageDetails - used if the connection breaks and we need to reprepare the message.\n * @returns payload containing success property (pertaining to success of adding slices to job) as well as lastSliceNumber of job \n */\nasync function safeSliceUpload(pileMessage, originalMessageDetails)\n{\n debugging('slice-upload') && console.debug('055: safeSliceUpload()', {\n slices: originalMessageDetails.pile.length,\n job: originalMessageDetails.job,\n });\n let payload = undefined;\n let errorTolerance = dcpConfig.job.sliceUploadErrorTolerance; // copy number of times we will tolerate non-success when uploading slices directly from config\n\n while (true) // eslint-disable-line no-constant-condition\n {\n try\n {\n const start = Date.now();\n debugging('slice-upload') && console.log('x-dbg-uploadStart', {length:pileMessage.signedMessage.length});\n // If the connection closes after we prepare the message but before our error tolerance is reached, re-prepare and continue trying to send message.\n if (!uploadConnection)\n {\n debugging('slice-upload') && console.log('x-dbg-openNewConnection');\n await createNewConnection();\n pileMessage = await reprepareMessage(originalMessageDetails); /* Assuming preparing a identical message will result in the same length */\n }\n\n debugging('slice-upload') && console.debug('070: sending addSliceData request...');\n payload = await uploadConnection.sendPreparedMessage(pileMessage);\n debugging('slice-upload') && console.debug('072: success=', { success: payload?.success, payload: payload?.payload || payload });\n if (!payload.success)\n {\n debugging('slice-upload') && console.log('x-dbg-uploadBackoff', {length:pileMessage.signedMessage.length});\n throw payload.payload;\n }\n else\n {\n debugging('slice-upload') && console.log('x-dbg-uploadProgress', { el: Date.now() - start});\n break;\n }\n }\n catch (error)\n {\n debugging('slice-upload') && console.debug('080: caught upload error:', error?.code, error?.message || error, `${errorTolerance-1} tries left...`);\n if (uploadConnection) {\n debugging('slice-upload') && console.debug(`089: closing connection`, uploadConnection.id);\n await uploadConnection.close(); // clean up the connection on upload error\n debugging('slice-upload') && console.debug(`091: closed`, uploadConnection?.id);\n }\n\n if (--errorTolerance <= 0)\n {\n debugging('slice-upload') && console.log('x-dbg-uploadError');\n throw error;\n }\n \n if (error.slicesSaved) {\n const newPile = originalMessageDetails.pile.slice(error.slicesSaved);\n\n debugging('slice-upload') && console.debug(`103: recursing into addSlices with a new pile of ${newPile.length}...`)\n // recursively call back into addSlices, with only the unsaved remainder of this pile\n const retry = await addSlices(newPile, originalMessageDetails.job);\n\n // if that worked, then break out of this loop\n if (retry.success) {\n payload = {\n success: retry.success,\n payload: retry,\n };\n break;\n }\n }\n }\n }\n debugging('slice-upload') && console.debug(`120< safeSliceUpload() is returning`, {\n success: payload.success,\n payload: payload.payload,\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* @param {*} jobAddress Address of job to upload the slices to \n* @returns payload containing success property (pertaining to success of adding slices to job) as well as lastSliceNumber of job\n*/\nasync function sliceUploadLogic(pile, mostToTake, jobAddress)\n{\n debugging('slice-upload') && console.debug(`111: sliceUploadLogic(), ${pile.length} slices, up to ${mostToTake}`);\n if (!uploadConnection)\n await createNewConnection();\n\n const slicesTaken = pile.length;\n let pileMessage = await uploadConnection.prepare('addSliceData', {\n job: jobAddress,\n dataValues: kvinMarshal(pile),\n });\n \n let pileSize = pileMessage.signedMessage.length;\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 safeSliceUpload(pileMessage, { job: jobAddress, 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 safeSliceUpload(pileMessage, { job: jobAddress, 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 safeSliceUpload(pileMessage, { job: jobAddress, 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 safeSliceUpload(pileMessage, { job: jobAddress, pile });\n newMostToTake = Math.ceil(mostToTake / ((2 / 3) * dcpConfig.job.uploadIncreaseFactor));\n }\n else\n throw new Error('hopefully impossible code in slice upload logic');\n\n let payload = uploadedSlices ? uploadedSlices.payload : undefined;\n debugging('slice-upload') && console.debug(`198< sliceUploadLogic() is returning`, { payload, newMostToTake });\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* @param {*} jobAddress Address of job these slices are for\n* @param {*} newTunedKvin undefined, or new version of kvin tuned for speed or size specifically. Use if defined.\n* @returns payload containing success property (pertaining to success of adding slices to job) as well as lastSliceNumber of job\n*/\nasync function addSlices(dataValues, jobAddress, newTunedKvin)\n{\n debugging('slice-upload') && console.debug(`178: addSlices:${uploadRefs}()`, {\n slices: dataValues.length,\n job: jobAddress,\n })\n if (newTunedKvin)\n tunedKvin = newTunedKvin;\n\n if (!Array.isArray(dataValues))\n throw new TypeError('Only data-by-value jobs may dynamically add slices');\n\n let mostToTake = dcpConfig.job.uploadInitialNumberOfSlices; // 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 uploadRefs++;\n\n for (let slice of dataValues)\n {\n pile.push(slice);\n slicesTaken++;\n if (slicesTaken === mostToTake)\n {\n let total = await sliceUploadLogic(pile, mostToTake, jobAddress);\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 addSlices(pile, jobAddress);\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 }\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 sliceUploadLogic(pile, mostToTake, jobAddress);\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 addSlices(pile, jobAddress);\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 if (!payload) {\n console.warn(`280: payload was somehow not an Object...`, payload);\n payload = {};\n }\n payload.mostToTake = mostToTake;\n\n // cleanup connection when done with this function and all recursive calls\n if (uploadConnection && uploadRefs === 1) {\n debugging('slice-upload') && console.debug(`250: closing uploadConnection`, uploadConnection?.id);\n await uploadConnection.close();\n debugging('slice-upload') && console.debug(`252: closed uploadConnection`, uploadConnection?.id);\n }\n uploadRefs--;\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 debugging('slice-upload') && console.log(`294< addSlices:${uploadRefs}() is returning:`, payload);\n return payload;\n}\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\n\nexports.addSlices = addSlices;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/job/upload-slices.js?");
4213
+
4214
+ /***/ }),
4215
+
4206
4216
  /***/ "./src/dcp-client/range-object.js":
4207
4217
  /*!****************************************!*\
4208
4218
  !*** ./src/dcp-client/range-object.js ***!
4209
4219
  \****************************************/
4210
4220
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4211
4221
 
4212
- eval("/**\n * @file range-object.js\n * @author Eddie Roosenmaallen, eddie@kingsds.network\n * Matthew Palma, mpalma@kingsds.network\n * @date October 2018\n * This module exports classes and methods for working with RangeObjects\n *\n * RangeObject:\n * new RangeObject(start, end[, step[, group]])\n * new RangeObject({ start, end[, step[, group]] })\n * new RangeObject('[object RangeObject {start},{end},{step},{group}]')\n *\n * In the first two forms, step and group are optional, defaulting to 1. In the string form, all paramteres are required\n *\n *\n * Methods:\n * toString() Returns a string descriptor, suitable to pass to a new constructor\n * toObject() Returns a vanilla ES object with the four parameters as properties\n * toArray() Resolve all values from the RangeObject, grouping if group > 1, as an array\n * nthValue(n) Look up a single value/group\n * slice([start[, end]]) Resolve a selection of values from start to end (exclusive).\n * Same semantics as Array.prototype.slice, including negative indexes\n * materialize(now) Resolve all values and store as an array (eg. for random distributions)\n *\n * Properties:\n * length Return the total number of values (or groups, if group > 1) generasted by the RangeObject\n * dist Name of the distributor function\n * values If present, the Distribution has been materialized\n * materializeOnScheduler If present and truthy, the distribution will be materialized on the scheduler before\n * instantiating tasks\n *\n * Examples:\n * bob = new RangeObject(1, 10)\n * bob.toArray() // [1,2,3,4,5,6,7,8,9,10]\n * bob.slice(-2) // [9,10]\n * fred = new RangeObject({ start:1, end: 1000, step: 3})\n * fred.toArray() // [1,4,7 (...) 994, 997, 1000]\n * fred.toObject() // { start: 1, end: 1000, step: 3, group: 1}\n * bill = new RO.RangeObject(0, 999, 3, 3)\n * rangeobject.js?1541083657962:42 Uncaught RangeError: Range must be divisible evenly into groups\n * at new RangeObject (rangeobject.js?1541083657962:42)\n * at <anonymous>:1:8\n * bill = new RO.RangeObject(0, 998, 3, 3)\n * bill.toArray() // [[0, 3, 6] (...), [990, 993, 996]]\n *\n * MultiRangeObject:\n * new MultiRangeObject(rol1, rol2, ... roln)\n * new MultiRangeObject([rol1, rol2, ... roln])\n * Each argument is either a RangeObject, an Array-like object (, or a Distribution object? NYI ~ER). The\n * values generated by the MRO will be a multi-dimentional array, where each element of the array is a vector\n * across all input ranges.\n */\n\n// Some utility functions to make validating the parameters easier\nconst assert = (premise, msg) => {\n if (!premise) throw new Error(msg);\n}\n\nconst assertNumeric = (val, msg) => {\n assert(typeof val === 'number', msg);\n assert(!Number.isNaN(val), msg);\n assert(Number.isFinite(val), msg);\n}\n\nconst RANGEOBJ_REGEX = /^\\[object RangeObject (-?\\d+(?:\\.\\d+)?),(-?\\d+(?:\\.\\d+)?),(-?\\d+(?:\\.\\d+)?),(\\d+|undefined)\\]$/;\n\n/** \n * Defines a consistent interface for each of the range object types\n * to inherit from, provides some array methods.\n * @access public\n */\nclass SuperRangeObject {\n constructor() {\n return new Proxy(this, {\n get: (target, name) => {\n if ((typeof name === 'string' || typeof name === 'number') && Number.isInteger(parseFloat(name))) {\n return target.nthValue(parseFloat(name));\n } else {\n return target[name];\n }\n }\n });\n }\n\n [Symbol.iterator]() {\n let index = 0;\n\n return {\n next: () => ({ value: this.nthValue(index++), done: index > this.length })\n };\n }\n\n get length() {\n return 0;\n }\n\n nthValue(n) {\n throw new Error(\"nthValue not overridden\");\n }\n\n toArray() {\n return this.slice()\n }\n\n /**\n * Generate array from range starting at value `start` and ending at value `end` via `nthValue`.\n * @param {number} [start=0] index to start slice\n * @param {number} [end] index to end slice, return rest of array if not provided.\n * @access public\n */\n slice(start, end) {\n if (typeof start === 'undefined') { start = 0 } else if (start < 0) { start += this.length }\n\n if (typeof end === 'undefined') { end = this.length } else if (end < 0) { end += (this.length) }\n\n if (end > this.length) { end = this.length }\n\n const arr = []\n for (let i = start; i < end; i++) { arr.push(this.nthValue(i)) }\n\n return arr\n }\n\n /**\n * Converts range to an Array and then calls `filter(...args)` on it.\n * @param {...any} args Same args as you would pass to Array#filter\n * @access public\n */\n filter(...args) {\n return this.toArray().filter(...args);\n }\n}\n\n/**\n * Range objects are vanilla ES objects used to describe value range sets for use by `compute.for()`.\n * The range must be increasing, i.e. `start` must be less than `end`.\n * Calculations made to derive the set of numbers in a range are carried out with `BigNumber`, \n * eg. arbitrary-precision, support. The numbers `Infinity` and `-Infinity` are not supported, and \n * the API does not differentiate between `+0` and `-0`.\n */\nclass RangeObject extends SuperRangeObject {\n /**\n * An object which represents a range of values.\n * @param {number|object} startOrObject Beginning of range, or object literal with `start` and `end` properties.\n * @param {number} end End of range\n * @param {number} [step=1] Step size in range\n * @param {number|undefined} [group] Groups in range\n * @access public\n * @extends SuperRangeObject\n * @example <caption>With implicit step size = 1.</caption>\n * let rangeObject = new RangeObject(0.5,3);\n * rangeObject.toArray();\n * // [ 0.5, 1.5, 2.5 ]\n * \n * @example <caption>With explicit step size.</caption>\n * let rangeObject = new RangeObject(1,9,3);\n * rangeObject.toArray();\n * // [ 1, 4, 7 ]\n * \n * @example <caption>With grouping.</caption>\n * let rangeObject = new RangeObject(1,9,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7 ] ]\n */\n constructor (start, end, step, group) {\n super();\n if (typeof start === 'string' && start.match(RANGEOBJ_REGEX)) {\n const parts = start.match(RANGEOBJ_REGEX)\n start = {\n start: parseFloat(parts[1]),\n end: parseFloat(parts[2]),\n step: parseFloat(parts[3]),\n group: (parts[4] === 'undefined'? undefined : parseFloat(parts[4]))\n }\n }\n\n if (typeof start === 'object') {\n this.start = start.start;\n this.end = start.end;\n this.step = start.step;\n this.group = start.group;\n } else {\n this.start = start;\n this.end = end;\n this.step = step;\n this.group = group;\n }\n\n assertNumeric(this.start, `Invalid start parameter \"${this.start}\", must be numeric and finite.`);\n assertNumeric(this.end, `Invalid end parameter \"${this.end}\", must be numeric and finite.`);\n\n // Ensure step moves in the correct direction for start-end (ie, negative if end < start)\n if (typeof this.step === 'undefined') {\n this.step = Math.sign(this.end - this.start);\n } else {\n assertNumeric(this.step, `Invalid step parameter \"${this.step}\", must be numeric and finite.`);\n if ((this.step === 0 && this.start !== this.end) || Math.sign(this.step) !== Math.sign(this.end - this.start)) {\n throw new Error('RangeObject step must approach end from start.');\n }\n }\n\n if (typeof this.group !== 'undefined') {\n // group must remain undefined if not provided because no grouping should occur if not provided.\n // As per spec, even if group is 1 it should group into arrays of 1 element\n assertNumeric(this.group, `Invalid group parameter \"${this.group}\", must be numeric and finite.`);\n assert(Number.isInteger(this.group), `Invalid group parameter \"${this.group}\", must be an integer.`);\n assert(this.group > 0, `Invalid group parameter \"${this.group}\", must be greater than zero.`);\n }\n }\n\n /**\n * @typedef {object} RangeLike\n * @property {number} start\n * @property {number} end\n * @property {number} nthValue\n */\n\n /**\n * @returns {boolean}\n */\n static isRangelike (r) {\n if (r instanceof RangeObject) { return true }\n if (typeof r === 'object' &&\n typeof r.start === 'number' &&\n typeof r.end === 'number' &&\n typeof r.nthValue === 'function') { return true }\n\n return false\n }\n\n /**\n * @returns {boolean}\n */\n static isRangeObject(r) {\n if (r instanceof RangeObject) { return true }\n\n return false\n }\n\n /**\n * Test whether a value can be passed to the RangeObject constructor\n * @param r Value to test\n * @param strict Optional. Truthy to disallow objects which already look Rangelike\n */\n static isProtoRangelike (r, strict = true) {\n if (typeof r === 'object' &&\n typeof r.start === 'number' &&\n typeof r.end === 'number') { return true }\n\n if (typeof r === 'string' &&\n r.match(RANGEOBJ_REGEX)) { return true }\n\n if (!strict && RangeObject.isRangelike(r)) { return true }\n\n return false\n }\n\n /**\n * Create string representation of range: [object RangeObject start,end,step,group]\n * @access public\n */\n toString () {\n return `[object RangeObject ${this.start},${this.end},${this.step},${this.group}]`\n }\n\n /**\n * Create object literal for range with properties: start, end, step, and group.\n * @access public\n */\n toObject () {\n return {\n start: this.start,\n end: this.end,\n step: this.step,\n group: this.group\n }\n }\n\n /**\n * Return nth value in range\n * @param {number} n\n * @access public\n * @example\n * let rangeObject = new RangeObject(1,10,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7, 10 ] ]\n * rangeObject.nthValue(1);\n * // [ 7, 10 ]\n */\n nthValue(n) {\n /**\n * `>=` since the value at index 7 in an array that's of length 7 is outside\n * its range\n */\n if (n < 0 || n >= this.length) {\n return undefined;\n }\n\n if (typeof this.group !== 'undefined') {\n const start = (this.group * this.step * n) + this.start\n const arr = []\n\n for (let i = 0; i < this.group && i + this.group * n < this.stepCount; i++) {\n arr.push(start + (i * this.step))\n }\n\n return arr;\n }\n\n return this.start + (n * this.step);\n }\n\n /**\n * Return max value in range\n * @access public\n * @returns {number}\n * @example\n * let rangeObject = new RangeObject(1,10,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7, 10 ] ]\n * rangeObject.max;\n * // 10\n */\n get max () {\n if (typeof this.group === 'undefined' && this.step === 1) { return this.end }\n\n let tail\n if (typeof this.group === 'undefined') {\n [ tail ] = this.slice(-1)\n } else {\n [ tail ] = this.slice(-1)[0].slice(-1)\n }\n return tail\n }\n\n /**\n * Boolean indicating whether all groups are filled.\n * Only defined for RangeObjects that group results.\n * @access public\n * @returns {boolean}\n * @example <caption>With remainder</caption>\n * let rangeObject = new RangeObject(1,9,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7 ] ]\n * rangeObject.hasRemainder;\n * // true\n * @example <caption>Without remainder</caption>\n * let rangeObject = new RangeObject(1,10,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7, 10 ] ]\n * rangeObject.hasRemainder;\n * // true\n */\n get hasRemainder () {\n if (typeof this.group === 'undefined') { return false }\n const groups = this.stepCount / this.group;\n\n return (groups !== Math.floor(groups));\n }\n\n /**\n * Number of elements in range, or number of groups if grouped.\n * @access public\n * @returns {number}\n * @example <caption>Without grouping</caption>\n * let rangeObject = new RangeObject(1,10,3);\n * rangeObject.toArray();\n * // [ 1, 4, 7, 10 ]\n * rangeObject.length;\n * // 4\n * @example <caption>With grouping</caption>\n * let rangeObject = new RangeObject(1,9,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7 ] ]\n * rangeObject.length;\n * // 2\n */\n get length () {\n return Math.ceil(this.stepCount / (this.group || 1));\n }\n\n /**\n * Number of steps in range (sort of like number of elements, except grouping is no longer relevant).\n * @access public\n * @returns {number}\n * @example <caption>Without grouping</caption>\n * let rangeObject = new RangeObject(1,10,3);\n * rangeObject.toArray();\n * // [ 1, 4, 7, 10 ]\n * rangeObject.stepCount;\n * // 4\n * @example <caption>With grouping</caption>\n * let rangeObject = new RangeObject(1,9,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7 ] ]\n * rangeObject.length;\n * // 3\n */\n get stepCount () {\n if (this.step === 0) return 1;\n return Math.floor(Math.abs((this.end - this.start) / this.step)) + 1;\n }\n}\n\n/**\n * Range objects are vanilla ES objects used to describe value range sets for use by `compute.for()`. \n * Calculations made to derive the set of numbers in a range are carried out with `BigNumber`, \n * eg. arbitrary-precision, support. The numbers `Infinity` and `-Infinity` are not supported, and \n * the API does not differentiate between `+0` and `-0`.\n */\nclass MultiRangeObject extends SuperRangeObject {\n /**\n * A multi-range object contains many {@link RangeObject}s. They are iterated over \n * with the fastest moving index going over the right-most range object in array order. Each element\n * of a multi range is a tuple of values from constituent ranges.\n * @param {RangeObject|RangeObject[]|object} arg - First range object, or array of range objects, or object with `ranges` key containing an array of range objects.\n * @param {RangeObject} rangeObject - If first argument is a RangeObject, subsquent arguments are range objects too.\n * @access public\n * @extends SuperRangeObject\n * @example\n * r0 = new RangeObject(1,2)\n * r1 = new RangeObject(1,3)\n * mro = new MultiRangeObject(r0, r1)\n * mro.toArray()\n * // [ [ 1, 1 ], [ 1, 2 ], [ 1, 3 ], [ 2, 1 ], [ 2, 2 ], [ 2, 3 ] ]\n */\n constructor () {\n super();\n var ranges = []\n\n if (arguments.length === 1 && typeof arguments[0] === 'string') {\n const inputs = JSON.parse(arguments[0])\n if (Array.isArray(inputs)) {\n ranges = inputs\n } else if (inputs.ranges) {\n ranges = inputs.ranges\n } else {\n ranges = [inputs]\n }\n } else if (arguments.length === 1 && Array.isArray(arguments[0])) {\n ranges = [...arguments[0]]\n } else if (arguments.length === 1 && !!arguments[0].ranges) {\n ranges = [...arguments[0].ranges]\n } else {\n ranges = [...arguments]\n }\n\n this.ranges = ranges.map(r => {\n if (RangeObject.isRangelike(r)) { return r }\n if (RangeObject.isRangeObject(r)) { return r }\n if (DistributionRange.isDistribution(r)) { return r }\n if (RangeObject.isProtoRangelike(r)) { return new RangeObject(r) }\n if (DistributionRange.isProtoDistribution(r)) { return new DistributionRange(r) }\n\n return Array.isArray(r) ? r : [r]\n })\n }\n\n /**\n * Test whether a value can be passed to the MultiRangeObject constructor\n * @param r Value to test\n * @param strict Optional. Truthy to disallow objects which already look Rangelike\n */\n static isProtoMultiRangelike (r, strict = true) {\n if (typeof r === 'object' &&\n Array.isArray(r.ranges)) { return true }\n\n return false\n }\n\n /**\n * Create string representation of this MultiRangeObject\n * @access public\n * @example\n * \"[object MultiRangeObject ' + this.ranges.length + ']\"\n */\n toString () {\n return '[object MultiRangeObject ' + this.ranges.length + ']'\n }\n\n /**\n * Create object literal with `ranges` property containing array of range objects.\n */\n toObject () {\n return { ranges: this.ranges }\n }\n\n /**\n * Returns a tuple of values from the ranges given by this multi range object.\n * @param {number} n index of multi-range tuple to return\n * @access public\n * @example\n * r0 = new RangeObject(1,2)\n * r1 = new RangeObject(1,3)\n * mro = new MultiRangeObject(r0, r1)\n * mro.toArray()\n * // [ [ 1, 1 ], [ 1, 2 ], [ 1, 3 ], [ 2, 1 ], [ 2, 2 ], [ 2, 3 ] ]\n * mro.nthValue(2)\n * // [ 1, 3 ]\n */\n nthValue (n) {\n if (n < 0 || n >= this.length) { return undefined }\n\n const indexes = []\n\n for (let r = (this.ranges.length - 1); r >= 0; r--) {\n const idx = n % this.ranges[r].length\n\n indexes.unshift(idx)\n\n n -= idx\n n /= this.ranges[r].length\n }\n\n const values = []\n\n for (let i = 0; i < indexes.length; i++) {\n values[i] = Array.isArray(this.ranges[i]) ? this.ranges[i][indexes[i]] : this.ranges[i].nthValue(indexes[i])\n }\n\n return values\n }\n\n /**\n * Boolean indicating whether any of the ranges in this multi-range object has a remainder. See {@link RangeObject#hasRemainder}.\n * @access public\n * @returns {boolean}\n */\n get hasRemainder () {\n for (let r of this.ranges) {\n if (r.hasRemainder) { return true }\n }\n\n return false\n }\n\n get length () {\n let len = 1\n\n this.ranges.forEach((r) => len *= r.length)\n\n return len\n }\n}\n\n// DistributionRange object wraps a distributing function into a RangeObject-like API\n// which can be dropped directly into a MultiRangeObject to generate input slices\nclass DistributionRange extends SuperRangeObject {\n constructor (n, dist, ...params) {\n super();\n\n const { distributor } = __webpack_require__(/*! ./stats-ranges */ \"./src/dcp-client/stats-ranges.js\");\n\n // If argv[0] is a string formatted as DistributionRange.toString(), then unpack it\n if (typeof n === 'string' && n.match(/^\\[object DistributionRange (\\w+?)\\((\\d+?)(?:,(.+?))?\\)\\]$/)) {\n const parts = n.match(/^\\[object DistributionRange (\\w+?)\\((\\d+?)(?:,(.+?))?\\)\\]$/)\n dist = parts[1]\n n = parseInt(parts[2])\n params = (parts[3] || '').split(',').map(e => parseFloat(e))\n }\n\n // If argv[0] is a string describing a DistributionRange, then unpack it\n if (typeof n === 'string' && n.match(/^(\\w+?)\\((\\d+?)(?:,(.+?))?\\)$/)) {\n const parts = n.match(/^(\\w+?)\\((\\d+?)(?:,(.+?))?\\)$/)\n dist = parts[1]\n n = parseInt(parts[2])\n params = (parts[3] || '').split(',').map(e => parseFloat(e))\n }\n\n // If argv[0] is a object of the right shape, then unpack it\n if (typeof n === 'object' &&\n typeof n.length === 'number' &&\n typeof n.dist === 'string' &&\n Array.isArray(n.params)) {\n // console.log('Unpacking proto-object', n)\n dist = n.dist\n params = n.params\n n = n.length\n if (Array.isArray(n.values)) { this.values = n.values }\n if (typeof n.materializeOnScheduler === 'boolean') { this.materializeOnScheduler = n.materializeOnScheduler }\n }\n\n Object.defineProperty(this, 'length', {\n value: n,\n enumerable: true\n })\n Object.defineProperty(this, 'dist', {\n value: dist,\n enumerable: true\n })\n Object.defineProperty(this, 'params', {\n value: params || [],\n enumerable: true\n })\n\n if (typeof distributor[dist] !== 'function') {\n // console.log({n,dist,params})\n throw new TypeError('dist param must point to an exported distributing function')\n }\n }\n\n /**\n * @returns {boolean}\n */\n static isDistribution (d) {\n return d instanceof DistributionRange\n }\n\n static isDistributionLike (d) {\n if (DistributionRange.isDistribution(d)) { return true }\n if (typeof d === 'object' &&\n typeof d.nthValue === 'function' &&\n typeof d.slice === 'function') { return true }\n\n return false\n }\n\n static isProtoDistribution (d) {\n if (typeof d === 'string' && d.match(/^\\[object DistributionRange (\\w+?)\\((\\d+?)(?:,(.+?))?\\)\\]$/)) { return true }\n if (typeof d === 'string' && d.match(/^(\\w+?)\\((\\d+?)(?:,(.+?))?\\)$/)) { return true }\n if (typeof d === 'object' &&\n typeof d.length === 'number' &&\n typeof d.dist === 'string' &&\n Array.isArray(d.params)) { return true }\n\n return false\n }\n\n toString () {\n return `[object DistributionRange ${this.dist}(${[this.length, ...this.params].join()})]`\n }\n\n toObject () {\n return {\n length: this.length,\n dist: this.dist,\n params: this.params,\n materializeOnScheduler: this.materializeOnScheduler || undefined,\n values: this.values || undefined\n }\n }\n\n nthValue (n) {\n if (n < 0 || n >= this.length) { return undefined }\n\n if (this.values) { return this.values[n] }\n\n const fn = distributor[this.dist]\n\n if (typeof fn === 'function') { return fn.apply(fn, [n, this.length, ...this.params]) }\n\n return undefined\n }\n\n /** Resolve the distribution to a static array\n * @param now If false, then set a flag to materialize on the scheduler. Default: materialize now\n */\n materialize (now = true) {\n if (now === false) { return this.materializeOnScheduler = true }\n\n this.values = this.toArray()\n }\n}\n\n/** Rehydrate an input range from a vanilla ES5 object to an appropriate rangelike object\n * @param obj Serialized job.data object (or JSON string)\n * @return as appropriate, a RangeObject, DistributionRange, MultiRangeObject, or array\n */\nfunction rehydrateRange (obj) {\n const { RemoteDataPattern } = __webpack_require__(/*! dcp/dcp-client/remote-data-pattern */ \"./src/dcp-client/remote-data-pattern.js\");\n const { RemoteDataSet } = __webpack_require__(/*! dcp/dcp-client/remote-data-set */ \"./src/dcp-client/remote-data-set.js\");\n\n if (typeof obj === 'string') {\n obj = JSON.parse(obj)\n }\n\n if (typeof obj === 'number') {\n return obj\n }\n\n if (obj instanceof RangeObject ||\n obj instanceof MultiRangeObject ||\n obj instanceof RemoteDataSet ||\n obj instanceof RemoteDataPattern ||\n obj instanceof DistributionRange) {\n return obj;\n }\n\n // If obj looks like a RemoteDataSet, make one of those\n if (RemoteDataSet.isProtoRemoteDataSetLike(obj)) {\n return new RemoteDataSet(obj)\n }\n \n // If obj looks like a RemoteDataPattern, make one of those\n if (RemoteDataPattern.isProtoRemoteDataPatternLike(obj)) {\n return new RemoteDataPattern(obj.pattern, obj.sliceCount)\n }\n\n // If obj is an iterable, coerce it to an array\n if (Symbol.iterator in Object(obj)) {\n return Array.from(obj)\n }\n\n // If obj looks like a MultiRangeObject, make one of those\n if (MultiRangeObject.isProtoMultiRangelike(obj)) {\n return new MultiRangeObject(obj)\n }\n\n // If obj looks rangelike, make a RangeObject\n if (RangeObject.isProtoRangelike(obj)) {\n return new RangeObject(obj)\n }\n\n // If obj looks like a proto-distribution, make a DistributionRange\n if (DistributionRange.isProtoDistribution(obj)) {\n return new DistributionRange(obj)\n }\n\n throw new TypeError(`obj cannot be cast to any supported Rangelike object: ${JSON.stringify(obj)}`)\n}\n\nexports.SuperRangeObject = SuperRangeObject;\nexports.RangeObject = RangeObject;\nexports.MultiRangeObject = MultiRangeObject;\nexports.DistributionRange = DistributionRange;\nexports.rehydrateRange = rehydrateRange;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/range-object.js?");
4222
+ eval("/**\n * @file range-object.js\n * @author Eddie Roosenmaallen, eddie@kingsds.network\n * Matthew Palma, mpalma@kingsds.network\n * @date October 2018\n * This module exports classes and methods for working with RangeObjects\n *\n * RangeObject:\n * new RangeObject(start, end[, step[, group]])\n * new RangeObject({ start, end[, step[, group]] })\n * new RangeObject('[object RangeObject {start},{end},{step},{group}]')\n *\n * In the first two forms, step and group are optional, defaulting to 1. In the string form, all paramteres are required\n *\n *\n * Methods:\n * toString() Returns a string descriptor, suitable to pass to a new constructor\n * toObject() Returns a vanilla ES object with the four parameters as properties\n * toArray() Resolve all values from the RangeObject, grouping if group > 1, as an array\n * nthValue(n) Look up a single value/group\n * slice([start[, end]]) Resolve a selection of values from start to end (exclusive).\n * Same semantics as Array.prototype.slice, including negative indexes\n * materialize(now) Resolve all values and store as an array (eg. for random distributions)\n *\n * Properties:\n * length Return the total number of values (or groups, if group > 1) generasted by the RangeObject\n * dist Name of the distributor function\n * values If present, the Distribution has been materialized\n * materializeOnScheduler If present and truthy, the distribution will be materialized on the scheduler before\n * instantiating tasks\n *\n * Examples:\n * bob = new RangeObject(1, 10)\n * bob.toArray() // [1,2,3,4,5,6,7,8,9,10]\n * bob.slice(-2) // [9,10]\n * fred = new RangeObject({ start:1, end: 1000, step: 3})\n * fred.toArray() // [1,4,7 (...) 994, 997, 1000]\n * fred.toObject() // { start: 1, end: 1000, step: 3, group: 1}\n * bill = new RO.RangeObject(0, 999, 3, 3)\n * rangeobject.js?1541083657962:42 Uncaught RangeError: Range must be divisible evenly into groups\n * at new RangeObject (rangeobject.js?1541083657962:42)\n * at <anonymous>:1:8\n * bill = new RO.RangeObject(0, 998, 3, 3)\n * bill.toArray() // [[0, 3, 6] (...), [990, 993, 996]]\n *\n * MultiRangeObject:\n * new MultiRangeObject(rol1, rol2, ... roln)\n * new MultiRangeObject([rol1, rol2, ... roln])\n * Each argument is either a RangeObject, an Array-like object (, or a Distribution object? NYI ~ER). The\n * values generated by the MRO will be a multi-dimentional array, where each element of the array is a vector\n * across all input ranges.\n */\n\n\n// Some utility functions to make validating the parameters easier\nconst assert = (premise, msg) => {\n if (!premise) throw new Error(msg);\n}\n\nconst assertNumeric = (val, msg) => {\n assert(typeof val === 'number', msg);\n assert(!Number.isNaN(val), msg);\n assert(Number.isFinite(val), msg);\n}\n\nconst RANGEOBJ_REGEX = /^\\[object RangeObject (-?\\d+(?:\\.\\d+)?),(-?\\d+(?:\\.\\d+)?),(-?\\d+(?:\\.\\d+)?),(\\d+|undefined)\\]$/;\n\n/** \n * Defines a consistent interface for each of the range object types\n * to inherit from, provides some array methods.\n * @access public\n */\nclass SuperRangeObject {\n constructor() {\n return new Proxy(this, {\n get: (target, name) => {\n if ((typeof name === 'string' || typeof name === 'number') && Number.isInteger(parseFloat(name))) {\n return target.nthValue(parseFloat(name));\n } else {\n return target[name];\n }\n }\n });\n }\n\n [Symbol.iterator]() {\n let index = 0;\n\n return {\n next: () => ({ value: this.nthValue(index++), done: index > this.length })\n };\n }\n\n get length() {\n return 0;\n }\n\n nthValue(n) {\n throw new Error(\"nthValue not overridden\");\n }\n\n toArray() {\n return this.slice()\n }\n\n /**\n * Generate array from range starting at value `start` and ending at value `end` via `nthValue`.\n * @param {number} [start=0] index to start slice\n * @param {number} [end] index to end slice, return rest of array if not provided.\n * @access public\n */\n slice(start, end) {\n if (typeof start === 'undefined') { start = 0 } else if (start < 0) { start += this.length }\n\n if (typeof end === 'undefined') { end = this.length } else if (end < 0) { end += (this.length) }\n\n if (end > this.length) { end = this.length }\n\n const arr = []\n for (let i = start; i < end; i++) { arr.push(this.nthValue(i)) }\n\n return arr\n }\n\n /**\n * Converts range to an Array and then calls `filter(...args)` on it.\n * @param {...any} args Same args as you would pass to Array#filter\n * @access public\n */\n filter(...args) {\n return this.toArray().filter(...args);\n }\n}\n\n/**\n * Range objects are vanilla ES objects used to describe value range sets for use by `compute.for()`.\n * The range must be increasing, i.e. `start` must be less than `end`.\n * Calculations made to derive the set of numbers in a range are carried out with `BigNumber`, \n * eg. arbitrary-precision, support. The numbers `Infinity` and `-Infinity` are not supported, and \n * the API does not differentiate between `+0` and `-0`.\n */\n/**\n * An object which represents a range of values.\n * @param {number|object} startOrObject Beginning of range, or object literal with `start` and `end` properties.\n * @param {number} end End of range\n * @param {number} [step=1] Step size in range\n * @param {number|undefined} [group] Groups in range\n * @access public\n * @extends SuperRangeObject\n * @example <caption>With implicit step size = 1.</caption>\n * let rangeObject = new RangeObject(0.5,3);\n * rangeObject.toArray();\n * // [ 0.5, 1.5, 2.5 ]\n * \n * @example <caption>With explicit step size.</caption>\n * let rangeObject = new RangeObject(1,9,3);\n * rangeObject.toArray();\n * // [ 1, 4, 7 ]\n * \n * @example <caption>With grouping.</caption>\n * let rangeObject = new RangeObject(1,9,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7 ] ]\n */\nclass RangeObject extends SuperRangeObject {\n \n constructor (start, end, step, group) {\n super();\n if (typeof start === 'string' && start.match(RANGEOBJ_REGEX)) {\n const parts = start.match(RANGEOBJ_REGEX)\n start = {\n start: parseFloat(parts[1]),\n end: parseFloat(parts[2]),\n step: parseFloat(parts[3]),\n group: (parts[4] === 'undefined'? undefined : parseFloat(parts[4]))\n }\n }\n\n if (typeof start === 'object') {\n this.start = start.start;\n this.end = start.end;\n this.step = start.step;\n this.group = start.group;\n } else {\n this.start = start;\n this.end = end;\n this.step = step;\n this.group = group;\n }\n\n assertNumeric(this.start, `Invalid start parameter \"${this.start}\", must be numeric and finite.`);\n assertNumeric(this.end, `Invalid end parameter \"${this.end}\", must be numeric and finite.`);\n\n // Ensure step moves in the correct direction for start-end (ie, negative if end < start)\n if (typeof this.step === 'undefined') {\n this.step = Math.sign(this.end - this.start);\n } else {\n assertNumeric(this.step, `Invalid step parameter \"${this.step}\", must be numeric and finite.`);\n if ((this.step === 0 && this.start !== this.end) || Math.sign(this.step) !== Math.sign(this.end - this.start)) {\n throw new Error('RangeObject step must approach end from start.');\n }\n }\n\n if (typeof this.group !== 'undefined') {\n // group must remain undefined if not provided because no grouping should occur if not provided.\n // As per spec, even if group is 1 it should group into arrays of 1 element\n assertNumeric(this.group, `Invalid group parameter \"${this.group}\", must be numeric and finite.`);\n assert(Number.isInteger(this.group), `Invalid group parameter \"${this.group}\", must be an integer.`);\n assert(this.group > 0, `Invalid group parameter \"${this.group}\", must be greater than zero.`);\n }\n }\n\n /**\n * @typedef {object} RangeLike\n * @property {number} start\n * @property {number} end\n * @property {number} nthValue\n */\n\n /**\n * @returns {boolean}\n */\n static isRangelike (r) {\n if (r instanceof RangeObject) { return true }\n if (typeof r === 'object' &&\n typeof r.start === 'number' &&\n typeof r.end === 'number' &&\n typeof r.nthValue === 'function') { return true }\n\n return false\n }\n\n /**\n * @returns {boolean}\n */\n static isRangeObject(r) {\n if (r instanceof RangeObject) { return true }\n\n return false\n }\n\n /**\n * Test whether a value can be passed to the RangeObject constructor\n * @param r Value to test\n * @param strict Optional. Truthy to disallow objects which already look Rangelike\n */\n static isProtoRangelike (r, strict = true) {\n if (typeof r === 'object' &&\n typeof r.start === 'number' &&\n typeof r.end === 'number') { return true }\n\n if (typeof r === 'string' &&\n r.match(RANGEOBJ_REGEX)) { return true }\n\n if (!strict && RangeObject.isRangelike(r)) { return true }\n\n return false\n }\n\n /**\n * Create string representation of range: [object RangeObject start,end,step,group]\n * @access public\n */\n toString () {\n return `[object RangeObject ${this.start},${this.end},${this.step},${this.group}]`\n }\n\n /**\n * Create object literal for range with properties: start, end, step, and group.\n * @access public\n */\n toObject () {\n return {\n start: this.start,\n end: this.end,\n step: this.step,\n group: this.group\n }\n }\n\n /**\n * Return nth value in range\n * @param {number} n\n * @access public\n * @example\n * let rangeObject = new RangeObject(1,10,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7, 10 ] ]\n * rangeObject.nthValue(1);\n * // [ 7, 10 ]\n */\n nthValue(n) {\n /**\n * `>=` since the value at index 7 in an array that's of length 7 is outside\n * its range\n */\n if (n < 0 || n >= this.length) {\n return undefined;\n }\n\n if (typeof this.group !== 'undefined') {\n const start = (this.group * this.step * n) + this.start\n const arr = []\n\n for (let i = 0; i < this.group && i + this.group * n < this.stepCount; i++) {\n arr.push(start + (i * this.step))\n }\n\n return arr;\n }\n\n return this.start + (n * this.step);\n }\n\n /**\n * Return max value in range\n * @access public\n * @returns {number}\n * @example\n * let rangeObject = new RangeObject(1,10,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7, 10 ] ]\n * rangeObject.max;\n * // 10\n */\n get max () {\n if (typeof this.group === 'undefined' && this.step === 1) { return this.end }\n\n let tail\n if (typeof this.group === 'undefined') {\n [ tail ] = this.slice(-1)\n } else {\n [ tail ] = this.slice(-1)[0].slice(-1)\n }\n return tail\n }\n\n /**\n * Boolean indicating whether all groups are filled.\n * Only defined for RangeObjects that group results.\n * @access public\n * @returns {boolean}\n * @example <caption>With remainder</caption>\n * let rangeObject = new RangeObject(1,9,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7 ] ]\n * rangeObject.hasRemainder;\n * // true\n * @example <caption>Without remainder</caption>\n * let rangeObject = new RangeObject(1,10,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7, 10 ] ]\n * rangeObject.hasRemainder;\n * // true\n */\n get hasRemainder () {\n if (typeof this.group === 'undefined') { return false }\n const groups = this.stepCount / this.group;\n\n return (groups !== Math.floor(groups));\n }\n\n /**\n * Number of elements in range, or number of groups if grouped.\n * @access public\n * @returns {number}\n * @example <caption>Without grouping</caption>\n * let rangeObject = new RangeObject(1,10,3);\n * rangeObject.toArray();\n * // [ 1, 4, 7, 10 ]\n * rangeObject.length;\n * // 4\n * @example <caption>With grouping</caption>\n * let rangeObject = new RangeObject(1,9,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7 ] ]\n * rangeObject.length;\n * // 2\n */\n get length () {\n return Math.ceil(this.stepCount / (this.group || 1));\n }\n\n /**\n * Number of steps in range (sort of like number of elements, except grouping is no longer relevant).\n * @access public\n * @returns {number}\n * @example <caption>Without grouping</caption>\n * let rangeObject = new RangeObject(1,10,3);\n * rangeObject.toArray();\n * // [ 1, 4, 7, 10 ]\n * rangeObject.stepCount;\n * // 4\n * @example <caption>With grouping</caption>\n * let rangeObject = new RangeObject(1,9,3,2);\n * rangeObject.toArray();\n * // [ [ 1, 4 ], [ 7 ] ]\n * rangeObject.length;\n * // 3\n */\n get stepCount () {\n if (this.step === 0) return 1;\n return Math.floor(Math.abs((this.end - this.start) / this.step)) + 1;\n }\n}\n\n/**\n * A sparse range object contains many {@link RangeObject}s. The range objects are stored as arrays and then are\n * concatenated into one array in the order that they were supplied to the constructor.\n * @param {RangeObject|RangeObject[]|object} arg - First range object, or array of range objects, or object with `sparse` key containing an array of range objects.\n * @param {RangeObject} rangeObject - If first argument is a RangeObject, subsquent arguments are range objects too.\n * @access public\n * @extends SuperRangeObject\n * @example\n * r0 = new RangeObject(1,2)\n * r1 = new RangeObject(1,3)\n * sro = new SparseRangeObject(r0, r1)\n * // [ 1, 2, 1, 2, 3]\n */\nclass SparseRangeObject extends SuperRangeObject {\n constructor() {\n super();\n let sparse = [];\n\n if (arguments.length === 1 && arguments[0].ranges) \n sparse = [...arguments[0].ranges];\n else \n sparse = [...arguments];\n \n if (sparse instanceof SparseRangeObject)\n throw new Error('Argument is of type sparse range object');\n \n // If sparse key is passed, make sure the arguments are only the range objects (compute.for() implementation) \n if (sparse[0].sparse)\n sparse = sparse[0].sparse;\n \n sparse.map( r =>\n {\n if (!RangeObject.isProtoRangelike(r))\n throw new Error('Argument is not of type RangeObject');\n })\n \n if (sparse[0].group)\n {\n for (let i = 0; i < sparse.length; i++)\n {\n if (sparse[i].group !== sparse[0].group)\n throw new Error('Range Object has different dimension than other range objects');\n }\n } \n \n this.sparse = true;\n this.ranges = sparse.map(r => \n { \n return new RangeObject(r);\n })\n \n }\n \n /**\n * Test whether a value can be passed to the SparseRangeObject constructor\n * @param r Value to test\n * @param strict Optional. Truthy to disallow objects which already look Rangelike\n */\n static isProtoSparseRangelike (r, strict = true) \n {\n if (typeof r === 'object' && r.sparse) { return true; }\n return false;\n }\n \n /**\n * Return nth value in range\n * @param {number} n\n * @access public\n * @example\n * let sparseRangeObject = new SparseRangeObject(1,3,1);\n * rangeObject.toArray();\n * // [ 1, 2, 3]\n * rangeObject.nthValue(1);\n * // 2\n */\n nthValue(n) \n {\n if (n < 0 || n >= this.length) { return undefined }\n \n let count = 0;\n let rangeCount = 0;\n while (count !== n)\n {\n if (count <= n)\n {\n for (let i = 0; i < this.ranges[rangeCount].length; i++)\n {\n if (count === n)\n return this.ranges[rangeCount][i];\n else\n count++;\n }\n }\n rangeCount++;\n }\n return this.ranges[rangeCount][0];\n }\n \n /**\n * Create object literal with `sparse` property and `range` property containing array of range objects.\n */\n toObject () {\n \n const obj = {\n sparse: true,\n ranges: this.ranges\n }\n return obj;\n }\n \n get length()\n {\n let len = 0;\n this.ranges.forEach((r) => len += r.length);\n\n return len;\n }\n \n}\n\n/**\n * Range objects are vanilla ES objects used to describe value range sets for use by `compute.for()`. \n * Calculations made to derive the set of numbers in a range are carried out with `BigNumber`, \n * eg. arbitrary-precision, support. The numbers `Infinity` and `-Infinity` are not supported, and \n * the API does not differentiate between `+0` and `-0`.\n */\nclass MultiRangeObject extends SuperRangeObject {\n /**\n * A multi-range object contains many {@link RangeObject}s. They are iterated over \n * with the fastest moving index going over the right-most range object in array order. Each element\n * of a multi range is a tuple of values from constituent ranges.\n * @param {RangeObject|RangeObject[]|object} arg - First range object, or array of range objects, or object with `ranges` key containing an array of range objects.\n * @param {RangeObject} rangeObject - If first argument is a RangeObject, subsquent arguments are range objects too.\n * @access public\n * @extends SuperRangeObject\n * @example\n * r0 = new RangeObject(1,2)\n * r1 = new RangeObject(1,3)\n * mro = new MultiRangeObject(r0, r1)\n * mro.toArray()\n * // [ [ 1, 1 ], [ 1, 2 ], [ 1, 3 ], [ 2, 1 ], [ 2, 2 ], [ 2, 3 ] ]\n */\n constructor () {\n super();\n var ranges = []\n\n if (arguments.length === 1 && typeof arguments[0] === 'string') {\n const inputs = JSON.parse(arguments[0])\n if (Array.isArray(inputs)) {\n ranges = inputs\n } else if (inputs.ranges) {\n ranges = inputs.ranges\n } else {\n ranges = [inputs]\n }\n } else if (arguments.length === 1 && Array.isArray(arguments[0])) {\n ranges = [...arguments[0]]\n } else if (arguments.length === 1 && !!arguments[0].ranges) {\n ranges = [...arguments[0].ranges]\n } else {\n ranges = [...arguments]\n }\n\n this.ranges = ranges.map(r => {\n if (RangeObject.isRangelike(r)) { return r }\n if (RangeObject.isRangeObject(r)) { return r }\n if (DistributionRange.isDistribution(r)) { return r }\n if (RangeObject.isProtoRangelike(r)) { return new RangeObject(r) }\n if (DistributionRange.isProtoDistribution(r)) { return new DistributionRange(r) }\n\n return Array.isArray(r) ? r : [r]\n })\n }\n\n /**\n * Test whether a value can be passed to the MultiRangeObject constructor\n * @param r Value to test\n * @param strict Optional. Truthy to disallow objects which already look Rangelike\n */\n static isProtoMultiRangelike (r, strict = true) {\n if ((typeof r === 'object') &&\n Array.isArray(r.ranges) &&\n !r.sparse)\n {\n return true;\n }\n\n return false\n }\n\n /**\n * Create string representation of this MultiRangeObject\n * @access public\n * @example\n * \"[object MultiRangeObject ' + this.ranges.length + ']\"\n */\n toString () {\n return '[object MultiRangeObject ' + this.ranges.length + ']'\n }\n\n /**\n * Create object literal with `ranges` property containing array of range objects.\n */\n toObject () {\n return { ranges: this.ranges }\n }\n\n /**\n * Returns a tuple of values from the ranges given by this multi range object.\n * @param {number} n index of multi-range tuple to return\n * @access public\n * @example\n * r0 = new RangeObject(1,2)\n * r1 = new RangeObject(1,3)\n * mro = new MultiRangeObject(r0, r1)\n * mro.toArray()\n * // [ [ 1, 1 ], [ 1, 2 ], [ 1, 3 ], [ 2, 1 ], [ 2, 2 ], [ 2, 3 ] ]\n * mro.nthValue(2)\n * // [ 1, 3 ]\n */\n nthValue (n) {\n if (n < 0 || n >= this.length) { return undefined }\n\n const indexes = []\n\n for (let r = (this.ranges.length - 1); r >= 0; r--) {\n const idx = n % this.ranges[r].length\n\n indexes.unshift(idx)\n\n n -= idx\n n /= this.ranges[r].length\n }\n\n const values = []\n\n for (let i = 0; i < indexes.length; i++) {\n values[i] = Array.isArray(this.ranges[i]) ? this.ranges[i][indexes[i]] : this.ranges[i].nthValue(indexes[i])\n }\n\n return values\n }\n\n /**\n * Boolean indicating whether any of the ranges in this multi-range object has a remainder. See {@link RangeObject#hasRemainder}.\n * @access public\n * @returns {boolean}\n */\n get hasRemainder () {\n for (let r of this.ranges) {\n if (r.hasRemainder) { return true }\n }\n\n return false\n }\n\n get length () {\n let len = 1\n\n this.ranges.forEach((r) => len *= r.length)\n\n return len\n }\n}\n\n// DistributionRange object wraps a distributing function into a RangeObject-like API\n// which can be dropped directly into a MultiRangeObject to generate input slices\nclass DistributionRange extends SuperRangeObject {\n constructor (n, dist, ...params) {\n super();\n\n this.distributor = (__webpack_require__(/*! ./stats-ranges */ \"./src/dcp-client/stats-ranges.js\").distributor);\n\n // If argv[0] is a string formatted as DistributionRange.toString(), then unpack it\n if (typeof n === 'string' && n.match(/^\\[object DistributionRange (\\w+?)\\((\\d+?)(?:,(.+?))?\\)\\]$/)) {\n const parts = n.match(/^\\[object DistributionRange (\\w+?)\\((\\d+?)(?:,(.+?))?\\)\\]$/)\n dist = parts[1]\n n = parseInt(parts[2])\n params = (parts[3] || '').split(',').map(e => parseFloat(e))\n }\n\n // If argv[0] is a string describing a DistributionRange, then unpack it\n if (typeof n === 'string' && n.match(/^(\\w+?)\\((\\d+?)(?:,(.+?))?\\)$/)) {\n const parts = n.match(/^(\\w+?)\\((\\d+?)(?:,(.+?))?\\)$/)\n dist = parts[1]\n n = parseInt(parts[2])\n params = (parts[3] || '').split(',').map(e => parseFloat(e))\n }\n\n // If argv[0] is a object of the right shape, then unpack it\n if (typeof n === 'object' &&\n typeof n.length === 'number' &&\n typeof n.dist === 'string' &&\n Array.isArray(n.params)) {\n // console.log('Unpacking proto-object', n)\n dist = n.dist\n params = n.params\n n = n.length\n if (Array.isArray(n.values)) { this.values = n.values }\n if (typeof n.materializeOnScheduler === 'boolean') { this.materializeOnScheduler = n.materializeOnScheduler }\n }\n\n Object.defineProperty(this, 'length', {\n value: n,\n enumerable: true\n })\n Object.defineProperty(this, 'dist', {\n value: dist,\n enumerable: true\n })\n Object.defineProperty(this, 'params', {\n value: params || [],\n enumerable: true\n })\n\n if (typeof this.distributor[dist] !== 'function') {\n // console.log({n,dist,params})\n throw new TypeError('dist param must point to an exported distributing function')\n }\n }\n\n /**\n * @returns {boolean}\n */\n static isDistribution (d) {\n return d instanceof DistributionRange\n }\n\n static isDistributionLike (d) {\n if (DistributionRange.isDistribution(d)) { return true }\n if (typeof d === 'object' &&\n typeof d.nthValue === 'function' &&\n typeof d.slice === 'function') { return true }\n\n return false\n }\n\n static isProtoDistribution (d) {\n if (typeof d === 'string' && d.match(/^\\[object DistributionRange (\\w+?)\\((\\d+?)(?:,(.+?))?\\)\\]$/)) { return true }\n if (typeof d === 'string' && d.match(/^(\\w+?)\\((\\d+?)(?:,(.+?))?\\)$/)) { return true }\n if (typeof d === 'object' &&\n typeof d.length === 'number' &&\n typeof d.dist === 'string' &&\n Array.isArray(d.params)) { return true }\n\n return false\n }\n\n toString () {\n return `[object DistributionRange ${this.dist}(${[this.length, ...this.params].join()})]`\n }\n\n toObject () {\n this.materialize();\n return {\n length: this.length,\n dist: this.dist,\n params: this.params,\n materializeOnScheduler: this.materializeOnScheduler || undefined,\n values: this.values || undefined\n }\n }\n\n nthValue (n) {\n if (n < 0 || n >= this.length) { return undefined }\n\n if (this.values) { return this.values[n] }\n\n const fn = this.distributor[this.dist]\n\n if (typeof fn === 'function') { return fn.apply(fn, [n, this.length, ...this.params]) }\n\n return undefined\n }\n\n /** Resolve the distribution to a static array\n * @param now If false, then set a flag to materialize on the scheduler. Default: materialize now\n */\n materialize (now = true) {\n if (now === false) { return this.materializeOnScheduler = true }\n\n this.values = this.toArray()\n }\n}\n\n/** Rehydrate an input range from a vanilla ES5 object to an appropriate rangelike object\n * @param obj Serialized job.data object (or JSON string)\n * @return as appropriate, a RangeObject, DistributionRange, MultiRangeObject, or array\n */\nfunction rehydrateRange (obj) {\n const { RemoteDataPattern } = __webpack_require__(/*! dcp/dcp-client/remote-data-pattern */ \"./src/dcp-client/remote-data-pattern.js\");\n const { RemoteDataSet } = __webpack_require__(/*! dcp/dcp-client/remote-data-set */ \"./src/dcp-client/remote-data-set.js\");\n\n if (typeof obj === 'string') {\n obj = JSON.parse(obj)\n }\n\n if (typeof obj === 'number') {\n return obj\n }\n\n if (obj instanceof RangeObject ||\n obj instanceof SparseRangeObject ||\n obj instanceof MultiRangeObject ||\n obj instanceof RemoteDataSet ||\n obj instanceof RemoteDataPattern ||\n obj instanceof DistributionRange) {\n return obj;\n }\n\n // If obj looks like a RemoteDataSet, make one of those\n if (RemoteDataSet.isProtoRemoteDataSetLike(obj)) {\n return new RemoteDataSet(obj)\n }\n \n // If obj looks like a RemoteDataPattern, make one of those\n if (RemoteDataPattern.isProtoRemoteDataPatternLike(obj)) {\n return new RemoteDataPattern(obj.pattern, obj.sliceCount)\n }\n\n // If obj is an iterable, coerce it to an array\n if (Symbol.iterator in Object(obj)) {\n return Array.from(obj)\n }\n \n // If obj looks like a SparseRangeObject, make one of those\n if (SparseRangeObject.isProtoSparseRangelike(obj))\n return new SparseRangeObject(obj);\n\n // If obj looks like a MultiRangeObject, make one of those\n if (MultiRangeObject.isProtoMultiRangelike(obj)) {\n return new MultiRangeObject(obj)\n }\n\n // If obj looks rangelike, make a RangeObject\n if (RangeObject.isProtoRangelike(obj)) {\n return new RangeObject(obj)\n }\n\n // If obj looks like a proto-distribution, make a DistributionRange\n if (DistributionRange.isProtoDistribution(obj)) {\n return new DistributionRange(obj)\n }\n\n throw new TypeError(`obj cannot be cast to any supported Rangelike object: ${JSON.stringify(obj)}`)\n}\n\nexports.SuperRangeObject = SuperRangeObject;\nexports.RangeObject = RangeObject;\nexports.MultiRangeObject = MultiRangeObject;\nexports.DistributionRange = DistributionRange;\nexports.SparseRangeObject = SparseRangeObject;\nexports.rehydrateRange = rehydrateRange;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/range-object.js?");
4213
4223
 
4214
4224
  /***/ }),
4215
4225
 
@@ -4259,7 +4269,7 @@ eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_mod
4259
4269
  \*************************************************/
4260
4270
  /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
4261
4271
 
4262
- 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=fca68c239b7cda20a34513139c096b25d2576370,' + 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?");
4272
+ 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=b5864d51c1e7a9b3024698306574b60003a91d62,' + 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?");
4263
4273
 
4264
4274
  /***/ }),
4265
4275
 
@@ -4321,7 +4331,7 @@ eval("/* provided dependency */ var Buffer = __webpack_require__(/*! ./node_modu
4321
4331
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4322
4332
 
4323
4333
  "use strict";
4324
- eval("/**\n * @file Wallet API - perform operations related to Addresses, Keystores, Key Pairs\n *\n * @author Wes Garland - wes@kingsds.network\n * @author Duncan Mays - duncan@kingsds.network\n * @author Badrdine Sabhi - badr@kingsds.network\n * @author Ryan Rossiter - ryan@kingsds.network\n * @date August 2019\n * @module dcp/wallet\n * @access public\n */\n\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('wallet');\n\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst sh = __webpack_require__(/*! dcp/utils/sh */ \"./src/utils/sh.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nif (DCP_ENV.platform === 'nodejs') {\n var path = requireNative('path');\n var fs = requireNative('fs')\n}\n\nexports._internalEth = __webpack_require__(/*! ./keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth; /* MUST load before sub-modules for ethereum module detection */\n\nconst ksCache = __webpack_require__(/*! ./ks-cache */ \"./src/dcp-client/wallet/ks-cache.js\");\nObject.assign(exports, __webpack_require__(/*! ./keystore */ \"./src/dcp-client/wallet/keystore.js\"));\nObject.assign(exports, __webpack_require__(/*! ./eth */ \"./src/dcp-client/wallet/eth.js\"));\nObject.assign(exports, __webpack_require__(/*! ./address-collection */ \"./src/dcp-client/wallet/address-collection.js\"));\nObject.assign(exports, __webpack_require__(/*! ./passphrase-prompt */ \"./src/dcp-client/wallet/passphrase-prompt.js\"));\nObject.assign(exports, __webpack_require__(/*! ./error-codes */ \"./src/dcp-client/wallet/error-codes.js\"));\n\nfunction isArrayLike(a)\n{\n return typeof a === 'object' && (Array.isArray(a) || typeof a.length === 'number');\n}\n\n/**\n * This property sets the name of the namespace that is used to generate the application data directory, which is used to store proxy keys when required,\n * and users’ bank and identity keys when supplied.\n * The default namespace name is the name specified in the package.json file for the application calling into this API.\n */\nexports.namespaceName = undefined;\n\n/**\n * This property gets/sets the full path name the application data directory, overriding namespaceName if it is changed.\n * The default value is ~/.dcp/applications/${namespaceName}/, where ~ represents the invoking user’s home directory.\n */\nexports.stateDir = undefined;\n\n/**\n * @access public\n * @typedef {object} LoadResult - Object returned by `wallet.load`\n * @property {Keystore} keystore - the instance of the keystore that was loaded\n * @property {boolean} safe - indicates that the keystore was read from a secure location.\n */\n\n\n/**\n * Form 1\n * \n * This function locates and reads a keystore file.\n * \n * **NodeJS** only: `filename` must be an absolute path, or begin withgin with `path.sep, '.' + path.sep`, or `'..' + path.sep`.\n * The safe flag is set to true to indicate that the Keystore file was stored safely; this happens if and only if the following conditions are true:\n * \n * 1. The containing directory and all its ancestors, up to and including the root directory, is not world-writable.\n * 2. The Keystore File is neither world-writable nor world-readable.\n * \n * @param {string} filename The keystore filename\n * @returns Object with the following properties:\n * - contents: the contents of the file\n * - safe: if the file was loaded from a safe path (perms check)\n * - filename: the name of the file that was actually loaded\n */\n/**\n * Form 2\n * \n * This function locates and reads a keystore file.\n *\n * @param {object} options - An options object\n * @param {string} [options.name='default'] - The keystore label, or filename\n * @param {string[]} [options.paths] - Override default keystore directory search path, normally the application data directory, followed by the `.dcp` \n * directory in the home directory belonging to the user invoking the program (NodeJS only). Note that the search path is never used when name specifies a \n * complete pathname.\n * @param {string} [options.dir] - Overrides paths, equivalent to `paths: [dir]`\n * @returns Object with the following properties:\n * - contents: the contents of the file\n * - safe: if the file was loaded from a safe path (perms check)\n * - filename: the name of the file that was actually loaded\n */\nfunction loadKeystore(options) {\n if (typeof options === 'string') {\n options = {\n name: sh.expandPath(options),\n };\n }\n\n /* Deep clone and use default values to augment the passed-in options object.\n * The deep clone is necessary because this API mutates properties of the object.\n */\n options = JSON.parse(JSON.stringify(Object.assign({}, {\n name: \"default\",\n paths: [ (__webpack_require__(/*! dcp/common/dcp-dot-dir */ \"./src/common/dcp-dot-dir.js\").dotDcpDir) ],\n }, options)));\n\n let filename;\n if (options.name.match(/[/\\\\]/)) {\n // if the name contains a slash (or backslash), then treat it as a filename\n filename = sh.expandPath(options.name);\n\n if (!fs.existsSync(filename)) {\n let err = new Error(`Could not locate keystore '${filename}'`);\n err.code = 'ENOENT';\n throw err;\n }\n } else {\n // otherwise, treat the name as a keystore label and search\n // for a keystore named `${options.name}.keystore` in options.paths\n\n if (options.dir)\n options.paths = [sh.expandPath(options.dir)];\n else if (exports.stateDir)\n options.paths.unshift(path.resolve(exports.stateDir))\n else if (exports.namespaceName)\n options.paths.unshift(path.resolve(sh.expandPath('~/.dcp/applications'), exports.namespaceName));\n \n let searchPaths = options.paths.map((searchPath) =>\n path.resolve(searchPath, `${options.name}.keystore`)\n );\n\n filename = searchPaths.find((fn) => {\n debugging('search') && console.log(`wallet - checking ${fn}`);\n return fs.existsSync(fn);\n });\n\n if (filename) {\n debugging('searchResult') && console.log(`wallet - found keystore at ${filename}`);\n } else {\n const errMsg = `Could not locate keystore named '${options.name}' in these locations: ${JSON.stringify(searchPaths)}`;\n let err = new Error(errMsg);\n debugging('searchResult') && console.error(`wallet - ${errMsg}`);\n err.code = 'ENOENT';\n throw err;\n }\n }\n\n debugging() && console.log(`wallet - loading '${filename}'`);\n //gets the keystore or private key from the file\n return {\n safe: exports.isSafe(filename),\n filename: filename,\n contents: fs.readFileSync(filename, 'utf-8').trim()\n };\n}\n\n/**\n * Form 1\n * \n * This function locates and reads a keystore file,\n * and instantiates a Keystore from it.\n * \n * **NodeJS** only: `filename` must be an absolute path, or begin withgin with `path.sep, '.' + path.sep`, or `'..' + path.sep`.\n * The safe flag is set to true to indicate that the Keystore file was stored safely; this happens if and only if the following conditions are true:\n * \n * 1. The containing directory and all its ancestors, up to and including the root directory, is not world-writable.\n * 2. The Keystore File is neither world-writable nor world-readable.\n * \n * @param {string} filename The keystore filename\n * @returns {module:dcp/wallet~LoadResult}\n * @async\n * @access public\n */\n/**\n * Form 2\n * \n * This function locates and reads a keystore file,\n * and instantiates a Keystore from it.\n *\n * @param {object} options - An options object\n * @param {string} [options.name='default'] - The keystore label, or filename\n * @param {string[]} [options.paths] - Override default keystore directory search path, normally the application data directory, followed by the `.dcp` \n * directory in the home directory belonging to the user invoking the program (NodeJS only). Note that the search path is never used when name specifies a \n * complete pathname.\n * @param {string} [options.dir] - Overrides paths, equivalent to `paths: [dir]`\n * @param {KeystoreConstructor} [options.KeystoreConstructor] - Constructor to use when creating the keystore\n * @returns {module:dcp/wallet~LoadResult}\n * @async\n * @access public\n */\nexports.load = async function wallet$$load(options) {\n var { contents, filename, safe } = loadKeystore.apply(this, arguments);\n var KeystoreConstructor = (options && options.KeystoreConstructor) || exports.Keystore;\n const keystore = await new KeystoreConstructor(JSON.parse(contents), false);\n if (!keystore.label)\n keystore.label = path.basename(filename, '.keystore');\n \n //return keystore in an object with a safety indicator\n return {\n keystore,\n filename,\n safe\n }\n}\n\n/**\n * Form 1\n * \n * This function locates and reads a keystore file,\n * and instantiates an Address from it.\n * \n * **NodeJS** only: `filename` must be an absolute path, or begin withgin with `path.sep, '.' + path.sep`, or `'..' + path.sep`.\n * The safe flag is set to true to indicate that the Keystore file was stored safely; this happens if and only if the following conditions are true:\n * \n * 1. The containing directory and all its ancestors, up to and including the root directory, is not world-writable.\n * 2. The Keystore File is neither world-writable nor world-readable.\n * \n * @param {string} filename The keystore filename\n * @access public\n */\n/**\n * Form 2\n * \n * This function locates and reads a keystore file,\n * and instantiates an Address from it.\n *\n * @param {object} options - An options object\n * @param {string} [options.name='default'] - The keystore label, or filename\n * @param {string[]} [options.paths] - Override default keystore directory search path, normally the application data directory, followed by the `.dcp` \n * directory in the home directory belonging to the user invoking the program (NodeJS only). Note that the search path is never used when name specifies a \n * complete pathname.\n * @param {string} [options.dir] - Overrides paths, equivalent to `paths: [dir]`\n * @access public\n */\nexports.loadAddress = function wallet$$loadAddress(options) {\n var { contents, filename, safe } = loadKeystore.apply(this, arguments);\n\n return {\n filename,\n address: new exports.Address(JSON.parse(contents).address),\n safe\n };\n}\n\n/**\n * @param {Object} keystore instance of Keystore to save\n * @param {string} filename the filename to save it in\n * @param {Object} writeFileOptions optional options object to pass to node's\n * fs.writeFile(). Note that the mode will\n * always force utf-8 output.\n * @param {Object|boolean} mkdirOptions optional options object to pass to node's fs.mkdir().\n * By default it will use recursive: true.\n * Disable mkdir with 'false'.\n */\nexports.save = async function wallet$$save(keystore, filename, writeFileOptions, mkdirOptions) {\n const dir = path.dirname(filename);\n if (mkdirOptions !== false && !fs.existsSync(dir)) {\n await fs.promises.mkdir(dir, {\n recursive: true,\n ...mkdirOptions,\n });\n }\n \n await fs.promises.writeFile(filename, JSON.stringify(keystore), {\n ...writeFileOptions,\n encoding: 'utf-8', // force utf-8 encoding\n });\n}\n\n/**\n * This function checks the safety requirements outlined in the Keystore API documentation\n * The requirements are that the file must neither be world readable or world writable, \n * or have any parent directory that is world writable. This function will return a boolean which indicates\n * weather or not these conditions are met.\n *\n * @param {string} - filePath, this is the path to the file we are checking the safety of, this\n * function will not work properly, and in fact will log a warning message, if that filePath \n * starts with either . or ..\n * @returns {boolean} - indicates weather or not the file provided satisfies safety criteria\n */\nexports.isSafe = function wallet$$isSafe(filePath) {\n if (!path.isAbsolute(filePath)) {\n throw new Error(`Not an absolute path: '${filePath}'`);\n }\n\n let pathArray = filePath.split(path.sep);\n pathArray[0] = pathArray[0] || path.parse(filePath).root;\n if (exports.isWorldReadable(filePath))\n return false;\n\n //checks that the file and all its ancestor directories are not world readable\n while (pathArray.length > 0) {\n let reassemble = path.join.apply(null, pathArray);\n if (exports.isWorldWritable(reassemble))\n return false;\n pathArray = pathArray.slice(0, pathArray.length - 1);\n }\n\n return true;\n}\n\n/**\n * This function takes a filepath and returns a boolean to indicate weather or not the location specified in the filepath is world readable\n *\n * @param {string} filePath - path to the file in question, does not work right if the filePath starts with . or ..\n *\n * @returns {boolean} - indicates weather the given file or directory is world readable\n */\nexports.isWorldReadable = function wallet$$isWorldReadable(filePath) {\n const mode = fs.statSync(filePath).mode;\n return mode & 4;\n}\n\n/**\n * This function takes a filepath and returns a boolean to indicate weather or not the location specified in the filepath is world writable\n *\n * @param {string} filePath - path to the file in question, does not work right if the filePath starts with . or ..\n *\n * @returns {boolean} - indicates weather the given file or directory is world writable.\n */\nexports.isWorldWritable = function wallet$$isWorldWritable(filePath) {\n debugging('safety') && console.debug(`wallet - checking write permissions on '${filePath}'`);\n const mode = fs.statSync(filePath).mode;\n return mode & fs.constants.W_OK;\n}\n\n/**\n * These functions examine parameters and accepts options relating to its environment, uses this information \n * to select a Keystore File, load it via wallet.load(), and return an AuthKeystore object.\n * If it cannot return an AuthKeystore object, this function will throw an Error.\n *\n * @param {object | string} param1 | param2 - Three accepted keys: name, which specifies the name of the keystore, dir, which specifies its directpry, \n * and checkEmpty, which controls weather or not unlock() will attempt an empty password on the keystore \n * before prompting the user.\n * @returns {Object} instance of Keystore\n */\nasync function wallet$$get_browser(options) {\n const ClientModal = __webpack_require__(/*! dcp/dcp-client/client-modal */ \"./src/dcp-client/client-modal/index.js\");\n const keystoreFileContents = DCP_ENV.getenv('dcp-wallet-oauth-integration') ? await ClientModal.getOauthLogin(options) : await ClientModal.getKeystoreFile(options);\n\n return new options.KeystoreConstructor(keystoreFileContents);\n}\n\nasync function wallet$$get_node(options)\n{\n async function inner()\n {\n const result = await exports.load(options);\n\n if (result && result.keystore)\n return result.keystore;\n return false;\n }\n\n return inner();\n}\n\nconst getKeystore_platform = DCP_ENV.switch({\n nodejs: wallet$$get_node,\n bravojs: wallet$$get_browser,\n 'vanilla-web': wallet$$get_browser,\n default: typeof navigator === 'undefined' ? wallet$$get_node : wallet$$get_browser\n});\n\n/** \n * Return a promise which resolves to an identity keystore. See {@link module:dcp/wallet.get|get}.\n *\n * If an identity keystore has never been generated or added to the wallet with addId, a new\n * one will be either fetched if there are NO arguments passed to this function.\n *\n * On browser platforms, an idKs will be dynamically generated if one has not been specified\n * beforehand. This behaviour can be defeated by passing arguments=[false], which will be treated \n * the same way as arguments=[].\n *\n * On browser platforms, we try to \"fetch\" the identity from a file upload, but ONLY if the\n * arguments to this function have been specified. The idea here is that the default behaviour,\n * for getId() to generate a random id, is the best behaviour from a UX POV unless the developer \n * is doing something really weird.\n *\n * On non-browser platforms, we \"fetch\" the identity via wallet.get(), which eventually falls\n * back to looking in ~/.dcp/id.keystore.\n *\n * Implementation note: cannot await between function call and initial population of ksCache in \n * walletGet_form1 if adding a new identity; otherwise, multiple parallel connections might \n * trigger new identity \"fetches\" etc.\n * \n * @returns {module:dcp/wallet.AuthKeystore}\n * @access public\n * @async\n */\nexports.getId = async function wallet$$getId()\n{\n var optionsOrNameOrNada;\n var ks;\n var options = {\n name: 'id',\n KeystoreConstructor: exports.IdKeystore\n };\n var generateKeystoreOnDemand = true;\n \n if (arguments.length === 1 && arguments[0] === false)\n {\n generateKeystoreOnDemand = false;\n delete arguments[0];\n }\n\n if (!isArrayLike(arguments[0])) /* forms 4,5,6 */\n optionsOrNameOrNada = arguments[0];\n else\n {\n Object.assign(options, walletGet_argsToOptions(exports.IdKeystore, arguments[0]));\n optionsOrNameOrNada = arguments[1]; /* re-write forms 4/5/6 to 2/1/3 */\n }\n\n if (typeof optionsOrNameOrNada === 'object')\n Object.assign(options, optionsOrNameOrNada); /* form 1 - options */\n else if (typeof optionsOrNameOrNada === 'undefined')\n ; /* form 2 - name */\n else if (typeof optionsOrNameOrNada === 'string')\n options.name = optionsOrNameOrNada; /* form 3 - nada */\n else\n throw new Error('unrecognized constructor form in wallet::getId');\n\n if (DCP_ENV.isBrowserPlatform && generateKeystoreOnDemand && arguments.length === 0)\n {\n options.fetch = function wallet$$getId$generate() {\n debugging() && console.debug('wallet - generating dynamic identity');\n return new exports.IdKeystore(null, ''); \n };\n }\n\n let cached = await ksCache.get(options.KeystoreConstructor, options.name, options.contextId);\n if (cached)\n return cached;\n\n ks = await walletGet_form1(options);\n\n debugging('identity') && console.debug('wallet - identity is', !ks ? ks : ks.address);\n return ks;\n}\n\n/** \n * Form 1\n * \n * This form loads a Keystore File via {@link module:dcp/wallet.load|load}.\n * The options object is used to override default parameters which help to identify and locate the Keystore File.\n * \n * **On NodeJS**: The options object is passed directly to `wallet.load`.\n * \n * **On Web**: The options object is primarily used to provide context to the user for showing a modal; \n * @param {object} options\n * @param {string} [options.name='default'] Override the default keystore name.\n * @param {string} [options.contextId] An optional, user-defined identifier used for caching keystores.\n * See `job.contextId` in the compute-api spec.\n * @param {string} [options.jobName] Optional name of the job that the keystore is being requested for.\n * @param {boolean} [options.checkEmpty=true] Try an empty password before prompting user\n * @param {function} [options.KeystoreConstructor] Override the constructor to use for the keystore.\n * @param {string} [options.msg] Custom message for that modal that describes the purpose for providing the keystore\n * @return {module:dcp/wallet.AuthKeystore} Return an authorization keystore.\n * @access public\n * @async\n */\n/**\n * Form 2\n * \n * This form is equivalent to `wallet.get({})`.\n * @return {module:dcp/wallet.AuthKeystore} Return an authorization keystore.\n * @access public\n * @async\n */\n/**\n * Form 3\n * \n * This form is equivalent to `wallet.get({name: string})`\n * @param {string} name Override the default keystore name.\n * @return {module:dcp/wallet.AuthKeystore} Return an authorization keystore.\n * @access public\n * @async\n */\n/**\n * Form 4\n * \n * This form accepts an Array-like object, formatted like `process.argv`, to specify options for invoking Form 1. \n * Its purpose is to unify Keystore, PrivateKey, and Address-related options for programs written in NodeJS.\n * @param {string[]} array Array-like object, formatted like `process.argv`\n * | Argument | Behaviour |\n * |---------------|-----------|\n * | --private-key | Generate a keystore corresponding to the specified private key, ignoring all other considerations |\n * | -i name | Override the `name` option for getId |\n * | -k name | Override the `name` option for get |\n * | -p | Set the `checkEmpty` option to false |\n * @return {module:dcp/wallet.AuthKeystore} Return an authorization keystore.\n * @access public\n * @async\n */\n/**\n * Form 5\n * \n * This form accepts an Array-like object, as described in form 4, and an options object \n * as described in form 1. Its behaviour is to generate an options object from the array, \n * merge the passed options object into this options object, and invoke form 1.\n * To be clear, the options argument has a higher precedence than the array argument.\n * @param {string[]} array Array-like object, formatted like `process.argv`. (See Form 4)\n * @param {object} options Options object, see Form 1.\n * @return {module:dcp/wallet.AuthKeystore} Return an authorization keystore.\n * @access public\n * @async\n */\n/**\n * Form 6\n * \n * This form is equivalent to `wallet.get(array, {name: string})`.\n * @param {string[]} array Array-like object, formatted like `process.argv`. (See Form 4)\n * @param {string} name Override the default keystore name.\n * @return {module:dcp/wallet.AuthKeystore} Return an authorization keystore.\n * @access public\n * @async\n */\nexports.get = async function wallet$$get()\n{\n var optionsOrNameOrNada;\n var ks;\n var options = {\n name: 'default',\n KeystoreConstructor: exports.AuthKeystore\n };\n\n if (!isArrayLike(arguments[0])) /* forms 4,5,6 */\n optionsOrNameOrNada = arguments[0];\n else\n {\n Object.assign(options, walletGet_argsToOptions(exports.AuthKeystore, arguments[0]));\n optionsOrNameOrNada = arguments[1]; /* re-write forms 4/5/6 to 2/1/3 */\n }\n\n if (typeof optionsOrNameOrNada === 'object')\n Object.assign(options, optionsOrNameOrNada); /* form 1 - options */\n else if (typeof optionsOrNameOrNada === 'undefined')\n ; /* form 2 - name */\n else if (typeof optionsOrNameOrNada === 'string')\n options.name = optionsOrNameOrNada; /* form 3 - nada */\n else\n throw new Error('unrecognized constructor form in wallet::get');\n \n return walletGet_form1(options);\n}\n\n/** @returns Promise => async */\nfunction walletGet_form1(options)\n{\n var ksPromise;\n \n if (ksCache.has(options.name, options.contextId))\n return ksCache.get(options.KeystoreConstructor, options.name, options.contextId);\n\n if (options.fetch)\n ksPromise = options.fetch(options);\n else\n ksPromise = getKeystore_platform(options);\n ksCache.set(options.KeystoreConstructor, options.name, options.contextId, ksPromise);\n \n return ksPromise;\n}\n\nfunction walletGet_argsToOptions(ksCtor, argv)\n{\n var options = {};\n\n for (let i=0; i < argv.length; i++)\n {\n if (argv[i][0] !== '-')\n continue;\n \n switch(argv[i][1]) {\n case 'p':\n argv.splice(i--, 1); \n options.checkEmpty = false;\n break;\n case 'i':\n if (ksCtor === exports.IdKeystore)\n options.name = argv[++i];\n break;\n case 'k':\n if (ksCtor === exports.AuthKeystore)\n options.name = argv[++i];\n break;\n }\n }\n}\n\n/** Keystore can be a keystore or a promise that resolves to a Keystore */\nfunction add_internal(keystore, options)\n{\n ksCache.set(options.KeystoreConstructor, options.name, options.contextId, keystore);\n}\n\n/** Add a temporary Keystore (in RAM) to the current wallet, which can be retrieved later by {@link module:dcp/wallet.get|get}.\n * @param {module:dcp/wallet.Keystore} keystore The keystore to add\n * @param {string} [name='default'] The name of the keystore\n * @param {string} [contextId] An optional identifier for caching keystores. See `job.contextId` in the compute-api spec.\n * @access public\n */\n/** Add a temporary Keystore (in RAM) to the current wallet, which can be retrieved later by {@link module:dcp/wallet.get|get}.\n * @param {Promise} keystore A promise which resolves to the keystore to add\n * @param {string} [name='default'] The name of the keystore\n * @param {string} [contextId] An optional identifier for caching keystores. See `job.contextId` in the compute-api spec.\n * @access public\n */\n/** Add a temporary Keystore (in RAM) to the current wallet, which can be retrieved later by {@link module:dcp/wallet.get|get}.\n * @param {DcpURL|URL} keystore A URL which can be used to fetch to the keystore to add\n * @param {string} [name='default'] The name of the keystore\n * @param {string} [contextId] An optional identifier for caching keystores. See `job.contextId` in the compute-api spec.\n * @access public\n */\nexports.add = function wallet$$add(keystore, name='default', contextId, KeystoreConstructor=exports.AuthKeystore)\n{\n return add_internal(keystore, { name, contextId, KeystoreConstructor });\n}\n\n/** \n * Same as {@link module:dcp/wallet.add|add}, except for retrieval by {@link module:dcp/wallet.getId|getId}\n * @param {module:dcp/wallet.Keystore} keystore The keystore to add\n * @param {string} [name='default'] The name of the keystore\n * @param {string} [contextId] An optional identifier for caching keystores. See `job.contextId` in the compute-api spec.\n * @access public\n */ \nexports.addId = function wallet$$addId(keystore, name='id', contextId, KeystoreConstructor=exports.IdKeystore)\n{\n return add_internal(keystore, { name, contextId, KeystoreConstructor });\n}\n\n/**\n * Removes all keystore entries from the cache.\n * @access public\n */\nexports.clear = function wallet$$clear() {\n ksCache.reset();\n}\n\nif (DCP_ENV.platform === 'nodejs') {\n try {\n exports._internals.ethereum_native = { util: requireNative('ethereumjs-util') };\n } catch(e) {}\n}\n\nexports.version = {\n api: '1.2.4',\n provides: '1.0.0'\n};\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/wallet/index.js?");
4334
+ eval("/**\n * @file Wallet API - perform operations related to Addresses, Keystores, Key Pairs\n *\n * @author Wes Garland - wes@kingsds.network\n * @author Duncan Mays - duncan@kingsds.network\n * @author Badrdine Sabhi - badr@kingsds.network\n * @author Ryan Rossiter - ryan@kingsds.network\n * @date August 2019\n * @module dcp/wallet\n * @access public\n */\n\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('wallet');\n\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst sh = __webpack_require__(/*! dcp/utils/sh */ \"./src/utils/sh.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nif (DCP_ENV.platform === 'nodejs') {\n var path = requireNative('path');\n var fs = requireNative('fs')\n}\n\nexports._internalEth = __webpack_require__(/*! ./keystore */ \"./src/dcp-client/wallet/keystore.js\")._internalEth; /* MUST load before sub-modules for ethereum module detection */\n\nconst ksCache = __webpack_require__(/*! ./ks-cache */ \"./src/dcp-client/wallet/ks-cache.js\");\nObject.assign(exports, __webpack_require__(/*! ./keystore */ \"./src/dcp-client/wallet/keystore.js\"));\nObject.assign(exports, __webpack_require__(/*! ./eth */ \"./src/dcp-client/wallet/eth.js\"));\nObject.assign(exports, __webpack_require__(/*! ./address-collection */ \"./src/dcp-client/wallet/address-collection.js\"));\nObject.assign(exports, __webpack_require__(/*! ./passphrase-prompt */ \"./src/dcp-client/wallet/passphrase-prompt.js\"));\nObject.assign(exports, __webpack_require__(/*! ./error-codes */ \"./src/dcp-client/wallet/error-codes.js\"));\n\nfunction isArrayLike(a)\n{\n return typeof a === 'object' && (Array.isArray(a) || typeof a.length === 'number');\n}\n\n/**\n * This property sets the name of the namespace that is used to generate the application data directory, which is used to store proxy keys when required,\n * and users’ bank and identity keys when supplied.\n * The default namespace name is the name specified in the package.json file for the application calling into this API.\n */\nexports.namespaceName = undefined;\n\n/**\n * This property gets/sets the full path name the application data directory, overriding namespaceName if it is changed.\n * The default value is ~/.dcp/applications/${namespaceName}/, where ~ represents the invoking user’s home directory.\n */\nexports.stateDir = undefined;\n\n/**\n * @access public\n * @typedef {object} LoadResult - Object returned by `wallet.load`\n * @property {Keystore} keystore - the instance of the keystore that was loaded\n * @property {boolean} safe - indicates that the keystore was read from a secure location.\n */\n\n\n/**\n * Form 1\n * \n * This function locates and reads a keystore file.\n * \n * **NodeJS** only: `filename` must be an absolute path, or begin withgin with `path.sep, '.' + path.sep`, or `'..' + path.sep`.\n * The safe flag is set to true to indicate that the Keystore file was stored safely; this happens if and only if the following conditions are true:\n * \n * 1. The containing directory and all its ancestors, up to and including the root directory, is not world-writable.\n * 2. The Keystore File is neither world-writable nor world-readable.\n * \n * @param {string} filename The keystore filename\n * @returns Object with the following properties:\n * - contents: the contents of the file\n * - safe: if the file was loaded from a safe path (perms check)\n * - filename: the name of the file that was actually loaded\n */\n/**\n * Form 2\n * \n * This function locates and reads a keystore file.\n *\n * @param {object} options - An options object\n * @param {string} [options.name='default'] - The keystore label, or filename\n * @param {string[]} [options.paths] - Override default keystore directory search path, normally the application data directory, followed by the `.dcp` \n * directory in the home directory belonging to the user invoking the program (NodeJS only). Note that the search path is never used when name specifies a \n * complete pathname.\n * @param {string} [options.dir] - Overrides paths, equivalent to `paths: [dir]`\n * @returns Object with the following properties:\n * - contents: the contents of the file\n * - safe: if the file was loaded from a safe path (perms check)\n * - filename: the name of the file that was actually loaded\n */\nfunction loadKeystore(options) {\n if (typeof options === 'string') {\n options = {\n name: sh.expandPath(options),\n };\n }\n\n /* Deep clone and use default values to augment the passed-in options object.\n * The deep clone is necessary because this API mutates properties of the object.\n */\n options = JSON.parse(JSON.stringify(Object.assign({}, {\n name: \"default\",\n paths: [ (__webpack_require__(/*! dcp/common/dcp-dot-dir */ \"./src/common/dcp-dot-dir.js\").dotDcpDir) ],\n }, options)));\n\n let filename;\n if (options.name.match(/[/\\\\]/)) {\n // if the name contains a slash (or backslash), then treat it as a filename\n filename = sh.expandPath(options.name);\n\n if (!fs.existsSync(filename)) {\n let err = new Error(`Could not locate keystore '${filename}'`);\n err.code = 'ENOENT';\n throw err;\n }\n } else {\n // otherwise, treat the name as a keystore label and search\n // for a keystore named `${options.name}.keystore` in options.paths\n\n if (options.dir)\n options.paths = [sh.expandPath(options.dir)];\n else if (exports.stateDir)\n options.paths.unshift(path.resolve(exports.stateDir))\n else if (exports.namespaceName)\n options.paths.unshift(path.resolve(sh.expandPath('~/.dcp/applications'), exports.namespaceName));\n \n let searchPaths = options.paths.map((searchPath) =>\n path.resolve(searchPath, `${options.name}.keystore`)\n );\n\n filename = searchPaths.find((fn) => {\n debugging('search') && console.log(`wallet - checking ${fn}`);\n return fs.existsSync(fn);\n });\n\n if (filename) {\n debugging('searchResult') && console.log(`wallet - found keystore at ${filename}`);\n } else {\n const errMsg = `Could not locate keystore named '${options.name}' in these locations: ${JSON.stringify(searchPaths)}`;\n let err = new Error(errMsg);\n debugging('searchResult') && console.error(`wallet - ${errMsg}`);\n err.code = 'ENOENT';\n throw err;\n }\n }\n\n debugging() && console.log(`wallet - loading '${filename}'`);\n //gets the keystore or private key from the file\n return {\n safe: exports.isSafe(filename),\n filename: filename,\n contents: fs.readFileSync(filename, 'utf-8').trim()\n };\n}\n\n/**\n * Form 1\n * \n * This function locates and reads a keystore file,\n * and instantiates a Keystore from it.\n * \n * **NodeJS** only: `filename` must be an absolute path, or begin withgin with `path.sep, '.' + path.sep`, or `'..' + path.sep`.\n * The safe flag is set to true to indicate that the Keystore file was stored safely; this happens if and only if the following conditions are true:\n * \n * 1. The containing directory and all its ancestors, up to and including the root directory, is not world-writable.\n * 2. The Keystore File is neither world-writable nor world-readable.\n * \n * @param {string} filename The keystore filename\n * @returns {module:dcp/wallet~LoadResult}\n * @async\n * @access public\n */\n/**\n * Form 2\n * \n * This function locates and reads a keystore file,\n * and instantiates a Keystore from it.\n *\n * @param {object} options - An options object\n * @param {string} [options.name='default'] - The keystore label, or filename\n * @param {string[]} [options.paths] - Override default keystore directory search path, normally the application data directory, followed by the `.dcp` \n * directory in the home directory belonging to the user invoking the program (NodeJS only). Note that the search path is never used when name specifies a \n * complete pathname.\n * @param {string} [options.dir] - Overrides paths, equivalent to `paths: [dir]`\n * @param {KeystoreConstructor} [options.KeystoreConstructor] - Constructor to use when creating the keystore\n * @param {boolean} [options.checkEmpty] - if empty password should be tried before prompting the user.\n * @returns {module:dcp/wallet~LoadResult}\n * @async\n * @access public\n */\nexports.load = async function wallet$$load(options) {\n var { contents, filename, safe } = loadKeystore.apply(this, arguments);\n var KeystoreConstructor = (options && options.KeystoreConstructor) || exports.Keystore;\n const keystore = await new KeystoreConstructor(JSON.parse(contents), false, options.checkEmpty);\n if (!keystore.label)\n keystore.label = path.basename(filename, '.keystore');\n \n //return keystore in an object with a safety indicator\n return {\n keystore,\n filename,\n safe\n }\n}\n\n/**\n * Form 1\n * \n * This function locates and reads a keystore file,\n * and instantiates an Address from it.\n * \n * **NodeJS** only: `filename` must be an absolute path, or begin withgin with `path.sep, '.' + path.sep`, or `'..' + path.sep`.\n * The safe flag is set to true to indicate that the Keystore file was stored safely; this happens if and only if the following conditions are true:\n * \n * 1. The containing directory and all its ancestors, up to and including the root directory, is not world-writable.\n * 2. The Keystore File is neither world-writable nor world-readable.\n * \n * @param {string} filename The keystore filename\n * @access public\n */\n/**\n * Form 2\n * \n * This function locates and reads a keystore file,\n * and instantiates an Address from it.\n *\n * @param {object} options - An options object\n * @param {string} [options.name='default'] - The keystore label, or filename\n * @param {string[]} [options.paths] - Override default keystore directory search path, normally the application data directory, followed by the `.dcp` \n * directory in the home directory belonging to the user invoking the program (NodeJS only). Note that the search path is never used when name specifies a \n * complete pathname.\n * @param {string} [options.dir] - Overrides paths, equivalent to `paths: [dir]`\n * @access public\n */\nexports.loadAddress = function wallet$$loadAddress(options) {\n var { contents, filename, safe } = loadKeystore.apply(this, arguments);\n\n return {\n filename,\n address: new exports.Address(JSON.parse(contents).address),\n safe\n };\n}\n\n/**\n * @param {Object} keystore instance of Keystore to save\n * @param {string} filename the filename to save it in\n * @param {Object} writeFileOptions optional options object to pass to node's\n * fs.writeFile(). Note that the mode will\n * always force utf-8 output.\n * @param {Object|boolean} mkdirOptions optional options object to pass to node's fs.mkdir().\n * By default it will use recursive: true.\n * Disable mkdir with 'false'.\n */\nexports.save = async function wallet$$save(keystore, filename, writeFileOptions, mkdirOptions) {\n const dir = path.dirname(filename);\n if (mkdirOptions !== false && !fs.existsSync(dir)) {\n await fs.promises.mkdir(dir, {\n recursive: true,\n ...mkdirOptions,\n });\n }\n \n await fs.promises.writeFile(filename, JSON.stringify(keystore), {\n ...writeFileOptions,\n encoding: 'utf-8', // force utf-8 encoding\n });\n}\n\n/**\n * This function checks the safety requirements outlined in the Keystore API documentation\n * The requirements are that the file must neither be world readable or world writable, \n * or have any parent directory that is world writable. This function will return a boolean which indicates\n * weather or not these conditions are met.\n *\n * @param {string} - filePath, this is the path to the file we are checking the safety of, this\n * function will not work properly, and in fact will log a warning message, if that filePath \n * starts with either . or ..\n * @returns {boolean} - indicates weather or not the file provided satisfies safety criteria\n */\nexports.isSafe = function wallet$$isSafe(filePath) {\n if (!path.isAbsolute(filePath)) {\n throw new Error(`Not an absolute path: '${filePath}'`);\n }\n\n let pathArray = filePath.split(path.sep);\n pathArray[0] = pathArray[0] || path.parse(filePath).root;\n if (exports.isWorldReadable(filePath))\n return false;\n\n //checks that the file and all its ancestor directories are not world readable\n while (pathArray.length > 0) {\n let reassemble = path.join.apply(null, pathArray);\n if (exports.isWorldWritable(reassemble))\n return false;\n pathArray = pathArray.slice(0, pathArray.length - 1);\n }\n\n return true;\n}\n\n/**\n * This function takes a filepath and returns a boolean to indicate weather or not the location specified in the filepath is world readable\n *\n * @param {string} filePath - path to the file in question, does not work right if the filePath starts with . or ..\n *\n * @returns {boolean} - indicates weather the given file or directory is world readable\n */\nexports.isWorldReadable = function wallet$$isWorldReadable(filePath) {\n const mode = fs.statSync(filePath).mode;\n return mode & 4;\n}\n\n/**\n * This function takes a filepath and returns a boolean to indicate weather or not the location specified in the filepath is world writable\n *\n * @param {string} filePath - path to the file in question, does not work right if the filePath starts with . or ..\n *\n * @returns {boolean} - indicates weather the given file or directory is world writable.\n */\nexports.isWorldWritable = function wallet$$isWorldWritable(filePath) {\n debugging('safety') && console.debug(`wallet - checking write permissions on '${filePath}'`);\n const mode = fs.statSync(filePath).mode;\n return mode & fs.constants.W_OK;\n}\n\n/**\n * These functions examine parameters and accepts options relating to its environment, uses this information \n * to select a Keystore File, load it via wallet.load(), and return an AuthKeystore object.\n * If it cannot return an AuthKeystore object, this function will throw an Error.\n *\n * @param {object | string} param1 | param2 - Three accepted keys: name, which specifies the name of the keystore, dir, which specifies its directpry, \n * and checkEmpty, which controls weather or not unlock() will attempt an empty password on the keystore \n * before prompting the user.\n * @returns {Object} instance of Keystore\n */\nasync function wallet$$get_browser(options) {\n const ClientModal = __webpack_require__(/*! dcp/dcp-client/client-modal */ \"./src/dcp-client/client-modal/index.js\");\n const keystoreFileContents = DCP_ENV.getenv('dcp-wallet-oauth-integration') ? await ClientModal.getOauthLogin(options) : await ClientModal.getKeystoreFile(options);\n\n return new options.KeystoreConstructor(keystoreFileContents);\n}\n\nasync function wallet$$get_node(options)\n{\n async function inner()\n {\n const result = await exports.load(options);\n\n if (result && result.keystore)\n return result.keystore;\n return false;\n }\n\n return inner();\n}\n\nconst getKeystore_platform = DCP_ENV.switch({\n nodejs: wallet$$get_node,\n bravojs: wallet$$get_browser,\n 'vanilla-web': wallet$$get_browser,\n default: typeof navigator === 'undefined' ? wallet$$get_node : wallet$$get_browser\n});\n\n/** \n * Return a promise which resolves to an identity keystore. See {@link module:dcp/wallet.get|get}.\n *\n * If an identity keystore has never been generated or added to the wallet with addId, a new\n * one will be either fetched if there are NO arguments passed to this function.\n *\n * On browser platforms, an idKs will be dynamically generated if one has not been specified\n * beforehand. This behaviour can be defeated by passing arguments=[false], which will be treated \n * the same way as arguments=[].\n *\n * On browser platforms, we try to \"fetch\" the identity from a file upload, but ONLY if the\n * arguments to this function have been specified. The idea here is that the default behaviour,\n * for getId() to generate a random id, is the best behaviour from a UX POV unless the developer \n * is doing something really weird.\n *\n * On non-browser platforms, we \"fetch\" the identity via wallet.get(), which eventually falls\n * back to looking in ~/.dcp/id.keystore.\n *\n * Implementation note: cannot await between function call and initial population of ksCache in \n * walletGet_form1 if adding a new identity; otherwise, multiple parallel connections might \n * trigger new identity \"fetches\" etc.\n * \n * @returns {module:dcp/wallet.AuthKeystore}\n * @access public\n * @async\n */\nexports.getId = async function wallet$$getId()\n{\n var optionsOrNameOrNada;\n var ks;\n var options = {\n name: 'id',\n KeystoreConstructor: exports.IdKeystore\n };\n var generateKeystoreOnDemand = true;\n \n if (arguments.length === 1 && arguments[0] === false)\n {\n generateKeystoreOnDemand = false;\n delete arguments[0];\n }\n\n if (!isArrayLike(arguments[0])) /* forms 4,5,6 */\n optionsOrNameOrNada = arguments[0];\n else\n {\n Object.assign(options, walletGet_argsToOptions(exports.IdKeystore, arguments[0]));\n optionsOrNameOrNada = arguments[1]; /* re-write forms 4/5/6 to 2/1/3 */\n }\n\n if (typeof optionsOrNameOrNada === 'object')\n Object.assign(options, optionsOrNameOrNada); /* form 1 - options */\n else if (typeof optionsOrNameOrNada === 'undefined')\n ; /* form 2 - name */\n else if (typeof optionsOrNameOrNada === 'string')\n options.name = optionsOrNameOrNada; /* form 3 - nada */\n else\n throw new Error('unrecognized constructor form in wallet::getId');\n\n if (DCP_ENV.isBrowserPlatform && generateKeystoreOnDemand && arguments.length === 0)\n {\n options.fetch = function wallet$$getId$generate() {\n debugging() && console.debug('wallet - generating dynamic identity');\n return new exports.IdKeystore(null, ''); \n };\n }\n\n let cached = await ksCache.get(options.KeystoreConstructor, options.name, options.contextId);\n if (cached)\n return cached;\n\n ks = await walletGet_form1(options);\n\n debugging('identity') && console.debug('wallet - identity is', !ks ? ks : ks.address);\n return ks;\n}\n\n/** \n * Form 1\n * \n * This form loads a Keystore File via {@link module:dcp/wallet.load|load}.\n * The options object is used to override default parameters which help to identify and locate the Keystore File.\n * \n * **On NodeJS**: The options object is passed directly to `wallet.load`.\n * \n * **On Web**: The options object is primarily used to provide context to the user for showing a modal; \n * @param {object} options\n * @param {string} [options.name='default'] Override the default keystore name.\n * @param {string} [options.contextId] An optional, user-defined identifier used for caching keystores.\n * See `job.contextId` in the compute-api spec.\n * @param {string} [options.jobName] Optional name of the job that the keystore is being requested for.\n * @param {boolean} [options.checkEmpty=true] Try an empty password before prompting user\n * @param {function} [options.KeystoreConstructor] Override the constructor to use for the keystore.\n * @param {string} [options.msg] Custom message for that modal that describes the purpose for providing the keystore\n * @return {module:dcp/wallet.AuthKeystore} Return an authorization keystore.\n * @access public\n * @async\n */\n/**\n * Form 2\n * \n * This form is equivalent to `wallet.get({})`.\n * @return {module:dcp/wallet.AuthKeystore} Return an authorization keystore.\n * @access public\n * @async\n */\n/**\n * Form 3\n * \n * This form is equivalent to `wallet.get({name: string})`\n * @param {string} name Override the default keystore name.\n * @return {module:dcp/wallet.AuthKeystore} Return an authorization keystore.\n * @access public\n * @async\n */\n/**\n * Form 4\n * \n * This form accepts an Array-like object, formatted like `process.argv`, to specify options for invoking Form 1. \n * Its purpose is to unify Keystore, PrivateKey, and Address-related options for programs written in NodeJS.\n * @param {string[]} array Array-like object, formatted like `process.argv`\n * | Argument | Behaviour |\n * |---------------|-----------|\n * | --private-key | Generate a keystore corresponding to the specified private key, ignoring all other considerations |\n * | -i name | Override the `name` option for getId |\n * | -k name | Override the `name` option for get |\n * | -p | Set the `checkEmpty` option to false |\n * @return {module:dcp/wallet.AuthKeystore} Return an authorization keystore.\n * @access public\n * @async\n */\n/**\n * Form 5\n * \n * This form accepts an Array-like object, as described in form 4, and an options object \n * as described in form 1. Its behaviour is to generate an options object from the array, \n * merge the passed options object into this options object, and invoke form 1.\n * To be clear, the options argument has a higher precedence than the array argument.\n * @param {string[]} array Array-like object, formatted like `process.argv`. (See Form 4)\n * @param {object} options Options object, see Form 1.\n * @return {module:dcp/wallet.AuthKeystore} Return an authorization keystore.\n * @access public\n * @async\n */\n/**\n * Form 6\n * \n * This form is equivalent to `wallet.get(array, {name: string})`.\n * @param {string[]} array Array-like object, formatted like `process.argv`. (See Form 4)\n * @param {string} name Override the default keystore name.\n * @return {module:dcp/wallet.AuthKeystore} Return an authorization keystore.\n * @access public\n * @async\n */\nexports.get = async function wallet$$get()\n{\n var optionsOrNameOrNada;\n var ks;\n var options = {\n name: 'default',\n KeystoreConstructor: exports.AuthKeystore\n };\n\n if (!isArrayLike(arguments[0])) /* forms 4,5,6 */\n optionsOrNameOrNada = arguments[0];\n else\n {\n Object.assign(options, walletGet_argsToOptions(exports.AuthKeystore, arguments[0]));\n optionsOrNameOrNada = arguments[1]; /* re-write forms 4/5/6 to 2/1/3 */\n }\n\n if (options.privateKey && typeof optionsOrNameOrNada === 'undefined') /* for 4 with private key - generate keystore with private key ignoring all other considerations */\n return new exports.AuthKeystore(options.privateKey, false)\n else if (typeof optionsOrNameOrNada === 'object')\n {\n if (Object.keys(optionsOrNameOrNada).length !== 0)\n Object.assign(options, optionsOrNameOrNada); /* form 1 - options */ \n }\n else if (typeof optionsOrNameOrNada === 'undefined')\n ; /* form 2 - name */\n else if (typeof optionsOrNameOrNada === 'string')\n options.name = optionsOrNameOrNada; /* form 3 - nada */\n else\n throw new Error('unrecognized constructor form in wallet::get');\n \n return walletGet_form1(options);\n}\n\n/** @returns Promise => async */\nfunction walletGet_form1(options)\n{\n var ksPromise;\n \n if (ksCache.has(options.name, options.contextId))\n return ksCache.get(options.KeystoreConstructor, options.name, options.contextId);\n\n if (options.fetch)\n ksPromise = options.fetch(options);\n else\n ksPromise = getKeystore_platform(options);\n ksCache.set(options.KeystoreConstructor, options.name, options.contextId, ksPromise);\n \n return ksPromise;\n}\n\nfunction walletGet_argsToOptions(ksCtor, argv)\n{\n var options = {};\n\n for (let i=0; i < argv.length; i++)\n {\n if (argv[i][0] !== '-')\n continue;\n \n switch(argv[i]) {\n case '--private-key':\n options.privateKey = argv[++i];\n break;\n case '-p':\n argv.splice(i--, 1); \n options.checkEmpty = false;\n break;\n case '-i':\n if (ksCtor === exports.IdKeystore)\n options.name = argv[++i];\n break;\n case '-k':\n if (ksCtor === exports.AuthKeystore)\n options.name = argv[++i];\n break;\n }\n }\n return options;\n}\n\n/** Keystore can be a keystore or a promise that resolves to a Keystore */\nfunction add_internal(keystore, options)\n{\n ksCache.set(options.KeystoreConstructor, options.name, options.contextId, keystore);\n}\n\n/** Add a temporary Keystore (in RAM) to the current wallet, which can be retrieved later by {@link module:dcp/wallet.get|get}.\n * @param {module:dcp/wallet.Keystore} keystore The keystore to add\n * @param {string} [name='default'] The name of the keystore\n * @param {string} [contextId] An optional identifier for caching keystores. See `job.contextId` in the compute-api spec.\n * @access public\n */\n/** Add a temporary Keystore (in RAM) to the current wallet, which can be retrieved later by {@link module:dcp/wallet.get|get}.\n * @param {Promise} keystore A promise which resolves to the keystore to add\n * @param {string} [name='default'] The name of the keystore\n * @param {string} [contextId] An optional identifier for caching keystores. See `job.contextId` in the compute-api spec.\n * @access public\n */\n/** Add a temporary Keystore (in RAM) to the current wallet, which can be retrieved later by {@link module:dcp/wallet.get|get}.\n * @param {DcpURL|URL} keystore A URL which can be used to fetch to the keystore to add\n * @param {string} [name='default'] The name of the keystore\n * @param {string} [contextId] An optional identifier for caching keystores. See `job.contextId` in the compute-api spec.\n * @access public\n */\nexports.add = function wallet$$add(keystore, name='default', contextId, KeystoreConstructor=exports.AuthKeystore)\n{\n return add_internal(keystore, { name, contextId, KeystoreConstructor });\n}\n\n/** \n * Same as {@link module:dcp/wallet.add|add}, except for retrieval by {@link module:dcp/wallet.getId|getId}\n * @param {module:dcp/wallet.Keystore} keystore The keystore to add\n * @param {string} [name='default'] The name of the keystore\n * @param {string} [contextId] An optional identifier for caching keystores. See `job.contextId` in the compute-api spec.\n * @access public\n */ \nexports.addId = function wallet$$addId(keystore, name='id', contextId, KeystoreConstructor=exports.IdKeystore)\n{\n return add_internal(keystore, { name, contextId, KeystoreConstructor });\n}\n\n/**\n * Removes all keystore entries from the cache.\n * @access public\n */\nexports.clear = function wallet$$clear() {\n ksCache.reset();\n}\n\nif (DCP_ENV.platform === 'nodejs') {\n try {\n exports._internals.ethereum_native = { util: requireNative('ethereumjs-util') };\n } catch(e) {}\n}\n\nexports.version = {\n api: '1.2.4',\n provides: '1.0.0'\n};\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/wallet/index.js?");
4325
4335
 
4326
4336
  /***/ }),
4327
4337
 
@@ -4332,7 +4342,7 @@ eval("/**\n * @file Wallet API - perform operations related to Addresses,
4332
4342
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4333
4343
 
4334
4344
  "use strict";
4335
- eval("/* 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 keystore.js\n * Wallet API Keystore classes\n *\n * @author Wes Garland - wes@kingsds.network\n * @author Duncan Mays - duncan@kingsds.network\n * @author Badrdine Sabhi - badr@kingsds.network\n * @author Ryan Rossiter - ryan@kingsds.network\n * @date April 2020\n */\n\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('wallet/keystore');\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\");\n\nconst { unlockFailErrorCode } = __webpack_require__(/*! dcp/dcp-client/wallet/error-codes */ \"./src/dcp-client/wallet/error-codes.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\n\nlet useWasm = true;\nlet secp256k1 = null;\n\n/* Place canonical versions of ethereumjs-util and ethereumjs-wallet on\n * exports._internalEth for reference by 'friend' modules. Try to load \n * native (fast) versions on NodeJS, but fallback to internal modules\n * when not present. Modules are argumented with fast keccak\n * hashing code via wasm for internal use.\n */\nexports._internalEth = {};\n\nif ((__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").platform) === 'nodejs')\n{\n /* Prefer peer dependencies over webpacked versions whenever possible, \n * because they will have native crypto via scrypt.\n */\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n try {\n requireNative('bindings')({\n bindings: 'keccak.node',\n try: [\n ['module_root', 'node_modules', 'keccak', 'build', 'Release', 'bindings'], // when running from dcp\n ['module_root', '..', 'keccak', 'build', 'Release', 'bindings'], // when running from dcp-client in dcp\n ],\n });\n exports._internalEth.util = requireNative('ethereumjs-util');\n exports._internalEth.wallet = requireNative('ethereumjs-wallet').default;\n useWasm = false; /* only disable wasm when using node AND have native util, wallet available */\n } catch(e) {\n exports._internalEth.util = __webpack_require__(/*! ethereumjs-util */ \"./node_modules/ethereumjs-util/dist.browser/index.js\");\n exports._internalEth.wallet = __webpack_require__(/*! ethereumjs-wallet */ \"./node_modules/ethereumjs-wallet/dist.browser/index.js\")[\"default\"];\n };\n} else {\n exports._internalEth.util = __webpack_require__(/*! ethereumjs-util */ \"./node_modules/ethereumjs-util/dist.browser/index.js\");\n exports._internalEth.wallet = __webpack_require__(/*! ethereumjs-wallet */ \"./node_modules/ethereumjs-wallet/dist.browser/index.js\")[\"default\"];\n}\n\nconst dcpEth = __webpack_require__(/*! ./eth */ \"./src/dcp-client/wallet/eth.js\");\nconst ethUtil = exports._internalEth.util;\nconst ethWallet = exports._internalEth.wallet;\n\nconst _ConstructOnly = Symbol('Construct Only');\n \n/** Generate a new private key */\nfunction generateNewPrivateKey() {\n return new dcpEth.PrivateKey(ethWallet.generate().getPrivateKey());\n}\n\n/** Accept json from any foreign ethereum wallet [that we understand] and a passphrase,\n * returning the address and private key\n *\n * @param json {string} A JSON keystore, eg. ethereum V3 wallet\n * @param passphrase {string} The passphrase to unlock the keystore\n * @returns {Object|undefined} An object having properties address and privateKey,\n * or undefined if it couldn't be parsed\n */\nasync function parseForeignKeystore(json, passphrase) {\n var fks;\n\n try {\n fks = await ethWallet.fromV3(json, passphrase, { nonStrict: true });\n } catch(e) {\n debugging() && passphrase && console.debug('warning: incorrect passphrase or foreign keystore not in wallet-v3 format', json);\n }\n\n if (!fks) try {\n fks = await ethWallet.fromV1(json, passphrase);\n } catch(e) {}\n\n if (!fks) try {\n fks = await ethWallet.fromEthSale(json, passphrase);\n } catch(e) {}\n\n return fks && {\n address: fks.address,\n privateKey: fks.getPrivateKey(),\n };\n}\n\n/**\n * This method creates functions related to locking and unlocking Keystores which share a common set\n * of private variables and are added to the keystore as own property. The private key must never be\n * stored in a variable in dcp-client which is not part of this private scope.\n *\n * @param ks {Object} instance of keystore\n * @param ew {String} json which represents an Ethereum V3 keystore [ethereum wallet]\n */\nfunction KeystoreLockFunctionsFactory(ks, ew)\n{\n const { Synchronizer } = __webpack_require__(/*! dcp/common/concurrency */ \"./src/common/concurrency.js\");\n \n var pkScope_privateKey; /* The private key when unlocked; falsey when locked */\n var pkScope_autoUnlockDuration; /* lock duration when doing auto-unlock (like sudo), or falsey */\n var pkScope_relockTime; /* when the Keystore will be considered locked again */\n var pkScope_lockTimerHnd; /* Timer handle when we have a pending automatic relock on the event queue */\n var pkScope_unlockBusy = new Synchronizer('idle', ['idle', 'busy']); /* mutex to single-thread unlock */\n var debugLabel = ks.label || ks.address;\n\n function pkScope_resetLockState() {\n debugging('timer') && console.debug(`wallet - reset lock timer state on ${debugLabel}`);\n\n if (pkScope_lockTimerHnd)\n clearTimeout(pkScope_lockTimerHnd)\n \n pkScope_privateKey = undefined;\n pkScope_autoUnlockDuration = 0;\n pkScope_relockTime = 0;\n pkScope_lockTimerHnd = undefined;\n }\n pkScope_resetLockState();\n\n /* Check the timer state, re-locking the Keystore as needed. It is\n * important to call this at the start of timer-sensitive operations, as\n * we can't rely on setTimeout to re-lock Keystores; we must consider the\n * possibility that starving the event loop could cause the private key\n * to be stored for longer than intended\n */\n function pkScope_lockIfTimerExpired() {\n debugging('timer') && console.log(`wallet - lockIfTimerExpired on ${debugLabel}`);\n if (pkScope_lockTimerHnd && pkScope_relockTime <= Date.now()) {\n pkScope_lock()\n }\n }\n \n /**\n * This method unlocks the Keystore object for either a single use or a given amount of time. When the \n * object is unlocked, `getPrivateKey` and related operations do not need to ask for a passphrase, as \n * the private key is \"remembered\" internally. If time is not specified, the object will be immediately \n * locked after the next call to `getPrivateKey`; otherwise, it will remain unlocked until the timer runs out.\n * \n * When the unlock timer is running, calling `unlock` can have multiple effects. In all cases the current\n * running timer will be increased to lockTimerDuration if that will increase the time left.\n * 1. If the autoReset flag is true for this invocation, the autoReset duration is updated only if it\n * will be increased. Thus autoReset is a sticky setting, it can be turned on but not off (except by locking)\n * 2. If the autoReset flag is false for this invocation, the current unlock duration is increased, but \n * the autoReset duration remains the same.\n * @method module:dcp/wallet.Keystore#unlock\n * @param {string} [passphrase] Passphrase to unlock keystore. If null|undefined, `passphrasePrompt` \n * function is invoked to solicit it from the user if the keystore\n * is not already unlocked.\n * @param {number} lockTimerDuration Number of seconds to keep keystore unlocked for. Intended to implement features like `sudo`\n * @param {boolean} [autoReset] Flag to determine if unlock timer should reset on subsequent calls.\n * @access public\n * @async\n * @throws Error with code 'DCP_WA:UNLOCKX' when keystore could not be unlocked.\n */\n async function pkScope_unlock(passphrase, lockTimerDuration, autoReset)\n {\n do /* spin lock to single thread this per keystore */\n {\n await pkScope_unlockBusy.until('idle');\n } while(!pkScope_unlockBusy.testAndSet('idle', 'busy'))\n\n try\n {\n pkScope_lockIfTimerExpired();\n if (!pkScope_privateKey)\n {\n async function pkScope_extractAndSetPrivateKey()\n {\n var fks;\n \n /* used by passphraseTries */\n function unlock_tryFn(passphrase)\n {\n if (pkScope_privateKey) /* unlocked by other \"thread\" - ideally would never happen... */\n return { address: ks.address, privateKey: pkScope_privateKey };\n return parseForeignKeystore(ew, passphrase);\n }\n\n /* used by passphraseTries */\n function unlock_skipCheckFn()\n {\n return !!pkScope_privateKey;\n };\n\n if (passphrase !== undefined && passphrase !== null)\n fks = await parseForeignKeystore(ew, passphrase);\n else\n {\n fks = await parseForeignKeystore(ew, '');\n if (!fks)\n fks = await (__webpack_require__(/*! ../wallet */ \"./src/dcp-client/wallet/index.js\").passphraseTries)({ label: ks.label , address: ks.address }, unlock_tryFn, unlock_skipCheckFn);\n }\n \n if (fks)\n pkScope_privateKey = new dcpEth.PrivateKey(fks.privateKey);\n }\n\n await pkScope_extractAndSetPrivateKey();\n if (!pkScope_privateKey)\n throw new DCPError(`Could not unlock keystore '${debugLabel}'`, unlockFailErrorCode);\n }\n assert(pkScope_privateKey);\n\n /* If in autoReset mode, increase the autoUnlockDuration if a longer one was specified */\n if (autoReset && lockTimerDuration > pkScope_autoUnlockDuration)\n pkScope_autoUnlockDuration = lockTimerDuration;\n \n // If lockTimerDuration not defined, set to 0s (immediately lock)\n lockTimerDuration = lockTimerDuration ? lockTimerDuration : 0;\n /* Make this unlock expire in lockTimerDuration seconds [or later if there was a previous longer one] */\n pkScope_scheduleRelock(lockTimerDuration);\n debugging('locks') && console.log(`wallet - ${debugLabel} is unlocked`);\n }\n finally\n {\n pkScope_unlockBusy.set('busy', 'idle');\n }\n }\n\n /** \n * Lock the Keystore; make private key no longer accessible without password entry.\n * @method module:dcp/wallet.Keystore#lock\n * @access public\n */\n function pkScope_lock()\n {\n let debugMsg = `wallet - lock ${debugLabel}`;\n if (pkScope_isUnlocked()) debugMsg += ' (was not locked)';\n debugging('locks') && console.log(debugMsg);\n \n pkScope_resetLockState();\n }\n\n /**\n * Returns private key, may prompt for password. If you need this you're probably doing something wrong.\n * @method module:dcp/wallet.Keystore#getPrivateKey\n * @returns {module:dcp/wallet.PrivateKey}\n * @async\n * @access public\n */\n async function pkScope_getPrivateKey()\n {\n let pk;\n\n pkScope_lockIfTimerExpired();\n if (pkScope_isLocked()) {\n await pkScope_unlock();\n pk = pkScope_privateKey;\n pkScope_lock();\n } else {\n pk = pkScope_privateKey;\n \n if (pkScope_autoUnlockDuration > 0) {\n debugging('timer') && console.log(`wallet - auto reset new lock expire time on ${debugLabel} - ${pkScope_autoUnlockDuration * 1000}ms`);\n pkScope_scheduleRelock(pkScope_autoUnlockDuration);\n } else if (!pkScope_lockTimerHnd) {\n pkScope_lock();\n }\n }\n \n debugging('getPrivateKey') && console.log(`wallet - ${debugLabel} pk=${pk}`);\n return pk;\n }\n\n /* Schedule the next automatic lock of this keystore, overriding any earlier-firing relock\n * @param {number} newDuration seconds until the keystore gets re-locked\n */\n function pkScope_scheduleRelock(newDuration)\n {\n var newDurationMs = newDuration * 1000;\n var next_relockTime = Date.now() + newDurationMs;\n\n if (next_relockTime < pkScope_relockTime)\n return;\n \n debugging('timer') && console.log(`wallet - setting lock expire time on ${debugLabel} - ${next_relockTime}s`);\n pkScope_relockTime = next_relockTime;\n if (pkScope_lockTimerHnd)\n clearTimeout(pkScope_lockTimerHnd);\n pkScope_lockTimerHnd = setTimeout(pkScope_lock, newDurationMs);\n if (DCP_ENV.platform === 'nodejs')\n pkScope_lockTimerHnd.unref();\n }\n\n /**\n * Is the keystore presently unlocked?\n * @method module:dcp/wallet.Keystore#isUnlocked\n * @returns {boolean}\n * @access public\n */\n function pkScope_isUnlocked() {\n return !!pkScope_privateKey;\n }\n\n /**\n * Is the keystore presently locked?\n * @method module:dcp/wallet.Keystore#isLocked\n * @returns {boolean}\n * @access public\n */\n function pkScope_isLocked() {\n return !pkScope_isUnlocked();\n }\n \n ks.isLocked = pkScope_isLocked;\n ks.isUnlocked = pkScope_isUnlocked;\n ks.unlock = pkScope_unlock;\n ks.lock = pkScope_lock;\n ks.getPrivateKey = pkScope_getPrivateKey;\n}\n\n/**\n * Form 1\n * \n * A Keystore object with a randomly generated privateKey and an address\n * that corresponds to it. This form will prompt the user for a password to encrypt itself with.\n * @async\n * @returns {module:dcp/wallet.Keystore}\n * @access public\n */\n/**\n * Form 2\n * \n * A Keystore object with the provided privateKey and an address that \n * corresponds to it. This form will prompt the user for a password to encrypt itself with.\n * @async\n * @param {string|object} privateKey an Ethereum v3 Keystore object, or privateKey in hex string format\n * @returns {module:dcp/wallet.Keystore}\n * @access public\n */\n/**\n * Form 3\n * \n * A Keystore object with the provided privateKey and an address that \n * corresponds to it, encrypted with the provided passphrase.\n * @async\n * @param {string|object} privateKey an Ethereum v3 Keystore object, or privateKey in hex string format\n * @param {string} passphrase\n * @returns {module:dcp/wallet.Keystore}\n * @access public\n */\n/**\n * Form 4\n * \n * A Keystore object parsed from the third party object. If the third party\n * object cannot be parsed the promise will reject.\n * @async\n * @param {object} privateKey an Ethereum v3 Keystore object\n * @param {string} passphrase\n * @returns {module:dcp/wallet.Keystore}\n * @access public\n */\n/**\n * Form 5\n * \n * A Keystore object parsed from the JSON string. If the JSON or resulting\n * object cannot be parsed the promise will reject.\n * @async\n * @param {string} JsonString - a JSON encoded keystore of some sort\n * @param {string} passphrase\n * @returns {module:dcp/wallet.Keystore}\n * @access public\n */\n/**\n * Form 6\n * \n * A Keystore object parsed from the third party object. If the third party\n * object cannot be parsed the promise will reject. The user will be prompted for a passphrase.\n * @async\n * @param {object} privateKey Ethereum v3 Keystore object\n * @returns {module:dcp/wallet.Keystore}\n * @access public\n */\n/**\n * Form 7\n * \n * A Keystore object with a randomly generated privateKey and an address\n * that corresponds to it. This form will used the supplied passphrase to encrypt the private key.\n * @async\n * @param {null} key unused in this form, when null it will be generated.\n * @param {string|null} passphrase the passphrase used to encrypt the new keystore\n * @returns Promise which resolves to {module:dcp/wallet.Keystore}\n * @access public\n * @example\n * let ks = await new Keystore(null, '');\n */\nexports.Keystore = function wallet$$Keystore() {\n if (arguments[0] === _ConstructOnly)\n return;\n\n let ctor = {}\n let ew /* ethereum wallet: (json) backing store for encrypted private keys */\n \n /**\n * Generate JSON object literal representation of this keystore. This can later be passed to\n * the constructor to generate a new Keystore object.\n * \n \n \n * @access public\n * @returns {object}\n * @function module:dcp/wallet.Keystore#toJSON\n */\n this.toJSON = function wallet$$Keystore$toJSON() {\n let base = Object.assign({}, ctor.ks || JSON.parse(ew));\n\n if (this.label)\n base.label = this.label;\n \n return base;\n }\n\n return (async () => {\n const { promptCreatePassphrase } = __webpack_require__(/*! ./passphrase-prompt */ \"./src/dcp-client/wallet/passphrase-prompt.js\");\n \n if (arguments.length === 2)\n ctor.passphrase = arguments[1] /* all forms which accept passphrases */\n \n if (arguments.length === 0) {\n ctor.privateKey = generateNewPrivateKey(); /* form 1 */\n this.address = ctor.privateKey.toAddress();\n const metaData = {\n label: \"New key\",\n address: this.address,\n };\n ctor.passphrase = await promptCreatePassphrase(metaData);\n }\n else if (arguments[0] === null) /* form 7 */\n ctor.privateKey = generateNewPrivateKey();\n else if (dcpEth.PrivateKey(arguments[0], true)) { /* form 2, 3 */\n ctor.privateKey = new dcpEth.PrivateKey(arguments[0]);\n this.address = ctor.privateKey.toAddress();\n if (ctor.passphrase === false)\n ctor.passphrase = '';\n if (typeof ctor.passphrase === 'undefined') {\n const metaData = {\n label: \"New key\",\n address: this.address,\n };\n ctor.passphrase = await promptCreatePassphrase() \n }\n }\n else if (arguments[0] instanceof exports.Keystore) { /* form 6 */\n ctor.privateKey = await arguments[0].getPrivateKey();\n }\n else if (typeof arguments[0] === 'string' ||\n typeof arguments[0] === 'object') { /* form 4, 5 */\n let jsonO\n\n if (typeof arguments[0] === 'object') { /* form 4 */\n ew = JSON.stringify(arguments[0])\n jsonO = arguments[0]\n } else { /* form 5 */\n ew = arguments[0]\n jsonO = JSON.parse(ew.trim())\n }\n if (jsonO.address)\n this.address = new dcpEth.Address(jsonO.address)\n else {\n let fks\n \n if (ctor.passphrase)\n fks = await parseForeignKeystore(ew, ctor.passphrase)\n else {\n fks = await parseForeignKeystore(ew, '')\n if (!fks)\n {\n /* ks has password */\n let metaData = { label: jsonO.label || jsonO.id };\n fks = await (__webpack_require__(/*! ../wallet */ \"./src/dcp-client/wallet/index.js\").passphraseTries)(metaData,\n async function wallet$$Keystore$Constructor$unlock_tryFn(passphrase) {\n await parseForeignKeystore(ew, passphrase)\n })\n }\n }\n\n if (!fks)\n throw new DCPError(`Could not unlock foreign keystore ${jsonO.label || jsonO.id} to determine public address`, unlockFailErrorCode);\n this.address = fks.address\n }\n if (jsonO.label)\n this.label = jsonO.label\n }\n\n /* By the bottom of this funnel, we have the \n * - defined this.address\n * - dcpEth.PrivateKey or a ethereum wallet (JSON string)\n * - passphrase or '' if making a Keystore from a dcpEth.PrivateKey\n *\n * Now we make sure that we can find the private key by unlocking ew, \n * have an address, and forget the private key\n */\n if (!ew) {\n let buf = ethUtil.toBuffer(ctor.privateKey);\n let i = ethWallet.fromPrivateKey(buf);\n ew = await i.toV3String(ctor.passphrase || '', { n: 1024 });\n }\n if (ctor.privateKey && !this.address) {\n this.address = ctor.privateKey.toAddress()\n }\n delete ctor.privateKey\n delete ctor.passphrase\n KeystoreLockFunctionsFactory(this, ew)\n\n return this\n })()\n}\n\n/**\n * Creates and returns stringification of SignedMessageObject in the format:\n * JSON.stringify({ owner: this.address, signature: signature, body: messageBody })\n * @async\n * @access public\n * @param {object} messageBody \n * @returns {Promise<string>}\n * @function module:dcp/wallet.Keystore#makeSignedMessage\n */\nexports.Keystore.prototype.makeSignedMessage = async function wallet$$Keystore$makeSignedMessage(messageBody) {\n return JSON.stringify(await this.makeSignedMessageObject(messageBody));\n}\n\n/** @typedef {object} SignedMessageObject\n * @property {object} body\n * @property {Signature} signature\n * @property {module:dcp/wallet.Address} owner\n */\n\n/**\n * Creates and returns stringification of SignedMessageObject in the format:\n * { owner: this.address, signature: signature, body: messageBody }\n * @async\n * @access public\n * @param {object} messageBody \n * @returns {Promise<SignedMessageObject>}\n * @function module:dcp/wallet.Keystore#makeSignedMessageObject\n */\nexports.Keystore.prototype.makeSignedMessageObject = async function wallet$$Keystore$makeSignedMessageObject(messageBody) {\n const signature = await this.makeSignature(messageBody);\n return { owner: this.address, signature: signature, body: messageBody };\n}\n\n/** @typedef {object} SignedLightWeightMessageObject\n * @property {object} body\n * @property {object} auth\n * @property {Signature} signature\n * @property {module:dcp/wallet.Address} owner\n */\n \n/**\n * Creates and returns SignedLightWeightMessageObject in the format:\n * { owner: this.address, signature: signature, auth: messageLightWeight, body: messageBody };\n * The signature is computed only on messageLightWeight, in order to improve perf when messageBody is large.\n * @async\n * @access public\n * @param {object} messageLightWeight \n * @param {object} messageBody \n * @returns {Promise<SignedLightWeightMessageObject>}\n * @function module:dcp/wallet.Keystore#makeSignedLightWeightMessageObject\n */\nexports.Keystore.prototype.makeSignedLightWeightMessageObject = async function wallet$$Keystore$makeSignedLightWeightMessageObject(messageLightWeight, messageBody) {\n const signature = await this.makeSignature(messageLightWeight);\n return { owner: this.address, signature: signature, auth: messageLightWeight, body: messageBody };\n}\n \n/** @typedef {object} Signature\n * @property {Uint8Array} r\n * @property {Uint8Array} s\n * @property {Uint8Array} v\n */\n\n/**\n * Creates and returns signature for messageBody\n * { owner: this.address, signature: signature, auth: messageLightWeight, body: messageBody };\n * The signature is computed only on messageLightWeight, in order to improve perf when messageBody is large.\n * @async\n * @access public\n * @param {object} messageBody \n * @returns {Promise<Signature>}\n * @function module:dcp/wallet.Keystore#makeSignature\n */\nexports.Keystore.prototype.makeSignature = async function wallet$$Keystore$makeSignature(messageBody)\n{\n const { messageToBuffer } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n \n if (typeof messageBody !== 'string') {\n messageBody = JSON.stringify(messageBody);\n }\n const privateKey = await this.getPrivateKey();\n \n if (useWasm && !secp256k1) {\n const { instantiateSecp256k1 } = __webpack_require__(/*! bitcoin-ts */ \"./node_modules/bitcoin-ts/build/module/index.js\"); \n secp256k1 = await instantiateSecp256k1();\n } \n const signFn = useWasm ? exports.ecsign : ethUtil.ecsign;\n const signature = signFn(ethUtil.hashPersonalMessage(messageToBuffer(messageBody)), ethUtil.toBuffer(privateKey.toString()));\n \n return signature;\n}\n\n/**\n * See {@link module:dcp/wallet.Keystore|Keystore constructor} for documentation on all the ways to call this constructor.\n * @classdesc Subclass of {@link module:dcp/wallet.Keystore|Keystore}\n * @class IdKeystore\n * @async\n * @extends module:dcp/wallet.Keystore\n * @access public\n */\nexports.IdKeystore = function walletAPI$$IdKeystore(privateKey, passphrase) { \n return this.constructor.prototype.constructor.apply(this, arguments);\n}\n\n/**\n * See {@link module:dcp/wallet.Keystore|Keystore constructor} for documentation on all the ways to call this constructor.\n * @classdesc Subclass of {@link module:dcp/wallet.Keystore|Keystore}\n * @class AuthKeystore\n * @async\n * @extends module:dcp/wallet.Keystore\n * @access public\n */\nexports.AuthKeystore = function walletAPI$$AuthKeystore(privateKey, passphrase) {\n return this.constructor.prototype.constructor.apply(this, arguments);\n}\n\nexports.IdKeystore.prototype = new exports.Keystore(_ConstructOnly);\nexports.AuthKeystore.prototype = new exports.Keystore(_ConstructOnly);\n\nexports.ecsign = async function (msgHash, privateKey) {\n var sig = secp256k1.signMessageHashRecoverableCompact(privateKey, msgHash);\n sig.signature = Buffer.from(sig.signature, 'Uint8Array');\n\n var ret = {};\n ret.r = sig.signature.slice(0, 32);\n ret.s = sig.signature.slice(32, 64);\n ret.v = sig.recoveryId + 27;\n \n return ret;\n}; \n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/wallet/keystore.js?");
4345
+ eval("/* 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 keystore.js\n * Wallet API Keystore classes\n *\n * @author Wes Garland - wes@kingsds.network\n * @author Duncan Mays - duncan@kingsds.network\n * @author Badrdine Sabhi - badr@kingsds.network\n * @author Ryan Rossiter - ryan@kingsds.network\n * @date April 2020\n */\n\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('wallet/keystore');\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\");\n\nconst { unlockFailErrorCode } = __webpack_require__(/*! dcp/dcp-client/wallet/error-codes */ \"./src/dcp-client/wallet/error-codes.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\n\nlet useWasm = true;\nlet secp256k1 = null;\n\n/* Place canonical versions of ethereumjs-util and ethereumjs-wallet on\n * exports._internalEth for reference by 'friend' modules. Try to load \n * native (fast) versions on NodeJS, but fallback to internal modules\n * when not present. Modules are argumented with fast keccak\n * hashing code via wasm for internal use.\n */\nexports._internalEth = {};\n\nif ((__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").platform) === 'nodejs')\n{\n /* Prefer peer dependencies over webpacked versions whenever possible, \n * because they will have native crypto via scrypt.\n */\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n try {\n requireNative('bindings')({\n bindings: 'keccak.node',\n try: [\n ['module_root', 'node_modules', 'keccak', 'build', 'Release', 'bindings'], // when running from dcp\n ['module_root', '..', 'keccak', 'build', 'Release', 'bindings'], // when running from dcp-client in dcp\n ],\n });\n exports._internalEth.util = requireNative('ethereumjs-util');\n exports._internalEth.wallet = requireNative('ethereumjs-wallet').default;\n useWasm = false; /* only disable wasm when using node AND have native util, wallet available */\n } catch(e) {\n exports._internalEth.util = __webpack_require__(/*! ethereumjs-util */ \"./node_modules/ethereumjs-util/dist.browser/index.js\");\n exports._internalEth.wallet = __webpack_require__(/*! ethereumjs-wallet */ \"./node_modules/ethereumjs-wallet/dist.browser/index.js\")[\"default\"];\n };\n} else {\n exports._internalEth.util = __webpack_require__(/*! ethereumjs-util */ \"./node_modules/ethereumjs-util/dist.browser/index.js\");\n exports._internalEth.wallet = __webpack_require__(/*! ethereumjs-wallet */ \"./node_modules/ethereumjs-wallet/dist.browser/index.js\")[\"default\"];\n}\n\nconst dcpEth = __webpack_require__(/*! ./eth */ \"./src/dcp-client/wallet/eth.js\");\nconst ethUtil = exports._internalEth.util;\nconst ethWallet = exports._internalEth.wallet;\n\nconst _ConstructOnly = Symbol('Construct Only');\n \n/** Generate a new private key */\nfunction generateNewPrivateKey() {\n return new dcpEth.PrivateKey(ethWallet.generate().getPrivateKey());\n}\n\n/** Accept json from any foreign ethereum wallet [that we understand] and a passphrase,\n * returning the address and private key\n *\n * @param json {string} A JSON keystore, eg. ethereum V3 wallet\n * @param passphrase {string} The passphrase to unlock the keystore\n * @returns {Object|undefined} An object having properties address and privateKey,\n * or undefined if it couldn't be parsed\n */\nasync function parseForeignKeystore(json, passphrase) {\n var fks;\n\n try {\n fks = await ethWallet.fromV3(json, passphrase, { nonStrict: true });\n } catch(e) {\n debugging() && passphrase && console.debug('warning: incorrect passphrase or foreign keystore not in wallet-v3 format', json);\n }\n\n if (!fks) try {\n fks = await ethWallet.fromV1(json, passphrase);\n } catch(e) {}\n\n if (!fks) try {\n fks = await ethWallet.fromEthSale(json, passphrase);\n } catch(e) {}\n\n return fks && {\n address: fks.address,\n privateKey: fks.getPrivateKey(),\n };\n}\n\n/**\n * This method creates functions related to locking and unlocking Keystores which share a common set\n * of private variables and are added to the keystore as own property. The private key must never be\n * stored in a variable in dcp-client which is not part of this private scope.\n *\n * @param ks {Object} instance of keystore\n * @param ew {String} json which represents an Ethereum V3 keystore [ethereum wallet]\n */\nfunction KeystoreLockFunctionsFactory(ks, ew)\n{\n const { Synchronizer } = __webpack_require__(/*! dcp/common/concurrency */ \"./src/common/concurrency.js\");\n \n var pkScope_privateKey; /* The private key when unlocked; falsey when locked */\n var pkScope_autoUnlockDuration; /* lock duration when doing auto-unlock (like sudo), or falsey */\n var pkScope_relockTime; /* when the Keystore will be considered locked again */\n var pkScope_lockTimerHnd; /* Timer handle when we have a pending automatic relock on the event queue */\n var pkScope_unlockBusy = new Synchronizer('idle', ['idle', 'busy']); /* mutex to single-thread unlock */\n var debugLabel = ks.label || ks.address;\n\n function pkScope_resetLockState() {\n debugging('timer') && console.debug(`wallet - reset lock timer state on ${debugLabel}`);\n\n if (pkScope_lockTimerHnd)\n clearTimeout(pkScope_lockTimerHnd)\n \n pkScope_privateKey = undefined;\n pkScope_autoUnlockDuration = 0;\n pkScope_relockTime = 0;\n pkScope_lockTimerHnd = undefined;\n }\n pkScope_resetLockState();\n\n /* Check the timer state, re-locking the Keystore as needed. It is\n * important to call this at the start of timer-sensitive operations, as\n * we can't rely on setTimeout to re-lock Keystores; we must consider the\n * possibility that starving the event loop could cause the private key\n * to be stored for longer than intended\n */\n function pkScope_lockIfTimerExpired() {\n debugging('timer') && console.log(`wallet - lockIfTimerExpired on ${debugLabel}`);\n if (pkScope_lockTimerHnd && pkScope_relockTime <= Date.now()) {\n pkScope_lock()\n }\n }\n \n /**\n * This method unlocks the Keystore object for either a single use or a given amount of time. When the \n * object is unlocked, `getPrivateKey` and related operations do not need to ask for a passphrase, as \n * the private key is \"remembered\" internally. If time is not specified, the object will be immediately \n * locked after the next call to `getPrivateKey`; otherwise, it will remain unlocked until the timer runs out.\n * \n * When the unlock timer is running, calling `unlock` can have multiple effects. In all cases the current\n * running timer will be increased to lockTimerDuration if that will increase the time left.\n * 1. If the autoReset flag is true for this invocation, the autoReset duration is updated only if it\n * will be increased. Thus autoReset is a sticky setting, it can be turned on but not off (except by locking)\n * 2. If the autoReset flag is false for this invocation, the current unlock duration is increased, but \n * the autoReset duration remains the same.\n * @method module:dcp/wallet.Keystore#unlock\n * @param {string} [passphrase] Passphrase to unlock keystore. If null|undefined, `passphrasePrompt` \n * function is invoked to solicit it from the user if the keystore\n * is not already unlocked.\n * @param {number} lockTimerDuration Number of seconds to keep keystore unlocked for. Intended to implement features like `sudo`\n * @param {boolean} [autoReset] Flag to determine if unlock timer should reset on subsequent calls.\n * @access public\n * @async\n * @throws Error with code 'DCP_WA:UNLOCKX' when keystore could not be unlocked.\n */\n async function pkScope_unlock(passphrase, lockTimerDuration, autoReset)\n {\n do /* spin lock to single thread this per keystore */\n {\n await pkScope_unlockBusy.until('idle');\n } while(!pkScope_unlockBusy.testAndSet('idle', 'busy'))\n\n try\n {\n pkScope_lockIfTimerExpired();\n if (!pkScope_privateKey)\n {\n async function pkScope_extractAndSetPrivateKey()\n {\n var fks;\n \n /* used by passphraseTries */\n function unlock_tryFn(passphrase)\n {\n if (pkScope_privateKey) /* unlocked by other \"thread\" - ideally would never happen... */\n return { address: ks.address, privateKey: pkScope_privateKey };\n return parseForeignKeystore(ew, passphrase);\n }\n\n /* used by passphraseTries */\n function unlock_skipCheckFn()\n {\n return !!pkScope_privateKey;\n };\n\n if (passphrase !== undefined && passphrase !== null)\n fks = await parseForeignKeystore(ew, passphrase);\n else\n {\n if (ks.checkEmpty !== false)\n fks = await parseForeignKeystore(ew, '');\n if (!fks)\n fks = await (__webpack_require__(/*! ../wallet */ \"./src/dcp-client/wallet/index.js\").passphraseTries)({ label: ks.label , address: ks.address }, unlock_tryFn, unlock_skipCheckFn);\n }\n \n if (fks)\n pkScope_privateKey = new dcpEth.PrivateKey(fks.privateKey);\n }\n\n await pkScope_extractAndSetPrivateKey();\n if (!pkScope_privateKey)\n throw new DCPError(`Could not unlock keystore '${debugLabel}'`, unlockFailErrorCode);\n }\n assert(pkScope_privateKey);\n\n /* If in autoReset mode, increase the autoUnlockDuration if a longer one was specified */\n if (autoReset && lockTimerDuration > pkScope_autoUnlockDuration)\n pkScope_autoUnlockDuration = lockTimerDuration;\n \n // If lockTimerDuration not defined, set to 0s (immediately lock)\n lockTimerDuration = lockTimerDuration ? lockTimerDuration : 0;\n /* Make this unlock expire in lockTimerDuration seconds [or later if there was a previous longer one] */\n pkScope_scheduleRelock(lockTimerDuration);\n debugging('locks') && console.log(`wallet - ${debugLabel} is unlocked`);\n }\n finally\n {\n pkScope_unlockBusy.set('busy', 'idle');\n }\n }\n\n /** \n * Lock the Keystore; make private key no longer accessible without password entry.\n * @method module:dcp/wallet.Keystore#lock\n * @access public\n */\n function pkScope_lock()\n {\n let debugMsg = `wallet - lock ${debugLabel}`;\n if (pkScope_isUnlocked()) debugMsg += ' (was not locked)';\n debugging('locks') && console.log(debugMsg);\n \n pkScope_resetLockState();\n }\n\n /**\n * Returns private key, may prompt for password. If you need this you're probably doing something wrong.\n * @method module:dcp/wallet.Keystore#getPrivateKey\n * @returns {module:dcp/wallet.PrivateKey}\n * @async\n * @access public\n */\n async function pkScope_getPrivateKey()\n {\n let pk;\n\n pkScope_lockIfTimerExpired();\n if (pkScope_isLocked()) {\n await pkScope_unlock();\n pk = pkScope_privateKey;\n pkScope_lock();\n } else {\n pk = pkScope_privateKey;\n \n if (pkScope_autoUnlockDuration > 0) {\n debugging('timer') && console.log(`wallet - auto reset new lock expire time on ${debugLabel} - ${pkScope_autoUnlockDuration * 1000}ms`);\n pkScope_scheduleRelock(pkScope_autoUnlockDuration);\n } else if (!pkScope_lockTimerHnd) {\n pkScope_lock();\n }\n }\n \n debugging('getPrivateKey') && console.log(`wallet - ${debugLabel} pk=${pk}`);\n return pk;\n }\n\n /* Schedule the next automatic lock of this keystore, overriding any earlier-firing relock\n * @param {number} newDuration seconds until the keystore gets re-locked\n */\n function pkScope_scheduleRelock(newDuration)\n {\n var newDurationMs = newDuration * 1000;\n var next_relockTime = Date.now() + newDurationMs;\n\n if (next_relockTime < pkScope_relockTime)\n return;\n \n debugging('timer') && console.log(`wallet - setting lock expire time on ${debugLabel} - ${next_relockTime}s`);\n pkScope_relockTime = next_relockTime;\n if (pkScope_lockTimerHnd)\n clearTimeout(pkScope_lockTimerHnd);\n pkScope_lockTimerHnd = setTimeout(pkScope_lock, newDurationMs);\n if (DCP_ENV.platform === 'nodejs')\n pkScope_lockTimerHnd.unref();\n }\n\n /**\n * Is the keystore presently unlocked?\n * @method module:dcp/wallet.Keystore#isUnlocked\n * @returns {boolean}\n * @access public\n */\n function pkScope_isUnlocked() {\n return !!pkScope_privateKey;\n }\n\n /**\n * Is the keystore presently locked?\n * @method module:dcp/wallet.Keystore#isLocked\n * @returns {boolean}\n * @access public\n */\n function pkScope_isLocked() {\n return !pkScope_isUnlocked();\n }\n \n ks.isLocked = pkScope_isLocked;\n ks.isUnlocked = pkScope_isUnlocked;\n ks.unlock = pkScope_unlock;\n ks.lock = pkScope_lock;\n ks.getPrivateKey = pkScope_getPrivateKey;\n}\n\n/**\n * Form 1\n * \n * A Keystore object with a randomly generated privateKey and an address\n * that corresponds to it. This form will prompt the user for a password to encrypt itself with.\n * @async\n * @returns {module:dcp/wallet.Keystore}\n * @access public\n */\n/**\n * Form 2\n * \n * A Keystore object with the provided privateKey and an address that \n * corresponds to it. This form will prompt the user for a password to encrypt itself with.\n * @async\n * @param {string|object} privateKey an Ethereum v3 Keystore object, or privateKey in hex string format\n * @returns {module:dcp/wallet.Keystore}\n * @access public\n */\n/**\n * Form 3\n * \n * A Keystore object with the provided privateKey and an address that \n * corresponds to it, encrypted with the provided passphrase.\n * @async\n * @param {string|object} privateKey an Ethereum v3 Keystore object, or privateKey in hex string format\n * @param {string} passphrase\n * @returns {module:dcp/wallet.Keystore}\n * @access public\n */\n/**\n * Form 4\n * \n * A Keystore object parsed from the third party object. If the third party\n * object cannot be parsed the promise will reject.\n * @async\n * @param {object} privateKey an Ethereum v3 Keystore object\n * @param {string} passphrase\n * @returns {module:dcp/wallet.Keystore}\n * @access public\n */\n/**\n * Form 5\n * \n * A Keystore object parsed from the JSON string. If the JSON or resulting\n * object cannot be parsed the promise will reject.\n * @async\n * @param {string} JsonString - a JSON encoded keystore of some sort\n * @param {string} passphrase\n * @returns {module:dcp/wallet.Keystore}\n * @access public\n */\n/**\n * Form 6\n * \n * A Keystore object parsed from the third party object. If the third party\n * object cannot be parsed the promise will reject. The user will be prompted for a passphrase.\n * @async\n * @param {object} privateKey Ethereum v3 Keystore object\n * @returns {module:dcp/wallet.Keystore}\n * @access public\n */\n/**\n * Form 7\n * \n * A Keystore object with a randomly generated privateKey and an address\n * that corresponds to it. This form will used the supplied passphrase to encrypt the private key.\n * @async\n * @param {null} key unused in this form, when null it will be generated.\n * @param {string|null} passphrase the passphrase used to encrypt the new keystore\n * @returns Promise which resolves to {module:dcp/wallet.Keystore}\n * @access public\n * @example\n * let ks = await new Keystore(null, '');\n */\nexports.Keystore = function wallet$$Keystore() {\n if (arguments[0] === _ConstructOnly)\n return;\n\n let ctor = {}\n let ew /* ethereum wallet: (json) backing store for encrypted private keys */\n \n /**\n * Generate JSON object literal representation of this keystore. This can later be passed to\n * the constructor to generate a new Keystore object.\n * \n \n \n * @access public\n * @returns {object}\n * @function module:dcp/wallet.Keystore#toJSON\n */\n this.toJSON = function wallet$$Keystore$toJSON() {\n let base = Object.assign({}, ctor.ks || JSON.parse(ew));\n\n if (this.label)\n base.label = this.label;\n \n return base;\n }\n\n return (async () => {\n const { promptCreatePassphrase } = __webpack_require__(/*! ../wallet */ \"./src/dcp-client/wallet/index.js\");\n \n if (arguments.length >= 2)\n ctor.passphrase = arguments[1] /* all forms which accept passphrases */\n \n if (arguments.length === 3)\n ctor.checkEmpty = arguments[2] /* for get form 4, setting checkempty to false */\n \n if (arguments.length === 0) {\n ctor.privateKey = generateNewPrivateKey(); /* form 1 */\n this.address = ctor.privateKey.toAddress();\n const metaData = {\n label: \"New key\",\n address: this.address,\n };\n ctor.passphrase = await promptCreatePassphrase(metaData);\n }\n else if (arguments[0] === null) /* form 7 */\n ctor.privateKey = generateNewPrivateKey();\n else if (dcpEth.PrivateKey(arguments[0], true)) { /* form 2, 3 */\n ctor.privateKey = new dcpEth.PrivateKey(arguments[0]);\n this.address = ctor.privateKey.toAddress();\n if (ctor.passphrase === false)\n ctor.passphrase = '';\n if (typeof ctor.passphrase === 'undefined') {\n const metaData = {\n label: \"New key\",\n address: this.address,\n };\n ctor.passphrase = await promptCreatePassphrase(metaData); \n }\n }\n else if (arguments[0] instanceof exports.Keystore) { /* form 6 */\n ctor.privateKey = await arguments[0].getPrivateKey();\n ew = JSON.stringify(arguments[0]);\n }\n else if (typeof arguments[0] === 'string' ||\n typeof arguments[0] === 'object') { /* form 4, 5 */\n let jsonO\n\n if (typeof arguments[0] === 'object') { /* form 4 */\n ew = JSON.stringify(arguments[0])\n jsonO = arguments[0]\n } else { /* form 5 */\n ew = arguments[0]\n jsonO = JSON.parse(ew.trim())\n }\n if (jsonO.address)\n this.address = new dcpEth.Address(jsonO.address)\n else {\n let fks\n \n if (ctor.passphrase)\n fks = await parseForeignKeystore(ew, ctor.passphrase)\n else {\n if (ctor.checkEmpty !== false)\n fks = await parseForeignKeystore(ew, '')\n if (!fks)\n {\n /* ks has password */\n let metaData = { label: jsonO.label || jsonO.id };\n fks = await (__webpack_require__(/*! ../wallet */ \"./src/dcp-client/wallet/index.js\").passphraseTries)(metaData,\n async function wallet$$Keystore$Constructor$unlock_tryFn(passphrase) {\n await parseForeignKeystore(ew, passphrase)\n })\n }\n }\n\n if (!fks)\n throw new DCPError(`Could not unlock foreign keystore ${jsonO.label || jsonO.id} to determine public address`, unlockFailErrorCode);\n this.address = fks.address\n }\n if (jsonO.label)\n this.label = jsonO.label\n }\n\n /* By the bottom of this funnel, we have the \n * - defined this.address\n * - dcpEth.PrivateKey or a ethereum wallet (JSON string)\n * - passphrase or '' if making a Keystore from a dcpEth.PrivateKey\n *\n * Now we make sure that we can find the private key by unlocking ew, \n * have an address, and forget the private key\n */\n if (!ew) {\n let buf = ethUtil.toBuffer(ctor.privateKey);\n let i = ethWallet.fromPrivateKey(buf);\n ew = await i.toV3String(ctor.passphrase || '', { n: 1024 });\n }\n if (ctor.privateKey && !this.address) {\n this.address = ctor.privateKey.toAddress()\n }\n this.checkEmpty = ctor.checkEmpty;\n delete ctor.privateKey\n delete ctor.passphrase\n KeystoreLockFunctionsFactory(this, ew)\n\n return this\n })()\n}\n\n/**\n * Creates and returns stringification of SignedMessageObject in the format:\n * JSON.stringify({ owner: this.address, signature: signature, body: messageBody })\n * @async\n * @access public\n * @param {object} messageBody \n * @returns {Promise<string>}\n * @function module:dcp/wallet.Keystore#makeSignedMessage\n */\nexports.Keystore.prototype.makeSignedMessage = async function wallet$$Keystore$makeSignedMessage(messageBody) {\n return JSON.stringify(await this.makeSignedMessageObject(messageBody));\n}\n\n/** @typedef {object} SignedMessageObject\n * @property {object} body\n * @property {Signature} signature\n * @property {module:dcp/wallet.Address} owner\n */\n\n/**\n * Creates and returns stringification of SignedMessageObject in the format:\n * { owner: this.address, signature: signature, body: messageBody }\n * @async\n * @access public\n * @param {object} messageBody \n * @returns {Promise<SignedMessageObject>}\n * @function module:dcp/wallet.Keystore#makeSignedMessageObject\n */\nexports.Keystore.prototype.makeSignedMessageObject = async function wallet$$Keystore$makeSignedMessageObject(messageBody) {\n const signature = await this.makeSignature(messageBody);\n return { owner: this.address, signature: signature, body: messageBody };\n}\n\n/** @typedef {object} SignedLightWeightMessageObject\n * @property {object} body\n * @property {object} auth\n * @property {Signature} signature\n * @property {module:dcp/wallet.Address} owner\n */\n \n/**\n * Creates and returns SignedLightWeightMessageObject in the format:\n * { owner: this.address, signature: signature, auth: messageLightWeight, body: messageBody };\n * The signature is computed only on messageLightWeight, in order to improve perf when messageBody is large.\n * @async\n * @access public\n * @param {object} messageLightWeight \n * @param {object} messageBody \n * @returns {Promise<SignedLightWeightMessageObject>}\n * @function module:dcp/wallet.Keystore#makeSignedLightWeightMessageObject\n */\nexports.Keystore.prototype.makeSignedLightWeightMessageObject = async function wallet$$Keystore$makeSignedLightWeightMessageObject(messageLightWeight, messageBody) {\n const signature = await this.makeSignature(messageLightWeight);\n return { owner: this.address, signature: signature, auth: messageLightWeight, body: messageBody };\n}\n \n/** @typedef {object} Signature\n * @property {Uint8Array} r\n * @property {Uint8Array} s\n * @property {Uint8Array} v\n */\n\n/**\n * Creates and returns signature for messageBody\n * { owner: this.address, signature: signature, auth: messageLightWeight, body: messageBody };\n * The signature is computed only on messageLightWeight, in order to improve perf when messageBody is large.\n * @async\n * @access public\n * @param {object} messageBody \n * @returns {Promise<Signature>}\n * @function module:dcp/wallet.Keystore#makeSignature\n */\nexports.Keystore.prototype.makeSignature = async function wallet$$Keystore$makeSignature(messageBody)\n{\n const { messageToBuffer } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n \n if (typeof messageBody !== 'string') {\n messageBody = JSON.stringify(messageBody);\n }\n const privateKey = await this.getPrivateKey();\n \n if (useWasm && !secp256k1) {\n const { instantiateSecp256k1 } = __webpack_require__(/*! bitcoin-ts */ \"./node_modules/bitcoin-ts/build/module/index.js\"); \n secp256k1 = await instantiateSecp256k1();\n } \n const signFn = useWasm ? exports.ecsign : ethUtil.ecsign;\n const signature = signFn(ethUtil.hashPersonalMessage(messageToBuffer(messageBody)), ethUtil.toBuffer(privateKey.toString()));\n \n return signature;\n}\n\n/**\n * See {@link module:dcp/wallet.Keystore|Keystore constructor} for documentation on all the ways to call this constructor.\n * @classdesc Subclass of {@link module:dcp/wallet.Keystore|Keystore}\n * @class IdKeystore\n * @async\n * @extends module:dcp/wallet.Keystore\n * @access public\n */\nexports.IdKeystore = function walletAPI$$IdKeystore(privateKey, passphrase) { \n return this.constructor.prototype.constructor.apply(this, arguments);\n}\n\n/**\n * See {@link module:dcp/wallet.Keystore|Keystore constructor} for documentation on all the ways to call this constructor.\n * @classdesc Subclass of {@link module:dcp/wallet.Keystore|Keystore}\n * @class AuthKeystore\n * @async\n * @extends module:dcp/wallet.Keystore\n * @access public\n */\nexports.AuthKeystore = function walletAPI$$AuthKeystore(privateKey, passphrase) {\n return this.constructor.prototype.constructor.apply(this, arguments);\n}\n\nexports.IdKeystore.prototype = new exports.Keystore(_ConstructOnly);\nexports.AuthKeystore.prototype = new exports.Keystore(_ConstructOnly);\n\nexports.ecsign = async function (msgHash, privateKey) {\n var sig = secp256k1.signMessageHashRecoverableCompact(privateKey, msgHash);\n sig.signature = Buffer.from(sig.signature, 'Uint8Array');\n\n var ret = {};\n ret.r = sig.signature.slice(0, 32);\n ret.s = sig.signature.slice(32, 64);\n ret.v = sig.recoveryId + 27;\n \n return ret;\n}; \n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/wallet/keystore.js?");
4336
4346
 
4337
4347
  /***/ }),
4338
4348
 
@@ -4353,7 +4363,7 @@ eval("/**\n * @file ks-cache.js\n * Wallet API Keystore cach
4353
4363
  \****************************************************/
4354
4364
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4355
4365
 
4356
- eval("/**\n * @file passphrase-prompt.js\n * Cross-platform passphrase prompt module for the Wallet API\n *\n * @author Wes Garland - wes@kingsds.network\n * @author Ryan Rossiter - ryan@kingsds.network\n * @date August 2019\n */\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)(\n 'dcp-client:wallet:passphrase-prompt',\n);\nconst ClientModal = __webpack_require__(/*! dcp/dcp-client/client-modal */ \"./src/dcp-client/client-modal/index.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\n\nconst debug = (...args) => {\n if (debugging()) {\n args.unshift('dcp-client:wallet:passphrase-prompt');\n console.debug(...args);\n }\n};\n\n// This is the format for the username for password managers in the browser.\nfunction generateUsername(metaData) {\n return (metaData.label || \"Key\") + \" - \" + (metaData.address || \"Address\");\n}\n\nfunction ppServiceList() {\n const { readPass } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n\n let head = ppServiceList.pending[0]\n\n if (ppServiceList.pending.length === 0) {\n clearTimeout(ppServiceList.timer)\n ppServiceList.timer = undefined\n return\n }\n \n if (head.pending) {\n if (!ppServiceList.timer)\n ppServiceList.timer = setTimeout(ppServiceList, 100)\n return\n }\n \n head.pending = true\n function afterPrompt(ppPromise) {\n ppServiceList.timer = setTimeout(ppServiceList, 100)\n let idx = ppServiceList.pending.indexOf(head)\n if (idx !== -1)\n ppServiceList.pending.splice(idx, 1)\n head.resolve(ppPromise)\n }\n\n if (head.skipCheckFn && head.skipCheckFn())\n return afterPrompt()\n\n readPass(head.prompt).then(afterPrompt);\n}\nppServiceList.pending = [];\nppServiceList.timer = null;\n\n/**\n * This function takes a string, prompt, prints it to the screen, and then\n * returns whatever the user types after it.\n *\n * @param {Object} metaData Options\n * @param {string} metaData.label Label for the keystore\n * @param {*} urgent Read below, careful handling of event loop\n * @param {*} skipCheckFn see ppServiceList\n */\nfunction wallet$$passphrasePrompt$node(prompt, urgent, skipCheckFn) {\n\n return new Promise(function(resolve, reject) {\n try {\n /* force the outputting of readPass() onto the event loop, so that pending \n * console.log messages (etc) get printed first, when the loop is serviced,\n * instead of while we are typing the password.\n *\n * Also, make it so that it stalls on the event loop if there is already a\n * a password prompt being displayed.\n */\n let listO = {\n resolve: resolve,\n reject: reject,\n prompt,\n skipCheckFn: skipCheckFn,\n };\n\n if (ppServiceList.pending.length === 0 || urgent)\n setTimeout(ppServiceList, 0)\n if (urgent)\n ppServiceList.pending.unshift(listO)\n else\n ppServiceList.pending.push(listO)\n } catch(e) {\n reject(e);\n }\n });\n}\n\nasync function promptCreatePassphrase(metaData={}) {\n if (DCP_ENV.platform === 'nodejs') {\n let pp1, pp2;\n \n do {\n pp1 = await wallet$$passphrasePrompt$node(\"Please enter new password:\");\n pp2 = await wallet$$passphrasePrompt$node(\"Please confirm new password by entering it again:\", true);\n } while(pp1 !== pp2);\n\n return pp1;\n } else {\n return await ClientModal.getPasswordCreation(generateUsername(metaData));\n }\n}\n\nlet createPassphraseFn = promptCreatePassphrase;\nexports.promptCreatePassphrase = function wallet$$create$passphrase(...args) {\n debugging() && console.debug('wallet - promptCreatePassphrase call site ', new Error('debug\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b '));\n return createPassphraseFn(...args);\n}\n\n// only for testing:\nexports.overrideCreatePassphrase = function wallet$$create$passphrase$override(fn) {\n createPassphraseFn = fn;\n}\n\n/** This function accepts arbitrary arguments and returns a string as provided by the user. \n * The default behaviour of this mode is in node, browser is a separate process in passphraseTries()\n * \n * @param {...any} args \n * @return {Promise<string>} \n */\nexports.passphrasePrompt = async function passphrasePrompt(...args) {\n const metaData = args[0];\n debug('passphrasePrompt args:', args);\n if (DCP_ENV.isBrowserPlatform) {\n const tryPassphrase = args[1];\n await ClientModal.getPasswordEntry(generateUsername(metaData), metaData.label, metaData.maxTries, tryPassphrase);\n } else { \n const skipCheckFn = args[1];\n const i = metaData.tries;\n const prompt = i === 0\n ? `Enter passphrase to unlock keystore '${metaData.label}': `\n : `Incorrect passphrase for keystore '${metaData.label}'. Please try again: `;\n const pp = await wallet$$passphrasePrompt$node(prompt, i > 0, skipCheckFn);\n return pp\n }\n}\n\n\n/** This function tries to get the correct passphrase\n * by asking the user up to exports.maxPassphraseTries times.\n * We give `passphrasePrompt` the callback `tryAndRememberPassphrase`\n * so that the passwordCreation.js modal knows if a password was correct or not.\n * This lets it trigger the browser to offer to save only correct passwords.\n * If all tries are used and tryFn never returns truey, this function returns false.\n *\n * @param {object} metaData metaData is passed to passphrasePrompt()\n * @param {function} tryFn function which trys to unlock the keystore with a supplied password\n */\nexports.passphraseTries = async function wallet$$passphraseTries(metaData, tryFn, skipCheckFn) {\n let res;\n metaData.maxTries = exports.maxPassphraseTries\n\n const tryAndRememberPassphrase = async function keystore$$tryPassphrase(pp) {\n const thisRes = await tryFn(pp);\n res = thisRes || res;\n return !!thisRes;\n }\n\n if (DCP_ENV.isBrowserPlatform) { //Special case for the web modal - this prompts\n await (__webpack_require__(/*! ../wallet */ \"./src/dcp-client/wallet/index.js\").passphrasePrompt)( metaData, tryAndRememberPassphrase ) // Just returns a passphrase\n } else { // Tries using normal passphrase function, the user cannot override number of passphrase tries\n let ppIsCorrect;\n for(let i = 0; !ppIsCorrect && i < metaData.maxTries; i++) {\n metaData.tries = i\n const pp = await (__webpack_require__(/*! ../wallet */ \"./src/dcp-client/wallet/index.js\").passphrasePrompt)(metaData, skipCheckFn) // Just returns a passphrase\n ppIsCorrect = await tryAndRememberPassphrase(pp);\n }\n }\n return res || false;\n}\n\nexports.maxPassphraseTries = 5;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/wallet/passphrase-prompt.js?");
4366
+ eval("/**\n * @file passphrase-prompt.js\n * Cross-platform passphrase prompt module for the Wallet API\n *\n * @author Wes Garland - wes@kingsds.network\n * @author Ryan Rossiter - ryan@kingsds.network\n * @date August 2019\n */\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)(\n 'dcp-client:wallet:passphrase-prompt',\n);\nconst ClientModal = __webpack_require__(/*! dcp/dcp-client/client-modal */ \"./src/dcp-client/client-modal/index.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\n\nconst debug = (...args) => {\n if (debugging()) {\n args.unshift('dcp-client:wallet:passphrase-prompt');\n console.debug(...args);\n }\n};\n\n// This is the format for the username for password managers in the browser.\nfunction generateUsername(metaData) {\n return (metaData.label || \"Key\") + \" - \" + (metaData.address || \"Address\");\n}\n\nfunction ppServiceList() {\n const { readPass } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n\n let head = ppServiceList.pending[0]\n\n if (ppServiceList.pending.length === 0) {\n clearTimeout(ppServiceList.timer)\n ppServiceList.timer = undefined\n return\n }\n \n if (head.pending) {\n if (!ppServiceList.timer)\n ppServiceList.timer = setTimeout(ppServiceList, 100)\n return\n }\n \n head.pending = true\n function afterPrompt(ppPromise) {\n ppServiceList.timer = setTimeout(ppServiceList, 100)\n let idx = ppServiceList.pending.indexOf(head)\n if (idx !== -1)\n ppServiceList.pending.splice(idx, 1)\n head.resolve(ppPromise)\n }\n\n if (head.skipCheckFn && head.skipCheckFn())\n return afterPrompt()\n\n readPass(head.prompt).then(afterPrompt);\n}\nppServiceList.pending = [];\nppServiceList.timer = null;\n\n/**\n * This function takes a string, prompt, prints it to the screen, and then\n * returns whatever the user types after it.\n *\n * @param {Object} metaData Options\n * @param {string} metaData.label Label for the keystore\n * @param {*} urgent Read below, careful handling of event loop\n * @param {*} skipCheckFn see ppServiceList\n */\nfunction wallet$$passphrasePrompt$node(prompt, urgent, skipCheckFn) {\n\n return new Promise(function(resolve, reject) {\n try {\n /* force the outputting of readPass() onto the event loop, so that pending \n * console.log messages (etc) get printed first, when the loop is serviced,\n * instead of while we are typing the password.\n *\n * Also, make it so that it stalls on the event loop if there is already a\n * a password prompt being displayed.\n */\n let listO = {\n resolve: resolve,\n reject: reject,\n prompt,\n skipCheckFn: skipCheckFn,\n };\n\n if (ppServiceList.pending.length === 0 || urgent)\n setTimeout(ppServiceList, 0)\n if (urgent)\n ppServiceList.pending.unshift(listO)\n else\n ppServiceList.pending.push(listO)\n } catch(e) {\n reject(e);\n }\n });\n}\n\nasync function promptCreatePassphrase(metaData={}) {\n if (DCP_ENV.platform === 'nodejs') {\n let pp1, pp2;\n do {\n pp1 = await (__webpack_require__(/*! ../wallet */ \"./src/dcp-client/wallet/index.js\").passphrasePrompt)('Enter a password for your Keystore:', metaData);\n pp2 = await (__webpack_require__(/*! ../wallet */ \"./src/dcp-client/wallet/index.js\").passphrasePrompt)(\"Please confirm new password by entering it again:\", metaData, true);\n } while(pp1 !== pp2);\n\n return pp1;\n } else {\n return await ClientModal.getPasswordCreation(generateUsername(metaData));\n }\n}\n\nlet createPassphraseFn = promptCreatePassphrase;\nexports.promptCreatePassphrase = function wallet$$create$passphrase(...args) {\n debugging() && console.debug('wallet - promptCreatePassphrase call site ', new Error('debug\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b\\b '));\n return createPassphraseFn(...args);\n}\n\n// only for testing:\nexports.overrideCreatePassphrase = function wallet$$create$passphrase$override(fn) {\n createPassphraseFn = fn;\n}\n\n/** This function accepts arbitrary arguments and returns a string as provided by the user. \n * The default behaviour of this mode is in node, browser is a separate process in passphraseTries()\n * \n * @param {...any} args \n * @return {Promise<string>} \n */\nexports.passphrasePrompt = async function passphrasePrompt(...args) {\n /**\n * This function was written out of spec, and over time has had a lot of additions to it that haven't been spec'd out.\n * To avoid breaking current applications that use the functionality included in this function, the function is overloaded to\n * process different kind of parameters.\n * \n * If the first argument is a string, it will be used as the message to display when passphrasePrompt is called. \n * metaData is an object containing the label of the keystore.\n * urgent is a boolean flag for ppServiceList() that works with the event loop.\n * tryPassphrase is a function (node implementation) and a string (browser implementation), which is used to skip input from user.\n * \n */\n debug('passphrasePrompt args:', args);\n \n /**\n * Form 1:\n * passphrasePrompt(prompt, metaData, urgent/tryPassphrase)\n * Prompt is provided in the hook and urgent/tryPassphrase is set based on the value. Urgent can only be a boolean, so if not a boolean, it is tryPassphrase.\n * \n * Form 2:\n * passphrasePrompt(metaData, tryPassphrase)\n * A default prompt is used\n */\n let prompt, metaData, urgent = false, tryPassphrase;\n if (typeof(args[0]) === 'string')\n {\n prompt = args[0];\n metaData = args[1];\n if (typeof(args[2]) === 'boolean')\n urgent = args[2]\n else\n tryPassphrase = args[2];\n }\n else\n {\n prompt = 'Enter your passphrase here:';\n metaData = args[0];\n if (args[1])\n tryPassphrase = args[1];\n }\n \n if (DCP_ENV.isBrowserPlatform) {\n await ClientModal.getPasswordEntry(generateUsername(metaData), metaData.label, metaData.maxTries, tryPassphrase);\n } else {\n let skipCheckFn = tryPassphrase;\n let pp = await wallet$$passphrasePrompt$node(prompt, urgent, skipCheckFn);\n \n return pp;\n }\n}\n\n\n/** This function tries to get the correct passphrase\n * by asking the user up to exports.maxPassphraseTries times.\n * We give `passphrasePrompt` the callback `tryAndRememberPassphrase`\n * so that the passwordCreation.js modal knows if a password was correct or not.\n * This lets it trigger the browser to offer to save only correct passwords.\n * If all tries are used and tryFn never returns truey, this function returns false.\n *\n * @param {object} metaData metaData is passed to passphrasePrompt()\n * @param {function} tryFn function which trys to unlock the keystore with a supplied password\n */\nexports.passphraseTries = async function wallet$$passphraseTries(metaData, tryFn, skipCheckFn) {\n let res;\n metaData.maxTries = exports.maxPassphraseTries\n\n const tryAndRememberPassphrase = async function keystore$$tryPassphrase(pp) {\n const thisRes = await tryFn(pp);\n res = thisRes || res;\n return !!thisRes;\n }\n\n if (DCP_ENV.isBrowserPlatform) { //Special case for the web modal - this prompts\n await (__webpack_require__(/*! ../wallet */ \"./src/dcp-client/wallet/index.js\").passphrasePrompt)( metaData, tryAndRememberPassphrase ) // Just returns a passphrase\n } else { // Tries using normal passphrase function, the user cannot override number of passphrase tries\n let prompt = 'Enter your passphrase:'\n let ppIsCorrect;\n for(let i = 0; !ppIsCorrect && i < metaData.maxTries; i++) {\n metaData.tries = i\n const pp = await (__webpack_require__(/*! ../wallet */ \"./src/dcp-client/wallet/index.js\").passphrasePrompt)(prompt, metaData, skipCheckFn) // Just returns a passphrase\n ppIsCorrect = await tryAndRememberPassphrase(pp);\n prompt = 'Incorrect passphrase, enter your passphrase again:'\n }\n }\n return res || false;\n}\n\nexports.maxPassphraseTries = 5;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/wallet/passphrase-prompt.js?");
4357
4367
 
4358
4368
  /***/ }),
4359
4369
 
@@ -4414,7 +4424,7 @@ eval("/**\n * @file This module implements the Worker API, used to create worker
4414
4424
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4415
4425
 
4416
4426
  "use strict";
4417
- eval("// NOTE - need timeout/postmessage function\n/**\n * @file dcp-client/worker/sandbox.js\n *\n * A sandbox that when constructed and assigned can do work for\n * a distributed slice. A sandbox runs for a single slice at a time.\n *\n * Usage:\n * let sandbox = new Sandbox()\n * await sandbox.start()\n * let results = await sandbox.run(slice)\n *\n * Debug flags:\n * Sandbox.debugWork = true // - turns off 30 second timeout to let user debug sandbox innards more easily\n * Sandbox.debugState = true // - logs all state transitions for this sandbox\n * Sandbox.debugEvents = true // - logs all events received from the sandbox\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * Ryan Rossiter, ryan@kingsds.network\n * @date May 2019\n * @module sandbox\n */\n/* global dcpConfig */\n// @ts-check\n\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('worker');\nconst { assert, assertEq3 } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst { rehydrateRange } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\nconst scopedKvin = new kvin.KVIN({Object: ({}).constructor,\n Array: ([]).constructor, \n Function: (()=>{}).constructor});\n\nlet timeDilation = 1;\nif (DCP_ENV.platform === 'nodejs') {\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n /** Make timers 10x slower when running in niim */\n timeDilation = (requireNative('module')._cache.niim instanceof requireNative('module').Module) ? 10 : 1;\n}\n\n/**\n * Wraps console.debug to emulate debug module prefixing messages on npm.\n * @param {...any} args\n */\nconst debug = (...args) => {\n if (debugging()) {\n console.debug('dcp-client:worker:sandbox', ...args);\n }\n};\n\nconst nanoid = (__webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\").nanoid);\n\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { fetchURI, encodeDataURI } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n\n// Sandbox states\nconst UNREADY = 'UNREADY' // No Sandbox (web worker, saworker, etc) has been constructed yet\nconst READYING = 'READYING' // Sandbox is being constructed and environment (bravojs, env) is being set up\nconst READY_FOR_ASSIGN = 'READY_FOR_ASSIGN' // Sandbox is ready to be assigned\nconst ASSIGNING = 'ASSIGNING' // Sandbox is running through assigning steps\nconst ASSIGNED = 'ASSIGNED' // Sandbox is assigned but not working\nconst WORKING = 'WORKING' // Sandbox is working\nconst TERMINATED = 'TERMINATED'\nconst EVAL_RESULT_PREFIX = 'evalResult::';\n\nclass SandboxError extends Error {}\nclass NoProgressError extends SandboxError { constructor(...args) { super(...args); this.errorCode = 'ENOPROGRESS'; } }\nclass SliceTooSlowError extends SandboxError { constructor(...args) { super(...args); this.errorCode = 'ESLICETOOSLOW'; } }\nclass UncaughtExceptionError extends SandboxError { constructor(...args) { super(...args); this.errorCode = 'EUNCAUGHT'; } }\nclass RemoteFetchError extends SandboxError { constructor(...args) { super(...args); this.errorCode = 'EFETCH'; }}\n\n/** @typedef {import('dcp/common/dcp-events').EventEmitter} EventEmitter */\n/** @typedef {import('./slice').Slice} Slice */\n/** @typedef {import('./supervisor-cache').SupervisorCache} SupervisorCache */\n/** @typedef {*} opaqueId */\n\n/**\n * @access public\n * @typedef {object} SandboxOptions\n * @constructor {function} [SandboxConstructor]\n * @property {boolean} [ignoreNoProgress] - When true, the sandbox will not be stopped for not calling progress\n */\n\nclass Sandbox extends EventEmitter {\n /**\n * A Sandbox (i.e. a worker sandbox) which executes distributed slices.\n *\n * @constructor\n * @param {SupervisorCache} cache\n * @param {SandboxOptions} options\n */\n constructor (cache, options, origins) {\n super('Sandbox');\n /** @type {SupervisorCache} */\n this.supervisorCache = cache;\n /** @type {SandboxOptions} */\n this.options = {\n ignoreNoProgress: false,\n ...options,\n SandboxConstructor: options.SandboxConstructor ||\n (__webpack_require__(/*! ./evaluators */ \"./src/dcp-client/worker/evaluators/index.js\").BrowserEvaluator),\n }\n this.allowedOrigins = origins;\n\n /** @type {opaqueId} */\n this.jobAddress = null;\n /** @type {object} */\n this.evaluatorHandle = null;\n /** @type {object} */\n this.capabilities = null;\n /** @type {EventEmitter} */\n this.ee = new EventEmitter('SandboxInternal')\n\n /** @type {string} */\n this._state = UNREADY;\n /** @type {boolean} */\n this.allocated = false;\n /** @type {number?} */\n this.progress = 100;\n /** @type {object} */\n this.progressReports = null;\n /** @type {object} */\n this.progressTimeout = null;\n /** @type {object} */\n this.sliceTimeout = null;\n /** @type {object} */\n this.rejectionData = null;\n /** @type {Slice} */\n this.slice = null;\n\n /** @type {number?} */\n this.started = null;\n /** @type {number?} */\n this.sliceStartTime = null;\n /** @type {boolean} */\n this.requiresGPU = false;\n /** @type {string|URL} */\n this.packageURL = dcpConfig.packageManager.location\n /** @type {number?} */\n this.id = Sandbox.getNewId();\n\n this.ringMessageHandlers = [\n this.handleRing0Message,\n this.handleRing1Message,\n this.handleRing2Message,\n this.handleRing3Message,\n ];\n\n this.resetSliceTimeReport();\n }\n\n get identifier() {\n if (this.allocated)\n return `${this.id}.${this.jobAddress}.${this.state}.${this.allocated}`;\n return `${this.id}.${this.jobAddress}.${this.state}`;\n }\n\n static getNewId() {\n return Sandbox.idCounter++;\n }\n\n get state () {\n return this._state\n }\n\n set state (value) {\n if (Sandbox.debugState) {\n console.debug(`sandbox - changing state of ${this.id}... ${this._state} -> ${value}`)\n }\n\n if (this.state === TERMINATED && value !== TERMINATED) {\n // For safety!\n throw new Error(`Sandbox set state violation, attepted to change state from ${this.state} to ${value}`);\n }\n\n this._state = value;\n }\n\n get isReadyForAssign () {\n return this.state === READY_FOR_ASSIGN;\n }\n\n get isAssigned () {\n return this.state === ASSIGNED;\n }\n\n get isWorking () {\n return this.state === WORKING;\n }\n\n get isTerminated () {\n return this.state === TERMINATED;\n }\n\n changeWorkingToAssigned () {\n if (this.isWorking)\n this.state = ASSIGNED;\n }\n\n setIsAssigning () {\n this.state = ASSIGNING;\n }\n\n /**\n * Readies the sandbox. This will result in the sandbox being ready and not assigned,\n * it will need to be assigned with a job before it is able to do work.\n *\n * @todo maybe preload specific modules or let the cache pass in what modules to load?\n * @throws on failure to ready\n */\n async start(delay = 0) {\n this.started = Date.now();\n this.state = READYING;\n\n if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay * timeDilation));\n\n try {\n // RING 0\n this.evaluatorHandle = new this.options.SandboxConstructor({\n name: `DCP Sandbox #${this.id}`,\n });\n this.evaluatorHandle.onerror = this.onerror.bind(this);\n\n const messageHandler = this.onmessage.bind(this);\n this.evaluatorHandle.onmessage = function onmessage(event)\n {\n let data;\n if (event.data.serialized)\n {\n data = kvin.parse(event.data.message);\n }\n else\n {\n data = kvin.unmarshal(event.data);\n }\n messageHandler({ data });\n }\n\n const evaluatorPostMessage = this.evaluatorHandle.postMessage.bind(this.evaluatorHandle);\n this.evaluatorHandle.postMessage = function postMessage(message)\n {\n evaluatorPostMessage(scopedKvin.marshal(message));\n }\n\n const ceci = this;\n this.evaluatorHandle.addEventListener('end', () => ceci.terminate(true));\n\n // Now in RING 1\n\n // Now in RING 2\n await this.describe();\n this.state = READY_FOR_ASSIGN;\n this.emit('ready', this);\n } catch (error) {\n console.warn('Failed to start the sandbox -', error.message);\n this.terminate(false);\n throw error;\n }\n }\n\n /**\n * This will assign the sandbox with a job, loading its sandbox code\n * into the sandbox.\n *\n * @param {string} jobAddress The address of the job to assign to\n * @throws on initialization failure\n */\n async assign(jobAddress) {\n this.jobAddress = jobAddress;\n this.job = await this.supervisorCache.fetchJob(jobAddress, this.allowedOrigins);\n\n assertEq3(this.job.address, jobAddress);\n assert(typeof this.job === 'object');\n assert(typeof this.job.requirements === 'object');\n assert(Array.isArray(this.job.dependencies));\n assert(Array.isArray(this.job.requirePath));\n\n // Extract public data from job, with defaults\n this.public = Object.assign({\n name: `Anonymous Job ${this.job.address.slice(0, 6)}`,\n description: 'Discreetly helping make the world smarter.',\n link: 'https://distributed.computer/about',\n }, this.job.public);\n\n // Future: We may want other filename tags for appliances // RR Nov 2019\n\n // Important: The order of applying requirements before loading the sandbox code\n // is important for modules and sandbox code to set globals over the whitelist.\n await this.applySandboxRequirements(this.job.requirements);\n await this.assignEvaluator();\n this.state = ASSIGNED;\n }\n\n async assignEvaluator() {\n debug('Begin assigning job to evaluator');\n const ceci = this;\n\n return new Promise(function sandbox$$assignEvaluatorPromise(resolve, reject) {\n const message = {\n request: 'assign',\n job: ceci.job,\n sandboxConfig: dcpConfig.worker.sandbox,\n };\n\n /* note - onFailListener used for removal. This is necessary due to a bug in ee.once. /wg Feb 2022 */\n \n const onSuccess = (event) => {\n // eslint-disable-next-line no-use-before-define\n ceci.ee.removeListener('reject', onFailListener);\n ceci.emit('assigned', event.jobAddress);\n debug('Job assigned to evaluator');\n resolve();\n };\n\n const onFail = (error) => {\n // eslint-disable-next-line no-use-before-define\n ceci.ee.removeListener('assigned', onSuccessListener);\n reject(error);\n };\n\n const onSuccessListener = ceci.ee.once('assigned', onSuccess);\n const onFailListener = ceci.ee.once('reject', onFail);\n ceci.evaluatorHandle.postMessage(message);\n });\n }\n\n /**\n * Evaluates a string inside the sandbox.\n *\n * @param {string} code - the code to evaluate in the sandbox\n * @param {string} filename - the name of the 'file' to help with debugging,\n * no longer working though?\n * @returns {Promise} - resolves with eval result on success, rejects\n * otherwise\n */\n eval(code, filename) {\n var ceci = this;\n \n return new Promise(function sandbox$$eval$Promise(resolve, reject) {\n let msgId = nanoid();\n let msg = {\n request: 'eval',\n data: code,\n filename,\n msgId, \n }\n\n const eventId = EVAL_RESULT_PREFIX + msgId;\n\n let onSuccess = (event) => {\n ceci.ee.removeListener('reject', onFailListener)\n resolve(event)\n }\n\n let onFail = (event) => {\n ceci.ee.removeListener(eventId, onSuccessListener)\n reject(event)\n }\n\n let onSuccessListener = ceci.ee.once(eventId, onSuccess);\n let onFailListener = ceci.ee.once('reject', onFail)\n\n ceci.evaluatorHandle.postMessage(msg)\n })\n }\n\n /**\n * Resets the state of the bootstrap, without resetting the sandbox function if assigned.\n * Mostly used to reset the progress status before reusing a sandbox on another slice.\n * Must be called after @start.\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n resetSandboxState () {\n var ceci = this;\n\n return new Promise(function sandbox$resetSandboxStatePromise(resolve, reject) {\n let successCb, failTimeout;\n let msg = {\n request: 'resetState',\n };\n\n successCb = ceci.ee.once('resetStateDone', function sandbox$resetSandboxState$success () {\n if (failTimeout === false)\n return; /* already rejected */\n clearTimeout(failTimeout);\n failTimeout = false;\n resolve();\n });\n\n failTimeout = setTimeout(function sandbox$resetSandboxState$fail() {\n if (failTimeout === false)\n return; /* already resolved */\n \n ceci.ee.removeListener('resetStateDone', successCb);\n ceci.terminate(false);\n failTimeout = false;\n\n reject(new Error('resetState never received resetStateDone event from sandbox'));\n }, 3000 * timeDilation); /* XXXwg need tuneable */\n\n assert(ceci.evaluatorHandle); // It is possible that ceci.terminate nulls out evaluatorHandle before getting here.\n ceci.evaluatorHandle.postMessage(msg);\n });\n }\n\n /**\n * Clear all timers that are set inside the sandbox (evaluator) environment.\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n clearSandboxTimers() {\n var ceci = this;\n \n return new Promise(function sandbox$clearSandboxTimersPromise(resolve, reject) {\n let successCb, failTimeout;\n let msg = {\n request: 'clearTimers',\n };\n\n successCb = ceci.ee.once('clearTimersDone', function sandbox$clearSandboxTimers$success() {\n if (failTimeout === false)\n return; /* already rejected */\n clearTimeout(failTimeout);\n failTimeout = false;\n resolve();\n });\n\n failTimeout = setTimeout(function sanbox$clearSandboxTimers$fail() {\n if (failTimeout === false)\n return; /* already resolved */\n \n ceci.ee.removeListener('clearTimersDone', successCb);\n ceci.terminate(false);\n failTimeout = false;\n \n reject(new Error('clearTimers never received clearTimersDone event from sandbox'));\n }, 3000 * timeDilation); /* XXXwg need tuneable */\n\n if (ceci.evaluatorHandle) // Sometimes ceci.terminate nulls out evaluatorHandle before getting here.\n ceci.evaluatorHandle.postMessage(msg);\n });\n }\n\n /**\n * Sends a post message to describe its capabilities.\n *\n * Side effect: Sets the capabilities property of the current sandbox.\n *\n * @returns {Promise} Resolves with the sandbox's capabilities. Rejects with\n * an error saying a response was not received.\n * @memberof Sandbox\n */\n describe() {\n debug('Beginning to describe evaluator');\n var ceci = this;\n \n return new Promise(function sandbox$describePromise(resolve, reject) {\n if (ceci.evaluatorHandle === null) {\n return reject(new Error('Evaluator has not been initialized.'));\n }\n\n /**\n * Opted to create a flag for the describe response being received so that\n * we don't have to *hoist* the timeout's id to clear it in the response\n * handler.\n */\n let didReceiveDescribeResponse = false;\n const describeResponseHandler = ceci.ee.once('describe', (data) => {\n didReceiveDescribeResponse = true;\n const { capabilities } = data;\n if (typeof capabilities === 'undefined') {\n reject(\n new Error('Did not receive capabilities from describe response.'),\n );\n }\n ceci.capabilities = capabilities;\n\n // Currently only used in tests. May use the event in the future.\n ceci.emit('described', capabilities);\n debug('Evaluator has been described');\n resolve(capabilities);\n });\n const describeResponseFailedHandler = () => {\n if (!didReceiveDescribeResponse) {\n ceci.ee.removeListener('describe', describeResponseHandler);\n ceci.terminate(false);\n reject(\n new Error(\n 'Describe message timed-out. No describe response was received from the describe command.',\n ),\n );\n }\n };\n\n const message = {\n request: 'describe',\n };\n\n // Arbitrarily set the waiting time.\n setTimeout(describeResponseFailedHandler, 6000 * timeDilation); /* XXXwg need tuneable */\n assert(ceci.evaluatorHandle); // It is possible that ceci.terminate nulls out evaluatorHandle before getting here.\n ceci.evaluatorHandle.postMessage(message);\n });\n }\n\n /**\n * Passes the job's requirements object into the sandbox so that the global\n * access lists can be updated accordingly.\n *\n * e.g. disallow access to OffscreenCanvas without\n * environment.offscreenCanvas=true present.\n *\n * Must be called after @start.\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n applySandboxRequirements(requirements) {\n var ceci = this;\n \n return new Promise(function sandbox$applySandboxRequirementsPromise(resolve, reject) {\n const message = {\n requirements,\n request: 'applyRequirements',\n };\n let wereRequirementsApplied = false;\n\n const successCb = ceci.ee.once(\n 'applyRequirementsDone',\n function sandbox$applyRequirements$success() {\n wereRequirementsApplied = true;\n resolve();\n },\n );\n\n assert(typeof message.requirements === 'object');\n ceci.evaluatorHandle.postMessage(message);\n\n setTimeout(function sandbox$finishApplySandboxRequirements() {\n if (!wereRequirementsApplied) {\n ceci.ee.removeListener('applyRequirementsDone', successCb);\n ceci.terminate(false);\n reject(\n new Error(\n 'applyRequirements never received applyRequirementsDone response from sandbox',\n ),\n );\n }\n }, 3000 * timeDilation); /* XXXwg needs tunable */\n });\n }\n\n /**\n * Executes a slice received from the supervisor.\n * Must be called after @start.\n *\n * @param {Slice} slice - bare minimum data required for the job/job code to be executed on\n * @param {number} [delay = 0] the delay that this method should wait before beginning work, used to avoid starting all sandboxes at once\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n\n async work (slice, delay = 0) {\n var ceci = this;\n\n if (!ceci.isAssigned) {\n throw new Error(\"Sandbox.run: Sandbox is not ready to work, state=\" + ceci.state);\n }\n\n ceci.state = WORKING;\n ceci.slice = slice;\n assert(slice);\n\n // cf. DCP-1720\n this.resetSliceTimeReport();\n \n // Now wait for the delay if provided, prevents many sandboxes starting at once from crashing the supervisor\n if (delay > 0) await new Promise(resolve => setTimeout(resolve, (delay + 1) * timeDilation));\n if (!ceci.isWorking) return; // sandbox.terminate could have been called during the delay timeout\n\n // Prepare the sandbox to begin work\n // will be replaced by `assign` message that should be called before emitting a `work` message\n if (ceci.jobAddress !== slice.jobAddress) {\n throw new Error(`Sandbox.run: Sandbox is already assigned and jobAddress doesn't match previous (${ceci.jobAddress} !== ${slice.jobAddress})`);\n }\n\n let sliceHnd = { job: ceci.public, sandbox: ceci };\n await ceci.resetSandboxState();\n if (!ceci.slice) {\n console.error(`Slice for job ${ceci.jobAddress} vanished during work initialization - aborting`);\n return;\n }\n\n let inputDatum;\n let dataError = false;\n try {\n if (ceci.slice.datumUri)\n inputDatum = await fetchURI(\n ceci.slice.datumUri,\n this.allowedOrigins,\n dcpConfig.worker.allowOrigins.fetchData,\n );\n else {\n let { mro } = await ceci.supervisorCache.fetchJob(\n ceci.jobAddress,\n this.allowedOrigins.concat(dcpConfig.worker.allowOrigins.fetchData)\n );\n const ro = rehydrateRange(mro);\n // -1 to prevent an OBOE since slice numbers start at 1.\n inputDatum = ro[ceci.slice.sliceNumber - 1];\n }\n } catch (err) {\n dataError = err;\n if(err.code === 'EFETCH')\n dataError.errorCode = 'EFETCH'\n else\n dataError.errorCode = 'EUNCAUGHTERROR'\n ceci.emit('workEmit', {\n eventName: 'error',\n payload: {\n message: dataError.message,\n stack:dataError.stack,\n name: ceci.public.name\n }\n });\n }\n\n debugging('sandbox') && debug(`Fetched datum: ${inputDatum}`);\n\n if (!ceci.slice) {\n console.error(`Slice for job ${ceci.jobAddress} vanished after data fetch - aborting`);\n return;\n }\n\n ceci.resetProgressTimeout();\n ceci.resetSliceTimeout();\n\n return new Promise(function sandbox$$workPromise(resolve, reject) {\n let onSuccess, onFail\n\n onSuccess = ceci.ee.once('resolve', function sandbox$$work$success (event) {\n ceci.ee.removeListener('reject', onFail)\n resolve(event)\n }.bind(ceci))\n\n onFail = ceci.ee.once('reject', function sandbox$$work$fail (err) {\n ceci.ee.removeListener('resolve', onSuccess)\n reject(err)\n }.bind(ceci))\n\n ceci.sliceStartTime = Date.now();\n ceci.progress = null;\n ceci.progressReports = {\n last: undefined,\n lastDeterministic: undefined,\n };\n\n ceci.resetProgressTimeout();\n ceci.resetSliceTimeout();\n ceci.emit('start', sliceHnd);\n \n if(dataError){\n ceci.ee.removeListener('resolve', onSuccess);\n ceci.ee.removeListener('reject', onFail);\n setTimeout(() => reject(dataError), 0)\n\n } else {\n ceci.evaluatorHandle.postMessage({\n request: 'main',\n data: inputDatum,\n })\n }\n })\n .then(async function sandbox$$work$then(event) {\n // Ceci is the complete callback when the slice completes\n // prevent any hanging timers from being fired\n await ceci.clearSandboxTimers();\n\n // TODO: Should sliceHnd just be replaced with ceci.public?\n ceci.emit('slice', sliceHnd); /** @todo: decide which event is right */\n ceci.emit('sliceFinish', event);\n ceci.emit('complete', event);\n\n ceci.changeWorkingToAssigned();\n ceci.slice = false;\n return event;\n })\n .catch((err) => { \n if (err.name === 'EWORKREJECT') {\n this.rejectionData = err;\n ceci.evaluatorHandle.postMessage({ request: 'resetAndGetCPUTime' })\n } else { // sandbox termination for rejected work happens in Supervisor.handleRejectedWork\n // Ceci is the reject callback for when the slice throws an error\n ceci.terminate(false);\n }\n\n ceci.emit('error', err, 'slice');\n\n if (err instanceof NoProgressError) {\n ceci.emit('workEmit', {\n eventName: 'noProgress',\n payload: {\n timestamp: Date.now() - ceci.sliceStartTime,\n data: ceci.slice.datumUri,\n progressReports: ceci.progressReports,\n }\n });\n }\n throw err;\n })\n .finally(function sandbox$$work$finally() {\n ceci.emit('end', sliceHnd);\n });\n }\n\n resetProgressTimeout() {\n if (this.progressTimeout) {\n clearTimeout(this.progressTimeout);\n this.progressTimeout = null;\n }\n\n this.progressTimeout = setTimeout(() => {\n if (this.options.ignoreNoProgress) {\n return console.warn(\"ENOPROGRESS silenced by localExec: In a remote worker, this slice would be stopped for not calling progress frequently enough.\");\n }\n\n this.ee.emit('reject', new NoProgressError(`No progress event was received in the last ${dcpConfig.worker.sandbox.progressTimeout / 1000} seconds.`));\n }, +dcpConfig.worker.sandbox.progressTimeout * timeDilation);\n }\n\n resetSliceTimeout() {\n if (this.sliceTimeout) clearTimeout(this.sliceTimeout);\n\n this.sliceTimeout = setTimeout(() => {\n if (Sandbox.debugWork) return console.warn(\"Sandbox.debugWork: Ignoring slice timeout\");\n\n this.ee.emit('reject', new SliceTooSlowError(`Slice took longer than ${dcpConfig.worker.sandbox.sliceTimeout / 1000} seconds.`));\n }, +dcpConfig.worker.sandbox.sliceTimeout * timeDilation);\n }\n \n async handleRing0Message(data) {\n debugging('event:ring-0') && debug('event:ring-0', data);\n //handling a true ring 0 message\n switch (data.request) {\n case 'sandboxLoaded':\n // emit externally\n this.emit('sandboxLoaded', this)\n break;\n\n case 'scriptLoaded':\n // emit externally\n this.emit('scriptLoaded', data);\n \n if(data.result !== \"success\") {\n this.onerror(data);\n }\n break;\n \n case 'clearTimersDone':\n this.ee.emit(data.request, data);\n break;\n case 'totalCPUTime':\n this.updateTime(data);\n if (this.ee.listenerCount('resolve') > 0) {\n this.completeData.timeReport = this.sliceTimeReport;\n this.ee.emit('resolve', this.completeData);\n delete this.completeData;\n } else {\n this.rejectionData.timeReport = this.sliceTimeReport\n this.emit('rejectedWorkMetrics', this.rejectionData) // If there is no internal listener for 'resolve', the slice was rejected and\n delete this.rejectionData; // we need to send the rejected metrics to the supervisor\n } \n break;\n case 'error':\n // Warning: rejecting here with just event.data.error causes issues\n // where the reject handlers modify the object so it interferes with the\n // workEmit event payload, wrapping in an Error instance copies the values\n let e = new Error(\n data.error.message,\n data.error.fileName,\n data.error.lineNumber);\n e.stack = data.error.stack;\n e.name = data.error.name;\n \n if (this.ee.listenerCount('reject') > 0) {\n this.ee.emit('reject', e);\n } else {\n // This will happen if the error is thrown during initialization\n throw e;\n }\n\n break;\n default:\n let error = new Error('Received unhandled request from sandbox: ' + data.request + '\\n\\t' + JSON.stringify(data));\n console.error(error);\n break; \n }\n }\n\n async handleRing1Message(data) {\n switch (data.request) {\n case 'applyRequirementsDone':\n // emit internally\n this.ee.emit(data.request, data)\n break;\n default:\n let error = new Error('Received unhandled request from sandbox ring 1: ' + data.request + '\\n\\t' + JSON.stringify(data));\n console.error(error)\n break; \n }\n }\n\n async handleRing2Message(data) {\n debugging('event:ring-2') && debug('event:ring-2', data);\n switch (data.request) {\n case 'dependency': {\n let moduleData;\n try {\n moduleData = await this.supervisorCache.fetchModule(data.data);\n } catch (error) {\n /*\n * In the event of an error here, we want to let the client know there was a problem in\n * loading their module. However, there hasn't yet been an actual slice assigned to the sandbox.\n * Therefore, we assign 'slice 0' to the sandbox, a slice that will never exist, and is used\n * purely for this purpose. \n */\n this.slice = {\n jobAddress: this.jobAddress,\n sliceNumber: 0,\n };\n\n const payload = {\n name: error.name,\n timestamp: error.timestamp,\n message: error.message,\n };\n\n this.emit('workEmit', {\n eventName: 'error',\n payload,\n });\n this.ee.emit('reject', error);\n break;\n }\n this.evaluatorHandle.postMessage({\n request: 'moduleGroup',\n data: moduleData,\n id: data.id,\n });\n break;\n }\n case 'error':\n /*\n * Ring 2 error messages will only fire for problems inside of the worker that are separate from\n * the work function. In most cases there are other handlers for situations where 'error' may be emitted\n * such as timeouts if the expected message isn't recieved. Thus, we will output the error, but nothing else.\n */\n console.error(data.error);\n break;\n case 'describe':\n case 'evalResult':\n case 'resetStateDone':\n case 'assigned':\n // emit internally\n this.ee.emit(data.request, data);\n break;\n case 'reject':\n // emit internally\n this.ee.emit(data.request, data.error);\n break;\n default: {\n const error = new Error(\n `Received unhandled request from sandbox ring 2. Data: ${JSON.stringify(\n data,\n null,\n 2,\n )}`,\n );\n console.error(error);\n break;\n }\n }\n }\n\n async handleRing3Message(data) {\n switch (data.request) {\n case 'complete':\n clearTimeout(this.progressTimeout);\n clearTimeout(this.sliceTimeout);\n this.progressTimeout = this.sliceTimeout = null;\n\n if (this.progress === null) {\n if (this.options.ignoreNoProgress) {\n console.warn(\"ENOPROGRESS silenced by localExec: Progress was not called during this slice's execution, in a remote sandbox this would cause the slice to be failed\");\n } else {\n // If a progress update was never received (progress === null) then reject\n this.ee.emit('reject', new NoProgressError('Sandbox never emitted a progress event.'));\n break;\n }\n }\n this.evaluatorHandle.postMessage({ request: 'resetAndGetCPUTime' })\n this.progress = 100;\n this.completeData = data;\n // The timing report and resolve will be emitted when the CPU time is received. \n break;\n case 'progress':\n let { progress, indeterminate, throttledReports, value } = data;\n this.progress = progress;\n const progressReport = {\n timestamp: Date.now() - this.sliceStartTime,\n progress,\n value,\n throttledReports,\n }\n this.progressReports.last = progressReport;\n if (!indeterminate) {\n this.progressReports.lastDeterministic = progressReport;\n }\n\n this.resetProgressTimeout();\n\n this.emit('sliceProgress', data);\n break;\n\n case 'noProgress':\n let { message } = data;\n\n this.ee.emit('reject', new NoProgressError(message));\n break;\n case 'console':\n this.emit('workEmit', {\n eventName: 'console',\n payload: data.payload\n });\n break;\n\n case 'emitEvent':/* ad-hoc event from the sandbox (work.emit) */\n this.emit('workEmit', {\n eventName: 'custom',\n payload: data.payload\n })\n break;\n case 'measurement':\n this.updateTime(data);\n break;\n case 'sandboxError': /* the sandbox itself has an error condition */\n {\n this.emit('sandboxError', data.error);\n break;\n }\n case 'workError': { /* the work function threw/rejected */\n this.emit('workEmit', {\n eventName: 'error',\n payload: data.error,\n });\n\n // Warning: rejecting here with just .data.error causes issues\n // where the reject handlers modify the object so it interferes with the\n // workEmit payload, wrapping in an Error instance copies the values\n const wrappedError = new UncaughtExceptionError(\n data.error.message,\n data.error.fileName,\n data.error.lineNumber,\n );\n wrappedError.stack = data.error.stack;\n wrappedError.name = data.error.name;\n\n if (this.ee.listenerCount('reject') > 0) {\n this.ee.emit('reject', wrappedError);\n } else {\n // This will happen if the error is thrown during initialization\n throw wrappedError;\n }\n break;\n }\n default:\n let error = new Error('Received unhandled request from sandbox ring 3: ' + data.request + '\\n\\t' + JSON.stringify(data));\n console.error(error)\n break; \n }\n }\n\n /**\n * Handles progress and completion events from sandbox.\n * Unless explicitly returned out of this function will re-emit the event\n * on @this.ee where the name of the event is event.data.request.\n *\n * @param {object} event - event received from the sandbox\n */\n async onmessage(event) {\n debugging('event') && debug('event', event);\n if (Sandbox.debugEvents) {\n console.debug('sandbox - eventDebug:', {\n id: this.id,\n state: this.state,\n event: JSON.stringify(event)\n })\n }\n\n const { data } = event;\n const ringLevel = data.ringSource\n\n // Give the data to a handler depending on ring level\n if (ringLevel === -1) {\n console.error('Message sent directly from raw postMessage. Terminating worker...');\n console.debug(event);\n return this.terminate(true);\n } else {\n const handler = this.ringMessageHandlers[ringLevel];\n if (handler) {\n handler.call(this, data.value);\n } else {\n console.warn(`No handler defined for message from ring ${ringLevel}`);\n console.debug(event);\n }\n }\n }\n\n /**\n * Error handler for the internal sandbox.\n * Currently just logs the errors that the sandbox spits out.\n */\n onerror(event) {\n console.error('Sandbox emitted an error:', event);\n this.terminate(true, true);\n }\n\n /**\n * Clears the timeout and terminates the sandbox and sometimes emits a reject event.\n *\n * @param {boolean} [reject = true] - if true emit reject event\n * @param {boolean} [immediate = false] - passed to terminate, used by standaloneWorker to immediately close the connection\n */\n terminate (reject = true, immediate = false) {\n const oldState = this.state;\n this.state = TERMINATED;\n \n clearTimeout(this.progressTimeout);\n clearTimeout(this.sliceTimeout);\n this.progressTimeout = this.sliceTimeout = null;\n \n if (this.evaluatorHandle && typeof this.evaluatorHandle.terminate === 'function') {\n try {\n this.evaluatorHandle.terminate(immediate);\n this.evaluatorHandle = null;\n } catch (e) {\n console.error(`Error terminating sandbox ${this.id} (${oldState}):`, e);\n } finally {\n this.emit('terminate', this);\n }\n }\n\n if (reject) {\n this.ee.emit('reject', new Error(`Sandbox ${this.id} (${oldState}) was terminated.`));\n }\n\n this.emit('terminated');\n }\n\n /**\n * Attempts to stop the sandbox from doing completing its current\n * set of work without terminating the working.\n */\n stop () {\n throw new Error('Sandbox.stop is not yet implemented.')\n }\n\n /**\n * ringNPostMessage can send a `measurement` request and update these\n * totals.\n */\n updateTime (measurementEvent) {\n ['total', 'CPU', 'webGL'].forEach((key) => {\n if (measurementEvent[key]) this.sliceTimeReport[key] += measurementEvent[key];\n })\n }\n\n resetSliceTimeReport () {\n this.sliceTimeReport = {\n total: 0,\n CPU: 0,\n webGL: 0,\n }\n }\n}\n\nSandbox.idCounter = 1;\nSandbox.debugWork = false;\nSandbox.debugState = false;\nSandbox.debugEvents = false;\n\nexports.Sandbox = Sandbox;\nexports.SandboxError = SandboxError;\nexports.NoProgressError = NoProgressError;\nexports.SliceTooSlowError = SliceTooSlowError;\nexports.UncaughtExceptionError = UncaughtExceptionError;\nexports.RemoteFetchError = RemoteFetchError;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/worker/sandbox.js?");
4427
+ eval("// NOTE - need timeout/postmessage function\n/**\n * @file dcp-client/worker/sandbox.js\n *\n * A sandbox that when constructed and assigned can do work for\n * a distributed slice. A sandbox runs for a single slice at a time.\n *\n * Usage:\n * let sandbox = new Sandbox()\n * await sandbox.start()\n * let results = await sandbox.run(slice)\n *\n * Debug flags:\n * Sandbox.debugWork = true // - turns off 30 second timeout to let user debug sandbox innards more easily\n * Sandbox.debugState = true // - logs all state transitions for this sandbox\n * Sandbox.debugEvents = true // - logs all events received from the sandbox\n *\n * @author Matthew Palma, mpalma@kingsds.network\n * Ryan Rossiter, ryan@kingsds.network\n * @date May 2019\n * @module sandbox\n */\n/* global dcpConfig */\n// @ts-check\n\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('worker');\nconst { assert, assertEq3 } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst { rehydrateRange } = __webpack_require__(/*! dcp/dcp-client/range-object */ \"./src/dcp-client/range-object.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\nconst scopedKvin = new kvin.KVIN({Object: ({}).constructor,\n Array: ([]).constructor, \n Function: (()=>{}).constructor});\n\nlet timeDilation = 1;\nif (DCP_ENV.platform === 'nodejs') {\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n /** Make timers 10x slower when running in niim */\n timeDilation = (requireNative('module')._cache.niim instanceof requireNative('module').Module) ? 10 : 1;\n}\n\n/**\n * Wraps console.debug to emulate debug module prefixing messages on npm.\n * @param {...any} args\n */\nconst debug = (...args) => {\n if (debugging()) {\n console.debug('dcp-client:worker:sandbox', ...args);\n }\n};\n\nconst nanoid = (__webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\").nanoid);\n\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { fetchURI, encodeDataURI } = __webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\");\n\n// Sandbox states\nconst UNREADY = 'UNREADY' // No Sandbox (web worker, saworker, etc) has been constructed yet\nconst READYING = 'READYING' // Sandbox is being constructed and environment (bravojs, env) is being set up\nconst READY_FOR_ASSIGN = 'READY_FOR_ASSIGN' // Sandbox is ready to be assigned\nconst ASSIGNING = 'ASSIGNING' // Sandbox is running through assigning steps\nconst ASSIGNED = 'ASSIGNED' // Sandbox is assigned but not working\nconst WORKING = 'WORKING' // Sandbox is working\nconst TERMINATED = 'TERMINATED'\nconst EVAL_RESULT_PREFIX = 'evalResult::';\n\nclass SandboxError extends Error {}\nclass NoProgressError extends SandboxError { constructor(...args) { super(...args); this.errorCode = 'ENOPROGRESS'; } }\nclass SliceTooSlowError extends SandboxError { constructor(...args) { super(...args); this.errorCode = 'ESLICETOOSLOW'; } }\nclass UncaughtExceptionError extends SandboxError { constructor(...args) { super(...args); this.errorCode = 'EUNCAUGHT'; } }\nclass RemoteFetchError extends SandboxError { constructor(...args) { super(...args); this.errorCode = 'EFETCH'; }}\n\n/** @typedef {import('dcp/common/dcp-events').EventEmitter} EventEmitter */\n/** @typedef {import('./slice').Slice} Slice */\n/** @typedef {import('./supervisor-cache').SupervisorCache} SupervisorCache */\n/** @typedef {*} opaqueId */\n\n/**\n * @access public\n * @typedef {object} SandboxOptions\n * @constructor {function} [SandboxConstructor]\n * @property {boolean} [ignoreNoProgress] - When true, the sandbox will not be stopped for not calling progress\n */\n\nclass Sandbox extends EventEmitter {\n /**\n * A Sandbox (i.e. a worker sandbox) which executes distributed slices.\n *\n * @constructor\n * @param {SupervisorCache} cache\n * @param {SandboxOptions} options\n */\n constructor (cache, options, origins) {\n super('Sandbox');\n /** @type {SupervisorCache} */\n this.supervisorCache = cache;\n /** @type {SandboxOptions} */\n this.options = {\n ignoreNoProgress: false,\n ...options,\n SandboxConstructor: options.SandboxConstructor ||\n (__webpack_require__(/*! ./evaluators */ \"./src/dcp-client/worker/evaluators/index.js\").BrowserEvaluator),\n }\n this.allowedOrigins = origins;\n\n /** @type {opaqueId} */\n this.jobAddress = null;\n /** @type {object} */\n this.evaluatorHandle = null;\n /** @type {object} */\n this.capabilities = null;\n /** @type {EventEmitter} */\n this.ee = new EventEmitter('SandboxInternal')\n\n /** @type {string} */\n this._state = UNREADY;\n /** @type {boolean} */\n this.allocated = false;\n /** @type {number?} */\n this.progress = 100;\n /** @type {object} */\n this.progressReports = null;\n /** @type {object} */\n this.progressTimeout = null;\n /** @type {object} */\n this.sliceTimeout = null;\n /** @type {object} */\n this.rejectionData = null;\n /** @type {Slice} */\n this.slice = null;\n\n /** @type {number?} */\n this.started = null;\n /** @type {number?} */\n this.sliceStartTime = null;\n /** @type {boolean} */\n this.requiresGPU = false;\n /** @type {string|URL} */\n this.packageURL = dcpConfig.packageManager.location\n /** @type {number?} */\n this.id = Sandbox.getNewId();\n\n this.ringMessageHandlers = [\n this.handleRing0Message,\n this.handleRing1Message,\n this.handleRing2Message,\n this.handleRing3Message,\n ];\n\n this.resetSliceTimeReport();\n }\n\n get identifier() {\n if (this.allocated)\n return `${this.id}.${this.jobAddress}.${this.state}.${this.allocated}`;\n return `${this.id}.${this.jobAddress}.${this.state}`;\n }\n\n static getNewId() {\n return Sandbox.idCounter++;\n }\n\n get state () {\n return this._state\n }\n\n set state (value) {\n if (Sandbox.debugState) {\n console.debug(`sandbox - changing state of ${this.id}... ${this._state} -> ${value}`)\n }\n\n if (this.state === TERMINATED && value !== TERMINATED) {\n // For safety!\n throw new Error(`Sandbox set state violation, attepted to change state from ${this.state} to ${value}`);\n }\n\n this._state = value;\n }\n\n get isReadyForAssign () {\n return this.state === READY_FOR_ASSIGN;\n }\n\n get isAssigned () {\n return this.state === ASSIGNED;\n }\n\n get isWorking () {\n return this.state === WORKING;\n }\n\n get isTerminated () {\n return this.state === TERMINATED;\n }\n\n changeWorkingToAssigned () {\n if (this.isWorking)\n this.state = ASSIGNED;\n }\n\n setIsAssigning () {\n this.state = ASSIGNING;\n }\n\n /**\n * Readies the sandbox. This will result in the sandbox being ready and not assigned,\n * it will need to be assigned with a job before it is able to do work.\n *\n * @todo maybe preload specific modules or let the cache pass in what modules to load?\n * @throws on failure to ready\n */\n async start(delay = 0) {\n this.started = Date.now();\n this.state = READYING;\n\n if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay * timeDilation));\n\n try {\n // RING 0\n this.evaluatorHandle = new this.options.SandboxConstructor({\n name: `DCP Sandbox #${this.id}`,\n });\n this.evaluatorHandle.onerror = this.onerror.bind(this);\n\n const messageHandler = this.onmessage.bind(this);\n this.evaluatorHandle.onmessage = function onmessage(event)\n {\n let data;\n if (event.data.serialized)\n {\n data = kvin.parse(event.data.message);\n }\n else\n {\n data = kvin.unmarshal(event.data);\n }\n messageHandler({ data });\n }\n\n const evaluatorPostMessage = this.evaluatorHandle.postMessage.bind(this.evaluatorHandle);\n this.evaluatorHandle.postMessage = function postMessage(message)\n {\n evaluatorPostMessage(scopedKvin.marshal(message));\n }\n\n const ceci = this;\n this.evaluatorHandle.addEventListener('end', () => ceci.terminate(true));\n\n // Now in RING 1\n\n // Now in RING 2\n await this.describe();\n this.state = READY_FOR_ASSIGN;\n this.emit('ready', this);\n } catch (error) {\n console.warn('Failed to start the sandbox -', error.message);\n this.terminate(false);\n throw error;\n }\n }\n\n /**\n * This will assign the sandbox with a job, loading its sandbox code\n * into the sandbox.\n *\n * @param {string} jobAddress The address of the job to assign to\n * @throws on initialization failure\n */\n async assign(jobAddress) {\n this.jobAddress = jobAddress;\n this.job = await this.supervisorCache.fetchJob(jobAddress, this.allowedOrigins);\n\n assertEq3(this.job.address, jobAddress);\n assert(typeof this.job === 'object');\n assert(typeof this.job.requirements === 'object');\n assert(Array.isArray(this.job.dependencies));\n assert(Array.isArray(this.job.requirePath));\n\n // Extract public data from job, with defaults\n this.public = Object.assign({\n name: `Anonymous Job ${this.job.address.slice(0, 6)}`,\n description: 'Discreetly helping make the world smarter.',\n link: 'https://distributed.computer/about',\n }, this.job.public);\n\n // Future: We may want other filename tags for appliances // RR Nov 2019\n\n // Important: The order of applying requirements before loading the sandbox code\n // is important for modules and sandbox code to set globals over the whitelist.\n await this.applySandboxRequirements(this.job.requirements);\n await this.assignEvaluator();\n this.state = ASSIGNED;\n }\n\n async assignEvaluator() {\n debug('Begin assigning job to evaluator');\n const ceci = this;\n\n return new Promise(function sandbox$$assignEvaluatorPromise(resolve, reject) {\n const message = {\n request: 'assign',\n job: ceci.job,\n sandboxConfig: dcpConfig.worker.sandbox,\n };\n\n /* note - onFailListener used for removal. This is necessary due to a bug in ee.once. /wg Feb 2022 */\n \n const onSuccess = (event) => {\n // eslint-disable-next-line no-use-before-define\n ceci.ee.removeListener('reject', onFailListener);\n ceci.emit('assigned', event.jobAddress);\n debug('Job assigned to evaluator');\n resolve();\n };\n\n const onFail = (error) => {\n // eslint-disable-next-line no-use-before-define\n ceci.ee.removeListener('assigned', onSuccessListener);\n reject(error);\n };\n\n const onSuccessListener = ceci.ee.once('assigned', onSuccess);\n const onFailListener = ceci.ee.once('reject', onFail);\n ceci.evaluatorHandle.postMessage(message);\n });\n }\n\n /**\n * Evaluates a string inside the sandbox.\n *\n * @param {string} code - the code to evaluate in the sandbox\n * @param {string} filename - the name of the 'file' to help with debugging,\n * no longer working though?\n * @returns {Promise} - resolves with eval result on success, rejects\n * otherwise\n */\n eval(code, filename) {\n var ceci = this;\n \n return new Promise(function sandbox$$eval$Promise(resolve, reject) {\n let msgId = nanoid();\n let msg = {\n request: 'eval',\n data: code,\n filename,\n msgId, \n }\n\n const eventId = EVAL_RESULT_PREFIX + msgId;\n\n let onSuccess = (event) => {\n ceci.ee.removeListener('reject', onFailListener)\n resolve(event)\n }\n\n let onFail = (event) => {\n ceci.ee.removeListener(eventId, onSuccessListener)\n reject(event)\n }\n\n let onSuccessListener = ceci.ee.once(eventId, onSuccess);\n let onFailListener = ceci.ee.once('reject', onFail)\n\n ceci.evaluatorHandle.postMessage(msg)\n })\n }\n\n /**\n * Resets the state of the bootstrap, without resetting the sandbox function if assigned.\n * Mostly used to reset the progress status before reusing a sandbox on another slice.\n * Must be called after @start.\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n resetSandboxState () {\n var ceci = this;\n\n return new Promise(function sandbox$resetSandboxStatePromise(resolve, reject) {\n let successCb, failTimeout;\n let msg = {\n request: 'resetState',\n };\n\n successCb = ceci.ee.once('resetStateDone', function sandbox$resetSandboxState$success () {\n if (failTimeout === false)\n return; /* already rejected */\n clearTimeout(failTimeout);\n failTimeout = false;\n resolve();\n });\n\n failTimeout = setTimeout(function sandbox$resetSandboxState$fail() {\n if (failTimeout === false)\n return; /* already resolved */\n \n ceci.ee.removeListener('resetStateDone', successCb);\n ceci.terminate(false);\n failTimeout = false;\n\n reject(new Error('resetState never received resetStateDone event from sandbox'));\n }, 3000 * timeDilation); /* XXXwg need tuneable */\n\n assert(ceci.evaluatorHandle); // It is possible that ceci.terminate nulls out evaluatorHandle before getting here.\n ceci.evaluatorHandle.postMessage(msg);\n });\n }\n\n /**\n * Clear all timers that are set inside the sandbox (evaluator) environment.\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n clearSandboxTimers() {\n var ceci = this;\n \n return new Promise(function sandbox$clearSandboxTimersPromise(resolve, reject) {\n let successCb, failTimeout;\n let msg = {\n request: 'clearTimers',\n };\n\n successCb = ceci.ee.once('clearTimersDone', function sandbox$clearSandboxTimers$success() {\n if (failTimeout === false)\n return; /* already rejected */\n clearTimeout(failTimeout);\n failTimeout = false;\n resolve();\n });\n\n failTimeout = setTimeout(function sanbox$clearSandboxTimers$fail() {\n if (failTimeout === false)\n return; /* already resolved */\n \n ceci.ee.removeListener('clearTimersDone', successCb);\n ceci.terminate(false);\n failTimeout = false;\n \n reject(new Error('clearTimers never received clearTimersDone event from sandbox'));\n }, 3000 * timeDilation); /* XXXwg need tuneable */\n\n if (ceci.evaluatorHandle) // Sometimes ceci.terminate nulls out evaluatorHandle before getting here.\n ceci.evaluatorHandle.postMessage(msg);\n });\n }\n\n /**\n * Sends a post message to describe its capabilities.\n *\n * Side effect: Sets the capabilities property of the current sandbox.\n *\n * @returns {Promise} Resolves with the sandbox's capabilities. Rejects with\n * an error saying a response was not received.\n * @memberof Sandbox\n */\n describe() {\n debug('Beginning to describe evaluator');\n var ceci = this;\n \n return new Promise(function sandbox$describePromise(resolve, reject) {\n if (ceci.evaluatorHandle === null) {\n return reject(new Error('Evaluator has not been initialized.'));\n }\n\n /**\n * Opted to create a flag for the describe response being received so that\n * we don't have to *hoist* the timeout's id to clear it in the response\n * handler.\n */\n let didReceiveDescribeResponse = false;\n const describeResponseHandler = ceci.ee.once('describe', (data) => {\n didReceiveDescribeResponse = true;\n const { capabilities } = data;\n if (typeof capabilities === 'undefined') {\n reject(\n new Error('Did not receive capabilities from describe response.'),\n );\n }\n ceci.capabilities = capabilities;\n\n // Currently only used in tests. May use the event in the future.\n ceci.emit('described', capabilities);\n debug('Evaluator has been described');\n resolve(capabilities);\n });\n const describeResponseFailedHandler = () => {\n if (!didReceiveDescribeResponse) {\n ceci.ee.removeListener('describe', describeResponseHandler);\n ceci.terminate(false);\n reject(\n new Error(\n 'Describe message timed-out. No describe response was received from the describe command.',\n ),\n );\n }\n };\n\n const message = {\n request: 'describe',\n };\n\n // Arbitrarily set the waiting time.\n setTimeout(describeResponseFailedHandler, 6000 * timeDilation); /* XXXwg need tuneable */\n assert(ceci.evaluatorHandle); // It is possible that ceci.terminate nulls out evaluatorHandle before getting here.\n ceci.evaluatorHandle.postMessage(message);\n });\n }\n\n /**\n * Passes the job's requirements object into the sandbox so that the global\n * access lists can be updated accordingly.\n *\n * e.g. disallow access to OffscreenCanvas without\n * environment.offscreenCanvas=true present.\n *\n * Must be called after @start.\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n applySandboxRequirements(requirements) {\n var ceci = this;\n \n return new Promise(function sandbox$applySandboxRequirementsPromise(resolve, reject) {\n const message = {\n requirements,\n request: 'applyRequirements',\n };\n let wereRequirementsApplied = false;\n\n const successCb = ceci.ee.once(\n 'applyRequirementsDone',\n function sandbox$applyRequirements$success() {\n wereRequirementsApplied = true;\n resolve();\n },\n );\n\n assert(typeof message.requirements === 'object');\n ceci.evaluatorHandle.postMessage(message);\n\n setTimeout(function sandbox$finishApplySandboxRequirements() {\n if (!wereRequirementsApplied) {\n ceci.ee.removeListener('applyRequirementsDone', successCb);\n ceci.terminate(false);\n reject(\n new Error(\n 'applyRequirements never received applyRequirementsDone response from sandbox',\n ),\n );\n }\n }, 3000 * timeDilation); /* XXXwg needs tunable */\n });\n }\n\n /**\n * Executes a slice received from the supervisor.\n * Must be called after @start.\n *\n * @param {Slice} slice - bare minimum data required for the job/job code to be executed on\n * @param {number} [delay = 0] the delay that this method should wait before beginning work, used to avoid starting all sandboxes at once\n *\n * @returns {Promise} - resolves with result on success, rejects otherwise\n */\n\n async work (slice, delay = 0) {\n var ceci = this;\n\n if (!ceci.isAssigned) {\n throw new Error(\"Sandbox.run: Sandbox is not ready to work, state=\" + ceci.state);\n }\n\n ceci.state = WORKING;\n ceci.slice = slice;\n assert(slice);\n\n // cf. DCP-1720\n this.resetSliceTimeReport();\n \n // Now wait for the delay if provided, prevents many sandboxes starting at once from crashing the supervisor\n if (delay > 0) await new Promise(resolve => setTimeout(resolve, (delay + 1) * timeDilation));\n if (!ceci.isWorking) return; // sandbox.terminate could have been called during the delay timeout\n\n // Prepare the sandbox to begin work\n // will be replaced by `assign` message that should be called before emitting a `work` message\n if (ceci.jobAddress !== slice.jobAddress) {\n throw new Error(`Sandbox.run: Sandbox is already assigned and jobAddress doesn't match previous (${ceci.jobAddress} !== ${slice.jobAddress})`);\n }\n\n let sliceHnd = { job: ceci.public, sandbox: ceci };\n await ceci.resetSandboxState();\n if (!ceci.slice) {\n console.error(`Slice for job ${ceci.jobAddress} vanished during work initialization - aborting`);\n return;\n }\n\n let inputDatum;\n let dataError = false;\n try {\n if (ceci.slice.datumUri)\n inputDatum = await fetchURI(\n ceci.slice.datumUri,\n this.allowedOrigins,\n dcpConfig.worker.allowOrigins.fetchData,\n );\n else {\n let { mro } = await ceci.supervisorCache.fetchJob(\n ceci.jobAddress,\n this.allowedOrigins.concat(dcpConfig.worker.allowOrigins.fetchData)\n );\n const ro = rehydrateRange(mro);\n // -1 to prevent an OBOE since slice numbers start at 1.\n inputDatum = ro[ceci.slice.sliceNumber - 1];\n }\n } catch (err) {\n dataError = err;\n if(err.code === 'EFETCH')\n dataError.errorCode = 'EFETCH'\n else\n dataError.errorCode = 'EUNCAUGHTERROR'\n ceci.emit('workEmit', {\n eventName: 'error',\n payload: {\n message: dataError.message,\n stack:dataError.stack,\n name: ceci.public.name\n }\n });\n }\n\n debugging('sandbox') && debug(`Fetched datum: ${inputDatum}`);\n\n if (!ceci.slice) {\n console.error(`Slice for job ${ceci.jobAddress} vanished after data fetch - aborting`);\n return;\n }\n\n ceci.resetProgressTimeout();\n ceci.resetSliceTimeout();\n\n return new Promise(function sandbox$$workPromise(resolve, reject) {\n let onSuccess, onFail\n\n onSuccess = ceci.ee.once('resolve', function sandbox$$work$success (event) {\n ceci.ee.removeListener('reject', onFail)\n resolve(event)\n }.bind(ceci))\n\n onFail = ceci.ee.once('reject', function sandbox$$work$fail (err) {\n ceci.ee.removeListener('resolve', onSuccess)\n reject(err)\n }.bind(ceci))\n\n ceci.sliceStartTime = Date.now();\n ceci.progress = null;\n ceci.progressReports = {\n last: undefined,\n lastDeterministic: undefined,\n };\n\n ceci.resetProgressTimeout();\n ceci.resetSliceTimeout();\n ceci.emit('start', sliceHnd);\n \n if(dataError){\n ceci.ee.removeListener('resolve', onSuccess);\n ceci.ee.removeListener('reject', onFail);\n setTimeout(() => reject(dataError), 0)\n\n } else {\n ceci.evaluatorHandle.postMessage({\n request: 'main',\n data: inputDatum,\n })\n }\n })\n .then(async function sandbox$$work$then(event) {\n // Ceci is the complete callback when the slice completes\n // prevent any hanging timers from being fired\n await ceci.clearSandboxTimers();\n\n // TODO: Should sliceHnd just be replaced with ceci.public?\n ceci.emit('slice', sliceHnd); /** @todo: decide which event is right */\n ceci.emit('sliceFinish', event);\n ceci.emit('complete', event);\n\n ceci.changeWorkingToAssigned();\n ceci.slice = false;\n return event;\n })\n .catch((err) => { \n if (err.name === 'EWORKREJECT') {\n this.rejectionData = err;\n ceci.evaluatorHandle.postMessage({ request: 'resetAndGetCPUTime' })\n } else { // sandbox termination for rejected work happens in Supervisor.handleRejectedWork\n // Ceci is the reject callback for when the slice throws an error\n ceci.terminate(false);\n }\n\n ceci.emit('error', err, 'slice');\n\n if (err instanceof NoProgressError) {\n ceci.emit('workEmit', {\n eventName: 'noProgress',\n payload: {\n timestamp: Date.now() - ceci.sliceStartTime,\n data: ceci.slice.datumUri,\n progressReports: ceci.progressReports,\n }\n });\n }\n throw err;\n })\n .finally(function sandbox$$work$finally() {\n ceci.emit('end', sliceHnd);\n });\n }\n\n resetProgressTimeout() {\n if (this.progressTimeout) {\n clearTimeout(this.progressTimeout);\n this.progressTimeout = null;\n }\n\n this.progressTimeout = setTimeout(() => {\n if (this.options.ignoreNoProgress) {\n return console.warn(\"ENOPROGRESS silenced by localExec: In a remote worker, this slice would be stopped for not calling progress frequently enough.\");\n }\n\n this.ee.emit('reject', new NoProgressError(`No progress event was received in the last ${dcpConfig.worker.sandbox.progressTimeout / 1000} seconds.`));\n }, +dcpConfig.worker.sandbox.progressTimeout * timeDilation);\n }\n\n resetSliceTimeout() {\n if (this.sliceTimeout) clearTimeout(this.sliceTimeout);\n\n this.sliceTimeout = setTimeout(() => {\n if (Sandbox.debugWork) return console.warn(\"Sandbox.debugWork: Ignoring slice timeout\");\n\n this.ee.emit('reject', new SliceTooSlowError(`Slice took longer than ${dcpConfig.worker.sandbox.sliceTimeout / 1000} seconds.`));\n }, +dcpConfig.worker.sandbox.sliceTimeout * timeDilation);\n }\n \n async handleRing0Message(data) {\n debugging('event:ring-0') && debug('event:ring-0', data);\n //handling a true ring 0 message\n switch (data.request) {\n case 'sandboxLoaded':\n // emit externally\n this.emit('sandboxLoaded', this)\n break;\n\n case 'scriptLoaded':\n // emit externally\n this.emit('scriptLoaded', data);\n \n if(data.result !== \"success\") {\n this.onerror(data);\n }\n break;\n \n case 'clearTimersDone':\n this.ee.emit(data.request, data);\n break;\n case 'totalCPUTime':\n this.updateTime(data);\n if (this.ee.listenerCount('resolve') > 0) {\n this.completeData.timeReport = this.sliceTimeReport;\n this.ee.emit('resolve', this.completeData);\n delete this.completeData;\n } else {\n this.rejectionData.timeReport = this.sliceTimeReport\n this.emit('rejectedWorkMetrics', this.rejectionData) // If there is no internal listener for 'resolve', the slice was rejected and\n delete this.rejectionData; // we need to send the rejected metrics to the supervisor\n } \n break;\n case 'error':\n // Warning: rejecting here with just event.data.error causes issues\n // where the reject handlers modify the object so it interferes with the\n // workEmit event payload, wrapping in an Error instance copies the values\n let e = new Error(\n data.error.message,\n data.error.fileName,\n data.error.lineNumber);\n e.stack = data.error.stack;\n e.name = data.error.name;\n \n if (this.ee.listenerCount('reject') > 0) {\n this.ee.emit('reject', e);\n } else {\n // This will happen if the error is thrown during initialization\n throw e;\n }\n\n break;\n default:\n let error = new Error('Received unhandled request from sandbox: ' + data.request + '\\n\\t' + JSON.stringify(data));\n console.error(error);\n break; \n }\n }\n\n async handleRing1Message(data) {\n switch (data.request) {\n case 'applyRequirementsDone':\n // emit internally\n this.ee.emit(data.request, data)\n break;\n default:\n let error = new Error('Received unhandled request from sandbox ring 1: ' + data.request + '\\n\\t' + JSON.stringify(data));\n console.error(error)\n break; \n }\n }\n\n async handleRing2Message(data) {\n debugging('event:ring-2') && debug('event:ring-2', data);\n switch (data.request) {\n case 'dependency': {\n let moduleData;\n try {\n moduleData = await this.supervisorCache.fetchModule(data.data);\n } catch (error) {\n /*\n * In the event of an error here, we want to let the client know there was a problem in\n * loading their module. However, there hasn't yet been an actual slice assigned to the sandbox.\n * Therefore, we assign 'slice 0' to the sandbox, a slice that will never exist, and is used\n * purely for this purpose. \n */\n this.slice = {\n jobAddress: this.jobAddress,\n sliceNumber: 0,\n };\n\n const payload = {\n name: error.name,\n timestamp: error.timestamp,\n message: error.message,\n };\n\n this.emit('workEmit', {\n eventName: 'error',\n payload,\n });\n this.ee.emit('reject', error);\n break;\n }\n this.evaluatorHandle.postMessage({\n request: 'moduleGroup',\n data: moduleData,\n id: data.id,\n });\n break;\n }\n case 'error':\n /*\n * Ring 2 error messages will only fire for problems inside of the worker that are separate from\n * the work function. In most cases there are other handlers for situations where 'error' may be emitted\n * such as timeouts if the expected message isn't recieved. Thus, we will output the error, but nothing else.\n */\n console.error(data.error);\n break;\n case 'describe':\n case 'evalResult':\n case 'resetStateDone':\n case 'assigned':\n // emit internally\n this.ee.emit(data.request, data);\n break;\n case 'reject':\n // emit internally\n this.ee.emit(data.request, data.error);\n break;\n default: {\n const error = new Error(\n `Received unhandled request from sandbox ring 2. Data: ${JSON.stringify(\n data,\n null,\n 2,\n )}`,\n );\n console.error(error);\n break;\n }\n }\n }\n\n async handleRing3Message(data) {\n switch (data.request) {\n case 'complete':\n clearTimeout(this.progressTimeout);\n clearTimeout(this.sliceTimeout);\n this.progressTimeout = this.sliceTimeout = null;\n\n if (this.progress === null) {\n if (this.options.ignoreNoProgress) {\n console.warn(\"ENOPROGRESS silenced by localExec: Progress was not called during this slice's execution, in a remote sandbox this would cause the slice to be failed\");\n } else {\n // If a progress update was never received (progress === null) then reject\n this.ee.emit('reject', new NoProgressError('Sandbox never emitted a progress event.'));\n break;\n }\n }\n this.evaluatorHandle.postMessage({ request: 'resetAndGetCPUTime' })\n this.progress = 100;\n this.completeData = data;\n // The timing report and resolve will be emitted when the CPU time is received. \n break;\n case 'progress':\n let { progress, indeterminate, throttledReports, value } = data;\n this.progress = progress;\n const progressReport = {\n timestamp: Date.now() - this.sliceStartTime,\n progress,\n value,\n throttledReports,\n }\n this.progressReports.last = progressReport;\n if (!indeterminate) {\n this.progressReports.lastDeterministic = progressReport;\n }\n\n this.resetProgressTimeout();\n\n this.emit('sliceProgress', data);\n break;\n\n case 'noProgress':\n let { message } = data;\n\n this.ee.emit('reject', new NoProgressError(message));\n break;\n case 'console':\n data.payload.message = scopedKvin.marshal(data.payload.message); \n this.emit('workEmit', {\n eventName: 'console',\n payload: data.payload, \n });\n break;\n\n case 'emitEvent':/* ad-hoc event from the sandbox (work.emit) */\n this.emit('workEmit', {\n eventName: 'custom',\n payload: data.payload\n })\n break;\n case 'measurement':\n this.updateTime(data);\n break;\n case 'sandboxError': /* the sandbox itself has an error condition */\n {\n this.emit('sandboxError', data.error);\n break;\n }\n case 'workError': { /* the work function threw/rejected */\n this.emit('workEmit', {\n eventName: 'error',\n payload: data.error,\n });\n\n // Warning: rejecting here with just .data.error causes issues\n // where the reject handlers modify the object so it interferes with the\n // workEmit payload, wrapping in an Error instance copies the values\n const wrappedError = new UncaughtExceptionError(\n data.error.message,\n data.error.fileName,\n data.error.lineNumber,\n );\n wrappedError.stack = data.error.stack;\n wrappedError.name = data.error.name;\n\n if (this.ee.listenerCount('reject') > 0) {\n this.ee.emit('reject', wrappedError);\n } else {\n // This will happen if the error is thrown during initialization\n throw wrappedError;\n }\n break;\n }\n default:\n let error = new Error('Received unhandled request from sandbox ring 3: ' + data.request + '\\n\\t' + JSON.stringify(data));\n console.error(error)\n break; \n }\n }\n\n /**\n * Handles progress and completion events from sandbox.\n * Unless explicitly returned out of this function will re-emit the event\n * on @this.ee where the name of the event is event.data.request.\n *\n * @param {object} event - event received from the sandbox\n */\n async onmessage(event) {\n debugging('event') && debug('event', event);\n if (Sandbox.debugEvents) {\n console.debug('sandbox - eventDebug:', {\n id: this.id,\n state: this.state,\n event: JSON.stringify(event)\n })\n }\n\n const { data } = event;\n const ringLevel = data.ringSource\n\n // Give the data to a handler depending on ring level\n if (ringLevel === -1) {\n console.error('Message sent directly from raw postMessage. Terminating worker...');\n console.debug(event);\n return this.terminate(true);\n } else {\n const handler = this.ringMessageHandlers[ringLevel];\n if (handler) {\n handler.call(this, data.value);\n } else {\n console.warn(`No handler defined for message from ring ${ringLevel}`);\n console.debug(event);\n }\n }\n }\n\n /**\n * Error handler for the internal sandbox.\n * Currently just logs the errors that the sandbox spits out.\n */\n onerror(event) {\n console.error('Sandbox emitted an error:', event);\n this.terminate(true, true);\n }\n\n /**\n * Clears the timeout and terminates the sandbox and sometimes emits a reject event.\n *\n * @param {boolean} [reject = true] - if true emit reject event\n * @param {boolean} [immediate = false] - passed to terminate, used by standaloneWorker to immediately close the connection\n */\n terminate (reject = true, immediate = false) {\n const oldState = this.state;\n this.state = TERMINATED;\n \n clearTimeout(this.progressTimeout);\n clearTimeout(this.sliceTimeout);\n this.progressTimeout = this.sliceTimeout = null;\n \n if (this.evaluatorHandle && typeof this.evaluatorHandle.terminate === 'function') {\n try {\n this.evaluatorHandle.terminate(immediate);\n this.evaluatorHandle = null;\n } catch (e) {\n console.error(`Error terminating sandbox ${this.id} (${oldState}):`, e);\n } finally {\n this.emit('terminate', this);\n }\n }\n\n if (reject) {\n this.ee.emit('reject', new Error(`Sandbox ${this.id} (${oldState}) was terminated.`));\n }\n\n this.emit('terminated');\n }\n\n /**\n * Attempts to stop the sandbox from doing completing its current\n * set of work without terminating the working.\n */\n stop () {\n throw new Error('Sandbox.stop is not yet implemented.')\n }\n\n /**\n * ringNPostMessage can send a `measurement` request and update these\n * totals.\n */\n updateTime (measurementEvent) {\n ['total', 'CPU', 'webGL'].forEach((key) => {\n if (measurementEvent[key]) this.sliceTimeReport[key] += measurementEvent[key];\n })\n }\n\n resetSliceTimeReport () {\n this.sliceTimeReport = {\n total: 0,\n CPU: 0,\n webGL: 0,\n }\n }\n}\n\nSandbox.idCounter = 1;\nSandbox.debugWork = false;\nSandbox.debugState = false;\nSandbox.debugEvents = false;\n\nexports.Sandbox = Sandbox;\nexports.SandboxError = SandboxError;\nexports.NoProgressError = NoProgressError;\nexports.SliceTooSlowError = SliceTooSlowError;\nexports.UncaughtExceptionError = UncaughtExceptionError;\nexports.RemoteFetchError = RemoteFetchError;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/worker/sandbox.js?");
4418
4428
 
4419
4429
  /***/ }),
4420
4430
 
@@ -4446,7 +4456,7 @@ eval("/**\n * @file worker/supervisor-cache.js\n *\n * A cache for the superviso
4446
4456
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4447
4457
 
4448
4458
  "use strict";
4449
- 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, compressJobMap, 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 if (DCP_ENV.platform === 'nodejs' && this.options.localExec) /* interval helps keep normal worker alive forever, which we don't want in localexec */\n this.watchdogInterval.unref();\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 if (ceci.eventRouterMessageQueue.length)\n ceci.resendRejectedMessages(ceci.eventRouterConnection, ceci.eventRouterMessageQueue);\n }\n this.eventRouterMessageQueue = [];\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 if (ceci.resultSubmitterMessageQueue.length)\n ceci.resendRejectedMessages(ceci.resultSubmitterConnection, ceci.resultSubmitterMessageQueue);\n }\n this.resultSubmitterMessageQueue = [];\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 if (ceci.packageManagerMessageQueue.length)\n ceci.resendRejectedMessages(ceci.packageManagerConnection, ceci.packageManagerMessageQueue);\n }\n this.packageManagerMessageQueue = [];\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 /**\n * Try sending messages that were rejected on an old instance of the given connection.\n */\n async resendRejectedMessages(connection, messageQueue) {\n var message = messageQueue.shift();\n\n do {\n try {\n await connection.send(message.operation, message.data);\n } catch (e) {\n debugging('supervisor') && console.error(`Failed to send message ${message.operation} to scheduler: ${e}. Will try again on a new connection.`);\n messageQueue.unshift(message);\n connection.close();\n break;\n }\n message = messageQueue.shift();\n } while (message)\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('Error generating default identity, try to do it another way.');\n this.defaultIdentityKeystore = await new wallet.IdKeystore(null, '');\n }\n } finally {\n if (this.defaultIdentityKeystore)\n debugging('supervisor') && console.debug('Set default identity =', this.defaultIdentityKeystore.address);\n else\n debugging('supervisor') && console.debug('Failed to set default identity, worker cannot work.');\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 // XXXpfr: April 13, 2022\n // I'm trying to understand and control when sandboxes get removed.\n // A sandbox in this.readiedSandboxes should never have returnSandbox/removeSandbox called on it except in stopWork.\n // Because of races and random worker crashes, it is hard to get this right, but I want to try.\n // If I don't fix this is the next 30 days or I forget, please delete this exception.\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 ${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 {string}\n */\n 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 {string}\n */\n 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 let statusPayload = {\n worker: this.workerOpaqueId,\n slices: [{\n job: jobAddress,\n sliceNumber: sliceNumber,\n status: 'begin',\n authorizationMessage,\n }],\n }\n \n this.resultSubmitterConnection.send('status', statusPayload).catch((error) => {\n debugging('supervisor') && console.error(`Error sending 'status' for slice ${sliceNumber} of job ${jobAddress}:\\n ${error}\\nWill try again on a new connection`);\n this.resultSubmitterMessageQueue.push({ operation: 'status', data: statusPayload });\n this.resultSubmitterConnection.close();\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 let authorizationMessage;\n try {\n // Sometimes a sliceNumber===0 workEmit comes in before the client bundle is properly loaded.\n // Also happens with minor dcp-client version mismatches.\n authorizationMessage = sandbox.slice.getAuthorizationMessage();\n } catch(e) {\n authorizationMessage = null;\n }\n\n if (!authorizationMessage)\n {\n console.warn(`workEmit: missing authorization message for job ${jobAddress}, slice: ${sliceNumber}`);\n return Promise.resolve();\n }\n \n let workEmitPayload = {\n eventName,\n payload,\n job: jobAddress,\n slice: sliceNumber,\n worker: this.workerOpaqueId,\n authorizationMessage,\n }\n \n const workEmitPromise = this.eventRouterConnection.send('workEmit', workEmitPayload).catch(error => {\n debugging('supervisor') && console.warn(`workEmit: unable to send ${eventName} for slice ${sliceNumber} of job ${jobAddress}: ${error.message}.\\nTrying again on a new connection.`);\n this.eventRouterMessageQueue.push({ operation: 'workEmit', data: workEmitPayload })\n this.eventRouterConnection.close();\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 // XXXpfr: April 13, 2022\n // I'm trying to understand and control when sandboxes get terminated.\n // Because of races and random worker crashes, it is impossible to not try to terminate a sandbox more than once.\n // But at some places where returnSandbox is we shouldn't see this behavior, hence this exception.\n // If I don't fix this is the next 30 days or I forget, please delete this exception.\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 debugging('supervisor') && console.error('479: Failed to send status update:', error/*.message*/);\n this.resultSubmitterMessageQueue.push({ operation: 'status', data: progressReportPayload })\n this.resultSubmitterConnection.close();\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 *\n * @param {any[]} newJobs - Jobs that should not be removed from\n * `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 || [], // force array; 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).catch((error) => {\n debugging('supervisor') && console.error(`Unable to request task from scheduler: ${error}. Will try again on a new connection.`);\n this.taskDistributorConnection.close(error, true);\n throw error; /* caught below */\n });\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}, authSlices: ${compressJobMap(authorizationMessage.auth.authSlices, true /* skipFirst*/, this.addressTruncationLength /* digits*/)}`);\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 ${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 while (this.sandboxes.length) {\n this.returnSandbox(this.sandboxes[0], null, false);\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 debugging('supervisor') && console.error('Failed to return slice', {\n sliceNumber: slice.sliceNumber,\n jobAddress: slice.jobAddress,\n status: slice.status,\n error,\n }, 'Will try again on a new connection.');\n this.resultSubmitterMessageQueue.push({ operation: 'status', data: payload });\n this.resultSubmitterConnection.close();\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 ${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 debugging('supervisor') && console.error('Failed to return slice(s)', { errorInfo, error }, 'Will try again on new connection.');\n this.resultSubmitterMessageQueue.push({ operation: 'status', data: { worker: this.workerOpaqueId, slices: slicePayload } })\n this.resultSubmitterConnection.close();\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 let resp = await this.resultSubmitterConnection.send(\n 'result',\n payloadData,\n ).catch((error) => {\n debugging('supervisor') && console.error(`Failed to submit result to scheduler for slice ${payloadData.slice} of job ${payloadData.job}:\\n ${error} \\nWill try again on new connection.`);\n this.resultSubmitterMessageQueue.push({ operation: 'result', data: payloadData });\n this.resultSubmitterConnection.close();\n error.handled = true;\n throw error; /* Caught in catch block below */\n });\n \n if (!resp.success)\n throw resp.payload;\n\n if (false) {}\n\n const receipt = {\n accepted: true,\n payment: resp.payload.slicePaymentAmount,\n };\n this.emit('submittedResult', resp.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 await this.returnSlice(slice);\n }\n } catch(error) {\n !error.handled && 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/**\n * When Supervisor.sliceTiming is set to be true, it displays the timings of a every slice\n * slice['pairingDelta'] = timespan of when slice is paired with sandbox until execution starts\n * slice['executionDelta'] = timespan of execution in sandbox\n * slice['resultDelta'] = timespan of when sandbox finishes executing until recordResult completes.\n * @type {boolean}\n */\nSupervisor.sliceTiming = false;\n\nexports.Supervisor = Supervisor;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/worker/supervisor.js?");
4459
+ 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, compressJobMap, 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 if (DCP_ENV.platform === 'nodejs' && this.options.localExec) /* interval helps keep normal worker alive forever, which we don't want in localexec */\n this.watchdogInterval.unref();\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 if (ceci.eventRouterMessageQueue.length)\n ceci.resendRejectedMessages(ceci.eventRouterConnection, ceci.eventRouterMessageQueue);\n }\n this.eventRouterMessageQueue = [];\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 if (ceci.resultSubmitterMessageQueue.length)\n ceci.resendRejectedMessages(ceci.resultSubmitterConnection, ceci.resultSubmitterMessageQueue);\n }\n this.resultSubmitterMessageQueue = [];\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 if (ceci.packageManagerMessageQueue.length)\n ceci.resendRejectedMessages(ceci.packageManagerConnection, ceci.packageManagerMessageQueue);\n }\n this.packageManagerMessageQueue = [];\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 /**\n * Try sending messages that were rejected on an old instance of the given connection.\n */\n async resendRejectedMessages(connection, messageQueue) {\n var message = messageQueue.shift();\n\n do {\n try {\n await connection.send(message.operation, message.data);\n } catch (e) {\n debugging('supervisor') && console.error(`Failed to send message ${message.operation} to scheduler: ${e}. Will try again on a new connection.`);\n messageQueue.unshift(message);\n connection.close();\n break;\n }\n message = messageQueue.shift();\n } while (message)\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('Error generating default identity, try to do it another way.');\n this.defaultIdentityKeystore = await new wallet.IdKeystore(null, '');\n }\n } finally {\n if (this.defaultIdentityKeystore)\n debugging('supervisor') && console.debug('Set default identity =', this.defaultIdentityKeystore.address);\n else\n debugging('supervisor') && console.debug('Failed to set default identity, worker cannot work.');\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 // XXXpfr: April 13, 2022\n // I'm trying to understand and control when sandboxes get removed.\n // A sandbox in this.readiedSandboxes should never have returnSandbox/removeSandbox called on it except in stopWork.\n // Because of races and random worker crashes, it is hard to get this right, but I want to try.\n // If I don't fix this is the next 30 days or I forget, please delete this exception.\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 ${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 {string}\n */\n 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 {string}\n */\n 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 let statusPayload = {\n worker: this.workerOpaqueId,\n slices: [{\n job: jobAddress,\n sliceNumber: sliceNumber,\n status: 'begin',\n authorizationMessage,\n }],\n }\n \n try /* resultSubmitterConnection can be null if worker is stopped */\n {\n this.resultSubmitterConnection.send('status', statusPayload).catch((error) => {\n debugging('supervisor') && console.error(`Error sending 'status' for slice ${sliceNumber} of job ${jobAddress}:\\n ${error}\\nWill try again on a new connection`);\n this.resultSubmitterMessageQueue.push({ operation: 'status', data: statusPayload });\n this.resultSubmitterConnection.close();\n });\n } catch (error)\n {\n debugging('supervisor') && console.error(`Failed to send 'status' for slice ${sliceNumber} of job ${jobAddress}:, no connection to result submitter:`, error);\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 let authorizationMessage;\n try {\n // Sometimes a sliceNumber===0 workEmit comes in before the client bundle is properly loaded.\n // Also happens with minor dcp-client version mismatches.\n authorizationMessage = sandbox.slice.getAuthorizationMessage();\n } catch(e) {\n authorizationMessage = null;\n }\n\n if (!authorizationMessage)\n {\n console.warn(`workEmit: missing authorization message for job ${jobAddress}, slice: ${sliceNumber}`);\n return Promise.resolve();\n }\n \n let workEmitPayload = {\n eventName,\n payload,\n job: jobAddress,\n slice: sliceNumber,\n worker: this.workerOpaqueId,\n authorizationMessage,\n }\n \n const workEmitPromise = this.eventRouterConnection.send('workEmit', workEmitPayload).catch(error => {\n debugging('supervisor') && console.warn(`workEmit: unable to send ${eventName} for slice ${sliceNumber} of job ${jobAddress}: ${error.message}.\\nTrying again on a new connection.`);\n this.eventRouterMessageQueue.push({ operation: 'workEmit', data: workEmitPayload })\n this.eventRouterConnection.close();\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 // XXXpfr: April 13, 2022\n // I'm trying to understand and control when sandboxes get terminated.\n // Because of races and random worker crashes, it is impossible to not try to terminate a sandbox more than once.\n // But at some places where returnSandbox is we shouldn't see this behavior, hence this exception.\n // If I don't fix this is the next 30 days or I forget, please delete this exception.\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 debugging('supervisor') && console.error('479: Failed to send status update:', error/*.message*/);\n this.resultSubmitterMessageQueue.push({ operation: 'status', data: progressReportPayload })\n this.resultSubmitterConnection.close();\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 *\n * @param {any[]} newJobs - Jobs that should not be removed from\n * `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 || [], // force array; 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) && (!requestPayload.localExec))\n requestPayload.workerComputeGroups.push(constants.computeGroups.public);\n debugging('computeGroups') && console.log(`Fetching work for ${requestPayload.workerComputeGroups.length} ComputeGroups: `, requestPayload.workerComputeGroups);\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).catch((error) => {\n debugging('supervisor') && console.error(`Unable to request task from scheduler: ${error}. Will try again on a new connection.`);\n this.taskDistributorConnection.close(error, true);\n throw error; /* caught below */\n });\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}, authSlices: ${compressJobMap(authorizationMessage.auth.authSlices, true /* skipFirst*/, this.addressTruncationLength /* digits*/)}`);\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 ${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 // 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 {\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 //\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 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 // 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 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 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 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 while (this.sandboxes.length) {\n this.returnSandbox(this.sandboxes[0], null, false);\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 try\n {\n return this.resultSubmitterConnection.send('status', payload) /* resultSubmitterConnection can be null if worker is stopped */\n .then(response => {\n return response;\n }).catch(error => {\n debugging('supervisor') && console.error('Failed to return slice', {\n sliceNumber: slice.sliceNumber,\n jobAddress: slice.jobAddress,\n status: slice.status,\n error,\n }, 'Will try again on a new connection.');\n this.resultSubmitterMessageQueue.push({ operation: 'status', data: payload });\n this.resultSubmitterConnection.close();\n });\n }\n catch (error)\n {\n debugging('supervisor') && console.error(`Failed to return slice ${slice.identifier}, no connection to result submitter:`, error);\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 ${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 debugging('supervisor') && console.error('Failed to return slice(s)', { errorInfo, error }, 'Will try again on new connection.');\n this.resultSubmitterMessageQueue.push({ operation: 'status', data: { worker: this.workerOpaqueId, slices: slicePayload } })\n this.resultSubmitterConnection.close();\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, total: 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.total = 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 let resp = await this.resultSubmitterConnection.send(\n 'result',\n payloadData,\n ).catch((error) => {\n debugging('supervisor') && console.error(`Failed to submit result to scheduler for slice ${payloadData.slice} of job ${payloadData.job}:\\n ${error} \\nWill try again on new connection.`);\n this.resultSubmitterMessageQueue.push({ operation: 'result', data: payloadData });\n this.resultSubmitterConnection.close();\n error.handled = true;\n throw error; /* Caught in catch block below */\n });\n \n if (!resp.success)\n throw resp.payload;\n\n if (false) {}\n\n const receipt = {\n accepted: true,\n payment: resp.payload.slicePaymentAmount,\n };\n this.emit('submittedResult', resp.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 await this.returnSlice(slice);\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/**\n * When Supervisor.sliceTiming is set to be true, it displays the timings of a every slice\n * slice['pairingDelta'] = timespan of when slice is paired with sandbox until execution starts\n * slice['executionDelta'] = timespan of execution in sandbox\n * slice['resultDelta'] = timespan of when sandbox finishes executing until recordResult completes.\n * @type {boolean}\n */\nSupervisor.sliceTiming = false;\n\nexports.Supervisor = Supervisor;\n\n\n//# sourceURL=webpack://dcp/./src/dcp-client/worker/supervisor.js?");
4450
4460
 
4451
4461
  /***/ }),
4452
4462
 
@@ -4489,7 +4499,7 @@ eval("/* provided dependency */ var Buffer = __webpack_require__(/*! ./node_modu
4489
4499
  /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
4490
4500
 
4491
4501
  "use strict";
4492
- eval("/**\n * @file protocol/connection/ack.js\n * @author KC Erb, kcerb@kingsds.network\n * @date January 2021\n *\n * The ConnectionAck is a factory for an ACK message class that is bound\n * to a connection instance.\n * \n * The ACK message is a type of message that bears the minimal information needed\n * to confirm to the recipient that the sender has received a message/batch.\n * \n * ACKs are a little different depending on weather they are being sent or received.\n * When sent, an ack is acknowledging a message that has been received.\n * When received, an ack corresponds to a message that the receiver has sent.\n * \n * This asymmetry is reflected in the constructor.\n */\n\n\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nlet nanoid;\nif (DCP_ENV.platform === 'nodejs') {\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n nanoid = requireNative('nanoid').nanoid;\n} else {\n nanoid = (__webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\").nanoid);\n}\nlet nonceSerialId = 0;\nconst ConnectionAck = (ConnectionMessage) => class Ack extends ConnectionMessage {\n /**\n * Create an AckMessage using details from the messaging we are ACKing.\n * or\n * Rehydrate an AckMessage from JSON.\n * @param {Message|object} messageOrAckJSON The message we are acknowledging receipt of or JSON.\n */\n constructor(messageOrAckJSON) {\n super();\n if (messageOrAckJSON.body) {\n let message = messageOrAckJSON;\n this.token = message.body.ackToken || message.body.token;\n this.messageId = message.body.id;\n this.type = 'ack';\n this.id = this.connection.messageFactory.generateMessageId();\n this.nonce = `${this.connection.id}-${nonceSerialId++}-${nanoid()}`;\n } else {\n let ackJSON = messageOrAckJSON;\n this.token = ackJSON.token;\n this.messageId = ackJSON.messageId;\n this.type = ackJSON.type;\n this.id = ackJSON.id;\n this.nonce = ackJSON.nonce;\n }\n }\n\n /**\n * This method is used to dehydrate the ack into a plain object before it is sent to the peer.\n * @returns {object} - An object that will be sent as-is to the peer.\n */\n toJSON() {\n const obj = super.toJSON();\n obj.id = this.id;\n obj.messageId = this.messageId;\n obj.token = this.token;\n obj.type = this.type;\n\n return obj;\n }\n}\n\nmodule.exports.ConnectionAck = ConnectionAck;\n\n\n//# sourceURL=webpack://dcp/./src/protocol-v4/connection/ack.js?");
4502
+ eval("/**\n * @file protocol/connection/ack.js\n * @author KC Erb, kcerb@kingsds.network\n * @date January 2021\n *\n * The ConnectionAck is a factory for an ACK message class that is bound\n * to a connection instance.\n * \n * The ACK message is a type of message that bears the minimal information needed\n * to confirm to the recipient that the sender has received a message/batch.\n * \n * ACKs are a little different depending on weather they are being sent or received.\n * When sent, an ack is acknowledging a message that has been received.\n * When received, an ack corresponds to a message that the receiver has sent.\n * \n * This asymmetry is reflected in the constructor.\n */\n\n\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nlet nanoid;\nif (DCP_ENV.platform === 'nodejs') {\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n nanoid = requireNative('nanoid').nanoid;\n} else {\n nanoid = (__webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\").nanoid);\n}\nlet nonceSerialId = 0;\nconst ConnectionAck = (ConnectionMessage) => class Ack extends ConnectionMessage {\n /**\n * Create an AckMessage using details from the messaging we are ACKing.\n * or\n * Rehydrate an AckMessage from JSON.\n * @param {Message|object} messageOrAckJSON The message we are acknowledging receipt of or JSON.\n */\n constructor(messageOrAckJSON) {\n super();\n if (messageOrAckJSON.body) {\n let message = messageOrAckJSON;\n this.token = message.body.ackToken || message.body.token;\n this.messageId = message.body.id;\n this.type = 'ack';\n this.id = this.connection.generateMessageId();\n this.nonce = `${this.connection.id}-${nonceSerialId++}-${nanoid()}`;\n } else {\n let ackJSON = messageOrAckJSON;\n this.token = ackJSON.token;\n this.messageId = ackJSON.messageId;\n this.type = ackJSON.type;\n this.id = ackJSON.id;\n this.nonce = ackJSON.nonce;\n }\n }\n\n /**\n * This method is used to dehydrate the ack into a plain object before it is sent to the peer.\n * @returns {object} - An object that will be sent as-is to the peer.\n */\n toJSON() {\n const obj = super.toJSON();\n obj.id = this.id;\n obj.messageId = this.messageId;\n obj.token = this.token;\n obj.type = this.type;\n\n return obj;\n }\n}\n\nmodule.exports.ConnectionAck = ConnectionAck;\n\n\n//# sourceURL=webpack://dcp/./src/protocol-v4/connection/ack.js?");
4493
4503
 
4494
4504
  /***/ }),
4495
4505
 
@@ -4500,7 +4510,7 @@ eval("/**\n * @file protocol/connection/ack.js\n * @author KC Erb, k
4500
4510
  /***/ ((module) => {
4501
4511
 
4502
4512
  "use strict";
4503
- 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?");
4513
+ 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.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?");
4504
4514
 
4505
4515
  /***/ }),
4506
4516
 
@@ -4520,7 +4530,7 @@ eval("/**\n * @file connection-constants.js\n * Constants fo
4520
4530
  \**********************************************************/
4521
4531
  /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
4522
4532
 
4523
- 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;\n await identity.unlock(null, this.connection.options.identityUnlockTimeout, 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?");
4533
+ 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\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;\n await identity.unlock(null, this.connection.options.identityUnlockTimeout, 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?");
4524
4534
 
4525
4535
  /***/ }),
4526
4536
 
@@ -4531,7 +4541,7 @@ eval("/**\n * @file protocol/connection/message.js\n * @author Ryan
4531
4541
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4532
4542
 
4533
4543
  "use strict";
4534
- eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_modules/process/browser.js */ \"./node_modules/process/browser.js\");\n/**\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 * Connection instance events:\n * - session: dcpsid new session established\n * - connect: url UI hint: internet available\n * - disconnect: url UI hint: internet not available\n * - readyStateChange: *** DO NOT USE **\n * - error: error emitted when errors happen that would otherwise go uncaught\n * - close: connection instance is closing\n * - end: Connection instance is closed\n * - send: msgObj when a message is sent to the peer; does not wait for ack; may re-trigger on reconnect\n * - ready: when the connection is ready for traffic (constructor promises resolved)\n *\n * State Transition Diagram for Connection.state:\n *\n * initial connecting established disconnected close-wait closing closed\n * ===========================================================================================================================\n * |-- i:connect ---->\n * |-- t:accept ----->\n * |-- t:establishTarget -->\n * |-- i:connect ---------->\n * |-- transportDisconnectHandler -->\n * <-- i:_reconnect ----------------------------------------|\n * |-i:useNewTransport-->\n * <-- t:useNewTransport --------|\n * |-- closeWait ----------------------------------------------------------->\n * |-- closeWait ----------------------------------->\n * |-- closeWait -->\n * |-- doClose --------------->\n * |-- close ------------------------------------------------------------------------------------------------------------> \n * |-- close ---------------------------------------------------------------------------->\n * |-- close ---------------------------------------------------->\n * |-- close ------------------->\n * |-- doClose -->\n *\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 IN CLOSING, CLOSE-WAIT OR CLOSED\n * DCPC-1002 - MESSAGE OWNER IS INVALID\n * DCPC-1003 - MESSAGE SIGNATURE INVALID \n * DCPC-1004 - MESSAGE BODY IS INVALID\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 - TRIED TO INITIATE CONNECTION AFTER SESSION ALREADY ESTABLISHED\n * DCPC-1013 - DEFAULT ERROR CODE WHEN CLOSING WITH REASON THATS NOT INSTANCE OF DCPERROR\n * DCPC-1014 - NO TRANSPORTS AVAILABLE\n * DCPC-1015 - CANNOT CONNECT WHEN CONNECTION ALREADY CLOSED\n * DCPC-1016 - ERROR CONNECTING VIA AVAILABLE TRANSPORTS\n * DCPC-1017 - FIRST PROTOCOL MESSAGE WAS DID NOT INVOLVE INITIAL CONNECT REQUEST\n */\n\n\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('dcp');\nconst dcpEnv = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\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 { 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 * Connection form 2:\n * @constructor\n * @param {object} [target]\n * @param {Promise} idKsPromise A promise which resolves to the identity keystore described\n * in form 1\n * @param {object} [options]\n * @see form 1\n */\n /**\n * Connection form 1\n * Create a DCP Connection object. This object could represent either the initiator or \n * target end of a connection, until it is specialized by either invoke the connect()\n * or accept() methods. Note that send() invokes connect() internally if not in an established\n * state.\n * @constructor\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} target Object (usually a dcpConfig fragment) describing the target.\n * This object may contain the following properties; 'location' is\n * mandatory:\n * - location: a URL or 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 one will\n * be chosen by examining IP addresses (assuming an IP bearer).\n * - identity: an object with an address property which is a promise\n * that resolves to an instance of wallet.Address which represents\n * to the target's identity; this overrides the initiator's \n * identity cache unless options.strict is truey.\n * \n * @param {Keystore} [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} [options] 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 * @returns instance of Connection that is specific to a target URL but not a role\n */\n constructor(target, idKeystore, options = {})\n {\n super('Connection');\n this.id = ++globalConnectionId;\n this.debugLabel = `connection(g:${this.id}):`;\n\n /* polymorphism strategy: rewrite to (configFragment, idksPromise, options) */\n if (target instanceof DcpURL)\n target = { location: target };\n else if (DcpURL.isURL(target))\n target = { location: new DcpURL(target) };\n else if (target instanceof String || typeof target === 'string')\n target = { location: new DcpURL(target) };\n assert(typeof target === 'object', target.location);\n\n /* idKeystore is always resolved by the time a session is established. */\n if (!idKeystore)\n this.identityPromise = wallet.getId();\n else if (idKeystore instanceof Promise)\n this.identityPromise = idKeystore;\n else if (idKeystore instanceof wallet.Keystore)\n this.identityPromise = Promise.resolve(idKeystore); \n else if (idKeystore instanceof wallet.Address)\n this.identityPromise = Promise.resolve(new wallet.Keystore(idKeystore, '')); \n\n this.identityPromise.then((keystore) => {\n /* This always happens by the time a role is assumed */\n delete this.identityPromise;\n this.identity = keystore;\n this.emit('ready');\n debugging('connection') && console.debug(this.debugLabel, 'identity is', this.identity.address);\n });\n\n this.target = target;\n this.url = this.target.location;\n \n // Init internal state / vars\n this.state = new Synchronizer(CONNECTION_STATES[0], CONNECTION_STATES);\n // DO NOT USE this.state.on('change', (s) => this.emit('readyStateChange', s) );\n\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 this.preDisconnectState = 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.receiver = new Receiver(this);\n this.sender = new Sender(this);\n \n debugging('connection') && console.debug(this.debugLabel, `new connection#${this.id}; ${this.url}`);\n\n /* Create a connection config as this.options which takes into\n * account system defaults and overrides for specific urls, origins, etc.\n */\n this.options = leafMerge(\n ({ /* hardcoded defaults insulate us from missing web config */\n 'connectTimeout': 90,\n 'lingerTimeout': 1800,\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 options\n );\n\n /* draw out errors quickly in dev */\n if ((process.env.DCP_NETWORK_CONFIG_BUILD || dcpConfig.build) === 'debug')\n {\n this.options.maxMessagesPerBatch /= 10;\n \n /* short timeouts and debuggers don't get along well */\n if (dcpEnv.platform === 'nodejs' && !(requireNative('module')._cache.niim instanceof requireNative('module').Module))\n {\n this.options.connectTimeout /= 10;\n this.options.lingerTimeout /= 20;\n this.options.identityUnlockTimeout /= 10;\n }\n }\n\n assert(this.options.identityUnlockTimeout > 0);\n assert(this.options.connectTimeout > 0);\n assert(this.options.lingerTimeout > 0);\n assert(typeof this.options.ttl.min === 'number');\n assert(typeof this.options.ttl.max === 'number');\n assert(typeof this.options.ttl.default === 'number');\n \n this.backoffTimeIterator = makeEBOIterator(500, dcpConfig.build === 'debug' ? 5000 : 60000); /** XXXwg make this configurable */\n\n this.secureLocation = determineIfSecureLocation(this);\n this.loggableDest = '<generic>';\n }\n\n /**\n * Specialize an instance of Connection for either initiator or target behaviour. Once specialized,\n * the role cannot be changed. This happens based on methods invoked; connect() or accept() cause\n * the change.\n *\n * This specialization also implies that the connection is fully ready for use, including resolution\n * of the identity promise if necessary. This is perhaps not the best place to ensure that, but it\n * provides a reliable - and already async - waypoint to observe that event.\n */\n async a$assumeRole(myRole)\n {\n assert(myRole === role.initiator || myRole === role.target);\n\n if (this.role === myRole)\n return;\n this.role = myRole;\n \n if (this.role === role.target)\n {\n this.debugLabel = `connection(t:${this.id}):`;\n this.sender.debugLabel = `sender(t#${this.id}):`;\n this.messageLedger.debugLabel = `message-ledger(t#${this.id}):`;\n this.loggableDest = '<target>';\n this.hasNtp = true;\n }\n else\n {\n this.debugLabel = `connection(i:${this.id}):`;\n this.sender.debugLabel = `sender(i#${this.id}):`;\n this.messageLedger.debugLabel = `message-ledger(i#${this.id}):`;\n this.loggableDest = this.url.href;\n this.hasNtp = false;\n }\n\n debugging('connection') && console.debug(this.debugLabel, `connection #${this.id} is ${this.role} for ${this.url}`);\n if (!this.identity)\n {\n assert(this.identityPromise);\n debugging('connection') && console.debug(this.debugLabel, `waiting for identity resolution`);\n await this.identityPromise;\n }\n }\n\n /**\n * API to establish a DCP connection. Implied by send().\n *\n * When invoked by the initiator, this method establishes the connection by connecting\n * to the target url provided to the constructor.\n */\n async connect() // eslint-disable-line require-await\n {\n if (this.role == role.target)\n return;\n \n if (!this.role)\n await this.a$assumeRole(role.initiator);\n \n if (this.state.is('initial'))\n {\n if (!this.connectPromise)\n this.connectPromise = this.a$_connect().then(() => delete this.connectPromise);\n return this.connectPromise;\n }\n\n if (this.state.is('disconnected'))\n {\n if (!this.connectPromise)\n this.connectPromise = this.a$_reconnect().then(() => delete this.connectPromise);\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-1015');\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 var transport;\n\n this.state.testAndSet('disconnected', 'connecting');\n try\n {\n do\n {\n transport = await this.a$connectToTargetTransport();\n } while (!transport && (this.transport && !this.transport.ready()) && !this.state.in(['closed', 'close-wait', 'closing']));\n\n debugging('connection') && console.debug(this.debugLabel, `reconnected via transport ${transport.socket.id}`);\n \n this.useNewTransport(transport);\n }\n catch (error)\n {\n if (error.code !== 'DCPC-1016' && error.code !== 'DCPC-1015')\n {\n /* Unreached unless there are bugs. */\n throw error;\n } \n this.close(error, true);\n }\n }\n\n async a$_connect()\n {\n const connectTimeout = setTimeout(() => { this.close(new DCPError('Connection could not be established within thirty seconds', 'DCPC-1006'), true) }, 30000)\n \n if (dcpEnv.platform === 'nodejs')\n connectTimeout.unref()\n \n var presharedPeerAddress, establishResults;\n var targetIdentity = await this.target.identity;\n var transport;\n \n assert(this.role === role.initiator);\n\n this.state.set('initial', 'connecting');\n do\n {\n transport = await this.a$connectToTargetTransport().catch((error) =>\n {\n debugging('connection') && console.debug(`${this.debugLabel} error connecting to target on transport layer:`, error);\n return { ready: () => {return false} };\n });\n } while(!transport.ready());\n this.adopt(transport);\n\n 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 peerAddress = new wallet.Address(establishResults.peerAddress);\n const dcpsid = establishResults.dcpsid;\n debugging('connection') && console.debug(this.debugLabel, 'dcpsid is', dcpsid);\n \n if (!this.options.strict && targetIdentity && determineIfSecureConfig())\n {\n if ( false\n || typeof targetIdentity !== 'object'\n || typeof targetIdentity.address !== 'object'\n || !(targetIdentity.address instanceof wallet.Address))\n targetIdentity = { address: new wallet.Address(targetIdentity) }; /* map strings and Addresses to ks ducks */\n\n presharedPeerAddress = targetIdentity.address;\n debugging('connection') && console.debug(this.debugLabel, 'Using preshared peer address', presharedPeerAddress);\n }\n this.ensureIdentity(peerAddress, presharedPeerAddress);\n\n /* At this point, new session is valid & security checks out - make Connection instance usable */\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-1012');\n\n this.state.set('connecting', 'established'); /* established => dcpsid has been set */\n clearTimeout(connectTimeout);\n this.emit('session', (this.dcpsid = dcpsid));\n this.emit('connect', 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 && dcpEnv.platform === 'nodejs')\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 * @param {object} transport the transport instance that triggered this handler. In some cases, it\n * is possible that this event is not serviced until after the connection\n * has already acquired a new transport instance, e.g. in a Target where\n * the initiator switched networks. This implies that it is possible for\n * more 'connect' events to be emitted than 'disconnect' events.\n */\n transportDisconnectHandler(transport)\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 if (transport !== this.transport) /* event no longer relevant */\n return;\n\n if (this.transport)\n {\n transport.close();\n delete this.transport;\n }\n \n if (this.state.is('established'))\n {\n this.state.set('established', 'disconnected');\n this.emit('disconnect', this.url); /* 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) /* hopefully impossible? */\n {\n debugging('connection') && console.debug(this.debugLabel, 'Not reconnecting - no session');\n return;\n }\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.dcpsid && !this.sender.inFlight && this.options.onDemand)\n {\n debugging('connection') && console.debug(this.debugLabel, `Not reconnecting ${this.dcpsid} until next message`);\n return;\n }\n \n if (this.state.is('connecting') && (!this.dcpsid || !this.peerAddress))\n {\n debugging('connection') && console.debug(this.debugLabel, `Disconnected while connecting, establishing transport and re-sending connect request.`);\n this.a$_reconnect();\n return;\n }\n\n /* At this point, we initiate a (re)connect attempt because either\n * - we haven't connected yet,\n * - we have something to send, or\n * - we are not an on-demand connection\n */\n if (!this.state.is('connecting'))\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' && error.code !== 'DCPC-1015')\n {\n /* Unreached unless there are bugs. */\n throw error;\n }\n }\n }\n \n /**\n * Initiators only\n *\n * Connect to a target at the transport level.\n * - Rejects when we give up on all transports.\n * - Resolves with a transport instance when we connect to one.\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$connectToTargetTransport()\n {\n const that = this;\n const availableTransports = [].concat(this.options.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 // Already trying to connect to target, don't try multiple times until we've aborted one attempt\n if (this.connectAbortTimer)\n return;\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.options.connectTimeout * 1000);\n\n if (this.options.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. */\n /* Resolves with { bool success, obj transport } or rejects with { error } if the transport cannot connect*/\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.target, that.options[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.debug(`Error connecting to ${that.url};`, error);\n connectWithTransport_resolve(ret);\n });\n \n /* let the connectAbortTimer interrupt this connect attempt */\n transportConnectIntr = () => { transport.close() };\n });\n }\n \n if (availableTransports.length === 0)\n {\n cleanup_ctt();\n return Promise.reject(new DCPError('no transports defined', 'DCPC-1014'));\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 /* Have connected to the remote at the transport level - OUT */\n if (success === true)\n {\n transportConnectIntr = false;\n cleanup_ctt();\n \n return transport;\n }\n\n /* Fast-fail: certain - but few - HTTP status codes let us know that this (or any) transport\n * will never work, so don't try those again.\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.`);\n quitMsg = error.message;\n quitCode = 'HTTP_' + error.httpStatus || 0;\n break;\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 (impossibleError)\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 * Method which must be invoked whenever a new transport needs to be assigned to the connection. \n * If we have previously adopted a transport, we close it first, which will prevent \n * [by transport definition] any message handlers from firing, even if the old transport instance\n * has buffered traffic.\n *\n * @param {object} transport transport instance\n */\n adopt(transport)\n {\n if (this.transport)\n this.transport.close();\n\n transport.on('message', (m) => this.handleMessage(m));\n transport.on('end', () => this.transportDisconnectHandler(transport));\n transport.on('close', () => this.transportDisconnectHandler(transport));\n \n this.transport = transport;\n }\n \n \n /**\n * Method that gets invoked when there is a new transport available for adoption.\n * This will adjust the state of the connection, adopt the transport then tell\n * the sender to pump the message queue.\n * @param {object} transport transport instance \n */\n useNewTransport(transport)\n {\n if (this.state.in(['closing', 'close-wait', 'closed']))\n {\n debugging('connection') && console.debug(`${this.debugLabel} got a new transport during closing. closing the new transport and not completing transport adoption.`)\n transport.close();\n return;\n }\n \n var preDisconnectState = (!this.dcpsid || !this.peerAddress) ? 'connecting' : 'established';\n if (this.state.isNot(preDisconnectState))\n this.state.set(['connecting', 'disconnected'], preDisconnectState);\n this.adopt(transport);\n this.emit('connect', this.url); // UI hint: \"internet available\" \n this.sender.notifyTransportReady();\n }\n \n /**\n * Method that must be invoked by the target to \"accept\" a new DCP Connection request\n * at the transport layer.\n * @param {object} transport \n */\n async accept(transport)\n {\n assert(!this.role);\n await this.a$assumeRole(role.target);\n this.state.set('initial', 'connecting');\n this.adopt(transport);\n }\n \n /**\n * This method is invoked by the target when it has handled the initial connect request from\n * the initiator, which contains the peerAddress and the first half of the dcpsid (the second half is\n * populated by receiver.handleFirstRequest before being passed here). It transitions the connection \n * into an established state at the protocol level.\n * Note - this is really not the right design for this, but it is invoked from handleFirstRequest\n * in ./receiver.js\n *\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('dummy'); // 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.emit('session', (this.dcpsid = dcpsid));\n debugging() && console.debug(this.debugLabel, 'dcpsid is', dcpsid);\n\n this.loggableDest = this.role === role.initiator ? this.url : peerAddress;\n this.state.set('connecting', 'established'); /* established => dcpsid has been set */\n\n debugging('connection') && console.debug(this.debugLabel, `Established session ${this.dcpsid} with ${this.peerAddress} for ${this.url}`);\n }\n\n /**\n * Check to see if the peer address conflicts with what we have in the global identity cache;\n * it does, throw an exception.\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 /**\n * This method uses the first request (if we're target) or ack to the first request \n * (if we're initiator) to memoize the address of the peer authorized to send to us. \n * The first message must only be a request (since it comes from sender.specialFirstSend)\n * that has a connect operation, or an acknowledgement of the first request. \n * All future messages' owners will be validated against this authorized sender.\n * @param {Object} message \n */\n setAuthorizedSender(message)\n {\n if (message.body.type !== 'request' && message.body.type !== 'ack')\n throw new DCPError('First protocol message was not a request or ack', 'DCPC-1017');\n \n if (message.body.type === 'request' && message.body.payload.operation !== 'connect')\n throw new DCPError('First protocol message did not contain the correct payload', 'DCPC-1017');\n \n if (message.body.type === 'ack' && this.sender.inFlight.message.payload.operation !== 'connect')\n throw new DCPError('First protocol acknowledgement was not for connect request', 'DCPC-1017');\n \n this.authorizedSender = message.owner;\n }\n \n /**\n * Emits an error event with the relevant error and closes the connection immediately.\n * @param {string} errorMessage \n * @param {string} errorCode \n */\n \n handleValidationError(errorMessage, errorCode)\n {\n var messageError;\n \n debugging('connection') && console.debug(this.debugLabel, 'Message failed validation -', errorMessage);\n this.emit('error', (messageError = new DCPError(`message failed validation: ${errorMessage}`, errorCode)))\n this.close(messageError, true);\n }\n\n /**\n * This method validates the message owner, signature and body before passing it onto\n * either the receiver (for a request, response or batch) or the messageLedger (for an ack).\n * If it's a request, response or batch, this method also provokes the connection to \n * send an acknowledgement (ack) to the peer to let them know we got their message.\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 * XXXsc did some auditing. we happy now?\n * @param {string} JSON-encoded unvalidated message object\n */\n async handleMessage (messageJSON) {\n var validation;\n var message;\n\n if (this.state.is('closed')) {\n debugging('connection') && console.debug(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 debugging('wire') && console.debug(this.debugLabel, `handleMessage: ${String(message && message.body && message.body.type).padEnd(10, ' ')} <- ${this.loggableDest}`);\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:', message.body);\n debugging('wire') && console.debug(this.debugLabel, `dup message ack: ${String(message.body.type).padEnd(10, ' ')} -> ${this.loggableDest}`);\n\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 validation = this.validateMessageOwner(message)\n if (validation.success !== true)\n {\n this.handleValidationError(validation.errorMessage, 'DCPC-1002');\n return;\n }\n \n validation = this.validateMessageSignature(message);\n if (validation.success !== true)\n {\n this.handleValidationError(validation.errorMessage, 'DCPC-1003');\n return;\n }\n\n validation = this.validateMessageBody(message);\n if (validation.success !== true)\n {\n this.handleValidationError(validation.errorMessage, validation.errorCode || 'DCPC-1004'); /* messages of type 'unhandled-message' may contain more information about the failure */\n return;\n }\n \n if (message.body.type === \"ack\") {\n const ack = new this.Ack(message.body);\n this.messageLedger.handleAck(ack);\n return;\n } else if (message.body.type !== 'unhandled-message') {\n this.lastMessage = message;\n await this.ackMessage(message);\n }\n \n this.receiver.handleMessage(message);\n }\n\n \n /**\n * This method takes either a Request, Response or Batch, creates an ack for it\n * and sends it to the peer. This ack contains the nonce we expect on the next\n * message from peer.\n * @param {Connection.Message} message \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\n debugging('wire') && console.debug(this.debugLabel, `ackMessage: ${String(message.body.type).padEnd(10, ' ')} -> ${this.loggableDest}`);\n\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 && this.lastMessage.body.nonce === messageJSON.body.nonce;\n }\n\n /**\n * Validate that the message came from the appropriate sender.\n * @param {Object} message the message to validate\n * @returns {Object} returns an object `ret` with either ret.success = true, \n * or ret.success = false accompanied by another property ret.errorMessage\n */\n validateMessageOwner(message)\n {\n if (!this.authorizedSender)\n {\n /* Capture the initial identity of the remote end during the connect operation */\n this.setAuthorizedSender(message);\n return { success: true }\n }\n else if (message.owner !== this.authorizedSender)\n {\n return { success: false, errorMessage: \"message came from unauthorized sender\" }\n }\n return { success: true }\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 validateMessageSignature(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 validateMessageBody(message)\n {\n try\n {\n if (message.body.type === 'unhandled-message')\n {\n /* This special message type may not have a dcpsid, peerAddress, etc., so it might not\n * validate. It is also not a \"real\" message and only used to report ConnectionManager routing \n * errors, so we just report here, drop it, and close the connection.\n *\n * Note also that this is probably the wrong way to handle this case - restarting daemons - but\n * that is a problem for another day. /wg nov 2021\n */\n debugging('connection') && console.warn(this.debugLabel, \"Target Error - target could not process message.\", JSON.stringify(message.body),\n \"Aborting connection.\");\n return { success: false, errorMessage: `target could not process message (${message.body.payload && message.body.payload.message || 'unknown error'})`, errorCode: message.body.payload && message.body.payload.code}\n }\n if (this.peerAddress && !this.peerAddress.eq(message.owner))\n {\n debugging('connection') && console.warn(this.debugLabel,\n \"Received message's owner address does not match peer address, aborting connection\\n\",\n \"(owner addr)\", message.owner, '\\n',\n \"(peer addr)\", this.peerAddress);\n return { success: false, errorMessage: \"received message owner 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(this.debugLabel,\n \"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 /* can get close in middle of connecting, which will have no nonce.*/\n if (body.type !== 'ack' && this.lastAck.nonce !== body.nonce && (body.payload && body.payload.operation !== 'close'))\n {\n /* When Target sends back ConnectionLessErrorResponse, it uses the nonce of the message that caused an error. */\n if (this.sender.inFlight && this.sender.inFlight.message.nonce === body.nonce)\n {\n debugging('connection') && console.debug(`${this.debugLabel} Received messages nonce matches nonce of our current inFlight message.`,\n \"There was a problem sending this message. Aborting connection. Reason:\\n\", body.payload);\n return { success: false, errorMessage: \"current inflight message returned an error\" }\n }\n debugging('connection') && console.warn(this.debugLabel,\"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, from receiver::handleOperation. This impllies that\n * that we must be in established or later state.\n */\n closeWait (errorCode = null)\n {\n var preCloseState, 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 /* XXXwg - this should only be established->close-wait. Why more? */\n this.state.set(['disconnected', 'connecting', 'established'], 'close-wait');\n \n /* Set preCloseState to close-wait so doClose doesn't send a close message back */\n preCloseState = this.state.valueOf();\n return this.doClose(preCloseState, reason, true);\n }\n\n /**\n * This method will begin closing the protocol connection. It transitions\n * the protocol into the correct state, and then begins the work of closing.\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] When true, the connection will not deliver any pending messages and instead\n * immediately send the peer a '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('initial'))\n {\n this.state.set('initial', 'closed');\n this.emit('close'); /* Don't emit dcpsid */\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 this.state.set(['connecting', 'established', 'disconnected'], 'closing');\n\n // Perform actual work of closing\n return this.doClose(preCloseState, reason, immediate);\n }\n\n /**\n * Sends close message to peer after sending all pending messages.\n * Note that close messages are sent without the expectation of a response.\n * @param {DCPError|string} reason reason for closing\n */\n async sendCloseGracefully(reason) \n {\n debugging('connection') && console.debug(`${this.debugLabel} gracefully sending close message to peer with reason ${reason}`)\n let errorCode = reason instanceof DCPError ? reason.code : 'DCPC-1011';\n \n /* This only resolves when close is next message in queue */\n const closeMessage = await this.prepare('close', { errorCode: errorCode });\n this.sendPreparedMessage(closeMessage);\n this.messageLedger.fulfillMessagePromise(closeMessage.message.id, {});\n }\n \n /**\n * Sends close message to peer immediately. Pending messages will not be sent.\n * Note that close messages are sent without expectation of response.\n * @param {DCPError|string} reason reason for closing\n */\n async sendCloseImmediately(reason)\n {\n debugging('connection') && console.debug(`${this.debugLabel} immediately sending close message to peer with reason ${reason}`);\n let errorCode = reason instanceof DCPError ? reason.code : 'DCPC-1011';\n \n /* Last param being `true` means that prepareMessage will return unsigned message. Does not queue message. */\n const closeMessage = await this.prepare('close', { errorCode: errorCode }, true);\n \n if (this.sender.inFlight)\n closeMessage.nonce = this.sender.inFlight.message.nonce;\n else\n closeMessage.nonce = this.sender.nonce;\n \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 this.sender.sendInFlightMessage();\n }\n \n /**\n * This method performs the core close functionality. It appropriately sends the close message\n * to the peer, fails any pending transmissions, shuts down our sender and underlying transport\n * and puts us into the 'closed' state, indicating this connection object is now useless.\n * When called from closeWait, it does not send a close message.\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 var rejectErr;\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' && this.transport) {\n try {\n if (immediate) {\n await this.sendCloseImmediately(reason);\n } else {\n await this.sendCloseGracefully(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 if (reason instanceof DCPError)\n rejectErr = reason;\n else\n {\n let errorMessage = reason instanceof Error ? reason : `Connection to ${this.loggableDest} closed (${reason})`;\n rejectErr = new DCPError(errorMessage, 'DCPC-1013');\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(); delete this.transport; }\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=${dcpsid}, url=${this.url ? this.url.href : 'unknown url'}:`, error);\n }\n finally\n {\n this.state.set(['closing', 'close-wait'], 'closed');\n this.emit('end'); /* end event resolves promises on other threads for closeWait and close (ugh) */\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. The connection will not be able to send \n * any messages until the prepared message here is either sent or discarded. If 'canBatch = true',\n * will return the unsigned message instead. In this case, enqueuing is handled by\n * `async Connection.send()`, allowing the message to be put in a batch before being signed.\n * @param {...any} messageData Data to build message with. Format is:\n * `operation {string}, \n * data {Object} (optional),\n * identity {wallet.Keystore} (optional),\n * canBatch {boolean} (optional)`\n * @returns {Promise<Object>} a promise which resolves to { message, signedMessage }\n */\n\n async prepare(...messageData)\n {\n if (this.state.isNot('established'))\n {\n await this.connect().catch((e) => {\n if (e.code !== 'DCPC-1015') /* If we're closed already, then swallow the error */\n { \n this.close(e, true);\n throw e;\n }\n });\n }\n \n \n let signedMessage, message = messageData[0];\n let canBatch = false;\n \n if (typeof messageData[messageData.length - 1] === 'boolean')\n canBatch = messageData.pop();\n \n if (!message.id)\n {\n message = this.messageFactory.buildMessage(...messageData); \n }\n \n debugging('connection') && console.debug(`${this.debugLabel} Created message ${message.id}.`);\n \n message.ackToken = this.sender.makeAckToken();\n message.batchable = canBatch;\n \n if (canBatch)\n return Promise.resolve(message);\n \n debugging('connection') && console.debug(`${this.debugLabel} Preparing message ${message.id} for sending...`); \n const messageWithNonce = await new Promise((resolve) =>\n {\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 this.sender.queue.push(message)\n this.sender.requestQueueService()\n })\n \n signedMessage = await messageWithNonce.sign();\n \n debugging('connection') && console.debug(`${this.debugLabel} Finished preparing message. ${message.id} is ready to be sent.`);\n \n return { message: messageWithNonce, signedMessage: signedMessage };\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 an 'id' 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('established'))\n await this.connect().catch((e) =>\n {\n if (e.code !== 'DCPC-1015') /* If we're closed already, then swallow the error */\n { \n this.close(e, true);\n throw e;\n }\n });\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.prepare(...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 const messageSentPromise = this.messageLedger.addMessage(message);\n this.sender.sendInFlightMessage();\n \n return messageSentPromise;\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.nonce = message.nonce;\n delete message.nonce;\n message.type = 'unhandled-message';\n this.sender.requestQueueService();\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 = performance.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 * 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.options.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}\nexports.Connection = Connection;\n\n\n//# sourceURL=webpack://dcp/./src/protocol-v4/connection/connection.js?");
4544
+ eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_modules/process/browser.js */ \"./node_modules/process/browser.js\");\n/**\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 * Connection instance events:\n * - session: dcpsid new session established\n * - connect: url UI hint: internet available\n * - disconnect: url UI hint: internet not available\n * - readyStateChange: *** DO NOT USE **\n * - error: error emitted when errors happen that would otherwise go uncaught\n * - close: connection instance is closing\n * - end: Connection instance is closed\n * - send: msgObj when a message is sent to the peer; does not wait for ack; may re-trigger on reconnect\n * - ready: when the connection is ready for traffic (constructor promises resolved)\n *\n * State Transition Diagram for Connection.state:\n *\n * initial connecting established disconnected close-wait closing closed\n * ===========================================================================================================================\n * |-- i:connect ---->\n * |-- t:accept ----->\n * |-- t:establishTarget -->\n * |-- i:connect ---------->\n * |-- transportDisconnectHandler -->\n * <-- i:_reconnect ----------------------------------------|\n * |-i:useNewTransport-->\n * <-- t:useNewTransport --------|\n * |-- closeWait ----------------------------------------------------------->\n * |-- closeWait ----------------------------------->\n * |-- closeWait -->\n * |-- doClose --------------->\n * |-- close ------------------------------------------------------------------------------------------------------------> \n * |-- close ---------------------------------------------------------------------------->\n * |-- close ---------------------------------------------------->\n * |-- close ------------------->\n * |-- doClose -->\n *\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 IN CLOSING, CLOSE-WAIT OR CLOSED\n * EINVAL - MESSAGE OWNER IS INVALID (formerly DCPC-1002)\n * MESSAGE SIGNATURE INVALID (formerly DCPC-1003)\n * MESSAGE BODY IS INVALID (formerly DCPC-1004)\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 - TRIED TO INITIATE CONNECTION AFTER SESSION ALREADY ESTABLISHED\n * DCPC-1013 - DEFAULT ERROR CODE WHEN CLOSING WITH REASON THATS NOT INSTANCE OF DCPERROR\n * DCPC-1014 - NO TRANSPORTS AVAILABLE\n * DCPC-1015 - CANNOT CONNECT WHEN CONNECTION ALREADY CLOSED\n * DCPC-1016 - ERROR CONNECTING VIA AVAILABLE TRANSPORTS\n * DCPC-1017 - FIRST PROTOCOL MESSAGE WAS DID NOT INVOLVE INITIAL CONNECT REQUEST\n * DCPC-1018 - INVALID ARGUMENT PROVIDED IN PLACE OF IDKEYSTORE\n * ENODCPSID - CONNECTION INSTANCE TRIED TO RE-CONNECT TO A TARGET WHICH DOES NOT HAVE A RECORD OF THAT SESSION\n */\n\n\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('dcp');\nconst dcpEnv = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\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 { 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 { 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';\nlet nanoid;\nif (dcpEnv.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\n\nlet globalConnectionId = 0;\nvar _msgId = 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 * Connection form 2:\n * @constructor\n * @param {object} [target]\n * @param {Promise} idKsPromise A promise which resolves to the identity keystore described\n * in form 1\n * @param {object} [options]\n * @see form 1\n */\n /**\n * Connection form 1\n * Create a DCP Connection object. This object could represent either the initiator or \n * target end of a connection, until it is specialized by either invoke the connect()\n * or accept() methods. Note that send() invokes connect() internally if not in an established\n * state.\n * @constructor\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} target Object (usually a dcpConfig fragment) describing the target.\n * This object may contain the following properties; 'location' is\n * mandatory:\n * - location: a URL or 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 one will\n * be chosen by examining IP addresses (assuming an IP bearer).\n * - identity: an object with an address property which is a promise\n * that resolves to an instance of wallet.Address which represents\n * to the target's identity; this overrides the initiator's \n * identity cache unless options.strict is truey.\n * \n * @param {Keystore} [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} [options] 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 * @returns instance of Connection that is specific to a target URL but not a role\n */\n constructor(target, idKeystore, options = {})\n {\n super('Connection');\n this.id = ++globalConnectionId;\n this.debugLabel = `connection(g:${this.id}):`;\n\n /* polymorphism strategy: rewrite to (configFragment, idksPromise, options) */\n if (target instanceof DcpURL)\n target = { location: target };\n else if (DcpURL.isURL(target))\n target = { location: new DcpURL(target) };\n else if (target instanceof String || typeof target === 'string')\n target = { location: new DcpURL(target) };\n assert(typeof target === 'object', target.location);\n\n /* idKeystore is always resolved by the time a session is established. */\n if (!idKeystore)\n this.identityPromise = wallet.getId();\n else if (idKeystore instanceof Promise)\n this.identityPromise = idKeystore;\n else if (idKeystore instanceof wallet.Keystore)\n this.identityPromise = Promise.resolve(idKeystore); \n else if (idKeystore instanceof wallet.Address)\n this.identityPromise = Promise.resolve(new wallet.Keystore(idKeystore, '')); \n else\n throw new DCPError('Invalid argument provided for IdKeystore', 'DCPC-1018');\n\n this.identityPromise.then((keystore) => {\n /* This always happens by the time a role is assumed */\n delete this.identityPromise;\n this.identity = keystore;\n this.emit('ready');\n debugging('connection') && console.debug(this.debugLabel, 'identity is', this.identity.address);\n });\n\n this.target = target;\n this.url = this.target.location;\n \n // Init internal state / vars\n this.state = new Synchronizer(CONNECTION_STATES[0], CONNECTION_STATES);\n // DO NOT USE this.state.on('change', (s) => this.emit('readyStateChange', s) );\n\n this.dcpsid = null;\n this.peerAddress = null;\n this.transport = null;\n this.messageLedger = new MessageLedger(this);\n this.authorizedSender = null;\n this.preDisconnectState = 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.receiver = new Receiver(this);\n this.sender = new Sender(this);\n \n debugging('connection') && console.debug(this.debugLabel, `new connection#${this.id}; ${this.url}`);\n\n /* Create a connection config as this.options which takes into\n * account system defaults and overrides for specific urls, origins, etc.\n */\n this.options = leafMerge(\n ({ /* hardcoded defaults insulate us from missing web config */\n 'connectTimeout': 90,\n 'lingerTimeout': 1800,\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 options\n );\n\n /* draw out errors quickly in dev */\n if ((process.env.DCP_NETWORK_CONFIG_BUILD || dcpConfig.build) === 'debug')\n {\n this.options.maxMessagesPerBatch /= 10;\n \n /* short timeouts and debuggers don't get along well */\n if (dcpEnv.platform === 'nodejs' && !(requireNative('module')._cache.niim instanceof requireNative('module').Module))\n {\n this.options.connectTimeout /= 10;\n this.options.lingerTimeout /= 20;\n this.options.identityUnlockTimeout /= 10;\n }\n }\n\n assert(this.options.identityUnlockTimeout > 0);\n assert(this.options.connectTimeout > 0);\n assert(this.options.lingerTimeout > 0);\n assert(typeof this.options.ttl.min === 'number');\n assert(typeof this.options.ttl.max === 'number');\n assert(typeof this.options.ttl.default === 'number');\n \n this.backoffTimeIterator = makeEBOIterator(500, dcpConfig.build === 'debug' ? 5000 : 60000); /** XXXwg make this configurable */\n\n this.secureLocation = determineIfSecureLocation(this);\n this.loggableDest = '<generic>';\n }\n \n generateMessageId()\n {\n return `${this.id}-${_msgId++}-${Date.now()}-${nanoid()}`;\n }\n\n /**\n * Specialize an instance of Connection for either initiator or target behaviour. Once specialized,\n * the role cannot be changed. This happens based on methods invoked; connect() or accept() cause\n * the change.\n *\n * This specialization also implies that the connection is fully ready for use, including resolution\n * of the identity promise if necessary. This is perhaps not the best place to ensure that, but it\n * provides a reliable - and already async - waypoint to observe that event.\n */\n async a$assumeRole(myRole)\n {\n assert(myRole === role.initiator || myRole === role.target);\n\n if (this.role === myRole)\n return;\n this.role = myRole;\n \n if (this.role === role.target)\n {\n this.debugLabel = `connection(t:${this.id}):`;\n this.sender.debugLabel = `sender(t#${this.id}):`;\n this.messageLedger.debugLabel = `message-ledger(t#${this.id}):`;\n this.loggableDest = '<target>';\n this.hasNtp = true;\n }\n else\n {\n this.debugLabel = `connection(i:${this.id}):`;\n this.sender.debugLabel = `sender(i#${this.id}):`;\n this.messageLedger.debugLabel = `message-ledger(i#${this.id}):`;\n this.loggableDest = this.url.href;\n this.hasNtp = false;\n }\n\n debugging('connection') && console.debug(this.debugLabel, `connection #${this.id} is ${this.role} for ${this.url}`);\n if (!this.identity)\n {\n assert(this.identityPromise);\n debugging('connection') && console.debug(this.debugLabel, `waiting for identity resolution`);\n await this.identityPromise;\n }\n }\n\n /**\n * API to establish a DCP connection. Implied by send().\n *\n * When invoked by the initiator, this method establishes the connection by connecting\n * to the target url provided to the constructor.\n */\n async connect() // eslint-disable-line require-await\n {\n if (this.role == role.target)\n return;\n \n if (!this.role)\n await this.a$assumeRole(role.initiator);\n \n if (this.state.is('initial'))\n {\n if (!this.connectPromise)\n this.connectPromise = Promise.race([this.connectTimer(), this.a$_connect()]).then(() => { clearTimeout(this.connectTimeoutPromise); delete this.connectPromise });\n return this.connectPromise;\n }\n\n if (this.state.is('disconnected'))\n {\n if (!this.connectPromise)\n this.connectPromise = this.a$_reconnect().then(() => delete this.connectPromise);\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-1015');\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 var transport;\n\n this.state.testAndSet('disconnected', 'connecting');\n try\n {\n do\n {\n transport = await this.a$connectToTargetTransport();\n } while (!transport && (this.transport && !this.transport.ready()) && !this.state.in(['closed', 'close-wait', 'closing']));\n\n debugging('connection') && console.debug(this.debugLabel, `reconnected via transport ${transport.socket.id}`);\n \n this.useNewTransport(transport);\n }\n catch (error)\n {\n if (error.code !== 'DCPC-1016' && error.code !== 'DCPC-1015')\n {\n /* Unreached unless there are bugs. */\n throw error;\n } \n this.close(error, true);\n }\n }\n\n connectTimer()\n {\n return new Promise((_resolve, reject) =>\n {\n this.connectTimeoutPromise = setTimeout(() =>\n { \n reject(new Error('Failed to establish connection within 30 seconds'));\n }, 30000);\n if (typeof this.connectTimeoutPromise.unref === 'function')\n this.connectTimeoutPromise.unref();\n });\n }\n \n async a$_connect()\n { \n var presharedPeerAddress, establishResults;\n var targetIdentity = await this.target.identity;\n var transport;\n \n assert(this.role === role.initiator);\n\n this.state.set('initial', 'connecting');\n do\n {\n transport = await this.a$connectToTargetTransport().catch((error) =>\n {\n debugging('connection') && console.debug(`${this.debugLabel} error connecting to target on transport layer:`, error);\n return { ready: () => {return false} };\n });\n } while(!transport.ready());\n this.adopt(transport);\n\n establishResults = await this.sender.establish().catch(error => {\n debugging('connection') && console.debug(this.debugLabel, `Could not establish DCP session ${this.transport ? 'over' + this.transport.name : '. Transport establishment was not complete'}:`, error);\n this.close(error, true);\n throw error;\n });\n const peerAddress = new wallet.Address(establishResults.peerAddress);\n const dcpsid = establishResults.dcpsid;\n debugging('connection') && console.debug(this.debugLabel, 'dcpsid is', dcpsid);\n \n if (!this.options.strict && targetIdentity && determineIfSecureConfig())\n {\n if ( false\n || typeof targetIdentity !== 'object'\n || typeof targetIdentity.address !== 'object'\n || !(targetIdentity.address instanceof wallet.Address))\n targetIdentity = { address: new wallet.Address(targetIdentity) }; /* map strings and Addresses to ks ducks */\n\n presharedPeerAddress = targetIdentity.address;\n debugging('connection') && console.debug(this.debugLabel, 'Using preshared peer address', presharedPeerAddress);\n }\n this.ensureIdentity(peerAddress, presharedPeerAddress);\n\n /* At this point, new session is valid & security checks out - make Connection instance usable */\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-1012');\n\n this.state.set('connecting', 'established'); /* established => dcpsid has been set */\n this.emit('session', (this.dcpsid = dcpsid));\n this.emit('connect', this.url);\n this.sender.notifyTransportReady();\n return Promise.resolve();\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 && dcpEnv.platform === 'nodejs')\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 * @param {object} transport the transport instance that triggered this handler. In some cases, it\n * is possible that this event is not serviced until after the connection\n * has already acquired a new transport instance, e.g. in a Target where\n * the initiator switched networks. This implies that it is possible for\n * more 'connect' events to be emitted than 'disconnect' events.\n */\n transportDisconnectHandler(transport)\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 if (transport !== this.transport) /* event no longer relevant */\n return;\n\n if (this.transport)\n {\n transport.close();\n delete this.transport;\n }\n \n if (this.state.is('established'))\n {\n this.state.set('established', 'disconnected');\n this.emit('disconnect', this.url); /* 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) /* hopefully impossible? */\n {\n debugging('connection') && console.debug(this.debugLabel, 'Not reconnecting - no session');\n return;\n }\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.dcpsid && !this.sender.inFlight && this.options.onDemand)\n {\n debugging('connection') && console.debug(this.debugLabel, `Not reconnecting ${this.dcpsid} until next message`);\n return;\n }\n \n if (this.state.is('connecting') && (!this.dcpsid || !this.peerAddress))\n {\n debugging('connection') && console.debug(this.debugLabel, `Disconnected while connecting, establishing transport and re-sending connect request.`);\n this.a$_reconnect();\n return;\n }\n\n /* At this point, we initiate a (re)connect attempt because either\n * - we haven't connected yet,\n * - we have something to send, or\n * - we are not an on-demand connection\n */\n if (!this.state.is('connecting'))\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' && error.code !== 'DCPC-1015')\n {\n /* Unreached unless there are bugs. */\n throw error;\n }\n }\n }\n \n /**\n * Initiators only\n *\n * Connect to a target at the transport level.\n * - Rejects when we give up on all transports.\n * - Resolves with a transport instance when we connect to one.\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$connectToTargetTransport()\n {\n const that = this;\n const availableTransports = [].concat(this.options.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 // Already trying to connect to target, don't try multiple times until we've aborted one attempt\n if (this.connectAbortTimer)\n return;\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.options.connectTimeout * 1000);\n\n if (this.options.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. */\n /* Resolves with { bool success, obj transport } or rejects with { error } if the transport cannot connect*/\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.target, that.options[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.debug(`Error connecting to ${that.url};`, error);\n connectWithTransport_resolve(ret);\n });\n \n /* let the connectAbortTimer interrupt this connect attempt */\n transportConnectIntr = () => { transport.close() };\n });\n }\n \n if (availableTransports.length === 0)\n {\n cleanup_ctt();\n return Promise.reject(new DCPError('no transports defined', 'DCPC-1014'));\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 /* Have connected to the remote at the transport level - OUT */\n if (success === true)\n {\n transportConnectIntr = false;\n cleanup_ctt();\n \n return transport;\n }\n\n /* Fast-fail: certain - but few - HTTP status codes let us know that this (or any) transport\n * will never work, so don't try those again.\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.`);\n quitMsg = error.message;\n quitCode = 'HTTP_' + error.httpStatus || 0;\n break;\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 (impossibleError)\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 * Method which must be invoked whenever a new transport needs to be assigned to the connection. \n * If we have previously adopted a transport, we close it first, which will prevent \n * [by transport definition] any message handlers from firing, even if the old transport instance\n * has buffered traffic.\n *\n * @param {object} transport transport instance\n */\n adopt(transport)\n {\n if (this.transport)\n this.transport.close();\n\n transport.on('message', (m) => this.handleMessage(m));\n transport.on('end', () => this.transportDisconnectHandler(transport));\n transport.on('close', () => this.transportDisconnectHandler(transport));\n \n this.transport = transport;\n }\n \n \n /**\n * Method that gets invoked when there is a new transport available for adoption.\n * This will adjust the state of the connection, adopt the transport then tell\n * the sender to pump the message queue.\n * @param {object} transport transport instance \n */\n useNewTransport(transport)\n {\n if (this.state.in(['closing', 'close-wait', 'closed']))\n {\n debugging('connection') && console.debug(`${this.debugLabel} got a new transport during closing. closing the new transport and not completing transport adoption.`)\n transport.close();\n return;\n }\n \n var preDisconnectState = (!this.dcpsid || !this.peerAddress) ? 'connecting' : 'established';\n if (this.state.isNot(preDisconnectState))\n this.state.set(['connecting', 'disconnected'], preDisconnectState);\n this.adopt(transport);\n this.emit('connect', this.url); // UI hint: \"internet available\" \n this.sender.notifyTransportReady();\n }\n \n /**\n * Method that must be invoked by the target to \"accept\" a new DCP Connection request\n * at the transport layer.\n * @param {object} transport \n */\n async accept(transport)\n {\n assert(!this.role);\n await this.a$assumeRole(role.target);\n this.state.set('initial', 'connecting');\n this.adopt(transport);\n }\n \n /**\n * This method is invoked by the target when it has handled the initial connect request from\n * the initiator, which contains the peerAddress and the first half of the dcpsid (the second half is\n * populated by receiver.handleFirstRequest before being passed here). It transitions the connection \n * into an established state at the protocol level.\n * Note - this is really not the right design for this, but it is invoked from handleFirstRequest\n * in ./receiver.js\n *\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('dummy'); // 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.emit('session', (this.dcpsid = dcpsid));\n debugging() && console.debug(this.debugLabel, 'dcpsid is', dcpsid);\n\n this.loggableDest = this.role === role.initiator ? this.url : peerAddress;\n this.state.set('connecting', 'established'); /* established => dcpsid has been set */\n\n debugging('connection') && console.debug(this.debugLabel, `Established session ${this.dcpsid} with ${this.peerAddress} for ${this.url}`);\n }\n\n /**\n * Check to see if the peer address conflicts with what we have in the global identity cache;\n * it does, throw an exception.\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 /**\n * This method uses the first request (if we're target) or ack to the first request \n * (if we're initiator) to memoize the address of the peer authorized to send to us. \n * The first message must only be a request (since it comes from sender.specialFirstSend)\n * that has a connect operation, or an acknowledgement of the first request. \n * All future messages' owners will be validated against this authorized sender.\n * @param {Object} message \n */\n setAuthorizedSender(message)\n {\n if (message.body.type !== 'request' && message.body.type !== 'ack')\n throw new DCPError('First protocol message was not a request or ack', 'DCPC-1017');\n \n if (message.body.type === 'request' && message.body.payload.operation !== 'connect')\n throw new DCPError('First protocol message did not contain the correct payload', 'DCPC-1017');\n \n if (message.body.type === 'ack' && this.sender.inFlight.message.payload.operation !== 'connect')\n throw new DCPError('First protocol acknowledgement was not for connect request', 'DCPC-1017');\n \n this.authorizedSender = message.owner;\n }\n \n /**\n * Emits an error event with the relevant error and closes the connection immediately.\n * @param {string} errorMessage \n * @param {string} errorCode \n */\n \n handleValidationError(errorMessage, errorCode)\n {\n var messageError;\n \n debugging('connection') && console.debug(this.debugLabel, 'Message failed validation -', errorMessage);\n this.emit('error', (messageError = new DCPError(`message failed validation: ${errorMessage}`, errorCode)))\n this.close(messageError, true);\n }\n\n /**\n * This method validates the message owner, signature and body before passing it onto\n * either the receiver (for a request, response or batch) or the messageLedger (for an ack).\n * If it's a request, response or batch, this method also provokes the connection to \n * send an acknowledgement (ack) to the peer to let them know we got their message.\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 * XXXsc did some auditing. we happy now?\n * @param {string} JSON-encoded unvalidated message object\n */\n async handleMessage (messageJSON) {\n var validation;\n var message;\n\n if (this.state.is('closed')) {\n debugging('connection') && console.debug(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 debugging('wire') && console.debug(this.debugLabel, `handleMessage: ${String(message && message.body && message.body.type).padEnd(10, ' ')} <- ${this.loggableDest}`);\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:', message.body);\n debugging('wire') && console.debug(this.debugLabel, `dup message ack: ${String(message.body.type).padEnd(10, ' ')} -> ${this.loggableDest}`);\n\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 validation = this.validateMessageDCPSID(message);\n if (validation.success !== true)\n {\n this.handleValidationError(validation.errorMessage, 'ENODCPSID');\n return;\n }\n\n validation = this.validateMessageOwner(message)\n if (validation.success !== true)\n {\n this.handleValidationError(validation.errorMessage, 'EINVAL');\n return;\n }\n \n validation = this.validateMessageSignature(message);\n if (validation.success !== true)\n {\n this.handleValidationError(validation.errorMessage, 'EINVAL');\n return;\n }\n\n validation = this.validateMessageBody(message);\n if (validation.success !== true)\n {\n this.handleValidationError(validation.errorMessage, validation.errorCode || 'EINVAL'); /* messages of type 'unhandled-message' may contain more information about the failure */\n return;\n }\n \n if (message.body.type === \"ack\") {\n const ack = new this.Ack(message.body);\n this.messageLedger.handleAck(ack);\n return;\n } else if (message.body.type !== 'unhandled-message') {\n this.lastMessage = message;\n await this.ackMessage(message);\n }\n \n this.receiver.handleMessage(message);\n }\n\n \n /**\n * This method takes either a Request, Response or Batch, creates an ack for it\n * and sends it to the peer. This ack contains the nonce we expect on the next\n * message from peer.\n * @param {Connection.Message} message \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\n debugging('wire') && console.debug(this.debugLabel, `ackMessage: ${String(message.body.type).padEnd(10, ' ')} -> ${this.loggableDest}`);\n\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 && this.lastMessage.body.nonce === messageJSON.body.nonce;\n }\n\n /**\n * Validate that the message came from the appropriate sender.\n * @param {Object} message the message to validate\n * @returns {Object} returns an object `ret` with either ret.success = true, \n * or ret.success = false accompanied by another property ret.errorMessage\n */\n validateMessageOwner(message)\n {\n if (!this.authorizedSender)\n {\n /* Capture the initial identity of the remote end during the connect operation */\n this.setAuthorizedSender(message);\n return { success: true }\n }\n else if (message.owner !== this.authorizedSender)\n {\n return { success: false, errorMessage: \"message came from unauthorized sender\" }\n }\n return { success: true }\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 validateMessageSignature(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 validateMessageDCPSID(message)\n {\n if (this.dcpsid !== null && message.dcpsid)\n {\n if (message.dcpsid !== this.dcpsid)\n {\n debugging('connection') && console.warn('Message has an invalid dcpsid, aborting connection');\n return { success: false, errorMessage: 'message has an invalid dcpsid' };\n }\n }\n \n return { success: true };\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 validateMessageBody(message)\n {\n try\n {\n if (message.body.type === 'unhandled-message')\n {\n /* This special message type may not have a dcpsid, peerAddress, etc., so it might not\n * validate. It is also not a \"real\" message and only used to report ConnectionManager routing \n * errors, so we just report here, drop it, and close the connection.\n *\n * Note also that this is probably the wrong way to handle this case - restarting daemons - but\n * that is a problem for another day. /wg nov 2021\n */\n debugging('connection') && console.warn(this.debugLabel, \"Target Error - target could not process message.\", JSON.stringify(message.body),\n \"Aborting connection.\");\n return { success: false, errorMessage: `target could not process message (${message.body.payload && message.body.payload.message || 'unknown error'})`, errorCode: message.body.payload && message.body.payload.code}\n }\n if (this.peerAddress && !this.peerAddress.eq(message.owner))\n {\n debugging('connection') && console.warn(this.debugLabel,\n \"Received message's owner address does not match peer address, aborting connection\\n\",\n \"(owner addr)\", message.owner, '\\n',\n \"(peer addr)\", this.peerAddress);\n return { success: false, errorMessage: \"received message owner 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(this.debugLabel,\n \"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 /* can get close in middle of connecting, which will have no nonce.*/\n if (body.type !== 'ack' && this.lastAck.nonce !== body.nonce && (body.payload && body.payload.operation !== 'close'))\n {\n /* When Target sends back ConnectionLessErrorResponse, it uses the nonce of the message that caused an error. */\n if (this.sender.inFlight && this.sender.inFlight.message.nonce === body.nonce)\n {\n debugging('connection') && console.debug(`${this.debugLabel} Received messages nonce matches nonce of our current inFlight message.`,\n \"There was a problem sending this message. Aborting connection. Reason:\\n\", body.payload);\n return { success: false, errorMessage: \"current inflight message returned an error\" }\n }\n debugging('connection') && console.warn(this.debugLabel,\"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 if (body.type === 'request') \n {\n if (body.payload.validity.time === undefined)\n return { success: false, errorMessage: 'received message does not have a valid time in its payload' };\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, from receiver::handleOperation. This impllies that\n * that we must be in established or later state.\n */\n closeWait (errorCode = null)\n {\n var preCloseState, 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 /* XXXwg - this should only be established->close-wait. Why more? */\n this.state.set(['disconnected', 'connecting', 'established'], 'close-wait');\n \n /* Set preCloseState to close-wait so doClose doesn't send a close message back */\n preCloseState = this.state.valueOf();\n return this.doClose(preCloseState, reason, true);\n }\n\n /**\n * This method will begin closing the protocol connection. It transitions\n * the protocol into the correct state, and then begins the work of closing.\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] When true, the connection will not deliver any pending messages and instead\n * immediately send the peer a '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('initial'))\n {\n this.state.set('initial', 'closed');\n this.emit('close'); /* Don't emit dcpsid */\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 this.state.set(['connecting', 'established', 'disconnected'], 'closing');\n\n // Perform actual work of closing\n return this.doClose(preCloseState, reason, immediate);\n }\n\n /**\n * Sends close message to peer after sending all pending messages.\n * Note that close messages are sent without the expectation of a response.\n * @param {DCPError|string} reason reason for closing\n */\n async sendCloseGracefully(reason) \n {\n debugging('connection') && console.debug(`${this.debugLabel} gracefully sending close message to peer with reason ${reason}`)\n let errorCode = reason instanceof DCPError ? reason.code : 'DCPC-1011';\n \n /* This only resolves when close is next message in queue */\n const closeMessage = await this.prepare('close', { errorCode: errorCode });\n this.sendPreparedMessage(closeMessage);\n this.messageLedger.fulfillMessagePromise(closeMessage.message.id, {});\n }\n \n /**\n * Sends close message to peer immediately. Pending messages will not be sent.\n * Note that close messages are sent without expectation of response.\n * @param {DCPError|string} reason reason for closing\n */\n async sendCloseImmediately(reason)\n {\n debugging('connection') && console.debug(`${this.debugLabel} immediately sending close message to peer with reason ${reason}`);\n let errorCode = reason instanceof DCPError ? reason.code : 'DCPC-1011';\n \n /* Last param being `true` means that prepareMessage will return unsigned message. Does not queue message. */\n const closeMessage = await this.prepare('close', { errorCode: errorCode }, true);\n \n if (this.sender.inFlight)\n closeMessage.nonce = this.sender.inFlight.message.nonce;\n else\n closeMessage.nonce = this.sender.nonce;\n \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 this.sender.sendInFlightMessage();\n }\n \n /**\n * This method performs the core close functionality. It appropriately sends the close message\n * to the peer, fails any pending transmissions, shuts down our sender and underlying transport\n * and puts us into the 'closed' state, indicating this connection object is now useless.\n * When called from closeWait, it does not send a close message.\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 var rejectErr;\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' && this.transport) {\n try {\n if (immediate) {\n await this.sendCloseImmediately(reason);\n } else {\n await this.sendCloseGracefully(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 if (reason instanceof DCPError)\n rejectErr = reason;\n else\n {\n let errorMessage = reason instanceof Error ? reason : `Connection to ${this.loggableDest} closed (${reason})`;\n rejectErr = new DCPError(errorMessage, 'DCPC-1013');\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(); delete this.transport; }\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=${dcpsid}, url=${this.url ? this.url.href : 'unknown url'}:`, error);\n }\n finally\n {\n this.state.set(['closing', 'close-wait'], 'closed');\n this.emit('end'); /* end event resolves promises on other threads for closeWait and close (ugh) */\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. The connection will not be able to send \n * any messages until the prepared message here is either sent or discarded. If 'canBatch = true',\n * will return the unsigned message instead. In this case, enqueuing is handled by\n * `async Connection.send()`, allowing the message to be put in a batch before being signed.\n * @param {...any} messageData Data to build message with. Format is:\n * `operation {string}, \n * data {Object} (optional),\n * identity {wallet.Keystore} (optional),\n * canBatch {boolean} (optional)`\n * @returns {Promise<Object>} a promise which resolves to { message, signedMessage }\n */\n\n async prepare(...messageData)\n {\n if (this.state.isNot('established'))\n {\n await this.connect().catch((e) => {\n if (e.code !== 'DCPC-1015') /* If we're closed already, then swallow the error */\n { \n this.close(e, true);\n throw e;\n }\n });\n }\n \n \n let signedMessage, message = messageData[0];\n let canBatch = false;\n \n if (typeof messageData[messageData.length - 1] === 'boolean')\n canBatch = messageData.pop();\n \n if (!message.id)\n {\n message = this.Request.buildMessage(...messageData);\n }\n \n debugging('connection') && console.debug(`${this.debugLabel} Created message ${message.id}.`);\n \n message.ackToken = this.sender.makeAckToken();\n message.batchable = canBatch;\n \n if (canBatch)\n return Promise.resolve(message);\n \n debugging('connection') && console.debug(`${this.debugLabel} Preparing message ${message.id} for sending...`); \n const messageWithNonce = await new Promise((resolve) =>\n {\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 this.sender.queue.push(message)\n this.sender.requestQueueService()\n })\n \n signedMessage = await messageWithNonce.sign();\n \n debugging('connection') && console.debug(`${this.debugLabel} Finished preparing message. ${message.id} is ready to be sent.`);\n \n return { message: messageWithNonce, signedMessage: signedMessage };\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 an 'id' 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('established'))\n await this.connect().catch((e) =>\n {\n if (e.code !== 'DCPC-1015') /* If we're closed already, then swallow the error */\n { \n this.close(e, true);\n throw e;\n }\n });\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.prepare(...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 const messageSentPromise = this.messageLedger.addMessage(message);\n this.sender.sendInFlightMessage();\n \n return messageSentPromise;\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.nonce = message.nonce;\n delete message.nonce;\n message.type = 'unhandled-message';\n this.sender.requestQueueService();\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 = performance.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 * 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.options.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}\nexports.Connection = Connection;\n\n\n//# sourceURL=webpack://dcp/./src/protocol-v4/connection/connection.js?");
4535
4545
 
4536
4546
  /***/ }),
4537
4547
 
@@ -4542,7 +4552,7 @@ eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_mod
4542
4552
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4543
4553
 
4544
4554
  "use strict";
4545
- eval("/** @file error-payload.js\n * @author Eddie Roosenmaallen <eddie@kingsds.network>, Wes Garland <wes@kingsds.network.\n * @date January 2020, Jan 2021\n *\n * This module exports the ErrorPayload class, which encapsulates errors for\n * transmission to the client.\n */\n\n\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n\n/**\n * Create a per-connection ErrorPayload constructor. The ErrorPayload is used to build\n * objects for instance of Error which can be stringified in dcp4 message payloads.\n *\n * @param {object} connectionThis an instance of Cnnnection\n * @returns {function} the constructor\n * @see Protocol-v4 Connection.ErrorPayload.\n */\nexports.ErrorPayloadCtorFactory = function connection$$ErrorPayloadCtorFactory(connectionThis)\n{\n var process;\n \n if ((__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").isBrowserPlatform))\n process = 'browser'\n else\n process = requireNative('path').basename(requireNative('process').argv[1] || 'node');\n\n /**\n * Connnection.ErrorPayload constructor, form 1.\n * @param {object} instanceof Error to turn into a ConnectionErrorpayload\n */\n /**\n * Connnection.ErrorPayload constructor, form 2.\n * @param {string} message Error message\n * @param {string} code Optional error code\n * @param {object} ctor Optional constructor. Default: Error\n */\n function ErrorPayload()\n {\n var error;\n \n if (typeof arguments[0] === 'object')\n {\n error = arguments[0];\n\n for (let prop of Object.getOwnPropertyNames(error))\n this[prop] = error[prop];\n }\n else\n {\n let [ message, code, ctor ] = arguments;\n\n if (!ctor)\n ctor = Error;\n\n error = new ctor(message);\n if (code)\n error.code = code;\n }\n\n this.type = 'protocol';\n this.process = process;\n this.name = error.name;\n this.origin = connectionThis.identity && connectionThis.identity.address;\n \n for (let prop of Object.getOwnPropertyNames(error))\n this[prop] = error[prop];\n }\n\n return ErrorPayload;\n}\n\n\n//# sourceURL=webpack://dcp/./src/protocol-v4/connection/error-payload.js?");
4555
+ eval("/** @file error-payload.js\n * @author Eddie Roosenmaallen <eddie@kingsds.network>, Wes Garland <wes@kingsds.network.\n * @date January 2020, Jan 2021\n *\n * This module exports the ErrorPayload class, which encapsulates errors for\n * transmission to the client.\n */\n\n\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n\n/**\n * Create a per-connection ErrorPayload constructor. The ErrorPayload is used to build\n * objects for instance of Error which can be stringified in dcp4 message payloads.\n *\n * @param {object} connectionThis an instance of Connection\n * @returns {function} the constructor\n * @see Protocol-v4 Connection.ErrorPayload.\n */\nexports.ErrorPayloadCtorFactory = function connection$$ErrorPayloadCtorFactory(connectionThis)\n{\n var process;\n \n if ((__webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\").isBrowserPlatform))\n process = 'browser'\n else\n process = requireNative('path').basename(requireNative('process').argv[1] || 'node');\n\n /**\n * Connnection.ErrorPayload constructor, form 1.\n * @param {Error} error instanceof Error to turn into a ConnectionErrorpayload.\n *\n * Connnection.ErrorPayload constructor, form 2.\n * @param {string} message Error message\n * @param {string} code Optional error code\n * @param {object} ctor Optional constructor. Default: Error\n * \n * Connnection.ErrorPayload constructor, form 3.\n * @param {object} object plain object and must be Error-shaped, with `message` and `name` being required\n *\n */\n function ErrorPayload()\n {\n var error;\n if (arguments.length === 1) // form 1 and 3\n {\n let isErrorShaped = !!arguments[0].message && !!arguments[0].name;\n // It should be instance of Error- form 1\n // or error-shaped object(must have both `name` or 'message` property)- form 3\n if (typeof arguments[0] === 'object' && (arguments[0] instanceof Error || isErrorShaped))\n error = arguments[0];\n else\n error = new TypeError(`'${arguments[0]}' is not an instance of error.`);\n }\n else // from 2 \n { \n let [ message, code, ctor ] = arguments;\n if (ctor !== undefined && !(new ctor() instanceof Error))\n error = new TypeError(`${ctor} is not an instance of Error`);\n else\n {\n if (!ctor)\n ctor = Error;\n\n error = new ctor(message);\n if (code)\n error.code = code;\n }\n }\n\n this.type = 'protocol';\n this.process = process;\n this.name = error.name;\n this.origin = connectionThis.identity && connectionThis.identity.address;\n\n for (let prop of Object.getOwnPropertyNames(error))\n this[prop] = error[prop];\n }\n\n return ErrorPayload;\n}\n\n\n//# sourceURL=webpack://dcp/./src/protocol-v4/connection/error-payload.js?");
4546
4556
 
4547
4557
  /***/ }),
4548
4558
 
@@ -4568,17 +4578,6 @@ eval("/**\n * @file protocol/connection/index.js\n * @author Ryan Ro
4568
4578
 
4569
4579
  /***/ }),
4570
4580
 
4571
- /***/ "./src/protocol-v4/connection/message-factory.js":
4572
- /*!*******************************************************!*\
4573
- !*** ./src/protocol-v4/connection/message-factory.js ***!
4574
- \*******************************************************/
4575
- /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
4576
-
4577
- "use strict";
4578
- eval("/**\n * @file protocol/connection/message-factory.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @date January 2020\n *\n * The MessageFactory class accepts either instances of message, or arguments\n * that it will use to construct a new Connection.Request. It also performs validation\n * on the message, assigning IDs to requests if required, before returning the well-formed message.\n */\n\n\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\n\nlet nanoid;\nif (DCP_ENV.platform === 'nodejs') {\n const { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\n nanoid = requireNative('nanoid').nanoid;\n} else {\n nanoid = (__webpack_require__(/*! nanoid */ \"./node_modules/nanoid/index.browser.js\").nanoid);\n}\n\nconst { Message } = __webpack_require__(/*! ../message */ \"./src/protocol-v4/message.js\");\n\nclass MessageFactory {\n constructor(connection) {\n this.connection = connection;\n this._msgId = 0;\n }\n\n generateMessageId() {\n return `${this.connection.id}-${this._msgId++}-${Date.now()}-${nanoid()}`;\n }\n\n buildMessage(...args) {\n let message;\n if (!(args[0] instanceof Message)) {\n message = new this.connection.Request(...args);\n } else {\n message = args[0];\n }\n\n if (!(message instanceof this.connection.Message)) {\n throw new Error(`Connection.send: The provided message is an instance of protocol.message but it did not originate from this connection. This connection has id ${this.connection.id}, but the connection associated with the message you used has id ${message.connection.id}.`);\n } else if (message instanceof this.connection.Request) {\n message.id = this.generateMessageId();\n } else if (message instanceof this.connection.Response) {\n if (!message.id) throw new Error(\"Connection.send: Cannot send a response that is not correlated to a request (it must have an ID)\");\n }\n\n return message;\n }\n}\n\nObject.assign(module.exports, {\n MessageFactory,\n});\n\n\n//# sourceURL=webpack://dcp/./src/protocol-v4/connection/message-factory.js?");
4579
-
4580
- /***/ }),
4581
-
4582
4581
  /***/ "./src/protocol-v4/connection/message-ledger.js":
4583
4582
  /*!******************************************************!*\
4584
4583
  !*** ./src/protocol-v4/connection/message-ledger.js ***!
@@ -4607,7 +4606,7 @@ eval("/**\n * @file protocol/connection/receiver.js\n * @author Ryan
4607
4606
  /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
4608
4607
 
4609
4608
  "use strict";
4610
- 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.options.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?");
4609
+ 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.options.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' && this.payload.operation !== null)\n {\n throw new Error('Invalid Request operation');\n }\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 method is used by the connection to create a request with an id that it will associate with its response.\n * @param {...any} args The arguments supplied to the request constructor, or an instance of request that needs an id.\n * @returns {Request} an instance of Connection.Request with an id property.\n */\n static buildMessage(...args)\n {\n let message;\n if (args[0] instanceof this)\n message = args[0];\n else\n message = new this(...args);\n message.id = this.connection.generateMessageId();\n return message;\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?");
4611
4610
 
4612
4611
  /***/ }),
4613
4612
 
@@ -4672,7 +4671,7 @@ eval("/**\n * @file protocol/message.js\n * @author Ryan Rossiter, r
4672
4671
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4673
4672
 
4674
4673
  "use strict";
4675
- eval("/**\n * @file connection/target.js\n * @author Wes Garland, wes@kingsds.network\n * @date March 2022\n */\n\n\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\n\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { PseudoExpress } = __webpack_require__(/*! ../node-libs/pseudo-express */ \"./src/node-libs/pseudo-express.js\");\nconst { Connection } = __webpack_require__(/*! ./connection */ \"./src/protocol-v4/connection/index.js\");\nconst { Message } = __webpack_require__(/*! ./message */ \"./src/protocol-v4/message.js\");\nconst { Listener } = __webpack_require__(/*! ./transport/listener */ \"./src/protocol-v4/transport/listener.js\");\nconst bearer = __webpack_require__(/*! ./transport/http-bearer */ \"./src/protocol-v4/transport/http-bearer.js\");\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('dcp');\nvar seqNum = 0;\n\n/**\n * @constructor\n * Instanciate a new Target. Targets listen for connections at the transport layer, and turns them into DCP Connections.\n *\n * @emits bearer-listening when we are listening on a given bearer; argument is underlying bearer Server instance\n * @emits listening when we are listening on at least one transport\n * @emits error when bad things happen\n *\n * @param {string} transportNames An array of names of transports, [\"socketio\", \"webrtc\"] to use to create\n * this Target.\n * @param {object} config must have .identity and .location\n * @param {function} listeningHandler [optional] function to invoke once the transport is listening. Takes name of the\n * transport and server instance associated with the event.\n * @param {object} options [optional] A DCP config options variable (like dcpConfig.dcp.listenOptions)\n */\nexports.Target = function Target(transportNames, config, listeningHandler, options)\n{\n const that = this;\n var initialTransportInstanceQueue = [];\n var idKs_unlocked = false; /* unlocked ks needed for daemon operation */\n var listening = false;\n var maintenanceIntervalMs = options && options.connectionSweepIntervalMs || 600000;\n\n assert(config, config.identity, config.location);\n\n if (config.identity instanceof Promise)\n config.identity.then(memoizeUnlockedIdentityKS);\n else\n memoizeUnlockedIdentityKS(config.identity);\n\n this.debugLabel = `target(${transportNames.join(' ')}:${++seqNum})`;\n this.connectionMemos = [];\n this.url = config.location;\n this.options = options;\n\n /* Wire up bearers - shared amongst compatible transport listeners */\n this.httpServer = bearer.httpServerFactory(config, (server) => { this.emit('bearer-listening', server )});\n Object.assign(this, new PseudoExpress());\n \n /* Wire up transport listeners */\n this.listeners = [];\n for (let transportName of transportNames)\n {\n const listener = new Listener(transportName, config, this, options);\n listener.on('connection', startupTransportConnectionHandler);\n listener.on('listening', (server) => startupListeningHandler(transportName, server));\n this.listeners.push(listener);\n }\n\n this.bindServer(this.httpServer);\n\n /* Wire up /status listeners - must happen after transport listeners hook httpServer */\n const statusUrl = new DcpURL(config.location).resolveUrl('./status');\n this.get(statusUrl.pathname, peShowStatus);\n this.get('/status', peShowStatus); /* outer server - not usually available via proxy */\n \n /* store an unlocked version of the idKs in instance-private memory for use by the Connection */\n async function memoizeUnlockedIdentityKS(idKs)\n {\n const pk = await idKs.getPrivateKey();\n const ks = await new wallet.Keystore(pk, '');\n\n idKs_unlocked = ks;\n\n if (listening)\n rockAndRoll();\n }\n\n /* queue up transport connnections until a listening event handler has fired and idKs promise resolved */\n function startupTransportConnectionHandler(transport)\n {\n debugging('target') && console.debug(that.debugLabel, 'startupTransportConnectionHandler; idKs unlocked:', Boolean(idKs_unlocked) + '; listening:', listening);\n\n if (!idKs_unlocked || !listening)\n initialTransportInstanceQueue.push(transport);\n else\n {\n rockAndRoll();\n that.handleNewTransportInstance.apply(transport);\n }\n }\n\n /* handle listening event for each transport as it comes up */\n function startupListeningHandler(transportName, listeningServer)\n {\n debugging('target') && console.debug(that.debugLabel, 'startupListeningHandler; idKs unlocked:', Boolean(idKs_unlocked));\n \n if (listeningHandler)\n listeningHandler(transportName, listeningServer);\n \n listening = true;\n if (idKs_unlocked)\n rockAndRoll();\n }\n\n /**\n * Listening event has fired for at least one transport, identity has resolved; target is fully up, \n * event order has been synchronized with possible blast of incoming connections, get rid of the\n * intermediate events and get to work.\n */\n function rockAndRoll()\n {\n var queuedTransport;\n\n if (!rockAndRoll.ran)\n {\n for (let listener of that.listeners)\n {\n listener.off('connection', startupTransportConnectionHandler);\n listener.on ('connection', (transport) => that.handleNewTransportInstance(transport));\n }\n rockAndRoll.ran = true;\n }\n\n that.identity = idKs_unlocked;\n debugging('target') && console.debug(that.debugLabel, 'identity is', that.identity.address);\n\n // eslint-disable-next-line no-unmodified-loop-condition\n while (initialTransportInstanceQueue && (queuedTransport = initialTransportInstanceQueue.shift()))\n that.handleNewTransportInstance(queuedTransport);\n initialTransportInstanceQueue = null; /* detect push races, free RAM */\n that.emit('listening', that);\n }\n\n /* If it takes more than 5s to start up, we have a very serious problem */\n setTimeout(() => {\n if (rockAndRoll.ran)\n return;\n const listeningError = new Error('unable to start listener');\n this.emit('error', listeningError);\n debugging('target') && console.debug(that.debugLabel, listeningError);\n }, 5000).unref();\n\n /* Occasionally sweep the connection list and clean up garbage. */\n this.maintenanceIntervalHnd = setInterval(() => this.doConnectionMaintenance(), maintenanceIntervalMs);\n this.maintenanceIntervalHnd.unref();\n}\nexports.Target.prototype = new EventEmitter('Target');\nObject.assign(exports.Target.prototype, PseudoExpress.prototype);\n\n/**\n * API to shutdown the target and its currently-active connections.\n */\nexports.Target.prototype.close = function Target$close()\n{\n clearInterval(this.maintenanceIntervalHnd);\n let numConnectionsToClose = 0, numConnectionsClosed = 0;\n \n debugging('target') && console.debug(this.debugLabel, `closing ${this.getSessions('established').length} established and ${this.getSessions('disconnected').length} sessions`);\n for (const dcpsid in this.connectionMemos)\n {\n let conn = this.connectionMemos[dcpsid].connection;\n numConnectionsToClose++;\n conn.close('server shutting down', true);\n conn.on('end', () => { numConnectionsClosed++; tryShutdown(); })\n }\n \n const that = this;\n function tryShutdown()\n {\n // Not done closing connections\n if (numConnectionsClosed !== numConnectionsToClose)\n return;\n\n for (let listener of that.listeners)\n listener.close();\n \n that.listeners.length = 0;\n that.unref();\n }\n}\n\n/**\n * Internal callback which is invoked whenever a new transport instance is created\n */\nexports.Target.prototype.handleNewTransportInstance = function handleNewTransportInstance(transport)\n{\n const that = this;\n \n debugging('target') && console.debug(this.debugLabel, 'adding new transport instance');\n\n /* new transport instances need their messages intercepted at this layer so that \n * we can figure out if they are new DCP connections or simply reconnections.\n */\n function messageHandler(message)\n {\n void that.a$interceptTransportMessage(message, transport, messageHandler);\n }\n\n transport.on('message', messageHandler);\n}\n\n/**\n * Internal method that memoizes a session for later use by the reconnect code.\n */\nexports.Target.prototype.memoizeSession = function memoizeSession(connection)\n{\n assert(connection.dcpsid);\n assert(!this.connectionMemos.hasOwnProperty(connection.dcpsid));\n\n this.connectionMemos[connection.dcpsid] = { connection, lastMessageStampMs: performance.now() };\n debugging('target') && console.debug(this.debugLabel, 'target registered new session', connection.dcpsid);\n}\n\n/**\n * Internal method invoked when a connection connects\n */\nexports.Target.prototype.noteConnectEvent = function noteConnectEvent(connection)\n{\n const cm = this.connectionMemos[connection.dcpsid];\n assert(connection.dcpsid);\n \n if (!cm)\n {\n debugging('target') && console.debug(this.debugLabel, `target connected after session ${connection.dcpsid} culled`);\n connection.close();\n return;\n }\n\n cm.disconnectTimeStamp = false;\n}\n\n/**\n * Internal method invoked when a connection closes\n */\nexports.Target.prototype.noteCloseEvent = function noteCloseEvent(connection)\n{\n delete this.connectionMemos[connection.dcpsid];\n}\n \n/**\n * Internal method invoked when a connection disconnects\n */\nexports.Target.prototype.noteDisconnectEvent = function noteDisconnectEvent(connection)\n{\n const cm = this.connectionMemos[connection.dcpsid];\n assert(connection.dcpsid);\n\n if (!cm)\n {\n debugging('target') && console.debug(this.debugLabel, `target disconnected after session ${connection.dcpsid} culled`);\n connection.close();\n return;\n }\n\n cm.disconnectTimeStamp = performance.now();\n}\n\n/**\n * Handle messages coming from the transport by associating them with an instance of Connection\n * and then having the Connection handle them. It's possible that we might multi-trigger on\n * messages arriving in a flurry just as we connect; this is due to an architecture flaw where\n * we don't really have a way to know what connection a given message is for until it has sent\n * a DCP-level message; either a connect message or a message with an existing dcpsid. Multi-trigger\n * should just get picked up by the dup detection.\n *\n * @param {string} rawMessage JSON-encoded DCP message\n * @param {object} transport a transport instance which is a child of this.transportListener\n * @param {function} messageHandler the function which is the transport message event handler that\n * called this function.\n */\nexports.Target.prototype.a$interceptTransportMessage = async function Target$interceptTransportMessage(rawMessage, transport, messageHandler)\n{\n var message;\n\n /* keep a memo on the transport instance so we don't accidentally try to create multiple connections for it */\n if (transport._targetInfo)\n transport._targetInfo.interceptCount++;\n else\n {\n transport._targetInfo = { interceptCount: 1 };\n debugging('target') && console.debug(this.debugLabel,`target intercepted ${rawMessage.length}-byte message #${transport._targetInfo.interceptCount}`);\n\n transport.off('message', messageHandler);\n try\n {\n message = JSON.parse(rawMessage);\n }\n catch (error)\n {\n console.error(`interceptTransportMessage: message #${transport._targetInfo.interceptCount} is not valid JSON (${String(rawMessage).slice(0,15)}...):`, error);\n transport.close();\n return;\n }\n\n if (typeof message !== 'object' || !message.hasOwnProperty('body'))\n {\n console.error(`interceptTransportMessage: no body in message #${transport._targetInfo.interceptCount} (${String(rawMessage).slice(0,15)}...)`);\n transport.close(true);\n return;\n }\n\n /* Sometimes, this transport has already been associated with a session, but messages were dispatched\n * before the intercept event handler was disabled. We pass them along to the connection either way,\n * but only try to do transport<>connection association for the first message. That is the *ONLY* \n * reason this code should ever look at _targetInfo.connection.\n */\n if (!transport._targetInfo.connection)\n {\n if (message.body.dcpsid)\n transport._targetInfo.connection = this.locateExistingConnection(transport, message);\n else\n transport._targetInfo.connection = await this.initializeNewConnection(transport, message); // eslint-disable-line require-atomic-updates\n }\n\n if (!transport._targetInfo.connection)\n {\n /* function return false has sent a ConnectionlessErrorResponse back at this point. */\n debugging('target') && console.error(`${this.debugLabel} interceptTransportMessage: invalid message #${transport._targetInfo.interceptCount} (${String(rawMessage).slice(0,15)}...)`);\n return;\n }\n }\n\n /* Message was intercepted, now we should have a Connection ready to handle it - handle\n * it immediately, so that it does not accidentally get handled out of order. The await\n * in the previous block will have allowed the 'connection' event to fire and the \n * identityPromise to resolve by now for new connections.\n */\n transport._targetInfo.connection.handleMessage(message);\n}\n\n/**\n * Initializes a Connection object for use by a target, based on a message from the transport layer.\n * This should be the first message from the transport connection (socket), and it must be a message\n * which contains a DCP 'connect' request.\n *\n * If the message is unsuitable for bootstrapping a Connection, a response message will be written \n * back on the transport instance and the transport will be closed.\n *\n * @param {object} transport the instance of Transport which originated the message\n * @param {object} transportMessage a DCP message, from the transport layer, which has been parsed\n * from JSON into an object but not otherwise modified.\n *\n * @returns - Connection when the message is suitable for establishing a Connection, or \n * - false when it is not; in that case the transport will be closed eventually\n * once an error response is sent, or it takes too long to send.\n */\nexports.Target.prototype.initializeNewConnection = function Target$initializeConnection(transport, transportMessage)\n{\n var message;\n var connection;\n\n if (!transportMessage.body || transportMessage.body.type !== 'batch')\n message = transportMessage;\n else\n {\n const messages = transportMessage.body && transportMessage.body.payload;\n if (!messages || !messages.length)\n return this.sendErrorResponse(transport, 'invalid message - empty batch', 'ENOBODY', transportMessage);\n message = messages[0];\n }\n\n if (!message.body)\n return this.sendErrorResponse(transport, 'invalid message - no body', 'ENOBODY', transportMessage);\n \n if (message.body.payload && message.body.payload.operation !== 'connect')\n return this.sendErrorResponse(transport, 'no such connection', 'ENODCPSID', transportMessage);\n\n /* At this point, we have a transport message for a brand new DCP connection */\n connection = new Connection(this.url, this.identity, this.options);\n connection.on('session', () => this.memoizeSession (connection));\n connection.on('connect', () => this.noteConnectEvent (connection));\n connection.on('disconnect', () => this.noteDisconnectEvent(connection));\n connection.on('close', () => this.noteCloseEvent (connection));\n connection.accept(transport).then(() => this.emit('connection', connection));\n\n return connection;\n}\n\n/**\n * @param {object} transport a transport instance which is a child of this.transportListener\n * @param {string} dcpsid DCP session id sent in the first message from the peer\n *\n * @returns instance of Connection or false\n */\nexports.Target.prototype.locateExistingConnection = function Target$locateExistingConnection(transport, message)\n{\n const dcpsid = message.body.dcpsid;\n var cm;\n\n assert(transport, dcpsid);\n this.doConnectionMaintenance();\n cm = this.connectionMemos[dcpsid];\n \n if (!cm)\n {\n debugging('target') && console.debug(this.debugLabel, `target received message after session ${dcpsid} culled or target restarted`);\n return this.sendErrorResponse(transport, 'invalid session ' + dcpsid, 'ENODCPSID', message);\n }\n\n debugging('target') && console.debug(this.debugLabel, 'message is for session', dcpsid);\n if (cm.connection.transport)\n cm.connection.transport.close();\n cm.connection.useNewTransport(transport);\n\n return cm.connection;\n}\n\n/**\n * Retrieve a list of connections' dcpsids and their statuses.\n * @param {string} connectionStatus [optional] connection.status to filter on\n * @returns {Array} list of { dcpsid, status }\n */\nexports.Target.prototype.getSessions = function Target$$getSessions(connectionStatus)\n{\n var sessions = [];\n \n for (let dcpsid in this.connectionMemos)\n {\n if (!connectionStatus || this.connectionMemos[dcpsid].connection.state.is(connectionStatus))\n {\n const session = { dcpsid, status: this.connectionMemos[dcpsid].status };\n if (dcpConfig.build === 'debug')\n session.__connection = this.connectionMemos[dcpsid]; /* troubleshooting only! */\n sessions.push(session);\n }\n }\n\n return sessions;\n}\n\n/**\n * Remove all connections from the target's memo list that have not have been\n * disconnected for more than lingerTimeout seconds.\n */\nexports.Target.prototype.doConnectionMaintenance = function Target$doConnectionMaintenance()\n{\n const now = performance.now();\n\n debugging('target') && console.debug(this.debugLabel, 'performing connection maintenance');\n for (let dcpsid in this.connectionMemos)\n {\n const cm = this.connectionMemos[dcpsid];\n if (!cm.disconnectTimeStamp)\n continue;\n\n const timeoutMs = cm.connection.options.lingerTimeout * 1000;\n if (now - cm.disconnectTimeStamp > timeoutMs)\n {\n debugging('target') && console.debug(`${this.debugLabel} culling session ${dcpsid}`)\n cm.connection.close();\n delete this.connectionMemos[dcpsid];\n }\n }\n}\n\n/** Remove any event loop references used by this target */\nexports.Target.prototype.unref = function Target$unref()\n{\n for (let listener of this.listeners)\n {\n if (listener.unref)\n listener.unref();\n }\n if (this.httpServer)\n this.httpServer.unref();\n}\n\n/**\n * A class for DCP-like messages that do not have an associated session.\n */\nclass ConnectionlessErrorResponse extends Message\n{\n #idKs;\n \n constructor(identity, errorMessage, errorCode, relatedMessage)\n {\n super('Connectionless Error Response');\n \n if (relatedMessage && relatedMessage.body.id)\n {\n this.id = relatedMessage.body.id;\n this.nonce = relatedMessage.body.nonce;\n this.dcpsid = relatedMessage.body.dcpsid;\n }\n\n this.success = false;\n this.time = Date.now();\n this.owner = identity.address;\n this.#idKs = identity;\n this.payload = {\n message: errorMessage,\n code: errorCode,\n };\n }\n \n toJSON()\n {\n return {\n ...super.toJSON(),\n id: this.id,\n nonce: this.nonce,\n dcpsid: this.dcpsid,\n type: 'unhandled-message'\n };\n }\n\n sign() /* async */\n {\n return this.#idKs.makeSignedMessage(this.toJSON());\n }\n}\n\nexports.Target.prototype.sendErrorResponse = function sendErrorResponse(transport, errorMessage, code, relatedMessage)\n{\n debugging('target') && console.debug(`${this.debugLabel} got an invalid message, sending back ConnectionLessErrorResponse with error message: ${errorMessage}`)\n const timer = setTimeout(() => transport.close(), 5000); /* keep lingering connections from chewing up resources on super-slow networks */\n \n const response = new ConnectionlessErrorResponse(this.identity, errorMessage, code, relatedMessage);\n response.sign().then((signedResponse) => transport.send(signedResponse))\n transport.on('drain', () => { transport.close(); clearTimeout(timer) });\n \n return false;\n}\n\n/**\n * A handler for PseudoExpress which displays the status of the Target's process\n * @returns false\n */\nfunction peShowStatus(request, response)\n{\n const process = __webpack_require__(/*! process */ \"./node_modules/process/browser.js\");\n const os = __webpack_require__(/*! os */ \"./node_modules/os-browserify/browser.js\");\n\n function fancy(object)\n {\n const padLen = 4 + Object.keys(object).reduce((curMax, key) => key.length > curMax ? key.length : curMax, 0);\n return '\\n' + Object.entries(object).map(([key, value]) => (' ' + key + ': ').padEnd(padLen) + value).join('\\n');\n }\n\n response.status = 200;\n response.set('content-type', 'text/plain; charset=utf8');\n response.set('cache-control', 'no-cache');\n \n const output = `${new Date()}\\n\nrequest: ${request.method} ${request.url}\nprogram: ${process.argv[1]}\npid: ${process.pid}\ndebugPort: ${process.debugPort}\nhostname: ${request.hostname} aka ${os.hostname()}\nuptime: ${process.uptime()}\nloadavg: ${os.loadavg()}\nresources: ${fancy(process.resourceUsage())}\nmemory: ${fancy(process.memoryUsage())}\ntotalmem: ${os.totalmem()}\nheaders: ${fancy(request.headers)}\n\nbody: \\n${request.body || ''}\n`;\n\n response.send(output);\n}\n\n\n//# sourceURL=webpack://dcp/./src/protocol-v4/target.js?");
4674
+ eval("/**\n * @file connection/target.js\n * @author Wes Garland, wes@kingsds.network\n * @date March 2022\n */\n\n\nconst wallet = __webpack_require__(/*! dcp/dcp-client/wallet */ \"./src/dcp-client/wallet/index.js\");\n\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { assert } = __webpack_require__(/*! dcp/common/dcp-assert */ \"./src/common/dcp-assert.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { PseudoExpress } = __webpack_require__(/*! ../node-libs/pseudo-express */ \"./src/node-libs/pseudo-express.js\");\nconst { Connection } = __webpack_require__(/*! ./connection */ \"./src/protocol-v4/connection/index.js\");\nconst { Message } = __webpack_require__(/*! ./message */ \"./src/protocol-v4/message.js\");\nconst { Listener } = __webpack_require__(/*! ./transport/listener */ \"./src/protocol-v4/transport/listener.js\");\nconst bearer = __webpack_require__(/*! ./transport/http-bearer */ \"./src/protocol-v4/transport/http-bearer.js\");\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('dcp');\nvar seqNum = 0;\n\n/**\n * @constructor\n * Instanciate a new Target. Targets listen for connections at the transport layer, and turns them into DCP Connections.\n *\n * @emits bearer-listening when we are listening on a given bearer; argument is underlying bearer Server instance\n * @emits listening when we are listening on at least one transport\n * @emits error when bad things happen\n *\n * @param {string} transportNames An array of names of transports, [\"socketio\", \"webrtc\"] to use to create\n * this Target.\n * @param {object} config must have .identity and .location\n * @param {function} listeningHandler [optional] function to invoke once the transport is listening. Takes name of the\n * transport and server instance associated with the event.\n * @param {object} options [optional] A DCP config options variable (like dcpConfig.dcp.listenOptions)\n */\nexports.Target = function Target(transportNames, config, listeningHandler, options)\n{\n const that = this;\n var initialTransportInstanceQueue = [];\n var idKs_unlocked = false; /* unlocked ks needed for daemon operation */\n var listening = false;\n var maintenanceIntervalMs = options && options.connectionSweepIntervalMs || 600000;\n\n assert(config, config.identity, config.location);\n\n if (config.identity instanceof Promise)\n config.identity.then(memoizeUnlockedIdentityKS);\n else\n memoizeUnlockedIdentityKS(config.identity);\n\n this.debugLabel = `target(${transportNames.join(' ')}:${++seqNum})`;\n this.connectionMemos = [];\n this.url = config.location;\n this.options = options;\n\n /* Wire up bearers - shared amongst compatible transport listeners */\n this.httpServer = bearer.httpServerFactory(config, (server) => { this.emit('bearer-listening', server )});\n Object.assign(this, new PseudoExpress());\n \n /* Wire up transport listeners */\n this.listeners = [];\n for (let transportName of transportNames)\n {\n const listener = new Listener(transportName, config, this, options);\n listener.on('connection', startupTransportConnectionHandler);\n listener.on('listening', (server) => startupListeningHandler(transportName, server));\n this.listeners.push(listener);\n }\n\n this.bindServer(this.httpServer);\n\n /* Wire up /status listeners - must happen after transport listeners hook httpServer */\n const statusUrl = new DcpURL(config.location).resolveUrl('./status');\n this.get(statusUrl.pathname, peShowStatus);\n this.get('/status', peShowStatus); /* outer server - not usually available via proxy */\n \n /* store an unlocked version of the idKs in instance-private memory for use by the Connection */\n async function memoizeUnlockedIdentityKS(idKs)\n {\n debugging('target') && console.debug(that.debugLabel, 'memoizeUnlockedIdentityKs: identity keystore unlocked. listening:', listening);\n\n if (!(idKs instanceof wallet.Keystore))\n throw new Error('Identity Keystore is not an instance of wallet.Keystore');\n\n \n const pk = await idKs.getPrivateKey();\n const ks = await new wallet.Keystore(pk, '');\n\n idKs_unlocked = ks;\n\n if (listening)\n rockAndRoll();\n }\n\n /* queue up transport connnections until a listening event handler has fired and idKs promise resolved */\n function startupTransportConnectionHandler(transport)\n {\n debugging('target') && console.debug(that.debugLabel, 'startupTransportConnectionHandler; idKs unlocked:', Boolean(idKs_unlocked) + '; listening:', listening);\n\n if (!idKs_unlocked || !listening)\n initialTransportInstanceQueue.push(transport);\n else\n {\n rockAndRoll();\n that.handleNewTransportInstance.apply(transport);\n }\n }\n\n /* handle listening event for each transport as it comes up */\n function startupListeningHandler(transportName, listeningServer)\n {\n debugging('target') && console.debug(that.debugLabel, 'startupListeningHandler; idKs unlocked:', Boolean(idKs_unlocked));\n \n if (listeningHandler)\n listeningHandler(transportName, listeningServer);\n \n listening = true;\n if (idKs_unlocked)\n rockAndRoll();\n }\n\n /**\n * Listening event has fired for at least one transport, identity has resolved; target is fully up, \n * event order has been synchronized with possible blast of incoming connections, get rid of the\n * intermediate events and get to work.\n */\n function rockAndRoll()\n {\n var queuedTransport;\n\n if (!rockAndRoll.ran)\n {\n for (let listener of that.listeners)\n {\n listener.off('connection', startupTransportConnectionHandler);\n listener.on ('connection', (transport) => that.handleNewTransportInstance(transport));\n }\n rockAndRoll.ran = true;\n }\n\n that.identity = idKs_unlocked;\n debugging('target') && console.debug(that.debugLabel, 'identity is', that.identity.address);\n\n // eslint-disable-next-line no-unmodified-loop-condition\n while (initialTransportInstanceQueue && (queuedTransport = initialTransportInstanceQueue.shift()))\n that.handleNewTransportInstance(queuedTransport);\n initialTransportInstanceQueue = null; /* detect push races, free RAM */\n that.emit('listening', that);\n }\n\n /* If it takes more than 5s to start up, we have a very serious problem */\n setTimeout(() => {\n if (rockAndRoll.ran)\n return;\n const listeningError = new Error('unable to start listener');\n this.emit('error', listeningError);\n debugging('target') && console.debug(that.debugLabel, listeningError);\n }, 5000).unref();\n\n /* Occasionally sweep the connection list and clean up garbage. */\n this.maintenanceIntervalHnd = setInterval(() => this.doConnectionMaintenance(), maintenanceIntervalMs);\n this.maintenanceIntervalHnd.unref();\n}\nexports.Target.prototype = new EventEmitter('Target');\nObject.assign(exports.Target.prototype, PseudoExpress.prototype);\n\n/**\n * API to shutdown the target and its currently-active connections.\n */\nexports.Target.prototype.close = function Target$close()\n{\n clearInterval(this.maintenanceIntervalHnd);\n let numConnectionsToClose = 0, numConnectionsClosed = 0;\n \n debugging('target') && console.debug(this.debugLabel, `closing ${this.getSessions('established').length} established and ${this.getSessions('disconnected').length} sessions`);\n for (const dcpsid in this.connectionMemos)\n {\n let conn = this.connectionMemos[dcpsid].connection;\n numConnectionsToClose++;\n conn.close('server shutting down', true);\n conn.on('end', () => { numConnectionsClosed++; tryShutdown(); })\n }\n \n const that = this;\n function tryShutdown()\n {\n // Not done closing connections\n if (numConnectionsClosed !== numConnectionsToClose)\n return;\n\n for (let listener of that.listeners)\n listener.close();\n \n that.listeners.length = 0;\n that.unref();\n }\n}\n\n/**\n * Internal callback which is invoked whenever a new transport instance is created\n */\nexports.Target.prototype.handleNewTransportInstance = function handleNewTransportInstance(transport)\n{\n const that = this;\n \n debugging('target') && console.debug(this.debugLabel, 'adding new transport instance');\n\n /* new transport instances need their messages intercepted at this layer so that \n * we can figure out if they are new DCP connections or simply reconnections.\n */\n function messageHandler(message)\n {\n void that.a$interceptTransportMessage(message, transport, messageHandler);\n }\n\n transport.on('message', messageHandler);\n \n /* Transport may have pending messages it didn't emit as there was no listener. Try here. */\n transport.emitReadyMessages();\n}\n\n/**\n * Internal method that memoizes a session for later use by the reconnect code.\n */\nexports.Target.prototype.memoizeSession = function memoizeSession(connection)\n{\n assert(connection.dcpsid);\n assert(!this.connectionMemos.hasOwnProperty(connection.dcpsid));\n\n this.connectionMemos[connection.dcpsid] = { connection, lastMessageStampMs: performance.now() };\n debugging('target') && console.debug(this.debugLabel, 'target registered new session', connection.dcpsid);\n}\n\n/**\n * Internal method invoked when a connection connects\n */\nexports.Target.prototype.noteConnectEvent = function noteConnectEvent(connection)\n{\n const cm = this.connectionMemos[connection.dcpsid];\n assert(connection.dcpsid);\n \n if (!cm)\n {\n debugging('target') && console.debug(this.debugLabel, `target connected after session ${connection.dcpsid} culled`);\n connection.close();\n return;\n }\n\n cm.disconnectTimeStamp = false;\n}\n\n/**\n * Internal method invoked when a connection closes\n */\nexports.Target.prototype.noteCloseEvent = function noteCloseEvent(connection)\n{\n delete this.connectionMemos[connection.dcpsid];\n}\n \n/**\n * Internal method invoked when a connection disconnects\n */\nexports.Target.prototype.noteDisconnectEvent = function noteDisconnectEvent(connection)\n{\n const cm = this.connectionMemos[connection.dcpsid];\n assert(connection.dcpsid);\n\n if (!cm)\n {\n debugging('target') && console.debug(this.debugLabel, `target disconnected after session ${connection.dcpsid} culled`);\n connection.close();\n return;\n }\n\n cm.disconnectTimeStamp = performance.now();\n}\n\n/**\n * Handle messages coming from the transport by associating them with an instance of Connection\n * and then having the Connection handle them. It's possible that we might multi-trigger on\n * messages arriving in a flurry just as we connect; this is due to an architecture flaw where\n * we don't really have a way to know what connection a given message is for until it has sent\n * a DCP-level message; either a connect message or a message with an existing dcpsid. Multi-trigger\n * should just get picked up by the dup detection.\n *\n * @param {string} rawMessage JSON-encoded DCP message\n * @param {object} transport a transport instance which is a child of this.transportListener\n * @param {function} messageHandler the function which is the transport message event handler that\n * called this function.\n */\nexports.Target.prototype.a$interceptTransportMessage = async function Target$interceptTransportMessage(rawMessage, transport, messageHandler)\n{\n var message;\n\n /* keep a memo on the transport instance so we don't accidentally try to create multiple connections for it */\n if (transport._targetInfo)\n transport._targetInfo.interceptCount++;\n else\n {\n transport._targetInfo = { interceptCount: 1 };\n debugging('target') && console.debug(this.debugLabel,`target intercepted ${rawMessage.length}-byte message #${transport._targetInfo.interceptCount}`);\n\n transport.off('message', messageHandler);\n try\n {\n message = JSON.parse(rawMessage);\n }\n catch (error)\n {\n console.error(`interceptTransportMessage: message #${transport._targetInfo.interceptCount} is not valid JSON (${String(rawMessage).slice(0,15)}...):`, error);\n transport.close();\n return;\n }\n\n if (typeof message !== 'object' || !message.hasOwnProperty('body'))\n {\n console.error(`interceptTransportMessage: no body in message #${transport._targetInfo.interceptCount} (${String(rawMessage).slice(0,15)}...)`);\n transport.close(true);\n return;\n }\n\n /* Sometimes, this transport has already been associated with a session, but messages were dispatched\n * before the intercept event handler was disabled. We pass them along to the connection either way,\n * but only try to do transport<>connection association for the first message. That is the *ONLY* \n * reason this code should ever look at _targetInfo.connection.\n */\n if (!transport._targetInfo.connection)\n {\n if (message.body.dcpsid)\n transport._targetInfo.connection = this.locateExistingConnection(transport, message);\n else\n transport._targetInfo.connection = await this.initializeNewConnection(transport, message); // eslint-disable-line require-atomic-updates\n }\n\n if (!transport._targetInfo.connection)\n {\n /* function return false has sent a ConnectionlessErrorResponse back at this point. */\n debugging('target') && console.error(`${this.debugLabel} interceptTransportMessage: invalid message #${transport._targetInfo.interceptCount} (${String(rawMessage).slice(0,15)}...)`);\n return;\n }\n }\n\n /* Message was intercepted, now we should have a Connection ready to handle it - handle\n * it immediately, so that it does not accidentally get handled out of order. The await\n * in the previous block will have allowed the 'connection' event to fire and the \n * identityPromise to resolve by now for new connections.\n */\n transport._targetInfo.connection.handleMessage(message);\n}\n\n/**\n * Initializes a Connection object for use by a target, based on a message from the transport layer.\n * This should be the first message from the transport connection (socket), and it must be a message\n * which contains a DCP 'connect' request.\n *\n * If the message is unsuitable for bootstrapping a Connection, a response message will be written \n * back on the transport instance and the transport will be closed.\n *\n * @param {object} transport the instance of Transport which originated the message\n * @param {object} transportMessage a DCP message, from the transport layer, which has been parsed\n * from JSON into an object but not otherwise modified.\n *\n * @returns - Connection when the message is suitable for establishing a Connection, or \n * - false when it is not; in that case the transport will be closed eventually\n * once an error response is sent, or it takes too long to send.\n */\nexports.Target.prototype.initializeNewConnection = function Target$initializeConnection(transport, transportMessage)\n{\n var message;\n var connection;\n\n if (!transportMessage.body || transportMessage.body.type !== 'batch')\n message = transportMessage;\n else\n {\n const messages = transportMessage.body && transportMessage.body.payload;\n if (!messages || !messages.length)\n return this.sendErrorResponse(transport, 'invalid message - empty batch', 'ENOBODY', transportMessage);\n message = messages[0];\n }\n\n if (!message.body)\n return this.sendErrorResponse(transport, 'invalid message - no body', 'ENOBODY', transportMessage);\n \n if (message.body.payload && message.body.payload.operation !== 'connect')\n return this.sendErrorResponse(transport, 'no such connection', 'ENODCPSID', transportMessage);\n\n /* At this point, we have a transport message for a brand new DCP connection */\n connection = new Connection(this.url, this.identity, this.options);\n connection.on('session', () => this.memoizeSession (connection));\n connection.on('connect', () => this.noteConnectEvent (connection));\n connection.on('disconnect', () => this.noteDisconnectEvent(connection));\n connection.on('close', () => this.noteCloseEvent (connection));\n connection.accept(transport).then(() => this.emit('connection', connection));\n\n return connection;\n}\n\n/**\n * @param {object} transport a transport instance which is a child of this.transportListener\n * @param {string} dcpsid DCP session id sent in the first message from the peer\n *\n * @returns instance of Connection or false\n */\nexports.Target.prototype.locateExistingConnection = function Target$locateExistingConnection(transport, message)\n{\n const dcpsid = message.body.dcpsid;\n var cm;\n\n assert(transport, dcpsid);\n this.doConnectionMaintenance();\n cm = this.connectionMemos[dcpsid];\n \n if (!cm)\n {\n debugging('target') && console.debug(this.debugLabel, `target received message after session ${dcpsid} culled or target restarted`);\n return this.sendErrorResponse(transport, 'invalid session ' + dcpsid, 'ENODCPSID', message);\n }\n\n debugging('target') && console.debug(this.debugLabel, 'message is for session', dcpsid);\n if (cm.connection.transport)\n cm.connection.transport.close();\n cm.connection.useNewTransport(transport);\n\n return cm.connection;\n}\n\n/**\n * Retrieve a list of connections' dcpsids and their statuses.\n * @param {string} connectionStatus [optional] connection.status to filter on\n * @returns {Array} list of { dcpsid, status }\n */\nexports.Target.prototype.getSessions = function Target$$getSessions(connectionStatus)\n{\n var sessions = [];\n \n for (let dcpsid in this.connectionMemos)\n {\n if (!connectionStatus || this.connectionMemos[dcpsid].connection.state.is(connectionStatus))\n {\n const session = { dcpsid, status: this.connectionMemos[dcpsid].status };\n if (dcpConfig.build === 'debug')\n session.__connection = this.connectionMemos[dcpsid]; /* troubleshooting only! */\n sessions.push(session);\n }\n }\n\n return sessions;\n}\n\n/**\n * Remove all connections from the target's memo list that have not have been\n * disconnected for more than lingerTimeout seconds.\n */\nexports.Target.prototype.doConnectionMaintenance = function Target$doConnectionMaintenance()\n{\n const now = performance.now();\n\n debugging('target') && console.debug(this.debugLabel, 'performing connection maintenance');\n for (let dcpsid in this.connectionMemos)\n {\n const cm = this.connectionMemos[dcpsid];\n if (!cm.disconnectTimeStamp)\n continue;\n\n const timeoutMs = cm.connection.options.lingerTimeout * 1000;\n if (now - cm.disconnectTimeStamp > timeoutMs)\n {\n debugging('target') && console.debug(`${this.debugLabel} culling session ${dcpsid}`)\n cm.connection.close();\n delete this.connectionMemos[dcpsid];\n }\n }\n}\n\n/** Remove any event loop references used by this target */\nexports.Target.prototype.unref = function Target$unref()\n{\n for (let listener of this.listeners)\n {\n if (listener.unref)\n listener.unref();\n }\n if (this.httpServer)\n this.httpServer.unref();\n}\n\n/**\n * A class for DCP-like messages that do not have an associated session.\n */\nclass ConnectionlessErrorResponse extends Message\n{\n #idKs;\n \n constructor(identity, errorMessage, errorCode, relatedMessage)\n {\n super('Connectionless Error Response');\n \n if (relatedMessage && relatedMessage.body.id)\n {\n this.id = relatedMessage.body.id;\n this.nonce = relatedMessage.body.nonce;\n this.dcpsid = relatedMessage.body.dcpsid;\n }\n\n this.success = false;\n this.time = Date.now();\n this.owner = identity.address;\n this.#idKs = identity;\n this.payload = {\n message: errorMessage,\n code: errorCode,\n };\n }\n \n toJSON()\n {\n return {\n ...super.toJSON(),\n id: this.id,\n nonce: this.nonce,\n dcpsid: this.dcpsid,\n type: 'unhandled-message'\n };\n }\n\n sign() /* async */\n {\n return this.#idKs.makeSignedMessage(this.toJSON());\n }\n}\n\nexports.Target.prototype.sendErrorResponse = function sendErrorResponse(transport, errorMessage, code, relatedMessage)\n{\n debugging('target') && console.debug(`${this.debugLabel} got an invalid message, sending back ConnectionLessErrorResponse with error message: ${errorMessage}`)\n const timer = setTimeout(() => transport.close(), 5000); /* keep lingering connections from chewing up resources on super-slow networks */\n \n const response = new ConnectionlessErrorResponse(this.identity, errorMessage, code, relatedMessage);\n response.sign().then((signedResponse) => transport.send(signedResponse))\n transport.on('drain', () => { transport.close(); clearTimeout(timer) });\n \n return false;\n}\n\n/**\n * A handler for PseudoExpress which displays the status of the Target's process\n * @returns false\n */\nfunction peShowStatus(request, response)\n{\n const process = __webpack_require__(/*! process */ \"./node_modules/process/browser.js\");\n const os = __webpack_require__(/*! os */ \"./node_modules/os-browserify/browser.js\");\n\n function fancy(object)\n {\n const padLen = 4 + Object.keys(object).reduce((curMax, key) => key.length > curMax ? key.length : curMax, 0);\n return '\\n' + Object.entries(object).map(([key, value]) => (' ' + key + ': ').padEnd(padLen) + value).join('\\n');\n }\n\n response.status = 200;\n response.set('content-type', 'text/plain; charset=utf8');\n response.set('cache-control', 'no-cache');\n \n const output = `${new Date()}\\n\nrequest: ${request.method} ${request.url}\nprogram: ${process.argv[1]}\npid: ${process.pid}\ndebugPort: ${process.debugPort}\nhostname: ${request.hostname} aka ${os.hostname()}\nuptime: ${process.uptime()}\nloadavg: ${os.loadavg()}\nresources: ${fancy(process.resourceUsage())}\nmemory: ${fancy(process.memoryUsage())}\ntotalmem: ${os.totalmem()}\nheaders: ${fancy(request.headers)}\n\nbody: \\n${request.body || ''}\n`;\n\n response.send(output);\n}\n\n\n//# sourceURL=webpack://dcp/./src/protocol-v4/target.js?");
4676
4675
 
4677
4676
  /***/ }),
4678
4677
 
@@ -4716,7 +4715,7 @@ eval("/**\n * @file listener.js\n * Generic API for transpor
4716
4715
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4717
4716
 
4718
4717
  "use strict";
4719
- eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_modules/process/browser.js */ \"./node_modules/process/browser.js\");\n/**\n * @file transport/socketio.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @author Wes Garland, wes@kingsds.network \n * @date January 2020, March 2022\n *\n * This module implements the SocketIO Transport that is\n * used by the protocol to connect and communicate with peers using\n * SocketIO connections.\n *\n * Transport can operate in either ack mode or non-ack-mode, depending on the value of this.ackMode.\n * Ack mode sends an extra socketio-layer packet back after each message is received, and theoretically\n * this can be use tod meter or measure bandwidth or potentially second-guess socketio's ability to\n * interleave its heartbeat messages OOB from the main traffic. This mode could potentially be enabled\n * on-demand as well, although it appears that it is not necessary to get the desired behaviour at this\n * point, so the feature is off-by-default (although reasonably well-tested).\n */\n\n\nconst { Transport } = __webpack_require__(/*! . */ \"./src/protocol-v4/transport/index.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { leafMerge } = __webpack_require__(/*! dcp/utils/obj-merge */ \"./src/utils/obj-merge.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst { setImmediateN } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\nconst { setImmediate } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.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\");\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('dcp');\nconst dcpEnv = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst bearer = __webpack_require__(/*! ./http-bearer */ \"./src/protocol-v4/transport/http-bearer.js\");\nconst semver = __webpack_require__(/*! semver */ \"./node_modules/semver/semver.js\");\n\nvar listenerSeq = 0;\nvar initiatorSeq = 0;\n\nif (debugging() && !process.env.DEBUG && dcpConfig.build === 'debug')\n{\n if (process.env.DEBUG)\n process.env.DEBUG += ',engine';\n else\n process.env.DEBUG = 'engine';\n\n if (debugging('socketio'))\n {\n /* DCP_DEBUG=dcp:socketio,dcp:verbose to see everything */\n process.env.DEBUG += ',socket.io-client:socket,socket.io:client,socket.io:server';\n if (debugging('verbose') && !debugging('all'))\n process.env.DEBUG += ',socket.io*,engine.io*'\n }\n}\n\nclass SocketIOTransport extends Transport\n{\n /**\n * @constructor\n * @param {socket} newSocket Target: an instance of socket.io Socket that represents\n * a new connection.\n * Initiator: unused\n */\n /**\n * @constructor create new instance of a socketio-flavoured Transport.\n *\n * @param {object} config dcpConfig fragment\n * @param {Url} url Initiator: has .location and optionally .friendLocation\n * Target: has .listen and .location\n *\n * @param {object} options The `socketio` property of a protocol-v4 connectionOptions \n * object, which was built from a combination of defaults and \n * various dcpConfig components.\n *\n * @returns instance suitable for target or initiator, depending on mode, but does not manage connections.\n */\n constructor(config, options={}, newSocket)\n {\n super('Protocol SocketIO Transport');\n const that = this;\n \n this.name = 'socketio';\n this.url = config.location;\n this.msgSequence = 0;\n this.isClosed = false; /* true => connection finalization begun */\n this.isFinal = false; /* true => connection finalization completed */\n this.hasConnected = false; /* true => one 'connect' event has been processed */\n this.initTimeout = false; /* timeout when connecting to allow target to reply with their mode message + buffer size, crash if they don't reply */\n this.modeSet = false; /* true => received response mode message */\n this.bufferSizeSet = false; /* true => received target's buffer size */\n this.rxFragments = {}; /* messages we are receiving in fragments */\n this.rxPending = []; /* strings which are messages that are complete and ready to emit, or objects that in this.rxFragments */\n this.txPending = []; /* messages we are receiving in fragments */\n this.txNumInFlight = 0; /* number of txPending sent but not acknowleged */\n this.txMaxInFlight = 2; /* less than 2 causes bad perf when mode.ack */\n this.debugInfo = { remoteLabel: '<unknown>' };\n this.mode = {\n ack: false,\n concat: false,\n };\n\n assert(DcpURL.isURL(this.url));\n assert(typeof options === 'object');\n\n this.options = this.buildOptions(options);\n\n if (!newSocket) /* Initiator */\n {\n this.debugLabel = `socketio(i:${++initiatorSeq}):`;\n this.rxReady = true; /* relax, said the night man */\n this.debugInfo.remoteLabel = config.location.href;\n if (this.options.disableModeAnnouncement !== false)\n this.sendModeList();\n this.sendMaxMessageInfo();\n\n bearer.a$isFriendlyUrl(config.friendLocation).then((useFriendLocation) => {\n const connectUrl = useFriendLocation ? config.friendLocation : config.location;\n\n debugging('socketio') && console.log(this.debugLabel, 'connecting to', connectUrl.href);\n this.socket = (__webpack_require__(/*! socket.io-client */ \"./node_modules/socket.io-client/build/cjs/index.js\").connect)(connectUrl.origin, this.options);\n this.socket.on('connect_error', (error) => !this.isClosed && this.handleConnectErrorEvent(error));\n this.socket.on('connect', () => !this.isClosed && this.handleConnectEvent());\n\n useSocket();\n }).catch((error) => {\n debugging() && console.error(this.debugLabel, error);\n this.emit('error', error);\n });\n }\n else /* Target - receives socketio instance from SocketIOListener.handleConnectionEvent */\n {\n if (dcpEnv.platform !== 'nodejs')\n throw new Error('socketio: target mode only supported in nodejs');\n\n this.debugLabel = 'socketio(t:<new>):';\n\n this.socket = newSocket;\n this.debugInfo.remoteLabel = this.socket.handshake.address.replace(/^::ffff:/,'');\n debugging('socketio') && console.debug(this.debugLabel, `new socket from ${this.debugInfo.remoteLabel}`\n + ` on ${this.socket.handshake && this.socket.handshake.url}, ${this.socket.id}`);\n\n useSocket();\n }\n\n function useSocket()\n {\n that.socket.compress(dcpConfig.build !== 'debug'); /* try to keep traffic sniffable in debug */\n that.socket.on('message', (msg) => !that.isFinal && that.handleMessageEvent(msg));\n that.socket.on('disconnect', (reason) => !that.isClosed && that.handleDisconnectEvent(reason));\n }\n }\n\n /**\n * API - determine if a transport is usable or not.\n * @returns true if the transport is not closed.\n */\n ready()\n {\n return !this.isClosed;\n }\n \n /**\n * Handle the socket.io disconnect event, which is fired upon disconnection.\n * For some reasons (explicit disconnection), the socket.io code will not try to reconnect, but\n * in all other cases, the socket.io client will normally wait for a small random delay and then \n * try to reconnect, but in our case, we simply tear the transport down and let the Connection\n * class handle reconnects, since it might prefer a different transport at this point anyhow.\n *\n * One tricky case here is ping timeout, since the ping timeout includes the time to transmit the\n * packet before it was sent.\n *\n * @param {string} reason Possible reasons for disconnection.\n */\n handleDisconnectEvent(reason)\n {\n debugging('socketio') && console.debug(this.debugLabel, `disconnected from ${this.debugInfo.remoteLabel}; reason=${reason}`);\n\n if (this.isClosed !== true)\n setImmediate(() => this.#close(reason));\n\n switch(reason)\n {\n case 'io client disconnect':\n return; /* we called socket.disconnect() from another \"thread\" */\n case 'io server disconnect':\n case 'ping timeout':\n case 'transport close':\n case 'transport error':\n this.emit('end', reason);\n /* fallthrough */\n default:\n }\n }\n\n /**\n * Handle a socketio message from the peer.\n *\n * Most of the time, a socketio message contains a DCP message, however we support other message\n * types as well, with a mechanism inspired by 3GPP TS 23.040 (GSM 03.40) message concatenation.\n */\n handleMessageEvent(msg)\n {\n if (typeof msg !== 'string')\n {\n debugging('socketio') && console.debug(this.debugLabel, `received ${typeof msg} message from peer`, this.debugInfo.remoteLabel);\n return;\n }\n\n try\n {\n switch (msg[0])\n {\n case '{': /* all DCP Messages are JSON */\n this.processMessage(msg);\n break;\n case 'E':\n this.processExtendedMessage(msg);\n break;\n case 'A':\n this.processAcknowledgement(msg);\n break;\n case 'T':\n debugging('socketio') && console.debug('Received debug message:', msg);\n break;\n default:\n /* adhoc messages not supported in this transport */\n throw new DCPError(this.debugLabel, `Unrecognized message type indicator from ${this.debugInfo.remoteLabel} (${msg.charCodeAt(0)})`, 'DCPC-1102');\n }\n }\n catch(error)\n {\n debugging('socketio') && console.debug(this.debugLabel, 'handleMessageEvent: transport error, closing\\n ', error);\n this.emit('error', error);\n setImmediate(() => this.#close(error.message));\n }\n }\n\n /**\n * Process an ordinary message. This is just a hunk of JSON that gets passed up to the connection.\n * @param {string} msg\n */\n processMessage(msg)\n {\n this.rxPending.push(msg);\n this.emitReadyMessages();\n if (this.mode.ack)\n this.socket.send('A0000-ACK'); /* Ack w/o header = normal message */\n }\n\n /**\n * Remote has acknowledged the receipt of a message fragment. When in ack mode, this\n * triggers us to send more messages from the txPending queue. We always ack event when \n * not in ack-mode. See processExtendedMessage() comment for header description.\n *\n * Future: this is where maxInFlight might be tuned, based on how fast we get here\n *\n * @param {string} _msg\n */\n processAcknowledgement(_msg)\n {\n if (this.txNumInFlight > 0)\n this.txNumInFlight--;\n else\n {\n if (this.options.strict || !this.mode.ack)\n throw new DCPError(this.debugLabel, `Synchronization error: received unexpected acknowledgement message from ${this.debugInfo.remoteLabel}`, 'DCPC-1109');\n else\n this.mode.ack = true;\n }\n \n this.drainTxPending_soon();\n }\n\n /**\n * Extended messages have the following format:\n * Octet(s) Description\n * 0 E (69) or A (65)\n * 1..4 Header size (N) in hexadecimal\n * 5 Header Type\n * C (67) => concatenation\n * 6..6 + N: Header\n *\n * Upon receipt, extended messages are acknowledged by retransmitting their header, but\n * with the first character changed from E to A.\n *\n * @param {string} msg\n */\n processExtendedMessage(msg)\n {\n const headerSize = parseInt(msg.slice(1,5), 16);\n const headerType = msg[5];\n const header = msg.slice(6, 6 + headerSize);\n\n switch(headerType)\n {\n case 'M':\n this.processExtendedMessage_mode(header, msg.slice(6 + headerSize));\n break;\n case 'B':\n this.processExtendedMessage_maxBufferSize(msg.slice(6 + headerSize));\n break;\n case 'C':\n this.processExtendedMessage_concatenated(header, msg.slice(6 + headerSize));\n break;\n default:\n if (this.options.strict)\n throw new DCPError(`Unrecognized extended message header type indicator (${msg.charCodeAt(5)})`, 'DCPC-1101');\n }\n\n if (this.mode.ack)\n this.socket.send('A' + msg.slice(1, 6 + headerSize));\n }\n\n /**\n * We have received a fragment of a concatenation extended message. Memoize the fragment, and\n * if this is the last fragment, transform the fragments list into a pending message string and\n * then emits all ready messages (pending message strings).\n *\n * This code is capable of handling messages whose fragments arrive out of order. Complete messages \n * are emitted in the order that their first parts are received. Socketio is supposed to be total\n * order-aware but I wanted a safety belt for intermingled messages without occupying the entire\n * socketio network buffer on the first pass of the event loop. Because the first message fragment\n * is emitted to socketio on the same event loop pass as their Connection::send calls, I believe\n * that are no cases where messages will be emitted out of order at the other end, even when \n * intermingled on the wire, and the Nth message is much longer than any (N+k)th messages.\n *\n * @param {object} header the extended message header\n * @param {string} payload the payload of the extended message, i.e. a message fragment\n */\n processExtendedMessage_concatenated(header, payload)\n {\n try\n {\n header = JSON.parse(header);\n }\n catch(e)\n {\n throw new DCPError(`invalid extended message header '${header}'`, 'DCPC-1103');\n }\n\n if (!this.options.strict && !this.mode.concat)\n this.mode.concat = true;\n \n if (!(header.total <= this.options.maxFragmentCount))\n throw new DCPError('excessive message fragmentation', 'DCPC-1107'); /* make it more difficult for attacker to force OOM us */\n\n if (!(header.seq < header.total))\n throw new DCPError(`corrupt header for part ${header.seq + 1} of ${header.total} of message ${header.msgId}`, 'DCPC-1108');\n \n /* First fragment received, initial data structures and memoize the message's arrival order via pending list */\n if (!this.rxFragments[header.msgId])\n {\n const fragments = [];\n this.rxFragments[header.msgId] = fragments;\n this.rxPending.push(fragments);\n }\n\n this.rxFragments[header.msgId][header.seq] = payload;\n debugging('socketio') && console.debug(this.debugLabel, 'received part', header.seq + 1, 'of', header.total, 'for message', header.msgId);\n\n if (header.total !== this.rxFragments[header.msgId].filter(el => el !== undefined).length)\n return;\n\n debugging('socketio') && console.debug(this.debugLabel, 'received all parts of message', header.msgId);\n \n const idx = this.rxPending.indexOf(this.rxFragments[header.msgId])\n this.rxPending[idx] = this.rxFragments[header.msgId].join('');\n delete this.rxFragments[header.msgId];\n this.emitReadyMessages();\n }\n\n processExtendedMessage_mode(_header, payload)\n {\n var modeList;\n \n try\n {\n modeList = JSON.parse(payload);\n }\n catch(e)\n {\n throw new DCPError(`invalid mode message payload '${payload}'`, 'DCPC-1105');\n }\n\n for (let mode of modeList)\n {\n debugging('socketio') && console.debug(this.debugLabel, ` - remote supports ${mode} mode`); \n switch(mode)\n {\n case 'ack':\n this.mode.ack = true;\n break;\n case 'concat':\n this.mode.concat = true;\n break;\n default:\n debugging('socketio') && console.debug(this.debugLabel, ' - remote requested unrecognized mode:', mode);\n break;\n }\n }\n\n // true if we are connecting a new transport, expecting the initial mode reply, and have received the target's buffer size\n if (this.initTimeout && this.bufferSizeSet)\n {\n clearTimeout(this.initTimeout);\n delete this.initTimeout;\n this.emit('connect');\n }\n this.modeSet = true;\n this.sendModeList();\n }\n \n processExtendedMessage_maxBufferSize(payload)\n {\n var receivedOptions = JSON.parse(payload); \n \n this.options.txMaxFragmentSize = receivedOptions.maxFragmentSize;\n this.options.txMaxFragmentCount = receivedOptions.maxFragmentCount;\n \n if (receivedOptions.txMaxInFlight < this.txMaxInFlight)\n this.txMaxInFlight = receivedOptions.txMaxInFlight;\n if (this.options.maxFragmentSize < 0)\n throw new DCPError(`maxFragmentSize was set to < 1, (${this.options.txMaxFragmentSize}). Server requested bad fragment size, cannot connect.`, 'DCPC-1111');\n if (this.txMaxInFlight < 1)\n throw new DCPError(`txMaxInFlight was set to < 1, (${this.txMaxInFlight}). Cannot send messages with fewer than 1 in flight.`, 'DCPC-1111');\n \n // true if we are connecting a new transport, expecting the initial buffer reply, and have received the target's mode designation\n if (this.initTimeout && this.modeSet)\n {\n clearTimeout(this.initTimeout);\n delete this.initTimeout;\n this.emit('connect');\n }\n this.bufferSizeSet = true;\n\n this.sendMaxMessageInfo(); \n }\n\n /* Internal method to tell peer what we can receive */\n sendModeList()\n {\n var rawMessage;\n var modeList = [];\n\n if (this.sentModeList)\n return;\n\n this.sentModeList = true;\n this.options.enableModeAck == true && modeList.push('ack');\n this.options.enableModeConcat !== false && modeList.push('concat');\n rawMessage = 'E0000M' + JSON.stringify(modeList);\n\n debugging('socketio') && console.debug(`${this.debugLabel} sending mode list to ${this.debugInfo.remoteLabel}`)\n if (this.socket)\n this.socket.send(rawMessage);\n else\n this.txPending.unshift({ message: rawMessage, msgSequence: ++this.msgSequence});\n }\n \n /* Internal method to exchange desired fragment size/max inflight messages/max fragments per message */\n sendMaxMessageInfo()\n {\n if (this.sentMessageInfo)\n return;\n\n /* Calculate the optimal maxFragmentSize based on maxHttpBufferSize.\n * Currently unsure of exactly what happens when socketio serializes our serialized\n * JSON. I suspect we may double-encode UTF-8 > 0x80. The largest UTF-8 character is\n * 5 bytes...so we ensure the maxFragmentSize is (maxHttpBufferSize / 5) - (fragmentOverhead * 5)\n * to allow for worst-case scenario\n */\n const fragmentOverhead = 1000 + 64 * 1024; /* derived from concatenation algorithm */\n\n let desiredMaxFragmentSize;\n if (this.options.maxHttpBufferSize)\n desiredMaxFragmentSize = (this.options.maxHttpBufferSize / 5) - (fragmentOverhead * 5);\n else\n desiredMaxFragmentSize = this.options.maxFragmentSize;\n const options = {\n maxFragmentSize: desiredMaxFragmentSize,\n maxFragments: this.options.maxFragmentCount,\n txMaxInFlight: this.txMaxInFlight,\n };\n\n const message = 'E0000B' + JSON.stringify(options);\n\n debugging('socketio') && console.debug(`${this.debugLabel} sending max http buffer size to ${this.debugInfo.remoteLabel}`);\n if (this.socket)\n this.socket.send(message);\n else\n this.txPending.push({ message: message, msgSequence: ++this.msgSequence });\n \n this.sentMessageInfo = true;\n }\n\n /**\n * Emit message events so that the connection can process them. The pending property is an\n * array which is used to order messages, so that they are emitted in the order they were\n * sent. There are edge cases in the concatenation code where a sender could theoretically\n * interleave multiple messages, and have a second, shorter, message fully received before \n * the first is fully transmitted.\n *\n * The pending property stores only strings or objects; objects are effectively positional\n * memoes which are turned into strings when they are ready to be emitted to the connection\n * for processing.\n */\n emitReadyMessages()\n {\n while (this.rxReady && this.rxPending.length && typeof this.rxPending[0] === 'string')\n this.emit('message', this.rxPending.shift());\n }\n \n /** \n * Close the current instance of Socket.\n *\n * This function effectively finalizes the socket so that it can't be used any more and\n * (hopefully) does not entrain garbage. A 'close' event is emitted at the end of socket\n * finalization and should be hooked by things that might entrain this transport instance to free\n * up memory, like maybe a list of active transports, etc.\n *\n * We add few extra hops on/off the event loop here to try and clean up any messages halfway in/out\n * the door that we might want to process. That happens between the setting of the isClosed and \n * isFinal flags.\n *\n * @param {string} reason\n */\n #close(reason)\n {\n const that = this;\n debugging('socketio') && console.debug(this.debugLabel, 'closing connection' + (reason ? ` (${reason})` : ''));\n\n function finalize(socket)\n {\n if (that.isFinal)\n return;\n\n if (socket)\n socket.removeAllListeners();\n that.isFinal = true;\n\n /* Free up memory that might be huge in case something entrains us */\n that.rxPending.length = 0;\n that.txPending.length = 0;\n for (let msgId in that.rxFragments)\n that.rxFragments[msgId].length = 0;\n }\n\n function closeSocket()\n {\n const socket = that.socket;\n \n try\n {\n if (socket)\n socket.disconnect();\n that.isClosed = true;\n that.emit('close', reason);\n delete that.socket;\n }\n finally\n {\n finalize(socket);\n }\n }\n\n if (!this.isClosed)\n {\n /* If we called close, drain any pending messages and then disconnect */\n if (reason === 'api-call')\n {\n this.drainTxPending();\n setImmediate(()=>closeSocket());\n }\n else\n closeSocket();\n }\n }\n\n /* API - close the transport, free up memory. */\n close()\n {\n assert(arguments.length === 0)\n this.#close('api-call');\n }\n\n /**\n * Handle the socketio connect_error event, which is fired when:\n * - the low-level connection cannot be established\n * - the connection is denied by the server in a middleware function\n *\n * In the first case, socket.io will automatically try to reconnect, after a delay,\n * except that will cancel that reconnection attempt here by killing the transport\n * instance so that the Connection class can try failing over to an alternate transport.\n *\n * @param {Error} error optional instance of error describing the underlying reason for the\n * connect failure. If the underlying reason is expressible by an\n * HTTP status code, this code will be placed as a Number in the\n * description property.\n */\n handleConnectErrorEvent(error)\n {\n debugging('socketio') && console.debug(this.debugLabel, `unable to connect to ${this.url}`, error);\n if (error.type === 'TransportError' && typeof error.description === 'number')\n error.httpStatus = Number(error.description);\n\n this.emit('connect-failed', error);\n this.#close(error.message);\n }\n\n /** \n * Handle the socketio connect event, which is fired by the Socket instance upon initiator\n * connection and reconnection to a target.\n */\n handleConnectEvent()\n {\n if (this.hasConnected === true)\n {\n /* this transport is not supposed to reuse connections, and we call .close() on disconnect \n * to prevent this. However, the API does not guarantee that will be race-free.\n */\n debugging('socketio') && console.debug(this.debugLabel, `*** reconnected to ${this.debugInfo.remoteLabel} (ignoring)`);\n return;\n }\n\n /* initial connection */\n this.hasConnected = true;\n debugging('socketio') && console.debug(this.debugLabel, 'connected to', this.debugInfo.remoteLabel);\n\n this.initTimeout = setTimeout(() => this.#close('no mode message received'), this.options.establishTimeout) /* give target 10 second to reply with capabilities before assuming bad connection and crashing */\n if (dcpEnv.platform === 'nodejs')\n this.initTimeout.unref();\n \n this.drainTxPending();\n }\n\n /**\n * API: Send a message to the transport instance on the other side of this connection.\n *\n * @param {string} message the message to send\n */\n send(message)\n {\n const msgSequence = ++this.msgSequence;\n\n debugging('socketio') && console.debug(this.debugLabel, `sending message ${msgSequence} to`, this.debugInfo.remoteLabel);\n debugging('socketio') && debugging('verbose') && !debugging('all') && console.debug(this.debugLabel, message);\n\n if (!this.socket)\n throw new DCPError('SocketIOTransport.send: Not connected', 'DCPC-1110');\n \n if ( false\n || !(message.length > this.options.txMaxFragmentSize)\n || !this.mode.concat)\n this.txPending.push({ message, msgSequence });\n else\n { /* This is a large message. Use message concatenation to send in fragments. A failure of any\n * fragment is a failure of the entire message and will be handled at the connection layer.\n */\n let total = Math.ceil(message.length / this.options.txMaxFragmentSize);\n \n if (total > this.options.txMaxFragmentCount)\n throw new DCPError('extended message has more fragments than the peer will allow', 'DCP-1112');\n \n for (let seq=0; seq < total; seq++)\n {\n let start = seq * this.options.txMaxFragmentSize;\n let fragment = message.slice(start, start + this.options.txMaxFragmentSize);\n let header = JSON.stringify({ msgId: msgSequence, seq, total });\n let extMessage = 'E' + header.length.toString(16).padStart(4, '0') + 'C' + header + fragment;\n\n this.txPending.push({ message: extMessage, msgSequence, seq, total });\n\n /* This should be impossible, but it also effectively kills the connection. */\n if (header.length > 0xFFFF)\n throw new DCPError('extended message header too long', 'DCPC-1104');\n }\n }\n\n this.drainTxPending_soon();\n }\n\n drainTxPending_soon()\n {\n if (this.drainingSoon)\n return;\n this.drainingSoon = true;\n\n setImmediate(() => this.drainTxPending());\n }\n \n /**\n * Drain messages from the txPending queue out to socketio's network buffer. We throw away traffic\n * when it can't be sent; this is no different from packets already on the wire when the other end\n * has changed IP number, etc.\n *\n * In ack mode, this drain is controlled by acknowledgement messages so that socketio can have the\n * opportunity to insert ping/pong messages in the data stream while sending a message large enough\n * to need to be sent via concatenation. [ NOTE - it appears to send ping/pong OOB in version 4.4, \n * so currently ack mode is off by default / wg Mar 2022 ]\n *\n * The reason we put non-concat messages through here is to avoid special casing a bunch of nasty\n * things for the sake of a little tiny bit of performance. Sending message<-ACK and queueing through\n * the drain code is necessary if we wind up interleaving multiple message sends in some interesting\n * edge cases; notably, fast-teardown, but a future Connection class that ran without nonces (or multiple\n * nonces) would need this, too.\n *\n * In non-ack mode, the drain is mostly uncontrolled, but we throw each message on a separate pass of\n * the event loop, to try and keep huge message blasts one connection from overwhelming a busy server,\n * as well allowing socketio the chance to send its heartbeat messages and make it less likely that it\n * will hit its http buffer size limit, because that causes an immediate disconnect.\n *\n * Non-ack-mode is compatible with previous dcp5 versions and probably a little faster.\n */\n drainTxPending()\n {\n const that = this;\n\n if (this.txPending.length)\n debugging('socketio') && console.debug(this.debugLabel, `drain tx pending queue, ${this.txPending.length} ready`\n + (this.mode.ack ? `; ${this.txMaxInFlight - this.txNumInFlight} slots available` : ''));\n delete this.drainingSoon;\n\n if (this.isClosed)\n return;\n \n for (let i=0;\n !this.isClosed && this.txPending.length && (this.txNumInFlight < this.txMaxInFlight || !this.mode.ack);\n i++)\n {\n const pendingElement = this.txPending.shift();\n\n if (i === 0)\n writeToSocket(pendingElement);\n else\n setImmediateN(() => writeToSocket(pendingElement), i);\n\n if (this.mode.ack)\n this.txNumInFlight++;\n\n if (this.txPending.length === 0) /* just sent the last message */\n this.emit('drain');\n }\n\n if (this.txPending.length)\n this.drainTxPending_soon();\n \n function writeToSocket(pendingElement)\n {\n if (that.isClosed)\n return;\n\n const { message, msgSequence, seq, total } = pendingElement;\n that.socket.send(message);\n\n if (typeof seq !== 'undefined')\n debugging('socketio') && console.debug(that.debugLabel, `sent fragment ${seq + 1}/${total} of message ${msgSequence} (${message.length} bytes)`);\n else\n debugging('socketio') && console.debug(that.debugLabel, `sent message ${msgSequence} (${message.length} bytes)`);\n }\n }\n}\nSocketIOTransport.prototype.buildOptions = buildOptions;\n\n/** \n * Build the socketio options object, based on the passed-in options, dcp debug state, and internal\n * object state. Method shared between SocketIOTransport and SocketIOListener.\n */\nfunction buildOptions(options)\n{\n options = leafMerge(\n /* Baked-in Defaults */\n ({\n autoUnref: dcpEnv.platform === 'nodejs' ? true : undefined, /* socket.io + webpack 5 { node:global } work-around */\n perMessageDeflate: dcpConfig.build !== 'debug',\n maxFragmentCount: 1000, /* used to limit memory on receiver */\n maxFragmentSize: 1e6, /* bytes */\n txMaxFragmentCount:1000, /* peer's limit for fragments */\n txMaxFragmentSize: 1e6, /* peer's limit for fragment size */\n maxHttpBufferSize: false, /* bytes; false = auto */\n establishTimeout: 10 * 1000, /* 10 seconds */\n pingTimeout: 30 * 1e3, /* s */\n pingInterval: 90 * 1e3, /* s */\n transports: ['polling', 'websocket'],\n upgrade: true,\n rememberUpgrade: true,\n autoConnect: true,\n cors: {\n origin: '*',\n methods: ['GET', 'POST']\n },\n }),\n options,\n /* Absolutely mandatory */\n ({\n path: this.url.pathname // eslint-disable-line no-invalid-this\n }));\n\n /* draw out errors quickly in dev */\n if ((process.env.DCP_NETWORK_CONFIG_BUILD || dcpConfig.build) === 'debug')\n {\n /* short timeouts and debuggers don't get along well */\n if (dcpEnv.platform === 'nodejs' && !(requireNative('module')._cache.niim instanceof requireNative('module').Module))\n {\n options.pingTimeout /= 5;\n options.pingInterval /= 5;\n }\n }\n\n return options;\n}\n \n/**\n * @constructor\n * API - Factory which creates a new socketio listener, or throws. Emits 'listening' if the server \n * parameter is not provided and we create it, once it is ready for connections.\n * \n * @bug It is possible for a connection event to sneak in before the 'listening' event is fired, due to \n * the two-step event trigger. The right fix is to proxy the event, but dcp-events::event-emitter.proxy\n * itself currently has the same bug. For now, we just have callbacks in API. /wg apr 2022\n * \n * @param {object} config dcpConfig fragment - has .listen and .location\n * @param {object} target the instance of Target to bind to\n * @param {object} options [optional] A DCP config options variable (like dcpConfig.dcp.listenOptions)\n */\nfunction SocketIOListener(config, target, options)\n{\n var matchRegex;\n \n this.url = config.location;\n this.options = this.buildOptions(options);\n this.connSequence = 0;\n this.seq = ++listenerSeq;\n this.debugLabel = `socketio(l:${this.seq}):`;\n this.isShutdown = false;\n\n if (dcpEnv.platform !== 'nodejs')\n throw new Error('target mode only supported in nodejs');\n\n target.httpServer.on('listening', () => this.emit('listening', target.httpServer));\n\n matchRegex = '^' + this.url.pathname;\n if (matchRegex.slice(-1) !== '/')\n matchRegex += '\\\\/';\n else\n matchRegex = matchRegex.slice(0, -1) + \"\\\\/\"\n\n matchRegex += '\\\\?EIO=[0-9]';\n debugging('socketio') && console.debug(this.debugLabel, `expecting requests to match regex ${matchRegex}`);\n\n target.all(new RegExp(matchRegex), (request) => {\n /* Socket.io does this intrinsically because it plays with the server's events directly,\n * but we \"handle\" it explicitly here so that we don't trip the 404 handler. \n */\n debugging('socketio') && console.debug(this.debugLabel, 'Handling', request.url);\n }, true);\n\n /* While we normally want to inherit options from parent to child socket via this.options,\n * we don't want this to happen for maxHttpBufferSize due to the way it is used and calculated\n * in the transport constructor. A buffer that is too small (eg false) will cause us to not\n * recognized socket.io SIDs.\n */\n \n this.options.maxHttpBufferSize = 1e7;\n \n const socketOptions = this.options;\n\n this.socketServer = requireNative('socket.io')(target.httpServer, socketOptions);\n this.socketServer.on('connection', (socket) => !this.isShutdown && this.handleConnectionEvent(config, socket));\n debugging() && console.debug(this.debugLabel, 'Socketio listener initialized for path', this.options.path);\n}\nSocketIOListener.prototype = new EventEmitter('socketio(l)');\nSocketIOListener.prototype.buildOptions = buildOptions;\n\nSocketIOListener.prototype.close = function SocketIOListener$$close()\n{\n if (!this.isShutdown)\n this.shutdown();\n this.socketServer.close(() => this.emit('close'));\n}\n\n/** Stop accepting new connections */\nSocketIOListener.prototype.shutdown = function SocketIOListener$$shutdown()\n{\n this.isShutdown = true;\n}\n\n/** Remove any event loop references used by this transport instance */\nSocketIOListener.prototype.unref = function SocketIOListener$$unref()\n{\n void false;\n}\n\n/**\n * Handle the socketio connection event, which is fired upon a connection from client.\n * Used by Target->SocketIOListener to create transport instances.\n *\n * It is expected that code hooking the 'connection' event will hook the 'message' in\n * the same pass of the event loop as it is invoked; otherwise, messages may be missed.\n *\n * @param {object} socket a new connection emitted by the socket.io server\n */\nSocketIOListener.prototype.handleConnectionEvent = function SocketIOListener$$handleConnectionEvent(config, socket)\n{\n var transport = new SocketIOTransport(config, this.options, socket);\n\n this.url = config.location || config.listen;\n this.connSequence++;\n transport.debugLabel = `socketio(t:${this.seq}.${this.connSequence}):`\n\n /* Ensure we don't emit any message events until the application or Connection class\n * has had the chance to set up message handlers via the connection event.\n */\n transport.rxReady = false;\n this.on('connection', () => setImmediate(() => {\n transport.rxReady = true;\n transport.emitReadyMessages();\n }));\n\n this.emit('connection', transport);\n}\n\n/* Define API */\nexports.TransportClass = SocketIOTransport;\nexports.Listener = SocketIOListener;\n\n\n//# sourceURL=webpack://dcp/./src/protocol-v4/transport/socketio.js?");
4718
+ eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_modules/process/browser.js */ \"./node_modules/process/browser.js\");\n/**\n * @file transport/socketio.js\n * @author Ryan Rossiter, ryan@kingsds.network\n * @author Wes Garland, wes@kingsds.network \n * @date January 2020, March 2022\n *\n * This module implements the SocketIO Transport that is\n * used by the protocol to connect and communicate with peers using\n * SocketIO connections.\n *\n * Transport can operate in either ack mode or non-ack-mode, depending on the value of this.ackMode.\n * Ack mode sends an extra socketio-layer packet back after each message is received, and theoretically\n * this can be use tod meter or measure bandwidth or potentially second-guess socketio's ability to\n * interleave its heartbeat messages OOB from the main traffic. This mode could potentially be enabled\n * on-demand as well, although it appears that it is not necessary to get the desired behaviour at this\n * point, so the feature is off-by-default (although reasonably well-tested).\n */\n\n\nconst { Transport } = __webpack_require__(/*! . */ \"./src/protocol-v4/transport/index.js\");\nconst { EventEmitter } = __webpack_require__(/*! dcp/common/dcp-events */ \"./src/common/dcp-events/index.js\");\nconst { leafMerge } = __webpack_require__(/*! dcp/utils/obj-merge */ \"./src/utils/obj-merge.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst { DCPError } = __webpack_require__(/*! dcp/common/dcp-error */ \"./src/common/dcp-error.js\");\nconst { setImmediateN } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.js\");\nconst { setImmediate } = __webpack_require__(/*! dcp/common/dcp-timers */ \"./src/common/dcp-timers.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\");\n\nconst debugging = (__webpack_require__(/*! dcp/debugging */ \"./src/debugging.js\").scope)('dcp');\nconst dcpEnv = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\nconst bearer = __webpack_require__(/*! ./http-bearer */ \"./src/protocol-v4/transport/http-bearer.js\");\nconst semver = __webpack_require__(/*! semver */ \"./node_modules/semver/semver.js\");\n\nvar listenerSeq = 0;\nvar initiatorSeq = 0;\n\nif (debugging() && !process.env.DEBUG && dcpConfig.build === 'debug')\n{\n if (process.env.DEBUG)\n process.env.DEBUG += ',engine';\n else\n process.env.DEBUG = 'engine';\n\n if (debugging('socketio'))\n {\n /* DCP_DEBUG=dcp:socketio,dcp:verbose to see everything */\n process.env.DEBUG += ',socket.io-client:socket,socket.io:client,socket.io:server';\n if (debugging('verbose') && !debugging('all'))\n process.env.DEBUG += ',socket.io*,engine.io*'\n }\n}\n\nclass SocketIOTransport extends Transport\n{\n /**\n * @constructor\n * @param {socket} newSocket Target: an instance of socket.io Socket that represents\n * a new connection.\n * Initiator: unused\n */\n /**\n * @constructor create new instance of a socketio-flavoured Transport.\n *\n * @param {object} config dcpConfig fragment\n * @param {Url} url Initiator: has .location and optionally .friendLocation\n * Target: has .listen and .location\n *\n * @param {object} options The `socketio` property of a protocol-v4 connectionOptions \n * object, which was built from a combination of defaults and \n * various dcpConfig components.\n *\n * @returns instance suitable for target or initiator, depending on mode, but does not manage connections.\n */\n constructor(config, options={}, newSocket)\n {\n super('Protocol SocketIO Transport');\n const that = this;\n \n this.name = 'socketio';\n this.url = config.location;\n this.msgSequence = 0;\n this.isClosed = false; /* true => connection finalization begun */\n this.isFinal = false; /* true => connection finalization completed */\n this.hasConnected = false; /* true => one 'connect' event has been processed */\n this.initTimeout = false; /* timeout when connecting to allow target to reply with their mode message + buffer size, crash if they don't reply */\n this.modeSet = false; /* true => received response mode message */\n this.bufferSizeSet = false; /* true => received target's buffer size */\n this.rxFragments = {}; /* messages we are receiving in fragments */\n this.rxPending = []; /* strings which are messages that are complete and ready to emit, or objects that in this.rxFragments */\n this.txPending = []; /* messages we are receiving in fragments */\n this.txNumInFlight = 0; /* number of txPending sent but not acknowleged */\n this.txMaxInFlight = 2; /* less than 2 causes bad perf when mode.ack */\n this.debugInfo = { remoteLabel: '<unknown>' };\n this.mode = {\n ack: false,\n concat: false,\n };\n\n assert(DcpURL.isURL(this.url));\n assert(typeof options === 'object');\n\n this.options = this.buildOptions(options);\n\n if (!newSocket) /* Initiator */\n {\n this.debugLabel = `socketio(i:${++initiatorSeq}):`;\n this.rxReady = true; /* relax, said the night man */\n this.debugInfo.remoteLabel = config.location.href;\n if (this.options.disableModeAnnouncement !== false)\n this.sendModeList();\n this.sendMaxMessageInfo();\n\n bearer.a$isFriendlyUrl(config.friendLocation).then((useFriendLocation) => {\n const connectUrl = useFriendLocation ? config.friendLocation : config.location;\n\n debugging('socketio') && console.log(this.debugLabel, 'connecting to', connectUrl.href);\n this.socket = (__webpack_require__(/*! socket.io-client */ \"./node_modules/socket.io-client/build/cjs/index.js\").connect)(connectUrl.origin, this.options);\n this.socket.on('connect_error', (error) => !this.isClosed && this.handleConnectErrorEvent(error));\n this.socket.on('connect', () => !this.isClosed && this.handleConnectEvent());\n\n useSocket();\n }).catch((error) => {\n debugging() && console.error(this.debugLabel, error);\n this.emit('error', error);\n });\n }\n else /* Target - receives socketio instance from SocketIOListener.handleConnectionEvent */\n {\n if (dcpEnv.platform !== 'nodejs')\n throw new Error('socketio: target mode only supported in nodejs');\n\n this.debugLabel = 'socketio(t:<new>):';\n\n this.socket = newSocket;\n this.debugInfo.remoteLabel = this.socket.handshake.address.replace(/^::ffff:/,'');\n debugging('socketio') && console.debug(this.debugLabel, `new socket from ${this.debugInfo.remoteLabel}`\n + ` on ${this.socket.handshake && this.socket.handshake.url}, ${this.socket.id}`);\n\n useSocket();\n }\n\n function useSocket()\n {\n that.socket.compress(dcpConfig.build !== 'debug'); /* try to keep traffic sniffable in debug */\n that.socket.on('message', (msg) => !that.isFinal && that.handleMessageEvent(msg));\n that.socket.on('disconnect', (reason) => !that.isClosed && that.handleDisconnectEvent(reason));\n }\n }\n\n /**\n * API - determine if a transport is usable or not.\n * @returns true if the transport is not closed.\n */\n ready()\n {\n return !this.isClosed;\n }\n \n /**\n * Handle the socket.io disconnect event, which is fired upon disconnection.\n * For some reasons (explicit disconnection), the socket.io code will not try to reconnect, but\n * in all other cases, the socket.io client will normally wait for a small random delay and then \n * try to reconnect, but in our case, we simply tear the transport down and let the Connection\n * class handle reconnects, since it might prefer a different transport at this point anyhow.\n *\n * One tricky case here is ping timeout, since the ping timeout includes the time to transmit the\n * packet before it was sent.\n *\n * @param {string} reason Possible reasons for disconnection.\n */\n handleDisconnectEvent(reason)\n {\n debugging('socketio') && console.debug(this.debugLabel, `disconnected from ${this.debugInfo.remoteLabel}; reason=${reason}`);\n\n if (this.isClosed !== true)\n setImmediate(() => this.#close(reason));\n\n switch(reason)\n {\n case 'io client disconnect':\n return; /* we called socket.disconnect() from another \"thread\" */\n case 'io server disconnect':\n case 'ping timeout':\n case 'transport close':\n case 'transport error':\n this.emit('end', reason);\n /* fallthrough */\n default:\n }\n }\n\n /**\n * Handle a socketio message from the peer.\n *\n * Most of the time, a socketio message contains a DCP message, however we support other message\n * types as well, with a mechanism inspired by 3GPP TS 23.040 (GSM 03.40) message concatenation.\n */\n handleMessageEvent(msg)\n {\n if (typeof msg !== 'string')\n {\n debugging('socketio') && console.debug(this.debugLabel, `received ${typeof msg} message from peer`, this.debugInfo.remoteLabel);\n return;\n }\n\n try\n {\n switch (msg[0])\n {\n case '{': /* all DCP Messages are JSON */\n this.processMessage(msg);\n break;\n case 'E':\n this.processExtendedMessage(msg);\n break;\n case 'A':\n this.processAcknowledgement(msg);\n break;\n case 'T':\n debugging('socketio') && console.debug('Received debug message:', msg);\n break;\n default:\n /* adhoc messages not supported in this transport */\n throw new DCPError(this.debugLabel, `Unrecognized message type indicator from ${this.debugInfo.remoteLabel} (${msg.charCodeAt(0)})`, 'DCPC-1102');\n }\n }\n catch(error)\n {\n debugging('socketio') && console.debug(this.debugLabel, 'handleMessageEvent: transport error, closing\\n ', error);\n this.emit('error', error);\n setImmediate(() => this.#close(error.message));\n }\n }\n\n /**\n * Process an ordinary message. This is just a hunk of JSON that gets passed up to the connection.\n * @param {string} msg\n */\n processMessage(msg)\n {\n this.rxPending.push(msg);\n this.emitReadyMessages();\n if (this.mode.ack)\n this.socket.send('A0000-ACK'); /* Ack w/o header = normal message */\n }\n\n /**\n * Remote has acknowledged the receipt of a message fragment. When in ack mode, this\n * triggers us to send more messages from the txPending queue. We always ack event when \n * not in ack-mode. See processExtendedMessage() comment for header description.\n *\n * Future: this is where maxInFlight might be tuned, based on how fast we get here\n *\n * @param {string} _msg\n */\n processAcknowledgement(_msg)\n {\n if (this.txNumInFlight > 0)\n this.txNumInFlight--;\n else\n {\n if (this.options.strict || !this.mode.ack)\n throw new DCPError(this.debugLabel, `Synchronization error: received unexpected acknowledgement message from ${this.debugInfo.remoteLabel}`, 'DCPC-1109');\n else\n this.mode.ack = true;\n }\n \n this.drainTxPending_soon();\n }\n\n /**\n * Extended messages have the following format:\n * Octet(s) Description\n * 0 E (69) or A (65)\n * 1..4 Header size (N) in hexadecimal\n * 5 Header Type\n * C (67) => concatenation\n * 6..6 + N: Header\n *\n * Upon receipt, extended messages are acknowledged by retransmitting their header, but\n * with the first character changed from E to A.\n *\n * @param {string} msg\n */\n processExtendedMessage(msg)\n {\n const headerSize = parseInt(msg.slice(1,5), 16);\n const headerType = msg[5];\n const header = msg.slice(6, 6 + headerSize);\n\n switch(headerType)\n {\n case 'M':\n this.processExtendedMessage_mode(header, msg.slice(6 + headerSize));\n break;\n case 'B':\n this.processExtendedMessage_maxBufferSize(msg.slice(6 + headerSize));\n break;\n case 'C':\n this.processExtendedMessage_concatenated(header, msg.slice(6 + headerSize));\n break;\n default:\n if (this.options.strict)\n throw new DCPError(`Unrecognized extended message header type indicator (${msg.charCodeAt(5)})`, 'DCPC-1101');\n }\n\n if (this.mode.ack)\n this.socket.send('A' + msg.slice(1, 6 + headerSize));\n }\n\n /**\n * We have received a fragment of a concatenation extended message. Memoize the fragment, and\n * if this is the last fragment, transform the fragments list into a pending message string and\n * then emits all ready messages (pending message strings).\n *\n * This code is capable of handling messages whose fragments arrive out of order. Complete messages \n * are emitted in the order that their first parts are received. Socketio is supposed to be total\n * order-aware but I wanted a safety belt for intermingled messages without occupying the entire\n * socketio network buffer on the first pass of the event loop. Because the first message fragment\n * is emitted to socketio on the same event loop pass as their Connection::send calls, I believe\n * that are no cases where messages will be emitted out of order at the other end, even when \n * intermingled on the wire, and the Nth message is much longer than any (N+k)th messages.\n *\n * @param {object} header the extended message header\n * @param {string} payload the payload of the extended message, i.e. a message fragment\n */\n processExtendedMessage_concatenated(header, payload)\n {\n try\n {\n header = JSON.parse(header);\n }\n catch(e)\n {\n throw new DCPError(`invalid extended message header '${header}'`, 'DCPC-1103');\n }\n\n if (!this.options.strict && !this.mode.concat)\n this.mode.concat = true;\n \n if (!(header.total <= this.options.maxFragmentCount))\n throw new DCPError('excessive message fragmentation', 'DCPC-1107'); /* make it more difficult for attacker to force OOM us */\n\n if (!(header.seq < header.total))\n throw new DCPError(`corrupt header for part ${header.seq + 1} of ${header.total} of message ${header.msgId}`, 'DCPC-1108');\n \n /* First fragment received, initial data structures and memoize the message's arrival order via pending list */\n if (!this.rxFragments[header.msgId])\n {\n const fragments = [];\n this.rxFragments[header.msgId] = fragments;\n this.rxPending.push(fragments);\n }\n\n this.rxFragments[header.msgId][header.seq] = payload;\n debugging('socketio') && console.debug(this.debugLabel, 'received part', header.seq + 1, 'of', header.total, 'for message', header.msgId);\n\n if (header.total !== this.rxFragments[header.msgId].filter(el => el !== undefined).length)\n return;\n\n debugging('socketio') && console.debug(this.debugLabel, 'received all parts of message', header.msgId);\n \n const idx = this.rxPending.indexOf(this.rxFragments[header.msgId])\n this.rxPending[idx] = this.rxFragments[header.msgId].join('');\n delete this.rxFragments[header.msgId];\n this.emitReadyMessages();\n }\n\n processExtendedMessage_mode(_header, payload)\n {\n var modeList;\n \n try\n {\n modeList = JSON.parse(payload);\n }\n catch(e)\n {\n throw new DCPError(`invalid mode message payload '${payload}'`, 'DCPC-1105');\n }\n\n for (let mode of modeList)\n {\n debugging('socketio') && console.debug(this.debugLabel, ` - remote supports ${mode} mode`); \n switch(mode)\n {\n case 'ack':\n this.mode.ack = true;\n break;\n case 'concat':\n this.mode.concat = true;\n break;\n default:\n debugging('socketio') && console.debug(this.debugLabel, ' - remote requested unrecognized mode:', mode);\n break;\n }\n }\n\n // true if we are connecting a new transport, expecting the initial mode reply, and have received the target's buffer size\n if (this.initTimeout && this.bufferSizeSet)\n {\n clearTimeout(this.initTimeout);\n delete this.initTimeout;\n this.emit('connect');\n }\n this.modeSet = true;\n this.sendModeList();\n }\n \n processExtendedMessage_maxBufferSize(payload)\n {\n var receivedOptions = JSON.parse(payload); \n \n this.options.txMaxFragmentSize = receivedOptions.maxFragmentSize;\n this.options.txMaxFragmentCount = receivedOptions.maxFragmentCount;\n \n if (receivedOptions.txMaxInFlight < this.txMaxInFlight)\n this.txMaxInFlight = receivedOptions.txMaxInFlight;\n if (this.options.maxFragmentSize < 0)\n throw new DCPError(`maxFragmentSize was set to < 1, (${this.options.txMaxFragmentSize}). Server requested bad fragment size, cannot connect.`, 'DCPC-1111');\n if (this.txMaxInFlight < 1)\n throw new DCPError(`txMaxInFlight was set to < 1, (${this.txMaxInFlight}). Cannot send messages with fewer than 1 in flight.`, 'DCPC-1111');\n \n // true if we are connecting a new transport, expecting the initial buffer reply, and have received the target's mode designation\n if (this.initTimeout && this.modeSet)\n {\n clearTimeout(this.initTimeout);\n delete this.initTimeout;\n this.emit('connect');\n }\n this.bufferSizeSet = true;\n\n this.sendMaxMessageInfo(); \n }\n\n /* Internal method to tell peer what we can receive */\n sendModeList()\n {\n var rawMessage;\n var modeList = [];\n\n if (this.sentModeList)\n return;\n\n this.sentModeList = true;\n this.options.enableModeAck == true && modeList.push('ack');\n this.options.enableModeConcat !== false && modeList.push('concat');\n rawMessage = 'E0000M' + JSON.stringify(modeList);\n\n debugging('socketio') && console.debug(`${this.debugLabel} sending mode list to ${this.debugInfo.remoteLabel}`)\n if (this.socket)\n this.socket.send(rawMessage);\n else\n this.txPending.unshift({ message: rawMessage, msgSequence: ++this.msgSequence});\n }\n \n /* Internal method to exchange desired fragment size/max inflight messages/max fragments per message */\n sendMaxMessageInfo()\n {\n if (this.sentMessageInfo)\n return;\n\n /* Calculate the optimal maxFragmentSize based on maxHttpBufferSize.\n * Currently unsure of exactly what happens when socketio serializes our serialized\n * JSON. I suspect we may double-encode UTF-8 > 0x80. The largest UTF-8 character is\n * 5 bytes...so we ensure the maxFragmentSize is (maxHttpBufferSize / 5) - (fragmentOverhead * 5)\n * to allow for worst-case scenario\n */\n const fragmentOverhead = 1000 + 64 * 1024; /* derived from concatenation algorithm */\n\n let desiredMaxFragmentSize;\n if (this.options.maxHttpBufferSize)\n desiredMaxFragmentSize = (this.options.maxHttpBufferSize / 5) - (fragmentOverhead * 5);\n else\n desiredMaxFragmentSize = this.options.maxFragmentSize;\n const options = {\n maxFragmentSize: desiredMaxFragmentSize,\n maxFragments: this.options.maxFragmentCount,\n txMaxInFlight: this.txMaxInFlight,\n };\n\n const message = 'E0000B' + JSON.stringify(options);\n\n debugging('socketio') && console.debug(`${this.debugLabel} sending max http buffer size to ${this.debugInfo.remoteLabel}`);\n if (this.socket)\n this.socket.send(message);\n else\n this.txPending.push({ message: message, msgSequence: ++this.msgSequence });\n \n this.sentMessageInfo = true;\n }\n\n /**\n * Emit message events so that the connection can process them. The pending property is an\n * array which is used to order messages, so that they are emitted in the order they were\n * sent. There are edge cases in the concatenation code where a sender could theoretically\n * interleave multiple messages, and have a second, shorter, message fully received before \n * the first is fully transmitted.\n *\n * The pending property stores only strings or objects; objects are effectively positional\n * memoes which are turned into strings when they are ready to be emitted to the connection\n * for processing.\n */\n emitReadyMessages()\n {\n while (this.rxReady && this.rxPending.length && typeof this.rxPending[0] === 'string' && this.listenerCount('message') > 0)\n this.emit('message', this.rxPending.shift());\n }\n \n /** \n * Close the current instance of Socket.\n *\n * This function effectively finalizes the socket so that it can't be used any more and\n * (hopefully) does not entrain garbage. A 'close' event is emitted at the end of socket\n * finalization and should be hooked by things that might entrain this transport instance to free\n * up memory, like maybe a list of active transports, etc.\n *\n * We add few extra hops on/off the event loop here to try and clean up any messages halfway in/out\n * the door that we might want to process. That happens between the setting of the isClosed and \n * isFinal flags.\n *\n * @param {string} reason\n */\n #close(reason)\n {\n const that = this;\n debugging('socketio') && console.debug(this.debugLabel, 'closing connection' + (reason ? ` (${reason})` : ''));\n\n function finalize(socket)\n {\n if (that.isFinal)\n return;\n\n if (socket)\n socket.removeAllListeners();\n that.isFinal = true;\n\n /* Free up memory that might be huge in case something entrains us */\n that.rxPending.length = 0;\n that.txPending.length = 0;\n for (let msgId in that.rxFragments)\n that.rxFragments[msgId].length = 0;\n }\n\n function closeSocket()\n {\n const socket = that.socket;\n \n try\n {\n if (socket)\n socket.disconnect();\n that.isClosed = true;\n that.emit('close', reason);\n delete that.socket;\n }\n finally\n {\n finalize(socket);\n }\n }\n\n if (!this.isClosed)\n {\n /* If we called close, drain any pending messages and then disconnect */\n if (reason === 'api-call')\n {\n this.drainTxPending();\n setImmediate(()=>closeSocket());\n }\n else\n closeSocket();\n }\n }\n\n /* API - close the transport, free up memory. */\n close()\n {\n assert(arguments.length === 0)\n this.#close('api-call');\n }\n\n /**\n * Handle the socketio connect_error event, which is fired when:\n * - the low-level connection cannot be established\n * - the connection is denied by the server in a middleware function\n *\n * In the first case, socket.io will automatically try to reconnect, after a delay,\n * except that will cancel that reconnection attempt here by killing the transport\n * instance so that the Connection class can try failing over to an alternate transport.\n *\n * @param {Error} error optional instance of error describing the underlying reason for the\n * connect failure. If the underlying reason is expressible by an\n * HTTP status code, this code will be placed as a Number in the\n * description property.\n */\n handleConnectErrorEvent(error)\n {\n debugging('socketio') && console.debug(this.debugLabel, `unable to connect to ${this.url}`, error);\n if (error.type === 'TransportError' && typeof error.description === 'number')\n error.httpStatus = Number(error.description);\n\n this.emit('connect-failed', error);\n this.#close(error.message);\n }\n\n /** \n * Handle the socketio connect event, which is fired by the Socket instance upon initiator\n * connection and reconnection to a target.\n */\n handleConnectEvent()\n {\n if (this.hasConnected === true)\n {\n /* this transport is not supposed to reuse connections, and we call .close() on disconnect \n * to prevent this. However, the API does not guarantee that will be race-free.\n */\n debugging('socketio') && console.debug(this.debugLabel, `*** reconnected to ${this.debugInfo.remoteLabel} (ignoring)`);\n return;\n }\n\n /* initial connection */\n this.hasConnected = true;\n debugging('socketio') && console.debug(this.debugLabel, 'connected to', this.debugInfo.remoteLabel);\n\n this.initTimeout = setTimeout(() => this.#close('no mode message received'), this.options.establishTimeout) /* give target 10 second to reply with capabilities before assuming bad connection and crashing */\n if (dcpEnv.platform === 'nodejs')\n this.initTimeout.unref();\n \n this.drainTxPending();\n }\n\n /**\n * API: Send a message to the transport instance on the other side of this connection.\n *\n * @param {string} message the message to send\n */\n send(message)\n {\n const msgSequence = ++this.msgSequence;\n\n debugging('socketio') && console.debug(this.debugLabel, `sending message ${msgSequence} to`, this.debugInfo.remoteLabel);\n debugging('socketio') && debugging('verbose') && !debugging('all') && console.debug(this.debugLabel, message);\n\n if (!this.socket)\n throw new DCPError('SocketIOTransport.send: Not connected', 'DCPC-1110');\n \n if ( false\n || !(message.length > this.options.txMaxFragmentSize)\n || !this.mode.concat)\n this.txPending.push({ message, msgSequence });\n else\n { /* This is a large message. Use message concatenation to send in fragments. A failure of any\n * fragment is a failure of the entire message and will be handled at the connection layer.\n */\n let total = Math.ceil(message.length / this.options.txMaxFragmentSize);\n \n if (total > this.options.txMaxFragmentCount)\n throw new DCPError('extended message has more fragments than the peer will allow', 'DCP-1112');\n \n for (let seq=0; seq < total; seq++)\n {\n let start = seq * this.options.txMaxFragmentSize;\n let fragment = message.slice(start, start + this.options.txMaxFragmentSize);\n let header = JSON.stringify({ msgId: msgSequence, seq, total });\n let extMessage = 'E' + header.length.toString(16).padStart(4, '0') + 'C' + header + fragment;\n\n this.txPending.push({ message: extMessage, msgSequence, seq, total });\n\n /* This should be impossible, but it also effectively kills the connection. */\n if (header.length > 0xFFFF)\n throw new DCPError('extended message header too long', 'DCPC-1104');\n }\n }\n\n this.drainTxPending_soon();\n }\n\n drainTxPending_soon()\n {\n if (this.drainingSoon)\n return;\n this.drainingSoon = true;\n\n setImmediate(() => this.drainTxPending());\n }\n \n /**\n * Drain messages from the txPending queue out to socketio's network buffer. We throw away traffic\n * when it can't be sent; this is no different from packets already on the wire when the other end\n * has changed IP number, etc.\n *\n * In ack mode, this drain is controlled by acknowledgement messages so that socketio can have the\n * opportunity to insert ping/pong messages in the data stream while sending a message large enough\n * to need to be sent via concatenation. [ NOTE - it appears to send ping/pong OOB in version 4.4, \n * so currently ack mode is off by default / wg Mar 2022 ]\n *\n * The reason we put non-concat messages through here is to avoid special casing a bunch of nasty\n * things for the sake of a little tiny bit of performance. Sending message<-ACK and queueing through\n * the drain code is necessary if we wind up interleaving multiple message sends in some interesting\n * edge cases; notably, fast-teardown, but a future Connection class that ran without nonces (or multiple\n * nonces) would need this, too.\n *\n * In non-ack mode, the drain is mostly uncontrolled, but we throw each message on a separate pass of\n * the event loop, to try and keep huge message blasts one connection from overwhelming a busy server,\n * as well allowing socketio the chance to send its heartbeat messages and make it less likely that it\n * will hit its http buffer size limit, because that causes an immediate disconnect.\n *\n * Non-ack-mode is compatible with previous dcp5 versions and probably a little faster.\n */\n drainTxPending()\n {\n const that = this;\n\n if (this.txPending.length)\n debugging('socketio') && console.debug(this.debugLabel, `drain tx pending queue, ${this.txPending.length} ready`\n + (this.mode.ack ? `; ${this.txMaxInFlight - this.txNumInFlight} slots available` : ''));\n delete this.drainingSoon;\n\n if (this.isClosed)\n return;\n \n for (let i=0;\n !this.isClosed && this.txPending.length && (this.txNumInFlight < this.txMaxInFlight || !this.mode.ack);\n i++)\n {\n const pendingElement = this.txPending.shift();\n\n if (i === 0)\n writeToSocket(pendingElement);\n else\n setImmediateN(() => writeToSocket(pendingElement), i);\n\n if (this.mode.ack)\n this.txNumInFlight++;\n\n if (this.txPending.length === 0) /* just sent the last message */\n this.emit('drain');\n }\n\n if (this.txPending.length)\n this.drainTxPending_soon();\n \n function writeToSocket(pendingElement)\n {\n if (that.isClosed)\n return;\n\n const { message, msgSequence, seq, total } = pendingElement;\n that.socket.send(message);\n\n if (typeof seq !== 'undefined')\n debugging('socketio') && console.debug(that.debugLabel, `sent fragment ${seq + 1}/${total} of message ${msgSequence} (${message.length} bytes)`);\n else\n debugging('socketio') && console.debug(that.debugLabel, `sent message ${msgSequence} (${message.length} bytes)`);\n }\n }\n}\nSocketIOTransport.prototype.buildOptions = buildOptions;\n\n/** \n * Build the socketio options object, based on the passed-in options, dcp debug state, and internal\n * object state. Method shared between SocketIOTransport and SocketIOListener.\n */\nfunction buildOptions(options)\n{\n options = leafMerge(\n /* Baked-in Defaults */\n ({\n autoUnref: dcpEnv.platform === 'nodejs' ? true : undefined, /* socket.io + webpack 5 { node:global } work-around */\n perMessageDeflate: dcpConfig.build !== 'debug',\n maxFragmentCount: 1000, /* used to limit memory on receiver */\n maxFragmentSize: 1e6, /* bytes */\n txMaxFragmentCount:1000, /* peer's limit for fragments */\n txMaxFragmentSize: 1e6, /* peer's limit for fragment size */\n maxHttpBufferSize: false, /* bytes; false = auto */\n establishTimeout: 10 * 1000, /* 10 seconds */\n pingTimeout: 30 * 1e3, /* s */\n pingInterval: 90 * 1e3, /* s */\n transports: ['polling', 'websocket'],\n upgrade: true,\n rememberUpgrade: true,\n autoConnect: true,\n cors: {\n origin: '*',\n methods: ['GET', 'POST']\n },\n }),\n options,\n /* Absolutely mandatory */\n ({\n path: this.url.pathname // eslint-disable-line no-invalid-this\n }));\n\n /* draw out errors quickly in dev */\n if ((process.env.DCP_NETWORK_CONFIG_BUILD || dcpConfig.build) === 'debug')\n {\n /* short timeouts and debuggers don't get along well */\n if (dcpEnv.platform === 'nodejs' && !(requireNative('module')._cache.niim instanceof requireNative('module').Module))\n {\n options.pingTimeout /= 5;\n options.pingInterval /= 5;\n }\n }\n\n return options;\n}\n \n/**\n * @constructor\n * API - Factory which creates a new socketio listener, or throws. Emits 'listening' if the server \n * parameter is not provided and we create it, once it is ready for connections.\n * \n * @bug It is possible for a connection event to sneak in before the 'listening' event is fired, due to \n * the two-step event trigger. The right fix is to proxy the event, but dcp-events::event-emitter.proxy\n * itself currently has the same bug. For now, we just have callbacks in API. /wg apr 2022\n * \n * @param {object} config dcpConfig fragment - has .listen and .location\n * @param {object} target the instance of Target to bind to\n * @param {object} options [optional] A DCP config options variable (like dcpConfig.dcp.listenOptions)\n */\nfunction SocketIOListener(config, target, options)\n{\n var matchRegex;\n \n this.url = config.location;\n this.options = this.buildOptions(options);\n this.connSequence = 0;\n this.seq = ++listenerSeq;\n this.debugLabel = `socketio(l:${this.seq}):`;\n this.isShutdown = false;\n\n if (dcpEnv.platform !== 'nodejs')\n throw new Error('target mode only supported in nodejs');\n\n if (target.httpServer.listening)\n setImmediate(() => this.emit('listening', target.httpServer)); /* setImmediate to allow event listeners to be added before emitting */\n else\n target.httpServer.on('listening', () => this.emit('listening', target.httpServer));\n\n matchRegex = '^' + this.url.pathname;\n if (matchRegex.slice(-1) !== '/')\n matchRegex += '\\\\/';\n else\n matchRegex = matchRegex.slice(0, -1) + \"\\\\/\"\n\n matchRegex += '\\\\?EIO=[0-9]';\n debugging('socketio') && console.debug(this.debugLabel, `expecting requests to match regex ${matchRegex}`);\n\n target.all(new RegExp(matchRegex), (request) => {\n /* Socket.io does this intrinsically because it plays with the server's events directly,\n * but we \"handle\" it explicitly here so that we don't trip the 404 handler. \n */\n debugging('socketio') && console.debug(this.debugLabel, 'Handling', request.url);\n }, true);\n\n /* While we normally want to inherit options from parent to child socket via this.options,\n * we don't want this to happen for maxHttpBufferSize due to the way it is used and calculated\n * in the transport constructor. A buffer that is too small (eg false) will cause us to not\n * recognized socket.io SIDs.\n */\n \n this.options.maxHttpBufferSize = 1e7;\n \n const socketOptions = this.options;\n\n this.socketServer = requireNative('socket.io')(target.httpServer, socketOptions);\n this.socketServer.on('connection', (socket) => !this.isShutdown && this.handleConnectionEvent(config, socket));\n debugging() && console.debug(this.debugLabel, 'Socketio listener initialized for path', this.options.path);\n}\nSocketIOListener.prototype = new EventEmitter('socketio(l)');\nSocketIOListener.prototype.buildOptions = buildOptions;\n\nSocketIOListener.prototype.close = function SocketIOListener$$close()\n{\n if (!this.isShutdown)\n this.shutdown();\n this.socketServer.close(() => this.emit('close'));\n}\n\n/** Stop accepting new connections */\nSocketIOListener.prototype.shutdown = function SocketIOListener$$shutdown()\n{\n this.isShutdown = true;\n}\n\n/** Remove any event loop references used by this transport instance */\nSocketIOListener.prototype.unref = function SocketIOListener$$unref()\n{\n void false;\n}\n\n/**\n * Handle the socketio connection event, which is fired upon a connection from client.\n * Used by Target->SocketIOListener to create transport instances.\n *\n * It is expected that code hooking the 'connection' event will hook the 'message' in\n * the same pass of the event loop as it is invoked; otherwise, messages may be missed.\n *\n * @param {object} socket a new connection emitted by the socket.io server\n */\nSocketIOListener.prototype.handleConnectionEvent = function SocketIOListener$$handleConnectionEvent(config, socket)\n{\n var transport = new SocketIOTransport(config, this.options, socket);\n\n this.url = config.location || config.listen;\n this.connSequence++;\n transport.debugLabel = `socketio(t:${this.seq}.${this.connSequence}):`\n\n /* Ensure we don't emit any message events until the application or Connection class\n * has had the chance to set up message handlers via the connection event.\n */\n transport.rxReady = false;\n this.on('connection', () => setImmediate(() => {\n transport.rxReady = true;\n transport.emitReadyMessages();\n }));\n\n this.emit('connection', transport);\n}\n\n/* Define API */\nexports.TransportClass = SocketIOTransport;\nexports.Listener = SocketIOListener;\n\n\n//# sourceURL=webpack://dcp/./src/protocol-v4/transport/socketio.js?");
4720
4719
 
4721
4720
  /***/ }),
4722
4721
 
@@ -4747,7 +4746,7 @@ eval("const DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/comm
4747
4746
  \************************************/
4748
4747
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4749
4748
 
4750
- eval("/** \n * @file encodeDataURI.js\n * @author Nazila Akhavan <nazila@kingsds.network>\n * @date Sep 2020\n * \n * Encode input and return the URI.\n */\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\n\nif (!DCP_ENV.isBrowserPlatform) {\n btoa = __webpack_require__(/*! btoa */ \"./node_modules/btoa/index.js\");\n}\n\n/**\n * Encode input to data:[<MIME-type>][;base64][;charset=<encoding>],<data>\n * @param { string | object } input \n * @param { string } [MIMEType] \n */\nexports.encodeDataURI = function utils$$encodeDataURI (input, MIMEType) {\n let inputType = typeof input;\n let temp, encoding;\n let textType, imageType;\n if(MIMEType) {\n temp = MIMEType.split(';');\n MIMEType = temp[0];\n if(temp[1]) encoding = temp[1];\n textType = MIMEType.match(/text\\/.*/);\n textType = textType? textType[0] : [];\n imageType = MIMEType.match(/image\\/.*/);\n imageType = imageType? imageType[0] : [];\n } else {\n switch (inputType) {\n case 'string':\n MIMEType = 'text/plain';\n break;\n\n case 'boolean':\n MIMEType = 'application/json';\n break;\n\n case 'number':\n case 'object':\n MIMEType = 'application/x-kvin';\n break;\n }\n }\n\n switch (MIMEType) {\n case 'application/javascript': \n case 'text/plain':\n if(encoding === 'base64') {\n return 'data:text/plain;base64,' + btoa(input);\n }\n return 'data:,' + encodeURI(input);\n\n case 'application/json':\n return 'data:application/json,' + encodeURI(JSON.stringify(input));\n\n case 'application/x-kvin':\n return 'data:application/x-kvin,' + encodeURI(kvin.serialize(input));\n\n case 'application/octet-stream':\n input = new Uint8Array(input);\n return 'data:application/octet-stream;base64,' + btoa(input); \n\n case textType:\n return `data:${textType},` + encodeURI(input);\n\n case imageType:\n if (inputType === 'string') return `data:${imageType};base64,` + btoa(input);\n input = new Uint8Array(input.buffer)\n return encodeURI(`data:${imageType};base64,` + btoa(input)); \n\n default:\n throw new Error(`The content type ${MIMEType} is not supported in encodeDataURI()!`)\n }\n}\n\n// https://stackoverflow.com/questions/11089732/display-image-from-blob-using-javascript-and-websockets\n// public method for encoding an Uint8Array to base64\n\nfunction encode(input) {\n var keyStr = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\";\n var output = \"\";\n var chr1, chr2, chr3, enc1, enc2, enc3, enc4;\n var i = 0;\n\n while (i < input.length) {\n chr1 = input[i++];\n chr2 = i < input.length ? input[i++] : Number.NaN; // Not sure if the index \n chr3 = i < input.length ? input[i++] : Number.NaN; // checks are needed here\n\n enc1 = chr1 >> 2;\n enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);\n enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);\n enc4 = chr3 & 63;\n\n if (isNaN(chr2)) {\n enc3 = enc4 = 64;\n } else if (isNaN(chr3)) {\n enc4 = 64;\n }\n output += keyStr.charAt(enc1) + keyStr.charAt(enc2) +\n keyStr.charAt(enc3) + keyStr.charAt(enc4);\n }\n return output;\n}\n\n \n \n\n\n//# sourceURL=webpack://dcp/./src/utils/encodeDataURI.js?");
4749
+ eval("/** \n * @file encodeDataURI.js\n * @author Nazila Akhavan <nazila@kingsds.network>\n * @date Sep 2020\n * \n * Encode input and return the URI.\n */\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\nconst DCP_ENV = __webpack_require__(/*! dcp/common/dcp-env */ \"./src/common/dcp-env.js\");\n\nif (!DCP_ENV.isBrowserPlatform) {\n btoa = __webpack_require__(/*! btoa */ \"./node_modules/btoa/index.js\");\n}\n/**\n * Encode input to data:[<MIME-type>][;base64][;charset=<encoding>],<data>\n * @param { string | object } input \n * @param { string } [MIMEType] \n */\nexports.encodeDataURI = function utils$$encodeDataURI (input, MIMEType) {\n let inputType = typeof input;\n let temp, encoding;\n let textType, imageType;\n if(MIMEType) {\n temp = MIMEType.split(';');\n MIMEType = temp[0];\n if(temp[1]) encoding = temp[1];\n textType = MIMEType.match(/text\\/.*/);\n textType = textType? textType[0] : [];\n imageType = MIMEType.match(/image\\/.*/);\n imageType = imageType? imageType[0] : [];\n } else {\n switch (inputType) {\n case 'string':\n MIMEType = 'text/plain';\n break;\n\n case 'boolean':\n MIMEType = 'application/json';\n break;\n\n default:\n MIMEType = 'application/x-kvin';\n break;\n }\n }\n\n switch (MIMEType) {\n case 'application/javascript': \n case 'text/plain':\n if(encoding === 'base64') {\n return 'data:text/plain;base64,' + btoa(input);\n }\n return 'data:,' + encodeURI(input);\n\n case 'application/json':\n return 'data:application/json,' + encodeURI(JSON.stringify(input));\n\n case 'application/x-kvin':\n return 'data:application/x-kvin,' + encodeURI(kvin.serialize(input));\n\n case 'application/octet-stream':\n input = new Uint8Array(input);\n return 'data:application/octet-stream;base64,' + btoa(input); \n\n case textType:\n return `data:${textType},` + encodeURI(input);\n\n case imageType:\n if (inputType === 'string') return `data:${imageType};base64,` + btoa(input);\n input = new Uint8Array(input.buffer)\n return encodeURI(`data:${imageType};base64,` + btoa(input)); \n\n default:\n throw new Error(`The content type ${MIMEType} is not supported in encodeDataURI()!`)\n }\n}\n\n// https://stackoverflow.com/questions/11089732/display-image-from-blob-using-javascript-and-websockets\n// public method for encoding an Uint8Array to base64\n\nfunction encode(input) {\n var keyStr = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\";\n var output = \"\";\n var chr1, chr2, chr3, enc1, enc2, enc3, enc4;\n var i = 0;\n\n while (i < input.length) {\n chr1 = input[i++];\n chr2 = i < input.length ? input[i++] : Number.NaN; // Not sure if the index \n chr3 = i < input.length ? input[i++] : Number.NaN; // checks are needed here\n\n enc1 = chr1 >> 2;\n enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);\n enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);\n enc4 = chr3 & 63;\n\n if (isNaN(chr2)) {\n enc3 = enc4 = 64;\n } else if (isNaN(chr3)) {\n enc4 = 64;\n }\n output += keyStr.charAt(enc1) + keyStr.charAt(enc2) +\n keyStr.charAt(enc3) + keyStr.charAt(enc4);\n }\n return output;\n}\n\n \n \n\n\n//# sourceURL=webpack://dcp/./src/utils/encodeDataURI.js?");
4751
4750
 
4752
4751
  /***/ }),
4753
4752
 
@@ -4822,7 +4821,7 @@ eval("/**\n * @file src/utils/inventory.js\n * Inventory met
4822
4821
  /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
4823
4822
 
4824
4823
  "use strict";
4825
- eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_modules/process/browser.js */ \"./node_modules/process/browser.js\");\n/**\n * @file just-fetch.js\n * @author Ryan Rossiter <ryan@kingsds.network>\n * @date June 2020\n *\n * Cross-platform method for performing an HTTP request.\n */\n\n\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\n\n /**\n * Make a request, as a promise, resolves with a string or json object\n *\n * @param {string | object} url - path of file to be fetched; if object must be instanceof URL or DcpURL\n * @param {string} [type = \"string\"] - expected type of the response\n * @param {string} [method = \"GET\"] - type of request to make\n * @param {boolean} [bustCache = false] - whether to add cache poison to the end of the url\n * @param {any} [body] - what to send as the body of the message. Objects are turned into into \n * standard CGI key=value encoding when the method is 'POST'\n * @returns {Promise} - resolves with response (json if type=\"JSON\", string otherwise), rejects on fail\n */\nexports.justFetch = function justFetch(url, type = 'string', method = 'GET', bustCache = false, body = undefined) {\n return new Promise((resolve, reject) => {\n let deeperErrorStack = new Error().stack;\n deeperErrorStack = deeperErrorStack.substring(deeperErrorStack.indexOf('\\n') + 1);\n\n if (bustCache)\n {\n let bustParam = ((typeof document !== 'undefined' && document.head && document.head.getAttribute('version')) || Date.now()); /* cache buster */\n\n if (typeof url === 'object' && url instanceof DcpURL)\n url = url.href;\n if (typeof url === 'string')\n url = new URL(url);\n url.search += (url.search ? '&' : '') + encodeURI(bustParam);\n }\n\n if (typeof url === 'object' && url.href) {\n url = url.href;\n }\n\n const xhr = new XMLHttpRequest();\n xhr.onloadend = function Protocol$$justFetch$onloadend() {\n try {\n delete xhr.onloadend;\n\n if (xhr.status >= 200 && xhr.status < 300) {\n if (xhr.getResponseHeader('content-type') && type === 'string') {\n type = xhr.getResponseHeader('content-type').split(';')[0];\n }\n \n let data = xhr.responseText;\n if (type === 'JSON' || type === 'application/json') {\n data = JSON.parse(data);\n }\n\n if (type === 'application/x-kvin' ) {\n data = kvin.deserialize(data);\n }\n \n resolve(data);\n } else {\n function makeFetchError()\n {\n if (xhr.status)\n return new Error(`HTTP Error ${xhr.status} fetching ${url}`);\n\n /* NodeJS via xmlhttprequest-ssl leaves an Error duck */\n if (xhr.statusText && xhr.statusText.code && xhr.statusText.message) {\n let error = new Error(xhr.statusText.message);\n let stack = error.stack;\n\n Object.assign(error, xhr.statusText);\n if (xhr.statusText.stack)\n error.stack = xhr.statusText.stack.split('\\n').slice(1).join('\\n') + '---\\n' + stack;\n else\n error.stack = stack;\n return error;\n }\n\n return new Error(`Network error fetching ${url}`);\n }\n\n const error = makeFetchError();\n \n error.request = Object.assign({}, xhr); /* Shallow clone so that console.log will work in node */\n error.request.method = method;\n error.request.location = url;\n error.stack += '\\n' + deeperErrorStack;\n if (xhr.status)\n error.code = 'HTTP_' + xhr.status;\n\n throw error;\n }\n } catch (e) {\n reject(e);\n }\n };\n\n xhr.open(method, url);\n if (!body)\n xhr.send();\n else\n {\n if (typeof body === 'object' && method.toUpperCase() === 'POST')\n {\n let entries = Object.entries(body);\n body = entries.map((kvp) => `${encodeURIComponent(kvp[0])}=${encodeURIComponent(kvp[1])}`).join('&');\n }\n xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');\n xhr.send(body);\n }\n });\n }\n\n/** Reformat an error (rejection) message from utils::justFetch, so that debugging code \n * can include (for example) a text-rendered version of the remote 404 page.\n *\n * @param {object} error The rejection from justFetch()\n * @returns {string} An error message, formatted with ANSI color when the output\n * is a terminal, suitable for writing directly to stdout. If\n * the response included html content (eg a 404 page), it is \n * rendered to text in this string.\n */\nexports.justFetchPrettyError = function utils$$justFetchPrettyError(error) {\n const chalk = new requireNative('chalk').constructor({enabled: (__webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\").useChalk)});\n var message, headers={};\n\n if (!error.request || !error.request.status)\n return error;\n\n error.request.getAllResponseHeaders().replace(/\\r/g,'').split('\\n').forEach(function(line) {\n var colon = line.indexOf(': ')\n headers[line.slice(0,colon)] = line.slice(colon+2)\n })\n\n message = `HTTP Status: ${error.request.status} for ${error.request.method} ${error.request.location}`\n\n switch(headers['content-type'].replace(/;.*$/, '')) {\n case 'text/plain':\n message += '\\n' + chalk.grey(error.request.responseText)\n break;\n case 'text/html': {\n let html = error.request.responseText;\n\n html = html.replace(/\\n<a/gi, ' <a'); /* html-to-text bug, affects google 301s /wg jun 2020 */\n message += chalk.grey((__webpack_require__(/*! html-to-text */ \"./node_modules/html-to-text/index.js\").fromString)(html, {\n wordwrap: parseInt(process.env.COLUMNS, 10) || 80,\n hideLinkHrefIfSameAsText: true,\n format: {\n heading: function (elem, fn, options) {\n var h = fn(elem.children, options);\n return '\\n====\\n' + chalk.yellow(chalk.bold(h.toUpperCase())) + '\\n====\\n';\n }\n }\n }));\n break;\n }\n }\n\n return message;\n} \n\n\n//# sourceURL=webpack://dcp/./src/utils/just-fetch.js?");
4824
+ eval("/* provided dependency */ var process = __webpack_require__(/*! ./node_modules/process/browser.js */ \"./node_modules/process/browser.js\");\n/**\n * @file just-fetch.js\n * @author Ryan Rossiter <ryan@kingsds.network>\n * @date June 2020\n *\n * Cross-platform method for performing an HTTP request.\n */\n\n\nconst { requireNative } = __webpack_require__(/*! dcp/dcp-client/webpack-native-bridge */ \"./src/dcp-client/webpack-native-bridge.js\");\nconst { DcpURL } = __webpack_require__(/*! dcp/common/dcp-url */ \"./src/common/dcp-url.js\");\nconst kvin = __webpack_require__(/*! kvin */ \"./node_modules/kvin/kvin.js\");\n\n /**\n * Make a request, as a promise, resolves with a string or json object\n *\n * @param {string | object} url - path of file to be fetched; if object must be instanceof URL or DcpURL\n * @param {string} [type = \"string\"] - expected type of the response\n * @param {string} [method = \"GET\"] - type of request to make\n * @param {boolean} [bustCache = false] - whether to add cache poison to the end of the url\n * @param {any} [body] - what to send as the body of the message. Objects are turned into into \n * standard CGI key=value encoding when the method is 'POST'\n * @returns {Promise} - resolves with response (json if type=\"JSON\", string otherwise), rejects on fail\n */\nexports.justFetch = function justFetch(url, type = 'string', method = 'GET', bustCache = false, body = undefined) {\n return new Promise((resolve, reject) => {\n let deeperErrorStack = new Error().stack;\n deeperErrorStack = deeperErrorStack.substring(deeperErrorStack.indexOf('\\n') + 1);\n\n if (bustCache)\n {\n let bustParam = ((typeof document !== 'undefined' && document.head && document.head.getAttribute('version')) || Date.now()); /* cache buster */\n\n if (typeof url === 'object' && url instanceof DcpURL)\n url = url.href;\n if (typeof url === 'string')\n url = new URL(url);\n url.search += (url.search ? '&' : '') + encodeURI(bustParam);\n }\n\n if (typeof url === 'object' && url.href) {\n url = url.href;\n }\n\n const xhr = new XMLHttpRequest();\n xhr.onloadend = function Protocol$$justFetch$onloadend() {\n try {\n delete xhr.onloadend;\n\n if (xhr.status >= 200 && xhr.status < 300) {\n if (xhr.getResponseHeader('content-type') && type === 'string') {\n type = xhr.getResponseHeader('content-type').split(';')[0];\n }\n \n let data = xhr.responseText;\n if (type === 'JSON' || type === 'application/json') {\n data = JSON.parse(data);\n }\n\n if (type === 'application/x-kvin' ) {\n data = kvin.deserialize(data);\n }\n \n resolve(data);\n } else {\n function makeFetchError()\n {\n if (xhr.status)\n return new Error(`HTTP Error ${xhr.status} fetching ${url}`);\n\n /* NodeJS via xmlhttprequest-ssl leaves an Error duck */\n if (xhr.statusText && xhr.statusText.code && xhr.statusText.message) {\n let error = new Error(xhr.statusText.message);\n let stack = error.stack;\n\n Object.assign(error, xhr.statusText);\n if (xhr.statusText.stack)\n error.stack = xhr.statusText.stack.split('\\n').slice(1).join('\\n') + '---\\n' + stack;\n else\n error.stack = stack;\n return error;\n }\n\n return new Error(`Network error fetching ${url}`);\n }\n\n const error = makeFetchError();\n \n error.request = Object.assign({}, xhr); /* Shallow clone so that console.log will work in node */\n error.request.method = method;\n error.request.location = url;\n error.stack += '\\n' + deeperErrorStack;\n if (xhr.status)\n error.code = 'HTTP_' + xhr.status;\n\n throw error;\n }\n } catch (e) {\n reject(e);\n }\n };\n\n xhr.open(method, url);\n if (!body)\n xhr.send();\n else\n {\n if (typeof body === 'object' && method.toUpperCase() === 'POST')\n {\n let entries = Object.entries(body);\n body = entries.map((kvp) => `${encodeURIComponent(kvp[0])}=${encodeURIComponent(kvp[1])}`).join('&');\n }\n xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');\n xhr.send(body);\n }\n });\n }\n\n/** Reformat an error (rejection) message from utils::justFetch, so that debugging code \n * can include (for example) a text-rendered version of the remote 404 page.\n *\n * @param {object} error The rejection from justFetch()\n * @returns {string} An error message, formatted with ANSI color when the output\n * is a terminal, suitable for writing directly to stdout. If\n * the response included html content (eg a 404 page), it is \n * rendered to text in this string.\n */\nexports.justFetchPrettyError = function utils$$justFetchPrettyError(error) {\n const chalk = new requireNative('chalk').constructor({enabled: (__webpack_require__(/*! dcp/utils */ \"./src/utils/index.js\").useChalk)});\n var message, headers={};\n\n if (!error.request || !error.request.status)\n return error;\n\n error.request.getAllResponseHeaders().replace(/\\r/g,'').split('\\n').forEach(function(line) {\n var colon = line.indexOf(': ')\n headers[line.slice(0,colon)] = line.slice(colon+2)\n })\n\n message = `HTTP Status: ${error.request.status} for ${error.request.method} ${error.request.location}`\n\n switch(headers['content-type'].replace(/;.*$/, '')) {\n case 'text/plain':\n message += '\\n' + chalk.grey(error.request.responseText)\n break;\n case 'text/html': {\n let html = error.request.responseText;\n\n html = html.replace(/\\n<a/gi, ' <a'); /* html-to-text bug, affects google 301s /wg jun 2020 */\n message += chalk.grey((__webpack_require__(/*! html-to-text */ \"./node_modules/html-to-text/index.js\").htmlToText)(html, {\n wordwrap: parseInt(process.env.COLUMNS, 10) || 80,\n hideLinkHrefIfSameAsText: true,\n format: {\n heading: function (elem, fn, options) {\n var h = fn(elem.children, options);\n return '\\n====\\n' + chalk.yellow(chalk.bold(h.toUpperCase())) + '\\n====\\n';\n }\n }\n }));\n break;\n }\n }\n\n return message;\n} \n\n\n//# sourceURL=webpack://dcp/./src/utils/just-fetch.js?");
4826
4825
 
4827
4826
  /***/ }),
4828
4827
 
@@ -5679,7 +5678,7 @@ eval("module.exports = JSON.parse('{\"1.3.132.0.10\":\"secp256k1\",\"1.3.132.0.3
5679
5678
  /***/ ((module) => {
5680
5679
 
5681
5680
  "use strict";
5682
- eval("module.exports = JSON.parse('{\"browser\":[\"deny-node\",\"kvin/kvin.js\",\"script-load-wrapper\",\"wrap-event-listeners\",\"event-loop-virtualization\",\"access-lists\",\"bravojs-init\",\"bravojs/bravo.js\",\"bravojs-env\",\"calculate-capabilities\",\"bootstrap\"],\"node\":[\"kvin/kvin.js\",\"sa-ww-simulation\",\"script-load-wrapper\",\"wrap-event-listeners\",\"event-loop-virtualization\",\"access-lists\",\"bravojs-init\",\"bravojs/bravo.js\",\"bravojs-env\",\"calculate-capabilities\",\"bootstrap\"],\"native\":[\"deny-node\",\"kvin/kvin.js\",\"sa-ww-simulation\",\"script-load-wrapper\",\"native-event-loop\",\"wrap-event-listeners\",\"event-loop-virtualization\",\"access-lists\",\"bravojs-init\",\"bravojs/bravo.js\",\"bravojs-env\",\"calculate-capabilities\",\"bootstrap\"],\"webGpuNative\":[\"deny-node\",\"kvin/kvin.js\",\"sa-ww-simulation\",\"script-load-wrapper\",\"native-event-loop\",\"wrap-event-listeners\",\"event-loop-virtualization\",\"access-lists\",\"bravojs-init\",\"bravojs/bravo.js\",\"bravojs-env\",\"calculate-capabilities\",\"webgpu-worker-environment.js\",\"bootstrap\"],\"nodeTesting\":[\"kvin/kvin.js\",\"sa-ww-simulation\",\"script-load-wrapper\",\"wrap-event-listeners\",\"event-loop-virtualization\",\"access-lists\",\"bravojs-init\",\"bravojs/bravo.js\",\"bravojs-env\",\"calculate-capabilities\",\"bootstrap\",\"testing.js\"],\"testing\":[\"deny-node\",\"kvin/kvin.js\",\"sa-ww-simulation\",\"script-load-wrapper\",\"native-event-loop\",\"wrap-event-listeners\",\"event-loop-virtualization\",\"access-lists\",\"bravojs-init\",\"bravojs/bravo.js\",\"bravojs-env\",\"calculate-capabilities\",\"bootstrap\",\"testing.js\"]}');\n\n//# sourceURL=webpack://dcp/./node_modules/dcp-client/generated/sandbox-definitions.json?");
5681
+ eval("module.exports = JSON.parse('{\"browser\":[\"deny-node\",\"kvin/kvin.js\",\"script-load-wrapper\",\"wrap-event-listeners\",\"event-loop-virtualization\",\"gpu-timers\",\"access-lists\",\"bravojs-init\",\"bravojs/bravo.js\",\"bravojs-env\",\"calculate-capabilities\",\"bootstrap\"],\"node\":[\"kvin/kvin.js\",\"sa-ww-simulation\",\"script-load-wrapper\",\"wrap-event-listeners\",\"event-loop-virtualization\",\"gpu-timers\",\"access-lists\",\"bravojs-init\",\"bravojs/bravo.js\",\"bravojs-env\",\"calculate-capabilities\",\"bootstrap\"],\"native\":[\"deny-node\",\"kvin/kvin.js\",\"sa-ww-simulation\",\"script-load-wrapper\",\"native-event-loop\",\"wrap-event-listeners\",\"event-loop-virtualization\",\"webgpu-worker-environment\",\"gpu-timers\",\"access-lists\",\"bravojs-init\",\"bravojs/bravo.js\",\"bravojs-env\",\"calculate-capabilities\",\"bootstrap\"],\"webGpuNative\":[\"deny-node\",\"kvin/kvin.js\",\"bootstrap\"],\"nodeTesting\":[\"kvin/kvin.js\",\"sa-ww-simulation\",\"script-load-wrapper\",\"wrap-event-listeners\",\"event-loop-virtualization\",\"gpu-timers\",\"access-lists\",\"bravojs-init\",\"bravojs/bravo.js\",\"bravojs-env\",\"calculate-capabilities\",\"bootstrap\",\"testing.js\"],\"testing\":[\"deny-node\",\"kvin/kvin.js\",\"sa-ww-simulation\",\"script-load-wrapper\",\"native-event-loop\",\"wrap-event-listeners\",\"event-loop-virtualization\",\"webgpu-worker-environment\",\"gpu-timers\",\"access-lists\",\"bravojs-init\",\"bravojs/bravo.js\",\"bravojs-env\",\"calculate-capabilities\",\"bootstrap\",\"testing.js\"]}');\n\n//# sourceURL=webpack://dcp/./node_modules/dcp-client/generated/sandbox-definitions.json?");
5683
5682
 
5684
5683
  /***/ }),
5685
5684